Skip to main content
Visitor II
July 18, 2025
Solved

Timer PWM with DMA stops too early on STM32G431

  • July 18, 2025
  • 7 replies
  • 1318 views

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.

  • Clock SYSCLK/HCLK: 170 MHz
  • TIM1 CH4 configured
    • Prescaler: 170-1 => 1 MHZ
    • Counter Preiod: 100-1 => 10 kHz
  • DMA
    • Memory -> Peripheral
    • Data Width: Word (32 bit)
  • 6 PWM pulses (= 600 us) should be sent out

TIM1 Mode + Configuration

tim1_mode.png

tim1_conf1.png

tim1_conf2.png

DMA Settings

dma_settings.png

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

h100_dma_uint32t.png

I tried also with Data Width

Half-Word

h100_dma_uint16t.png

Byte

h100_dma_uint8t.png

Without stopping DMA, it's like that:

The last puls recurs:

h100_dma_uint32t_nostop.png

Do you know why it's like that?

Do I need to have a different callback?

Thanks for helping...

Andreas

    This topic has been closed for replies.
    Best answer by waclawek.jan

    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

    7 replies

    Technical Moderator
    July 18, 2025
    ABach.4Author
    Visitor II
    July 18, 2025

    No. Thank you. I'll check them out.

    Technical Moderator
    July 18, 2025

    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.

    Graduate II
    July 18, 2025

    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.

    ABach.4Author
    Visitor II
    July 18, 2025

    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.

    Graduate II
    July 18, 2025

    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...


    ABach.4Author
    Visitor II
    July 18, 2025

    I looked at the example TIM_DMA

    STM32CubeG4/Projects/NUCLEO-G431KB/Examples/TIM/TIM_DMA at master · STMicroelectronics/STM32CubeG4 · GitHub

    Here it uses circular mode, not normal mode (and another timer, TIM3_CH3):

    G431KB_dma_settings.png

    And the period is computed at compile or runtime rather than using the "Device Configuration Tool":

    • Clock SYSCLK/HCLK: 150 MHz
    • TIM1 CH4 configured
      • Prescaler: 0 => 150 MHZ
      • Counter Period: 8537-1 => 17.57 kHz
    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.

     

    Back to my example:

    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);
    }

    h100_dma_uint32t_color.png

    • Is TIM1_CH4 not good because the period doesn't fit well?
    • Should I use a different callback that fits better to DMA?
    ABach.4Author
    Visitor II
    July 18, 2025

    One more information:

    WeActStudio_STM32G431CoreBoard.jpeg

     

    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
    };

     h100_dma_uint32t_8pulses_0.png

    With 100:

    uint32_t data[] = {
    		30,
    		80,
    		20,
    		60,
    		10,
    		50,
    		100,
    		100
    };

     h100_dma_uint32t_8pulses_100.png

     

    I try to repeate it with 3 pulses and 2 repetitions.

    Super User
    July 19, 2025

    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

    ABach.4Author
    Visitor II
    July 19, 2025

    Hello @waclawek.jan,

    Thank you for the explanation.

    • HAL_TIM_PWM_Stop_DMA() disables the peripheral and threestates it, maybe with the macro __HAL_TIM_DISABLE(). I'm not sure.
    HAL_StatusTypeDef HAL_TIM_PWM_Stop_DMA(TIM_HandleTypeDef *htim, uint32_t Channel)
    {
     // [...]
    
     /* Disable the Peripheral */
     __HAL_TIM_DISABLE(htim);
    
     // [...]
    }
    • In DMA Mode "Normal" the DMA stops after the specific count. Because I didn't use 0 but 50 as the last pulse, the DMA stopped but the TIM_CH4 stays with the last pulse forever.
    • Add one zero as the last pulse solves the problem
    uint16_t data[] = {
    		30,
    		80,
    		20,
    		60,
    		10,
    		50,
    		0
    };
    
    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+1);
     while (1) {
     //
     }
    }

    The oscilloscope gives the result

    h100_dma_uint16t_single.png

    It's just strange that HAL_TIM_PWM_PulseFinishedCallback() comes 2 pulses to early. But if no one helps me, I'll just have to live with it.

    Super User
    July 19, 2025

    > It's just strange that HAL_TIM_PWM_PulseFinishedCallback() comes 2 pulses to early.

    I don't use Cube/HAL so don't know what's exactly HAL_TIM_PWM_PulseFinishedCallback(), but if it's called from the DMA's Transfer Complete interrupt, then that must happen before the last pulse, as the last DMA transfer is triggered by the pulse one before that last pulse (as far as I know Cube/HAL, that may be triggered by the CCx event rather than Update event, which is incorrect, but that's Cube/HAL for ya). And, as you have Compare Preload enabled, that adds one more timer cycle to the "lag".

    JW