1 RTC定时器
1.1 RTC定时器介绍
RTC定时器被叫做实时时钟(real time clock)。 CPU内部有很多定时器,像看门狗WDT,PWM定时器,高精度定时器Timer等等, 只在“启动”即“通电时”运行,断电时停止。当然,如果时钟不能连续跟踪时间,则必须手动设置。那么当关机后就没办法自动计数统计时间了。
定时器的本质就是计数器,有向上计数,也有向下计数。RTC有一个与主机单独分离的电源,如纽扣电池(备用电池),即使主机电源关闭,它也保持计数定时功能。这也是为什么我们手机关机后时间还能保持准确。再比如以前的老诺基亚手机,拆掉电池就时间不准了,因为rtc电源被切断了,无法在计数,RTC定时器的计数器会被清0,需要手动设置当前时间。
RTC一般都是用纽扣电池给外部晶振和电路供电。
!
1.2 RTC定时器原理
以IMX6U芯片的RTC定时器为例,I.MX6U 内部也有 个 RTC 模块,但是不叫作“RTC”,而是叫做“SNVS”。
RTC模块结构图如下:
SNVS 分为两个子模块:SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP),这两个域的电源来源如下:
1 2
| SNVS_LP:专用的 always-powered-on 电源域,系统主电源和备用电源都可以为其供电。 SNVS_HP:系统(芯片)电源。
|
系统主电源断电以后 SNVS_HP 也会断电,但是在备用电源支持下,SNVS_LP 是不会断电的,而且 SNVS_LP 是和芯片复位隔离开的,因此 SNVS_LP 相关的寄存器的值会一直保存着, 也就是low Power Domain是不受系统电源影响。
上图各个序号含义如下:
1 2 3 4
| 1. VDD_HIGH_IN 是系统(芯片)主电源,这个电源会同时供给给 SNVS_HP 和 SNVS_LP。 2. VDD_SNVS_IN 是纽扣电池供电的电源,这个电源只会供给给 SNVS_LP,保证在系统主电源 VDD_HIGH_IN 掉电以后 SNVS_LP 会继续运行。 3. SNVS_HP 部分。 4. SNVS_LP 部分,此部分有个 SRTC,这个就是要使用的 RTC。
|
SRTC 需要外界提供一个 32.768KHz 的时钟,I.MX6U-ALPHA 核心板上的 32.768KHz 的晶振就是提供这个时钟的。
1.3 RTC定时器寄存器
1 2 3 4 5
| SNVS_SRTCMR[14:0]代表SRTC计数器的高15位 SNVS_SRTCLR[31:15]代表SRTC计数器的低17位 注意:是以 1970 年 1 月 1 日0点0分0秒为起点,加上经过的总秒数即可得到现在的时间点。 SNVS_HPCOMR[31], NPSWA_EN位,非特权软件访问控制位,如果非特权软件要访问 SNVS 的话此位必须为 1。 SNVS_LPCR[0], SRTC_ENV位,使能 STC 计数器。
|
1.4 RTC裸机源码展示
NXP 官方 SDK 包是针对 I.MX6ULL 编写的,因此文件 MCIMX6Y2.h中的结构体 SNVS_Type 里面的寄存器是不全的,我们需要在其中加入本章实验所需要的寄存器,修改 SNVS_Type 为如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
typedef struct { __IO uint32_t HPLR; __IO uint32_t HPCOMR; __IO uint32_t HPCR; __IO uint32_t HPSICR; __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;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #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); 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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
| #include "bsp_rtc.h" #include "stdio.h"
void rtc_init(void) {
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(); }
void rtc_enable(void) {
SNVS->LPCR |= 1 << 0; while(!(SNVS->LPCR & 0X01)); }
void rtc_disable(void) {
SNVS->LPCR &= ~(1 << 0); while(SNVS->LPCR & 0X01); }
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; }
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; if(rtc_isleapyear(i)) days += 1; }
days += monthdays[datetime->month]; if(rtc_isleapyear(i) && (datetime->month >= 3)) days += 1;
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; }
void rtc_setdatetime(struct rtc_datetime *datetime) { unsigned int seconds = 0; unsigned int tmp = SNVS->LPCR; rtc_disable(); seconds = rtc_coverdate_to_seconds(datetime); SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); SNVS->LPSRTCLR = (unsigned int)(seconds << 15); if (tmp & 0x1) rtc_enable(); }
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; 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)) daysPerMonth[2] = 29;
for(x = 1; x <= 12; x++) { if (days <= daysPerMonth[x]) { datetime->month = x; break; } else { days -= daysPerMonth[x]; } } datetime->day = days; }
unsigned int rtc_getseconds(void) { unsigned int seconds = 0; seconds = (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15); return seconds; }
void rtc_getdatetime(struct rtc_datetime *datetime) { u64 seconds; seconds = rtc_getseconds(); rtc_convertseconds_to_datetime(seconds, datetime); }
|
可以看到RTC定时器是以秒为计时单位的,每过1s SRTC计数器的值加1。
首先调用rtc_init
初始化并启动,然后调用rtc_setdatetime
设定当前日期时间,调用rtc_getdatetime
获取当前日期时间,期间会利用rtc_convertseconds_to_datetime
把总秒数转换成当前的日期和时间。
2 PWM定时器
2.1 pwm定时器介绍
imx6ull一共有 8 路 PWM 信号,每个 PWM 包含一个 16 位的计数器和一个 4 x 16 的数据 FIFO。一路框图如下:
1 2 3 4 5 6 7
| ①、此部分是一个选择器,用于选择 PWM 信号的时钟源,一共有三种时钟源:ipg_clk,pg_clk_highfreq 和 ipg_clk_32k。 ②、这是一个 12 位的分频器,可以对①中选择的时钟源进行分频。 ③、这是 PWM 的 16 位计数器寄存器,保存着 PWM 的计数值。 ④、这是 PWM 的 16 位周期寄存器,此寄存器用来控制 PWM 的频率。 ⑤、这是 PWM 的 16 位采样寄存器,此寄存器用来控制 PWM 的占空比。 ⑥、此部分是 PWM 的中断信号,PWM 是提供中断功能的,如果使能了相应的中断的话就会产生中断。 ⑦、此部分是 PWM 对应的输出 IO,产生的 PWM 信号就会从对应的 IO 中输出。
|
2.2 PWM控制器
2.2.1 PWMx_PWMPR寄存器-周期设置
PWM 的 16 位计数器是个上计数器,此计数器会从 0X0000 开始计数,直到计数值等于寄存器PWMx_PWMPR(x=1~8)+ 1,然后计数器就会重新从0X0000 开始计数,如此往复。PWMx_PWMPR设置频率。PWM周期公式如下:
PWM_FRE = PWM_CLK / (PERIOD + 2)
也就是PWMO(Hz) = PCLK(Hz) / (PERIOD + 2)
比如当前PWM_CLK=1MHz, 要产生1KHz的PWM,那么PERIOD = 1000000/1K - 2 = 998。,如下设置1000,即可得到PERIOD=998,也就是1khz.
1 2 3 4 5 6 7 8
| void pwm1_setperiod_value(unsigned int value) { unsigned int regvalue = 0; if(value < 2) regvalue = 2; else regvalue = value - 2; PWM1->PWMPR = (regvalue & 0XFFFF); }
|
2.2.2 PWMx_PWMSAR寄存器-占空比
设置Sample采样寄存器,Sample数据会写入到FIFO中。当计数器的值小于 SAMPLE 的时候输出高电平(或低电平)。当计数器值大于等于 SAMPLE,小于寄存器PWM1_PWMPR 的 PERIO 的时候输出低电平(或高电平)。
假如我们要设置 PWM 信号的占空比为 50%,那么就可以将 SAMPLE 设置为(PERIOD + 2) / 2 = 1000 / 2=500。
如下设置50,即可得到sample=500,也就是占空比50%.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct backlight_dev_struc { unsigned char pwm_duty; }; struct backlight_dev_struc backlight_dev; void pwm1_setsample_value(unsigned int value) { PWM1->PWMSAR = (value & 0XFFFF); } void pwm1_setduty(unsigned char duty) { unsigned short preiod; unsigned short sample; backlight_dev.pwm_duty = duty; preiod = PWM1->PWMPR + 2; sample = preiod * backlight_dev.pwm_duty / 100; pwm1_setsample_value(sample); }
|
2.2.3 PWMCR 控制寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| FWM(bit27:26):FIFO 水位线,用来设置 FIFO 空余位置为多少的时候表示 FIFO 为空。 设置为 0 的时候表示 FIFO 空余位置大于等于 1 的时候 FIFO 为空; 设置为 1 的时候表示 FIFO 空余位置大于等于 2 的时候 FIFO 为空; 设置为 2 的时候表示 FIFO 空余位置大于等于 3 的时候FIFO 为空; 设置为 3 的时候表示 FIFO 空余位置大于等于 4 的时候 FIFO 为空。 STOPEN(bit25):此位用来设置停止模式下 PWM 是否工作,为 0 的话表示在停止模式下PWM 不工作,为 1 的话表示停止模式下激活 PWM。 DOZEN(bit24):此位用来设置休眠模式下 PWM 是否工作,为 0 的话表示在休眠模式下PWM 不工作,为 1 的话表示休眠模式下激活 PWM。 WAITEN(bit23):此位用来设置等待模式下 PWM 是否工作,为 0 的话表示在等待模式下PWM 不工作,为 1 的话表示等待模式下激活 PWM。 DEGEN(bit22):此位用来设置调试模式下 PWM 是否工作,为 0 的话表示在调试模式下PWM 不工作,为 1 的话表示调试模式下激活 PWM。 BCTR(bit21):字节交换控制位,用来控制 16 位的数据进入 FIFO 的字节顺序。为 0 的时候不进行字节交换,为 1 的时候进行字节交换。 HCRT(bit20):半字交换控制位,用来决定从 32 位 IP 总线接口传输来的哪个半字数据写入采样寄存器的低 16 位中。 POUTC(bit19:18):PWM 输出控制控制位,用来设置 PWM 输出模式, 为 0 的时候表示PWM 先输出高电平,当计数器值和采样值相等的话就输出低电平。 为 1 的时候相反,当为 2 或者 3 的时候 PWM 信号不输出。本章我们设置为 0, 也就是一开始输出高电平,当计数器值和采样值相等的话就改为低电平,这样采样值越大高电平时间就越长,占空比就越大。 CLKSRC(bit17:16):PWM 时钟源选择, 为 0 的话关闭; 为 1 的话选择 ipg_clk 为时钟源; 为 2 的话选择 ipg_clk_highfreq 为时钟源; 为 3 的话选择 ipg_clk_32k 为时钟源。本章我们设置为 1,也就是选择 ipg_clk 为 PWM 的时钟源,因此 PWM 时钟源频率为 66MHz。 PRESCALER(bit15:4):分频值,可设置为 0~4095,对应着 1~4096 分频。 SWR(bit3):软件复位,向此位写 1 就复位 PWM,此位是自清零的,当复位完成以后此位会自动清零。 REPEAT(bit2:1):重复采样设置,此位用来设置 FIFO 中的每个数据能用几次。 可设置 0~3,分别表示 FIFO 中的每个数据能用 1~4 次。本章我们设置为 0,即 FIFO 中的每个数据只能用一次。 EN(bit0):PWM 使能位,为 1 的时候使能 PWM,为 0 的时候关闭 PWM。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void pwm1_enable(void) { PWM1->PWMCR |= 1 << 0; } void pwm1_init(void) { PWM1->PWMCR = 0; PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4);
pwm1_setperiod_value(1000);
backlight_dev.pwm_duty = 50; for(i = 0; i < 4; i++) { pwm1_setduty(backlight_dev.pwm_duty); }
PWM1->PWMIR |= 1 << 0; system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); GIC_EnableIRQ(PWM1_IRQn); PWM1->PWMSR = 0;
pwm1_enable(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| bit[27:26] : 01 当FIFO中空余位置大于等于2的时候FIFO空标志值位 bit[25] :0 停止模式下PWM不工作 bit[24] : 0 休眠模式下PWM不工作 bit[23] : 0 等待模式下PWM不工作 bit[22] : 0 调试模式下PWM不工作 it[21] : 0 关闭字节交换 bit[20] : 0 关闭半字数据交换 bit[19:18] : 00 PWM输出引脚在计数器重新计数的时候输出高电平,在计数器计数值达到比较值以后输出低电平 bit[17:16] : 01 PWM时钟源选择IPG CLK = 66MHz bit[15:4] : 65 分频系数为65+1=66,PWM时钟源 = 66MHZ/66=1MHz bit[3] : 0 PWM不复位 bit[2:1] : 00 FIFO中的sample数据每个只能使用一次。 bit[0] : 0 先关闭PWM,后面再使能
|
2.2.4 PWM1_PWMIR中断控制寄存器
CIE(bit2):比较中断使能位,为 1 的时候使能比较中断,为 0 的时候关闭比较中断。
RIE(bit1):翻转中断使能位,当计数器值等于采样值并回滚到 0X0000 的时候就会产生此中断,为 1 的时候使能翻转中断,为 0 的时候关闭翻转中断。
FIE(bit0):FIFO 空中断,为 1 的时候使能,为 0 的时候关闭。前面代码写的是使能FIFO空中断.
2.2.5 PWM1_PWMSR 状态寄存器
FWE(bit6):FIFO 写错误事件,为 1 的时候表示发生了 FIFO 写错误。
CMP(bit5):FIFO 比较事件发标志位,为 1 的时候表示发生 FIFO 比较事件。
ROV(bit4):翻转事件标志位,为 1 的话表示翻转事件发生。
FE(bit3):FIFO 空标志位,为 1 的时候表示 FIFO 位空。
FIFOAV(bit2:0):此位记录 FIFO 中的有效数据个数,有效值为 04,分别表示 FIFO 中有04 个有效数据
初始化先清0,中断服务程序读取状态,并且清中断。FIFO 中的采样值每个周期都会少一个,所以需要不断的向 FIFO 中写入采样值,防止其为空。我们可以使能 FIFO 空中断,这样当 FIFO 为空的时候就会触发相应的中断,然后在中断处理函数中向 FIFO 写入采样值。
1 2 3 4 5 6 7 8 9 10 11 12 13
| void pwm1_irqhandler(void) { if(PWM1->PWMSR & (1 << 3)) { pwm1_setduty(backlight_dev.pwm_duty); PWM1->PWMSR |= (1 << 3); } }
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); GIC_EnableIRQ(PWM1_IRQn); PWM1->PWMSR = 0; pwm1_enable();
|
2.3 测试
初始化时设置占空比为50%,测试代码读取按键,每次该按键按下就对占空比加10%,如果占空比超过100%,重新从10%开始。
1 2 3 4 5 6 7 8 9 10 11 12 13
| while(1) { keyvalue = key_getvalue(); if(keyvalue == KEY0_VALUE) { duty += 10; if(duty > 100) duty = 10; lcd_shownum(50 + 72, 90, duty, 3, 16); pwm1_setduty(duty); } delayms(10); }
|
占空比10%时亮度波形如下,亮度很暗。
占空比90%时亮度如下: