All guides
TECHNICAL GUIDESTM32L4R5TIMERS2026

STM32L4R5 Timers
PWM, input capture, encoder & low-power.

Register-accurate map of every TIMx and LPTIMx on the STM32L4R5 (RM0432 / DS12023): time-base math, PWM edge/center, complementary outputs with dead-time, input capture, quadrature encoder and one-pulse — with copy-pasteable bare-metal and HAL code.

01 Timer landscape & clock tree

The STM32L4R5 (Cortex-M4F, up to 120 MHz) carries two advanced-control timers, five general-purpose timers, two basic timers and two low-power timers. Picking the right one is the first design decision: only TIM1/TIM8 do complementary outputs with dead-time, only TIM2/TIM5 are 32-bit, and only LPTIM keeps running in Stop mode.

Complete timer map

TimerClassWidthChannelsComp. outDead-time / BreakRep. cnt (RCR)Bus
TIM1Advanced16-bit4 (CH1–CH4)CH1N–CH3NYes16-bitAPB2
TIM8Advanced16-bit4 (CH1–CH4)CH1N–CH3NYes16-bitAPB2
TIM2General32-bit4APB1
TIM3General16-bit4APB1
TIM4General16-bit4APB1
TIM5General32-bit4APB1
TIM6Basic16-bit0 (time-base / DAC trig)APB1
TIM7Basic16-bit0 (time-base / DAC trig)APB1
TIM15General16-bit2 (CH1, CH2)CH1NBreak only8-bitAPB2
TIM16General16-bit1 (CH1)CH1NBreak only8-bitAPB2
TIM17General16-bit1 (CH1)CH1NBreak only8-bitAPB2
LPTIM1Low-power16-bit1 outAPB1
LPTIM2Low-power16-bit1 outAPB1

Timer clock derivation (the doubling rule)

The counter clock CK_INT is not simply PCLK. STM32L4 applies the classic APB doubling rule: if the APB prescaler is 1, the timer clock equals PCLKx; if the APB prescaler is greater than 1, the timer clock is 2 × PCLKx. There is no TIMPRE bit on L4 (that is an F7/H7 feature).

SYSCLK≤ 120 MHz on STM32L4R5 (needs Range 1 boost, Flash 5 WS, and the on-chip or external SMPS path).
APB1 presc = 1PCLK1 = 120 MHz → TIM2–7 clock = 120 MHz.
APB1 presc = 2PCLK1 = 60 MHz → TIM2–7 clock = 2 × 60 = 120 MHz (doubling kicks in).
APB2 prescSame rule for TIM1/8/15/16/17 fed from PCLK2.

Confirm the exact numbers at runtime instead of hard-coding: HAL_RCC_GetPCLK1Freq() / HAL_RCC_GetPCLK2Freq() return PCLK, and the HAL RCC_ClkInitTypeDef path already accounts for the doubling when it programs peripherals.

Enable the clock first — always

TimerRCC enable bit
TIM1RCC->APB2ENR |= RCC_APB2ENR_TIM1EN
TIM8RCC->APB2ENR |= RCC_APB2ENR_TIM8EN
TIM15 / 16 / 17RCC->APB2ENR |= RCC_APB2ENR_TIM15EN / _TIM16EN / _TIM17EN
TIM2 … TIM7RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN … _TIM7EN
LPTIM1RCC->APB1ENR1 |= RCC_APB1ENR1_LPTIM1EN
LPTIM2RCC->APB1ENR2 |= RCC_APB1ENR2_LPTIM2EN
DELAY AFTER CLOCK ENABLE

On STM32L4 a peripheral register may still read the old value one or two cycles after the RCC_...ENR write. Insert a dummy read-back (volatile uint32_t tmp = RCC->APB1ENR1; (void)tmp;) before touching the timer registers, exactly as the HAL __HAL_RCC_TIMx_CLK_ENABLE() macro does internally.

GPIO alternate-function map (representative)

Timer pins need MODER = 10 (alternate function) plus the correct AF number in AFRL/AFRH. AF is per-timer-family, so the same pin can be two different channels depending on the AF you select (e.g. PC6 = TIM3_CH1 @AF2 or TIM8_CH1 @AF3). Always confirm the exact pin/package combination in DS12023 Table 27 (Alternate function mapping).

Timer familyAFExample pins
TIM1AF1PA8/9/10/11 = CH1–4; PB13/14/15 = CH1N/2N/3N; PA7=CH1N; PA6=BKIN
TIM2AF1PA0/1/2/3 = CH1–4; PA5 or PA15 = CH1
TIM3AF2PA6/7=CH1/2, PB0/1=CH3/4; PC6/7/8/9 = CH1–4
TIM4AF2PB6/7/8/9 = CH1–4; PD12/13/14/15 = CH1–4
TIM5AF2PA0/1/2/3 = CH1–4
TIM8AF3PC6/7/8/9 = CH1–4; PA7=CH1N, PB0=CH2N, PB1=CH3N
TIM15AF14PA2/3 = CH1/2; PB14/15 = CH1/2; PB13=CH1N
TIM16AF14PA6 or PB8 = CH1; PB6=CH1N
TIM17AF14PA7 or PB9 = CH1; PB7=CH1N
LPTIM1AF1PC1=OUT, PB5/PC0=OUT, PB6=ETR, PB7=IN1
LPTIM2AF14PA4/PA8=OUT, PB1=IN1, PC3=ETR

DMAMUX request lines (STM32L4+ has a DMAMUX)

On the STM32L4R5 the two DMA controllers are decoupled from peripherals by DMAMUX1: you write the request-line number into DMAMUX1_CxCR.DMAREQ_ID (or pass DMA_REQUEST_TIMx_yy to the HAL). The timer block is contiguous:

TimerCH1CH2CH3CH4UPTRIGCOM
TIM142434445464748
TIM849505152535455
TIM25657585960
TIM3616263646566
TIM46768697071
TIM5727374757677
TIM1578798081
TIM168283
TIM178485

TIM6_UP and TIM7_UP (used to pace the DAC) sit in the low-numbered part of the DMAMUX table; prefer the symbolic DMA_REQUEST_TIM6_UP / DMA_REQUEST_TIM7_UP constants rather than a hand-typed number. To fire a DMA request the channel must set its DMA-enable bit in TIMx_DIER (UDE, CC1DECC4DE, TDE, COMDE).

02 Time-base engine: PSC / ARR / CNT / RCR

Every TIMx is a prescaler feeding a counter that wraps at an auto-reload value. Master the three-register triangle (PSC, ARR, CNT) and the update event, and PWM/capture/encoder become thin layers on top.

The datapath

 CK_INT ──►[ PSC 16-bit ]──► CK_CNT ──►[ CNT ]──► compare vs ARR ──► Update Event (UEV)
           divide by (PSC+1)          up/down             │
                                                          ├─► sets UIF in TIMx_SR
                                                          ├─► reloads shadow PSC/ARR/CCRx
                                                          └─► every (RCR+1) overflows (adv. timers)

 f_CK_CNT = f_CK_INT / (PSC + 1)
 f_UEV    = f_CK_INT / ((PSC + 1) × (ARR + 1))        ; edge-aligned, up- or down-counting
 f_UEV    = f_CK_INT / ((PSC + 1) × 2 × ARR)          ; center-aligned (up then down)

Core registers

TIMx_PSC16-bit prescaler (all timers). Counter clock = CK_INT / (PSC+1). Preloaded: takes effect at next update.
TIMx_ARRAuto-reload. 16-bit for most, 32-bit for TIM2 & TIM5. Preload gated by CR1.ARPE.
TIMx_CNTLive counter. UIFCPY (bit 31) can mirror the update flag when CR1.UIFREMAP=1 for atomic 32-bit reads.
TIMx_RCRRepetition counter (TIM1/8 16-bit, TIM15/16/17 8-bit). UEV occurs only every RCR+1 counter cycles.
TIMx_CR1CEN(0) enable · UDIS(1) · URS(2) · OPM(3) · DIR(4) · CMS[1:0](6:5) · ARPE(7) · CKD[1:0](9:8) · UIFREMAP(11).
TIMx_EGRSoftware event generation. UG(0) forces an update (reloads PSC/ARR/RCR now) without waiting for overflow.
TIMx_SR / DIERStatus flags (UIF, CCxIF, CCxOF, TIF) and their interrupt/DMA enables (UIE, CCxIE, UDE, CCxDE, TIE).

Counting modes (CR1.DIR & CR1.CMS)

ModeCMS[1:0]DIRPeriod (ticks)Compare flag set on
Edge, up000ARR + 1
Edge, down001ARR + 1
Center, mode 1012 × ARRcounting down only
Center, mode 2102 × ARRcounting up only
Center, mode 3112 × ARRcounting up and down

Worked frequency example

Target: exactly 1 kHz update on TIM3 with a 120 MHz timer clock. Choose a prescaler so the counter runs at a round rate, e.g. PSC = 119 → CK_CNT = 1 MHz. Then ARR + 1 = 1 MHz / 1 kHz = 1000 → ARR = 999. A 16-bit timer can reach much lower frequencies via a larger PSC; the 32-bit TIM2/TIM5 can time a full 35.8 s window at 120 MHz without any prescaler.

timebase_1khz.c — bare-metal 1 kHz tick interrupt on TIM3
#include "stm32l4r5xx.h"   /* CMSIS device header (via stm32l4xx.h) */

void TIM3_timebase_1kHz(void)
{
    /* 1. Clock the timer, then read-back so the enable settles */
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM3EN;
    (void)RCC->APB1ENR1;

    /* 2. CK_CNT = 120 MHz / (119+1) = 1 MHz */
    TIM3->PSC = 120 - 1;

    /* 3. UEV = 1 MHz / (999+1) = 1 kHz */
    TIM3->ARR = 1000 - 1;

    /* 4. Buffer ARR so mid-flight changes are glitch-free */
    TIM3->CR1 |= TIM_CR1_ARPE;

    /* 5. Force an update NOW so PSC/ARR shadow registers load
     *    before the counter starts (otherwise first period is wrong) */
    TIM3->EGR  = TIM_EGR_UG;
    TIM3->SR  &= ~TIM_SR_UIF;          /* clear the flag UG just set */

    /* 6. Enable update interrupt and start counting */
    TIM3->DIER |= TIM_DIER_UIE;
    TIM3->CR1  |= TIM_CR1_CEN;

    NVIC_SetPriority(TIM3_IRQn, 5);
    NVIC_EnableIRQ(TIM3_IRQn);
}

volatile uint32_t g_ticks;
void TIM3_IRQHandler(void)
{
    if (TIM3->SR & TIM_SR_UIF) {
        TIM3->SR = ~TIM_SR_UIF;        /* rc_w0: write 0 to clear */
        g_ticks++;
    }
}
WHY THE UG PULSE MATTERS

PSC and ARR are shadowed. If you set them and immediately enable the counter without generating an update event, the counter uses the reset shadow values (PSC=0, ARR=0xFFFF) for the very first period. Writing TIM_EGR_UG copies the preload values into the active registers instantly — do it before CEN.

03 PWM generation (edge & center-aligned)

PWM is a capture/compare channel in output mode. The counter sweeps 0→ARR; a comparator against CCRx toggles the output. Frequency lives in ARR, duty lives in CCRx.

The output-compare registers

TIMx_CCMR1/2Per-channel mode. CCxS[1:0]=00 → output. OCxM[2:0] in bits 6:4 (12:14 for the 2nd channel of the pair), plus OCxM[3] in bit 16/24. OCxPE = preload enable, OCxFE = fast enable.
TIMx_CCERCCxE enables the OCx pin, CCxP sets polarity (0 = active-high). For TIM1/8: CCxNE/CCxNP control the complementary OCxN pin.
TIMx_CCRxCompare value = duty. 16-bit (32-bit on TIM2/TIM5). Buffered when OCxPE=1.

PWM mode & duty math

OCxM[3:0]ModeUp-counting output (channel active while…)
0110PWM mode 1CNT < CCRx → active; CNT ≥ CCRx → inactive
0111PWM mode 2CNT < CCRx → inactive; CNT ≥ CCRx → active
0100Force inactiveoutput held low (software)
0101Force activeoutput held high (software)
1000Retrig. OPM 1one-pulse variants (OCxM[3]=1 extended modes)
 EDGE-ALIGNED, PWM mode 1, up-counting:
   f_PWM = f_CK_INT / ((PSC+1) × (ARR+1))
   Duty  = CCRx / (ARR+1)                    ; CCRx=0 → 0% ; CCRx=ARR+1 → 100%

 CENTER-ALIGNED (CMS≠00):
   f_PWM = f_CK_INT / ((PSC+1) × 2 × ARR)    ; half the edge-aligned frequency
   Duty  = CCRx / ARR
   → symmetric edges about the counter peak; preferred for motor / audio.

Bare-metal: 20 kHz, 25 % PWM on TIM3_CH1 (PC6, AF2)

120 MHz / (1 × 6000) = 20 kHz with PSC=0, ARR=5999; 25 % → CCR1 = 1500.

pwm_tim3_ll.c — register-level PWM
#include "stm32l4r5xx.h"

void TIM3_CH1_pwm_20kHz_25pct(void)
{
    /* ---- GPIO: PC6 as AF2 (TIM3_CH1) ---- */
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOCEN;
    (void)RCC->AHB2ENR;

    GPIOC->MODER  &= ~GPIO_MODER_MODE6;         /* clear pin 6 mode  */
    GPIOC->MODER  |=  (0x2u << GPIO_MODER_MODE6_Pos); /* 10 = alt func */
    GPIOC->OSPEEDR |= (0x3u << GPIO_OSPEEDR_OSPEED6_Pos); /* high speed */
    GPIOC->AFR[0] &= ~GPIO_AFRL_AFSEL6;
    GPIOC->AFR[0] |=  (2u << GPIO_AFRL_AFSEL6_Pos);  /* AF2 = TIM3     */

    /* ---- Timer time-base ---- */
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM3EN;
    (void)RCC->APB1ENR1;

    TIM3->PSC = 1 - 1;                 /* no prescale, CK_CNT = 120 MHz */
    TIM3->ARR = 6000 - 1;             /* 120e6/6000 = 20 kHz            */
    TIM3->CCR1 = 1500;               /* 1500/6000 = 25 % duty          */

    /* ---- Channel 1 as PWM mode 1, preload on ---- */
    TIM3->CCMR1 &= ~TIM_CCMR1_CC1S;   /* CC1S=00 → output              */
    TIM3->CCMR1 &= ~TIM_CCMR1_OC1M;
    TIM3->CCMR1 |=  (TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1); /* 0110      */
    TIM3->CCMR1 |=  TIM_CCMR1_OC1PE;  /* buffer CCR1                    */

    TIM3->CCER  &= ~TIM_CCER_CC1P;    /* active-high                    */
    TIM3->CCER  |=  TIM_CCER_CC1E;    /* enable OC1 pin                 */

    TIM3->CR1   |=  TIM_CR1_ARPE;     /* buffer ARR                     */
    TIM3->EGR    =  TIM_EGR_UG;       /* load shadows                   */
    TIM3->CR1   |=  TIM_CR1_CEN;      /* GO                             */
}

/* Change duty at runtime (safe because CCR1 is buffered): */
static inline void TIM3_CH1_set_duty(uint16_t permille)  /* 0..1000 */
{
    TIM3->CCR1 = (uint16_t)(((uint32_t)(TIM3->ARR + 1) * permille) / 1000u);
}

HAL: the same PWM channel

pwm_tim3_hal.c — HAL PWM
#include "stm32l4xx_hal.h"

TIM_HandleTypeDef htim3;

void TIM3_CH1_pwm_hal(void)
{
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_TIM3_CLK_ENABLE();

    GPIO_InitTypeDef g = {0};
    g.Pin       = GPIO_PIN_6;
    g.Mode      = GPIO_MODE_AF_PP;
    g.Pull      = GPIO_NOPULL;
    g.Speed     = GPIO_SPEED_FREQ_HIGH;
    g.Alternate = GPIO_AF2_TIM3;
    HAL_GPIO_Init(GPIOC, &g);

    htim3.Instance           = TIM3;
    htim3.Init.Prescaler     = 1 - 1;
    htim3.Init.CounterMode   = TIM_COUNTERMODE_UP;
    htim3.Init.Period        = 6000 - 1;          /* ARR → 20 kHz */
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim3);

    TIM_OC_InitTypeDef oc = {0};
    oc.OCMode     = TIM_OCMODE_PWM1;
    oc.Pulse      = 1500;                          /* 25 % duty */
    oc.OCPolarity = TIM_OCPOLARITY_HIGH;
    oc.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim3, &oc, TIM_CHANNEL_1);

    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

/* Runtime duty update: */
/*   __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, new_ccr); */

For center-aligned PWM in HAL, set CounterMode = TIM_COUNTERMODE_CENTERALIGNED1/2/3 (mirrors CMS[1:0]) and remember the frequency halves for a given ARR.

04 Advanced control: complementary + dead-time

Only TIM1 and TIM8 drive a channel and its inverted twin (OCx / OCxN) with a hardware-inserted dead-time — the feature that makes half-bridge and 3-phase inverter drive safe. It hinges on one register the general-purpose timers do not have: TIMx_BDTR.

BDTR — break and dead-time register

MOE (15)Main Output Enable. Outputs are dead until this is 1. Cleared by a break event.
AOE (14)Automatic Output Enable: MOE re-asserts at the next update after a break clears (else set MOE by software).
BKP (13) / BKE (12)Break input polarity / enable. BK2 fields (bits 24–25 etc.) are the second break input.
OSSR (11) / OSSI (10)Off-state selection in Run / Idle: what the pin does when the channel is disabled.
LOCK[1:0] (9:8)Write-once lock level protecting critical bits — set last, cannot be cleared without reset.
DTG[7:0] (7:0)Dead-time generator setup (see formula below).

Dead-time formula

t_DTS is the dead-time/sampling clock, derived from CK_INT by CR1.CKD[1:0] (00 → t_DTS = t_CK_INT, 01 → ×2, 10 → ×4). DTG is decoded piecewise so you can reach both fine and coarse dead-times from one 8-bit field:

DTG[7:5]Dead-time (DT)Step
0xxDTG[7:0] × t_DTSt_DTS
10x(64 + DTG[5:0]) × 2 × t_DTS2 × t_DTS
110(32 + DTG[4:0]) × 8 × t_DTS8 × t_DTS
111(32 + DTG[4:0]) × 16 × t_DTS16 × t_DTS

Example at CK_INT = 120 MHz, CKD=00 → t_DTS = 8.33 ns. For ~500 ns dead-time: 500 / 8.33 ≈ 60 → DTG = 60 (0x3C, in the 0xx range). For ~1 µs you would use the 10x range: (64 + n) × 2 × 8.33 ns.

Bare-metal: TIM1_CH1 + CH1N complementary PWM with dead-time

tim1_complementary_ll.c
#include "stm32l4r5xx.h"

/* PA8 = TIM1_CH1 (AF1), PB13 = TIM1_CH1N (AF1) */
void TIM1_complementary_pwm(void)
{
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN | RCC_AHB2ENR_GPIOBEN;
    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
    (void)RCC->APB2ENR;

    /* PA8 → AF1 */
    GPIOA->MODER  &= ~GPIO_MODER_MODE8;
    GPIOA->MODER  |=  (0x2u << GPIO_MODER_MODE8_Pos);
    GPIOA->AFR[1] &= ~GPIO_AFRH_AFSEL8;
    GPIOA->AFR[1] |=  (1u << GPIO_AFRH_AFSEL8_Pos);
    /* PB13 → AF1 */
    GPIOB->MODER  &= ~GPIO_MODER_MODE13;
    GPIOB->MODER  |=  (0x2u << GPIO_MODER_MODE13_Pos);
    GPIOB->AFR[1] &= ~GPIO_AFRH_AFSEL13;
    GPIOB->AFR[1] |=  (1u << GPIO_AFRH_AFSEL13_Pos);

    /* 120 MHz / 6000 = 20 kHz, 50 % duty */
    TIM1->PSC  = 0;
    TIM1->ARR  = 6000 - 1;
    TIM1->CCR1 = 3000;

    TIM1->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_OC1M);
    TIM1->CCMR1 |=  (TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1) /* PWM1 */
                 |   TIM_CCMR1_OC1PE;

    /* Enable BOTH OC1 and OC1N, both active-high */
    TIM1->CCER  |= TIM_CCER_CC1E | TIM_CCER_CC1NE;

    /* Dead-time = 60 × t_DTS ≈ 500 ns; MOE must be set on advanced timers */
    TIM1->CR1  &= ~TIM_CR1_CKD;                 /* CKD=00 → t_DTS = 8.33 ns */
    TIM1->BDTR  = (60u << TIM_BDTR_DTG_Pos)     /* DTG field                */
                | TIM_BDTR_OSSR                 /* drive off-state in run   */
                | TIM_BDTR_MOE;                 /* MAIN OUTPUT ENABLE       */

    TIM1->CR1  |= TIM_CR1_ARPE;
    TIM1->EGR   = TIM_EGR_UG;
    TIM1->CR1  |= TIM_CR1_CEN;
}

HAL: dead-time via TIM_BreakDeadTimeConfigTypeDef

tim1_complementary_hal.c
TIM_HandleTypeDef htim1;

void TIM1_complementary_hal(void)
{
    /* ...clock + PA8/PB13 GPIO AF1 init as above... */
    htim1.Instance = TIM1;
    htim1.Init.Prescaler   = 0;
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim1.Init.Period      = 6000 - 1;
    htim1.Init.RepetitionCounter = 0;
    htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim1);

    TIM_OC_InitTypeDef oc = {0};
    oc.OCMode      = TIM_OCMODE_PWM1;
    oc.Pulse       = 3000;                    /* 50 % */
    oc.OCPolarity  = TIM_OCPOLARITY_HIGH;
    oc.OCNPolarity = TIM_OCNPOLARITY_HIGH;
    oc.OCIdleState = TIM_OCIDLESTATE_RESET;
    oc.OCNIdleState= TIM_OCNIDLESTATE_RESET;
    HAL_TIM_PWM_ConfigChannel(&htim1, &oc, TIM_CHANNEL_1);

    TIM_BreakDeadTimeConfigTypeDef bd = {0};
    bd.OffStateRunMode  = TIM_OSSR_ENABLE;
    bd.OffStateIDLEMode = TIM_OSSI_DISABLE;
    bd.LockLevel        = TIM_LOCKLEVEL_OFF;
    bd.DeadTime         = 60;                 /* raw DTG value */
    bd.BreakState       = TIM_BREAK_DISABLE;
    bd.AutomaticOutput  = TIM_AUTOMATICOUTPUT_DISABLE;
    HAL_TIMEx_ConfigBreakDeadTime(&htim1, &bd);

    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);     /* OC1  */
    HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);  /* OC1N */
}
MOE IS THE #1 GOTCHA

On TIM1/TIM8/TIM15/16/17 nothing appears on the pins until BDTR.MOE = 1. HAL sets it inside HAL_TIM_PWM_Start/PWMN_Start; at register level you must set it yourself. A break event (or AOE=0 after break) clears MOE — the PWM will silently stop until you re-arm it.

05 Input capture

Input capture latches CNT into CCRx on an edge of the mapped input — the tool for measuring an external signal's period, frequency, pulse width or phase. Use a 32-bit timer (TIM2/TIM5) when the input can be slow, to avoid counter wrap headaches.

Input-side CCMR / CCER fields

CCxS[1:0]01 → ICx mapped to TIx (direct); 10 → mapped to the other TI (indirect, for PWM-input); 11 → mapped to TRC.
ICxF[3:0]Digital input filter — N consecutive samples at f_DTS-derived rate before an edge is accepted (debounce).
ICxPSC[1:0]Capture prescaler: capture every 1/2/4/8 edges.
CCxP / CCxNPEdge select: 00 rising, 01 falling, 11 both edges. CCxE arms the capture.
CCxIF / CCxOFCapture flag (in SR) / over-capture flag (a second capture arrived before you read CCRx).

Frequency / period math

 Δ = (capture_N − capture_N-1) mod (ARR+1)     ; ticks between two same-edge captures
 t_period = Δ / f_CK_CNT ,   f_CK_CNT = f_CK_INT / (PSC+1)
 f_signal = f_CK_CNT / Δ

 Pulse-width (PWM input): use TWO channels on the SAME TI —
   CH1 captures rising (period),  CH2 captures falling (high time),
   via slave-mode reset on TI1FP1. Duty = CCR2 / CCR1.

Bare-metal: measure input frequency on TIM2_CH1 (PA0, AF1)

tim2_capture_ll.c — 32-bit period capture with IRQ
#include "stm32l4r5xx.h"

volatile uint32_t g_period_ticks;    /* CK_CNT ticks between rising edges */
volatile uint8_t  g_valid;

void TIM2_CH1_capture_init(void)
{
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN;
    (void)RCC->APB1ENR1;

    /* PA0 → AF1 (TIM2_CH1) input */
    GPIOA->MODER  &= ~GPIO_MODER_MODE0;
    GPIOA->MODER  |=  (0x2u << GPIO_MODER_MODE0_Pos);
    GPIOA->AFR[0] &= ~GPIO_AFRL_AFSEL0;
    GPIOA->AFR[0] |=  (1u << GPIO_AFRL_AFSEL0_Pos);

    /* Free-running 1 MHz time base (PSC=119), full 32-bit range */
    TIM2->PSC = 120 - 1;
    TIM2->ARR = 0xFFFFFFFFu;

    /* CH1 = input, mapped to TI1 (CC1S=01), light filter (ICxF=0011) */
    TIM2->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_IC1F);
    TIM2->CCMR1 |=  (1u << TIM_CCMR1_CC1S_Pos)
                 |  (3u << TIM_CCMR1_IC1F_Pos);

    /* Rising edge (CC1P=0, CC1NP=0), enable capture */
    TIM2->CCER  &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP);
    TIM2->CCER  |=  TIM_CCER_CC1E;

    TIM2->DIER  |=  TIM_DIER_CC1IE;      /* capture interrupt */
    TIM2->EGR    =  TIM_EGR_UG;
    TIM2->CR1   |=  TIM_CR1_CEN;

    NVIC_SetPriority(TIM2_IRQn, 5);
    NVIC_EnableIRQ(TIM2_IRQn);
}

void TIM2_IRQHandler(void)
{
    static uint32_t last;
    if (TIM2->SR & TIM_SR_CC1IF) {
        uint32_t now = TIM2->CCR1;       /* reading CCR1 clears CC1IF */
        g_period_ticks = now - last;    /* 32-bit wrap is automatic   */
        last = now;
        g_valid = 1;
        /* if (TIM2->SR & TIM_SR_CC1OF) { overcapture — ISR too slow } */
    }
}
/* f_signal[Hz] = 1e6 / g_period_ticks   (because CK_CNT = 1 MHz) */

HAL equivalent: HAL_TIM_IC_Init + HAL_TIM_IC_ConfigChannel (ICPolarity = RISING, ICSelection = DIRECTTI), start with HAL_TIM_IC_Start_IT, and read HAL_TIM_ReadCapturedValue inside HAL_TIM_IC_CaptureCallback.

06 Encoder interface mode

In encoder mode the timer becomes a quadrature counter: CNT tracks position, counting up or down automatically from the phase relationship of two inputs (TI1/TI2). No interrupts per edge — you just read CNT. Available on TIM1/8 and TIM2/3/4/5.

SMS encoder modes (TIMx_SMCR)

SMS[2:0]ModeCounts onResolution
001Encoder mode 1TI2FP2 edges (dir. from TI1FP1 level)×2
010Encoder mode 2TI1FP1 edges (dir. from TI2FP2 level)×2
011Encoder mode 3both TI1FP1 & TI2FP2 edges×4

For a standard quadrature encoder use mode 3 (SMS=011) for full ×4 resolution. Swapping the two channel wires (or toggling one CCxP polarity) reverses the counting direction. Note SMS is a 4-bit field on L4 (SMS[3] lives in SMCR bit 16) but encoder modes only use the low 3 bits.

Bare-metal: TIM3 quadrature encoder (PA6=CH1, PA7=CH2, AF2)

tim3_encoder_ll.c
#include "stm32l4r5xx.h"

void TIM3_encoder_init(uint16_t counts_per_rev_x4)
{
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM3EN;
    (void)RCC->APB1ENR1;

    /* PA6, PA7 → AF2 inputs */
    GPIOA->MODER  &= ~(GPIO_MODER_MODE6 | GPIO_MODER_MODE7);
    GPIOA->MODER  |=  (0x2u << GPIO_MODER_MODE6_Pos)
                  |   (0x2u << GPIO_MODER_MODE7_Pos);
    GPIOA->AFR[0] &= ~(GPIO_AFRL_AFSEL6 | GPIO_AFRL_AFSEL7);
    GPIOA->AFR[0] |=  (2u << GPIO_AFRL_AFSEL6_Pos)
                  |   (2u << GPIO_AFRL_AFSEL7_Pos);

    /* CH1=TI1, CH2=TI2, both direct inputs, with input filters */
    TIM3->CCMR1 = (1u << TIM_CCMR1_CC1S_Pos)   /* CC1S=01 → IC1 on TI1 */
                | (1u << TIM_CCMR1_CC2S_Pos)   /* CC2S=01 → IC2 on TI2 */
                | (5u << TIM_CCMR1_IC1F_Pos)   /* filter               */
                | (5u << TIM_CCMR1_IC2F_Pos);

    /* Non-inverted; CCxP selects the counting direction reference */
    TIM3->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP
                  | TIM_CCER_CC2P | TIM_CCER_CC2NP);
    TIM3->CCER |=  TIM_CCER_CC1E | TIM_CCER_CC2E;

    /* Encoder mode 3 (×4): SMS = 011 */
    TIM3->SMCR &= ~TIM_SMCR_SMS;
    TIM3->SMCR |=  (TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0);

    /* Wrap at one mechanical revolution so CNT reads absolute position */
    TIM3->ARR = counts_per_rev_x4 - 1;
    TIM3->CNT = 0;
    TIM3->CR1 |= TIM_CR1_CEN;
}

/* Read anywhere, no IRQ needed: */
static inline uint16_t enc_position(void) { return (uint16_t)TIM3->CNT; }
static inline int enc_direction(void) {                 /* 1=down, 0=up */
    return (TIM3->CR1 & TIM_CR1_DIR) ? 1 : 0;
}

HAL: fill a TIM_Encoder_InitTypeDef with EncoderMode = TIM_ENCODERMODE_TI12 (×4), HAL_TIM_Encoder_Init, then HAL_TIM_Encoder_Start(&htim, TIM_CHANNEL_ALL) and poll __HAL_TIM_GET_COUNTER(&htim). For velocity, sample CNT on a periodic tick and take the wrap-corrected delta.

07 One-pulse mode

One-pulse mode (OPM) makes the counter run exactly one cycle then stop — perfect for a precise programmable delay-then-pulse triggered by an external event (ultrasonic trigger, camera strobe, gate driver interlock).

How it works

CR1.OPM1 → CEN is auto-cleared at the next update event; the counter fires one period and halts.
OCxM = PWM mode 2With up-counting, the output is inactive until CNT reaches CCRx, then active until ARR → a delay followed by a pulse.
Slave trigger (SMCR)SMS=110 (Trigger mode) starts the counter on a TIx edge; TS selects the trigger. Combined with OPM this yields a hardware-timed single shot.
 Up-counting, PWM mode 2, OPM:
   t_DELAY = CCRx        / f_CK_CNT       ; idle time before the edge
   t_PULSE = (ARR+1−CCRx)/ f_CK_CNT       ; width of the single pulse
   ( f_CK_CNT = f_CK_INT / (PSC+1) )

Bare-metal: 10 µs delay then 5 µs pulse on TIM16_CH1

PSC=119 → CK_CNT = 1 MHz (1 tick = 1 µs). Delay 10 µs → CCR1 = 10; total 15 µs → ARR = 15−1 = 14; pulse = (15−10) = 5 µs.

tim16_onepulse_ll.c
#include "stm32l4r5xx.h"

/* PB8 = TIM16_CH1 (AF14) */
void TIM16_one_pulse(void)
{
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
    RCC->APB2ENR |= RCC_APB2ENR_TIM16EN;
    (void)RCC->APB2ENR;

    GPIOB->MODER  &= ~GPIO_MODER_MODE8;
    GPIOB->MODER  |=  (0x2u << GPIO_MODER_MODE8_Pos);
    GPIOB->AFR[1] &= ~GPIO_AFRH_AFSEL8;
    GPIOB->AFR[1] |=  (14u << GPIO_AFRH_AFSEL8_Pos);

    TIM16->PSC  = 120 - 1;      /* 1 MHz  */
    TIM16->ARR  = 15  - 1;      /* total 15 us */
    TIM16->CCR1 = 10;           /* 10 us delay, then 5 us pulse */

    /* PWM mode 2 (0111) so output goes high only after CCR1 */
    TIM16->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_OC1M);
    TIM16->CCMR1 |=  (TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0)
                  |   TIM_CCMR1_OC1PE;
    TIM16->CCER  |= TIM_CCER_CC1E;

    /* TIM16 is a break-capable timer → MOE required */
    TIM16->BDTR  |= TIM_BDTR_MOE;

    TIM16->CR1   |= TIM_CR1_OPM;    /* single shot */
    TIM16->EGR    = TIM_EGR_UG;
}

/* Fire one pulse (software trigger). For a hardware trigger, program
 * SMCR: SMS=110 (trigger) + TS= instead of setting CEN here. */
void TIM16_fire(void) { TIM16->CR1 |= TIM_CR1_CEN; }
HAL PATH

HAL_TIM_OnePulse_Init(&htim, TIM_OPMODE_SINGLE), then HAL_TIM_OnePulse_ConfigChannel and HAL_TIM_OnePulse_Start(&htim, TIM_CHANNEL_1). For trigger-started pulses configure the slave mode with HAL_TIM_SlaveConfigSynchro (SlaveMode = TIM_SLAVEMODE_TRIGGER).

08 LPTIM — low-power timer

LPTIM1/LPTIM2 are 16-bit timers that keep counting in Stop 0/1/2 when the main timers are frozen — clocked from LSE/LSI/HSI16 rather than the APB. They do PWM, one-shot, timeout and pulse-counting with microamp-level power. The register set and, critically, the write protocol differ from TIMx.

Register set (note: not TIMx-compatible)

LPTIM_CFGRCKSEL (internal/external clock), PRESC[2:0] (÷1…÷128), WAVE / WAVPOL (waveform & polarity), TRIGEN/TRIGSEL, ENC (encoder), COUNTMODE.
LPTIM_CRENABLE (must be set before writing ARR/CMP), CNTSTRT (continuous), SNGSTRT (single-shot), plus RSTARE / COUNTRST.
LPTIM_ARR / CMPAuto-reload (period) and compare (duty). Writes need synchronization — see below.
LPTIM_ISR / ICR / IERFlags: ARRM, CMPM, and the ARROK / CMPOK "write accepted" flags; ICR clears them; IER enables interrupts.
LPTIM_CNTCounter — being in another clock domain, read it twice and accept the value only when two reads match.

Clock selection (RCC_CCIPR)

LPTIM does not use the APB timer clock for counting. Pick its kernel clock with RCC_CCIPR.LPTIM1SEL[1:0] / LPTIM2SEL[1:0]: 00 = PCLK, 01 = LSI, 10 = HSI16, 11 = LSE. Use LSE (32.768 kHz) or LSI for Stop-mode operation.

PWM frequency / duty

 f_LPTIM_out = f_lptim_ker / ( PRESC × (ARR + 1) )
 Duty is set by CMP relative to ARR (WAVPOL selects polarity).
 e.g. LSE = 32768 Hz, PRESC=1, ARR=255  → ~128 Hz PWM, CMP=128 → ~50 %.
lptim1_pwm_ll.c — PWM that survives Stop mode
#include "stm32l4r5xx.h"

void LPTIM1_pwm_from_LSE(void)
{
    /* Assume LSE already started (RCC_BDCR.LSEON, LSERDY). */
    RCC->CCIPR = (RCC->CCIPR & ~RCC_CCIPR_LPTIM1SEL)
               | (0x3u << RCC_CCIPR_LPTIM1SEL_Pos);   /* 11 = LSE */
    RCC->APB1ENR1 |= RCC_APB1ENR1_LPTIM1EN;
    (void)RCC->APB1ENR1;

    /* Config MUST be written while DISABLED */
    LPTIM1->CR = 0;
    LPTIM1->CFGR = (0u << LPTIM_CFGR_PRESC_Pos);  /* PRESC=÷1, internal clk */
    /* WAVE=0, WAVPOL=0 → standard PWM, active per compare */

    /* Enable the peripheral BEFORE loading ARR/CMP */
    LPTIM1->CR |= LPTIM_CR_ENABLE;

    /* Write period, wait for ARROK, then compare, wait for CMPOK */
    LPTIM1->ARR = 256 - 1;
    while (!(LPTIM1->ISR & LPTIM_ISR_ARROK)) { }
    LPTIM1->ICR = LPTIM_ICR_ARROKCF;

    LPTIM1->CMP = 128;                    /* ~50 % */
    while (!(LPTIM1->ISR & LPTIM_ISR_CMPOK)) { }
    LPTIM1->ICR = LPTIM_ICR_CMPOKCF;

    /* Start continuous counting → PWM appears on LPTIM1_OUT */
    LPTIM1->CR |= LPTIM_CR_CNTSTRT;
}
LPTIM WRITE PROTOCOL — DO NOT SKIP

ARR and CMP live in the low-power clock domain. You must (1) set ENABLE first, (2) write ARR/CMP, (3) poll ARROK/CMPOK before writing them again or starting. Writing a new value before the previous one is acknowledged is discarded and corrupts the waveform. Also: with the internal clock you cannot change PRESC while enabled — reconfigure only after clearing ENABLE.

09 Gotchas / common mistakes

The bugs that eat afternoons on STM32L4 timers are almost always one of these.

Forgot the RCC enable / read-backWriting timer registers before the clock is enabled (and settled) silently does nothing. Always enable in RCC and add the dummy read-back.
No UG before CENFirst PWM/tick period is wrong because PSC/ARR shadows were not loaded. Write TIMx_EGR = UG (then clear the UIF it raises) before starting.
MOE not set (TIM1/8/15/16/17)Advanced & break-capable timers keep outputs Hi-Z until BDTR.MOE=1. A break event also clears MOE and silently stops PWM.
Wrong timer-clock assumptionBecause of the ×2 APB rule, the timer clock is often double PCLK. Derive it from HAL_RCC_GetPCLKxFreq() instead of hard-coding 120 MHz.
Duty math off-by-oneEdge-aligned period is ARR+1 ticks, not ARR. 100 % duty needs CCRx ≥ ARR+1. Center-aligned uses 2×ARR and CCRx/ARR.
16-bit overflow in captureMeasuring a slow signal on a 16-bit timer: the counter wraps between edges and your delta is garbage. Use TIM2/TIM5 (32-bit) or count overflows in the update IRQ.
CCRx not bufferedChanging duty on the fly without OCxPE (preload) can glitch mid-period. Set OCxPE=1 (and ARPE for ARR) so updates apply at the next cycle boundary.
Reading CCRx does not equal reading CNTIn input capture, reading TIMx_CCRx clears CCxIF; reading CNT does not. Read CCRx in the ISR, and watch CCxOF for over-capture (ISR too slow).
Encoder direction reversedSwap the two input wires, flip one CCxP polarity, or switch encoder mode 1↔2 to reverse counting — do not negate in software.
Wrong AF numberPC6 is TIM3_CH1 at AF2 but TIM8_CH1 at AF3. Selecting the wrong AF routes the pin to the wrong timer. Cross-check DS12023 Table 27 for your exact package.
LPTIM ARROK/CMPOK skippedWriting LPTIM ARR/CMP without polling the OK flags (or writing before ENABLE) corrupts the waveform. Follow the LPTIM write protocol exactly.
32-bit CNT torn readOn TIM2/TIM5 an async CNT read can tear. Use CR1.UIFREMAP + the UIFCPY bit, or read in a loop until two reads agree, when precise 32-bit values matter.
QUICK REFERENCE

Frequency: f = f_CK_INT / ((PSC+1)(ARR+1)) edge · /(2·ARR) center. Duty: CCRx/(ARR+1). Dead-time low range: DTG × t_DTS, t_DTS = t_CK_INT × 2^CKD. Encoder ×4 = SMS 011. LPTIM PWM: f = f_ker/(PRESC·(ARR+1)). Primary sources: RM0432 (timer chapters, DMAMUX table), DS12023 (AF map, clock tree).