Back to Technology

STM32 Part 4: Timers, PWM & Input Capture

March 31, 2026 Wasil Zafar 28 min read

From SysTick millisecond timing to 8-channel complementary PWM and high-speed input capture — a complete guide to every STM32 timer mode used in real production firmware.

Table of Contents

  1. STM32 Timer Architecture
  2. Time Base & Delay
  3. PWM Generation
  4. Input Capture
  5. PWM Input Mode
  6. Encoder Interface
  7. Output Compare & One-Pulse Mode
  8. Exercises
  9. Timer Configuration Tool
  10. Conclusion & Next Steps
Series Context: This is Part 4 of the 18-part STM32 Unleashed series. In Part 3 we mastered UART communication. Here we tackle the STM32 timer subsystem — the beating heart of real-time embedded systems, controlling everything from LED brightness to motor drives.

STM32 Unleashed: HAL Driver Development

Your 18-step learning path • Currently on Step 4
1
Architecture & CubeMX Setup
STM32 family, clock tree, HAL vs LL, CubeMX workflow, first project
Completed
2
GPIO & Button Debounce
GPIO modes, pull-up/down, EXTI, software debounce, HAL_GPIO_ReadPin
Completed
3
UART Communication
Polling, interrupt, DMA modes, printf retargeting, ring buffers
Completed
4
Timers, PWM & Input Capture
TIM basics, PWM generation, input capture, encoder mode
You Are Here
5
ADC & DAC
Single/continuous conversion, DMA, injected channels, DAC waveforms
6
SPI Protocol
SPI master/slave, full-duplex, DMA transfers, sensor drivers
7
I2C Protocol
I2C master, 7/10-bit addressing, DMA, multi-master, error handling
8
DMA & Memory Efficiency
DMA streams, circular mode, memory-to-memory, zero-copy patterns
9
Interrupt Management & NVIC
Priority grouping, preemption, ISR design, HAL callbacks, latency
10
Low-Power Modes
Sleep, Stop, Standby modes, RTC wakeup, LP UART, power profiling
11
RTC & Calendar
RTC configuration, alarms, backup registers, calendar subseconds
12
CAN Bus
FDCAN/bxCAN, filters, message frames, error handling, automotive use
13
USB CDC Virtual COM Port
USB FS/HS, CDC class, virtual serial, control transfers, descriptors
14
FreeRTOS Integration
Tasks, queues, semaphores, mutexes, CMSIS-RTOS2 wrapper, stack sizing
15
Bootloader Development
Custom IAP bootloader, UART/USB DFU, flash programming, jump-to-app
16
External Storage: SD & QSPI Flash
FATFS on SD card, QSPI NOR flash, memory-mapped execution, wear levelling
17
Ethernet & TCP/IP Stack
LwIP integration, DHCP, TCP server, HTTP, MQTT, Ethernet DMA descriptors
18
Production Readiness
Watchdog, HardFault handler, flash option bytes, code signing, CI/CD

STM32 Timer Architecture

The STM32 timer subsystem is one of the most powerful — and most misunderstood — parts of the microcontroller. On an STM32F407, you have up to 14 timers of three distinct types, each with its own bus connection, channel count, and capability set. Understanding which timer to reach for and how to configure it correctly separates reliable embedded firmware from code that works by accident.

Basic, General-Purpose & Advanced Timers

STM32 timers are grouped into three families. Basic timers (TIM6, TIM7) are minimal — they have a counter, prescaler, auto-reload register, and an update interrupt. They are ideal for simple periodic interrupts, DAC triggering, and generating a timebase without consuming a general-purpose timer. General-purpose timers (TIM2–TIM5, TIM9–TIM14) add capture/compare channels, PWM generation, input capture, encoder mode, and output compare. Advanced timers (TIM1, TIM8) are supersets of general-purpose timers and add complementary outputs with programmable deadtime insertion, break inputs for safety shutdown, and repetition counters — essential for motor control H-bridge drive.

Key Timer Registers

Before using the HAL you must understand what it is writing. The following registers control timer behaviour:

  • CNT — the current counter value. Counts up (or down in centre-aligned mode) on every timer clock edge after prescaler division.
  • PSC — prescaler. Divides the timer input clock by (PSC+1). Write 0 for no division; write 83 to get 1 MHz from an 84 MHz clock.
  • ARR — auto-reload register. The counter resets to 0 (upcounting) when it reaches ARR, generating an update event. ARR+1 equals the period in timer clock ticks.
  • CCR1–CCR4 — capture/compare registers. In PWM mode, the output is high (Mode 1) while CNT < CCR. In input capture mode, hardware writes the current CNT value here on the selected edge.
  • CR1 — control register 1. Bits CEN (counter enable), DIR (direction), CMS (centre-aligned), ARPE (ARR preload enable), OPM (one-pulse mode).
  • DIER — DMA/interrupt enable register. UIE enables the update interrupt; CCxIE enables capture/compare interrupt on channel x.
  • SR — status register. UIF is the update interrupt flag; CCxIF is the capture/compare flag. Must be cleared in the ISR.
  • CCMR1/CCMR2 — capture/compare mode registers. Set OCxM bits for PWM Mode 1 (110) or Mode 2 (111); set CCxS bits to select input vs output direction.

Clock Sources & Frequency Formula

On the STM32F4, timers on APB1 (TIM2–TIM7, TIM12–TIM14) receive a clock of 2 × PCLK1 when the APB1 prescaler is anything other than 1. With PCLK1 = 42 MHz and APB1 prescaler = 2, the timer clock is 84 MHz. Timers on APB2 (TIM1, TIM8–TIM11) similarly receive 2 × PCLK2 when the APB2 prescaler ≠ 1. This timer clock doubling rule is a frequent source of confusion — always verify your CubeMX clock tree before calculating prescaler values.

The fundamental frequency formula for a timer update event is:

F_update = TIMx_CLK / ((PSC + 1) × (ARR + 1))

To generate a 1 Hz update interrupt on TIM6 with an 84 MHz timer clock: PSC = 8399, ARR = 9999 gives 84,000,000 / (8400 × 10000) = 1 Hz exactly.

Timer Bus (F4) Channels Resolution Complementary Encoder Advanced Features
TIM1 APB2 4 (+ 4N) 16-bit Yes (CH1N–CH3N) Yes Break input, deadtime, rep. counter
TIM2 APB1 4 32-bit No Yes Long period measurements
TIM3 APB1 4 16-bit No Yes
TIM4 APB1 4 16-bit No Yes
TIM5 APB1 4 32-bit No Yes Long period measurements
TIM6 APB1 0 (basic) 16-bit No No DAC trigger source
TIM7 APB1 0 (basic) 16-bit No No DAC trigger source
TIM8 APB2 4 (+ 4N) 16-bit Yes (CH1N–CH3N) Yes Break input, deadtime, rep. counter
TIM9–14 APB1/2 1–2 16-bit No No Lightweight IC/OC

The following code initialises TIM6 for a 1 Hz update interrupt — the simplest possible timer use case and the foundation for understanding the HAL timer API:

/* TIM6 1 Hz Update Interrupt — Basic Time-Base Example
 * Timer clock: 84 MHz (APB1 × 2, APB1 prescaler = 2)
 * PSC = 8399  → 84 MHz / 8400 = 10 kHz timer clock
 * ARR = 9999  → 10 kHz / 10000 = 1 Hz update event
 */

TIM_HandleTypeDef htim6;

void TIM6_Init(void)
{
    __HAL_RCC_TIM6_CLK_ENABLE();

    htim6.Instance               = TIM6;
    htim6.Init.Prescaler         = 8399;          /* 84 MHz / 8400 = 10 kHz  */
    htim6.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim6.Init.Period            = 9999;           /* 10 kHz / 10000 = 1 Hz   */
    htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

    if (HAL_TIM_Base_Init(&htim6) != HAL_OK) {
        Error_Handler();
    }

    HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
}

/* Start the timer with update interrupt enabled */
void TIM6_Start(void)
{
    HAL_TIM_Base_Start_IT(&htim6);
}

/* IRQ handler — defined in stm32f4xx_it.c */
void TIM6_DAC_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim6);
}

/* HAL callback — fires every 1 second */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);   /* Toggle LED every 1 s    */
    }
}

Time Base & Delay

HAL_Delay() is a blocking function: it burns CPU cycles in a while loop checking the SysTick counter. For most applications this is fine during startup, but in real firmware you almost never want to block the main loop or an ISR. The STM32 timer system offers multiple non-blocking approaches.

HAL_TIM_Base_Start_IT

After calling HAL_TIM_Base_Init(), you have two choices: HAL_TIM_Base_Start() runs the timer without interrupts (useful for using CNT as a free-running counter), and HAL_TIM_Base_Start_IT() enables the update interrupt so HAL_TIM_PeriodElapsedCallback() fires when ARR rolls over. Always enable the NVIC for the timer before calling Start_IT.

Non-Blocking Tick Comparison

TIM2 and TIM5 are 32-bit timers — unlike 16-bit timers, they can run for over 51 seconds at 84 MHz with no prescaler, or over 14 hours with PSC=83 (1 MHz tick). A free-running 1 MHz counter makes non-blocking delays trivial: record the counter value at the start of a wait, then poll until the difference exceeds the target without ever blocking.

DWT Cycle Counter as Microsecond Timer

The ARM Cortex-M4 Debug and Trace unit includes a Data Watchpoint and Trace (DWT) block. Its CYCCNT register counts every CPU clock cycle and is accessible from user code once the DEM_CR register enables it. At 168 MHz it overflows every ~25 seconds. Dividing the delta by the CPU clock frequency gives an elapsed time in microseconds — with no timer peripheral consumed at all.

/* Non-blocking 1 ms timebase via TIM6 update interrupt
 * Plus DWT microsecond counter on Cortex-M4
 */

#include "stm32f4xx_hal.h"

/* ── TIM6 non-blocking ms counter ─────────────────────────── */
static volatile uint32_t tim6_ms_ticks = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        tim6_ms_ticks++;
    }
}

uint32_t TIM6_GetTick(void)   { return tim6_ms_ticks; }

/* Non-blocking delay: returns 1 when elapsed, 0 otherwise */
uint8_t TIM6_DelayElapsed(uint32_t start_tick, uint32_t delay_ms)
{
    return ((TIM6_GetTick() - start_tick) >= delay_ms) ? 1 : 0;
}

/* Usage example — non-blocking LED toggle every 500 ms */
void App_Run(void)
{
    static uint32_t last_toggle = 0;

    if (TIM6_DelayElapsed(last_toggle, 500)) {
        last_toggle = TIM6_GetTick();
        HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
    }
    /* other tasks execute here without blocking */
}

/* ── DWT microsecond timer ─────────────────────────────────── */
void DWT_Init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;  /* Enable DWT             */
    DWT->CYCCNT       = 0;                             /* Reset cycle counter    */
    DWT->CTRL        |= DWT_CTRL_CYCCNTENA_Msk;       /* Start counting cycles  */
}

uint32_t DWT_GetMicros(void)
{
    /* SystemCoreClock is 168000000 on STM32F407 at max speed */
    return DWT->CYCCNT / (SystemCoreClock / 1000000U);
}

void DWT_DelayUs(uint32_t us)
{
    uint32_t start = DWT_GetMicros();
    while ((DWT_GetMicros() - start) < us) { /* spin */ }
}
Critical Note: DWT_CYCCNT is a 32-bit register. At 168 MHz it wraps every 25.6 seconds. The subtraction DWT_GetMicros() - start correctly handles wrap-around because unsigned integer arithmetic is modular, provided the delay is shorter than the wrap period.

PWM Generation

Pulse-Width Modulation is the most common timer application in embedded systems. STM32 timers can generate up to four independent PWM channels per timer (eight for TIM1/TIM8 with complementary outputs), all sharing the same period (ARR) but with independently configurable duty cycles (CCR values).

PWM Mode 1 vs Mode 2

In PWM Mode 1 (CCMR bits OCxM = 110): the output channel is active high while CNT < CCRx, and low while CNT ≥ CCRx. This is the standard mode for most applications. In PWM Mode 2 (OCxM = 111): the logic is inverted — active low while CNT < CCRx. Mode 2 is used when driving active-low loads or when you need an inverted signal without additional hardware.

Duty cycle in percentage = CCRx / (ARR + 1) × 100%. Setting CCRx = 0 gives 0% (always low in Mode 1), and CCRx = ARR + 1 gives 100% (always high). In practice, setting CCRx = ARR guarantees a 1-tick low pulse each period for zero-crossing detection in motor drives.

HAL PWM API

The PWM HAL requires three calls: HAL_TIM_PWM_Init() configures the time-base, HAL_TIM_PWM_ConfigChannel() sets the mode and initial CCR for each channel, and HAL_TIM_PWM_Start() enables the output. Runtime duty cycle changes use the macro __HAL_TIM_SET_COMPARE(&htim, TIM_CHANNEL_1, new_ccr) — this writes directly to the CCR shadow register and takes effect at the next update event if ARPE is enabled.

/* 20 kHz PWM on TIM3_CH1 (PA6) — runtime duty cycle sweep 0-100%
 * TIM3 clock = 84 MHz (APB1 × 2)
 * PSC = 0  → 84 MHz timer clock
 * ARR = 4199  → 84 MHz / 4200 = 20 kHz
 */

TIM_HandleTypeDef htim3;

void TIM3_PWM_Init(void)
{
    TIM_OC_InitTypeDef oc_config = {0};

    __HAL_RCC_TIM3_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* Configure PA6 as TIM3_CH1 alternate function */
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin       = GPIO_PIN_6;
    gpio.Mode      = GPIO_MODE_AF_PP;
    gpio.Pull      = GPIO_NOPULL;
    gpio.Speed     = GPIO_SPEED_FREQ_HIGH;
    gpio.Alternate = GPIO_AF2_TIM3;
    HAL_GPIO_Init(GPIOA, &gpio);

    /* Time-base: 20 kHz period */
    htim3.Instance               = TIM3;
    htim3.Init.Prescaler         = 0;
    htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim3.Init.Period            = 4199;           /* ARR: 20 kHz              */
    htim3.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim3);

    /* Channel 1: PWM Mode 1, 0% initial duty */
    oc_config.OCMode       = TIM_OCMODE_PWM1;
    oc_config.Pulse        = 0;                    /* CCR1 = 0 → 0% duty      */
    oc_config.OCPolarity   = TIM_OCPOLARITY_HIGH;
    oc_config.OCFastMode   = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim3, &oc_config, TIM_CHANNEL_1);

    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

/* Sweep duty cycle 0-100% in a 2-second ramp */
void TIM3_PWM_Sweep(void)
{
    static uint32_t last_tick = 0;
    static uint32_t step      = 0;

    if ((HAL_GetTick() - last_tick) >= 20) {       /* Update every 20 ms      */
        last_tick = HAL_GetTick();
        step = (step + 1) % 101;                   /* 0 to 100, then repeat   */

        uint32_t ccr = (step * 4200) / 100;        /* Scale to ARR+1=4200     */
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr);
    }
}

Complementary Outputs & Deadtime

TIM1 and TIM8 are the only STM32F4 timers with complementary outputs (TIMx_CH1N, CH2N, CH3N). These drive the low-side switches of an H-bridge or three-phase inverter. The critical addition is deadtime: a brief period where both the high-side and low-side outputs are forced low simultaneously, preventing shoot-through current that would destroy the power stage. Deadtime is set in the TIM_BreakDeadTimeConfigTypeDef structure via HAL_TIMEx_ConfigBreakDeadTime().

/* Complementary PWM on TIM1_CH1 / CH1N with 100 ns deadtime
 * TIM1 clock = 168 MHz (APB2 × 2, APB2 prescaler = 2)
 * PSC = 0, ARR = 8399 → 168 MHz / 8400 = 20 kHz
 * Deadtime unit = 168 MHz / 1 = ~5.95 ns per tick
 * DTG = 17 → 17 × 5.95 ns ≈ 101 ns deadtime
 */

TIM_HandleTypeDef        htim1;
TIM_OC_InitTypeDef       oc1;
TIM_BreakDeadTimeConfigTypeDef bdt;

void TIM1_ComplementaryPWM_Init(void)
{
    __HAL_RCC_TIM1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    /* PA8 = TIM1_CH1, PB13 = TIM1_CH1N */
    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_8; g.Mode = GPIO_MODE_AF_PP;
    g.Pull = GPIO_NOPULL; g.Speed = GPIO_SPEED_FREQ_HIGH;
    g.Alternate = GPIO_AF1_TIM1;
    HAL_GPIO_Init(GPIOA, &g);

    g.Pin = GPIO_PIN_13;
    HAL_GPIO_Init(GPIOB, &g);

    /* Time-base */
    htim1.Instance = TIM1;
    htim1.Init.Prescaler = 0;
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim1.Init.Period = 8399;
    htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim1);

    /* Channel 1: 50% duty */
    oc1.OCMode     = TIM_OCMODE_PWM1;
    oc1.Pulse      = 4200;                         /* 50% duty cycle           */
    oc1.OCPolarity = TIM_OCPOLARITY_HIGH;
    oc1.OCNPolarity= TIM_OCNPOLARITY_HIGH;
    oc1.OCFastMode = TIM_OCFAST_DISABLE;
    oc1.OCIdleState = TIM_OCIDLESTATE_RESET;
    oc1.OCNIdleState= TIM_OCNIDLESTATE_RESET;
    HAL_TIM_PWM_ConfigChannel(&htim1, &oc1, TIM_CHANNEL_1);

    /* Deadtime: ~101 ns */
    bdt.OffStateRunMode  = TIM_OSSR_DISABLE;
    bdt.OffStateIDLEMode = TIM_OSSI_DISABLE;
    bdt.LockLevel        = TIM_LOCKLEVEL_OFF;
    bdt.DeadTime         = 17;                     /* 17 × 5.95 ns ≈ 101 ns   */
    bdt.BreakState       = TIM_BREAK_DISABLE;
    bdt.BreakPolarity    = TIM_BREAKPOLARITY_HIGH;
    bdt.AutomaticOutput  = TIM_AUTOMATICOUTPUT_ENABLE;
    HAL_TIMEx_ConfigBreakDeadTime(&htim1, &bdt);

    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
    HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);  /* Enable complementary CH  */
}

Input Capture

Input capture is the inverse of output compare: instead of the CPU writing to CCRx to control an output, hardware writes the current CNT value into CCRx the instant it detects a selected edge on the corresponding pin. Two consecutive rising-edge captures give you the period of an unknown signal with hardware precision — no software loop timing jitter, no missed edges (as long as the period is longer than two interrupt service times).

Period & Frequency Measurement

The measurement algorithm is straightforward: on the first rising edge, store CCRx as capture1. On the second rising edge, store CCRx as capture2. The period in timer ticks is capture2 - capture1 (unsigned subtraction handles counter wrap-around on 16-bit timers — it works correctly as long as the period fits in a uint16_t). Convert to seconds: period_s = (capture2 - capture1) / (TIMx_CLK / (PSC+1)).

For wide frequency ranges, choose the prescaler so that the shortest expected period fills at least 100 ticks (for 1% accuracy). A prescaler of 84 (giving 1 MHz timer clock) handles signals from 15 Hz to 500 kHz comfortably on a 16-bit timer.

HAL_TIM_IC_Start_IT

After configuring the channel as input capture with HAL_TIM_IC_ConfigChannel(), start the capture interrupt with HAL_TIM_IC_Start_IT(). The HAL fires HAL_TIM_IC_CaptureCallback() for each captured edge. Read the captured value with HAL_TIM_ReadCapturedValue().

/* Input Capture on TIM3_CH2 (PB5) — frequency measurement
 * TIM3 clock = 84 MHz, PSC = 83 → 1 MHz timer clock (1 µs resolution)
 * Measures signals from ~15 Hz (period = 65535 µs) to ~500 kHz
 */

TIM_HandleTypeDef htim3_ic;

static volatile uint32_t ic_capture1    = 0;
static volatile uint32_t ic_capture2    = 0;
static volatile uint8_t  ic_edge_count  = 0;
static volatile float    ic_frequency   = 0.0f;

void TIM3_IC_Init(void)
{
    TIM_IC_InitTypeDef ic_config = {0};

    __HAL_RCC_TIM3_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    /* PB5 = TIM3_CH2 input */
    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_5; g.Mode = GPIO_MODE_AF_PP;
    g.Pull = GPIO_PULLDOWN; g.Speed = GPIO_SPEED_FREQ_HIGH;
    g.Alternate = GPIO_AF2_TIM3;
    HAL_GPIO_Init(GPIOB, &g);

    /* Time-base: 1 MHz clock */
    htim3_ic.Instance           = TIM3;
    htim3_ic.Init.Prescaler     = 83;             /* 84 MHz / 84 = 1 MHz      */
    htim3_ic.Init.CounterMode   = TIM_COUNTERMODE_UP;
    htim3_ic.Init.Period        = 0xFFFF;         /* Max period for 16-bit    */
    htim3_ic.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    HAL_TIM_IC_Init(&htim3_ic);

    /* CH2: rising edge capture, no prescaler, no filter */
    ic_config.ICPolarity  = TIM_ICPOLARITY_RISING;
    ic_config.ICSelection = TIM_ICSELECTION_DIRECTTI;
    ic_config.ICPrescaler = TIM_ICPSC_DIV1;
    ic_config.ICFilter    = 0;
    HAL_TIM_IC_ConfigChannel(&htim3_ic, &ic_config, TIM_CHANNEL_2);

    HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(TIM3_IRQn);

    HAL_TIM_IC_Start_IT(&htim3_ic, TIM_CHANNEL_2);
}

void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(&htim3_ic); }

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) {
        if (ic_edge_count == 0) {
            ic_capture1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
            ic_edge_count = 1;
        } else {
            ic_capture2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
            uint32_t period_ticks = (ic_capture2 - ic_capture1) & 0xFFFF;
            if (period_ticks > 0) {
                /* Timer runs at 1 MHz = 1 tick per µs */
                ic_frequency = 1000000.0f / (float)period_ticks;
            }
            ic_edge_count = 0;                     /* Ready for next pair      */
        }
    }
}

PWM Input Mode

PWM input mode is a hardware shortcut that uses both CCR1 and CCR2 of the same timer channel to measure both the period and the duty cycle of an incoming PWM signal simultaneously — without any software state machine. Channel 1 (TI1FP1) is mapped to the rising edge, triggering a reset and starting the period measurement. Channel 2 (TI1FP2) is mapped to the falling edge, capturing the pulse width. After both channels fire you have: CCR1 = period in ticks and CCR2 = active pulse width in ticks. Duty cycle = (CCR2 / CCR1) × 100%.

/* PWM Input Mode on TIM4_CH1 (PB6) — simultaneous period & duty cycle
 * TIM4 clock = 84 MHz, PSC = 83 → 1 MHz timer clock
 * Measures PWM signals with periods up to 65.5 ms (≥15 Hz)
 */

TIM_HandleTypeDef htim4_pwmin;

static volatile uint32_t pwmin_period    = 0;    /* in µs                    */
static volatile uint32_t pwmin_pulse     = 0;    /* in µs                    */
static volatile float    pwmin_duty      = 0.0f; /* 0.0 to 100.0             */

void TIM4_PWMInput_Init(void)
{
    TIM_IC_InitTypeDef ic_ch1 = {0}, ic_ch2 = {0};
    TIM_SlaveConfigTypeDef slave = {0};

    __HAL_RCC_TIM4_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_6; g.Mode = GPIO_MODE_AF_PP;
    g.Pull = GPIO_PULLDOWN; g.Speed = GPIO_SPEED_FREQ_HIGH;
    g.Alternate = GPIO_AF2_TIM4;
    HAL_GPIO_Init(GPIOB, &g);

    htim4_pwmin.Instance         = TIM4;
    htim4_pwmin.Init.Prescaler   = 83;
    htim4_pwmin.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim4_pwmin.Init.Period      = 0xFFFF;
    HAL_TIM_IC_Init(&htim4_pwmin);

    /* CH1: rising edge — period measurement */
    ic_ch1.ICPolarity  = TIM_ICPOLARITY_RISING;
    ic_ch1.ICSelection = TIM_ICSELECTION_DIRECTTI;
    ic_ch1.ICPrescaler = TIM_ICPSC_DIV1;
    ic_ch1.ICFilter    = 4;                        /* Light filter on noisy signals */
    HAL_TIM_IC_ConfigChannel(&htim4_pwmin, &ic_ch1, TIM_CHANNEL_1);

    /* CH2: falling edge — pulse width measurement (indirect TI1) */
    ic_ch2.ICPolarity  = TIM_ICPOLARITY_FALLING;
    ic_ch2.ICSelection = TIM_ICSELECTION_INDIRECTTI;
    ic_ch2.ICPrescaler = TIM_ICPSC_DIV1;
    ic_ch2.ICFilter    = 4;
    HAL_TIM_IC_ConfigChannel(&htim4_pwmin, &ic_ch2, TIM_CHANNEL_2);

    /* Slave mode: reset on TI1FP1 rising edge */
    slave.SlaveMode       = TIM_SLAVEMODE_RESET;
    slave.InputTrigger    = TIM_TS_TI1FP1;
    slave.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;
    slave.TriggerFilter   = 4;
    HAL_TIM_SlaveConfigSynchro(&htim4_pwmin, &slave);

    HAL_NVIC_SetPriority(TIM4_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(TIM4_IRQn);

    HAL_TIM_IC_Start_IT(&htim4_pwmin, TIM_CHANNEL_1);
    HAL_TIM_IC_Start_IT(&htim4_pwmin, TIM_CHANNEL_2);
}

void TIM4_IRQHandler(void) { HAL_TIM_IRQHandler(&htim4_pwmin); }

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM4 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        pwmin_period = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        pwmin_pulse  = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
        if (pwmin_period > 0) {
            pwmin_duty = (float)pwmin_pulse / (float)pwmin_period * 100.0f;
        }
    }
}
Design Tip: PWM input mode is ideal for reading RC servo signals (50 Hz, 1–2 ms pulse), decoding ESC feedback, or measuring the output of another MCU's PWM channel. The hardware filter (ICFilter bits) suppresses glitches from long cables — start with 4 and increase if you see spurious captures.

Encoder Interface

Quadrature encoders are attached to motors, knobs, and position sensors. They output two 90°-phase-shifted square waves (A and B channels). The STM32 timer encoder interface mode hardware-decodes these signals and increments or decrements the CNT register — no ISR needed, zero CPU load, and no missed pulses. X4 mode counts all four edges per cycle (both rising and falling of both A and B), quadrupling the effective resolution.

Connect encoder channel A to TIMx_CH1 and channel B to TIMx_CH2. Set TIM_ENCODERMODE_TI12 for X4 mode (both channels, both edges). The CNT register now represents absolute position (modulo ARR+1). To detect direction, read two consecutive CNT values: if the second is larger the motor is moving forward; if smaller it is moving in reverse (accounting for wrap-around at ARR).

/* Quadrature Encoder on TIM4_CH1 (PB6) and TIM4_CH2 (PB7) — X4 mode
 * TIM4 ARR = 0xFFFF (full 16-bit range, −32768 to +32767 relative)
 * Velocity calculated every 10 ms using position difference
 */

TIM_HandleTypeDef htim4_enc;

static volatile int32_t  enc_position  = 0;       /* Accumulated position     */
static volatile int32_t  enc_velocity  = 0;       /* Counts per 10 ms period  */

void TIM4_Encoder_Init(void)
{
    TIM_Encoder_InitTypeDef enc_config = {0};

    __HAL_RCC_TIM4_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    g.Mode = GPIO_MODE_AF_PP;
    g.Pull = GPIO_PULLUP;                          /* Encoder signals need pull-up */
    g.Speed = GPIO_SPEED_FREQ_HIGH;
    g.Alternate = GPIO_AF2_TIM4;
    HAL_GPIO_Init(GPIOB, &g);

    htim4_enc.Instance         = TIM4;
    htim4_enc.Init.Prescaler   = 0;
    htim4_enc.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim4_enc.Init.Period      = 0xFFFF;
    HAL_TIM_Encoder_Init(&htim4_enc, &enc_config);

    /* X4 mode — count all four edges for maximum resolution */
    enc_config.EncoderMode = TIM_ENCODERMODE_TI12;
    enc_config.IC1Polarity  = TIM_ICPOLARITY_RISING;
    enc_config.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    enc_config.IC1Prescaler = TIM_ICPSC_DIV1;
    enc_config.IC1Filter    = 6;                   /* Filter glitches on long cables */
    enc_config.IC2Polarity  = TIM_ICPOLARITY_RISING;
    enc_config.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    enc_config.IC2Prescaler = TIM_ICPSC_DIV1;
    enc_config.IC2Filter    = 6;
    HAL_TIM_Encoder_Init(&htim4_enc, &enc_config);

    HAL_TIM_Encoder_Start(&htim4_enc, TIM_CHANNEL_ALL);
}

/* Call from a 10 ms periodic task (e.g., TIM6 callback) */
void Encoder_Update_10ms(void)
{
    static int32_t prev_cnt = 0;
    int32_t curr_cnt = (int32_t)(int16_t)__HAL_TIM_GET_COUNTER(&htim4_enc);

    enc_velocity  = curr_cnt - prev_cnt;           /* signed delta per 10 ms  */
    enc_position += enc_velocity;
    prev_cnt      = curr_cnt;
}

int32_t Encoder_GetPosition(void) { return enc_position; }
int32_t Encoder_GetVelocity(void) { return enc_velocity; } /* counts/10ms */
Practical Note: Cast __HAL_TIM_GET_COUNTER() to (int16_t) before assigning to int32_t. This correctly sign-extends the 16-bit counter value, so wrap-around from 65535 to 0 (backward motion) appears as −1 in the delta calculation rather than +65535.

Output Compare & One-Pulse Mode

Output compare toggle mode (OCxM = 011) flips the output pin polarity every time CNT reaches CCRx. Because each toggle fires once per period at the CCR match point, the output frequency is half the timer update rate. This is useful for generating precise square waves without needing a dedicated PWM channel, and the frequency can be changed in real time by writing a new CCRx value while the timer runs.

One-pulse mode (OPM bit in CR1) automatically stops the counter after one update event. Combined with output compare, this produces a single pulse of programmable width on a GPIO. The pulse length is determined by CCRx and ARR: the output goes active at CCRx and returns inactive at ARR. Trigger the pulse by software (__HAL_TIM_ENABLE()) or by an external trigger through the slave mode controller.

/* One-Pulse Mode on TIM2_CH1 (PA0) — 50 µs pulse triggered by software
 * TIM2 clock = 84 MHz, PSC = 83 → 1 MHz (1 µs resolution)
 * CCR1 = 1     → output goes active 1 µs after trigger
 * ARR  = 51    → output goes inactive at 51 µs; timer stops
 * Net pulse width = ARR - CCR1 = 50 µs
 */

TIM_HandleTypeDef htim2_opm;

void TIM2_OPM_Init(void)
{
    TIM_OC_InitTypeDef oc = {0};

    __HAL_RCC_TIM2_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_0; g.Mode = GPIO_MODE_AF_PP;
    g.Pull = GPIO_NOPULL; g.Speed = GPIO_SPEED_FREQ_HIGH;
    g.Alternate = GPIO_AF1_TIM2;
    HAL_GPIO_Init(GPIOA, &g);

    htim2_opm.Instance         = TIM2;
    htim2_opm.Init.Prescaler   = 83;              /* 1 MHz timer clock        */
    htim2_opm.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2_opm.Init.Period      = 51;              /* ARR: timer stops here    */
    htim2_opm.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    htim2_opm.Init.OnePulseMode = TIM_OPMODE_SINGLE;  /* OPM bit set          */
    HAL_TIM_OnePulse_Init(&htim2_opm, TIM_OPMODE_SINGLE);

    oc.OCMode     = TIM_OCMODE_PWM2;             /* Active low → pulse HIGH   */
    oc.Pulse      = 1;                            /* CCR1: delay before pulse  */
    oc.OCPolarity = TIM_OCPOLARITY_HIGH;
    HAL_TIM_OC_ConfigChannel(&htim2_opm, &oc, TIM_CHANNEL_1);
}

/* Fire a single 50 µs pulse — call from application code */
void TIM2_OPM_TriggerPulse(void)
{
    __HAL_TIM_SET_COUNTER(&htim2_opm, 0);        /* Reset counter to 0       */
    HAL_TIM_OC_Start(&htim2_opm, TIM_CHANNEL_1); /* Enable output, start CNT */
}

Exercises

Exercise 1 Beginner

Servo Control with 50 Hz PWM

Generate a 50 Hz PWM signal (20 ms period) on TIM3_CH1 at 7.5% duty cycle. On a standard RC servo this corresponds to the neutral position (1.5 ms pulse). Verify the period and pulse width with an oscilloscope. Then write a sweep function that linearly interpolates the duty cycle from 5% (1 ms, full CCW) to 10% (2 ms, full CW) over 2 seconds, then returns. Observe the servo shaft tracking the position command. Calculate the required ARR and CCR values for your specific timer clock frequency before coding.

TIM3 PWM Mode 1 Servo Control HAL_TIM_PWM_Start
Exercise 2 Intermediate

3-Channel UART-Controlled LED Dimmer

Configure TIM3 with three PWM output channels at 20 kHz (TIM3_CH1, CH2, CH3 on three different GPIO pins). Accept duty cycle commands over UART in the format Rx:value\n (e.g., R1:75\n for 75% on channel 1). Parse the command in the UART interrupt callback from Part 3, then call __HAL_TIM_SET_COMPARE() to update the appropriate CCR. All three channels must remain active at their current duty cycles while any one is being updated — do not reinitialise the timer. Implement bounds checking: reject values outside 0–100.

Multi-Channel PWM UART Integration __HAL_TIM_SET_COMPARE Real-Time Control
Exercise 3 Advanced

Auto-Ranging Frequency Counter

Build a frequency counter that measures an unknown signal in the range 0.1 Hz to 1 MHz using input capture on TIM3_CH2. Implement auto-ranging: start with PSC=83 (1 MHz timer clock). If two consecutive captures return a period <200 ticks (frequency >5 kHz) and the prescaler has headroom, halve the prescaler to extend range. If captures return period >32767 ticks (risk of overflow on 16-bit), double the prescaler. After stabilising on the correct range, also measure duty cycle using PWM input mode on the same signal. Output frequency (Hz to 6 significant figures) and duty cycle (%) over UART every 250 ms. Verify accuracy against a signal generator set to 100 kHz. Target ±0.1% frequency accuracy.

Input Capture PWM Input Mode Auto-Ranging Frequency Accuracy

Timer Configuration Tool

Use this tool to document your STM32 timer configuration — instance selection, operating mode, clock settings, prescaler, auto-reload values, and channel assignments. Download as Word, Excel, PDF, or PPTX for project documentation or hardware review.

STM32 Timer Configuration Generator

Document your timer setup — operating mode, clock, prescaler, channels. Download as Word, Excel, PDF, or PPTX.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Next Steps

In this article we covered the full breadth of the STM32 timer subsystem:

  • Timer architecture: basic timers (TIM6/7) for simple interrupts, general-purpose timers (TIM2–5, 9–14) for capture/compare/encoder, and advanced timers (TIM1/8) for complementary PWM with safety features. The clock doubling rule (2×PCLK when APBx prescaler ≠ 1) must be verified before computing prescaler values.
  • Non-blocking time bases: the TIM6 free-running counter for millisecond timing and the DWT CYCCNT register for microsecond timing — zero timer peripherals consumed for the DWT approach.
  • PWM generation: Mode 1 standard PWM with runtime duty cycle changes via __HAL_TIM_SET_COMPARE(), and complementary PWM on TIM1/TIM8 with programmable deadtime insertion for H-bridge and inverter applications.
  • Input capture: two-edge frequency measurement with hardware precision, and PWM input mode that measures both period and duty cycle simultaneously using a single timer's slave mode reset.
  • Encoder interface: X4 quadrature decoding with zero CPU overhead, sign-extended counter delta for velocity calculation, and hardware input filtering for noisy signals.
  • One-pulse mode: a single deterministic pulse of programmable width triggered by software or external event, essential for ultrasonic ranging, LiDAR triggering, and step-motor timing.

Next in the Series

In Part 5: ADC & DAC, we master the STM32 ADC architecture — resolution options, sampling time selection, scan mode with DMA, injected channels for emergency sampling, analog watchdog thresholds, and multi-ADC simultaneous modes. We then generate precision waveforms with the 12-bit DAC driven by DMA and a timer trigger for a CPU-free sine wave output.

Technology