Wii Nunchuk USB Gamepad and Mouse

This projects let's you use a Nintendo Nunchuk controller as an USB gamepad on your computer (a joystick, 2 buttons, and it also 3 axis of acceleration data). It also let's you use it as an USB mouse, but this function can be disabled by a jumper. Tilting the Nunchuk is the same as moving the mouse wheel.

Let me get some important and frequently mentioned web links out of the way:

This tutorial will show you how to make a USB gamepad and USB mouse device using an ATmega168 and a Nintendo Wii Nunchuk Controller.

Since the ATmega168 does not have any built-in USB functionality, I used V-USB by Objective Development to implement the USB functionality.

The amount of flash memory required by this project is actually less than what is available on an ATmega8 chip, so you can use an ATmega8 instead if you reconfigure the project. I used an ATmega168 because that's what I had.

Hardware

I will draw a series of circuit schematic diagrams, follow along (with a breadboard maybe? just build with me!). First, take care of some basic stuff.

From the above information, we start building something like this:

Now make the circuit suitable with V-USB. Take a look at the circuit diagram taken from V-USB's website:

That circuit is for a project that needs 3.3V (those two diodes have a voltage drop that drops the 5V down to about 3.3V), we'll change it to use 5V instead. Take a look at USBasp's schematic:

Notice the added 3.6V Zener diodes, and how the 1.5 kohm resistor changed into a 2.2 kohm resistor (I'm not sure why, but do it). The Zener diodes are there because the USB bus should still be 3.3V, so they make sure the bus is still safe for other 3.3V devices even when your 5V device is plugged in.

From this change, we come up with something like this:

Take note of where the D+ and D- lines are connected to the ATmega168, these will be defined in "usbconfig.h" (look for "USB_CFG_IOPORTNAME", "USB_CFG_DMINUS_BIT", "USB_CFG_DPLUS_BIT")

The Nunchuk controller communicates with the ATmega168 by the TWI bus. First take a few notes:

The first problem to solve: reading data from a Nintendo Nunchuk Controller.

The Nintendo Nunchuk Controller is a TWI device (also sometimes known as I2C). The ATmega168 has a built-in TWI module which we are going to use. There are a few things to note:

So we add to the circuit:

Here are some cheap cables that you can buy and cut up in order to connect the Nunchuk.

The pin-out of the extension port is shown below, the order from left to right is the same order looking at the back of the Wiimote.
Ground No Connection SCL
SDA Device Detect (Ignore Me) Vcc (3.3V)

Now at some point I realized that I would like to disable the mouse functionality somehow, since in a video game, I'd like to use a real mouse while the movement is controlled by the joystick. I added a jumper to the pin PC0 to enable the mouse.

That's pretty much the step-by-step of how to build the circuit. Here's the whole thing:

Firmware

Let's start coding! I'm using my own AVR Project IDE for this. You should download my project files from here (so you don't ACTUALLY have to follow me step by step). Start a new project (apply the template called "maximum optimization"), and start with a blank "main.c" file. The project should be configured with:

Go download the V-USB software from here. Copy the folder "usbdrv" into your project folder. Include these files into your project: "usbdrv.c", "usbdrvasm.S".

Now you need to configure V-USB by writing "usbconfig.h" by copying and modifying the provided "usbconfig-prototype.h" . Here's what I changed:

Add "usbconfig.h" to your project. Good, now V-USB is configured. Now you need to understand that a joystick and mouse are HID (Human Interface Device) devices. Your USB device will regularly send back a chunk of data to the computer called a "report", but your computer does not know how to interpret this data. To help the computer understand what your device is sending, we need to write a HID report descriptor. (This is also why we don't need to write any drivers)

I used this USB HID report descriptor tool (somewhere on that page) to help me write the descriptor. Use the tool's "save as" feature to save as a .h header file, which is how I generated the "hid_desc.h" file. Some modification afterwards was required to make it work with V-USB.

Take a look at the contents of "hid_desc.h" and add it to the project. I will talk about it as you read it from top down. Please note that even I don't understand this report descriptor stuff fully, I'm just making sense of some examples I've seen and passing what I figured out to you. I'm not even sure if what I wrote is right but it works on my computer (Windows 7 Pro 64 bit).

My report descriptor has two major halves, one for the joystick report and one for the mouse report. Take notice of "REPORT_ID", the joystick part uses 1 for its "REPORT_ID" and the mouse uses 2. This makes it so that the first byte of my report is an ID telling the computer what kind of data I'm sending. If this byte is 1, then the information that follows is to be used for the joystick, and if this byte is 2, then the information is for the mouse. This is how two devices can be implemented with one USB device.


0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
0x09, 0x04,                    // USAGE (Joystick)
0xa1, 0x01,                    // COLLECTION (Application)
0x09, 0x01,                    //   USAGE (Pointer)
0xa1, 0x00,                    //   COLLECTION (Physical)
0x85, 0x01,                    //     REPORT_ID (1)




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)


Look at the first half, after the "REPORT_ID". I then tell the computer that I want to report the X axis and Y axis readings of the joystic, each reading should have values between -127 and 127, and each value should use 8 bits, and there are 2 such values.


0x09, 0x30,                    //     USAGE (X)
0x09, 0x31,                    //     USAGE (Y)
0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
0x75, 0x08,                    //     REPORT_SIZE (8)
0x95, 0x02,                    //     REPORT_COUNT (2)
0x81, 0x02,                    //     INPUT (Data,Var,Abs)


Then I have three more readings that are 8 bits between -127 and 127, these are for the three accelerometer axis.


0x09, 0x33,                    //     USAGE (Rx)
0x09, 0x34,                    //     USAGE (Ry)
0x09, 0x35,                    //     USAGE (Rz)
0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
0x75, 0x08,                    //     REPORT_SIZE (8)
0x95, 0x03,                    //     REPORT_COUNT (3)
0x81, 0x02,                    //     INPUT (Data,Var,Abs)


Then I tell it that I want to report the status of a range of buttons, button 1 and button 2. They should be either on or off, each using 1 bit. These are used for the two buttons on the Nunchuk controller.


0x05, 0x09,                    //     USAGE_PAGE (Button)
0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
0x29, 0x02,                    //     USAGE_MAXIMUM (Button 2)
0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
0x75, 0x01,                    //     REPORT_SIZE (1)
0x95, 0x02,                    //     REPORT_COUNT (2)
0x81, 0x02,                    //     INPUT (Data,Var,Abs)


But wait, that only uses 2 bits, and a byte should be 8 bits, so I then tell the computer to expect 6 more bits that are meaningless.


0x75, 0x01,                    //     REPORT_SIZE (1)
0x95, 0x06,                    //     REPORT_COUNT (6)
0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)


Now look at the 2nd half of the descriptor, the half about the mouse. After "REPORT_ID" (which I've already explained), I tell the computer to expect data represeting the status of three buttons ranging from button 1 to button 3. Each button status should be represented by a single bit, either on or off. And then like before, add 5 more useless meaningless padding bits to make 8 bits.


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)
0x75, 0x01,                    //     REPORT_SIZE (1)
0x95, 0x03,                    //     REPORT_COUNT (3)
0x81, 0x02,                    //     INPUT (Data,Var,Abs)




0x75, 0x01,                    //     REPORT_SIZE (1)
0x95, 0x05,                    //     REPORT_COUNT (5)
0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)


Then the computer should expect three values, betweem -127 and 127, each value using 8 bits, and there are three such values. These are used for the mouse movement axis and the mouse wheel movement. These values are relative to the values before (hence the "INPUT (Data,Var,Rel)" and not "INPUT (Data,Var,Abs)").


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)


Now from this info, I can define the two data structures needed to represent the reports. These are found at the bottom of "hid_desc.h". Read the comments.


typedef struct
{
    uint8_t report_id; // report_id should be 1
    int8_t jx; // joystick x axis
    int8_t jy; // joystick y axis
    int8_t ax; // accelerometer x axis
    int8_t ay; // accelerometer y axis
    int8_t az; // accelerometer z axis
    uint8_t but; // button mask ( . . . . . . C Z )
} joystick_report_t;

typedef struct
{
    uint8_t report_id; // report_id should be 2
    uint8_t but; // button mask ( . . . . . M R L )
    int8_t x; // mouse x movement
    int8_t y; // mouse y movement
    int8_t wheel; // mouse wheel movement
} mouse_report_t;


There's one last thing we need to do to get the V-USB part of the project working, there's a function called "usbFunctionSetup" that needs to be put into "main.c".


uchar usbFunctionSetup(uchar 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) */
        {
            if (rq->wValue.bytes[0] == 1) // check report ID
            {
                joystick_report.report_id = 1; // always 1
                // reset all data
                joystick_report.jx = 0;
                joystick_report.jy = 0;
                joystick_report.ax = 0;
                joystick_report.ay = 0;
                joystick_report.az = 0;
                joystick_report.but = 0;
                
                usbMsgPtr = &joystick_report; // send it
                return sizeof(joystick_report);
            }
            else if (rq->wValue.bytes[0] == 2)
            {
                mouse_report.report_id = 2; // always 2
                mouse_report.x = 0;
                mouse_report.y = 0;
                mouse_report.but = 0;
                mouse_report.wheel = 0;
                
                usbMsgPtr = &mouse_report; // send it
                return sizeof(mouse_report);
            }
        }
        else if (rq->bRequest == USBRQ_HID_GET_IDLE)
        {
            usbMsgPtr = &idleRate;
            return sizeof(idleRate);
        }
        else if (rq->bRequest == USBRQ_HID_SET_IDLE)
        {
            idleRate = rq->wValue.bytes[1];
        }
    }
    else
    {
        // no vendor specific requests implemented
    }
    
    return 0; // send nothing
}


The idea is that this handles class specific request data coming from the computer to your device. We are making a joystick and mouse so this is pretty short. The computer can request the specific report, set or get the idle rate. We store the idle rate in a global char variable but ignore it.

Good, now we can figure out the next part of the puzzle, reading data from a Nintendo Nunchuk.

Before continueing, you should read up on the following resources:

Also by now you should just load up the contents of "main.c" and read it along with this web page.

As I've already said, the Nunchuk talks with the ATmega168 using the TWI bus. The TWI code used in this project was not written by me, they are two files that are also used by the Wire library (an Arduino library). The files are "twi.c" and "twi.h". I modified both files. In twi.c, I modified the initialization function so that it did not touch the IO ports of the ATmega168 (since the pull-up resistors must be implemented externally), and in twi.h, I defined CPU_FREQ to be F_CPU (which should be 12 MHz, defined as a compiler option).

Note that all Nunchuk controllers use a TWI device address of 0x52. This is defined in my code as "NUNCHUK_ADDR".

First understand that the Nunchuk operates like a device with a set of registers. You tell it which register address to point at, and then the next set of write (or read) operations will be done to (or from) that register and the ones that follow (each operation auto-increments the pointer).

Take a look at the functions "twi_writeTo" and "twi_readFrom" in "twi.c".


/* 
 * Function twi_writeTo
 * Desc     attempts to become twi bus master and write a
 *          series of bytes to a device on the bus
 * Input    address: 7bit i2c device address
 *          data: pointer to byte array
 *          length: number of bytes in array
 *          wait: boolean indicating to wait for write or not
 * Output   0 .. success
 *          1 .. length to long for buffer
 *          2 .. address send, NACK received
 *          3 .. data send, NACK received
 *          4 .. other twi error (lost bus arbitration, bus error, ..)
 */
uint8_t twi_writeTo(uint8_t address, uint8_t* data, uint8_t length, uint8_t wait)




/* 
 * Function twi_readFrom
 * Desc     attempts to become twi bus master and read a
 *          series of bytes from a device on the bus
 * Input    address: 7bit i2c device address
 *          data: pointer to byte array
 *          length: number of bytes to read into array
 * Output   number of bytes read
 */
uint8_t twi_readFrom(uint8_t address, uint8_t* data, uint8_t length)


The address is always 0x52, defined as "NUNCHUK_ADDR" as I said. To initialize the Nunchuk. We write 0x00 to register address 0x40.


uint8_t nc_data[6];
    
// initialize nunchuk by writing 0x00 to address 0x40
nc_data[0] = 0x40;
nc_data[1] = 0x00;
twi_writeTo(NUNCHUK_ADDR, nc_data, 2, 1);
_delay_us(500); // the nunchuk needs some time to process


To read the joystick, accelerometer, and button data, set the register address to 0x00 and read 6 bytes.


// read data by setting read pointer to 0 and then reading 6 bytes
nc_data[0] = 0;
twi_writeTo(NUNCHUK_ADDR, nc_data, 1, 1);
_delay_us(500); // the nunchuk needs some time to process
twi_readFrom(NUNCHUK_ADDR, nc_data, 6);


But the data is encrypted, we can decrypt it like so:


// decode the encrypted data
for (int i = 0; i < 6; i++)
{
    nc_data[i] = (nc_data[i] ^ 0x17) + 0x17;
}


The above method is from Windmeadow Labs (linked above). Why is decrypting the data so easy? It's because usually the Wii tells the Nunchuk how to encrypte the data, but we never told it to, so it's using the default. (This is a really simplified way of explaining it)

Now if you keep reading "main.c", the code does some math to get the numbers in range, and load it up to the two report data structures. If mouse is disabled by the jumper on pin PC0, then the mouse data is zero-ed. The code waits until it is able to send the data via USB and then sends it.


while (1)
{
    usbPoll();
    if (usbInterruptIsReady())
    {            
        usbSetInterrupt((void *)&joystick_report, sizeof(joystick_report)); // report joystick data




// report mouse data at least once
while (1)
{
    usbPoll();
    if (usbInterruptIsReady())
    {
        usbSetInterrupt((void *)&mouse_report, sizeof(mouse_report));
        break;
    }
}


Of course, the code does this in a gigantic main loop forever. Reading from Nunchuk -> Decrypte -> Math -> Send USB Joystick Data -> Send USB Mouse Data -> Repeat.

The file "nunchuk_config.h" is there to help store some numbers which help with the math, allowing you to calibrate the joystick and accelerometer readings.

Now you should be able to compile and build the project. Upload the generated hex file to the chip! You must use the right fuse bits though, so make sure that you set the fuses to use external crystal oscillator faster than 8 Mhz with the highest power-down and reset time, do not divide the clock by 8, and do not output the clock on PB0. The reset vector shouldn't be enabled (unless you are using a bootloader). Brown-out detection is not needed, nor is watch-dog timer. Reset pin and SPI download should be always enabled. In the end, you should have:

Or refer to this page.

Electrical Waveforms

Well I talked about the code but what does this thing actually do in real life? Here's a logic analyzer waveform snapshot of the device after it's connected. You'll need Saleae Logic software 1.0.33 to view it (zoom in and analyze, etc). Below is only a preview screenshot:

Credits