在嵌入式开发中,RTC(Real-Time Clock,实时时钟)是一个重要的硬件模块,用来跟踪当前的时间和日期。与一般的系统时钟不同,RTC 具有独立的电源(通常为电池),即使系统断电或进入低功耗模式,RTC 仍然能够保持运行。
这个当前时间与日期,指的是我的 RTC 中设置的时间和日期,一般不是 PC 的当前时间和日期;
RTC的基本特性
独立性:RTC 时钟是独立于系统主处理器运行的,即使系统进入低功耗模式或关机,RTC 仍然能正常计时。这通常是通过一块备用电池(如纽扣电池)来供电实现的,如果没有电池,系统断电后 RTC 会停止工作,导致时间信息丢失。
低功耗:RTC 的设计非常节能,因为它需要在设备断电或处于待机状态下保持时间。典型的 RTC 功耗极低,使得它能够在备用电池的支持下持续工作多年。
精度:RTC 时钟的频率通常由一个外部的 32.768kHz晶振 提供,这种频率的晶振能够精确跟踪时间变化。之所以选择这个频率,是因为它可以通过二进制的除法很方便地分频为1秒。
32.768kHz 可以是默认,也可以是可设置的
计时功能:除了记录当前时间,RTC 通常还能提供闹钟、定时等功能。例如,可以设定某个时间触发闹钟中断,唤醒处理器执行特定任务。
RTC时钟的组成部分
晶振(Oscillator):通常,RTC 时钟使用一个 32.768kHz 的石英晶体振荡器来提供计时参考。这种晶振因为频率较低,功耗小,且稳定性高,适合长时间精确计时。
备用电池:为了确保系统断电后,RTC 仍能继续保持正确的时间,通常会有一个小型纽扣电池为其供电。即使系统关闭或断电,RTC 依然可以正常工作。
寄存器:RTC 中包含用于存储当前时间、日期、以及闹钟设定的寄存器。操作系统或嵌入式程序可以通过访问这些寄存器来获取或设置时间信息。
RTC 与系统时钟的区别
RTC 中时间和日期的存储
RTC 负责跟踪当前的时间和日期,这些信息通常以寄存器的形式存储在 RTC 芯片中。不同 RTC 芯片的实现略有不同,但一般来说,RTC 会将时间和日期以二进制编码的十进制(BCD)格式或直接的二进制格式存储。
STC8H
以 PCF8563 为例
PCF8563:PCF8563 是一款低功耗的 I2C RTC 时钟芯片,能够以 BCD 格式存储时间和日期信息,并具有时钟报警、时钟输出等功能。它具有低功耗、集成度高、工作稳定等特点,适用于需要长时间运行且功耗要求较低的应用场景。
PCF8563 通过I2C总线与主控芯片(如微控制器)进行通信。
存储格式
PCF8563 芯片使用 BCD 格式进行存储
原理图
引脚说明:
- #INT: 中断引脚。当触发到定时任务时,会触发引脚高低电平变化。
- SCL和SDA:为I2C通讯的两个引脚。用来保证MCU和RTC时钟芯片间进行通讯的。
- OSCI:振荡器输入
- OSCO:振荡器输出
- Vss:地
- SDA:串行数据 I/O
- SCL:串行时钟输入
- CLKOUT:时钟输出(开漏)
- VDD:正电源
实现时间的设置与读取
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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
| #include "STC8G_H_Delay.h" #include "STC8G_H_GPIO.h" #include "STC8G_H_NVIC.h" #include "STC8G_H_Switch.h" #include "STC8G_H_I2C.h" #include "STC8G_H_UART.h"
void UART_GPIO_Config(void) {
GPIO_InitTypeDef UART_GPIO_init; UART_GPIO_init.Mode = GPIO_PullUp; UART_GPIO_init.Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_Inilize(GPIO_P3, &UART_GPIO_init); }
void UART_Config(void) { COMx_InitDefine UART_Init; UART_Init.UART_Mode = UART_8bit_BRTx; UART_Init.UART_BRT_Use = BRT_Timer1; UART_Init.UART_BaudRate = 115200; UART_Init.Morecommunicate = DISABLE; UART_Init.UART_RxEnable = DISABLE; UART_Init.BaudRateDouble = DISABLE; UART_Configuration(UART1, &UART_Init); UART1_SW(UART1_SW_P30_P31); }
void main() { struct RTC_Time time = { 2024, 9, 12, 18, 4, 24, 3 }; EA = 1; UART_Config(); RTC_Init(); RTC_WriteTime(&time); while(1) { RTC_ReadTime(&time); printf("%d-%d-%d %d:%d:%d \n",time.year, (int)time.month, (int)time.day, (int)time.hour, (int)time.minute, (int)time.second); printf("week=%d\n", (int)time.week);
delay_ms(250); delay_ms(250); delay_ms(250); delay_ms(250); } } ==================================================================
u8 timeDate[7];
#define Decimal2BCD(time) ((time / 10) << 4 + (time %10)
#define DCB2Decima(i, b) (((timeDate[i] & b) >> 4) * 10) + (timeDate[i] & 0x0F)
#define DEV_ADDR 0xA2 #define MEN_ADDR 0x02 void RTC_GPIO_Config(void) { GPIO_InitTypeDef RTC_GPIO_init; RTC_GPIO_init.Mode = GPIO_OUT_OD; RTC_GPIO_init.Pin = GPIO_Pin_2 | GPIO_Pin_3; GPIO_Inilize(GPIO_P3, &RTC_GPIO_init); }
void RTC_I2C_Config() { I2C_InitTypeDef I2C_Init; I2C_Init.I2C_Speed = 13; I2C_Init.I2C_Enable = ENABLE; I2C_Init.I2C_Mode = I2C_Mode_Master; I2C_Init.I2C_MS_WDTA = DISABLE; I2C_Init(&I2C_Init); I2C_SW(I2C_P33_P32); } void RTC_Init(void) { EAXSFR(); RTC_GPIO_Config(); RTC_I2C_Config(); }
void RTC_WriteTime(RTC_Time* time) { timeDate[0] = Decima2BCD(time->RTC_Sec); timeDate[1] = Decima2BCD(time->RTC_Min); timeDate[2] = Decima2BCD(time->RTC_Hour); timeDate[3] = Decima2BCD(time->RTC_Day); timeDate[4] = (time->RTC_Week); timeDate[5] = Decima2BCD(time->RTC_Month); timeDate[6] = (((time->RTC_Year) % 100 / 10) << 4) + ((time->RTC_Year) % 10); if(time->RTC_Year > 2100) { timeDate[5] |= 0x80; }else { timeDate[5] &= 0x80; } I2C_WriteNbyte(DEV_ADDR, MEN_ADDR, timeDate, 7); }
void RTC_ReadTime(RTC_Time* time) { I2C_ReadNbyte(DEV_ADDR, MEN_ADDR, timeDate, 7); time->RTC_Sec = DCB2Decima(0, 0x70); time->RTC_Min = DCB2Decima(1, 0x70); time->RTC_Hour = DCB2Decima(2, 0x70); time->RTC_Day = DCB2Decima(3, 0x70); time->RTC_Week = timeDate[4]; time->RTC_Month = DCB2Decima(5, 0x70); time->RTC_Year = DCB2Decima(6, 0x70); if(timeDate[5] * 0x80) { time->RTC_Year += 2100; }else { time->RTC_Year += 2000; } } ==================================================================
#ifndef __RTC_H #define __RTC_H #include "type_def.h"
type struct { u16 RTC_Year; u8 RTC_Month; u8 RTC_Day; u8 RTC_Hour; u8 RTC_Min; u8 RTC_Sec; u8 RTC_Week; }RTC_Time;
void RTC_Init();
RTC_WriteTime(RTC_Time* time); RTC_ReadTime(RTC_Time* time); #endif
|
实现闹钟
这里为了实现闹钟和定时器先介绍两个寄存器:
控制 / 状态寄存器
这里我们要关注下AF、TF、AIE、TIE
AF:报警标志位,当报警事件发生时,AF 标志位会被置为 1。需要我们手动软件置零。
TF:定时器标志位,当定时器事件触发时,TF 标志位会被置为 1。需要我们手动软件置零。
AIE:报警中断使能位,AIE 控制的是闹钟的中断使能。如果 AIE 置位为 1,AF 置位时将触发中断。
TIE:定时器中断使能位,TIE 控制的是定时器的中断使能。如果 TIE 置位为 1,TF 置位时将触发中断。
报警寄存器
这里可以看到我们想要设置某个闹钟时间,只需要将他的最高位置 0,余下的 7 位设置为具体的时间即可。
这里要理解所谓的实现闹钟的功能,并不是说我们设定一个时间后,这个 RTC 到达指定时间后自己会发声,而是指他在达到我们设置顶的事件后他的中断输出引脚会产生一个低电平(或者是高电平),我们要去捕捉这个中断,然后通过蜂鸣器去实现这个闹钟的功能,定时器也是同理。
实现
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
|
void RTC_EXTI_Config(){ EXTI_InitTypeDef init; init.EXTI_Mode = EXT_MODE_RiseFall; Ext_Inilize(EXT_INT3,&init); NVIC_INT3_Init(ENABLE , Priority_0); }
void RTC_StartAlarm(RTC_Alarm * alarm) { u8 arr[4] = {0x80 , 0x80 , 0x80 , 0x80 };
I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat |= 0x02;
rtc_dat &= ~0x08; I2C_WriteNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); if(alarm->minute != -1) { arr[0] = Decimal2BCD(alarm->minute ); arr[0] &= ~0x80 ;
} if(alarm->hour != -1) { arr[1] = Decimal2BCD(alarm->hour ); arr[1] &= ~0x80 ; } if(alarm->day != -1) { arr[2] = Decimal2BCD(alarm->day ); arr[2] &= ~0x80 ; } if(alarm->week != -1) { arr[3] = Decimal2BCD(alarm->week ); arr[3] &= ~0x80 ; } I2C_WriteNbyte(DEV_ADD, ALA_ADD, arr, 4); }
u8 RTC_ISAlarmINT() { u8 dat; I2C_ReadNbyte(0xA2, 0x01, &dat, 1); return dat & 0x08; }
===========================================================================
extern void handle_tit3_interrupte();
void INT3_ISR_Handler (void) interrupt INT3_VECTOR {
WakeUpSource = 4; handle_tit3_interrupte(); } =============================================================================
void handle_tit3_interrupte() { if(RTC_ISAlarmINT()) { os_create_task(TASK_BUZZER); }else { os_create_task(TASK_LED); } }
void RTC_ClearAlarmFlag() {
I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat &= ~0x08; I2C_WriteNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); }
void RTC_StopAlarm(){ I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat &= ~0x02; I2C_WriteNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); }
|
实现定时器
倒计时定时器寄存器
大致与上面闹钟同理:区别在于两个参数:
TD1、TD0:用于设置定时器的时钟频率,
定时器倒计数数值寄存器位描述:具体倒计时要数的数,通过这两个可以设置倒计数的时长
以 64 Hz 为例,那么 设置寄存器值位 64 就是 1s
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
|
void RTC_StartTimer(RTC_Hz hz , u8 count){ I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat |= 1; rtc_dat &= ~0x04; I2C_WriteNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat = 0x80 | hz; I2C_WriteNbyte(DEV_ADD, TIM1_ADD, &rtc_dat, 1); rtc_dat = count; I2C_WriteNbyte(DEV_ADD, TIM2_ADD, &rtc_dat, 1); }
void RTC_StopTimer() { I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat &= ~0x01; I2C_WriteNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); }
void RTC_ClearTimerFlag() {
I2C_ReadNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); rtc_dat &= ~0x04;
I2C_WriteNbyte(DEV_ADD, CON_ADD, &rtc_dat, 1); }
|
问题
一、在 PCF8563芯片中,晶振频率为32.768kHz,但我们为什么可以选择 32.768kHz、1024kHz、32Hz、1Hz,他不是只有个 32.768kHz 的晶振么
在 PCF8563 实时时钟(RTC)芯片中,虽然它使用了32.768kHz的晶振作为基准频率,但通过内部的分频电路,可以将该基准频率分频为其他不同的频率输出,如1024Hz、32Hz、和1Hz。这意味着尽管晶振频率固定为32.768kHz,但通过硬件电路的分频机制,PCF8563芯片可以输出不同的时钟频率。
二、I2C 的总线到底如何配置
I2C(Inter-Integrated Circuit)总线的速度是指主设备和从设备之间数据传输的速率。
公式为:总线速度 = Fosc/2/(Speed*2+4),0~63
FOSC:这是系统的主时钟频率(通常是微控制器的晶振频率)。
MSSPEED:用于控制 I2C 速度的一个寄存器值或配置参数。
2 和 4:这是 I2C 控制器内部的定值,用于分频公式中的基准值。
MSSPEED 是一个控制分频的参数,用于将主时钟(FOSC)分频成较低的频率,从而得到 I2C 总线所需的时钟频率(SCL)。
通常情况下,I2C 的速度取决于具体应用的要求、设备能力和电路布局。如果连接的设备需要高速传输,则可以选择快速模式或更高的模式;而在低功耗应用中,标准模式的 100 kbit/s 速度可能就足够了。
三、在开发文档中 MSSPEED 所对应的值为时钟数,那么这个是时钟数是什么?
在开发文档中提到的 “时钟数” 是指 I2C 总线传输一个数据位所需的时钟周期数。具体来说,它是 I2C 通信中每发送或接收一位数据时,SCL(串行时钟线)所需要的时钟周期数。