第十七章 DS1302时钟
1. 导入
DS1302 是常用的实时时钟(RTC)芯片,采用类似三线SPI的时序(CE、SCLK、IO),内置掉电保持与涓流充电电路,并提供少量RAM。它以 BCD 格式存储年、月、日、时、分、秒,支持 12/24 小时制。配合 32.768kHz 晶振与后备电池,即便主电掉电也能继续计时。
本章目标:
- 掌握 DS1302 引脚、供电与时钟电路连接。
- 熟悉读写寄存器、BCD 编解码、CH位与WP位使用。
- 编写完整驱动:初始化、设置时间、读取时间。
- 将时间以数码管或串口方式显示。
2. 硬件设计
- 晶振:32.768kHz 晶体接 DS1302 的 X1、X2,引脚内部含电容,一般无需外部电容。
- 供电:
VCC接 +5V(或+3.3V,取决于系统电压)。VBAT接 3V 纽扣电池(CR2032)以掉电保持,GND 共地。
- 三线接口(任意通用IO均可,以下示例以 P1.0~P1.2):
CE→ P1.2(片选,高有效)SCLK→ P1.1(时钟,空闲低,上升沿采样)IO→ P1.0(数据,双向)
- 其他:建议在
VCC与GND处加去耦电容(如 0.1µF)。
引脚速查(常用)
- CE:片选(高有效)
- SCLK:串行时钟(空闲低)
- IO:数据线(LSB 先传,写入在 SCLK 上升沿采样,读取在 SCLK 上升沿后稳定)
- X1/X2:32.768kHz 晶体
- VBAT:后备电池
- VCC/GND:主电源/地
3. 软件设计
3.1 寄存器与关键位
- 地址规则:偶地址=写,奇地址=读;最低位为R/W位。
- 秒:0x80/0x81(bit7=CH,1=停止振荡,需清0)
- 分:0x82/0x83
- 时:0x84/0x85(24h 模式 bit7=0;12h 模式 bit7=1)
- 日:0x86/0x87(日期/日 of month)
- 月:0x88/0x89
- 周:0x8A/0x8B(1~7)
- 年:0x8C/0x8D(00~99)
- 控制:0x8E/0x8F(bit7=WP写保护,1=保护,0=可写)
- 涓流充电:0x90(按需配置,常用关闭)
- 数据均为 BCD 格式存储(十位在高4位,个位在低4位)。
3.2 基础工具与引脚定义(C51)
c
#include <reg52.h>
#include <intrins.h>
sbit DS1302_IO = P1^0; // 数据(双向)
sbit DS1302_SCLK = P1^1; // 时钟
sbit DS1302_CE = P1^2; // 片选(高有效)
// 简单微延时(视时序需要可适当增加 _nop_ 次数)
static void ds_delay(void) {
_nop_(); _nop_(); _nop_(); _nop_();
}
static unsigned char bin2bcd(unsigned char b) {
return ((b / 10) << 4) | (b % 10);
}
static unsigned char bcd2bin(unsigned char bcd) {
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
3.3 总线底层读写(LSB先行)
c
// 进入传输(CE拉高,SCLK置低)
static void ds1302_enable(void) {
DS1302_CE = 0;
DS1302_SCLK = 0;
DS1302_CE = 1;
ds_delay();
}
// 结束传输
static void ds1302_disable(void) {
DS1302_CE = 0;
ds_delay();
}
// 发送1字节(LSB先)
static void ds1302_write_byte(unsigned char dat) {
unsigned char i;
for (i = 0; i < 8; i++) {
DS1302_IO = (dat & 0x01); // 输出当前位(LSB)
ds_delay();
DS1302_SCLK = 1; // 上升沿采样
ds_delay();
DS1302_SCLK = 0;
dat >>= 1; // 下一位
}
}
// 读取1字节(LSB先)
static unsigned char ds1302_read_byte(void) {
unsigned char i, dat = 0;
// 释放IO为输入:8051口写1即可呈现高阻输入状态
DS1302_IO = 1;
for (i = 0; i < 8; i++) {
// 读取当前位(LSB)
if (DS1302_IO) {
dat |= (1 << i);
}
ds_delay();
DS1302_SCLK = 1; // 时钟上升沿,器件准备下一位
ds_delay();
DS1302_SCLK = 0;
}
return dat;
}
3.4 读写寄存器封装
c
// 写寄存器(addr 为偶地址)
static void ds1302_write_reg(unsigned char addr, unsigned char dat) {
ds1302_enable();
ds1302_write_byte(addr & 0xFE); // 确保为写(LSB=0)
ds1302_write_byte(dat);
ds1302_disable();
}
// 读寄存器(addr 为偶地址,此处内部自动+1为读)
static unsigned char ds1302_read_reg(unsigned char addr) {
unsigned char dat;
ds1302_enable();
ds1302_write_byte((addr & 0xFE) | 0x01); // 读命令(LSB=1)
dat = ds1302_read_byte();
ds1302_disable();
return dat;
}
3.5 关键初始化:清WP、清CH
c
// 取消写保护
static void ds1302_write_enable(void) {
ds1302_write_reg(0x8E, 0x00); // WP=0
}
// 开启写保护(可选)
static void ds1302_write_protect(void) {
ds1302_write_reg(0x8E, 0x80); // WP=1
}
// 清除CH位(启动振荡器),只在秒寄存器写时确保bit7=0
static void ds1302_clear_CH(void) {
unsigned char sec = ds1302_read_reg(0x80);
if (sec & 0x80) {
sec &= 0x7F;
ds1302_write_enable();
ds1302_write_reg(0x80, sec);
// ds1302_write_protect(); // 需要时再加保护
}
}
3.6 时间结构与读写接口
c
typedef struct {
unsigned char year; // 0~99
unsigned char month; // 1~12
unsigned char date; // 1~31
unsigned char week; // 1~7
unsigned char hour; // 0~23(按24小时制)
unsigned char min; // 0~59
unsigned char sec; // 0~59
} rtc_time_t;
// 设置时间(24小时制)
void ds1302_set_time(rtc_time_t *t) {
unsigned char h = bin2bcd(t->hour) & 0x3F; // 24小时制:bit7=0
ds1302_write_enable();
ds1302_write_reg(0x80, bin2bcd(t->sec) & 0x7F); // CH=0
ds1302_write_reg(0x82, bin2bcd(t->min));
ds1302_write_reg(0x84, h);
ds1302_write_reg(0x86, bin2bcd(t->date));
ds1302_write_reg(0x88, bin2bcd(t->month));
ds1302_write_reg(0x8A, bin2bcd(t->week));
ds1302_write_reg(0x8C, bin2bcd(t->year));
// 可按需配置涓流充电寄存器 0x90(一般保持默认或关闭)
// ds1302_write_protect(); // 若需保护,最后开启
}
// 读取时间(转为BIN)
void ds1302_get_time(rtc_time_t *t) {
unsigned char sec = ds1302_read_reg(0x80) & 0x7F; // 去CH
unsigned char min = ds1302_read_reg(0x82);
unsigned char hour = ds1302_read_reg(0x84);
unsigned char date = ds1302_read_reg(0x86);
unsigned char month= ds1302_read_reg(0x88);
unsigned char week = ds1302_read_reg(0x8A);
unsigned char year = ds1302_read_reg(0x8C);
t->sec = bcd2bin(sec);
t->min = bcd2bin(min);
// 24小时:bit7=0,小时BCD在 bit5..0
t->hour = bcd2bin(hour & 0x3F);
t->date = bcd2bin(date);
t->month = bcd2bin(month);
t->week = bcd2bin(week);
t->year = bcd2bin(year);
}
3.7 示例一:首次上电设置时间,随后循环读取
可通过是否发现 CH=1(振荡关闭)来判断是否需要"首次初始化时间"。
c
void delay_ms(unsigned int ms) {
unsigned int i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 125; j++);
}
void ds1302_init_and_maybe_set(void) {
// 若秒寄存器CH=1,则清CH并设置一次默认时间
unsigned char sec = ds1302_read_reg(0x80);
if (sec & 0x80) {
rtc_time_t t = { .year=23, .month=12, .date=31, .week=7,
.hour=23, .min=59, .sec=50 };
ds1302_set_time(&t);
} else {
ds1302_clear_CH(); // 保险操作
}
}
void main() {
rtc_time_t now;
ds1302_init_and_maybe_set();
while (1) {
ds1302_get_time(&now);
// TODO: 将 now 显示到数码管/串口
// 示例延时
delay_ms(500);
}
}
3.8 示例二:4位数码管显示"HH:MM"
沿用前文动态数码管思路,仅演示核心代码(共阴段码):
c
// 共阴段码(0~9)
unsigned char code seg_code[] = {
0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F
};
// 显示 HH:MM,第二位加小数点作为"冒号"
void show_hhmm(unsigned char h, unsigned char m) {
unsigned char d0 = h / 10;
unsigned char d1 = h % 10;
unsigned char d2 = m / 10;
unsigned char d3 = m % 10;
// 位选:P2.0~P2.3 低有效;段选:P0
// 一次扫描演示:建议放定时器中断中做1ms扫描
P0 = seg_code[d0]; P2 = (P2 & 0xF0) | 0x0E; delay_ms(1);
P0 = seg_code[d1] | 0x80; P2 = (P2 & 0xF0) | 0x0D; delay_ms(1); // 点亮":"
P0 = seg_code[d2]; P2 = (P2 & 0xF0) | 0x0B; delay_ms(1);
P0 = seg_code[d3]; P2 = (P2 & 0xF0) | 0x07; delay_ms(1);
}
void main() {
rtc_time_t now;
ds1302_init_and_maybe_set();
while (1) {
ds1302_get_time(&now);
// 快速循环扫描,形成稳定显示
show_hhmm(now.hour, now.min);
}
}
3.9 示例三:串口打印时间
c
extern void uart_init(void);
extern void uart_send_byte(unsigned char ch);
static void uart_send_str(char *s){ while(*s) uart_send_byte(*s++); }
static void uart_send_2d(unsigned char v) {
uart_send_byte('0' + v/10);
uart_send_byte('0' + v%10);
}
void main() {
rtc_time_t now;
uart_init();
ds1302_init_and_maybe_set();
while (1) {
ds1302_get_time(&now);
uart_send_2d(now.year); uart_send_byte('-');
uart_send_2d(now.month); uart_send_byte('-');
uart_send_2d(now.date); uart_send_byte(' ');
uart_send_2d(now.hour); uart_send_byte(':');
uart_send_2d(now.min); uart_send_byte(':');
uart_send_2d(now.sec); uart_send_str("\r\n");
delay_ms(1000);
}
}
4. 小结
- 硬件要点:32.768kHz 晶振接 X1/X2;VBAT 接纽扣电池;三线时序用普通IO即可。
- 关键寄存器:秒寄存器 CH 位需清0;控制寄存器 WP 写保护需清0后方可写。
- 通信时序:LSB 先行;写在 SCLK 上升沿采样;读在上升沿后取值。
- 代码实现:提供寄存器基础读写、时间结构封装、初始化/设置/读取完整流程。
- 显示与验证:给出 4位数码管与串口打印两种展示方案。
常见问题与排查
- 读到不变化或全零:检查 CE/SCLK/IO 接线与极性;确认 CE 在传输开始与结束的拉高拉低时序。
- 时间不走或复位:CH 位未清;或晶振未起振(焊接、型号、污染)。
- 写不进去:WP 未清(控制寄存器0x8E bit7=1);或未按偶地址写、奇地址读规则。
- 显示跳变或闪烁:将数码管扫描放入定时器中断,主循环仅更新显示数据。