Skip to content
3 min read

Devlog 0x02: The Code

Devlog 0x02: The Code
Photo by Vishnu Mohanan / Unsplash

Honestly, I was debating whether to write this devlog or not. The logic of the code is very simple and nothing super complex and impressive. Basically, take STM32's USB HID library - edit USB HID Descriptor, scan all key states, and done.

Modifying STM32 USB HID Library

Making a USB library from scratch is a whole pain in the butt-load. I tried, and honestly hours of researching made me end up nowhere. Instead of that, I will use the tools that is already in front of me.

The USB HID Library is very simple to alter since everything is implemented for you. All we need to do is to change the report descriptor.

__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE]  __ALIGN_END =
{
	    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
	    0x09, 0x06,                    // USAGE (Keyboard)
	    0xa1, 0x01,                    // COLLECTION (Application)
	    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
	    0x19, 0xe0,                    //   USAGE_MINIMUM (Keyboard LeftControl)
	    0x29, 0xe7,                    //   USAGE_MAXIMUM (Keyboard Right GUI)
	    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
	    0x75, 0x01,                    //   REPORT_SIZE (1)
	    0x95, 0x08,                    //   REPORT_COUNT (8)
	    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	    0x95, 0x01,                    //   REPORT_COUNT (1)
	    0x75, 0x08,                    //   REPORT_SIZE (8)
	    0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)
	    0x95, 0x05,                    //   REPORT_COUNT (5)
	    0x75, 0x01,                    //   REPORT_SIZE (1)
	    0x05, 0x08,                    //   USAGE_PAGE (LEDs)
	    0x19, 0x01,                    //   USAGE_MINIMUM (Num Lock)
	    0x29, 0x05,                    //   USAGE_MAXIMUM (Kana)
	    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
	    0x95, 0x01,                    //   REPORT_COUNT (1)
	    0x75, 0x03,                    //   REPORT_SIZE (3)
	    0x91, 0x03,                    //   OUTPUT (Cnst,Var,Abs)
	    0x95, 0x08,                    //   REPORT_COUNT (8)
	    0x75, 0x08,                    //   REPORT_SIZE (8)
	    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	    0x25, 0x65,                    //   LOGICAL_MAXIMUM (101)
	    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
	    0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
	    0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
	    0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
	    0xc0                           // END_COLLECTION
};

Yes, I know that it says HID_MOUSE_ReportDesc . I did not alter the variable name since by default the library is for a mouse HID device. This descriptor should make the USB device identify itself as a USB Keyboard HID class, with modifier keys with 8KRO

Making What Essentially Is Keyboard

To send a USB HID report, we first need to define a struct that matches the HID report descriptor expected by the host. This struct represents one keyboard report packet that will be sent over USB.

typedef struct
{
	uint8_t MODIFIER;
	uint8_t RESERVED;
	uint8_t KEYS[8];
} USBReport;

This layout is not arbitrary. It directly follows the USB HID Boot Keyboard specification, which most operating systems understand without any custom drivers.

To store the mapping for quick indexed access, we store the HID keywords per each key in an array

uint8_t key_mapping[] = {0x1A, 0x08, 0x07, 0x06, 0x1B, 0x1D, 0x04, 0x14};

Now for the main loop:

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  uint8_t count = 0;
	  memset(&report, 0, sizeof(report));

	  for (int i = 0; i < 8; i++) {
		  if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0 << i)) {
		      report.KEYS[i] = key_mapping[i];
		  }
	  }

	  USBD_HID_SendReport(&hUsbDeviceFS, &report, sizeof(report));
	  HAL_Delay(1);

  }

Each iteration:

  1. Clears the previous report to avoid “stuck” keys.
  2. Reads the state of all eight GPIO pins.
  3. If a pin is high, the corresponding HID keycode is placed into the report.
  4. Sends the report to the host over USB
  5. Profit

Improvements?

That’s pretty much it. There are no fancy I²C peripherals, no CAN buses roaring in the background, and no SPI-cy communication going on here—sorry. The code is intentionally simple, focusing only on what’s needed to make a USB HID keyboard work.

That said, there is definitely room for improvement. At the time of writing, some input latency is noticeable. This is most likely caused by the HAL_Delay(1) in the main loop, which limits how often input states are sampled and reports are sent. While a 1 ms delay may seem small, it can still introduce perceptible lag in a system that relies on frequent polling.