STM32 —— RTC 时间读取

实验目的

了解实时时钟RTC的原理。STM32 芯片自带 RTC,因此不须像其他 MCU 需外接 RTC 模块。请编程实现 STM32 的日历读取、设置和输出。要求:

  1. 读取 RTC 初始时间,验证是否为 1970年1月1日零分零秒;

  2. 将 RTC 时间调整为当前时间,并以 2021年x月x日x分x秒的格式从串口输出(或输出到OLED屏),每1s改变一次;

  3. 如果输出内容中需加入“星期x”,请修改代码。

实验原理

根据实验要求,我们需要读取 RTC 模块的日历,这里我们就需要知道在 STM32 中是自带 RTC 的,不像其他的 MCU 一样需要外接,这里我们就可以直接设置并通过 HAL 库调用一些 RTC 的函数,完成指定的要求,由于 RTC 掉电后会重置,这里设计了代码来防止 RTC 在掉电后被重置导致时间回到初始设置状态

具体的 RTC 原理可以看我的另一篇博客:STM32 —— RTC 入门

HAL 库方法

CubeMX 项目配置

RCC 配置

设置高速外部时钟 HSE 选择外部时钟源,使能外部晶振 LSE

时钟配置

这里说明一下为什么要使用 LSE 外部晶振:

RTC 设备因为其独特的运行方式(即掉电依旧运行)使用 HSE 分频时钟或者 LSI 的时候,在主电源 VDD 掉电的情况下,这两个时钟来源都会受到影响,资源消耗太大,小小的纽扣电池根本吃不消。没法保证 RTC 正常工作.所以 RTC 一般都时钟低速外部时钟 LSE

USART1 配置
RTC 配置

RTC_OUT: Not RTC_OUT

Tamper: ×

第一个是是否使能 tamper(PC13)引脚上输出校正的秒脉冲时钟,第二个是 RTC 入侵检测校验功能

RTC 校验功能,使能侵入检测功能。RTC 时钟经 64 分频输出到侵入检测引脚 TAMPER 上,当 TAMPER 引脚上的信号从 0 变成 1 或者从 1 变成 0 (取决于备份控制寄存器 BKP_CR 的 TPAL 位),会产生一个侵入检测事件。侵入检测事件将所有数据备份寄存器内容清除

也就是第一个是使能 tamper(PC13)引脚作为时钟脉冲输出,第二个是使能 tamper(PC13)引脚作为入侵检测功能

在第一个任务的时候,我们是要进行读取默认的系统时间,这里不需要进行设置:

第二个任务要求我们进行时间设置,这里直接设置成相对应该的时间即可:

TIM2 配置
引脚配置

代码设计

首先我们需要知道两个 RTC 的中断:

RTC_IRQHandler()	// RTC 全局中断

RTCAlarm_IRQHandler()	// 闹钟中断函数

我们首先可以看 stm32f1xx_hal_rtc.h ,这里在库中已经给我们封装好了一些能够获取目前板载系统时间和日历的一些内容,并且提供了一些设定闹钟的函数,我们直接使用即可

下面我们对函数中的三个变量进行分析:

RTC_HandleTypeDef *hrtc	// 我们要调用的封装了 RTC 的串口

RTC_TimeTypeDef *sTime/sData	// 我们获取的时间的对象,HAL 库中已经给出了对应的结构体,我们只需要声明对应的结构体成员即可

uint32_t Format	// 这是我们所需要的时间或日期的数据编排格式,我们需要填写我们事先在前面设置的格式即可

前面我们已经实现了定时器,这里我们依旧使用定时器方法进行设计,这样可以提高 cpu 使用效率,并且这里也采用我们前面提到过的重写 printf 函数的方式进行输出

重写 fputc 函数:

int fputc(int ch,FILE *f){
		uint8_t temp[] = {ch};
		HAL_UART_Transmit(&huart1,(uint8_t*)temp,sizeof(temp),0xFFFF);
		return ch;
}

启动定时器 TIM2:

HAL_TIM_Base_Start_IT(&htim2);

重写定时器回调函数与 RTC 计时器获取时间:

RTC_DateTypeDef Date;
RTC_TimeTypeDef Time;

uint8_t str[] = "ppqppl\r\n";

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	static int time = 0;
	if(htim->Instance == TIM2){
		time++;
		if(time%1000 == 0){
			HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_5);
			HAL_RTC_GetDate(&hrtc,&Date,RTC_FORMAT_BIN);
			HAL_RTC_GetTime(&hrtc,&Time,RTC_FORMAT_BIN);
			printf("%02d年-%02d月-%02d日\r\n",Date.Year,Date.Month,Date.Date);	// Date.WeekDay表示日期
			printf("%02d时:%02d分:%02d秒\r\n",Time.Hours,Time.Minutes,Time.Seconds);
			HAL_UART_Transmit(&huart1,(uint8_t*)str,sizeof(str),0xFFFF);
			printf("\r\n");
		}
	}
}

注意:这里由于 RTC 会占用 PC13 引脚,所以这里选择使用 PA5 外接小灯泡进行测试

如果我们需要读取日期,并且需要显示当前时间的话,我们就需要在 CubeMX 中设置好时间,然后在代码中对时间进行简单的判断和计算即可显示正确的时间日期,由于其他代码相同,这里只提供最终输出的代码段,代码如下:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	static int time = 0;
	if(htim->Instance == TIM2){
		time++;
		if(time%1000 == 0){
			HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_5);
			HAL_RTC_GetDate(&hrtc,&Date,RTC_FORMAT_BIN);
			HAL_RTC_GetTime(&hrtc,&Time,RTC_FORMAT_BIN);
			printf("%02d年-%02d月-%02d日-",2000+Date.Year,Date.Month,Date.Date);	// 由于在 CubeMX 中设置的年份只是后两位,这里需要加上 2000 来表示当前年份
			switch(Date.WeekDay){	// Date.WeekDay 是数字 1-7 ,分别代表星期一到星期日
				case 1:{
					printf("星期一\r\n");
					break;
				}
				case 2:{
					printf("星期二\r\n");
					break;
				}
				case 3:{
					printf("星期三\r\n");
					break;
				}
				case 4:{
					printf("星期四\r\n");
					break;
				}
				case 5:{
					printf("星期五\r\n");
					break;
				}
				case 6:{
					printf("星期六\r\n");
					break;
				}
				case 7:{
					printf("星期日\r\n");
					break;
				}
				default:{
					printf("没有获取到WeekDay数据!\r\n");
					break;
				}
			}
			printf("%02d时:%02d分:%02d秒\r\n",Time.Hours,Time.Minutes,Time.Seconds);
			HAL_UART_Transmit(&huart1,(uint8_t*)str,sizeof(str),0xFFFF);
			printf("\r\n");
			time = 0;
		}
	}
}

当然我们也可以在 rtc.c 中通过代码去设置时间,但是要注意的是,这里对变量赋值应该是 16 进制的,否则可能会导致输出异常,代码如下:

sTime.Hours = 0x12;
sTime.Minutes = 0x0;
sTime.Seconds = 0x0;

DateToUpdate.WeekDay = RTC_WEEKDAY_FRIDAY;
DateToUpdate.Month = RTC_MONTH_NOVEMBER;
DateToUpdate.Date = 0x4;
DateToUpdate.Year = 0x22;

标准库方法

若有需要,后续会更新标准库写法

寄存器方法

若有需要,后续会更新寄存器写法

运行测试

虚拟串口测试

虚拟串口测试:

这里由于串口测试没有放着环境,只能看到输出情况

Proteus 仿真模拟

由于 Proteus 无法使用外部晶振,所以 RTC 这里选择的是 LSI 运行

仿真图如下:

图中给出了外部 LSE 的正确接发(虽然无法使用)

运行结果:

由于时间过长,这里将时间缩短为 0.1 秒

接线示例

运行结果

结果分析

这里可以看到,RTC 基于 HAL 库的初始日期并不是 1970 年,而是编译器设置的默认值,在 CubeMX 中默认值是 00 年,也就是说,默认值只是设置了年份的后两位,只要我们在输出时年份进行简单计算后,就可以设置成任意年限

波形检测

做波形检测没有意义,这里直接省略这一步骤

错误解决方法

  1. 串口输出问题

对于重写 printf 串口输出,会存在一个致命的问题,就是当你的函数存在问题时,会出现乱码,重写 fputc 函数和注意事项如下:

int fputc(int ch,FILE *f){
		uint8_t temp[] = {ch};	// 这里注意一定要将 ch 存在字符串数组中,不能直接将 ch 转为 uint8_t 类型
		HAL_UART_Transmit(&huart1,temp,1,0xFFFF);	// 这里特别要注意输出的一定要是上面声明的 temp ,不能直接输出(uint8_t *)ch ,否则就会产生输出乱码
		return ch;
}
  1. 时间输出问题

对于时间的获取,函数调用如下:

HAL_RTC_GetDate(&hrtc,&Date,RTC_FORMAT_BIN);
HAL_RTC_GetTime(&hrtc,&Time,RTC_FORMAT_BIN);

需要注意的是,这里函数的第三个参数是时间的格式,我们一定要选择 RTC_FORMAT_BIN 格式,不然我们就会发现输出的时间与实际设置的时间不符,甚至可能会出现 17 月份或星期 9 这样诡异的时间点

  1. 仿真运行问题

如果我们在使用 MDK 进行仿真的时候出现如下问题:

*** error 65: access violation at 0x40021000 : no 'read' permission

则是因为较新版本默认用的是 CM 内核,如果使用的是 STM 系列就会出现这种错误,只要把参数设置成相应的 STM 系列的就可以使用了

这个报错本质上就是因为我们的 Memory_map 地址不够大,导致我们程序无法正常运行,虽然程序没有问题,但是会产生仿真运行中程序中断报错

参考文档

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐