I feel like I am running out of options as to why this is occurring as the RM isn't that accurate to setup DMA + PWM using Timers.

The goal is to use TIM15 as a PWM generator running @ 800kHz running until 24 pulses are outputted and then stop until I command it to run again. Within these 24 pulses different duty cycle can occur from 64% for HIGH and 32% for LOW. This TIM15 is using the Repetition Counter register to indicate when 24 pulses as gone by.

What I have working thus far is a nice 24 pulse PWM stream but with only LOW (32%) present even though my buffer that gets transferred to CCR1 contains HIGH values (64%)

The configuration for the TIM15 is as follows:

    LL_DMA_InitTypeDef      WS2812B_Config_DMA;
    LL_GPIO_InitTypeDef     WS2812B_Config_GPIO;
    LL_TIM_InitTypeDef      WS2812B_Config_TIM15;
    LL_TIM_OC_InitTypeDef   WS2812B_Config_OC;
    LL_TIM_BDTR_InitTypeDef WS2812B_Config_BDTR;
    //Configure GPIO
    WS2812B_Config_GPIO.Alternate  = LL_GPIO_AF_4;
    WS2812B_Config_GPIO.Mode       = LL_GPIO_MODE_ALTERNATE;
    WS2812B_Config_GPIO.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
    WS2812B_Config_GPIO.Pin        = LL_GPIO_PIN_2;
    WS2812B_Config_GPIO.Pull       = LL_GPIO_PULL_DOWN;
    WS2812B_Config_GPIO.Speed      = LL_GPIO_SPEED_FREQ_VERY_HIGH;
    LL_GPIO_Init(GPIOA, &WS2812B_Config_GPIO);
    //Configure DMA
    WS2812B_Config_DMA.MemBurst = LL_DMA_MBURST_SINGLE;
    WS2812B_Config_DMA.MemoryOrM2MDstAddress = (uint32_t)WS28128B_BUFF;
    WS2812B_Config_DMA.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_HALFWORD;
    WS2812B_Config_DMA.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
    WS2812B_Config_DMA.Mode = LL_DMA_MODE_NORMAL;
    WS2812B_Config_DMA.NbData = MAX_BUFF;
    WS2812B_Config_DMA.PeriphBurst = LL_DMA_PBURST_SINGLE;
    WS2812B_Config_DMA.PeriphOrM2MSrcAddress = (uint32_t)&TIM15->CCR1;
    WS2812B_Config_DMA.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_HALFWORD;
    WS2812B_Config_DMA.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT;
    WS2812B_Config_DMA.PeriphRequest = LL_DMAMUX1_REQ_TIM15_UP;
    WS2812B_Config_DMA.Priority = LL_DMA_PRIORITY_LOW;
    //Interrupt: DMA
    NVIC_SetPriority(DMA1_Stream4_IRQn, 0);
    NVIC_SetPriority(TIM15_IRQn, 0);
    LL_DMA_Init(DMA1, LL_DMA_STREAM_4, &WS2812B_Config_DMA);
    WS2812B_Config_TIM15.Autoreload    = (240e6/WS2812B_FREQ) - 1;
    WS2812B_Config_TIM15.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
    WS2812B_Config_TIM15.CounterMode   = LL_TIM_COUNTERMODE_UP;
    WS2812B_Config_TIM15.Prescaler     = 0;
    WS2812B_Config_TIM15.RepetitionCounter = MAX_BUFF - 1;
    LL_TIM_Init(TIM15, &WS2812B_Config_TIM15);
    WS2812B_Config_OC.CompareValue = 0;
    WS2812B_Config_OC.OCIdleState = LL_TIM_OCIDLESTATE_LOW;
    WS2812B_Config_OC.OCMode = LL_TIM_OCMODE_PWM1;
    WS2812B_Config_OC.OCNIdleState = LL_TIM_OCIDLESTATE_LOW;
    WS2812B_Config_OC.OCNPolarity = LL_TIM_OCPOLARITY_HIGH;
    WS2812B_Config_OC.OCPolarity = LL_TIM_OCPOLARITY_HIGH;
    WS2812B_Config_OC.OCState = LL_TIM_OCSTATE_ENABLE;
    LL_TIM_OC_Init(TIM15, LL_TIM_CHANNEL_CH1, &WS2812B_Config_OC);
    WS2812B_Config_BDTR.Break2Filter = LL_TIM_BREAK2_FILTER_FDIV1;
    WS2812B_Config_BDTR.Break2Polarity = LL_TIM_BREAK2_POLARITY_LOW;
    WS2812B_Config_BDTR.Break2State = LL_TIM_BREAK2_DISABLE;
    WS2812B_Config_BDTR.BreakFilter = LL_TIM_BREAK_FILTER_FDIV1;
    WS2812B_Config_BDTR.BreakPolarity = LL_TIM_BREAK_POLARITY_LOW;
    WS2812B_Config_BDTR.BreakState = LL_TIM_BREAK_DISABLE;
    WS2812B_Config_BDTR.DeadTime = 0;
    WS2812B_Config_BDTR.LockLevel = LL_TIM_LOCKLEVEL_OFF;
    LL_TIM_BDTR_Init(TIM15, &WS2812B_Config_BDTR);
    LL_TIM_OC_EnablePreload(TIM15, LL_TIM_CHANNEL_CH1);

The interrupts that are currently active are DMA1_Stream 4 Transfer Complete & TIM15_Update. For the DMA, I just clear the TC4 interrupt to indicate that everything from the buffer has been transferred over to CCR1 and clear it.

The TIM15_Update interrupt indicates that 24 pulses has gone and I handle it accordingly

The interrupts in how they are setup are as follows:

extern "C" void DMA1_Stream4_IRQHandler() {
    if (LL_DMA_IsActiveFlag_TC4(DMA1)) {
extern "C" void TIM15_IRQHandler() {
    if (LL_TIM_IsActiveFlag_UPDATE(TIM15)) {
        LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_4);
        WS2812B::block = false;

The buffer contents that I've been using:

LOW = 0x5F = 32% duty cycle

HIGH = 0xBF = 64% duty cycle

WS28128B_BUFF[0]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[1]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[2]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[3]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[4]    uint32_t    0xbf (Hex)  
WS28128B_BUFF[5]    uint32_t    0xbf (Hex)  
WS28128B_BUFF[6]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[7]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[8]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[9]    uint32_t    0x5f (Hex)  
WS28128B_BUFF[10]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[11]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[12]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[13]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[14]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[15]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[16]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[17]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[18]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[19]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[20]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[21]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[22]   uint32_t    0x5f (Hex)  
WS28128B_BUFF[23]   uint32_t    0x5f (Hex)  

Picture of logic analyzer enter image description here

  • \$\begingroup\$ Does this answer your question? STM32) Why DMA can't approach global variable? You'll almost certainly find that your WS28128B_BUF[] array is in the DTCM block at 0x20000000. \$\endgroup\$
    – brhans
    Jun 30 at 16:41
  • \$\begingroup\$ @brhans Hi! Thank you for your reply. That is one of the first things I checked. Yes its a global variable however I have adjusted my linker script to add those types of data into normal RAM which in this case is 0x240xxxxxxxx. \$\endgroup\$
    – Leoc
    Jun 30 at 16:51
  • \$\begingroup\$ Looks to me like you have your WS2812B_Config_DMA source and destination mixed up. You've put (uint32_t)WS28128B_BUFF as the destination and (uint32_t)&TIM15->CCR1 as the source. \$\endgroup\$
    – brhans
    Jun 30 at 17:00
  • \$\begingroup\$ @brhans Thank you for the reply again! I double checked and I could be wrong but in this context this is a Memory -> Peripheral transfer. As mentioned in the code above its set to WS2812B_Config_DMA.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH. I can see where the confusion lies as the struct has src and dst listed within its name, I believe this only applies to Memory -> Memory transfer to indicate which will be the source and destination otherwise its just asking for the addresses of memory and peripheral and will either be a sink or source depending on the DIR bit. \$\endgroup\$
    – Leoc
    Jun 30 at 17:11
  • 1
    \$\begingroup\$ @brhans I rather not use HAL for overhead reasons. Mentioned in the above code the DMAMUX has been already defined as: WS2812B_Config_DMA.PeriphRequest = LL_DMAMUX1_REQ_TIM15_UP I am narrowing down the issue in the repetition counter. I mistooken this and it looks like it repeats the same signal up to X amount of pulses, so it was repeating 24 Lows if that make sense. I have set Repetition counter back to 0 and I am starting to see Highs in there, PWM looks little funny but I believe its a step to the right direction. \$\endgroup\$
    – Leoc
    Jun 30 at 17:50

1 Answer 1


The issue was my misunderstanding of what the repetition counter did. I was under the impression that the repetition counter was responsible for keeping track of pulses. In reality it just repeated the value within the CCR1 register to the X amount in the repetition register, in this case 24 times.

The way I solved it was to remove the repetition counter as it no longer fits my criteria and got the PWM working as expected.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.