TIM2 PWM newbie needs help with bare metal code

Post here first, or if you can't find a relevant section!
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Thanks to GonzoG,dannyf and ag123, this is my first attempt to write code for STM32 with bare metal coding.
We started talking here about other subject:
I decided to make a separate post for this issue.
I'm sorry, please be patient, I am just starting and this part of the road is hard.
Based upon a YouTube original example for an F4 engine, I had to make changes for my F103C8T6 BluePill.
Well...it doesn't work. I should be seeing a PWM signal in PA0, 50% duty cycle, using TIM2.
I am really interested in direct register access coding. I don't like HAL.
Could you help me find the errors so I can study and learn ?
Here is my code.
Thank You !


Code: Select all

#define PIN 0
#define PERIOD  100
#define DUTY    50

void setup() 
{
  // enable clocks for GPIOA and TIMER2

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
  
  // set alternate function on PA0
  GPIOA->CRL &= ~0x01; GPIOA->CRL |= (1 << 2);        // '10'
  GPIOA->CRL |= (1 << 4) ; GPIOA->CRL &= ~(1 << 3); // '10'

  // set channel to output mode (defalult after reset)
  TIM2->CCMR1 &= ~0x03;
  
  // select the polarity by writing de CCPx bit
  TIM2->CCER &= ~0x03;
  
  // select PWM mode 1 or 2 upcounting or downcounting by writing OCxM bits in CCMRx register
  TIM2->CCMR1 |= (0x06 << 5);
  
  // Program the period and the duty cycle respectively in ARR and CCRx registers
  TIM2->PSC = 7999;  // divides the 16 MHz clock so the counter runs at 1KHz
  TIM2->ARR = PERIOD;
  TIM2->CCR1 = DUTY;  
  
  //set the preload bit in the CCMR register and the ARPE bit in the CR1 register
  TIM2->CCMR1 |= TIM_CCMR1_OC2PE;
  TIM2->CR1 |= TIM_CR1_ARPE;
  
  // select the counting mode PWM edge aligned mode, 
  // a) the counter must be configured up-counting or down-counting
  // b) the counter must be center aligned counting mode (CMS bits different from '00')
  // Leave the default  DIR = 0 (UPCOUNT) CMS = 0 (EDGE ALIGNED) 
  //-------------
  // Enable the capture / compare
  
  TIM2->CCER |= 0x01;           //  enable compare
  TIM2->CR1 |= TIM_CR1_CEN;     //  enable count
  
  while(1)   ;
 }



void loop() 
{
}
dannyf
Posts: 446
Joined: Sat Jul 04, 2020 7:46 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by dannyf »

didn't read it thorougly but those chips have a bug - you have to enable the moe bit.
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Thank you dannyf, I will Google 'moe' and see if I get the idea.
I took the example from a neat video, but unfortunately this guy uses F4, and the defines and register names in bare metal language are different and is a real pain in $%#@%$& to find the equivalents.

The guy in the video never mentioned a bug ... but I believe you. Can happen.

Roberto
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Ohhh ! I found a mistake of mine in the code.
The GPIO pin is wrong for this timer. It is not PA0, should be PA11
But PA11 is taken by the USB interface, so I will swith to TIM1_CH! , PA8
I will correct it and see ...
I am sorry.
I'll be back to report.
dannyf, the MOE bit is set in this code.

Roberto
dannyf
Posts: 446
Joined: Sat Jul 04, 2020 7:46 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by dannyf »

here is my code for pwm2

Code: Select all

//initialize pwm to TxCCP_PS (prescaler) and TxCCP_PR (period)
void pwm2Init(uint16_t TxCCP_PS) {
	//route the clock to timer
	//route the clock to timer
	RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

	//source from internal clock -> disable slave mode
	TIM2->SMCR &=~TIM_SMCR_SMS;			//clear sms->use internal clock

	//stop the timer to configure it
	//TIM2->CR1 &=~TIM_CR1_CEN;			//clear cen. 0=disable the timer, 1=enable the timer
	//TIM2->CR1 &=~TIM_CR1_CKD;			//clear CKD0..1. 0b00->1x clock; 0b01->2:1 clock, 0b10->4:1 clk; 0b11->reserved
	//TIM2->CR1 &=~TIM_CR1_DIR;			//clear DIR bit. 0=upcounter, 1=downcounter
	//TIM2->CR1 &=~TIM_CR1_OPM;			//clear opm bit. 0=periodic timer, 1=one-shot timer
	//or to simply zero the register
	//TIM2->CR1 = 0;						//much easier
	TIM2->CR1 =	(0<<8) |				//0->1:1 clock, 1->2:1 clock, 2->4:1 clock, 3->reserved
				(0<<7) |				//1->APR buffered, 0->APR not buffered
				(0<<5) |				//0->edge-aligned, 1->center-aligned mode 1, 2->center-aligned mode 2, 3->center-aligned mode 3
				(0<<4) |				//0->upcounter, 1->downcounter
				(0<<3) |				//0->continous mode, 1->one pulse mode
				(0<<2) |				//update request source
				(0<<1) |				//0->UEV enabled, 1->UEV disabled
				(0<<0) |				//0->counter disabled, 1->counter enabled
				0x00;
	TIM2->CR2 = 0;						//default value
	TIM2->SMCR= 0;						//default value

	//set the prescaler
	TIM2->PSC = TxCCP_PS - 1;					//set the prescaler
	TIM2->RCR = 0;						//repetition counter = 0 (=no repetition)
	TIM2->ARR = PWM_PR;						//auto reload register / period = 0; - need to change for downcounters
	TIM2->CNT = 0;						//reset the counter

	//clear the status register bits for capture / compare flags
	TIM2->SR &=~(TIM_SR_CC1IF | TIM_SR_CC2IF | TIM_SR_CC3IF | TIM_SR_CC4IF | TIM_SR_UIF);
	//disable the interrupt by clearing the enable bits
	TIM2->DIER &=~(TIM_DIER_CC1IE | TIM_DIER_CC2IE | TIM_DIER_CC3IE | TIM_DIER_CC4IE | TIM_DIER_UIE);

	RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;				//enable AFIO
	AFIO->MAPR = (AFIO->MAPR &~AFIO_MAPR_TIM2_REMAP) | (AFIO_MAPR_TIM2_REMAP & TIM2REMAP);	//select the remap scheme
	//configure CCP1..4


	//configure CCP1
	TIM2->CCMR1 = 	(TIM2->CCMR1 &~0x00ff) |
					(0<<7) |			//0->OC1REF not affedted by ETRF, 1->OC1REF cleared by ETRF high
					(6<<4) |			//0->frozen (for time base), 1->active on match, 2->inactive on match, 3->toggle, 4->inactive, 5->active, 6->pwm mode 1, 7->pwm mode 2
					(0<<3) |			//0->preload disabled, 1->preload enabled
					(0<<2) |			//0->fast disabled, 1->fast enabled
					(0<<0) |			//0->ch1 as output, 1->ch1 as input, 2->ch1 as input, 3->ch1 as input
					0x00;
	TIM2->CCER = 	(TIM2->CCER &~(0x0f << 0)) |
					(0<< 3) |			//0->normal polarity for CC1N, 1->inverse polarity
					(0<< 2) |			//0->disable CC1N, 1->enable CC1N
					(0<< 1) |			//0->normal polarity for CC1, 1->inverse polarity
					(1<< 0) |			//1->enable CC1, 0->disable CC1
					0x00;
	TIM2->CCR1 = 0;						//0% duty cycle

	//configure gpio for pwm output
#if defined(TIM2CH1toPIN)
	TIM2CH1toPIN();
#endif

	//configure CCP2
	TIM2->CCMR1 = 	(TIM2->CCMR1 &~0xff00) |
					(0<<15) |			//0->OC1REF not affedted by ETRF, 1->OC1REF cleared by ETRF high
					(6<<12) |			//0->frozen (for time base), 1->active on match, 2->inactive on match, 3->toggle, 4->inactive, 5->active, 6->pwm mode 1, 7->pwm mode 2
					(0<<11) |			//0->preload disabled, 1->preload enabled
					(0<<10) |			//0->fast disabled, 1->fast enabled
					(0<<8) |			//0->ch1 as output, 1->ch1 as input, 2->ch1 as input, 3->ch1 as input
					0x00;
	TIM2->CCER = 	(TIM2->CCER &~(0x0f << 4)) |
					(0<< 7) |			//0->normal polarity for CC2N, 1->inverse polarity
					(0<< 6) |			//0->disable CC2N, 1->enable CC2N
					(0<< 5) |			//0->normal polarity for CC2, 1->inverse polarity
					(1<< 4) |			//1->enable CC2, 0->disable CC2
					0x00;
	TIM2->CCR2 = 0;						//0% duty cycle

	//configure gpio for pwm output
#if defined(TIM2CH2toPIN)
	TIM2CH2toPIN();
#endif

	//configure CCP3
	TIM2->CCMR2 = 	(TIM2->CCMR1 &~0x00ff) |
					(0<<7) |			//0->OC1REF not affedted by ETRF, 1->OC1REF cleared by ETRF high
					(6<<4) |			//0->frozen (for time base), 1->active on match, 2->inactive on match, 3->toggle, 4->inactive, 5->active, 6->pwm mode 1, 7->pwm mode 2
					(0<<3) |			//0->preload disabled, 1->preload enabled
					(0<<2) |			//0->fast disabled, 1->fast enabled
					(0<<0) |			//0->ch1 as output, 1->ch1 as input, 2->ch1 as input, 3->ch1 as input
					0x00;
	TIM2->CCER = 	(TIM2->CCER &~(0x0f << 8)) |
					(0<<11) |			//0->normal polarity for CC3N, 1->inverse polarity
					(0<<10) |			//0->disable CC3N, 1->enable CC3N
					(0<< 9) |			//0->normal polarity for CC3, 1->inverse polarity
					(1<< 8) |			//1->enable CC3, 0->disable CC3
					0x00;
	TIM2->CCR3 = 0;						//0% duty cycle

	//configure gpio for pwm output
#if defined(TIM2CH3toPIN)
	TIM2CH3toPIN();
#endif

	//configure CCP4
	TIM2->CCMR2 = 	(TIM2->CCMR1 &~0xff00) |
					(0<<15) |			//0->OC1REF not affedted by ETRF, 1->OC1REF cleared by ETRF high
					(6<<12) |			//0->frozen (for time base), 1->active on match, 2->inactive on match, 3->toggle, 4->inactive, 5->active, 6->pwm mode 1, 7->pwm mode 2
					(0<<11) |			//0->preload disabled, 1->preload enabled
					(0<<10) |			//0->fast disabled, 1->fast enabled
					(0<<8) |			//0->ch1 as output, 1->ch1 as input, 2->ch1 as input, 3->ch1 as input
					0x00;
	TIM2->CCER = 	(TIM2->CCER &~(0x0f << 12)) |
					(0<<15) |			//0->normal polarity for CC4N, 1->inverse polarity
					(0<<14) |			//0->disable CC4N, 1->enable CC4N
					(0<<13) |			//0->normal polarity for CC4, 1->inverse polarity
					(1<<12) |			//1->enable CC4, 0->disable CC4
					0x00;
	TIM2->CCR4 = 0;						//0% duty cycle

	//configure gpio for pwm output
#if defined(TIM2CH4toPIN)
	TIM2CH4toPIN();
#endif

	TIM2->EGR = 0xff;					//force an update
	TIM2->BDTR |= TIM_BDTR_MOE;			//enable MOE bit
	//enable the timer.
	TIM2->CR1 |= TIM_CR1_CEN;			//enable the timer

}
the output on a given pin is enabled by the TIM2CHntoPIN() macros:

Code: Select all

#define TIM2CH1toPIN()		TIM2CH1toPA0()			//PA0, PA15
by commenting out this macro, you are muting the pwm output.
mapping the macro to "...toPA0()" or "...toPA15()" redirects the output.

In this case, TIM2CH1toPA0() is further defined as

Code: Select all

#define TIM2CH1toPA0()		do {GIO_AFPP(GPIOA, 1<< 0); AFIO->MAPR = (AFIO->MAPR &~AFIO_MAPR_TIM2_REMAP) | (0x00 << 8);} while (0)
essentially enabling the right af function on a given pin.

the code will be different on different chips but the basic logic works.

hope it helps.
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Thank you dannyf, I will check all this and be back to report.

Roberto
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Bare Metal STM32F103C8T6 BluePill
PWM - TIM1 - CH1 - 1200 Hz on PA8

OK, after joining the pieces together I could make it work. Of course I have lots of questions and doubts.
dannyf, next step is studying your last post with code. I will soon report on this.

At first it was hard, but you were right, it is not so difficult. This is a remarcable, dense, piece of silicon hardware.

ChatGPT also made a nice job helping here. Not perfect but being patient, it is very useful.

Now I need to generate an interrupt event on each PWM cycle. I don't know how to enable the TIM1 overflow interrupt and point to an interrupt handler function. ;)

NOTE: I am sorry. I quit using HID-bootloader. I ended re-flashing the bootloader after each programming operation. It began to lose the ability of recreate the serial port. This bug is documented but, having an ST-Link, I decided to decline the serial port for the sake of fast reliable programming. Maybe one of these days I will try to find a fix.



Here is the working code with comments:

Thank you all !

Roberto

Code: Select all

/* TIM1 CH1 - 1200 Hz PWM on PA8 */

#define PERIOD      10000
#define DUTY        1000
#define PRESCALER   5

static int d;

void setup() 
{
  
/*  Enable the clocks for GPIOA and TIM1 */
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;

/* Set PA8 as alternate function output push-pull */
  GPIOA->CRH &= ~(GPIO_CRH_CNF8 | GPIO_CRH_MODE8);  
  GPIOA->CRH |= GPIO_CRH_CNF8_1;  /* 10: Alternate function output Push-pull */
  GPIOA->CRH |= GPIO_CRH_MODE8_1; /* 10: Output mode, max speed 2 MHz. */

/*  MOE: Main output enable
    This bit is cleared asynchronously by hardware as soon as the break input is active. It is set
    by software or automatically depending on the AOE bit. It is acting only on the channels
    which are configured in output.
    0: OC and OCN outputs are disabled or forced to idle state.
    1: OC and OCN outputs are enabled if their respective enable bits are set (CCxE, CCxNE in
    TIMx_CCER register). */
  TIM1->BDTR |= TIM_BDTR_MOE;

/****** Configure TIM1 ********/

/*  ARR[15:0]: Auto-reload value
    ARR is the value to be loaded into the actual auto-reload register.
    Refer to the Section 16.3.1: Time-base unit for more details about ARR update and behavior.
    The counter is blocked while the auto-reload value is null. */
    TIM1->CR1 &= ~TIM_CR1_CEN; // Disable TIM1

/*  Set the prescaler value to achieve the desired PWM frequency
    PSC[15:0]: Prescaler value
    The counter clock frequency CK_CNT is equal to fCK_PSC / (PSC[15:0] + 1).
    PSC contains the value to be loaded in the active prescaler register at each update event
    (including when the counter is cleared through UG bit of TIMx_EGR register or through
    trigger controller when configured in “reset mode”). */
    TIM1->PSC = PRESCALER; // Divide the 72 MHz clock by 8

/*  Set the auto-reload value to determine the PWM period
    ARR[15:0]: Auto-reload value. ARR is the value to be loaded into the actual 
    auto-reload register. The counter is blocked while the auto-reload value is null. */
    TIM1->ARR = PERIOD - 1;

  // Configure TIM1 channel 1 in PWM mode 1
  TIM1->CCMR1 &= ~TIM_CCMR1_CC1S;
  TIM1->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;
  TIM1->CCER &= ~TIM_CCER_CC1P; // Set polarity to active high

  // Set the duty cycle of TIM1 channel 1
  TIM1->CCR1 = DUTY;

/*  CC1E: Capture/Compare 1 output enable.
    CC1 channel configured as output:
    0: Off - OC1 is not active.
    1: On - OC1 signal is output on the corresponding output pin. */
    TIM1->CCER |= TIM_CCER_CC1E; 

/*  CEN: Counter enable
    0: Counter disabled
    1: Counter enabled
    CEN is cleared automatically in one-pulse mode, when an update event occurs */

  TIM1->CR1 |= TIM_CR1_CEN;

  while(true)
  {
    for( d = 1 ; d < PERIOD ; ++d)
    {
      TIM1->CCR1 = d;
      delayMicroseconds(300);
    }
    for(  ; d > 0 ; --d)
    {
      TIM1->CCR1 = d;
      delayMicroseconds(300);
    }  
  }
}

void loop()
{ 
}

dannyf
Posts: 446
Joined: Sat Jul 04, 2020 7:46 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by dannyf »

Good progress.

May want to separate the code pieces, one to initiate the pwm, one to set the period, one to set the duty cycle and one to enable the pin output, ...

That makes the code much more reusable - the only reason we write code now is so we don't write code in the future.
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Sure dannyf, I am never so untidy and styleless. You are right. It is just that I always like to understand each piece of code and in this case of the STM32, bare metal is what best fits my personal story with micros. This structure is dense and takes some time to get used to the many fine functions in a peripheral register.

Today I will sit down with your code. Of course flexible coding allows you not to think things over.

Tell me, how to attach an overflow interrupt to this same code?

I tend to think in just one bit to enable OVF IRQ and fill a vector with the handler function address. As I can see, in STM32 there is always some more, not always apparently related. I am asking to see if I can make it work during this weekend :)
RobertoBerner
Posts: 36
Joined: Tue May 09, 2023 10:45 pm

Re: TIM2 PWM newbie needs help with bare metal code

Post by RobertoBerner »

Hi dear friends, I started a new post trying to add a frustrating overflow interrupt to my PWM TIM1 generated signal.
Right here:
Thank you for you patient help.
Roberto
Post Reply

Return to “General discussion”