RTC实时时钟应用完全指南¶
概述¶
RTC(Real-Time Clock,实时时钟)是嵌入式系统中用于保持准确时间的重要组件。即使在系统断电的情况下,RTC也能通过备用电池继续运行,确保时间信息不丢失。RTC广泛应用于数据记录、定时任务、时间戳生成等场景。
什么是RTC¶
RTC是一个独立的时钟电路,具有以下特点:
- 独立运行:不依赖主处理器,有独立的晶振和电源
- 低功耗:通常只需要几微安的电流
- 电池备份:使用纽扣电池(如CR2032)在断电时保持运行
- 高精度:温度补偿晶振可达±2ppm精度
- 多功能:支持闹钟、定时器、方波输出等功能
RTC的基本组成¶
RTC系统组成:
┌─────────────────────────────────────┐
│ RTC芯片 │
│ ┌──────────────────────────────┐ │
│ │ 时钟计数器 │ │
│ │ - 秒、分、时 │ │
│ │ - 日、月、年 │ │
│ │ - 星期 │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ 闹钟寄存器 │ │
│ │ - 闹钟1 │ │
│ │ - 闹钟2 │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ 控制寄存器 │ │
│ │ - 使能控制 │ │
│ │ - 中断控制 │ │
│ │ - 方波输出 │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ 32.768kHz晶振 │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
│ │
│ │
I2C/SPI接口 电池备份电源
│ │
↓ ↓
主控制器 CR2032电池
RTC vs 软件定时器¶
RTC与软件定时器对比:
┌──────────────┬─────────┬─────────────┐
│ 特性 │ RTC │ 软件定时器 │
├──────────────┼─────────┼─────────────┤
│ 精度 │ 高 │ 中等 │
│ 功耗 │ 极低 │ 依赖MCU │
│ 断电保持 │ 是 │ 否 │
│ 独立性 │ 独立 │ 依赖MCU │
│ 成本 │ 增加 │ 无额外成本 │
│ 复杂度 │ 中等 │ 简单 │
│ 长期准确性 │ 好 │ 一般 │
└──────────────┴─────────┴─────────────┘
使用场景:
RTC:
- 需要长期准确时间
- 系统会断电
- 需要定时唤醒
- 数据记录时间戳
软件定时器:
- 短期计时
- 系统不断电
- 对精度要求不高
- 相对时间测量
应用场景¶
RTC在嵌入式系统中的典型应用:
- 数据记录器:为传感器数据添加时间戳
- 定时控制:定时开关设备、定时采集数据
- 日程管理:闹钟、提醒、日程安排
- 电力系统:电能表、事件记录
- 工业控制:生产记录、设备维护提醒
- 物联网设备:时间同步、定时上报
- 医疗设备:用药提醒、数据记录
- 安防系统:事件时间戳、定时布防
本文内容概览¶
本教程将涵盖以下内容:
- RTC基础知识:RTC原理、晶振选择、精度分析
- 常用RTC芯片:DS1307、DS3231、PCF8563特性对比
- I2C RTC应用:DS1307/DS3231驱动开发
- SPI RTC应用:高速RTC接口实现
- 时间管理:时间读写、格式转换、时区处理
- 闹钟功能:闹钟配置、中断处理、定时唤醒
- 电池备份:电池选择、功耗优化、电量监测
- 温度补偿:TCXO原理、温度读取、精度提升
- 时间同步:NTP同步、GPS同步、手动校准
- 实战项目:数据记录器、定时控制器、电子日历
第一部分:RTC基础知识¶
1.1 RTC工作原理¶
时钟计数原理:
RTC时钟计数原理:
32.768kHz晶振
│
↓
┌─────────┐
│ 分频器 │ ÷ 32768
└─────────┘
│
↓
1 Hz
│
↓
┌─────────┐
│ 秒计数器│ 0-59
└─────────┘
│
↓
┌─────────┐
│ 分计数器│ 0-59
└─────────┘
│
↓
┌─────────┐
│ 时计数器│ 0-23 (24小时制)
└─────────┘ 0-12 (12小时制)
│
↓
┌─────────┐
│ 日计数器│ 1-31
└─────────┘
│
↓
┌─────────┐
│ 月计数器│ 1-12
└─────────┘
│
↓
┌─────────┐
│ 年计数器│ 00-99 (两位年)
└─────────┘ 2000-2099 (四位年)
为什么使用32.768kHz?
- 2^15 = 32768
- 分频15次得到1Hz
- 便于数字电路实现
- 功耗低
- 晶振尺寸小
BCD码存储:
/**
* @brief BCD码(Binary-Coded Decimal)
*
* BCD码用4位二进制表示一位十进制数
*
* 示例:
* 十进制 59 = BCD 0x59
* 十进制 23 = BCD 0x23
*
* 高4位:十位
* 低4位:个位
*/
/**
* @brief 十进制转BCD码
* @param dec: 十进制数(0-99)
* @return BCD码
*/
uint8_t dec_to_bcd(uint8_t dec)
{
return ((dec / 10) << 4) | (dec % 10);
}
/**
* @brief BCD码转十进制
* @param bcd: BCD码
* @return 十进制数
*/
uint8_t bcd_to_dec(uint8_t bcd)
{
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
1.2 晶振选择¶
32.768kHz晶振特性:
晶振参数:
1. 频率精度
- 标准晶振:±20ppm
- 高精度晶振:±10ppm
- TCXO(温补晶振):±2ppm
精度影响:
±20ppm = ±1.7秒/天 = ±52秒/月
±10ppm = ±0.86秒/天 = ±26秒/月
±2ppm = ±0.17秒/天 = ±5秒/月
2. 负载电容
- 典型值:6pF, 7pF, 9pF, 12.5pF
- 需要匹配外部电容
- 影响频率精度
3. ESR(等效串联电阻)
- 典型值:35kΩ - 70kΩ
- 影响起振时间和功耗
4. 温度特性
- 标准晶振:抛物线特性
- 25°C时精度最高
- 温度偏离时精度下降
5. 功耗
- 典型值:1-2μW
- 影响电池寿命
外部电容计算:
/**
* @brief 晶振外部电容计算
*
* CL = (C1 * C2) / (C1 + C2) + Cstray
*
* 其中:
* CL: 晶振负载电容(数据手册给出)
* C1, C2: 外部电容
* Cstray: 寄生电容(约5pF)
*
* 通常选择 C1 = C2,则:
* C1 = C2 = 2 * (CL - Cstray)
*
* 示例:
* CL = 12.5pF
* Cstray = 5pF
* C1 = C2 = 2 * (12.5 - 5) = 15pF
*/
// 常用配置
// CL = 6pF: C1 = C2 = 2pF (不常用)
// CL = 7pF: C1 = C2 = 4pF
// CL = 9pF: C1 = C2 = 8pF
// CL = 12.5pF: C1 = C2 = 15pF (最常用)
1.3 精度分析¶
影响RTC精度的因素:
RTC精度影响因素:
1. 晶振精度
- 初始精度:±20ppm
- 老化:±3ppm/年
- 温度漂移:±0.04ppm/°C²
2. 温度影响
- 标准晶振温度系数:-0.04ppm/°C²
- 25°C时精度最高
- 温度偏离25°C时精度下降
温度漂移计算:
ΔT = T - 25°C
Δf = -0.04 * ΔT² ppm
示例:
T = 0°C, ΔT = -25°C
Δf = -0.04 * 625 = -25ppm
误差 = 2.16秒/天
3. 电源电压
- 电压变化影响振荡频率
- 典型值:±0.5ppm/V
4. 负载电容
- 电容偏差影响频率
- 典型值:±1ppm/pF
5. PCB布局
- 寄生电容
- 寄生电感
- 干扰
精度提升方法:
- 使用TCXO(温补晶振)
- 软件温度补偿
- 定期校准
- 优化PCB布局
第二部分:常用RTC芯片¶
2.1 DS1307 RTC芯片¶
DS1307特性:
DS1307 实时时钟芯片:
基本特性:
- 接口:I2C(100kHz)
- 地址:0x68(固定)
- 电源:5V(4.5V-5.5V)
- 备用电源:3V(2.0V-3.5V)
- 功耗:1.5mA(5V)/ 500nA(备用)
- 精度:±2分钟/月(25°C)
- 晶振:32.768kHz
- 日历:2000-2099年
- RAM:56字节
功能:
- 秒、分、时、日、月、年、星期
- 12/24小时制
- 闰年自动调整
- 方波输出(1Hz, 4kHz, 8kHz, 32kHz)
- 56字节NVRAM
引脚定义:
1. X1 - 晶振输入
2. X2 - 晶振输出
3. VBAT - 备用电池
4. GND - 地
5. SDA - I2C数据线
6. SCL - I2C时钟线
7. SQW - 方波输出
8. VCC - 电源(5V)
优点:
- 价格低廉
- 使用简单
- 5V兼容
缺点:
- 精度较低
- 无温度补偿
- 功耗较高
DS1307寄存器映射:
/**
* @brief DS1307寄存器地址
*/
#define DS1307_ADDR 0x68 // I2C地址
#define DS1307_REG_SECOND 0x00 // 秒寄存器(00-59)
#define DS1307_REG_MINUTE 0x01 // 分寄存器(00-59)
#define DS1307_REG_HOUR 0x02 // 时寄存器(00-23/01-12)
#define DS1307_REG_DAY 0x03 // 星期寄存器(01-07)
#define DS1307_REG_DATE 0x04 // 日寄存器(01-31)
#define DS1307_REG_MONTH 0x05 // 月寄存器(01-12)
#define DS1307_REG_YEAR 0x06 // 年寄存器(00-99)
#define DS1307_REG_CONTROL 0x07 // 控制寄存器
#define DS1307_REG_RAM 0x08 // RAM起始地址(08-3F)
/**
* @brief DS1307寄存器位定义
*/
// 秒寄存器
#define DS1307_CH_BIT 0x80 // 时钟停止位(1=停止,0=运行)
// 时寄存器
#define DS1307_12_24_BIT 0x40 // 12/24小时制(1=12小时,0=24小时)
#define DS1307_AM_PM_BIT 0x20 // AM/PM位(1=PM,0=AM)
// 控制寄存器
#define DS1307_OUT_BIT 0x80 // 输出控制位
#define DS1307_SQWE_BIT 0x10 // 方波使能位
#define DS1307_RS1_BIT 0x02 // 速率选择位1
#define DS1307_RS0_BIT 0x01 // 速率选择位0
/**
* @brief 方波频率选择
*/
typedef enum {
DS1307_SQW_1HZ = 0, // 1Hz
DS1307_SQW_4KHZ, // 4.096kHz
DS1307_SQW_8KHZ, // 8.192kHz
DS1307_SQW_32KHZ // 32.768kHz
} ds1307_sqw_freq_t;
2.2 DS3231 RTC芯片¶
DS3231特性:
DS3231 高精度实时时钟芯片:
基本特性:
- 接口:I2C(400kHz)
- 地址:0x68(固定)
- 电源:2.3V-5.5V
- 备用电源:2.3V-5.5V
- 功耗:200μA(3.3V)/ 2μA(备用)
- 精度:±2ppm(0°C-40°C)= ±1分钟/年
- 晶振:内置TCXO(温补晶振)
- 日历:2000-2099年
- 温度传感器:±3°C精度
功能:
- 秒、分、时、日、月、年、星期
- 12/24小时制
- 闰年自动调整
- 两个可编程闹钟
- 可编程方波输出(1Hz-8kHz)
- 温度补偿
- 温度传感器
- 32kHz输出
- 老化偏移寄存器
引脚定义:
1. 32KHZ - 32kHz输出
2. VCC - 电源
3. INT/SQW - 中断/方波输出
4. RST - 复位(低电平有效)
5. GND - 地
6. VBAT - 备用电池
7. SDA - I2C数据线
8. SCL - I2C时钟线
优点:
- 高精度(±2ppm)
- 内置TCXO
- 温度补偿
- 低功耗
- 宽电压范围
- 两个闹钟
缺点:
- 价格较高
- 无NVRAM
DS3231寄存器映射:
/**
* @brief DS3231寄存器地址
*/
#define DS3231_ADDR 0x68
// 时间寄存器
#define DS3231_REG_SECOND 0x00
#define DS3231_REG_MINUTE 0x01
#define DS3231_REG_HOUR 0x02
#define DS3231_REG_DAY 0x03
#define DS3231_REG_DATE 0x04
#define DS3231_REG_MONTH 0x05
#define DS3231_REG_YEAR 0x06
// 闹钟1寄存器
#define DS3231_REG_ALARM1_SEC 0x07
#define DS3231_REG_ALARM1_MIN 0x08
#define DS3231_REG_ALARM1_HOUR 0x09
#define DS3231_REG_ALARM1_DATE 0x0A
// 闹钟2寄存器
#define DS3231_REG_ALARM2_MIN 0x0B
#define DS3231_REG_ALARM2_HOUR 0x0C
#define DS3231_REG_ALARM2_DATE 0x0D
// 控制和状态寄存器
#define DS3231_REG_CONTROL 0x0E
#define DS3231_REG_STATUS 0x0F
#define DS3231_REG_AGING 0x10
// 温度寄存器
#define DS3231_REG_TEMP_MSB 0x11
#define DS3231_REG_TEMP_LSB 0x12
/**
* @brief DS3231控制寄存器位定义
*/
#define DS3231_EOSC_BIT 0x80 // 振荡器使能(0=使能)
#define DS3231_BBSQW_BIT 0x40 // 电池供电时方波使能
#define DS3231_CONV_BIT 0x20 // 温度转换
#define DS3231_RS2_BIT 0x10 // 速率选择位2
#define DS3231_RS1_BIT 0x08 // 速率选择位1
#define DS3231_INTCN_BIT 0x04 // 中断控制
#define DS3231_A2IE_BIT 0x02 // 闹钟2中断使能
#define DS3231_A1IE_BIT 0x01 // 闹钟1中断使能
/**
* @brief DS3231状态寄存器位定义
*/
#define DS3231_OSF_BIT 0x80 // 振荡器停止标志
#define DS3231_EN32KHZ_BIT 0x08 // 32kHz输出使能
#define DS3231_BSY_BIT 0x04 // 忙标志
#define DS3231_A2F_BIT 0x02 // 闹钟2标志
#define DS3231_A1F_BIT 0x01 // 闹钟1标志
2.3 PCF8563 RTC芯片¶
PCF8563特性:
PCF8563 低功耗实时时钟芯片:
基本特性:
- 接口:I2C(400kHz)
- 地址:0x51(固定)
- 电源:1.0V-5.5V
- 备用电源:1.0V-5.5V
- 功耗:0.25μA(3V,典型值)
- 精度:±30ppm(25°C)
- 晶振:32.768kHz
- 日历:2000-2099年
功能:
- 秒、分、时、日、月、年、星期
- 闹钟功能
- 定时器功能
- 时钟输出(32.768kHz-1/60Hz)
- 中断输出
- 低功耗模式
引脚定义:
1. OSCI - 晶振输入
2. OSCO - 晶振输出
3. INT - 中断输出
4. VSS - 地
5. SDA - I2C数据线
6. SCL - I2C时钟线
7. CLKOUT - 时钟输出
8. VDD - 电源
优点:
- 超低功耗
- 宽电压范围
- 价格适中
- 定时器功能
缺点:
- 精度一般
- 无温度补偿
- 功能相对简单
芯片对比:
RTC芯片对比:
┌──────────┬─────────┬─────────┬─────────┐
│ 特性 │ DS1307 │ DS3231 │ PCF8563 │
├──────────┼─────────┼─────────┼─────────┤
│ 精度 │ ±2分/月 │ ±1分/年 │ ±5分/月 │
│ 电源电压 │ 5V │ 2.3-5.5V│ 1-5.5V │
│ 功耗(运行)│ 1.5mA │ 200μA │ 0.25μA │
│ 功耗(备用)│ 500nA │ 2μA │ 0.25μA │
│ I2C速度 │ 100kHz │ 400kHz │ 400kHz │
│ 温度补偿 │ 无 │ 有 │ 无 │
│ 闹钟数量 │ 0 │ 2 │ 1 │
│ NVRAM │ 56字节 │ 无 │ 无 │
│ 温度传感器│ 无 │ 有 │ 无 │
│ 价格 │ 低 │ 高 │ 中 │
└──────────┴─────────┴─────────┴─────────┘
选择建议:
- 低成本应用:DS1307
- 高精度应用:DS3231
- 低功耗应用:PCF8563
- 需要NVRAM:DS1307
- 需要温度:DS3231
第三部分:I2C RTC驱动开发¶
3.1 DS1307驱动实现¶
/**
* @file ds1307.c
* @brief DS1307 RTC驱动实现
* @author 嵌入式知识平台
*/
#include <stdint.h>
#include <stdbool.h>
#include "stm32f1xx_hal.h"
/* I2C句柄(需要在main.c中初始化) */
extern I2C_HandleTypeDef hi2c1;
/**
* @brief 时间结构
*/
typedef struct {
uint8_t second; // 秒(0-59)
uint8_t minute; // 分(0-59)
uint8_t hour; // 时(0-23)
uint8_t day; // 星期(1-7,1=星期一)
uint8_t date; // 日(1-31)
uint8_t month; // 月(1-12)
uint16_t year; // 年(2000-2099)
} rtc_time_t;
/**
* @brief I2C写寄存器
* @param reg: 寄存器地址
* @param data: 数据
* @return HAL状态
*/
static HAL_StatusTypeDef ds1307_write_reg(uint8_t reg, uint8_t data)
{
uint8_t buffer[2] = {reg, data};
return HAL_I2C_Master_Transmit(&hi2c1, DS1307_ADDR << 1, buffer, 2, 100);
}
/**
* @brief I2C读寄存器
* @param reg: 寄存器地址
* @param data: 数据指针
* @return HAL状态
*/
static HAL_StatusTypeDef ds1307_read_reg(uint8_t reg, uint8_t *data)
{
HAL_StatusTypeDef status;
// 发送寄存器地址
status = HAL_I2C_Master_Transmit(&hi2c1, DS1307_ADDR << 1, ®, 1, 100);
if (status != HAL_OK) {
return status;
}
// 读取数据
return HAL_I2C_Master_Receive(&hi2c1, DS1307_ADDR << 1, data, 1, 100);
}
/**
* @brief I2C读多个寄存器
* @param reg: 起始寄存器地址
* @param data: 数据缓冲区
* @param len: 数据长度
* @return HAL状态
*/
static HAL_StatusTypeDef ds1307_read_regs(uint8_t reg, uint8_t *data, uint8_t len)
{
HAL_StatusTypeDef status;
status = HAL_I2C_Master_Transmit(&hi2c1, DS1307_ADDR << 1, ®, 1, 100);
if (status != HAL_OK) {
return status;
}
return HAL_I2C_Master_Receive(&hi2c1, DS1307_ADDR << 1, data, len, 100);
}
/**
* @brief 初始化DS1307
* @return HAL状态
*/
HAL_StatusTypeDef ds1307_init(void)
{
uint8_t data;
HAL_StatusTypeDef status;
// 读取秒寄存器
status = ds1307_read_reg(DS1307_REG_SECOND, &data);
if (status != HAL_OK) {
return status;
}
// 检查时钟停止位
if (data & DS1307_CH_BIT) {
// 时钟已停止,启动时钟
data &= ~DS1307_CH_BIT;
status = ds1307_write_reg(DS1307_REG_SECOND, data);
if (status != HAL_OK) {
return status;
}
}
// 设置24小时制
status = ds1307_read_reg(DS1307_REG_HOUR, &data);
if (status != HAL_OK) {
return status;
}
if (data & DS1307_12_24_BIT) {
// 当前是12小时制,转换为24小时制
data &= ~DS1307_12_24_BIT;
status = ds1307_write_reg(DS1307_REG_HOUR, data);
}
return status;
}
/**
* @brief 设置时间
* @param time: 时间结构指针
* @return HAL状态
*/
HAL_StatusTypeDef ds1307_set_time(const rtc_time_t *time)
{
uint8_t buffer[8];
// 准备数据(转换为BCD码)
buffer[0] = DS1307_REG_SECOND;
buffer[1] = dec_to_bcd(time->second);
buffer[2] = dec_to_bcd(time->minute);
buffer[3] = dec_to_bcd(time->hour);
buffer[4] = dec_to_bcd(time->day);
buffer[5] = dec_to_bcd(time->date);
buffer[6] = dec_to_bcd(time->month);
buffer[7] = dec_to_bcd(time->year - 2000);
// 写入所有时间寄存器
return HAL_I2C_Master_Transmit(&hi2c1, DS1307_ADDR << 1, buffer, 8, 100);
}
/**
* @brief 读取时间
* @param time: 时间结构指针
* @return HAL状态
*/
HAL_StatusTypeDef ds1307_get_time(rtc_time_t *time)
{
uint8_t buffer[7];
HAL_StatusTypeDef status;
// 读取所有时间寄存器
status = ds1307_read_regs(DS1307_REG_SECOND, buffer, 7);
if (status != HAL_OK) {
return status;
}
// 转换BCD码为十进制
time->second = bcd_to_dec(buffer[0] & 0x7F); // 忽略CH位
time->minute = bcd_to_dec(buffer[1]);
time->hour = bcd_to_dec(buffer[2] & 0x3F); // 忽略12/24位
time->day = bcd_to_dec(buffer[3]);
time->date = bcd_to_dec(buffer[4]);
time->month = bcd_to_dec(buffer[5]);
time->year = bcd_to_dec(buffer[6]) + 2000;
return HAL_OK;
}
/**
* @brief 设置方波输出
* @param freq: 方波频率
* @param enable: 使能标志
* @return HAL状态
*/
HAL_StatusTypeDef ds1307_set_square_wave(ds1307_sqw_freq_t freq, bool enable)
{
uint8_t control = 0;
if (enable) {
control = DS1307_SQWE_BIT | (freq & 0x03);
}
return ds1307_write_reg(DS1307_REG_CONTROL, control);
}
/**
* @brief 写NVRAM
* @param addr: RAM地址(0-55)
* @param data: 数据指针
* @param len: 数据长度
* @return HAL状态
*/
HAL_StatusTypeDef ds1307_write_ram(uint8_t addr, const uint8_t *data, uint8_t len)
{
uint8_t buffer[57]; // 1字节地址 + 56字节数据
if (addr + len > 56) {
return HAL_ERROR; // 超出范围
}
buffer[0] = DS1307_REG_RAM + addr;
memcpy(&buffer[1], data, len);
return HAL_I2C_Master_Transmit(&hi2c1, DS1307_ADDR << 1, buffer, len + 1, 100);
}
/**
* @brief 读NVRAM
* @param addr: RAM地址(0-55)
* @param data: 数据缓冲区
* @param len: 数据长度
* @return HAL状态
*/
HAL_StatusTypeDef ds1307_read_ram(uint8_t addr, uint8_t *data, uint8_t len)
{
if (addr + len > 56) {
return HAL_ERROR;
}
uint8_t reg = DS1307_REG_RAM + addr;
return ds1307_read_regs(reg, data, len);
}
3.2 DS3231驱动实现¶
/**
* @file ds3231.c
* @brief DS3231 RTC驱动实现
*/
/**
* @brief 初始化DS3231
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_init(void)
{
uint8_t control, status;
HAL_StatusTypeDef hal_status;
// 读取控制寄存器
hal_status = ds1307_read_reg(DS3231_REG_CONTROL, &control);
if (hal_status != HAL_OK) {
return hal_status;
}
// 启用振荡器
control &= ~DS3231_EOSC_BIT;
// 设置为中断模式(不是方波模式)
control |= DS3231_INTCN_BIT;
// 写回控制寄存器
hal_status = ds1307_write_reg(DS3231_REG_CONTROL, control);
if (hal_status != HAL_OK) {
return hal_status;
}
// 读取状态寄存器
hal_status = ds1307_read_reg(DS3231_REG_STATUS, &status);
if (hal_status != HAL_OK) {
return hal_status;
}
// 清除振荡器停止标志
if (status & DS3231_OSF_BIT) {
status &= ~DS3231_OSF_BIT;
hal_status = ds1307_write_reg(DS3231_REG_STATUS, status);
}
return hal_status;
}
/**
* @brief 设置时间(DS3231使用相同的时间格式)
*/
HAL_StatusTypeDef ds3231_set_time(const rtc_time_t *time)
{
return ds1307_set_time(time); // 时间寄存器格式相同
}
/**
* @brief 读取时间
*/
HAL_StatusTypeDef ds3231_get_time(rtc_time_t *time)
{
return ds1307_get_time(time); // 时间寄存器格式相同
}
/**
* @brief 读取温度
* @param temperature: 温度指针(单位:°C)
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_get_temperature(float *temperature)
{
uint8_t buffer[2];
HAL_StatusTypeDef status;
// 读取温度寄存器
status = ds1307_read_regs(DS3231_REG_TEMP_MSB, buffer, 2);
if (status != HAL_OK) {
return status;
}
// 转换温度值
// MSB: 整数部分(有符号)
// LSB: 小数部分(高2位,0.25°C分辨率)
int8_t temp_msb = (int8_t)buffer[0];
uint8_t temp_lsb = buffer[1] >> 6;
*temperature = temp_msb + (temp_lsb * 0.25f);
return HAL_OK;
}
/**
* @brief 强制温度转换
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_force_temp_conversion(void)
{
uint8_t control;
HAL_StatusTypeDef status;
// 读取控制寄存器
status = ds1307_read_reg(DS3231_REG_CONTROL, &control);
if (status != HAL_OK) {
return status;
}
// 设置CONV位
control |= DS3231_CONV_BIT;
// 写回控制寄存器
return ds1307_write_reg(DS3231_REG_CONTROL, control);
}
/**
* @brief 闹钟匹配模式
*/
typedef enum {
DS3231_ALARM_EVERY_SECOND, // 每秒触发
DS3231_ALARM_MATCH_SECOND, // 匹配秒
DS3231_ALARM_MATCH_MIN_SEC, // 匹配分和秒
DS3231_ALARM_MATCH_HOUR_MIN_SEC,// 匹配时、分、秒
DS3231_ALARM_MATCH_DATE_HOUR_MIN_SEC, // 匹配日期、时、分、秒
DS3231_ALARM_MATCH_DAY_HOUR_MIN_SEC // 匹配星期、时、分、秒
} ds3231_alarm_mode_t;
/**
* @brief 闹钟结构
*/
typedef struct {
uint8_t second; // 秒(仅闹钟1)
uint8_t minute; // 分
uint8_t hour; // 时
uint8_t day_date; // 日期或星期
bool is_day; // true=星期,false=日期
ds3231_alarm_mode_t mode; // 匹配模式
} ds3231_alarm_t;
/**
* @brief 设置闹钟1
* @param alarm: 闹钟结构指针
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_set_alarm1(const ds3231_alarm_t *alarm)
{
uint8_t buffer[5];
uint8_t control;
HAL_StatusTypeDef status;
// 准备闹钟数据
buffer[0] = DS3231_REG_ALARM1_SEC;
buffer[1] = dec_to_bcd(alarm->second);
buffer[2] = dec_to_bcd(alarm->minute);
buffer[3] = dec_to_bcd(alarm->hour);
buffer[4] = dec_to_bcd(alarm->day_date);
// 设置匹配模式(A1M位)
switch (alarm->mode) {
case DS3231_ALARM_EVERY_SECOND:
buffer[1] |= 0x80;
buffer[2] |= 0x80;
buffer[3] |= 0x80;
buffer[4] |= 0x80;
break;
case DS3231_ALARM_MATCH_SECOND:
buffer[2] |= 0x80;
buffer[3] |= 0x80;
buffer[4] |= 0x80;
break;
case DS3231_ALARM_MATCH_MIN_SEC:
buffer[3] |= 0x80;
buffer[4] |= 0x80;
break;
case DS3231_ALARM_MATCH_HOUR_MIN_SEC:
buffer[4] |= 0x80;
break;
case DS3231_ALARM_MATCH_DATE_HOUR_MIN_SEC:
// 日期匹配
break;
case DS3231_ALARM_MATCH_DAY_HOUR_MIN_SEC:
// 星期匹配
buffer[4] |= 0x40;
break;
}
// 写入闹钟寄存器
status = HAL_I2C_Master_Transmit(&hi2c1, DS3231_ADDR << 1, buffer, 5, 100);
if (status != HAL_OK) {
return status;
}
// 使能闹钟1中断
status = ds1307_read_reg(DS3231_REG_CONTROL, &control);
if (status != HAL_OK) {
return status;
}
control |= DS3231_A1IE_BIT;
return ds1307_write_reg(DS3231_REG_CONTROL, control);
}
/**
* @brief 设置闹钟2(无秒匹配)
* @param alarm: 闹钟结构指针
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_set_alarm2(const ds3231_alarm_t *alarm)
{
uint8_t buffer[4];
uint8_t control;
HAL_StatusTypeDef status;
// 准备闹钟数据(闹钟2无秒寄存器)
buffer[0] = DS3231_REG_ALARM2_MIN;
buffer[1] = dec_to_bcd(alarm->minute);
buffer[2] = dec_to_bcd(alarm->hour);
buffer[3] = dec_to_bcd(alarm->day_date);
// 设置匹配模式
switch (alarm->mode) {
case DS3231_ALARM_EVERY_SECOND: // 闹钟2最快每分钟
buffer[1] |= 0x80;
buffer[2] |= 0x80;
buffer[3] |= 0x80;
break;
case DS3231_ALARM_MATCH_MIN_SEC:
buffer[2] |= 0x80;
buffer[3] |= 0x80;
break;
case DS3231_ALARM_MATCH_HOUR_MIN_SEC:
buffer[3] |= 0x80;
break;
case DS3231_ALARM_MATCH_DATE_HOUR_MIN_SEC:
break;
case DS3231_ALARM_MATCH_DAY_HOUR_MIN_SEC:
buffer[3] |= 0x40;
break;
default:
break;
}
// 写入闹钟寄存器
status = HAL_I2C_Master_Transmit(&hi2c1, DS3231_ADDR << 1, buffer, 4, 100);
if (status != HAL_OK) {
return status;
}
// 使能闹钟2中断
status = ds1307_read_reg(DS3231_REG_CONTROL, &control);
if (status != HAL_OK) {
return status;
}
control |= DS3231_A2IE_BIT;
return ds1307_write_reg(DS3231_REG_CONTROL, control);
}
/**
* @brief 清除闹钟标志
* @param alarm_num: 闹钟号(1或2)
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_clear_alarm(uint8_t alarm_num)
{
uint8_t status;
HAL_StatusTypeDef hal_status;
// 读取状态寄存器
hal_status = ds1307_read_reg(DS3231_REG_STATUS, &status);
if (hal_status != HAL_OK) {
return hal_status;
}
// 清除对应的闹钟标志
if (alarm_num == 1) {
status &= ~DS3231_A1F_BIT;
} else if (alarm_num == 2) {
status &= ~DS3231_A2F_BIT;
}
// 写回状态寄存器
return ds1307_write_reg(DS3231_REG_STATUS, status);
}
/**
* @brief 检查闹钟标志
* @param alarm_num: 闹钟号(1或2)
* @return true: 闹钟触发, false: 未触发
*/
bool ds3231_check_alarm(uint8_t alarm_num)
{
uint8_t status;
if (ds1307_read_reg(DS3231_REG_STATUS, &status) != HAL_OK) {
return false;
}
if (alarm_num == 1) {
return (status & DS3231_A1F_BIT) != 0;
} else if (alarm_num == 2) {
return (status & DS3231_A2F_BIT) != 0;
}
return false;
}
/**
* @brief 使能32kHz输出
* @param enable: 使能标志
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_enable_32khz(bool enable)
{
uint8_t status;
HAL_StatusTypeDef hal_status;
hal_status = ds1307_read_reg(DS3231_REG_STATUS, &status);
if (hal_status != HAL_OK) {
return hal_status;
}
if (enable) {
status |= DS3231_EN32KHZ_BIT;
} else {
status &= ~DS3231_EN32KHZ_BIT;
}
return ds1307_write_reg(DS3231_REG_STATUS, status);
}
第四部分:时间管理¶
4.1 时间格式转换¶
/**
* @file rtc_time.c
* @brief RTC时间管理函数
*/
#include <time.h>
#include <stdio.h>
/**
* @brief RTC时间转Unix时间戳
* @param rtc_time: RTC时间结构
* @return Unix时间戳(秒)
*/
time_t rtc_to_timestamp(const rtc_time_t *rtc_time)
{
struct tm timeinfo = {0};
timeinfo.tm_sec = rtc_time->second;
timeinfo.tm_min = rtc_time->minute;
timeinfo.tm_hour = rtc_time->hour;
timeinfo.tm_mday = rtc_time->date;
timeinfo.tm_mon = rtc_time->month - 1; // tm_mon: 0-11
timeinfo.tm_year = rtc_time->year - 1900; // tm_year: 从1900年起
timeinfo.tm_wday = rtc_time->day - 1; // tm_wday: 0-6
return mktime(&timeinfo);
}
/**
* @brief Unix时间戳转RTC时间
* @param timestamp: Unix时间戳
* @param rtc_time: RTC时间结构指针
*/
void timestamp_to_rtc(time_t timestamp, rtc_time_t *rtc_time)
{
struct tm *timeinfo = localtime(×tamp);
rtc_time->second = timeinfo->tm_sec;
rtc_time->minute = timeinfo->tm_min;
rtc_time->hour = timeinfo->tm_hour;
rtc_time->date = timeinfo->tm_mday;
rtc_time->month = timeinfo->tm_mon + 1;
rtc_time->year = timeinfo->tm_year + 1900;
rtc_time->day = timeinfo->tm_wday + 1;
}
/**
* @brief 格式化时间字符串
* @param rtc_time: RTC时间结构
* @param buffer: 输出缓冲区
* @param format: 格式字符串
*
* 格式说明:
* %Y - 年(4位)
* %m - 月(2位)
* %d - 日(2位)
* %H - 时(24小时制,2位)
* %M - 分(2位)
* %S - 秒(2位)
* %w - 星期(1-7)
*/
void rtc_format_time(const rtc_time_t *rtc_time, char *buffer, const char *format)
{
time_t timestamp = rtc_to_timestamp(rtc_time);
struct tm *timeinfo = localtime(×tamp);
strftime(buffer, 64, format, timeinfo);
}
/**
* @brief 解析时间字符串
* @param str: 时间字符串(格式:YYYY-MM-DD HH:MM:SS)
* @param rtc_time: RTC时间结构指针
* @return 0: 成功, -1: 失败
*/
int rtc_parse_time(const char *str, rtc_time_t *rtc_time)
{
int year, month, date, hour, minute, second;
if (sscanf(str, "%d-%d-%d %d:%d:%d",
&year, &month, &date, &hour, &minute, &second) != 6) {
return -1;
}
// 验证范围
if (year < 2000 || year > 2099 ||
month < 1 || month > 12 ||
date < 1 || date > 31 ||
hour < 0 || hour > 23 ||
minute < 0 || minute > 59 ||
second < 0 || second > 59) {
return -1;
}
rtc_time->year = year;
rtc_time->month = month;
rtc_time->date = date;
rtc_time->hour = hour;
rtc_time->minute = minute;
rtc_time->second = second;
// 计算星期
rtc_time->day = rtc_calculate_day_of_week(year, month, date);
return 0;
}
/**
* @brief 计算星期几(Zeller公式)
* @param year: 年
* @param month: 月
* @param date: 日
* @return 星期(1-7,1=星期一)
*/
uint8_t rtc_calculate_day_of_week(uint16_t year, uint8_t month, uint8_t date)
{
if (month < 3) {
month += 12;
year--;
}
int c = year / 100;
int y = year % 100;
int w = (c / 4) - 2 * c + y + (y / 4) + (13 * (month + 1) / 5) + date - 1;
w = ((w % 7) + 7) % 7; // 确保结果为正
// 转换为1-7(1=星期一)
return (w == 0) ? 7 : w;
}
/**
* @brief 判断闰年
* @param year: 年份
* @return true: 闰年, false: 平年
*/
bool rtc_is_leap_year(uint16_t year)
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
/**
* @brief 获取月份天数
* @param year: 年份
* @param month: 月份(1-12)
* @return 天数
*/
uint8_t rtc_get_days_in_month(uint16_t year, uint8_t month)
{
const uint8_t days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month < 1 || month > 12) {
return 0;
}
uint8_t day_count = days[month - 1];
// 闰年2月有29天
if (month == 2 && rtc_is_leap_year(year)) {
day_count = 29;
}
return day_count;
}
4.2 时区处理¶
/**
* @brief 时区偏移(小时)
*/
static int8_t timezone_offset = 8; // 默认东八区(北京时间)
/**
* @brief 设置时区
* @param offset: 时区偏移(-12到+14)
*/
void rtc_set_timezone(int8_t offset)
{
if (offset >= -12 && offset <= 14) {
timezone_offset = offset;
}
}
/**
* @brief 获取时区
* @return 时区偏移
*/
int8_t rtc_get_timezone(void)
{
return timezone_offset;
}
/**
* @brief UTC时间转本地时间
* @param utc_time: UTC时间
* @param local_time: 本地时间
*/
void rtc_utc_to_local(const rtc_time_t *utc_time, rtc_time_t *local_time)
{
time_t timestamp = rtc_to_timestamp(utc_time);
timestamp += timezone_offset * 3600; // 加上时区偏移
timestamp_to_rtc(timestamp, local_time);
}
/**
* @brief 本地时间转UTC时间
* @param local_time: 本地时间
* @param utc_time: UTC时间
*/
void rtc_local_to_utc(const rtc_time_t *local_time, rtc_time_t *utc_time)
{
time_t timestamp = rtc_to_timestamp(local_time);
timestamp -= timezone_offset * 3600; // 减去时区偏移
timestamp_to_rtc(timestamp, utc_time);
}
第五部分:电池备份与功耗优化¶
5.1 电池选择¶
RTC备用电池选择:
常用电池类型:
1. CR2032(最常用)
- 电压:3V
- 容量:220mAh
- 尺寸:20mm × 3.2mm
- 寿命:5-10年(典型)
- 价格:低
- 应用:大多数RTC
2. CR2025
- 电压:3V
- 容量:165mAh
- 尺寸:20mm × 2.5mm
- 寿命:3-7年
- 应用:空间受限场合
3. CR1220
- 电压:3V
- 容量:40mAh
- 尺寸:12.5mm × 2.0mm
- 寿命:1-3年
- 应用:小型设备
4. 超级电容
- 电压:5.5V
- 容量:0.1-1F
- 寿命:无限次充放电
- 保持时间:数小时到数天
- 应用:频繁更换电池不便的场合
电池寿命计算:
寿命(年)= 电池容量(mAh)/ (RTC功耗(μA)× 24 × 365) / 1000
示例:
CR2032: 220mAh
DS3231备用功耗:2μA
寿命 = 220 / (2 × 24 × 365) / 1000 = 12.5年
实际寿命会受以下因素影响:
- 温度(高温加速放电)
- 自放电率(约1-2%/年)
- 电池质量
- 存储时间
5.2 功耗优化¶
/**
* @brief RTC功耗优化配置
*/
/**
* @brief 配置DS3231低功耗模式
* @return HAL状态
*/
HAL_StatusTypeDef ds3231_configure_low_power(void)
{
uint8_t control, status;
HAL_StatusTypeDef hal_status;
// 读取控制寄存器
hal_status = ds1307_read_reg(DS3231_REG_CONTROL, &control);
if (hal_status != HAL_OK) {
return hal_status;
}
// 禁用32kHz输出(节省功耗)
hal_status = ds1307_read_reg(DS3231_REG_STATUS, &status);
if (hal_status != HAL_OK) {
return hal_status;
}
status &= ~DS3231_EN32KHZ_BIT;
hal_status = ds1307_write_reg(DS3231_REG_STATUS, status);
if (hal_status != HAL_OK) {
return hal_status;
}
// 禁用方波输出(使用中断模式)
control |= DS3231_INTCN_BIT;
// 禁用电池供电时的方波输出
control &= ~DS3231_BBSQW_BIT;
return ds1307_write_reg(DS3231_REG_CONTROL, control);
}
/**
* @brief 检测电池电压(需要外部电路)
* @param voltage: 电压指针(单位:V)
* @return 0: 成功, -1: 失败
*/
int rtc_check_battery_voltage(float *voltage)
{
// 这需要外部ADC电路来测量VBAT引脚电压
// 示例代码(需要根据实际硬件调整)
extern ADC_HandleTypeDef hadc1;
uint32_t adc_value;
// 启动ADC转换
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
adc_value = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
// 转换为电压(假设12位ADC,参考电压3.3V,分压比1:1)
*voltage = (adc_value * 3.3f) / 4095.0f;
return 0;
}
/**
* @brief 电池电量警告
* @return true: 电量低, false: 电量正常
*/
bool rtc_battery_low_warning(void)
{
float voltage;
if (rtc_check_battery_voltage(&voltage) == 0) {
// CR2032电池低于2.5V时需要更换
return (voltage < 2.5f);
}
return false;
}
5.3 电源切换¶
RTC电源切换机制:
主电源和备用电源切换:
┌─────────────┐
│ 主电源 │ VCC (3.3V/5V)
│ (VCC) │
└──────┬──────┘
│
├──────→ RTC芯片
│
┌──────┴──────┐
│ 电源切换 │
│ 电路 │
└──────┬──────┘
│
┌──────┴──────┐
│ 备用电源 │ VBAT (3V)
│ (VBAT) │
└─────────────┘
切换条件:
- VCC > VBAT + 0.2V:使用主电源
- VCC < VBAT - 0.2V:切换到备用电源
切换过程:
1. 检测VCC电压下降
2. 自动切换到VBAT
3. RTC继续运行
4. 功耗降低到备用模式
5. VCC恢复后自动切换回主电源
注意事项:
- 切换过程无缝,不影响时间
- 备用模式下功耗极低(μA级)
- 某些功能可能被禁用(如温度转换)
- 中断输出可能受影响
第六部分:时间同步¶
6.1 NTP时间同步¶
/**
* @file rtc_ntp.c
* @brief RTC NTP时间同步
*/
#include <string.h>
/**
* @brief NTP时间戳结构
*/
typedef struct {
uint32_t seconds; // 从1900年1月1日起的秒数
uint32_t fraction; // 秒的小数部分
} ntp_timestamp_t;
/**
* @brief NTP数据包结构
*/
typedef struct {
uint8_t li_vn_mode; // 闰秒指示、版本号、模式
uint8_t stratum; // 层级
uint8_t poll; // 轮询间隔
uint8_t precision; // 精度
uint32_t root_delay; // 根延迟
uint32_t root_dispersion; // 根离散
uint32_t ref_id; // 参考ID
ntp_timestamp_t ref_timestamp; // 参考时间戳
ntp_timestamp_t orig_timestamp; // 原始时间戳
ntp_timestamp_t recv_timestamp; // 接收时间戳
ntp_timestamp_t trans_timestamp; // 传输时间戳
} ntp_packet_t;
/**
* @brief NTP时间戳转Unix时间戳
* @param ntp_time: NTP时间戳
* @return Unix时间戳
*/
time_t ntp_to_unix_time(uint32_t ntp_time)
{
// NTP时间从1900年开始,Unix时间从1970年开始
// 1900-1970年之间有70年 = 2208988800秒
const uint32_t NTP_UNIX_OFFSET = 2208988800UL;
return (time_t)(ntp_time - NTP_UNIX_OFFSET);
}
/**
* @brief Unix时间戳转NTP时间戳
* @param unix_time: Unix时间戳
* @return NTP时间戳
*/
uint32_t unix_to_ntp_time(time_t unix_time)
{
const uint32_t NTP_UNIX_OFFSET = 2208988800UL;
return (uint32_t)unix_time + NTP_UNIX_OFFSET;
}
/**
* @brief 通过NTP同步RTC时间
* @param ntp_server: NTP服务器地址
* @return 0: 成功, -1: 失败
*/
int rtc_sync_with_ntp(const char *ntp_server)
{
// 这里需要网络功能支持(WiFi/Ethernet)
// 示例代码框架
ntp_packet_t ntp_packet = {0};
uint32_t ntp_time;
time_t unix_time;
rtc_time_t rtc_time;
// 1. 构建NTP请求包
ntp_packet.li_vn_mode = 0x1B; // LI=0, VN=3, Mode=3(客户端)
// 2. 发送NTP请求到服务器(需要UDP socket)
// send_udp_packet(ntp_server, 123, &ntp_packet, sizeof(ntp_packet));
// 3. 接收NTP响应
// receive_udp_packet(&ntp_packet, sizeof(ntp_packet));
// 4. 提取传输时间戳
ntp_time = ntohl(ntp_packet.trans_timestamp.seconds);
// 5. 转换为Unix时间戳
unix_time = ntp_to_unix_time(ntp_time);
// 6. 转换为RTC时间
timestamp_to_rtc(unix_time, &rtc_time);
// 7. 设置RTC时间
return (ds3231_set_time(&rtc_time) == HAL_OK) ? 0 : -1;
}
6.2 GPS时间同步¶
/**
* @brief 从GPS NMEA数据同步时间
* @param nmea_sentence: NMEA语句($GPRMC或$GPGGA)
* @return 0: 成功, -1: 失败
*/
int rtc_sync_with_gps(const char *nmea_sentence)
{
// 解析GPRMC语句
// $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
// 时间 状态 纬度 经度 速度 航向 日期
char time_str[11];
char date_str[7];
char status;
rtc_time_t rtc_time;
// 检查是否是GPRMC语句
if (strncmp(nmea_sentence, "$GPRMC", 6) != 0) {
return -1;
}
// 解析NMEA语句
if (sscanf(nmea_sentence, "$GPRMC,%10[^,],%c,%*[^,],%*[^,],%*[^,],%*[^,],%*[^,],%*[^,],%6[^,]",
time_str, &status, date_str) != 3) {
return -1;
}
// 检查GPS状态
if (status != 'A') {
return -1; // GPS数据无效
}
// 解析时间:HHMMSS.sss
int hour, minute, second;
if (sscanf(time_str, "%2d%2d%2d", &hour, &minute, &second) != 3) {
return -1;
}
// 解析日期:DDMMYY
int date, month, year;
if (sscanf(date_str, "%2d%2d%2d", &date, &month, &year) != 3) {
return -1;
}
// 构建RTC时间
rtc_time.hour = hour;
rtc_time.minute = minute;
rtc_time.second = second;
rtc_time.date = date;
rtc_time.month = month;
rtc_time.year = 2000 + year;
rtc_time.day = rtc_calculate_day_of_week(rtc_time.year, rtc_time.month, rtc_time.date);
// GPS时间是UTC时间,需要转换为本地时间
rtc_time_t local_time;
rtc_utc_to_local(&rtc_time, &local_time);
// 设置RTC时间
return (ds3231_set_time(&local_time) == HAL_OK) ? 0 : -1;
}
6.3 手动校准¶
/**
* @brief RTC时间校准
* @param offset_seconds: 偏移秒数(正数=快,负数=慢)
* @return 0: 成功, -1: 失败
*/
int rtc_calibrate(int32_t offset_seconds)
{
rtc_time_t current_time;
time_t timestamp;
// 读取当前时间
if (ds3231_get_time(¤t_time) != HAL_OK) {
return -1;
}
// 转换为时间戳
timestamp = rtc_to_timestamp(¤t_time);
// 应用偏移
timestamp += offset_seconds;
// 转换回RTC时间
timestamp_to_rtc(timestamp, ¤t_time);
// 设置新时间
return (ds3231_set_time(¤t_time) == HAL_OK) ? 0 : -1;
}
/**
* @brief DS3231老化偏移校准
* @param offset: 老化偏移值(-128到127)
* @return HAL状态
*
* 老化偏移寄存器用于补偿晶振老化导致的频率偏移
* 每个LSB约等于0.1ppm的频率调整
*/
HAL_StatusTypeDef ds3231_set_aging_offset(int8_t offset)
{
return ds1307_write_reg(DS3231_REG_AGING, (uint8_t)offset);
}
第七部分:实战项目¶
7.1 项目一:数据记录器¶
/**
* @file project_data_logger.c
* @brief 数据记录器项目
*/
/**
* @brief 数据记录结构
*/
typedef struct {
rtc_time_t timestamp; // 时间戳
float temperature; // 温度
float humidity; // 湿度
uint16_t light; // 光照强度
} data_record_t;
/**
* @brief 初始化数据记录器
* @return 0: 成功, -1: 失败
*/
int data_logger_init(void)
{
// 初始化RTC
if (ds3231_init() != HAL_OK) {
return -1;
}
// 配置低功耗模式
ds3231_configure_low_power();
// 设置闹钟2(每分钟触发一次)
ds3231_alarm_t alarm = {
.minute = 0,
.hour = 0,
.day_date = 0,
.is_day = false,
.mode = DS3231_ALARM_EVERY_SECOND // 闹钟2最快每分钟
};
ds3231_set_alarm2(&alarm);
return 0;
}
/**
* @brief 记录数据
* @param record: 数据记录指针
* @return 0: 成功, -1: 失败
*/
int data_logger_log(data_record_t *record)
{
// 读取当前时间
if (ds3231_get_time(&record->timestamp) != HAL_OK) {
return -1;
}
// 读取传感器数据(示例)
record->temperature = 25.5f; // 实际应从传感器读取
record->humidity = 60.0f;
record->light = 500;
// 将数据写入存储(SD卡、Flash等)
// save_to_storage(record);
// 格式化并打印
char time_str[64];
rtc_format_time(&record->timestamp, time_str, "%Y-%m-%d %H:%M:%S");
printf("[%s] Temp: %.1f°C, Humidity: %.1f%%, Light: %d\r\n",
time_str, record->temperature, record->humidity, record->light);
return 0;
}
/**
* @brief 数据记录器主循环
*/
void data_logger_loop(void)
{
// 检查闹钟2标志
if (ds3231_check_alarm(2)) {
// 清除闹钟标志
ds3231_clear_alarm(2);
// 记录数据
data_record_t record;
data_logger_log(&record);
}
}
7.2 项目二:定时控制器¶
/**
* @file project_timer_controller.c
* @brief 定时控制器项目
*/
/**
* @brief 定时任务结构
*/
typedef struct {
uint8_t hour; // 触发时间(时)
uint8_t minute; // 触发时间(分)
bool enabled; // 使能标志
void (*callback)(void); // 回调函数
} timer_task_t;
#define MAX_TIMER_TASKS 10
static timer_task_t timer_tasks[MAX_TIMER_TASKS];
/**
* @brief 添加定时任务
* @param hour: 小时
* @param minute: 分钟
* @param callback: 回调函数
* @return 任务ID,-1表示失败
*/
int timer_add_task(uint8_t hour, uint8_t minute, void (*callback)(void))
{
for (int i = 0; i < MAX_TIMER_TASKS; i++) {
if (!timer_tasks[i].enabled) {
timer_tasks[i].hour = hour;
timer_tasks[i].minute = minute;
timer_tasks[i].callback = callback;
timer_tasks[i].enabled = true;
return i;
}
}
return -1; // 任务列表已满
}
/**
* @brief 删除定时任务
* @param task_id: 任务ID
*/
void timer_remove_task(int task_id)
{
if (task_id >= 0 && task_id < MAX_TIMER_TASKS) {
timer_tasks[task_id].enabled = false;
}
}
/**
* @brief 检查并执行定时任务
*/
void timer_check_tasks(void)
{
rtc_time_t current_time;
// 读取当前时间
if (ds3231_get_time(¤t_time) != HAL_OK) {
return;
}
// 检查所有任务
for (int i = 0; i < MAX_TIMER_TASKS; i++) {
if (timer_tasks[i].enabled) {
if (current_time.hour == timer_tasks[i].hour &&
current_time.minute == timer_tasks[i].minute &&
current_time.second == 0) { // 在整分钟时触发
// 执行回调
if (timer_tasks[i].callback != NULL) {
timer_tasks[i].callback();
}
}
}
}
}
/**
* @brief 示例回调函数
*/
void turn_on_light(void)
{
printf("Turning on light at scheduled time\r\n");
// HAL_GPIO_WritePin(LIGHT_GPIO_Port, LIGHT_Pin, GPIO_PIN_SET);
}
void turn_off_light(void)
{
printf("Turning off light at scheduled time\r\n");
// HAL_GPIO_WritePin(LIGHT_GPIO_Port, LIGHT_Pin, GPIO_PIN_RESET);
}
/**
* @brief 定时控制器初始化
*/
void timer_controller_init(void)
{
// 初始化RTC
ds3231_init();
// 添加定时任务
timer_add_task(7, 0, turn_on_light); // 早上7:00开灯
timer_add_task(22, 0, turn_off_light); // 晚上22:00关灯
}
7.3 项目三:电子日历¶
/**
* @file project_calendar.c
* @brief 电子日历项目
*/
/**
* @brief 显示当前时间和日期
*/
void calendar_display(void)
{
rtc_time_t current_time;
char time_str[64];
char date_str[64];
const char *weekdays[] = {"", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
// 读取时间
if (ds3231_get_time(¤t_time) != HAL_OK) {
return;
}
// 格式化时间
snprintf(time_str, sizeof(time_str), "%02d:%02d:%02d",
current_time.hour, current_time.minute, current_time.second);
// 格式化日期
snprintf(date_str, sizeof(date_str), "%04d-%02d-%02d %s",
current_time.year, current_time.month, current_time.date,
weekdays[current_time.day]);
// 显示(LCD/OLED/串口)
printf("\r\n=== Electronic Calendar ===\r\n");
printf("Time: %s\r\n", time_str);
printf("Date: %s\r\n", date_str);
// 读取温度(DS3231)
float temperature;
if (ds3231_get_temperature(&temperature) == HAL_OK) {
printf("Temp: %.2f°C\r\n", temperature);
}
printf("==========================\r\n");
}
/**
* @brief 显示月历
* @param year: 年份
* @param month: 月份
*/
void calendar_display_month(uint16_t year, uint8_t month)
{
const char *month_names[] = {
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
uint8_t days_in_month = rtc_get_days_in_month(year, month);
uint8_t first_day = rtc_calculate_day_of_week(year, month, 1);
printf("\r\n %s %d\r\n", month_names[month], year);
printf("Mon Tue Wed Thu Fri Sat Sun\r\n");
printf("----------------------------\r\n");
// 打印前导空格
for (int i = 1; i < first_day; i++) {
printf(" ");
}
// 打印日期
for (int day = 1; day <= days_in_month; day++) {
printf("%3d ", day);
uint8_t weekday = rtc_calculate_day_of_week(year, month, day);
if (weekday == 7) { // 星期日,换行
printf("\r\n");
}
}
printf("\r\n");
}
第八部分:故障排除¶
8.1 常见问题¶
RTC常见问题及解决方案:
1. RTC时间不准确
原因:
- 晶振精度低
- 温度影响
- 负载电容不匹配
- 晶振老化
解决:
- 使用高精度晶振或TCXO
- 使用DS3231(内置TCXO)
- 检查外部电容值
- 定期校准
- 使用NTP/GPS同步
2. RTC时间丢失
原因:
- 备用电池耗尽
- 电池接触不良
- 电池未安装
- 振荡器停止
解决:
- 更换电池
- 检查电池座
- 检查VBAT连接
- 检查振荡器使能位
3. I2C通信失败
原因:
- 上拉电阻缺失或不合适
- 地址错误
- 时序不正确
- 总线冲突
解决:
- 添加4.7kΩ上拉电阻
- 确认I2C地址(DS1307/DS3231: 0x68)
- 降低I2C时钟频率
- 检查总线上其他设备
4. 闹钟不触发
原因:
- 闹钟未使能
- 中断引脚未连接
- 匹配模式设置错误
- 闹钟标志未清除
解决:
- 检查闹钟使能位
- 连接INT引脚到MCU
- 检查闹钟匹配模式
- 及时清除闹钟标志
5. 时间跳变
原因:
- 读取时序问题
- 秒进位时读取
- 多线程竞争
解决:
- 连续读取两次,比较是否一致
- 避免在秒进位时读取
- 使用互斥锁保护
6. 功耗过高
原因:
- 方波输出未禁用
- 32kHz输出未禁用
- 温度转换频繁
解决:
- 禁用不需要的输出
- 降低温度转换频率
- 使用低功耗模式
7. 温度读数异常(DS3231)
原因:
- 温度转换未完成
- 读取时序错误
解决:
- 等待温度转换完成
- 检查BSY标志
- 强制温度转换
8. 年份错误
原因:
- 年份寄存器只有两位
- 世纪位处理错误
解决:
- 软件处理世纪(2000-2099)
- 检查年份范围
8.2 调试技巧¶
/**
* @brief RTC诊断函数
* @return 0: 正常, -1: 异常
*/
int rtc_diagnose(void)
{
printf("\r\n=== RTC Diagnostics ===\r\n");
// 1. 测试I2C通信
printf("Testing I2C communication...\r\n");
uint8_t test_data;
if (ds1307_read_reg(DS3231_REG_SECOND, &test_data) != HAL_OK) {
printf("ERROR: I2C communication failed\r\n");
return -1;
}
printf("OK: I2C communication successful\r\n");
// 2. 检查振荡器状态
printf("Checking oscillator status...\r\n");
uint8_t status;
ds1307_read_reg(DS3231_REG_STATUS, &status);
if (status & DS3231_OSF_BIT) {
printf("WARNING: Oscillator stop flag set\r\n");
} else {
printf("OK: Oscillator running\r\n");
}
// 3. 读取并显示时间
printf("Reading current time...\r\n");
rtc_time_t current_time;
if (ds3231_get_time(¤t_time) == HAL_OK) {
printf("Time: %04d-%02d-%02d %02d:%02d:%02d\r\n",
current_time.year, current_time.month, current_time.date,
current_time.hour, current_time.minute, current_time.second);
} else {
printf("ERROR: Failed to read time\r\n");
return -1;
}
// 4. 读取温度(DS3231)
printf("Reading temperature...\r\n");
float temperature;
if (ds3231_get_temperature(&temperature) == HAL_OK) {
printf("Temperature: %.2f°C\r\n", temperature);
} else {
printf("WARNING: Failed to read temperature\r\n");
}
// 5. 检查电池电压
printf("Checking battery voltage...\r\n");
float voltage;
if (rtc_check_battery_voltage(&voltage) == 0) {
printf("Battery voltage: %.2fV\r\n", voltage);
if (voltage < 2.5f) {
printf("WARNING: Battery voltage low, replace soon\r\n");
}
}
printf("======================\r\n");
return 0;
}
/**
* @brief RTC寄存器转储
*/
void rtc_dump_registers(void)
{
uint8_t regs[19];
printf("\r\n=== RTC Register Dump ===\r\n");
// 读取所有寄存器
if (ds1307_read_regs(0x00, regs, 19) == HAL_OK) {
printf("Addr Value Description\r\n");
printf("---- ----- -----------\r\n");
printf("0x00 0x%02X Seconds\r\n", regs[0]);
printf("0x01 0x%02X Minutes\r\n", regs[1]);
printf("0x02 0x%02X Hours\r\n", regs[2]);
printf("0x03 0x%02X Day\r\n", regs[3]);
printf("0x04 0x%02X Date\r\n", regs[4]);
printf("0x05 0x%02X Month\r\n", regs[5]);
printf("0x06 0x%02X Year\r\n", regs[6]);
printf("0x07 0x%02X Alarm1 Seconds\r\n", regs[7]);
printf("0x08 0x%02X Alarm1 Minutes\r\n", regs[8]);
printf("0x09 0x%02X Alarm1 Hours\r\n", regs[9]);
printf("0x0A 0x%02X Alarm1 Day/Date\r\n", regs[10]);
printf("0x0B 0x%02X Alarm2 Minutes\r\n", regs[11]);
printf("0x0C 0x%02X Alarm2 Hours\r\n", regs[12]);
printf("0x0D 0x%02X Alarm2 Day/Date\r\n", regs[13]);
printf("0x0E 0x%02X Control\r\n", regs[14]);
printf("0x0F 0x%02X Status\r\n", regs[15]);
printf("0x10 0x%02X Aging Offset\r\n", regs[16]);
printf("0x11 0x%02X Temp MSB\r\n", regs[17]);
printf("0x12 0x%02X Temp LSB\r\n", regs[18]);
}
printf("========================\r\n");
}
第九部分:最佳实践¶
9.1 设计建议¶
RTC应用设计最佳实践:
1. 芯片选择
- 低成本应用:DS1307
- 高精度应用:DS3231
- 低功耗应用:PCF8563
- 需要温度:DS3231
- 需要NVRAM:DS1307
2. 硬件设计
- 使用推荐的负载电容
- 晶振和电容靠近RTC芯片
- 保持晶振走线短而对称
- 避免在晶振下方走线
- 添加I2C上拉电阻(4.7kΩ)
- 电池座要可靠
- 考虑电池电压监测电路
3. 软件设计
- 初始化时检查振荡器状态
- 定期检查电池电压
- 实现时间同步机制
- 使用互斥锁保护RTC访问
- 处理闰年和月份天数
- 实现时区支持
- 记录时间设置历史
4. 功耗优化
- 禁用不需要的输出
- 使用中断而非轮询
- 降低I2C时钟频率
- 使用低功耗模式
- 选择低功耗RTC芯片
5. 可靠性
- 实现看门狗检测
- 定期校准时间
- 备份重要时间信息
- 处理电源切换
- 实现错误恢复机制
6. 用户体验
- 提供时间设置界面
- 显示电池状态
- 支持多种时间格式
- 实现夏令时调整
- 提供时区选择
9.2 代码规范¶
/**
* @brief RTC错误处理宏
*/
#define RTC_CHECK(func) \
do { \
HAL_StatusTypeDef status = (func); \
if (status != HAL_OK) { \
printf("RTC Error: %s failed with status %d\r\n", #func, status); \
return -1; \
} \
} while(0)
/**
* @brief 安全的时间读取
* @param time: 时间结构指针
* @return 0: 成功, -1: 失败
*/
int rtc_read_time_safe(rtc_time_t *time)
{
rtc_time_t time1, time2;
int retry = 3;
while (retry--) {
// 读取两次
RTC_CHECK(ds3231_get_time(&time1));
HAL_Delay(1);
RTC_CHECK(ds3231_get_time(&time2));
// 比较是否一致
if (memcmp(&time1, &time2, sizeof(rtc_time_t)) == 0) {
memcpy(time, &time1, sizeof(rtc_time_t));
return 0;
}
}
return -1; // 读取失败
}
/**
* @brief 时间有效性检查
* @param time: 时间结构指针
* @return true: 有效, false: 无效
*/
bool rtc_time_is_valid(const rtc_time_t *time)
{
if (time->year < 2000 || time->year > 2099) return false;
if (time->month < 1 || time->month > 12) return false;
if (time->date < 1 || time->date > 31) return false;
if (time->hour > 23) return false;
if (time->minute > 59) return false;
if (time->second > 59) return false;
if (time->day < 1 || time->day > 7) return false;
// 检查日期是否在月份范围内
uint8_t days = rtc_get_days_in_month(time->year, time->month);
if (time->date > days) return false;
return true;
}
总结¶
关键要点¶
- RTC基础
- RTC是独立的时钟电路,使用32.768kHz晶振
- 支持电池备份,断电后继续运行
- 时间以BCD码存储
-
精度受晶振、温度、电压等因素影响
-
芯片选择
- DS1307:低成本,5V,精度±2分/月,有NVRAM
- DS3231:高精度,±1分/年,内置TCXO,有温度传感器
-
PCF8563:超低功耗,宽电压,精度±5分/月
-
I2C通信
- 正确配置I2C参数
- 添加上拉电阻
- 实现读写函数
-
处理通信错误
-
时间管理
- BCD码与十进制转换
- Unix时间戳转换
- 时间格式化
- 时区处理
-
闰年和星期计算
-
闹钟功能
- DS3231支持两个独立闹钟
- 多种匹配模式
- 中断输出
-
定时唤醒
-
电池备份
- CR2032最常用
- 电池寿命计算
- 功耗优化
-
电压监测
-
时间同步
- NTP网络同步
- GPS时间同步
- 手动校准
- 老化偏移补偿
学习路径¶
- 入门阶段
- 理解RTC基本原理
- 学习I2C通信
- 掌握DS1307/DS3231使用
-
实现基本时间读写
-
进阶阶段
- 实现闹钟功能
- 时间格式转换
- 时区处理
-
电池管理
-
高级阶段
- 时间同步
- 温度补偿
- 功耗优化
- 实际项目应用
下一步学习¶
参考资料¶
- Maxim Integrated DS1307 Datasheet
- Maxim Integrated DS3231 Datasheet
- NXP PCF8563 Datasheet
- I2C-bus specification and user manual (NXP)
- Application Note: RTC Design Considerations
- IEEE 1588 Precision Time Protocol
- RFC 5905: Network Time Protocol Version 4
相关主题¶
本文档持续更新中,最后更新时间:2026-04-06