51单片机基础-DS1302时钟

第十七章 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(数据,双向)
  • 其他:建议在 VCCGND 处加去耦电容(如 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);或未按偶地址写、奇地址读规则。
  • 显示跳变或闪烁:将数码管扫描放入定时器中断,主循环仅更新显示数据。

相关推荐
知花实央l3 小时前
【数字逻辑】 74HC74转JK触发器+74HC112做二分频+74HC161设计10进制计数器(附接线图)
1024程序员节
阿洛学长3 小时前
高质量 AI 提示词之(从 0-1 开发 Vue 项目)
vue·ai编程·1024程序员节
半梦半醒*4 小时前
ELK2——logstash
linux·运维·elk·elasticsearch·centos·1024程序员节
CodeAmaz4 小时前
ELK(Elasticsearch + Logstash + Kibana + Filebeat)采集方案
java·elk·elasticsearch·1024程序员节
吃饭最爱4 小时前
redis的基础知识
1024程序员节
weixiao04304 小时前
Linux网络 NAT、代理服务、内⽹穿透
1024程序员节
B站_计算机毕业设计之家4 小时前
基于大数据的游戏数据可视化分析与推荐系统 Steam游戏 电子游戏 娱乐数据 Flask框架 selenium爬虫 协同过滤推荐算法 python✅
大数据·python·深度学习·游戏·信息可视化·1024程序员节·steam
不语n4 小时前
点亮LED
单片机·嵌入式硬件
云望无线图传模块5 小时前
无线图传模块:引领科技未来的创新突破
1024程序员节·无线通信模块·无线模块·远距离无线模块