实时时钟是很常用的一个外设,通过实时时钟我们就可以知道年、月、日和时间等信息。因此在需要记录时间的场合就需要实时时钟,可以使用专用的实时时钟芯片来完成此功能,但是现在大多数的MCU 或者MPU 内部就已经自带了实时时钟外设模块。比如I.MX6U 内部的SNVS 就提供了RTC 功能,本章我们就学习如何使用I.MX6U 内部的RTC 来完成实时时钟功能。
I.MX6U RTC 简介
如果学习过STM32 的话应该知道,STM32 内部有一个RTC 外设模块,这个模块需要一个32.768KHz 的晶振,对这个RTC 模块进行初始化就可以得到一个实时时钟。I.MX6U 内部也有个RTC 模块,但是不叫作“RTC”,而是叫做“SNVS”,这一点要注意!本章我们参考《I.MX6UL参考手册》,而不是《I.MX6ULL 参考手册》,因为《I.MX6ULL 参考手册》很多SNVS 相关的 寄存器并没有给出来,不知道是为何?但是《I.MX6UL 参考手册》里面是完整的。所以本章我们使用《I.MX6UL 参考手册》,如果直接在《I.MX6UL 参考手册》的书签里面找“RTC”相关的字眼是找不到的。I.MX6U 系列的RTC 是在SNVS 里面,也就是《I.MX6UL 参考手册》的第46 章“Chapter 46 Secure Non-Volatile Storage(SNVS)”。
SNVS 直译过来就是安全的非易性存储,SNVS 里面主要是一些低功耗的外设,包括一个安全的实时计数器(RTC)、一个单调计数器(monotonic counter)和一些通用的寄存器,本章我们肯定只使用实时计数器(RTC)。SNVS 里面的外设在芯片掉电以后由电池供电继续运行,I.MX6U-ALPHA 开发板上有一个纽扣电池,这个纽扣电池就是在主电源关闭以后为SNVS 供电的,如图25.1.1 所示: 因为纽扣电池在掉电以后会继续给SNVS 供电,因此实时计数器就会一直运行,这样的话时间信息就不会丢失,除非纽扣电池没电了。在有纽扣电池作为后备电源的情况下,不管系统主电源是否断电,SNVS 都正常运行。SNVS 有两部分:SNVS_HP 和SNVS_LP,系统主电源断电以后SNVS_HP 也会断电,但是在后备电源支持下,SNVS_LP 是不会断电的,而且SNVS_LP是和芯片复位隔离开的,因此SNVS_LP 相关的寄存器的值会一直保存着。
SNVS 分为两个子模块:SNVS_HP 和SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP),这两个域的电源来源如下: SNVS_LP:专用的always-powered-on 电源域,系统主电源和备用电源都可以为其供电。 SNVS_HP:系统(芯片)电源。 SNVS 的这两个子模块的电源如图25.1.2 所示:
图25.1.2 中各个部分功能如下: ①、VDD_HIGH_IN 是系统(芯片)主电源,这个电源会同时供给给SNVS_HP 和SNVS_LP。 ②、VDD_SNVS_IN 是纽扣电池供电的电源,这个电源只会供给给SNVS_LP,保证在系 统主电源VDD_HIGH_IN 掉电以后SNVS_LP 会继续运行。 ③、SNVS_HP 部分。 ④、SNVS_LP 部分,此部分有个SRTC,这个就是我们本章要使用的RTC。
其实不管是SNVS_HP 还是SNVS_LP,其内部都有一个SRTC,但是因为SNVS_HP 在系统电源掉电以后就会关闭,所以我们本章使用的是SNVS_LP 内部的SRTC。毕竟我们肯定都不想开发板或者设备每次关闭以后时钟都被清零,然后开机以后先设置时钟。
其实不管是SNVS_HP 里面的RTC,还是SNVS_LP 里面的SRTC,其本质就是一个定时器,和我们在第八章讲的EPIT 定时器一样,只要给它提供时钟,它就会一直运行。SRTC 需要外界提供一个32.768KHz 的时钟,I.MX6U-ALPHA 核心板上的32.768KHz 的晶振就是提供这个时钟的。寄存器SNVS_LPSRTCMR 和SNVS_LPSRTCLR 保存着秒数,直接读取这两个寄存器的值就知道过了多长时间了。一般以1970 年1 月1 日为起点,加上经过的秒数即可得到现在的时间和日期,原理还是很简单的。SRTC 也是带有闹钟功能的,可以在寄存器SNVS_LPAR 中写入闹钟时间值,当时钟值和闹钟值匹配的时候就会产生闹钟中断,要使用时钟功能的话还需要进行一些设置,本章我们就不使用闹钟了。
接下来我们看一下本章要用到的与SRTC 相关的部分寄存器,首先是SNVS_HPCOMR 寄存器,这个寄存器我们只用到了位:NPSWA_EN(bit31),这个位是非特权软件访问控制位,如果非特权软件要访问SNVS 的话此位必须为1。
接下来看一下寄存器SNVS_LPCR寄存器,此寄存器也只用到了一个位:SRTC_ENV(bit0),此位为1 的话就使能STC 计数器。
最后来看一下寄存器SNVS_SRTCMR 和SNVS_SRTCLR,这两个寄存器保存着RTC 的秒数,按照NXP 官方的《6UL 参考手册》中的说法,SNVS_SRTCMR 保存着高15 位,SNVS_SRTCLR保存着低32 位,因此SRTC 的计数器一共是47 位。
但是!我在编写驱动的时候发现按照手册上说的去读取计数器值是错误的!具体表现就是时间是混乱的,因此我在查找了NXP 提供的SDK 包中的fsl_snvs_hp.c 以及Linux 内核中的rtc-snvs.c 这两个驱动文件以后发现《6UL 参考手册》上对SNVS_SRTCMR 和SNVS_SRTCLR 的 解释是错误的,经过查阅这两个文件,得到如下结论:
①、SRTC计数器是32位的,不是47位! ②、SNVS_SRTCMR的bit14:0这15位是SRTC计数器的高15位。 ③、SNVS_SRTCLR的bit31:bit15这17位是SRTC计数器的低17位。
按照上面的解释去读取这两个寄存器就可以得到正确的时间,如果要调整时间的话也是向这两个寄存器写入要设置的时间值对应的秒数就可以了,但是要修改这两个寄存器的话要先关闭SRTC。
关于SNVS 中和RTC 有关的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6UL 参考手册》第2931 页的46.7 小节。本章我们使用I.MX6U 的SNVS_LP 的SRTC,配置步骤如下: 1、初始化SNVS_SRTC 初始化SNVS_LP 中的SRTC。 2、设置RTC 时间 第一次使用RTC 肯定要先设置时间。 3、使能RTC 配置好RTC 并设置好初始时间以后就可以开启RTC 了。
硬件原理分析
本试验用到的资源如下: ①、指示灯LED0。 ②、RGB LCD 接口。 ③、SRTC。
SRTC 需要外接一个32.768KHz 的晶振,在I.MX6U-ALPHA 核心板上就有这个32.768KHz的晶振,原理图如图25.2.1 所示:
实验程序编写
本实验对应的例程路径为:开发板光盘-> 1、裸机例程-> 16_rtc。
修改文件MCIMX6Y2.h
在第十三章移植的NXP 官方SDK 包是针对I.MX6ULL 编写的,因此文件MCIMX6Y2.h中的结构体SNVS_Type 里面的寄存器是不全的,我们需要在其中加入本章实验所需要的寄存器,修改SNVS_Type 为如下所示:
1 typedef struct {
2 __IO uint32_t HPLR;
3 __IO uint32_t HPCOMR;
4 __IO uint32_t HPCR;
5 __IO uint32_t HPSICR;
6 __IO uint32_t HPSVCR;
7 __IO uint32_t HPSR;
8 __IO uint32_t HPSVSR;
9 __IO uint32_t HPHACIVR;
10 __IO uint32_t HPHACR;
11 __IO uint32_t HPRTCMR;
12 __IO uint32_t HPRTCLR;
13 __IO uint32_t HPTAMR;
14 __IO uint32_t HPTALR;
15 __IO uint32_t LPLR;
16 __IO uint32_t LPCR;
17 __IO uint32_t LPMKCR;
18 __IO uint32_t LPSVCR;
19 __IO uint32_t LPTGFCR;
20 __IO uint32_t LPTDCR;
21 __IO uint32_t LPSR;
22 __IO uint32_t LPSRTCMR;
23 __IO uint32_t LPSRTCLR;
24 __IO uint32_t LPTAR;
25 __IO uint32_t LPSMCMR;
26 __IO uint32_t LPSMCLR;
27 }SNVS_Type;
编写实验程序
本章实验在上一章例程的基础上完成,更改工程名字为“rtc”,然后在bsp 文件夹下创建名为“rtc”的文件夹,然后在bsp/rtc 中新建bsp_rtc.c 和bsp_rtc.h 这两个文件。在bsp_rtc.h 中输入如下内容:
1 #ifndef _BSP_RTC_H
2 #define _BSP_RTC_H
3
13 #include "imx6ul.h"
14
15
16 #define SECONDS_IN_A_DAY (86400)
17 #define SECONDS_IN_A_HOUR (3600)
18 #define SECONDS_IN_A_MINUTE (60)
19 #define DAYS_IN_A_YEAR (365)
20 #define YEAR_RANGE_START (1970)
21 #define YEAR_RANGE_END (2099)
22
23
24 struct rtc_datetime
25 {
26 unsigned short year;
27 unsigned char month;
28 unsigned char day;
29 unsigned char hour;
30 unsigned char minute;
31 unsigned char second;
32 };
33
34
35 void rtc_init(void);
36 void rtc_enable(void);
37 void rtc_disable(void);
38 unsigned int rtc_coverdate_to_seconds(struct rtc_datetime
*datetime);
39 unsigned int rtc_getseconds(void);
40 void rtc_setdatetime(struct rtc_datetime *datetime);
41 void rtc_getdatetime(struct rtc_datetime *datetime);
42
43 #endif
第16 到21 行定义了一些宏,比如一天多少秒、一小时多少秒等等,这些宏将用于将秒转换为时间,或者将时间转换为秒。第24 行定义了一个结构体rtc_datetime,此结构体用于描述日期和时间参数。剩下的就是一些函数声明了,很简单。
在文件bsp_rtc.c 中输入如下内容:
1 #include "bsp_rtc.h"
2 #include "stdio.h"
3
4
7 void rtc_init(void)
8 {
9
13 SNVS->HPCOMR |= (1 << 31);
14
15 #if 0
16 struct rtc_datetime rtcdate;
17
18 rtcdate.year = 2018U;
19 rtcdate.month = 12U;
20 rtcdate.day = 13U;
21 rtcdate.hour = 14U;
22 rtcdate.minute = 52;
23 rtcdate.second = 0;
24 rtc_setDatetime(&rtcdate);
25 #endif
26 rtc_enable();
27 }
28
29
文件bsp_rtc.c 里面一共有9 个函数,依次来看一下这些函数的意义。函数rtc_init 明显是初始化rtc 的,主要是使能RTC,也可以在rtc_init 函数里面设置时间。函数rtc_enable 和rtc_disable分别是RTC 的使能和禁止函数。函数rtc_isleapyear 用于判断某一年是否为闰年。函数rtc_coverdate_to_seconds 负责将给定的日期和时间信息转换为对应的秒数。函数rtc_setdatetime 用于设置时间,也就是设置寄存器SNVS_LPSRTCMR 和SNVS_LPSRTCLR 。函数rtc_convertseconds_to_datetime 用于将给定的秒数转换为对应的时间值。函数rtc_getseconds 获取SRTC 当前秒数,其实就是读取寄存器SNVS_LPSRTCMR 和SNVS_LPSRTCLR,然后将其结合成32 位的值。最后一个函数rtc_getdatetime 是获取时间值。
我们在main 函数里面先初始化RTC,然后进入3S 倒计时,如果这3S 内按下了KEY0 按键,那么就设置SRTC 的日期。如果3S 倒计时结束以后没有按下KEY0,也就是没有设置SRTC时间的话就进入while 循环,然后读取RTC 的时间值并且显示在LCD 上,在文件main.c 中输入如下所示内容:
1 #include "bsp_clk.h"
2 #include "bsp_delay.h"
3 #include "bsp_led.h"
4 #include "bsp_beep.h"
5 #include "bsp_key.h"
6 #include "bsp_int.h"
7 #include "bsp_uart.h"
8 #include "bsp_lcd.h"
9 #include "bsp_lcdapi.h"
10 #include "bsp_rtc.h"
11 #include "stdio.h"
12
13
18 int main(void)
19 {
20 unsigned char key = 0;
21 int t = 0;
22 int i = 3;
23 char buf[160];
24 struct rtc_datetime rtcdate;
25 unsigned char state = OFF;
26
27 int_init();
28 imx6u_clkinit();
29 delay_init();
30 clk_enable();
31 led_init();
32 beep_init();
33 uart_init();
34 lcd_init();
35 rtc_init();
36
37 tftlcd_dev.forecolor = LCD_RED;
38 lcd_show_string(50, 10, 400, 24, 24,
(char*)"ALPHA-IMX6UL RTC TEST");
39 tftlcd_dev.forecolor = LCD_BLUE;
40 memset(buf, 0, sizeof(buf));
41
42 while(1)
43 {
44 if(t==100)
45 {
46 t=0;
47 printf("will be running %d s......\r", i);
48
49 lcd_fill(50, 40,370, 70, tftlcd_dev.backcolor);
50 sprintf(buf, "will be running %ds......", i);
51 lcd_show_string(50, 40, 300, 24, 24, buf);
52 i--;
第35 行调用函数rtc_init 初始化RTC。
第42 到73 行是倒计时3S,如果在这3S 内按下了KEY0 按键就会调用函数rtc_setdatetime设置当前的时间。如果3S 倒计时结束以后没有按下KEY0 那就表示不需要设置时间,跳出循环,执行下面的代码。
第79 到89 行就是主循环,此循环每隔1S 调用函数rtc_getdatetime 获取一次时间值,并且通过串口打印给SecureCRT 或者在LCD 上显示。
编译下载验证
编写Makefile 和链接脚本
修改Makefile 中的TARGET 为rtc,然后在在INCDIRS 和SRCDIRS 中加入“bsp/rtc”,修改后的Makefile 如下:
1 CROSS_COMPILE ?= arm-linux-gnueabihf-
2 TARGET ?= rtc
3
4
5
6 INCDIRS := imx6ul \
7 stdio/include \
8 bsp/clk \
9 bsp/led \
10 bsp/delay \
11 bsp/beep \
12 bsp/gpio \
13 bsp/key \
14 bsp/exit \
15 bsp/int \
16 bsp/epittimer \
17 bsp/keyfilter \
18 bsp/uart \
19 bsp/lcd \
20 bsp/rtc
21
22 SRCDIRS := project \
23 stdio/lib \
24 bsp/clk \
25 bsp/led \
26 bsp/delay \
27 bsp/beep \
28 bsp/gpio \
29 bsp/key \
30 bsp/exit \
31 bsp/int \
32 bsp/epittimer \
33 bsp/keyfilter \
34 bsp/uart \
35 bsp/lcd \
36 bsp/rtc
37
38
39
40 clean:
41 rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)
第2 行修改变量TARGET 为“rtc”,也就是目标名称为“rtc”。 第20 行在变量INCDIRS 中添加RTC 驱动头文件(.h)路径。 第36 行在变量SRCDIRS 中添加RTC 驱动驱动文件(.c)路径。
链接脚本保持不变。
编译下载
使用Make 命令编译代码,编译成功以后使用软件imxdownload 将编译完成的rtc.bin 文件下载到SD 卡中,命令如下:
chmod 777 imxdownload
./imxdownload rtc.bin /dev/sdd
烧写成功以后将SD 卡插到开发板的SD 卡槽中,然后复位开发板。程序一开始进入3S倒计时,如图25.4.2.1 所示: 如果在倒计数结束之前按下KEY0,那么RTC 就会被设置为我们代码中设置的时间和日期值,RTC 运行如图24.4.2.2 所示: 我们在main 函数中设置的时间是2018 年1 月15 日,16 点23 分0 秒,在倒计数结束之前按下KEY0 按键设置RTC,图24.4.2.2 中的时间就是我们设置以后的时间。
|