Introduction: USB Wii Classic Controller

About: I am an electrical engineer. I graduated from U of Waterloo. I used to work for Adafruit Industries as an EE. Now I work for Sony PlayStation as a EE.
This project will show you how to create a real USB game pad using a 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
Some of these pre-requisite skills would be nice
  • General electronics
  • AVR programming
The code will be based on V-USB, and the circuit will be based on a implementation V-USB example circuitry called USnooBie.

The entire project is provided as a ZIP file download below. Also see below for a flowchart of how the code will work.



Step 1: Using the AVR With V-USB

For the microcontroller, we'll be using a ATmega328P. This microcontroller has enough memory resources and a built-in TWI/I2C peripheral to accomplish our goals. However, it does not have built-in USB, which is a problem that we are going to solve using 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 official website

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...

  1. Make sure you've defined the processor and clock speed correctly (V-USB only supports certain clock speeds)
  2. Copy the folder "usbdrv" from the downloaded package into your project folder
  3. 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
  4. Inside the folder "usbdrv", there is a "usbconfig-prototype.h", copy that file into your main project directory, and rename it to "usbconfig.h"
  5. Edit "usbconfig.h", this will be explained in detail later
  6. Use "#include" statements to include "usbconfig.h" and then "usbdrv/usbdrv.h"
  7. 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")
Then your project must implement the some functions...
  • 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
More Reading:

Step 2: Building the Circuit

To simplify the process of creating V-USB based devices, I created the USnooBie.

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 resistors

These 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 resistor

This 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 resistor

This 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 diodes

These 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 diode

This 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 LED

This 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 connector

This 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 switch

One 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 socket

A 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 capacitors

The 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 crystal

The 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 jumper

A 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 fuse

This 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 capacitor

This 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 regulator

This 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 headers

There 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 ground

Use 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 bus

Use 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 check

Plug 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 microcontroller

Insert 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 uses two wires, usually called D+ and D-, to transmit serial data using non-return-to-zero coding. These two wires are a differential pair, meaning D-'s signal will always be D+'s inverted state. This allows it to transfer data really fast over a long cable (5 meter cable at 480 Mbit/s if using USB 2.0 high speed), read this for more http://en.wikipedia.org/wiki/Differential_pair

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

Here's the USB in a Nutshell page on descriptors, you need to refer to this some times.

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

When you make a HID device, the device sends short reports at regular intervals to the computer. These reports contain information about which buttons are pressed and how much the joysticks have moved. However, in order for the host to understand the data format, the device must "describe" the reports to the host. This is why we write a USB HID report descriptor.

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 COLLECTION
Then 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

The host will eventually send some requests to our device, maybe to request a descriptor, the report, or set some parameters (notably, the "idle rate").

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

Since you do not want to cut the cable on your Wii Classic Controller, go buy a cheap extension cable for Wiimote accessories. These have a female connector that you will need to cut off. Strip the outer coating of the extension cable to access the wires inside.

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

I2C stands for Inter-Integrated Circuit. I2C busses are also known as TWI for Two Wire Interface, since it uses only two wires.

Related Readings:
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

The Wii Classic Controller has an address of 0x52, which must be specified as the first byte of all I2C transmissions so it knows that it's being spoken to.

In our project source code, we initialize the Wii Classic Controller by:
  1. Initializing the TWI/I2C module of the AVR, enabling the pull-up resistors
  2. Sending it 0x40 0x00
  3. Sending it a sequence of false encryption keys, so the decryption is easy
The data traveling on the bus after this initialization have been performed will still be encrypted, but since we've just gave it the fake keys, we know exactly how to decrypt it. See source code for details.

In the main loop of the code, data is read from the Wii Classic Controller by
  1. Sending it 0x00, this sets a read pointer
  2. Reading 6 bytes of data from it
Here's the data format for the 6 bytes of data that we read from the Wii Classic Controller:
 Bit
Byte76543210
0RX<4:3>LX<5:0>
1RX<2:1>LY<5:0>
2RX<0>LT<4:3>RY<4:0>
3LT<2:0>RT<4:0>
4BDRBDDBLTB-BHB+BRT 1
5BZLBBBYBABXBZRBDLBDU

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

The project can be opened with AVR Studio 5 or AVR-Project-IDE, open it and build the project. The hex file that is generated can be loaded into the ATmega328P using AVRDUDE.

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

I purchased a small plastic enclosure, and a prototyping circuit board. In the front of the enclosure, I cut a hole for the USB connector and the Wii extension connector. I glued the Wii extension connector into the correct location on the circuit board and soldered the wires to the circuit board. I replicated the USnooBie circuitry onto the circuit board. The circuit board is then mounted into the enclosure. This created a neat final finished product.

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

Plug it in and it should cause your operating system to automatically install the drivers required. If the vendor ID and product ID is causing problems (conflicts with older drivers, etc), go modify them in "usbconfig.h", recompile the project, and re-program the AVR with the new recompiled hex file.

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

By modifying the HID report descriptors slightly, we can also turn this project into a USB keyboard or USB mouse, or a combination device. I have attached all of the source code.


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

I've become pretty handy with the USnooBie. It is very easy for me to turn this project into a USB mouse instead, or even create a USB combination device that is a mouse, gamepad, and keyboard all at once. I have already done this over and over again using the Wii Nunchuk and PlayStation controllers. USnooBie makes this really easy.

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.