BlocNotes

Notepad of a tinker, maker, hacker or whatever you call it :)

STM32 - Custom USB HID device step by step

Step by step guide to do a custom USB HID device on STM32 using ST CubeMX. There is already one page addressing it but without any details for beginners.
I will use my custom board based on STM32L0, but any Nucleo can be used by wiring a USB cable to 5V, GND, USBD+, USBD-.

Step 1 - CubeMX

Open it, start a projet to select your processor or board.

  • In Pinout tab configuration->peripheral->usb and check Device (FS). It will tell CubeMX that we want to use USB pins.
  • You can check Activate OE if you want to see a led blink when there is traffic on USB lines. Then connect a resistor+led on the pin.
  • Now, to speed up development, we want to use ST's USB library, so in configuration->USB_Device select Human interface device class (HID). Be sure to avoid custom class, we want ST working example to test our setup. It will generate a mouse device.
  • Then in Clock configuration tab select USBCLK from RC 48 MHZ if your uC is compatible: ST implemented a nice feature allowing us to avoid accurate external quartz by trimming internal 48 MHz RC against USB clock send by the host. If your uC is not crystal-less you must find a way to have accurate 48 MHz on USBCLK (hint: use HSE)
  • Then in Configuration tab select Middlewares->USB_Devices. Here you can customize the Device descriptor tab with PID/VID, manufacturer...
  • Now, generate the project for your IDE.

Step 2 - Test the mouse example

First, before modifying anything, we will try ST's mouse example. The USB stack is handled by ST's library so we just need to call it to move the mouse.

  • Modify main.c to declare and initialise data to be send to the computer, in USER CODE BEGIN 1:
  // HID Mouse
  struct mouseHID_t {
      uint8_t buttons;
      int8_t x;
      int8_t y;
      int8_t wheel;
  };
  struct mouseHID_t mouseHID;
  mouseHID.buttons = 0;
  mouseHID.x = 10;
  mouseHID.y = 0;
  mouseHID.wheel = 0;
  • Then send them in USER CODE BEGIN 3:
    // Send HID report
    mouseHID.x = 10;
    USBD_HID_SendReport(&hUsbDeviceFS, &mouseHID, sizeof(struct mouseHID_t));
    HAL_Delay(1000);

Compile, then test: the mouse is now slowly drifting to the right :-)

Step 3 - Modifications for custom HID

Now we want to change mouse example to our custom descriptor. For this tutorial we will use keyboard+consumer device (media) descriptor explained in my previous post.
We can add it in Middlewares->...->Src->usbd_hid.c.

Find HID_MOUSE_ReportDesc array and comment or delete it, then add the custom descriptor.

__ALIGN_BEGIN static uint8_t HID_CUSTOM_ReportDesc[HID_CUSTOM_REPORT_DESC_SIZE]  __ALIGN_END = {  
  // 78 bytes
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x06,        // Usage (Keyboard)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x01,        //   Report ID (1)
  0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x08,        //   Report Count (8)
  0x19, 0xE0,        //   Usage Minimum (0xE0)
  0x29, 0xE7,        //   Usage Maximum (0xE7)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x01,        //   Logical Maximum (1)
  0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x95, 0x03,        //   Report Count (3)
  0x75, 0x08,        //   Report Size (8)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x64,        //   Logical Maximum (100)
  0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
  0x19, 0x00,        //   Usage Minimum (0x00)
  0x29, 0x65,        //   Usage Maximum (0x65)
  0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              // End Collection
  0x05, 0x0C,        // Usage Page (Consumer)
  0x09, 0x01,        // Usage (Consumer Control)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x02,        //   Report ID (2)
  0x05, 0x0C,        //   Usage Page (Consumer)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x01,        //   Logical Maximum (1)
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x08,        //   Report Count (8)
  0x09, 0xB5,        //   Usage (Scan Next Track)
  0x09, 0xB6,        //   Usage (Scan Previous Track)
  0x09, 0xB7,        //   Usage (Stop)
  0x09, 0xB8,        //   Usage (Eject)
  0x09, 0xCD,        //   Usage (Play/Pause)
  0x09, 0xE2,        //   Usage (Mute)
  0x09, 0xE9,        //   Usage (Volume Increment)
  0x09, 0xEA,        //   Usage (Volume Decrement)
  0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              // End Collection
};

Then in USBD_HID_CfgDesc change

  • bInterfaceSubClass to 0, the keyboard does not respect boot specifications
  • nInterfaceProtocol to 1 (keyboard)

All references to mouse in the file should be changed to custom.

In Middlewares->...->Src->usbd_hid.h, change:

  • HID_EPIN_SIZE to 5, the max report size
  • HID_CUSTOM_REPORT_DESC_SIZE to 78, the length of our new descriptor.

Finally in main.c:
Add reports initialization in USER CODE BEGIN 1

  // HID Keyboard
  struct keyboardHID_t {
      uint8_t id;
      uint8_t modifiers;
      uint8_t key1;
      uint8_t key2;
      uint8_t key3;
  };
  struct keyboardHID_t keyboardHID;
  keyboardHID.id = 1;
  keyboardHID.modifiers = 0;
  keyboardHID.key1 = 0;
  keyboardHID.key2 = 0;
  keyboardHID.key3 = 0;
  // HID Media
  struct mediaHID_t {
    uint8_t id;
    uint8_t keys;
  };
  struct mediaHID_t mediaHID;
  mediaHID.id = 2;
  mediaHID.keys = 0;

And send it in USER CODE BEGIN 3

    // Send HID report
    mediaHID.keys = USB_HID_VOL_DEC;
    USBD_HID_SendReport(&hUsbDeviceFS, &mediaHID, sizeof(struct mediaHID_t));
    HAL_Delay(30);
    mediaHID.keys = 0;
    USBD_HID_SendReport(&hUsbDeviceFS, &mediaHID, sizeof(struct mediaHID_t));
    HAL_Delay(30);

    keyboardHID.modifiers = USB_HID_MODIFIER_RIGHT_SHIFT;
    keyboardHID.key1 = USB_HID_KEY_L;
    USBD_HID_SendReport(&hUsbDeviceFS, &keyboardHID, sizeof(struct keyboardHID_t));
    HAL_Delay(30);
    keyboardHID.modifiers = 0;
    keyboardHID.key1 = 0;
    USBD_HID_SendReport(&hUsbDeviceFS, &keyboardHID, sizeof(struct keyboardHID_t));

Note: There is no need to use two structures if memory optimisation is required, but it simplify the example.

Compile, send to uC and...
The volume is decreasing while 'L' characters appears on your screen :-)

Full code can be found on Github, and commit diff from example is there.

Edit 21/02/17: Correction of consumer control descriptor size