我们身边的许多电子产品,如汽车、风扇、照明灯和玩具,都离不开PWM(脉冲宽度调制)技术的默默支持。它通过调节脉冲信号的特性,来实现对电机速度、灯光亮度等的精细控制。本文将深入探讨单片机实现PWM输出的几种方式,并以STM32为例,详细讲解硬件PWM的配置步骤与常见问题。
什么是PWM?
PWM的全称是Pulse Width Modulation,即脉冲宽度调制。
网上的解释可能有些抽象,但通过下面这张图,你就能直观地理解PWM——它其实就是由一系列高低电平变化组成的脉冲信号。

通过改变这个信号的频率(脉冲周期)和占空比(高电平在一个周期内的比例),就能将其应用在各种各样的场合,这也是嵌入式开发中控制外部设备的常用手段。

PWM常见输出方式
从本质上讲,产生PWM波就是控制一个IO口以特定的时间周期,交替输出高电平和低电平。实现这一目标的方法有多种,我们可以根据对CPU资源的占用和实现复杂度,将其分为几个级别。
1. 新手级别
在while循环中,使用阻塞延时来控制IO口高低电平输出:
while(1)
{
IO口高电平
Delay阻塞延时
IO口低电平
Delay阻塞延时
}
这里的阻塞延时可以是简单的软件模拟延时,也可以是定时器的阻塞等待。这种方法简单直白,但会完全占用CPU,且延时精度容易受干扰。
2. 入门级别
在while循环中,使用非阻塞延时来控制IO口:
while(1)
{
IO口高电平
Delay非阻塞延时
IO口低电平
Delay非阻塞延时
}
非阻塞延时可以通过检测定时器标志位,或者利用RTOS(实时操作系统)提供的延时函数来实现。这种方法释放了CPU,允许其在延时期间处理其他任务,但程序流程依然需要不断查询状态。
3. 熟悉级别
利用定时器中断来控制IO口高低电平:
配置定时器中断 -> 启动定时器 -> 在中断服务函数中翻转IO口电平...
这种方法将PWM生成的时序完全交给定时器硬件和中断机制,主程序无需干预波形生成,效率更高。
4. 熟练级别
直接使用单片机的硬件PWM外设功能:
配置PWM对应的IO引脚,以及定时器的PWM输出模式 -> 设置频率和占空比 -> 启动定时器,PWM波形自动输出...
这是最理想的方式,配置完成后,硬件会自动输出PWM波,完全无需CPU干预。应用程序可以这样调用:
void AppTask(void *p_arg)
{
PWM_TIM_Configuration(); // 硬件配置
PWM_Output(频率, 占空比); // 设定输出参数
while(1)
{
//自己的应用代码
}
}
几种方式的比较:
前三种方式都需要CPU不同程度地参与PWM波的生成,会占用CPU资源。特别是前两种,不仅占用高,精度也相对较差。第三种中断方式在PWM频率较高时,也会频繁打断CPU,消耗可观的资源。而第四种硬件PWM方式,则是将这项工作完全交给了定时器外设,是高效且精准的首选方案。
硬件输出PWM实例(以STM32F1为例)
下面我们以常见的STM32F103系列单片机为例,详细讲解如何使用硬件定时器输出PWM波形。
首先,定义PWM定时器相关的宏,这决定了定时器的计数时钟基础。
//定时器计数时钟(1M次/秒)
#define PWM_COUNTER_CLOCK 1000000
//预分频值(与系统时钟、计数值有关)
#define PWM_PRESCALER_VALUE (SystemCoreClock/PWM_COUNTER_CLOCK - 1)
PWM硬件配置函数
这个函数完成了GPIO、定时器时基和PWM输出通道的初始化。
/**
* @brief 定时器PWM输出配置
* @param 无
* @retval 无
*/
void PWM_TIM_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
/* 时钟配置 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
/* 引脚配置:以PA0(TIM2_CH1)为例 */
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);
/* 时基配置 */
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模式配置:通道1 */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
TIM_OCInitStructure.TIM_Pulse = 0; //初始占空比(比较值,暂定)
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:高电平有效
TIM_OC1Init(TIM2, &TIM_OCInitStructure); //初始化通道1
}
PWM输出控制函数
这个函数用于动态调整PWM波的频率和占空比。
/**
* @brief 输出PWM
* @param Frequency:频率
Dutycycle:占空比(0-100)
* @retval 无
*/
void PWM_Output(uint32_t Frequency, uint32_t Dutycycle)
{
uint32_t tim_period;
uint32_t tim_pulse;
tim_period = PWM_COUNTER_CLOCK/Frequency - 1; //计算出计数周期(决定输出频率)
tim_pulse = (tim_period + 1)*Dutycycle / 100; //计算出脉宽值(决定PWM占空比)
TIM_Cmd(TIM2, DISABLE); //修改参数前先关闭定时器
TIM_SetCounter(TIM2, 0); //计数清零
TIM_SetAutoreload(TIM2, tim_period); //更改自动重装载值(频率)
TIM_SetCompare1(TIM2, tim_pulse); //更改比较值(占空比)
TIM_Cmd(TIM2, ENABLE); //重新使能定时器
}
应用代码示例
在应用程序中,只需初始化并调用输出函数即可。
void AppTask(void *p_arg)
{
PWM_TIM_Configuration(); // 硬件配置
PWM_Output(1000, 20); // 输出1KHz频率,20%占空比的PWM波
while(1)
{
//应用代码
}
}
使用逻辑分析仪测量上述代码输出的波形,可以看到准确的1KHz、20%占空比方波。

说明: 本例使用的是STM32标准外设库,有助于深入理解寄存器和底层原理。如果你想快速实现功能,也可以使用STM32CubeMX工具进行图形化配置,它能自动生成初始化代码。

硬件PWM配置注意事项
想要更精确地控制PWM并满足复杂应用需求,就需要深入了解其原理。下面列举几个常见的配置要点。
1. 引脚重映射
有些定时器通道的引脚是默认的,有些则需要重映射(Remap)。例如,如果你想使用STM32F1的PB11引脚作为TIM2_CH4输出PWM,就需要查看数据手册的引脚定义表。

从表中可以看到PB11的复用功能包含TIM2_CH4,但通常它可能不是默认映射,此时需要在代码中开启重映射功能:
//使能AFIO时钟(重映射必需)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//将TIM2通道4完全重映射到PB11(具体映射方式需查手册)
GPIO_PinRemapConfig(GPIO_FullRemap_TIM2, ENABLE);
2. 频率与占空比精度
精度受限于定时器的位数。如果使用16位定时器,那么周期值(TIM_Period)和比较值(TIM_Pulse)都不能超过65535。我们的计算函数:
tim_period = PWM_COUNTER_CLOCK/Frequency - 1;
tim_pulse = (tim_period + 1)*Dutycycle / 100;
需要确保tim_period和tim_pulse的值在0-65535之间。如果想获得极低的频率(如0.01Hz)或极高的占空比精度(0.01%),可以考虑使用32位定时器(如STM32F4系列的TIM2/TIM5),其计数范围大大增加,精度也更高。
3. 更多细节
不同系列的STM32单片机,其硬件PWM的配置寄存器可能略有差异,建议在开发时多参考对应型号的官方参考手册和标准外设库例程。如今大多数现代单片机都集成了硬件PWM功能,这是最推荐的方式。如果你的单片机没有硬件PWM,那么上文提到的“定时器中断”方式是一个不错的软件替代方案。
希望本文对你在云栈社区学习和探索嵌入式技术有所帮助。通过理解从软件模拟到硬件控制的演进,你能更好地根据项目需求选择最合适的PWM实现方式,从而设计出更高效、更可靠的产品。