无32kHz晶振的嵌入式开发:RTC时钟源切换与LSI校准实战
上一版定制板做原理图评审时硬件同事为了省两个负载电容和一颗32.768kHz无源晶振直接在BOM里把Xtal去掉了。当时我嘴上没说什么心里已经预感到后面这块custom board的软件开发会多出不少事。省一颗32kHz Xtal表面上只是硬件少了一个无源器件实际上是把“时基不准”这个包袱从硬件账本挪到了软件账本上而软件要接住这个包袱远没有想象中简单。这篇就把我在没有32kHz晶振的板子上做嵌入式软件开发的完整过程记录下来覆盖替代方案、初始化改动、校准方法、低功耗唤醒重设计、实测数据和踩过的坑给正在或即将面对同样板子的朋友一个参考。1. 32kHz晶振在定制板里到底管了哪些事1.1 从2的15次方说起为什么偏偏是32768HzMCU内部一般有两个低速时钟来源LSE外部低速晶振和LSI内部低速RC。LSE要正常工作板上就得有一颗32.768kHz晶振和两颗负载电容。这个频率不是随便定的2的15次方正好等于32768也就是说32768Hz经过15级二分频之后刚好是1Hz可以直接作为RTC日历计数的秒脉冲。所以几乎所有带RTC的MCU参考手册上第一推荐方案都是外接32kHz晶振。在软件层面HAL库、标准外设库、各种协议栈的默认配置也几乎全都默认LSE是存在的。一旦定制板上没有这颗晶振挨个排查“哪些代码还在等LSE就绪”是一件很烦人的事。我见过不少工程师第一反应是“没有晶振就不启用RTC呗”但真正做产品时往往没那么简单。低功耗产品需要定时唤醒、需要记录事件时间戳、需要看门狗兜底这些功能都牵扯到低速时钟域不是说关就关的。1.2 没有32kHz Xtal后软件侧受到影响的模块清单为了讲清楚影响面我列一张表按外设维度梳理一遍外设/功能模块默认时钟依赖缺失LSE后的影响RTC日历通常首选LSE初始化可能直接失败即使切到LSI走时精度也大幅下降RTC闹钟/定时唤醒通常首选LSE定时唤醒时间点漂移积累误差不可忽略独立看门狗IWDG多数用LSI不依赖LSE功能不受影响但复位超时时间随LSI漂移低功耗定时器LPTIM可选LSE或LSI唤醒周期精度变差需要重新评估业务容忍度低功耗串口/模拟外设部分依赖低速时钟视具体芯片和设计而定主系统时钟HSE/HSI/PLL独立基本不受影响这里有个容易被忽视的点很多MCU的RTC本身并不强制要求LSE但开发工具的默认配置、官方例程、以及网上能搜到的大部分代码都默认用LSE。所以切到LSI不是改一个宏定义的事而是要把初始化流程、分频参数、校准策略、唤醒逻辑全部重新过一遍。2. 没有LSE之后可选的时钟方案与精度账2.1 方案一内部LSI RC最省钱也最“不省心”内部LSI是芯片内部自带的RC振荡器标称频率通常在32kHz到32.768kHz之间具体要看型号。它的最大优势是零BOM成本、不占用引脚、不需要起振等待代码里打开一个开关就能用。但代价非常明确精度差。LSI的误差在常温下可能就有1%到3%的量级全温度范围可能跑到5%以上。作为对比一颗普通的32.768kHz晶振常温精度是±20ppm左右也就是0.002%。这两个数据差着三个数量级带来的直接后果就是如果RTC用LSI且不做任何补偿一天走时误差可以达到几分钟甚至半小时。即便LSI标称是32.768kHz实际每一颗芯片出厂值也不一样同一颗芯片在不同温度下还会继续漂。这是RC振荡器的物理特性决定的软件补偿能做但做不完美。2.2 方案二LSE旁路模式板子上没晶体但有外部时钟有一种情况需要单独拎出来PCB上没有32.768kHz晶振但板上其他器件比如另一个MCU、蓝牙芯片、以太网PHY、实时时钟芯片可能已经有32.768kHz时钟输出。这时候可以把外部时钟信号直接接到MCU的OSC32_IN引脚并在软件里把LSE配置成旁路模式Bypass。这样RTC、LPTIM等外设仍然认为自己在使用LSE精度取决于外部时钟源本身。这个方案严格来说不算“没有32kHz时钟”但在实际定制板的设计里很常见与其在板上放两颗晶振不如让一颗晶振同时给多个芯片提供时钟。缺点是引脚可能不够、信号完整性要处理、而且外部时钟源并非在所有低功耗状态下都保持输出。如果你手上的板子属于这种情况软件的改动量会小很多只需要把LSE的初始化从晶振模式改成旁路模式即可。2.3 方案三拿主晶振HSE来顶上有些MCU的RTC时钟源不仅可以是LSE/LSI还支持从主系统时钟HSE分频得到。如果板上有一颗8MHz或25MHz主晶振而且这颗主晶振的精度本来就很好那用HSE分频驱动RTC确实能把走时精度拉回ppm级别。不过这个方案有一个很硬的条件HSE在低功耗模式下通常会被关闭。也就是说系统进入STOP或Standby模式后RTC如果还依赖HSE就会直接停摆。这对需要深睡眠保持计时的产品是致命的。所以HSE分频方案更适用于“浅睡眠为主、深睡眠不要求计时”的场景比如一些一直保持唤醒但希望省掉32kHz晶振成本的产品。实际工程中我通常只在SRAM保持的停机模式下用HSE兜底RTC真正的深睡眠会另想办法。2.4 三个方案的精度、功耗与适用场景对比时钟来源标称频率常温精度温漂程度深睡眠可用适用场景外接LSE晶振32768Hz±20ppm左右小可用所有场景尤其需要精准日历外部旁路时钟32768Hz取决于输入源取决于输入源可用板级已有32kHz时钟源内部LSI RC32kHz~32.768kHz±1%~±5%大可用对时间精度不敏感的低功耗产品HSE分频8MHz/25MHz分频取决于主晶振较小通常不可用浅睡眠为主、深睡眠不要求计时有一个基本概念必须建立ppm是百万分之一。1ppm折算成一天误差大约是0.0864秒。消费电子产品走时精度做到±100ppm以内通常问题不大但LSI动辄百分之几相当于几万ppm完全不是一个量级。所以后面所有软件工作本质都是在和这个巨大的基础误差做斗争。3. RTC改到内部LSI上初始化、分频和校准的完整改法3.1 CubeMX/HAL初始化把时钟源从LSE切到LSI以STM32 HAL库为例这是改动量最直接的入口。CubeMX生成代码时如果RTC时钟源默认选的是LSE生成的MspInit回调里就会去打开LSE并等待LSE稳定。板上没有LSE时这段代码会一直卡在超时循环里RTC初始化永远走不过去。正确做法是先把RTC的时钟源明确配置为LSI。在CubeMX里RTC配置页面的Clock Source可以直接选LSI如果你手写初始化代码关键改动在HAL_RTC_MspInit回调里void HAL_RTC_MspInit(RTC_HandleTypeDef* hrtc) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_LSI; RCC_OscInitStruct.LSIState RCC_LSI_ON; if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSI); __HAL_RCC_RTC_ENABLE(); }注意这里我把OscillatorType从默认的LSE改成了LSI并且用RCC_RTCCLKSOURCE_LSI把RTC时钟源指到了LSI上。如果只在RCC_OscConfig里打开LSI、但RTC时钟源还是LSE那RTC依然起不来反而配置更混乱。3.2 预分频系数要重新算不是随便沿用默认值这是没有LSE的板子上最容易翻车的地方。RTC要把输入的时钟分频到1Hz靠的是异步预分频和同步预分频两个参数。以STM32为例RTCCLK为32.768kHz时标准配置是hrtc.Init.AsynchPrediv 127; hrtc.Init.SynchPrediv 255;因为(127 1) * (255 1) 128 * 256 32768刚好把32768Hz分频到1Hz。但LSI的实际频率并不一定是32768Hz。比如某颗芯片的LSI实测为33200Hz如果你还用127和255这组参数实际分频分母是32768那么RTC计数的1秒实际上只需要32768/33200 0.987秒也就是说RTC走快了一天大概快1.3%。所以正确的做法是先测出这颗芯片LSI的实际频率再重新配预分频系数。比如说实测33200Hz那就找一组整数A和S让(A 1) * (S 1)最接近33200。假定异步预分频取99同步预分频取331那么(99 1) * (331 1) 100 * 332 33200刚好命中。代码里改两个参数而已但背后的逻辑要清楚RTC的秒脉冲是由RTCCLK频率除以这两个分频值得来的输入频率变了分频分母必须跟着变。3.3 大误差靠分频粗调小误差靠校准寄存器精修调整预分频系数只能把误差压到整数分频的余量范围因为实际频率往往不是整数而且受温度影响随时在变。残余误差再靠RTC校准寄存器去修。现在主流MCU的RTC基本都带校准功能STM32是RTC_CALR寄存器道理都差不多在每2的20次方个时钟周期里要么多插几个时钟、要么少收几个时钟从而微调平均频率。校准分辨率大约在1ppm以内但校准范围通常只有几百ppm。注意这里有个关键问题如果LSI的基础误差是百分之几也就是上万ppm校准寄存器那几百ppm的范围根本不够用。所以校准寄存器只能用来修“分频粗调之后的残余”不能指望它直接把一颗烂RC振荡器拉成晶振精度。实操时我的做法是两步走先用频率计或者示波器接MCO引脚把LSI实际频率测准然后按3.2节的方法重算分频系数让RTC秒脉冲误差压到几百ppm以内。再跑24小时统计RTC与参考时间的偏差换算成ppm写入校准寄存器做精调。3.4 自动校准与温度补偿的工程实现思路如果你的产品工作环境温度基本恒定那单次校准就够了一次校准能把误差压到几十ppm级别日常使用完全没问题。但如果产品在室外或者温差大的环境里LSI会随温度漂移单次校准的结论很快就失效。这时候有几个现实做法可以选周期性对时校准设备只要有机会联网、收到无线报文、或者其他对时源就重新计算RTC误差并刷新校准寄存器。这是性价比最高的方案。温度查表补偿产品本身有温度传感器的话可以提前在实验室测出LSI频率随温度变化的曲线做一张补偿表运行期间根据当前温度查表调整分频或者校准值。外部时间源同步比如GPS、基站、NTP等每次收到精确时间后校准一次不依赖长期走时准确性。做这类补偿时我强烈建议把校准过程封装成一个独立模块不要散落在业务代码里。这样既能快速替换补偿策略也方便在实验室里对着测试数据调试。4. 看门狗、低功耗定时器与唤醒逻辑的重设计4.1 IWDG照常能用但超时时间的“标称值”不可信了独立看门狗IWDG通常不依赖LSE它的时钟源基本都来自LSI。所以省掉32kHz晶振对IWDG的功能没有影响但它同样面临LSI精度差的问题。IWDG的超时时间计算公式大致是超时时间 预分频系数 * (重装载值 1) / LSI频率如果LSI实际频率比标称高超时时间就变短比标称低超时时间就变长。比如标称5秒复位的看门狗在LSI偏大5%的情况下可能4.75秒就复位了偏小5%则可能拖到5.26秒。所以代码里喂狗周期不能卡着标称值的最小余量来设计。我的经验是喂狗周期至少取最坏情况超时时间的一半比如你希望系统最迟10秒内被复位那就按LSI可能偏大20%去算假设实际复位窗口只有8秒喂狗周期就要控制在4秒以内甚至更保守。4.2 LPTIM低功耗定时换时钟源后的唤醒精度评估低功耗定时器LPTIM的时钟源通常可以在LSE和LSI之间选。没有LSE后只能选LSI或者在某些芯片上用内部HFO分频。这里要做的不是改代码而是先评估业务对唤醒时间的容忍度。比如一个温湿度传感器计划每10分钟醒来采集一次如果LSI误差2%实际唤醒周期可能在9分48秒到10分12秒之间波动。对于数据上报类产品这种误差基本无感因为上报时刻本来就不需要和全球时间对齐。但如果你的产品是那种“多个节点约好在同一时刻醒来互相通信”的设计那LSI方案的概率基本是定时唤醒时刻错开通信窗口对不上。这种情况要么保留LSE晶振要么就得在协议层加入节点间的同步机制代价不小。4.3 深睡眠中需要“时间戳”时的折中方案有些产品虽然没有高精度走时需求但要求每次唤醒后能记录“当前是什么时刻”。这时候可以利用RTC基于LSI继续走时把时间戳误差控制在一个可接受范围内。关键是区分两种需求与绝对时间对齐还是只要求相对时间差。如果是后者比如只需要知道“距离上次唤醒过去了多久”LSI完全够用。进深睡眠前记录一个基准时间醒来后读RTC做差只要误差不超过业务容忍范围就行。如果是前者比如设备需要每天零点上报数据、或者根据预定时间表执行动作那就必须在进入睡眠前或者每次唤醒时做一次绝对时间校准。最常见的做法是设备醒来后先做无线对时再把对时结果写回RTC然后才执行业务逻辑。4.4 别忘了无线协议栈对低频时钟的隐性要求这一条特别容易被忽略尤其是第一次做低功耗无线产品的人。很多无线协议栈比如老版本nRF52的SoftDevice、部分BLE mesh协议、LoRaWAN协议栈默认要求外部32.768kHz晶振作为协议栈的低频时钟。以nRF52为例SoftDevice初始化时需要指定低频时钟源。你可以选LFXO外部晶振也可以选LFRC内部RC但选了LFRC后协议栈的广播间隔、连接事件定时会出现明显抖动某些高精度功能直接不可用而且每次Sleep和Wake之间的调校逻辑也不一样。如果你的定制板没有32kHz晶振又跑这种协议栈一定要在项目早期就去确认协议栈是否支持“无外部低频晶振”的运行模式。等到射频联调阶段才发现协议栈定时基准不对那改动成本和排查成本都会非常高。5. 实测复盘精度、功耗与三个最容易翻车的地方5.1 一个真实项目的精度数据从1.8%漂移到80ppm我之前有一块低功耗采集板用的MCU是STM32L0系列去掉32.768kHz晶振后做了完整的实测对比。第一轮测试直接把RTC时钟源切成LSI分频参数沿用默认的127/255。常温24小时下来RTC比真实时间快了约26分钟换算过来是1.8%的误差。这代表LSI实际频率比32.768kHz高了约1.8%。第二轮测试我用频率计测出这颗芯片的LSI实际频率然后重算分频系数让秒脉冲尽量接近真实1Hz。残余误差大约在200ppm左右也就是一天约17秒。第三轮测试再配合RTC校准寄存器做精调跑了72小时后统计常温下的误差压到了约80ppm折算成一天约7秒。这个精度对很多消费类设备已经够用了。不过把板子放进高低温箱里-20℃环境下误差又放大到了大约1.2%一天能差出17分钟。这说明单点校准只能覆盖恒温场景产品环境温度变化大时必须做多温度点补偿或者周期对时。5.2 去掉晶振后的功耗不是什么都没省晶振本身的工作电流很低一颗32.768kHz晶振加振荡电路的电流一般也就0.2uA到1uA级别。去掉它之后深睡眠电流理论上会略微下降但这只是纸面上的收益。现实情况是LSI误差带来的唤醒时间不确定可能导致你的产品需要更早醒来等待外部事件或者更频繁地做时间同步最后整机平均功耗反而比有晶振时更高。另外如果无线协议栈因为缺少外部低频时钟而不得不增加同步报文、缩短睡眠周期那功耗开销就更大了。我建议用功耗分析仪分别测有晶振和无晶振两版方案的24小时平均电流不要只看数据手册上的“休眠电流”更不要只看静态IBAT电流。整机平均电流才是判断功耗方案是否有效的唯一标准。5.3 坑1CubeMX默认配置会让RTC初始化直接失败最常见的现象生成代码后上电程序卡在HAL_RTC_Init或者HAL_RCC_OscConfig里出不来。原因是CubeMX生成代码时RTC时钟源默认选了LSELSE不存在时HAL就死等LSE就绪。排查顺序先确认RCC_OscConfig里OscillatorType是不是已经把LSI拉进来了。再确认__HAL_RCC_RTC_CONFIG选的是不是RCC_RTCCLKSOURCE_LSI。最后确认RTC预分频参数是否和实际LSI频率匹配。如果你和我一样是习惯手写初始化代码的也建议先从这三点查起。5.4 坑2校准寄存器方向搞反越校越偏RTC校准寄存器是有方向的。RTC走快了说明时钟频率偏高你要让它变慢RTC走慢了说明频率偏低你要让它变快。听起来简单但实际计算时很容易把符号搞反结果校完之后误差翻倍。建议第一次做校准的时候不要直接写入大数值。先写一个小的校准增量跑12小时或24小时确认误差方向是收敛的再继续加大校准值。这个过程虽然慢但至少不会把板子调到完全没法看的地步。我遇到过有人一次性写入最大校准值结果RTC一整天快了三个多小时后来查明白是符号反了。5.5 坑3从STOP唤醒后RTC读取到“脏时间”还有一个容易踩的点系统从STOP模式唤醒后HSI等主时钟需要重新稳定如果这时候直接调用HAL_RTC_GetTime去读时间可能会读到不稳定的值或者因为外设时钟没有同步而出现误数据。我的做法是唤醒后先做时钟稳定等待再读RTC而且读完之后要做连续性检查连续读两次时间如果两次差值异常大就再读一次。不要小看这个细节在低功耗产品里唤醒瞬间的稳定逻辑是RTC数据可靠性的关键。为了不遗漏步骤我每次验证都会走一遍下面的清单常温连续跑72小时记录RTC偏移曲线。高低温箱分别跑24小时评估温漂。用MCO引脚或者调试器实测LSI频率。整机功耗测试对比有晶振与无晶振版本的平均电流。模拟掉电、复位、低功耗唤醒多种路径确认RTC时间不出现跳变。6. 什么情况下宁可把32kHz晶振装回去6.1 这几类产品不要省需要长期独立走时的计量类设备比如电表、水表、热量表行业标准对时间精度有硬性要求。需要在深睡眠中保持精确日历和定时唤醒的电池供电设备比如野外传感器、资产追踪器。无线协议栈明确要求外部32kHz时钟的BLE、LoRa、Zigbee产品。工作温度范围宽、又没法定期联网对时的设备。这类产品省掉32kHz晶振省下的可能是一两毛钱成本但为了抹平LSI误差要付出的软件开发和测试成本会在项目后期以数倍甚至数十倍的代价还回来。6.2 这几类产品可以省设备经常联网、每次通信都会对时的场景比如Wi-Fi插座、智能灯、蓝牙门锁。对绝对时间不敏感、只需要相对计时或定时唤醒的产品。深睡眠时长较短、允许定时唤醒误差分钟级的产品。板级已有其他32kHz时钟源可以共用的产品。6.3 做决定前算一笔完整账一颗32.768kHz晶振的批量采购价大约在几分钱到两毛钱之间加上两个负载电容整机BOM成本增加也没多少。真正的成本差异在别处省掉晶振之后软件需要额外的校准模块、更复杂的低功耗唤醒逻辑、更长的实验室测试时间、以及售后阶段可能出现的走时不准投诉。所以我不太建议单纯为了省物料成本去掉32kHz晶振。更合理的原因应该是结构空间受限、引脚不够、或者板级已经有其他方案可以替代。如果只是因为“看着可以省”那后面大概率是要回炉的。我自己的经验是只要产品需求文档里出现过“定时唤醒”“日历”“时间戳”这三个词里的任何一个默认就别省这颗晶振如果真的因为客观原因必须省那从硬件原理图阶段就要把软件开发同步拉进来评估不要等板子回来了才发现低速时钟域一堆窟窿要补。希望这份复盘对正在做类似定制板的你有点帮助。