恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

STM32定时器PWM输出全解析:从底层原理到配置调试避坑

  • 首页
  • 资讯中心
  • /
  • STM32定时器PWM输出全解析:从底层原理到配置调试避坑

相关资讯

音游高难度谱面挑战指南:从零到SSS FC的进阶之路 2026/9/3 2:04:22
Python数据分析实战:演唱会热度与粉丝行为可视化项目全解析 2026/9/3 1:59:21
STM32单片机从入门到实战:B站精选教程与系统学习指南 2026/9/3 1:59:21

最新资讯

程序员全链路成长路径:从误选专业到深耕一线,普通人技术降本增效实战指南
2026独立开发者全链路运营方案:亏损翻盘、持续盈利的落地避坑指南
普通人社交数字化成长路径 破除聚光灯效应 轻松实现无压力社交
Grok机器人改进建议如何结构化征集与落地实践
汽车朝向检测数据集:VOC+YOLO双格式细粒度标注实践
三相方波逆变电路原理与MATLAB仿真实践

今日推荐

零基础装 OpenClaw 小龙虾 AI:Windows 一键部署教程与避坑要点
Hermes Agent 本地部署新方案:Windows 整合包减少依赖报错
实测 OpenClaw 一键包,5 分钟完成本地自动化环境搭建

本周热门

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

STM32定时器PWM输出全解析:从底层原理到配置调试避坑

发布时间:2026/9/3 2:04:22
STM32定时器PWM输出全解析:从底层原理到配置调试避坑 简介STM32 HAL库PWM输出程序是一份面向入门开发者的定时器PWM例程配套齐全、可直接编译运行。它着重解决新手在PWM应用中的常见问题如何用STM32CubeMX选择TIM定时器并设置预分频器、自动重装载值和比较通道如何调用HAL库完成初始化、配置占空比、启动与停止输出以及如何处理定时器中断与溢出事件适合用来学习PWM调速、调光、蜂鸣器控制等基础场景。压缩包共73个文件以46个.h头文件和19个.c源文件为主体另有.ioc图形化配置工程、.uvprojx MDK工程、启动文件及编译好的hex固件整体仅517KB体积小巧解压后Src、Inc、Drivers等目录按驱动层、用户代码和HAL底层分离层次清楚便于对照学习代码组织方式。工程代码完整展示了从HAL_TIM_PWM_Init、HAL_TIM_PWM_ConfigChannel到HAL_TIM_PWM_Start/Stop的调用流程并给出MSP回调与中断处理的代码框架还包含定时器溢出、更新事件和电源管理方面的注意事项为后续扩展多通道PWM、死区控制等高级功能打下基础。当前已有8834人浏览学习是一份高效上手的实践参考。 玩STM32的人迟早会碰到PWM输出。我见过不少刚从标准库转HAL库的朋友在CubeMX里把定时器通道勾成PWM Generation CHx然后照着教程调用HAL_TIM_PWM_Start结果板子上波形出不来或者出来之后频率跟预期差一大截。问题多半不在API而在对定时器底层机制理解不够——PWM输出的整个过程其实就是定时器里一个计数器和几个寄存器在硬件层面反复“比大小”。这篇文章不打算一步步截图带你点CubeMX而是把PWM输出这条链路从头到尾讲透从原理到配置再到调参和进阶应用适合刚入门想真正搞懂PWM、而不是只会抄配置的同学。1. 先搞明白PWM输出在定时器底层到底是怎么发生的1.1 一个跑圈的计数器一个硬件比较器STM32的定时器TIM本质是一套硬件计数器系统和CPU无关却能持续工作。核心有三个东西CNT当前计数值、ARR自动重装载值、CCR捕获比较寄存器。CNT按照定时器时钟的节拍从0开始累加数到ARR的瞬间归零重新开始这个循环就是PWM的周期。PWM模式就是在CNT累加过程中持续把CNT和CCR做比较比较结果直接控制引脚输出高低电平。比如最常见的PWM模式1当CNT小于CCR时引脚输出有效电平当CNT大于等于CCR时引脚输出无效电平。这个比较是硬件完成的CPU全程不需要干预所以一旦配置好定时器就能自己一直输出PWM。把定时器想成操场跑圈就很好理解CNT是正在跑的运动员ARR是一圈的总长度CCR是设在跑道上的一个检查点OCx引脚就是一面旗子——运动员在检查点之前旗子就举起来过了检查点就放下一圈跑完重新再来。1.2 占空比、频率、分辨率三者是跷跷板PWM的三个核心参数对应着三个公式频率 定时器时钟 / ((PSC1) * (ARR1))占空比 CCR / (ARR1)分辨率 1 / (ARR1)以STM32F103C8T6系统时钟72MHz为例如果PSC71、ARR999、CCR500那么计数器的时钟是72MHz/(711)1MHz一次完整周期要数1000个脉冲所以PWM频率1MHz/10001kHz占空比500/100050%分辨率0.1%。注意这三者是相互约束的不是想调哪个就调哪个。分辨率由ARR决定ARR越大分辨率越高但ARR增大会拉低频率。想要频率高同时分辨率也高就只能压缩PSC降低计数时钟。我见过很多人只改ARR去调频率结果占空比从50%变成了别的数值——因为占空比是CCR除以(ARR1)ARR变了分母就变了。这三个参数要放在一起联调不能只动其中一个。1.3 PWM模式1和模式2以及极性到底怎么理解CubeMX里PWM模式有两个选项PWM Mode1和PWM Mode2。Mode1是CNT小于CCR时输出有效电平Mode2正好相反。但很多人忽略了一个更隐蔽的概念——有效电平本身是由极性Polarity决定的。Polarity选High时有效电平是高电平选Low时有效电平是低电平。所以同样是Mode1极选High和极选Low输出的波形在相位上是反的。我遇到过有人做LED共阳接法想用低电平点亮直接把Polarity改成Low结果发现CCR设得越小灯反而越亮跟自己预期完全反了。原因很简单Polarity Low之后有效电平变成低电平而Mode1下CNT小于CCR时输出有效电平也就是CCR越小低电平时间越短灯当然就越暗。这种地方很容易把人绕晕。实际调参建议绝大多数场景用PWM Mode1 Polarity High然后通过CCR的值来决定占空比这样最符合直觉。只有在外部电路需要反向逻辑时比如共阳LED、驱动芯片的IN反相输入才去动极性而且动完要重新理解CCR和亮度的关系。2. CubeMX里每一项配置到底在改什么寄存器2.1 时基配置先定分辨率再反推分频值打开CubeMX在Timers里选中一个定时器把Clock Source选为Internal Clock然后在Parameter Settings里能看到PrescalerPSC、Counter Mode、Counter PeriodARR这几个选项。Counter Mode一般选Up向上计数模式除非你后续要做中心对齐的互补PWM和死区控制才需要选Center Aligned Mode。计算顺序有讲究。我的习惯是先确定目标频率和可接受的分辨率由分辨率反推ARR再由ARR和频率反推PSC而不是先拍脑袋填PSC。举个例子想要1kHz的PWM分辨率要0.1%那么ARR11000ARR999然后定时器时钟如果是72MHzPSC 72MHz/(1000*1kHz) - 1 71。这样算出来就是PSC71、ARR999。这里有一个F103系列最容易踩的大坑APB1总线分频系数不等于1时挂在APB1上的定时器时钟是APB1的两倍。F103的APB1最高只能到36MHz所以默认情况下APB1分频器是2而TIM2/TIM3/TIM4这些通用定时器的时钟反而是72MHz。很多人在CubeMX的Clock Configuration里看到APB1显示36MHz就以为TIM2/TIM3/TIM4也是36MHz结果算出来的PSC全错输出频率差一半。这个点能坑掉一半的初学者。2.2 输出通道模式、极性、初始脉冲在CubeMX里把某个通道比如TIM2的CH1选为PWM Generation CHx之后会出现几个参数Mode选PWM Mode1即可原因前面已经说过。Pulse这是CCR的初始值也就是初始占空比建议一开始设一个明确的值比如500方便确认波形。Polarity默认High除非你有反向需求。Fast Mode和Output Compare PreloadFast Mode一般不勾Preload建议保持Enable。这里要特别注意GPIO的配置状态。CubeMX会自动把对应的引脚设置成复用功能但你在GPIO设置里最好再确认一下F103要选AF_PP复用推挽F4/H7要确认AF值选到了正确的定时器复用功能。我见过太多人波形出不来最后发现CubeMX自动生成时引脚根本不是复用模式而是普通GPIO输出——虽然CubeMX一般会自动处理但如果你手动改过引脚状态这个错误就非常隐蔽。2.3 预装载改占空比什么时候生效这里有个坑CubeMX的PWM配置界面里有两个跟预装载相关的选项Output Compare PreloadOCxPE对应CCR预装载和AutoReloadPreloadARPE对应ARR预装载。默认都是Enable作用是让寄存器的更新发生在下一个更新事件一个完整PWM周期结束时而不是写入的瞬间立即生效。这样设计是为了防止毛刺如果在计数器运行途中直接改ARR或CCR电路输出波形可能被拦腰截断出现一个不完整的高电平或低电平脉冲。预装载相当于加了一个缓冲系统会等一个周期结束再切换参数。但这个机制也会带来一个让人困惑的现象代码里用__HAL_TIM_SET_COMPARE改了CCR波形没有立刻变而是等了一个周期才变。对于控制频率不高的场景LED、舵机完全无所谓反正周期以毫秒计但在需要精确同步的场景下你就得用“强制产生更新事件”或者干脆关闭预装载来保证写入立即生效。我在做PWM触发ADC采样时就遇到过这个问题——我改了CCR想移动采样点结果ADC采到的还是旧值后来才意识到是预装载在“挡路”。还有一个经常被忽略的点Pulse的初始值会直接被写进CCR寄存器但即使是Enable了预装载这个初值是在初始化时写入的之后的更新事件才会用新的CCR。这一点如果没搞明白容易在调试时怀疑自己“代码没生效”。3. HAL库API拆解Start、SetCompare和看不见的默认状态3.1 HAL_TIM_PWM_Start一句话背后做了三件事HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1)这个函数几乎人人会写但很少有人看它到底做了什么。翻一下stm32f1xx_hal_tim.c源码PWM_Start内部实际做了三件事第一使能对应输出通道。它调用TIM_CCxChannelCmd把TIMx-CCER寄存器里对应的CCxE位置1这一步的作用是把OCx引脚的输出接通到定时器让比较结果真正流到引脚上。第二对高级定时器处理主输出使能。如果是TIM1、TIM8这类高级定时器还需要把TIMx-BDTR寄存器的MOE位置1否则整个定时器的PWM输出全部是封锁的引脚上什么都没有。这一步HAL库在Start函数内部已经做了。第三使能计数器。通过把TIMx-CR1的CEN位置1让CNT开始跑起来。注意CEN位置1之后是从0开始计数而不是从你上次停止的位置继续——很多人没注意到这个细节。所以HAL_TIM_PWM_Start的语义是“打通输出链路并启动计数器”它不负责重新配置占空比占空比在初始化时已经写好了。3.2 改占空比三条路都能走运行中改占空比HAL库给的方式非常多本质上都是在写CCR寄存器// 方式一HAL库宏性能和寄存器操作一样 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500); // 方式二HAL库函数封装带参数断言 HAL_TIM_PWM_SetCompare(htim2, TIM_CHANNEL_1, 500); // 方式三直接操作寄存器 htim2.Instance-CCR1 500;我平时最常用__HAL_TIM_SET_COMPARE因为它是宏直接展开成寄存器写操作快且没有额外开销而且语义清晰。HAL_TIM_PWM_SetCompare本质上也是写CCR但会带断言检查适合代码审查严格的项目。有一个很容易踩的边界问题CCR的值如果大于等于ARR1占空比会饱和到100%波形恒为高电平。如果后续你又手动把ARR改小了而CCR没有跟着改那么占空比可能一直是100%——这种“占空比突然不受控制”的情况排查时先看CCR和ARR的关系多半就是数值越界了。3.3 为什么波形出不来三个我实测最多的原因第一GPIO没有正确配置成复用功能。CubeMX自动生成的代码一般不会错但如果你在别的工程里手写Init或者中途调整过GPIO引脚很可能还是普通GPIO模式定时器信号根本没有接到引脚上。排查办法很简单看MX_GPIO_Init里这个引脚是不是AF_PP以及MX_TIMx_Init之后有没有执行HAL_TIM_PWM_Start。第二定时器的时钟源没有配置。CubeMX里如果没把定时器Clock Source设成Internal Clock代码里就不会使能定时器的外部时钟定时器外设可能根本没有时钟脉冲CNT不走比较逻辑永远不触发。这种情况HAL_TIM_PWM_Start执行了不报错但引脚没有任何输出排查费劲。第三高级定时器的MOE位被“偷偷”清了。前面提到HAL_TIM_PWM_Start会自动处理MOE但如果你在Init之后、调用Start之前做了一些寄存器操作比如读了BDTR寄存器、或者调用了HAL_TIM_PWM_Stop又改为手动StartMOE可能在某个环节被清零。这时候引脚依旧没有输出但CCER的CCxE已经置位了看起来就是“一切正常但没波形”。遇到TIM1/TIM8输出异常第一件事检查BDTR寄存器的MOE位。另外有个新手高发问题在CubeMX里改了配置之后没有重新生成代码仍然在跑旧的初始化程序。这个问题看着低级但真实存在因为CubeMX生成的代码覆盖手写修改是另一个常见坑——所以我会在工程里把手动改动的部分尽量集中放在一个单独的函数里避免被重新生成时覆盖。4. 按场景调参LED、舵机、电机对PWM的要求完全不同4.1 LED呼吸灯200Hz到1kHz256级亮度梯度就够LED对PWM频率的要求不算高人眼对60Hz以上的闪烁就不太敏感了但实际体验下来低于100Hz在快速移动时还是会有频闪感200Hz到1kHz是舒适区。频率再高就没有意义了LED驱动电路尤其是带恒流驱动的反而可能因切换太快而产生噪声。呼吸灯的关键是亮度梯度的平滑度。实测下来占空比可以用256级就足够平滑肉眼基本分辨不出分层128级在某些低亮度区间会看到明显的台阶感。对应的做法就是ARR取255CCR从0开始循环递增递减while (1) { for (uint16_t i 0; i 255; i 2) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, i); HAL_Delay(15); } for (uint16_t i 255; i 0; i - 2) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, i); HAL_Delay(15); } }这个代码CPU会一直忙在延时上如果主循环还要干别的事建议把渐变逻辑放到定时器中断里驱动或者直接用DMA来更新CCR后面会专门讲。4.2 舵机20ms周期是死规矩脉宽才是关键舵机控制最容易让人误解的一点它不是按占空比来算的而是按高电平的绝对脉宽来算的。标准模拟舵机的控制信号是周期20ms频率50Hz高电平0.5ms到2.5ms对应0°到180°。所以配置时要让计数器的时钟是1MHz这样CCR的值直接就是微秒数。按72MHz系统时钟来算PSC71得到计数时钟1MHzARR19999计数20000个脉冲正好20ms。CCR500对应0.5ms约0°CCR1500对应1.5ms约90°CCR2500对应2.5ms约180°非常直观。舵机对PWM频率很敏感有些数字舵机可以接受更高刷新率但很多模拟舵机一旦频率超过50Hz就会发热、抖动、甚至失控。不要在舵机上追求“高刷新率”20ms周期就是最稳的选择。另外如果你用低分辨率的ARR比如ARR999也能算出来对应脉宽但周期就不是20ms了有些舵机依然能转但控制精度和稳定性会下降不建议这样偷懒。4.3 电机调速频率低于15kHz容易啸叫电机PWM调速是另一个典型场景频率选择逻辑和LED完全不同。人耳能听到的声波范围是20Hz到20kHzPWM驱动电机时线圈在通断瞬间会产生机械振动频率落在人耳可听范围内就会啸叫。实测10kHz以下啸叫很明显15kHz以上基本听不到了所以业界常见的电机PWM频率是16kHz到20kHz。但频率也不是越高越好。20kHz以上虽然人耳听不到但MOSFET的开关损耗会明显上升驱动芯片发热量增大。做有刷电机调速我一般先选16kHz再根据实际发热和噪音情况微调。电机控制还有一个容易被忽视的点主控引脚的3.3V逻辑电平不一定能被驱动芯片正确识别。很多电机驱动芯片的逻辑输入是5V电平标准直接用3.3V高低电平去驱动有些芯片能识别有些芯片在高电平阈值边缘徘徊表现就是“有时候能转有时候不能转”。这种情况加一个电平转换电路或者选支持3.3V逻辑的驱动芯片比在代码里反复折腾强得多。5. 进阶玩法DMA、ADC触发、互补输出的实战经验5.1 一个定时器能不能同时捕获和输出PWM这个问题的答案是可以但有限制。我在项目里常用TIM3的CH1做输入捕获来读取遥控器的PWM信号同时用TIM3的CH2输出自己生成的PWM实测完全没问题。原因在于定时器的每个通道都有独立的CCR和独立的捕获/比较逻辑CH1的输入捕获会把捕获到的信号值写入CCR1CH2的输出比较会把软件写入的CCR2拿去做比较两者互不干扰。但有一个必须明白的约束同一个定时器的所有通道共用一个计数器和一个ARR。也就是说输入捕获信号和输出PWM信号的频率尺度和ARR强绑定。如果输入的PWM是50Hz舵机信号需要ARR19999输出的PWM也必须是接近50Hz这个量级如果输入是20kHz的高频信号需要ARR很小而输出要1kHz同一个定时器做不到因为ARR只有一个。这种需要两个定时器或者硬件资源分开的场景就得换方案。另外同一个通道不能同时做输入捕获和输出比较。CCR只有一个输入捕获由硬件写入CCR输出比较由软件写入CCR二者直接冲突。如果非要用同一个引脚既接收又发送PWM只能靠运行时切换定时器模式并重新配置工程上不推荐时序抖动的坑会非常深。5.2 PWMDMA占空比自动批量更新想让LED按一段预设的曲线平滑变化或者让步进电机按S形加减速曲线运行又不愿意让CPU一直去改CCR可以用PWMDMA。核心思路把一段占空比序列放在内存数组里DMA每次在定时器更新事件时自动从数组搬运一个值到CCR寄存器DMA设置为循环模式数组里的值就能一遍遍重复执行。CubeMX里的配置要点DMA请求源选择TIMx_UP更新事件这是每次CNT溢出时自动触发的。DMA方向选MemoryToPeripheral外设地址固定为要更新的CCR寄存器内存地址指向占空比数组。DMA模式选Circular循环内存地址递增外设地址不递增。数据宽度Memory和Peripheral都选Half Word16位因为CCR是16位寄存器。实际效果就是CPU配置好之后完全不用管PWM占空比会自动按数组里的值走完一遍又一遍。我做呼吸灯时就把256级渐变数组放进去主循环完全空闲。这个方案也有一个代价DMA在搬运时会占用一点总线带宽但对本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号