gwordal

Lesson 3 of 5 · 24 min

Timers and PWM

A timer is the first peripheral that makes a microcontroller feel different from a program. It is a counter that runs on its own, driven by the clock, with no CPU involvement. Point its comparator at a pin and you get PWM with perfect timing even while your code does something else. On the NUCLEO-L476RG we use TIM2, a 32-bit general-purpose timer, with the CPU running at 80 MHz.

How a timer counts

A timer has three numbers that matter:

  • PSC, the prescaler. It divides the input clock before it reaches the counter.
  • ARR, the auto-reload register. The counter CNT counts up from 0 to ARR, then wraps to 0. This wrap is the update event.
  • CCR, a capture/compare register for each channel. When CNT reaches CCR, the channel output changes state.

The counter ticks at f_clk / (PSC + 1). The + 1 is there because a register value of 0 has to mean "divide by 1", not "stop". Counting from 0 to ARR is ARR + 1 steps, so the update frequency is:

f = f_clk / ((PSC + 1) x (ARR + 1))

On the L476 the APB1 bus is not divided when SYSCLK is 80 MHz, so TIM2 receives f_clk = 80 MHz. (On the F103, APB1 is limited to 36 MHz, but timers on it are clocked at double that, 72 MHz, because of a hardware rule: whenever the bus prescaler is not 1, the timer clock is twice the bus clock. Always check the clock tree for your chip.)

Worked example 1: a 1 kHz PWM

The target is 1 kHz, a period of 1 ms. First pick PSC so the tick is a round number. PSC = 79 divides by 80, giving 80 MHz / 80 = 1 MHz, a 1 microsecond tick. Then ARR + 1 = 1 MHz / 1 kHz = 1000, so ARR = 999. Check:

f = 80,000,000 / (80 x 1000) = 1000 Hz

The period is 1000 ticks, so the duty cycle in steps of 0.1 percent comes straight from CCR.

Worked example 2: a 50 Hz servo signal

A hobby servo expects a pulse every 20 ms (50 Hz), with a pulse width of 1 to 2 ms encoding the angle (1.5 ms is centre). Keep PSC = 79 so the tick is still 1 microsecond. Then ARR + 1 = 20 ms / 1 microsecond = 20000, so ARR = 19999:

f = 80,000,000 / (80 x 20000) = 50 Hz

Now CCR is the pulse width in microseconds, directly:

Pulse widthCCRDuty
1.0 ms (one end)10005 percent
1.5 ms (centre)15007.5 percent
2.0 ms (other end)200010 percent

Between 1000 and 2000 there are 1000 distinct positions. Spread over a typical 180 degree servo, that is a resolution of 0.18 degrees per step, finer than the servo can mechanically resolve.

Why not PSC = 0 for maximum resolution? On a 16-bit timer such as TIM3, ARR cannot exceed 65535. For 50 Hz you need ARR + 1 = 80 MHz / (50 x (PSC + 1)) at most 65536, so PSC must be at least 24. TIM2 is 32-bit and would allow PSC = 0, but a round 1 microsecond tick makes the numbers easy to check by hand, which is worth more than resolution you cannot use.

PWM duty from CCR

In PWM mode 1 the output is high while CNT is below CCR and low from CCR until the wrap. Therefore:

duty = CCR / (ARR + 1)

For the 1 kHz example, CCR = 250 gives 250 / 1000 = 25 percent, and CCR = 500 gives 50 percent. Average output voltage on a 3.3 V pin is duty x 3.3 V, so 25 percent is 0.825 V averaged. Changing duty means changing CCR only; the frequency stays untouched.

5V0Vaverage 1.25V
The pin is only ever fully on or fully off. The motor or LED reacts to the average.

Registers: PWM on PA0

TIM2 channel 1 appears on pin PA0 as alternate function 1 (the Arduino A0 header on the Nucleo). The alternate function number comes from the datasheet's pin table.

// Clocks: GPIOA and TIM2
RCC->AHB2ENR  |= RCC_AHB2ENR_GPIOAEN;
RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN;

// PA0 to alternate function mode (10), AF1
GPIOA->MODER = (GPIOA->MODER & ~(3u << 0)) | (2u << 0);
GPIOA->AFR[0] = (GPIOA->AFR[0] & ~(0xFu << 0)) | (1u << 0);

// Time base: 1 kHz
TIM2->PSC = 79;                 // 80 MHz / 80 = 1 MHz tick
TIM2->ARR = 999;                // 1000 ticks per period
TIM2->CCR1 = 250;               // 25 percent duty

// Channel 1: PWM mode 1 (110) with preload so CCR changes apply cleanly
TIM2->CCMR1 = (6u << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE;
TIM2->CCER |= TIM_CCER_CC1E;    // connect output to the pin

TIM2->EGR = TIM_EGR_UG;         // force an update to load PSC and ARR
TIM2->CR1 |= TIM_CR1_CEN;       // start counting

For the servo, keep PSC = 79 and change only ARR = 19999 and CCR1 = 1500. The EGR line matters: PSC and ARR are buffered, and without an update event the new values do not take effect until the first natural wrap.

With HAL the equivalent is HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1) after CubeMX has filled the prescaler and period, and later __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500) to move the servo.

Check yourself

f_clk = 80 MHz, PSC = 39, and you want 2 kHz. What ARR do you need?

Check yourself

With ARR = 999, which CCR gives a 30 percent PWM duty?