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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
