Keil5下STM32 PWM输出实战:从原理到呼吸灯的完整实现
你有没有试过用一个电位器调LED亮度,结果发现调节不顺、手感差还容易坏?或者想控制电机转速,却发现电压调起来像“一档、二档”那样生硬?
其实这些问题,早在几十年前就有了解决方案——PWM(脉宽调制)。而今天,我们手里的STM32微控制器,配合Keil5开发环境,就能以极低成本、超高精度实现这一切。
本文不是手册翻译,也不是寄存器堆砌,而是带你真正搞懂PWM是怎么跑起来的,并一步步在Keil5中配置出可调占空比的PWM信号,最终做出一个流畅的“呼吸灯”效果。全程基于HAL库 + STM32CubeMX辅助生成代码,适合刚入门嵌入式的新手和需要快速上手项目的工程师。
为什么STM32做PWM又快又好?
先说结论:硬件定时器自动翻转IO,CPU几乎不参与,精准又省力。
传统软件延时模拟PWM(比如HAL_GPIO_WritePin(); delay_us();)有个致命问题——只要中断一打断,波形就变形。更别提你要同时控制多个设备了,根本忙不过来。
而STM32内置了多个通用定时器(TIM2~TIM5等),它们就像一个个独立的小闹钟,能自己计数、比较、翻转GPIO,完全不需要CPU盯着。你只需要告诉它:“每1ms响一次”,“高电平持续0.3ms”,剩下的事它全包了。
这就好比你让一个人手动开关水龙头来控制平均水流大小 vs 安装一个电磁阀由定时器自动控制开闭时间。哪个更稳、更准、更省人力?答案显而易见。
PWM到底是什么?一句话讲清楚
PWM = 固定频率的方波 + 可变的高电平时间。
听起来抽象?换个说法:
- 占空比10% → IO在一个周期里只亮10%的时间 → 平均电压是电源电压的10%
- 占空比90% → 亮90%的时间 → 看起来就很亮
- 频率够高(>100Hz)→ 肉眼看不出闪烁 → 感觉就是“连续变暗/变亮”
所以,PWM的本质是用数字开关动作去逼近模拟量输出。STM32干这个活,简直是降维打击。
核心部件:定时器是如何生成PWM的?
STM32的PWM主要靠通用定时器完成,比如TIM3、TIM4。它的内部结构并不复杂,关键角色只有三个:
| 组件 | 作用 |
|---|---|
| 计数器(CNT) | 自动递增,从0加到ARR后归零,形成周期 |
| 自动重装载寄存器(ARR) | 决定计数上限,即PWM周期长度 |
| 捕获/比较寄存器(CCR) | 设定翻转点,决定占空比 |
工作流程很简单:
- 定时器启动,CNT开始往上加(0 → 1 → 2 …)
- 当CNT < CCR 时,输出高电平;
- 当CNT ≥ CCR 时,输出低电平;
- CNT达到ARR后归零,重新开始下一周期。
这就是所谓的PWM模式1(向上计数有效)。
📌 举个例子:
假设定时器时钟为1MHz(每个计数耗时1μs),ARR=999(共1000个计数),CCR=250
→ 周期 = 1000 × 1μs = 1ms → 频率 = 1kHz
→ 高电平时间 = 250 × 1μs = 250μs → 占空比 = 25%
公式总结一下:
$$
f_{PWM} = \frac{f_{CLK}}{(PSC+1) \times (ARR+1)}, \quad \text{Duty} = \frac{CCR}{ARR+1}
$$
其中PSC是预分频器,用来降低输入时钟频率,方便得到合适的PWM频率。
实战配置:用Keil5 + CubeMX点亮第一路PWM
我们现在要做的,是在PA6引脚上输出一路1kHz、初始50%占空比的PWM,并通过主循环动态调整,做出呼吸灯效果。
第一步:用STM32CubeMX画出硬件连接
虽然最终代码在Keil5里运行,但强烈建议先用STM32CubeMX图形化配置,避免手敲寄存器出错。
操作步骤如下:
- 打开CubeMX,选择你的芯片型号(如STM32F103C8T6)
- 在Pinout图中找到PA6,点击设为TIM3_CH1(表示使用TIM3通道1)
- 进入System Core → RCC,启用外部晶振(HSE)
- 进入Clock Configuration,设置系统主频为72MHz(F1系列最大值)
- 进入Timers → TIM3,配置如下:
- Clock Source: Internal Clock
- Channel1: PWM Generation CH1
- Prescaler (PSC): 71 → 分频后时钟 = 72MHz / 72 = 1MHz
- Counter Period (ARR): 999 → 周期1000 → 1kHz频率 - 点击“Generate Code”,选择MDK-ARM(Keil)格式导出项目
这样,CubeMX会自动生成初始化代码,包括时钟树、GPIO复用、定时器配置等全套内容。
第二步:打开Keil5工程,查看关键函数
导入生成的.uvprojx文件到Keil5后,你会看到几个重要函数出现在main.c中:
void MX_TIM3_PWM_Init(void); // 初始化TIM3为PWM模式 void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* htim_pwm); // 底层硬件初始化以及两个全局句柄:
TIM_HandleTypeDef htim3;这些都不用改,CubeMX已经帮你写好了。
第三步:启动PWM并动态调节占空比
在main()函数中,加入以下逻辑:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_PWM_Init(); // 启动TIM3通道1的PWM输出(对应PA6) HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); uint16_t duty = 0; uint8_t direction = 1; // 1表示增加,0表示减少 while (1) { // 更新占空比 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty); if (direction) duty += 5; else duty -= 5; if (duty >= 1000) direction = 0; if (duty == 0) direction = 1; HAL_Delay(10); // 控制变化速度,约2秒一个来回 } }关键点解析:
HAL_TIM_PWM_Start()是必须调用的!否则即使配置好了,IO也不会输出PWM。__HAL_TIM_SET_COMPARE()是宏,直接修改CCR寄存器值,效率极高。- 使用
duty变量从0到1000线性变化,模拟呼吸灯的渐亮渐灭。 HAL_Delay(10)控制每步间隔10ms,整个周期约4秒,视觉舒适。
烧录程序后,接一个LED和限流电阻到PA6,就能看到柔和的呼吸效果!
寄存器级理解:HAL库背后发生了什么?
你以为只是调了个函数?其实HAL库默默做了很多事。我们来看看MX_TIM3_PWM_Init()里究竟干了啥:
htim3.Instance = TIM3; htim3.Init.Prescaler = 71; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 999; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim3);这段代码设置了定时器的基本参数:
- PSC=71 → 输入时钟72MHz → 定时器时钟变为1MHz
- ARR=999 → 计数0~999共1000步 → 每步1μs → 周期1ms → 频率1kHz
接着配置输出通道:
sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; // 初始CCR值 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);- OCMode设为PWM1 → 向上计数时,CNT < CCR 输出高电平
- Pulse=500 → 初始占空比50%
- 极性为HIGH → 正常输出,高电平有效
最后HAL库还会调用HAL_TIM_PWM_MspInit()来配置:
- GPIOA第6脚为复用推挽输出(AFPP)
- 开启TIM3时钟
- 设置NVIC中断优先级(如果需要)
这一整套流程下来,硬件层面就已经准备就绪,只等你一声令下启动即可。
常见坑点与调试技巧
别以为生成代码就万事大吉,下面这几个坑我当年都踩过:
❌ 坑1:忘记调HAL_TIM_PWM_Start()
现象:程序跑了,但PA6一直是低电平或高电平,没有波形。
原因:只完成了初始化,没启动定时器输出。
✅ 解法:务必调用HAL_TIM_PWM_Start(&htimx, TIM_CHANNEL_x);
❌ 坑2:GPIO没配置成复用功能
现象:示波器测不到波形,或者波形异常。
原因:PA6虽然连到了TIM3_CH1,但如果没设成AFPP模式,信号出不来。
✅ 解法:检查HAL_TIM_PWM_MspInit()中是否调用了HAL_GPIO_Init(),模式是否为GPIO_MODE_AF_PP。
❌ 坑3:PWM频率不对
现象:本该1kHz,结果测出来几百Hz或几kHz。
原因:系统时钟没配对!例如误将PLL倍频系数写错,导致实际主频不是72MHz。
✅ 解法:用CubeMX配置时钟时仔细核对;也可用HAL_RCC_GetSysClockFreq()打印当前频率验证。
✅ 调试建议:
- 用示波器或逻辑分析仪看真实波形,不要靠猜
- 先固定占空比测试频率是否正确
- 再逐步改变CCR值观察占空比变化
- 若无仪器,可用万用表测PA6平均电压,随duty变化应呈线性关系
实际应用场景拓展
掌握了基础PWM输出,你可以轻松扩展到更多实用场景:
| 应用 | 实现方式 |
|---|---|
| LED调光 | 改变CCR实现亮度渐变,支持RGB三色混合调色 |
| 直流电机调速 | PWM驱动MOSFET,控制电机平均电压 → 调速 |
| 舵机角度控制 | 50Hz PWM,占空比0.5ms~2.5ms对应0°~180° |
| 蜂鸣器音调模拟 | 改变频率实现不同音符,占空比影响音量 |
| DC-DC电源反馈 | 数字PID调节PWM占空比稳定输出电压 |
而且STM32一个定时器最多支持4个通道(CH1~CH4),意味着你可以在TIM3上同时输出四路独立PWM,分别控制四个LED或两台电机正反转。
最佳实践建议
为了让你的PWM系统更稳定高效,这里总结几点经验:
| 项目 | 推荐做法 |
|---|---|
| 频率选择 | LED调光选1–10kHz;电机控制建议≥20kHz避开人耳听觉范围 |
| 分辨率优化 | 提高ARR值可提升占空比精度,但注意不超过65535 |
| 功耗管理 | 不用时调用HAL_TIM_PWM_Stop()关闭定时器节省功耗 |
| 动态调节 | 使用__HAL_TIM_SET_COMPARE()实时更新,避免重启定时器 |
| 多通道同步 | 多个通道共享同一ARR,天然同步,适合三相控制 |
小结:你已经掌握了一个核心技能
到现在为止,你应该已经明白:
- PWM不是魔法,它是基于定时器的硬件行为
- ARR决定频率,CCR决定占空比,PSC用于分频
- Keil5 + CubeMX组合极大简化了配置过程
- HAL库封装让开发者无需直面寄存器也能高效开发
- 动态调节占空比可以实现呼吸灯、调速、调色等多种功能
更重要的是,你现在已经具备了向电机控制(如FOC)、数字电源设计、逆变器开发等高级领域迈进的基础能力。
下一步,不妨试试用ADC读取电位器电压,再用这个值去控制PWM占空比,做一个真正的“数字电位器”。这才是嵌入式的乐趣所在。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。