学习目标

  1. 理解RTC时钟的基本原理和使用方法。
  2. 学会使用STC8H单片机的RTC时钟模块进行时间的读取和设置。
  3. 掌握使用RTC时钟进行定时和闹钟功能的实现,了解时钟中断的工作原理。
  4. 了解I2C总线的基本原理和特点
  5. 掌握I2C通讯的使用
  6. 掌握中断处理
  7. 熟悉从芯片手册获取有效信息

    学习内容

    RTC时钟

    RTC时钟是一种实时时钟芯片,通常与微控制器或计算机等设备配合使用,提供高精度的时间和日期信息,以便于设备进行时间相关的操作,如记录数据、定时执行任务、闹钟提醒等。
    RTC时钟的应用场景非常广泛,例如计算机主板、智能家居、物联网设备、工业自动化等领域。在这些应用中,RTC时钟可以提供高精度的时间戳、定时任务、日历功能等,从而为系统提供更加可靠的时间基准。
    除了时间和日期信息,一些RTC时钟芯片还集成了温度传感器、电池备份等功能,以提供更加全面的服务。例如,在断电情况下,RTC时钟的备用电池可以维持时钟的运行,以保证时间和日期信息的准确性。
    以下是几种常见的RTC时钟芯片及其特点和应用场景:

  8. DS1302:DS1302是一款低功耗时钟模块,集成了时钟、日历和时钟报警功能,能够以BCD格式存储时间和日期信息。它具有低功耗、简单易用、成本低等特点,适用于需要长时间运行且功耗要求较低的应用场景。

  9. DS3231:DS3231是一款高精度的I2C RTC时钟芯片,能够以二进制格式存储时间和日期信息,并具有时钟报警、温度补偿等功能。它具有高精度、低功耗、高可靠性等特点,适用于对时钟精度要求较高的应用场景,如电子钟、精密计时器等。
  10. PCF8563:PCF8563是一款低功耗的I2C RTC时钟芯片,能够以BCD格式存储时间和日期信息,并具有时钟报警、时钟输出等功能。它具有低功耗、集成度高、工作稳定等特点,适用于需要长时间运行且功耗要求较低的应用场景。
  11. RV-4162-C7:RV-4162-C7是一款高精度的I2C RTC时钟芯片,能够以二进制格式存储时间和日期信息,并具有时钟输出、时钟同步、时钟校准等功能。它具有高精度、低功耗、抗干扰能力强等特点,适用于对时钟精度要求较高的应用场景,如高精度计时器、高精度工控系统等。
  12. MCP7940N:MCP7940N是一款低功耗的I2C RTC时钟芯片,能够以BCD格式存储时间和日期信息,并具有时钟输出、时钟同步、时钟报警等功能。它具有低功耗、成本低等特点,适用于需要长时间运行且功耗要求较低的应用场景,如电子钟、自动售货机等。

我们开发板中采用的是PCF8563

原理图

image.png
79.png
原理图外围设计:

  1. 外部电池: 确保断电后能正常工作
  2. 晶振:确保震荡频率准确。
  3. 肖特基二极管:防止电流倒灌。

引脚说明:

  1. INT: 中断引脚。当触发到定时任务时,会触发引脚高低电平变化。
  2. SCL和SDA:为I2C通讯的两个引脚。用来保证MCU和RTC时钟芯片间进行通讯的。

    PCF8563寄存器

    118.png

    控制与状态寄存器

    用来配置控制和状态切换的寄存器。
    119.png
    120.png

    设备地址

    1. // 设备地址
    2. #define PCF8563_ADDR 0x51 << 1
    3. // 存储地址:时间的存储地址开始位置
    4. #define PCF8563_REG_TD 0x02

    I2C环境初始化

    ```c

    include “Config.h”

    include “GPIO.h”

    include “Delay.h”

include “I2C.h”

include “UART.h”

include “NVIC.h”

include “Switch.h”

/*

  1. 初始化IO口,将P32,P33初始化开漏OD模式
  2. 初始化I2C协议 \ UART

    1. EAXSFR();
    2. EA = 1
  3. 通过I2C读取RTC时钟芯片数据

  4. 通过I2C给RTC时钟芯片写数据 */

void GPIO_config() { P3_MODE_OUT_OD(GPIO_Pin_2 | GPIO_Pin_3); }

void UART_config(void) { // >>> 记得添加 NVIC.c, UART.c, UART_Isr.c <<< COMx_InitDefine COMx_InitStructure; //结构定义 COMx_InitStructure.UART_Mode = UART_8bit_BRTx; //模式, UART_ShiftRight,UART_8bit_BRTx,UART_9bit,UART_9bit_BRTx COMx_InitStructure.UART_BRT_Use = BRT_Timer1; //选择波特率发生器, BRT_Timer1, BRT_Timer2 (注意: 串口2固定使用BRT_Timer2) COMx_InitStructure.UART_BaudRate = 115200ul; //波特率, 一般 110 ~ 115200 COMx_InitStructure.UART_RxEnable = ENABLE; //接收允许, ENABLE或DISABLE COMx_InitStructure.BaudRateDouble = DISABLE; //波特率加倍, ENABLE或DISABLE UART_Configuration(UART1, &COMx_InitStructure); //初始化串口1 UART1,UART2,UART3,UART4

  1. NVIC_UART1_Init(ENABLE,Priority_1); //中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3
  2. UART1_SW(UART1_SW_P30_P31); // 引脚选择, UART1_SW_P30_P31,UART1_SW_P36_P37,UART1_SW_P16_P17,UART1_SW_P43_P44

} /** I2C初始化函数 */ void I2C_config(void) { I2C_InitTypeDef I2C_InitStructure;

  1. I2C_InitStructure.I2C_Mode = I2C_Mode_Master; //主从选择 I2C_Mode_Master, I2C_Mode_Slave
  2. I2C_InitStructure.I2C_Enable = ENABLE; //I2C功能使能, ENABLE, DISABLE
  3. I2C_InitStructure.I2C_MS_WDTA = DISABLE; //主机使能自动发送, ENABLE, DISABLE
  4. I2C_InitStructure.I2C_Speed = 13; //总线速度=Fosc/2/(Speed*2+4), 0~63
  5. // 400K = 24M / 2 / (Speed * 2 + 4):
  6. // 400 = 12000 / (Speed * 2 + 4)
  7. // Speed * 2 = 26
  8. I2C_Init(&I2C_InitStructure);
  9. NVIC_I2C_Init(I2C_Mode_Master,DISABLE,Priority_0); //主从模式, I2C_Mode_Master, I2C_Mode_Slave; 中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3
  10. I2C_SW(I2C_P33_P32); //I2C_P14_P15,I2C_P24_P25,I2C_P33_P32

}

define NUMBER 7

void main() { // 设备地址 read A3h and write A2h u8 dev_addr = 0x51 << 1; // (设备地址 << 1) | 0 = 写地址. // 存储地址 u8 mem_addr = 0x02; // 用于接收从机传来的数据 u8 p[NUMBER]; // 保存时间信息 u8 second, minute, hour, day, week, month; u16 year;

  1. // 开启扩展寄存器使能
  2. EAXSFR();
  3. GPIO_config();
  4. UART_config();
  5. I2C_config();
  6. EA = 1;

// 4. 通过I2C给RTC时钟芯片写数据 // void I2C_WriteNbyte(u8 dev_addr, u8 mem_addr, u8 *p, u8 number);

  1. printf("--------------------------------read\n");
  2. while(1) {
  3. // 3. 通过I2C读取RTC时钟芯片数据
  4. I2C_ReadNbyte(dev_addr, mem_addr, &p, NUMBER);
  5. printf("%d:%d:%d \n", (int)hour, (int)minute, (int)second);
  6. delay_ms(250);
  7. delay_ms(250);
  8. delay_ms(250);
  9. delay_ms(250);
  10. }

}

  1. <a name="gVYRi"></a>
  2. ### RTC寄存器数据读取
  3. ```c
  4. I2C_ReadNbyte(PCF8563_ADDR, 0x02, dat, 7);
  5. second = (dat[0] & 0x0F) + ((dat[0] >> 4) & 0x07) * 10;
  6. minute = (dat[1] & 0x0F) + ((dat[1] >> 4) & 0x07) * 10;
  7. hour = (dat[2] & 0x0F) + ((dat[2] >> 4) & 0x03) * 10;
  8. day = (dat[3] & 0x0F) + ((dat[3] >> 4) & 0x03) * 10;
  9. weekday = dat[4] & 0x07;
  10. month = (dat[5] & 0x0F) + ((dat[5] >> 4) & 0x01) * 10;
  11. year = ((dat[6] >> 4) & 0x0F) * 10 + (dat[6] & 0x0F);
  12. year += ((dat[5] >> 7) & 0x01) * 100 + 1900;

RTC寄存器数据写入

  1. year = 2023;
  2. month = 12;
  3. day = 31;
  4. weekday = 0;
  5. hour = 23;
  6. minute = 59;
  7. second = 50;
  8. if(year >= 2000) {
  9. c = 1;
  10. }
  11. tmp[0] = ((second / 10) << 4) + (second % 10);
  12. tmp[1] = ((minute / 10) << 4) + (minute % 10);
  13. tmp[2] = ((hour / 10) << 4) + (hour % 10);
  14. tmp[3] = ((day / 10) << 4) + (day % 10);
  15. tmp[4] = weekday % 7;
  16. tmp[5] = (c << 7) + ((month / 10) << 4) + (month % 10);
  17. tmp[6] = (u8)(((year % 1000) / 10) << 4) + (u8)((year % 1000) % 10);
  18. I2C_WriteNbyte(PCF8563_ADDR, 0x02, tmp, 7);

RTC闹钟设置

通过配置寄存器来配置闹钟
122.png
123.png
124.png
125.png
闹钟事件触发后,通过外部中断触发,操作流程如下
121.png

  1. u8 config;
  2. // 先读配置
  3. I2C_ReadNbyte(PCF8563_ADDR, 0x01, &config, 1);
  4. // 再去设置, 设置的时候别动别人的配置
  5. config |= 0x02;
  6. config &= ~0x08;//clear clock标记
  7. I2C_WriteNbyte(PCF8563_ADDR, 0x01, &config, 1);
  1. void ext_int3_call(void) {
  2. u8 tmp[7];
  3. u16 year;
  4. u8 month, day, weekday, hour, minute, second, c = 0;
  5. u8 config[1] = {0};
  6. printf("alarm \r\n");
  7. // 读取状态
  8. I2C_ReadNbyte(PCF8563_ADDR, 0x01, config, 1);
  9. printf("config: %d\r\n", (int)config[0]);
  10. // 判断闹钟是否被激活
  11. if((config[0] >> 3) & 0x01 == 1) {
  12. //清除 alarm 标记
  13. config[0] &= ~0x08;
  14. I2C_WriteNbyte(RTC_ADDR, 0x01, config, 1);
  15. I2C_ReadNbyte(RTC_ADDR, 0x01, config, 1);
  16. printf("config: %d\r\n", (int)config[0]);
  17. }
  18. }

RTC计数器设置

通过配置寄存器来配置计数器。
127.png
128.png
129.png

  1. u8 config;
  2. // 先读配置
  3. I2C_ReadNbyte(PCF8563_ADDR, 0x01, &config, 1);
  4. // 再去设置, 设置的时候别动别人的配置
  5. config |= 0x01;
  6. config &= ~0x04;//clear timer标记
  7. I2C_WriteNbyte(PCF8563_ADDR, 0x01, &config, 1);
  1. u8 config[1] = {0};
  2. I2C_ReadNbyte(PCF8563_ADDR, 0x01, config, 1);
  3. printf("config: %d\r\n", (int)config[0]);
  4. if((config[0] >> 2) & 0x01 == 1) {
  5. printf("timer \r\n");
  6. config[0] &= ~0x04;
  7. I2C_WriteNbyte(PCF8563_ADDR, 0x01, config, 1);
  8. }

驱动封装

头文件封装

  1. #ifndef __PCF8563_H__
  2. #define __PCF8563_H__
  3. #include "config.h"
  4. #include "I2C.h"
  5. #define PCF8563_SCL P32
  6. #define PCF8563_SDA P33
  7. #define PCF8563_INT P37
  8. #define PCF8563_ADDR 0x51 << 1
  9. #define PCF8563_ADDR_W 0xA2
  10. #define PCF8563_ADDR_R 0xA3
  11. #define PCF8563_SCL_INIT() {P3M1 |= 0x04, P3M0 |= 0x04;}
  12. #define PCF8563_SDA_INIT() {P3M1 |= 0x08, P3M0 |= 0x08;}
  13. #define PCF8563_INT_INIT() {P3M1 &= ~0x80, P3M0 &= ~0x80, INT_CLKO |= (1 << 5);}
  14. //u16 year;
  15. //u8 month, day, weekday, hour, minute, second
  16. // 定义clock
  17. typedef struct {
  18. u16 year;
  19. u8 month;
  20. u8 day;
  21. u8 weekday;
  22. u8 hour;
  23. u8 minute;
  24. u8 second;
  25. } Clock_t;
  26. //定义alarm
  27. typedef struct {
  28. u8 hour;
  29. u8 enableHour;
  30. u8 minute;
  31. u8 enableMinute;
  32. u8 day;
  33. u8 enableDay;
  34. u8 weekday;
  35. u8 enableWeekday;
  36. } Alarm_t;
  37. // 国产芯片的HZ1有问题,不要使用,建议使用HZ64
  38. enum TimerFreq{ HZ4096 = 0, HZ64 = 1, HZ1 = 2, HZ1_60 = 3};
  39. extern void PCF8563_on_alarm(void);
  40. extern void PCF8563_on_timer(void);
  41. void PCF8563_init(void);
  42. void PCF8563_get_clock(Clock_t *c);
  43. void PCF8563_set_clock(Clock_t c);
  44. void PCF8563_enable_alarm();
  45. void PCF8563_set_alarm(Alarm_t a);
  46. void PCF8563_disable_alarm();
  47. void PCF8563_enable_timer();
  48. void PCF8563_set_timer(enum TimerFreq freq, u8 period);
  49. void PCF8563_disable_timer();
  50. #endif
  • 定义结构体Clock_t表示时间数据,通过这个结构体承载数据,方便读取和设置。
  • 定义结构体Alarm_t表示闹钟数据,通过这个结构体承载数据,方便读取和设置。
  • 定义枚举TimerFreq限定计时器设置的范围。 ```c

    include “PCF8563.h”

    include

void PCF8563_init(void) { PCF8563_SCL_INIT(); PCF8563_SDA_INIT(); PCF8563_INT_INIT(); }

void PCF8563_get_clock(Clock_t c) { u8 dat[7]; I2C_ReadNbyte(PCF8563_ADDR, 0x02, dat, 7); c->second = (dat[0] & 0x0F) + ((dat[0] >> 4) & 0x07) 10; c->minute = (dat[1] & 0x0F) + ((dat[1] >> 4) & 0x07) 10; c->hour = (dat[2] & 0x0F) + ((dat[2] >> 4) & 0x03) 10; c->day = (dat[3] & 0x0F) + ((dat[3] >> 4) & 0x03) 10; c->weekday = dat[4] & 0x07; c->month = (dat[5] & 0x0F) + ((dat[5] >> 4) & 0x01) 10; c->year = ((dat[6] >> 4) & 0x0F) 10 + (dat[6] & 0x0F); c->year += ((dat[5] >> 7) & 0x01) 100 + 1900; }

void PCF8563_set_clock(Clock_t clk) { u8 tmp[7]; u8 c = 0; if(clk.year >= 2000) { c = 1; } tmp[0] = ((clk.second / 10) << 4) + (clk.second % 10); tmp[1] = ((clk.minute / 10) << 4) + (clk.minute % 10); tmp[2] = ((clk.hour / 10) << 4) + (clk.hour % 10); tmp[3] = ((clk.day / 10) << 4) + (clk.day % 10); tmp[4] = clk.weekday % 7; tmp[5] = (c << 7) + ((clk.month / 10) << 4) + (clk.month % 10); tmp[6] = (u8)(((clk.year % 1000) / 10) << 4) + (u8)((clk.year % 1000) % 10); I2C_WriteNbyte(PCF8563_ADDR, 0x02, tmp, 7); }

void PCF8563_enable_alarm() { u8 config; // 先读配置 I2C_ReadNbyte(PCF8563_ADDR, 0x01, &config, 1); // 再去设置, 设置的时候别动别人的配置 config |= 0x02; config &= ~0x08;//clear clock标记 I2C_WriteNbyte(PCF8563_ADDR, 0x01, &config, 1); }

void PCF8563_set_alarm(Alarm_t a) { u8 tmp[4]; tmp[0] = ((a.minute / 10) << 4) + (a.minute % 10); if(a.enableMinute == 0) { tmp[0] += (1 << 7); } tmp[1] = ((a.hour / 10) << 4) + (a.hour % 10); if(a.enableHour == 0) { tmp[1] += (1 << 7); } tmp[2] = ((a.day / 10) << 4) + (a.day % 10); if(a.enableDay == 0) { tmp[2] += (1 << 7); } tmp[3] = a.weekday % 7; if(a.enableWeekday == 0) { tmp[3] += (1 << 7); } I2C_WriteNbyte(PCF8563_ADDR, 0x09, tmp, 4); }

void PCF8563_disable_alarm() { u8 config[1]; // 先读配置 I2C_ReadNbyte(PCF8563_ADDR, 0x01, config, 1); // 再去设置, 设置的时候别动别人的配置 config[0] &= ~0x02; config[0] &= ~0x08;//clear clock标记 I2C_WriteNbyte(PCF8563_ADDR, 0x01, config, 1); }

void PCF8563_enable_timer() { u8 config; // 先读配置 I2C_ReadNbyte(PCF8563_ADDR, 0x01, &config, 1); // 再去设置, 设置的时候别动别人的配置 config |= 0x01; config &= ~0x04;//clear timer标记 I2C_WriteNbyte(PCF8563_ADDR, 0x01, &config, 1); }

void PCF8563_set_timer(enum TimerFreq freq, u8 period) { u8 config; config = freq + (1 << 7);//计数频率 + timer enable I2C_WriteNbyte(PCF8563_ADDR, 0x0E, &config, 1);

  1. config = period; // config, period
  2. I2C_WriteNbyte(PCF8563_ADDR, 0x0F, &config, 1);

}

void PCF8563_disable_timer() { u8 config[1]; // 先读配置 I2C_ReadNbyte(PCF8563_ADDR, 0x01, config, 1); // 再去设置, 设置的时候别动别人的配置 config[0] &= ~0x01; config[0] &= ~0x04;//clear timer标记 I2C_WriteNbyte(PCF8563_ADDR, 0x01, config, 1); }

void Ext_INT3 (void) interrupt INT3_VECTOR { u8 config[1]; // 先读配置 I2C_ReadNbyte(PCF8563_ADDR, 0x01, &config, 1);

  1. // 判断闹钟是否被激活 Alarm Flag && AIE
  2. if((config[0] & 0x08) && (config[0] & 0x02)) {
  3. //清除 alarm 标记
  4. config[0] &= ~0x08;
  5. I2C_WriteNbyte(PCF8563_ADDR, 0x01, config, 1);
  6. PCF8563_on_alarm();
  7. }
  8. // 判断计时器是否被激活 Timer Flag && TIE
  9. if((config[0] & 0x04) && (config[0] & 0x01)) {
  10. //清除 timer 标记
  11. config[0] &= ~0x04;
  12. I2C_WriteNbyte(PCF8563_ADDR, 0x01, config, 1);
  13. PCF8563_on_timer();
  14. }

}

  1. - `Ext_INT3`中断函数为当前`STC8H`平台可用的。如果切换平台需要进行对应的移植操作。
  2. <a name="WlA85"></a>
  3. ### BCD(Binary-Coded Decimal)
  4. BCDBinary-Coded Decimal)是一种用二进制编码表示十进制数字的格式。<br />在BCD格式中,**每个十进制数位用4个二进制位**来表示。BCD的目的是使得数字的编码与显示更加直观和容易处理。在BCD格式中,每个十进制数位的取值范围是09。<br />例如,数字5BCD表示为0101,数字9BCD表示为1001。这种表示方法使得每个十进制数位都独立地编码,方便在数字处理和显示设备上进行操作。<br />10进制数转BCD数:
  5. ```c
  6. // 十位取出左移4位 + 个位 (得到BCD数)
  7. #define WRITE_BCD(val) ((val / 10) << 4) + (val % 10)

BCD数转10进制数:

  1. // 将高4位乘以10 + 低四位 (得到10进制数)
  2. #define READ_BCD(val) (val >> 4) * 10 + (val & 0x0F)

一些状态分析

PCF8563的规则:

  1. 通电后,就开始工作,内部可配置寄存器(时间,闹钟,定时器)
  2. 如果已经有电池,但是单片机断电了,单片机重新通电后,单片机应该遵守PCF8563中已经配置的规则(时间,闹钟,定时器)

    有源晶振和无源晶振

    晶振可分为有源晶振与无源晶振。一般我们说的“晶振”指的是有源晶振,而无源晶振通常叫“晶体”,或者叫“谐振器”。两者最大的区别是:
  • 有源晶振自身即可起振
  • 无源晶振则需要外加专门的时钟电路才能起振

总体来看,有源晶振的精度、稳定度等方面均要好于无源晶振,尤其是在精密测量领域,大部分用的都是高档的有源晶振,以方便把各种补偿技术集成在一起,减少设计复杂性。


无源晶振 有源晶振
别名 晶体/谐振器 振荡器
引脚数量 2个 4个

借助时钟电路产生震荡信号 自身可产生振荡信号
特性 精度较低、信号质量较差、稳定性较差 精度高、信号质量较好、稳定性较强
价格 较低 较高

练习题

  1. 设置时间,并在数码管显示。
  2. 设置闹钟,到闹钟时间,则播放音乐或者震动。
  3. 制作码表,通过独立按键控制开始或停止,通过数码管显示码表读数。