PWM Generation Patterns#

PWM output is the most common timer application in embedded systems — LED dimming, motor speed control, servo positioning, and switch-mode power supply gate drive all rely on precisely timed high/low transitions. The STM32 timer peripheral generates hardware PWM without CPU intervention once configured: the counter free-runs, the output compare unit toggles the pin, and the CPU only intervenes to change the duty cycle. Getting from “a PWM signal exists” to “a production-quality PWM driver” requires understanding output compare modes, complementary outputs, dead-time insertion, and the break input — all of which live in timer registers, not software loops.

Output Compare Modes: PWM Mode 1 and 2#

Each timer channel has a Capture/Compare Register (CCRx) that sets the duty cycle threshold. In PWM Mode 1 (the most common), the output is active (high) when CNT < CCRx and inactive (low) when CNT >= CCRx. PWM Mode 2 inverts this: active when CNT >= CCRx.

ModeOutput when CNT < CCROutput when CNT >= CCR
PWM Mode 1Active (high)Inactive (low)
PWM Mode 2Inactive (low)Active (high)

Duty cycle is simply CCR / (ARR + 1) for edge-aligned PWM. With ARR = 999 and CCR = 250, duty = 25.0%. Setting CCR = 0 produces 0% duty (output always low), and CCR > ARR produces 100% duty (output always high).

/* 1 kHz PWM on TIM3 CH1 (PA6), 84 MHz timer clock */
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();

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);

TIM_HandleTypeDef htim3 = {0};
htim3.Instance               = TIM3;
htim3.Init.Prescaler         = 84 - 1;     /* 1 MHz tick */
htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
htim3.Init.Period            = 1000 - 1;    /* 1 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      = 500;              /* 50% 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);

Changing the duty cycle at runtime requires only a CCR write:

/* Update duty cycle to 75% without stopping the timer */
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 750);

The CCR register is double-buffered by default in PWM mode (preload enabled via OCxPE bit). The new value takes effect at the next update event, preventing glitches mid-cycle.

PWM Resolution#

The ARR value directly determines the number of discrete duty cycle steps. With ARR = 99, only 100 duty levels exist (0% to 100% in 1% steps). For LED dimming where human perception is logarithmic, coarse resolution produces visible stepping at low brightness. For power converters, coarse resolution increases output ripple.

ARRDuty stepsResolutionMax PWM freq at 84 MHz (PSC=0)
991001.0%840 kHz
99910000.1%84 kHz
41994200~0.024%20 kHz
6553565536~0.0015%1.28 kHz

A tradeoff exists between PWM frequency and duty cycle resolution — higher frequency requires a smaller ARR, reducing the number of available duty steps.

Edge-Aligned vs Center-Aligned PWM#

In edge-aligned (count-up) mode, the counter resets to 0 at overflow, producing a single switching edge per period. In center-aligned mode, the counter counts up to ARR then back down to 0, producing two compare matches per cycle — one on the way up, one on the way down. The output is naturally symmetric around the center of the period.

Center-aligned PWM is standard for three-phase motor control because:

  • Switching events across phases are staggered, reducing DC bus current ripple
  • The symmetric waveform has lower harmonic distortion
  • ADC sampling at the counter peak or valley coincides with the midpoint of the current waveform, improving measurement accuracy
/* Center-aligned PWM for motor control on TIM1 */
htim1.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1;
htim1.Init.Period      = 4200 - 1;  /* 168 MHz / 4200 / 2 = 20 kHz */

Note: in center-aligned mode, the effective PWM frequency is f_clk / ((PSC+1) * 2 * ARR) because the counter traverses ARR twice per period (up and down).

Complementary Outputs and Dead-Time Insertion#

Advanced timers (TIM1 and TIM8 on STM32F4/H7) provide complementary output pairs: CH1/CH1N, CH2/CH2N, CH3/CH3N. These drive half-bridge configurations where an N-channel high-side MOSFET and an N-channel low-side MOSFET must never conduct simultaneously.

The Break and Dead-Time Register (BDTR) controls dead-time insertion — a brief interval where both outputs are inactive, preventing shoot-through. The dead-time generator (DTG field, 8 bits) specifies the delay in timer clock cycles, with a nonlinear encoding:

DTG[7:5]Dead-time formulaRange at 168 MHz
0xxDTG[6:0] × t_DTS0–762 ns
10x(64 + DTG[5:0]) × 2 × t_DTS762 ns–1.52 µs
110(32 + DTG[4:0]) × 8 × t_DTS1.52–3.02 µs
111(32 + DTG[4:0]) × 16 × t_DTS3.02–6.0 µs

where t_DTS depends on the CKD (clock division) bits in TIMx_CR1.

/* TIM1 complementary PWM with 500 ns dead-time at 168 MHz */
TIM_BreakDeadTimeConfigTypeDef bdt = {0};
bdt.DeadTime        = 84;                  /* 84 × (1/168 MHz) ≈ 500 ns */
bdt.BreakState      = TIM_BREAK_ENABLE;
bdt.BreakPolarity   = TIM_BREAKPOLARITY_HIGH;
bdt.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;
bdt.OffStateRunMode = TIM_OSSR_ENABLE;
bdt.OffStateIDLEMode = TIM_OSSI_ENABLE;
HAL_TIMEx_ConfigBreakDeadTime(&htim1, &bdt);

/* Start complementary PWM on CH1 and CH1N */
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);

Break Input: Emergency Shutdown#

The break input (BKIN pin) provides a hardware path to disable all timer outputs within one clock cycle — no interrupt latency, no software involvement. When the break input activates, all outputs transition to a predefined safe state (configured via OSSR and OSSI bits).

Typical applications:

  • Overcurrent detection from a comparator output drives BKIN
  • Motor controller fault (e.g., gate driver FAULT output)
  • External emergency stop signal
/* Break input on PA6 (BKIN of TIM1) — active high */
bdt.BreakState    = TIM_BREAK_ENABLE;
bdt.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
bdt.BreakFilter   = 0xF;  /* Maximum input filter for noise rejection */

After a break event, the MOE (Main Output Enable) bit in BDTR is cleared. With AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE, MOE re-asserts automatically at the next update event after the break input de-asserts. Without automatic output, firmware must explicitly set MOE to resume operation.

MOSFET Drive Patterns#

PWM outputs ultimately drive power transistors. The polarity and dead-time requirements depend on the bridge topology:

Low-side N-channel MOSFET: Direct drive from a push-pull timer output. PWM Mode 1 with active-high polarity drives the gate high to turn on. No complementary output needed.

Half-bridge (high-side + low-side N-channel): Requires complementary outputs with dead-time. The high-side MOSFET needs a bootstrap or charge pump gate driver (e.g., IR2110, DRV8301). The timer drives the gate driver inputs; the driver handles level shifting.

Full H-bridge: Two complementary channel pairs (CH1/CH1N for one leg, CH2/CH2N for the other). Direction control comes from swapping which channel pair is PWM-modulated and which is held static.

Tips#

  • Set CCR preload enable (OCxPE) in PWM mode — this is automatic when using HAL_TIM_PWM_ConfigChannel(), but when writing registers directly, forgetting to set CCMRx.OCxPE causes duty cycle changes to take effect mid-cycle, producing output glitches.
  • For LED dimming, use ARR >= 1000 to get at least 10-bit equivalent resolution — human eyes detect brightness steps below about 8-bit resolution, especially at low duty cycles.
  • Calculate dead-time from the MOSFET gate driver’s turn-off propagation delay plus the MOSFET’s turn-off time — adding 20–50% margin covers temperature variation and component tolerance. Typical dead-time for low-voltage MOSFETs is 100–500 ns.
  • On ESP32, the MCPWM (Motor Control PWM) peripheral provides hardware dead-time and complementary outputs — use mcpwm_deadtime_enable() rather than implementing dead-time in software.

Caveats#

  • MOE (Main Output Enable) defaults to 0 on advanced timers — TIM1 and TIM8 outputs remain tristated until MOE is set; HAL_TIM_PWM_Start() sets MOE automatically, but direct register code that forgets TIM1->BDTR |= TIM_BDTR_MOE produces no output with no error indication.
  • Dead-time encoding is nonlinear — the DTG field does not map linearly to nanoseconds; a DTG value of 200 does not produce twice the dead-time of DTG=100. The four encoding ranges in BDTR must be consulted, or HAL_TIMEx_ConfigBreakDeadTime() used for correct computation.
  • Complementary outputs require GPIO AF configuration for both CHx and CHxN pins — configuring only the main channel pin while forgetting the complementary pin is a common omission that leaves CHxN floating.
  • Break filter introduces latency — setting BreakFilter to maximum (0xF) adds up to 32 timer clock cycles of input delay; for overcurrent protection, this delay (190 ns at 168 MHz) must be short enough to protect the power stage.
  • PWM duty cycle of exactly 0% and 100% behave differently across modes — in PWM Mode 1, CCR=0 produces a constant low output, but CCR=ARR+1 (not CCR=ARR) is needed for true 100% duty; CCR=ARR produces a brief low pulse at the counter overflow.

In Practice#

  • A motor driver that exhibits shoot-through (both high-side and low-side conducting simultaneously) on startup, blowing the fuse or tripping the protection, typically results from MOE being set before the dead-time configuration is applied — configure BDTR completely before enabling outputs.
  • A PWM signal that appears correct on the scope but has a narrow glitch at the duty cycle transition point indicates the CCR preload is not enabled — the new CCR value takes effect immediately rather than at the update event, causing a partial cycle.
  • An LED that flickers visibly at low brightness despite a stable CCR value often traces to insufficient PWM frequency — below 200 Hz, human flicker perception is engaged; increasing the timer frequency to 1 kHz or above while maintaining adequate ARR resolution resolves the flicker.
  • A complementary output that measures 0 V on CHxN despite correct CH output typically reveals a missing GPIO alternate function configuration on the complementary pin — both the main and N-channel pins must be configured as AF push-pull with the correct AF number.
  • A half-bridge that runs normally but fails under heavy load commonly indicates insufficient dead-time — the MOSFET turn-off time increases with temperature and load current, and marginal dead-time values that work on the bench fail at operating temperature.
Page last modified: February 28, 2026