ARTICLE DETAIL

资讯详情

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

STM32定时器时间基准详解:PSC、ARR与CNT原理及配置

STM32定时器时间基准详解:PSC、ARR与CNT原理及配置 1. 从一次“定时不准”的翻车说起刚接触 STM32 那会儿我对定时器的理解就停留在“设个时间到点进中断”这个层面。直到有次做一个红外遥控解码的项目用 TIM2 做输入捕获代码逻辑反复检查都没问题可解出来的脉宽数据就是飘忽不定误差能到百分之十几。折腾了大半天最后发现问题出在我压根没搞清楚一件事定时器到底在数什么它数的那个“数”时间基准是从哪来的这个问题听起来像是新手才会问的但我后来发现不少做了两三年 STM32 的朋友对 PSC、ARR、CNT 这三个寄存器的理解依然是模糊的。知道要配这两个参数但为什么这么配、配完之后定时周期到底怎么算、时钟源又是怎么一路传到计数器上的心里没底。一旦遇到需要精确定时的场景比如 PWM 死区控制、输入捕获测频率、或者用 TIM1 做中心对齐的互补输出就容易翻车。这篇内容就是把我这些年踩过的坑、验证过的计算过程、以及实际调试中总结出来的经验完整地梳理一遍。核心围绕 STM32 的定时器时间基准展开重点讲清楚三件事时钟从哪来、PSC 和 ARR 到底怎么配合、CNT 数的到底是什么。涉及到的芯片以 STM32F103 系列为主但原理对 F4、F7、H7 这些系列同样适用只是总线频率和时钟树细节有差异。适合谁看如果你正在学 STM32 的定时器或者已经用过但总觉得“知其然不知其所以然”又或者你正在做需要精确定时的项目PWM、输入捕获、编码器接口、DAC 触发等那这篇内容应该能帮你把这块知识补扎实。我会尽量用生活化的类比来解释同时给出可以直接抄的计算公式和配置代码。2. 定时器的时钟到底从哪来2.1 时钟树定时器的“粮食”是怎么运过来的要搞清楚定时器在数什么得先搞清楚它的“心跳”从哪来。STM32 内部有一棵时钟树你可以把它想象成一个小区的供水系统总水源是外部晶振或者内部 RC 振荡器经过一系列的水泵PLL和阀门分频器最终把水送到每家每户各个外设。对于 STM32F103 来说常见的配置是外部 8MHz 晶振经过 PLL 倍频到 72MHz这个 72MHz 就是系统时钟 SYSCLK。然后 SYSCLK 经过 AHB 预分频器通常不分频得到 HCLK也是 72MHz。HCLK 再经过 APB 预分频器分给各个外设。这里有个关键点也是很多人容易忽略的地方APB 总线上的定时器时钟并不等于 APB 总线的时钟频率。STM32 的参考手册里写得很清楚当 APB 预分频系数为 1 时定时器时钟等于 APB 时钟当 APB 预分频系数不为 1 时定时器时钟是 APB 时钟的 2 倍。举个例子假设 APB1 预分频系数设为 2那么 APB1 总线时钟是 36MHz但挂在 APB1 上的定时器TIM2、TIM3、TIM4、TIM5的时钟是 72MHz。这个“倍频”机制是硬件自动完成的不需要你手动配置但你必须知道它的存在否则算出来的定时周期会差一倍。我见过不少人在算定时时间的时候直接拿 APB1 的 36MHz 去算结果实际定时时间只有预期的一半然后开始怀疑人生。所以第一步先确认你的定时器挂在哪条总线上然后查清楚它的实际时钟频率。2.2 TIM1 和 TIM8 的特殊待遇TIM1 和 TIM8 是高级定时器它们挂在 APB2 总线上。在 STM32F103 中APB2 的预分频系数通常设为 1所以 APB2 时钟就是 72MHzTIM1 和 TIM8 的时钟也是 72MHz。这一点比 APB1 上的定时器要简单一些不需要考虑倍频的问题。但高级定时器有它自己的复杂性比如它支持互补输出、死区插入、刹车功能这些功能在电机控制中非常有用。如果你用 TIM1 做中心对齐的 PWM 输出那它的时钟基准依然是 72MHz只是计数模式变成了先向上再向下频率计算方式会有所不同这个后面会详细说。2.3 内部时钟源 vs 外部时钟源上面说的都是定时器使用内部时钟CK_INT的情况也就是时钟来自 APB 总线。但 STM32 的定时器还支持外部时钟源比如外部时钟模式 1来自 TIx 引脚、外部时钟模式 2来自 ETR 引脚、内部触发输入ITRx来自其他定时器。这些模式在特定场景下非常有用比如用外部脉冲做计数、多个定时器级联等。不过对于大多数应用来说用的都是内部时钟。你只需要在 CubeMX 里把 Clock Source 设为 Internal Clock 就行对应的寄存器操作是清除 SMCR 寄存器的 SMS 位。这一步看起来简单但如果你不小心配成了外部时钟模式定时器就不会按你预期的频率计数现象就是“定时器不动了”或者“计数频率完全不对”。提示在调试定时器问题时第一件事就是确认时钟源配置是否正确。可以通过读取 SMCR 寄存器的 SMS 位来验证或者直接在 CubeMX 里检查 Clock Source 的设置。3. PSC 和 ARR定时器的两个核心“旋钮”3.1 PSC 预分频器把快时钟变慢PSC 的全称是 Prescaler预分频器。它的作用很简单把输入的时钟频率降低。你可以把它想象成一个齿轮减速箱输入轴转得快输出轴转得慢减速比就是 PSC 的值加 1。为什么是加 1因为 PSC 是一个 16 位的寄存器它的值从 0 到 65535。当 PSC 0 时不分频输出频率等于输入频率当 PSC 1 时分频系数为 2输出频率是输入频率的一半以此类推。所以实际的分频系数是 PSC 1。这里有一个非常重要的细节PSC 寄存器有影子寄存器shadow register。你写入的新值不会立即生效而是要等到下一个更新事件UEV发生时才会被传送到影子寄存器。这个机制的目的是防止在计数过程中改变分频比导致输出异常。如果你在运行中动态修改 PSC必须手动产生一个更新事件或者等待当前计数周期结束新值才会生效。我在做动态调频的时候就踩过这个坑以为写了 PSC 就立刻生效结果发现频率没变还以为是代码没执行。后来查了手册才知道影子寄存器的存在。解决办法很简单在修改 PSC 之后把 EGR 寄存器的 UG 位置 1手动产生一个更新事件新值就会立即加载。3.2 ARR 自动重装载寄存器定时的“终点线”ARR 的全称是 Auto-Reload Register自动重装载寄存器。它决定了计数器的计数上限。在向上计数模式下CNT 从 0 开始每个时钟周期加 1当 CNT 的值等于 ARR 的值时下一个时钟周期 CNT 会回到 0同时产生一个更新事件如果使能了更新中断就会进中断。ARR 也有影子寄存器和 PSC 一样新值在更新事件时才会生效。这个机制在 PWM 输出中特别有用可以保证占空比在周期边界处平滑更新不会产生毛刺。ARR 的位数取决于具体的定时器。对于 STM32F103 的通用定时器TIM2~TIM5ARR 是 16 位的最大值 65535。对于高级定时器 TIM1 和 TIM8ARR 也是 16 位的。但到了 STM32F4 系列TIM2 和 TIM5 的 ARR 变成了 32 位可以支持更长的定时周期。这一点在选型的时候要注意如果你需要很长的定时时间比如几秒甚至几十秒32 位的 ARR 会方便很多。3.3 定时周期的完整计算公式把 PSC 和 ARR 放在一起定时器的更新事件频率可以用这个公式计算更新事件频率 定时器时钟频率 / ((PSC 1) × (ARR 1))反过来如果你想要一个特定的定时周期 T那么T ((PSC 1) × (ARR 1)) / 定时器时钟频率举个例子假设 TIM2 的时钟是 72MHz你想要 1ms 的定时周期也就是 1kHz 的更新频率。那么((PSC 1) × (ARR 1)) 72,000,000 / 1,000 72,000你可以选择 PSC 71ARR 999这样 (711) × (9991) 72 × 1000 72,000正好满足要求。这也是 CubeMX 里最常用的配置之一。但这里有个取舍PSC 和 ARR 的组合不是唯一的。你可以选 PSC 7199ARR 9也可以达到同样的频率。那为什么通常选 PSC 71ARR 999 呢因为 ARR 决定了计数的分辨率。ARR 越大每个计数周期对应的时间越短定时精度越高。如果你用 ARR 9那计数器的分辨率只有 10 个刻度稍微有点误差就会导致定时不准。所以一般原则是在满足定时周期要求的前提下尽量让 ARR 大一些PSC 小一些。当然这也不是绝对的。如果 ARR 太大更新中断的频率就会很高CPU 负担会加重。所以需要根据实际需求平衡。对于 1ms 的定时中断ARR 999 是比较合理的选择。4. CNT 到底在数什么从寄存器到实际波形4.1 CNT 的本质一个不断累加的计数器CNT 是计数器寄存器它的值在每个定时器时钟周期经过 PSC 分频后的时钟发生变化。在向上计数模式下CNT 从 0 开始每个时钟周期加 1直到等于 ARR 的值然后下一个周期回到 0如此循环。你可以把 CNT 想象成一个秒表的读数。PSC 决定了秒表走得多快ARR 决定了秒表走到多少就归零。CNT 就是当前秒表的读数。当你需要知道“现在过了多少时间”时读 CNT 的值乘以每个计数周期的时间就是已经过去的时间。这个特性在输入捕获中非常有用。比如你要测量一个脉冲的宽度可以在上升沿捕获 CNT 的值在下降沿再捕获一次两次的差值乘以计数周期就是脉冲宽度。这就是输入捕获测频率的基本原理。4.2 计数模式向上、向下、中心对齐STM32 的定时器支持三种计数模式向上计数CNT 从 0 加到 ARR然后归零。这是最常用的模式。向下计数CNT 从 ARR 减到 0然后重新加载 ARR。中心对齐CNT 先从 0 加到 ARR再从 ARR 减到 0如此循环。这种模式产生的 PWM 波形是对称的谐波分量更小在电机控制中很常用。中心对齐模式的频率计算和向上计数不一样。在中心对齐模式下一个完整的周期包括向上计数和向下计数两个阶段所以更新事件的频率是中心对齐更新频率 定时器时钟频率 / ((PSC 1) × (ARR 1) × 2)注意这里多了一个 ×2。如果你从向上计数模式切换到中心对齐模式而忘记调整 ARR定时频率就会变成原来的一半。这个坑我在做电机驱动的时候踩过当时纳闷为什么 PWM 频率突然降了一半后来才发现是计数模式的问题。4.3 更新事件定时器的“心跳信号”更新事件Update Event是定时器的一个重要概念。它在以下几种情况下产生CNT 溢出向上计数时超过 ARR或向下计数时低于 0软件触发设置 EGR 寄存器的 UG 位从模式控制器产生的更新更新事件可以做很多事情产生中断、触发 DMA 请求、更新影子寄存器、触发 ADC 转换等。在 CubeMX 中你可以选择是否使能更新中断。如果使能了每次更新事件都会进中断服务函数。这里有一个实际经验如果你的定时中断频率很高比如几十 kHz中断服务函数的执行时间就必须非常短。否则中断会嵌套、堆积导致系统响应变慢。我一般会在中断里只做最简单的操作比如置一个标志位把复杂的处理放到主循环里。如果中断频率实在太高可以考虑用 DMA 来搬运数据减少 CPU 的介入。5. 实操从零配置一个 1kHz 的定时中断5.1 CubeMX 配置步骤下面以 STM32F103C8T6 为例用 CubeMX 配置 TIM2 产生 1kHz 的更新中断。假设系统时钟已经配置为 72MHz。在 CubeMX 的 Pinout 视图中找到 TIM2将 Clock Source 设为 Internal Clock。在 Configuration 标签页中点击 TIM2 进入参数配置。在 Parameter Settings 中设置 Prescaler (PSC) 为 71Counter Period (ARR) 为 999。在 NVIC Settings 中勾选 TIM2 global interrupt使能更新中断。生成代码。生成后的代码中MX_TIM2_Init 函数会自动配置好这些参数。你只需要在 main 函数中调用 HAL_TIM_Base_Start_IT(htim2) 启动定时器然后在 stm32f1xx_it.c 中找到 TIM2_IRQHandler在里面调用 HAL_TIM_IRQHandler(htim2)最后在 HAL_TIM_PeriodElapsedCallback 回调函数中写你的业务逻辑。5.2 手动计算验证配置完成后我们可以手动验证一下定时周期是否正确。定时器时钟 72MHz PSC 71分频系数 72 ARR 999计数上限 1000 更新频率 72,000,000 / (72 × 1000) 1000Hz 定时周期 1ms如果你在中断里翻转一个 GPIO然后用示波器观察应该能看到一个 500Hz 的方波因为每 1ms 翻转一次周期是 2ms。这个验证方法很简单但非常有效可以快速确认定时器配置是否正确。5.3 代码实现与中断处理下面是关键代码片段/* TIM2 初始化配置 */ TIM_HandleTypeDef htim2; void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim2); sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(htim2, sClockSourceConfig); sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig); } /* 中断回调函数 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); } }注意AutoReloadPreload这个参数它对应 ARR 的影子寄存器使能位。设为 ENABLE 时写入 ARR 的新值会在更新事件时生效设为 DISABLE 时新值立即生效。在大多数应用中建议设为 ENABLE这样可以避免在计数过程中改变 ARR 导致的波形异常。6. 常见问题与排查技巧实录6.1 定时器不工作先查时钟使能这是最常见的问题之一。定时器的时钟没有使能寄存器写不进去定时器自然不工作。在标准库中需要调用 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE)在 HAL 库中CubeMX 会自动生成 __HAL_RCC_TIM2_CLK_ENABLE()。如果你手动写代码千万别忘了这一步。排查方法读取 TIM2 的 CR1 寄存器如果值为 0说明时钟没使能或者配置没生效。6.2 定时周期差一倍检查 APB 预分频系数前面提到过APB1 的定时器时钟在预分频系数不为 1 时是 APB 时钟的 2 倍。如果你算出来的定时周期和实际差一倍大概率是这个原因。解决办法查时钟树确认定时器的实际时钟频率然后重新计算 PSC 和 ARR。6.3 中断进不去检查 NVIC 配置有时候定时器配置没问题CNT 也在正常计数但中断就是进不去。这时候要检查 NVIC 是否使能了对应的中断通道以及中断优先级是否配置正确。在 CubeMX 中NVIC 的配置在 NVIC Settings 标签页里勾选对应的中断即可。如果你用的是标准库需要调用 NVIC_Init 函数来配置。另一个容易忽略的点是在中断服务函数中必须调用 HAL_TIM_IRQHandler否则中断标志位不会被清除会导致中断反复触发。6.4 动态修改 PSC 或 ARR 不生效影子寄存器在作怪前面说过PSC 和 ARR 都有影子寄存器。如果你在运行中修改了这两个寄存器的值但没有产生更新事件新值不会生效。解决办法修改后手动设置 EGR 寄存器的 UG 位或者等待下一个更新事件自然发生。在 HAL 库中可以使用 __HAL_TIM_SET_PRESCALER 和 __HAL_TIM_SET_AUTORELOAD 宏来修改然后调用 HAL_TIM_GenerateEvent(htim2, TIM_EVENTSOURCE_UPDATE) 来立即生效。6.5 常见问题速查表现象可能原因排查方法定时器完全不工作时钟未使能检查 RCC 配置读取 CR1 寄存器定时周期差一倍APB 预分频系数不为 1查时钟树确认定时器实际时钟中断进不去NVIC 未使能或优先级错误检查 NVIC 配置确认中断服务函数动态修改不生效影子寄存器未更新手动产生更新事件PWM 无输出输出通道未使能或引脚配置错误检查 CCER 寄存器和 GPIO 复用配置中心对齐频率减半计数模式改变但 ARR 未调整重新计算 ARR注意 ×2 系数提示调试定时器问题时善用调试器观察 CNT、PSC、ARR 寄存器的值比盲目改代码高效得多。如果条件允许用示波器观察输出波形可以直观地看到频率和占空比是否正确。7. 进阶话题输入捕获与 PWM 输出的时间基准7.1 输入捕获测频率CNT 的差值就是时间输入捕获的原理很简单当引脚上出现指定的边沿上升沿或下降沿时硬件自动把当前 CNT 的值锁存到捕获寄存器CCR中。通过读取两次捕获的值就能算出两个边沿之间的时间间隔。假设定时器时钟是 72MHzPSC 71那么每个计数周期是 1us。如果上升沿捕获到 CCR1 1000下降沿捕获到 CCR2 2500那么高电平时间就是 (2500 - 1000) × 1us 1500us。这就是输入捕获测脉宽的基本方法。但这里有一个问题如果脉冲宽度超过 ARR 的计数范围CNT 会溢出捕获值会回绕。解决办法是开启溢出中断记录溢出次数然后在计算时把溢出次数考虑进去。这个技巧在处理低频信号时非常必要。7.2 PWM 输出ARR 决定频率CCR 决定占空比PWM 输出的频率由 PSC 和 ARR 决定占空比由捕获比较寄存器CCR决定。在向上计数模式下当 CNT CCR 时输出高电平当 CNT CCR 时输出低电平。所以占空比 CCR / (ARR 1)。举个例子ARR 999CCR 300那么占空比就是 300 / 1000 30%。如果你想要 50% 的占空比就把 CCR 设为 500。这里有一个实用技巧在运行中修改 CCR 可以动态调整占空比而且不会影响频率。这在调光、调速等应用中非常有用。但要注意如果使能了 CCR 的预装载功能新值会在更新事件时生效避免产生毛刺。7.3 多个定时器级联一个定时器作为另一个的时钟源STM32 支持定时器级联也就是用一个定时器的输出作为另一个定时器的时钟输入。这种模式在需要超长定时或者多个定时器同步时非常有用。具体配置方法是把主定时器的 TRGO 输出设为 Update Event从定时器的 Clock Source 设为 Internal Trigger InputITRx然后选择对应的触发源。这样主定时器每次更新时从定时器的 CNT 就会加 1。通过这种方式可以把两个 16 位定时器级联成一个 32 位的定时器大大扩展定时范围。这个配置在 CubeMX 中稍微有点绕需要仔细选择 Master/Slave 模式。我建议先用示波器验证主定时器的 TRGO 输出是否正常再配置从定时器这样可以快速定位问题。8. 我个人的几条实操心得关于定时器的配置我总结了几个在实际项目中反复验证过的经验分享出来供参考。第一先算后配不要凭感觉。每次配置定时器之前先把定时器时钟频率、PSC、ARR 这三个值算清楚写在纸上或者注释里。我见过太多人直接在 CubeMX 里随便填个数然后发现定时不对再回头改浪费大量时间。计算公式就那一个花两分钟算一下能省半小时调试。第二ARR 尽量取大PSC 尽量取小。在满足定时周期要求的前提下让 ARR 大一些可以提高计数分辨率减少量化误差。但也要注意中断频率不能太高否则 CPU 负担会加重。一般来说ARR 在几百到几万之间比较合适。第三善用更新中断和 DMA。如果你的应用需要定期采集数据或者更新输出用更新中断触发 ADC 或 DMA 是非常高效的方式。这样 CPU 不需要参与搬运数据只需要在数据准备好之后处理即可。我在做音频采样的时候就用了这个方案TIM2 触发 ADCADC 通过 DMA 把数据搬到缓冲区CPU 占用率非常低。第四调试时先看波形再看代码。定时器的问题很多时候看波形比看代码更直观。如果手头没有示波器可以用一个 GPIO 在中断里翻转然后用逻辑分析仪或者甚至 LED 来观察。虽然精度不高但至少能确认定时器有没有在跑、频率大概对不对。第五注意不同系列芯片的差异。STM32F1、F4、H7 的定时器在位数、时钟树、功能上都有差异。比如 F4 的 TIM2 和 TIM5 是 32 位的H7 的定时器时钟频率更高支持的功能也更多。在移植代码的时候一定要重新确认时钟配置和寄存器定义不要直接照搬。最后再分享一个小技巧如果你需要非常精确的定时可以考虑使用 DWTData Watchpoint and Trace的周期计数器。DWT 的 CYCCNT 寄存器会记录 CPU 的周期数精度比定时器更高而且不需要配置中断。不过 DWT 是 Cortex-M 内核的特性不是所有 STM32 都支持使用前需要确认。我在做微秒级延时的时候用过这个方案比用定时器方便很多。
返回列表