STM32F103+DS1302实时时钟+OLED显示完整教程(含原理、接线、源码、实物演示)
前言
本篇教程基于 STM32F103C8T6 最小系统板搭配 DS1302 时钟模块、0.96 寸 I2C OLED 显示屏,完整讲解硬件接线、DS1302 底层通信原理、BCD 码转换、断电走时机制,附带完整工程逻辑、实物现象演示,预留源码插入板块,零基础也能看懂、直接复刻实验。
DS1302 是低成本常用实时时钟芯片,自带 32.768kHz 晶振计时,搭配纽扣备用电池可实现主控断电后持续计时,单片机仅负责读写时间、屏幕刷新,是电子设计、单片机课程实训高频项目。

一、整体工程实现功能
整体工作流程文字概括:
- DS1302 外接 32.768kHz 晶振产生基准时钟信号,内部分频得到 1 秒脉冲,自动累计秒、分、时、年 / 月 / 日 / 星期;
- STM32 通过 PC13/PC14/PC15 三根 GPIO(RST/CLK/DAT)三线通信,读取芯片内部 BCD 格式时间寄存器;
- 单片机完成 BCD 码转十进制数值校验;
- 通过软件 I2C 驱动 OLED 屏幕,实时刷新日历、时分秒;
- DS1302 配备备用纽扣电池,STM32 整机断电后芯片独立计时,重新上电无需重新校准时间。
核心特点:
- 三线自定义通信,不占用硬件 I2C/SPI 外设;
- 断电续航,无需单片机持续供电计时;
- 自动校验时间合法性,异常自动重置默认时间;
- OLED 报错提示,方便快速排查接线故障。
二、硬件接线说明(STM32F103C8T6)
2.1 DS1302 与单片机引脚对应
| DS1302 模块引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| CLK | PC15 | 通信时钟,单片机单向输出 |
| DAT | PC14 | 双向数据线,写输出、读输入 |
| RST/CE | PC13 | 通信使能,高电平开启数据传输 |
| VCC | 3.3V | 模块主供电,禁止 5V 直连 |
| GND | GND | 单片机与时钟模块共地(必须连接) |

2.2 OLED 显示屏(软件 I2C)接线
| OLED 引脚 | STM32 引脚 | 功能 |
|---|---|---|
| SCL | PB8 | I2C 时钟线 |
| SDA | PB9 | I2C 数据线 |
| VCC | 3.3V | 屏幕供电 |
| GND | GND | 共地 |
2.3 PC13~PC15 引脚使用关键注意点
PC13、PC14、PC15 属于 STM32 低速弱驱动引脚,专门适配 DS1302 这类低速外设,本项目配置 IO 速度为 2MHz,读写时序加入 1μs 软件延时,保证通信稳定,同时方便示波器测量时序信号。
2.4 DAT 双向引脚原理(重点)
DAT 单线同时实现读写,程序中必须动态切换 IO 模式:
- 向 DS1302 写命令 / 写时间:PC14 配置推挽输出;
- 读取芯片时间数据:发送读指令后,PC14 切换浮空输入;
- 单次通信结束,重新拉低数据线并切回输出。 若读取时不切换输入模式,单片机会与 DS1302 电平冲突,出现乱码、读取失败。
三、DS1302 核心工作原理
3.1 32.768kHz 晶振计时原理
晶振每秒振荡 32768 次,数值满足32768=2^15,芯片内部连续 15 次二分频: 32768Hz ÷ 2¹⁵ = 1Hz,精准生成 1 秒计数脉冲。 内部计数器自动进位:秒满 60 进分、分满 60 进时,内置日历逻辑自动区分大小月、闰年,更新日期与星期。
3.2 双电源断电续时机制
芯片内置电源自动切换电路,两路供电:
- 主电源:单片机 3.3V 供电,整机工作时为主供电;
- 备用电池:模块背面 CR2032 纽扣电池。 主控断电后,芯片自动切换电池供电,仅维持晶振与计数器运行,功耗极低,电池可续航数月。 故障现象:无电池 / 电池装反 / 没电,断电后时间清零,每次上电重置默认时间。
3.3 时钟停止位 CH(秒寄存器最高位)
| CH 位电平 | 工作状态 |
|---|---|
| 0 | 晶振正常起振,持续计时 |
| 1 | 振荡器停止,时间冻结 |
设置时间标准流程:先置 CH=1 暂停计时,写完年月日时分秒全部寄存器后,再清除 CH 位,统一开始计时,避免写入过程中时间跳变。
3.4 三线串行通信规则
DS1302 无标准 SPI/I2C 协议,自定义三线通信,所有字节低位先行(先发 bit0,后发 bit7):
- RST 拉低、CLK 拉低,初始化通信总线;
- RST 拉高,使能芯片,允许收发数据;
- 写操作:单片机 CLK 上升沿发送数据位;
- 读操作:发送读指令后切换 DAT 为输入,CLK 下降沿读取芯片输出数据;
- 全部字节传输完成,RST 拉低结束通信。
3.5 寄存器地址与 BCD 存储格式
1)时间寄存器读写地址
| 数据项 | 写地址 | 读地址 | 数值范围 |
|---|---|---|---|
| 秒 | 0x80 | 0x81 | 00~59,bit7 为 CH 停止位 |
| 分 | 0x82 | 0x83 | 00~59 |
| 时 | 0x84 | 0x85 | 00~23(24 小时制) |
| 日 | 0x86 | 0x87 | 01~31 |
| 月 | 0x88 | 0x89 | 01~12 |
| 星期 | 0x8A | 0x8B | 1~7 |
| 年份 | 0x8C | 0x8D | 00~99(仅存储后两位) |
| 写保护寄存器 | 0x8E | 0x8F | bit7 为 WP 写保护位 |
| 涓流充电控制 | 0x90 | 0x91 | 本项目关闭充电,写入 0x00 |
2)压缩 BCD 码转换逻辑
芯片不直接存储十进制,使用 1 字节压缩 BCD 码:高 4 位代表十位,低 4 位代表个位。 示例:十进制 30 → 高 4 位 0011(3),低 4 位 0000(0),BCD 数值 0x30。 转换公式: 十进制转 BCD:BCD = (num/10 << 4) | (num%10) BCD 转十进制:num = (BCD>>4)*10 + (BCD&0x0F)
演示时间 2026-08-14 12:30:01 对应 BCD 值: 年 0x26、月 0x08、日 0x14、星期 0x05、时 0x12、分 0x30、秒 0x01
3)写保护 WP 位操作规则
WP=1:锁定所有寄存器,禁止修改时间; WP=0:解锁,允许读写日历寄存器。 校准时间固定步骤:解锁写保护→暂停时钟写入时间→清除 CH 启动计时→重新上锁。
3.6 上电整体程序逻辑
- STM32 上电,初始化系统延时、OLED 屏幕;
- 配置 PC13/14/15 为 DS1302 通信 IO;
- 读取 DS1302 当前时间,校验合法性;
- 异常判定(时钟停止 / 数据非法 / 年份早于 2026),自动写入默认时间 2026-08-14 12:30:01;
- 关闭涓流充电,开启写保护;
- 主循环循环读取时间,BCD 转十进制校验;
- 秒数发生变化时刷新 OLED 屏幕,无变化跳过刷新降低占用。
四、工程源码板块(可直接复制粘贴代码)
本章节预留完整源码,工程分为 DS1302 底层驱动、OLED 驱动、主函数三部分,全部基于标准库 STM32F103 开发,可直接复制到 Keil 工程使用。
4.1 工程文件架构说明
| 文件路径 | 核心函数 | 功能 |
|---|---|---|
| DS1302.c | DS1302_Init() | IO 初始化、上电时间校验 |
| 同上 | DS1302_WriteRegister() | 单字节寄存器写入时序 |
| 同上 | DS1302_ReadRegister() | 单字节寄存器读取时序 |
| 同上 | DS1302_SetTime() | 十进制时间写入芯片,自动 BCD 转换 |
| 同上 | DS1302_GetTime() | 批量读取时间、BCD 转十进制、数据校验 |
| 同上 | DS1302_ResetToDefault() | 强制恢复默认演示时间 |
| main.c | DS1302_ShowTime() | OLED 正常时间显示函数 |
| main.c | DS1302_ShowError() | 通信故障报错界面打印 |
4.2 源码插入区
main.c代码
/* DS1302 clock teaching demonstration for STM32F103C8T6. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "DS1302.h"
#define DISPLAY_POLL_TIME_MS 100U
static void DS1302_ShowTime(const DS1302_TimeTypeDef *time);
static void DS1302_ShowError(void);
int main(void)
{
DS1302_TimeTypeDef time;
u8 last_second = 0xFFU;
u8 showing_error = 0U;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
DS1302_Init();
while(1)
{
if(DS1302_GetTime(&time) != 0U)
{
if((time.second != last_second) || (showing_error != 0U))
{
DS1302_ShowTime(&time);
last_second = time.second;
showing_error = 0U;
}
}
else if(showing_error == 0U)
{
DS1302_ShowError();
showing_error = 1U;
}
delay_ms(DISPLAY_POLL_TIME_MS);
}
}
static void DS1302_ShowTime(const DS1302_TimeTypeDef *time)
{
static u8 week_text[7][4] =
{
"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"
};
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"DS1302 RTC CLOCK", 12);
OLED_ShowNum(0, 14, time->year, 4, 16);
OLED_ShowChar(32, 14, '-', 16);
OLED_ShowNum(40, 14, time->month, 2, 16);
OLED_ShowChar(56, 14, '-', 16);
OLED_ShowNum(64, 14, time->day, 2, 16);
OLED_ShowString(88, 14, week_text[time->week - 1U], 16);
OLED_ShowNum(16, 34, time->hour, 2, 24);
OLED_ShowChar(40, 34, ':', 24);
OLED_ShowNum(52, 34, time->minute, 2, 24);
OLED_ShowChar(76, 34, ':', 24);
OLED_ShowNum(88, 34, time->second, 2, 24);
OLED_Refresh();
}
static void DS1302_ShowError(void)
{
OLED_Clear();
OLED_ShowString(0, 0, (u8 *)"DS1302 RTC ERROR", 12);
OLED_ShowString(0, 16, (u8 *)"CHECK WIRING:", 12);
OLED_ShowString(0, 32, (u8 *)"CLK PC15 DAT PC14", 12);
OLED_ShowString(0, 48, (u8 *)"RST PC13 VCC 3V3", 12);
OLED_Refresh();
}
DS1302.c代码
/**
* @file DS1302.c
* @brief DS1302 实时时钟模块三线串行驱动
* @note 本文件采用 UTF-8 编码;通信命令和数据均按最低位优先传输。
*/
#include "DS1302.h"
#include "delay.h"
/* DS1302 时钟及控制寄存器的写地址,读地址等于对应写地址加 1。 */
#define DS1302_REG_SECOND_WRITE 0x80U /* 秒寄存器,最高位为停振控制位 */
#define DS1302_REG_MINUTE_WRITE 0x82U /* 分寄存器 */
#define DS1302_REG_HOUR_WRITE 0x84U /* 时寄存器 */
#define DS1302_REG_DATE_WRITE 0x86U /* 日期寄存器 */
#define DS1302_REG_MONTH_WRITE 0x88U /* 月份寄存器 */
#define DS1302_REG_WEEK_WRITE 0x8AU /* 星期寄存器 */
#define DS1302_REG_YEAR_WRITE 0x8CU /* 年份寄存器,只保存年份后两位 */
#define DS1302_REG_WP_WRITE 0x8EU /* 写保护寄存器 */
#define DS1302_REG_TRICKLE_WRITE 0x90U /* 涓流充电控制寄存器 */
/* RST、CLK 和 DAT 三个信号线的电平控制宏。 */
#define DS1302_RST_LOW() GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_RST_PIN)
#define DS1302_RST_HIGH() GPIO_SetBits(DS1302_GPIO_PORT, DS1302_RST_PIN)
#define DS1302_CLK_LOW() GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_CLK_PIN)
#define DS1302_CLK_HIGH() GPIO_SetBits(DS1302_GPIO_PORT, DS1302_CLK_PIN)
#define DS1302_DAT_LOW() GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_DAT_PIN)
#define DS1302_DAT_HIGH() GPIO_SetBits(DS1302_GPIO_PORT, DS1302_DAT_PIN)
/* 文件内部使用的底层辅助函数。 */
static void DS1302_GPIO_Init(void);
static void DS1302_DAT_SetOutput(void);
static void DS1302_DAT_SetInput(void);
static void DS1302_WriteByte(u8 data);
static u8 DS1302_ReadByte(void);
static void DS1302_ReadClockRegisters(u8 *data);
static u8 DS1302_DecimalToBCD(u8 value);
static u8 DS1302_BCDToDecimal(u8 value);
static u8 DS1302_IsValidTime(const DS1302_TimeTypeDef *time);
static u8 DS1302_DaysInMonth(u16 year, u8 month);
static void DS1302_Delay(void)
{
/* DS1302 允许最高约 2 MHz 时钟;1 us 延时便于示波器教学观察。 */
delay_us(1U);
}
/**
* @brief 初始化 RST、CLK、DAT 三个 GPIO
*
* PC13~PC15 属于低驱动能力引脚,因此使用 2 MHz 推挽输出模式。
* 空闲状态下 RST、CLK 和 DAT 均保持低电平。
*/
static void DS1302_GPIO_Init(void)
{
GPIO_InitTypeDef gpio_init;
RCC_APB2PeriphClockCmd(DS1302_GPIO_CLOCK, ENABLE);
DS1302_RST_LOW();
DS1302_CLK_LOW();
DS1302_DAT_LOW();
gpio_init.GPIO_Pin = DS1302_RST_PIN | DS1302_CLK_PIN | DS1302_DAT_PIN;
gpio_init.GPIO_Speed = GPIO_Speed_2MHz;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(DS1302_GPIO_PORT, &gpio_init);
}
static void DS1302_DAT_SetOutput(void)
{
GPIO_InitTypeDef gpio_init;
/* 先准备输出数据,再改变引脚方向,避免 DAT 上出现无效高脉冲。 */
DS1302_DAT_LOW();
gpio_init.GPIO_Pin = DS1302_DAT_PIN;
gpio_init.GPIO_Speed = GPIO_Speed_2MHz;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(DS1302_GPIO_PORT, &gpio_init);
}
/**
* @brief 将双向 DAT 引脚切换为浮空输入
* @note 读取 DS1302 数据前必须释放 DAT,避免单片机和芯片同时驱动总线。
*/
static void DS1302_DAT_SetInput(void)
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Pin = DS1302_DAT_PIN;
gpio_init.GPIO_Speed = GPIO_Speed_2MHz;
gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DS1302_GPIO_PORT, &gpio_init);
}
/**
* @brief 向 DS1302 串行发送一个字节
* @param data 待发送数据
* @note DS1302 的命令和数据均为低位在前,并在 CLK 上升沿采样 DAT。
*/
static void DS1302_WriteByte(u8 data)
{
u8 bit_index;
for(bit_index = 0U; bit_index < 8U; bit_index++)
{
DS1302_CLK_LOW();
if((data & 0x01U) != 0U)
{
DS1302_DAT_HIGH();
}
else
{
DS1302_DAT_LOW();
}
DS1302_Delay();
DS1302_CLK_HIGH();
DS1302_Delay();
data >>= 1;
}
}
/**
* @brief 从 DS1302 串行读取一个字节
* @return 按最低位优先顺序接收后组合得到的数据
*/
static u8 DS1302_ReadByte(void)
{
u8 bit_index;
u8 data = 0U;
for(bit_index = 0U; bit_index < 8U; bit_index++)
{
/* 下降沿后 DS1302 给出下一位数据。 */
DS1302_CLK_LOW();
DS1302_Delay();
if(GPIO_ReadInputDataBit(DS1302_GPIO_PORT, DS1302_DAT_PIN) != Bit_RESET)
{
data |= (u8)(1U << bit_index);
}
DS1302_CLK_HIGH();
DS1302_Delay();
}
return data;
}
/**
* @brief 完成一次寄存器写事务
*
* RST 拉高后先发送写命令,再发送一个数据字节;事务结束后将 RST 拉低。
*/
void DS1302_WriteRegister(u8 address, u8 data)
{
DS1302_RST_LOW();
DS1302_CLK_LOW();
DS1302_DAT_SetOutput();
DS1302_Delay();
DS1302_RST_HIGH();
DS1302_Delay();
DS1302_WriteByte((u8)(address & 0xFEU));
DS1302_WriteByte(data);
DS1302_RST_LOW();
DS1302_CLK_LOW();
DS1302_DAT_LOW();
DS1302_Delay();
}
/**
* @brief 完成一次寄存器读事务
*
* 发送读命令后立即把 DAT 切换为输入,再接收 DS1302 返回的数据。
*/
u8 DS1302_ReadRegister(u8 address)
{
u8 data;
DS1302_RST_LOW();
DS1302_CLK_LOW();
DS1302_DAT_SetOutput();
DS1302_Delay();
DS1302_RST_HIGH();
DS1302_Delay();
DS1302_WriteByte((u8)(address | 0x01U));
DS1302_DAT_SetInput();
data = DS1302_ReadByte();
DS1302_RST_LOW();
DS1302_CLK_LOW();
DS1302_DAT_SetOutput();
DS1302_DAT_LOW();
DS1302_Delay();
return data;
}
/**
* @brief 连续读取全部日期时间寄存器
* @param data 至少包含 8 个字节的接收缓冲区
*/
static void DS1302_ReadClockRegisters(u8 *data)
{
u8 second_before;
u8 second_after;
u8 retry = 0U;
/*
* 逐个读取寄存器,与教学参考程序保持一致,也兼容部分突发读取
* 不稳定的 DS1302 兼容芯片。若读取过程中恰好跨秒,则整组重读。
*/
do
{
second_before = DS1302_ReadRegister(DS1302_REG_SECOND_WRITE);
data[1] = DS1302_ReadRegister(DS1302_REG_MINUTE_WRITE);
data[2] = DS1302_ReadRegister(DS1302_REG_HOUR_WRITE);
data[3] = DS1302_ReadRegister(DS1302_REG_DATE_WRITE);
data[4] = DS1302_ReadRegister(DS1302_REG_MONTH_WRITE);
data[5] = DS1302_ReadRegister(DS1302_REG_WEEK_WRITE);
data[6] = DS1302_ReadRegister(DS1302_REG_YEAR_WRITE);
second_after = DS1302_ReadRegister(DS1302_REG_SECOND_WRITE);
retry++;
} while((second_before != second_after) && (retry < 3U));
data[0] = second_after;
data[7] = DS1302_ReadRegister(DS1302_REG_WP_WRITE);
}
/** @brief 将十进制数转换为 DS1302 使用的压缩 BCD 码。 */
static u8 DS1302_DecimalToBCD(u8 value)
{
return (u8)(((value / 10U) << 4) | (value % 10U));
}
/** @brief 将压缩 BCD 码转换为十进制数。 */
static u8 DS1302_BCDToDecimal(u8 value)
{
return (u8)(((value >> 4) * 10U) + (value & 0x0FU));
}
/** @brief 检查一个字节的高、低半字节是否都属于有效十进制数字。 */
static u8 DS1302_IsValidBCD(u8 value)
{
if(((value & 0x0FU) > 9U) || (((value >> 4) & 0x0FU) > 9U))
{
return 0U;
}
return 1U;
}
/**
* @brief 计算指定月份的实际天数
* @note DS1302 年份范围为 2000~2099,因此闰年按能被 4 整除判断即可。
*/
static u8 DS1302_DaysInMonth(u16 year, u8 month)
{
static const u8 days[12] =
{
31U, 28U, 31U, 30U, 31U, 30U,
31U, 31U, 30U, 31U, 30U, 31U
};
u8 result;
if((month < 1U) || (month > 12U))
{
return 0U;
}
result = days[month - 1U];
if((month == 2U) && ((year % 4U) == 0U))
{
result = 29U;
}
return result;
}
/** @brief 检查日期、时间及星期各字段是否处于合法范围。 */
static u8 DS1302_IsValidTime(const DS1302_TimeTypeDef *time)
{
if(time == 0)
{
return 0U;
}
if((time->year < 2000U) || (time->year > 2099U))
{
return 0U;
}
if((time->month < 1U) || (time->month > 12U))
{
return 0U;
}
if((time->day < 1U) || (time->day > DS1302_DaysInMonth(time->year, time->month)))
{
return 0U;
}
if((time->hour > 23U) || (time->minute > 59U) || (time->second > 59U))
{
return 0U;
}
if((time->week < DS1302_WEEK_MONDAY) || (time->week > DS1302_WEEK_SUNDAY))
{
return 0U;
}
return 1U;
}
/**
* @brief 将十进制日期时间写入 DS1302
*
* 写入前关闭写保护并设置停振位,全部字段写完后最后写秒寄存器,
* 清除停振位并启动计时,随后重新开启写保护。
*/
u8 DS1302_SetTime(const DS1302_TimeTypeDef *time)
{
if(DS1302_IsValidTime(time) == 0U)
{
return 0U;
}
/* 停钟后依次写入各字段,最后写秒寄存器并启动,保证起点一致。 */
DS1302_WriteRegister(DS1302_REG_WP_WRITE, 0x00U);
DS1302_WriteRegister(DS1302_REG_SECOND_WRITE,
(u8)(DS1302_DecimalToBCD(time->second) | 0x80U));
DS1302_WriteRegister(DS1302_REG_MINUTE_WRITE, DS1302_DecimalToBCD(time->minute));
DS1302_WriteRegister(DS1302_REG_HOUR_WRITE, DS1302_DecimalToBCD(time->hour));
DS1302_WriteRegister(DS1302_REG_DATE_WRITE, DS1302_DecimalToBCD(time->day));
DS1302_WriteRegister(DS1302_REG_MONTH_WRITE, DS1302_DecimalToBCD(time->month));
DS1302_WriteRegister(DS1302_REG_WEEK_WRITE, time->week);
DS1302_WriteRegister(DS1302_REG_YEAR_WRITE,
DS1302_DecimalToBCD((u8)(time->year - 2000U)));
DS1302_WriteRegister(DS1302_REG_TRICKLE_WRITE, 0x00U);
DS1302_WriteRegister(DS1302_REG_SECOND_WRITE, DS1302_DecimalToBCD(time->second));
DS1302_WriteRegister(DS1302_REG_WP_WRITE, 0x80U);
return 1U;
}
/**
* @brief 读取 DS1302 并转换为十进制日期时间
*
* 除了常用的 24 小时制,也兼容芯片中可能已经存在的 12 小时制数据;
* 若检测到停振位、非法 BCD 码或越界日期,则读取失败。
*/
u8 DS1302_GetTime(DS1302_TimeTypeDef *time)
{
u8 raw[8];
u8 hour_raw;
u8 hour;
if(time == 0)
{
return 0U;
}
DS1302_ReadClockRegisters(raw);
if((raw[0] & 0x80U) != 0U)
{
return 0U;
}
if((DS1302_IsValidBCD((u8)(raw[0] & 0x7FU)) == 0U) ||
(DS1302_IsValidBCD((u8)(raw[1] & 0x7FU)) == 0U) ||
(DS1302_IsValidBCD((u8)(raw[3] & 0x3FU)) == 0U) ||
(DS1302_IsValidBCD((u8)(raw[4] & 0x1FU)) == 0U) ||
(DS1302_IsValidBCD(raw[6]) == 0U))
{
return 0U;
}
hour_raw = raw[2];
if((hour_raw & 0x80U) != 0U)
{
/* 兼容芯片中原有的 12 小时制数据。 */
if(DS1302_IsValidBCD((u8)(hour_raw & 0x1FU)) == 0U)
{
return 0U;
}
hour = DS1302_BCDToDecimal((u8)(hour_raw & 0x1FU));
if((hour < 1U) || (hour > 12U))
{
return 0U;
}
if(hour == 12U)
{
hour = 0U;
}
if((hour_raw & 0x20U) != 0U)
{
hour = (u8)(hour + 12U);
}
}
else
{
if(DS1302_IsValidBCD((u8)(hour_raw & 0x3FU)) == 0U)
{
return 0U;
}
hour = DS1302_BCDToDecimal((u8)(hour_raw & 0x3FU));
}
time->second = DS1302_BCDToDecimal((u8)(raw[0] & 0x7FU));
time->minute = DS1302_BCDToDecimal((u8)(raw[1] & 0x7FU));
time->hour = hour;
time->day = DS1302_BCDToDecimal((u8)(raw[3] & 0x3FU));
time->month = DS1302_BCDToDecimal((u8)(raw[4] & 0x1FU));
time->week = (u8)(raw[5] & 0x07U);
time->year = (u16)(2000U + DS1302_BCDToDecimal(raw[6]));
return DS1302_IsValidTime(time);
}
/** @brief 写入教学演示规定的起始日期和时间。 */
void DS1302_ResetToDefault(void)
{
DS1302_TimeTypeDef start_time;
start_time.year = 2026U;
start_time.month = 8U;
start_time.day = 14U;
start_time.hour = 12U;
start_time.minute = 30U;
start_time.second = 1U;
start_time.week = DS1302_WEEK_FRIDAY;
(void)DS1302_SetTime(&start_time);
}
/**
* @brief 初始化 DS1302,并在必要时设置教学起始时间
*
* 已由备用电池正常维持的有效时间不会在单片机重新上电时被覆盖。
*/
void DS1302_Init(void)
{
DS1302_TimeTypeDef current_time;
DS1302_GPIO_Init();
/*
* 只有停振位被置位、日期时间数据无效,或者芯片仍处于 2000 年
* 初始状态时才设置教学起始时间。
* 正常关机后,备用电池会维持 DS1302 继续走时;再次上电绝不重设。
*/
if((DS1302_GetTime(¤t_time) == 0U) || (current_time.year < 2026U))
{
DS1302_ResetToDefault();
}
/* 禁止涓流充电,避免常见 CR2032 模块给不可充电电池充电。 */
DS1302_WriteRegister(DS1302_REG_WP_WRITE, 0x00U);
DS1302_WriteRegister(DS1302_REG_TRICKLE_WRITE, 0x00U);
DS1302_WriteRegister(DS1302_REG_WP_WRITE, 0x80U);
}
DS1302.h代码
/**
* @file DS1302.h
* @brief STM32F103C8T6 驱动 DS1302 实时时钟模块的接口声明
* @note 本文件采用 UTF-8 编码,所有时间参数均使用十进制数表示。
*/
#ifndef __DS1302_H
#define __DS1302_H
#include "sys.h"
/**
* DS1302 与 STM32F103C8T6 的连接:
* RST/CE -> PC13
* DAT/IO -> PC14
* CLK -> PC15
*
* PC13~PC15 的输出能力较弱,驱动中使用 2 MHz GPIO 速度;
* DS1302 模块请使用 3.3 V 供电,使 DAT 电平与 STM32 匹配。
*/
#define DS1302_GPIO_PORT GPIOC
#define DS1302_GPIO_CLOCK RCC_APB2Periph_GPIOC
#define DS1302_RST_PIN GPIO_Pin_13
#define DS1302_DAT_PIN GPIO_Pin_14
#define DS1302_CLK_PIN GPIO_Pin_15
/* 星期编码:1 表示星期一,依次递增,7 表示星期日。 */
#define DS1302_WEEK_MONDAY 1U
#define DS1302_WEEK_TUESDAY 2U
#define DS1302_WEEK_WEDNESDAY 3U
#define DS1302_WEEK_THURSDAY 4U
#define DS1302_WEEK_FRIDAY 5U
#define DS1302_WEEK_SATURDAY 6U
#define DS1302_WEEK_SUNDAY 7U
/**
* @brief DS1302 日期时间结构体
*
* year 保存完整年份(例如 2026),其他成员均保存十进制数,
* 驱动会在读写芯片时自动完成十进制数与 BCD 码之间的转换。
*/
typedef struct
{
u16 year; /* 年:2000~2099 */
u8 month; /* 月:1~12 */
u8 day; /* 日:1~31,驱动会检查每月实际天数 */
u8 hour; /* 时:0~23 */
u8 minute; /* 分:0~59 */
u8 second; /* 秒:0~59 */
u8 week; /* 星期:1~7,星期一为 1 */
} DS1302_TimeTypeDef;
/**
* @brief 初始化 DS1302 使用的 GPIO 和芯片状态
* @note 仅在时钟停止、数据无效或年份早于 2026 时写入教学起始时间;
* 正常重新上电时保留由备用电池维持的当前时间。
*/
void DS1302_Init(void);
/**
* @brief 读取当前日期和时间
* @param time 用于接收十进制日期时间的结构体指针
* @return 读取且校验成功返回 1,芯片数据无效或指针为空返回 0
*/
u8 DS1302_GetTime(DS1302_TimeTypeDef *time);
/**
* @brief 设置日期和时间
* @param time 指向待写入十进制日期时间的结构体
* @return 参数合法且完成写入返回 1,参数非法或指针为空返回 0
*/
u8 DS1302_SetTime(const DS1302_TimeTypeDef *time);
/** @brief 强制恢复到 2026-08-14(星期五)12:30:01。 */
void DS1302_ResetToDefault(void);
/**
* @brief 向 DS1302 指定寄存器写入一个字节
* @param address 寄存器写地址,例如秒寄存器写地址为 0x80
* @param data 要写入的数据
*/
void DS1302_WriteRegister(u8 address, u8 data);
/**
* @brief 从 DS1302 指定寄存器读取一个字节
* @param address 可传入寄存器写地址或读地址,函数会自动将最低位置 1
* @return 从寄存器读出的原始数据
*/
u8 DS1302_ReadRegister(u8 address);
#endif
五、实物演示效果与现象分析
5.1 正常运行屏幕显示效果
OLED 分三行稳定显示日历与时间,格式如下:
DS1302 RTC CLOCK
2026-08-14 FRI
12:30:01
各行含义:
- 第一行:项目标题 DS1302 实时时钟;
- 第二行:完整年月日 + 星期英文缩写;
- 第三行:24 小时制时分秒,每秒自动刷新变化。
程序优化逻辑:主循环每 100ms 读取一次时钟,仅当秒数字发生变化时刷新屏幕,减少 I2C 通信损耗。

5.2 通信故障报错界面
DS1302 接线错误、芯片损坏、时序异常时,屏幕弹出故障提示:
DS1302 RTC ERROR
CHECK WIRING:
CLK PC15 DAT PC14
RST PC13 VCC 3V3
5.3 实物测试现象拆解
现象 1:刚下载程序,秒数 1~2 秒后才跳动
原因:32.768kHz 晶振上电需要短暂起振,分频满 1 秒脉冲后秒数才会进位,属于正常现象,无需维修。
现象 2:通电正常走时,断电重启恢复默认 12:30:01
故障定位:备用电池供电失效,依次排查:
- 模块未安装 CR2032 纽扣电池;
- 电池正负极装反;
- 电池电压耗尽(低于 2.7V);
- 电池座金属弹片氧化、接触不良;
- 模块备用电源走线虚焊断路。
现象 3:屏幕时间完全静止,秒数不增加
分步排查方案:
- 测量 DS1302 VCC 与 GND 电压,确认 3.3V 供电正常、共地可靠;
- 可写入时间但不走时:32.768kHz 晶振虚焊、损坏;
- 屏幕持续 ERROR 报错:CLK/DAT/RST 三根信号线接反、断线;
- 时间随机跳变乱码:杜邦线过长、接地不良、DAT 引脚接触松动。
现象 4:禁止涓流充电说明
DS1302 自带涓流充电寄存器,本项目写入 0x00 关闭充电。市面上通用模块使用不可充电 CR2032 一次性纽扣电池,开启充电会存在漏液、鼓包安全隐患;若使用专用可充电电池模块,再根据手册开启充电功能。
六、常见故障排查汇总
| 故障现象 | 根本原因 | 解决办法 |
|---|---|---|
| 每次上电重置默认时间 | 备用电池失效 / 接触不良 | 更换纽扣电池,清理电池座触点 |
| OLED 持续打印 RTC ERROR | DS1302 三根通信线接错 | 核对 PC13/RST、PC14/DAT、PC15/CLK 接线 |
| 能读写寄存器,秒数不增长 | 32.768kHz 晶振损坏 / 虚焊 | 重新焊接晶振,更换时钟模块 |
| 时间数据乱码、偶尔跳数 | 线路无共地、引线过长 | 缩短杜邦线,所有模块 GND 统一连接单片机地 |
| 无法修改时间,写入无效 | WP 写保护位未清零 | 设置时间前先向 0x8E 寄存器写入 0x00 解锁 |
| DAT 读写失败,读取全 0 | IO 未动态切换输入输出 | 读时序中及时将 PC14 切换浮空输入模式 |
更多推荐



所有评论(0)