STM32 Timer Essentials with HAL: Modes, PWM, and Input Capture
Timer capabilities at a glance
- 16-bit counter with up, down, and center-aligned (up/down) counting modes and auto-reload
- 16-bit prescaler
- Multiple independent channels per timer supporting:
- Input capture
- Output compare
- PWM generation
- One-pulse mode
- Advanced-control timers can generate complementary outputs
- Interrupt/DMA events include:
- Update events (counter overflow/underflow, re-initialization)
- Trigger events (start/stop/initialize via internal/external triggers)
- Capture/Compare events (input capture, output compare)
Note: The timer’s input clock is the APB timer clock. On many STM32 families, if the APB prescaler is greater than 1, the timer clock is 2× APB frequency; otherwise it equals the APB frequency.
1. Counter modes and base timing
Counting modes
- Up-counting: CNT increments from 0 to ARR, generates an update event, then rolls over to 0
- Down-counting: CNT decrements from ARR to 0, generates an update event, then reloads ARR
- Center-aligned (up/down): CNT counts 0 → ARR−1 (event) then ARR−1 → 1 (event) then repeats
Overflow period calculation
Time per update event:
T = (ARR + 1) × (PSC + 1) / Ftim
- ARR: Auto-reload value
- PSC: Prescaler value
- Ftim: Timer input clock (after APB and x2 rules)
Example: Ftim = 84 MHz, PSC = 8399, ARR = 999
T = (999+1) × (8399+1) / 84,000,000 = 0.1 s (100 ms)
CubeMX pointers
- Ensure the timer is on APB1 or APB2 as intended and set the corresponding clock tree
- Clock source: Internal Clock
- Base configuraton fields:
- Prescaler (PSC, 16-bit)
- Counter Mode: Up, Down, or Center Aligned
- Auto-Reload (ARR, 16-bit)
- Auto-reload preload: enable if you want buffered ARR updates
- Enable Update interrupt and set NVIC priority (lower number = higher priority)
Minimal HAL example (base timer interrupt)
Start the base timer interrupt and toggle a GPIO in the update callback.
// Start timer update interrupt
auto status = HAL_TIM_Base_Start_IT(&htim3);
// Update event (overflow/underflow) callback
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
2. PWM generation
PWM (Pulse-Width Modulation) modulates the duty cycle of a periodic waveform. LEDs and motors smooth out the rapid on/off transitions, yielding perceived analog behavior.
Frequency basics
- Frequency f (Hz) = 1 / period (s)
- 50 Hz → 20 ms period (e.g., common servo frame rate)
- 1 kHz → 1 ms period; 10 kHz → 100 µs period
PWM timing math
- Timer tick frequency Ftick = Ftim / (PSC + 1)
- PWM period = (ARR + 1) / Ftick
- PWM frequency Fpwm = Ftick / (ARR + 1) = Ftim / ((PSC + 1) × (ARR + 1))
- Duty cycle for a channel is set by CCRx in [0, ARR]
- PWM modes:
- Mode 1: output active while CNT < CCRx
- Mode 2: output inactive while CNT < CCRx
Example: Ftim = 72 MHz, PSC = 71, ARR = 999 → Ftick = 1 MHz, Fpwm = 1 MHz / 1000 = 1 kHz
CubeMX configuration
- Clock source: Internal
- Enable a PWM channel (e.g., TIM3 CH2)
- Base:
- Prescaler: 71 (for 1 MHz tick at 72 MHz Ftim)
- Counter Period: 999 (for 1 kHz PWM)
- Channel:
- PWM Mode 1 or 2
- Polarity: High or Low
HAL code example
Start PWM and ramp the duty cycle.
// Enable PWM output on TIM3_CH2
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
// Set initial duty (e.g., 10%)
uint32_t period = __HAL_TIM_GET_AUTORELOAD(&htim3); // ARR
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, (period + 1) / 10);
// Simple breathing effect
static uint32_t duty = 0;
static int32_t step = 1; // increase/decrease
for (;;)
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, duty);
if ((step > 0 && duty >= period) || (step < 0 && duty == 0))
{
step = -step;
}
duty += step;
HAL_Delay(5);
}
3. Input capture
Input capture latches the counter value on an input edge. It is commonly used to measure pulse widths (high or low duration) and periods (time between successive edges).
Key options:
- Polarity: rising, falling, or both (edge selection)
- Filter: digital filtering to suppress spurious edges; samples at fDTS and requires consecutive qualified samples
- Channel mappinng: cofnigure which TIx feeds ICy (direct/indirect/combined)
- Prescaler: capture every 1st, 2nd, 4th, or 8th qualifying edge
CubeMX configuration example
- TIM4 Channel 1 as Input Capture (PB6 on many STM32F1 parts)
- Internal Clock
- IC selection: Direct
- Pull-up/down according to your circuitry
- Polarity: start with Falling (to measure low pulse width)
- Enable Update interrupt and IC interrupt
- Base timing: set PSC for 1 µs resolution
- Example: Ftim = 72 MHz → PSC = 71 → Ftick = 1 MHz (1 tick = 1 µs)
- ARR = 0xFFFF (max window ≈ 65,536 µs per wrap)
HAL code example: measure low-level duration (falling→rising)
This example resets the counter on the falling edge, counts with 1 µs resolution, and captures the elapsed time on the next rising edge, accounting for counter overflows.
typedef struct {
volatile uint8_t measuring; // 0 = waiting for falling, 1 = waiting for rising
volatile uint8_t ready; // 1 when a measurement is ready
volatile uint32_t wraps; // number of CNT overflows during measurement
volatile uint32_t ccr; // captured CCR value at rising edge
} PulseMeter;
static PulseMeter g_pulse = {0};
// Start base timer and input capture with interrupts
void IC_Start(void)
{
HAL_TIM_Base_Start_IT(&htim4);
// Begin by detecting a falling edge (start of low pulse)
__HAL_TIM_SET_CAPTUREPOLARITY(&htim4, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1);
}
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM4 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
if (!g_pulse.measuring)
{
// Falling edge: reset counter and prepare to latch on rising edge
__HAL_TIM_SET_COUNTER(htim, 0);
g_pulse.wraps = 0;
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
g_pulse.measuring = 1;
}
else
{
// Rising edge: capture CCR and mark ready
g_pulse.ccr = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
g_pulse.ready = 1;
// Re-arm for next measurement (falling edge)
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
g_pulse.measuring = 0;
}
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM4 && g_pulse.measuring)
{
// Count timer wraps that occur while waiting for the rising edge
g_pulse.wraps++;
}
}
// Poll the result in your main loop when ready
void PollPulseWidthUs(void)
{
if (g_pulse.ready)
{
uint32_t ticksPerWrap = __HAL_TIM_GET_AUTORELOAD(&htim4) + 1; // ARR + 1
uint32_t totalTicks = g_pulse.wraps * ticksPerWrap + g_pulse.ccr;
// With 1 MHz timer tick, totalTicks equals microseconds directly
printf("Low pulse width: %lu us\n", (unsigned long)totalTicks);
g_pulse.ready = 0;
}
}