ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 C5A3R PWM输出实战:定时器配置、频率占空比动态调整与避坑指南

STM32 C5A3R PWM输出实战:定时器配置、频率占空比动态调整与避坑指南 拿到C5A3R这颗料之后我最想干的事不是点灯也不是跑串口而是先让它输出一路干净的PWM。原因很简单PWM这玩意儿太常用了电机调速、舵机转角、LED亮度、TEC温控功率甚至DCDC的反馈环路全是靠频率和占空比这两个参数说话的。这篇是C5A3R开发系列的第四篇前几篇已经把时钟、GPIO、串口这些基础功能跑通了这篇重点放在PWM的输出、动态修改频率和占空比以及在实测过程中几个值得注意的坑上。先说清楚这篇适合谁。如果你刚接触STM32想搞懂定时器输出PWM的基本流程能看如果你已经在用F1或G4系列现在要切到C5系列那更要看——C5的内核变成了Cortex-M33定时器寄存器结构和HAL库接口虽然大体延续了F系列的风格但细节上有不少差异直接照搬老工程的代码很容易翻车。我会把初始化、运行中改参数、示波器实测这三件事串在一起讲尽量把每个选择背后的理由也讲明白。1. C5A3R的定时器家族PWM能出在哪几路时钟怎么算1.1 一路PWM背后牵扯到的硬件单元PWM信号本质是定时器计数溢出和比较匹配这两个事件的产物。C5A3R的定时器资源基本上覆盖了通用场景高级定时器TIM1通用定时器TIM2、TIM3、TIM4、TIM5基本定时器TIM6、TIM7再加上低功耗定时器LPTIM。以我手上这块LQFP64封装的芯片为例引脚足够把TIM1的四路互补输出和TIM2/TIM3/TIM4的独立通道都引出来做一个小型电机控制板或者多路LED驱动板是绰绰有余的。刚上手时最容易忽略的一点是C5A3R没有继承F系列里那种某个定时器的某个通道固定绑定在某个引脚的直觉。虽然STM32的引脚复用AF表最终还是要查参考手册但CubeMX里选引脚时它会直接过滤出可用AF这点比翻手册方便得多。我实际配置时先确认了一下TIM3的PC6/PC7能不能复用出CH1/CH2然后在CubeMX的Pinout视图中直接做了设置这样比对着几百页的AF表硬找快得多。1.2 定时器时钟翻倍逻辑APB分频的联动效应这是PWM频率计算里最邪门也最容易被忽略的一环。很多人直接在CubeMX里看到APB1外设时钟是125MHz于是觉得挂APB1上的TIM3时钟就是125MHz然后就拿125MHz去除PSC和ARR算频率——这样算出来的结果往往是实际波形的两倍或者恰好差一个系数。STM32的时钟树规则是如果APBx预分频系数为1那么定时器时钟就等于APBx外设时钟如果APBx预分频系数大于1那么定时器时钟是APBx外设时钟的两倍。C5系列沿用了这个逻辑原因在于内核的APB总线时钟被强制分频后定时器需要更高的时钟来保证计数精度所以内部把2倍频补了回来。按我这块板上配置来举例SYSCLK跑250MHzAHB预分频为1APB1时钟设为125MHz意味着APB1预分频系数是2那么挂在APB1下的TIM3/TIM4定时器时钟就是125MHz×2250MHz。如果我再把APB1预分频改成1定时器时钟会跟着掉到125MHz此时即便PSC和ARR都不变PWM频率也会直接减半。实测时用示波器量到频率不对优先怀疑的不是代码而是时钟树这个翻倍逻辑。1.3 定时器选型不同场景别拿同一款硬上TIM1是高级定时器能输出带死区的互补PWM也能配置刹车输入做故障保护适合驱动半桥、全桥这类拓扑。TIM2/TIM3/TIM4/TIM5是通用定时器能出最基础的PWM其中TIM2和TIM5的计数寄存器是32位的ARR可以顶到很大的数值做低频PWM时不需要担心溢出TIM3和TIM4是16位的ARR最大值只有65535如果时钟不分频最低频率也降不到很低这时候要么拉大PSC要么改用32位定时器。TIM6/TIM7基本定时器没有比较输出通道PWM和它们没关系但可以拿来触发ADC采样这一点后面讲中心对齐模式时会提到。选定时器有个很实用的判断顺序有没有互补和死区需求有就上TIM1有没有极低频需求有就上TIM2/TIM5只是普通调速、调光TIM3/TIM4就够了如果系统一直在低功耗模式里晃悠偶尔才需要输出一个低频PWM那LPTIM才是正解——它不依赖主时钟靠LSI就能跑。2. 从CubeMX到初始化PWM能出波的前置条件2.1 时钟树配置和引脚复用里的细节在CubeMX里选好C5A3R型号之后第一步还是先确认时钟树。我用的是外部晶振HSE配置SYSCLK250MHz然后AHB250MHzAPB1125MHz预分频2APB2250MHz预分频1。按前面的翻倍逻辑TIM1和TIM3的定时器时钟都是250MHz这样算PWM频率时基准统一不容易乱。接着配置引脚复用。以PC6作为TIM3_CH1为例在Pinout视图里把PC6选中后芯片会自动把可复用的外设列出来选择TIM3_CH1即可。这里我踩过一次坑只设置了GPIO的输出模式为AF推挽但忘记在GPIO配置里选中GPIO_AFRL/GPIO_AFRH中正确的AF值。C5系列的GPIO复用表非常庞大同样的引脚AF值不同复用的外设可能完全不同比如PC6可以复用为TIM3_CH1也可能复用为其他功能如果AF选错初始化不报错但引脚上死活不出波形。我后来习惯在CubeMX里生成代码后手动检查一下GPIO初始化里Pin的AF号是不是正确。2.2 初始化结构体的几个关键字段CubeMX生成的基础PWM初始化代码长这样主要逻辑在TIM_OC_InitTypeDef里static void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 249; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 499; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); }解释一下这几个参数。Prescaler249Period999Pulse499基准时钟250MHz代入公式PWM频率 250MHz / ((2491) × (9991)) 250MHz / 250000 1kHz占空比 (4991) / (9991) 50%初值其实并不关键关键是理解PSC、ARR和CCR三者之间的关系。PSC决定计数脉冲的步长ARR决定一个周期数多少个脉冲CCR决定输出在哪个比较点翻转。AutoReloadPreload这一项我建议直接开成ENABLE它的含义是ARR值不会立即写入影子寄存器而是等更新事件才生效。这样在运行中修改频率时波形不会出现一个极短的毛刺周期后文会细说。2.3 生成代码后别漏掉启动函数初始化函数只是把定时器配置好并没有真正开始输出PWM。在main函数里需要显式启动通道HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);这句话最容易忘。忘了的话所有初始化看起来都是对的但引脚上是死的电平。启动之后如果需要停止输出用HAL_TIM_PWM_Stop即可。3. 运行中修改占空比和频率不要只改一个寄存器3.1 修改占空比的三种方式占空比直接由CCR寄存器决定。在运行中改变CCR的值下一周期立即就会体现。常用的三种方式// 方式一HAL提供的宏推荐 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 800); // 方式二直接操作句柄寄存器 htim3.Instance-CCR1 800; // 方式三通过HAL句柄访问 TIM3-CCR1 800;方式一是最安全的因为宏内部对寄存器做了类型转换而且语义明确。方式二、三虽然也能用但在代码审计中语义不够清晰。实测下来把CCR从499改到800时示波器上第一个完整周期开始高电平时间就变了没有中间毛刺因为CCR本身也受预装载位控制CubeMX默认把OC的预装载也开启了。这里顺便说一句cube中OC的预装载没有独立开关时HAL库会在配置通道时根据OCMode自动处理预装载位所以一般情况下不用手动设置。3.2 修改频率的两条路改ARR还是改PSC修改PWM频率本质上只有两条路改ARR或者改PSC。两者效果完全不同。改ARR是调整计数器溢出的周期长度改变的是计数总量。在PSC不变的情况下周期和ARR的关系是线性的频率精度可以做得很细。比如PSC0时ARR每增加1频率只变化一个定时器时钟周期对250MHz来说就是4ns的步进。改ARR的另一个好处是计数过程是平滑的不会出现计数周期跳变的巨大毛刺。改PSC则是改变计数脉冲的步长。PSC的值往往很大而且它不像ARR那样参与比较匹配逻辑其实PSC的变化需要等到更新事件才生效。如果在运行中把PSC从249改成499计数器会突然从一个节奏换到另一个节奏虽然波形最终会稳定到目标频率但过渡过程中会产生一个明显的不规则周期。在电机驱动这类对周期稳定性敏感的场合这是不能接受的。所以我的习惯是如果需要动态调速优先改ARR把PSC尽量设小甚至设为0这样频率调节分辨率最高CPU也不需要做太大位宽的运算。如果是要切换几个差距很大的频段比如从1kHz切到100kHz才考虑动PSC毕竟PSC可调范围更宽能一步到位。3.3 改ARR时千万别把占空比给丢了这是整个PWM动态修改里最容易翻车的地方。ARR和CCR不是独立的占空比的定义是占空比 CCR / (ARR1)当你只改ARR而不改CCR时占空比会立刻发生跳变。比如原本ARR999CCR500占空比50%现在为了把频率提高一倍把ARR改成499CCR还是500占空比直接变成100%输出恒高了。这在电机控制里是灾难性的。所以运行时改频率必须同步计算新的CCRvoid pwm_set_freq_keep_duty(TIM_HandleTypeDef *htim, uint32_t channel, uint32_t new_arr, uint32_t current_ccr) { __HAL_TIM_SET_AUTORELOAD(htim, new_arr); __HAL_TIM_SET_COMPARE(htim, channel, current_ccr); }如果希望占空比稳定在某个百分比那就需要显式计算void pwm_set_freq_keep_duty_ratio(TIM_HandleTypeDef *htim, uint32_t channel, uint32_t new_arr, uint8_t duty_percent) { uint32_t new_ccr (uint32_t)(((uint64_t)new_arr 1) * duty_percent / 100); __HAL_TIM_SET_AUTORELOAD(htim, new_arr); __HAL_TIM_SET_COMPARE(htim, channel, new_ccr); }中间用uint64_t做乘法是为了避免new_arr比较大时32位溢出。C5系列虽然主频高但PWM频率的动态范围也大这种细节值得多写一行。3.4 预装载位的影响为什么改了ARR没立刻生效如果CubeMX里AutoReloadPreload是ENABLE那么写入ARR的新值不会立刻进入影子寄存器而是要等一次更新事件。更新事件在计数溢出时产生。也就是说改了ARR之后当前周期还是会按旧ARR走完下一个周期才开始用新值。这在有些场景下是好事可以避免周期突变但在另一些场景下比如想立刻改变频率做扫频测试就会觉得响应慢了一拍。想让它立即生效可以手动产生一次更新事件把影子寄存器刷新__HAL_TIM_SET_AUTORELOAD(htim3, 499); __HAL_TIM_GENERATE_EVENT(htim3, TIM_EVENTSOURCE_UPDATE);注意这里有个小坑__HAL_TIM_GENERATE_EVENT这个操作会同时把更新标志位清掉如果此时还开着更新中断就得先确认中断状态已经被正确响应否则可能出现一次误触发中断。我在做呼吸灯测试时就因为这个偶发进了一次定时器中断排查了半天。4. 实测示波器上的波形和数据验证4.1 固定频率下改占空比的实测记录把TIM3_CH1接到示波器基准时钟250MHzPSC249ARR999定时器时钟250MHzPWM频率1kHz。依次写入不同的CCR值记录示波器测量的高电平时间目标CCR理论占空比实测高电平时间实测占空比偏差10010%0.101ms10.1%0.1%30030%0.298ms29.8%-0.2%49950%0.501ms50.1%0.1%79980%0.798ms79.8%-0.2%94995%0.952ms95.2%0.2%偏差主要来自示波器自身测量噪声和触发抖动实际PWM的占空比精度是非常高的毕竟CCR是硬件比较器的输出不依赖软件逐周期干预。4.2 固定50%占空比切换频率的实测记录接下来在占空比保持在50%的情况下依次改变ARR值同时算出对应的CCR实测如下ARRPSC理论频率理论CCR实测频率偏差9992491kHz499999.9Hz-0.01%2492494kHz1244.002kHz0.05%994920kHz4920.01kHz0.05%199100kHz9100.03kHz0.03%94250kHz4249.88kHz-0.05%100kHz以上的高频率段波形整体还是方波形状但上升沿和下降沿开始变得圆滑原因是驱动电路的压摆率和示波器探头带宽限制。在20kHz以内的区间频率非常精准这归功于硬件计数器的确定性——软件没有介入逐周期调整所有翻转都是由比较器硬件完成的。4.3 中心对齐模式与ADC采样点的配合电机控制和逆变器里大家不只在PWM通道上看波形还经常需要在实际电流上做采样。这时候建议使用中心对齐模式。TIM1可以配置成Center-aligned模式计数器先向上计数再向下计数PWM波形相对于计数周期是对称的被从中间折叠成两半。中心对齐的核心好处是可以把ADC的采样时刻对准PWM周期的正中间。在这个时间点上电流纹波通常处于中间位置MOS管的开关噪声也刚好避开。ST官方推荐的玩法是TIM1配置为center-aligned开启TIM1的TRGO2输出连接ADC触发源同时把ADC采样窗口对准CCR匹配事件。这样每次PWM的中心点一到ADC自动启动转换CPU完全不用干预。配置中心对齐的HAL代码htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; // 中心对齐模式下计数器从0加到ARR再从ARR减到0一个完整周期是2*ARR中心对齐模式下PWM的频率等于定时器时钟 / (2 × (PSC1) × (ARR1))和边沿对齐差一个2倍系数修改ARR时要注意频率换算。5. 实际应用中的选择电机、舵机和故障保护5.1 PWM频率和电机的关系很多人第一次驱动直流电机时随便填一个频率就开始测结果电机要么啸叫要么力矩忽大忽小。PWM频率对电机的影响特别大频率过低时电流纹波大电枢电感滤波不干净电机会发出人耳明显可闻的啸叫同时转矩脉动大转速不稳频率过高时MOSFET的开关损耗直线上升驱动芯片过热但转速反而不见得更平稳。我自己的经验有刷直流电机的驱动10kHz到20kHz是一个舒适区。把这几个参数同时写进代码里调好之后电机的运转声会从滋滋变成近无声。再往上走比如做到50kHz以上对MOS管的驱动能力和栅极电阻的要求就上来了普通低速驱动板不建议硬拉高频。反而是在无感方波BLDC里换相频率由电转速决定PWM载波频率选15kHz到25kHz比较常见兼顾了噪声和开关损耗的平衡。5.2 舵机控制固定周期、只变脉冲宽度舵机是个特殊的PWM应用它不在乎占空比这个百分比而在乎高电平脉冲的绝对宽度。标准舵机的控制信号是50Hz也就是周期固定为20ms高电平在0.5ms到2.5ms之间变化对应0°到180°。用定时器出这个信号时把ARR和PSC设成让周期恰好是20ms。如果定时器时钟是250MHzPSC设成249得到1MHz每微秒一个计数那么ARR19999时周期(199991)/1MHz20ms。CCR的范围在500到2500之间对应0.5ms到2.5ms的高电平。这里有个容易忽略的细节占空比的计算公式在这里依然成立但舵机厂商标注的脉宽范围才是真正的规格书不是看占空比百分之多少。5.3 刹车输入故障时靠硬件钳住PWM输出高级定时器TIM1除了能输出互补PWM还有一个非常实用的硬件机制刹车输入Break Input。过流信号、过压信号、急停信号都可以接进刹车引脚当刹车事件触发时TIM1的所有PWM输出会立即进入预设的安全状态不用CPU写一行中断代码。这个延迟是纳秒级甚至更短比靠软件中断响应快一两个数量级。配置刹车输入需要用到HAL库的扩展接口TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 100; // 死区时间单位取决于TDTS sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeConfig);实际测试时我把GPIO接到刹车脚给一个高电平示波器看到PWM立即变为低电平完全没有软件参与。在这种场景下刹车输入比任何标志位检查都可靠。有个细节刹车事件发生后会有锁存清除刹车源后需要重新触发起动或者调用相应的清标志函数复位MOE位输出才恢复。5.4 DMA方式更新CCR让CPU彻底解放如果只是想做一个平滑变化的PWM比如呼吸灯、渐变亮度或者需要输出一个预定义的PWM包络与其在定时器更新中断里频繁改CCR不如用DMA自动搬运。HAL库提供了HAL_TIM_PWM_Start_DMA接口把一个CCR值的数组地址交给DMADMA会在每个特定事件把数组里的新值搬运到CCR寄存器循环输出。配合中心对齐模式这就是一个简陋但可用的任意波形发生器。我试过用DMA循环输出一个32点三角波数组让PWM占空比从10%平滑过渡到90%再回来CPU占用率几乎为0。如果以后要做的项目里会有复杂的PWM调制需求比如电机FOC里实时改变占空比DMA这块值得深入研究。6. 运行时频率动态调整的一个工程化技巧最后分享一个我在实际项目里反复用到的处理方式。如果系统需要在运行时频繁调整PWM频率而不是只在初始化时设置一次尽量避免在每个控制周期里用浮点或整数除法现场计算PSC和ARR。因为定时器本身是硬件确定性的但除法运算耗时不定中断上下文里尤其危险。更好的做法是预先把所有需要使用的频率档位对应的ARR/CCR值计算好存进常量数组运行时按序号查表即可。typedef struct { uint32_t arr; uint32_t ccr; } pwm_profile_t; const pwm_profile_t pwm_profile[] { {2499, 1249}, // 50kHz占空比50% {4999, 2499}, // 25kHz占空比50% {9999, 4999}, // 12.5kHz占空比50% }; void pwm_apply_profile(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t idx) { __HAL_TIM_SET_AUTORELOAD(htim, pwm_profile[idx].arr); __HAL_TIM_SET_COMPARE(htim, channel, pwm_profile[idx].ccr); }查表方式的最大好处是切换频率时不存在实时计算误差而且所有占空比都是预设好的不会因为除法取整产生微小偏差。另一个附带的好处是代码审查时参数一目了然调试时可以快速定位是哪一档的参数出了问题。TEC温控这类需要极小步进调节功率的场景如果只靠PID算法算占空比偶尔会遇到CCR更新太频繁导致波形抖动的问题。这时可以适当放宽更新间隔让每个占空比值至少维持几个PWM周期这样温度曲线既平滑PWM输出也稳定。在做C5A3R的PWM实测过程中我最大的感受是芯片本身跑250MHz毫无压力定时器资源也足够丰富真正决定PWM质量的反而是时钟树配置和预装载位的使用习惯。每次调频率不准先查时钟树分频再查预装载有没有开启这两步排查完90%的问题都能定位。输出PWM和修改频率占空比只是定时器知识的第一层后续如果再写互补PWM和死区配置以及基于DMA的多路PWM联动我会继续把这部分经验整理出来。
返回列表