2025-07-18 2:04 AM
Hi there,
I would like to control WS2812B LEDs with a PWM timer and DMA.
There are various tutorials on this, but they have not helped me and I am now asking you. Because it requires slightly different configurations depending on the model (F3, F4, G4, G0), I'm a bit overwhelmed.
I start a DMA transfer, wait for the last pulse with an interrupt and stop the DMA transfer. So far so good in theory. I still have problems with the implementation.
Even the simplest DMA example without WS2812B protocol does not work for me.
TIM1 Mode + Configuration
DMA Settings
Source:
TIM_HandleTypeDef htim1;
DMA_HandleTypeDef hdma_tim1_ch4; /* never used?! */
uint32_t data[] = {
30,
80,
20,
60,
10,
50
};
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim1, TIM_CHANNEL_4);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM1_Init();
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_4, (uint32_t *) data, 6);
while (1) {
/* */
}
}
The result is here:
After 4 pulses, the 5th pulse starts but didn't finish
I tried also with Data Width
Half-Word
Byte
Without stopping DMA, it's like that:
The last puls recurs:
Do you know why it's like that?
Do I need to have a different callback?
Thanks for helping...
Andreas
Solved! Go to Solution.
2025-07-19 7:46 AM
The SPI-(or UART-)based solution may be better, but the problem with the timer-based solution is, that you call a function which disables the timer output (i.e. threestates it), as witnessed by the oscilloscope track. Just don't do that. Some Cube/HAL functions may not do what you think they do just based on their name - pending more thorough documentation, they are open source so you can easily look up what are they doing, or just don't use them and program the microcontroller normally.
Here, you don't want to *stop the DMA*. Unless set to Circular, DMA stops automatically when it transfers all data it is set to transfer (in its NDTR register). What you want is a steady output from TIM, and one easy way to achieve that is by setting the respective TIMx_CCRx to zero. So, simply add one zero as an extra element in the array you are transferring using DMA to TIMx_CCRx.
JW
2025-07-18 3:03 AM
Hello @ABach.4
Did you check the example below please:
2025-07-18 5:07 AM
No. Thank you. I'll check them out.
2025-07-18 5:46 AM
Hello @ABach.4
I tested the example on my side. The signal starts low before the timer initialization and goes high after the timer is initialized. If you want the last pulse to behave normally (i.e., not remain high indefinitely), you need to deinitialize the timer inside the HAL_TIM_PWM_PulseFinishedCallback() function.
2025-07-18 7:35 AM
BTW: Consider using SPI to generate data stream for WS2812B. In my opinion it's easier then with timer and i would also say more reliable.
You can simply set SPI bitrate to for example 8Mb/s (bit duration 125ns), then if you send for example pattern 0b01110000, it generates 3*125=475ns pulse. Pattern 0b01111100 creates 5x125=625ns pulse. Which are the L and H signals for WS2812. You can also change bitrate to transfer multiple "pulses" in one SPI byte, or change SPI data size and clock to precise tune pulse lengths.
2025-07-18 7:39 AM
I looked at the example TIM_DMA
Here it uses circular mode, not normal mode (and another timer, TIM3_CH3):
And the period is computed at compile or runtime rather than using the "Device Configuration Tool":
TIM_HandleTypeDef htim3;
DMA_HandleTypeDef hdma_tim3_ch3;
uint32_t aCCValue_Buffer[3] = {0, 0, 0};
uint32_t uwTimerPeriod = 0;
int main(void)
{
HAL_Init();
BSP_LED_Init(LED2); /* does that woek?! I haven't found BSP_LED_Init() */
SystemClock_Config();
uwTimerPeriod = (uint32_t)((SystemCoreClock / 17570) - 1);
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM3_Init();
aCCValue_Buffer[0] = (uint32_t)(((uint32_t) 75 * (uwTimerPeriod - 1)) / 100);
aCCValue_Buffer[1] = (uint32_t)(((uint32_t) 50 * (uwTimerPeriod - 1)) / 100);
aCCValue_Buffer[2] = (uint32_t)(((uint32_t) 25 * (uwTimerPeriod - 1)) / 100);
if (HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_3, aCCValue_Buffer, 3) != HAL_OK) {
Error_Handler();
}
while (1) {
//
}
}
static void MX_TIM3_Init(void)
{
// [...]
htim3.Instance = TIM3;
htim3.Init.Prescaler = 0;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = uwTimerPeriod; /* next time I change something in IOC, it gets overwriten, right? */
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
// [...]
}
When the PWM starts, it goes infinitely. No end.
The rest is almost the same.
In my example above, I send 6 PWM pulses, but after the 4 pulse, the 5th pulse starts but didn't finish. There is no 6th pulse anymore. So HAL_TIM_PWM_PulseFinishedCallback() comes too early, not after the 6th pulse but at the start of the 5th pulse and stop the DMA too early.
I don't care it the timer starts LOW and ends HIGH.
Source extract:
uint32_t data[] = {
30,
80,
20,
60,
10,
50
};
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
HAL_TIM_PWM_Stop_DMA(&htim1, TIM_CHANNEL_4);
}
int main(void)
{
// [...]
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_4, (uint32_t *) data, 6);
}
2025-07-18 8:08 AM
One more information:
One option would be that I add 2 additional pulses with 0 or 100, but this is like a hack, right?
/* 8 rather than 6 pulses */
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_4, (uint32_t *) data, 8);
With 0
uint32_t data[] = {
30,
80,
20,
60,
10,
50,
0,
0
};
With 100:
uint32_t data[] = {
30,
80,
20,
60,
10,
50,
100,
100
};
I try to repeate it with 3 pulses and 2 repetitions.
2025-07-18 8:30 AM
Could also be an option.
You are refering to SPI Mode "Half-Duplex Master" and only use MOSI? It we use a SCK that is not used: less pins.
I'll give it a try.
2025-07-18 10:27 AM
I was using full duplex master mode because I'm not familiar with half-duplex mode. Both SCK and MISO pin can be used to other purposes, just don't configure them as "SPI alternate function".
Nice thing on that method is fact that you dont need to care about timer stopping. SPI can be feeded by DMA without gaps like timer. Disadvantage is higher time deviations due sparse SPI bitrates options. But i've never get problems with that even on slower MCUs like STM8, AVR...
2025-07-18 11:22 AM
Primary understand DMA width, why is here byte or word. Exactly require equal with peripheral data =
16bit timers = half word 32bit timers = word...
Next to understand is DMA controller ends and report complete on moment last data is proceed to peripheral = not equal timer arive this time real = timer cant stop (DMA stop do it)
And last timer mode PWM1 have own rules and exist more modes as this...
Too care require HAL use for example on other as tim peripherals can HAL delegate complete callback to real send data and wait to this after DMA complete, but TIMs not doing this fo OC.