ARTICLE DETAIL

资讯详情

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

STM32定时器PSC/ARR与时钟源:避开隐形2倍和加一陷阱

STM32定时器PSC/ARR与时钟源:避开隐形2倍和加一陷阱 写这期内容前我先说一个真实经历有次给一块板子写定时器中断目标 1ms 进一次中断我在初始化里把 PSC 填 71、ARR 填 499算出来的结果明明对着公式接到示波器上却看到中断间隔是 0.5ms。当时第一反应是怀疑晶振频率不对或者板子上的 8MHz 晶振虚焊了排查了快两个小时最后才发现问题根本不在硬件而在 STM32 定时器时钟树上那个“隐形 2 倍”。STM32 定时器的 PSC预分频系数、ARR自动重装载值和时钟源是三个连老手都容易中招的点。公式就一句话定时时间 (PSC1) × (ARR1) / 定时器时钟频率。但把这句话落到不同场景、不同模式下隐藏的边界问题就全出来了。这篇文章就把我踩过的、帮别人排查过的三类高频错误拆开讲尤其是每个参数背后“为什么这样算才对”的逻辑适合刚学定时器的小白也适合配置 PWM、输入捕获时总差那么一点的老手对照自查。1. 时钟源的坑APB1 分频到定时器时钟之间的“隐形 2 倍”1.1 为什么用 36MHz 算定时时间实际中断却比预期快一倍很多刚接触 STM32 的人拿到一个工程第一件事是打开系统时钟配置看到“APB1 36MHz”这一行就觉得挂在 APB1 上的定时器 TIM2~TIM7 的时钟就是 36MHz。然后算 1ms 中断Tclk 36MHz PSC 71 ARR 499 时间 (711) × (4991) / 36MHz 1ms式子没毛病运算也完全正确但接上逻辑分析仪一看实际中断间隔是 0.5ms。为什么因为 STM32 里有一条规则当 APBx 预分频系数不等于 1 时定时器时钟会自动变成 APBx 的 2 倍。这个工程里系统时钟 SYSCLK 72MHzAPB1 最大只能跑到 36MHz所以 RCC 里 APB1 的预分频系数是 2。按照规则APB1 外设时钟是 36MHz但挂在 APB1 总线上的定时器 TIM2~TIM7 的时钟会被自动乘以 2变成 72MHz。我用 36MHz 去算自然就少算了一倍时间。这个“隐形 2 倍”的坑最阴的地方在于CubeMX 生成的时钟树图里APB1 外设时钟显示 36MHz而定时器时钟那一栏却显示 72MHz。很多人根本没注意这两行数据分别代表什么直接把 36 抄进公式。我后面带人做项目时第一句话就是让他们把时钟树里“Timer Clock”这个值抄下来不要抄“APB1 Bus Clock”。1.2 倍频规则记忆法只要 APBx 分频系数不是 1定时器时钟就自动 ×2我总结了一个非常简单的记法不用背太多细节如果 APBx 预分频系数是 1也就是对系统时钟不分频那么挂在这条总线上的定时器时钟 APBx 时钟。如果 APBx 预分频系数大于 1那么挂在这条总线上的定时器时钟 APBx 时钟 × 2。这条规则有一个很实际的影响你把系统主频调低了比如 SYSCLK 从 72MHz 降到 64MHzAPB1 最大频率要求 32MHz此时分频系数等于 2所以定时器时钟又变成 64MHz。也就是说只要 SYSCLK 是 APB1 上限的两倍定时器时钟恰好等于 SYSCLK很多人因此觉得“定时器时钟就是系统时钟”但这是巧合不是原理。一旦你把 APB1 预分频改成 1定时器时钟立刻变成 SYSCLK 的一半此时不重新算 PSC 和 ARR所有定时和 PWM 频率都会偏。举个例子某低功耗工程把 SYSCLK 降到 36MHzAPB1 保持 36MHz 不分频这时 TIM2 时钟只有 36MHz 而不是 72MHz。如果你沿用原来在 72MHz 下算好的 PSC/ARR中断频率直接翻倍。这不是寄存器没配好是时钟源的理解问题。1.3 不同系列和不同定时器时钟来源差异有多大时钟源这部分还有第二个容易忽略的点不是所有定时器都挂在 APB1 上。以 STM32F1 为例TIM1 和 TIM8 这两个高级定时器挂在 APB2 总线上APB2 的时钟可能和 APB1 不同。比如系统时钟 72MHz 时APB2 72MHz预分频 1APB1 36MHz预分频 2。此时 TIM1 的时钟是 72MHzTIM2 的时钟因为倍频也是 72MHz看起来没有区别。但如果系统时钟是 64MHzAPB2 设成 64MHz 不分频而 APB1 设成 32MHz 分频 2TIM1 时钟就是 64MHzTIM2 时钟也是 64MHz。很多情况下两者确实相同但一旦你的 APB1 或 APB2 预分频配置不是“分频 2”两边立刻不一样。所以我给读者的建议是别按“定时器在哪个总线”去猜频率直接看参考手册的时钟树图或者读 RCC_CFGR 寄存器里 PPRE1/PPRE2 两个字段的实际值。就算用 HAL 库也没必要完全依赖 CubeMX 生成的结果自己动手把这条链捋一遍后面排查问题会快很多。实际写代码时判断当前定时器时钟最简单的方法是这样// 假设系统时钟已经被配置为 72MHzAPB1 分频系数为 2 // TIM2 的定时器时钟 APB1 × 2 72MHz uint32_t tim2_clock HAL_RCC_GetPCLK1Freq() * 2; // 如果 APB1 没有分频预分频系数为 1则不需要乘 2 // 这里要注意具体是否乘 2取决于 RCC_CFGR 的 PPRE1 字段不过这句代码只适合 F1/F4 这类“APB 分频不为 1 就自动倍频”的架构换到不同系列还是要看时钟树。记住一点永远不要靠人肉背频率要在代码里或者时钟树里确认。2. PSC 的坑预分频不是“系数”是“步长”2.1 PSC0 到底代表几分频先把寄存器值和分频系数的关系焊死PSCPrescaler预分频器是十六位寄存器取值范围 0~65535。真正生效的分频系数是 PSC 加 1也就是分频系数 PSC 1这意味着 PSC 0 时分频系数是 1也就是对定时器输入时钟不做任何分频PSC 1 时分频系数是 2PSC 71 时分频系数是 72。这个“加一”关系跟 ARR 的“加一”是两回事要分开记不要混在一起。为什么会有这个加一因为寄存器里存的“预分频值”实际上是一个“计数起点”。硬件计数器要从 PSC 这个值倒数到 0数 PSC1 个时钟周期才输出一个计数时钟。用生活里的例子说如果电梯从 5 楼下到 0 楼中间经过了 6 层楼而不是 5 层。PSC 就是那个“5楼”真正的楼层数是“6”。很多人在这个点上犯迷糊的原因是想配置 72 分频直接在代码里写 TIM_Prescaler 72结果实际分频是 73一个周期就多了 73/72 ≈ 1.4% 的时间。单看一次可能无所谓累积到几百个周期PWM 频率和串口波特率都会偏。2.2 PSC 和 ARR 谁管总时长、谁管分辨率把定时器工作理解成钟表PSC 决定秒针走一格需要多少时间ARR 决定钟表盘上一共有多少格。定时器整体溢出周期公式是溢出周期 (PSC1) × (ARR1) / Tclk这里 PSC 与 ARR 在公式中是乘法关系但对结果的影响角度完全不同PSC 控制“计数步长”。步长越小计数器每次加一所代表的时间越短分辨率越高。步长越大计数器溢出越慢但每次计数能表示的物理时间变粗。ARR 控制“总步数”。在一个溢出周期内计数器要从 0 数到 ARR共 ARR1 步。举例说明想要 1ms 定时定时器时钟 72MHz可以有两种方案方案PSCARR步长总步数实际时间A719991us10001msB7199910us1001ms两个方案的总时间都是 1ms但在 PWM 或输入捕获场景下差别非常大。方案 A 的步长是 1us占空比可以调节到 1us 的精度方案 B 的步长是 10us占空比的调节粒度只能到 10us。如果信号周期本身只有 100us方案 B 的占空比只能调出 10% 的整数倍完全没法精细控制。所以正确做法是先把 PSC 往小设尽量提高计数分辨率再用 ARR 凑周期。只有需要特别长溢出时间、或者计数器溢出太快导致频繁进中断的场景才考虑加大 PSC。2.3 动态修改 PSC 时会遇到的重载问题另一个和 PSC 有关的隐蔽问题是“重载”。STM32 定时器的 PSC 寄存器是带影子寄存器的你在代码里改 PSC不会立刻生效而要等当前计数周期结束更新事件后新的预分频值才会被装载。影子寄存器可以理解为“后台备用参数”。你在前台改了值硬件会把新值暂存到后台等一个安全的时机再切到前台。这样做的目的是防止计数器工作到一半突然改变计数时钟频率导致波形变形。但如果程序里改完 PSC 后希望立刻生效就必须手动触发一次更新事件__HAL_TIM_SET_PRESCALER(htim2, 71); __HAL_TIM_GENERATE_UPDATE_EVENT(htim2, TIM_EVENTSOURCE_UPDATE);注意触发更新事件会同时把 CNT 计数器清零如果你的逻辑里不能接受计数器清零就要考虑在特定时刻去改或者接受这个行为。在实时性要求高的场景比如伺服电机控制中动态调整脉冲频率这个“当前周期结束”的延迟可能造成一拍误差需要在算法上做补偿。我在一个步进电机控制项目里遇到过这个坑程序跑到一半调用函数把 PSC 改了想着下个周期就开始用新频率发脉冲结果实际波形在下一个周期仍然用旧频率跑完再下一个周期才切到新频率。查了很久才发现是影子寄存器在搞鬼。后来改为先计算好需要多少计数周期再把 ARR 和 PSC 一起更新避免中间状态。2.4 输入捕获场景里 PSC 设太大频率根本测不准PSC 在输入捕获测频率时的影响是新手最容易忽略的。输入捕获的原理是捕获到一个边沿时把当前计数器的值 CNT 记下来下一次边沿再捕获一个值两者相减就是一个信号周期的计数个数。频率公式是信号频率 Tclk / ((PSC1) × (CNT2 - CNT1))如果你的 PSC 设得太大单个计数周期就变长分辨率就下降。比如定时器时钟 72MHzPSC 设成 719计数时钟变成 100kHz每个计数代表 10us。现在要测一个 100kHz 的信号信号周期是 10usCNT 差值只有 1 个计数算出来要么是 100kHz 要么是 50kHz误差大得没法用。但同样条件下PSC 0计数时钟仍然是 72MHz100kHz 信号的一个周期内有 720 个计数误差就小得多。所以测高频信号时PSC 应该设 0用最大分辨率测低频信号时反而可以把 PSC 调大因为计数器不会溢出但要注意溢出中断。这里的核心思路是PSC 应该在“分辨率够用”和“计数器不溢出”之间取平衡而不是随手按公式凑一个看起来很整的数。3. ARR 的坑边界值、PWM 占空比和中心对齐模式的“加一”问题3.1 ARR1 才是真正溢出点丢了这个“1”时间就少了千分之一ARRAuto-Reload Register自动重装载寄存器决定的是计数器从 0 数到哪个值后产生溢出事件。如果 ARR 999计数器会数 0、1、2……999一共 1000 个数才溢出。所以你实际得到的溢出周期是溢出周期 (PSC1) × (ARR1) / Tclk很多人算时间时直接把 ARR 当作计数量比如想要 1000 个计数周期就把 ARR 写成 1000实际却变成了 1001 个周期。对于中断频率来说多一个计数周期通常只影响千分之一看起来不严重但在 PWM 应用中尤其是需要精确频率和占空比的场合这个“1”会让频率偏低那么一丁点累积起来无法忽略。要理解为什么硬件不直接把 ARR 当作“数量”而是当作“终值”可以回想一下为什么不把 ARR 设计成 1000 就代表数到 1000 次。这是寄存器设计的惯例计数器的取值天然从 0 开始ARR 是“末位数”所以末位数 1 才是总数量。这就好比赛跑起跑线是 0终点线是 ARR你跑过的整数点是 0 到 ARR一共 ARR1 个点。我自己的检查习惯是写出公式后把 PSC1 和 ARR1 分别标出来再顺手把“如果用 PSC、ARR 直接代进去会差多少”也算一下。养成这个习惯后很少会因为边界问题翻车。3.2 PWM 频率、占空比和 CCR 的关系ARR 只是频段不是占空比PWM 输出有三个关键参数频率、占空比、相位。很多人搞混 ARR 和 CCR捕获/比较寄存器的分工ARR 控制 PWM 周期频率它决定计数器数到多少溢出。CCR 控制占空比它决定计数器值到达哪个位置时翻转输出电平。以向上计数模式为例CNT 从 0 开始增加。当 CNT CCR 时输出一个电平比如高电平。当 CNT ≥ CCR 时输出另一个电平比如低电平。当 CNT 到达 ARR 后溢出回到 0重复下一个周期。所以占空比 CCR / (ARR1)。假设 ARR 999想要 50% 占空比CCR 应该设为 500也就是 500/1000 50%而不是把 ARR 设成 500。如果仅仅为了得到 20kHz 的频率把 ARR 设成了 3599对应 20kHz72MHz 时钟然后又把 ARR 当成 3600 去算占空比就会有一个隐含的 0.03% 误差。这个误差在电机控制里通常没什么影响但在高精度电源或 LED 调光里会导致光通量不一致。还有一个边界要特别注意向上计数模式下CCR 0 时整个周期都输出低电平CCR ARR 时输出只有在最后一个计数周期才变低也就是高电平占 ARR/(ARR1)接近 100% 但差一点点。想要真正的 100% 占空比要么让 CCR ARR部分系列允许要么用强制输出功能。这个细节在写电机驱动死区补偿时非常容易踩。3.3 中心对齐模式下 ARR 的语义与 ADC 采样时刻设置中心对齐模式Center-Aligned Mode是高级定时器的一个重要特性也是 ARR 最容易算错的地方之一。普通向上计数模式计数器从 0 数到 ARR然后直接回 0一个周期是 ARR1 个计数。中心对齐模式计数器从 0 数到 ARR然后反向从 ARR 数回 0一个完整的 PWM 周期包含上行0→ARR和下行ARR→0两段总计数数量约为 2×ARR严谨说是 2×ARR 个单位具体溢出点定义会因计数器重载发生在顶部还是底部而异。正是因为上升段和下降段各占一段中心对齐模式下 PWM 频率的计算变成PWM频率 Tclk / ((PSC1) × 2 × ARR) // 近似具体边界以手册为准这和向上计数模式的公式之间差了一个 2 倍。如果你用“向上计数”那套公式去配中心对齐模式PWM 频率会直接差一倍而且波形看起来又很像样容易让人摸不着头脑。中心对齐模式真正的价值在于PWM 的输出变化发生在计数器值等于 CCR 的时刻此时正弦波和三角波的“中心点”对齐谐波含量更低特别适合电机控制。很多人在写“高级定时器 PWM 中心对齐模式和 ADC 采样时刻点设置”这类需求时最容易犯的错是把 ADC 触发采样放在更新事件上。更新事件发生在计数器到达 ARR 或 0 附近而这一时刻恰好处于 PWM 开关切换的边界附近采样到的电流电压往往带有开关噪声。正确方法是把 ADC 触发时刻设置在 CCR 匹配附近。比如用一个 ADC 注入组定时器配置为“CCR 匹配时触发采样”然后把 CCR 放在 PWM 周期中间位置避开边沿噪声。有人习惯把 CCR 同时用于 PWM 占空比和 ADC 触发这时要意识到两者可能冲突需要另外开一个定时器通道或使用 TRGO2 事件来触发 ADC。4. 三个真实案例把 PSC、ARR、时钟源的坑串在一起排查4.1 案例 A1ms 更新中断实测变成 0.5ms先查时钟树还是先查 PSC一个很典型的排查现场TIM3 用来做 1ms 调度时钟CubeMX 配置 APB1 36MHzPSC 71ARR 499。中断里打个 GPIO 翻转示波器一看周期是 1ms高电平 0.5ms、低电平 0.5ms但进入中断的间隔是 0.5ms。此时如果按“先改 PSC把时间放大”的思路去调会把 ARR 改成 999最后的结果是中断 1ms看起来修好了但根本原因没找到。等到哪天把系统主频或 APB1 分频改了问题会再次出现。正确顺序应该是先确认 RCC_CFGR 的 PPRE1 值。如果 APB1 分频系数是 2则 TIM3 的实际时钟 36MHz × 2 72MHz。把公式改成 72MHz 重新计算PSC 71ARR 999。用调试器读 TIM3-CNT 的变化速度验证计数时钟是否符合预期。我在帮人排查这个问题时还会提醒他注意 HAL 库和标准外设库的字段命名差异标准外设库里 TIM_TimeBaseStructure.TIM_Prescaler 和 TIM_Period 分别对应 PSC 和 ARRHAL 库则是 htim.Init.Prescaler 和 htim.Init.Period。字段里填的就是寄存器原始值不需要在外面手动减一。4.2 案例 B20kHz/50% PWM 输出异常一个“加一”让频率和占空比全偏另一个项目要求 TIM1 输出 20kHz、占空比 50% 的 PWM 去驱动一个压电蜂鸣器。开发者按 72MHz 时钟算想要 20kHz周期 72MHz / 20000 3600于是把 ARR 填 3600CCR 填 1800。实测频率是 19.994kHz占空比是 1801/3601 ≈ 50.01%偏差看着不大但他后面对频率精度要求到 0.01%这个误差就很尴尬。正确的计算应该是周期计数 72MHz / 20kHz 3600 ARR 3600 - 1 3599 CCR 3600 / 2 1800 // 占空比 1800 / 3600 50%这里暴露的就是 ARR 的“加一”问题ARR 是计数终点不是计数总数。占空比的分母是 ARR1不是 ARR。我建议在实际工程中把这种计算封装成一个函数因为每次手动减一太容易出错void pwm_config(TIM_HandleTypeDef *htim, uint32_t freq_hz, uint32_t duty_permille) { uint32_t timer_clock HAL_RCC_GetPCLK1Freq() * 2; // 以挂载在 APB1 上的定时器为例 uint32_t period_count timer_clock / freq_hz; __HAL_TIM_SET_AUTORELOAD(htim, period_count - 1); __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, period_count * duty_permille / 1000); }注意这里有个前提如果 APB1 没有分频就不能简单乘 2。更稳的做法是把时钟源信息作为参数传进去别在函数里猜。4.3 案例 C输入捕获测 10kHz 方波误差大到怀疑引脚接错第三个案例来自一个转速测量项目。目标是测量一个编码器输出的方波频率范围 1kHz~10kHz。代码里定时器 TIM4 用输入捕获通道PSC 设成 719ARR 设成 0xFFFF65535。测 1kHz 时结果很准测 10kHz 时误差超过 5%开发者怀疑是引脚接触不良换了线、换了上拉电阻问题依旧。原因很简单PSC 719 时计数时钟 72MHz / 720 100kHz每个计数代表 10us。10kHz 方波的周期是 100usCNT 差值只有 10 个计数。每个计数的边界误差是 ±1所以误差最大可能达到 ±10%测出 10% 的波动完全正常。解决办法是把 PSC 减小。测 10kHz 时用 PSC 0、ARR 0xFFFF 就够因为 72MHz / 10kHz 7200 个计数远没到 65535 的溢出上限测 1kHz 时周期计数是 72000超过十六位计数器的上限此时才考虑增大 PSC 或者读溢出次数。一个简单的自适应策略是先用较大 PSC 测量一个粗略周期根据粗略周期决定是否减小 PSC重新测量如果周期计数值接近 65535说明要加大 PSC 或开启溢出次数统计。这个案例说明PSC 的设置不是“随便给个值让计数器不溢出就行”而是要结合被测信号的频率范围去算保证计数差值尽量大才能保证测量精度。4.4 配置定时器前的自查清单个人经验总结这些年在定时器上踩的坑多了我慢慢养成了一个习惯不管用 CubeMX 还是手写寄存器配置完定时器后都按下面这个清单过一遍基本能避开 90% 的“算错”问题确认定时器时钟源APBx 预分频是几定时器时钟是 APBx×2 还是就是 APBx不要凭印象读寄存器或者看时钟树。确认 PSC 和 ARR 的语义差异PSC 决定步长ARR 决定终点。算时间时用 PSC1 和 ARR1不要只加其中一个。确认 PWM 模式向上计数还是中心对齐中心对齐模式下 ARR 的计数周期语义不一样公式里的 2 倍别丢。确认影子寄存器行为动态修改 PSC/ARR 后要不要手动触发更新事件改动生效的时机是否符合控制逻辑最后用示波器或逻辑分析仪实测而不是只看代码算出来的理论值。示波器上的实际频率和占空比永远比纸上计算更能反映真相。有一次我用这套清单帮一个朋友排查一个延时函数卡死的问题。他以为卡死在定时器初始化实际是修改 ARR 后没有触发更新事件新值和旧值混着用更新中断标志永远等不到程序一直卡在等待标志位。这类问题如果只盯着 PSC 和 ARR 的值反复试很容易白费几个小时。先捋清时钟树、边界值和生效时机再动手改参数效率会高很多。
返回列表