Hamaluik.com

Making A Custom Teensy3 HID Joystick

Well, as per probably 99% of blogs out there, I haven't updated this one in.. forever, which is unfortunate but surely not the end of the world. Anyway, I recently got married and for our wedding we decided we wanted to include some arcade games for a more unique, personal, and fun wedding experience. Me being the overly-ambitious type that I am decided it would be even more spectacular to create our own wedding arcade ("Wedcade" for short)! These actually turned out pretty decently in the end, and I'll try to write up a post or two about them (as well as the arcade cabinets!) later, but for now I want to talk about using the Teensy3 as a joystick (namely using Teensyduino).

Now, normally with the Teensy3, if you load up the joystick example, it will give you a generic joystick with 32 buttons, 6 axes, and 1 hat switch. This should be more than enough for anybody, and it was what I was planning on using. However, for whatever unknown reason, when I tried to use this joystick with Unity (the game development suite I decided to use for the wedcade), Unity refused to acknowledge it and said the joystick was unsupported. This was a bit frustrating as it is really just a generic HID joystick, no drivers needed. I had my suspicions however that there were just simply too many buttons and / or axes or whatever for Unity to deal with, so it shut down the joystick altogether. With that in mind, I set out to make the Teensy enumerate as a generic joystick with only the axes and buttons that I needed it for!

Everything I did here was for Teensyduino, so all files should be located in the Arduino/hardware/teensy/cores/teensy3 folder. Further, I am defining a new USB device / type called "arcade" which is two pairs of axes (one for each player) and a total of 16 buttons (though I'm not using all 16 in my current version of the arcade).

I started off by editing usb_desc.h. This file is just a bunch of definitions that help set up the USB defitions in all the other files. After the USB_FLIGHTSIM definition area, I added a definition set which looks like:

#elif defined(USB_ARCADE)
    #define VENDOR_ID   0x16C0
    #define PRODUCT_ID  0x0489
    #define DEVICE_CLASS    0x03
    #define MANUFACTURER_NAME {'B', 'l', 'a', 'z', 'i', 'n', 'g', 'M', 'a', 'm', 'm', 'o', 't', 'h'}
    #define MANUFACTURER_NAME_LEN 14
    #define PRODUCT_NAME    {'W', 'e', 'd', 'c', 'a', 'd', 'e', ' ', 'C', 'o', 'n', 't', 'r', 'o', 'l', 'l', 'e', 'r'}
    #define PRODUCT_NAME_LEN  18
    #define EP0_SIZE              64
    #define NUM_ENDPOINTS        2
    #define NUM_USB_BUFFERS    30
    #define NUM_INTERFACE        1
    #define ARCADE_INTERFACE      0 // Joystick
    #define ARCADE_ENDPOINT    1
    #define ARCADE_SIZE        16
    #define ARCADE_INTERVAL    1
    #define ARCADE_DESC_OFFSET  (9)
    #define CONFIG_DESC_SIZE  (9 + 9+9+7)
    #define ENDPOINT1_CONFIG  ENDPOINT_TRANSIMIT_ONLY

With usb_desc.h in place, I added the following to the usb_desc.c file (right below the #ifdef JOYSTICK_INTERFACE ... #endif section:

#ifdef ARCADE_INTERFACE
static uint8_t arcade_report_desc[] = {
    0x05, 0x01,                 // USAGE_PAGE (Generic Desktop)
    0x09, 0x04,                 // USAGE (Joystick)
    0xa1, 0x01,                 // COLLECTION (Application)
    0x09, 0x04,                 //   USAGE (Joystick)
    0xa1, 0x00,                 //   COLLECTION (Physical)
    0x09, 0x30,                 //   USAGE (X)
    0x09, 0x31,                 //   USAGE (Y)
    0x09, 0x33,                 //   USAGE (Rx)
    0x09, 0x34,                 //   USAGE (Ry)
    0x75, 0x08,                 //   REPORT_SIZE (8)
    0x95, 0x04,                 //   REPORT_COUNT (4)
    0x45, 0x7f,                 //   PHYSICAL_MAXIMUM (127)
    0x35, 0x81,                 //   PHYSICAL_MINIMUM (-127)
    0x81, 0x02,                 //   INPUT (Data,Var,Abs)
    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)
    0x75, 0x01,                 //   REPORT_SIZE (1)
    0x95, 0x10,                 //   REPORT_COUNT (16)
    0x81, 0x02,                 //   INPUT (Data,Var,Abs)
    0xc0,                       //   END_COLLECTION
    0xc0                        // END_COLLECTION
};
#endif

This is the actual USB descriptor and was created using the somewhat clunky USB Descriptor Tool. It defines that we have a joystick device with 4 physical axes and 16 physical buttons. Each of the 4 axes will report a single byte with a value between -127 and 127 and each button will report a value of 0 or 1. All 16 buttons will be combined into 2 bytes of data so they're all packed together (for a total packet size of 6 bytes). For more information on how to create something like this you can try http://www.frank-zhao.com/cache/hid_tutorial_1.php and/or the pdf at http://www.picbasic.co.uk/forum/showthread.php?t=11950. It took me a little bit to get this right, so I wish you the best of luck if you want to deviate substantially from this!

With the descriptor written, add the following to the usb_desc.c file (right below the line "#endif // JOYSTICK_INTERFACE"):

#ifdef ARCADE_INTERFACE
        // interface descriptor, USB spec 9.6.5, page 267-269, Table 9-12
        9,                                    // bLength
        4,                                    // bDescriptorType
        ARCADE_INTERFACE,                    // bInterfaceNumber
        0,                                    // bAlternateSetting
        1,                                    // bNumEndpoints
        0x03,                                  // bInterfaceClass (0x03 = HID)
        0x00,                                  // bInterfaceSubClass
        0x00,                                  // bInterfaceProtocol
        0,                                    // iInterface
        // HID interface descriptor, HID 1.11 spec, section 6.2.1
        9,                                    // bLength
        0x21,                                  // bDescriptorType
        0x11, 0x01,                          // bcdHID
        0,                                    // bCountryCode
        1,                                    // bNumDescriptors
        0x22,                                  // bDescriptorType
        LSB(sizeof(arcade_report_desc)),        // wDescriptorLength
        MSB(sizeof(arcade_report_desc)),
        // endpoint descriptor, USB spec 9.6.6, page 269-271, Table 9-13
        7,                                    // bLength
        5,                                    // bDescriptorType
        ARCADE_ENDPOINT | 0x80,            // bEndpointAddress
        0x03,                                  // bmAttributes (0x03=intr)
        ARCADE_SIZE, 0,                    // wMaxPacketSize
        ARCADE_INTERVAL,                      // bInterval
#endif // ARCADE_INTERFACE

This is another section that defines the usb interface we're using. It's pretty much copy-pasted from the JOYSTICK_INTERFACE section, with name changes to reference the arcade aspect of it.

Finally, near the bottom of the file, add (just below the #ifdef JOYSTICK_INTERACE ... #endif section):

#ifdef ARCADE_INTERFACE
        {0x2200, ARCADE_INTERFACE, arcade_report_desc, sizeof(arcade_report_desc)},
        {0x2100, ARCADE_INTERFACE, config_descriptor+ARCADE_DESC_OFFSET, 9},
#endif

This just actually includes the descriptor in the list and should be all we need to do to set up the USB descriptors. Now we just need to write a class to actually use our joystick with. I'll post my code here, but it is more or less a direct copy-paste and slight strip-down of the usb_joystick.h / usb_joystick.c files found in the directory:

// usb_arcade.h
#ifndef _USB_ARCADE_H_
#define _USB_ARCADE_H_

#if defined(USB_ARCADE)

#include <inttypes.h>

// C language implementation
#ifdef __cplusplus
extern "C" {
#endif
int usb_arcade_send(void);
// we have packets that are 6 bytes long
extern uint8_t usb_arcade_data[6];
#ifdef __cplusplus
}
#endif

// C++ interface
#ifdef __cplusplus
class usb_arcade_class
{
private:
    static uint8_t auto_send;

public:
    void button(uint8_t button, bool val) {
        if (--button >= 16) return;
        if (val) usb_arcade_data[button >= 8 ? 5 : 4] |= (1 << (button >= 8 ? (button - 8) : button));
        else usb_arcade_data[button >= 8 ? 5 : 4] &= ~(1 << (button >= 8 ? (button - 8) : button));
        if(auto_send) usb_arcade_send();
    }

    void axis(uint8_t axis, int8_t val) {
        if(axis >= 4) return;
        if(val > 0) usb_arcade_data[axis] = 127;
        else if(val < 0) usb_arcade_data[axis] = -127;
        else usb_arcade_data[axis] = 0;
        if(auto_send) usb_arcade_send();
    }

    void setAutoSend(bool send) {
        auto_send = send;
    }

    void send() {
        usb_arcade_send();
    }
};
extern usb_arcade_class Arcade;

#endif // __cplusplus
#endif // USB_ARCADE
#endif // _USB_ARCADE_H_
// usb_arcade.c
#include "usb_dev.h"
#include "usb_arcade.h"
#include "core_pins.h" // for yield()
#include "HardwareSerial.h"
#include <string.h> // for memcpy()

#ifdef USB_ARCADE // defined by usb_dev.h -> usb_desc.h

uint8_t usb_arcade_data[6];

// Maximum number of transmit packets to queue so we don't starve other endpoints for memory
#define TX_PACKET_LIMIT 3

static uint8_t transmit_previous_timeout=0;

// When the PC isn't listening, how long do we wait before discarding data?
#define TX_TIMEOUT_MSEC 30

#if F_CPU == 96000000
  #define TX_TIMEOUT (TX_TIMEOUT_MSEC * 596)
#elif F_CPU == 48000000
  #define TX_TIMEOUT (TX_TIMEOUT_MSEC * 428)
#elif F_CPU == 24000000
  #define TX_TIMEOUT (TX_TIMEOUT_MSEC * 262)
#endif

int usb_arcade_send(void)
{
    uint32_t wait_count=0;
    usb_packet_t *tx_packet;

    while (1) {
        if (!usb_configuration) {
            return -1;
        }
        if (usb_tx_packet_count(ARCADE_ENDPOINT) < TX_PACKET_LIMIT) {           tx_packet = usb_malloc();           if (tx_packet) break;       }       if (++wait_count > TX_TIMEOUT || transmit_previous_timeout) {
                                                                        transmit_previous_timeout = 1;
                                                                        return -1;
        }
        yield();
    }

    transmit_previous_timeout = 0;
    memcpy(tx_packet->buf, usb_arcade_data, 6);
    tx_packet->len = 6;
    usb_tx(ARCADE_ENDPOINT, tx_packet);

    return 0;
}

#endif // USB_ARCADE

All this code does is provide an interface for defining usb packets to be sent to the computer and gives us the functions Arcade.button(button, val) and Arcade.axis(axis, val) to use instead of manually writing the packets and sending them ourselves. In order to make these classes accessible in the Arduino environment, add the following to the usb_inst.cpp file:

#ifdef USB_ARCADE
usb_arcade_class Arcade;
uint8_t usb_arcade_class::auto_send = 0;
#endif

And the following to WProgram.h:

#include "usb_arcade.h"

Finally, in order to actually make the Arduino environment use the new arcade descriptors, add the following lines to the file Arduino/hardware/teensy/boards.txt (after the teensy3.menu.usb.flightsim entries):

teensy3.menu.usb.arcade.name=Wedcade Controller
teensy3.menu.usb.arcade.build.define0=-DUSB_ARCADE
teensy3.menu.usb.arcade.fake_serial=teensy_gateway

And that's pretty much it! I included the sketch I used so you can see how it works, but basically I have my buttons connected to a bunch of pins. I then poll these at 50 Hz and send an update packet to the computer at each iteration. I ended up not using the axes at all because although they worked in Windows, they did not work properly in Unity. Instead I just mapped each of the up / right / down / left directions to a button on the controller and used the buttons for directional input instead of the axes.

const int ledPin = 13;

// joystick colour mappings
// g  l
// y  r
// o  d
// r  u

// player 1 bank
const int P1_A = 2;
const int P1_B = 3;
const int P1_LEFT = 4;
const int P1_RIGHT = 5;
const int P1_DOWN = 6;
const int P1_UP = 7;

const int START_BUTTON = 8;

// player 2 bank
const int P2_UP = 15;
const int P2_DOWN = 14;
const int P2_RIGHT = 17;
const int P2_LEFT = 16;
const int P2_B = 18;
const int P2_A = 19;

boolean ledOn = false;
unsigned long lastTime = 0;

void setup()
{
    pinMode(ledPin, OUTPUT);

    pinMode(P1_A, INPUT_PULLUP);
    pinMode(P1_B, INPUT_PULLUP);
    pinMode(P1_LEFT, INPUT_PULLUP);
    pinMode(P1_RIGHT, INPUT_PULLUP);
    pinMode(P1_DOWN, INPUT_PULLUP);
    pinMode(P1_UP, INPUT_PULLUP);
    pinMode(START_BUTTON, INPUT_PULLUP);
    pinMode(P2_A, INPUT_PULLUP);
    pinMode(P2_B, INPUT_PULLUP);
    pinMode(P2_LEFT, INPUT_PULLUP);
    pinMode(P2_RIGHT, INPUT_PULLUP);
    pinMode(P2_DOWN, INPUT_PULLUP);
    pinMode(P2_UP, INPUT_PULLUP);

    // read the entire joystick at once instead of per event
    Arcade.setAutoSend(false);

    lastTime = millis();
}

void loop()
{
    unsigned long time = millis();
    // run at 50 Hz
    if(time - lastTime >= 20)
    {
        lastTime = time;

        // read the data of all our buttons
        // our buttons
        Arcade.button(1, 1 - digitalRead(P1_A));
        Arcade.button(2, 1 - digitalRead(P1_B));
        Arcade.button(3, 1 - digitalRead(P2_A));
        Arcade.button(4, 1 - digitalRead(P2_B));

        Arcade.button(5, 1 - digitalRead(START_BUTTON));

        // now our axes
        Arcade.axis(0, digitalRead(P1_LEFT) == 0 ? -1 : (digitalRead(P1_RIGHT) == 0 ? 1 : 0));
        Arcade.axis(1, digitalRead(P1_UP) == 0 ? -1 : (digitalRead(P1_DOWN) == 0 ? 1 : 0));
        Arcade.axis(2, digitalRead(P2_LEFT) == 0 ? -1 : (digitalRead(P2_RIGHT) == 0 ? 1 : 0));
        Arcade.axis(3, digitalRead(P2_UP) == 0 ? -1 : (digitalRead(P2_DOWN) == 0 ? 1 : 0));

        // also use buttons for the axes cuz unity is a derp
        Arcade.button(6, 1 - digitalRead(P1_UP));
        Arcade.button(7, 1 - digitalRead(P1_RIGHT));
        Arcade.button(8, 1 - digitalRead(P1_DOWN));
        Arcade.button(9, 1 - digitalRead(P1_LEFT));
        Arcade.button(10, 1 - digitalRead(P2_UP));
        Arcade.button(11, 1 - digitalRead(P2_RIGHT));
        Arcade.button(12, 1 - digitalRead(P2_DOWN));
        Arcade.button(13, 1 - digitalRead(P2_LEFT));

        // send a packet now
        Arcade.send();

        // toggle our led
        ledOn = !ledOn;
        digitalWrite(ledPin, ledOn);
    }
}

I apologize for sloppy or incoherent code, but I was mostly figuring things out from scratch with a tight time budget, and my only goal was to get this to just work, which they thankfully did in the end!

That's all for now, please let me know if you run into any issues or if what I wrote here didn't make sense! I can't promise I can fix it but I will certainly promise to try!