
简介围绕STM32F429与DS1307实时时钟芯片的I2C通信例程面向嵌入式初学者及需要快速上手STM32外设开发的工程师。内容涵盖GPIO复用模式配置、I2C外设初始化、读写时序实现以及通过HAL库完成时间设置与读取的完整思路可作为学习I2C协议和RTC驱动设计的实践参考。资源包共1437个文件以C源码、头文件、启动文件及SVD描述文件为主另有PDF说明、工程配置、编译库与文档整体体积84.33MB目录结构清晰便于对照查阅和移植到其他STM32项目。当前已有100人学习下载适合想深入理解嵌入式时间管理、外设驱动编写及HAL库用法的人群是一份兼顾原理讲解与工程实操的典型例程。1. 接上DS1307之前先想清楚这颗RTC芯片在你的F429工程里处于什么位置很多用STM32F429做电子时钟的初学者代码能跑、数码管能跳但一掉电时间就回到初始值问题几乎都出在“主控断电后没人走秒”。DS1307就是为此存在的一颗I2C接口的实时时钟芯片靠备用电池在掉电后继续计时主控上电后通过I2C总线把时间读回来。它和F429搭配的难点不在芯片本身而在I2C的时序、BCD码换算和总线出错时的恢复手段。这篇按外设例程最常见的工程结构来拆先确认硬件接法和寄存器布局再写可复用的驱动骨架最后落到状态机与调试参数。适合已经点亮过F429、手头有DS1307模块或想把时钟功能做进课程设计的人。代码统一用STM32标准库写容易移植到F1/F4系列。2. 硬件连接与寄存器布局DS1307和F429的I2C1为什么需要外部上拉电阻2.1 极简接线DS1307只占F429两个引脚DS1307是8引脚DIP/SOIC封装与F429通信只需两根线SCL时钟和SDA数据。以I2C1为例STM32F429的PB6复用为I2C1_SCLPB7复用为I2C1_SDA。其余引脚按数据手册接即可DS1307引脚接到哪里说明SCLPB6I2C1_SCL时钟线开漏输出需要外部上拉SDAPB7I2C1_SDA数据线开漏输出需要外部上拉VCC3.3V主电源DS1307工作电压范围4.5~5.5V但实际模块大多兼容3.3VVBATCR2032正极或3V纽扣电池掉电后给RTC供电耗电约500nAX1 / X232.768kHz晶振晶振负载电容常见为12.5pF模块上一般已焊好GND地必须与F429共地提示市场上很多DS1307模块直接带电池座和晶振接到开发板时确认模块上的上拉电阻是否为4.7kΩ。如果用的是裸芯片SCL和SDA必须自行接上拉电阻到VCC否则I2C通信会间歇性失败。2.2 寄存器地址和BCD编码读出来的“秒”不是你想的秒DS1307内部时间寄存器从0x00到0x06分别是秒、分、时、星期、日、月、年末尾0x07是控制寄存器。一个反直觉的地方是时间数据用BCD码存储例如秒寄存器读到0x59不代表十进制的59秒而是十位是5、个位是9的编码——直接拿这个数字往串口发显示出来就是十进制的89。所以驱动里必须做BCD转十进制的换算。寄存器0x00的bit7是CH位Clock Halt这一位为1时芯片内部振荡器停振时间不走。市面上某些模块出厂时CH位是1读出来的时间永远是固定值这是“掉电后不走时”之外最常见的翻车点。新芯片首次上电后写秒寄存器前把CH位置0芯片才会开始计时。0x02小时寄存器的bit6是12/24小时制标志bit5是AM/PM位。驱动里建议固定用24小时制写小时寄存器时要保证该位为0否则读回的小时数会带11小时偏差。0x06年份寄存器只有低7位范围0~99软件需要自行加2000换算。2.3 F429的I2C外设初始化标准库必调的三个寄存器组F429的I2C外设挂在APB1总线上初始化分成三步开时钟、配GPIO复用、调I2C参数。GPIO配置成复用开漏外设内部不提供上拉所以必须依赖外部电阻void ds1307_i2c_init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // I2C1时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // PB6/PB7时钟 gpio.GPIO_Mode GPIO_Mode_AF; // 复用功能 gpio.GPIO_OType GPIO_OType_OD; // 开漏输出 gpio.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉弱 gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOB, gpio); GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1); // PB6 - I2C1_SCL GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1); // PB7 - I2C1_SDA I2C_DeInit(I2C1); // 先复位外设清掉残留错误标志 i2c.I2C_Mode I2C_Mode_I2C; i2c.I2C_ClockSpeed 400000; // 快速模式 i2c.I2C_DutyCycle I2C_DutyCycle_2; // 占空比 2:1配合400kHz i2c.I2C_OwnAddress1 0x0A; // 主机地址任意非0即可 i2c.I2C_Ack I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, i2c); I2C_Cmd(I2C1, ENABLE); }逻辑说明GPIO_PuPd_UP只是辅助上拉阻值在30~50kΩ量级只能防止输入悬浮不足以满足400kHz快速模式下的上升沿要求。真实通信质量依赖外部4.7kΩ电阻这也是很多人把速率降到100kHz后“突然能用了”的原因——不是代码变对了而是慢速模式下对上升沿要求变宽了。2.4 I2C总线死锁与外部上拉电阻的实测边界I2C协议里如果主机在传输中途复位而从机仍在输出低电平SDA会被一直拉低总线进入死锁状态。F429没有硬件自动恢复机制代码里需要在每次通信前检查I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)若持续忙则做软件复位I2C_Cmd(I2C1, DISABLE)后置位I2C_SWSTRT再重新使能外设。这一招能解决大部分总线卡死问题但前提是外部上拉电阻存在没有上拉时软件反复复位也没用因为SDA根本拉不回来。3. 写一个可移植的bsp_ds1307.c单字节读写、BCD换算和时间结构体3.1 头文件与引脚宏把硬件约束写进bsp_ds1307.h例程里一般把驱动拆成bsp_ds1307.h和bsp_ds1307.c两个文件。头文件里定义I2C外设、引脚、设备地址和时间结构体方便以后换到I2C2或其他F4型号时只改一处#ifndef __BSP_DS1307_H #define __BSP_DS1307_H #include stm32f4xx.h #define DS1307_I2C I2C1 #define DS1307_I2C_CLK RCC_APB1Periph_I2C1 #define DS1307_I2C_GPIO GPIOB #define DS1307_I2C_GPIO_CLK RCC_AHB1Periph_GPIOB #define DS1307_I2C_SCL_PIN GPIO_Pin_6 #define DS1307_I2C_SDA_PIN GPIO_Pin_7 #define DS1307_ADDR_W 0xD0 // 7位地址0x68左移1位 写位 #define DS1307_ADDR_R 0xD1 // 7位地址0x68左移1位 读位 typedef struct { uint8_t sec; uint8_t min; uint8_t hour; uint8_t week; uint8_t day; uint8_t month; uint16_t year; } ds1307_time_t; void ds1307_init(void); uint8_t ds1307_read_byte(uint8_t reg); void ds1307_write_byte(uint8_t reg, uint8_t val); uint8_t ds1307_get_time(ds1307_time_t *t); uint8_t ds1307_set_time(ds1307_time_t *t); #endif说明DS1307的7位从机地址是0x68左移一位后加上读写位得到0xD0和0xD1。标准库的I2C_Send7bitAddress需要传入完整8位地址所以直接定义这两个宏避免在读写函数里反复移位造成笔误。3.2 单字节读写函数从起始条件到停止条件的完整状态机F429的标准库I2C驱动是阻塞式的每步都要用while等待事件标志。DS1307是慢速设备400kHz下没问题但要注意超时保护——用变量倒计时代替死等否则总线异常时程序会卡死在while里uint8_t ds1307_write_byte(uint8_t reg, uint8_t val) { uint16_t timeout 1000; while (I2C_GetFlagStatus(DS1307_I2C, I2C_FLAG_BUSY)) if (--timeout 0) return 1; // 总线忙超时 I2C_GenerateSTART(DS1307_I2C, ENABLE); timeout 1000; while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_MODE_SELECT)) if (--timeout 0) return 1; I2C_Send7bitAddress(DS1307_I2C, DS1307_ADDR_W, I2C_Direction_Transmitter); timeout 1000; while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) if (--timeout 0) return 1; I2C_SendData(DS1307_I2C, reg); timeout 1000; while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) if (--timeout 0) return 1; I2C_SendData(DS1307_I2C, val); timeout 1000; while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) if (--timeout 0) return 1; I2C_GenerateSTOP(DS1307_I2C, ENABLE); return 0; }参数说明第一次等待的EVENT_MASTER_BYTE_TRANSMITTED是“地址寄存器地址发送完成”第二次才是“数据字节发送完成”。如果只等一次事件就发数据会在从机还未释放SDA时报错。所有事件等待都带超时退出返回值非0表示通信失败调用方可以据此做错误计数或重试。读单字节比写多一个重复起始条件先发设备写地址和寄存器号不产生停止条件随后再发一次起始条件切换为读方向uint8_t ds1307_read_byte(uint8_t reg) { uint16_t timeout 1000; uint8_t val 0; while (I2C_GetFlagStatus(DS1307_I2C, I2C_FLAG_BUSY)) if (--timeout 0) return 0xFF; I2C_GenerateSTART(DS1307_I2C, ENABLE); while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_MODE_SELECT)) if (--timeout 0) return 0xFF; I2C_Send7bitAddress(DS1307_I2C, DS1307_ADDR_W, I2C_Direction_Transmitter); while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) if (--timeout 0) return 0xFF; I2C_SendData(DS1307_I2C, reg); while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) if (--timeout 0) return 0xFF; // 重复起始方向切到读 I2C_GenerateSTART(DS1307_I2C, ENABLE); while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_MODE_SELECT)) if (--timeout 0) return 0xFF; I2C_Send7bitAddress(DS1307_I2C, DS1307_ADDR_R, I2C_Direction_Receiver); while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) if (--timeout 0) return 0xFF; // 在接收前关闭ACK配合最后一次读取自动停止 I2C_AcknowledgeConfig(DS1307_I2C, DISABLE); while (!I2C_CheckEvent(DS1307_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)) if (--timeout 0) return 0xFF; val I2C_ReceiveData(DS1307_I2C); I2C_GenerateSTOP(DS1307_I2C, ENABLE); I2C_AcknowledgeConfig(DS1307_I2C, ENABLE); return val; }提示读取最后一个字节前必须关闭ACK否则从机会认为主机还要继续收数据停止条件发出后总线时序会乱掉。标准库的I2C_EVENT_MASTER_BYTE_RECEIVED在关闭ACK模式下依然有效实测在F429上可以正常读到末字节。3.3 时间转换与读写接口BCD互转、年份低两位和CH位处理时间结构体里的year用uint16_t存完整公元年份写入DS1307时只取低两位并转成BCD读出时再转回完整年份static uint8_t bcd2dec(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } static uint8_t dec2bcd(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); } uint8_t ds1307_get_time(ds1307_time_t *t) { uint8_t raw; raw ds1307_read_byte(0x00); if (raw 0xFF) return 1; // 读失败 if (raw 0x80) return 2; // CH位为1时钟停振 t-sec bcd2dec(raw 0x7F); t-min bcd2dec(ds1307_read_byte(0x01) 0x7F); t-hour bcd2dec(ds1307_read_byte(0x02) 0x3F); t-week bcd2dec(ds1307_read_byte(0x03) 0x07); t-day bcd2dec(ds1307_read_byte(0x04) 0x3F); t-month bcd2dec(ds1307_read_byte(0x05) 0x1F); t-year 2000 bcd2dec(ds1307_read_byte(0x06)); return 0; } uint8_t ds1307_set_time(ds1307_time_t *t) { if (t-year 2000) t-year 2000; ds1307_write_byte(0x00, dec2bcd(t-sec) 0x7F); // CH位置0启动振荡器 ds1307_write_byte(0x01, dec2bcd(t-min) 0x7F); ds1307_write_byte(0x02, dec2bcd(t-hour) 0x3F); // 强制24小时制 ds1307_write_byte(0x03, dec2bcd(t-week) 0x07); ds1307_write_byte(0x04, dec2bcd(t-day) 0x3F); ds1307_write_byte(0x05, dec2bcd(t-month) 0x1F); ds1307_write_byte(0x06, dec2bcd(t-year - 2000) 0x7F); return 0; }逻辑说明写秒寄存器时用0x7F掩码把CH位固定为0这是驱动首次上电后的关键动作——不写这一位芯片时间可能停在00:00:00不动。小时写0x3F保证bit6为0即始终24小时制。4. 例程实战按键扫描状态机、时间设置流程和I2C卡死的定位4.1 把读时间从主循环里摘出去状态机在这里的价值DS1307读一次时间要连续读7个寄存器I2C是半双工总线通信期间如果有中断服务函数里也碰I2C数据必然错乱。例程主循环应当设计成“按键扫描→按需读时间→刷新显示”三步而不是每轮循环都去读一遍RTCtypedef enum { STATE_SHOW, STATE_SET_YEAR, STATE_SET_MONTH, STATE_SET_DAY, STATE_SET_HOUR, STATE_SET_MIN, STATE_SET_SEC, STATE_SAVE } clock_state_t; clock_state_t state STATE_SHOW; ds1307_time_t cur; while (1) { key key_scan(); // 非阻塞按键返回键值 switch (state) { case STATE_SHOW: if (key KEY_OK) state STATE_SET_YEAR; else if (key KEY_REFRESH || tick_500ms) ds1307_get_time(cur); // 500ms刷新一次降低总线占用 break; case STATE_SET_YEAR: if (key KEY_UP) cur.year; if (key KEY_DOWN) cur.year--; if (key KEY_OK) state STATE_SET_MONTH; break; // 其余set状态类似操作同一个cur结构体 case STATE_SAVE: ds1307_set_time(cur); state STATE_SHOW; break; } lcd_show_clock(cur, state); }逻辑说明状态机把“设置状态”和“显示状态”分离设置过程中不会触发读RTC避免写入过程中被读操作打断。显示状态中500ms读一次已经很充足因为DS1307的秒精度足够不需要每帧都读总线。刷新标志用定时器置位配合按键扫描在同一个主循环里完成。4.2 总线卡死怎么定位从逻辑分析仪波形到软件复位I2C问题最隐蔽的是“上电时好运行一段时间后偶尔卡死”。此时程序会停在I2C_CheckEvent的等待循环里。处理步骤按优先级排列先看SDA电平是否被拉低再查从机地址是否有应答最后查是否有遗漏的读序列。逻辑分析仪抓I2C波形时如果起始条件之后只有地址没有后续数据说明主机发完地址但没有收到从机ACK重点检查DS1307的电源和SDA上拉如果波形停在中间某位说明从机拉低了时钟线进行时钟拉伸此时需要等待而不是复位。void ds1307_bus_reset(void) { GPIO_InitTypeDef gpio; uint16_t timeout; // 先把PB6/PB7切回普通推挽输出手动翻转模拟起始/停止条件 gpio.GPIO_Mode GPIO_Mode_OUT; gpio.GPIO_OType GPIO_OType_PP; gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(GPIOB, gpio); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); for (timeout 0; timeout 5; timeout) { delay_us(5); } // 9个SCL脉冲给从机释放SDA的机会 for (timeout 0; timeout 9; timeout) { GPIO_ResetBits(GPIOB, GPIO_Pin_6); delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); delay_us(5); } // 恢复复用开漏 ds1307_i2c_init(); }参数说明GPIO速度降到2MHz是为了让手动翻转的边沿更缓避免在总线上产生振铃导致DS1307误识别。9个时钟脉冲是I2C规范里标准的恢复手段实际遇到0x68地址完全无响应时这个函数往往比单纯调大上拉电阻更管用。4.3 例程关键参数表上拉、速率、晶振与备用电池的合理取值参数推荐值不合适的表现I2C上拉电阻4.7kΩ400kHz时可用2.2kΩ1kΩ以下功耗偏高大于10kΩ时高速通信失败I2C时钟速率100kHz起步稳定后改400kHz速率过高且示波器看上升沿缓信号质量差晶振负载电容12.5pF与晶振匹配偏小会走快偏大走慢日差超过10秒备用电池电压2.0~3.5V低于2V时时间可能回退到00:00:00软件超时值1000次循环太短会误判正常慢速应答为超时主循环读间隔500ms过短增加总线占用过长闹钟类功能延迟变大5. 把DS1307调“准”的三个手段CH位、SQW秒脉冲与备份寄存器校验5.1 用SQW引脚输出秒脉冲校准晶振偏差DS1307的0x07控制寄存器里RS1/RS0两位可以选择SQW输出频率001Hz014096Hz108192Hz1132768Hz。把SQW设成1Hz输出用F429的输入捕获引脚测量两次上升沿之间的间隔就能算出当前晶振快慢ds1307_write_byte(0x07, 0x10); // SQWE1, RS10, RS00, 输出1Hz // 配置PA0为输入捕获上升沿测两次捕获值的差值 // 捕获值差 / 定时器频率 实际走秒时长 // 若测得1.0002s说明RTC每天快约17秒实测中如果每天偏差稳定可以用软件补偿每秒叠加一个校正值或者在设置时间时把秒数偏移半秒。这是不改晶振电容的前提下最实用的做法。5.2 读秒后再写时间先停振再写避免秒跳变直接向秒寄存器写入数值时如果芯片正好在边界处进位可能出现“写入59秒下一秒变00分00秒”这种错乱。常见做法是把控制寄存器0x07的bit7原本是OUT位当普通IO用不行所以要先用CH位停振再写全部时间寄存器最后置CH位为0恢复走时uint8_t raw; raw ds1307_read_byte(0x00); ds1307_write_byte(0x00, raw | 0x80); // CH1停振 // 依次写 分 时 日 月 年 ds1307_write_byte(0x01, dec2bcd(min)); ds1307_write_byte(0x02, dec2bcd(hour) 0x3F); ds1307_write_byte(0x04, dec2bcd(day)); ds1307_write_byte(0x05, dec2bcd(month)); ds1307_write_byte(0x06, dec2bcd(year)); ds1307_write_byte(0x00, dec2bcd(sec) 0x7F); // CH0恢复走时5.3 把写入结果读回来用F429备份寄存器做验证Ds1307没有写保护位但也意味着一处误写就会破坏时间数据。一个实用技巧是写完后立刻读回比较同时把比较结果存进F429的备份寄存器RTC_BKP。掉电再上电后在初始化阶段检查备份值如果不是预期魔数就认为外部时间芯片不可信重新要求用户设置时间uint8_t tmp ds1307_read_byte(0x02) 0x3F; if (tmp ! (dec2bcd(cur.hour) 0x3F)) { tmp ds1307_read_byte(0x00); ds1307_write_byte(0x00, tmp | 0x80); // 停振后重写 ds1307_set_time(cur); } if (PWR_GetFlagStatus(PWR_FLAG_BKP) RESET) // 备份域刚经历掉电 { PWR_BackupAccessCmd(ENABLE); RTC_WriteBackupRegister(RTC_BKP_DR0, 0xA5A5); // 写入校验魔数 }注意备份寄存器不属于DS1307例程的直接范围但这是工程上判断“RTC数据是否可信”的常见做法。靠CH位判断只能看出振荡器是否停过看不出时间内容是否被意外改写。备份寄存器校验能把“外部RTC没电了”和“主控重启了”区分开调试时少走很多弯路。本文还有配套的精品资源点击获取