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 width | CCR | Duty |
|---|---|---|
| 1.0 ms (one end) | 1000 | 5 percent |
| 1.5 ms (centre) | 1500 | 7.5 percent |
| 2.0 ms (other end) | 2000 | 10 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.
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?