ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32内部HSI时钟精度导致定时器周期偏差的排查与校准

2026/8/30 6:08:13 拓冰建站 浏览量
STM32内部HSI时钟精度导致定时器周期偏差的排查与校准 上周帮一个客户调STM32C091CCT6的定时器遇到一个非常典型的问题代码里明明配置的是1000ms的中断周期他用逻辑分析仪量GPIO翻转波形测出来却是995ms。差了5ms说大不大说小不小但对一个用内部HSI做时钟源的项目来说这个现象几乎可以锁定一个方向——HSI 48MHz的实际频率并不是精准的48.000MHz。这个问题值得写一篇完整的排查记录。因为STM32C0系列本身定位就是低成本、精简外设、内部时钟优先的MCU很多人拿到手直接用HSI跑很少有人会去较真“HSI到底准不准”这件事。但一旦你开始用定时器做精确定时、做RTC补偿、对外通信波特率这个误差就会变成实实在在的坑。这篇内容我会把排查思路、原理计算、实测数据和修正方案全部展开花十分钟读完以后再遇到定时器周期对不上你至少有三个方向可以查。1. 现象与问题定义先说现场情况。板子上电后PB0翻转一次逻辑分析仪抓到高电平到下一次高电平的间隔是995ms左右稳定波动在±0.2ms以内。代码里Timer配置的目标是1000ms进一次更新中断中断里翻转IO。1.1 现象描述本质上这是一个“定时器周期整体偏短”的问题。它不是偶发跳变也不是温度漂移导致的不稳定而是每次测量几乎都稳定在995ms附近。这类问题基本可以排除代码逻辑错误比如中断里面做了耗时操作挤占了下一次中断、或者ARR被意外修改等。重点要查的是时基来源也就是“定时器每秒数了多少个脉冲”。如果把995ms这个数字换成百分比偏差是0.5%。这个幅度放在HSI内部RC振荡器身上非常合理但如果放在外部晶振HSE身上就有点可疑了。所以第一步怀疑对象就是HSI。1.2 为什么首先怀疑HSISTM32C0系列主打低成本很多型号甚至没有HSE引脚比如部分封装只支持内部时钟。C091CCT6这个型号属于C0系列里外设相对丰富的料但内部HSI 48MHz依然是最常用的系统时钟来源。HSI本质是芯片内部的RC振荡器RC振荡器的频率精度天然受两个因素影响出厂时的初始误差和运行中的温漂。STM32的HSI在出厂时会做一次校准校准值存在系统存储区上电后自动加载到HSICAL字段。但校准本身只能保证在特定温度和电压点附近把频率拉近到48MHz不可能做到晶振那样的几十ppm精度。数据手册上HSI48在全温度范围内的精度通常在±1%左右0.5%的偏差完全在合理区间内。1.3 关于“995ms”的量级判断我们可以先做一个快速反推如果定时器计数周期实际时间比预期短了0.5%说明定时器时钟实际频率比48MHz高了约0.5%也就是大约48.24MHz。这个数字对于HSI来说一点都不离谱恰恰说明HSI确实在正常工作只是“精度”不像我们默认的那样理想。这时候要判断的是0.5%的偏差对当前应用是否可接受。如果只是点个LED肉眼根本看不出差别如果是累计计时一小时就差18秒一天差7分多钟这个就要认真对待了。我们先不急着下结论按顺序把原理和排查过程走一遍。2. 背后原理HSI、时钟树与定时器时基要彻底弄明白995ms从哪来得先把时钟链路上每个环节拆开看。系统运行时的定时器周期由三个因素共同决定时钟源频率、预分频器PSC、自动重装载值ARR。这三者任何一个偏离预期最终周期就会跟着偏。2.1 HSI 48MHz是什么样一个时钟HSI 48MHz在STM32C0上是一个内部RC振荡器上电后不需要外部晶振就可以作为系统时钟。因为省掉了外部晶振硬件BOM成本降低PCB面积也省了这是C0系列的一个核心卖点。但代价就是时钟精度远低于晶振。RC振荡器是靠内部电容充放电时间常数来产生频率的工艺偏差、温度变化、供电电压波动都会改变充放电速度。STM32在出厂时会对HSI做一次修调让它在常温、标称电压下尽量接近48MHz。这个修调值被固化在芯片里软件可以读到它也可以在此基础上进一步微调。2.2 HSI的出厂校准和温漂限制STM32的HSI校准值在每次复位后自动加载到RCC的时钟控制寄存器里所以你不需要写任何初始化代码HSI就会以出厂校准后的状态运行。这个校准值是在测试温度下测出来的如果你的板子工作环境温度偏离测试点HSI频率就会偏移。另外供电电压也会影响。内核电压越高RC振荡器频率通常会偏高。很多低功耗应用在电池供电场景下电压从3.3V掉到3.0VHSI频率就可能偏移0.3%到0.8%。所以有时候同一个代码在开发板上用USB供电测出来995ms换到电池供电再测可能变成1006ms这都是正常现象。2.3 定时器时基计算公式与拆解定时器溢出周期的计算公式是溢出周期 (PSC 1) × (ARR 1) / TIM_CLK其中TIM_CLK是定时器模块实际拿到的时钟频率PSC是预分频值ARR是自动重装载值。这里最容易踩坑的是PSC和ARR都要加1因为寄存器里存的是“分频系数减1”和“计数值减1”。举个例子配置1000ms定时系统时钟48MHz预分频PSC4799实际分频4800自动重装载ARR9999实际计数10000次定时器时钟频率 48MHz / 4800 10kHz计数10000次的时间 10000 / 10kHz 1000ms这个计算看起来很简单问题在于TIM_CLK到底是不是48MHz。很多人默认TIM_CLK等于系统主频但在STM32上这不一定成立。2.4 一个很容易被忽视的点TIM时钟可能不等于系统主频在STM32C0上看定时器挂在哪条总线上很重要。APB1和APB2的预分频器如果设置为1那么该总线上的定时器时钟等于系统主频但如果APB预分频器大于1定时器时钟会被加倍。比如APB预分频为2APB时钟是24MHz但定时器时钟是48MHz因为定时器输入时钟被硬件自动同步到APB的2倍频。如果你在配置系统时钟时用了HAL库的HAL_RCC_ClockConfig()它会自动处理一部分但如果你手动修改了APB分频然后直接用SystemClock频率去算定时器参数结果就会差一半或者偏差更奇怪。这种问题经常被误判为“定时器慢了一倍”或者“定时器周期不对”实际上只是时钟树配置没有对齐。所以在进入下一步排查之前先确认三件事系统时钟是不是48MHzTIM所在总线的APB预分频是多少定时器实际输入时钟是多少3. 系统排查一步步定位偏差来源排查这种事情不能靠猜要按顺序来。我一般把排查流程分成四步确认时钟树配置、核对定时器寄存器、反向推算实际频率、排除测量干扰。每一步都有对应的验证方法。3.1 第一步确认系统时钟与总线分频先用调试器或者直接在代码里读寄存器。最直接的方式是读RCC_CFGR确认系统时钟源是HSI还是PLL再确认AHB、APB1、APB2的预分频值。如果你用的是HAL库可以直接调HAL_RCC_GetSysClockFreq()返回当前系统时钟频率。C091的典型配置下默认就是HSI 48MHz作为系统时钟AHB不分频APB也不分频。这种配置下定时器时钟也就是48MHz。如果你在这个环节发现APB分频不是1那就要重点检查定时器的时钟是否存在倍频关系。这一步的目的不是找bug而是把时钟链路确认死排除后面所有关于“定时器时钟不是48MHz”的可能性。3.2 第二步核对定时器配置检查配置寄存器时重点看TIMx_CR1的CKD位它控制定时器自己的时钟分频一般写00表示不分频。然后看TIMx_PSC和TIMx_ARR是不是和预期一致。特别要注意PSC和ARR有没有“差1”的情况。我还遇到过一种情况代码里先用__HAL_TIM_SET_AUTORELOAD()设置了ARR之后又被初始化函数重新覆盖。这种问题在寄存器级调用的工程里很容易出现特别是多个外设初始化文件共用同一个定时器句柄时。定位方法也很简单在启动定时器之前加一个断点把寄存器实际值读出来比对设定值。HAL库可以用TIMx-PSC和TIMx-ARR直接读。3.3 第三步反向推算实际时钟频率如果PSC4799、ARR9999都确认无误实测周期还是995ms那就可以反推出定时器时钟实际频率实际频率 (PSC 1) × (ARR 1) / 实测周期 4800 × 10000 / 0.995 ≈ 48.24MHz这个结果说明定时器时钟源HSI的实际频率大约是48.24MHz高于标称的48MHz。到了这一步问题基本锁定在HSI频率本身而不是定时器配置。反推出来的频率还能帮你判断偏差是否在合理范围内。0.5%的误差对HSI来说是正常的如果反推出来是50MHz或者60MHz那大概率是时钟源被切到了别的倍频路径或者是APB分频计算错了。3.4 第四步排除测量工具和中断延迟干扰排查完芯片本身之后还要确认测量手段是否正确。用逻辑分析仪测GPIO翻转周期时采样率决定了时间分辨率。比如逻辑分析仪采样率是1MHz那么每个采样点间隔1µs测一个1s的周期误差最多也就1µs量级不会产生5ms的偏差。除非采样率远低于信号频率否则测量工具本身不太可能引入这么大误差。中断延迟也不能忽略。如果ISR里做了大量浮点运算、打印日志、甚至调用HAL_Delay那么中断响应时间会拉长。但要注意定时器更新中断如果发生了“来不及响应”的情况表现出来的是周期抖动或者偶发漏中断而不是稳定偏短。稳定995ms这种特征更像是时基本身偏快而不是软件延迟。4. STM32C091CCT6实测记录与计算结果为了验证上面的分析我搭了一个最简单的测试环境把整个排查过程量化一遍。如果你手上有同型号芯片可以直接照着测。4.1 测试环境主控STM32C091CCT6开发板自制最小系统板3.3V供电去耦电容齐全调试工具ST-Link V2测量工具逻辑分析仪采样率25MHz8通道 示波器交叉验证编译环境STM32CubeIDE 1.15HAL库初始化代码使用HSI作为系统时钟不启用PLLAHB和APB都不分频。RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.HSIDiv RCC_HSI_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_NONE; HAL_RCC_OscConfig(RCC_OscInitStruct); RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_HSI; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_0);定时器用TIM3配置为PSC4799ARR9999更新中断翻转PB0。4.2 实测数据连续测量10次GPIO翻转周期结果如下次数周期(ms)偏差(ms)折算TIM时钟(MHz)1995.2-4.848.232995.1-4.948.243995.3-4.748.224995.0-5.048.245995.2-4.848.236995.1-4.948.247995.3-4.748.228995.2-4.848.239995.0-5.048.2410995.1-4.948.2410次测量极差只有0.3ms说明频率一致性很好不是热噪声或者供电纹波造成的随机抖动而是系统性的初始偏差。再用示波器在相同引脚确认了一遍测到的周期同样是995ms左右排除了逻辑分析仪自身时基误差的影响。4.3 误差来源归纳从实测数据推算HSI实际频率约为48.23~48.24MHz比标称48MHz高了约0.48%。这属于HSI内部的出厂校准残差加上芯片工作温度与出厂校准温度不同导致的温漂。如果这个偏差是你不能接受的需要进一步做校正。但首先要区分一个概念HSI偏差是固定偏差加漂移的组合固定偏差可以软件补偿漂移很难完全消除。对大部分应用来说只要把固定偏差补偿掉时基精度就能提升一个量级。5. 常见问题速查与避坑做完上面这一整套排查你可能会发现自己的问题根本不在HSI而是死在某个更基础的地方。这里我把调试过程中遇到过的高频问题整理成一张速查表覆盖配置类、时钟源类和测量类三大方向。5.1 配置类问题问题现象可能原因排查与解决定时器周期比预期整数倍偏长APB预分频1导致TIM时钟被加倍确认TIM所在总线的APB分频按实际TIM_CLK计算定时器周期比预期稍长/稍短偏差在0.5%左右PSC/ARR读写顺序错误或寄存器被覆盖启动前断点检查TIMx-PSC与TIMx-ARR实际值定时器周期比预期略短0.1%左右PSC或ARR写入时分频值忘记减1重新核对公式 (PSC1)×(ARR1)/TIM_CLK配置值没问题但运行中周期变化ARR被DMA或其它外设意外修改加断点或MPU写保护检查谁在写定时器寄存器周期完全不固定忽长忽短中断响应超时更新事件被丢缩短ISR或改用硬件方式触发别在ISR里做耗时操作这里特别说一个容易踩的点如果你的更新中断里做了HAL_GPIO_TogglePin()之外的事情比如打印调试信息那USART波特率如果是基于HSI算出来的发送本身也可能消耗不等的时间。测试定时器精度时ISR里最好只翻转IO不做任何别的事。5.2 时钟源类问题问题现象可能原因排查与解决定时器周期比预期慢一倍APB分频导致TIM时钟APB×2但没算进去按实际时钟树计算别拿系统主频直接套温度升高后周期变长HSI温漂RC频率随温度变化评估温漂范围必要时换HSE或加软件补偿供电从3.3V降到3.0V之后周期变化HSI随供电电压漂移确认电压范围若精度要求高改HSE用HSI但配置了PLL发现周期离谱PLL配置不当或PLL未锁定先确认SYSCLK源是HSI还是PLL读RCC_CFGR复位后第一次定时不准之后正常HSI启动稳定时间不够启动后延时几百us再开启定时器其中一个经典场景是“GD32 ADC Timer”和“GD32定时器慢了一倍”这类搜索热词。GD32和STM32的定时器时钟树逻辑类似同样存在APB预分频导致TIM时钟倍频的问题。如果拿STM32的代码直接改到GD32上先看APB分频配置和HAL库对TIM时钟频率的处理方式否则程序跑起来定时器周期不对非常正常。5.3 测量与外部干扰问题现象可能原因排查与解决逻辑分析仪测出稳定偏短逻辑分析仪自身时基不准用示波器交叉验证或用频率计直接测MCO输出用手摸芯片/板子后周期变化HSI随温度变化人体热辐射影响这是物理特性加热环境下评估温漂板子靠近电机/继电器时周期抖动EMI干扰耦合到振荡器或IO增加滤波电容IO加RC滤波改屏蔽测量IO引脚带了重负载翻转沿变缓逻辑分析仪触发点偏移IO驱动能力调高或测量时断开负载我实测时用25MHz采样率的逻辑分析仪测1S周期哪怕采样率再低一些也不至于差出5ms。如果你测出来一个稳定的995ms先别怀疑设备多半是芯片时基本身的问题。反过来如果测出来的周期每次都比上一次少几ms、看起来像在持续漂移那才需要考虑测量设备或者温度上升过程的影响。6. 修正与校准方案到这里问题已经定位清楚了HSI实际频率48.24MHz导致定时器周期从1000ms变成995ms。接下来看怎么修。方案分三档不动它、硬件换晶振、软件校准。三档的成本和效果差别很大按需求选。6.1 接受误差的场景如果你的应用是LED指示灯、蜂鸣器、按键扫描去抖、传感器周期性采样这类场景0.5%的误差完全可以接受。按键扫描20ms去抖实际19.9ms人感觉不出来。LED呼吸灯1s周期实际995ms肉眼根本看不出差别。这类应用没必要增加成本去换晶振也不要盲目加校准代码增加复杂度。6.2 硬件层面切换HSE如果项目对时间精度有硬性要求比如需要长期累计计时、做电力系统对时、或者对外通信波特率要求严格那么最好的方案是换外部晶振。STM32C091支持HSE外部晶振精度通常能做到20ppm甚至更好比HSI高两个数量级。代价是硬件上多两颗电容和一个晶振PCB上多一小块布局空间。对于本来就不缺成本的工控、电力、医疗设备直接上HSE最省心。软件上把系统时钟源从HSI切到HSE定时器参数不用改只要HSE频率还是按12MHz或24MHz选方便PLL配合。切换之后的计时精度我实测过一批STM32C091外部8MHz晶振方案24小时累计偏差在2秒以内完全够绝大多数应用场景使用。6.3 软件校准实测频率修正ARR/PSC如果不想改硬件又想把0.5%的固定偏差补掉软件校准是最经济的方式。思路很简单先测出实际周期或实际时钟频率然后反向修正ARR或PSC。以本文的实测数据为例目标周期1000ms实测周期995ms偏差系数K 995 / 1000 0.995修正方式把ARR从9999改成(int)(9999 × 0.995) 9949这样理论周期变成4800 × (99491) / 48.24MHz ≈ 990.0ms。等等这个结果不对因为我用了实际频率48.24MHz来计算。这里要统一口径修正的目标是在保持PSC不变的情况下减少计数值使实际周期回到1000ms。正确计算应该是期望的实际计数时间 1000ms 定时器时钟实际频率 48.24MHz 实际计数频率 48.24MHz / (PSC1) 48.24MHz / 4800 10.05kHz 需要的计数次数 1000ms × 10.05kHz 10050次注意这里需要的计数次数不是减少而是增加等一下我重新推一下。之前PSC4799ARR9999理论设计时认为定时器时钟是48MHz。实际时钟是48.24MHz实际计数频率是48.24MHz/480010.05kHz。计数10000次需要的时间是10000/10.05kHz995ms。要让这个实际时间达到1000ms需要计数次数 1000ms × 10.05kHz 10050次。所以ARR应该改成10049。这里我刚才写错了修正一下因为时钟偏快要让周期变长、回到1000ms应该增加计数次数而不是减少。同理如果用理论频率48MHz去算固定偏差补偿系数应该是K 实际时钟频率 / 标称时钟频率 48.24 / 48.00 1.005 ARR_new (int)(9999 × 1.005) ≈ 10049所以修正后的配置是PSC保持4799ARR改成10049。这个结果和用实测周期反推是一样的ARR_new (ARR1) × (48.24/48) - 1 ≈ 10049。代码里加一行宏定义就行。#define HSI_ACTUAL_FREQ_HZ 48240000UL #define HSI_CALIB_FACTOR (HSI_ACTUAL_FREQ_HZ / 48000000.0) #define TIM_ARR_CALIB(x) ((uint32_t)((x 1) * HSI_CALIB_FACTOR - 1)) TIM3-ARR TIM_ARR_CALIB(9999);这样改完之后定时器周期就能回到1000ms附近。但要注意这只补偿了固定偏差温度变化带来的漂移仍然存在。如果需要更高精度就要使用动态校准。6.4 动态校准思路动态校准的核心思想是找一个精准的参考时钟源用它来实时测量HSI的实际频率然后动态调整定时器参数。可行的参考源有三个LSE外部32.768kHz晶振如果板上有时钟晶振外部输入的标准频率信号比如GPS秒脉冲、工频50Hz过零信号内部LSI精度太差只能做粗校准以LSE为参考的实现思路是用某个定时器工作在输入捕获模式捕获LSE引脚上的32768Hz方波在1秒内统计捕获到的上升沿次数如果计数接近32768说明主时钟频率正确如果计数明显偏多说明HSI偏快。另一种更简单的做法是用MCO引脚输出HSI时钟用频率计直接测出实际频率然后把这个频率写进Flash里的校准参数区。每次上电时读取计算ARR补偿值。这个方法不需要额外的参考时钟输入只需要产线测试时进行一次校准适合批量生产场景。我不建议在产品里用太复杂的动态校准算法。因为每次上电重新测量HSI频率本身就耗时而且测温漂需要持续采样软件复杂度上去了收益却不一定值得。对绝大多数场景固定补偿加温度范围评估就够了。7. 我的结论与建议这台C091最终按照客户需求做了固定补偿因为客户知道自己的工作环境温度相对稳定板子电源也是固定的LDO输出实测ARR改成10049之后连续跑了一整天累计偏差小于200ms。如果换成HSE这个数字可以做到1秒以内但客户不想增加硬件成本固定补偿已经满足了需求。从这次调试里我总结出一个经验用STM32C0做定时相关功能时不要把HSI默认当成“48.000MHz”。它的真实频率大概率在47.8MHz到48.3MHz之间这取决于你的板子工作温度和供电电压。设计阶段就要决定是否接受这个误差而不是等产品做出来了再返工。另外一个建议是每个项目的启动日志里最好把SystemCoreClock、TIM_CLK、PSC、ARR这些关键参数全部打印出来。这个习惯在调试时能帮你省很多时间。尤其当代码经过多个人维护之后没有人能保证时钟配置没有被改过。最后分享一个小技巧如果你怀疑HSI不准但不想接示波器测定时器引脚可以在代码里通过MCO把系统时钟引出来用万用表频率档或者逻辑分析仪直接量MCO引脚上的频率。STM32C0的MCO配置很简单选HSI作为MCO源量出来就是HSI的实际频率。这样一个操作就能确认时基问题比反复改定时器参数要快得多。