2026-06-10 · Davide Carrese

STM32 Advanced Timer PWM:
Dead-Time Insertion and Complementary Outputs

STM32 · TIM1 · PWM · Dead-Time · Register-Level

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:

  1. Complementary outputs (CHxN). Each channel can drive a second pin with the inverted waveform — essential for half-bridge and full-bridge topologies.
  2. 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.
  3. 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:

BitsFieldDescription
15MOEMain Output Enable — must be set to 1 for any output to appear on the pins
14AOEAutomatic Output Enable — if set, MOE is re-asserted automatically after the next update event following a break
13BKPBreak Polarity — 0: break active low, 1: break active high
12BKEBreak Enable — 1 to enable the BRK input
11OSSROff-State Selection for Run mode — 0: outputs inactive when MOE=0, 1: outputs in idle state
10OSSIOff-State Selection for Idle mode — same logic for idle state
9–8LOCKLock configuration: 00 = no lock, 01/10/11 = progressively locked write protection levels
7–0DTG[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_DTSMax Dead-Time at 168 MHz
001 × t_CK_INT255 × 5.95 ns = 1.52 µs
012 × t_CK_INT255 × 11.90 ns = 3.04 µs
104 × t_CK_INT255 × 23.81 ns = 6.07 µs
11Reserved (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 Must Be Set AFTER All Other BDTR Fields

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]FormulaStep SizeMax at 168 MHz, CKD=00
0xxDTG × t_DTS1 × t_DTS127 × 5.95 ns = 755 ns
10x(64 + DTG[5:0]) × 2 × t_DTS2 × t_DTS127 × 2 × 5.95 ns = 1.51 µs
11x(32 + DTG[4:0]) × 8 × t_DTS8 × t_DTS63 × 8 × 5.95 ns = 3.0 µs
111(32 + DTG[4:0]) × 16 × t_DTS16 × t_DTS63 × 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:

  1. 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.
  2. Add a tim1_set_duty(uint16_t duty) helper that writes CCR1 with the preload already enabled — this guarantees no glitch mid-cycle.
  3. 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.
  4. 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.
  5. 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.
  6. 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

Practical Checklist

  1. ☐ Clock enabled for GPIOA, GPIOB, and TIM1 (RCC->AHB1ENR, RCC->APB2ENR).
  2. ☐ GPIO AF set to AF1 for CH1, CH1N, and BRK pins.
  3. ☐ GPIO output speed set to 11 (very high) on all PWM pins.
  4. ☐ TIM1_CR1 configured with CMS=01 (centre-aligned), ARPE=1.
  5. ☐ ARR and CCR loaded; prescaler set to achieve desired PWM frequency.
  6. ☐ CCMR1 configured for PWM mode 1 with preload enabled.
  7. ☐ CCER: CC1E=1, CC1NE=1, polarities as required.
  8. ☐ BDTR: DTG calculated, BKE/BKP configured, MOE=1 written LAST in a separate instruction.
  9. ☐ Break pin externally pulled to inactive level; verified with multimeter.
  10. ☐ Scope check: CH1 and CH1N both low during dead-band transitions.
  11. ☐ Software break test: TIM1->EGR |= TIM_EGR_BG → outputs go safe; clear → PWM resumes.

Choosing the Right Timer Mode for Your Topology

ApplicationTimerModeChannelsDead-Time
Half-bridge DC-DCTIM1 CH1Centre-alignedCH1 + CH1N200–500 ns
3-phase BLDCTIM1 CH1–CH3Centre-alignedCH1/1N + CH2/2N + CH3/3N500–2000 ns
H-bridge DC motorTIM1 CH1–CH2Edge-alignedCH1/1N + CH2/2N300–800 ns
LLC resonantTIM1 CH1Edge-alignedCH1 + CH1N200–400 ns
BLDC with Hall sensorsTIM1 CH1–CH3 + TIM4 HallCentre-alignedCH1/1N + CH2/2N + CH3/3NUse commutation, not dead-time PWM

References

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.