Introduction: USB Wii Classic Controller
Through the steps, you will learn about:
- How USB works
- How I2C works
- How to read data from the Wii Classic Controller
- General electronics
- AVR programming
The entire project is provided as a ZIP file download below. Also see below for a flowchart of how the code will work.
Attachments
Step 1: Using the AVR With V-USB
"V-USB is a software-only implementation of a low-speed USB device for Atmel’s AVR® microcontrollers, making it possible to build USB hardware with almost any AVR® microcontroller, not requiring any additional chip."
V-USB uses a set of hardware and some very special assembly programming techniques to bit-bang the non-return-to-zero (NRZ) binary code that USB uses to communicate. The files provided by V-USB will be compiled into our program in order to create a USB device with our ATmega328P
Please visit the download section of V-USB's website to obtain a copy of the latest version. In my project source code, it's already included.
To compile V-USB into your project...
- Make sure you've defined the processor and clock speed correctly (V-USB only supports certain clock speeds)
- Copy the folder "usbdrv" from the downloaded package into your project folder
- In your project manager or makefile, include "usbdrv.c" and "usbdrvasm.S", such that the object file that's generated will become linked into your project
- Inside the folder "usbdrv", there is a "usbconfig-prototype.h", copy that file into your main project directory, and rename it to "usbconfig.h"
- Edit "usbconfig.h", this will be explained in detail later
- Use "#include" statements to include "usbconfig.h" and then "usbdrv/usbdrv.h"
- Make sure that "usbdrv/usbdrv.h" is able to find "usbconfig.h", if it's not able to, use the use "-I" to your makefile or edit "usbdrv/usbdrv.h" to change the file path to "usbconfig.h" (to "../usbconfig.h")
- You must initialize V-USB, and then enable interrupts in the AVR
- It's almost a standard practice to fake disconnect, wait a few milliseconds, and reconnect to the computer, during starting the code. This makes sure your device and computer are in a "reset" state to start off.
-
A request handler function must be implemented, even if you don't perform real actions in it, you must implement it yourself. Look for "usbFunctionSetup" later.
- In our example, we need to use this function to handle two special requests, you'll see it later
- The gamepad is a HID device, a USB HID Report Descriptor must be written and stored in your code
Step 2: Building the Circuit
USnooBie official site
You can buy it from Seeed Studio, it should come with all the parts you need and the bootloader all ready to go.
The USnooBie is a microcontroller kit that does not require any sort of AVR programmer or USB-to-serial converters to load and run compiled code. It's hardware design allows the user to develop low cost USB devices with Atmel's AVR ATmega microcontrollers. It can also be used to develop projects which are not USB devices. It is even compatible with Arduino.
USnooBie Assembly Instructions and Parts Breakdown
This is a short version of the official assembly guide, visit the official assembly guide to read more details.
Assemble the USnooBie according to these steps. The smaller components must be soldered first, before the larger components, this makes assembly easier. The parts required are also described here so this document also acts as a part list so you may find replacement components.
Two 68 ohm resistorsThese resistors limit the current between the USB device (microcontroller) and the USB host (computer) on the D+ and D- lines of the USB bus. They act as terminating resistors, so the terminating impedance matches the USB cable's characteristic impedance, reducing signal reflections. They are small and low components and are thus soldered first. These should be two 68 ohm 1/4 watt +/- 5% tolerance carbon film resistors. | ||
D- pull-up resistorThis resistor is placed on the D- line of the USB bus. When D- is pulled up, it indicates to the USB host that the USB device is a low speed USB 1.1 device. This resistor is usually 2.2 kilo-ohm if pulling up to 5V and 1.5 kilo-ohm when pulling up to 3.3V. 1.8 kilo-ohm works well with both 5V and 3.3V. This resistor should be one 1.8 kilo ohm 1/4 watt +/- 5% tolerance carbon film resistor. Note: the original design used a 1.7 kilo ohm resistor, the kit being sold is provided with a 1.8 kilo ohm resistor, either should work. The schematics may show a 1.7 kilo ohm resistor (typo, sorry). | ||
LED current limit resistorThis resistor limits the current for the power indication LED. If this current is not limited, then the LED's lifespan is drastically reduced. This resistor should be a 330 ohm 1/4 watt +/- 5% tolerance carbon film resistor. | ||
Two 3.6V Zener diodesThese 3.6V Zener diodes ensures that the signal on the D+ and D- lines of the USB bus are within acceptable limits. This allows the USB device to run at 5V without damaging other devices on the USB bus. These should be 1N5227B 3.6V Zener diodes. There have been reports that certain Zener diodes will not work. 200mW Zener diodes may not work but 500mW Zener diodes will (source: http://forums.obdev.at/viewtopic.php?f=8&t=4677). Ensure that you place these parts in the correct orientation as indicated by the symbol on the PCB. The triangle on the symbol points in the direction which the stripe on the diode should be. | ||
Reverse current protection diodeThis part is not included in the kit provided by Seeed Studio. You must replace this part with a jumper wire or else the USnooBie will not receive power from the USB port. | ||
Power indicator LEDThis LED indicates that there is power on the power bus. Note that it does not indicate the amount of power, so even if it is lit, it does not guarantee that certain components are receiving enough voltage. This must be a 3mm diameter standard LED. This LED may be omitted if you want to save power or you want a "stealthy" USB device. Ensure that you place this part in the correct orientation as indicated by the symbol on the PCB. If you are unable to determine the direction of the LED, then you should test the LED before installing it. The "flat side" should be the cathode, which should be negative to light up, while the "round side" is the anode, which should be positive to light up. Use a 3V coin cell battery to perform this test really quickly to avoid damaging the LED. | ||
USB A male connectorThis allows the USnooBie to be plugged directly into a USB port, or you can buy a USB extension cable from the dollar store to connect it. | ||
Two tactile SPST momentary on push button switchOne button is used to reset the AVR microcontroller, the other button acts as a bootloader activation button. Upon reset, the AVR runs the bootloader section code which checks whether or not the bootloader activation button is held down. If it is held down, the bootloader becomes a USBasp device so you may load your own code into the AVR microcontroller. If it is not held down, then the bootloader jumps to the application section to run the code you have previously loaded. This bootloader activation button is placed on the D- line, when pressed during normal use (not during boot time), it will cause the USB device to appear disconnected from the USB host. This is useful in certain situations when you require your device to disconnect without physically disconnecting. The Omron B3F-1000 tactile SPST momentarty on push button switch should be used here. | ||
28 pin DIP chip socketA 28 pin DIP chip socket is used to hold the AVR ATmega microcontroller. Due to the placement of the three tandum capacitors, a 28 pin DIP chip socket must be used (or two 14 pin DIP chip sockets, the PCB layout is designed to allow this) to hold the AVR ATmega microcontroller. The chip socket should have a gap down its center, giving you room to place the three capacitors. Solder in the sockets first, then insert the capacitors through the gap. See the picture provided. Ensure that you place this part in the correct orientation as indicated by the symbol on the PCB. Do not insert the chip into the socket until the board passes some simple testing (later steps). | ||
Three monolithic capacitorsThe 0.1uF capacitor is a decoupling capacitor which smooths out fine ripples on the power bus. The code on this capacitor should be 104 (meaning 0.1uF). The two 27pF capacitors cleans the signals from the 12 MHz crystal. The code on these capacitor should be 270 (meaning 27 pF). These capacitors can be monolithic or ceramic. | ||
12 MHz crystalThe 12 MHz crystal is the clock source for the AVR microcontroller. It is 12 MHz because that's the best clock speed for 3.3V opertation that is supported by V-USB. The crystal must be a 12 MHz crystal in a HC49 package. Low profile packaging is prefered, as long as the pin spacing is the same. | ||
Voltage selection jumperA three pin header is used to select the voltage on the power bus, a shunt block is used on the 3 pin header to make the connection that makes the selection. This allows you to choose between using the 5V power supply from the USB port or using the 3.3V power supply provided by the 3.3V voltage regulator. Do not install the jumper shunt block until the board has passed some tests (described in later steps). | ||
PTC resettable fuseThis fuse protects the USB host from damage during short circuit situations by cutting off current. The fuse will heat up when current reaches unacceptable levels and it will become a resistor, limiting the current drastically, and when the fuse cools down, it loses its resistance and conducts current again. This will protect your computer if you accidentally short your power bus. Since it resets itself automatically after cooling down, it will never need to be replaced (unlike an ordinary fuse). Note that the USB bus can only supply up to 500mA of current, the fuse provided will build up resistance once it reaches 250mA and cut off power completely if the current reaches 500mA. For most applications, this amount of power is enough, if you require more power, consider utilizing an external power source as the power supply, instead of your computer. This component should be the RXE025 from Tyco Electronics, it is the same PTC resettable fuse sold on SparkFun. It has a I-hold of 250mA and I-trip of 500mA. | ||
4.7 uF electrolytic capacitorThis capacitor smooths out large slow ripples on the power bus, and acts as a small reservoir during sudden current draw. This should be a 4.7 uF electrolytic capacitor rated at 10 volts in radial packaging. Ensure that you place this part in the correct orientation as indicated by the symbol on the PCB. The capacitor should have a strip on the side with negative (minus signs) symbols, which corresponds to the negative side of the capacitor symbol on the PCB (opposite to the pad with the positive + symbol). | ||
3.3V low dropout voltage regulatorThis should be a TC1262 in TO-220 packaging. It is a low dropout voltage regulator that will step down the 5V USB power down to 3.3V. This may be omitted if you do not want a 3.3V power source. Ensure that you place this part in the correct orientation as indicated by the symbol on the PCB. The metal heatsink on the voltage regulator should face towards the inside of the board (as indicated by the thicker silkscreened line).
This component must be a 3.3V low dropout voltage regulator in 3 pin TO-220 packaging. Microchip's TC1262 or similar may be used.
| ||
Male headersThere are three groups of male headers. One long group that has 16 pins, two shorter groups with 6 pins each. These male headers allow you to insert the USnooBie into a breadboard. These headers should go on the bottom of the PCB. To make soldering these header pins easier, you can try inserting them into the breadboard first, and then placing the USnooBie PCB on top, so that the breadboard keeps the header pins straight and holds them in place for you while you solder from the PCB's top side. | ||
Continuity testing the groundUse a multimeter's continuity tester to check that all pins/pads/joints that are supposed to be ground are connected to each other and only each other. If this test passes, then you should be able to check the voltages while powered up without worrying too much about a short causing massive current draw. | ||
Continuity testing the power busUse a multimeter's continuity tester to check that all pins/pads/joints that are supposed to be on the power bus are connected to each other and only to each other. Do this while the voltage selection jumper shunt block is not installed. | ||
Voltage checkPlug in the USnooBie into a powered USB port and check the voltages on the pads/pins/joints which are supposed to be 5V. Do the same for the ones that are supposed to be 3.3V. Install the jumper shunt block on to the voltage selection jumper pin header. Check that you are able to select the voltage on the power bus by moving the jumper shunt block. When there is power on the power bus, the power indicator LED should also light up. | ||
Insert the microcontrollerInsert the AVR ATmega328P microcontroller into the 28 pin DIP chip socket to finish constructing the USnooBie. If the correct bootloader is already loaded on the microcontroller and the microcontroller's fuse bit settings are correct, you can start to use the USnooBie (if you buy it from Seeed Studio, then this is done for you already). Follow the instructions for loading code onto the USnooBie to check that it functions as a USB device when connected to a computer. |
Usage Guide
This is the short version of the official usage guide, go visit the official usage guide for more details.
To enter the bootloader, hold down the bootloader-activation button, then press and release the reset button, then release the bootloader-activation button. The bootloader should appear to your computer as an USBasp programmer, so you may now use it as though you were using an USBasp with AVRDUDE.
A typical AVRDUDE command would start with "avrdude -c usbasp -p atmega328p", and to load a hex file into flash memory, it looks like "avrdude -c usbasp -p atmega328p -U flash:w:filename.hex". You will not be able to modify any fuse-bits, which prevents you from "damaging" the bootloader.
There's a good chance that you'll require the USBasp drivers in order for the USBaspLoader bootloader to work. The drivers can be found here
Step 3: Understanding USB
USB busses have a device and host, the computer is usually the host, our game pad is a device, more specifically, a HID (human interface device). It is important to note that the host always initiate communication, or the host checks the device frequently to see if it has anything to say. The device does not have the ability to initiate communication, it can only wait until it's spoken to.
There are pull-up resistors on D+ or D- depending on whether or not the device is USB 1.0, USB 1.1, or USB 2.0. The presense of these pull-up resistors is also how a computer knows when something has connected. On the USnooBie and most V-USB circuitry, the pull-up resistor is always on the D- signal since V-USB is only capable of implementing low speed USB devices.
The two 68 ohm resistors on the D+ and D- signals are terminating resistors, their impedance are calculated (taking into account the AVR's internal circuitry) to be matched with the characteristic impedance of the USB cable. This minimizes signal reflections. Read http://en.wikipedia.org/wiki/Transmission_line to learn more.
When a device connects to a host, the host tries to "enumerate" the device. If it fails to do so (device not responding, or responding with garbage), that's when Windows says "device not recognized".
The host and device talks over channels called "endpoints", endpoints are identified by a number. There are some endpoints that a reserved for special use, while others can be configured to operate in different modes (interrupt, bulk, etc).
The host will always first use the "control endpoint" (endpoint 0) first to request a description of the device, this "descriptor" will contain the device identifiers (vendor ID and product ID, etc), along with its device class, subclass, etc (HID like a mouse or keyboard? or maybe mass storage?). Then the configuration descriptor is requested, which also contains the number of endpoints available on the device. Each endpoint has its own descriptor as well. All of this data are sent as packets of data bytes representing a well known specified data structure.
V-USB and other USB frameworks/stacks have APIs and other methods to allow the programmer to change the content in the descriptors. You need to first understand each descriptor, and then check the documentation on V-USB to see how to change them (I'll show you later).
The host makes the requests by sending "setup packets" to the "control endpoint". Setup packets have a defined structure making it easy for the device to understand what the host wants. V-USB (and similar frameworks) usually handles the default setup packets. A programmer can write drivers that sends custom setup packets, in which case the firmware must handle the setup packets manually, V-USB (and others) provides some methods to help with that.
Later on in this instructable, I have included a dump of the descriptors captured by my USB traffic analyzer. You can take a look and match it up with USB specifications to see what each portion represents.
Once all the descriptors have been retrieved from the device, the host can then understand the device and communicate with it. We'll look at all the descriptors in detail later.
I have another Instructable which shows you how to build a USB keyboard that types out the code stored in RFID tags: https://www.instructables.com/id/USB-RFID-Reading-Keyboard/
Homework: Read USB in a Nutshell http://www.beyondlogic.org/usbnutshell/usb1.shtml which is pretty much a USB bible
Important Note: Most of the USB terminology is from the perspective of the host (the computer), so the words "in" and "input" means from the device to the host, and the words "out" or "output" mean from the host to device.
Step 4: USB Descriptors
To recap, we need to worry about the device descriptor, configuration descriptor, interface descriptor, endpoint descriptor, and string descriptors. There is also a USB Human Interface Device Report Descriptor that we will need to write later.
The device descriptor will tell the computer information about the device in general. Info such as the USB standard it meets, it's device class & device subclass, protocol, vendor, product ID, and a few optional strings such as product name, manufacture name, and serial number. It will also indicate how many configurations there are available for this device (this is almost always just one).
For this project, the device class is set to 0, meaning "defer to interface", so our interface descriptor will describe this device as a Human Interface Device (HID). The device subclass and protocol are irrelevant. The vendor ID and product ID can be anything (sort of, we'll talk about this later). In the source code, I've set the manufacture string to my website, and the name of the device is "Wii Pad".
Each configuration descriptor will indicate how the device is powered, how much power it needs, and how many interfaces it has. There is also a string that describes each configuration (I haven't seen this used). Different configuration can be selected but usually there is only one available configuration.
For this project, the configuration will indicate that this gamepad will be powered by the USB port and it will need about 100 mA of current (no it doesn't, but 100 is a nice number and well over our real requirements). There is only one interface.
Each interface descriptor contains info about the number of endpoints in the interface, and then the interface class, interface subclass, and interface protocol of this particular interface.
For this project, we will be using endpoint #0, which is the "control endpoint" (default for standard requests from the computer), and endpoint #1, which is a "interrupt-in" endpoint that we'll be sending USB HID reports (these reports contain the gamepad data) through. The class will be 0x03, indicating Human Interface Device (HID), the subclass and protocol are both 0x00.
More Reading:
Step 5: USB HID Reports and Report Descriptors
Take a look at your Wii Classic Controller. It has 15 buttons (round up to 16 for simplicity), and two joysticks. This means we need to send 15 bits of data for the buttons, and 4 numbers, one for each axis of joystick data. We define our data format to look like this:
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | |
Byte 0 | Button | Button | Button | Button | Button | Button | Button | Button |
Byte 1 | Button | Button | Button | Button | Button | Button | Button | Button |
Byte 2 | Left Stick X Axis as Signed Char Integer | |||||||
Byte 3 | Left Stick Y Axis as Signed Char Integer | |||||||
Byte 4 | Right Stick X Axis as Signed Char Integer | |||||||
Byte 5 | Right Stick Y Axis as Signed Char Integer |
And then we can define a data structure in C/C++
struct gamepad_report_t { uint16_t buttons; int8_t left_x; int8_t left_y; int8_t right_x; int8_t right_y; }Writing a report descriptor involves describing a usage context first, and then describing the meaning of the data relative to the usage context, and then describing the data in terms of its range and size.
First, make the computer understand that the device is a gamepad
USAGE_PAGE (Generic Desktop) USAGE (Game Pad) COLLECTION (Application) COLLECTION (Physical) ... END COLLECTION END COLLECTIONThen describe the button data (16 bits)
USAGE_PAGE (Button) USAGE_MINIMUM (Button 1) USAGE_MAXIMUM (Button 16) LOGICAL_MINIMUM (0) LOGICAL_MAXIMUM (1) REPORT_COUNT (16) REPORT_SIZE (1) INPUT (Data,Var,Abs)Then describe the 4 axis data as signed 8-bit integers
USAGE_PAGE (Generic Desktop) USAGE (X) USAGE (Y) USAGE (Z) USAGE (Rx) LOGICAL_MINIMUM (-127) LOGICAL_MAXIMUM (127) REPORT_SIZE (8) REPORT_COUNT (4) INPUT (Data,Var,Abs)
- NOTE: Z is used to represent the right stick's X axis, Rx is used to represent the right stick's Y axis. This doesn't make sense but this is how most existing USB game pads work. I have tested this using Battlefield Bad Company 2, it works.
- NOTE: Use "absolute" for something like joysticks, but "relative" for things like mouse.
Finally, the report descriptor looks like:
USAGE_PAGE (Generic Desktop) USAGE (Game Pad) COLLECTION (Application) COLLECTION (Physical) USAGE_PAGE (Button) USAGE_MINIMUM (Button 1) USAGE_MAXIMUM (Button 16) LOGICAL_MINIMUM (0) LOGICAL_MAXIMUM (1) REPORT_COUNT (16) REPORT_SIZE (1) INPUT (Data,Var,Abs) USAGE_PAGE (Generic Desktop) USAGE (X) USAGE (Y) USAGE (Z) USAGE (Rx) LOGICAL_MINIMUM (-127) LOGICAL_MAXIMUM (127) REPORT_SIZE (8) REPORT_COUNT (4) INPUT (Data,Var,Abs) END COLLECTION END COLLECTION
Now that we have a report descriptor, how do we make our AVR tell this stuff to the computer? Go download the official "HID Descriptor Tool" from http://www.usb.org/developers/hidpage/ . Use it to put this stuff in, and the tool will generate the correct binary array for you.
When you save your result, go to "File" -> "Save As", and then make sure you choose "Header File (*.h)" in the save dialog. Then open the file, it should look like
char ReportDescriptor[ some number here (size of array)] { a lot of bytes here };
Great, the size of array needs to be copied into V-USB's "usbconfig.h" where it says "USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH", and "char ReportDescriptor" needs to be renamed to "PROGMEM char usbHidReportDescriptor" so that it is stored into the AVR's flash memory. You end up with something like:
PROGMEM char usbHidReportDescriptor[46] = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x05, // USAGE (Game Pad) 0xa1, 0x01, // COLLECTION (Application) 0xa1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x10, // USAGE_MAXIMUM (Button 16) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x10, // REPORT_COUNT (16) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x09, 0x32, // USAGE (Z) 0x09, 0x33, // USAGE (Rx) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7f, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x04, // REPORT_COUNT (4) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION 0xc0 // END_COLLECTION };That piece of code, plus the struct typedef we did earlier, is included in our project source code (see whole source code) later.
More Reading:
Step 6: Handling USB Standard Requests
Each request comes in a standard format, which can be mapped to a data structure that describes the request in terms of its size, meaning, and contents. It looks like:
typedef struct usbRequest{ uchar bmRequestType; uchar bRequest; usbWord_t wValue; usbWord_t wIndex; usbWord_t wLength; }usbRequest_t;
V-USB's API requires you to make a function called "usbFunctionSetup" which takes 8 bytes in. These 8 bytes are casted to the type "usbRequest_t". Then, the request is decoded and fulfilled. There are three things we must handle:
- Request for the report
- Request for the report descriptor
- Request to set the idle rate
So the piece of code looks like:
static unsigned char idleRate; unsigned char usbFunctionSetup(unsigned char data[8]) { usbRequest_t *rq = (void *)data; if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) /* class request type */ { if (rq->bRequest == USBRQ_HID_GET_REPORT) /* wValue: ReportType (highbyte), ReportID (lowbyte) */ { usbMsgPtr = &gamepad_report; return sizeof(gamepad_report); } else if (rq->bRequest == USBRQ_HID_GET_IDLE) { usbMsgPtr = &idleRate; return 1; } else if (rq->bRequest == USBRQ_HID_SET_IDLE) { idleRate = rq->wValue.bytes[1]; } } else { // no vendor specific requests implemented } return 0; }This piece of code goes into the whole source code.
More Reading:
Step 7: Connecting the Wii Classic Controller
For prototyping purposes, I have soldered the wires to a male header.
Note that the device-detection wire on the cheap extension cables do not actually work (they are connected directly to +3.3V), thus I am unable to truly detect a device-connect or disconnect event in the software.
The signals are:
- Ground, connect to ground on the USnooBie
- +3.3V power supply, connect to USnooBie's +3.3V pin
- SDA (this is the I2C data line), connect to PC4 on USnooBie
- SCL (I2C clock line), connect to PC5 on USnooBie
Make sure that USnooBie is supplied with 3.3V by using the voltage selection jumper. This is so that the internal pull-up resistors (on the I2C lines) of the ATmega328P will use 3.3V instead of 5V. Also the Wii Classic Controller must be powered with 3.3V as well.
Step 8: TWI / I2C Explained
Related Readings:
- http://en.wikipedia.org/wiki/I%C2%B2C
- I2C Tutorial - Embedded Labs
- https://www.instructables.com/id/music-playing-alarm-clock/step22 < my other instructable that explains I2C
On a TWI bus, the two signal wires are SDA and SCL, basically data and clock. These signals are open drain (meaning its logic level is either high impedance or low, it cannot ever be high), but there must be a pull-up resistor on each of these signals (we are using the AVR's internal pull-up resistors). This is significant because any device on a TWI bus can drive the signals low at any time, so the signal can only become high when all the devices allow it to become high. This allows devices to detect when the bus is occupied ("arbitration using SDA") and also allow a slow device to dictate the speed of the clock, or even pause a transmission if the slower device is too busy (doing this is called "clock stretching". These facts makes the TWI bus good for communication between a bunch of chips using only two wires.
Every transaction is between a master (the one driving the clock signal) and a slave device. Every transaction starts with a "start condition" and ends with a "end condition". A start condition is when the bus master drives SDA low first, then driving SCL low second. An end condition is when the master releases the TWI bus by releasing SCL and then releasing SDA.
After the start condition, the master has to choose which device to talk to by sending a 7 bit address byte. The 8th (last being sent) bit indicates whether or not the master wishes to read (1) from or write (0) to the slave being addressed. If the master is writing, it will then send more data. If the master is reading, it will release the SDA line so the slave sends data (but the master is still driving the clock). When addressed
All bytes are sent MSB first (most significant bit first). Every byte is optionally ended by an acknowledgement/nacknowledgement. Check the device datasheet to see what the device will expect or will send back. Usually, to quote Wikipedia: "If the master wishes to write to the slave then it repeatedly sends a byte with the slave sending an ACK bit. (In this situation, the master is in master transmit mode and the slave is in slave receive mode.) If the master wishes to read from the slave then it repeatedly receives a byte from the slave, the master sending an ACK bit after every byte but the last one. (In this situation, the master is in master receive mode and the slave is in slave transmit mode.)"
More intricate details are usually specific to a particular device, and such information will come from its datasheet.
When I use I2C/TWI with AVR microcontrollers, I use the low level layer of the "Wire" library for Arduino. The Wire library is the C++ wrapper for the lower level "twi.c" and "twi.h" module, which I modify slighly and compile into my own code (since I don't usually use C++). It takes care of almost everything.
The Wii Classic Controller has a I2C address of 0x52, keep that in mind. Using "twi.c" and "twi.h", to send some data to the Wii Classic Controller, start by creating a byte array containing the data to be sent, and then pass that to the "twi_writeTo" function, along with the destination address, the amount of data to send, and tell it "wait until all data is sent". The code will look like:
unsigned char dataArray[3] = { 'a', 'b', 'c', }; twi_writeTo(0x52, dataArray, 3, 1);
To read three bytes, use the function "twi_readFrom", and tell it the address, the data is saved to a array you pass in, and you specify the amount of data. The code looks like:
unsigned char dataArray[3]; twi_readFrom(0x52, dataArray, 3);
Step 9: Reading the Wii Classic Controller
In our project source code, we initialize the Wii Classic Controller by:
- Initializing the TWI/I2C module of the AVR, enabling the pull-up resistors
- Sending it 0x40 0x00
- Sending it a sequence of false encryption keys, so the decryption is easy
In the main loop of the code, data is read from the Wii Classic Controller by
- Sending it 0x00, this sets a read pointer
- Reading 6 bytes of data from it
Bit | ||||||||
Byte | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0 | RX<4:3> | LX<5:0> | ||||||
1 | RX<2:1> | LY<5:0> | ||||||
2 | RX<0> | LT<4:3> | RY<4:0> | |||||
3 | LT<2:0> | RT<4:0> | ||||||
4 | BDR | BDD | BLT | B- | BH | B+ | BRT | 1 |
5 | BZL | BB | BY | BA | BX | BZR | BDL | BDU |
LX,LY are the left Analog Stick X and Y (0-63), RX and RY are the right Analog Stick X and Y (0-31), and LT and RT are the Left and Right Triggers (0-31). The left Analog Stick has twice the precision of the other analog values.
BD{L,R,U,D} are the D-Pad direction buttons. B{ZR,ZL,A,B,X,Y,+,H,-} are the discrete buttons. B{LT,RT} are the digital button click of LT and RT. All buttons are 0 when pressed.
Credit to http://wiibrew.org/wiki/Wiimote/Extension_Controllers for this valuable information.In our project, the data format must be sent through USB in this format:
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | |
Byte 0 | Button | Button | Button | Button | Button | Button | Button | Button |
Byte 1 | Button | Button | Button | Button | Button | Button | Button | Button |
Byte 2 | Left Stick X Axis as Signed Char Integer | |||||||
Byte 3 | Left Stick Y Axis as Signed Char Integer | |||||||
Byte 4 | Right Stick X Axis as Signed Char Integer | |||||||
Byte 5 | Right Stick Y Axis as Signed Char Integer |
Our source code does a bit of simple binary math to perform the transformation. Note that buttons needs to be 1 when pressed so we invert the button data from the Wii Classic Controller.
When the microcontroller starts up, it also takes a reference center reading for the joysticks to calibrate it, this center value is combined with a low-pass-filtered offset to eliminate noisy readings.
See the whole source code for more details.
I've also attached a logic analyzer session file ( .logicsession file, can be viewed with Saleae Logic 1.1.9), and a text format log file of I2C communications with the Wii Classic Controller. You can download these and study the communication in-depth.
Step 10: Compiling and Loading the Code
If you are using the USnooBie, then you can follow this guide:
Usage Guide
This is the short version of the official usage guide, go visit the official usage guide for more details.
To enter the bootloader, hold down the bootloader-activation button, then press and release the reset button, then release the bootloader-activation button. The bootloader should appear to your computer as an USBasp programmer, so you may now use it as though you were using an USBasp with AVRDUDE.
A typical AVRDUDE command would start with "avrdude -c usbasp -p atmega328p", and to load a hex file into flash memory, it looks like "avrdude -c usbasp -p atmega328p -U flash:w:filename.hex". You will not be able to modify any fuse-bits, which prevents you from "damaging" the bootloader.
There's a good chance that you'll require the USBasp drivers in order for the USBaspLoader bootloader to work. The drivers can be found here
Step 11: Finishing Up
To replicate the circuit, see the downloads page for USnooBie, it's open source and all the schematic and PCB files are available.
This step is optional, depending on how much money you want to spend.
Step 12: Testing and Using
Do not disconnect the Wii Classic Controller from the circuit while it is plugged into USB. If you do, you might need to disconnect and reconnect the USB cable in order to force a reset. This is because the cheap extension cable connector does not allow me to implement device detection properly. In the source code, I'm depending on the watch-dog timer to automatically perform this reconnection, the watchdog timer will timeout in the TWI function while waiting for the rely from the Wii Classic Controller and cause the reset, which causes the simulated reconnection.
Open up the joystick thing in Window's control panel and watch it work.
If you want to use it in a video game, be sure to map all the buttons first.
If a joystick axis is inverted, then you can go into the source code and multiply its value by negative one. In fact, you can do a lot of creative things in the source code! You can add scaling to the joysticks, or rapid tapping behaviour to the buttons if you want.
Step 13: Bonus: Keyboard and Mouse
USB Mouse
The HID report descriptor has been modified to indicate that the usage is a mouse pointer. The X and Y movements are now relative instead of absolute. There are only three bits used for the mouse buttons (left click, right click, middle click), there is also vertical mouse wheel scrolling, and horizontal scrolling (doesn't really work without Logitech drivers, as it's not a standard feature).
The data structure becomes something like this:
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | |
Byte 0 | Useless | Useless | Useless | Useless | Useless | Middle Button | Left Button | Right Button |
Byte 1 | X Axis Relative Movement as Signed Integer | |||||||
Byte 2 | Y Axis Relative Movement as Signed Integer | |||||||
Byte 3 | Vertical Scroll as Signed Integer | |||||||
Byte 4 | Horizontal Scroll as Signed Integer |
The corresponding C data struct looks like:
static struct mouse_report_t { uint8_t buttons; // button mask ( . . . . . M L R ) int8_t x; // mouse x movement int8_t y; // mouse y movement int8_t v_wheel; // mouse wheel movement int8_t h_wheel; // mouse wheel movement } mouse_report;
The HID report descriptor looks like
PROGMEM char usbHidReportDescriptor[61] = { // make sure the size matches USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH in usbconfig.h 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) 0xa1, 0x01, // COLLECTION (Application) 0x09, 0x01, // USAGE (Pointer) 0xa1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x03, // USAGE_MAXIMUM (Button 3) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x03, // REPORT_COUNT (3) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x09, 0x38, // USAGE (Wheel) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7f, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x03, // REPORT_COUNT (3) 0x81, 0x06, // INPUT (Data,Var,Rel) 0x05, 0x0c, // USAGE_PAGE (Consumer Devices) 0x0a, 0x38, 0x02, // USAGE (Undefined) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x06, // INPUT (Data,Var,Rel) 0xc0, // END_COLLECTION 0xc0 // END_COLLECTION };
Some minor changes were made in "usbconfig.h", mainly the vendor and product IDs were changed to clone a Logitech brand mouse. The product and manufacture strings were changed but this does not take effect in Windows due to the fact that Windows update finds the product information from Windows Update. The length of the HID report descriptor is also changed to match the size of the array.
USB Keyboard
The HID report descriptor has been modified to indicate that the usage is a keyboard. This descriptor is slightly more complicated. We send 8 bytes of data, the first byte is a modifier byte containing bit flags for the shift, CTRL, ALT, and other modifier keys. The 2nd byte is useless. The last 6 bytes contain key codes (not ASCII) of the keys being pressed.
The C data struct looks like:
static struct keyboard_report_t { uint8_t modifier; // bit flags for shift, ctrl, and alt, and other stuff uint8_t reserved; // useless for now uint8_t key[6]; // HID keycodes } keyboard_report;
The HID report descriptor looks like
PROGMEM char usbHidReportDescriptor[63] = { // make sure the size matches USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH in usbconfig.h 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xa1, 0x01, // COLLECTION (Application) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x08, // REPORT_COUNT (8) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x95, 0x05, // REPORT_COUNT (5) 0x75, 0x01, // REPORT_SIZE (1) 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (Num Lock) 0x29, 0x05, // USAGE_MAXIMUM (Kana) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) 0x95, 0x06, // REPORT_COUNT (6) 0x75, 0x08, // REPORT_SIZE (8) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x65, // LOGICAL_MAXIMUM (101) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0xc0 // END_COLLECTION };
This descriptor is sort of "standardized" so that it works without an operating system. "usbconfig.h" contains some changes to the interface subclass and protocol to enable "boot protocol" so that it works without an operating system (such as when you are in BIOS menus).
A V-USB function called "usbFunctionWrite" is also created to handle the situation when the computer wants to tell the keyboard to turn on or off status LEDs, such as the CAPS LOCK, NUM LOCK, or SCROLL LOCK. The function is written but it doesn't actually do anything.
// data from the computer is handled here // this is actually going to be stuff like NUM LOCK, CAPS LOCK, and SCROLL LOCK LED data unsigned char usbFunctionWrite(unsigned char *data, unsigned char len) { // ignore this data return len; }
The function "usbFunctionSetup" is a little longer to handle some more stuff such as changing the current protocol, and calling "usbFunctionWrite".
Some other minor changes were made in "usbconfig.h", mainly the vendor and product IDs were changed. The length of the HID report descriptor is also changed to match the size of the array.
USB Combo Device
I know how to write the descriptor and data structures for a USB combination device (all-in-one keyboard-mouse-gamepad) but the performance is kind of bad. However, it does sort of work.
The trick is using "Report ID" inside collections of the descriptor. Each collection represents a different device with a unique ID. The ID is sent at the top of each report. So the descriptor looks like this (really long):
PROGMEM char usbHidReportDescriptor[176] = { // make sure the size matches USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH in usbconfig.h // start of keyboard report descriptor 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x01, // REPORT_ID (1) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x08, // REPORT_COUNT (8) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x95, 0x05, // REPORT_COUNT (5) 0x75, 0x01, // REPORT_SIZE (1) 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (Num Lock) 0x29, 0x05, // USAGE_MAXIMUM (Kana) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) 0x95, 0x06, // REPORT_COUNT (6) 0x75, 0x08, // REPORT_SIZE (8) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x65, // LOGICAL_MAXIMUM (101) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0xc0, // END_COLLECTION // start of mouse report descriptor 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) 0xa1, 0x01, // COLLECTION (Application) 0x09, 0x01, // USAGE (Pointer) 0xa1, 0x00, // COLLECTION (Physical) 0x85, 0x02, // REPORT_ID (2) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x03, // USAGE_MAXIMUM (Button 3) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x03, // REPORT_COUNT (3) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x09, 0x38, // USAGE (Wheel) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7f, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x03, // REPORT_COUNT (3) 0x81, 0x06, // INPUT (Data,Var,Rel) 0x05, 0x0c, // USAGE_PAGE (Consumer Devices) 0x0a, 0x38, 0x02, // USAGE (Undefined) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x06, // INPUT (Data,Var,Rel) 0xc0, // END_COLLECTION 0xc0, // END_COLLECTION // start of gamepad report descriptor 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x05, // USAGE (Game Pad) 0xa1, 0x01, // COLLECTION (Application) 0xa1, 0x00, // COLLECTION (Physical) 0x85, 0x03, // REPORT_ID (3) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x10, // USAGE_MAXIMUM (Button 16) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x10, // REPORT_COUNT (16) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) // left X 0x09, 0x31, // USAGE (Y) // left Y 0x09, 0x32, // USAGE (Z) // right X 0x09, 0x33, // USAGE (Rx) // right Y 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7f, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x04, // REPORT_COUNT (4) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION 0xc0 // END_COLLECTION };
And the structures now include a report ID:
static struct keyboard_report_t { uint8_t report_id; uint8_t modifier; // bit flags for shift, ctrl, and alt, and other stuff uint8_t reserved; // useless for now uint8_t key[6]; // HID keycodes } keyboard_report; static struct mouse_report_t { uint8_t report_id; uint8_t buttons; // button mask ( . . . . . M L R ) int8_t x; // mouse x movement int8_t y; // mouse y movement int8_t v_wheel; // mouse wheel movement int8_t h_wheel; // mouse wheel movement } mouse_report; static struct gamepad_report_t { uint8_t report_id; uint16_t buttons; int8_t left_x; int8_t left_y; int8_t right_x; int8_t right_y; } gamepad_report;
Inside "usbFunctionSetup", we need to check which report is requested by checking "wValue" for the report ID:
// check report ID requested if (rq->wValue.bytes[0] == 1) { usbMsgPtr = &keyboard_report; return sizeof(keyboard_report); } else if (rq->wValue.bytes[0] == 2) { usbMsgPtr = &mouse_report; return sizeof(mouse_report); } else if (rq->wValue.bytes[0] == 3) { usbMsgPtr = &gamepad_report; return sizeof(gamepad_report); }
Before sending each report, the report ID is set in the data structure.
keyboard_report.report_id = 1; // set report ID so computer knows what data struct is sent // wait until ready to send, then send it while (1) { usbPoll(); if (usbInterruptIsReady()) { usbSetInterrupt((unsigned char *)(&keyboard_report), sizeof(keyboard_report)); break; } }
And in the end, windows does recognize this device as a combination device.
Step 14: Learn More
On my website, I have plenty of tutorials showing you how to make these things, with examples using RFID readers, PlayStation controllers, optical mouse sensors, etc.
https://www.instructables.com/id/USB-RFID-Reading-Keyboard/ < Please watch me! I've put an insane amount of effort into this video.
http://www.frank-zhao.com/usnoobie/tut_proj.php
Also try to apply the technique used to read the Wii Classic Controller to your other projects. I originally purchased it as a control method for a RC radio transmitter. Wii accessories are an extremely inexpensive way to add a solid input device to any project.