ARTICLE DETAIL

资讯详情

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

STM32C5A3R PWM实战:动态调频调占空比与寄存器配置详解

STM32C5A3R PWM实战:动态调频调占空比与寄存器配置详解 STM32C5A3R 开发系列写到第四篇前面已经把手上的工程从点亮 LED 一路推进到串口通信这次回到一个控制类项目里几乎绕不开的需求输出 PWM并且能在运行中修改频率和占空比。为什么单独把这块拿出来写因为我在这个芯片上调 PWM 的时候发现真正花时间的不是把波形点亮的那个“HAL_TIM_PWM_Start”而是后面这些事到底填多大的 PSC 和 ARR 才是想要的频率、改频率之后为什么占空比跟着跑了、示波器量出来的频率和算出来差一倍又是什么鬼。这篇笔记就把我看过的寄存器关系、CubeMX 里的配置顺序、实测踩的坑一次讲清楚。我默认你已经有一个基于 STM32CubeMX 生成的 STM32C5A3R 工程CubeMX 版本和 HAL 库只要是近两年的版本操作基本一致。下面直接进入主题。1. PWM 波形是怎么在 STM32C5A3R 内部形成的1.1 定时器选型先分清高级、通用、基本三档STM32C5A3R 的定时器资源和 F1/G0 那些老伙伴套路很像分三档高级定时器TIM1、TIM8带互补输出、死区插入、刹车输入专门伺候电机控制和逆变器这类需要“两路反相 PWM”的场景。通用定时器TIM2、TIM3、TIM4 这些有独立通道可以输出标准 PWM日常调个灯、转个风扇、控制舵机都够用。基本定时器TIM6、TIM7 一类的只有时基功能不能输出 PWM。这篇的主角我选的是通用定时器 TIM2通道 1。一来是它不受死区、互补这些高级功能干扰配置最简单方便说清楚 PSC、ARR、CCR 三个寄存器的关系二来通用定时器在大多数封装上都有多个引脚映射可选不用和别的外设抢管脚。等你把通用定时器跑明白了将来用 TIM1 做带死区的高级 PWM也就是在这个基础上补几行配置的问题。1.2 占空比的本质CNT 和 CCR 的不断比较理解 PWM 输出最核心的一件事定时器内部有一个计数器 CNT它在计数时钟的驱动下从 0 往上加加到 ARR 设定的值之后就溢出归零开始下一个周期。与此同时定时器还有一个比较寄存器 CCR硬件每一刻都在拿 CNT 和 CCR 比较CNT CCR输出引脚为高电平CNT ≥ CCR输出引脚为低电平。这一个周期里高电平占了多大比例就是占空比。这个机制听上去简单但它解释了很多现象为什么 CCR 写 0 输出就一直是低电平为什么 CCR 一旦超过 ARR 输出就变成了常高为什么改频率后占空比会跟着变——因为 ARR 一变CNT 一个周期的总长度就变了可 CCR 还是老值高电平的“绝对时间”没变但“相对比例”变了。1.3 频率公式和 ARR1 这个幽灵PWM 频率的完整公式是f_pwm f_timer_clk / ((PSC 1) * (ARR 1))占空比公式是占空比 CCR / (ARR 1) * 100%这两个公式里的 “1” 我单独拿出来说。PSC 预分频器和 ARR 自动重载寄存器硬件上是真的“分频系数”减一。也就是说你在 CubeMX 里填 Prescaler 31实际分频系数是 32填 Counter Period 999实际计数周期是 1000 个计数时钟。我见过好几个新手在算频率的时候把 ARR999 直接当成 999 个计数周期结果算出来的理论频率和示波器实测总是差那个千分之一对不上号。后面所有代码和计算都严格围绕 “ARR1” 展开这会省掉很多排查时间。举个例子假设我在这个工程里的 TIM2 内核时钟是 32 MHz想要 1 kHz PWM计数分辨率做到 0.1% 左右设 PSC 31则计数时钟 32MHz / 32 1MHz设 ARR 999则 PWM 频率 1MHz / 1000 1kHz若要占空比 50%则 CCR (ARR1) × 50% 500。这套参数最后算出来的频率是 1000.0Hz占空比精度是 0.1% 一档。这个三位小数的占空比粒度对灯光、风扇、充电枪 CP 信号这类应用都足够了。2. CubeMX 初始化阶段两处最容易影响实际波形的地方2.1 先核对时钟树里的 TIM 内核时钟别只看 APBx 总线时钟这是我在 STM32C5A3R 上调 PWM 遇到的第一个大坑也是网上问得最多的现象“我明明在 CubeMX 里算好了 PSC 和 ARR为什么示波器量出来频率正好差一倍”原因就在于定时器时钟不一定等于 APBx 总线时钟。STM32 有个惯性设计如果 APB 预分频器不是 1那么挂在这个总线上的定时器时钟会翻倍。也就是说APB1 如果是 16MHzTIM2 的内核时钟可能是 32MHz。你在 CubeMX 的 Clock Configuration 页面看左侧那一列时钟树找到 TIM2 那一行上面写的时钟值才是真正喂给定时器的频率所有 PSC、ARR 的计算都要以此为准。我现在的工程里 TIM2 内核时钟就是 32MHz所以后文的计算和代码都以 32MHz 为基准。你要是照着做请务必先打开自己工程的 Clock Configuration 看一眼把 32MHz 替换成实际数值。这里错一步后面全是错的。2.2 定时器参数面板六个值老实填好在 CubeMX 里选择 Timers → TIM2然后按下面设置配置项值说明Clock SourceInternal Clock使用内部时钟不做外部编码器之类的事Channel1PWM Generation CH1让 CH1 工作在 PWM 模式下PrescalerPSC31实际分频 32 倍Counter PeriodARR999计数周期为 1000Auto-reload preloadEnable打开 ARR 影子寄存器CH1 PulseCCR499初始 CCR约 50% 占空比这里注意 CubeMX 界面里 Counter Period 写的是“Counter Period”很多教程说“ARR”其实就是同一个东西。Pulse 就是比较值 CCR直接决定初始占空比。还有 PWM Mode 一般默认是 PWM Mode 1向上计数这个组合是绝大多数场景的正确选择。PWM Mode 2 和 Mode 1 的输出极性正好相反如果你在别处复制配置时不小心改成了 Mode 2占空比会和预期互补逻辑全反。2.3 预装载寄存器到底要不要开CubeMX 里的 “Auto-reload preload” 对应的是 ARR 的影子寄存器功能我建议默认 Enable。解释一下为什么。影子寄存器的意思是你通过代码修改 ARR 时这个新值并不会立刻进入硬件实际工作的寄存器而是要等一次更新事件UEV才整体生效。这样一来ARR 从“旧值”切到“新值”正好发生在两个 PWM 周期的边界上而不是在 CNT 数到一半的某个任意时刻。如果没有这个影子寄存器CNT 可能正数到 500你突然把 ARR 改成 99CNT 瞬间超过上限这一拍就会输出一个不完整的畸形周期。同样的逻辑也适用于 CH1 的 Output compare preload。在 CubeMX 里把 CCR 的 preload 也打开运行中改占空比会等下一个周期边界才更新看起来更平滑。如果你要做某些实时性要求非常高的电源控制希望 CCR 一改立刻反应到引脚上那可以把这个 preload 关掉。但从通用 PWM 应用的角度我统一建议开着。3. 修改频率与占空比先算对账再动寄存器3.1 只改占空比一个宏就能解决如果你只需要在频率不变的情况下调占空比那 HAL 库里现成的宏就够了__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 300);这个宏本质就是往 TIM2 的 CCR1 寄存器里写一个数。比如上面 ARR999 的例子里CCR300 就是 30% 占空比。写 0 是 0%写 999 时会有 999/1000 ≈ 99.9%看起来就是几乎常高。想要绝对 100% 就把 CCR 写成大于 ARR 的数。这条路径非常简单但有一个前提ARR 不能变。一旦频率变了ARR 变了你再写同一个 CCR 数字占空比就完全不是原来的比例了。这就是下一个问题的主角。3.2 只改频率PSC 不动ARR 和 CCR 必须联动先展示一个反面案例。假设我现在还是 1kHz、50% 占空比的 PWM寄存器状态是 PSC31、ARR999、CCR500。我想把频率改成 2kHz于是只写了一行__HAL_TIM_SET_AUTORELOAD(htim2, 499);这时 ARR499计数周期变成 500PWM 频率涨到 2kHz。可是 CCR 还是 500比 ARR 还大输出直接变常高。就算我把 CCR 设置成 400占空比也是 400/50080%而不是我原本想要的 50%。正确的做法是改频率的同时按新的 ARR 重新计算 CCRnew_ccr (uint32_t)((uint64_t)(new_arr 1) * duty_percent / 100);意思就是新 CCR 目标占空比 × 新周期。这个联动关系必须刻在脑子里。实际工程里我不会写两个分散的宏调用去改频率因为太容易漏算 CCR 了。后面第 4 节我会给一个封装好的函数直接把“指定频率 指定占空比”丢进去函数内部算好三个寄存器的值。3.3 同时改频率和占空比时用什么节奏更新寄存器更复杂的场景是运行中连续调速比如风扇从 800rpm 平滑加速到 3000rpm或者呼吸灯从亮到暗再循环。这种场景要同时改 ARR 和 CCR而且改写的时机有讲究。最简单、也最不容易出错的方式是写一个统一的函数一次计算好 PSC、ARR、CCR 三个值然后依次调用__HAL_TIM_SET_PRESCALER、__HAL_TIM_SET_AUTORELOAD、__HAL_TIM_SET_COMPARE写入。只要预装载功能开着三个新值都会在下一次 UEV 时一起生效输出波形基本察觉不到抖动。如果你对现有 PWM 波形的相位有严格要求那可以在定时器更新中断里改写这些寄存器保证写入时序和 PWM 周期严格同步。不过大多数普通控制应用没必要这么精细统一写一次已经足够平滑。唯一要注意的是不要图省事只改 ARR 而把 CCR 留在旧值上。还有一个不常用的“立即生效”手段手动把 EGR 寄存器的 UG 位置 1强制产生一次更新事件。但这个操作会把 CNT 清零PWM 波形整体相位会跳变对相位敏感的场景是灾难。我在做一般电机调速时基本不用这里提出来是想告诉你它有这个副作用别在排查间歇性毛刺的时候忘了这条。4. HAL 工程落地一份可以直接抄的代码4.1 启动 PWM 的最小代码CubeMX 生成工程后只需要在 main 函数里加一行启动调用HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);放在 MX_TIM2_Init() 之后。打开 PWM 输出后引脚上就会按 CubeMX 里配置的初始参数输出波形。这个调用还有个配对函数 HAL_TIM_PWM_Stop需要临时关输出时调用。如果你的电路里有外部使能引脚最好用 GPIO 控制使能避免直接 Stop 定时器后引脚电平不定带来的误动作。4.2 一个统一修改频率和占空比的函数先声明下面的函数以 TIM2 内核时钟 32MHz 为基准实际使用时改成你自己的定时器时钟值。#define TIM2_CLK_HZ 32000000u /* 改为 CubeMX 时钟树里 TIM2 的实际时钟 */ /** * brief 设置 PWM 频率和占空比 * param freq_hz 目标频率单位 Hz * param duty_permille 目标占空比单位千分比1000 表示 100% */ void pwm_set_freq_duty(uint32_t freq_hz, uint32_t duty_permille) { uint32_t psc, arr, ccr; uint64_t tmp; /* * 核心逻辑从 PSC0 开始逐级加大预分频 * 只要 ARR 没超过 16 位上限 65535就采用这一组参数。 * 这样能保证计数分辨率尽可能高。 */ psc 0; while (1) { uint32_t timer_clk TIM2_CLK_HZ / (psc 1u); arr timer_clk / freq_hz; if (arr 1u) { arr - 1u; } if (arr 0xFFFFu) { break; } psc; } /* CCR (ARR1) * 占空比用 uint64_t 避免中间结果溢出 */ tmp ((uint64_t)(arr 1u) * duty_permille) / 1000u; ccr (uint32_t)tmp; __HAL_TIM_SET_PRESCALER(htim2, psc); __HAL_TIM_SET_AUTORELOAD(htim2, arr); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); }为什么占空比参数用千分比而不是百分比因为百分比在 0~100 的整数空间里对 ARR 较小的场景误差太大。比如 ARR49 时每 1% 的步进只有 0.5 个计数周期精度惨不忍睹。用千分比后10% 是 100 个千分点虽然 ARR 小时依然有舍入但整体误差好很多。这个细节在低 ARR、高频率的场景尤其值钱。4.3 在主循环里的实际调用方式以下面这段演示代码为例int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); while (1) { /* 10kHz占空比 30% */ pwm_set_freq_duty(10000, 300); HAL_Delay(3000); /* 改成 1kHz占空比 75% */ pwm_set_freq_duty(1000, 750); HAL_Delay(3000); } }在切换那一下建议用示波器盯着 CH1 引脚观察一下。只要预装载开着这个函数内部三个寄存器写入后波形会在下一个周期边界统一更新不该出现肉眼可见的毛刺。还有一点如果你的应用希望某个固定频率下只调占空比那直接调用__HAL_TIM_SET_COMPARE就够了不需要每次走过一遍这个函数。这个函数适合频率和占空比都要变的场合。真的需要极快速修改占空比时频繁除法计算反而浪费时间。5. 用示波器实测波形时我的排查思路5.1 先看频率差一半是时钟树问题差一点是“1”问题示波器探头夹到 PWM 引脚第一眼先看频率对不对。如果实际频率正好是理论值的一半十有八九是定时器时钟没算对。回到 CubeMX 的时钟树界面看 TIM2 那一行的时钟频率到底是多少核对 APB1 预分频是否为 1。如果 APB1 预分频不是 1TIM2 时钟大概率等于 APB1 时钟的 2 倍。你不看时钟树直接在代码里写“32MHz”算参数出来就可能差一半。如果差的不是整数倍比如理论 1000Hz实测 999Hz那就是公式里的 “ARR1” 被忽略了。Counter Period 填 999 时实际周期是 1000这个差别在低频时会被示波器读出来。5.2 没有输出按这个顺序查检查是否调用了 HAL_TIM_PWM_Start。这个最容易遗漏定时器初始化函数 MX_TIM2_Init 只是配置寄存器并不会让引脚产生波形。检查 GPIO 复用。CubeMX 生成工程一般会自动配好引脚复用但如果你手工改过引脚或者从别的工程复制代码AFR 寄存器可能漏配。网上搜“stm32 pwm 没波形”十有八九是这个问题。检查引脚有没有和其他外设冲突。C5A3R 的多路复用很丰富有时同一个引脚会被分配给串口和定时器CubeMX 里会标红冲突但自己手写代码时容易忽视。5.3 占空比不对劲先怀疑 CCR 被 ARR 改动波及占空比异常最常见的原因是 CCR 和 ARR 失配。我前面演示过ARR 变了而 CCR 没变占空比会剧烈跳动。这种问题在示波器上看起来非常诡异有时你会看到波形突然变成常高有时是高电平宽度没变但频率变了。排查方法很简单用调试器或者直接读寄存器把 TIM2-PSC、TIM2-ARR、TIM2-CCR1 三个值打出来代入公式算一遍看是哪个值不符合预期。绝大多数情况是 CCR 没按新 ARR 重新计算。另外还有一个容易看走眼的情况示波器探头的地线没夹紧或者用了太长的接地线会在高频 PWM 上看到很多振铃但那不是寄存器的问题是测量回路电感的问题。频率上到几十 kHz 时把探头地线尽量缩短再判断。5.4 几个常见异常速查表现象最常见原因处理建议完全无波形没启动 PWM 或 GPIO 复用没配查 HAL_TIM_PWM_Start 和 AF 配置频率差一半定时器时钟没按时钟树实际值算打开 Clock Configuration 核对 TIM2 CLK频率略低于理论ARR1 被忽略检查公式中的 1占空比突变改 ARR 后 CCR 没联动用函数统一计算后再写入波形偶发畸形ARPE 没开或参数写入时序不当打开 Auto-reload preload高电平顶端有振铃探头地线过长缩短地线用接地弹簧最后分享一个我在 C5A3R 上调 PWM 收获最大的习惯不管改频率还是占空比都先花 10 秒在纸上把 PSC、ARR、CCR 三个值代进公式算一遍再动手写代码。寄存器是人写的写错了芯片不会报错示波器只会给你一个反直觉的波形。按照这节的排查顺序基本十分钟内能定位问题。这套思路同样适用于你之后用 TIM1 做带死区互补 PWM、用 DMA 做多路同步输出的场景底层都是同一套计数比较逻辑。
返回列表