我的网站织梦,西安网站优化seo,重庆潼南网站建设,提供网站建设和制作目录 前言
一、RTC基本硬件结构
二、Unix时间戳
2.1 unix时间戳定义
2.2 时间戳与日历日期时间的转换
2.3 指针函数使用注意事项
三、RTC和BKP硬件结构 四、驱动代码解析 前言 STM32F103C8T6外部低速时钟LSE#xff08;一般为32.768KHz#xff09;用的引脚是PC14和PC…
目录 前言
一、RTC基本硬件结构
二、Unix时间戳
2.1 unix时间戳定义
2.2 时间戳与日历日期时间的转换
2.3 指针函数使用注意事项
三、RTC和BKP硬件结构 四、驱动代码解析 前言 STM32F103C8T6外部低速时钟LSE一般为32.768KHz用的引脚是PC14和PC15所以这两个引脚一定不要再外接其它的电路比如按键、LED灯之类的会导致LSE时钟频率出错甚至不起振。 附上完整代码压缩包链接包含所用到的用户手册和STM32实战手册
https://jcnwdt8hb184.feishu.cn/wiki/AUXQwtZ6AipW16kUAn0cIi0anMe?form_wx_login1
一、RTC基本硬件结构 具体的框图可以查看用户手册309页的图154。RTC的时钟输入源有三种选择外部高速时钟8MHz128分频、外部低速时钟LSE32.768KHz、内部低速时钟LSI40KHz。只有选择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 就是位带别名区的首地址0x42000000CR_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个小时的时区偏移量那么得到的时间戳就是零时区时间戳BtimeInfo localtime(timeStamp);就得到零时区日历时间2001-9-9 9:46:40。