1. 项目概述
最近在做一个需要精确时间记录的项目,选用了经典的DS1302实时时钟模块搭配STM32F103C8T6开发板。这个组合在嵌入式领域非常常见,但实际开发过程中发现网上很多代码示例存在各种问题,要么是HAL库适配不完整,要么是时序控制不够精准。经过一番折腾,终于实现了一个稳定可靠的解决方案,现在把完整代码和实现思路分享给大家。
DS1302作为一款性价比较高的RTC芯片,虽然精度比不上DS3231,但对于大多数不需要超高精度的时间记录应用来说已经完全够用。它最大的优势在于接口简单、成本低廉,而且自带31字节的RAM可以存储一些额外数据。
2. 硬件连接与准备
2.1 所需材料清单
- STM32F103C8T6最小系统板(Blue Pill)1个
- DS1302实时时钟模块1个
- 32.768kHz晶振(通常模块已自带)
- CR2032电池座(用于断电保持)
- 杜邦线若干
- ST-Link V2调试器1个
2.2 硬件接线图
DS1302与STM32的连接非常简单,只需要3根信号线:
code复制DS1302 STM32F103C8T6
-------------------------
VCC -> 3.3V
GND -> GND
RST -> PA11
SCLK -> PA8
I/O -> PB4
注意:DS1302的VCC2接3.3V,如果有备用电池可以接到VCC1,这样断电时时钟还能继续走时。
2.3 硬件注意事项
-
晶振选择:虽然模块自带晶振,但如果发现走时不准,可以尝试更换质量更好的32.768kHz晶振,并确保负载电容匹配。
-
电源处理:DS1302的工作电压范围是2.0-5.5V,与STM32的3.3V逻辑电平完全兼容。如果使用5V供电的DS1302模块,需要注意电平转换。
-
布线建议:时钟信号线尽量短,避免并行走线,减少干扰。如果必须长距离布线,可以考虑加入适当的上拉电阻。
3. 软件实现详解
3.1 开发环境配置
- 安装Keil MDK-ARM(建议V5.25以上版本)
- 安装STM32CubeMX(V6.8.1)
- 安装SEGGER RTT Viewer(用于调试输出)
- 安装STM32F1xx HAL库(通过CubeMX自动安装)
3.2 工程创建与配置
使用STM32CubeMX创建工程:
- 选择STM32F103C8Tx系列芯片
- 配置系统时钟为72MHz(HSE 8MHz,PLL 9倍频)
- 启用SWD调试接口
- 配置PA11、PA8为GPIO输出,PB4为GPIO输入/输出
- 生成Keil工程代码
3.3 DS1302驱动实现
3.3.1 头文件定义(ds1302.h)
c复制#ifndef __DS1302_H
#define __DS1302_H
#include "stm32f1xx_hal.h"
// 类型定义
typedef uint8_t u8;
typedef uint16_t u16;
// 引脚定义
#define CE_L HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET)
#define CE_H HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_SET)
#define SCLK_L HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET)
#define SCLK_H HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET)
#define DATA_L HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_RESET)
#define DATA_H HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_SET)
#define DATA_Read HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4)
// 时间结构体
typedef struct {
u16 year;
u8 month;
u8 day;
u8 hour;
u8 minute;
u8 second;
u8 week;
} TIMEData;
// 函数声明
void ds1302_gpio_init(void);
void ds1302_write_onebyte(u8 data);
void ds1302_wirte_rig(u8 address, u8 data);
u8 ds1302_read_rig(u8 address);
void ds1302_init(void);
void ds1302_data_out_init(void);
void ds1302_data_in_init(void);
void ds1302_read_time(void);
void ds1302_read_realTime(TIMEData *time);
void delay_us(uint32_t us);
#endif
3.3.2 核心驱动实现(ds1302.c)
c复制#include "ds1302.h"
#include "main.h"
#include "gpio.h"
// 全局变量
TIMEData TimeData;
u8 read_time[7];
// 微秒延时函数
void delay_us(uint32_t us) {
uint32_t ticks = (HAL_RCC_GetHCLKFreq() / 1000000) * us;
uint32_t start = SysTick->VAL;
while((SysTick->VAL - start) < ticks);
}
// IO配置为输出
void ds1302_data_out_init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
// IO配置为输入
void ds1302_data_in_init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
// 写一字节数据
void ds1302_write_onebyte(u8 data) {
ds1302_data_out_init();
for(u8 i=0; i<8; i++) {
SCLK_L;
if(data & 0x01) DATA_H;
else DATA_L;
SCLK_H;
data >>= 1;
}
}
// 写寄存器
void ds1302_wirte_rig(u8 address, u8 data) {
CE_L; SCLK_L; delay_us(1);
CE_H; delay_us(2);
ds1302_write_onebyte(address);
ds1302_write_onebyte(data);
CE_L; SCLK_L; delay_us(2);
}
// 读寄存器
u8 ds1302_read_rig(u8 address) {
u8 data = 0;
CE_L; SCLK_L; delay_us(3);
CE_H; delay_us(3);
ds1302_write_onebyte(address);
ds1302_data_in_init();
delay_us(2);
for(u8 i=0; i<8; i++) {
data >>= 1;
SCLK_H; delay_us(4);
SCLK_L; delay_us(14);
if(DATA_Read) data |= 0x80;
}
CE_L; DATA_L;
return data;
}
// DS1302初始化
void ds1302_init(void) {
// 关闭写保护
ds1302_wirte_rig(0x8E, 0x00);
// 设置初始时间:2026-02-03 15:40:00 星期二
ds1302_wirte_rig(0x80, 0x00); // 秒
ds1302_wirte_rig(0x82, 0x40); // 分
ds1302_wirte_rig(0x84, 0x15); // 时
ds1302_wirte_rig(0x86, 0x03); // 日
ds1302_wirte_rig(0x88, 0x02); // 月
ds1302_wirte_rig(0x8A, 0x02); // 星期
ds1302_wirte_rig(0x8C, 0x26); // 年
// 开启写保护
ds1302_wirte_rig(0x8E, 0x80);
}
// 读取BCD时间
void ds1302_read_time(void) {
read_time[0] = ds1302_read_rig(0x81); // 秒
read_time[1] = ds1302_read_rig(0x83); // 分
read_time[2] = ds1302_read_rig(0x85); // 时
read_time[3] = ds1302_read_rig(0x87); // 日
read_time[4] = ds1302_read_rig(0x89); // 月
read_time[5] = ds1302_read_rig(0x8B); // 星期
read_time[6] = ds1302_read_rig(0x8D); // 年
}
// BCD转十进制
void ds1302_read_realTime(TIMEData *time) {
ds1302_read_time();
time->second = (read_time[0]>>4)*10 + (read_time[0]&0x0F);
time->minute = (read_time[1]>>4)*10 + (read_time[1]&0x0F);
time->hour = (read_time[2]>>4)*10 + (read_time[2]&0x0F);
time->day = (read_time[3]>>4)*10 + (read_time[3]&0x0F);
time->month = (read_time[4]>>4)*10 + (read_time[4]&0x0F);
time->week = read_time[5] & 0x07;
time->year = (read_time[6]>>4)*10 + (read_time[6]&0x0F) + 2000;
}
// GPIO初始化
void ds1302_gpio_init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 配置RST(PA11)和SCLK(PA8)为输出
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始状态
CE_L;
SCLK_L;
DATA_L;
}
3.4 主程序实现(main.c)
c复制#include "main.h"
#include "gpio.h"
#include "ds1302.h"
#include "SEGGER_RTT.h"
#include <stdio.h>
#define TIME_BUFFER_SIZE 64
TIMEData sys_time;
char time_buffer[TIME_BUFFER_SIZE];
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
// 初始化调试输出
SEGGER_RTT_Init();
SEGGER_RTT_printf(0, "DS1302 RTC Test Start!\r\n");
// 初始化DS1302
ds1302_gpio_init();
ds1302_init();
HAL_Delay(100);
while(1) {
// 读取当前时间
ds1302_read_realTime(&sys_time);
// 格式化时间字符串
sprintf(time_buffer, "20%02d-%02d-%02d %02d:%02d:%02d 星期%d",
sys_time.year%100,
sys_time.month,
sys_time.day,
sys_time.hour,
sys_time.minute,
sys_time.second,
sys_time.week);
// 输出时间
SEGGER_RTT_printf(0, "Current Time: %s\r\n", time_buffer);
HAL_Delay(1000);
}
}
void SystemClock_Config(void) {
// 标准72MHz时钟配置
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
4. 关键问题与解决方案
4.1 时序控制问题
DS1302对时序要求比较严格,特别是读写操作时的时钟边沿与数据稳定时间。常见问题包括:
-
读写数据不稳定:通常是由于时序控制不精确导致。解决方案是严格按照数据手册的时序要求,在关键位置加入适当的延时。
-
时间读取错误:可能是由于在数据变化期间读取造成的。建议在读取时间前先停止时钟(写入0x80寄存器),读取完成后再启动时钟。
4.2 BCD码转换问题
DS1302使用BCD码存储时间数据,需要进行转换才能得到十进制数值。常见错误包括:
-
忘记处理高位和低位的分离:BCD码的每个字节高4位和低4位分别代表十位和个位。
-
星期数据格式不一致:不同厂家的DS1302模块对星期的定义可能不同,有的周日是0,有的是1,需要根据实际模块调整。
4.3 电源管理问题
-
断电后时间不保持:检查VCC1是否接入了备用电池,电池电压是否足够(CR2032标称3V,低于2V可能无法正常工作)。
-
时间走时不准:检查晶振是否正常起振,负载电容是否匹配(通常6pF),PCB布线是否合理(避免长走线引入干扰)。
5. 性能优化建议
-
降低功耗:如果不频繁读取时间,可以在两次读取之间将DS1302置于低功耗模式(通过控制CE引脚)。
-
提高精度:可以通过软件补偿来提高走时精度。记录一段时间内的误差,然后在程序中动态调整。
-
批量读取:DS1302支持突发模式连续读取所有时间寄存器,可以减少通信时间。
-
RAM利用:DS1302有31字节的额外RAM,可以用来存储系统配置或其他数据。
6. 扩展功能实现
6.1 闹钟功能实现
虽然DS1302本身没有硬件闹钟功能,但可以通过软件实现:
c复制// 检查是否到达设定时间
uint8_t check_alarm(TIMEData *current, TIMEData *alarm) {
return (current->hour == alarm->hour) &&
(current->minute == alarm->minute) &&
(current->second == alarm->second);
}
// 在主循环中添加
TIMEData alarm_time = {0};
alarm_time.hour = 8;
alarm_time.minute = 30;
alarm_time.second = 0;
if(check_alarm(&sys_time, &alarm_time)) {
SEGGER_RTT_printf(0, "Alarm Triggered!\r\n");
// 执行闹钟动作...
}
6.2 时间校准功能
可以通过串口接收PC或手机的时间来校准:
c复制void set_time_from_string(char *time_str) {
TIMEData new_time;
sscanf(time_str, "%hu-%hhu-%hhu %hhu:%hhu:%hhu",
&new_time.year, &new_time.month, &new_time.day,
&new_time.hour, &new_time.minute, &new_time.second);
// 关闭写保护
ds1302_wirte_rig(0x8E, 0x00);
// 写入新时间
ds1302_wirte_rig(0x80, ((new_time.second/10)<<4)|(new_time.second%10));
ds1302_wirte_rig(0x82, ((new_time.minute/10)<<4)|(new_time.minute%10));
ds1302_wirte_rig(0x84, ((new_time.hour/10)<<4)|(new_time.hour%10));
ds1302_wirte_rig(0x86, ((new_time.day/10)<<4)|(new_time.day%10));
ds1302_wirte_rig(0x88, ((new_time.month/10)<<4)|(new_time.month%10));
ds1302_wirte_rig(0x8C, (((new_time.year-2000)/10)<<4)|((new_time.year-2000)%10));
// 开启写保护
ds1302_wirte_rig(0x8E, 0x80);
}
7. 项目总结
通过这个项目,我们实现了STM32F103C8T6与DS1302实时时钟模块的完整驱动开发。关键点包括:
-
精确的时序控制是DS1302稳定工作的关键,特别是读写操作时的延时处理。
-
BCD码与十进制时间的转换需要特别注意高低位的处理。
-
电源管理对RTC模块尤为重要,备用电池的选择和连接直接影响断电后的时间保持。
-
通过软件可以实现更多扩展功能,如闹钟、定期任务等。
这个方案已经在实际项目中验证过稳定性,代码可以直接用于各种需要时间记录的应用场景。对于需要更高精度的场合,可以考虑使用DS3231等更高级的RTC芯片,但DS1302在大多数应用中已经足够,且成本更低。