尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32C542 PWM实战:频率与占空比动态调节及常见坑

STM32C542 PWM实战:频率与占空比动态调节及常见坑 先说一下背景。这篇是STM32C542开发系列的第4篇前几篇我们解决了工程的搭建、GPIO翻转、串口打印这些基本盘这次轮到PWM。PWM在嵌入式里可以算是出场率最高的外设功能之一LED调光、蜂鸣器发声、舵机角度、电机转速、加热器功率、模拟电压输出几乎都靠它。这篇我会把三件核心事讲透怎么在STM32C542上输出一路PWM、怎么在程序运行中动态修改频率、怎么在运行中修改占空比顺便把CubeMX配置、HAL库实现、计算公式以及我实际调试时踩过的一堆坑都放出来。用这颗芯片做开发的朋友可以直接当一份参考手册来看。如果你之前只接触过51单片机或Arduino用软件延时翻转IO去模拟PWM那换成STM32C542之后体验会完全不一样定时器独立产生波形CPU只管改几个寄存器不需要在中断里一秒翻转几千次。这个转变才是“单片机的PWM”和“MCU外设的PWM”之间真正的门槛。1. 先搞清楚PWM到底在干什么1.1 频率和占空比这两个参数是正交的PWM全称脉冲宽度调制说白了就是MCU在某个GPIO引脚上输出一串高、低电平高低电平按固定的节奏切换。节奏切换得快慢就是“频率”一个周期里面高电平时间占多少比例就是“占空比”。频率决定的是切换速度占空比决定的是平均输出水平。打个比方PWM控制LED亮度和你用水龙头调节水流是一个原理。频率相当于你每秒开关水龙头的次数占空比相当于每次把手柄拧开多大角度或者持续多长时间。每秒开关10次、每次开一半和每秒开关1000次、每次开一半平均流量几乎一样但实际体验完全不同前者LED的亮度是肉眼可察的闪烁后者看起来就是稳定变暗。放在电机上频率太低会有顿挫和啸叫频率太高开关损耗会上去。这也是为什么选PWM参数时不能只看占空比还得确定合适的频率范围。早期的模拟PWM常用555定时器或者专用的PWM控制芯片比如3525这类来实现占空比靠电位器手动调频率靠RC常数决定。现在用MCU就灵活太多了频率和占空比都是寄存器里的数字程序随时可以改甚至可以在运行中动态变化这是硬件PWM芯片完全比不了的。1.2 PWM频率与占空比的计算公式以及最容易算错的那个1在STM32的定时器上做PWM核心过程是定时器计数器从0往上数到ARRAuto-Reload Register每个计数时钟加1当计数值和预设的比较值CCR相等时翻转输出电平计到ARR之后清零重新开始下一个周期。所以只要搞清楚计数时钟和计数范围频率和占空比就都能算出来。在经典的边沿对齐、向上计数、PWM模式1下公式如下PWM频率 定时器时钟 / (PSC 1) / (ARR 1)占空比 CCR / (ARR 1)这里最容易被忽略的是那个“1”。ARR99不代表计数器跑了99个点而是从0计到99一共100个点。我见过不少人拿着时间公式算频率怎么算都差一点最后发现就是少了这个1。同样占空比是CCR/(ARR1)不是CCR/ARR。ARR比较大的时候差1个数的影响可以忽略但在高频、低ARR的场合误差就非常明显。比如ARR49、CCR25按ARR算占空比是51%实际是50%当输出接的是精密执行器这个误差是没法接受的。我做一个20kHz电机PWM来举例。假设定时器时钟为100MHz想得到20kHz的输出(PSC1)(ARR1)100MHz/20kHz5000。一种配置是PSC4即5分频、ARR999计数1000拍这样频率就是100M/(51000)20kHz占空比调CCR250就是25%。选择PSC和ARR不是随手填的它决定占空比的调节分辨率如果你的ARR只有49那占空比最小步进是1/502%要控制LED亮度还行但要做细腻的电机调速或者伺服控制就会明显感觉到挡位不够顺。把ARR做大比如4999占空比步进就能到0.02%调节效果会平滑很多。这里说的定时器时钟并不一定等于系统主频。STM32内部有APB总线分频定时器有时还会自动倍频所以配置PWM之前先到CubeMX的Clock Configuration页面看清楚“Timer clocks”到底显示多少。很多人把SYSCLK改成250MHz想当然以为定时器就是250MHz结果频率全部偏了查半天查不出原因。2. 在STM32C542上规划PWM资源2.1 用哪个定时器、哪个引脚别背映射表STM32C542这颗芯片的定时器资源大致是这样的TIM1是高级控制定时器有互补输出通道和刹车Break功能TIM2是32位通用定时器计数范围大TIM3和TIM4是16位通用定时器另外还有几个基础定时器以及低功耗LPTIM低功耗模式也能跑。做普通PWM输出TIM2、TIM3、TIM4就足够了如果后面要驱动H桥、要做上下桥臂互补输出那就得认准TIM1。引脚映射我不建议背。STM32不同封装的复用关系并不像老51那样固定与其抱着引脚定义表挨个对不如直接在CubeMX图形界面上点引脚打开了复用菜单再去选。比如PA0可以复用成TIM2_CH1PA6可以复用成TIM3_CH1PB6可以复用成TIM4_CH1具体以你使用的芯片型号和CubeMX识别结果为准。一个容易踩的坑是GPIO速度配置。做低频PWMGPIO Speed选Low也没问题但频率一旦到了一两百kHz以上输出波形的边沿会变圆上升沿拉长高电平电平还没完全上去就下来了。把GPIO Speed设置为High甚至Very High波形会立刻好很多。高频PWM还要注意GPIO的负载能力不要直接驱动大电流设备PWM输出只负责给信号功率放大交给MOS管或者驱动芯片。2.2 边沿对齐、中心对齐、互补输出三种模式怎么选边沿对齐是最常用的模式计数器从0开始一路加到ARR然后归零一个周期的波形靠“加满归零”产生。优点是配置简单频率占空比计算直观缺点是波形两侧不对称高次谐波分量相对大一些对电磁干扰敏感的场景要小心。中心对齐模式下计数器先向上加到ARR再向下减到0一个完整PWM周期包含两段计数波形在时间轴上是左右对称的。这种模式在电机控制里很受欢迎因为电流纹波更小谐波含量低同时PWM波形中心点固定方便ADC在合适时刻采样。中心对齐的频率计算和边沿对齐不一样大约可以理解为f_clk/(PSC1)/(2*ARR)占空比也不再是简单的CCR/(ARR1)。实际做电机项目时建议直接参考参考手册的公式或者在CubeMX里配置完之后用示波器实测校准不要想当然套公式。互补输出是TIM1这种高级定时器才有的能力。一个通道可以同时输出CH1和CH1NCH1N和CH1电平相反配合死区时间用来控制全桥或者半桥的上下两个开关管。死区的作用我在后面会详细说这里先记住如果你做H桥驱动上下桥臂绝对不能同时导通互补输出加死区是硬件层面的保护机制不是软件延时能替代的。2.3 用CubeMX从零拉出一路PWM工程我习惯用STM32CubeMX生成初始化代码然后在HAL库基础上做二次开发。以TIM2输出一路PWM为例操作流程大致是这样。第一步新建STM32C542的工程在System Core的RCC里选择时钟源。如果你板子上有外部晶振就选HSE没有就用内部HSI精度稍差但胜在省事。然后进Clock Configuration页面把系统时钟配到合适的频率并且注意APB总线下那句“Timer clocks”的显示值它会直接决定你后面PWM频率的准确性。第二步在Categories列表里找到Timers打开TIM2Clock Source选Internal Clock这样定时器就挂在APB时钟下面。紧接着配置PrescalerPSC、Counter PeriodARR和Counter Mode。比如要做20kHz定时器时钟100MHz可以设PSC4、ARR999Counter Mode选Up。这里有个Auto-Reload Preload的选项建议勾上让ARR值在更新事件时统一生效避免运行中改频率时波形出现毛刺。第三步在TIM2的PWM Generation通道里选Channel1然后在下面的参数区把Mode设为PWM Mode 1Pulse设一个初始比较值也就是初始CCRPolarity保持High。Pulse就是你以后要动态修改占空比的那个值先填一个合理的初值比如250。Output compare preload也建议Enabled。第四步在GPIO设置里确认定时器通道对应的引脚被配置为复用功能Alternate Function。如果你的板子上PA0引不出来可以在System view里换其他复用引脚CubeMX会自动把外部引脚映射改好。生成工程之后真正让PWM跑起来的代码其实只有一行HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);这一行启动定时器PWM输出之后CPU完全不需要管波形硬件自己在那跑。很多人会误以为需要开中断才能输出PWM不是这样中断只是给你在周期边界做动态参数修改用的不是波形产生的必要条件。3. 用HAL库输出PWM并动态调节频率与占空比3.1 基础输出与停止PWM不需要CPU干预CubeMX生成的代码已经把定时器初始化好了主函数里直接启动就行。如果你用的是TIM2通道1启动方式就是上面那行HAL_TIM_PWM_Start。停止的话调用HAL_TIM_PWM_Stop波形立刻停在高电平或低电平这个停止后的电平状态由此时比较器的输出决定如果你希望停止后引脚保持确定电平最好先把CCR改成0或者更大的值再停止。注意HAL_TIM_PWM_Start和HAL_TIM_PWM_Start_IT的区别。前者只启动PWM输出不开启任何中断后者会额外开启更新中断每个PWM周期结束都会触发一次中断回调。做动态调节时可以灵活选择但千万别以为不调用Start_IT就没有PWM输出这是一个新手非常容易产生的误解。PWM是纯硬件外设行为和中断没有绑定关系。在PWM运行期间我们可以随时读取计数器当前值uint32_t cnt __HAL_TIM_GET_COUNTER(htim2);这个值用来观察定时器是否真的在走。如果程序和示波器都对不上先确认一下这个计数器的值变化情况能快速定位问题。3.2 动态改占空比一条宏就能搞定运行中修改占空比最直接的方法是用HAL库提供的宏__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500);这条宏的本质是往比较寄存器CCR里写入新值。占空比 CCR/(ARR1)所以如果你希望占空比是50%而ARR999那么CCR就填500。改完立即生效当然是否立即起作用取决于CCR是否开启了预装载Output compare preload。CubeMX里如果勾选了这个选项则新的比较值会在下一个更新事件也就是PWM周期边界才生效这样能保证一个周期内波形完整不会出现半截波形跳变的现象。动态改占空比最常见的应用是呼吸灯和舵机控制。呼吸灯就是主循环里不断改变CCR值从0递增到ARR再递减回0中间加点延时就能做出柔和的呼吸效果。舵机控制则不同舵机一般要求50Hz的PWM周期也就是20ms一个周期脉宽在1ms到2ms之间对应0度到180度。假设定时器时钟100MHzPSC99这样定时器计数频率是1MHz计数周期1微秒ARR设19999就是20ms。CCR设为1000时高电平持续1ms舵机在0度CCR设为2000时高电平持续2ms舵机在180度中间线性映射就可以。用__HAL_TIM_SET_COMPARE根据角度实时计算CCR这就是舵机控制的全部核心。用Macro改CCR有一个好处它不关心底层具体实现写起来方便。但在有些项目里需要保证修改的时序一致性比如同时改ARR和CCR时直接两条宏顺序执行会存在一个极短的不一致窗口虽然大多数场景不影响但如果要求严格可以在更新中断回调里集中修改保证寄存器值在同一个更新事件边界生效。3.3 运行中改频率改ARR还是改PSC两种路线对比修改频率就比改占空比稍微复杂一点了。频率由PSC和ARR两个寄存器共同决定所以有两条修改路线各有优缺点。第一条路线是改PSC保持ARR不动。这种方式最大的好处是占空比几乎不受影响因为占空比CCR/(ARR1)ARR没变CCR也没变占空比天然保持不变。比如当前PSC4、ARR999占空比是25%现在想把频率提升到40kHz只需要反推新的PSC100MHz/(1000*40kHz)2.5取整后PSC约等于1到2频率会有偏差但占空比还是25%。这种方法的缺点也很明显频率调节是离散的不是给个目标频率就能精确命中只能靠PSC取整后的值逼近。第二条路线是改ARR。ARR变化后频率变化更细腻因为ARR本身是整数可以连续调整。但坑也在这里ARR一变占空比CCR/(ARR1)的分母就变了占空比会跟着跳。你本来调的是50%一改频率占空比可能变成48%或者53%这在很多应用里是不可接受的。所以走ARR路线时必须同步修改CCR保持比例关系。代码这样写uint32_t new_arr 499; // 新的ARR uint32_t duty_rate 50; // 期望占空比单位% __HAL_TIM_SET_AUTORELOAD(htim2, new_arr); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, new_arr * duty_rate / 100);先给一个简洁的对比表修改方式占空比影响频率连续性典型场景改PSC基本不变离散能取到的频率有限需要调频又不想额外处理占空比的场合改ARR会变化必须同步改CCR连续可调需要精细频率调节且能同步处理占空比的场合还有一点要提醒PSC寄存器写入新值之后并不会立刻生效它需要等到一次更新事件才会被装载到影子寄存器。如果你希望它马上生效可以在修改后调用__HAL_TIM_GENERATE_UPDATE(htim2, TIM_EVENTSOURCE_UPDATE);但这样做会把计数器立刻清零导致当前这个PWM周期不完整波形上会看到一次明显的相位跳变。如果用在电机上可能会造成一次转矩突变。更好的做法是让PWM自然跑等一个更新事件到来时再改或者直接在更新中断回调里修改PSC和ARR这样天然对齐了周期边界。如果你希望每个PWM周期都能同步修改一次参数可以开启PWM更新中断HAL_TIM_PWM_Start_IT(htim2, TIM_CHANNEL_1);然后在回调里修改void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { __HAL_TIM_SET_AUTORELOAD(htim2, new_arr); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, new_ccr); } }这种写法尤其适合做变频电机或数字电源每一个PWM周期都是完整、无毛刺的新参数。4. 进阶玩法把这些技能用到真实工况里4.1 LED呼吸灯、舵机、电机调速三种经典实操PWM最直观的练手案例是呼吸灯。只要在主循环里逐渐增加CCR到ARR再逐渐减小回来就能看到LED亮度平滑变化。要注意两个细节一是变化的步长决定呼吸的顺滑度步长太大会有台阶感二是延时不能太长太长了呼吸会有卡顿感看起来像动画掉帧。舵机上文已经说过50Hz周期、1ms到2ms脉宽。真实项目中要注意舵机电源和MCU电源隔离舵机负载变化会引起电源电压波动严重时会让MCU复位或者PWM信号畸变。我给舵机供电都是从独立稳压芯片走不会直接从MCU的3.3V上拉。电机调速是PWM的核心应用场景。直流电机调速通常是高频率PWM配合H桥驱动频率一般选在10kHz到20kHz避开人耳可听范围噪声会小很多。占空比越大平均电压越高转速越快。但电机这种感性负载对PWM波形的上升沿、下降沿要求很高硬件上要有续流二极管或者对应桥臂的阻尼软件上不能把上下桥臂互补输出做成同时导通这就是死区存在的意义。4.2 硬件级故障保护刹车输入PWM的保险丝高级定时器TIM1有一个刹车Break功能这是纯硬件级别的保护逻辑。你可以把一个外部信号接到刹车输入引脚比如来自硬件比较器的过流信号、驱动芯片的故障输出正常情况下这个信号是无效电平一旦发生异常刹车信号有效定时器输出会被硬件立即封锁不管程序此时在干什么PWM波形立刻停止。这个功能在电机驱动和电源设计里非常重要。软件保护往往需要中断响应、CPU判断、再执行停机指令整个过程少说几百纳秒到几微秒而硬件刹车是在纳秒级别完成的还没有软件跑飞的故障风险。很多驱动芯片的FAULT引脚就是专门用来接这个的。在CubeMX中配置TIM1的Break and DeadTime settings选择刹车输入源、极性以及刹车后输出电平的状态。我一般会开“Automatic Output Enable”之外的功能默认配置基本够用。启动带刹车功能的PWM时和普通PWM有个区别需要调用HAL_TIM_PWM_Start如果刹车已经配置为自动失能恢复时要重新使能输出。这个功能我建议每个做电机项目的朋友都用上哪怕现在觉得不需要也要把引脚预留出来后续改板子会容易很多。4.3 中心对齐PWM与ADC采样时机的配合在电机驱动里中心对齐PWM比边沿对齐更常用。它的波形对称电机电流纹波小噪声响应好。更重要的是中心对齐模式下PWM的上升沿和下降沿对称分布在周期中点两侧这样更容易安排ADC的采样时刻。电机电流采样讲究“在PWM周期中间、开关噪声最小的时候采”。如果使用边沿对齐模式电流纹波的峰值位置比较难对准中心对齐模式下计数器到达0或者到达ARR的时刻正好对应开关状态的中间点这时候电流相对平稳。通常做法是把ADC触发信号挂到定时器的TRGO事件上使得上升到峰值或下降到谷底时自动触发采样软件就不需要精确控制采样时刻。与ADC的配合可以比平时稍微展开一些配置ADC的External Trigger Source为定时器事件并设置好触发 polarity然后把你需要的电流采样时刻在PWM参数里定下来。这个流程对于做FOC矢量控制的人来说几乎是标配我们在C542上也能用得上。4.4 用PWM做低成本DAC模拟电压输出有些应用需要输出一个模拟电压比如控制一个0到10V的可调设备或者产生一个可变的基准电压。如果你不想额外加DAC芯片用PWM加RC低通滤波就能得到一个平均电压原理很简单占空比50%输出高电平电压3.3V经过滤波后的直流平均电压就是1.65V占空比25%平均电压就是0.825V。设计RC滤波器时时间常数要远大于PWM周期纹波才能压得足够低。时间常数太大又会导致响应变慢比如频率为1kHz的PWMRC时间常数取5ms左右纹波大约在几十毫伏量级但调节响应速度也只有几十赫兹牺牲了一部分动态性能。所以这个方案适合需要慢变化的模拟信号不适合用来做音频或高精度参考电压。我做过类似方案的传感器电流环给定配合软件校准实际精度能做到1%以内成本和布线面积都远小于加一颗DAC。4.5 动态变频应用举例从蜂鸣器到充电枪CP检测动态修改频率和占空比的实战场景很多。蜂鸣器播放不同音调就是修改频率播放不同音量靠修改占空比。我以前做过一个用STM32的PWM播放简谱的小项目每个音符对应一个频率通过改ARR实现音量固定效果至少比用IO翻转播放高了好几个档次。另外一个偏门的场景是充电桩充电枪CP信号。充电枪的CP信号本质是一个1kHz左右的PWM占空比携带了充电电流上限信息。用MCU测这个占空比最方便的方法是定时器输入捕获配置好上升沿和下降沿捕获计算高电平时间和整个周期的比值就能解出占空比。这个过程本质上和PWM输出是同一个定时器的另一面一个管输出一个管测量。理解了PWM的原理输入捕获也就不难理解。5. 我踩过的坑调试PWM时的常见问题与排查5.1 完全没有输出先按这个顺序查PWM没波形是新手最常见的问题按下面顺序排查一般都能解决。第一步检查CubeMX里定时器通道是否真的使能了。有人配好了时钟但忘记了在TIMx里把Channel1配置成PWM Generation程序里却没有报错只是没波形。第二步检查主函数是否调用了HAL_TIM_PWM_Start。CubeMX只负责初始化真正输出PWM必须要程序里调用启动函数。这一步漏掉的概率不低尤其是从F1系列老工程移植过来的人容易把旧代码里这行删了。第三步检查GPIO复用。引脚没有配置为复用功能而是普通GPIO输出波形自然出不来。在CubeMX的GPIO设置里看Mode是否为Alternate Function。第四步用调试器读定时器寄存器的值。打开调试器看TIMx的CR1寄存器里的CEN位是否为1CCER里的CC1E是否为1CNT是否在走动。如果CNT一直卡在0大概率是时钟没配好回Clock Configuration页面检查Timer clocks。5.2 频率不对、占空比不准多半是算法问题频率不对先检查你算的时候有没有漏掉那个“1”。PSC和ARR都要加1再参与计算很细微但很容易错。其次是定时器时钟来源某次我把APB1分频改了定时器时钟从100MHz变成了50MHz频率瞬间全错。所以每次改动系统时钟后都要重新确认CubeMX里的Timer clocks显示值。占空比不准则要检查CCR和ARR的比例关系。如果你在CubeMX里ARR设了99然后想要50%占空比CCR应该填50即50/10050%如果你填的是49那占空比就是49%看上去“差不多”但严格需求下就是不对。还有一个隐藏坑是占空比达到100%的问题如果你把CCR设成和ARR相等在某些定时器配置下输出是99.9%而不是100%想要真正的100%占空比需要让CCR大于等于ARR1。同理想输出0%占空比CCR要设成0。5.3 动态调节导致的波形异常要重点检查预装载和同步更新运行中改频率或者占空比偶尔会看到波形跳变、占空比抖动甚至输出瞬间乱掉。这通常有两个原因一是你在一个PWM周期中间直接改了ARR和CCR新值立即生效导致当前周期的宽度突变二是PSC修改后没有等更新事件PSC是预装载寄存器新值要等更新事件才生效你只写了寄存器但没产生更新事件修改看着没效果。解决办法就是前面提到的把Auto-Reload Preload打开让新值在更新事件时统一装载如果一定要立即生效调用__HAL_TIM_GENERATE_UPDATE但做好波形瞬间异常的心理预期。在更新中断里同步修改是最稳妥的动态调节方案。还有一个经验是同时修改ARR和CCR时不要一条接一条地直接写而是在计算好新的ARR和CCR之后一次性写入或者先在同一段临界区里关闭中断再写入防止修改到一半被打断。对于大多数应用SHUTDOWN时序没那么严格但如果你做的是电源或者电机控制这点细节往往决定项目稳不稳。5.4 示波器之外怎么验证PWM输出没有示波器不代表没法调PWM。最简单粗暴的办法是接一个LED看亮度变化。频率高了人眼看不到闪烁亮度变化还是能看出来的能验证占空比是否在变但没法测具体数值。想要看数值用逻辑分析仪最实用几十块钱的就能轻松读出PWM的频率和占空比。逻辑分析仪只需要关心电平变化时间精度足够测大多数PWM信号比示波器便宜很多。如果你连逻辑分析仪都没有还有一个土办法用PWM加RC滤波再用万用表测平均电压。比如高电平3.3V占空比50%滤波后万用表读数应该接近1.65V占空比25%读数接近0.825V。只要万用表精度还行就能验证PWM的占空比是否和预期一致。当然这个方法存在负载影响测量时注意RC滤波的输出阻抗。个人经验我调试PWM时最少会同时开逻辑分析仪和串口打印。逻辑分析仪看波形串口打印打印当前CCR和ARR的值两头一对应基本能快速定位是寄存器没写对还是配置没生效。这个方法效率很高我建议你也养成这个习惯。最后再分享一个小技巧如果你需要在一个项目里同时输出多路不同频率、不同占空比的PWM我的建议是不要共用一个定时器硬凑而是先规划好定时器和通道资源把固定频率的、动态调频的、需要ADC同步的分开。例如TIM2做一路可变速的电机PWMTIM3做一路固定频率的LED调光TIM4留给输入捕获测外部信号。这样代码结构清晰互相之间不会因为改频率而干扰。我在STM32C542上做多路PWM时就是按这个思路规划的后续扩展新功能基本不需要推倒重来。
返回列表