目录
[寄存器 SNVS_LPSRTCMR](#寄存器 SNVS_LPSRTCMR)
[寄存器 SNVS_LPSRTCLR](#寄存器 SNVS_LPSRTCLR)
[寄存器 SNVS_LPTAR](#寄存器 SNVS_LPTAR)
前言
实时时钟是很常用的一个外设,通过实时时钟我们就可以知道年、月、日和时间等信息。可以使用专用的实时时钟芯片来完成此功能,很多芯片内部也自带了实时时钟外设模块,比如 I.MX6U 内部的SNVS 就提供了 RTC 功能。
本讲实验我们使用正点原子开发板 I.MX6U ,跟着官方例程一起来学习如何使用 I.MX6U 内部的 RTC 来完成实时时钟功能。
RTC模块:SNVS
SNVS介绍
I.MX6U 内部的RTC 模块,叫做"SNVS",SNVS 直译过来就是安全的非易性存储。SNVS 里面主要是一些低功耗的外设,包括一个安全的实时计数器(RTC)、一个单调计数器(monotonic counter)和一些通用的寄存器。
本讲实验我们只使用实时计数器(RTC),SNVS 里面的外设在芯片掉电以后由电池供电继续运行。
I.MX6UALPHA 开发板上有一个纽扣电池,这个纽扣电池就是在主电源关闭以后为 SNVS 供电的,如图:

纽扣电池在开发板掉电以后会继续给 SNVS 供电,因此实时计数器就会一直运行,这样的话时间信息就不会丢失,除非纽扣电池没电了。在有纽扣电池作为后备电源的情况下,不管系统主电源是否断电, SNVS 都正常运行。
SNVS 分为两个子模块: SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP),这两个域的电源来源如下:
- SNVS_LP:专用的 always-powered-on 电源域,系统主电源和备用电源都可以为其供电。
- SNVS_HP:系统(芯片)电源。
系统主电源断电以后 SNVS_HP 也会断电,但是在后备电源支持下, SNVS_LP 是不会断电的,而且 SNVS_LP是和芯片复位隔离开的,因此 SNVS_LP 相关的寄存器的值会一直保存着。
SNVS 的这两个子模块的电源如图:

- VDD_HIGH_IN 是系统(芯片)主电源,这个电源会同时供给给 SNVS_HP 和 SNVS_LP。
- VDD_SNVS_IN 是纽扣电池供电的电源,这个电源只会供给给 SNVS_LP,保证在系统主电源 VDD_HIGH_IN 掉电以后 SNVS_LP 会继续运行。
- SNVS_HP 部分。
- SNVS_LP 部分,此部分有个 SRTC,这个就是我们本讲实验要使用的 RTC。
NVS_LP 里面的 SRTC,其本质就是一个定时器,只要给它提供时钟,它就会一直运行。所以 I.MX6U-ALPHA 核心板上有一个32.768KHz 的晶振,就是提供这个时钟的。
SNVS寄存器
寄存器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR 保存着秒数,直接读取这两个寄存器的值就知道过了多长时间了。一般以 1970 年 1 月 1 日为起点,加上经过的秒数即可得到现在的时间和日期。
寄存器 SNVS_LPSRTCMR
SNVS_LP安全实时计数器MSB寄存器包含LP安全实时计数器的15个最高有效位。


寄存器 SNVS_LPSRTCLR
SNVS_LP安全实时计数器LSB寄存器包含安全实时计数器的32个最低有效位。
寄存器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR,按照NXP官方的《6UL参考手册》中的说法, SNVS_SRTCMR保存着高15位, SNVS_SRTCLR保存着低 32 位,因此 SRTC 的计数器一共是 47 位。
但是正点原子官方左盟主,在查找了 NXP 提供的 SDK 包中的 fsl_snvs_hp.c 以及 Linux 内核中的 rtcsnvs.c 这两个驱动文件以后发现《6UL 参考手册》上对 SNVS_SRTCMR 和 SNVS_SRTCLR 的解释是错误的,经过查阅这两个文件,得到如下结论:
- SRTC 计数器是 32 位的,不是 47 位!
- SNVS_LPSRTCMR 的 bit14:0 这 15 位是 SRTC 计数器的高 15 位。
- SNVS_LPSRTCLR 的 bit31:bit15 这 17 位是 SRTC 计数器的低 17 位。
(我不理解但大为震撼,手册和代码对不上那要手册干嘛)
要调整时间的话也是向这两个寄存器写入要设置的时间值对应的秒数就可以了,但是要修改这两个寄存器的话记得先关闭 SRTC。
寄存器 SNVS_LPTAR
SRTC 也是带有闹钟功能的,可以在寄存器 SNVS_LPTAR 中写入闹钟时间值,当时钟值和闹钟值匹配的时候就会产生闹钟中断,要使用时钟功能的话还需要进行一些设置,本讲我们不使用闹钟。


寄存器SNVS_HPCOMR
SNVS_HP命令寄存器包含SNVS块的命令、配置和控制位。除了功能状态下的标准写访问外,此寄存器的某些字段还可以在检查和软故障状态下写入。这是一个特权写寄存器。

这个寄存器我们只用到了位: NPSWA_EN(bit31),这个位是非特权软件访问控制位,如果非特权软件要访问 SNVS 的话此位必须为 1。

寄存器SNVS_LPCR
SNVS_LP控制寄存器包含SNVS LP部分的各种控制位。
这个寄存器也只用到了一个位:SRTC_ENV(bit0),此位为 1 的话就使能 STC 计数器。
SNVS配置步骤
- 初始化 SNVS_SRTC:初始化 SNVS_LP 中的 SRTC。
- 设置 RTC 时间:第一次使用 RTC 肯定要先设置时间。
- 使能 RTC:配置好 RTC 并设置好初始时间以后就可以开启 RTC 了。
硬件原理图
本试验用到的资源如下:指示灯 LED0、 SRTC。
SRTC 需要外接一个 32.768KHz 的晶振,在 I.MX6U-ALPHA 核心板上就有这个 32.768KHz的晶振,原理图如图:

实验程序编写
MCIMX6Y2.h
之前移植的 NXP 官方 SDK 包是针对 I.MX6ULL 编写的,因此文件 MCIMX6Y2.h中的结构体 SNVS_Type 里面的寄存器是不全的。
我们需要在文件 MCIMX6Y2.h 加入I.MX6UL手册有的且我们所需要的寄存器,修改 SNVS_Type 为如下所示:
cpp
/** SNVS - Register Layout Typedef */
typedef struct {
__IO uint32_t HPLR; /**< SNVS_HP Lock register, offset: 0x0 */
__IO uint32_t HPCOMR; /**< SNVS_HP Command register, offset: 0x4 */
__IO uint32_t HPCR; /**< SNVS_HP Control register, offset: 0x8 */
__IO uint32_t HPSICR; /**< SNVS_HP Control register, offset: 0x8 */
__IO uint32_t HPSVCR;
__IO uint32_t HPSR;
__IO uint32_t HPSVSR;
__IO uint32_t HPHACIVR;
__IO uint32_t HPHACR;
__IO uint32_t HPRTCMR;
__IO uint32_t HPRTCLR;
__IO uint32_t HPTAMR;
__IO uint32_t HPTALR;
__IO uint32_t LPLR;
__IO uint32_t LPCR;
__IO uint32_t LPMKCR;
__IO uint32_t LPSVCR;
__IO uint32_t LPTGFCR;
__IO uint32_t LPTDCR;
__IO uint32_t LPSR;
__IO uint32_t LPSRTCMR;
__IO uint32_t LPSRTCLR;
__IO uint32_t LPTAR;
__IO uint32_t LPSMCMR;
__IO uint32_t LPSMCLR;
}SNVS_Type;
rtc.h
在 bsp 文件夹下创建名为"rtc"的文件夹,然后在 bsp/rtc 中新建 bsp_rtc.c 和 bsp_rtc.h 这两个文件。
rtc.h文件中定义了一些宏,比如一天多少秒、一小时多少秒等等,这些宏将用于将秒转换为时间,或者将时间转换为秒。
定义了一个结构体 rtc_datetime,此结构体用于描述日期和时间参数。
cpp
#ifndef _BSP_RTC_H
#define _BSP_RTC_H
#include "imx6ul.h"
/* 相关宏定义 */
#define SECONDS_IN_A_DAY (86400) /* 一天86400秒 */
#define SECONDS_IN_A_HOUR (3600) /* 一个小时3600秒 */
#define SECONDS_IN_A_MINUTE (60) /* 一分钟60秒 */
#define DAYS_IN_A_YEAR (365) /* 一年365天 */
#define YEAR_RANGE_START (1970) /* 开始年份1970年 */
#define YEAR_RANGE_END (2099) /* 结束年份2099年 */
/* 时间日期结构体 */
struct rtc_datetime
{
unsigned short year; /* 范围为:1970 ~ 2099 */
unsigned char month; /* 范围为:1 ~ 12 */
unsigned char day; /* 范围为:1 ~ 31 (不同的月,天数不同).*/
unsigned char hour; /* 范围为:0 ~ 23 */
unsigned char minute; /* 范围为:0 ~ 59 */
unsigned char second; /* 范围为:0 ~ 59 */
};
/* 函数声明 */
void rtc_init(void);
void rtc_enable(void);
void rtc_disable(void);
unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime);
unsigned int rtc_getseconds(void);
void rtc_setdatetime(struct rtc_datetime *datetime);
void rtc_getdatetime(struct rtc_datetime *datetime)
;
#endif
rtc.c
文件 bsp_rtc.c 里面一共有 9 个函数:
函数rtc_init:初始化rtc,主要是使能RTC,也可以在rtc_init函数里面设置时间。
cpp
/*
* 描述:初始化RTC
*/
void rtc_init(void)
{
/*
* 设置HPCOMR寄存器
* bit[31] 1 : 允许访问SNVS寄存器,一定要置1
* bit[8] 1 : 此位置1,需要签署NDA协议才能看到此位的详细说明,
* 这里不置1也没问题
*/
SNVS->HPCOMR |= (1 << 31) | (1 << 8);
#if 0
struct rtc_datetime rtcdate;
rtcdate.year = 2018U;
rtcdate.month = 12U;
rtcdate.day = 13U;
rtcdate.hour = 14U;
rtcdate.minute = 52;
rtcdate.second = 0;
rtc_setDatetime(&rtcdate); //初始化时间和日期
#endif
rtc_enable(); //使能RTC
}
函数rtc_enable:RTC 的使能函数。
cpp
/*
* 描述: 开启RTC
*/
void rtc_enable(void)
{
/*
* LPCR寄存器bit0置1,使能RTC
*/
SNVS->LPCR |= 1 << 0;
while(!(SNVS->LPCR & 0X01));//等待使能完成
}
函数rtc_disable: RTC 的禁止函数。
cpp
/*
* 描述: 关闭RTC
*/
void rtc_disable(void)
{
/*
* LPCR寄存器bit0置0,关闭RTC
*/
SNVS->LPCR &= ~(1 << 0);
while(SNVS->LPCR & 0X01);//等待关闭完成
}
函数 rtc_isleapyear :用于判断某一年是否为闰年。
cpp
/*
* @description : 判断指定年份是否为闰年,闰年条件如下:
* @param - year: 要判断的年份
* @return : 1 是闰年,0 不是闰年
*/
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;
}
函数rtc_coverdate_to_seconds :负责将给定的日期和时间信息转换为对应的秒数。
cpp
/*
* @description : 将时间转换为秒数
* @param - datetime: 要转换日期和时间。
* @return : 转换后的秒数
*/
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;
}
函数 rtc_setdatetime:用 于 设 置 时 间 , 也 就 是 设 置 寄 存 器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR 。
cpp
/*
* @description : 设置时间和日期
* @param - datetime: 要设置的日期和时间
* @return : 无
*/
void rtc_setdatetime(struct rtc_datetime *datetime)
{
unsigned int seconds = 0;
unsigned int tmp = SNVS->LPCR;
rtc_disable(); /* 设置寄存器HPRTCMR和HPRTCLR的时候一定要先关闭RTC */
/* 先将时间转换为秒 */
seconds = rtc_coverdate_to_seconds(datetime);
SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); /* 设置高16位 */
SNVS->LPSRTCLR = (unsigned int)(seconds << 15); /* 设置地16位 */
/* 如果此前RTC是打开的在设置完RTC时间以后需要重新打开RTC */
if (tmp & 0x1)
rtc_enable();
}
函 数rtc_convertseconds_to_datetime :用于将给定的秒数转换为对应的时间值。
cpp
/*
* @description : 将秒数转换为时间
* @param - seconds : 要转换的秒数
* @param - datetime: 转换后的日期和时间
* @return : 无
*/
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;
}
函数 rtc_getseconds :获取 SRTC 当前秒数,其实就是读取寄存器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR,然后将其结合成 32 位的值。
cpp
/*
* @description : 获取RTC当前秒数
* @param : 无
* @return : 当前秒数
*/
unsigned int rtc_getseconds(void)
{
unsigned int seconds = 0;
seconds = (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15);
return seconds;
}
函数 rtc_getdatetime: 是获取时间值。
cpp
/*
* @description : 获取当前时间
* @param - datetime: 获取到的时间,日期等参数
* @return : 无
*/
void rtc_getdatetime(struct rtc_datetime *datetime)
{
//unsigned int seconds = 0;
u64 seconds;
seconds = rtc_getseconds();
rtc_convertseconds_to_datetime(seconds, datetime);
}
main.c
在 main 函数里面先初始化 RTC,然后进入 3S 倒计时, 如果这 3S 内按下了 KEY0 按键,那么就设置 SRTC 的日期。如果 3S 倒计时结束以后没有按下 KEY0,也就是没有设置 SRTC时间的话就进入 while 循环,然后读取 RTC 的时间值并且显示在 LCD 上。
cpp
#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_uart.h"
#include "bsp_lcd.h"
#include "bsp_lcdapi.h"
#include "bsp_rtc.h"
#include "stdio.h"
/*
* @description : main函数
* @param : 无
* @return : 无
*/
int main(void)
{
unsigned char key = 0;
int i = 3, t = 0;
char buf[160];
struct rtc_datetime rtcdate;
unsigned char state = OFF;
int_init(); /* 初始化中断(一定要最先调用!) */
imx6u_clkinit(); /* 初始化系统时钟 */
delay_init(); /* 初始化延时 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */
beep_init(); /* 初始化beep */
uart_init(); /* 初始化串口,波特率115200 */
lcd_init(); /* 初始化LCD */
rtc_init(); /* 初始化RTC */
tftlcd_dev.forecolor = LCD_RED;
lcd_show_string(50, 10, 400, 24, 24, (char*)"ALPHA-IMX6UL RTC TEST"); /* 显示字符串*/
lcd_show_string(50, 40, 200, 16, 16, (char*)"ATOM@ALIENTEK");
lcd_show_string(50, 60, 200, 16, 16, (char*)"2019/3/27");
tftlcd_dev.forecolor = LCD_BLUE;
memset(buf, 0, sizeof(buf));
while(1)
{
if(t==100) //1s时间到了
{
t=0;
printf("will be running %d s......\r", i);
lcd_fill(50, 90, 370, 110, tftlcd_dev.backcolor); /* 清屏 */
sprintf(buf, "will be running %ds......", i);
lcd_show_string(50, 90, 300, 16, 16, buf);
i--;
if(i < 0)
break;
}
key = key_getvalue();
if(key == KEY0_VALUE)
{
rtcdate.year = 2018;
rtcdate.month = 1;
rtcdate.day = 15;
rtcdate.hour = 16;
rtcdate.minute = 23;
rtcdate.second = 0;
rtc_setdatetime(&rtcdate); /* 初始化时间和日期 */
printf("\r\n RTC Init finish\r\n");
break;
}
delayms(10);
t++;
}
tftlcd_dev.forecolor = LCD_RED;
lcd_fill(50, 90, 370, 110, tftlcd_dev.backcolor); /* 清屏 */
lcd_show_string(50, 90, 200, 16, 16, (char*)"Current Time:"); /* 显示字符串 */
tftlcd_dev.forecolor = LCD_BLUE;
while(1)
{
rtc_getdatetime(&rtcdate);
sprintf(buf,"%d/%d/%d %d:%d:%d",rtcdate.year, rtcdate.month, rtcdate.day, rtcdate.hour, rtcdate.minute, rtcdate.second);
lcd_fill(50,110, 300,130, tftlcd_dev.backcolor);
lcd_show_string(50, 110, 250, 16, 16,(char*)buf); /* 显示字符串 */
state = !state;
led_switch(LED0,state);
delayms(1000); /* 延时一秒 */
}
return 0;
}
编译成功后将代码下载到SD卡,再将SD卡插入到开发板的卡槽中,复位重启开发板,观察现象。
笔者囊中羞涩,没有买LCD显示屏,按照官方的开发指南,实验效果应该大概如下:
