目录
[2.1 unix时间戳定义](#2.1 unix时间戳定义)
[2.2 时间戳与日历日期时间的转换](#2.2 时间戳与日历日期时间的转换)
[2.3 指针函数使用注意事项](#2.3 指针函数使用注意事项)
前言
STM32F103C8T6外部低速时钟LSE(一般为32.768KHz)用的引脚是PC14和PC15,所以这两个引脚一定不要再外接其它的电路,比如按键、LED灯之类的,会导致LSE时钟频率出错甚至不起振。
附上完整代码压缩包链接,包含所用到的用户手册和STM32实战手册:
https://jcnwdt8hb184.feishu.cn/wiki/AUXQwtZ6AipW16kUAn0cIi0anMe?form_wx_login=1
一、RTC基本硬件结构
具体的框图可以查看用户手册309页的图154。RTC的时钟输入源有三种选择,外部高速时钟(8MHz)128分频、外部低速时钟LSE(32.768KHz)、内部低速时钟LSI(40KHz)。只有选择LSE做时钟输入源,才能实现主电源掉电后,由电池(给VBAT供电)供电继续工作。
假设选择LSE做时钟输入源,预分频器系数PSC可以选择32767。DIV是一个向下递减的计数器,装载32767这个数值,每来一个时钟脉冲就减1,递减到0又重新装载32767,产生溢出事件,这样输出的时钟频率就是1Hz,对应周期1S。这个1S的时钟信号可以用来提供给32位的计数器CNT,每来一个1代表1S。所以CNT计数的数值就代表多少秒。
用户手册上的框图。RTCCLK就是上面说的三种时钟源之一,主要用来给CNT计数器计数用的;而PCLK1是用来,通过APB1接口获取以及写入寄存器数据用的。
以前用过的RTC时钟芯片DS1302,它是可以设置日历时间年月日等等的,而STM32F103C8T6的RTC却只有一个32位的CNT计数器。其实这个CNT计数器也可以理解为定时器,不过它和普通的定时器不同的是,当主电源掉电以后,它还能通过电池(给VBAT供电)工作。
下图是STM32F4系列的RTC框图,它就可以设置日历时间。
虽然STM32F103的RTC没有设置日历的功能,但是它便宜,那么,只有一个CNT计数器,如何把它转换为我们日常生活中需要的日历时间:年月日时分秒。
二、Unix时间戳
2.1 unix时间戳定义
RTC内的CNT计数器就可以用来存储时间戳,然后在软件内将时间戳转换为日历时间。
2.2 时间戳与日历日期时间的转换
时间戳转换为日历时间并不需要手撕代码,下面是C标准库提供的转换函数。划线的函数是裸机开发RTC常用的函数。
time_t是对uint32_t类型的重定义,struct tm是time.h头文件中定义的一个结构体,成员见下图,注意其中月的范围是0~11,所以写代码的时候要加1,年是从1900起的数值,所以年要加1900。
第一个函数time都是用在操作系统里面的,裸机开发用不了。
函数localtime能将时间戳也就是CNT秒计数器存的数值转换为日历时间,在内部已经自动写了闰年,大小月等等的判断。
2.3 指针函数使用注意事项
struct tm *localtime(const time_t *)是一个指针函数,对于指针函数,使用的时候要格外的注意,它返回的地址可能有下图中说明的三种情况,当然,基于高内聚低耦合原则,不会使用全局变量,那就只能是静态变量,或者用malloc,calloc在堆上申请的 内存空间。
如果这个函数用的是malloc申请的地址,那么在使用之后就必须使用free,否则会造成内存泄露。
如何知道函数使用的究竟是那种方法呢?由于没办法点开源文件,所以我们只能自己设法写代码验证。 可以看到,两次返回的地址都是一样的,说明函数使用的是静态变量。
也可以自己设计一种用malloc申请内存的方法,看看不同之处。可以看到,没有用free释放内存,导致两次打印的结果是不同的。
以后写代码越来越多,肯定会接触到很多指针函数,使用的时候都要小心,看看头文件里有没有说明,使用后需要用free释放内存,比如下面这个例子,这是ESP32的HAL开源库中的一个函数,他就使用了calloc申请地址,然后返回,这个时候就需要我们手动是否内存,否则就会造成内存泄露,而且这样的错误可能比较难排查(至少对我这种水平来说是这样)。
这种带creat的函数,要注意一般都是成对出现的,用delete就可以释放掉内存。
三、RTC和BKP硬件结构
下面是RTC硬件结构框图,可以看到,预分频器、计数器和闹钟都是位于后备区域,待机时维持供电。
下图是PN学堂GD32F303ZET6开发板上的RTC电路,当主电源3V3供电时,BAT54C内的2号二极管导通,1号截止,由3V3给VBAT供电;当主电源掉电时,1号导通2号截止,由3V的纽扣电池BT1给VBAT供电。
什么是后备区域呢 ?这就涉及到另一个片上外设BKP,在后备区域内,除了有之前提到的RTC的那些寄存器,还有42个2字节的寄存器用于存储并保护用户数据,比如说一些配置参数、系数可以放到这里面。
注意,这42个寄存器和内存一样是掉电丢失的,所以如果没有主电源供电了,那必须要有纽扣电池之类的给VBAT供电,它才可以工作。
该图是GD32F303ZET6的图。RTC信号输出和RTC校准:可以配置RTC信号输出寄存器,通过一个引脚(GD32F303ZET6是PC16)将RTC的时钟输出,然后去检测这个时钟信号,如果发现偏差较大,可以配置校准寄存器用来校准。
侵入检测寄存器作用,假如产品安全要求较高,不想让别人去拆、分析,就可以使用侵入检测。
四、驱动代码解析
就只有一个驱动函数,在rtc_drv.c文件中。我将基于寄存器,逐行分析。
#define MAGIC_CODE 0x5a5a//模码
/**
***********************************************************
* @brief RTC驱动初始化
* @param
* @return
***********************************************************
*/
void RtcDrvInit(void)
(1)这个函数内部就两行代码,其实不写这个函数也行。
RCC_APB1PeriphResetCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_PWR, DISABLE);
/*- - - - - - - -复位后备寄存器- - - - - - - - */
PWR_DeInit();
(2)开启时钟,就去用户手册找RCC_APB1ENR,就是把7.3.8 APB1 外设时钟使能寄存器(RCC_APB1ENR)的位28、27置1。
/*- - - - - - - -设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟- - - - - - - - */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
(3) 这个函数内部使用了位带操作,关于这部分的内容,在STM32实战手册中有详细说明,能看懂就行。
/*- - - - - - - -设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。- - - - - - - - */
PWR_BackupAccessCmd(ENABLE);
/下面是这个函数在库中的具体内容_________/
/**
* @brief Enables or disables access to the RTC and backup registers.
* @param NewState: new state of the access to the RTC and backup registers.
* This parameter can be: ENABLE or DISABLE.
* @retval None
*/
void PWR_BackupAccessCmd(FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_FUNCTIONAL_STATE(NewState));
*(__IO uint32_t *) CR_DBP_BB = (uint32_t)NewState;
}
/下面是这个函数中的某些变量具体内容_____/
//你必须要自己在keil中去查看才能看明白,这些内容只能用于辅助你理解
#define CR_DBP_BB (PERIPH_BB_BASE + (CR_OFFSET * 32) + (DBP_BitNumber * 4))
PERIPH_BB_BASE 就是位带别名区的首地址0x42000000,CR_OFFSET 就是我要配置的这个外设寄存器相当于位带区基地址的偏移量,DBP_BitNumber 就是我要配置外设寄存器中的第几位。
#define CR_OFFSET (PWR_OFFSET + 0x00)
#define PWR_OFFSET (PWR_BASE - PERIPH_BASE)
其中PWR_BASE就是要配置的外设寄存器的地址,去查看,地址是0x40007000,之后通过这个地址,在用户手册2.3存储器映像中去找对应的外设,发现是电源控制PWR。
通过#define DBP_BitNumber 0x08 可以知道配置的是第八位。也就是4.4.1 电源控制寄存器(PWR_CR)的第八位。
(4)打开LSE时钟,是配置用户手册7.3.9 备份域控制寄存器(RCC_BDCR)位0;
等待LSE稳定这部分库函数的代码写的很巧妙,值得仔细分析。
/*- - - - - - - -打开外部低速时钟LSE,并等待其稳定- - - - - - - - */
RCC_LSEConfig(RCC_LSE_ON);
while ( RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET );
/__下面是等待LSE稳定这个函数在库中的具体内容//**
* @brief Checks whether the specified RCC flag is set or not.
* @param RCC_FLAG: specifies the flag to check.
*
* For @b STM32_Connectivity_line_devices, this parameter can be one of the
* following values:
* @arg RCC_FLAG_HSIRDY: HSI oscillator clock ready
* @arg RCC_FLAG_HSERDY: HSE oscillator clock ready
* @arg RCC_FLAG_PLLRDY: PLL clock ready
* @arg RCC_FLAG_PLL2RDY: PLL2 clock ready
* @arg RCC_FLAG_PLL3RDY: PLL3 clock ready
* @arg RCC_FLAG_LSERDY: LSE oscillator clock ready
* @arg RCC_FLAG_LSIRDY: LSI oscillator clock ready
* @arg RCC_FLAG_PINRST: Pin reset
* @arg RCC_FLAG_PORRST: POR/PDR reset
* @arg RCC_FLAG_SFTRST: Software reset
* @arg RCC_FLAG_IWDGRST: Independent Watchdog reset
* @arg RCC_FLAG_WWDGRST: Window Watchdog reset
* @arg RCC_FLAG_LPWRRST: Low Power reset
*
* For @b other_STM32_devices, this parameter can be one of the following values:
* @arg RCC_FLAG_HSIRDY: HSI oscillator clock ready
* @arg RCC_FLAG_HSERDY: HSE oscillator clock ready
* @arg RCC_FLAG_PLLRDY: PLL clock ready
* @arg RCC_FLAG_LSERDY: LSE oscillator clock ready
* @arg RCC_FLAG_LSIRDY: LSI oscillator clock ready
* @arg RCC_FLAG_PINRST: Pin reset
* @arg RCC_FLAG_PORRST: POR/PDR reset
* @arg RCC_FLAG_SFTRST: Software reset
* @arg RCC_FLAG_IWDGRST: Independent Watchdog reset
* @arg RCC_FLAG_WWDGRST: Window Watchdog reset
* @arg RCC_FLAG_LPWRRST: Low Power reset
*
* @retval The new state of RCC_FLAG (SET or RESET).
*/
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG)
{
uint32_t tmp = 0;
uint32_t statusreg = 0;
FlagStatus bitstatus = RESET;
/* Check the parameters */
assert_param(IS_RCC_FLAG(RCC_FLAG));
/* Get the RCC register index */
tmp = RCC_FLAG >> 5;
if (tmp == 1) /* The flag to check is in CR register */
{
statusreg = RCC->CR;
}
else if (tmp == 2) /* The flag to check is in BDCR register */
{
statusreg = RCC->BDCR;
}
else /* The flag to check is in CSR register */
{
statusreg = RCC->CSR;
}
/* Get the flag position */
tmp = RCC_FLAG & FLAG_Mask;
if ((statusreg & ((uint32_t)1 << tmp)) != (uint32_t)RESET)
{
bitstatus = SET;
}
else
{
bitstatus = RESET;
}
/* Return the flag status */
return bitstatus;
}
/下面是分析,只是用于辅助理解,必须自己动手查看___//** @defgroup RCC_Flag
* @{
*/
#define RCC_FLAG_HSIRDY ((uint8_t)0x21)
#define RCC_FLAG_HSERDY ((uint8_t)0x31)
#define RCC_FLAG_PLLRDY ((uint8_t)0x39)
#define RCC_FLAG_LSERDY ((uint8_t)0x41)
#define RCC_FLAG_LSIRDY ((uint8_t)0x61)
#define RCC_FLAG_PINRST ((uint8_t)0x7A)
#define RCC_FLAG_PORRST ((uint8_t)0x7B)
#define RCC_FLAG_SFTRST ((uint8_t)0x7C)
#define RCC_FLAG_IWDGRST ((uint8_t)0x7D)
#define RCC_FLAG_WWDGRST ((uint8_t)0x7E)
#define RCC_FLAG_LPWRRST ((uint8_t)0x7F)//分析
这些数字设计的十分巧妙,高3位用于区分要配置哪个寄存器,低五位用于识别是配置寄存器中的第几位。根据高三位,黄色部分配置RCC_CR寄存器、绿色配置RCC_BDCR寄存器、蓝色配置RCC_CSR寄存器。
我们带入参数((uint8_t)0x41)分析,也就是说RCC_FLAG = ((uint8_t)0x41);
那么tmp = RCC_FLAG >> 5;结果是0x02,下面这个条件成立。
else if (tmp == 2) /* The flag to check is in BDCR register */
{
statusreg = RCC->BDCR;
}
语句 tmp = RCC_FLAG & FLAG_Mask;用于获取RCC_FLAG的低5位;用来识别是要配置寄存器中的第几位。也就是查看用户手册,7.3.9 备份域控制寄存器(RCC_BDCR)中的第1位是否被硬件置一了。
if ((statusreg & ((uint32_t)1 << tmp)) != (uint32_t)RESET)
{
bitstatus = SET;
}
(5)设置时钟源为LSE就是配置用户手册,7.3.9 备份域控制寄存器(RCC_BDCR)的bit9:8
使能时钟这个函数,里面也是用了位带操作,用同样的方法可以知道是对 7.3.9 备份域控制寄存器(RCC_BDCR)的bit15进行配置。
/*- - - - - - - -设置RTC时钟源为外部低速时钟LSE,并使能- - - - - - - - */
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
(6) 用户手册,16.4.2 RTC控制寄存器低位(RTC_CRL),查看位3是否被置1。
/*- - - - - - - -等待APB1接口时钟和RTC时钟同步- - - - - - - - */
RTC_WaitForSynchro();
(7)用户手册,16.4.2 RTC控制寄存器低位(RTC_CRL),查看位5是否被置1。
/*- - - - - - - -等待上次对 RTC 寄存器写操作完成- - - - - - - - */
RTC_WaitForLastTask();
(8)用户手册,16.4.3 RTC预分频装载寄存器(RTC_PRLH/RTC_PRLL),写入数值。
/*- - - - - - - -设置分频值32767- - - - - - - - */
RTC_SetPrescaler(32767);//32768-1
、、、、、、、、、函数具体内容
/**
* @brief Sets the RTC prescaler value.
* @param PrescalerValue: RTC prescaler new value.
* @retval None
*/
void RTC_SetPrescaler(uint32_t PrescalerValue)
{
/* Check the parameters */
assert_param(IS_RTC_PRESCALER(PrescalerValue));
RTC_EnterConfigMode();//16.4.2 RTC控制寄存器低位(RTC_CRL),位4置1
/* Set RTC PRESCALER MSB word */
RTC->PRLH = (PrescalerValue & PRLH_MSB_MASK) >> 16;//高16位写入0
/* Set RTC PRESCALER LSB word */
RTC->PRLL = (PrescalerValue & RTC_LSB_MASK);//低16位写入0x7fff
RTC_ExitConfigMode();//16.4.2 RTC控制寄存器低位(RTC_CRL),位4置0
}
(9)同之前
/*- - - - - - - -等待上次对 RTC 寄存器写操作完成- - - - - - - - */
RTC_WaitForLastTask();
(10)用户手册,16.4.5 RTC计数器寄存器 (RTC_CNTH / RTC_CNTL),写入数值。
/*- - - - - - - -设置时间1970-01-01 00:00:00- - - - - - - - */
RTC_SetCounter(0);
(11)向后备区域BKP的BKP_DR1寄存器中写入模码MAGIC_CODE,只要主电源供电或者VBAT有纽扣电池供电,那么即使复位,BKP寄存器中的内容也不会丢失。
BKP_WriteBackupRegister(BKP_DR1, MAGIC_CODE);
写入这个模码的作用是什么?在代码中,只有读取BKP_DR1中的内容与模码MAGIC_CODE相同时,才会执行上面讲述的所有代码。当设备第一次上电时,BKP_DR1中的内容肯定不是这个模码,就会执行这些初始化代码,而设备复位之后,由于BKP_DR1中已经有模码了,就不会再执行这些代码了。
有一个好处,如果复位后不执行这些代码,那么也就不会再初始化时间戳为0,我们CNT计数器中的时间戳就还是一直在计数的值。
if ( BKP_ReadBackupRegister(BKP_DR1) != MAGIC_CODE )
{
/*- - - - - - - -复位后备寄存器- - - - - - - - */
PWR_DeInit();
/*- - - - - - - -设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟- - - - - - - - */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
/*- - - - - - - -设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。- - - - - - - - */
PWR_BackupAccessCmd(ENABLE);
/*- - - - - - - -打开外部低速时钟LSE,并等待其稳定- - - - - - - - */
RCC_LSEConfig(RCC_LSE_ON);
while ( RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET );
/*- - - - - - - -设置RTC时钟源为外部低速时钟LSE,并使能- - - - - - - - */
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
/*- - - - - - - -等待APB1接口时钟和RTC时钟同步- - - - - - - - */
RTC_WaitForSynchro();
/*- - - - - - - -等待上次对 RTC 寄存器写操作完成- - - - - - - - */
RTC_WaitForLastTask();
/*- - - - - - - -设置分频值32767- - - - - - - - */
RTC_SetPrescaler(32767);//32768-1
/*- - - - - - - -等待上次对 RTC 寄存器写操作完成- - - - - - - - */
RTC_WaitForLastTask();
/*- - - - - - - -设置时间1970-01-01 00:00:00- - - - - - - - */
RTC_SetCounter(0);
BKP_WriteBackupRegister(BKP_DR1, MAGIC_CODE);
return;
}
(12) 那么,复位后要执行的初始化代码是哪些呢?为什么是这些代码需要执行呢?
/*- - - - - - - -设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟- - - - - - - - */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
/*- - - - - - - -设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。- - - - - - - - */
PWR_BackupAccessCmd(ENABLE);
/*- - - - - - - -等待APB1接口时钟和RTC时钟同步- - - - - - - - */
RTC_WaitForSynchro();
/*- - - - - - - -等待上次对 RTC 寄存器写操作完成- - - - - - - - */
RTC_WaitForLastTask();
其它代码不用执行的原因可以参考下图
五、其它代码部分解析
/**
***********************************************************
* @brief 设置时间
* @param time,输入,日历时间
* @return
***********************************************************
*/
void SetRtcTime(RtcTime_t *time)
{
time_t timeStamp;//时间戳
struct tm timeInfo;
memset(&timeInfo, 0, sizeof(timeInfo));//结构体初始化
timeInfo.tm_year = time->year - 1900;
timeInfo.tm_mon = time->month - 1;
timeInfo.tm_mday = time->date;
timeInfo.tm_hour = time->hour;
timeInfo.tm_min = time->minute;
timeInfo.tm_sec = time->second;
timeStamp = mktime(&timeInfo) - 8 * 60 * 60;
/*等待上次对 RTC 寄存器写操作完成*/
RTC_WaitForLastTask();
/*设置时间*/
RTC_SetCounter(timeStamp);//因为这里面是基于零时区实现的,要想得到东八区即北京时间,时间戳就要减8*60*60S
}
/**
***********************************************************
* @brief 获取时间
* @param time,输出,日历时间
* @return
***********************************************************
*/
void GetRtcTime(RtcTime_t *time)
{
time_t timeStamp;
struct tm* timeInfo;
timeStamp = RTC_GetCounter() + 8 * 60 * 60;
timeInfo = localtime(&timeStamp);
time->year = timeInfo->tm_year + 1900;
time->month = timeInfo->tm_mon + 1;
time->date = timeInfo->tm_mday;
time->hour = timeInfo->tm_hour;
time->minute = timeInfo->tm_min;
time->second = timeInfo->tm_sec;
}
在函数void SetRtcTime(RtcTime_t *time)中,有这样一行代码:
timeStamp = mktime(&timeInfo) - 8 * 60 * 60;
而在函数void GetRtcTime(RtcTime_t *time)中,却是这样一行代码:
timeStamp = RTC_GetCounter() + 8 * 60 * 60;
在函数void SetRtcTime(RtcTime_t *time)中,假如RtcTime_t *time成员的值为2001-9-9 9:46:40,那么通过mktime(&timeInfo)获得的零时区时间戳就是B,因为这些函数都是基于零时区的。我们希望东八区即北京时间的日历时间是通过零时区添加偏移得到的,那么时间戳B减去8个小时的偏移,就得到2001-9-9 9:46:40的东八区时间戳1000000000。
假设,我们已经在主函数中写了如下代码。
int main(void)
{
DrvInit();
AppInit();
RtcTime_t time = {2001, 9, 9, 9, 46, 40};
SetRtcTime(&time);
while(1)
{
TaskHandler();
}
}
那么,在函数void GetRtcTime(RtcTime_t *time)中,RTC_GetCounter()得到的零时区时间戳就是1000000000。而localtime(&timeStamp);也是基于零时区进行转换的,如果timeStamp就是1000000000的话,转换的日历时间就是2001-9-9 1:46:40。但如果RTC_GetCounter()得到的时间戳加上8个小时的时区偏移量,那么得到的时间戳就是零时区时间戳B,timeInfo = localtime(&timeStamp);就得到零时区日历时间2001-9-9 9:46:40。