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
Timer
Class
Width
Channels
Comp. out
Dead-time / Break
Rep. cnt (RCR)
Bus
TIM1
Advanced
16-bit
4 (CH1–CH4)
CH1N–CH3N
Yes
16-bit
APB2
TIM8
Advanced
16-bit
4 (CH1–CH4)
CH1N–CH3N
Yes
16-bit
APB2
TIM2
General
32-bit
4
—
—
—
APB1
TIM3
General
16-bit
4
—
—
—
APB1
TIM4
General
16-bit
4
—
—
—
APB1
TIM5
General
32-bit
4
—
—
—
APB1
TIM6
Basic
16-bit
0 (time-base / DAC trig)
—
—
—
APB1
TIM7
Basic
16-bit
0 (time-base / DAC trig)
—
—
—
APB1
TIM15
General
16-bit
2 (CH1, CH2)
CH1N
Break only
8-bit
APB2
TIM16
General
16-bit
1 (CH1)
CH1N
Break only
8-bit
APB2
TIM17
General
16-bit
1 (CH1)
CH1N
Break only
8-bit
APB2
LPTIM1
Low-power
16-bit
1 out
—
—
—
APB1
LPTIM2
Low-power
16-bit
1 out
—
—
—
APB1
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).
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.
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).
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:
Timer
CH1
CH2
CH3
CH4
UP
TRIG
COM
TIM1
42
43
44
45
46
47
48
TIM8
49
50
51
52
53
54
55
TIM2
56
57
58
59
60
—
—
TIM3
61
62
63
64
65
66
—
TIM4
67
68
69
70
71
—
—
TIM5
72
73
74
75
76
77
—
TIM15
78
—
—
—
79
80
81
TIM16
82
—
—
—
83
—
—
TIM17
84
—
—
—
85
—
—
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, CC1DE…CC4DE, 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.
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)
Mode
CMS[1:0]
DIR
Period (ticks)
Compare flag set on
Edge, up
00
0
ARR + 1
—
Edge, down
00
1
ARR + 1
—
Center, mode 1
01
—
2 × ARR
counting down only
Center, mode 2
10
—
2 × ARR
counting up only
Center, mode 3
11
—
2 × ARR
counting 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]
Mode
Up-counting output (channel active while…)
0110
PWM mode 1
CNT < CCRx → active; CNT ≥ CCRx → inactive
0111
PWM mode 2
CNT < CCRx → inactive; CNT ≥ CCRx → active
0100
Force inactive
output held low (software)
0101
Force active
output held high (software)
1000
Retrig. OPM 1
one-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)
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
0xx
DTG[7:0] × t_DTS
t_DTS
10x
(64 + DTG[5:0]) × 2 × t_DTS
2 × t_DTS
110
(32 + DTG[4:0]) × 8 × t_DTS
8 × t_DTS
111
(32 + DTG[4:0]) × 16 × t_DTS
16 × 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
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●
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]
Mode
Counts on
Resolution
001
Encoder mode 1
TI2FP2 edges (dir. from TI1FP1 level)
×2
010
Encoder mode 2
TI1FP1 edges (dir. from TI2FP2 level)
×2
011
Encoder mode 3
both 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.
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
#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.
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.