TLDR: Built a USB HID device on STM32 BluePill to wake headless servers after power loss. What started as an Arduino project evolved into a cleaner libopencm3 implementation, teaching us when direct hardware access beats abstraction.
The Problem: Server Wake After Power Loss
Power outages are a headache for server rooms. While servers can automatically power on, many need a USB keyboard event to fully wake up. Commercial solutions exist but are expensive for such a simple task – sending a single keystroke after power returns.
Hardware Considerations
The development process uncovered a hardware detail: BluePill boards often ship with a 1.5kΩ USB pullup resistor instead of the standard 10kΩ. This incorrect value specifically affects the USB reset behavior during enumeration, a critical part of device initialization.
The 10-second startup delay serves two essential purposes: allowing the power line to stabilize after restoration and preventing repeated wake attempts during power fluctuations.
1. Native Arduino Implementation
We started with the standard Arduino core’s USB stack, built on top of STM32’s HAL. While straightforward to implement, it only supports preconfigured keyboard devices without any customization options:
[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = arduino
build_flags =
-D PIO_FRAMEWORK_ARDUINO_ENABLE_HID
-D USBCON
-D USB_MANUFACTURER="Unknown"
2. Maple Bootloader with USBComposite
Our second approach used the USBComposite library with Maple bootloader. This introduced multiple complications. The Maple bootloader already implements a USB DFU (Device Firmware Upgrade) device, creating potential USB enumeration conflicts. The incorrect pullup resistor value (1.5kΩ vs 10kΩ) on most BluePills makes USB enumeration timing unreliable, especially during power fluctuations. Additionally, the Maple bootloader occupies the first 8KB of flash memory (0x8000000-0x8002000) and places its vector table at 0x8002000, while our application expects it at 0x8000000. This led to unexpected interrupt handling – visible during GDB debugging as unknown signal handlers being called after USB initialization. While we could relocate our vector table using the NVIC_VTOR register as shown below, the combined reliability issues made this solution too complex for our power-loss recovery use case.
[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = blackmagic
board_build.core = maple
board_build.variant = F103CB
build_flags =
-D VECT_TAB_ADDR=0x8000000 # Point NVIC_VTOR to new vector table location
-Wl,--section-start=.text=0x8000000
The vector table maps hardware interrupts to their handler functions. On STM32F1, it starts at the flash base address 0x8000000 by default. When changing this location via NVIC_VTOR, all interrupts including the crucial USB handlers must be properly realigned – a process the Maple bootloader complicates by occupying the standard location.
3. The libopencm3 Solution
After encountering these limitations, we turned to libopencm3 for direct hardware access. This framework provides precise control without bootloader assumptions or USB stack abstractions. The implementation became remarkably cleaner:
[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = libopencm3
upload_protocol = blackmagic
Implementation Details
System Configuration
The implementation runs at 48MHz, required for USB operation. We use the SysTick timer configured at 1kHz to handle timing for both the LED feedback and the startup delay:
systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8);
systick_set_reload(5999); // 1kHz @ 48MHz/8
systick_interrupt_enable();
systick_counter_enable();
Visual Feedback
An LED provides crucial status information: blinking at 1Hz during the 10-second countdown, then remaining steady to indicate the keystroke has been sent. This simple feedback mechanism helps verify device operation and power stability:
if (ticks % 1000 < 500) {
gpio_clear(GPIOC, LED_PIN);
} else {
gpio_set(GPIOC, LED_PIN);
}
USB Implementation
Our HID implementation uses a standard keyboard report descriptor, chosen to be widely compatible. The space key (0x2C) was selected as our wake event because it's universally recognized and typically doesn't trigger unwanted actions in most system states.
Conclusion
While Arduino excels at rapid prototyping, its abstractions can sometimes complicate rather than simplify. The direct hardware access provided by libopencm3 led to a cleaner implementation, free from bootloader assumptions and interrupt complications. Unfortunately, I haven't been able to test it yet together with the server.
Full implementation available on GitHub