今天我们来搞一下I.MX6UL的RTC,这个RTC确切来说是个SRTC。要注意点是,6U和6ULL的RTC在48章SNVS里,但是6ULL里并没有讲清楚RTC相关功能的寄存器,反而在6U的手册里写明白了。主要是因为SNVS有些内容是和加密有关的,里面的具体内容要和恩智浦签订NDA协议才能拿到具体内容。所以6U和6ULL手册里的SNVS里的内容是有区别的,所以这一章的内容基本上是参考了IMX6UL的参考手册总结的。
SNVS
SNVS(Secure Non-Volatile Storage)从字面上来看就是安全的非易失性存储。I.MX6U的SNVS分了SNVS_HP和SNVS_LP两个组件,我们用到的RTC属于SNVS_LP,它是一个备用电池供电的低功耗模块,包括一个安全的实时计数器和与1个通用计数器,当整个Soc掉电时,这个模块通过电池供电,SNVS_LP寄存器继续保持计数功能。整体架构图如下
按照上面的图来看,我想不明白为什么要做一套高功耗的SNVS,在系统掉电以后是无法保存数据的,还怎么个非易视数据?还没搞清其中用法。
RTC使用
RTC一般使用32.768MHz的晶振,经过2^15=32767,对应32788分频能拿到1Hz的时钟源。I.MX的RTC使用很简单,打开RTC,RTC就开始工作了,我们只需要读取RTC计数器的值来换算时间就可以了,计数器的值保存在两个寄存器里。其实I.MX还给RTC提供了中断等功能,但是我们只用他来做一个最简单的RTC计时用,和PC主板上BIOS上那个电池记录时间底效果一样。
NXP的RTC搞得比较诡异,他不叫RTC,叫做HP和LP,LP对应是SRTC,HP对应RTC,但是如果我们使用的是HP(RTC)在系统掉电以后,即便板子上装了纽扣电池,重新上电以后还是初始时间(时间戳,从1970年1月1日0时0分0秒)。并且Soc上的RTC一般精度都比较差,我们就大致了解一下功能就可以了。整个RTC使用只有几个寄存器可操作,连CCM里的时钟树对他都没有涉及(我估计RTC是通过另外的晶振提供的时钟,和24MHz那个时钟源没关系)。
启动RTC
SNVS_LPCR寄存器bit[0]置1。
时间戳的获取和设置
这里按照6U参考手册获取时间戳的方法有些问题:按照手册上所说,LPSRTCMR是SRTC的高15位,LPSRTCLR是SRTC的低32位(IMX6UL手册46.7.22,46.7.23),对应RTC计数器是15+32=47位
但是按照这个思路取出来时间是混乱的。大佬直接给出了解决方案:LPSRTCLR寄存器的bit[31:15]是SRTC的低17位,LPSRTCMR是计数器的高15位。也就是SRTC计数器是32位的。这个方法是参考NXP给出的SDK里提供的SNVS_HP的驱动得到的。
并且在Linux内核里也是这样操作都(内核里的rtc是取了47位但是右移了15位只保留了32位)。
常用寄存器
RTC相关寄存器只有3个
SNVS_HPCOMR
寄存器结构如下表
里面主要用到的就是bit[31]
如果非权限的软件想要读取SNVS寄存器的值,就要把这个bit置1,还有bit[8]也是和权限相关的,教程上给的建议是置1。
还有就是前面的LPSRTCMR和LPSRTCCLR两个保存计数器值的寄存器。注意的是:
- LPSRTCMR[14:0]是SRTC计数器的高15位
- LPSRTCLR[31:15]是SRTC计数器的低17位
是不是很绕,这个一定要记清楚!
RTC使用
这个使用实在没什么可说的,一共就三个寄存器。但是里面有些算法的内容:通过时间戳转换成日期(还要考虑到闰年),通过格式化的日期换算成时间戳(这个时候真心怀念Pyhton的好了!)。整个代码放下来:
/** * @file bsp_rtc.c * @author your name (you@domain.com) * @brief RTC驱动 * @version 0.1 * @date 2022-01-20 * * @copyright Copyright (c) 2022 * */ #include "bsp_rtc.h" /** * @brief RTC初始化 * */ void rtc_init(void) { SNVS->HPCOMR |= (1<<31) | (1<<8); /*下面的部分为给RTC赋个初始值,实际过程放在按钮触发的中断里了*/ #if 0 struct rtc_datetime Date_init; Date_init.year = 2022; Date_init.month = 1; Date_init.day = 19; Date_init.hour = 2; Date_init.minute = 13; Date_init.minute = 12; rtc_setvalue(&Date_init); #endif rtc_enable(); } /** * @brief rtc使能 * */ void rtc_enable(void) { SNVS->LPCR |= (1<<0); while((SNVS->LPCR & 0x01) == 0);//等待使能成功 } /** * @brief rtc禁止 * */ void rtc_disable(void) { SNVS->LPCR &= ~(1<<0); while(((SNVS->LPCR) & 0x01) == 1);//等待禁用成功 } /** * @brief 获取时间戳 * * @return uint64_t 时间戳——int64 */ uint64_t rtc_getseconds(void) { static uint64_t seconds = 0; seconds = (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15); return seconds; } /** * @brief 设置时间戳至RTC寄存器 * * @param datetime 日期时间结构体 */ void rtc_setvalue(struct rtc_datetime *datetime) { unsigned int temp = SNVS->LPCR; //当前RTC运行状态 uint64_t seconds = 0; seconds = rtc_coverdate_to_seconds(datetime); rtc_disable(); SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); //高17位 SNVS->LPSRTCLR = (unsigned int)(seconds << 15); //低15位 if(temp &0x01) //如果设置前RTC为运行,重启RTC {rtc_enable();} } /** * @brief 获取日期——时间 * * @param datetime 通过指针返回值 */ void rtc_getdatetime(struct rtc_datetime *datetime) { uint64_t seconds = 0; seconds = rtc_getseconds(); rtc_convertseconds_to_datetime(seconds,datetime); } /** * @brief 闰年判定 * * @param year 年 * @return unsigned char 1时为闰年 */ unsigned char rtc_isleapyear(unsigned short year) { unsigned char value=0; if(year % 400 == 0) value = 1; else { if((year % 4 == 0) && (year % 100 != 0)) value = 1; else value = 0; } return value; } /** * @brief 时间戳转换为时间结构体 * * @param seconds 时间戳 * @param datetime */ void rtc_convertseconds_to_datetime(u64 seconds, struct rtc_datetime *datetime) { u64 x; u64 secondsRemaining, days; unsigned short daysInYear; /* 每个月的天数 */ unsigned char daysPerMonth[] = {0U, 31U, 28U, 31U, 30U, 31U, 30U, 31U, 31U, 30U, 31U, 30U, 31U}; secondsRemaining = seconds; /* 剩余秒数初始化 */ days = secondsRemaining / SECONDS_IN_A_DAY + 1; /* 根据秒数计算天数,加1是当前天数 */ secondsRemaining = secondsRemaining % SECONDS_IN_A_DAY; /*计算天数以后剩余的秒数 */ /* 计算时、分、秒 */ datetime->hour = secondsRemaining / SECONDS_IN_A_HOUR; secondsRemaining = secondsRemaining % SECONDS_IN_A_HOUR; datetime->minute = secondsRemaining / 60; datetime->second = secondsRemaining % SECONDS_IN_A_MINUTE; /* 计算年 */ daysInYear = DAYS_IN_A_YEAR; datetime->year = YEAR_RANGE_START; while(days > daysInYear) { /* 根据天数计算年 */ days -= daysInYear; datetime->year++; /* 处理闰年 */ if (!rtc_isleapyear(datetime->year)) daysInYear = DAYS_IN_A_YEAR; else /*闰年,天数加一 */ daysInYear = DAYS_IN_A_YEAR + 1; } /*根据剩余的天数计算月份 */ if(rtc_isleapyear(datetime->year)) /* 如果是闰年的话2月加一天 */ daysPerMonth[2] = 29; for(x = 1; x <= 12; x++) { if (days <= daysPerMonth[x]) { datetime->month = x; break; } else { days -= daysPerMonth[x]; } } datetime->day = days; } /** * @brief 日期时间结构体转换为时间戳 * * @param datetime * @return unsigned int */ unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime) { unsigned short i = 0; unsigned int seconds = 0; unsigned int days = 0; unsigned short monthdays[] = {0U, 0U, 31U, 59U, 90U, 120U, 151U, 181U, 212U, 243U, 273U, 304U, 334U}; for(i = 1970; i < datetime->year; i++) { days += DAYS_IN_A_YEAR; /* 平年,每年365天 */ if(rtc_isleapyear(i)) days += 1;/* 闰年多加一天 */ } days += monthdays[datetime->month]; if(rtc_isleapyear(i) && (datetime->month >= 3)) days += 1;/* 闰年,并且当前月份大于等于3月的话加一天 */ days += datetime->day - 1; seconds = days * SECONDS_IN_A_DAY + datetime->hour * SECONDS_IN_A_HOUR + datetime->minute * SECONDS_IN_A_MINUTE + datetime->second; return seconds; }
头文件里定义了一个新的结构体还有一些宏给那几个秒和格式化时间直接换算使用
/** * @file bsp_rtc.h * @author your name (you@domain.com) * @brief RTC头文件 * @version 0.1 * @date 2022-01-20 * * @copyright Copyright (c) 2022 * */ #ifndef __BSP_RTC_H #define __BSP_RTC_H #include "imx6ul.h" /*和时间有关的宏定义*/ #define SECONDS_IN_A_DAY (86400) #define SECONDS_IN_A_HOUR (3600) #define SECONDS_IN_A_MINUTE (60) #define DAYS_IN_A_YEAR (365) #define YEAR_RANGE_START (1970) #define YEAR_RANGE_END (2099) /*时间相关结构体*/ struct rtc_datetime { unsigned short year; unsigned char month; unsigned char day; unsigned char hour; unsigned char minute; unsigned char second; }; void rtc_init(void); void rtc_enable(void); void rtc_disable(void); void rtc_getdatetime(struct rtc_datetime *datetime); uint64_t rtc_getseconds(void); void rtc_convertseconds_to_datetime(u64 seconds, struct rtc_datetime *datetime); unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime); unsigned char rtc_isleapyear(unsigned short year); void rtc_setvalue(struct rtc_datetime *datatime); void rtc_getdatetime(struct rtc_datetime *datetime); #endif
可以注意一下,我把初始化里面一段代码屏蔽掉了,否则每次上电都会执行初始化程序时候都会给RTC初始化个时间。我把这个初始化时间的部分放在了按键触发的中断里(EPIT按键消抖),注意是EPIT触发的那个服务函数。
/** * @brief 初始化日期 * * @param gicciar * @param param */ void filter_irqhandler(int gicciar,void *param) { struct rtc_datetime rtc_init_date; rtc_init_date.year = 2022; rtc_init_date.month = 1; rtc_init_date.day = 19; rtc_init_date.hour = 18; rtc_init_date.minute = 36; rtc_init_date.second = 20; if(EPIT1->SR &= (1<<0)) //1时时间到 { filtertimer_stop(); if(gpio_pinread(GPIO1,18) == 0) { rtc_setvalue(&rtc_init_date); } } EPIT1->SR |= (1<<0); //清除EPIT1_SR标志位 }
由于没有配显示屏,最后的效果我是通过串口打印出来的,打印的时候还顺道把时间戳打印出来了,看下main函数
int main(void) { int_init(); imx6u_clkinit(); clk_enable(); delay_init(); uart_init(); keyfilter_init(); rtc_init(); struct rtc_datetime datetime; uint64_t s=0; printf("RTC测试:\r\n"); while(1) { rtc_getdatetime(&datetime); s = rtc_getseconds(); printf("当前时间:%d年%d月%d日%d时%d分%d秒",datetime.year,datetime.month,datetime.day, datetime.hour,datetime.minute,datetime.second); printf("\r\n"); printf("%lld",s); delay_ms(2000); } return 0; }
注意那个打印时间戳的语句,时间戳是64位的,要用%lld来指定格式。
最后的效果
程序是从上电开始跑的,到第六行我通过按键对时间进行了一次复位,可以发现时间发生了变化。