STM32 PWM输出实战:从定时器配置到舵机电机控制
简介一份面向STM32入门开发的PWM输出实验工程资源适合刚接触定时器应用的嵌入式学习者也适合需要快速验证PWM驱动逻辑的开发人员。资源基于STM32F10x标准外设库围绕定时器PWM生成展开详细覆盖TIM1/TIM2/TIM3等定时器资源选型、预分频器与自动重载值设定、比较寄存器对占空比的控制、PWM1/PWM2模式选择、通道极性与死区时间设置以及中断与DMA在PWM事件中的使用同时给出通过示波器检查频率和占空比的调试思路对电机控制、LED调光、信号发生等场景有直接参考价值。压缩包共131个文件以34个h头文件和33个c源文件为核心包含完整的Keil工程配置、编译脚本、hex/axf烧录文件、map文件及说明文档整体约2.31MB目录结构清晰下载解压后可直接打开工程对照学习。已有799人学习下载。通过学习这份例程可以理清PWM初始化到波形验证的完整流程掌握固件库TIM_TimeBaseInit、TIM_OCInit等接口的配置方法为后续嵌入式开发打下基础。1. PWM输出不只是一盏呼吸灯先搞懂它在工程里的真实用法很多新手拿到STM32开发板跟着例程跑通PWM实验看到LED灯由亮到暗平滑变化就觉得哦原来PWM就是控制亮度。这个理解不算错但远远不够。PWM在嵌入式项目里的地位基本等同于模拟输出的廉价替代方案——你可以用它调光、调速、控制舵机角度、驱动蜂鸣器音调、甚至做简易DAC输出音频。它的本质就一句话通过快速切换高低电平让负载感受到的平均能量连续可调。你可能会问为什么非要用PWM直接输出一个可调的模拟电压不行吗答案是STM32内部没有真正的DAC部分型号有但引脚少、精度有限而PWM配合外部滤波电路几乎可以零成本模拟任意电压。更关键的是很多负载LED、电机、舵机、加热丝本身的惯性就能天然滤波根本不需要额外的RC电路。所以PWM在单片机领域的使用频率极高几乎每个像样的嵌入式项目里都能看到它的身影。1.1 从LED亮度到舵机角度PWM到底在调节什么PWM有两个核心参数频率和占空比。频率决定PWM波形每秒钟重复的次数占空比决定一个周期内高电平持续的时间比例。以LED为例人眼对超过几十赫兹的闪烁基本无感所以频率在1kHz左右就能看到平滑的亮度变化但舵机对频率有硬性要求标准舵机期望的是50Hz周期20ms如果给到1kHz舵机轻则发抖重则直接烧毁。这是初学者最容易踩的坑——把调LED亮度那套参数直接套到舵机上。占空比的作用则因负载而异。LED的亮度大致跟占空比成正比直流电机的转速也近似成正比但舵机不一样舵机看的是高电平脉冲的宽度而不是占空比本身。标准舵机的控制脉宽是0.5ms~2.5ms对应0°到180°在50Hz周期下占空比只有2.5%~12.5%。如果你用hal库函数直接把占空比调到50%舵机大概率会直接打到极限位置并嗡嗡响。所以做PWM实验时脑子里一定要区分调节占空比和调节脉宽这两件事。1.2 STM32定时器输出通道的资源分配逻辑STM32的PWM输出不是随便找个GPIO就能干的它必须绑定在定时器的特定通道上。以最常见的STM32F103C8T6为例它的TIM1是高级定时器有4个输出通道CH1~CH4对应引脚PA8、PA9、PA10、PA11TIM2是通用定时器通道对应PA0~PA3TIM3对应PA6、PA7、PB0、PB1。如果你在CubeMX里配置TIM1的PWM输出却把引脚分配到PB0上CubeMX会直接报错因为PB0只属于TIM3的CH3。实际项目里做选型时我一般先查数据手册里的Alternate function mapping表确认目标引脚支持哪个定时器的哪个通道然后再决定用哪个定时器。比如PA8这个引脚它既能做TIM1_CH1也能做USART1_CK还能做MCO——如果你已经把PA8用作串口时钟输出那TIM1_CH1就得换到PA9或者干脆换定时器。这类冲突在后期调试时非常隐蔽波形出不来时排查半天结果发现是引脚被复用成了别的功能。2. 用STM32CubeMX快速搭建PWM输出工程配置面板逐项拆解现在STM32开发的主流方式已经从标准外设库寄存器操作过渡到了HAL库CubeMX图形化配置。我个人的建议是新手直接学HAL库理由很简单——CubeMX生成的代码已经把时钟树、GPIO复用、定时器初始化全部封装好了你只需要关心业务逻辑开发效率高出一大截。标准库虽然代码看起来更性感但工程配置的繁琐程度足够劝退一批人。这一节我以STM32F103C8T6为例完整走一遍CubeMX配置流程然后把配置面板里每一项的含义讲透。这样你做完实验后能真正理解自己配置的是什么而不是照葫芦画瓢。2.1 开发环境准备与工程创建的基本动作先交代环境STM32CubeMX 6.x版本固件包F1系列建议1.8.x以上IDE用Keil MDK 5.3x调试器用ST-Link。这些版本不必完全一致但CubeMX和固件包的版本差距不要太大我遇到过固件包版本过旧导致生成的代码跟HAL库API对不上的情况。打开CubeMX后第一步是选择芯片型号。在Part Number搜索框输入STM32F103C8T6双击选中进入配置界面。这里有个操作细节容易被忽略在Pinout Configuration视图里左侧的Categories树形菜单找到Timers展开后能看到TIM1、TIM2、TIM3等定时器。选择TIM2在Mode栏的Channel1下拉框里选择PWM Generation CH1此时右侧的芯片封装图会自动把PA0高亮并显示为TIM2_CH1的复用状态。这一瞬间你就完成了最核心的引脚分配。接着点击Clock Configuration标签页检查时钟树。默认情况下HCLK是8MHz外部晶振或内部RC我们需要把它配置为72MHz。方法是在HCLK输入框直接输入72回车CubeMX会自动计算PLL参数。这一步如果不做PWM频率计算会完全偏离预期后面会展开讲。2.2 定时器配置面板里每个参数的真实含义在Timers - TIM2的Configuration区域你会看到Prescaler、Counter Mode、Counter Period、Auto-reload preload、Pulse等参数。这些参数是PWM的核心但很多人只知道填数值不知道含义导致频率算不对。先看Prescaler预分频系数。STM32的定时器时钟源频率很高F103最高72MHz直接拿这个频率计数计数器溢出太快PWM周期会太短。预分频器的本质就是分频把时钟频率除以Prescaler 1后再给计数器。注意硬件上Prescaler是16位的取值范围0~65535而且实际分频值是写入值加1因为硬件从0开始计数。Counter Mode选Up表示计数器从0向上计数计到Counter Period的值后溢出归零重新开始。Counter Period也叫ARRAuto-Reload Register它决定了一个PWM周期的计数次数。PWM频率的计算公式就是PWM频率 定时器时钟频率 / (Prescaler 1) / (Counter Period 1)举个例子目标频率1kHz定时器时钟72MHz选Prescaler 71则分频后时钟为1MHzCounter Period设为999输出频率就是1MHz / (999 1) 1kHz。这两个值搭配组合很多工程上的一般做法是先确定目标频率再根据频率量级选择Prescaler让Counter Period落在几百到几千之间这样占空比调节的粒度足够细。Pulse参数就是CCRCapture/Compare Register它决定占空比。当计数器计数值小于CCR时引脚输出高电平大于CCR时输出低电平。所以占空比 Pulse / (Counter Period 1) * 100%。默认Pulse是0也就是占空比0%输出一直为低。这一步很多新手会漏掉配置完发现引脚没波形其实是因为Pulse没填。Auto-reload preload选项建议选Enabled。它的作用是当你在程序里修改ARR值时新值不会立刻生效而是在当前计数周期结束后才加载。如果不开启ARR更新可能发生在计数过程中间导致PWM波形出现短暂的异常周期。对于动态变频的场景比如电机调速起步阶段的频率爬升这个选项能避免很多奇怪问题。3. 从CubeMX生成代码到HAL库APIPWM输出背后的调用逻辑CubeMX配置完后点击GENERATE CODE它会自动生成一个MDK-ARM工程。打开工程你会发现定时器初始化的代码已经在tim.c里写好了包括GPIO复用配置、定时器参数配置、PWM通道配置。你唯一需要动手的是在主循环里调用启动函数以及按需要修改占空比。这一章节的核心是讲清楚两件事时钟树是怎么把72MHz喂给定时器的以及HAL库那几个API函数到底做了什么。搞懂这两点遇到波形不对你就会知道该去查哪里。3.1 时钟树链路从8MHz晶振到72MHz定时器时钟F103的时钟树是典型的多级倍频结构。外部8MHz晶振HSE经过PLL锁相环倍频到72MHz作为系统主时钟SYSCLK再通过AHB预分频器默认不分频给到APB1和APB2总线。这里有个非常关键的细节TIM2、TIM3等通用定时器挂在APB1上而APB1预分频器的默认配置是2分频即APB1时钟为36MHz。但STM32的定时器时钟有个特殊机制——当APB1预分频系数大于1时定时器时钟会自动乘以2所以TIM2的时钟仍然是72MHz。这就是为什么CubeMX生成的代码里TIM2的时钟源显示72MHz而不是36MHz。如果你用的是TIM1高级定时器它挂在APB2上APB2默认不分频即36MHz但同样的补偿机制会让TIM1时钟也变成72MHz。所以无论用哪个定时器做PWM默认情况下时钟源都是72MHz。这个知识点面试时经常被问到实际调试时也很重要——如果你手动修改了APB1或APB2的分频系数PWM频率就会跟着变此时需要重新计算预分频参数。3.2 HAL库API调用顺序与底层寄存器行为CubeMX生成的main.c里定时器PWM初始化的调用顺序大概是这样的MX_GPIO_Init(); MX_TIM2_Init();注意MX_TIM2_Init()只是配置了定时器的参数和引脚复用并没有真正启动PWM输出。要让PWM波形出现在引脚上你还需要调用HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);这行代码做了什么它首先使能定时器TIM2的更新中断和比较输出然后设置TIM2的CR1寄存器中的CEN位启动计数器计数。计数器开始跑之后引脚上才有波形。修改占空比的函数是__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pulse_value);这个宏直接写CCR1寄存器。注意修改CCR是即时生效的不需要重新启动PWM。所以呼吸灯实验的核心就是在一个循环里不断改变pulse_value然后加一小段延时就能看到LED亮度平滑变化。有一点需要特别提醒如果你在主循环里用HAL_Delay()来控制呼吸节奏延时期间定时器仍然在跑PWM不会中断。但如果你的程序里有其他中断服务函数占用了很长时间比如串口接收大量数据CCR虽然被修改了但波形依然会正常输出只是占空比更新频率可能不够平滑。对呼吸灯这种视觉应用无所谓但如果是精密电机控制建议在定时器更新中断里修改CCR保证每个周期都能更新一次。4. 三种负载三种调法LED呼吸灯、舵机、直流电机的PWM实测标定理论讲完必须上实践。这一章我选了三个有代表性的负载LED、舵机、直流电机。它们的PWM需求差异非常大正好用来验证前面讲的频率和占空比知识。你做完这三个实验基本就能应对工作中80%的PWM应用场景。4.1 LED呼吸灯动态修改CCR值的平滑处理用TIM2_CH1PA0接一个LED限流电阻330欧姆接3.3V。频率选1kHz以上避免肉眼可见的闪烁。占空比从0逐步增加到100%再从100%减回0循环往复。关键代码就这么多uint16_t pulse 0; uint8_t dir 1; while (1) { HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); for (pulse 0; pulse 999; pulse 5) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pulse); HAL_Delay(2); } for (pulse 999; pulse 0; pulse - 5) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pulse); HAL_Delay(2); } }这个例子里ARR是999所以Pulse范围0~999步进5意味着占空比每次变化0.5%。HAL_Delay(2)控制呼吸节奏。实测时你会发现一个现象LED从0到10%的亮度变化很明显但从50%到100%的亮度变化相对微弱。这是因为LED的亮度与电流近似成正比但人眼对亮度的感知是对数关系。所以要让呼吸看起来均匀步进应该按百分比非线性调整比如低亮度区域用小步进高亮度区域用大步进。工程上的做法是查表或者用指数函数生成亮度曲线。4.2 舵机控制50Hz周期与脉宽标定的实测数据舵机实验建议用独立电源供电不要直接吃开发板的3.3V否则舵机转动瞬间的电流冲击会导致开发板电压跌落轻则PWM波形畸变重则单片机重启。舵机信号线接PA0地线跟开发板共地即可。代码框架不变只是重配定时器参数Prescaler7172MHz分频到1MHzCounter Period19999计数到20000正好20ms即50HzPulse范围500~2500对应0.5ms~2.5ms。实测标定的时候先用最小脉宽500记录舵机角度再逐步增加脉宽每步加50记录角度变化。你会发现一个有意思的现象新买的廉价舵机标称0.5ms对应0°2.5ms对应180°但实际可能偏了10°~15°。原因在于舵机内部电位器的线性度和死区差异。所以如果你要做精确角度控制比如机械臂一定要在安装前做一次脉宽-角度标定把实测值存成表而不是直接套用标称值。另外舵机对PWM频率极其敏感50Hz是绝大多数模拟舵机的工作频率。如果我给你一个48Hz或者53Hz的信号舵机也能动但会有明显的抖动或嗡嗡声。数字舵机通常支持更高的频率如300Hz但一定要先确认舵机说明书不能盲试。我在实验室里就见过有人用1kHz的PWM去驱动模拟舵机几分钟后舵机就冒烟了。4.3 直流电机调速驱动芯片与PWM互补输出的坑直流电机不能直接接在单片机引脚上因为电机的工作电流远超出GPIO的驱动能力。常见的方案是L298N、TB6612这类驱动芯片或者用MOS管搭建H桥。以TB6612为例它的PWMA引脚接收PWM信号AIN1和AIN2控制电机方向。用HAL库输出PWM到PWMA后你会遇到第一个坑PWM频率的选择。电机本身的电感会对PWM频率产生滤波效果频率太低电机噪音大频率太高驱动芯片的开关损耗上升。实测下来10kHz~30kHz是多数直流电机的甜点区间。我习惯先用20kHz听不到噪音电机运行也平稳。第二个坑是关于电机堵转的。占空比调到100%时电机两端电压就是电源电压如果你用的是TB6612此时芯片的功耗会明显上升如果散热不好芯片可能过热保护甚至烧毁。所以电机调速实验不要长时间满占空比运行加个散热片是基本操作。5. 实测翻车记录为什么我的PWM波形出不来这一章是我最想分享的实战内容。每个从PWM实验爬起来的人几乎都经历过代码明明没错但示波器就是没波形的崩溃时刻。我把自己和身边的工程师踩过的坑做了个合集按排查顺序排列希望能帮你省下半天调试时间。5.1 引脚复用冲突是最隐蔽的凶手最典型的例子PA0这个引脚在STM32F103C8T6上同时是TIM2_CH1和WKUP唤醒引脚。如果你在CubeMX里同时使能了RTC的Tamper功能它可能偷偷把PA0配置成了其他用途。而GPIO初始化是分模块执行的你在GPIO_Init里对PA0的配置可能会被后面的模块覆盖。排查方法是在初始化完成后的主循环里读一下PA0对应的GPIO AFR寄存器确认复用功能是否真的指向TIM2_CH1。还有一类冲突来自调试器本身。F103的PA13、PA14默认是SWDIO和SWCLK如果你在这两个引脚上接了别的外设且恰好配置成了复用功能调试器可能直接连接失败。解决方法是在CubeMX的SYS菜单里把Debug选项配置为Serial Wire串行调试模式确保SWD引脚被保留给调试器。5.2 占空比100%时舵机反而抖动的排查过程这个坑我印象很深。当时做四足机器人项目PWM输出一切正常但舵机在占空比100%脉宽2.5ms时剧烈抖动而不是稳定在180°。示波器观察波形频率准确50Hz脉宽也确实是2.5ms怎么看都正常。后来排查到电源环节整个系统用USB线供电舵机满脉宽时电流需求突然变大USB口电压被拉低到4.6V。舵机内部的逻辑电路在这个电压下工作不稳定控制信号内部的电平判定时好时坏最终表现为抖动。换用外部5V电源后问题立刻消失。这个案例告诉你PWM波形正常不代表整个系统正常。看到异常现象时先怀疑电源再看波形最后才是代码。很多诡异的PWM问题根源都在供电环节。5.3 用示波器观测PWM时的探棒与触发设置最后简单说一下示波器的使用。测PWM波形时探棒地线夹子要尽量短直接夹在开发板的GND上不要用长接地线否则容易引入噪声波形边缘会出现振铃。触发电平设到1.65V也就是3.3V电源电压的一半能稳定触发。时间档位根据频率设置比如1kHz的PWM建议用200us/格到500us/格能清晰看到几个完整周期。如果你没有示波器可以用逻辑分析仪或者退而求其次用万用表的频率档测频率用直流电压档测平均电压。占空比50%时3.3V引脚的直流电压读数应该接近1.65V这个方法能粗测占空比是否生效但无法看到波形细节。5.4 一个定时器同时输出多路PWM的注意事项回到热词里那个问题一个定时器能不能同时接收PWM数据和输出PWM波形。答案是同一个定时器的多个通道可以同时输出PWM但它们的频率必然相同因为频率由ARR和Prescaler决定是定时器级的参数占空比可以各自独立因为每个通道有自己的CCR寄存器。所以如果你要输出不同频率的PWM必须用不同的定时器。这是配置时最容易忽略的限制。以TIM2为例CH1~CH4四路PWM共享同一个频率但可以设置四个不同的占空比。很多LED灯带驱动就是这么干的——四路独立调光频率统一用一颗MCU就能搞定。6. 从例程到项目PWM实验完成后的三个进阶方向跑通PWM实验只算入门真正把它用起来你还需要知道几个进阶方向。这里我根据自己的项目经验总结三个值得深入研究的方向每个都对应实际工程中的典型需求。第一个方向是互补PWM和死区插入。如果你要用STM32控制MOSFET全桥电路比如无刷电机驱动器TIM1和TIM8的高级定时器可以输出一对互补的PWM信号并且支持插入死区时间。死区的作用是防止上下桥臂同时导通导致短路时间一般设几百纳秒到几微秒。CubeMX里在Break and Dead-Time配置区域设置实测时要用示波器双通道同时观察上下桥臂的波形确认死区时间符合预期。第二个方向是DMA与PWM结合。前面提到动态修改CCR如果每次更新CCR都走CPU在高频率PWM场景下CPU占用率会很高。改进方式是使用DMA把一组占空比数据放到内存数组里通过DMA定时搬运到CCR寄存器CPU完全不用参与。热词里提到的stm32f103 pwm dma pa1 pa3就是这个玩法。做LED呼吸灯时用DMA搬运一个周期的亮度曲线数据CPU占用率从30%降到几乎为0。第三个方向是PWM输入捕获。同一个定时器不仅能输出PWM它的其他通道还能用来捕获外部PWM信号的频率和占空比。这在遥控器信号解码比如接收航模接收机的PPM信号里非常常用。捕获模式的基本原理是测量相邻两个上升沿的时间差得到周期测量同一周期内上升沿到下降沿的时间差得到高电平宽度进而算出占空比。CubeMX里把通道配置为Input Capture模式即可。我个人在实际项目里的体会是PWM这个外设虽然看起来简单但一旦跟具体的负载、具体的系统架构结合起来需要踩的坑远比你想象的多。如果你现在刚跑通例程建议不要急着往复杂功能上堆先把手上的LED和舵机玩明白用示波器把每种参数下的波形都亲眼看一下形成参数-波形-负载表现三者的直觉对应关系。有了这个直觉后面再看DMA、死区、捕获这些进阶功能会顺很多。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →