BERGSONNE

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 screen open 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.

VID / PID
The device enumerates as VID 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))
Why packed
Without __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 padding

config.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)