Skip to main content
Graduate II
September 5, 2025
Question

PWM DMA to drive WS281x LED generates inconsistent period and waveform

  • September 5, 2025
  • 7 replies
  • 922 views

I'm trying to drive the PWM of an STM32G0 but the waveform generated had an inconsistent period between bit 0 and bit 1, and I can't understand why

nico23_0-1757060068608.png

I'm using TIMER 4 on Channel 2. On the main.c I'm initializing MX_DMA_Init

void MX_DMA_Init(void)
{
 /* DMA controller clock enable */
 __HAL_RCC_DMA1_CLK_ENABLE();

 /* DMA1_Channel2_3_IRQn interrupt configuration */
 HAL_NVIC_SetPriority(DMA1_Channel2_3_IRQn, 0, 0);
 HAL_NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);
}

and TIM4 configured as 

void MX_TIM4_Init(void)
{
 /* USER CODE BEGIN TIM4_Init 0 */
 HAL_TIM_Base_DeInit(&htim4);
 /* USER CODE END TIM4_Init 0 */

 TIM_ClockConfigTypeDef sClockSourceConfig = {0};
 TIM_MasterConfigTypeDef sMasterConfig = {0};
 TIM_OC_InitTypeDef sConfigOC = {0};

 htim4.Instance = TIM4;
 htim4.Init.Prescaler = 0;
 htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
 htim4.Init.Period = 60 - 1;
 htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
 htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
 if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
 {
 Error_Handler();
 }
 sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
 if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
 {
 Error_Handler();
 }
 if (HAL_TIM_PWM_Init(&htim4) != HAL_OK)
 {
 Error_Handler();
 }
 sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
 sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
 if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
 {
 Error_Handler();
 }
 sConfigOC.OCMode = TIM_OCMODE_PWM1;
 sConfigOC.Pulse = 0;
 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
 sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
 if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
 {
 Error_Handler();
 }
 
 /* USER CODE BEGIN TIM4_Init 2 */

 /* USER CODE END TIM4_Init 2 */
 HAL_TIM_MspPostInit(&htim4);
}

The HAL_TIM_MspPostInit has 

__HAL_RCC_GPIOD_CLK_ENABLE();
 /**TIM4 GPIO Configuration
 PD13 ------> TIM4_CH2
 PD14 ------> TIM4_CH3
 PD15 ------> TIM4_CH4
 */
 GPIO_InitStruct.Pin = OUT_uC3_Pin|OUT_uC2_Pin|OUT_uC1_Pin;
 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
 GPIO_InitStruct.Pull = GPIO_NOPULL;
 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
 GPIO_InitStruct.Alternate = GPIO_AF2_TIM4;
 HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

with 

#define OUT_uC3_Pin GPIO_PIN_13
#define OUT_uC3_GPIO_Port GPIOD

The led_render function correctly fills the buffer with

 // Fill with alternating pattern: HI, LO, HI, LO...
 for(uint8_t i = 0; i < WR_BUF_LEN; i++) {
 wr_buf[i] = (i % 2) ? PWM_HI : PWM_LO;
 }
 
 HAL_TIM_PWM_Start_DMA(&htim4, TIM_CHANNEL_2, (uint32_t *) wr_buf, WR_BUF_LEN);

and in the .h

#define PWM_HI (38)
#define PWM_LO (19)
// LED parameters
#define NUM_BPP (3) // WS2812B
#define NUM_PIXELS (18)
#define NUM_BYTES (NUM_BPP * NUM_PIXELS)

But for some reason I'm seeing a the strange, not correct, waveform

Am I doing something wrong?

    This topic has been closed for replies.

    7 replies

    Super User
    September 5, 2025

    What is the expected waveform and how is the observed one different?

    [EDIT] Assuming timer clock is equal to system clock, 60 cycles may be sufficient for DMA, but AFAIK Cube/HAL does not use the Update event to load TIMx_CCRx but the CC event, which may leave much less time, and result in DMA not feeding the TIMx_CCRx fast enough. I don't say this *is* the problem here (as I don't know what are the symptoms, see my first question), but may be a thing to consider. Try longer times, or higher system clock frequency. 

    JW

    nico23Author
    Graduate II
    September 5, 2025

    Hi @waclawek.jan and thanks for answering.

    Expected waveform has a bunch of 1 and 0 encoding the LEDs colours for istance

    nico23_0-1757076753094.png

    Instead the one I previously upload has only zeros. 

    System clock is 48Mhz (HAL_RCC_GetSysClockFreq and HAL_RCC_GetPCLK1Freq both return 48M) so the period for each beat should be around 1,25us

    For a '1' bit (PWM_HI = 38):

    • High time: ~0.79μs (38/60 × 1.25μs)
    • Low time: ~0.46μs (22/60 × 1.25μs)
    • Total period: 1.25μs

    For a '0' bit (PWM_LO = 19):

    • High time: ~0.40μs (19/60 × 1.25μs)
    • Low time: ~0.85μs (41/60 × 1.25μs)
    • Total period: 1.25μs

    but instead I'm seeing different periods. Below a working PWM generated by DMA

    nico23_1-1757076999520.png

    WHat do you mean with Try longer times, or higher system clock frequency. ?

    Graduate
    September 5, 2025

    What is the declaration of wr_buf and where is it placed?

    nico23Author
    Graduate II
    September 5, 2025

    Hi @gbm and thanks for the answer

    wr_buff is defined on top of adressable_LED.c (same file where led_render function is in)

    // LED write buffer
    #define WR_BUF_LEN (NUM_BPP * 8 * 2)
    uint8_t wr_buf[WR_BUF_LEN] = { 0 };
    uint_fast8_t wr_buf_p = 0;

     

    Technical Moderator
    September 5, 2025

    Hello @nico23 

    Please refer to the example TIM_PWMOutput in the STM32CubeG0 package.

    nico23Author
    Graduate II
    September 5, 2025

    Hi @Saket_Om 

    the example doesn't uses DMA. Do you suggest to avoid using it? Also, I need to drive the PWM using the buffer

    Technical Moderator
    September 5, 2025
    Graduate
    September 5, 2025

    Timer DMA expects 32-bit samples. You are preparing 8-bit duty values, then writing them as 32-bit words - that doesn't make too much sense - 4 values are written as single 32-bit duty value.

    Sidenote: it is much easier and more memory-efficient to use UART or SPI instead of a timer for controlling WS2812-alikes.

    nico23Author
    Graduate II
    September 5, 2025

    I'm using this method/code because it was already implemented into an STM32F091 and it worked pretty well. I'm using 8-bit as the value is always < 256 so, I would save space on storing the buffer and the value copied should be teh same, right?

    I've basically ported it to the STM32G0 but I'm stuck with this issue.

    About your sidenote, I didn't know about that. I'll check if PD15 could be set to use UART or SPI instead of a timer (but I think I'll have to re-write a great part of the code)

    nico23Author
    Graduate II
    September 8, 2025

    Could it be an issue on the MOSFET that actually drives the OUT and not the microcontroller output itself?

    nico23_0-1757331858336.png

    I'm currently attached to OUT, and not OUT_uC, and I'm unable to have a probe at the OUT_uC  

     

     

    Super User
    September 8, 2025

    What hardware is this, your own, or some "known good" board such as Nucleo or Disco?

    In the former case, what's the power supply, are all VSS/VDD pins connected properly and with proper voltage at them, what's the primary clock source, how are FLASH waitstates set up?

    Is there any other code running, or is this result of a minimal example doing nothing but the DMA/PWM thing?

    JW

    nico23Author
    Graduate II
    September 9, 2025

    Yes, it is custom hardware. Yes, all VSS/VDD pins are connected properly with the proper voltage. The primary clock source is the STM32G0 internal oscillator.

    The code is part of a bigger project so, yeah, there are other things running

    Super User
    September 9, 2025

    Can't those other things be in the way?

    In other words, prepare a simple minimal program which does nothing but the PWM+DMA, and try.

    JW

    Graduate
    September 9, 2025

    Is there a pullup resisitor at the WS2812 input? The transisitor output is effectively open drain, so you need to enforce logic high state somehow.

    And again, make sure that timer duty data is of uint32_t type an the DMA is programmed to transfer words.

    Depending on the MCU family, it may also work with uint16_t and 16-bit transfers but definitely NOT with uint8_t and 8-bit transfers.

    Super User
    September 9, 2025

    @gbm,

    > Depending on the MCU family, it may also work with uint16_t and 16-bit transfers but definitely NOT with uint8_t and 8-bit transfers.

    It may work.

    The single-port DMA in 'G0 zero-extends the data if destination width (here, DMA_CCRx.PSIZE) is higher than source width (here, DMA_CCRx.MSIZE), see Programmable data width and endian behavior table. In this detail the single-port DMA behaves differently from the dual-port DMAs e.g. in 'F4. We haven't been shown how exactly is the DMA set up.

    JW