USB HID Reports
A composite USB device that exposes two functions at once — a CDC serial console for debug output and a HID interface for vendor-defined reports — over a single cable, read on the host with hidapi.
Overview
The firmware targets a Core.ST.H5.1 at the "low" clock setting. Both interfaces run simultaneously over one USB connection:
- CDC — a once-per-second heartbeat printing the running HID report count. Open it with
screen. - HID — a 6-byte packed struct sent every 100 ms, read with
hidapi. - Simultaneous — separate endpoints, no conflict; you can keep
screenopen alongside the reader.
Firmware
One file does it all. core_usb_init() stands up the composite CDC+HID device in a single call; pad 9 toggles once per second as a heartbeat LED.
/**
* USB Composite (CDC + HID) — demo
*
* CDC: prints heartbeat with HID report count every second
* HID: sends a vendor-defined report every 100ms
*
* On the host:
* CDC: screen /dev/tty.usbmodem* 115200
* HID: python3 hid_read.py
*
* Both interfaces work simultaneously.
*/
#include "core.h"
#include "core_usb.h"
#include "core_usb_hid.h"
typedef struct __attribute__((packed)) {
uint32_t counter;
uint16_t sensor;
} hid_report_t;
int main(void)
{
core_init();
core_usb_init();
core_pad_output(9);
hid_report_t report = { 0, 0 };
uint32_t last_cdc = 0;
uint16_t sample = 0;
while (1) {
uint32_t now = _systick_ticks;
/* CDC heartbeat every second */
if (now - last_cdc >= 1000) {
last_cdc = now;
core_pad_toggle(9);
if (core_usb_connected()) {
core_usb_printf("HID reports sent: %lu\r\n", report.counter);
}
}
/* HID report every 100ms.
* Reports are zero-padded to 64 bytes by the driver. */
report.counter++;
report.sensor = sample++; /* Simulated ramp — replace with real sensor */
core_usb_hid_send((const uint8_t *)&report, sizeof(report));
core_delay_ms(100);
}
}- The main loop reads the systick counter directly via
_systick_ticks. Once per second it toggles pad 9 and — only if a CDC host has asserted DTR (core_usb_connected()) — prints the count. - On every iteration (~100 ms) it increments the counter, advances the ramp, and sends the report, so the heartbeat’s “reports sent” climbs by about ten each second.
- HID reports send unconditionally — the host’s HID driver polls the interrupt endpoint whether or not anyone has the serial port open.
core_usb_hid_send()takes the 6-byte buffer; the driver zero-pads it to the 64-byte HID report.
Composite device
The Core appears to the host as one device bundling two functions: a CDC ACM serial port and a HID interface. Because it combines device classes, it advertises itself as a composite device and uses an Interface Association Descriptor to group the CDC interfaces into one logical serial port; the host tells CDC from HID by their class descriptors.
0x1209 (pid.codes) / PID 0x0001 — the host HID code matches on this pair, which is exactly what hid_read.py opens.Report struct
The HID report is a packed struct so the binary layout matches exactly between firmware and host — uint32_t counter then uint16_t sensor, 6 bytes, zero-padded to 64 on send.
typedef struct __attribute__((packed)) {
uint32_t counter;
uint16_t sensor;
} hid_report_t;The host unpacks the first six bytes with a matching format:
counter, sensor = struct.unpack_from("<IH", bytes(data))__attribute__((packed)) the compiler may insert two bytes of padding between counter and sensor, making the struct 8 bytes and breaking the "<IH" unpack.Python reader
hid_read.py opens the device by VID/PID, reads 64-byte reports in blocking mode, and decodes the first six bytes as counter + sensor. It prints the device’s manufacturer/product strings on connect and exits with a clear message if hidapi is missing or the device can’t be opened.
#!/usr/bin/env python3
import sys, struct
try:
import hid
except ImportError:
print("Error: hidapi not installed. Run: pip3 install hidapi")
sys.exit(1)
VID = 0x1209
PID = 0x0001
device = hid.device()
device.open(VID, PID)
device.set_nonblocking(False)
print(f"Connected: {device.get_manufacturer_string()} {device.get_product_string()}")
while True:
data = device.read(64, timeout_ms=1000)
if not data:
continue
if len(data) >= 6:
counter, sensor = struct.unpack_from("<IH", bytes(data))
print(f"{counter:>10d} {sensor:>8d}")Report layout
[0:3] uint32_t counter (little-endian)
[4:5] uint16_t sensor value
[6:63] zero paddingconfig.json
A minimal descriptor — a Core.ST.H5.1 at the "low" clock, no pads mapped. The composite CDC+HID device is stood up by core_usb_init(), and pad 9 (the heartbeat LED) is configured at runtime.
{
"core": "Core.ST.H5.1",
"clock": "low",
"pads": {}
}Build & run
Build and flash, then run the two host tools in separate terminals:
cd examples/h1-usb-hid
make
make flash # BOOTLOADER := 0 in this example, so plain flash
screen /dev/tty.usbmodem* 115200 # CDC debug heartbeat
python3 hid_read.py # HID reports (one-time: pip3 install hidapi)
