STM32 Advanced Timer PWM:
Dead-Time Insertion and Complementary Outputs
A register-level walkthrough of TIM1 and TIM8 advanced-control timers: centre-aligned PWM generation, dead-time insertion for half-bridge drivers, complementary CHxN outputs, break input handling, and a practical motor-control example tested on STM32F4.
If you have ever designed a half-bridge gate driver, a synchronous buck converter, or a three-phase inverter, you know that driving the high-side and low-side MOSFETs simultaneously — even for a nanosecond — means shoot-through current, blown transistors, and a very expensive silence. The STM32 advanced-control timers (TIM1 and TIM8 on F4/G4/H7 families) solve this at the hardware level with programmable dead-time insertion, complementary output pairs, and automatic break-input shutdown.
This article covers TIM1 configuration at the register level: from clock setup and centre-aligned PWM mode through dead-time calculation, complementary channel enabling, and break-input wiring. No HAL abstraction — you will see every register write and understand exactly what the silicon is doing. The code examples target an STM32F407VG at 168 MHz, but the principles are identical across F4, G4, and H7 families.
What Makes TIM1/TIM8 "Advanced"
A general-purpose timer like TIM2–TIM5 can produce independent PWM signals on its four channels. TIM1 and TIM8 add three features that matter for power-electronics and motor-control applications:
- Complementary outputs (CHxN). Each channel can drive a second pin with the inverted waveform — essential for half-bridge and full-bridge topologies.
- Dead-time insertion. A programmable delay between turning off one output and turning on its complement. The timer hardware inserts this gap automatically, with 8-bit resolution scaled by the timer clock.
- Break input (BRK). A dedicated pin that, when asserted, forces all outputs to a predefined safe state (typically OFF). This is wired to the over-current protection signal from the gate driver IC.
These features let you build a motor-drive or power-converter control loop in hardware, without relying on software to manage switching dead-bands — a critical safety property that survives CPU lockups.
The BDTR Register: Your Dead-Time Control Centre
The TIM1_BDTR (Break and Dead-Time Register) is the single most important register for advanced-timer PWM. Its structure:
| Bits | Field | Description |
|---|---|---|
| 15 | MOE | Main Output Enable — must be set to 1 for any output to appear on the pins |
| 14 | AOE | Automatic Output Enable — if set, MOE is re-asserted automatically after the next update event following a break |
| 13 | BKP | Break Polarity — 0: break active low, 1: break active high |
| 12 | BKE | Break Enable — 1 to enable the BRK input |
| 11 | OSSR | Off-State Selection for Run mode — 0: outputs inactive when MOE=0, 1: outputs in idle state |
| 10 | OSSI | Off-State Selection for Idle mode — same logic for idle state |
| 9–8 | LOCK | Lock configuration: 00 = no lock, 01/10/11 = progressively locked write protection levels |
| 7–0 | DTG[7:0] | Dead-Time Generator — the delay value, scaled by tDTS |
Dead-time is computed as:
t_DEAD = DTG[7:0] × t_DTS
where t_DTS is the timer dead-time prescaler clock, derived from TIMx_CR1.CKD[1:0]:
| CKD[1:0] | t_DTS | Max Dead-Time at 168 MHz |
|---|---|---|
| 00 | 1 × t_CK_INT | 255 × 5.95 ns = 1.52 µs |
| 01 | 2 × t_CK_INT | 255 × 11.90 ns = 3.04 µs |
| 10 | 4 × t_CK_INT | 255 × 23.81 ns = 6.07 µs |
| 11 | Reserved (same as 10 on some families) | — |
For most gate drivers (IR2110, IRS2003, L6387), a dead-time of 200–500 ns is typical. With CKD=00 at 168 MHz you get ~6 ns granularity, which is more than adequate.
Practical Example: Half-Bridge PWM with Dead-Time on TIM1
Let's configure TIM1 to drive a half-bridge on channel 1 (CH1 = PA8, CH1N = PA7) with 20 kHz centre-aligned PWM, 300 ns dead-time, and a break input on PB12.
Step 1: GPIO and Clock Setup
// Enable clocks RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN; RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // PA8 = TIM1_CH1 (AF1), PA7 = TIM1_CH1N (AF1) GPIOA->MODER &= ~(GPIO_MODER_MODER7 | GPIO_MODER_MODER8); GPIOA->MODER |= (GPIO_MODER_MODER7_1 | GPIO_MODER_MODER8_1); // AF mode GPIOA->AFR[0] &= ~(0xF << 28); // PA7 AF1 GPIOA->AFR[0] |= (0x1 << 28); GPIOA->AFR[1] &= ~(0xF << 0); // PA8 AF1 GPIOA->AFR[1] |= (0x1 << 0); // PB12 = TIM1_BKIN (AF1) GPIOB->MODER &= ~GPIO_MODER_MODER12; GPIOB->MODER |= GPIO_MODER_MODER12_1; GPIOB->AFR[1] &= ~(0xF << 16); GPIOB->AFR[1] |= (0x1 << 16);
Step 2: Timer Base Configuration — Centre-Aligned Mode
// 20 kHz PWM with 168 MHz clock in centre-aligned mode: // f_PWM = f_CLK / (2 × ARR) // ARR = 168e6 / (2 × 20e3) = 4200 TIM1->CR1 = 0; // Reset control register TIM1->CR1 |= TIM_CR1_CMS_0; // CMS=01: centre-aligned mode 1 (up/down) TIM1->CR1 |= TIM_CR1_ARPE; // Auto-reload preload enable TIM1->CR1 &= ~TIM_CR1_DIR; // DIR=0: count up by default TIM1->PSC = 0; // No prescaler on timer clock TIM1->ARR = 4199; // Period = 4200-1 TIM1->RCR = 0; // Repetition counter (not used here)
Centre-aligned mode (CMS=01) counts up to ARR, then down to 0. This doubles the effective period for a given ARR value compared to edge-aligned mode. Centre-aligned PWM is standard for motor drives because it reduces EMI by staggering the switching edges and produces symmetrical current ripple.
Step 3: Channel 1 PWM Configuration
// CH1 PWM mode 1: active when CNT < CCR, inactive when CNT >= CCR TIM1->CCMR1 &= ~(TIM_CCMR1_OC1M | TIM_CCMR1_CC1S); TIM1->CCMR1 |= (TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1); // OC1M=110: PWM mode 1 TIM1->CCMR1 |= TIM_CCMR1_OC1PE; // Output compare preload // Set initial duty cycle: 50% = 2100 TIM1->CCR1 = 2100; // Enable CH1 and CH1N outputs // CC1P=0: active high, CC1NP=0 (CH1N active high after dead-time insertion) TIM1->CCER |= TIM_CCER_CC1E; // CH1 output enable TIM1->CCER |= TIM_CCER_CC1NE; // CH1N output enable TIM1->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP); // Both active high
Important: CH1N is NOT a simple inversion of CH1. TIM1 inserts the dead-time between CH1 going low and CH1N going high, and vice versa. During the dead-band, both outputs are low (inactive state for active-high polarity). The CH1N signal is the logical complement of CH1, shifted by the dead-time delay.
Step 4: Dead-Time and Break Configuration
// Dead-time: 300 ns at 168 MHz (t_CK_INT = 5.95 ns, CKD=00) // DTG = t_DEAD / t_CK_INT = 300 ns / 5.95 ns ≈ 50 // DTG=50 falls in range 0–127 (DTG[7]=0): dead-time = DTG × t_DTS, no scaling TIM1->BDTR = 0; TIM1->BDTR |= (50 << 0); // DTG[7:0] = 50 → ~298 ns dead-time TIM1->BDTR |= TIM_BDTR_BKE; // Break enable TIM1->BDTR |= TIM_BDTR_BKP; // Break active high TIM1->BDTR |= TIM_BDTR_AOE; // Auto output enable after break clears TIM1->BDTR |= TIM_BDTR_MOE; // Main Output Enable — CRITICAL
MOE is write-protected when LOCK[1:0] is non-zero. Even at LOCK=00, ST recommends writing MOE in the final BDTR write, not in the same instruction as DTG/BKE/AOE. On some STM32F4 silicon revisions, writing MOE simultaneously with DTG can cause a one-cycle glitch where outputs briefly enable with DTG=0. A separate write is safer. Also note that if the BRK input is asserted at the moment MOE is set, the outputs will not enable — your over-current protection is functional from power-up.
Step 5: Start the Timer
// Generate an update event to load shadow registers TIM1->EGR |= TIM_EGR_UG; // Enable the counter TIM1->CR1 |= TIM_CR1_CEN;
At this point, PA8 and PA7 should output complementary 20 kHz PWM with a 300 ns dead-band. Verify with an oscilloscope: probe CH1 and CH1N simultaneously and zoom in on the rising/falling edges — you should see both signals low during the transition.
Three Dead-Time Ranges — When DTG Exceeds 127
The BDTR register packs an 8-bit DTG field but supports dead-times far beyond 255 × t_DTS by using three encoding ranges:
| DTG[7:5] | Formula | Step Size | Max at 168 MHz, CKD=00 |
|---|---|---|---|
| 0xx | DTG × t_DTS | 1 × t_DTS | 127 × 5.95 ns = 755 ns |
| 10x | (64 + DTG[5:0]) × 2 × t_DTS | 2 × t_DTS | 127 × 2 × 5.95 ns = 1.51 µs |
| 11x | (32 + DTG[4:0]) × 8 × t_DTS | 8 × t_DTS | 63 × 8 × 5.95 ns = 3.0 µs |
| 111 | (32 + DTG[4:0]) × 16 × t_DTS | 16 × t_DTS | 63 × 16 × 5.95 ns = 6.0 µs |
For an IGBT-based inverter switching at 5 kHz, you might need 2–4 µs dead-time. Set CKD=01 (2 × t_CK_INT) and DTG in the third range. Here is a helper function that computes the correct DTG value for any dead-time:
/**
* Compute TIM1/TIM8 DTG register value for a given dead-time.
* @param dead_time_ns Desired dead-time in nanoseconds
* @param timer_clk_hz Timer input clock frequency (APB2 × timer prescaler)
* @param ckd CKD[1:0] value (0–3)
* @return DTG[7:0] value to write to BDTR
*/
uint8_t compute_dtg(uint32_t dead_time_ns, uint32_t timer_clk_hz, uint8_t ckd) {
uint32_t t_dts_ps = (1000000000000ULL / timer_clk_hz) * (1UL << ckd);
uint32_t dead_ps = dead_time_ns * 1000UL;
// Range 1: 0 ≤ DTG ≤ 127, step = t_DTS
if (dead_ps <= 127UL * t_dts_ps)
return (uint8_t)(dead_ps / t_dts_ps);
// Range 2: DTG=10xxxxxx, step = 2 × t_DTS, offset = 64 × 2 × t_DTS
uint32_t t_dts2 = t_dts_ps * 2;
if (dead_ps <= (127UL * t_dts2))
return 0x80 | (uint8_t)((dead_ps - 64UL * t_dts2) / t_dts2);
// Range 3: DTG=110xxxxx, step = 8 × t_DTS, offset = 32 × 8 × t_DTS
uint32_t t_dts8 = t_dts_ps * 8;
if (dead_ps <= (63UL * t_dts8))
return 0xC0 | (uint8_t)((dead_ps - 32UL * t_dts8) / t_dts8);
// Range 4: DTG=111xxxxx, step = 16 × t_DTS
uint32_t t_dts16 = t_dts_ps * 16;
return 0xE0 | (uint8_t)((dead_ps - 32UL * t_dts16) / t_dts16);
}
Break Input: The Hardware Safety Net
The break input is not optional in power-stage designs. Wire it directly from the gate-driver's fault output (IR2110 pin 5, L6387 pin 6, or a comparator monitoring the shunt resistor). When the BRK pin is asserted, TIM1 hardware immediately forces all channel outputs to their safe state — regardless of CPU state, interrupt latency, or software bugs.
Configure the break action with TIM1_BDTR and the optional break filter in TIM1_BDTR_BKF:
// Break filter: N=4 consecutive samples at f_DTS to debounce the BRK input
TIM1->BDTR |= (4 << 16); // BKF[3:0] = 4 → BRK must be stable for 4 × t_DTS
// After a break: clear the flag, re-enable MOE
// In your ISR or fault handler:
void TIM1_BRK_IRQHandler(void) {
if (TIM1->SR & TIM_SR_BIF) {
TIM1->SR &= ~TIM_SR_BIF; // Clear break interrupt flag
// All outputs are already in safe state. Log the event.
// DO NOT blindly re-enable MOE — first check that
// the external fault condition has been cleared.
}
}
The break source can also be triggered by software (TIM1_EGR |= TIM_EGR_BG) or by the clock failure event on some STM32 families — useful for testing your fault handler without shorting actual hardware.
How I Would Approach This on a Client Project
When I deliver a motor-drive or power-converter firmware module, the TIM1 configuration follows a strict sequence that I have refined over multiple client projects:
- Create a
tim1_pwm_init(TIM1_Config *cfg)function with a struct containing all parameters: frequency, dead-time_ns, polarity, break polarity, initial duty. No magic numbers anywhere. - Add a
tim1_set_duty(uint16_t duty)helper that writesCCR1with the preload already enabled — this guarantees no glitch mid-cycle. - Verify dead-time with a scope on every prototype. Even if the register math is correct, gate-driver propagation delay differences between high-side and low-side paths can eat into your dead-band. Measure, don't assume.
- Wire the break input to a fault latch. The gate-driver fault output is often a pulse, not a level. Add an SR latch (or use the MCU's internal EXTI with a flag) so the break stays asserted until firmware acknowledges it.
- Test the break path at every board power-up. Assert BRK via software (
EGR.BG) and confirm all outputs go to the safe state. Then clear it and verify PWM resumes cleanly. This takes 30 seconds and catches wiring errors that would be destructive at full power. - Never change dead-time at runtime. The BDTR register has write-protection levels that can interfere. If you need variable dead-time for different operating points, configure the worst-case value and use a different modulation strategy.
Common Pitfalls
- MOE never set. The most frequent bring-up issue: timer is running, CCR is loaded, but no signal on the pins.
BDTR.MOEdefaults to 0 after reset. You must set it explicitly. - AF not configured on CHxN pin. CH1 (PA8) gets configured, but PA7 (CH1N) is left as GPIO input. The timer generates the complementary waveform internally but it never reaches the pin.
- GPIO output speed too low. At 168 MHz, GPIO output rise/fall times dominate switching losses. Set
GPIOx->OSPEEDRto 11 (very high speed) for all timer output pins. - Break pin floating. If BKE is set but the BRK pin is unconnected (or pulled to the active level by a weak internal pull-up/down), the outputs will never enable. Always verify the BRK pin state with a multimeter before debugging the timer.
- Using TIM1 CHxN without dead-time. If you want complementary outputs without dead-time (e.g., for a differential signalling application), set DTG=0. The CHxN output will be the exact inverse of CHx, edge-aligned.
Practical Checklist
- ☐ Clock enabled for GPIOA, GPIOB, and TIM1 (
RCC->AHB1ENR,RCC->APB2ENR). - ☐ GPIO AF set to AF1 for CH1, CH1N, and BRK pins.
- ☐ GPIO output speed set to 11 (very high) on all PWM pins.
- ☐ TIM1_CR1 configured with CMS=01 (centre-aligned), ARPE=1.
- ☐ ARR and CCR loaded; prescaler set to achieve desired PWM frequency.
- ☐ CCMR1 configured for PWM mode 1 with preload enabled.
- ☐ CCER: CC1E=1, CC1NE=1, polarities as required.
- ☐ BDTR: DTG calculated, BKE/BKP configured, MOE=1 written LAST in a separate instruction.
- ☐ Break pin externally pulled to inactive level; verified with multimeter.
- ☐ Scope check: CH1 and CH1N both low during dead-band transitions.
- ☐ Software break test:
TIM1->EGR |= TIM_EGR_BG→ outputs go safe; clear → PWM resumes.
Choosing the Right Timer Mode for Your Topology
| Application | Timer | Mode | Channels | Dead-Time |
|---|---|---|---|---|
| Half-bridge DC-DC | TIM1 CH1 | Centre-aligned | CH1 + CH1N | 200–500 ns |
| 3-phase BLDC | TIM1 CH1–CH3 | Centre-aligned | CH1/1N + CH2/2N + CH3/3N | 500–2000 ns |
| H-bridge DC motor | TIM1 CH1–CH2 | Edge-aligned | CH1/1N + CH2/2N | 300–800 ns |
| LLC resonant | TIM1 CH1 | Edge-aligned | CH1 + CH1N | 200–400 ns |
| BLDC with Hall sensors | TIM1 CH1–CH3 + TIM4 Hall | Centre-aligned | CH1/1N + CH2/2N + CH3/3N | Use commutation, not dead-time PWM |
References
- RM0090: STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 Reference Manual — Chapter 17: Advanced-Control Timers (TIM1 & TIM8)
- RM0440: STM32G4 Series Reference Manual — Chapter 26: Advanced-Control Timers
- AN4013: STM32 Cross-Series Timer Overview
- STM32CubeF4 TIM Examples (GitHub) — Complementary Signals, 6-Step, PWM Input
- International Rectifier AN-978: HV Floating MOS-Gate Driver ICs (IR2110/IR2113 application note)
- STM32F4xx HAL Driver: stm32f4xx_hal_tim.c — HAL_TIM_PWM_ConfigChannel(), HAL_TIMEx_ConfigBreakDeadTime()
Comments
Have you debugged a dead-time configuration issue on STM32? Send me an email — I update the article with real-world findings.

Comments
Have comments? Send me an email.