A WS2812B LED driver for the STM32F102 (48 MHz). Drives an arbitrary-length strip over a single wire using hardware PWM + DMA — no blocking loops, no CPU intervention during transmission.
Protocol
The WS2812B protocol encodes each bit as a fixed-width 1.25 µs pulse with a variable duty cycle:
| Bit | High | Low |
|---|---|---|
1 |
0.6 µs | 0.65 µs |
0 |
0.3 µs | 0.95 µs |
| Reset | — | > 50 µs |
Timer config
f = 1/T = 1/(1.25 * 10^-6) = 800000 = 800 kHz PWM Frequency
f(pwm) = F(clk)/(ARR+1)(PSC+1)
f = 800 kHz → (ARR+1)(PSC+1) = 60
PSC = 0, ARR = 59
CCR values are precomputed as:
#define WS2812B_HI_VAL 30 // 0.6/1.25 * 60 ≈ 29
#define WS2812B_LO_VAL 15 // 0.3/1.25 * 60 ≈ 14The reset pulse is appended directly to the DMA buffer (64 zero-valued slots ≈ 80 µs), so no manual delay is needed after a frame.
#define WS2812B_RST_CYCLES 64 // 80 µs reset
#define BITS_PER_LED 24 // GRB, MSB first
#define DMA_BUF_LEN ((NUM_LEDS * BITS_PER_LED) + WS2812B_RST_CYCLES)Each entry in WS2812B_DMA_BUF[] is a uint32_t CCR value (HI_VAL or LO_VAL). The DMA is configured Memory → Peripheral, Word (32-bit) to match the width of TIMx_CCRx.
Colors are stored in a union that mirrors the GRB wire order (MSB first):
typedef union {
struct { uint8_t b; uint8_t r; uint8_t g; } color;
uint32_t data;
} WS2812B_LED_ATTR;The struct ordering (B, R, G in memory) places G in the most-significant byte of data, so iterating bits 23 → 0 naturally produces the G7…G0, R7…R0, B7…B0 sequence required by the protocol.
// Set a single LED's color (buffered)
void WS2812B_SetColor(uint16_t index, uint8_t r, uint8_t g, uint8_t b);
// Flush buffer to LEDs via DMA (non-blocking, returns HAL_BUSY if previous transfer is in flight)
HAL_StatusTypeDef WS2812B_Update(void);
// Call from HAL_TIM_PWM_PulseFinishedCallback
void WS2812B_Callback(void);WS2812B_Update() starts a DMA transfer and uses a volatile flag to prevent overlapping transmissions, which is cleared in the DMA completion callback.
The 24-bit data can be observed in the above image.
To further validate bit-level correctness, a test pattern was transmitted by setting the blue channel to 10 (binary: 00001010) while keeping other channels constant.
The included main loop drives a sine-wave brightness envelope over a rainbow color wheel, creating a traveling wave across the strip:
// phase_step controls spatial spread; angle controls wave position
float local_angle = angle + (i * phase_step);
uint8_t brightness = (uint8_t)((sinf(local_angle) + 1.0f) * 10.0f);
Wheel(hue + i, &r, &g, &b); // rainbow color per LEDHAL_Delay(10) sets frame rate; angle += 0.05f controls wave speed; hue++ drifts the rainbow.
---
- STM32CubeMX — clock peripheral config
- arm-none-eabi-gcc + make — build
- OpenOCD — flash and debug
make -j16 DEBUG=1 -f STM32Make.make
openocd -f openocd.cfg -c "program build/debug/ws2812.elf verify reset exit"Timing is tuned for a 48 MHz HCLK. Changing the system clock requires recalculating ARR and CCR values.