STM32C542 PWM输出与动态调频调占空比实战指南 这篇是STM32C542开发记录的第4篇。前面几篇把工程模板和基础外设跑通之后我一直想找一个能覆盖多种调试场景的外设来练手最后选中的是PWM输出。原因很简单PWM这东西看着基础但一旦牵扯到频率和占空比的动态调节定时器的预装载、更新事件、输出比较这些概念就全都会串起来。STM32C542上跑PWM本质就是弄清楚一个公式f_PWM timer_clock / ((PSC1) * (ARR1))以及一个寄存器CCR。搞明白这两件事灯亮度、蜂鸣器音调、电机调速、开关电源的占空比控制就都有了底。这篇文章会把思路、CubeMX配置、HAL库代码、实测波形和踩坑记录都过一遍。适合手里正好在调STM32C542的人也适合其他STM32系列想彻底搞懂PWM动态调节的人。我尽量把每一步为什么这么做讲清楚而不是只丢一段能跑的代码。1. PWM输出到底在配置什么1.1 占空比和频率两个决定控制效果的参数PWM的物理本质是一串周期固定的方波高电平时间占整个周期的比例叫占空比。频率决定动作的快慢占空比决定“平均能量”的大小。用LED举例频率只要超过几十Hz人眼就感觉不到闪烁这时改变占空比就是在改变亮度用蜂鸣器举例占空比50%时改变频率听到的就是音调变化用直流电机举例频率决定电流纹波和噪声占空比决定等效电压。所以你做任何PWM应用第一件事不是打开CubeMX配寄存器而是先搞清楚负载对频率和占空比分别有什么要求。普通LED呼吸灯1kHz绰绰有余电机驱动通常用2kHz到20kHzDCDC开关电源动不动就100kHz以上要求完全不同。STM32C542的定时器PWM在这些场景里都能用但不同频率段下能达到的调节精度差别很大这个点在后面第4章会展开说。1.2 定时器为什么能自动输出PWMSTM32定时器的PWM输出可以这样理解内部有一个计数器cnt从0加到ARR加到ARR后再回0如此循环这就决定了周期。CCR是一个比较值每次cnt与CCR比较当cnt小于CCR时输出高电平大于等于CCR时输出低电平。这样一来占空比就是CCR和ARR之间的比例而周期就是计数器跑完一个ARR需要的时间。用数学写出来就是计数频率 timer_clock / (PSC 1)PWM频率 计数频率 / (ARR 1) timer_clock / ((PSC 1) * (ARR 1))占空比 CCR / (ARR 1)这个公式是整个PWM调频调占空比的地基。后面的所有代码包括动态改频率、改占空比本质都是在改这三个寄存器PSC、ARR、CCR。只要把这三者的关系印在脑子里读任何ST的PWM代码都会轻松很多。1.3 为什么不用GPIO翻转模拟PWM新手常见的思路是用定时器中断每次中断里翻转GPIO用变量控制脉宽。这在低频下能用但有三个问题。第一中断频率越高CPU被占得越狠到了几十kHz中断处理和GPIO翻转本身的开销已经让系统几乎没有余量干别的。第二脉冲宽度完全靠软件执行时间决定精度受中断延迟抖动影响波形会一高一低地晃。第三想同时控制多路PWM软件模拟基本是灾难。而硬件定时器PWM的好处是计数器、比较器、输出极性都在硬件里自动跑改CCR只影响当前周期之后的占空比CPU开销几乎为零。所以除非是极简单的场景否则直接走定时器。STM32C542上的定时器资源比较丰富通用定时器TIMx足够做大部分PWM输出带死区互补和刹车功能的高级定时器更适合电机控制。这篇我用通用定时器TIM3来演示因为它的输出引脚和中断配置都比较宽松适合当试验台。2. 工程准备与CubeMX配置2.1 芯片选型和工程创建用STM32CubeMX创建工程在Part Number搜索框输入STM32C542选中对应型号。首次使用需要下载固件包CubeMX会提示在线安装确认即可。工程创建本身很简单但有一个习惯要养成先明确你要用的引脚和定时器资源再在Pinout视图里分配不然很容易出现引脚冲突。我这次默认用TIM3_CH1输出在CubeMX的Pinout视图左侧找到TIM3勾选Channel1 PWM Generation。引脚会自动分配我这边分配到了PA6但不同型号、不同封装可能映射不同以CubeMX实际布局为准。不要死记PA6一定对应TIM3_CH1这个在不同系列上是有差异的。2.2 时钟树里的定时器时钟很多PWM频率对不上问题就出在时钟树。CubeMX默认会生成系统时钟配置但定时器时钟不一定等于系统主频。比如我把主频设置在160MHz如果APB1分频系数为1TIM3挂在这个总线上它的输入时钟就是160MHz如果APB1分频系数为2那么挂在这个总线上的定时器时钟通常会自动倍频到和HCLK一样仍然是160MHz。这个规则在不同系列上略有差别最稳妥的办法是在CubeMX的Clock Configuration页面里直接看TIM3的时钟频率是多少把那串数字抄下来后面计算全用它。我这次工程的时钟树配置下TIM3的输入时钟是160MHz。如果你用的主频不同后面所有公式里的timer_clock都要换成你自己的值。2.3 TIM3的PWM生成配置在CubeMX的Timers - TIM3界面里做以下配置Clock Source选择Internal ClockChannel1选择PWM Generation CH1Prescaler设为159Counter Period设为999Pulse设为500Auto-reload preload设为EnableOutput极性设为High。解释一下这些值怎么来的。目标频率1kHztimer_clock为160MHz套公式计数频率 160MHz / (159 1) 1MHzPWM频率 1MHz / (999 1) 1kHz占空比 500 / (999 1) 50%Auto-reload preload这里我建议一开始就打开。这个选项的意思是ARR值先写入预装载寄存器等更新事件到来时才真正生效。后面做动态调频时配合这个特性可以避免很多波形突变的问题。Output极性选High的意思是计数器值小于CCR时输出高电平这也符合大多数人的直觉。2.4 硬件连接和验证工具PWM输出引脚接示波器或逻辑分析仪。手头没有示波器的话先接一个LED串联1k电阻配合人眼观察呼吸灯效果也能验证基本功能。但要说一句LED只能看到占空比变化看不到频率变化频率需要用示波器或者至少能用脉冲计数的方式验证。我这把USART2也接上了通过USB转串口模块连到电脑。后面动态调节频率和占空比我打算用串口命令来控制这样比每次改代码重新烧录高效得多。如果你只是做固定PWM输出串口这一步可以跳过。3. 代码实现稳定输出并能动态调节3.1 先用HAL库把PWM跑起来CubeMX生成工程后默认只初始化了定时器并没有启动PWM输出。要在main函数里加一行HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);这个函数会配置输出比较通道使能并把PWM信号真正映射到引脚。没有这一行前面CubeMX的配置再完整也不会从IO口冒出波形。启动之后用示波器或者LED就能看到1kHz、50%的方波了。在main函数里建议再加一个while(1)里的延时或者直接空循环。PWM是硬件自动输出的不需要CPU持续参与所以while循环里什么都不干波形也会正常走。3.2 动态调占空比为什么只改CCR就够了修改占空比最简单一个宏搞定__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 450);这个宏实际是把CCR寄存器写入新值。只要CCR在0到ARR范围内占空比就从原来的50%变成了45%。它不会影响PWM周期计数器也不需要重新初始化硬件会自动在下一个计数周期按新CCR比较。这里有个容易忽略的点如果你在CubeMX里把Pulse字段设成大于ARR的数那输出会一直保持高电平。这不是占空比100%而是一种异常状态看起来像引脚被拉死了。所以给CCR赋值时最好加一个范围判断至少保证不超过ARR。3.3 动态调频率PSC和ARR的正确修改顺序改频率就不是改一个寄存器那么简单了。因为频率由(PSC1)*(ARR1)共同决定理论上既可以改PSC也可以改ARR。但实际调参我建议固定一个变量只改另一个否则很容易把占空比同时带偏。我常用的做法是固定PSC只改ARR。占空比要跟着重算因为ARR变了CCR必须按新的ARR比例换算。把整个逻辑封装成这样一个函数#define PWM_TIMER_CLOCK 160000000UL #define PWM_PSC 159 // 固定分频计数频率 1MHz void pwm_set_freq_duty(uint32_t freq_hz, uint32_t duty_percent) { uint32_t arr, ccr; if (freq_hz 0 || duty_percent 100) { return; } arr PWM_TIMER_CLOCK / ((PWM_PSC 1) * freq_hz) - 1; if (arr 0xFFFF) { arr 0xFFFF; } ccr (arr 1) * duty_percent / 100; __HAL_TIM_DISABLE(htim3); __HAL_TIM_SET_AUTORELOAD(htim3, arr); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); __HAL_TIM_CLEAR_FLAG(htim3, TIM_FLAG_UPDATE); __HAL_TIM_ENABLE(htim3); }为什么先关定时器再改因为直接修改ARR的瞬间计数器可能已经越过新ARR如果没同步好会先输出一个异常的宽脉冲或窄脉冲。关掉再开是最简单粗暴但可靠的办法输出中断时间在几微秒内对多数应用无所谓。如果要求严格不断流那要打开ARR预装载用更新事件同步代码更复杂。对于通用调试先停后改就够了。关于PSC的选择确定一个PSC后频率调节范围就和ARR位数绑定了。比如PSC159时计数频率为1MHzARR的16位最大值是65535能输出的最低频率约1MHz/65536≈15.26Hz最高频率受占空比分辨率限制。如果ARR太小比如只有10那占空比只能按10%的步进调整根本没法精细控制。所以到了高频段我会考虑调小PSC让计数频率提上去。3.4 串口遥控一个能用的调试命令纯代码里写死频率没意思我加了一个串口解析PC发送“PWM 1000 50\r\n”板子收到后解析出频率和占空比调用上面的pwm_set_freq_duty。串口用中断接收每次收到一行完整数据就触发解析。简化版解析如下int pwm_parse_cmd(char *buf) { uint32_t freq, duty; if (sscanf(buf, PWM %lu %lu, freq, duty) 2) { if (freq 10 || freq 1000000) { return -1; } if (duty 100) { return -1; } pwm_set_freq_duty(freq, duty); return 0; } return -1; }接收缓冲区处理要注意“粘包”问题。串口收到的不是一个完整命令包可能分多次到达。我这里用一个简单的环形缓冲区收到\r或\n才认为一行结束。解析做完后把当前频率和占空比回显给上位机方便确认。这种“串口PWM”的组合非常实用后续你要做电机、LED、蜂鸣器控制都可以沿用这一套指令交互模式。4. 实测验证与参数误差分析4.1 示波器上的实测值算的和量的基本对得上我用示波器分别测了几组数据汇总如下设定频率(Hz)实测频率(Hz)设定占空比(%)实测占空比(%)备注100100.0320.019.9低频下非常准10001000.150.050.0基础配置几乎无偏差1000010001.975.074.8误差开始出现1000009992310.010.6高频时测量误差和器件误差都在增大500000498.7k30.031.2占空比步进变大明显受限低频段算的和量的基本一致误差主要在示波器本身的测量精度上。到了100kHz以上误差来源变多一是探头和线缆的寄生电容导致边沿变缓二是ARR变小后占空比步进变大。比如100kHz时PSC159的情况下ARR110占空比最小步进是10%那30%和31.2%的差别一部分就是步进带来的。4.2 频率、分辨率与占空比精度如何权衡占空比分辨率本质是1/(ARR1)。在固定PSC下频率越高ARR越小分辨率越差。以PSC159、计数频率1MHz为例目标频率(Hz)ARR1占空比最小步进100100000.01%100010000.1%100001001%1000001010%10000001无法调这说明一个铁律想在高频下精细调占空比必须提高计数频率。比如把PSC改成0计数频率变成160MHz100kHz时ARR11600占空比步进变成0.0625%好很多。但代价是ARR变小时16位ARR能输出的最低频率会变高所以在低频需求和高频分辨率之间要做一个取舍。我在实际做频率调节时会写一个自动选择PSC的函数让ARR1落在某个理想区间比如1000到10000。这样兼顾频率范围和占空比分辨率。代码量不大但效果很明显尤其是你要扫频测试的时候。4.3 动态调节时最容易看到的毛刺把PWM从1kHz、50%直接改成10kHz、50%时如果顺序是先改ARR后改CCR中间会有一两个周期的占空比异常。原因前面提到了ARR已经变成新值但CCR还没跟上如果旧CCR大于新ARR输出就会一直保持高电平。我在示波器上实际看到过这个现象切换瞬间出来一个全高脉冲宽度比正常周期长很多如果驱动的是功率器件这种毛刺轻则产生噪声重则搞坏东西。所以动态调频时ARR和CCR必须同步更新。上面pwm_set_freq_duty函数里先disable再同时改ARR和CCR就是防这个。另一个容易忽略的点是modify_freq时如果使用了PSC变化PSC也是预装载的同样要等更新事件才生效。先disable再enable相当于人为制造了一个更新事件所以PSC的修改也能同步生效。5. 常见问题与排查技巧5.1 PWM完全没输出怎么办PWM没输出这个问题十次有八次是下面几个原因没有调用HAL_TIM_PWM_Start。启动函数没执行引脚就是个普通IO示波器看到一条平线。引脚复用被覆盖。CubeMX生成的代码一般没错但如果你手动改过GPIO配置可能把AF模式覆盖成普通输出。定时器时钟没配好。时钟树里TIM3的时钟如果是0计数器根本不会跑。CCER里的输出极性不对。极性配置为High时CNT小于CCR输出高电平配置为Low则反逻辑占空比看起来会反转。排查建议按顺序来先确认代码里有没有启动函数再看GPIO复用再用调试器看CNT寄存器是不是在跑最后看CCER寄存器。只要CNT在跳波形就一定会出来剩下的只是极性或者引脚映射的问题。5.2 频率对不上先查三个数频率对不上的情况绝大多数出在计算上。最经典的坑是把定时器时钟直接用成了HCLK但实际定时器时钟和APB1分频有关。ST的规则是如果APB分频系数为1定时器时钟等于APB时钟如果分频系数大于1定时器时钟等于APB时钟的2倍。这里非常容易出错。第二个坑是PSC和ARR忘记加1。这两个寄存器描述的是分频倍数减1实际倍数是寄存器值加1。我见过好几个人都忘了加1导致频率差一整倍。第三个坑是CubeMX生成的时钟树里定时器时钟可能不是整数比如从PLL分频出来后是159.7MHz之类。这时不要四舍五入成160MHz去算直接按实际值算不然频率越高误差越明显。5.3 改了CCR占空比纹丝不动改CCR无效常见原因有两个。一个是CCR的预装载没有生效你写入的值进了预装载寄存器但没等到更新事件所以输出还是旧值。另一个是你可能在某个中断或者别的代码路径里又覆盖了CCR。实际遇到这种问题我一般这么做先在调试器里读CCR寄存器确认硬件里的值到底是新还是旧。如果寄存器值已经更新但输出没变那问题在预装载或者影子寄存器如果寄存器值本身就是旧的那就要查代码逻辑看是不是被别的函数改回去了。还有一种隐蔽情况CubeMX生成的初始Pulse字段大于ARR导致输出一直高电平这时候改小CCR也无效因为CCR虽然变了但比较逻辑里CCR本来就一直大于ARR。这种问题用调试器一读就能发现。5.4 调节频率时输出会抖动如何进一步优化先停后改的方案已经能用但要求极端平滑时可以采用更严格的同步方案开启ARR预装载在更新事件中断里修改ARR和CCR让所有参数在同一个更新时刻生效或者用DMA在更新事件时把新参数搬运到影子寄存器或者使用更高分辨率的定时器外设具体看芯片资源。我的看法是通用试验阶段先停再改已经足够等要做到电机FOC或者数字电源再上同步更新方案。否则代码复杂度上去了出错概率也上去了没必要为了一个基本功能提前优化。这里放一个排查速查表方便你对着查现象可能原因定位方法完全没有波形未调用PWM_Start、引脚复用不对、时钟没开检查启动函数、AF配置、调试器看CNT频率偏差持续很大APB分频导致定时器时钟算错、PSC/ARR忘加1查CubeMX时钟树实际值重算公式占空比和设定值不符CCR溢出、极性反、预装载未更新读CCR寄存器看CCER极性改CCR后输出不变被其他代码覆盖、预装载未生效断点检查CCR写入路径动态调频时出现毛刺ARR和CCR不同步更新先disable再改或使用预装载同步这块板子我玩下来最大的感受是PWM看似简单但它几乎是理解定时器系统的钥匙。你一旦搞清楚了CNT、ARR、CCR三者的关系后面做输入捕获、编码器接口、PWM测量都会顺很多。我后面计划把这套PWM输出扩展成带死区控制的互补输出用来驱动半桥电路到时候再接着写。眼下如果你正在调STM32C542的PWM建议先把频率和占空比的公式抄在纸上再对照CubeMX里的实际时钟值算一遍能少走很多弯路。