Qurak

Make Code Talk

0%

STM32之HAL库学习——电子时钟项目(1)

——Jan.23.2021

目录

本次项目采用正点原子STM32F407探索者开发版和STM32cubeIDE软件开发

  • STM32的RTC时钟功能简单介绍
  • 电子时钟项目计划
  • 开发过程、遇到的问题和解决方案

一、STM32的RTC时钟功能简单介绍

(1)RTC介绍

RTC(Real-Time Clock)是一个独立的BCD计数器。RTC可以提供一个日历时钟两个可编程闹钟中断以及一个具有中断功能的周期性可编程唤醒标志。

在F407中的RTC功能是比较强大的,两个32位的寄存器包含了秒、分、小时、星期、日期、月份和年份(BCD表示)。还提供了二进制格式的亚秒值。系统可以自动将补偿闰年的天数,还可以进行夏令时的补偿。此外还可以用数字校准功能对晶振校准。

⚠️:上电复位后,所有RTC寄存器都会收到保护,以防止可能的非正常写访问。无论什么情况下,只要电源保持在工作范围内,RTC便不会停止工作。

(2)功能说明

最近刚开始接触STM32的RTC功能,在研究手册和相关教程的时候萌生了自制电子显示时钟的想法,我会把我的研究记录在这里,RTC功能相对较为丰富,建议找到官方的用户手册研究一下,这里我在用到特定功能的时候也会简要介绍一下。

  • 时钟树和预分频:RTC时钟源从LSE时钟、LSI振荡器时钟以及HSE时钟三者中选择。时钟的预分频分为两个可编程的预分频器,可以用RTC_PRER寄存器的PREDIV_A(7位异步与分频器)和PREDIV_S配置(15位同步预分频器)配置,但这边需要注意一点:如果同时用两个与分频器且需要最大程度较低功耗,推荐将异步预分频器配置为较高的值。各个分频的结果有:

    公式

    其中fCK_Spre既可用于更新日历,也可以用做16位唤醒自动充装在定时器的时基。

  • 实时时钟和日历RTC_SSR对应亚秒,RTC_TR对应时间,RTC_DR对应日期。每隔两个RTCCLK周期,日历值就会复制到影子寄存器中,并将RTC_ISR寄存器的标志位RSF为1。在停机和待机模式下不会执行复制操作。推出这两种模式时,影子寄存器会2个RTCCLK周期内进行更新。读取日历寄存器时一般访问影子寄存器,但也可以设置RTC_CRBYPSHAD控制位为1来直接访问日历里寄存器。同时,当BYPSHAD=0模式下读取数据,APB时钟频率至少为RTC时钟频率的7倍。一般正常配置时钟都会满足该要求。

(3)寄存器

完整的配置和注意要求可以参考中文手册。这里提一下几个重要的寄存器:

  • 时间寄存器、日期寄存器(RTC_TR, RTC_DR):初始化参数值以及设置格式,只能初始化的时候写操作。
  • 控制寄存器(RTC_CR):控制RTC内部的各个功能使能。
  • 初始化和状态寄存器(RTC_ISR):检测是否初始化以及检查当前状态。
  • 预分频寄存器写保护寄存器校准寄存器

(4)HAL库函数说明

采用HAL库最大的优点在于许多基本操作已经帮你封装成一个个规整的函数,因此我们需要了解这些函数以及调用相应的函数就能实现大部分功能。下面介绍一下会用到的几个HAL库的函数。

首先介绍一下时间和日期的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Time
typedef struct
{
uint8_t Hours; /*!< Specifies the RTC Time Hour.
This parameter must be a number between Min_Data = 0 and Max_Data = 12 if the RTC_HourFormat_12 is selected.
This parameter must be a number between Min_Data = 0 and Max_Data = 23 if the RTC_HourFormat_24 is selected */

uint8_t Minutes; /*!< Specifies the RTC Time Minutes.
This parameter must be a number between Min_Data = 0 and Max_Data = 59 */

uint8_t Seconds; /*!< Specifies the RTC Time Seconds.
This parameter must be a number between Min_Data = 0 and Max_Data = 59 */

uint8_t TimeFormat; /*!< Specifies the RTC AM/PM Time.
This parameter can be a value of @ref RTC_AM_PM_Definitions */

uint32_t SubSeconds; /*!< Specifies the RTC_SSR RTC Sub Second register content.
This parameter corresponds to a time unit range between [0-1] Second
with [1 Sec / SecondFraction +1] granularity */

uint32_t SecondFraction; /*!< Specifies the range or granularity of Sub Second register content
corresponding to Synchronous pre-scaler factor value (PREDIV_S)
This parameter corresponds to a time unit range between [0-1] Second
with [1 Sec / SecondFraction +1] granularity.
This field will be used only by HAL_RTC_GetTime function */

uint32_t DayLightSaving; /*!< Specifies DayLight Save Operation.
This parameter can be a value of @ref RTC_DayLightSaving_Definitions */

uint32_t StoreOperation; /*!< Specifies RTC_StoreOperation value to be written in the BCK bit
in CR register to store the operation.
This parameter can be a value of @ref RTC_StoreOperation_Definitions */
}RTC_TimeTypeDef;
// Date
typedef struct
{
uint8_t WeekDay; /*!< Specifies the RTC Date WeekDay.
This parameter can be a value of @ref RTC_WeekDay_Definitions */

uint8_t Month; /*!< Specifies the RTC Date Month (in BCD format).
This parameter can be a value of @ref RTC_Month_Date_Definitions */

uint8_t Date; /*!< Specifies the RTC Date.
This parameter must be a number between Min_Data = 1 and Max_Data = 31 */

uint8_t Year; /*!< Specifies the RTC Date Year.
This parameter must be a number between Min_Data = 0 and Max_Data = 99 */

}RTC_DateTypeDef;

/**
* @brief RTC Alarm structure definition
*/
typedef struct
{
RTC_TimeTypeDef AlarmTime; /*!< Specifies the RTC Alarm Time members */

uint32_t AlarmMask; /*!< Specifies the RTC Alarm Masks.
This parameter can be a value of @ref RTC_AlarmMask_Definitions */

uint32_t AlarmSubSecondMask; /*!< Specifies the RTC Alarm SubSeconds Masks.
This parameter can be a value of @ref RTC_Alarm_Sub_Seconds_Masks_Definitions */

uint32_t AlarmDateWeekDaySel; /*!< Specifies the RTC Alarm is on Date or WeekDay.
This parameter can be a value of @ref RTC_AlarmDateWeekDay_Definitions */

uint8_t AlarmDateWeekDay; /*!< Specifies the RTC Alarm Date/WeekDay.
If the Alarm Date is selected, this parameter must be set to a value in the 1-31 range.
If the Alarm WeekDay is selected, this parameter can be a value of @ref RTC_WeekDay_Definitions */

uint32_t Alarm; /*!< Specifies the alarm .
This parameter can be a value of @ref RTC_Alarms_Definitions */
}RTC_AlarmTypeDef;

然后是读取时间和日期的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** The first function is HAL_RTC_GetTime
* @brief Gets RTC current time.
* @param hrtc pointer to a RTC_HandleTypeDef structure that contains
* the configuration information for RTC.
* @param sTime Pointer to Time structure
* @param Format Specifies the format of the entered parameters.
* This parameter can be one of the following values:
* @arg RTC_FORMAT_BIN: Binary data format
* @arg RTC_FORMAT_BCD: BCD data format
* @note You can use SubSeconds and SecondFraction (sTime structure fields returned) to convert SubSeconds
* value in second fraction ratio with time unit following generic formula:
* Second fraction ratio * time_unit= [(SecondFraction-SubSeconds)/(SecondFraction+1)] * time_unit
* This conversion can be performed only if no shift operation is pending (ie. SHFP=0) when PREDIV_S >= SS
* @note You must call HAL_RTC_GetDate() after HAL_RTC_GetTime() to unlock the values
* in the higher-order calendar shadow registers to ensure consistency between the time and date values.
* Reading RTC current time locks the values in calendar shadow registers until current date is read.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
/** The second function is HAL_RTC_GetDate
* @brief Gets RTC current date.
* @param hrtc pointer to a RTC_HandleTypeDef structure that contains
* the configuration information for RTC.
* @param sDate Pointer to Date structure
* @param Format Specifies the format of the entered parameters.
* This parameter can be one of the following values:
* @arg RTC_FORMAT_BIN: Binary data format
* @arg RTC_FORMAT_BCD: BCD data format
* @note You must call HAL_RTC_GetDate() after HAL_RTC_GetTime() to unlock the values
* in the higher-order calendar shadow registers to ensure consistency between the time and date values.
* Reading RTC current time locks the values in calendar shadow registers until Current date is read.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)

二、电子时钟项目计划

项目计划

在研究RTC的时候发现,教程入门基本结束了,然后萌生了制作一个自制电子时钟的想法,并列出了一些计划,之后会断断续续地续上更新(如果我还没放弃的话233)。

项目计划

Step1:显示时钟

(1)初始化配置

新建一个Project,取名为“RTC Display”然后我们选择好芯片开始配置:

  • 显示模块:需要启用FSMC和PB15。(详情可参考上一个LCD显示教程)

  • RTC模块:需要启用内部低速晶振LSE选项,STM32F407中这个晶振的频率为32.768KHz。然后使能RTC的时钟源和日期。这里由于没有涉及到闹钟和唤醒,就不用使能闹钟和唤醒。查看下面设置Configuration可以设置参数。这里时间设置为我开始项目的日期。时间就暂时设置为12时00分00秒(24小时制)。

  • 注意⚠️:这里建议将数值格式设置为二进制(Binary)方便后续数值调用和计算。

    LSE时钟源
    • LSE设置
    时钟树配置
    • 时钟树设置

    • RTC参数相关设置:

      • 其中异步分频和同步分频分别默认即可(32768 = (127+1) x (255+1) 是自动设置为 1Hz )
      • 时间格式建议设置为二进制Binary data format,方便后面计算和输出。
      • ⚠️注意:这里设置参数的格式有十进制和十六进制和不检查no check,但是这里尝试了一下好像选择十进制和十六进制生成代码的时候都是默认16进制。这里建议选择no check然后直接输入十进制数即可。(PS:如果要写成16进制则要加前缀0x)

然后就基本设置好了,我们点击Generate Code开始工程。

(2)显示模块导入

在上一篇教程中有比较详细的示范(缝合),废话不多说,直接开始。

  • 首先我们找到之前准备好的LCD驱动文件,我这边一共是三个:lcd.c, lcd.h, font.h然后复制到我们的工作去(workplace)中,在IDE中点击刷新即可看到我们新增的文件,这里我是放在MyFunction中。然后就是打开工程设置,找到路径选项,在编译路径中将LCD的文件夹添加,然后点击源文件(source)再将lcd.c添加。

  • 然后我们在初始化的位置添加LCD初始化函数LCD_Init(),点击编译看是否已经完成导入。若没有报错就说明可以用了。我们就可以调用驱动中的函数进行显示了。

    1
    2
    3
    4
    5
    6
    7
    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_FSMC_Init();
    MX_RTC_Init();
    /* USER CODE BEGIN 2 */
    LCD_Init(); //LCD初始化函数
    /* USER CODE END 2 */
  • PS:如果想要在单片机上看到效果,还需要打开背光源PB15引脚输出高电平才可以。因此要添加一句HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);

    后来想了想,考虑到目前只用这个单片机以及今后方便,我们可以直接在LCD_Init()中,在最后几行的位置将本来被注释掉打开背光代码替换为HAL库函数形式,这样以后就不用在初始化后再加一个HAL_GPIO_WritePin了。

    1
    2
    3
    4
    5
    6
    ...
    LCD_Display_Dir(0); //1为横屏,0为竖屏
    //LCD_LED=1; //点亮背光
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET); //替换后的代码
    LCD_Clear(WHITE);
    ...

(3)RTC时钟获取和显示

生成完代码后,我们可以在main.c中检查RTC初始化函数MX_RTC_Init()看是否参数初始化正确。检查无误即可开始本次时钟显示的核心函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
//代码自动生成的句柄 hrtc
RTC_HandleTypeDef hrtc;
//我们定义的两个变量,为了方便后续的显示更新设置为全局变量time和date
static RTC_TimeTypeDef time;
static RTC_DateTypeDef date;
//需要调用的函数
HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);
/** 其中需要注意两点
* 1、为了确保获取值的准确性,在调用GetDate之前必须先调用GetTime才可以,这个在函数说明中有提到。
* 2、注意获取的格式为二进制RTC_FORMAT_BIN,这样数据格式才不会混淆
*/

实现方案

根据对项目和单片机的理解,一开始对时钟显示有几个方案

  • 利用唤醒(wakeup)功能实现秒中断,在中断服务函数中重新获取time和date的值然后再输出显示。
  • 利用大循环while(1)进行快速刷新,然后以较快的频率检测时间,并保存到time和date中,最后在进行快速刷新显示。
  • 在大循环中采用HAL_Delay(1000)函数实现每秒检测。

其中方案一每秒中断获取时间是可行的,但是通过中断服务函数来进行显示可能会导致显示的时间稍有延迟(显示读写显存需要一定时间)以及考虑到后续添加功能需要区分中断优先级,可能对未来有影响,因此暂时不采用;而方案二采用快速刷新的手段,加上全局变量改变参数time和date,虽然相比于方案一对CPU占用较大,但是目前来说CPU也挺闲的~~(不是)~~影响不大;至于方案三,由于HAL_Delay()函数的时基是由系统时钟所决定的,因此在定时的时候并没有专业的RTC来的高精度高稳定性以及自动校准补偿功能,时间相对粗略,并不合适。综合考虑,采用方案二。

为了让代码结构比较简洁,我设置了一个显示函数:

1
2
3
4
5
6
7
8
9
10
//在固定范围刷新显示
void UpdateDisplay(RTC_TimeTypeDef t, RTC_DateTypeDef d)
{
LCD_ShowNum(50, 150, d.Year + 2000, 4, 24);
LCD_ShowNum(125, 150, d.Month, 2, 24);
LCD_ShowNum(175, 150, d.Date, 2, 24);
LCD_ShowNum(50, 200, t.Hours, 2, 24);
LCD_ShowNum(100, 200, t.Minutes, 2, 24);
LCD_ShowNum(150, 200, t.Seconds, 2, 24);
}

然后在大循环中调用

1
2
3
4
5
6
7
8
9
10
11
12
 /* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
UpdateDisplay(time,date); //刷新显示内容
HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN); //更新time的值
HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN); //更新date的值
HAL_Delay(50); //延时50毫秒,屏幕刷新率为20Hz
}
/* USER CODE END 3 */

最后的成果,主要参数都能正确的显示,刷新也很流畅。而且很显然,只需要再加入亿点细节就可以了~(笑哭

时钟显示

最后通过手机的秒表功能辅助验证计时是否准确。

三、开发过程遇到的问题和解决方案

  • 保存格式错误产生的显示错误:保存格式应为二进制形式,如果保存为BCD码格式,则读取时间后仍是BCD码形式但是处理器仍是以二进制形式进行处理,导致数字显示不对。同时GetTimeGetDate两个函数的读取格式也应该设置为RTC_FORMAT_BIN。我设置完初始化格式为二进制后忘了设置读入格式,导致最后显示仍为不正确的。
  • 时钟源选择不正确:时钟一定要用内部晶振LSE,如果采用振荡电路产生的32K时钟信号,时钟是不稳定的。实际测试下来每20秒会慢4秒左右。所以首先要在RCC中打开LSE,然后在时钟树中将RTC时钟源设置为LSE。
  • 显示写入操作逻辑不正确:在流畅显示时一开始以为LCD写入显存会叠加,所以每次更新前都会调用函数LCD_Clear(WHITE)清空屏幕,再显示新的内容,但是这样做会明显加重CPU负担,导致屏幕更新间隔卡顿十分明显,闪烁的显示令人不适。后来仔细研究了一下显存和显示,发现写入显存是覆盖的操作,因此只需要在同个位置写入新的符号,就会替换掉原先的符号,所以应该把清空屏幕的LCD_Clear(WHITE)函数去掉,这样即便是20Hz的刷新率,时间变化显示也是很流畅的。如果为了降低功耗可以进一步降低频率。
  • 复位和开机程序错误:一般情况下,我们下载好程序之后单片机断电和复位后仍可以重新开始。但是在实际操作的时候发现了按下复位或者断电重启之后,程序会陷入一个混沌状态。最后上网查询资料发现可能是因为复位和重启后,软硬件初始化还没有完全稳定,直接进入程序可能会导致配置不正确。在程序的开头增加一个延时10ms左右即可解决问题,但不知道是否还有其他解决方法。

总结一下,这次试一次全新的尝试,以前都是跟着教程或者网上现成的代码配置照抄一下就好了,这次却是从一个小项目的想法开始,然后去网上收集许多的资料,再通过阅读芯片的用户手册来配置引脚。从上次的LCD显示开始,编写程序和引脚配置更多的需要自己的对于单片机的理解和判断,以及程序也需要自己从无到有的编写。许多方案都可以实现的情况下需要一个个去尝试和修改,最终选择出一个适合自己的方案。我认为这种模式下的学习效率似乎会更高一点,虽然辛苦了一点,但是总体来说还是十分有趣的。

后续还会继续按照计划增加新的功能……敬请期待~~(咕咕咕)~~


END