STM32F103 RTC实战:从备份域到OLED与DS18B20的完整实现
2026/9/13 9:08:12 网站建设 项目流程

简介:这是一份基于STM32F103的RTC实时时钟综合应用工程,面向嵌入式初学者与物联网开发者,演示如何利用内置RTC模块保持精准时间,并通过I2C总线驱动OLED屏显示日期、时间与温度,配合按键完成调时等人机交互。工程采用Keil MDK开发,共93个文件、压缩后仅475KB,其中包含44个.h头文件与42个.c源文件,覆盖RTC、OLED、按键、DS18B20温度采集、IIC通信、延时、串口等核心模块,另有启动文件与应用配置文件,目录结构清晰,适合对照学习底层驱动与中断处理思路。已有377人关注学习。整个项目包含完整的初始化、时间读取与设置流程,以及OLED显示、按键扫描与温度采集示例,可直接烧录运行并二次开发,是理解STM32片上外设组合应用的实用参考资料。

1. 一块电池撑起的时间:从 RTC 掉电保持说起

做嵌入式设备的人多半遇到过这种尴尬:设备断电重启后,时间回到 2000 年 1 月 1 日。如果项目只是做个时钟显示倒无所谓,但一旦涉及数据记录、定时上报、日志时间戳,时间错乱直接导致业务数据作废。STM32F103 内置的 RTC(Real-Time Clock)模块就是为这个场景设计的——它挂在备份域供电回路上,主电源断开后由 VBAT 引脚上的纽扣电池继续供电,秒计数器照常走时。这份工程是基于标准外设库(StdPeriph_Lib V3.5)实现的完整 RTC 应用,包含 RTC 初始化与时间设置、I2C 接口 OLED 显示、独立按键调时、DS18B20 温度采集四部分。适合两类读者:一类是把 RTC 当“黑盒”调用、想搞明白备份域寄存器和 BKP 寄存器关系的开发者;另一类是想在一个工程里同时理顺 OLED 驱动、按键消抖和单总线传感器读取的入门者。下文所有代码基于 STM32F103C8T6,主频 72MHz,标准外设库 V3.5,Keil MDK5 工程可直接编译下载。

2. 从寄存器到代码:RTC 模块的配置链路与 BKP 域操作

2.1 为什么 RTC 寄存器要挂在备份域上

STM32F103 的 RTC 由两部分组成:一个 32 位可编程预分频器和一个 32 位向上计数寄存器(RTC_CNT)。计数器的时钟源来自 RTC 时钟域,这个时钟域由 LSE(32.768kHz 外部低速晶振)、LSI(40kHz 内部低速 RC 振荡器)或 HSE 分频后的时钟驱动。关键在于,RTC 的供电域是 VBAT 和 VDD 的“或”关系——主电源掉电时,VBAT 引脚上的电池自动接管。因此 RTC_CNT、RTC_PRL、RTC_CNT 等寄存器都位于备份域内,访问前必须先使能 PWR 和 BKP 外设时钟,并操作 PWR_CR 寄存器的 DBP 位打开备份域写保护。

这个设计带来的实际影响有两个。第一,RTC 的初始化代码必须放在系统上电后最先执行的位置,因为只要 VBAT 有电,RTC 就在走时,你读到的 RTC_CNT 是持续累加的秒数。第二,RTC 一旦配置过,再次上电时不能重复执行初始化序列,否则会触发 RTC 配置标志位(RCC_CSR 的 RTCEN 位和 RTC_CRL 的 RTCEN 位)的保护机制,导致写操作被忽略。工程里通过检查 BKP_ReadBackupRegister 的备份寄存器值来决定是否跳过初始化,这是官方推荐做法,也是判断 RTC 是否“跑过”的依据。

2.2 时钟选型:LSE 还是 LSI

代码中 RTC 时钟源选择 LSE 外部低速晶振。选 LSE 而不是 LSI,原因在于精度:LSI 的频率在 30kHz 到 50kHz 之间漂移,具体值因芯片个体和温度而异,用来做日历时钟一天能差出几十秒;LSE 晶振的精度通常在 ±20ppm 以内,换算下来一天误差不到 2 秒。代价是 LSE 起振慢,上电后需要等待 LSE_RDY 标志置位,极端情况下要等 1 到 2 秒。如果你的板子上没焊 32.768kHz 晶振,代码会卡死在等待循环里,这时可以改用 LSI,但要在校准上做补偿——这属于后文排错章节的讨论范围。

RTC 预分频器的配置是一个数学问题。RTC 时钟源频率为 32.768kHz,要得到 1Hz 的秒脉冲,需要设置 RTC_PRL = 32767,即 32768 分频。如果使用 LSI(约 40kHz),分频值应设为 39999,并在校准寄存器中做微调。代码中的 RTC_SetPrescaler(32767) 就是写预分频寄存器,这个值决定了秒计数器 RTC_CNT 的递增频率,不要随意改动,否则时间跑快或跑慢。

2.3 初始化与时间设置的可复现代码

下面这段是工程中 RTC 初始化的核心函数,已按标准外设库风格整理。注意这里用的是库函数封装,如果直接操作寄存器,本质是相同的——先打开备份域写保护,再操作 RTC 寄存器。

void RTC_Init(void) { // 使能 PWR 和 BKP 外设时钟,访问备份域寄存器的前提 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 打开备份域写保护:PWR_CR 的 DBP 位置 1 PWR_BackupAccessCmd(ENABLE); // 检查备份寄存器标志,判断是否首次配置 if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) { // 使能 LSE 外部低速晶振,等待其稳定 RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 选择 LSE 作为 RTC 时钟源,使能 RTC 时钟 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); // 等待 RTC 寄存器同步和写入完成 RTC_WaitForSynchro(); RTC_WaitForLastTask(); // 使能 RTC 秒中断,用于主循环轮询或唤醒 RTC_ITConfig(RTC_IT_SEC, ENABLE); RTC_WaitForLastTask(); // 配置预分频:32768 / 32767 -> 1Hz 秒计数 RTC_SetPrescaler(32767); RTC_WaitForLastTask(); // 设置初始时间:2024 年 1 月 1 日 00:00:00 // 换算为 UNIX 时间戳(UTC+8 时区需自行调整) RTC_SetCounter(1704067200); RTC_WaitForLastTask(); // 写入备份寄存器标志,下次上电跳过初始化 BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } else { // 非首次上电,等待 RTC 与 APB1 总线同步后直接使用 RTC_WaitForSynchro(); } }

逻辑说明:这段代码的关键在 if/else 分支。首次上电时写入备份寄存器标志,此后每次复位或重新上电,RTC 配置被跳过,只等待同步——因为 RTC 时钟域和 APB1 总线时钟域不同步,直接读 RTC_CNT 可能拿到中间值,RTC_WaitForSynchro 就是等 RTC 寄存器内容映射到 APB1 接口。值得注意的参数是 RTC_SetCounter 的入参,它是一个 UNIX 时间戳(自 1970 年 1 月 1 日以来的秒数)。工程中通常用时间戳而不是年月日时分秒的数组,因为时间戳可以直接参与加减运算,省去日期转换的麻烦。

2.4 读时间与 UTC 转换

RTC 读时间同样需要注意同步问题。直接读 RTC_CNT 寄存器时,由于 RTC 时钟与 APB1 时钟异步,读到的值可能比实际值大或小(在边界翻转时)。标准做法是先等同步标志,再读取。

uint32_t RTC_GetTimeStamp(void) { // 等待 RTC 寄存器同步完成,防止读到跳变中的值 RTC_WaitForSynchro(); // 读取当前秒计数值 return RTC_GetCounter(); } void RTC_TimeToDateTime(uint32_t timestamp, RTC_DateTime_t *dt) { // 基于格里高利历的日期换算算法 // 使用时间戳换算标准公式,注意 1900 年起始的偏移校准 // 此处省略具体换算实现,可参考 C 标准库 gmtime 逻辑 }

一个实用建议:UTC 与本地时间的偏移不要在 RTC 层处理。RTC 始终存储 UTC 时间戳,显示层做时区偏移。否则夏令时切换或跨时区调试时,你会被时间错乱搞到头大。日期换算的开销极低,一个除法取模的事情,现代 MCU 上微秒级完成,不需要用查表法优化。

3. OLED 显示链路:I2C 时序与 0.96 寸屏的驱动细节

3.1 I2C 地址与起始时序

工程中的 OLED 模块型号为 0.96 寸 128x64,SSD1306 控制器,通过 I2C 接口连接。它的 I2C 地址由 SA0 引脚的电平决定:SA0 接地时地址为 0x78(7 位地址 0x3C),接 VCC 时为 0x7A(7 位地址 0x3D)。代码里使用软件模拟 I2C——用两个 GPIO 引脚模拟 SCL 和 SDA 时序。选软件模拟而不是硬件 I2C 的理由很现实:STM32F103 的硬件 I2C 在总线异常后容易锁死,需要繁琐的恢复时序,而软件模拟只需要把引脚配成开漏输出,加上拉电阻,就能很好避免这类问题。

软件模拟 I2C 的起始条件是:SCL 高电平时,SDA 产生一个高到低的跳变。停止条件相反:SCL 高电平时,SDA 从低到高。数据位在 SCL 高电平期间必须保持稳定,在 SCL 低电平期间变化。这个时序用 GPIO 的 set/reset 操作直接实现,延时通常设为 5us 左右(对应 100kHz~400kHz 的 I2C 速率)。

void OLED_WriteByte(uint8_t dat, uint8_t cmd) { uint8_t i; // I2C 起始信号:SCL=1 时 SDA 拉低 I2C_Start(); // 发送设备地址:0x78(SA0=0 写方向) I2C_SendByte(OLED_ADDR_WRITE); I2C_WaitAck(); // 控制字节:0x00 表示后跟命令,0x40 表示后跟数据 if (cmd == OLED_CMD) I2C_SendByte(0x00); else I2C_SendByte(0x40); I2C_WaitAck(); // 写入数据或命令字节 I2C_SendByte(dat); I2C_WaitAck(); // 停止信号 I2C_Stop(); }

代码说明:控制字节是 SSD1306 在 I2C 模式下的特有机制,0x00 表示后续字节是命令,0x40 表示后续字节是显示数据(GRAM 内容)。每次 I2C 事务可以连续发送多个命令字节(先发 0x00 控制字节,再跟一串命令),但工程中为了代码简洁,每次只发一个字节。逻辑说明:OLED 的 GRAM 有 128x64 位,SSD1306 将显示内存分 8 页(Page0~Page7),每页 128 字节,对应屏幕上的 8 行像素块。写显示数据时,需要先设置页地址和列地址,再连续写入数据字节。

3.2 显存刷新策略与汉字字模存储

OLED 驱动方式有两种:直接写和显存缓冲。工程里采用的是直接写,即每次更新内容时,直接操作 SSD1306 的 GRAM。这种方式省 RAM但有个问题:频繁局部刷新时,屏幕会出现闪烁。 我一般建议在资源允许时 加一个显存缓冲区,先修改缓冲区,再整体写入。128x64 的显存只需 1KB RAM,对 F103 的内存压力不大。

字模的存储方式直接决定了编码的复杂度和字模的读取方式。工程中使用的是标准的16x16汉字字模——每个汉字占32字节,按行排列。字模数据的排列顺序与SSD1306的页扫描方式(每页8像素,逐列扫描)是对应的。取模方式不同,字模的数据顺序就不同,直接用错的字模数据,汉字会出现上下或左右颠倒。

// OLED 显示字符串(支持可见 ASCII 和 16x16 汉字混排) void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size) { uint8_t ch; // 遍历字符串 while (*str != '\0') { ch = *str++; // ASCII 可视字符范围 0x20~0x7E if (ch >= 0x20 && ch <= 0x7E) { OLED_ShowChar(x, y, ch, size); x += size / 2; // ASCII 字符宽度为汉字的一半 if (x > 128 - size / 2) // 到达右边界,换行 { x = 0; y += size; } } else { // 中文字符:UTF-8 编码下占 3 字节,需合并后查表 // 取前两字节 + 第三字节组合成 GB2312 区位码 // 实际工程中通过字模取模软件生成数组,按内码索引查表 OLED_ShowChinese(x, y, ch, size); x += size; if (x > 128 - size) { x = 0; y += size; } } } }

注意一个工程细节:标准外设库版本的 OLED 驱动中,汉字编码通常采用 GB2312 或 GBK,而编辑器的源文件编码如果是 UTF-8,中文字符串的存储方式与显示函数的解析方式不一致,导致显示乱码。 常见做法是显示函数根据编码方式,将 UTF-8 三字节合并为 GBK 内码去查表。 或者使用专门的取模软件,把需要的汉字预先取出,定义成数组变量,直接通过下标访问,避开编码转换问题。 我的建议是:在屏幕上要显示的内容如果固定,就预置字模数组;如果是动态内容,就用显示函数。这样取模工具生成一个 .h 文件包含所有需要的汉字字模数组,代码做好索引即可。

3.3 OLED 初始化序列

SSD1306 上电后默认进入睡眠模式,需要发送初始化命令才能点亮。初始化序列是固定的,参照数据手册即可。下面给出与工程一致的序列:

void OLED_Init(void) { // 延时等待 SSD1306 内部复位完成 delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 页寻址模式:便于按页写入 OLED_WriteCmd(0xB0); // 设置页起始地址 Page0 OLED_WriteCmd(0xC8); // 扫描方向:从上到下(COM0~COM63) OLED_WriteCmd(0x00); // 设置低列起始地址 OLED_WriteCmd(0x10); // 设置高列起始地址 OLED_WriteCmd(0x40); // 设置显示起始行,0x40 为行 0 OLED_WriteCmd(0x81); // 设置对比度 OLED_WriteCmd(0x7F); // 对比度值(1~256 可调,越大越亮) OLED_WriteCmd(0xA1); // 段重映射:Segin0->SEG0,镜像设置 OLED_WriteCmd(0xA6); // 正常显示(非反色) OLED_WriteCmd(0xA8); // 设置复用比 OLED_WriteCmd(0x3F); // 1/64 duty(128x64 屏) OLED_WriteCmd(0xA4); // 恢复 RAM 内容显示 OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); // 无偏移 OLED_WriteCmd(0xD5); // 设置时钟分频因子 OLED_WriteCmd(0x80); // 分频因子 F=1,振荡器频率 8:1 OLED_WriteCmd(0xD9); // 设置预充电周期 OLED_WriteCmd(0x22); // 相位 1=2,相位 2=2 OLED_WriteCmd(0xDA); // 设置引脚硬件配置 OLED_WriteCmd(0x12); // 使用顺序 COM 配置,无左右偏移 OLED_WriteCmd(0xDB); // 设置 VCOMH 电平 OLED_WriteCmd(0x20); // 0.77xVCC OLED_WriteCmd(0x8D); // 设置电荷泵 OLED_WriteCmd(0x14); // 使能电荷泵(关键:不使能则屏幕不亮) OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); // 清屏 }

重要参数说明:0x8D 和 0x14 这一对命令经常被遗漏,如果没有使能电荷泵,OLED 上电后会保持黑屏但 I2C 通信正常——很多初学者卡在这一步,以为屏幕坏了或驱动有 bug。对比度寄存器 0x81 的值决定屏幕亮度,工程中设为 0x7F,若要降低功耗可以减到 0x3F。

4. 按键交互:中断读取与消抖处理的工程取舍

4.1 GPIO 外部中断配置

工程中有按键用于调整时间和切换显示。按键读取方式有原生查询和外部中断两种,工程采用 EXTI 外部中断。选用中断而不是查询的原因:RTC 每秒产生一次秒中断,加上 OLED 刷新、DS18B20 读取,主循环可能被占满,轮询按键会漏掉短按或长按。EXTI 方式只在按键动作发生时唤醒 CPU处理上下文,不占用主循环时间。

void EXTI_Key_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 使能 GPIOA 和 AFIO 时钟(外部中断需要复用功能时钟) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 配置 PA0 为下拉输入模式,按键另一端接 VCC 或 GND GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 将 PA0 映射到 EXTI0 线 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // 配置 EXTI0:下降沿触发(按键按下时 PA0 由高变低) EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); // 配置 NVIC:EXTI0 中断优先级设为 2(数值越小优先级越高) NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }

关键参数:EXTI_Trigger_Falling 对应按键按下瞬间的电平跳变方向。如果按键接在 VCC 与 GPIO 之间,按下时 GPIO 被拉低,就配下降沿;按键接在 GND 与 GPIO 之间,按下时 GPIO 被拉高,就配上升沿。工程中采用上拉输入加接地按键,所以用下降沿。NVIC 优先级分组需要在 main 函数开头调用 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2) 配置,否则中断优先级按默认分组工作,可能与别的中断冲突。

4.2 消抖与长短按识别

按键按下时,机械触点会产生抖动,持续时间约 5~20ms。在中断服务函数里直接做状态判断,抖动可能造成一次按下触发多次中断。常见的处理办法有硬件上并联 100nF 电容、软件上的延时消抖或计数消抖。

这里的环境资源少,使用延时的消抖方式简单有效,但会阻塞 CPU。 一个改进方法是利用 RTC 秒中断作为时间基准,在秒中断里递减消抖计数。

volatile uint8_t key_debounce_cnt = 0; volatile uint8_t key_press_event = 0; volatile uint16_t key_hold_time = 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // 消抖:若计数达到阈值则确认为有效按下 if (key_debounce_cnt >= 2) { key_press_event = 1; // 标记有效按键事件 key_hold_time = 0; // 清零长按计时 } else { key_debounce_cnt++; } EXTI_ClearITPendingBit(EXTI_Line0); } } // 在 1ms 定时器中断或 RTC 秒中断中调用 void Key_Scan(void) { if (key_press_event) { // 处理短按事件:切换 OLED 显示页面 / 进入调时模式 key_press_event = 0; } // 长按检测:每 1ms 计一次,超过 2000ms 判定为长按 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == RESET) { key_hold_time++; if (key_hold_time > 2000) { // 长按进入快速调时模式 key_hold_time = 0; } } }

逻辑说明:消抖计数法的思路是——按键抖动产生的边缘无法稳定地跨越计数阈值,只有真正的按下能保持电平稳定,连续性触发中断。 如果按键中断一拍完就松开,计数器被清掉,不会误报。这里的定时器中断和按键中断配合,避免了在中断服务函数中做长时间等待,属于常用做法。

4.3 调时状态机

调时逻辑处理时,用状态机管理当前的时间修改单元(秒、分、时、日、月、年)。短按切换状态,长按退出调时。以下列出状态定义:

typedef enum { TIME_SET_NONE = 0, // 正常显示模式 TIME_SET_HOUR, // 调小时 TIME_SET_MIN, // 调分钟 TIME_SET_SEC, // 调秒(归零用) TIME_SET_YEAR, // 调年份 TIME_SET_MONTH, // 调月份 TIME_SET_DAY // 调日期 } TimeSetState_t; TimeSetState_t time_set_state = TIME_SET_NONE;

注意边界判断:月的变化会改变当月最大天数,年份的闰年也要处理。 这部分逻辑能直接复用日期换算算法的逆过程。 常见做法是读取当前显示的时间戳,将其转成分量的日期结构,在结构体上做字段增减,再调 RTC_SetCounter 写回。 数据结构层面你只需要维护一个 uint32_t 时间戳,一切日期计算都在显示层处理,否则会出现改万年历还要维护一个 y/m/d 数组的混乱局面。

5. 单总线 DS18B20:温度采集与 RTC 数据同屏展示

5.1 单总线时序与 ROM 校验

DS18B20 的温度数据用单总线协议(1-Wire)传输,一根数据线完成读写双向通信。 时序分为初始化、写 0/1、读 0/1 四种。 与 I2C 不同,单总线的时序时间要求是微秒级的,对延时函数的精度非常敏感。 工程中使用的 GPIO 模拟时序,核心延时由空循环实现。

关键概念:DS18B20 的 64 位 ROM 码在出厂时激光写入,包含 8 位家族码、48 位序列号和 8 位 CRC 校验。 总线上只要接了一个传感器,可以跳过 ROM 匹配(0xCC 命令)直接转换温度;多个传感器时需要用 0x55 命令匹配对应的 ROM 码。 工程里直接采用跳过 ROM 的方式,适合单点测温场景。 实际部署时若总线上挂了多片 DS18B20,每条总线上不同地址的器件,需要扫描并保存其 ROM 码,代码复杂度会高一个量级,选型时可以改用一个带唯一 ID 的 TMP117 等 I2C 温度传感器。

5.2 温度转换与读取姿势

float DS18B20_GetTemperate(void) { uint8_t low, high; int16_t temp_raw; float temp; // 复位单总线,检测设备是否存在 if (DS18B20_Reset() == 0) return -999.0f; // 设备不存在 // 跳过 ROM 匹配,直接向所有设备发送温度转换命令 DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); // 等待转换完成:12 位精度下最长 750ms delay_ms(750); // 再次复位,发送读暂存器命令 if (DS18B20_Reset() == 0) return -999.0f; DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读取暂存器中 9 字节数据 low = DS18B20_ReadByte(); // 温度低字节 high = DS18B20_ReadByte(); // 温度高字节 // 组合成 16 位有符号原始值,右移 4 位去掉小数位 temp_raw = (high << 8) | low; temp = temp_raw * 0.0625f; // 12 位分辨率下每位代表 0.0625°C return temp; }

延时 750ms 是 DS18B20 在 12 位分辨率下的最大转换时间——必须等待转换完成,否则读回的数据是上次转换的旧值。应用层需要权衡:如果系统要求每秒刷新一次温度,750ms 的等待会占用大量时间。工程优化方案是——主循环先发起温度转换命令,然后去做 OLED 刷新和按键扫描,最后回来读温度,整个过程被拆成异步流程。若工程中直接使用阻塞延时,读出的数据不正确时,先检查延时是否足够,再检查时序是否被中断打断。

一个体感知识:DS18B20 的精度是 ±0.5°C,换算出的温度值不是准确的物理温度,要校准的话,在冰水混合物里测量一次,校正偏移量参数即可。这与 RTC 的校准字节类似,都是对传感器本身偏差的补偿。

6. 掉电与校准:RTC 在真实产品中的三个坑

这一章把实践中常遇到的三个问题展开。都是不看原理很难定位的坑,说结论、给解法。

坑一:RTC 时间掉了,但 VBAT 电压正常

这个问题排查时,要区分“初始化代码有 bug”还是“备份域掉电写失败”。 RTC 初始化代码里 if 判断读备份寄存器,如果 BKP_DR1 读回的值一直是 0,说明备份域已经掉电——可能原因不是 VBAT 没接好,而是复位时 RTC 写操作还没完成 就把备份域写保护关掉了。 解决办法是加一个掉电检测电容(VBAT 引脚并联 1uF 到 GND)保持几毫秒,确保 RTC 写操作完成后再切备用电源。

坑二:LSE 晶振不起振

LSE 晶振对负载电容要求是 6~12.5pF 之间,很多低成本 PCB 上直接省掉了匹配电容,导致起振困难。 定位方式是执行初始化代码后隔一会再读 RCC_FLAG_LSERDY,如果一直是 RESET,排查晶振焊盘和电容布局。 调试时也可临时切换到 LSI,但时间会跑得快,不能用产品发布。

坑三:RTC 秒中断与外部中断的优先级冲突

RTC 秒中断优先级如果高于 EXTI 按键中断,按键响应会出现不确定的延迟;如果反过来,按键处理会打断 RTC 的寄存器同步,读取出现随机跳变。 统一的做法是预取 NVIC_PriorityGroup_2 分组,RTC 中断设 PreemptionPriority=1,按键 EXTI 设 PreemptionPriority=2,这样 RTC 的时间基准不会被 UI 操作打断。

校准方法:利用 RTC 的写保护寄存器不能直接做时间校准,但可以在示波器上测量 RTC 秒中断输出引脚(TAMPER 引脚复用)的频率,对比标准 1Hz 偏差。RTC 校准在 F103 中需要通过 BKP 寄存器配合特定命令序列才能写校准值,这是小众机制,处于产品需求,推荐的做法是每分钟用外部参考时间源(如 GNSS 模块)对时,直接 RTC_SetCounter 校准,比在寄存器层面做校准要简单稳定。

最后给一个调试建议:RTC 秒中断里可以使用示波器直接测电平反转,将 PB5 配置为推挽输出,在秒中断服务函数中翻转 PB5。 这样可以用示波器看到 1Hz 方波,判断走时是否精确。 这一个方法能帮你过滤掉一半的 RTC 问题,值的尝试。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询