STM32 Timer Input Capture: Measuring Frequency and Duty Cycle at Register Level
Measuring an external signal's frequency and duty cycle is one of the most common embedded tasks — RPM sensors, PWM feedback lines, encoder pulses, and protocol timing debug all rely on accurate period capture. The STM32 timer input capture mode lets you timestamp edges with timer-resolution accuracy. This article walks through the register-level configuration, covers the dual-channel method for simultaneous frequency and duty-cycle measurement, and addresses the overflow handling that every production firmware needs.
How input capture works
Every STM32 general-purpose timer (TIM2–TIM5, TIM9–TIM17 on F4; TIM2–TIM8, TIM15–TIM17 on G4) has capture/compare channels. In input capture mode, the channel detects a programmed edge on its input pin (TI1, TI2) and latches the free-running counter value into the capture register (CCR). The core sequence:
- The timer counter TIMx_CNT runs freely, typically at the timer clock frequency divided by the PSC prescaler.
- An edge on the input pin triggers a capture: TIMx_CCRx receives the current CNT value.
- The CCxIF flag in TIMx_SR asserts; an interrupt or DMA request can be generated.
- By reading successive captures and computing the difference, you derive the signal period and pulse width.
The input path includes programmable filtering, edge polarity selection, and optional prescaling (every 2nd, 4th, or 8th edge). These are configured in TIMx_CCMR1 (for channels 1, 2) or TIMx_CCMR2 (for channels 3, 4).
Single-channel measurement: frequency only
With one channel configured on the rising edge, each capture gives a timestamp of consecutive rising edges. The difference between two successive captures is the signal period in timer ticks. The frequency is:
f_signal = timer_clock / (PSC + 1) / period_ticks
This works well for stable signals but cannot measure duty cycle. The timer must be configured so that its counter does not overflow between two edges — otherwise the period calculation is garbage.
Register configuration: TIM2 channel 1, rising edge, STM32F401
// TIM2 clock: APB1 timer clock = 84 MHz (if APB1 prescaler != 1, timer clock = 2 * APB1)
// PSC = 83 → counter clock = 84 MHz / 84 = 1 MHz (1 μs per tick)
// ARR = 0xFFFF → 16-bit period, overflow at ~65.5 ms
TIM2->PSC = 83; // Prescaler: 84 MHz → 1 MHz
TIM2->ARR = 0xFFFF; // Auto-reload max (16-bit)
// CCMR1: CC1 channel configured as input, mapped on TI1, no prescaler, no filter
TIM2->CCMR1 = TIM_CCMR1_CC1S_0; // CC1S = 01: channel as input, IC1 mapped on TI1
// CCER: CC1 active on rising edge, enable capture
TIM2->CCER = TIM_CCER_CC1E; // CC1E=1, CC1P=0 (rising edge)
// Enable CC1 interrupt
TIM2->DIER = TIM_DIER_CC1IE;
// Enable counter
TIM2->CR1 = TIM_CR1_CEN;
In the interrupt handler, read TIM2->CCR1 and compute the delta with the previous capture:
uint32_t prev_capture = 0;
uint32_t period_ticks = 0;
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_CC1IF) {
TIM2->SR = ~TIM_SR_CC1IF;
uint32_t now = TIM2->CCR1;
period_ticks = (now >= prev_capture)
? (now - prev_capture)
: (0xFFFF - prev_capture + now + 1);
prev_capture = now;
}
}
Dual-channel measurement: frequency and duty cycle
To measure both frequency and duty cycle, you need two captures per period: one on the rising edge (timestamp A), one on the falling edge (timestamp B). The high time = B − A, period = next_rising − A. The STM32 timer architecture supports this directly through two channels on the same input:
- Channel 1 (IC1): mapped to TI1, rising edge — captures the start of the high pulse.
- Channel 2 (IC2): mapped to TI1, falling edge — captures the end of the high pulse.
This is configured via TI1FP1 (rising) and TI1FP2 (falling) in the CCMR1 register. Both channels share the same input pin, but each has its own edge polarity in CCER.
Register configuration: TIM2 channels 1 + 2, same TI1 input
// Same PSC and ARR as above
TIM2->PSC = 83; // 1 μs per tick
TIM2->ARR = 0xFFFF;
// CCMR1: IC1 on TI1 (rising), IC2 on TI1 (falling) — TI1FP1 and TI1FP2
TIM2->CCMR1 = TIM_CCMR1_CC1S_0 // CC1S=01: IC1 on TI1
| TIM_CCMR1_CC2S_0; // CC2S=01: IC2 on TI1
// CCER: CC1 rising, CC2 falling
TIM2->CCER = TIM_CCER_CC1E // CC1 rising, enable
| TIM_CCER_CC2E // CC2 enable
| TIM_CCER_CC2P; // CC2 falling edge
// Enable both capture interrupts
TIM2->DIER = TIM_DIER_CC1IE | TIM_DIER_CC2IE;
TIM2->CR1 = TIM_CR1_CEN;
ISR with duty-cycle computation
typedef struct {
uint32_t period_ticks;
uint32_t high_ticks;
float frequency_hz;
float duty_cycle_pct;
} input_capture_result_t;
static volatile input_capture_result_t result = {0};
static uint32_t rising_a = 0, rising_b = 0;
static uint32_t falling = 0;
static uint8_t edge_seq = 0; // 0 = wait rising, 1 = wait falling
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_CC1IF) { // Rising edge
TIM2->SR = ~TIM_SR_CC1IF;
rising_b = TIM2->CCR1;
if (edge_seq == 0) {
rising_a = rising_b;
edge_seq = 1; // Now wait for falling
} else {
// Complete period: rising_a → falling → rising_b
uint32_t period;
if (rising_b >= rising_a)
period = rising_b - rising_a;
else
period = (0xFFFF - rising_a) + rising_b + 1;
uint32_t high;
if (falling >= rising_a)
high = falling - rising_a;
else
high = (0xFFFF - rising_a) + falling + 1;
result.period_ticks = period;
result.high_ticks = high;
result.frequency_hz = 1000000.0f / period; // 1 MHz tick
result.duty_cycle_pct = (float)high / period * 100.0f;
rising_a = rising_b;
edge_seq = 1; // Continue tracking
}
}
if (TIM2->SR & TIM_SR_CC2IF) { // Falling edge
TIM2->SR = ~TIM_SR_CC2IF;
falling = TIM2->CCR2;
}
}
This gives you a running measurement updated once per period. The 16-bit timer limits measurable periods to ~65.5 ms at 1 MHz tick rate. For slower signals, reduce PSC or use a 32-bit timer like TIM2 or TIM5 on F4 series.
Handling timer overflow (16-bit rollover)
The 16-bit general-purpose timers (TIM3, TIM4 on F4) overflow every 65536 ticks. If the signal period exceeds this, the delta calculation breaks. The standard solution is to count overflows in the update interrupt and extend the captured value to 32 bits:
static volatile uint32_t overflow_count = 0;
void TIM3_IRQHandler(void) {
if (TIM3->SR & TIM_SR_UIF) { // Update (overflow)
TIM3->SR = ~TIM_SR_UIF;
overflow_count++;
}
}
// In the capture ISR, extend to 32 bits:
uint32_t now_ext = (overflow_count << 16) | TIM3->CCR1;
This technique works up to (2^32 − 1) / timer_clock seconds — at 1 MHz that is about 4295 seconds. For submicrosecond precision, use a 32-bit timer (TIM2 or TIM5) directly, which eliminates the overflow tracking overhead.
Input filtering and noise rejection
Real signals glitch. The STM32 timer input has a programmable digital filter on the TIx input, configured in the ICxF bits of CCMR1/CCMR2. The filter samples the input at f_DTS (usually the timer clock) and asserts a validated edge only after N consecutive samples match:
- ICF = 0b0000: no filter — edge captured on first detected transition. Fastest, but noise on the input produces false captures.
- ICF = 0b0011: N = 4 samples at f_DTS — rejects short glitches below 4 timer clocks.
- ICF = 0b1001–1111: N = 8 samples at f_DTS/8 (slower sampling, more rejection).
For a typical PWM feedback line on a motor controller board, I use ICF = 4 or 5 (N = 6–8 at f_DTS) which rejects contact bounce and switching noise without introducing more than a few microseconds of delay.
// CCMR1 with IC1 filter = 6 samples at f_DTS
TIM2->CCMR1 = TIM_CCMR1_CC1S_0
| TIM_CCMR1_IC1F_2 // IC1F = 100 → N=8 samples
| TIM_CCMR1_CC2S_0
| TIM_CCMR1_IC2F_2; // IC2F = 100 → same filter on IC2
Practical example: measuring a servo PWM signal on STM32F401
A standard RC servo PWM has a 50 Hz base frequency (20 ms period) with a pulse width of 1–2 ms. With PSC = 83 and 84 MHz timer clock, the tick is 1 MHz:
- Expected period: 20 ms = 20000 ticks
- Pulse width range: 1000–2000 ticks (1–2 ms)
- 16-bit TIM3 is sufficient (max 65535 ticks)
The ISR from the dual-channel example above produces:
Period: 20000 ticks → 50.00 Hz
High: 1500 ticks → 7.50% duty → 1.50 ms pulse
I verify this on my bench by feeding the PWM from a second timer output back to the capture input. This loopback test catches configuration bugs before connecting real hardware.
Common pitfalls
Wrong timer clock frequency
The timer clock is not always the same as SYSCLK. On STM32F4, if APB1 prescaler is not 1, timers on APB1 (TIM2–TIM7) run at 2 × APB1 clock. A common bug: assuming TIM2 runs at 42 MHz when APB1 is 42 MHz with prescaler > 1 — it actually runs at 84 MHz. Always read RCC->CFGR and compute the actual timer clock before setting PSC.
Channel remapping (TI1 vs TI2)
By default, channel 1 captures from TI1 (TIMx_CH1 pin) and channel 2 from TI2. The dual-channel trick (IC2 on TI1) works through the TI1FP2 routing, but only if CCMR1 CC2S = 01. If you set CC2S = 10 or 11, channel 2 switches to TI2 or TRC (internal trigger). This is the most common register-level mistake in dual input capture.
16-bit overflow on slow signals
A 1 Hz signal at 1 MHz tick rate needs 1000000 ticks — impossible with a 16-bit timer without overflow tracking. Either increase PSC to lower the tick rate (trading resolution for range) or use a 32-bit timer. On F4, TIM2 and TIM5 are 32-bit. On G4, only TIM2 is 32-bit (TIM5 is gone).
Missing edge sequence
The dual-channel ISR above assumes a rising edge arrives first. If the signal starts low, the first edge is rising — fine. If the board is connected mid-cycle and the signal is high, the first edge seen is falling, and the state machine computes garbage. Add a startup synchronisation: ignore the first 2–3 captures, or initialise edge_seq after confirming both channels have fired at least once.
Practical checklist
- ☐ Timer clock frequency verified from APB prescaler values (not assumed from SYSCLK).
- ☐ PSC chosen so the maximum expected signal period fits within the timer ARR.
- ☐ CCMR1 CCxS bits correctly set: CC1S=01 for IC1 on TI1, CC2S=01 for IC2 on TI1.
- ☐ CCER edge polarity correct: CC1P=0 (rising), CC2P=1 (falling).
- ☐ Input filter (ICxF) set to reject expected noise without introducing measurement delay.
- ☐ Overflow ISR enabled and overflow counter read in the capture ISR if using 16-bit timers.
- ☐ Edge sequence synchronisation: discard the first 2–3 captures or initialise after both edges seen.
- ☐ GPIO alternate function configured correctly for the timer channel pin in AF mode.
- ☐ Loopback test: feed a known PWM from another timer output to verify capture accuracy.
How I would approach this on a client project
On a production firmware project, I never embed the input capture logic directly in the timer ISR with global variables. Instead I write a dedicated measurement module with a state machine, a result queue, and a configuration structure:
typedef struct {
TIM_TypeDef *tim;
uint8_t channel; // 1 or 2 for single, 3 for dual
uint32_t psc;
uint16_t arr;
uint8_t ic_filter; // ICF value
uint8_t edge; // RISING, FALLING, or BOTH (dual)
} input_capture_config_t;
typedef struct {
float frequency_hz;
float duty_cycle_pct;
uint32_t timestamp_ms;
bool valid;
} measurement_t;
int8_t ic_start(const input_capture_config_t *cfg);
int8_t ic_read(measurement_t *out, uint32_t timeout_ms);
The module uses a ring buffer of measurements so the application can poll at its own pace without losing data. The ISR is minimal — it writes captures to the ring and advances the edge state — while the business logic (frequency and duty computation) runs in the application context. This separation makes the module reusable across projects and testable with synthetic captures.
The configuration structure lives with the board's bsp_timers.h file. When the client changes the signal source (different encoder, different PWM frequency), they edit the configuration table, not the ISR logic — no regressions, no copy-paste mistakes.
Sources and further reading
- STM32F401 Reference Manual (RM0368) — Chapter 14: General-purpose timers (TIM2–TIM5). Input capture mode description and register map.
- STM32G4 Reference Manual (RM0440) — Chapter 24: Timer overview with HRTIM integration.
- ST Application Note AN4776 — General-purpose timer cookbook for STM32 microcontrollers. Practical recipes for input capture, output compare, PWM, and one-pulse mode.
- ST Application Note AN4013 — STM32 cross-series timer overview. Timer feature differences across STM32 families.
- ST Application Note AN4300 — Using STM32 timer input capture to measure frequency and duty cycle.
- STM32CubeF4 firmware package — TIM_InputCapture example in Projects/STM32F401RE-Nucleo/Examples/TIM/.

Comments
Have comments? Send me an email.