octokeyz

Firmware

Table of contents

Single bare-metal C firmware for the STM32F042K6/K4 (ARM Cortex-M0). No HAL, no RTOS -- direct register access throughout. A single binary runs on both hardware variants: the firmware probes for an SSD1306 display at startup and silently disables display features if none is found. See the hardware pages for octokeyz and octokeyz-mega.

The firmware implements a USB HID device with a custom vendor protocol. Host-side interaction is handled by the Go client library or any custom HID implementation that speaks the protocol described below.

Building from source

Prerequisites

  • CMake 3.25 or later
  • Ninja (recommended) or Make
  • ARM GNU Toolchain (arm-none-eabi)
  • Git

The following dependencies are fetched automatically via CMake FetchContent:

Configure and build

cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja
cmake --build build

Output artifacts

The build produces the following in the build/firmware/ directory:

FileDescription
octokeyz.elfELF binary
octokeyz.elf.mapLinker map
octokeyz.binRaw binary
octokeyz.hexIntel HEX
octokeyz.dfuDFU with suffix (for dfu-util)

The firmware version is derived from git tags matching the v[0-9]* pattern. Between tagged releases, the version includes the commit count and abbreviated hash (e.g. 0.0.72-1b14).

Memory layout

The linker script (firmware/STM32F042KxTx_FLASH.ld) targets the smaller K4 memory to ensure compatibility with both STM32F042K4 and K6 variants:

RegionStart addressSize
Flash0x0800000016 KB
RAM0x200000006 KB

Flashing

Using USB DFU

Automated builds from the latest source are available from the rolling release.

Firmware flashing can be done over USB using the STM32's built-in DFU bootloader. There are several ways to enter DFU mode:

Empty microcontroller: An empty microcontroller boots directly into the DFU bootloader, so it is ready to flash.

Button combo: With an octokeyz firmware already running on the microcontroller, hold buttons 1 and 5 simultaneously while plugging in the USB cable. The microcontroller boots directly into the DFU bootloader.

Bootloader pin header: Connect a jumper to JP1 (Boot to DFU) pin header and plug the USB cable. The microcontroller boots directly into the DFU bootloader.

Once in DFU mode, follow the instructions from my generic Hardware Build Manual's "STM32 (USB DFU)" section.

With st-flash (from https://github.com/stlink-org/stlink) installed, run:

cmake --build build --target octokeyz-stlink-write

Linux udev rules

Linux users may have issues connecting to the macropad as a normal user.

The file share/udev/60-octokeyz.rules grants device access to users in the plugdev group and to logged-in users via the uaccess tag:

SUBSYSTEMS=="usb", DRIVERS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="6184", GROUP="plugdev", MODE="0660", TAG+="uaccess"

Install it:

sudo cp share/udev/60-octokeyz.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger

Architecture overview

Clock configuration

The firmware runs on HSI48 at 48 MHz (internal oscillator, no external crystal). Flash wait state is set to 1 cycle as required at this frequency. AHB and APB buses run undivided.

Peripheral map

PeripheralFunction
GPIOA PA0-PA7Button inputs (internal pull-up)
GPIOB PB0LED output (TIM3 CH3, alternate function)
GPIOA PA9 / PA10I2C1 SCL / SDA (display, open-drain with pull-up)
USB FSUSB device
I2C1SSD1306 display communication (address 0x3c)
DMA1 Channel 2I2C1 TX -- display data transfer
TIM3LED PWM generation
TIM16Display clear delay (one-pulse mode)
TIM17HID idle rate timer (one-pulse mode)
RTC BKP0RBootloader magic value handling

Main loop

The main loop is a polling loop that calls usbd_task() for USB processing and display_task() for display I2C DMA management. Button state is read in the USB IN endpoint callback by reading the GPIOA IDR register. A new input report is sent when the button state changes, or when the HID idle timer expires. Output reports (LED control, display data, display clear) are processed in the OUT endpoint callback.

Source files

FilePurpose
main.cClock init, GPIO setup, USB and display init, main loop (usbd_task() + display_task()), USB callback implementations
descriptors.cUSB device/config/HID descriptors, string descriptors, HID report descriptor
display.cSSD1306 driver: I2C + DMA init, double-buffered line rendering, font rasterization, delayed clear
led.cTIM3 PWM setup, 5-state LED control (on, flash, slow blink, fast blink, off)
idle.cHID idle rate tracking via TIM17, SET_IDLE/GET_IDLE request handling
bootloader.cDFU entry detection (RTC backup register check), system memory remap and jump, reset trigger

USB HID protocol

Device identity

FieldValue
USB version2.0 Full-Speed
Device classHID (interface-level)
VID0x1d50 (generously provided by OpenMoko)
PID0x6184
Manufacturerrgm.io
Productoctokeyz
Serial numberSTM32 unique device ID
Max power100 mA (bus-powered)
HID version1.11

Endpoints

The firmware only defines endpoint 1 in both directions:

DirectionTypeMax packet sizeInterval
INInterrupt64 bytes10 ms
OUTInterrupt64 bytes10 ms

HID reports

IDKindSize (bytes)Description
1Input1Button states -- bits 0-7 map to buttons 1-8, 1 = pressed
1Output1LED control -- bits 0-2 encode state: 1=on, 2=flash, 3=slow blink, 4=fast blink, 5=off
1Feature1Capabilities -- bit 0: device has a display
2Output23Display line data -- byte 0: line number (5 bits), byte 1: alignment (2 bits, 1=left, 2=right, 3=center), bytes 2-22: ASCII text (21 chars max)
2Feature3Display capabilities -- byte 0: number of lines, byte 1: characters per line, byte 2 bit 0: supports clear
3Output2Display clear with delay -- 16-bit little-endian milliseconds

Report sizes listed above do not include the report ID byte.

Vendor usage pages

Usage pageNameContains
0xFF00octokeyzApplication collection, capabilities feature
0xFF01octokeyz KeyButton states (keys 1-8)
0xFF02octokeyz LEDLED control (states 1-5)
0xFF03octokeyz DisplayDisplay capabilities, line data, clear command

Note

This is a custom vendor HID protocol, not a standard keyboard or consumer device. Interacting with it requires the Go client library or a custom implementation that understands the report structure and vendor usage pages described above.

HID idle rate

The default idle rate is 500 ms (value 125 in 4 ms units), configurable via standard HID SET_IDLE and GET_IDLE requests. When idle rate is non-zero, button state reports are sent periodically even if no state change has occurred. Setting idle rate to 0 disables periodic reports -- the device only sends a report when button state actually changes.