有一回项目组拿来一块定制板,MCU、Flash、传感器、射频前端都齐了,唯独PCB上RTC相关的位置全部空着,BOM里根本没有32.768kHz晶振这玩意。这板子的目标产品是个低功耗数据采集终端,平时基本都在睡大觉,醒来要能报出准的时间,还要按预设周期把采集到的数据发出去。做软件的人一看就明白,这不是硬件漏焊,是成本控制把低速晶振砍了。问题直接抛给我:没有32kHz Xtal,软件到底怎么处理时间、唤醒、休眠这一整套逻辑?
这篇文章就把我在这个定制板卡软件开发过程中的完整思路、方案选型、代码改法和踩坑记录整理出来。适合正在做定制板卡、低功耗物联网设备,或者手里刚好拿了一块没有低速晶振的板子、不知道从哪下手的固件开发工程师。内容不挑具体平台,但示例会以STM32为主,因为它的HAL库和CubeMX工具大家用得最多,其他平台思路完全一样。
1. 先说清楚:32kHz晶振到底在板子上负责什么
很多人觉得32kHz晶振不就是给RTC用的吗?少了它顶多时间不准。实际上一颗32.768kHz晶振在系统里的角色比想象中多得多。要搞清楚没有它为什么难受,先得知道它到底服务哪些模块。
32.768kHz这个频率不是随便定的。2的15次方正好等于32768,所以32.768kHz经过15级二进制分频,正好得到1Hz的秒脉冲。这个特性让它在数字电路中特别好用,RTC的秒计数、日历切换几乎都是基于这个分频链设计的。外部低速晶振在芯片手册里一般叫LSE(Low Speed External),特点是精度高、温漂小、功耗低,常年稳定在32.768kHz附近,所以被当作RTC和低功耗定时器的基准。
1.1 低速时钟的三个主力用途
第一个用途是RTC日历。设备需要记录当前时间、日期,或者计算两个时间点之间的差值,就需要一个稳定的秒级基准。LSE精准稳定,所以绝大多数带RTC的MCU默认都用它。
第二个用途是低功耗唤醒定时器。系统进入sleep或者deep-sleep之后,主时钟通常会停掉,但需要一个计时器在指定时间把系统拉起来。这个定时器的时钟源如果是外部晶振,唤醒时间点就很准;如果用的是内部RC,误差会直接累积。
第三个用途更隐蔽,是无线协议栈的sleep clock。BLE、Zigbee这些协议在收发完数据后,射频部分也进入睡眠,但协议栈需要一个32kHz左右的时钟来维持连接事件调度、定时唤醒、链路层时间补偿。比如Nordic的nRF52系列如果没有外部32.768kHz晶振,协议栈会强制使用内部LPO,连接间隔、扫描窗口这些参数的时间精度就会受影响,严重时甚至会增大丢包概率。
1.2 为什么厂商敢把32kHz晶振省掉
省掉一颗32.768kHz晶振,对硬件成本的影响并不是大家想象的“就几毛钱”。在大批量产品里,一颗晶振加上两颗负载电容、PCB布局空间、焊接工序,综合成本累加起来相当可观。而且晶振属于有源器件里比较“娇气”的,震动、温度冲击都可能引起停振或者频率跳变,砍掉它还能少一个可靠性隐患。
那是不是硬件工程师不懂RTC的重要性?不是的。关键是很多MCU内部都集成了低速RC振荡器,芯片厂商在手册里给出的典型值就是32kHz左右,精度虽然比不上外部晶振,但省掉了所有外部元件。很多产品对时间精度的要求其实没那么苛刻,比如一个数据采集器只要知道“距离上次上报过去了多久”,日误差几十秒完全可以接受。再加上现在物联网设备普遍会联网同步时间,走时不准可以被周期校准拉回来,这就让硬件设计师有了砍掉晶振的底气。
但对软件工程师来说,板子上没有32kHz晶振,意味着所有默认设计都被推翻了。你不能再照着CubeMX默认生成的代码跑,也不能假设RTC的时钟源是完美稳定的。你的工作目标就变成:在没有外部低速晶振的前提下,用软件手段让系统的时间、唤醒、通信调度尽量接近“有晶振”时的表现,并且把精度损失控制在业务可接受范围内。
2. 没有32kHz晶振,能用什么替补时钟源
软件要改,首先得知道能用什么替代方案。这一节我把实际项目中可行的替补方案逐个拆开讲,包括硬指标、优缺点、以及选型时容易忽略的坑。
2.1 内部低速RC(LSI/LPO)的硬指标
内部低速RC是大多数芯片自带的廉价替代方案。不同厂商叫法不一样,STM32叫LSI,Nordic叫LPO,NXP的部分型号叫LPOsc或FIRC,但本质都一样:一个集成在MCU内部的RC振荡器,标称频率在32kHz左右。
LSI的硬指标必须拿到手册里逐项看,尤其这三个参数:
- 频率范围:不同芯片差异很大。有的LSI在工厂校准后能做到32.768kHz±1%以内,有的没校准的能偏到±5%,甚至全温度范围±10%。拿到芯片后第一件事是把具体型号的datasheet找出来,确认室温频率准确度,不要想当然。
- 温漂系数:RC振荡器的频率和温度强相关。一本正经的datasheet会给出全温度范围内的频率变化曲线,很多低成本芯片只在典型值里给个范围,那就只能自己实测。
- 启动时间:内部RC起振一般很快,微秒到毫秒级,相比外部晶振的几百毫秒到秒级是巨大优势。但要注意,有些芯片的LSI启动稳定后频率还会缓慢漂移,RTC刚上电的几秒钟走时不稳定。
内部RC的好处是零成本、无需外部元件、起振快。坏处是精度差、温漂大、每颗芯片个体差异明显。如果你的产品放在室内恒温环境,日误差可能只有几秒;如果放在户外冬夏温差大的地方,一天漂几十秒甚至上百秒都很正常。
2.2 从外部高速晶振分频这种野路子
这个思路是:既然芯片内部有高速晶振(比如8MHz、16MHz的HSE),能不能从它分频出32kHz信号供给RTC和低速外设?理论上可以,但实际用起来问题很多。
首先,系统睡眠时通常会把HSE关掉,否则待机电流会多出几百微安甚至毫安级,这对低功耗设备是灾难性的。如果你希望RTC在休眠时继续走,HSE必须保持运行,这直接和低功耗目标冲突。其次,从HSE分频出来的低频信号,相位噪声和抖动可能比外部晶振差,对RTC精度并没有明显改善。最后,部分芯片的RTC时钟源选项里根本没有“HSE分频”这一项,你想用也用不了。
不过有一种情况适用:系统本身在整个生命周期里从不睡眠,高速晶振一直开着,只是板子恰好没有32kHz晶振。这种场景下,有一些芯片支持把HSE分频输出给RTC,软件上只需要在RTC时钟源选择时配置为HSE分频路径。但说实话,这种产品形态不太常见,我见过的大多数没有32kHz晶振的板子,恰恰是低功耗物联网设备,所以这个方案只能算“在某些特定条件下可行”。
2.3 换一颗带晶振的RTC芯片值不值
如果产品对时间精度要求非常高,比如一天误差不能超过1秒,或者设备长期离线不能经常校准,那软件再折腾也弥补不了内部RC的物理局限。这时候最靠谱的方案是外挂一颗带集成晶振的RTC芯片。
市面上常见的型号有RX8025T、PCF85063A、DS3231等,它们内部已经集成好了32.768kHz晶振,精度可以做到几个ppm,甚至秒级年误差。软件上只需要通过I2C读写芯片内部的寄存器,读取和设置时间即可。这个方案的代价是BOM成本增加、PCB面积变多、代码多一套I2C驱动。
我当时的项目原本计划外挂一颗RX8025T,结果采购说交期要六周,产品等不起,最后只能走内部RC+软件校准路线。所以这个决定一定要在软件开写之前和硬件、采购一起确认清楚,不然代码写到一半再换方案,工作量翻倍。
3. 软件上怎么把这些方案落到实处的
确定没有外部32kHz晶振、又不能临时换RTC芯片之后,剩下能做的就是靠软件硬扛了。这一节重点说STM32平台上的实操细节,包括CubeMX配置、HAL库代码改动、低功耗唤醒定时器的适配,以及校准接口的设计。
3.1 底层配置:以STM32 HAL为例切换RTC时钟源
STM32的CubeMX默认配置几乎都是把LSE当作RTC时钟源,如果你的板子没有LSE晶振,默认生成的代码会在RTC初始化时直接卡死或者RTC不走。正确的做法是手动把时钟源切到LSI。
CubeMX图形界面里,在RTC配置页把Active Clock Source从LSE改成LSI,然后再在时钟树配置里确认LSI被使能。生成的代码会变成这样,但实际工程里我更喜欢直接手写初始化代码,因为你还要做校准、测量频率这些额外操作,完全依赖CubeMX反而碍事:
RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; __HAL_RCC_RTC_ENABLE(); RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI; RCC_OscInitStruct.LSIState = RCC_LSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; HAL_RCC_OscConfig(&RCC_OscInitStruct); PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct); hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; hrtc.Init.SynchPrediv = 255; HAL_RTC_Init(&hrtc);这段代码里最要命的是分频系数的选择。STM32 HAL默认的AsynchPrediv和SynchPrediv组合,是基于“LSI正好等于32.768kHz”这个假设来配的。如果LSI实际频率是33.5kHz,你按默认配置算出来的秒就是33.5kHz/(127+1)/(255+1)得到的周期,走一天下来会快出一个不小的数字。
所以切到LSI之后,必须测量该芯片的实际LSI频率,再倒推合适的预分频值。测量方法我后面会详细写,这里先说思路:通过定时器或者外部引脚把LSI输出捕捉出来,和已知的高精度时基对比,算出实际频率,然后配置AsynchPrediv和SynchPrediv,让分频后尽可能接近1Hz。
3.2 低功耗唤醒定时器同样要跟着改
RTC只是低速时钟的一个使用者,低功耗唤醒定时器(比如STM32的RTC WakeUp Timer、LPTIM,或者Nordic的RTC实例)同样依赖这个时基。改了RTC时钟源之后,唤醒周期会被误差放大。举个实际例子:你设置唤醒周期为10秒,LSI实际频率偏高2%,那真实唤醒间隔约10.2秒。单次看不出来,一天下来累积误差就是1700多秒,近半个小时。
低功耗设备如果依赖这个唤醒定时器去决定“什么时候联网发送数据”,那这种误差会直接导致通信时间点错乱。
解决思路是在应用层别直接拿RTC计数值当作真实时间,而是引入一个“逻辑时间变量”,每次唤醒中断时按当前校准后的秒长累加。比如你测出当前LSI秒长是1.003秒,那每次RTC中断,逻辑时间就加1003毫秒,就算中断唤醒间隔有偏差,应用层看到的逻辑时间也是可控的。
另外,如果用RTOS并且开了tickless模式,要注意低功耗定时器的时钟源也必须跟着切换。我遇到过一个问题:任务调度的时基在休眠时用的是RTC唤醒中断,唤醒后恢复Systick,结果LSI漂移导致tickless计算出来的睡眠时间不准,代码里出现“任务被唤醒后时间戳倒流”的诡异情况。排查了很久,最后发现是低功耗定时器的补偿值没有根据实际时钟频率修正。
3.3 预留校准接口,把精度补回来
内部RC不是不能用,但必须把校准能力内置到固件架构里。我在驱动层会预留一个时间校准接口,不管最后外部参考源是什么,上层都能调用它来修正RTC误差。
接口可以设计成这样一个固定函数:
// 输入:当前偏差,单位ppm // 实现:根据芯片具体寄存器调整RTC校准值 void rtc_apply_correction(int32_t ppm);内部实现一般有两种方式。一种是通过RTC的校准寄存器,比如STM32L4系列RTC有CALR寄存器,可以按0.954ppm步进进行调整;另一种更通用粗暴的方式是直接改写RTC的当前计数,比如发现本地时间快了5秒,就直接把RTC的时间往回拨5秒。前者适合细调,后者适合粗调。
我的建议是两层都做:粗调用寄存器校准,细调用逻辑时间补偿。这样上层业务只需要说“我的时间偏差了多少ppm”,底层驱动负责把修正做到计时链路里。把校准接口放在驱动层,是为了以后换芯片方便,不用动业务代码。
4. 精度校准:没晶振之后的走时校正方案
没晶振,不代表时间精度就一定烂到家。只要系统有办法从外部获得准确时间,或者能在产线做一次静态校准,走时误差就可以被控制在业务可接受的范围内。这一节把我在项目中用过的几种校准方案整理出来,重点讲计算方法和注意事项。
4.1 用外部精确源做周期校准
这个方案适用于联网设备。设备每次联网后,从服务器获取权威时间,与本地RTC时间做差,得到误差值,然后换算成ppm写入校准寄存器,或者直接调整RTC计数值。
计算公式很简单:
ppm = (本地时间 - 参考时间) / 经过时间 * 1e6
关键在于经过时间的选取。如果只跑了几秒钟就测量偏差,通信耗时、任务调度延迟、代码执行时间都会引入噪声,算出来的ppm根本不靠谱。我的经验是至少观察10分钟以上再做一次完整计算,如果设备联网频率很低,可以连续观察几次再收敛。
还有一个容易被忽略的点:校准不能太激进。比如单次测量显示系统快了2000ppm,直接一次性调到位,下次测量又显示慢300ppm,来回调整会让时间忽快忽慢,用户体验很差。建议加一个低通滤波,或者限制每次调整的最大步进,比如每次最多只校准100ppm,让时间平滑收敛到正确值。
4.2 温度补偿和分段校准思路
LSI的温漂比晶振大得多。如果你的设备在室内,温度变化不大,做一次静态校准就差不多了。但如果设备要放在户外、车载、冷链运输这种温度剧烈变化的环境,静态校准根本不够,需要动态补偿。
动态补偿的做法是:在设备运行时定期读取MCU内部温度传感器,根据温度-频率偏移曲线,实时调整RTC的校准值。这个方案的关键是曲线数据。你可以从芯片手册里找典型的LSI温漂曲线,也可以自己做一批板子在温箱里标定。标定工作量不小,但一旦有了曲线,代码实现就很直接了:
- 每隔一段时间(比如30秒)读一次内部温度传感器;
- 查表得到当前温度对应的频率偏移ppm;
- 调用rtc_apply_correction写入最新校准值。
如果你的产品没有时间精力做温补,但又有离线计时需求,我建议老老实实加上外置RTC芯片。软件再努力也改变不了物理器件本身的温漂特性,硬要凑合,最后验收时挨骂的还是自己。
4.3 同步策略:让RTC在关键节点对齐
除了平滑校准,有时候也需要“硬同步”。比如设备长期离线后刚开机,或者从飞行模式恢复,手里没有任何参考时间,本地RTC可能已经偏了好几分钟。这种场景下,一旦拿到外部准确时间,直接设置RTC时间比慢慢校准高效得多。
策略选择上有讲究。如果业务允许时间跳变,比如设备只用于记录传感器数据,时间戳偶尔跳一下没关系,那就直接HAL_RTC_SetTime,最简单可靠。如果业务要求时间单调递增,比如用于计费、前后端对账,那就要做平滑调整,每次调整上限比如1秒,多个周期慢慢拉回。
我在一个项目中做过折中方案:每次联网后,如果偏差在3秒以内,用校准寄存器慢慢纠正;如果偏差超过3秒,直接硬设置到服务器时间。这个阈值可以根据实际业务容忍度调整,但有个原则:能平滑纠正就尽量平滑,只有偏差大到不能接受时才允许跳变。
5. 实操记录:我在这个项目里踩过的坑
理论说完了,讲点实际项目里的血泪经验。没有32kHz晶振这事情,表面上只是时钟源切换,实际开发中会冒出一堆奇奇怪怪的问题。我把几个印象最深的记录下来,希望能帮你少走弯路。
5.1 睡眠电流测出来偏大的真凶
项目初期,我们以为砍掉32kHz晶振后待机功耗肯定更低,结果第一版样机实测睡眠电流比预期高了整整60多微安。60微安对一次充电用半年的设备来说,相当于白白浪费了很大一部分电池容量,根本没法交差。
排查的时候先怀疑是外部器件漏电,量了一圈都没问题。最后把MCU的每个外设时钟挨个关掉,发现LSI的驱动配置有问题。很多STM32芯片的LSI可以配置成“输出模式”,把LSI信号引到某个引脚做外部时钟源。CubeMX默认生成的代码在某些系列上会把LSI输出打开,就算引脚悬空,内部驱动电路也在持续耗电。关闭LSI输出功能后,电流立刻降下来了。
这个坑提醒我:切到LSI后,要检查LSI本身是不是被配置成了输出模式,以及所有用到LSI的外设(RTC、LPTIM、IWDG)是否真的都挂在这个时钟树下,不要因为省了一颗晶振反而引入额外功耗。
5.2 日历芯片和内部RC起振打架
项目中期,产品经理又提出一个需求,希望设备能多一个硬RTC备份时间,防止MCU复位后时间丢失。硬件工程师在改版时预留了外置RTC芯片接口,代码里要同时驱动MCU内部RTC和外置RTC芯片。
问题出在低功耗唤醒后的时序上。外置RTC芯片通过I2C通信,而I2C时钟来自MCU的系统时钟,系统时钟又需要在唤醒后经过锁相环重新稳定。因为内部RC的启动特性不如外部晶振稳定,唤醒后立刻去读外置RTC,偶发出现I2C总线超时,日志里看到一堆NACK错误。
解决办法是在唤醒处理函数里加一个延迟,等系统时钟稳定后再去访问外置RTC。具体延迟时间需要实测,我这边定在1ms到2ms之间就能稳定通过。如果你们用的芯片起振时间更长,这个数字要适当加大。另外,外置RTC的初始化代码尽量别放在中断里,挪到主循环或单独的任务里执行,降低时序冲突的概率。
5.3 批量产线上的校准数据怎么烧进去
静态校准要求每块板子在出厂前测出LSI的实际频率偏差,再把校正值写进去。这需要产线测试流程里多一步,最初生产同事很抵触,觉得增加了节拍时间。
我们设计了一个很轻量的软校准流程。设备烧录固件后,进入产测模式,上位机发送一个CAL_MEAS <duration_ms>指令,设备收到后开始用RTC计时,持续时间到后,把RTC计数的变化值通过串口返回。上位机用自己的高精度时钟记录实际耗时,两者对比就能算出当前板子的ppm偏差。上位机把偏差值写入Flash指定区域,固件启动时读取并应用。
整个流程在产线上多花不到3秒,但产品走时精度从日误差几十秒提升到了几秒以内,效果非常明显。如果你的产品有联网功能,产线校准可以和无线校准协同进行:产线写一个初值,设备首次联网后自动修正,后续就能维持一个很好的精度水平。
6. 常见问题速查表
我把整个开发过程中最常遇到的问题整理成一张速查表,方便你遇到类似情况时快速定位。这张表不是万能的,但能覆盖大多数没有32kHz晶振的定制板卡开发场景。
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| RTC不走,读出来永远是0 | 时钟源没切到LSI,CubeMX默认还在用LSE | 检查RTC时钟源选择,确保LSI使能且稳定 |
| RTC走时快/慢得很夸张 | 分频系数基于标称频率配置,没按实际LSI频率调整 | 测量LSI实际频率,倒推AsynchPrediv和SynchPrediv |
| 睡眠唤醒时间点偏差大到影响业务 | 唤醒定时器依赖LSI,误差随周期累积 | 应用层用逻辑时间累加,或者做周期校准 |
| 睡眠电流比预想中高 | LSI输出功能开启,或无关外设时钟没有关闭 | 关闭LSI的外部输出引脚,逐个检查时钟门控 |
| 唤醒后I2C读外置RTC失败 | 系统时钟未完全稳定,I2C时序冲突 | 唤醒后延迟1ms以上再访问,或把读取放到主循环 |
| 批量产品走时一致性差 | 每颗芯片LSI个体差异大 | 产线软校准,写入每台设备的独立偏差值 |
| 联网设备校准后时间反复横跳 | 单次测量噪声大,校准过于激进 | 加滤波,限制单次校准步进,拉长校准观察窗口 |
| RTOS任务时间戳出现倒流 | tickless模式低估或高估了睡眠时长 | 根据LSI误差修正tickless的补偿参数 |
每个项目情况不同,遇到问题最好先看芯片手册里“时钟控制器”和“RTC”这两个章节,很多答案其实都在手册里,只是平时不细看。没有32kHz晶振的开发挑战,说到底不是技术上的绝路,而是要在心里始终绷着一根弦:系统的时间基准已经变了,所有依赖时间的逻辑都必须重新过一遍。
这批项目做下来,我的体会是:没有32kHz晶振的板子并不可怕,可怕的是软件团队始终按“有晶振”的思路去写代码。把时钟源切换、校准接口、低功耗唤醒策略这些条件从一开始就作为设计输入,后面会省掉很多debug时间。如果你现在正拿着一块没有低速晶振的板子发愁,建议先从确认芯片的LSI规格和可校准能力入手,再决定是直接使用内部RC、还是外挂一颗带晶振的RTC芯片。两条路都通,关键是别让软件在错误的时钟假设上裸奔。