Fundamentals 8 min read

Mastering PWM on STM32: From Simple Loops to Hardware Timer Output

This article explains PWM fundamentals, compares several output methods ranging from blocking loops to hardware timer generation, and provides complete STM32F1 example code—including macro definitions, timer configuration, output functions, and practical tips on pin mapping and precision.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Mastering PWM on STM32: From Simple Loops to Hardware Timer Output

PWM (Pulse Width Modulation) is widely used for motor speed control, LED dimming, communication modulation, and many other applications. By adjusting frequency and duty cycle, a digital signal can emulate analog behavior.

What is PWM?

PWM is a pulse signal composed of high and low voltage levels. Changing the period (frequency) and the proportion of the high level within each period (duty cycle) yields different output effects.

PWM Output Methods

The article classifies five typical PWM implementation levels:

Beginner (blocking loop) : Use a while loop with blocking delays to toggle an IO pin high and low.

while(1) {
    // set pin high
    Delay();
    // set pin low
    Delay();
}

Entry (non‑blocking loop) : Replace blocking delays with non‑blocking mechanisms such as timer checks or RTOS delays.

while(1) {
    // set pin high
    NonBlockingDelay();
    // set pin low
    NonBlockingDelay();
}

Intermediate (timer interrupt) : Configure a timer interrupt that toggles the pin, reducing CPU load but still requiring interrupt handling.

Timer interrupt → enable → ISR toggles IO level.

Advanced (hardware PWM) : Use the MCU’s built‑in PWM peripheral; after configuring the timer and output pin, the hardware automatically generates the waveform without CPU intervention.

Configure PWM‑capable IO → start timer → hardware outputs PWM.

Comparison : The first three methods involve CPU intervention and can introduce timing errors, whereas hardware PWM offers precise, CPU‑free output.

Hardware PWM Example on STM32F1

The following example shows how to generate a 1 kHz PWM signal with 20 % duty cycle using the STM32 standard peripheral library.

Macro Definitions

// Timer counting clock (1 MHz)
#define PWM_COUNTER_CLOCK         1000000
// Prescaler value (depends on system clock)
#define PWM_PRESCALER_VALUE       (SystemCoreClock/PWM_COUNTER_CLOCK - 1)

Timer Configuration Function

/**
  * @brief  Configure timer for PWM output
  */
void PWM_TIM_Configuration(void)
{
    GPIO_InitTypeDef        GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef       TIM_OCInitStructure;

    /* Enable clocks */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    /* GPIO configuration */
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* Time base configuration */
    TIM_TimeBaseStructure.TIM_Prescaler = PWM_PRESCALER_VALUE;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    /* PWM mode configuration */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0; // initial pulse width
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
}

PWM Output Function

/**
  * @brief  Output PWM with given frequency and duty cycle
  * @param  Frequency  Desired frequency (Hz)
  * @param  Dutycycle  Desired duty cycle (0‑100)
  */
void PWM_Output(uint32_t Frequency, uint32_t Dutycycle)
{
    uint32_t tim_period = PWM_COUNTER_CLOCK / Frequency - 1;
    uint32_t tim_pulse  = (tim_period + 1) * Dutycycle / 100;

    TIM_Cmd(TIM2, DISABLE);
    TIM_SetCounter(TIM2, 0);
    TIM_SetAutoreload(TIM2, tim_period);
    TIM_SetCompare1(TIM2, tim_pulse);
    TIM_Cmd(TIM2, ENABLE);
}

Application Task

void AppTask(void *p_arg)
{
    PWM_TIM_Configuration();
    PWM_Output(1000, 20); // 1 kHz, 20 % duty
    while(1) {
        // user code
    }
}

The resulting waveform is shown below:

Configuration Tips

Pin Remapping : If the chosen pin requires alternate function mapping, enable the AFIO clock and call GPIO_PinRemapConfig(GPIO_FullRemap_TIM2, ENABLE);.

Frequency & Duty‑Cycle Precision : Using a 32‑bit timer expands the achievable range (e.g., 0.01 Hz, 0.01 % duty). With a 16‑bit timer, values must stay below 65535.

Parameter Validation : In production code, add checks to prevent overflow of period or pulse values.

Series Differences : Different STM32 families may have slight variations in PWM registers; consult the reference manual and official examples.

When a microcontroller lacks a hardware PWM channel, the timer‑interrupt method can be used as a fallback.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

HardwaretimerMicrocontrollerPWMSTM32
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.