-
The USB dance
02/12/2019 at 18:55 • 0 commentsIn 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:
- usb_poll() checks if anything has been received and if so it decides if it should handle it as a SETUP (host configures the device) or a OUT situation (device gets asked for the controller readings).
- usb_write_packet() pushes data to the buffer that corresponds to the specific endpoints OUT buffer. This micro controller supports up to 16 endpoints and each of those has an IN and OUT buffer. That means that you can't overwrite data in IN buffer by pushing to the OUT buffer with usb_write_packet() - seemed important when I tried to debug what's not working.
- init_i2c() in this situation is setting the I2C peripheral, configures the Wii Nunchuk and reads it's ID to confirm everything is fine.
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.
-
First steps with STM32 USB firmware
02/10/2019 at 15:59 • 0 commentsI started by setting up the project structure in a similar way satoshinm did. As I've mentioned before, this article by him - making a Rubber Ducky with Blue Pill - helped me a bit.
The project references the libopencm3 repository and provides 2 Makefiles. Top level Makefile fetches the libopencm3, builds it and builds the firmware. Makefile in the src directory only builds the firmware binary.
Before I could actually flash the boards I decided to check the resistors on the USB lines. The Blue Pill boards suffer from a wrong resistor values. The boards I received used 4.7k resistors, while they should be 1.5k. I desoldered them and soldered in ones with the proper values (1.5k).
I started by copying the code from this example. I stripped it from the DFU functionality.
I also changed the clock source to external 8MHz. In libopencm3 that meant going from:
rcc_clock_setup_in_hsi_out_48mhz()
to
rcc_clock_setup_in_hse_8mhz_out_72mhz()
What puzzled me a bit in libopencm3 is that you can stumble on functions that seem to do the same yet are named differently. An example of that would be:
gpio_mode_setup()
and
gpio_set_mode()
If you look into this file though you can find the answer:
/* * Note: The F2 and F4 series have a completely new GPIO peripheral with * different configuration options. Here we implement a different API partly to * more closely match the peripheral capabilities and also to deliberately * break compatibility with old F1 code so there is no confusion with similar * sounding functions that have very different functionality. */
...oh, ok then. That meant that for a STM32F1 the gpio_set_mode() was the way to go.
After that I worked on setting SysTick in such a way that it fired 100 times per second.
systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8); // SysTick interrupt every N clock pulses: set reload to N-1 // 72MHz / 8 = 9MHz // 9000000Hz / 90000 = 100Hz systick_set_reload(89999); systick_interrupt_enable();
After plugging the board to the PC it did get recognised. I didn't went too deep into what happens when I plug the board in. I went on to add the I2C functionality so I could poll the Wii Nunchuk controller. Little did I know...
But that's a story for the next episode! Stay tuned!
-
Gathering information
02/06/2019 at 17:35 • 0 commentsI've started by ordering 5 Blue pill boards... they are super cheap but it takes awhile for them to get here (I ordered them through AliExpress). In the meantime I started research.
There are 3 topics that I had to go through:
- USB specification and how it actually works
- OSes USB devices handling
- USB device firmware
I have started by going through the USB specification... It really is a confusing mess. I can't say I would design a universal standard better. It's not an easy task. I can't say USB is well designed though. I've played around with connecting Playstation 2 game pad and sniffing the USB packets with WireShark. I've also went through libopencm3's USB examples. Combining those different sources of information gave me some idea about USB, but it also made me realize there is way more than what HID USB device can teach you about USB.
When it comes to the firmware I've already decided I want to use libopencm3 library and that the Windows has to be first OS that handles this device. Depending on how it goes I might tackle Linux in the future. Except the libopencm3's examples, the Pill duck project gave me a bit of insight of how firmware like that could look like.
I admire the Handmade Network started by Casey Muratori and his Handmade Hero project. I share the attitude of avoiding third party libraries. I do belive it's possible to do a lot of low level things on your own. Because of that I wanted to write a legit USB device driver. That means I wanted to use direct OS calls.... but...
I researched the topic of writing a USB device driver for Windows and once again... it's a mess. There are multiple ways of doing it and I'm still not sure where are the boundaries between those. After some experimentation i've decided to go with the hidapi library. There are multiple reasons why I ended up using that one, and I will explain it better in next posts.