DS1302实时时钟芯片 - 从入门到精通

DS1302实时时钟芯片 - 从入门到精通

适合嵌入式小白的完整学习指南
基于蓝桥杯STC15F2K60S2单片机
版本:V1.0 | 日期:2026-02-08


:open_book: 目录

  1. 芯片简介
  2. 硬件连接
  3. 工作原理
  4. 代码详解
  5. 实战应用
  6. 常见问题
  7. 进阶练习

1. 芯片简介

1.1 DS1302是什么?

DS1302是一个实时时钟芯片(RTC),就像一个独立的电子手表:

  • :white_check_mark: 能记录:秒、分、时、日、月、年、星期
  • :white_check_mark: 自动处理闰年(到2100年)
  • :white_check_mark: 断电后靠备用电池继续走时
  • :white_check_mark: 12/24小时模式可选
  • :white_check_mark: 额外31字节RAM可存数据

1.2 核心参数

参数 说明
工作电压 2.0V ~ 5.5V
功耗 2.0V时 < 300nA
接口类型 3线串行(CE、I/O、SCLK)
晶振频率 32.768kHz
封装 8脚DIP或SO

1.3 为什么用DS1302?

  1. 接口简单:只需3根线,比IIC/SPI更省引脚
  2. 功耗极低:备用电池能用好几年
  3. 价格便宜:几块钱一片
  4. 蓝桥杯必考:几乎每届省赛都会涉及

2. 硬件连接

2.1 引脚定义

DS1302芯片(俯视图)
     ┌─────┐
VCC2 │1   8│ VCC1(备用电源,接纽扣电池+)
  X1 │2   7│ SCLK(时钟线)
  X2 │3   6│ I/O (数据线)
 GND │4   5│ CE  (片选/使能)
     └─────┘

2.2 蓝桥杯标准接法(你的电路板)

sbit SCK = P1^7;  // 时钟线 SCLK
sbit SDA = P2^3;  // 数据线 I/O
sbit RST = P1^3;  // 片选线 CE(也叫RST/CE)

实际连接

单片机         DS1302
-------       --------
P1^7   →      SCLK (7脚)
P2^3   ↔      I/O  (6脚)  ⚠️双向!需要能输入输出
P1^3   →      CE   (5脚)
GND    →      GND  (4脚)
5V     →      VCC2 (1脚)
电池+  →      VCC1 (8脚)  可选,没有断电会丢失时间

2.3 晶振连接

  X1(2脚) ━━━[32.768kHz晶振]━━━ X2(3脚)

:warning: 注意:晶振紧贴芯片,走线越短越好,否则影响时钟精度!


3. 工作原理

3.1 通信时序(最重要!)

DS1302采用同步串行通信,类似SPI但更简单:

:pushpin: 写入时序

       ┌─────────────────┐
CE     │                 └──  必须先拉高!
       ┌──┐  ┌──┐  ┌──┐
SCLK ──┘  └──┘  └──┘  └──  时钟打节拍
         ┌────┬────┬────
I/O  ────┤ D0 │ D1 │ D2 ...  数据在SCLK上升沿被锁存

关键点

  1. CE先拉高 = 开始通信
  2. 数据从bit 0开始(LSB优先)
  3. SCLK上升沿锁存数据
  4. CE拉低 = 结束通信

:pushpin: 读取时序

       ┌─────────────────┐
CE     │                 └──
       ┌──┐  ┌──┐  ┌──┐
SCLK ──┘  └──┘  └──┘  └──
         ────┬────┬────
I/O  读<── D0 │ D1 │ D2 ...  数据在SCLK下降沿输出

关键点

  • 读取时先发命令,再读8位数据
  • SCLK下降沿时读取I/O上的数据

3.2 命令字节格式

每次读写都要先发送一个命令字节:

 Bit:   7    6    5    4    3    2    1    0
      ┌────┬────┬─────────────────┬────┬────┐
      │ 1  │RAM │  地址A4-A0      │ RD │ WR │
      │固定│/CK │                 │ /  │ /  │
      └────┴────┴─────────────────┴────┴────┘

Bit 7: 必须是1
Bit 6: 0=时钟/日历寄存器, 1=RAM
Bit 5-1: 寄存器地址
Bit 0: 0=写, 1=读

举例

0x80 = 1000 0000B → 写秒寄存器
0x81 = 1000 0001B → 读秒寄存器
0x84 = 1000 0100B → 写时寄存器
0x85 = 1000 0101B → 读时寄存器

3.3 寄存器地址表

寄存器 读地址 写地址 数据范围 BCD格式
0x81 0x80 00-59
0x83 0x82 00-59
0x85 0x84 0-23 (24h)
1-12 (12h)
0x87 0x86 1-31
0x89 0x88 1-12
星期 0x8B 0x8A 1-7
0x8D 0x8C 00-99
写保护 0x8F 0x8E - -

重要标志位

  • 秒寄存器bit7 (CH位):时钟停止标志
    • CH=1 → 时钟停止
    • CH=0 → 时钟运行
  • 写保护寄存器bit7 (WP位):写保护
    • WP=1 → 禁止写入
    • WP=0 → 允许写入

3.4 BCD码详解(重点!)

DS1302使用BCD(Binary-Coded Decimal)码存储时间:

什么是BCD码?

十进制23 → 普通二进制 00010111 (0x17)
十进制23 → BCD码      00100011 (0x23)
                      ││││││││
                      ││└┴┴┴└─→ 个位:3
                      └┴──────→ 十位:2

转换公式

// 十进制 → BCD码
BCD = (十位 * 16) + 个位
    = (数值/10 * 16) + (数值%10)
例:23 → (2*16) + 3 = 0x23

// BCD码 → 十进制
十进制 = (高4位 * 10) + 低4位
      = (BCD/16 * 10) + (BCD%16)
例:0x23 → (2*10) + 3 = 23

你的代码中的转换

// 写入时:十进制转BCD
Write_Ds1302_Byte(0x84, ucRtc[i]/10*16 + ucRtc[i]%10);

// 读取时:BCD转十进制
ucRtc[i] = temp/16*10 + temp%16;

4. 代码详解

4.1 底层函数:写一个字节

void Write_Ds1302(unsigned char temp)
{
    unsigned char i;
    for (i=0; i<8; i++)      // 循环8次,每次发送1位
    {
        SCK = 0;             // ① 先拉低时钟
        SDA = temp & 0x01;   // ② 取出最低位放到数据线
        temp >>= 1;          // ③ 右移,准备下一位
        SCK = 1;             // ④ 时钟上升沿,DS1302锁存数据
    }
}

图解过程

发送0x23 (00100011B)

循环1: temp=00100011, SDA=1 → SCK上升 → 锁存bit0=1
循环2: temp=00010001, SDA=1 → SCK上升 → 锁存bit1=1
循环3: temp=00001000, SDA=0 → SCK上升 → 锁存bit2=0
...依此类推

4.2 底层函数:读一个字节

unsigned char Read_Ds1302_Byte(unsigned char address)
{
    unsigned char i, temp = 0x00;

    RST = 0; _nop_();        // ① 确保CE是低电平
    SCK = 0; _nop_();        // ② 确保时钟是低电平
    RST = 1; _nop_();        // ③ CE拉高,开始通信

    Write_Ds1302(address);   // ④ 发送读命令(如0x81)

    for (i=0; i<8; i++)      // ⑤ 读取8位数据
    {
        SCK = 0;             // ⑥ 时钟拉低
        temp >>= 1;          // ⑦ 先右移一位,准备接收
        if (SDA)             // ⑧ 读取数据线
            temp |= 0x80;    // ⑨ 如果是1,设置最高位
        SCK = 1;             // ⑩ 时钟拉高,准备下一位
    }

    RST = 0; _nop_();        // ⑪ CE拉低,结束通信
    SCK = 0; _nop_();
    SCK = 1; _nop_();
    SDA = 0; _nop_();
    SDA = 1; _nop_();        // 复位I/O引脚状态

    return temp;
}

关键理解

  • temp >>= 1 先执行,所以第一个读到的bit 0会移到最高位
  • 循环8次后,bit 0在最低位,bit 7在最高位(正确顺序)

4.3 应用函数:写一个寄存器

void Write_Ds1302_Byte(unsigned char address, unsigned char dat)
{
    RST = 0; _nop_();        // ① CE拉低
    SCK = 0; _nop_();        // ② 时钟拉低
    RST = 1; _nop_();        // ③ CE拉高,开始通信

    Write_Ds1302(address);   // ④ 发送命令字节(如0x80)
    Write_Ds1302(dat);       // ⑤ 发送数据字节(如0x23)

    RST = 0;                 // ⑥ CE拉低,结束通信
}

4.4 高级函数:设置时间

// 输入:ucRtc[0]=11, ucRtc[1]=12, ucRtc[2]=13
// 目标:设置时间为 13:12:11
void Set_Rtc(unsigned char *ucRtc)
{
    unsigned char i;

    // ① 关闭写保护
    Write_Ds1302_Byte(0x8e, 0x00);

    // ② 停止时钟(写秒寄存器时bit7=1)
    Write_Ds1302_Byte(0x80, 0x80);

    // ③ 循环写入时、分、秒
    for (i=0; i<3; i++)
    {
        // 地址:0x84(时) → 0x82(分) → 0x80(秒)
        // 数据:十进制转BCD码
        Write_Ds1302_Byte(0x84 - 2*i,
                          ucRtc[i]/10*16 + ucRtc[i]%10);
    }

    // ④ 开启写保护
    Write_Ds1302_Byte(0x8e, 0x80);
}

地址计算逻辑

i=0: 0x84 - 0 = 0x84 → 写时寄存器, ucRtc[0]=13 → 0x13
i=1: 0x84 - 2 = 0x82 → 写分寄存器, ucRtc[1]=12 → 0x12
i=2: 0x84 - 4 = 0x80 → 写秒寄存器, ucRtc[2]=11 → 0x11

:warning: 重要:最后写秒寄存器时,0x11的bit7=0,自动启动时钟!


4.5 高级函数:读取时间

void Read_Rtc(unsigned char *ucRtc)
{
    unsigned char i, temp;

    EA = 0;  // ① 关中断,防止读取过程被打断

    for (i=0; i<3; i++)
    {
        // ② 读取寄存器(0x85时→0x83分→0x81秒)
        temp = Read_Ds1302_Byte(0x85 - 2*i);

        // ③ BCD转十进制
        ucRtc[i] = temp/16*10 + temp%16;
    }

    EA = 1;  // ④ 开中断
}

为什么要关中断?
防止读取过程中定时器中断打断,造成时间跳变(如12:59:59→13:00:00时正好被打断)。


5. 实战应用

5.1 你的main.c分析

// 全局变量
idata unsigned char ucRtc[3] = {11,12,13};  // 时分秒数组

void main()
{
    System_Init();       // 系统初始化
    Set_Rtc(ucRtc);      // 设置初始时间为13:12:11
    Timer1_Init();       // 启动定时器(1ms中断)

    while(1)
    {
        Key_Proc();           // 按键处理
        Led_Proc();           // LED处理
        Seg_Proc();           // 数码管处理
        Get_Time();           // 读取时间
        Get_Temperature();    // 读取温度
    }
}

5.2 定时读取时间

idata unsigned char Time_Slow_Down;  // 慢速计数器

void Get_Time()
{
    if (Time_Slow_Down < 100) return;  // 未到100ms
    Time_Slow_Down = 0;                // 清零

    Read_Rtc(ucRtc);  // 读取时间到ucRtc数组
    // 此时 ucRtc[0]=时, ucRtc[1]=分, ucRtc[2]=秒
}

// 定时器中断(每1ms执行一次)
void Timer1_Isr(void) interrupt 3
{
    Time_Slow_Down++;  // 累加计数器
    // ...
}

逻辑:每100ms读一次DS1302,减少通信次数。


5.3 完整时钟示例

// 显示时间到数码管:HH-MM-SS
void Display_Time()
{
    Seg_Buf[0] = ucRtc[0] / 10;     // 时十位
    Seg_Buf[1] = ucRtc[0] % 10;     // 时个位
    Seg_Buf[2] = 11;                // '-' 符号
    Seg_Buf[3] = ucRtc[1] / 10;     // 分十位
    Seg_Buf[4] = ucRtc[1] % 10;     // 分个位
    Seg_Buf[5] = 11;                // '-' 符号
    Seg_Buf[6] = ucRtc[2] / 10;     // 秒十位
    Seg_Buf[7] = ucRtc[2] % 10;     // 秒个位
}

5.4 按键调时功能

unsigned char mode = 0;  // 0=正常, 1=调时, 2=调分, 3=调秒

void Key_Proc()
{
    if (Key_Down == 4)  // S4切换模式
    {
        mode = (mode + 1) % 4;
    }

    if (Key_Down == 5)  // S5加1
    {
        if (mode == 1) ucRtc[0] = (ucRtc[0] + 1) % 24;  // 时+1
        if (mode == 2) ucRtc[1] = (ucRtc[1] + 1) % 60;  // 分+1
        if (mode == 3) ucRtc[2] = (ucRtc[2] + 1) % 60;  // 秒+1
        Set_Rtc(ucRtc);  // 写入DS1302
    }
}

6. 常见问题

:cross_mark: 问题1:时间不走

现象:读取的时间一直是初始值
原因

  1. CH位=1,时钟停止
  2. 晶振没焊接或损坏
  3. 写保护未关闭

解决

// 确保启动时钟
Write_Ds1302_Byte(0x8e, 0x00);  // 关闭写保护
Write_Ds1302_Byte(0x80, 0x00);  // CH=0,启动时钟

:cross_mark: 问题2:时间乱码

现象:数码管显示25:78:99
原因:BCD转换错误

检查代码

// ❌ 错误:直接写十进制
Write_Ds1302_Byte(0x84, 23);  // 会写入0x17,显示为27!

// ✅ 正确:转BCD
Write_Ds1302_Byte(0x84, 0x23);  // 或 23/10*16 + 23%10

:cross_mark: 问题3:通信失败

现象:读取全0或全FF
原因

  1. 引脚定义错误
  2. 时序不对
  3. I/O引脚配置问题

检查

// 确认引脚
sbit SCK = P1^7;  // 对应硬件电路
sbit SDA = P2^3;
sbit RST = P1^3;

// I/O引脚要配置为准双向口或推挽输出
P2M1 &= ~0x08; P2M0 &= ~0x08;  // P2.3配置为准双向

:cross_mark: 问题4:断电丢失时间

原因:没接备用电池
解决:VCC1引脚接CR2032纽扣电池(3V)


:cross_mark: 问题5:时间偶尔跳变

原因:读取时被中断打断
解决:读取时关中断

EA = 0;          // 关总中断
Read_Rtc(ucRtc); // 读取
EA = 1;          // 开总中断

7. 进阶练习

:bullseye: 练习1:完整日期显示

任务:显示年-月-日
提示

void Set_Date(unsigned char year, unsigned char month, unsigned char day)
{
    Write_Ds1302_Byte(0x8e, 0x00);
    Write_Ds1302_Byte(0x8c, year/10*16 + year%10);   // 年
    Write_Ds1302_Byte(0x88, month/10*16 + month%10); // 月
    Write_Ds1302_Byte(0x86, day/10*16 + day%10);     // 日
    Write_Ds1302_Byte(0x8e, 0x80);
}

:bullseye: 练习2:闹钟功能

任务:到设定时间蜂鸣器响
提示

if (ucRtc[0] == alarm_hour && ucRtc[1] == alarm_min)
{
    BEEP = !BEEP;  // 蜂鸣器翻转
}

:bullseye: 练习3:使用RAM存储数据

任务:掉电保存按键次数
提示

// 写RAM(地址0xC0-0xFD)
Write_Ds1302_Byte(0xc0, count);  // 写入
count = Read_Ds1302_Byte(0xc1);  // 读取

:bullseye: 练习4:12小时制显示

任务:显示AM/PM
提示

// 读取时寄存器
unsigned char hour_reg = Read_Ds1302_Byte(0x85);
if (hour_reg & 0x80)  // bit7=1表示12小时制
{
    unsigned char hour = (hour_reg & 0x1F) / 16 * 10 + (hour_reg & 0x0F);
    if (hour_reg & 0x20)  // bit5=1表示PM
        显示PM;
    else
        显示AM;
}

:books: 学习检查清单

完成以下任务,确保你真正掌握DS1302:

  • 能画出DS1302的引脚连接图
  • 理解3线串行通信的时序
  • 掌握BCD码与十进制的转换
  • 能解释命令字节的每一位含义
  • 理解CH位和WP位的作用
  • 能独立写出Write_Ds1302函数
  • 能独立写出Read_Ds1302_Byte函数
  • 能实现设置时间功能
  • 能实现读取时间并显示
  • 能添加按键调时功能
  • 知道为什么读取时要关中断
  • 能排查常见的时间不走、乱码问题

:link: 参考资料

  1. 数据手册DS1302.pdf(已在你的资料包中)
  2. 你的驱动代码Driver/ds1302.cds1302.h
  3. 你的主程序User/main.c
  4. 蓝桥杯官方论坛:搜索DS1302历年真题

:light_bulb: 学习建议

第一阶段:理论理解(1天)

  1. 通读本文档,理解通信原理
  2. 对照数据手册,理解寄存器定义
  3. 手画时序图,加深理解

第二阶段:代码分析(1天)

  1. 逐行阅读ds1302.c,添加注释
  2. 修改main.c,显示时间到数码管
  3. 用逻辑分析仪观察波形(如果有)

第三阶段:功能扩展(2天)

  1. 实现按键调时
  2. 添加日期显示
  3. 实现闹钟功能
  4. 完成蓝桥杯历年真题中的DS1302题目

:envelope: 遇到问题怎么办?

  1. 先看本文档的"常见问题"章节
  2. 检查硬件连接(90%的问题出在这里)
  3. 用万用表测电压:VCC应为5V,晶振两脚应有微弱交流电压
  4. 对比工作代码:你的ds1302.c是经过验证的
  5. 分步调试:先保证读写一个字节正确,再测试整体功能

祝你学习顺利!掌握DS1302后,你就可以做出各种时钟项目了! :tada:


文档版本:V1.0
最后更新:2026-02-08
适用于:蓝桥杯单片机竞赛 STC15F2K60S2

2 个赞