The SK6812 line of LEDs requires a specific digital waveform at its logic input. This signal is how we communicate the color and brightness information for each LED. We can use one of our MCU's timers in PWM mode to generate this waveform, but first we need to understand the specific timing the LEDs require.
Before getting into the SK6812 timing, it is helpful to understand a few of the STM32 peripherals and registers we will be using: the timer, ARR, CCR, and DMA.
At its simplest, a hardware timer is just a counter. The MCU provides the timer with a clock, and the timer increments its internal counter every clock cycle. For example, if our timer is running at 16 MHz, the counter increments 16 million times per second. More in depth, the Auto-Reload Register, or ARR, tells the timer when to reset this counter and start again. If ARR is set to 18, for example, the timer counts:
0, 1, 2, 3 ... 17, 18
After reaching 18, it rolls back to 0 and repeats.
Because we started at 0, this gives us 19 total timer counts. By controlling ARR, we are therefore controlling the period, or frequency, of our PWM signal.
The Capture/Compare Register, or CCR, controls where within that period the PWM output changes state. In the PWM mode we are using, we can think of CCR as determining how long the output stays HIGH. For example, imagine that our timer counts from 0 to 18. If CCR is set relatively low, the output will only remain HIGH for a small portion of that period. If CCR is set higher, it will remain HIGH for a larger portion of the period.
Therefore, ARR and CCR control two different things:
ARR = How long is the entire PWM period?
CCR = How long within that period does the signal remain HIGH?
This gives us control over both the frequency and duty cycle of our PWM waveform.
The last piece is DMA, or Direct Memory Access. Normally, if we wanted to continuously change CCR, our CPU would have to repeatedly write a new value into the CCR register. For the SK6812, this would have to happen for every single bit we transmit. DMA gives peripherals such as our timer the ability to transfer data to or from memory without requiring the CPU to manually perform every transfer. In our case, we can create an array in memory containing all of the CCR values we want to send:
6, 12, 12, 6, ...
DMA can then automatically transfer each value from this array into the timer's CCR register at the appropriate time. This is particularly useful because the timer handles the precise PWM timing, DMA handles feeding it the next value, and the CPU is free to do something else.
With that background, we can now determine what ARR and CCR values we actually need for the SK6812.
According to the datasheet, the symbol period is approximately 1.2 µs. This is the amount of time we have to transmit a single bit, regardless of whether that bit is a 0 or a 1.
We can convert this period into a frequency using:
f = 1 / T and f = 1 / 1.2 µs ≈ 833 kHz
Therefore, we want our timer to generate a new PWM period at approximately 833 kHz. Our timer clock is running at 16 MHz. This means the timer counter increments once every 62.5 ns:
1 / 16 MHz = 62.5 ns
We can then determine how many timer counts fit inside one 1.2 µs SK6812 symbol:
16 MHz / 833 kHz ≈ 19 counts
Since the timer counts from 0 through ARR, the total number of counts is ARR + 1. Therefore, we can set our ARR to approximately:
ARR = 19 - 1 = 18
This means the timer will count from 0 to 18 before starting the next PWM period. That entire period represents one bit being sent to the SK6812.
Next, we need to determine our CCR value. While ARR determines the total length of the PWM period, CCR determines how long the signal stays HIGH during that period.
In a normal PWM application, we could use this to set an arbitrary duty cycle. For example, setting CCR to approximately half of the timer period would produce a roughly 50% duty cycle.
However, we are not actually using PWM to directly control the brightness of the LED. Instead, we are taking advantage of the PWM hardware to generate the specific HIGH and LOW timings required by the SK6812 communication protocol.
The SK6812 differentiates a 0 from a 1 based on how long the signal remains HIGH during each symbol. A 0 has a relatively short HIGH time, while a 1 has a longer HIGH time. The total symbol period remains approximately the same.
For our timer configuration, we can represent these two values using approximately:
Bit 0: CCR = 6
Bit 1: CCR = 12
This is where DMA becomes particularly useful. Instead of having the CPU manually update CCR every 1.2 µs, we can create a buffer containing all of our 6s and 12s. DMA then feeds these values into the timer's CCR register one after another.
For example:
LED Data: 0 1 1 0
CCR Buffer: 6 12 12 6
The process might look something like this:
Memory → DMA → CCR → Timer → SK6812