SD3302 驱动:轻量级模块化,一键集成,高效易用。
前言
SD3302 实时时钟(RTC)芯片以其高精度、硬件写保护、闹钟输出和温度补偿功能,成为可靠的计时解决方案,它通过标准 I²C(7 位地址) 总线与 MCU 通信,内部提供 122 字节寄存器(包括时间、闹钟、控制、电池状态、70 字节用户 SRAM 及 8 字节唯一 ID),并内建 谐振电容 + 数字温度补偿,无需外置晶振调校即可实现宽温自动校时;内置 定时/报警中断(最长可设 100 年),配合后备电池智能充电与欠压检测;更支持 SD2068/SD3068 软件兼容、管脚兼容 1302,轻松替换旧款。
然而,网上关于 SD3302/ SD2068/SD3068 的驱动资料极为零散,几乎找不到一份完整、结构清晰的示例。本文将一次性呈现完整的 SD3302 驱动及底层 I²C 驱动代码,并配以详尽注释,帮助读者无需跨文件即可快速上手。
芯片简介
为什么选择 SD3302?
- 高精度温度补偿
内置数字温度补偿与谐振电容,无需外部校准,走时更加稳定可靠。 - 硬件写保护
防止意外写入覆盖时钟寄存器,确保数据安全。 - 硬件闹钟中断
通过 INT 引脚触发中断,无需轮询即可唤醒 MCU,极大节省功耗。 - 122 字节寄存器空间
集成时间、闹钟、控制、电池状态、70 B 用户 SRAM 及唯一 8 B ID,一线全掌控。 - 标准 I²C 接口
7 位地址,兼容 STM32、Arduino、ESP32 等主流 MCU,即插即用。 - 超长定时能力
硬件闹钟可配置至 100 年,满足各类超长定时或定期唤醒需求。 - 后备电池智能管理
内置充电与欠压检测电路,断电续航、实时监测电量状态。 - 向下兼容
软件兼容 SD2068/SD3068,管脚与 1302 完全一致,升级零成本。
硬件连接与软件依赖
管脚定义
硬件连接
目前我使用的MCU是普冉的PY32F030X8系列芯片。
SD3302 引脚 | 功能 | MCU 接脚 | 说明 |
---|---|---|---|
VCC | 电源 | 3.3V | 需加 0.1µF 陶瓷电容 |
GND | 地 | GND | — |
SDA | I²C 数据 | PA9(I2C1_SDA) | 配置上拉 |
SCL | I²C 时钟 | PA8 (I2C1_SCL) | 配置上拉 |
INT | 中断输出 | PC2(EXTI2) | 下降沿触发 |
- 别忘了给 SDA/SCL 上拉电阻,至少 4.7K,使 I²C 总线稳定。
寄存器映射示意图
下图为 SD3302 的寄存器地址分布图,便于查阅和开发过程中对功能字段的快速定位:
驱动框架设计
本驱动架构分为两大模块,便于分层开发与后期移植:
- SD3302 功能驱动:负责芯片寄存器读写与功能逻辑(包含
.h
声明与.c
实现)。 - I²C 底层驱动:封装 HAL 的收发操作,并配置 MSP 初始化(包含
.h
、.c
和外设初始化代码)。
两者通过统一接口函数 APP_MasterTransmit
和 APP_MasterReceive
进行通信,既提高了可读性,也便于在不同平台或 I²C 控制器间快速移植与复用。
👉 总之就是“上层专心控制功能,下层专心传话”,互不打扰,合作愉快。😄
SD3302 驱动源码及说明事项
SD3302程序源码
SD3302.h
/*** @file SD3302.h* @author cyWu (1917507415@qq.com)* @brief SD3302 实时时钟驱动框架头文件* @version 0.1* @date 2025-04-24**/
#ifndef __SD3302_H
#define __SD3302_H#include "main.h"/* I2C 设备地址 (7位) */
#define SD3302_I2C_ADDR 0x32/*------------------------ 寄存器地址定义 ------------------------*/
#define SEC_REG 0x00 // 秒寄存器
#define MIN_REG 0x01 // 分寄存器
#define HOUR_REG 0x02 // 时寄存器
#define WEEK_REG 0x03 // 星期寄存器
#define DAY_REG 0x04 // 日寄存器
#define MONTH_REG 0x05 // 月寄存器
#define YEAR_REG 0x06 // 年寄存器#define ALARM_SEC 0x07 // 报警秒寄存器
#define ALARM_MIN 0x08 // 报警分寄存器
#define ALARM_HOUR 0x09 // 报警时寄存器
#define ALARM_WEEK 0x0A // 报警星期寄存器
#define ALARM_DAY 0x0B // 报警日寄存器
#define ALARM_MONTH 0x0C // 报警月寄存器
#define ALARM_YEAR 0x0D // 报警年寄存器
#define ALARM_ENABLE 0x0E // 报警使能寄存器#define CTRL1_REG 0x0F // 控制寄存器 1
#define CTRL2_REG 0x10 // 控制寄存器 2
#define TEMP_REG 0x12 // 温度传感器寄存器
#define TD0_REG 0x13 // 定时器寄存器#define ID_BASE_REG 0x72 // 设备 ID 起始地址/*------------------------ 控制位定义 ------------------------*/
#define WRTC1 (1 << 7) // 写保护位 1
#define WRTC2 (1 << 2) // 写保护位 2
#define WRTC3 (1 << 7) // 写保护位 3
#define INTAE (1 << 1) // 报警中断允许位
#define INTFE (1 << 0) // 报警中断允许位
#define INTDE (1 << 2) // 报警中断允许位
#define INTAF (1 << 5) // 报警中断标志位
#define INTS1 (1 << 5) // INT中断输出设置1
#define INTS0 (1 << 4) // INT中断输出设置0
#define IM (1 << 6) // 报警中断模式位
#define TEMP_COMP_EN (1 << 7) // 温度补偿使能/*------------------------ 结构体定义 ------------------------*/
typedef struct
{uint8_t seconds;uint8_t minutes;uint8_t hours;uint8_t week;uint8_t day;uint8_t month;uint8_t year;
} RTC_TimeTypeDef;typedef struct
{uint8_t sec;uint8_t min;uint8_t hour;uint8_t week;uint8_t day;uint8_t month;uint8_t year;uint8_t enable; // 报警使能位
} RTC_AlarmTypeDef;/* 函数声明 */
void SD3302_Init(void);
void RTC_GetTime(RTC_TimeTypeDef *time);
void RTC_SetTime(RTC_TimeTypeDef *time);
void RTC_SetAlarm(RTC_AlarmTypeDef *alarm);
void RTC_GetAlarm(RTC_AlarmTypeDef *alarm);
void Read_ChipID(uint8_t *id_buffer);
void Test_ReadChipID(void);
void Test_RTC_SetTime(void);
void Test_RTC_Alarm(void);
uint32_t rtcDateTimeToSeconds(RTC_TimeTypeDef *rtc);
int rtcDiffS(RTC_TimeTypeDef *start, RTC_TimeTypeDef *now);
int rtcDiffS_uint(uint32_t *startSec, uint32_t *nowSec);
uint32_t get_current_rtc_seconds(void);
#endif /* __SD3302_H */
SD3302.c
/*** @file SD3302.c* @author cyWu (1917507415@qq.com)* @brief SD3302 实时时钟驱动框架源文件* @version 0.1* @date 2025-04-24**/#include "sd3302.h"
#include "I2c.h"extern void APP_MasterTransmit(uint16_t DevAddress, uint8_t *pData, uint16_t Size);
extern void APP_MasterReceive(uint16_t DevAddress, uint8_t *pData, uint16_t Size);/*------------------------ 私有函数 ------------------------*/
/* 解锁写保护 */
static void WriteUnlock(void)
{uint8_t reg = SEC_REG;uint8_t buffer[17];APP_MasterTransmit(SD3302_I2C_ADDR, ®, 1);APP_MasterReceive(SD3302_I2C_ADDR, buffer, 17);uint8_t data[2] = {CTRL2_REG, WRTC1 | buffer[16]};APP_MasterTransmit(SD3302_I2C_ADDR, data, 2);data[0] = CTRL1_REG;data[1] = WRTC2 | WRTC3 | buffer[15];APP_MasterTransmit(SD3302_I2C_ADDR, data, 2);
}/* 锁定写保护 */
static void WriteLock(void)
{uint8_t reg = SEC_REG;uint8_t buffer[17];APP_MasterTransmit(SD3302_I2C_ADDR, ®, 1);APP_MasterReceive(SD3302_I2C_ADDR, buffer, 17);uint8_t data[2] = {CTRL1_REG, (~(WRTC2 | WRTC3)) & buffer[15]};APP_MasterTransmit(SD3302_I2C_ADDR, data, 2);data[0] = CTRL2_REG;data[1] = (~WRTC1) | buffer[16];APP_MasterTransmit(SD3302_I2C_ADDR, data, 2);
}/* BCD 转换为十进制 */
static uint8_t BCD_To_Dec(uint8_t bcd)
{return ((bcd >> 4) * 10) + (bcd & 0x0F);
}/* 十进制转换为 BCD */
static uint8_t Dec_To_BCD(uint8_t dec)
{return ((dec / 10) << 4) | (dec % 10);
}/*------------------------ 设备初始化 ------------------------*/
void SD3302_Init(void)
{SD3302_INT_GPIO_CLK_ENABLE();GPIO_InitTypeDef GPIO_InitStruct = {0};// INT 下降沿中断GPIO_InitStruct.Pin = GPIO_PIN_2;GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;GPIO_InitStruct.Pull = GPIO_PULLUP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);HAL_NVIC_EnableIRQ(EXTI2_3_IRQn); /* Enable EXTI interrupt */HAL_NVIC_SetPriority(EXTI2_3_IRQn, 0, 0);
}/*------------------------ 时间操作 ------------------------*/
void RTC_SetTime(RTC_TimeTypeDef *time)
{uint8_t data[8];data[0] = SEC_REG;data[1] = Dec_To_BCD(time->seconds);data[2] = Dec_To_BCD(time->minutes);data[3] = Dec_To_BCD(time->hours) | 0x80; // 强制 24 小时模式data[4] = Dec_To_BCD(time->week);data[5] = Dec_To_BCD(time->day);data[6] = Dec_To_BCD(time->month);data[7] = Dec_To_BCD(time->year);WriteUnlock();APP_MasterTransmit(SD3302_I2C_ADDR, data, 8);WriteLock();
}void RTC_GetTime(RTC_TimeTypeDef *time)
{uint8_t reg = SEC_REG;uint8_t buffer[7];APP_MasterTransmit(SD3302_I2C_ADDR, ®, 1);APP_MasterReceive(SD3302_I2C_ADDR, buffer, 7);time->seconds = BCD_To_Dec(buffer[0]);time->minutes = BCD_To_Dec(buffer[1]);time->hours = BCD_To_Dec(buffer[2] & 0x3F);time->week = BCD_To_Dec(buffer[3] & 0x07);time->day = BCD_To_Dec(buffer[4]);time->month = BCD_To_Dec(buffer[5]);time->year = BCD_To_Dec(buffer[6]);
}/*------------------------ 报警配置 ------------------------*/
/*** @brief 设置 RTC 报警时间* @param alarm: 需要设置的报警时间结构体指针* @retval None** @note SD3302 的报警寄存器地址从 0x07 开始,依次存放 秒、分、时、星期、日、月、年、报警允许位。* 该函数先读取寄存器内容,然后修改相关报警寄存器值,再写回完整数据。*/
void RTC_SetAlarm(RTC_AlarmTypeDef *alarm)
{uint8_t reg = SEC_REG; // 读取的起始寄存器地址(从 0x00 读取完整的时间和报警数据)uint8_t buffer[18]; // 用于存储读取的寄存器数据,避免影响其他寄存器内容WriteUnlock(); // 解除写保护,允许修改 RTC 配置// 读取 RTC 当前时间和报警寄存器内容,避免修改不相关数据APP_MasterTransmit(SD3302_I2C_ADDR, ®, 1); // 发送要读取的寄存器地址APP_MasterReceive(SD3302_I2C_ADDR, &buffer[1], 17); // 读取 17 字节数据(完整 RTC 和报警数据)uint8_t data[11];data[0] = ALARM_SEC; // 报警秒寄存器data[1] = Dec_To_BCD(alarm->sec); // 设置秒报警值data[2] = Dec_To_BCD(alarm->min); // 设置分钟报警值data[3] = Dec_To_BCD(alarm->hour); // 设置小时报警值data[4] = Dec_To_BCD(alarm->week); // 设置星期报警值data[5] = Dec_To_BCD(alarm->day); // 设置日期报警值data[6] = Dec_To_BCD(alarm->month); // 设置月份报警值data[7] = Dec_To_BCD(alarm->year); // 设置年份报警值data[8] = alarm->enable; // 报警允许寄存器(控制哪些字段启用报警)data[10] = ((buffer[17] | INTAE | INTS0 | IM) & (~(INTFE | INTDE | INTS1))); // 设置控制寄存器(启用报警中断,确保不会影响其它控制位)// 将修改后的数据写回 RTCAPP_MasterTransmit(SD3302_I2C_ADDR, data, 11);WriteLock(); // 重新启用写保护,防止误操作
}/*** @brief 读取 RTC 报警时间* @param alarm: 存储读取到的报警时间信息* @retval None** @note 读取 SD3302 的报警寄存器数据(从 0x07 开始),并转换为十进制格式存入结构体*/
void RTC_GetAlarm(RTC_AlarmTypeDef *alarm)
{uint8_t reg = SEC_REG; // 读取的起始寄存器地址uint8_t buffer[17]; // 存储读取的数据// 读取报警时间寄存器(从 0x07 开始,共 8 字节报警相关数据)APP_MasterTransmit(SD3302_I2C_ADDR, ®, 1); // 发送要读取的寄存器地址APP_MasterReceive(SD3302_I2C_ADDR, buffer, 17); // 读取 17 字节数据// 解析报警时间数据alarm->sec = BCD_To_Dec(buffer[7]); // 读取秒报警alarm->min = BCD_To_Dec(buffer[8]); // 读取分钟报警alarm->hour = BCD_To_Dec(buffer[9]); // 读取小时报警alarm->week = BCD_To_Dec(buffer[10]); // 读取星期报警alarm->day = BCD_To_Dec(buffer[11]); // 读取日期报警alarm->month = BCD_To_Dec(buffer[12]); // 读取月份报警alarm->year = BCD_To_Dec(buffer[13]); // 读取年份报警alarm->enable = buffer[14]; // 读取报警允许寄存器(直接存储,不进行 BCD 转换)
}/*------------------------ 读取芯片 ID ------------------------*/
void Read_ChipID(uint8_t *id_buffer)
{uint8_t reg = ID_BASE_REG;APP_MasterTransmit(SD3302_I2C_ADDR, ®, 1);APP_MasterReceive(SD3302_I2C_ADDR, id_buffer, 8);
}/*------------------------ 测试函数 ------------------------*/
// 读取芯片 ID
void Test_ReadChipID(void)
{uint8_t chipID[8] = {0}; // 存储读取的 IDprintf("Reading Chip ID...\n");Read_ChipID(chipID); // 读取芯片 ID// 检查是否成功读取if (chipID[0] == 0x00 || chipID[0] == 0xFF){printf("Failed to read Chip ID! Check I2C connection.\n");}else{printf("Chip ID: ");for (int i = 0; i < 8; i++){printf("%02X ", chipID[i]); // 以十六进制打印}printf("\n");}
}// 设置时间
void Test_RTC_SetTime(void)
{RTC_TimeTypeDef setTime;RTC_TimeTypeDef getTime;// 设置要写入的时间setTime.year = 24; // 2024年setTime.month = 3; // 3月setTime.day = 10; // 10日setTime.week = 7; // 星期天setTime.hours = 16; // 14时setTime.minutes = 25; // 25分setTime.seconds = 50; // 36秒printf("Setting RTC Time...\n");// 写入 RTC 时间RTC_SetTime(&setTime);// 读取 RTC 时间进行验证RTC_GetTime(&getTime);// 检查设置的时间是否成功写入if (setTime.year == getTime.year &&setTime.month == getTime.month &&setTime.day == getTime.day &&setTime.week == getTime.week &&setTime.hours == getTime.hours &&setTime.minutes == getTime.minutes &&setTime.seconds == getTime.seconds){printf("RTC Time Set Successfully!\n");printf("RTC Time: %04d-%02d-%02d %02d:%02d:%02d (Weekday: %d)\n",getTime.year + 2000, getTime.month, getTime.day,getTime.hours, getTime.minutes, getTime.seconds,getTime.week);}else{printf("Failed to Set RTC Time! Check I2C connection.\n");printf("RTC Time: %04d-%02d-%02d %02d:%02d:%02d (Weekday: %d)\n",getTime.year + 2000, getTime.month, getTime.day,getTime.hours, getTime.minutes, getTime.seconds,getTime.week);}
}// 设置报警
void Test_RTC_Alarm(void)
{RTC_AlarmTypeDef alarm_set;RTC_AlarmTypeDef alarm_read;// 示例:设置报警时间为 08:30:00(只比较时和分,故其它字段置 0)alarm_set.sec = 0; // 秒alarm_set.min = 30; // 分alarm_set.hour = 8; // 时alarm_set.week = 0; // 星期不比较则置0alarm_set.day = 0; // 日不比较则置0alarm_set.month = 0; // 月不比较则置0alarm_set.year = 0; // 年不比较则置0// 报警使能:使能“分钟”(对应位 1)和“小时”(对应位 2),0x02 | 0x04 = 0x06alarm_set.alarm_enable = 0x06;// 设置报警RTC_SetAlarm(&alarm_set);// 读取报警配置验证是否正确RTC_GetAlarm(&alarm_read);if (alarm_set.sec == alarm_read.sec &&alarm_set.min == alarm_read.min &&alarm_set.hour == alarm_read.hour &&alarm_set.week == alarm_read.week &&alarm_set.day == alarm_read.day &&alarm_set.month == alarm_read.month &&alarm_set.year == alarm_read.year &&alarm_set.enable == alarm_read.enable){printf("Alarm Set Successfully!\n");}else{printf("Failed to Set Alarm! Check I2C connection.\n");}// printf("Alarm read back:\n");// printf(" Time: %02d:%02d:%02d\n", alarm_read.hour, alarm_read.min, alarm_read.sec);// printf(" Weekday: %d\n", alarm_read.week);// printf(" Day: %d, Month: %d, Year: %d\n", alarm_read.day, alarm_read.month, alarm_read.year);// printf(" Enable: 0x%02X\n", alarm_read.enable);
}
函数说明
WriteUnlock()
/WriteLock()
采用“读取 → 保留原值 → 修改目标位 → 写回”的标准流程,仅操作写保护相关位,确保其它控制位不被误改,保障写入操作安全可靠。BCD_To_Dec()
/Dec_To_BCD()
实现 BCD(Binary-Coded Decimal)与十进制之间的双向转换,用于 RTC 时间寄存器的数据格式转换,使人类可读的时间与芯片格式无缝对接。SD3302_Init()
初始化 SD3302 的 INT 中断引脚,将其配置为下降沿触发的外部中断,并开启对应 NVIC 通道,使芯片具备报警中断输出能力。RTC_SetTime()
/RTC_GetTime()
通过一次性写入或读取时间相关寄存器,实现对当前“时、分、秒、星期、日期、月份、年份”等字段的高效配置和获取,自动处理 24 小时格式位。RTC_SetAlarm()
/RTC_GetAlarm()
采用安全的“读-改-写”流程,对闹钟时间字段和报警使能位进行配置与读取,保证仅修改需要的字段,不影响其他寄存器内容,防止潜在误操作。Read_ChipID()
读取芯片内置的 8 字节唯一 ID,可用于通信认证、版本标识或产品追踪,是识别 SD3302 芯片身份的重要手段。Test_\*()
系列函数
封装基础测试流程,例如设置/读取时间、闹钟或读取 ChipID,结合printf()
输出,适用于快速验证驱动是否初始化成功、通信是否正常,方便开发调试阶段使用。
注意事项
细心的同学可能注意到,在获取闹钟寄存器的值时,程序中并不是直接从闹钟秒寄存器(ALARM_SEC
)发起读取,而是先从秒寄存器(SEC_REG
)一并读出连续寄存器。原因在于,SD3302 在接收到读取命令后不会自动释放总线(也就是MCU不发送 STOP 位),因为我使用的是硬件 I²C ,使用的是HAL库的 I²C 驱动,而现有的 I²C 驱动在每次接收完毕后都会强制发出 STOP 条件,。如果从 ALARM_SEC
单独发起读取,STOP 信号就会中断当前事务,导致只能得到秒寄存器的数据。
查看数据手册中可以发现,从制定的内部地址中读取数据是没有STOP位的
而以下是我的程序发送数据时抓取到的数据波形,硬件 I²C 在发送设置内部地址的时候总是会自带一个STOP位,导致读取到的数据时从寄存器00开始读的,也就是获取到的数据是秒寄存器的值
为了解决这一问题,我们采用另外一种获取方式,连续读取整个寄存器区块:从 SEC_REG
开始,一次性获取秒、分、时以及闹钟字段中的所有数据。
I²C 驱动代码
I²C.h
#ifndef __I2C_H
#define __I2C_H#include "main.h"#define I2C_ADDRESS 0xA0 //本机I2C地址
#define I2C_SPEEDCLOCK 100000 //传输速率
#define I2C_DUTYCYCLE I2C_DUTYCYCLE_16_9 //占空比void i2c_init(void);
void APP_MasterTransmit(uint16_t DevAddress, uint8_t *pData, uint16_t Size);
void APP_MasterReceive(uint16_t DevAddress, uint8_t *pData, uint16_t Size);
#endif
I²C.c
#include "I2c.h"I2C_HandleTypeDef I2cHandle;void i2c_init(void)
{/* I2C initialization */I2cHandle.Instance = I2C; /* I2C */I2cHandle.Init.ClockSpeed = I2C_SPEEDCLOCK; /* I2C communication speed */I2cHandle.Init.DutyCycle = I2C_DUTYCYCLE; /* I2C Duty cycle */I2cHandle.Init.OwnAddress1 = I2C_ADDRESS; /* I2C address */I2cHandle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; /* Disable general call */I2cHandle.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; /* Enable clock stretching */if (HAL_I2C_Init(&I2cHandle) != HAL_OK){APP_ErrorHandler();}HAL_Delay(10);
}/*** @brief I2C Send data* @param DevAddress:Slave address* @param pData:pData pointer to Send buffer* @param Size:Size Amount of data to be sent* @retval None*/
void APP_MasterTransmit(uint16_t DevAddress, uint8_t *pData, uint16_t Size)
{if(HAL_I2C_Master_Transmit(&I2cHandle, (DevAddress << 1), (uint8_t *)pData, Size, 1000) != HAL_OK){Log("I2C Send data error");}
}/*** @brief I2C receive data* @param DevAddress:Slave address* @param pData:pData pointer to receive buffer* @param Size:Size Amount of data to be received* @retval None*/
void APP_MasterReceive(uint16_t DevAddress, uint8_t *pData, uint16_t Size)
{if(HAL_I2C_Master_Receive(&I2cHandle, (DevAddress << 1) | 0x01, (uint8_t *)pData, Size, 1000) != HAL_OK){Log("I2C receive data error");}
}
函数说明
i2c_init()
完成 I²C 总线的初始化,包括:- 配置实例:
I2cHandle.Instance = I2C1
; - 设置通信参数:
ClockSpeed
(速率)、DutyCycle
(占空比)、OwnAddress1
(本机地址)、AddressingMode
(7 位)、DualAddressMode
、GeneralCallMode
、NoStretchMode
; - 调用
HAL_I2C_Init(&I2cHandle)
启动外设,若返回错误,则进入APP_ErrorHandler()
; - 初始化完成后加一个短延时(
HAL_Delay(10)
)以确保总线稳定。
- 配置实例:
APP_MasterTransmit(uint16_t DevAddress, uint8_t \*pData, uint16_t Size)
主机模式下向从机发送数据:- 将 7 位从机地址左移 1 位(
DevAddress << 1
)构造 8 位寻址格式; - 调用
HAL_I2C_Master_Transmit(&I2cHandle, addr, pData, Size, 1000)
发送Size
字节数据,最后一个参数为超时时间(ms); - 如果返回值不是
HAL_OK
,则调用Log("I2C Send data error")
打印错误信息,便于定位问题。
- 将 7 位从机地址左移 1 位(
APP_MasterReceive(uint16_t DevAddress, uint8_t \*pData, uint16_t Size)
主机模式下从从机接收数据:- 将 7 位从机地址左移 1 位并在最低位置加
| 0x01
标记读操作; - 调用
HAL_I2C_Master_Receive(&I2cHandle, addr, pData, Size, 1000)
接收Size
字节数据; - 若返回值非
HAL_OK
,则调用Log("I2C receive data error")
打印错误,帮助快速排查通信故障。
- 将 7 位从机地址左移 1 位并在最低位置加
IO初始化与闹钟外部触发(可选)
如果不需要使用 SD3302 的 INT 引脚触发闹钟中断,可在**SD3302_Init()**屏蔽掉闹钟配置 GPIO,以下是SDA和SCL的初始化。
void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c)
{GPIO_InitTypeDef GPIO_InitStruct = {0};__HAL_RCC_I2C1_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/* PA8=SCL, PA9=SDA */GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;GPIO_InitStruct.Pull = GPIO_PULLUP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;GPIO_InitStruct.Alternate = GPIO_AF12_I2C; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);/* 复位 I2C */__HAL_RCC_I2C1_FORCE_RESET();__HAL_RCC_I2C1_RELEASE_RESET();
}void EXTI2_3_IRQHandler(void)
{if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_2)) {__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_2);// 在此可调用用户回调或标记闹钟事件}
}
使用示例
main.c
#include "sd3302.h"
#include "i2c.h"int main(void)
{HAL_Init();SystemClock_Config(); // 时钟树配置i2c_init(); // 初始化 I²CSD3302_Init(); // 初始化 GPIO 中断(可选)// 1. 读取并打印芯片 IDTest_ReadChipID();// 2. 设置并验证当前时间Test_RTC_SetTime();// 3. 设置并验证闹钟Test_RTC_Alarm();while (1) {// 在主循环中可处理闹钟标志或其他逻辑}
}
常见问题
- I²C 地址需左移一位
HAL 接口中使用DevAddress << 1
构造 8 位寻址格式,否则无法正确找到从机。 - SDA/SCL 未上拉或时序抖动
如果缺少上拉电阻、线路过长或信号干扰,读写易失败,建议加装 4.7 K–10 K 上拉,并尽量缩短布线。 - 未调用
WriteUnlock()
即写寄存器
SD3302 对时间和报警寄存器默认上锁,必须先执行解锁流程,否则写入操作会被芯片忽略。 - 多字节读取被 STOP 中断
SD3302 不会自动发送 STOP,因此在读闹钟等多寄存器时务必使用 Repeated Start(“无 STOP”)连续读,否则只会得到首字节数据。 - BCD 转换错误导致时间异常
如果Dec_To_BCD()
或BCD_To_Dec()
实现有误,读写的时间字段会整体偏移,检查高低四位运算逻辑是否正确。 - 24 小时模式位(0x80)缺失
写入时未在小时寄存器设置 0x80 强制 24 小时模式,导致读取到的小时数据不准确。 - 中断优先级未配置或冲突
如果EXTI2_3_IRQn
优先级与其他中断冲突,闹钟 INT 输出可能被屏蔽,需在 NVIC 中合理分配优先级。 - INT 引脚悬空抖动
将 INT 引脚配置为上拉或保持定义的空闲电平,避免悬空导致误触发或抖动。