Working with STM32 and Timers: Update PWM duty cycle using DMA

In the previous guide (here), we took a look at how to configure the timer in PWM and we were able to update the duty cycle of the PWM to fade two LEDs using polling mode. In this guide, we shall look how to configure timer to update the duty cycle using DMA (Direct Memory Access).

In this guide, we will cover the following:

  • Configure the timer in PWM.
  • Configure the DMA.
  • Code.
  • Demo

1. Configure the Timer in PWM Mode:

We start off by configuring the GPIO (check the topic here):

void GPIO_Init(void)
{
	#define AF01 0x01
	RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_GPIOBEN;
	GPIOA->MODER|=GPIO_MODER_MODE0_1|GPIO_MODER_MODE1_1;
	GPIOA->AFR[0]|=(0x01<<0)|(0x01<<4);

}

Then we enable clock access to timer2 and configure the prescaler and ARR value:

RCC->APB1ENR|=RCC_APB1ENR_TIM2EN; //enable clock access tto tim2
TIM2->PSC=0; //set prescaller to 0 (no divider)
TIM2->ARR=1600; //set the maximum count value
TIM2->CNT=0; //seset the current count

Now, in order to make the timer work with the DMA, we need to configure CCDS: Capture/compare DMA selection in Control Register 2 (CR2):

  • CCDS: Capture/compare DMA selection
TIM2->CR2|=TIM_CR2_CCDS;

From DMA/Interrupt enable register, we need to enable the following:

  • UDE: Update DMA request enable
  • CC1DE: Capture/Compare 1 DMA request enable
  • CC2DE: Capture/Compare 2 DMA request enable
  • TDE: Trigger DMA request enable
TIM2->DIER|=(TIM_DIER_UDE)|TIM_DIER_CC1DE|TIM_DIER_CC2DE;
TIM2->DIER|=TIM_DIER_TDE;

Finally enable the PWM channel:

TIM2->CCMR1=TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC2M_2|TIM_CCMR1_OC2M_1; //configure the pins as PWM
TIM2->CCER|=TIM_CCER_CC1E|TIM_CCER_CC2E; //enbale channel1 and channel2

Please note that we didn’t start the timer yet.

2. Configuring the DMA:

Before configuring the DMA, we need to know which DMA, stream and channel to use.

From the reference manual, DMA section, we can find DMA request map

Hence, we need to use DMA1, Stream5 and Stream6 and channel 3

We can start by enabling clock access to DMA1 as following:

RCC->AHB1ENR|=RCC_AHB1ENR_DMA1EN;

The disable the stream and wait until it is disabled

RCC->AHB1ENR|=RCC_AHB1ENR_DMA1EN;
DMA1_Stream5->CR&=DMA_SxCR_EN;
while((DMA1_Stream5->CR)&DMA_SxCR_EN){;}

DMA1_Stream6->CR&=DMA_SxCR_EN;
while((DMA1_Stream6->CR)&DMA_SxCR_EN){;}

Then we can configure both stream as following:

  • Channel 3
  • Memory size to 32
  • Peripheral size to 32
  • Memory increment mode
  • Circular mode
  • Direction memory to peripheral
#define CH3   ((1<<26)|(1<<25))
#define M_SI32 (1<<14)
#define P_SI32 (1<<12)
#define M_INC  (1<<10)
#define Cir    (1<<8)
#define M2P    (1<<6)
DMA1_Stream5->CR|=CH3|M_SI32|P_SI32|M_INC|Cir|M2P;
DMA1_Stream6->CR|=CH3|M_SI32|P_SI32|M_INC|Cir|M2P;



Set number of transfer to 200 (in this case):

DMA1_Stream5->NDTR=(uint16_t)200;
DMA1_Stream6->NDTR=(uint16_t)200;

Peripheral address to be CCR1 for Stream5

DMA1_Stream5->PAR=(uint32_t)(&TIM2->CCR1);

CCR2 for Stream6

DMA1_Stream6->PAR=(uint32_t)(&TIM2->CCR2);

Memory address to be lookup table 1 for Stream5

DMA1_Stream5->M0AR=(uint32_t)(&lookUp1);

lookup table 2 for Stream6

DMA1_Stream6->M0AR=(uint32_t)(&lookUp2);

Enable both stream:

DMA1_Stream6->CR|=DMA_SxCR_EN;

DMA1_Stream5->CR|=DMA_SxCR_EN;

and Finally enable the timer:

TIM2->CR1|=1; //enable timer

3. Code:

Hence the entire code shall be like this:

#include "stm32f4xx.h"                  // Device header

#define CH3   ((1<<26)|(1<<25))
#define M_SI32 (1<<14)
#define P_SI32 (1<<12)
#define M_INC  (1<<10)
#define Cir    (1<<8)
#define M2P    (1<<6)
#define PFCTRL (1<<5)
// Look up tables with 200 entries each, normalised to have max value of 1600 which is the period of the PWM loaded into register ICR1.
uint32_t lookUp1[] = {0,50 ,100 ,151 ,201 ,250 ,300 ,349 ,398 ,446 ,494 ,542 ,589 ,635 ,681
	,726 ,771 ,814 ,857 ,899 ,940 ,981 ,1020 ,1058 ,1095 ,1131 ,1166 ,1200 ,1233 ,1264
	,1294 ,1323 ,1351 ,1377 ,1402 ,1426 ,1448 ,1468 ,1488 ,1505 ,1522 ,1536 ,1550 ,1561
	,1572 ,1580 ,1587 ,1593 ,1597 ,1599 ,1600 ,1599 ,1597 ,1593 ,1587 ,1580 ,1572 ,1561
	,1550 ,1536 ,1522 ,1505 ,1488 ,1468 ,1448 ,1426 ,1402 ,1377 ,1351 ,1323 ,1294 ,1264
	,1233 ,1200 ,1166 ,1131 ,1095 ,1058 ,1020 ,981 ,940 ,899 ,857 ,814 ,771 ,726 ,681 ,635
	,589 ,542 ,494 ,446 ,398 ,349 ,300 ,250 ,201 ,151 ,100 ,50,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
	,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
	,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
	,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0};

uint32_t lookUp2[] = {0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
	,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
	,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0
	,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,50 ,100 ,151 ,201 ,250 ,300 ,349 ,398 ,446 ,494
	,542 ,589 ,635 ,681 ,726 ,771 ,814 ,857 ,899 ,940 ,981 ,1020 ,1058 ,1095 ,1131 ,1166 ,1200 ,1233
	,1264 ,1294 ,1323 ,1351 ,1377 ,1402 ,1426 ,1448 ,1468 ,1488 ,1505 ,1522 ,1536 ,1550 ,1561 ,1572 ,1580
	,1587 ,1593 ,1597 ,1599 ,1600 ,1599 ,1597 ,1593 ,1587 ,1580 ,1572 ,1561 ,1550 ,1536 ,1522 ,1505 ,1488
	,1468 ,1448 ,1426 ,1402 ,1377 ,1351 ,1323 ,1294 ,1264 ,1233 ,1200 ,1166 ,1131 ,1095 ,1058 ,1020 ,981
	,940 ,899 ,857 ,814 ,771 ,726 ,681 ,635 ,589 ,542 ,494 ,446 ,398 ,349 ,300 ,250 ,201 ,151 ,100 ,50 ,0,0};




void GPIO_Init(void);
void Timer2_init(void);




int main()


	{
	GPIO_Init();
	Timer2_init();

	while(1)
		{


		}


	}






void GPIO_Init(void)
{
	#define AF01 0x01
	RCC->AHB1ENR|=RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_GPIOBEN;
	GPIOA->MODER|=GPIO_MODER_MODE0_1|GPIO_MODER_MODE1_1;
	GPIOA->AFR[0]|=(0x01<<0)|(0x01<<4);

}
void  Timer2_init(void){

//init timer;
RCC->APB1ENR|=RCC_APB1ENR_TIM2EN; //enable clock access tto tim2
TIM2->PSC=0; //set prescaller to 0 (no divider)
TIM2->ARR=1600; //set the maximum count value
TIM2->CNT=0; //seset the current count
TIM2->CR2|=TIM_CR2_CCDS;
TIM2->DIER|=(TIM_DIER_UDE)|TIM_DIER_CC1DE|TIM_DIER_CC2DE;
TIM2->DIER|=TIM_DIER_TDE;
TIM2->CCMR1=TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC2M_2|TIM_CCMR1_OC2M_1; //configure the pins as PWM
TIM2->CCER|=TIM_CCER_CC1E|TIM_CCER_CC2E; //enbale channel1 and channel2

//init dma


RCC->AHB1ENR|=RCC_AHB1ENR_DMA1EN;
DMA1_Stream5->CR&=DMA_SxCR_EN;
while((DMA1_Stream5->CR)&DMA_SxCR_EN){;}
DMA1_Stream5->CR|=CH3|M_SI32|P_SI32|M_INC|Cir|M2P;
DMA1_Stream5->NDTR=(uint16_t)200;

DMA1_Stream5->PAR=(uint32_t)(&TIM2->CCR1);

DMA1_Stream5->M0AR=(uint32_t)(&lookUp1);


DMA1_Stream6->CR&=DMA_SxCR_EN;
while((DMA1_Stream6->CR)&DMA_SxCR_EN){;}
DMA1_Stream6->CR|=CH3|M_SI32|P_SI32|M_INC|Cir|M2P;
DMA1_Stream6->NDTR=(uint16_t)200;

DMA1_Stream6->PAR=(uint32_t)(&TIM2->CCR2);

DMA1_Stream6->M0AR=(uint32_t)(&lookUp2);


DMA1_Stream6->CR|=DMA_SxCR_EN;


DMA1_Stream5->CR|=DMA_SxCR_EN;
TIM2->CR1|=1; //enable timer
}

4. Demo:

When you probe both PA0 and PA1 using an oscilloscope, you should get the following:

Happy coding 🙂

3 Comments

  • Thamanoon Kedwiriyakarn Posted January 31, 2023 7:47 am

    Hello,
    Thank you very much for your expanation. It’s very helpful guide.

  • Ndidi Posted December 8, 2023 10:17 pm

    Why Enable ude,tde,cc1de and cc2de when what u actually need is just your ccxde

    • Husamuldeen Posted December 13, 2023 3:47 am

      Hi,
      you are correct.
      The code was a test code when I accidentally used it rather than the final version.
      However, enabling those it irrelevant since they won’t be triggered at all.

Add Comment

Your email address will not be published. Required fields are marked *