PocketOracle 每次醒来都想回答一个问题:「现在几点」。ESP32-C3 深度睡眠等于一次 重启,内部定时器全部失忆,芯片自己不知道墙上时间。所以板上加了一颗 EPSON RX8010SJ 实时钟(I2C 地址 0x32),主控睡觉时它靠自己的电源继续走时,醒来一问 便知。这篇文章记录这颗 RTC 的驱动落地过程。
先说一个 bring-up 花絮:原理图/BOM 上曾把它标成 RX8025T。两者都是爱普生的 I2C RTC,名字像双胞胎,寄存器布局却完全不同(RX8010 的时间寄存器从 0x10 开始,RX8025T 从 0x00 开始)。驱动落地前一定要按实际芯片的手册来——最终 确认板上丝印对应的是 RX8010SJ。
挂在别人的总线上
RX8010SJ 和加速度计 SC7A20 共享同一条 I2C 总线。ESP-IDF 新版 i2c_master 驱动的模型是「总线」和「设备」两级:总线创建一次,设备各自往上挂。项目里 让 SC7A20 的驱动负责创建总线,RTC 驱动通过一个访问器拿现成的总线句柄:
esp_err_t rx8010_init(void)
{
if (s_ok) return ESP_OK;
// 总线由 sc7a20_init() 创建,必须先跑
i2c_master_bus_handle_t bus = sc7a20_get_i2c_bus();
if (bus == NULL) return ESP_ERR_INVALID_STATE;
i2c_device_config_t cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x32,
.scl_speed_hz = 100000,
};
// 挂设备 → 读一次 CTRL 确认应答 → 清 FLAG
}「RTC 组件依赖加速度计组件」看着有点怪,但在这块板上总线的上拉配置、
深睡眠 hold 都是加速度计 bring-up 时一并处理的,让总线只有一个创建者,
比两个组件各自抢着建总线干净。代价是初始化顺序有了隐式约定:
sc7a20_init() 必须先于 rx8010_init()。
时间寄存器:BCD 码
时间落在 7 个连续寄存器里,数值全是 BCD 码(每半个字节一位十进制数):
| 寄存器 | 地址 | 内容 |
|---|---|---|
| SEC/MIN/HOUR | 0x10–0x12 | 秒/分/时,HOUR 用 24 小时制 |
| WDAY/MDAY/MON/YEAR | 0x13–0x16 | 星期/日/月/年(年 = 2000+值) |
转换只需要两个宏:
#define BCD_TO_DEC(x) (((x) >> 4) * 10 + ((x) & 0x0F))
#define DEC_TO_BCD(x) ((((x) / 10) << 4) | ((x) % 10))读取时还要按位掩码剥离标志位:秒寄存器的 bit7 是 VL 位(下文主角), 小时的 bit7 是 12/24 制标记。掩码忘了剥,时间就会偶尔「跳」出一个怪值。
读时间:burst 读消除撕裂
第一直觉是逐个寄存器读 7 次。但这有时间撕裂问题:读秒和读分之间如果 恰好发生进位(59 秒跳下一分),拼出来的就是一个从未存在过的时间。 解法是把 7 个字节压缩进一条 I2C 事务连续读出,进位无法再插进读取 过程的中间:
uint8_t reg = REG_SEC, buf[7];
i2c_master_transmit_receive(s_dev, ®, 1, buf, 7, 100); // 一次读 0x10-0x16
time->vl = (buf[0] >> 7) & 1;
time->second = BCD_TO_DEC(buf[0] & 0x7F);
// ...写时间同理用 burst 写,并且前后要包一层 RESET 保护(下一节)。
写时间:RESET 位 + burst 写
更新时间不能对着正在走数的寄存器直接写。RX8010 的流程是:
wr(REG_CTRL, 0x40); // 1. CTRL 的 RESET 位置 1,暂停走时
// 2. 一条事务 burst 写 7 个时间寄存器,VL 位顺手写 0(清除)
uint8_t buf[8] = { REG_SEC, bcd_sec, bcd_min, bcd_hour,
weekday & 0x07, bcd_day, bcd_mon, bcd_year };
i2c_master_transmit(s_dev, buf, 8, 100);
wr(REG_CTRL, 0x00); // 3. RESET 清零,时钟恢复走动set_time 入口还做了参数范围校验(秒 0–59、月 1–12……),非法值直接
拒绝——BCD 码写进一个 70 的「分钟」,芯片不会替你报错。
VL 位:时间可不可信,芯片自己知道
RX8010 的妙处是 VL(Voltage Low)标志:一旦芯片电源跌破维持走时的下限, 它把秒寄存器 bit7 置 1,并一直保留到软件主动写时间清除。也就是说, 时间是否经历过掉电,芯片比你清楚:
bool rx8010_is_valid(void)
{
rx8010_time_t t;
if (rx8010_get_time(&t) != ESP_OK) return false;
return !t.vl; // VL=0 才可信
}主流程每次唤醒都检查一遍:
if (!rx8010_is_valid()) {
ESP_LOGW(TAG, "RTC time invalid (VL set) — battery may have been disconnected");
}没有 VL 位之前,「设备换过电池、时间回到了默认值」这种事只能靠猜
(比如判断年份是不是 2000);有了它,一句话就能把不可信时间拦在门外。
开机自检(selftest)也把 rx8010_init → get_time → is_valid 串成一项
检查,连时间值一起打到串口日志里,pytest 直接拿这些行做断言。
初始化顺手做的两件事
- 读一次 CTRL 寄存器当「在不在」探测——总线通、芯片应答才算初始化成功;
- 清 FLAG 寄存器(AF/TF/UF,报警/定时器/更新标志),保证从干净状态开始。
小结
外部 RTC 驱动的代码量很小(200 行以内),真正花心思的是三件事:按实际 芯片手册核对寄存器表(别信 BOM 上的名字)、burst 读写保证一致性、用 VL 位回答「这时间敢不敢用」。对一个每天醒几次的深度睡眠设备来说,这颗 几毛钱的芯片就是它与世界时间唯一的连线。