上周帮一位朋友排查PWM输出问题STM32F103RCT6TIM3_CH1输出10kHz PWM驱动一个功能模块。PSC35、ARR99看起来算得很工整。示波器一量输出20kHz正好差了两倍。他反复确认PSC和ARR都没填错最后才发现问题根本不在这两个寄存器而在定时器的真实时钟源。这种事在STM32开发里太常见了——PSC、ARR、时钟源这三个参数单独拎出来谁都觉得简单放在一起却能把人绕晕。如果你也遇到过“公式算对了实测频率对不上”的情况这篇文章就把三个坑都掰开揉碎讲清楚为什么会错、怎么避免。1. 定时器的时基链路先搞懂计数器是怎么“走”起来的1.1 一条链看懂 PSC、ARR、CNT一个STM32定时器不管高级定时器还是基本定时器核心永远是时基单元也就是三个寄存器加一个时钟源预分频器寄存器PSC、计数器CNT、自动重装载寄存器ARR。信号路径是这样的CK_PSC定时器输入时钟→ PSC预分频 → CK_CNT计数器时钟→ CNT逐周期加1或减1 → CNT与ARR比较 → 产生更新事件UEV所以最基本的溢出频率公式是f_update f_ck_psc / ((PSC 1) × (ARR 1))其中f_ck_psc是送到定时器外设的时钟频率不是单片机的主频这点后面专门讲。溢出周期则是T_update (PSC 1) × (ARR 1) / f_ck_psc可以用接水桶来理解水龙头是时钟源每秒流出一定量的水PSC像一个粗漏斗先把水流降一个挡位ARR是水桶容量桶接满溢出一次溢出就是更新事件。漏斗口径错了或者桶容量算错了溢出的节奏肯定不对。为什么公式里到处是“1”因为PSC和ARR都是从0开始往上计数的。PSC从0递增到设定值一共经过了(PSC1)个输入时钟周期CNT从0递增到ARR一共计了(ARR1)个计数脉冲。忘记这个“1”是绝大多数定时器配置错误的起点。1.2 不同定时器时基结构有细微差别STM32的定时器分三类时基链路大同小异但细节会影响计算基本定时器TIM6/TIM7只有时基功能没有输入捕获也没有PWM输出最多用来做定时中断或驱动DAC。通用定时器TIM2/TIM3/TIM4/TIM5最常用有PSC、ARR、捕获比较通道可以输出PWM、做输入捕获、编码器模式。高级定时器TIM1/TIM8在通用定时器的基础上多了重复计数寄存器RCR、死区发生器、互补输出和刹车输入。重点坑位就在RCR上——更新事件不是每次CNT溢出都产生而是要溢出(RCR1)次才产生一次。以F103为例常见型号的定时器资源定时器类型总线位数是否带RCRTIM1高级APB216位是TIM2通用APB132位否TIM3/TIM4通用APB116位否TIM5通用APB132位否TIM6/TIM7基本APB116位否TIM8高级APB216位是F103ZE这类大容量型号才有TIM5和TIM8小容量型号没有。选型之前先查手册别对着只有4个定时器的型号愣是写TIM5的代码。1.3 为什么“配置正确”还会出错公式本身没有任何歧义错只可能错在三处PSC的边界值理解多1还是少1、ARR在不同计数模式下的周期含义中心对齐要除以2、以及定时器时钟源的真实频率APB分频导致的倍频。这三个问题错一个计算结果就会偏离实际而且偏离的方式往往不是几Hz而是直接差2倍甚至一个数量级。下面逐个拆。2. PSC分频系数永远比寄存器值大1这一条最容易阴沟翻船2.1 PSC71才是72分频不是72这是一个老生常谈但仍然高频踩中的点。假设定时器输入时钟是72MHz想把计数器时钟降到1MHz也就是做72分频那么PSC应该写多少正确算法72MHz / (PSC 1) 1MHz所以PSC 1 72PSC 71。如果你写PSC72实际分频是73倍计数器时钟是72MHz/73≈986.3kHz并不是1MHz。单独看这个数误差只有1.37%好像无所谓。但如果你基于这个计数器时钟继续算ARR目标频率越高误差越明显。举个实际例子想输出400kHz的PWM假设ARR算出来是2那么正确1MHz / (21)≈333.3kHz错误986.3kHz / (21)≈328.8kHz差了4.5kHz在电机控制或音频应用里完全不能接受。反方向还有一个常见错误就是把“分频系数”和“寄存器值”混在一起。有些人用CubeMX习惯了口填PSC71到了手写寄存器的时候想当然写PSC72觉得“这样才是72分频”。记住一句话分频系数 PSC寄存器值 1任何情况下都不例外。2.2 影子寄存器改了PSC不会立刻生效PSC不是一个直接作用的寄存器它背后有一个影子寄存器。你往PSC寄存器里写的新值并不会立刻加载到预分频器的实际工作逻辑里而是要等到一次更新事件UEV到来时才会被装载。这个机制带来的典型现象是程序运行时动态修改PSC想改变PWM频率结果第一个输出周期还是旧频率从第二个周期才切到新频率。如果你在改PSC之后立刻用示波器去抓波形很容易误判成“修改没生效”。在初始化阶段这个问题更隐蔽。标准库的TIM_TimeBaseInit在配置完PSC和ARR之后会对TIMx_EGR寄存器的UG位置1主动产生一次更新事件把PSC和ARR的影子寄存器都刷新。所以大部分时候你感觉不到影子寄存器的存在。但如果你是自己手写寄存器配置或者后期在代码里单独修改某个参数而没有触发UG新值就一直压在影子寄存器里工作频率还是旧的。我的建议是但凡在运行中改PSC或者ARR改完之后手动执行一次“TIMx-EGR TIM_EGR_UG”强制加载。这样能避免无数“改了没反应”的排查时间。2.3 PSC0代表1分频这个细节在外部时钟模式下很致命PSC0表示预分频器不分频严格说不对PSC0表示1分频输入时钟直接通过计数器时钟等于定时器输入时钟。这个表述区别平时无所谓但在外部时钟模式下就变成了安全问题。当定时器使用外部时钟模式例如用ETR引脚或TI1引脚作为计数时钟如果PSC0外部信号直接进入计数器。外部信号的频率上限受限于定时器本身的最高工作频率以及输入滤波器的参数。如果输入信号频率过高或者边沿抖动严重计数器会出现漏计、误计。这种情况下PSC0等于把定时器暴露在危险频率下。处理办法外部信号进入定时器之前先确认信号频率宁可把PSC设大一点降低计数器时钟也不能让计数器时钟逼近甚至超过定时器主频。尤其是做外部脉冲计数、测频这类应用PSC的选择直接影响计数精度。这个坑我们在第5章的案例里还会遇到。3. ARR同样是周期中心对齐模式让ARR的含义直接“减半”3.1 递增模式ARR1才是总计数个数通用定时器默认向上计数模式CNT从0开始每个计数器时钟脉冲加1直到等于ARR然后归零产生更新事件。所以一次溢出周期里CNT经历了0、1、2……ARR总共是(ARR1)个脉冲。想要PSC71计数器时钟1MHz的前提下输出1kHz更新事件f_update 1MHz / (ARR 1) 1kHz因此ARR 1 1000ARR 999。这个“ARR999”是无数教程里的标准写法看起来太自然了以至于很多人换了个场景就忘了为什么是999而不是1000。当你把ARR写1000之后实际频率是1MHz/1001≈999Hz。单看这一个周期1ms变成1.001ms误差千分之一做LED闪烁看不出来但用在PID控制周期、软件定时累计、串口超时判断里这个误差会持续累积几小时后偏差就非常明显。我的经验是每一处定时器配置代码旁边都写一行注释明确标出目标频率和当前PSC/ARR对应的实际频率。这样以后回看代码不用重新心算就能发现位数有没有错。3.2 中心对齐模式PWM频率要除以2中心对齐模式Center-aligned mode下计数器先从0向上计数到ARR然后从ARR向下计数到0如此反复形成一个三角波。一个完整的PWM周期包含两段上升段和下降段总计数次数是2×ARR而不是ARR1。中心对齐模式下的PWM频率f_pwm CK_CNT / (2 × ARR)对比边沿对齐递增模式f_pwm CK_CNT / (ARR 1)同一个ARR值中心对齐模式的输出频率差不多是边沿对齐的一半。很多人把递增模式的公式套到中心对齐模式里结果PWM频率直接少一半示波器上一看就懵了。反过来如果你在CubeMX里选了中心对齐模式又想得到和边沿对齐相同的频率ARR必须设为原来的一半左右。举个例子计数器时钟1MHz想得到20kHz PWM边沿对齐ARR49中心对齐ARR25。忽略了这一点两个方案的PWM频率会差一倍。另外要注意中心对齐模式有CMS位可以设置在向上计数到ARR时产生更新事件、向下计数到0时产生更新事件或者两者都产生。这会影响你中断触发的频率和时机。建议在调试中断频率时先看看CR1寄存器的CMS位到底是什么值不要凭经验猜。3.3 高级定时器的RCR又多了一层分频TIM1/TIM8的高级定时器里有一个重复计数寄存器RCR。它的作用是CNT溢出多少次之后才产生一次真正的更新事件。所以高级定时器的更新事件频率公式变成f_update CK_CNT / ((ARR 1) × (RCR 1))如果RCR0那么每次溢出都产生更新事件和通用定时器一样。如果RCR2那么CNT要溢出3次才产生一次更新事件。这个寄存器的坑在于它不像PSC和ARR那样每次初始化都会被常规代码清零。CubeMX生成的代码通常会设置RCR0但如果你是在现有工程上改造或从别人手里接过一个板子RCR里可能残留了非零值导致中断频率比计算的慢了好几倍。有一次我排查一个PWM中断频率问题计算值明明是10kHz实测中断只有2kHz折腾了很久最后发现RCR4。所以用高级定时器的时候先看一眼RCR这个动作应该像看PSC、ARR一样自然。3.4 PWM占空比与100%占空比异常的真相很多人在调试PWM时会碰到“输出100%占空比异常”的现象代码里CCR明明设了一个中间值输出却一直是高电平。这个问题分两种常见情况。第一种CCR值大于等于ARR。此时计数器永远也计不到CCR比较匹配永远不成立PWM输出直接保持高电平也就是常说的100%占空比。这在数学上没错问题是很多人算ARR的时候少减了1导致ARR偏小CCR轻易就“越界”了。第二种常见操作是把ARR设成0。在递增模式下ARR0意味着CNT永远是0更新事件每个计数器时钟都会产生PWM比较逻辑也随之进入一种非预期状态输出会异常。调试这类问题时我习惯先读一遍CCR、ARR、CNT三个寄存器的实时值确认CCR是否小于ARR。如果CCR正常但输出还是恒高再去查定时器通道的极性和输出使能配置。绝大多数“100%占空比异常”最后都落在CCR/ARR的大小关系上而不是硬件坏了。4. 时钟源定时器时钟不等于APB外设时钟这个坑最大也最隐蔽4.1 APB预分频器不为1时定时器时钟自动翻倍这是STM32定时器配置里最大的坑也是我那位朋友PWM频率差两倍的根源。STM32的时钟树里定时器时钟CK_INT并不直接等于APB外设时钟PCLK1或PCLK2而是要满足一个倍频规则如果APBx预分频器为1则定时器时钟 PCLKx如果APBx预分频器大于1则定时器时钟 PCLKx × 2原因不复杂APB总线的最高频率有限制比如F103的APB1最高只能跑到36MHz。如果系统时钟72MHzAPB1必须分频2才能满足36MHz的上限。但定时器希望获得更高的时钟所以硬件上做了个倍频器只要APB1预分频器不为1送给定时器的时钟就自动变成PCLK1的两倍也就是72MHz。问题就在这里很多人用CubeMX或者RCC_GetClocksFreq读到的PCLK1是36MHz拿这个36MHz去算PSC和ARR结果定时器实际工作时钟是72MHz所有频率计算结果直接翻倍。比如你按36MHz算出PSC35、ARR99想要10kHz实际却是20kHz。F103在72MHz系统时钟下的典型值外设PCLKAPB预分频定时器实际时钟TIM1/TIM872MHzAPB2172MHzTIM2-TIM736MHzAPB1272MHz注意F103的APB2预分频通常设为1所以PCLK272MHzTIM1/TIM8的时钟就是72MHz没有倍频。而APB1预分频为2PCLK136MHzTIM2-TIM7的时钟是36MHz乘以2等于72MHz。在F407这类主频168MHz的芯片上规则同样适用但数值不同如果SYSCLK168MHzAPB1预分频4PCLK142MHzTIM2-TIM7时钟84MHzAPB2预分频2PCLK284MHzTIM1/TIM8/TIM9-TIM11时钟168MHz所以计算之前先做一件事去RCC_CFGR寄存器里读APB1和APB2的预分频值或者直接在调试器里看RCC时钟配置结构体把定时器真实时钟确认下来再套公式。4.2 外部时钟模式PSC继续生效但边界条件变了除了内部时钟定时器还能把外部信号当作时钟源。两种常见方式外部时钟模式1TIM_TS_TI1FP1/TI2FP2从TI1或TI2引脚输入外部信号经过滤波、极性和边沿检测后作为计数器时钟。外部时钟模式2TIM_ETR从ETR引脚直接输入经过ETR自身的预分频器ETPS、滤波和极性选择后作为计数器时钟。在外部时钟模式下PSC依然生效公式里的f_ck_psc变成了外部输入信号的频率。比如外部信号500HzPSC99ARR9那么更新事件频率就是500 / ((991) × (91)) 0.5Hz也就是每2秒产生一次更新事件。注意这里的外部信号频率必须远低于定时器内部时钟否则输入捕获的稳定性和精度都会出问题。我一般遵循外部信号频率不超过定时器时钟四分之一的经验值超过就加滤波或者换分频方案。还有一点容易忽略外部时钟模式下ARR不再是“用户希望的时间”而是“用户希望计多少个外部脉冲”。很多人习惯性地把ARR按内部时钟的微秒/毫秒去填导致中断周期完全不对。先想清楚你数的是什么脉冲再填ARR。4.3 编码器模式计数器时钟由边沿决定ARR又变成“位置范围”编码器模式是定时器的特殊工作模式计数器时钟不是固定频率而是由编码器A/B两相输出的边沿决定。简单来说1倍频只对A相或B相的单边沿计数电机一圈输出N个脉冲计数器一圈计N次。2倍频对A、B两相的某一相同边沿计数一圈计2N次。4倍频对A、B两相的全部边沿计数一圈计4N次。编码器模式下CNT在0和ARR之间来回计数方向由A/B相位差决定。ARR在这里的作用不是产生更新事件频率而是限制计数范围。如果想在电机转一圈时计数恰好覆盖整个范围那么4倍频ARR应设置为4N-1因为要从0开始计才能计满4N个值1倍频ARR应设置为N-1这里有个很常见的错误有人在配置CubeMX的编码器模式时把ARR设成编码器线数N而不是4N-1结果电机转四分之一圈计数器就溢出一次位置数据完全错乱。我做编码器位置读取基本都先用4倍频把分辨率拉满然后根据实际应用决定ARR是“一圈溢出一次”还是“多圈累计”。这两种设计对应的ARR公式完全不同动手配置之前先写下这个公式别嫌麻烦。4.4 国产替代型号的时钟差异GD32、APM32不是100%复制这几年GD32、APM32等国产型号用得越来越多很多人直接把STM32工程移植过去定时器配置常常出问题。核心原因之一就是时钟树细节并不完全一致。以GD32F450为例它的定时器时钟有一个专门的函数rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL2);这个函数控制定时器时钟的预分频倍率。如果你调用的是MUL2那么当APBx预分频大于1时定时器时钟APBx时钟×2和STM32逻辑一致但如果配置成MUL4定时器时钟就会变成APBx时钟×4定时器时钟和STM32完全不同。移植STM32代码到GD32时第一件事不是改引脚而是把GD32的RCU定时器时钟配置函数翻出来确认倍率设置和原STM32芯片是否一致。APM32的定时器在多数型号上和STM32兼容性好一些但仍然建议在初始化时用库函数读回实际时钟频率验证一遍而不是盲目相信“完全兼容”。5. 三个真实案例复盘从错误频率反推出问题根源5.1 案例一PWM频率刚好差两倍APB1倍频器漏算现象手写标准库配置TIM3_CH1输出10kHz PWM。PSC35ARR99程序逻辑没问题示波器实测20kHz。排查链路先验算PSC和ARR按36MHz时钟计算(351)×(991)3600分频36MHz/360010kHz没错。怀疑公式用错递增模式PWM1没有中心对齐公式没问题。用调试器读TIM3的PSC、ARR寄存器发现写入值和预期一致。回到时钟本身显示TIM3时钟是多少调试器里看RCC_CFGR发现APB1预分频器2PCLK136MHz但根据倍频规则TIM3实际时钟36MHz×272MHz。用72MHz重新验算72MHz/360020kHz和实测完全吻合。修复把PSC改成71ARR改成99这样72MHz/(72×100)10kHz。或者保持PSC35把ARR改成199因为72MHz/(36×200)10kHz。选择哪一种看具体精度需求。这个案例的核心教训是PSC和ARR只是“分配”参数它们之上还有一层时钟源。只要定时器真实时钟算错后面的一切计算全部白费。排查顺序应该永远是先确认时钟树再算PSC/ARR。5.2 案例二1ms中断变成1.001msARR的边界磨损现象TIM2产生周期性中断目标1ms。PSC71计数器时钟1MHzARR1000按公式算1MHz/10001kHz应该是1ms。实测两个中断间隔是1.001ms。排查链路示波器量一个GPIO翻转周期确实差大约1μs。重新读公式1MHz/(ARR1)1kHz所以ARR999才对代码里写的是1000导致实际频率999Hz周期1.001ms。差值的来源ARR1000意味着CNT要经过1001个脉冲才溢出比预期的1000个多了一个。修复把ARR改成999。同时在代码注释里写上PSC7172分频ARR9991000计数1MHz/10001kHz1ms。不要小看这1μs的误差。做1ms软件定时器时确实影响不大但如果把这个定时器用作PID周期或者累计计时误差会线性累积。一小时下来偏了3.6秒在需要长时间计时的场景里就不可接受了。5.3 案例三输入捕获测频率不准计数器溢出坑了捕获值现象用TIM4的输入捕获模式测一个约1kHz的方波频率。定时器时钟72MHzPSC0ARR0xFFFF。相邻两次上升沿的时间差换算出来频率总是不对比如测出1.3kHz。排查链路读取两次捕获的CCR值计算差值。第一次CCR65000第二次CCR7500看起来差值应该是7500-65000负数说明中间发生了计数器溢出。验证1kHz方波一个周期是1ms72MHz计数下需要72000个计数脉冲而ARR0xFFFF65535计满就溢出回0。一个1ms周期里计数器溢出了至少一次捕获值经过回绕后计算出来的时间差自然就是错的。修复增大计数器的最大量程。把PSC从0改成71计数器时钟从72MHz降到1MHz1ms对应1000个计数ARR保持0xFFFF足够覆盖。修复后测到的差值稳定在1000左右换算频率1kHz问题解决。这个案例里的深层问题输入捕获测频率不能只关注捕获通道本身还要保证计数器在一个被测信号周期内不溢出。PSC越大计数分辨率越低但可测的最大周期越长PSC越小分辨率越高但计数器更快溢出。这是输入捕获参数设计里的一个经典权衡没有固定答案只能根据被测信号的频率范围去折中。6. 聊点实在的我处理定时器配置问题的固定流程这几个坑全部踩过之后我总结了一套固定的处理流程每次配置定时器都按这个顺序来基本能消灭九成以上的低级错误。第一步翻芯片手册或调试器确认定时器真实时钟。不猜不默认不拿“上次那个芯片也是这么配的”当依据。尤其在不同型号间移植时这一步必须重做。第二步明确计数模式。边沿对齐、中心对齐、编码器模式各对应不同的ARR公式先写清楚要什么再填参数。第三步用公式验算一次目标频率。算出结果后立刻和期望值对比多少倍或者差多少一眼就能看出有没有“2倍错误”或者“±1错误”。第四步上示波器或逻辑分析仪实测。软件计算永远不能替代实测。翻转一个GPIO、量一下频率、确认无误后再接负载这是最稳妥的流程。最后分享一个小技巧调试定时器的时候别总盯着PSC和ARR本身先把CNT的实时值和更新标志读出来。CNT是否在按预期速度递增比任何理论计算都更能反映真实情况。如果CNT不动先查RCC时钟有没有开如果CNT跳变有规律但频率不对再回头看PSC和ARR如果CNT乱跳多半是外部信号或时钟树的问题。这条排查顺序比盲目改参数高效得多。