STM32F103无外部晶振:用内部LSI实现RTC日期计时与校准

发布时间:2026/9/16 21:16:43
STM32F103无外部晶振:用内部LSI实现RTC日期计时与校准 简介ST公司出品的基于Cortex-M3内核的微控制器在仅使用内部低速时钟源的前提下实现日期时间管理与串口发送是面向嵌入式开发者和初级学习者的完整工程资源。该资源适用于外部晶振不可用或追求低功耗的硬件设计场景能够解决实时时钟依赖外部晶振的常见问题。压缩包内共包含一百九十二个文件主要有C语言源程序文件、头文件、编译生成的目标文件、依赖关系文件以及交叉引用文件同时包含Keil工程文件和链接脚本可完整复现编译与调试环境整体压缩包大小约为三点八六兆字节。资料详细展示了低速时钟初始化、预分频因子配置、日历读取接口及串口输出函数等关键代码同时附带标准外设库便于对照学习寄存器级原理并配有工程配置文件可帮助理解不同文件在工程中的分工。目前已有两千零七十三人学习使用适合需要快速实现带时间戳显示功能的项目参考可显著减少时钟模块的排错时间。1. 没有外部晶振STM32F103C8T6 用内部低速时钟也能跑日期常见的 STM32F103C8T6 最小系统板上只焊了 8MHz 主晶振很多板子根本没有 32.768kHz 低速晶振。做日期时间功能时默认的 LSE 时钟源起不来不少人直接放弃内部 RTC改用 DS1302 或 DS3231 外挂方案。其实 F103 内部还有一路大约 40kHz 的内部低速时钟LSI不依赖任何外部元件就能把 RTC 秒计数器推起来再由软件换算成年月日时分秒。这就是“基于内部低速时钟的日期”的完整含义。难点在于 LSI 的出厂精度和温漂远比晶振差直接按 40kHz 配置一天误差可能是几分钟甚至几十分钟。本文按这条 LSI 路线把 RTC 配置、日期换算、频率校准和掉电验证讲到能直接落地。2. 内部低速时钟的精度边界与 RTC 时钟源选型2.1 F103 的 RTC 为什么离不开低速时钟源STM32F103 的 RTC 是一个 32 位秒计数器本身不认识年月日驱动它前进的时钟必须来自“低速”档。参考手册里 RTC 的时钟源选项有禁用、LSE、LSI 和 HSE/128 四种其中 HSE/128 依赖外部 8MHz 主晶振一旦最小系统板把 HSE 拆掉或者进入低功耗模式关闭 HSE这条路也不通。真正不花钱、不占引脚的只有内部低速时钟 LSI。LSI 在 F103 上主要服务两个外设独立看门狗 IWDG 和 RTC。它的典型频率是 40kHz但数据手册给出的范围很宽30kHz 到 60kHz 都算合格芯片出厂不做精确微调。这个精度对 IWDG 来说无所谓狗只要大概时间正确就行对 RTC 来说直接决定日期走时快慢。很多人第一次用 LSI 驱动 RTC 后发现时间一天慢掉几十分钟第一反应是代码写错了实际上先要接受一个事实LSI 没校准过按 40kHz 配预分频只是估算。2.2 最小系统板上缺失的 LSE 晶振与 LSI 的取舍LSE 是 32.768kHz 外部晶振选这个频率是因为 2 的 15 次方用 15 位分频就能得到 1Hz硬件设计最省事。但最小系统板为了节约成本和布局空间普遍不焊这颗晶振PC14 和 PC15 引脚甚至被直接拉出来做普通 GPIO。如果要把 RTC 跑起来要么自己补焊一颗 32.768kHz 晶振和两个负载电容要么就接受 LSI 方案。取舍的实质是LSE 精度高、温漂小、掉电后在 VBAT 供电下还能继续走LSI 零成本、不需要任何外围元件但精度差且断电后不能依赖它维持日期。如果产品对日期精度要求不高只是记录运行时长、日志时间戳或者每天通过 4G、Wi-Fi 校时一次LSI 完全没有问题。如果要求“断电一年再上电日期仍然准确”LSI 从一开始就不该进入选型范围。2.3 LSI 与 LSE 关键参数对比下表是 F103 数据手册给出的典型参数实际数值以当前批次数据手册为准。参数LSI 内部低速时钟LSE 外部低速晶振典型频率40kHz32.768kHz频率范围30kHz 到 60kHz通常 32.7665kHz 到 32.7695kHz是否需要外围元件不需要晶振加两个负载电容初始精度未校准偏差可达百分之几常见 20ppm 左右温漂明显全温范围变化大晶体本身较小约 10ppm 到 50ppm掉电保持VDD 掉电后停振VBAT 供电时继续振荡典型用途IWDG、粗计时、秒级日志精密 RTC、低功耗日历从表里能直接看出LSI 不是一个“高精度时钟”而是一个“能用的内部时钟源”。用它做日期功能必须先测量再校准。2.4 精度指标直接换算成日误差选 LSI 之前先用一个简单计算判断误差是否可接受。假设预分频按 40000 配置实际 LSI 频率是 39.5kHzfloat actual_lsi 39500.0f; // 实测 LSI 频率 float prescaler 40000.0f; // 当前分频值 float rtc_count_per_real_day 86400.0f * (actual_lsi / prescaler); float error_seconds rtc_count_per_real_day - 86400.0f; printf(日误差 %.1f 秒\n, error_seconds);这段代码的逻辑是真实一天有 86400 秒RTC 分频器每秒需要prescaler个 LSI 脉冲。实际一天内 LSI 产生actual_lsi * 86400个脉冲除以分频值就是 RTC 在这一天内记录的秒数。39.5kHz 时会得出大约 -1080 秒也就是一天慢 18 分钟。实际芯片不会固定在 39.5kHz温度一变频率还会漂。所以真正做产品时必须采用第 4 章的实测校准流程否则日期功能只能算“会走”不能算“能查”。3. 基于内部低速时钟的 RTC 初始化与日期写入3.1 启动 LSI 并配置 RTC 时钟源的最小驱动用 HAL 库初始化的代码如下核心步骤是打开备份域写保护、启动 LSI、等待 LSI 就绪、选择 RTC 时钟源、使能 RTC。RTC_HandleTypeDef hrtc {0}; static void MX_RTC_Init(void) { __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 解锁备份域写保护 __HAL_RCC_LSI_ENABLE(); // 打开内部低速时钟 while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) RESET) {} __HAL_RCC_BACKUPRESET_FORCE(); // 复位备份域清掉旧的 RTCSEL __HAL_RCC_BACKUPRESET_RELEASE(); __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSI); __HAL_RCC_RTC_ENABLE(); hrtc.Instance RTC; hrtc.Init.AsynchPrediv 39999; // 分频系数 40000 hrtc.Init.OutPut RTC_OUTPUT_DISABLE; HAL_RTC_Init(hrtc); }三个地方需要解释。第一备份域复位会同时清掉 BKP 寄存器和 LSE 相关配置如果之前存过掉电标志必须先读出来保存。第二RCC_RTCCLKSOURCE_LSI表示把 RTC 时钟接到 LSI 上这是整个方案的关键。第三F103 的 RTC 预分频寄存器只有 20 位HAL_RTC_Init里的AsynchPrediv就是这 20 位的分频值最大支持到 1048575。等待 LSI 就绪的循环没有超时保护实际工程里建议加一个计数上限防止 LSI 失效时程序卡死。3.2 20 位预分频怎么凑出 1HzRTC 预分频的目的是把 LSI 频率除以某个整数得到 1Hz 的秒脉冲。公式很简单预分频系数 LSI实际频率 / 1Hz 预分频寄存器值 预分频系数 - 1按典型 40kHz 计算预分频系数是 40000寄存器写 39999。如果实测 LSI 是 40237Hz寄存器就写 40236。最低位减一是硬件约定的写 39999 意味着计数器从 39999 数到 0一共 40000 个脉冲恰好 1 秒。正因为 LSI 频率范围宽预分频值不能靠猜。先按 40000 跑起来再用第 4 章的方法实测频率修正这是 LSI 方案和 LSE 方案最大的区别。LSE 几乎没有这种问题因为 32.768kHz 晶振的精度是 ppm 级别的。3.3 日期秒数与年月日互转RTC 计数器是 Unix 时间戳风格从 1970 年 1 月 1 日 00:00:00 开始累计秒数。F103 硬件不会换算日期所有年月日换算必须在软件层完成。下面这组函数用循环累加的方式处理闰年逻辑直观对 Cortex-M3 来说开销也可接受。static const uint8_t month_days[] {31,28,31,30,31,30,31,31,30,31,30,31}; static uint8_t is_leap(uint16_t y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } uint32_t date_to_epoch(uint16_t y, uint8_t m, uint8_t d, uint8_t hh, uint8_t mm, uint8_t ss) { uint32_t days 0; uint16_t yy; uint8_t i; for (yy 1970; yy y; yy) { days is_leap(yy) ? 366 : 365; } for (i 1; i m; i) { days month_days[i - 1]; if (i 2 is_leap(y)) days; } days d - 1; return days * 86400UL hh * 3600UL mm * 60UL ss; }反向转换用逐日扣除的方式从 1970 年开始扣掉每个整年的天数再扣每个月的天数void epoch_to_date(uint32_t ts, uint16_t *y, uint8_t *m, uint8_t *d, uint8_t *hh, uint8_t *mm, uint8_t *ss) { uint32_t days ts / 86400UL; uint16_t year 1970; *hh (ts % 86400UL) / 3600UL; *mm (ts % 3600UL) / 60UL; *ss ts % 60UL; while (days (is_leap(year) ? 366 : 365)) { days - is_leap(year) ? 366 : 365; year; } *y year; *m 1; while (1) { uint8_t md month_days[*m - 1]; if (*m 2 is_leap(year)) md 29; if (days md) break; days - md; (*m); } *d days 1; }date_to_epoch把年月日时分秒合成一个 32 位秒数epoch_to_date反向拆回。循环次数最多一百多次在 72MHz 主频下占用时间可以忽略。用以下已知时间戳验证转换函数能快速定位闰年和进位错误时间戳期望日期时间01970-01-01 00:00:009466848002000-01-01 00:00:0017040672002024-01-01 00:00:0021474836472038-01-19 03:14:073.4 把时间写入 RTC 计数器并正确读回得到秒数后写入 RTC 的 32 位计数器。F103 的计数器被拆成CNTH和CNTL两个 16 位寄存器写时必须先写高 16 位再写低 16 位并且在配置模式下操作uint32_t ts date_to_epoch(2025, 7, 16, 9, 30, 0); while ((RTC-CRL RTC_CRL_RTOFF) 0) {} // 等上次操作结束 RTC-CRL | RTC_CRL_CNF; // 进入配置模式 RTC-CNTH ts 16; RTC-CNTL ts 0xFFFF; RTC-CRL ~RTC_CRL_CNF; // 退出配置模式 while ((RTC-CRL RTC_CRL_RTOFF) 0) {} // 等写操作完成先置CNF位再写退出配置模式后硬件会把数据真正锁存到计数器。读回时顺序相反要先读CNTL再读CNTH否则刚好在低 16 位回绕时可能读到高低位不一致的值。稳妥做法是读两次CNTLuint32_t rtc_read_counter(void) { uint32_t lo, hi, lo2; lo RTC-CNTL; hi RTC-CNTH; lo2 RTC-CNTL; if (lo2 lo) hi; // 低16位发生了回绕高位加1 return (hi 16) | lo2; }这段代码在计数器从 0x0000FFFF 翻到 0x00010000 的瞬间也能给出正确值比盲目连续读三次更可靠。4. 用 MCO 实测 LSI 频率和日计时校准4.1 把 LSI 引到 MCO 引脚测量频率LSI 的标称频率不能直接当作真实频率用最可靠的办法是把它引到 MCO 引脚用频率计或示波器实测。F103 的 MCO 输出在 PA8通过RCC_CFGR寄存器的 MCO 位选择时钟源。MCO 位是[6:4]置为0b101表示输出 LSIGPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin GPIO_PIN_8; gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gpio); RCC-CFGR ~RCC_CFGR_MCO; RCC-CFGR | (0x05 4); // MCO[2:0] 101输出 LSIPA8 同时也是 TIM1_CH1 的引脚配置成 MCO 后由时钟控制器直接驱动和定时器没有冲突。测量时注意探针电容LSI 是内部弱 RC 振荡器长跳线或高容性探头会把频率拉低几十一百赫兹。建议用 10x 探头测量时间至少累计 10 秒取平均避免单次周期抖动影响读数。实测得到 40237Hz 或 39680Hz 都是正常现象没有所谓“芯片坏了”的判断标准除非读数跑到 30kHz 以下或 60kHz 以上。4.2 根据实测频率重写预分频寄存器拿到实测频率后把预分频值改成实测频率 - 1。RTC 不需要关闭直接进配置模式写PRLH和PRLLuint32_t f_lsi 40237; // 频率计实测值 uint32_t prl_new f_lsi - 1; while ((RTC-CRL RTC_CRL_RTOFF) 0) {} RTC-CRL | RTC_CRL_CNF; // 进入配置模式 RTC-PRLH prl_new 16; RTC-PRLL prl_new 0xFFFF; RTC-CRL ~RTC_CRL_CNF; // 退出配置模式 while ((RTC-CRL RTC_CRL_RTOFF) 0) {}写入PRL后RTC 的秒脉冲从下一秒开始按新频率生成计数器不会重新归零。这意味着可以在系统运行中随时修正走时不需要重新初始化 RTC也不需要重新设置日期。如果换了一块芯片或者环境温度变化超过 20℃LSI 频率可能又变了。每次上电执行一次几分钟的频率测量再写入是保证日期精度的基本操作。4.3 没有频率计时用日误差反推校准手头没有频率计也可以用日误差反算。先设置一个已知准确的参考时间运行 24 小时记录 RTC 与参考时间的偏差drift_sec。这里的drift_sec定义为“RTC 显示秒数减去参考秒数”正数表示走得快负数表示走得慢。新的预分频值按下面的公式计算float drift -1080.0f; // 实测日误差单位秒 uint32_t old_prl 39999; uint32_t new_prl; new_prl (uint32_t)((float)(old_prl 1) * (86400.0f drift) / 86400.0f) - 1;以drift -1080为例说明 RTC 每天少走了 1080 秒属于走得慢修正后new_prl远小于 39999秒脉冲变快把误差补回来。校准方向可以用下面这张表记忆日误差现象预分频调整方向drift 大于 0日期走得快增大 PRLdrift 小于 0日期走得慢减小 PRLdrift至少要累计观察 24 小时才有参考价值只测 1 小时得到的秒级误差会被启动时间、人工读数时间放大。校准后再次观察 24 小时确认误差落在可接受范围内这段操作就完成了。写入 PRL 的瞬间不要选在整点附近避免日志里出现时间回退或跳变的疑问。5. 掉电与待机场景下验证日期走时的边界5.1 VBAT 域与 LSI 的供电关系很多人会问F103 有 VBAT 引脚接上纽扣电池后掉电时 RTC 是不是还能继续走这个问题的答案取决于 RTC 时钟源。备份域内包含 RTC 寄存器、BKP 寄存器和 LSE 振荡器但 LSI 不在这个供电域里。也就是说完全切断 VDD、只靠 VBAT 供电时LSE 还能振荡LSI 会停。所以基于 LSI 的日期功能只能保证“MCU 在主电源存在的情况下进入停机或待机模式时继续计时”不能保证“系统彻底断电几个月后日期还正确”。如果产品有这个需求必须换 LSE 晶振或者在外部加一颗带电池的实时时钟芯片。5.2 用待机唤醒验证 LSI 日期是否真的在走验证 LSI 在低功耗模式下是否维持计时最直接的办法是进入待机模式让 RTC 闹钟在 5 秒后唤醒系统唤醒后对比日期时间。static void enter_standby_with_rtc(void) { uint32_t now rtc_read_counter(); uint32_t alarm now 5; while ((RTC-CRL RTC_CRL_RTOFF) 0) {} RTC-CRL | RTC_CRL_CNF; RTC-ALRH alarm 16; RTC-ALRL alarm 0xFFFF; RTC-CRL ~RTC_CRL_CNF; while ((RTC-CRL RTC_CRL_RTOFF) 0) {} RTC-CRL ~RTC_CRL_ALRF; // 清闹钟标志 RTC-CRH | RTC_CRH_ALRIE; // 开闹钟中断 EXTI-RTSR | EXTI_RTSR_TR17; // 闹钟映射到 EXTI17 EXTI-IMR | EXTI_IMR_MR17; NVIC_EnableIRQ(RTC_IRQn); HAL_PWR_EnterSTANDBYMode(); // 进入待机等待闹钟唤醒 }醒来后调用rtc_read_counter与唤醒前的now 5比较误差在一个秒计数以内说明 LSI 在待机期间保持了计时。如果唤醒后计数器没变化先检查PWR时钟是否使能、闹钟中断是否使能再看 PA8 上是否意外接了干扰源。5.3 校准前后的验收记录做验收时不要只记录“准不准”要把测量条件一起记下来。下面是一份可行的记录格式测试项校准前校准后LSI 实测频率未测量按 40kHz40237Hz预分频寄存器值3999940236室温25℃25℃待机 18 小时误差-810 秒1.2 秒折算日误差-1080 秒/天1.6 秒/天校准后常温日误差能压到几秒以内这是 LSI 方案的合理水平再往下压就不现实了。如果应用场景温度变化范围很大最好在不同温度点分别记录 LSI 频率做成查表校准。最后再强调一句验收记录里务必写明测量仪器、室温、MCO 探针型号和读取时间否则换一块板子重测时你会发现这些条件直接影响 LSI 的读数。本文还有配套的精品资源点击获取