STM32单片机学习(7)

1.独立看门狗 —IWDG

1.1 关于独立看门狗

MCU可能工作在一些复杂环境,可能受到某些电磁干扰出现程序跑飞,导致死循环无法继续执行工作,看门狗的作用就是为了避免这种情况。看门狗的本质也是一个定时器,在启动后,需要在一定时间内再给它一个信号,俗称“喂狗”,如果没有按时“喂狗”,说明MCU可能处于非正常状态,这时看门狗就向MCU发送个复位信号,使整个系统重启,重新进入正常的工作状态。

STM32F103系列有两个看门狗,独立看门狗(Independent Watchdog,IWDG)和窗口看门狗(Windowwatchdog,WWDG)。

独立看门狗结构图如图 1.1.1 所示,上半部分是寄存器核心,下半部分是工作流程。可以将工作流程分解为四个步骤。

  • 图 1.1.1 独立看门狗结构图

在这里插入图片描述

①时钟:独立看门狗的时钟源来自LSI(内部低速时钟),意味着不受外部晶振电路影响,同时就算系统主时钟发生故障时,也可以正常工作。使用内部晶振,也意味精度并不高,因此只适合应用在对时间精度要求比较低的场合。
LSI的频率为40KHz,通过预分频寄存器(IWDG_PR)设置8位预分频器的分频因子(支持2 n 分频,2≤n≤8),由此可以得出计数器时钟为:
在这里插入图片描述
其中,pre为预分频寄存器的值,范围:0≤pre≤6。

②计数:独立看门狗的计数器是一个12位的递减计数器,计数最大值为0xFFF,当计数器递减到0时,就会产生一个复位信号,重启整个系统。如果在递减到0之前,将重装载数值写入递减计数器,就会由重装载数值开始递减到0,如此反复,就永远不会到0,也就不会产生复位信号,这个重装载计数值写入递减计数器的过程就叫“喂狗”。
重装载数值来自重装载寄存器(IWDG_RLR),这个值大小决定独立看门狗的溢出时间(复位倒计时),溢出时间为:
在这里插入图片描述
其中,pre为预分频寄存器的值,范围:0≤pre≤6,rlr为重载寄存器的值,范围:0~0xFFF。
这里还有一个键值寄存器(IWDG_KY),往该写一些关键值,以实现不同的效果,这里关键值共有三个:

  • 写0xAAAA:将重载寄存器(IWDG_RLR)的值更新到计数器;
  • 写0x5555:默认预分频寄存器(IWDG_PR)和重装载寄存器(IWDG_RLR)有写保护,不允许直接写值,需要先向键值寄存器(IWDG_KY)写0x5555去保护,才能写预分频寄存器(IWDG_PR)和重装载寄存器(IWDG_RLR)。写完后,向键值寄存器(IWDG_KY)写入其它任意值,将重新启用写保护;
  • 写0xCCCC:启动独立看门狗,一旦启动,无法通过其它方式关闭,只能复位后恢复默认关闭状态;

③独立看门狗复位:当独立看门狗的递减计数器递减到0,将发生复位。假设当前预分频寄存器值为6(256分频),重装载寄存器的值为15,则溢出时间为:
在这里插入图片描述
即启动独立看门狗后,需要在0.96s内喂狗一次,否则系统将复位。

④状态寄存器:预分频器和重装载数值的工作状态,可通过状态寄存器(IWDG_SR)获取。该寄存器只有最低两位有效,含义如下:

  • Bit[1] Reload Value Update,RVU:独立看门狗计数器重载值更新状态,读出为1表示重装载值正在更新,读出为0表示更新完毕;
  • Bit[0] Prescaler Value Update,PVU: 独立看门狗预分频值更新状态,出为1表示预分频值正在更新,读出为0表示更新完毕;

因此,只有在RVU和PVU为0的情况下,才能分别设置重装载寄存器和预分频寄存器的值。

1.2 硬件设计

独立看门狗为MCU内部资源,不涉及新增硬件设计。

1.3 软件设计

1.3.1 软件设计思路

实验目的:本实验展示独立看门狗的效果,通过按键切换自动喂狗模式和不自动喂狗模式,感受两者区别。

  1. 初始独立看门狗相关参数:时钟预分频、设置重装载值等;
  2. 主函数编写控制逻辑:通过按键切换不自动喂狗模式和自动喂狗模式。不自动喂狗模式中,不做任何操作,观察打印,何时复位重启。自动喂狗模式中,不断喂狗,观察打印,是否复位重启。

1.3.2 软件设计讲解

  1. 初始化独立看门狗喂狗时间

初始化独立看门狗,如代码段 1.3.1 所示。

代码段 1.3.1 初始化独立看门狗(driver_iwdg.c)

/*
* 函数名:void IWDG_Init(uint16_t period)
* 输入参数:period-设置喂狗周期,单位 ms
* 输出参数:无
* 返回值:无
* 函数作用:初始化独立看门狗的喂狗时间
* 刷新时间计算:Prescaler/LSI*Reload
*/
void IWDG_Init(uint16_t period)
{
	hiwdg.Instance = IWDG; // 选择独立看门狗
	hiwdg.Init.Prescaler = IWDG_PRESCALER_256; // 设置预分频
	hiwdg.Init.Reload = 40000/256*period/1000; // 设置重装载值
	if (HAL_IWDG_Init(&hiwdg) != HAL_OK) // 初始化独立看门狗
	{
		Error_Handler();
	}
}
  • 11行:选择独立看门狗;
  • 12行:设置时钟预分频,支持2 n 分频(2≤n≤8),这里为2 8 =256分频;
  • 13行:设置重装载寄存器值,由传入的喂狗时间计算得到;
  • 14~17行:初始化独立看门狗;
  1. 喂狗

HAL库提供刷新独立看门狗函数“HAL_IWDG_Refresh()”,直接使用即可,如代码段1.3.2 所示。

代码段 1.3.2 喂狗(driver_iwdg.c)

/*
* 函数名:void ClearIWDG(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:刷新独立看门狗的计数器,俗称“喂狗”
*/
void ClearIWDG(void)
	{
	if (HAL_IWDG_Refresh(&hiwdg) != HAL_OK) // 将重载寄存器的值更新到计数器
	{
		Error_Handler();
	}
	printf("--------- 喂狗 --------\n\r");
}
  1. 主函数控制逻辑

在主函数里,首先初始化设置喂狗周期时间,接着通过按键切换喂狗/不喂狗模式,通过观察两者模式区别,感受独立看门狗的效果,如代码段 1.3.3 所示。

代码段 1.3.3 主函数控制逻辑(main.c)

// 初始化 IWDG,设置喂狗周期 1000ms=1s
IWDG_Init(1000);

while(1)
{
	if(up_flag != true) // 不喂狗,喂狗周期结束后将复位重启
		printf("*当前模式:不自动喂狗模式 %d \n\r", run_cnt);
	else
	{
		printf("*当前模式:自动喂狗模式 %d \n\r", run_cnt);
		ClearIWDG(); // 喂狗周期内喂狗,不会复位重启
	}
	
	run_cnt++;
	HAL_Delay(100);
}

2. 窗口看门狗 —WWDG

2.1 关于窗口看门狗

独立看门狗和窗口看门狗的效果类似,都是检测系统发生软件错误或死机时,通过复位重启使系统重新正常工作。

独立看门狗包含一个12位递减计数器,从用户定义的t开始递减到0,必须在t~0之间喂狗,否则复位重启。

窗口看门狗包含一个7位递减计数器,从用户定义的t开始递减到64,必须在t~64之间喂狗,在t之前或者64之后喂狗,也会导致复位重启。这里的t值,称之为窗口上限,由用户自定义设置;这里的64,称之为窗口下限,是系统固定的。窗口看门狗计数器必须在上窗口和下窗口之间被刷新(喂狗),不能过早,也不能过晚,这也就窗口看门狗中的“窗口”含义。

窗口看门狗的窗口下限值为63,而窗口看门狗递减计数器又是7位,因此用户可设置的窗口上限范围为63<x<128。如图 2.1.1 所示,假设递减计数器初值为127,随着时间的递增,它的值将逐渐减少。

现在还需要定一个窗口上边界,这个上边界位于下边界和递减计数器初值之间,假设为80。那么计数器从127递减到80这个时间段,是不允许喂狗的,一旦喂狗将复位重启;80递减到63这个时间段,则必须喂狗;63这个下边界是固定的,一旦递减计数器小于等于这个值,系统将复位。可以看到,整个过程,有两个关键数值,一个是递减计数器当前计数值,对应寄存器WWDG_CR[6:0],一个是窗口上边界值,对应寄存器WWDG_CFG[6:0]。

如图 2.1.2 所示,为窗口看门狗结构图,可以看作四部分组成:

①时钟:窗口看门狗的时钟源来自PCLK1(最高36HMz),经过4096分频,再经过WWDG_CFG的Bits[8:7]位WDGTB分频得到,WDGTB支持2 n 分频(0≤n≤3),由此可以得出计数器时钟为:
在这里插入图片描述
其中,pre为预分频寄存器的值,范围:0≤pre≤3。

②计数:窗口看门狗的计数器是一个7位的递减计数器,计数最大值为0x7F,当计数器递减到0x3F时,就会产生一个复位信号,重启整个系统。当递减计数器递减到0x40时,如果使能了提前唤醒中断(WWDG_CFG的Bits[9]位EWI设置为1),则会产生提前唤醒中断,在该中断可以保存重要数据或者向WWDG_CR重新写入新计数器值,完成喂狗操作。一旦0x40变为0x39,系统将进行复位,因此必须在一个窗口看门狗计数周期内完成喂狗操作。

WWDG_CR的Bits[7]位WDGA为窗口看门狗使能位,当为1时,窗口看门狗才工作。

  • 图 2.1.1 窗口看门狗工作原理
    在这里插入图片描述

  • 图 2.1.2 窗口看门狗结构
    在这里插入图片描述

③窗口:窗口看门狗的WWDG_CFG的Bits[6:0]位为窗口上边界值,该值应小于计数器最大值0x7F,大于窗口下边界值0x3F。

④逻辑与或:在分析这块之前,先梳理下图中符号的的含义。与门,必须输入都为真,输
出才为真;或门,输入其中一个为真,输出就为真;如果输入/输出线带有小圆圈,表示逻辑非。

通过与门、非门的组合,实现了不同情况下的复位效果。现在从复位结尾倒回去分析,如果想结尾Aout的复位为假,而输入Ain2看门狗使能为真,则Ain1必为假才行。Bin2为递减计数器最高位,如果为1,则肯定大于0x3F,再逻辑非,Bin2为0,同时需要Bin1也为假。Cin2写WWDG_CR,为真,此时整个系统的真假由Cin1决定。当T6:0>W6:0时,Cin1=1,此时对应计数器值在上边界以上。当T6:0<W6:0时,Cin1=0,此时对应计数器值在上边界以下。

两个看门狗的相同点和不同点,如表 2.1.1 所示。

表 2.1.1 独立/窗口看门狗的相同点和不同点
在这里插入图片描述

2.2 硬件设计

窗口看门狗为MCU内部资源,不涉及新增硬件设计。

2.3 软件设计

2.3.1 软件设计思路

实验目的:本实验展示窗口看门狗的效果,通过按键切换不同的场景:不自动喂狗模式、自动喂狗模式和中断喂狗模式,感受三者区别。

  1. 初始窗口看门狗相关参数:时钟预分频、设置窗口值和计数值等;
  2. 初始化窗口看门狗硬件相关参数:时钟使能、设置中断优先级和使能;
  3. 编写窗口看门狗提前唤醒中断回调函数;
  4. 主函数编写控制逻辑:通过按键切换三种模式。不自动喂狗模式中,不做任何操作,观察打印,何时复位重启;自动喂狗模式中,先延时一段时间,再喂狗,观察打印,是否复位重启;中断喂狗模式中,使能看门狗中断,再提前唤醒中断里喂狗,观察打印,是否复位重启。

2.3.2软件设计讲解

  1. 初始化 初始化 窗口 看门狗

初始化窗口看门狗,如代码段 2.3.1 所示。

代码段 2.3.1 窗口看门狗初始化(driver_wwdg.c)

/*
* 函数名:void WWDG_Init(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化窗口看门狗
* 计算方式:窗口看门狗时钟:PCLK1(36MHz)/4096/Prescaler(8) ≈ 1098Hz ≈ 0.9ms
								|--------|----------|-------|
				窗口看门狗计数器: 120 80 63 0
									36ms 15.3ms
				即:启动看门狗后,36ms 内不能喂狗,36ms~15.3ms 需要喂狗
*/
void WWDG_Init(void)
{
	hwwdg.Instance = WWDG; // 指定窗口看门狗
	hwwdg.Init.Prescaler = WWDG_PRESCALER_8; // 时钟预分频, 支持 2^n 分频(0≤n≤3)
	hwwdg.Init.Window = 80; // 设置窗口上边界值,范围:0x40~0x7F
	hwwdg.Init.Counter = 120; // 设置计数值,范围:0x40~0x7F
	hwwdg.Init.EWIMode = WWDG_EWI_ENABLE; // 提前唤醒中断使能
	if (HAL_WWDG_Init(&hwwdg) != HAL_OK)
	{
		Error_Handler();
	}
}
  • 15行:指定窗口看门狗;
  • 16行:设置时钟预分频,支持2 n 分频(0≤n≤3);这里为2 3 =8分频,PCLK1为36MHz,计数得到当前窗
  • 看门狗时钟频率约为1098Hz ,即0.9ms计数一次;
  • 17行:设置窗口上边界值,范围0x40~0x7F,这里设置为80;
  • 18行:设置计数值,范围0x40~0x7F,这里设置为120;
  • 19行:使能以前唤醒中断
  • 20~23行:使用HAL库提供的“HAL_WWDG_Init()”函数,初始化窗口看门狗;

“HAL_WWDG_Init()”函数会回调“HAL_WWDG_MspInit()”进行硬件相关初始化,如代码段 2.3.2
所示。

代码段 2.3.2 窗口看门狗硬件相关初始化(driver_wwdg.c)

/*
* 函数名:void HAL_WWDG_MspInit(WWDG_HandleTypeDef* wwdgHandle)
* 输入参数:wwdgHandle-WWDG 句柄
* 输出参数:无
* 返回值:无
* 函数作用:HAL_WWDG_Init 回调硬件初始化
*/
void HAL_WWDG_MspInit(WWDG_HandleTypeDef* wwdgHandle)
{
	if(wwdgHandle->Instance==WWDG)
	{
		__HAL_RCC_WWDG_CLK_ENABLE(); // 使能 WWDG 时钟
		
		HAL_NVIC_SetPriority(WWDG_IRQn,0,0); // 设置 WWDG 中断优先级
		HAL_NVIC_EnableIRQ(WWDG_IRQn); // 使能 WWDG 中断
	}
}
  • 12行:窗口看门狗时钟来源于PCLK1,需要相应使能;
  • 14行:设置窗口看门狗中断优先级;
  • 15行:使能窗口看门狗中断;
  1. 窗口看门狗 窗口看门狗 中断处理

使能中断后,当窗口看门狗计数到0x40时,会进去提前唤醒中断,在该中断处理函数里,用户可以保存数据或喂狗,如代码段 2.3.3 所示。

代码段 2.3.3 窗口看门狗中断处理函数(driver_wwdg.c)

/*
* 函数名:void WWDG_IRQHandler(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:WWDG 的中断处理函数
*/
void WWDG_IRQHandler(void)
{
	HAL_WWDG_IRQHandler(&hwwdg);
}

/*
* 函数名:void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef* hwwdg)
* 输入参数:hwwdg-WWDG 句柄
* 输出参数:无
* 返回值:无
* 函数作用:提前唤醒中断回调函数
*/
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef* hwwdg)
{
	ClearWWDG();
	printf("-------复位前保存数据------\n\r");
	printf("--------- 软件喂狗 --------\n\r");
}
  1. 窗口看门狗喂狗

调用HAL库提供的“HAL_WWDG_Refresh()”刷新计数器值,实现喂狗操作,如代码段2.3.4 所示。

代码段 2.3.4 窗口看门狗喂狗(driver_wwdg.c)

/*
* 函数名:void ClearIWDG(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:刷新独立看门狗的计数器,俗称“喂狗”
*/
void ClearWWDG(void)
{
	if (HAL_WWDG_Refresh(&hwwdg) != HAL_OK)
	{
		Error_Handler();
	}
}
  1. 主函数控制逻辑

主函数通过按键实现三者模式切换,如代码段 2.3.5 所示。

代码段 2.3.5 主函数控制逻辑(main.c)

KeyInit(); // 初始化按键
WWDG_Init(); // 初始化 WWDG

counter_value = WWDG->CR & 0X7F; // 获取计数值
windows_value = WWDG->CFR & 0x7F; // 获取窗口值
delay_value = (counter_value - windows_value)*0.9; // 计算多少 ms 后可喂狗

while(1)
{
	if(up_flag == 0)
	{
		printf("*当前模式:不自动喂狗模式 %d \n\r", run_cnt);
	}
	else if(up_flag == 1)
	{
		HAL_Delay(delay_value);
		ClearWWDG();
		printf("*当前模式:自动喂狗模式 %d \n\r", run_cnt);
	}
	else if(up_flag == 2)
	{
		HAL_NVIC_EnableIRQ(WWDG_IRQn); // 使能 WWDG 中断
		printf("*当前模式:中断喂狗模式 %d \n\r", run_cnt);
	}
	HAL_NVIC_DisableIRQ(WWDG_IRQn); // 去能 WWDG 中断
	run_cnt++;
	HAL_Delay(1);
}
  • 1行:初始化按键;
  • 2行:初始化窗口看门狗,初始化后窗口看门狗启动;
  • 4行:获取窗口看门狗当前计数值;
  • 5行:获取窗口看门狗当前上窗口值;
  • 6行:计数得到多少ms后才该喂狗;
  • 10~13行:无按键按下(默认状态),不自动喂狗模式,不做任何操作;
  • 14~19行:KEY1_UP按下一次,自动喂狗模式,延时上窗口时间后,执行喂狗操作;
  • 20~24行:KEU1_UP按下两次后,中断喂狗模式,使能窗口看门狗中断,在中断里自动喂狗;
  • 25行:在主循环里关闭窗口看门狗中断,以免其它模式计数到0x40时,进入中断;

3. 低功耗模式 —PWR

3.1 关于低功耗

嵌入式行业中,很多应用场合对功耗有比较严格的要求,比如智能手环依靠很小的锂电池供电,需要尽可能的低功耗运行,才能保证续航。

STM32上电后,默认工作在运行模式,HCLK为CPU提供时钟,根据实际需求,只打开所需外设的时钟,以尽可能的节省电量消耗。

有些特定场合,不需要CPU持续运行(比如等待某外部事件后才执行相应代码),此时可以进入低功耗模式节省电量消耗。

电源系统结构,如图 3.1.1 所示,整个结构可分为四部分。

①V DDA 供电区域:该部分为ADC、DAC等部分模拟外设供电。独立出电源方便进行滤波去耦电容。该区域由V DDA 输入,V SSA 接地,V REF 提供参考电压。

②V DD 供电区域:该部分为I/O、待机电路、电压调节器供电。电压调节器可以运行在“运行模式”、“停止模式”和“待机模式”,为后面的1.8V供电区域供电。本章所讲解的低功耗,实质就是对电压调节器进行配置,使其工作在不同的模式,实现低功耗。前面介绍的IWDG属于V DD 供电区域,不受低功耗模式影响,系统处于低功耗模式也能正常工作。

③1.8V供电区域:该部分为CPU核心、存储器以及数字部分外设供电。电压调节器为其提供1.8V左右电压,因此本区域被称为1.8V供电区域。电压调节器处于运行模式时,本区域正常运行;处于停止模式时,本区域所有时钟关闭,外设保持“暂停”状态,会保留内核寄存器以及SRAM内容;处于待机模式时,本区域断电,所有寄存器(后备供电区域的寄存器除外)和SRAM数据丢失。

④后备供电区域:该部分为LSE 32K晶振、后备寄存器、RCC BDCR寄存器和RTC供电。系统正常工作时,由V DD 供电;系统突然调电时,由V BAT 供电。本区域不受低功耗模式影响,比如电压调节器处于待机模式,RTC也正常工作,后备寄存器数据也不会丢失。

  • 图 3.1.1 STM32 电源系统结构
    在这里插入图片描述
    STM32提供三种低功耗模式,以适应不同层次的低功耗需求,如表 3.1.1 所示。

表 3.1.1 低功耗模式汇总
在这里插入图片描述
①睡眠模式( (Sleep Mode )
该模式关闭CPU内核时钟,内核停止运行,但其它外设,包括内核外设(NVIC、SysTick等)依旧正常运行。如果通过“WFI(Wait for Interrupt)”方式进入睡眠模式,则需要中断唤醒;如果通过“WFE(Waitfor Event)”方式进入睡眠模式,则需要事件唤醒。

②停机模式( (Stop Mode )
该模式进一步关闭HSI和HSE振荡器,使用高速时钟作为时钟源所有外设都停止运行,电压调节器保持开启或低功耗模式,因此1.8V供电区域没有断电,SRAM、外设寄存器的数据依旧保留,不会消失。使用任一外部中断(I/O、RTC闹钟等)唤醒后,可以从停止处继续运行。

③待机 模式( (Standby Mode )
该模式进一步关闭电压调节器,因此1.8V供电区域断电,SRAM、外设寄存器的数据全部丢失,通过WKUP(PA0)上升沿、RTC闹钟、NRST复位或IWDG复位才能唤醒,唤醒后效果类似复位,代码需要重新运行。

3.2 软件设计

3.2.1 软件设计思路

实验目的:本实验通过三个按键,分别进入三种低功耗模式,通过串口打印和LED三色灯展示系统状态,都使用KEY1_UP(PA0)唤醒,体验三种低功耗模式。

  1. 系统正常启动,通过按键修改模式状态,切换相应低功耗模式;
  2. 在睡眠模式,任一中断/事件都可以唤醒,因此要注意关闭其它可能的中断;
  3. 在停机模式唤醒后,系统默认使用HSI作为系统时钟,此时需要重新初始化HSE;
  4. 在待机模式,系统相当于复位,因此不会再执行按键中断处理函数;

3.2.2 软件设计讲解

  1. 按键中断切换低功耗模式

在按键中断处理函数中,根据按键不同,修改模式标志,供主函数切换模式,如代码段 3.2.1 所示。

代码段 3.2.1 按键中断处理函数(driver_key.c)

/*
* 函数名:void HAL_GPIO_EXTI_Callback(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:外部中断处理函数的回调函数,用以处理不同引脚触发的中断服务最终函数
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == KEY_UP_GPIO_PIN)
	{
		// 睡眠模式时:作为任一中断
		// 停机模式时:作为外部中断
		// 待机模式时:作为 WKUP 唤醒源(PA0)
		if (mode_flag == 1)
		{
			HAL_ResumeTick(); // 被唤醒后,恢复滴答时钟
			printf("-->从睡眠模式 恢复<--\n\r");
		}
		else if (mode_flag == 2)
		{
			SystemClock_Config(); // 被唤醒后,默认为 HSI 时钟,需要重新初始化使用 HSE
			printf("-->从停机模式 恢复<--\n\r");
		}
		else if (mode_flag == 3)
		{
			//待机模式类似复位,不会进本中断,也不会打印本语句
			printf("-->从待机模式 恢复<--\n\r");
		}
		mode_flag = 0; // 进入正常模式
	}
	else if(GPIO_Pin == KEY_LEFT_GPIO_PIN) // 进入睡眠模式
	{
		mode_flag = 1;
	}
	else if(GPIO_Pin == KEY_DOWN_GPIO_PIN) // 进入停机模式
	{
		mode_flag = 2;
	}
	else if(GPIO_Pin == KEY_RIGHT_GPIO_PIN) // 进入待机模式
	{
		mode_flag = 3;
	}
}
  • 10~31行:在低功耗模式中,按键KEY1_UP按下,系统唤醒,进入中断处理函数;
  • 15~19行:唤醒后,如果mode_flag为1,表示之前是睡眠模式,此时恢复滴答时钟;
  • 20~24行:唤醒后,如果mode_flag为2,表示之前是停机模式,此时重新配置系统时钟HSE;
  • 25~29行:待机模式类似复位,不会进本中断,判断条件永远不会成立;
  • 30行:唤醒后,修改mode_flag,进入正常模式;
  • 32~35行:如果按键KEY3_LEFT按下,修改mode_flag为2,准备进入睡眠模式;
  • 36~39行:如果按键KEY2_DOWN按下,修改mode_flag为2,准备进入停机模式;
  • 40~43行:如果按键KEY4_RIGHT按下,修改mode_flag为3,准备进入待机模式;
  1. 主函数控制逻辑

在主函数里,根据mode_flag标志位值,切换到相应低功耗模式,如代码段 30.3.2 所示。

代码段 3.3.2 主函数控制逻辑(main.c)

printf("#默认进入正常模式\n\r");
printf(" #按键 Up(KEY1)唤醒 \n\r");
printf("#按键 Left(KEY3)睡眠模式 #按键 Down(KEY2)停机模式 #按键 Right(KEY4)待机模式\n\r");

while(1)
{
	switch(mode_flag)
	{
		case 0:
		{
			// 正常模式三色灯闪烁
			printf("-->正常模式 \n\r");
			RLED(OFF);
			GLED(OFF);
			BLED(OFF);
			HAL_Delay(500);
			RLED(ON);
			GLED(ON);
			BLED(ON);
			HAL_Delay(500);
			
			break;
		}
		case 1:
		{
			printf("-->进入睡眠模式 \n\r");
			RLED(ON); // 只红色灯亮
			GLED(OFF);
			BLED(OFF);
			
			HAL_SuspendTick(); // 暂停滴答时钟,防止通过滴答时钟中断唤醒
			HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); // 进入睡眠模式
			
			break;
		}
		case 2:
		{
			printf("-->进入停机模式 \n\r");
			RLED(OFF);
			GLED(ON); // 只绿色灯亮
			BLED(OFF);
		
			HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_STOPENTRY_WFI); // 进入停机模式
			
		break;
		}
		case 3:
		{
			printf("-->进入待机模式 \n\r");
			RLED(OFF);
			GLED(OFF);
			BLED(ON); // 只蓝色灯亮
			
			__HAL_RCC_PWR_CLK_ENABLE(); // 使能 PWR 时钟
			HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // 使能 WKUP 引脚唤醒
			// 如果 WUKP 引脚已经为高,使能 WKUP 将产生一次唤醒事件,因此这里需要清除
			__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); // 清除 WKUP 标志
			
		if(__HAL_PWR_GET_FLAG(PWR_FLAG_SB) != RESET)
		{
			__HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB); // /清除待机模式标志
		}
		HAL_PWR_EnterSTANDBYMode(); // 进入待机 WSS 模式
			break;
		}
		default:
		{
			break;
		}
	}
}
  • 9~23行:mode_flag为0,处于正常模式,三色LED灯闪烁;
  • 24~35行:mode_flag为1,处于睡眠模式,只亮红色灯;
    • 31行:暂停滴答时钟,防止通过滴答时钟中断唤醒睡眠;
    • 32行:使用HAL提供的“HAL_PWR_EnterSLEEPMode()”进入睡眠模式,参数1设置电压调节器是处于正常运行还是低功耗运行状态,参数2设置“WFI(Wait for Interrupt)”方式进入睡眠模式,还是“WFE(Wait for Event)”方式进入睡眠模式;
  • 37~47行:mode_flag为2,处于停机模式,只亮绿色灯;
    • 44行:使用HAL提供的“HAL_PWR_EnterSTOPMode()”进入停机模式,参数与前面类似;
  • 48~68行:mode_flag为3,处于待机模式,只亮蓝色灯(进入待机模式后灯会停止工作);
    • 55行:使能PWR时钟;
    • 56行:使能WKUP引脚唤醒;
    • 57行:如果WUKP引脚已经为高,使能WKUP将产生一次唤醒事件,WUF会置1,这里清除该标志;
    • 60~63行:如果从待机模式启动,SBF会置1,这里清除该标志;
    • 64行:使用HAL提供的“HAL_PWR_EnterSTANDBYMode()”进入待机模式;

4. 实时时钟 —RTC

4.1关于RTC

实时时钟(Real Time Clock,RTC),是一个可以不使用系统主电源供电的定时器。在系统主电源断开的情况下,依靠纽扣电池供电继续计时,可以维持几个月到几年的持续工作。在系统恢复主电源工作时,为系统提供日历、时间信息。

举个常见的应用例子,比如台式电脑主机,必须接电源才能工作,此时可以正常显示当前日期、时间。电脑关机一段时间后,重新开机,系统没有联网,也能正确显示当前日期、时间。电脑主机里面也有一颗纽扣电池,在主电源断开时,为主板上的RTC模块供电,记录时间,开机后提供给系统。

RTC通常有两种,一种是外部时钟芯片提供实时时钟,比如DS1302时钟芯片;另一种是MCU内部集成RTC模块。STM32F103内部就集成了RTC模块,可以通过配置相应的寄存器来实现实时时钟的功能。

RTC位于后备供电区域,可以由系统主电源V DD 供电,也可由V BAT 供电。在硬件设计上,实现了优先V DD 供电,V DD 断开时,自动切换到V BAT 供电。也就说,无论是系统断电,还是处于任一低功耗模式,只要都V BAT 不断,都不影响RTC正常工作。只有当系统电源V DD 和纽扣电池V BAT 都断开时,RTC才停止工作。

除此之外,外部低速时钟(LSE)和BKP后备寄存器也都位于后备供电区域,也就意味着系统掉电,如果有纽扣电池供电,它们依旧可以正常工作。BKP后备寄存器中包含了42个16位的寄存器,共可保存84字节的数据,可以用于保存一些重要数据,但注意需要纽扣电池供电维持数据的保存。

RTC包含一个32位向上计数器,由前面“图 9.1.1 时钟树”可以得知RTC的时钟源有三个。第一个由外部高速时钟源(HSE)经过128分频得到,第二个来自外部低速时钟源(LSE),第三个来自内部低速时钟源(LSI)。只有LSE在系统主电源掉电时,可以由V BAT 供电,因此如果想RTC在主电源掉电也能运行,只能选择LSE提供时钟。

外部低速时钟(LSE)的晶振频率为32.768KHz,经过2 15 =32768分频后,刚好为1Hz,即计数器每计数一次,刚好就是一秒。
此外,当系统处于待机模式时,还能通过RTC闹钟实现唤醒。

如图 4.1.1 为RTC简化结构,可以分为两部分,位于后备供电区域的RTC核心接口(灰色)和APB1接口。

图 4.1.1 RTC 简化结构

RTC核心接口部分掉电时可在V BAT 供电时正常工作,该部分的所有寄存器都为16位,两个16位再组成一个32位RTC寄存器。

①RTC预分频器:RTCCLK来自LSE,为32.768KHz。RTC_PRL预分频装载寄存器,将RTCCLK分频为TR_CLK提供给RTC_CNT,分频计算公式为:
在这里插入图片描述
通常将PRL[19:0]设置为32767,这样f TR_CLK =1Hz,即1秒计数一次。
RTC_DIV预分频余数寄存器,是一个只读寄存器,它用于对RTCCLK计数。假设RTC_PRL为32767,首先将该值重装载到RTC_DIV,RTCCLK上升沿每发生一次,RTC_DIV值减1,直到减到0,也就是32768分频,输出TR_CLK,时间刚好为1s,然后再重装载RTC_DIV,如此反复。通过读取RTC_DIV值,也就可以获知经历了多少1/32768秒,得到更小的时间段。
如果在RTC_CR寄存器中使能了SECIE位(Second interrupt enable),则每个TR_CLK周期RTC会产生一个中断。通常TR_CLK周期为1秒,因此该中断也被称为秒中断。

②32位可编程计数器:RTC_CNT是一个32位可编程的向上计数器,使用TR_CLK为基准进行计数,计数满后,产生溢出标志位。从图中可以看到该事件没有连接到中断控制器,也就无法配置为中断信号,估计原因是当RTC_CLK周期为1秒,计数器从0溢出到0xFFFFFFFF,需要4294967295/60/60/24/365=136年,不至于运行个程序,136年后产生一次中断。

如果需要某一时间产生中断,可以设置RTC闹钟寄存器RTC_ALR,当RTC_ALR与RTC_CNT匹配,则产生闹钟标志,如果开启相应中断使能,则会触发中断。

③待机唤醒:上一章低功耗模式提到,待机模式唤醒的方式有四种,其中就有RTC闹钟唤醒。当RTC闹钟发生时,可以实现退出待机模式。

④RTC控制寄存器:MCU通过APB1总线与APB1接口的RTC控制寄存器RTC_CR进行通信。当系统电源掉电时,APB1接口停止工作,重新上电后,APB1接口和RTC后备供电区域的寄存器可能存在时钟不同步,因此读写RTC相寄存器前,必须先检查TRC_CR中的寄存器同步标志位RSF(Registers synchronized flag),确保该位置1,即RTC核心与APB1时钟同步。以下两种情况,可能会导致时钟不同步:

  • 发生系统复位或电源复位;
  • 从待机模式或停机模式唤醒;

Unix时间戳和时间转换。RTC本质是一个计数器,当计数周期TR_CLK为1秒时,计数器相当于一个秒表。将计数器为0的时刻定为计时元年,当前时间则为计时元年加上计数器计数值。

如今大多数操作系统都使用Unix时间戳,将格林威治时间1970年01月01日00时00分00秒(纪念Unix系统生日)作为计时元年,随后每增加1秒,Unix时间戳加1。

因此,只要将当前Unix时间戳写入RTC_CNT寄存器,以后任一时间读出计数值,即可得知对应时间。

4.2 硬件设计

如图 4.2.1 为开发板RTC部分的原理图,J5为纽扣电池座,需要插上配套的纽扣电池(型号:CR2032引脚间距:1.25mm 典型电压:3.2V)。VDD_3V3来自系统12V输入,VDD_BAT连接MCU的V BAT 供电引脚。

D4型号为BAT54C的双向二极管,当VDD_3V3和纽扣电池都通电时,VDD_3V3大于纽扣电池电压,VDD_3V3为VDD_BAT供电;当12V输入断开,VDD_3V3电压为0,此时纽扣电池为VDD_BAT供电。

  • 图 31.2.1 RTC 供电电路
    在这里插入图片描述

4.3 软件设计

4.3.1软件设计思路

实验目的:本实验通过使用RTC和BKP后备寄存器,让串口输出日期和时钟,且断电启动后能继续计时,体验RTC效果。

  1. 初始化RTC相关参数:配置时钟预分频;
  2. 初始化RTC涉及的硬件相关参数:选择RTC时钟源并使能相关时钟;
  3. 设置RTC时间,并将时间保存到BKP后备寄存器,以便后面获取;
  4. 设置RTC闹钟,使能RTC闹钟中断,编写中断回调函数;
  5. 主函数编写控制逻辑:传入Unix时间戳,换算成对应时间设置到RTC,并通过串口打印,当闹钟产生时,打印闹钟信息;

4.3.2 软件设计讲解

  1. 初始化定时器

RTC的相关参数设置比较少,只需设置预分频系数即可,如代码段 4.3.1 所示。

代码段 4.3.1 RTC 初始化(driver_rtc.c)

/*
* 函数名:void RTC_Init(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化 RTC
*/
void RTC_Init(void)
{
	hrtc.Instance = RTC;
	hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND; // 设置 RTC 预分频,范围(0~0xFFFFF)
	// 可设置为 RTC_AUTO_1_SECOND,将自动计算 1 秒周期对应的预分频值
	
	if (HAL_RTC_Init(&hrtc) != HAL_OK) // 初始化 RTC
	{
		Error_Handler();
	}
}

这里设置预分频为“RTC_AUTO_1_SECOND”,可自动获取时钟计算出1秒周期对应的预分频值。在“HAL_RTC_Init()”里会回调“HAL_RTC_MspInit()”初始化涉及的硬件操作,如代码段 4.3.2 所示。同时,该初始化函数会调用“HAL_RTC_WaitForSynchro()”, 完成APB1接口和RTC后备供电区域的寄存器同步。

代码段 4.3.2 RTC 硬件相关初始化(driver_rtc.c)

/*
* 函数名:void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
* 输入参数:hrtc-RTC 句柄
* 输出参数:无
* 返回值:无
* 函数作用:HAL_RTC_Init 回调硬件初始化
*/
void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
RCC_OscInitTypeDef RCC_OscInitStruct;
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct;

// 开启 LSE 时钟源
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE; // 选择要配置的振荡器 LSE
RCC_OscInitStruct.LSEState = RCC_LSE_ON; // LSE 状态:开启
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; // PLL 无配置
if(HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 配置设置的 RCC_OscInitStruct
{
printf("Error: LSE 时钟源开启失败 \n\r");
}

// 配置 RTC 时钟源为 LSE
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; // 选择要配置的外设 RTC
PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; // RTC 时钟源选择 LSE
if(HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) // 配置设置的 PeriphClkInitStruct
{
printf("Error: RTC 时钟源配置失败 \n\r");
}

// 启用 RTC 时钟
__HAL_RCC_RTC_ENABLE();
}
  • 14~20行:只有LSE在后备供电区域,RTC只有使用LSE才能在主电源断电时工作,这里开启LSE时钟源;
    • 14行:选择要配置的振荡器,选择LSE;
    • 15行:设置LSE状态,设置开启;
    • 16行:设置PLL,不配置;
    • 17行:配置以上LSE属性;
  • 23~28行:开启LSE时钟后,这里设置LSE为RTC时钟源;
    • 23行:选择要配置的外设,选择RTC;
    • 24行:选择RTC时钟源,选择LSE;
    • 25行:配置以上LSE属性;
    • 31行:使能RTC时钟;
  1. 设置RTC 时间

使用HAL库提供的日期设置函数“HAL_RTC_SetDate()”和时间设置函数“HAL_RTC_SetTime()”,实现对RTC的设置,同时使用BKP后备寄存器保存日期,以便复位后获取,如代码段 4.3.3 所示。
代码段 31.3.3 设置 RTC 时间(driver_rtc.c)

/*
* 函数名:void RTC_TimeAndDataInit(RTC_DateTimeTypeDef time)
* 输入参数:time-日期时间
* 输出参数:无
* 返回值:无
* 函数作用:初始化 RTC 的日期和时间
*/
void RTC_TimeAndDataInit(RTC_DateTimeTypeDef time)
{
	uint16_t bkup_dr1_value = 0;
	RTC_TimeTypeDef nRTC_TimeStructure;
	RTC_DateTypeDef nRTC_DateStructure;
	
	// 使能访问后备寄存器相关条件
	__HAL_RCC_PWR_CLK_ENABLE(); // 使能 PWR 电源时钟
	__HAL_RCC_BKP_CLK_ENABLE(); // 使能 BKP 后备寄存器时钟
	HAL_PWR_EnableBkUpAccess(); // 使能对 BKP 后备寄存器访问
	
	/* 将 time.Timestamp 时间戳 保存到后备寄存器 */
	bkup_dr1_value = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); //读取第一个后备寄存器内容
	
	// 通过读取后备寄存器 DR1 的值,判断是否为首次运行/写入,0x1515 为自定义值
	if(bkup_dr1_value != 0x1515) // 首次写入 设置时间和保存时间
	{
		// 准备日期数据
		nRTC_DateStructure.Year = time.RTC_DateStructure.Year = time.year - 2000;
		nRTC_DateStructure.Month = time.RTC_DateStructure.Month;
		nRTC_DateStructure.Date = time.RTC_DateStructure.Date;
		nRTC_DateStructure.WeekDay = RTC_WEEKDAY_MONDAY; // 星期会自动计算,这里传入任意值
		// 设置日期 (数据格式 BIN,即使用 10 进制表示,比如 20 秒 使用 20 表示)
		if(HAL_RTC_SetDate(&hrtc, &nRTC_DateStructure, RTC_FORMAT_BIN) != HAL_OK)
		{
			printf("Error: RTC 设置日期失败 \n\r");
		}
		
		// 准备时间数据
		nRTC_TimeStructure.Hours = time.RTC_TimeStructure.Hours;
		nRTC_TimeStructure.Minutes = time.RTC_TimeStructure.Minutes;
		nRTC_TimeStructure.Seconds = time.RTC_TimeStructure.Seconds;
		
		// 设置时间
		if(HAL_RTC_SetTime(&hrtc, &nRTC_TimeStructure, RTC_FORMAT_BIN) != HAL_OK)
		{
		printf("Error: RTC 设置时间失败 \n\r");
		}
		
		// 保存日期数据到后备寄存器 DR2~4
		HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, time.Timestamp&0xFFFF); //保存低 16 位
		HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR3, (time.Timestamp>>16)&0xFFFF); //保存高 16 位
		HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR4, time.TimeZone); //保存时区信息
		
		// 向后备寄存器 DR1 写入标记 0x1515,表示后备寄存器已经保存了数据
		HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x1515);
	}
	else // 之前已经写过后备寄存器
	{
		if (__HAL_RCC_GET_FLAG(RCC_FLAG_PORRST) != RESET)
		{
			printf("发生电源复位....\n\r");
		}
		else if (__HAL_RCC_GET_FLAG(RCC_FLAG_PINRST) != RESET)
		{
			printf("发生外部复位....\n\r");
		}
		__HAL_RCC_CLEAR_RESET_FLAGS(); // 清除复位标记
			
		// 读出后备寄存器保存的时间戳
		time.Timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) + \
			            (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3)<<16);
		time.TimeZone = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR4);
		Timestamp_To_Time(&time); // 时间戳转化为具体时间
			
		// 将日期恢复到 RTC
		if(HAL_RTC_SetDate(&hrtc, &time.RTC_DateStructure, RTC_FORMAT_BIN) != HAL_OK)
		{
			printf("Error: RTC 恢复日期失败 \n\r");
		}
	}
}
  • 15~17行:如果想访问后备寄存器,需依次使能PWR电源时钟、BKP后备寄存器时钟、使能访问权限;
  • 20行:获取第一个后备寄存器DR1的内容;
  • 23~54行:获取的后备寄存器DR1不是提前定义的0x1515,说明之前没有写过BKP寄存器,且RTC也没设置过时间,需要这里设置RTC时间;
    • 26行:设置年份;HAL库提供的“RTC_DateTypeDef”结构体中的Year,范围为0~99,2000年为起始年。因此,如果要设置2021年,只需设置为21即可;
    • 27行:设置月份;
    • 28行:设置号数;
    • 29行:设置星期,星期可以不用设置,HAL库会自动根据年月日,算出对应星期;
    • 31行~34行:使用“HAL_RTC_SetDate()”设置日期,参数3为数据格式,这里设置格式为“BIN”,即使用10进制表示,比如20秒,使用20表示;如果设置格式为“BCD”,即使用16进制表示,比如20秒,使用0x20表示;
    • 37~39行:设置时分秒;
    • 42~45行:使用“HAL_RTC_SetTime()”设置时间,依旧采用“BIN”格式;
    • 48~50行:将当前时间的Unix时间戳(32位),保存到DR1和DR2中,将时区信息保存到DR3中。这里保存Unix时间戳是因为里面包含日期信息,下次主电源断电重启后,可以从后备寄存器获取日期信息。“HAL_RTC_SetDate()”和“HAL_RTC_SetTime()”并不会将当前Unix时间戳值写到RTC计数器,RTC计数器依旧从0开始计数,因此只能从RTC计数器获取时间信息,不能保存日期信息。也就是说,掉电重启后,时分秒正常计数,而年月份变为默认的20000101。如果想掉电重启后,日期信息也存在,需要提前保存到BKP后备寄存器;
    • 53行:修改BKP后备寄存器DR1的值为0x1515,表示已经设置过时间,且保存过日期信息;
  • 55~78行:当掉电重启进入时间初始化,发现KP后备寄存器DR1的值已经为0x1515,说明已经设置过时间了,不需要使用初始值设置。而应该从后备寄存器获取值,进行设置;
  • 57~65行:检测上一次发生何种复位,并清除复位标志;
  • 68~70行:从后备寄存器获取Unix时间戳和时区信息;
  • 71行:将时间戳换算为具体时间,方便后面使用;
  • 74~77行:将年月日恢复到RTC;

设置完时间后,封装打印函数,间隔1秒将时间打印出来,如代码段 4.3.4 所示。

代码段 4.3.4 RTC 时间打印(driver_rtc.c)

/*
* 函数名:void RTC_TimeAndDate_Show(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:显示时间和日期
*/
void RTC_TimeAndDateDisp(void)
{
	uint8_t rtc_temp=1;
	uint16_t alarm_cnt = 0;
	
	RTC_TimeTypeDef nRTC_TimeStructure;
	RTC_DateTypeDef nRTC_DateStructure;
	
	RTC_DateTimeTypeDef curr_time = {
	.TimeZone = 8 * 60 *60,
	};
	while(1)
	{
		HAL_RTC_GetDate(&hrtc, &nRTC_DateStructure, RTC_FORMAT_BIN); // 从 RTC 获取日期
		HAL_RTC_GetTime(&hrtc, &nRTC_TimeStructure, RTC_FORMAT_BIN); // 从 RTC 获取时间
		
		// 每秒打印一次
		if(rtc_temp != nRTC_TimeStructure.Seconds) // 当 Seconds 加 1 时,不等成立
		{
			// 打印日期
			printf("%d-%02d-%02d %s ",
				nRTC_DateStructure.Year + 2000, nRTC_DateStructure.Month,
				nRTC_DateStructure.Date, weekday[nRTC_DateStructure.WeekDay]);
				
			// 打印时间
			printf("%02d:%02d:%02d ",
				nRTC_TimeStructure.Hours, nRTC_TimeStructure.Minutes,nRTC_TimeStructure.Seconds);
				
			// 打印时间戳
			curr_time.RTC_DateStructure = nRTC_DateStructure;
			curr_time.RTC_TimeStructure = nRTC_TimeStructure;
			curr_time.year = nRTC_DateStructure.Year + 2000;
			Time_To_Timestamp(&curr_time);
			printf("%u \n\r", curr_time.Timestamp);
			// 保存日期数据到后备寄存器 DR2 和 DR3
			HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, curr_time.Timestamp&0xFFFF); //保存低 16 位
			HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR3, (curr_time.Timestamp>>16)&0xFFFF); //保存高 16 位
			
			// 如果发生 RTC 闹钟
			if(alarm_flag == 1)
			{
				if( (alarm_cnt++) == 10) // 持续 10 秒
				{
					alarm_cnt = 0;
					alarm_flag = 0;
				}
				printf("DiDiDi...\n\r");
			}
		}
		rtc_temp = nRTC_TimeStructure.Seconds; // 打印完后,两者相等,等待下一次秒加 1
	}
}
  • 22行:使用“HAL_RTC_GetDate()”从RTC获取日期;

  • 23行:使用“HAL_RTC_GetTime()”从RTC获取时间;

  • 26行:只要RTC的秒增1,判断等式不成立,则进行后面打印;

  • 29~31行:打印年月日、星期,其中年需要加上2000,才是实际对应的年份;

  • 34~35行:打印时分秒;

  • 38~42行:将年月日、时分秒,换算成时间戳并打印;

  • 44~45行:每秒更新BKP后备寄存器的Unix时间戳,随时断电,日期信息也不会丢失;

  • 48~56行:当闹钟标志位发生变化,打印10秒闹钟信息;

  • 58行:打印完后,两者相等,等待下一次秒加1;

  1. 闹钟设置和中断处理

当闹钟时间等于计数时间,将产生RTC闹钟中断,在这之前需要先使能闹钟和设置闹钟时间,如代码段4.3.5 所示。

代码段 4.3.5 RTC 闹钟配置(driver_rtc.c)

/*
* 函数名:void RTC_AlarmSet(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:闹钟配置
*/
void RTC_AlarmSet(void)
{
	RTC_AlarmTypeDef RTC_AlarmStructure;
	
	HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 1, 0); // 设置中断优先级
	HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn); // 使能中断
	
	RTC_AlarmStructure.Alarm = RTC_ALARM_A; // 设置闹钟 ID
	RTC_AlarmStructure.AlarmTime.Hours = ALARM_HOURS; // 设置闹钟时间
	RTC_AlarmStructure.AlarmTime.Minutes = ALARM_MINUTES;
	RTC_AlarmStructure.AlarmTime.Seconds = ALARM_SECONDS;
	// 初始化中断方式闹钟 (数据格式 BCD,即使用 16 进制表示,比如 20 秒 使用 0x20 表示)
	HAL_RTC_SetAlarm_IT(&hrtc,&RTC_AlarmStructure, RTC_FORMAT_BCD);
}
  • 12~13行:设置RTC闹钟中断优先级和使能;
  • 15~20行:设置闹钟值;
    • 15行:设置闹钟ID,不重要;
    • 16~18行:设置闹钟时分秒;
    • 20行:初始化中断方式的RTC闹钟;

在中断回调函数修改闹钟标志位,以便主函数查询获知产生了RTC闹钟中断,进行相应的操作,如代码段 4.3.6 所示。

代码段 4.3.6 RTC 中断回调函数(driver_rtc.c)

/*
* 函数名:void RTC_Alarm_IRQHandler(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:RTC 闹钟中断的中断处理函数
*/
void RTC_Alarm_IRQHandler(void)
{
HAL_RTC_AlarmIRQHandler(&hrtc);
}

/*
* 函数名:void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
* 输入参数:hrtc-RTC 句柄
* 输出参数:无
* 返回值:无
* 函数作用:RTC 中断回调函数,修改闹钟标志位
*/
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
alarm_flag = 1;
}
  1. Unix 时间戳与时间转化

定义新结构体“RTC_DateTimeTypeDef”,包含HAL库的日期结构体、时间结构体、时区、Unix时间戳等,方便作为转换函数的参数,结构体如代码段4.3.7 所示。

代码段 4.3.7 日期时间结构体(driver_rtc.h)

typedef struct
{
	RTC_DateTypeDef RTC_DateStructure; // 日期结构体(其中 year 范围为 0~99)
	RTC_TimeTypeDef RTC_TimeStructure; // 时间结构体
	uint16_t year; // 完整年份 year=RTC_DateStructure.Year+2000
	uint16_t TimeZone; // 时区
	uint32_t Timestamp;// Unix 时间戳
	
} RTC_DateTimeTypeDef;

时间戳转换方式不是RTC重点,这里不赘述,源码如代码段4.3.8 所示

代码段 4.3.8 Unix 时间戳转换(driver_rtc.c)

/*
* 函数名:void Time_To_Timestamp(RTC_DateTimeTypeDef *time)
* 输入参数:*time-日期时间指针
* 输出参数:time->Timestamp-Uinx 时间戳

* 返回值:无
* 函数作用:将日期时间换算为 Uinx 时间戳
*/
void Time_To_Timestamp(RTC_DateTimeTypeDef *time)
{
	int month = time->RTC_DateStructure.Month;
	
	if (0 >= (month -= 2))
	{
		month += 12;
		time->year -= 1;
	}
	time->RTC_DateStructure.Month = month;
	
	time->Timestamp = ((((uint32_t)(time->year/4 - time->year/100 + time->year/400 + 367*time->RTC_DateStructure.Month
	/12 + \
		time->RTC_DateStructure.Date) + time->year*365 - 719499)*24 + time->RTC_TimeStructure.Hours)*60 + \
		time->RTC_TimeStructure.Minutes)*60 + time->RTC_TimeStructure.Seconds - time->TimeZone;
}
	
/*
* 函数名:void Timestamp_To_Time(RTC_DateTimeTypeDef *time)
* 输入参数:Timestamp-Uinx 时间戳
* 输出参数:*time-日期时间地址
* 返回值:无
* 函数作用:将 Uinx 时间戳换算为日期时间
*/
void Timestamp_To_Time(RTC_DateTimeTypeDef *time)
{
	uint16_t year = 1970;
	uint32_t Counter = 0, CounterTemp;
	uint8_t Month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	uint8_t i;
	
	time->Timestamp = time->Timestamp + time->TimeZone;
	
	while(Counter <= time->Timestamp)
	{
		CounterTemp = Counter;
		Counter += 31536000;
		if(Is_Leap_Year(year))
		{
		Counter += 86400;
		}
		year++;
	}
	
	time->year = year - 1;
	time->RTC_DateStructure.Year = time->year -2000;
	
	Month[1] = (Is_Leap_Year(time->year)?29:28);
	Counter = time->Timestamp - CounterTemp;
	CounterTemp = Counter/86400;
	Counter -= CounterTemp*86400;
	time->RTC_TimeStructure.Hours = Counter/3600;
	time->RTC_TimeStructure.Minutes = Counter%3600/60;
	time->RTC_TimeStructure.Seconds = Counter%60;
	
	for(i=0; i<12; i++)
	{
		if(CounterTemp < Month[i])
		{
			time->RTC_DateStructure.Month = i + 1;
			time->RTC_DateStructure.Date = CounterTemp + 1;
			break;
		}
		CounterTemp -= Month[i];
	}
}
/*
* 函数名:uint8_t Is_Leap_Year(uint16_t year)
* 输入参数:year-年
* 输出参数:无
* 返回值:1-闰年 0-平年
* 函数作用:判断年份数是否为闰年
*/
uint8_t Is_Leap_Year(uint16_t year)
{
	if(((year)%4==0 && (year)%100!=0) || (year)%400==0)
	return 1; //闰年
	return 0; //平年
}
  1. 主函数控制逻辑
    在主函数里,使用Unix时间戳定义一个初始时间,随后设置RTC和闹钟,如代码段 4.3.9 所示。
    代码段 4.3.9 主函数控制逻辑(main.c)
printf("**********************************************\n\r");
printf("-->xxx www.xxx.net\n\r");
printf("-->RTC 实时时钟实验\n\r");
printf("**********************************************\n\r");

// 定义某北京时间作为初始时间
RTC_DateTimeTypeDef beijing_time = {
	.RTC_DateStructure = {0}, // 日期结构体,用于存放年月日
	.RTC_TimeStructure = {0}, // 时间结构体,用于存放时分秒
	.TimeZone = 8*60*60, // 东八区(UTC/GMT+08:00)比世界协调时间 UTC/格林尼治时间 GMT 快 8 小时
	.Timestamp = 1609430400, // 2021-01-01 00:00:00 的时间戳
};
Timestamp_To_Time(&beijing_time); // 将时间戳转换成各年月日时间保持存在 RTC_DateStructure 和 RTC_TimeStructure

RTC_Init(); // RTC 初始化
RTC_TimeAndDataInit(beijing_time); // RTC 时间和日期初始化
RTC_AlarmSet(); // RTC 闹钟设置

RTC_TimeAndDateDisp(); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值