Close

The USB dance

A project log for Wii Nunchuk as a USB HID controller

Project in which Wii Nunchuk controller is used as a USB HID controller for computer graphics apps.

MichalMichal 4 days ago0 Comments

In this log I wanted to describe my experience with implementing the USB firmware. Unfortunately I'm not writing this log as I go. Most of the things I have to recall from memory, so I might omit some struggles I went through.

The biggest problem with implementing reliable USB device firmware came from combining both I2C communication with USB functionality. Let me explain my initial idea.

I wanted the main loop to look like so:

main
{
  init_i2c();
  init_usb();
  systick_ON();

  while
  {
    data = i2c_read();
    usb_poll();
  }
}

SysTick
{
  usb_write_packet(data);
}

Before we go any further: 

This resulted in the OS failing to enumerate device. After some experimentation it was clear that running a while loop in which I2C polled the controller was not working well with the SysTick exception.

The next iteration looked like so:

main
{
  init_i2c();
  init_usb();
  systick_ON();

  while
  {
    usb_poll();
  }
}

SysTick
{
  data = i2c_read();
  usb_write_packet(data);
}

The problem with enumeration persisted. What actually gave me a reliable results was something as follows:

control_request_callback ()
{
  systick_ON();
}

main
{
  init_i2c_peripheral_only();
  init_usb();

  while
  {
    usb_poll();
  }
}

SysTick
{
  if (controller == UNITILIZED)
  {
    i2c_configure_controller();
    controller = INITILIZED;
  }
  else if (controller == INITILIZED)
  {
    i2c_read_controller_ID();
    controller = PRESENT;
  }
  else if (controller == PRESENT)
  {
    i2c_read();
  }

  usb_write_packet();
}

 That basically meant that first 2 SysTicks are used for setting up the controller. In the previous code snippets I've omitted the control_request_callback() function. That function handles SETUP requests sent by the host. Before the host sends this type of request only usb_poll() function runs - it's necessary to actually recognise that the SETUP packet has been received.

That honestly became contrived and I'm not a fan of this solution. Unfortunately not having any logic analyser I could only reason on the timings. I would like to revisit this loop when I can my hands back on some proper equipment.

Another thing I wanted to mention is some descriptors configuration. USB has a multitude of descriptors which are used for describing the device. The top level descriptor is called the Device Descriptor. What seems to be important is that by setting bDeviceClass, bDeviceSubClass and bDeviceProtocol fields to 0 the responsibility of holding the data the identifies the device lies on the Interface Descriptor. 

What's also important is the idVendor (VID) and idProduct (PID) numbers. At some point I've decided that my device will pretend it's a game pad. What it pretends to be isn't that important at this stage. My understanding was that for a HID device that Windows doesn't have drivers it will use generic HID driver. I've opened the Linux list of known VIDs and PIDs. I've chosen 0x0079 for the VID and 0x0011 for the PID. Why? Because DragonRise Inc. Gamepad sounds awesome. I've also hoped that Windows will be too cool to know what to do with something that was made by such a cringy company.

One thing that is specific for the HID device is an additional descriptor, so called HID Descriptor. Its role is to be a wrapper for the HID Report - a description of what the actual data that gets polled means. The syntax for writing report like that is confusing. After some reading I've created this monster:

static const uint8_t hid_report_descriptor[] = {
  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, 0x02, /*     USAGE_MAXIMUM (Button 2)         */
  0x15, 0x00, /*     LOGICAL_MINIMUM (0)              */
  0x25, 0x01, /*     LOGICAL_MAXIMUM (1)              */
  0x95, 0x02, /*     REPORT_COUNT (2)                 */
  0x75, 0x01, /*     REPORT_SIZE (1)                  */
  0x81, 0x02, /*     INPUT (Data,Var,Abs)             */
  0x95, 0x01, /*     REPORT_COUNT (1)                 */
  0x75, 0x06, /*     REPORT_SIZE (6)                  */
  0x81, 0x01, /*     INPUT (Cnst,Ary,Abs)             */
  0x05, 0x01, /*     USAGE_PAGE (Generic Desktop)     */
  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, 0x06, /*     INPUT (Data,Var,Rel)             */
  0xc0,       /*   END_COLLECTION                     */
  0xc0        /* END_COLLECTION                       */
};

This report describes two buttons - represented by two bits and a stick represented by X and Y axes. Both ranging from -127 to 127 in value (that might require a fix since it's more of a 0-255 scale). It might be worth pointing out that for the buttons the unused bits are also described.

I fully understand that this report is hard to read for someone who hasn't seen something like that before. I encourage you to read more about that. I don't feel competent enough to explain what's going on.

Sorry if this log has been messy. I hope it still provides some value.

Discussions