Keil代码RTC
155.8.2Keil代码RTC
/*
* 整体目的:把 CubeIDE RTC 示例移植到 Keil 工程,读取由 LSE 驱动的 RTC 日期时间,
* 格式化后经阻塞式 I2C1 OLED 驱动显示日期、英文星期和时分秒。
* 设计思路:rtc.h 暴露唯一句柄和初始化入口;rtc.c 初始化备份域访问与 RTC;主循环
* 先读取日期并选择星期字符串,再读取时间,每隔约 1 秒刷新 OLED。
* 硬件依据:SystemClock_Config() 已启用 PC14/PC15 上的 32.768 kHz LSE 并选择它作为
* RTCCLK;OLED 经 PB6/SCL、PB7/SDA 连接 I2C1,显示工作仍依赖主电源。
* 执行效果:屏幕第 2、4、6 页附近分别显示日期、星期与时间;%2d 使用空格补宽度,
* 并不会像 %02d 那样补零。
* 课程边界:配套 KEIL/9RTC 已注释首次设定日期时间的代码;若备份域此前没有有效值,
* 仅初始化 RTC 不能凭空得到正确日历,而且 F1 HAL 的日期状态仍需软件保存恢复。
* 代码范围:摘录 rtc.h、rtc.c 与 main.c 的实际关键路径,不展开 HAL 库函数内部实现。
*/
/* rtc.h:让其他源文件访问同一 RTC 句柄和初始化函数。 */
#ifndef __RTC_H /* 若该头文件尚未展开,则进入声明区。 */
#define __RTC_H /* 定义课程工程使用的包含保护宏,防止重复声明。 */
#include "main.h" /* 引入 HAL RTC 类型、状态常量及 Error_Handler() 声明。 */
extern RTC_HandleTypeDef hrtc; /* 只声明句柄;真正存储空间由 rtc.c 中的定义提供。 */
void Init_RTC(void); /* 声明 Keil 工程使用的 RTC 初始化入口。 */
#endif /* 结束 __RTC_H 对应的条件编译区域。 */
/* rtc.c:配套 Keil 工程的实际 RTC 初始化代码。 */
RTC_HandleTypeDef hrtc; /* 定义全工程唯一的 RTC HAL 句柄。 */
void Init_RTC(void) /* 初始化 RTC 外设,但当前版本不重新设置日历值。 */
{ /* RTC 初始化函数体开始。 */
RTC_TimeTypeDef sTime = {0}; /* 原设置代码被注释后该局部时间变量实际未被使用。 */
RTC_DateTypeDef DateToUpdate = {0}; /* 原设置代码被注释后该局部日期变量也未被使用。 */
hrtc.Instance = RTC; /* 将句柄绑定到 STM32F1 的硬件 RTC。 */
hrtc.Init.AsynchPrediv = /* 设置 RTC 秒计数所需的预分频方式。 */
RTC_AUTO_1_SECOND; /* 让 HAL 按当前 RTCCLK 自动配置约 1 Hz 秒节拍。 */
hrtc.Init.OutPut = /* 选择 RTC 输出引脚的内部信号来源。 */
RTC_OUTPUTSOURCE_ALARM; /* 保留移植工程配置;本节没有实际设置闹钟业务。 */
if (HAL_RTC_Init(&hrtc) != HAL_OK) /* 初始化 RTC,并判断 HAL 是否成功。 */
{ /* 初始化失败分支开始。 */
Error_Handler(); /* 进入统一错误处理,避免继续读取未就绪 RTC。 */
} /* RTC 初始化结果判断结束。 */
/* 配套工程已将首次设定时间、日期的整段赋值与调用注释掉。
原值为 12:00:00、2023-10-31、星期二,且输入格式为 BCD;
下列两行仅概括最终调用位置,不会参与当前编译,也不会在启动时重设 RTC。 */
/* HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD); */
/* HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BCD); */
} /* RTC 初始化函数结束。 */
void HAL_RTC_MspInit( /* 定义 RTC 初始化期间由 HAL 调用的板级支持钩子。 */
RTC_HandleTypeDef *rtcHandle) /* 接收当前正在初始化的 RTC 句柄。 */
{ /* RTC MSP 初始化函数体开始。 */
if (rtcHandle->Instance == RTC) /* 只在目标确实是 RTC 时配置备份域相关资源。 */
{ /* RTC 硬件支持配置分支开始。 */
HAL_PWR_EnableBkUpAccess(); /* 允许软件写入受保护的备份域寄存器。 */
__HAL_RCC_BKP_CLK_ENABLE(); /* 打开 BKP 接口时钟,使备份寄存器可以访问。 */
__HAL_RCC_RTC_ENABLE(); /* 打开 RTC 外设时钟;LSE 选择已在系统时钟函数完成。 */
} /* RTC 实例判断结束。 */
} /* RTC MSP 初始化结束。 */
/* main.c:while(1) 中的实际显示片段;BIN 格式可直接按普通整数处理。 */
HAL_RTC_GetDate( /* 读取当前日期和星期;F1 HAL 同时维护软件日期状态。 */
&hrtc, /* 指定全工程唯一的 RTC 句柄。 */
&RTC_Date, /* 把结果写入全局日期结构。 */
RTC_FORMAT_BIN); /* 请求普通二进制整数,而不是压缩 BCD。 */
sprintf( /* 将日期格式化到 OLED 共用文本缓冲区。 */
(char *)RTC_Buff, /* 将字节数组作为 C 字符串输出位置。 */
"Date:20%2d-%2d-%2d", /* 20 是应用约定的世纪;%2d 以空格补足两列。 */
RTC_Date.Year, /* 输出年份后两位,例如 23。 */
RTC_Date.Month, /* 输出月份数值。 */
RTC_Date.Date); /* 输出当月日期。 */
OLED_ShowString( /* 把日期文本通过 OLED 驱动写入显示 RAM。 */
0U, /* 从最左侧 x=0 开始。 */
2U, /* 使用驱动定义的第 2 页行起点。 */
RTC_Buff, /* 显示刚生成的日期字符串。 */
16U); /* 使用 16 点字符字模。 */
switch (RTC_Date.WeekDay) /* 按 F1 HAL 的 0~6 星期编号选择英文名称。 */
{ /* 星期分支表开始。 */
case 0: /* 0 对应 Sunday。 */
OLED_ShowString(0U, 4U, "Sunday", 16U); /* 在第 4 页行显示 Sunday。 */
break; /* 结束星期日分支,避免继续执行下一项。 */
case 1: /* 1 对应 Monday。 */
OLED_ShowString(0U, 4U, "Monday", 16U); /* 在同一位置覆盖显示 Monday。 */
break; /* 结束星期一分支。 */
case 2: /* 2 对应 Tuesday。 */
OLED_ShowString(0U, 4U, "Tuesday", 16U); /* 显示 Tuesday。 */
break; /* 结束星期二分支。 */
case 3: /* 3 对应 Wednesday。 */
OLED_ShowString(0U, 4U, "Wednesday", 16U); /* 显示较长的 Wednesday 文本。 */
break; /* 结束星期三分支。 */
case 4: /* 4 对应 Thursday。 */
OLED_ShowString(0U, 4U, "Thursday", 16U); /* 显示 Thursday。 */
break; /* 结束星期四分支。 */
case 5: /* 5 对应 Friday。 */
OLED_ShowString(0U, 4U, "Friday", 16U); /* 显示 Friday。 */
break; /* 结束星期五分支。 */
case 6: /* 6 对应 Saturday。 */
OLED_ShowString(0U, 4U, "Saturday", 16U); /* 显示 Saturday。 */
break; /* 结束星期六分支。 */
default: /* 无效星期编号进入兜底分支。 */
break; /* 不刷新星期区域;正常 HAL 数据不应走到这里。 */
} /* 星期分支表结束。 */
HAL_RTC_GetTime( /* 读取当前 RTC 的时、分、秒。 */
&hrtc, /* 选择同一个 RTC 句柄。 */
&RTC_Time, /* 把结果写入全局时间结构。 */
RTC_FORMAT_BIN); /* 返回可直接十进制格式化的二进制字段。 */
sprintf( /* 用时间文本覆盖 RTC_Buff 中原有日期内容。 */
(char *)RTC_Buff, /* 把字节缓冲区作为字符串输出目标。 */
"Time:%2d:%2d:%2d", /* 保留课程格式;个位数前补空格而不是 0。 */
RTC_Time.Hours, /* 输出小时字段。 */
RTC_Time.Minutes, /* 输出分钟字段。 */
RTC_Time.Seconds); /* 输出秒字段。 */
OLED_ShowString( /* 把时间字符串写到 OLED 下方区域。 */
0U, /* 从屏幕最左侧开始。 */
6U, /* 使用驱动定义的第 6 页行起点。 */
RTC_Buff, /* 显示当前时间文本。 */
16U); /* 使用与日期一致的 16 点字模。 */
HAL_Delay(1000U); /* 约 1 秒后再刷新;未严格对齐 RTC 秒边沿。 */
一、实时时钟
本节把上一节由 CubeIDE 生成的 RTC 配置移植到已有 OLED/I2C 驱动的 Keil 工程。实际数据路径是:RTC 计数器和 HAL 的日期状态 -> RTC_Time、RTC_Date 结构体 -> sprintf 格式化到 RTC_Buff -> OLED_ShowString 将字符字模经 I2C1 发给 OLED。显示正常只能说明这条链路工作;是否能在掉电后保持完整的日期时间,还取决于 LSE、VBAT 及软件保存策略。
1.1 打开工程
- 使用配套的
KEIL/9RTC/MDK-ARM/temp.uvprojx,其基础工程已含 GPIO、UART、软件 I2C 和 OLED 驱动。 - 在 Keil 的工程树中把
Core/Src/rtc.c加入编译组,并确保Core/Inc位于 Include Paths;仅把文件放进磁盘目录而未加入工程,链接阶段仍不会生成Init_RTC和hrtc。 - 本工程目标器件为 STM32F103ZE。RTC 时钟源的引脚和备用供电应以实际开发板原理图为准,不能仅凭型号假定已焊接 LSE 晶体或电池。
1.2 添加所需头文件
rtc.h用头文件保护符避免同一编译单元重复展开;它声明extern RTC_HandleTypeDef hrtc;,并声明Init_RTC(void)。main.c需要包含rtc.h,rtc.c也需要包含它。extern只是声明对象定义在别处,真正的定义只能保留一份:rtc.c中的RTC_HandleTypeDef hrtc;。- 配套
main.c仍包含tim.h,但Init_TIM2()已注释。若后续不再使用定时器输入捕获及其回调,可在确认没有其他引用后再清理,不能为了消除表面“未使用”直接删掉仍被中断文件调用的代码。
1.3 复制RTC代码
- 迁移的最小单元是
rtc.c、rtc.h、main.c中的初始化调用、系统时钟中 LSE/RTC 的选择,以及 HAL RTC 驱动源文件;只复制Init_RTC()不能使 RTC 获得时钟。 HAL_RTC_MspInit()允许访问备份域、打开 BKP 时钟和 RTC 外设时钟。LSE 的启动及 RTC 时钟源选择则在SystemClock_Config()中完成,两处职责不同。RTC_AUTO_1_SECOND是该 F1 HAL 对预分频的自动配置选项。使用 32.768 kHz LSE 时,预分频得到 1 Hz,RTC 的硬件计数器每秒递增一次。
1.4 添加主函数
- 本工程的初始化顺序为
HAL_Init()、SystemClock_Config()、GPIO/UART/I2C、Init_RTC()、OLED。必须先完成系统和外设时钟配置,再初始化依赖这些时钟的模块。 - 循环中先读日期再读时间,是 F1 HAL 为维护软件日期所采用的典型读取顺序。两个调用都使用
RTC_FORMAT_BIN,因此Month、Hours等字段可直接用%d输出;若指定RTC_FORMAT_BCD,字段是压缩 BCD,不能当作普通十进制整数打印。
1.5 打开RTC所在工程
- 应以
KEIL/9RTC/Core/Src/rtc.c、main.c和工程文件中的源文件列表为准,而不是以转写中的临时编辑画面为准。 - 系统时钟函数实际启用了 HSE 和 LSE,并通过
HAL_RCCEx_PeriphCLKConfig()把 RTC 时钟源选为RCC_RTCCLKSOURCE_LSE。若 LSE 不起振,HAL_RCC_OscConfig()会失败并进入Error_Handler()。
1.6 复制RTC所在工程代码
rtc.h中函数原型末尾的分号不可缺少:void Init_RTC(void);。这是声明,不是函数实现。- 本工程同时编译了
stm32f1xx_hal_rtc.c与stm32f1xx_hal_rtc_ex.c。前者提供HAL_RTC_Init、读写日期时间等实现;仅包含头文件只会让编译器认识函数声明,不能解决链接时的未定义符号。 - 移植 CubeMX 生成的
SystemClock_Config()时必须连同相关宏、HAL RCC 驱动和实际硬件条件一起核对。把函数文本粘入另一个工程并不自动保证晶振频率、电源和管脚连接正确。
1.7 编译错误处理
1.7.1 编译错误示例分析
- “某变量未定义”不一定表示该变量从未书写;它也可能是前一个声明少了分号、头文件未被找到,或变量只在另一个
.c文件中定义却没有extern声明。 - 应先看报错文件、行号和第一条错误。后续大量错误常是第一处语法错误导致编译器无法继续正确解析的连锁结果。
1.7.2 编译错误原因查找
- 预处理器会把
#include "rtc.h"的内容展开到main.c。因此rtc.h缺分号时,诊断可能显示在main.c的包含位置,但根因在头文件。 - 分清三类问题:找不到头文件是 Include Paths 或文件名问题;编译期未声明是头文件内容或可见性问题;链接期未定义通常是
.c未加入工程或符号名不一致。
1.7.3 编译错误修正过程
- 先补正
rtc.h中的语法,再确认Core/Src/rtc.c已被工程编译,最后确认 HAL 源文件列表含 RTC 驱动。每完成一步都保存并重新编译,以免多个改动叠加后无法定位原因。 rtc_ex不是因为“预先包含就能避免报错”而必须加入;是否需要它应由调用的 API 和链接结果决定。此配套工程已经将其列入 HAL 驱动组,可以保留原工程配置。
1.8 查看问题并解决
- 将故障范围缩小为时钟、RTC 读取、字符串格式化、I2C/OLED 显示四段。可先用 UART 输出
RTC_Buff验证读数,再检查 OLED,而不是一开始同时修改所有模块。 - 调试时不要长期用大段注释替代理解。临时注释应有明确目的,例如确认问题是否出在
HAL_RTC_GetTime()之前;验证后恢复或形成最终、可编译的代码。
1.9 将之前的主函数代码粘过来
- 配套代码保留
Init_UART1(),且两处HAL_UART_Transmit()已注释。恢复其中一处可把同一份RTC_Buff输出到串口,便于与 OLED 显示比较。 - 向串口插入临时标识字符可以帮助区分版本,但最终代码应移除无意义标记,并检查缓冲区长度和字符串结束符。
1.10 覆盖之前用到的变量定义
RTC_Time和RTC_Date是两个全局结构体,分别承接HAL_RTC_GetTime()与HAL_RTC_GetDate()的结果;RTC_Buff[200]承接格式化文本。- 同一作用域中不能重复定义同名变量。删除变量前应先用工程范围搜索确认它没有被回调、其他源文件或调试代码引用。
- 此处的全局变量规模很小,便于初学调试;项目扩大后可将显示状态封装为结构体,减少跨模块直接访问。
1.11 查看效果
- 日期显示调用为
OLED_ShowString(0, 2, RTC_Buff, 16),时间为OLED_ShowString(0, 6, RTC_Buff, 16);0是横向起点,第二个参数是 OLED 驱动使用的页行起点,不是“第 0 或第 6 个像素”。 - 显示异常时,先确认 OLED 已初始化、I2C 总线正常,再查看
RTC_Buff的实际内容。不能仅凭屏幕空白就断定 RTC 初始化失败。
1.12 日期时间同时显示
- 工程在同一循环内读日期、显示日期、显示星期、读时间、显示时间,然后延时 1 秒,因而视觉上同时显示。它不是中断刷新,刷新周期还会受 I2C 发送和格式化耗时的轻微影响。
- 日期文本中的
20%2d是配套代码针对两位年份字段的写法;当Year为23时输出2023。它只适用于 2000--2099 的显示约定,不是通用四位年份算法。 - 若需要显示中文“星期一”,应确保字库中有相应字模;本工程最终选择英文星期,避免额外字模处理。
1.13 周一到周日的英文
Monday、Tuesday、Wednesday、Thursday、Friday、Saturday、Sunday 是正确的英文拼写。特别注意 Wednesday 中的 d,以及 Thursday 中的 h;代码中的字符串大小写会原样显示。
1.14 日期时间同时显示并注释
1.14.1 代码结构与初始化
RTC_TimeTypeDef保存时、分、秒,RTC_DateTypeDef保存年、月、日、星期。它们是读取结果的容器,不会自行让硬件走时。- 当前
Init_RTC()只初始化 RTC;设定默认时间和日期的两段HAL_RTC_SetTime()、HAL_RTC_SetDate()已被注释。这一选择用于避免每次软件复位都重写计数器。
1.14.2 代码中星期的显示逻辑
switch (RTC_Date.WeekDay)将 HAL 返回的枚举值映射为英文字符串,并在每个case后使用break防止继续执行下一个分支。- STM32F1 HAL 的定义是
RTC_WEEKDAY_SUNDAY = 0、RTC_WEEKDAY_MONDAY = 1,依次到RTC_WEEKDAY_SATURDAY = 6。配套switch的 0--6 映射与该驱动一致。
1.14.3 代码修正与完善
default当前不显示任何内容;若WeekDay非法,旧星期字符串会留在屏幕上。可在调试时显示固定错误提示,或在刷新前清除该显示区域。- 星期名长度不同:
Wednesday为 9 个字符,而Sunday为 6 个字符。由长字符串切换到短字符串时,OLED_ShowString()若不擦除尾部,右侧可能残留旧字符。应先清除该行或输出固定宽度的填充字符串。
1.14.4 例题1: 一周七天的命名及文化底蕴
本节源码实际练习的是英文星期字符串与枚举映射,并未实现文化背景相关功能。不同软件可把周日或周一视为一周开始日,但嵌入式程序不能据此猜测枚举值,必须查当前 HAL 头文件;在这个 F1 工程中周日明确为 0。
1.14.5 代码中日期与星期的显示调整
- 显示星期前先调用
HAL_RTC_GetDate(),因为WeekDay属于RTC_DateTypeDef,不在RTC_TimeTypeDef内。 - 不应把“周日通常是 0 或 7”的泛化约定直接写入程序。具体值由库定义决定;本工程的 0--6 映射已与
stm32f1xx_hal_rtc.h对齐。
1.14.6 代码中switch语句的完善
case匹配一个确定值,break结束当前分支,default处理未覆盖值。漏掉break会发生贯穿,导致同一次循环连续写入多个星期字符串。- 若改用字符串数组,可用枚举值作下标,但必须先范围检查,避免非法值越界;对于初学阶段,当前显式
switch更直观。
1.14.7 显示行数的解释与现象观察
- SSD1306 类 128x64 OLED 以 8 像素为一页。16 像素字高要占两页,因此起点
y=0、2、4、6正好形成四个不重叠的逻辑行:标题、日期、星期、时间。 - 这也是时间放在
y=6的原因:它占用最后两页6和7。若传入奇数起点或超过6的 16 像素文本,字模可能与相邻行重叠或越界。
1.14.8 最终代码调整与显示效果
- 最终工程使用
RTC_Time、RTC_Date和RTC_Buff;转写中出现的time、delta等说法不是该工程的最终变量名,不应据此修改源码。 - 预期页面从上到下为固定中文标题、日期、英文星期和时间。出现不完整字符时,优先检查星期行的长短字符串残留与 I2C 通信,而非盲目重设 RTC。
1.15 断电后时间归零问题处理
- 注释掉
HAL_RTC_SetTime()和HAL_RTC_SetDate()可以避免每次启动把硬件计数器重设为演示值,但这不是完整的“断电日期保持”方案。 - F1 的 RTC 硬件核心是秒计数器、预分频和闹钟;日期由 HAL 在
RTC_HandleTypeDef.DateToUpdate中以软件方式维护。该状态在普通 RAM,复位、停止或备用状态后会丢失。当前工程没有写入任何 BKP 数据寄存器,因此它不能独立恢复完整日期和星期。 - 若 VDD 断电而 VBAT 仍有效、LSE 持续振荡,RTC 计数器可以继续走时;若 VBAT 也失电或 LSE 没有工作,计数器也会停止。STM32F103 的 VBAT 供电范围及 LSE/备份域连接均需按数据手册和开发板原理图核对。
- 实际产品应在备份寄存器设置首次初始化标志,并保存足以恢复日期的状态;有标志时不重复设时,无标志时才让用户设定初值。还应设计掉电后跨多日的日期推进策略,不能仅依赖本工程循环中的显示调用。
1.16 系统时钟配置分析
- 配套
SystemClock_Config()同时设置RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_LSE,使 HSE 经 PLL 供主系统使用,LSE 作为 RTC 的低速时钟源;两种振荡器用途不同。 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC与PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE才完成 RTC 源选择。仅在HAL_RTC_MspInit()中打开 RTC 时钟,并没有选择 LSE。- LSE 通常为 32.768 kHz,适合整除为 1 Hz。HSE/128、LSI 也可作 F1 RTC 源,但精度、功耗和后备供电行为不同,切换前必须重新评估预分频和硬件条件。
1.17 屏幕显示情况
- 正常时应每秒更新一次末行秒数;日期、星期只在跨日时变化。重置后若时间仍在走而日期变为默认值,正是本工程未持久化 HAL 软件日期的表现之一。
- 下载调试器、按复位键和真正切断 VDD 是不同操作。验证后备功能时,要分别测试软件复位、VDD 短时掉电且 VBAT 保持、VBAT 也断电三种场景,并记录结果。
1.18 百分号问题处理
sprintf中%2d的含义是“最小字段宽度为 2”,数值不足两位时在左侧补空格,例如5输出为5;它不是“数字占两个字节”。数值超过两位时字段会自动变宽。- 若希望时钟显示为
05:07:09,格式应为%02d:%02d:%02d。0是补零标志,2是最小宽度。日期同理可用%02d,并应评估缓冲区大小。 sprintf不检查目标容量;本例文本短于RTC_Buff[200],暂不会溢出,但更通用的代码应使用snprintf(RTC_Buff, sizeof RTC_Buff, ...)并处理截断结果。
1.19 定义结构体
RTC_TimeTypeDef与RTC_DateTypeDef已是 HAL 定义的结构体,分别保存读取出的时间和日期字段;无需再为同一用途重复设计结构体。- 若增加按键设时状态,可定义独立的应用层结构体保存“当前编辑字段、待提交日期时间、按键事件”等,而不要直接把未校验的按键值写进 RTC。
1.20 通过按键调节时间功能
- 配套
9RTC工程没有按键 GPIO 初始化、扫描、消抖或HAL_RTC_SetTime()/HAL_RTC_SetDate()的实际调用;转写中的“四个按键”“按七次保存”是后续设计思路,不是本工程已实现的功能。 - 一个可靠的实现至少需要:将按键抖动转换为单次事件;选择年、月、日、时、分、秒字段;限制每个字段的有效范围并处理闰年;只在用户确认时一次性写入时间和日期;必要时同步更新备份域中的初始化标志和日期状态。
- 设定过程中应在内存副本中修改并预览,取消时不触及 RTC。这样可避免用户调整到一半时写入无效日期,也减少频繁改写备份域。