前言

本篇教程基于 STM32F103C8T6 最小系统板搭配 DS1302 时钟模块、0.96 寸 I2C OLED 显示屏,完整讲解硬件接线、DS1302 底层通信原理、BCD 码转换、断电走时机制,附带完整工程逻辑、实物现象演示,预留源码插入板块,零基础也能看懂、直接复刻实验。

DS1302 是低成本常用实时时钟芯片,自带 32.768kHz 晶振计时,搭配纽扣备用电池可实现主控断电后持续计时,单片机仅负责读写时间、屏幕刷新,是电子设计、单片机课程实训高频项目。

视频讲解:STM32F103+DS1302实时时钟+OLED显示完整教程(含原理、接线、源码、实物演示)https://www.bilibili.com/video/BV145gK6WEVc/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

一、整体工程实现功能

整体工作流程文字概括:

  1. DS1302 外接 32.768kHz 晶振产生基准时钟信号,内部分频得到 1 秒脉冲,自动累计秒、分、时、年 / 月 / 日 / 星期;
  2. STM32 通过 PC13/PC14/PC15 三根 GPIO(RST/CLK/DAT)三线通信,读取芯片内部 BCD 格式时间寄存器;
  3. 单片机完成 BCD 码转十进制数值校验;
  4. 通过软件 I2C 驱动 OLED 屏幕,实时刷新日历、时分秒;
  5. DS1302 配备备用纽扣电池,STM32 整机断电后芯片独立计时,重新上电无需重新校准时间。

核心特点:

  • 三线自定义通信,不占用硬件 I2C/SPI 外设;
  • 断电续航,无需单片机持续供电计时;
  • 自动校验时间合法性,异常自动重置默认时间;
  • OLED 报错提示,方便快速排查接线故障。

二、硬件接线说明(STM32F103C8T6)

2.1 DS1302 与单片机引脚对应

DS1302 模块引脚STM32 引脚功能说明
CLKPC15通信时钟,单片机单向输出
DATPC14双向数据线,写输出、读输入
RST/CEPC13通信使能,高电平开启数据传输
VCC3.3V模块主供电,禁止 5V 直连
GNDGND单片机与时钟模块共地(必须连接)

2.2 OLED 显示屏(软件 I2C)接线

OLED 引脚STM32 引脚功能
SCLPB8I2C 时钟线
SDAPB9I2C 数据线
VCC3.3V屏幕供电
GNDGND共地

2.3 PC13~PC15 引脚使用关键注意点

PC13、PC14、PC15 属于 STM32 低速弱驱动引脚,专门适配 DS1302 这类低速外设,本项目配置 IO 速度为 2MHz,读写时序加入 1μs 软件延时,保证通信稳定,同时方便示波器测量时序信号。

2.4 DAT 双向引脚原理(重点)

DAT 单线同时实现读写,程序中必须动态切换 IO 模式:

  1. 向 DS1302 写命令 / 写时间:PC14 配置推挽输出;
  2. 读取芯片时间数据:发送读指令后,PC14 切换浮空输入;
  3. 单次通信结束,重新拉低数据线并切回输出。 若读取时不切换输入模式,单片机会与 DS1302 电平冲突,出现乱码、读取失败。

三、DS1302 核心工作原理

3.1 32.768kHz 晶振计时原理

晶振每秒振荡 32768 次,数值满足32768=2^15,芯片内部连续 15 次二分频: 32768Hz ÷ 2¹⁵ = 1Hz,精准生成 1 秒计数脉冲。 内部计数器自动进位:秒满 60 进分、分满 60 进时,内置日历逻辑自动区分大小月、闰年,更新日期与星期。

3.2 双电源断电续时机制

芯片内置电源自动切换电路,两路供电:

  1. 主电源:单片机 3.3V 供电,整机工作时为主供电;
  2. 备用电池:模块背面 CR2032 纽扣电池。 主控断电后,芯片自动切换电池供电,仅维持晶振与计数器运行,功耗极低,电池可续航数月。 故障现象:无电池 / 电池装反 / 没电,断电后时间清零,每次上电重置默认时间。

3.3 时钟停止位 CH(秒寄存器最高位)

CH 位电平工作状态
0晶振正常起振,持续计时
1振荡器停止,时间冻结

设置时间标准流程:先置 CH=1 暂停计时,写完年月日时分秒全部寄存器后,再清除 CH 位,统一开始计时,避免写入过程中时间跳变。

3.4 三线串行通信规则

DS1302 无标准 SPI/I2C 协议,自定义三线通信,所有字节低位先行(先发 bit0,后发 bit7):

  1. RST 拉低、CLK 拉低,初始化通信总线;
  2. RST 拉高,使能芯片,允许收发数据;
  3. 写操作:单片机 CLK 上升沿发送数据位;
  4. 读操作:发送读指令后切换 DAT 为输入,CLK 下降沿读取芯片输出数据;
  5. 全部字节传输完成,RST 拉低结束通信。

3.5 寄存器地址与 BCD 存储格式

1)时间寄存器读写地址
数据项写地址读地址数值范围
0x800x8100~59,bit7 为 CH 停止位
0x820x8300~59
0x840x8500~23(24 小时制)
0x860x8701~31
0x880x8901~12
星期0x8A0x8B1~7
年份0x8C0x8D00~99(仅存储后两位)
写保护寄存器0x8E0x8Fbit7 为 WP 写保护位
涓流充电控制0x900x91本项目关闭充电,写入 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 上电整体程序逻辑

  1. STM32 上电,初始化系统延时、OLED 屏幕;
  2. 配置 PC13/14/15 为 DS1302 通信 IO;
  3. 读取 DS1302 当前时间,校验合法性;
  4. 异常判定(时钟停止 / 数据非法 / 年份早于 2026),自动写入默认时间 2026-08-14 12:30:01;
  5. 关闭涓流充电,开启写保护;
  6. 主循环循环读取时间,BCD 转十进制校验;
  7. 秒数发生变化时刷新 OLED 屏幕,无变化跳过刷新降低占用。

四、工程源码板块(可直接复制粘贴代码)

本章节预留完整源码,工程分为 DS1302 底层驱动、OLED 驱动、主函数三部分,全部基于标准库 STM32F103 开发,可直接复制到 Keil 工程使用。

4.1 工程文件架构说明

文件路径核心函数功能
DS1302.cDS1302_Init()IO 初始化、上电时间校验
同上DS1302_WriteRegister()单字节寄存器写入时序
同上DS1302_ReadRegister()单字节寄存器读取时序
同上DS1302_SetTime()十进制时间写入芯片,自动 BCD 转换
同上DS1302_GetTime()批量读取时间、BCD 转十进制、数据校验
同上DS1302_ResetToDefault()强制恢复默认演示时间
main.cDS1302_ShowTime()OLED 正常时间显示函数
main.cDS1302_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(&current_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

各行含义:

  1. 第一行:项目标题 DS1302 实时时钟;
  2. 第二行:完整年月日 + 星期英文缩写;
  3. 第三行: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

故障定位:备用电池供电失效,依次排查:

  1. 模块未安装 CR2032 纽扣电池;
  2. 电池正负极装反;
  3. 电池电压耗尽(低于 2.7V);
  4. 电池座金属弹片氧化、接触不良;
  5. 模块备用电源走线虚焊断路。
现象 3:屏幕时间完全静止,秒数不增加

分步排查方案:

  1. 测量 DS1302 VCC 与 GND 电压,确认 3.3V 供电正常、共地可靠;
  2. 可写入时间但不走时:32.768kHz 晶振虚焊、损坏;
  3. 屏幕持续 ERROR 报错:CLK/DAT/RST 三根信号线接反、断线;
  4. 时间随机跳变乱码:杜邦线过长、接地不良、DAT 引脚接触松动。
现象 4:禁止涓流充电说明

DS1302 自带涓流充电寄存器,本项目写入 0x00 关闭充电。市面上通用模块使用不可充电 CR2032 一次性纽扣电池,开启充电会存在漏液、鼓包安全隐患;若使用专用可充电电池模块,再根据手册开启充电功能。

六、常见故障排查汇总

故障现象根本原因解决办法
每次上电重置默认时间备用电池失效 / 接触不良更换纽扣电池,清理电池座触点
OLED 持续打印 RTC ERRORDS1302 三根通信线接错核对 PC13/RST、PC14/DAT、PC15/CLK 接线
能读写寄存器,秒数不增长32.768kHz 晶振损坏 / 虚焊重新焊接晶振,更换时钟模块
时间数据乱码、偶尔跳数线路无共地、引线过长缩短杜邦线,所有模块 GND 统一连接单片机地
无法修改时间,写入无效WP 写保护位未清零设置时间前先向 0x8E 寄存器写入 0x00 解锁
DAT 读写失败,读取全 0IO 未动态切换输入输出读时序中及时将 PC14 切换浮空输入模式
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐