简介:一份专为嵌入式开发者准备的STM32与PCF8563实时时钟芯片通信软件包,基于标准库函数实现,主要解决在STM32平台上通过I²C总线驱动PCF8563,同时利用串口进行调试和数据交互的工程难题。压缩包共190个文件,大小约4.66MB,主要包括32个C源码文件、33个H头文件,以及o/crf/lst等大量编译中间文件;此外还有uvprojx/uvoptx工程配置、hex/axf烧录与调试文件,拿到后可直接用Keil打开编译并烧录验证。源码覆盖I²C初始化、PCF8563日期时间读取与设置、闹钟控制等常用驱动,并额外包含UART串口通信逻辑,可输出调试日志或作为备用通信通道。对于需要快速集成PCF8563的STM32应用,这套代码能显著降低底层开发门槛,缩短项目周期,尤其适合单片机入门学习、课程设计或产品原型验证。整体工程结构清晰,文件归类完整,已有691人学习下载,具备较大的参考意义。
1. 库函数串口通讯与 pcf8563 软件栈:先把三件事拆开再合上
把“库函数串口通讯 pcf8563 软件”这个标题拆开看,实际是把三件事焊在一起:STM32 用标准外设库操作 PCf8563 这颗 I2C 接口的实时时钟芯片,再通过串口和上位机对话。单独看每一件都不难,难的是它们交界处的字节序、应答位、断帧处理和时序容错——比如读秒寄存器时没屏蔽 VL 位,返回的 BCD 直接跳出 0~59;又比如上位机发来一条被 0x0A 截断的 SET TIME 命令,解析器如果不够严格,会把 59 秒写成 0x59 的十六进制。这三块拼起来后的完整链路,才是这个标题真正想讲的。适合正在用 stm32 标准库新建工程、要给设备加对时功能或接工控屏的开发者。
2. pcf8563 关键寄存器与 I2C 读写时序
2.1 时间寄存器从 0x02 开始,数据全是 BCD 码
PCF8563 的器件地址是 7 位 0x51,对应 I2C 写地址 0xA2、读地址 0xA3。它的时间寄存器不像 DS1307 那样从 0x00 排开,而是从 0x02 开始连续排布,0x00 和 0x01 被控制/状态寄存器占掉。写驱动前先理解这张表,否则地址偏移错了会读到一堆看似正常的乱码。
| 寄存器偏移 | 含义 | 位说明 | 取值范围(BCD) |
|---|---|---|---|
| 0x00 | 控制/状态1 | bit5 STOP,1 暂停振荡器 | 0x00 |
| 0x01 | 控制/状态2 | bit2 TI_TP,定时器中断标志 | 0x00 |
| 0x02 | 秒 | bit7 VL 电压低标志,bit6:0 秒 | 00–59 |
| 0x03 | 分钟 | bit6:0 分钟 | 00–59 |
| 0x04 | 小时 | bit5:0 小时 | 00–23 |
| 0x05 | 日 | bit5:0 日 | 01–31 |
| 0x06 | 星期 | bit2:0 星期 | 00–06 |
| 0x07 | 月/世纪 | bit7 世纪位 C,bit4:0 月 | 01–12 |
| 0x08 | 年 | bit7:0 年 | 00–99 |
这里的重点不是地址多大多小,而是数据格式。PCF8563 全部时间数据都用 BCD 码存储,0x59 代表 59 秒,0x33 代表 33,绝不是十进制数值。很多第一次写驱动的人把读回来的字节直接打印,看到 0x5A 当成 90 秒,其实那是 5 和 10 的组合,属于非法值。工程里我一般维护两个几行的小函数bcd2dec和dec2bcd,后续所有寄存器读写都走这两层转换。
读寄存器时还要记得按位掩码。秒寄存器的 bit7 是 VL,欠压标志;小时寄存器只有低 6 位有效;月寄存器的 bit7 还兼任世纪位。这些状态位不能混进 BCD 数值里,否则一边读一边产生非法时间。比较稳妥的做法是读回原始字节后先与掩码做与操作,再交给bcd2dec。
2.2 I2C 起始、停止和应答的库函数写法
STM32 标准外设库(SPL,标题里的“库函数”一般指的就是它)里没有直接的 I2C 位操作 API,需要用 GPIO_SetBits 和 GPIO_ResetBits 模拟时序。模拟 I2C 的代码结构在不同平台上是完全一样的,换到 APM32、GD32 或者 C51 平台,只要把引脚操作宏换掉就能跑通,这也是“库函数”写法在工程移植时的最大优势。
先看一套最基本的宏定义和时序函数。
#define I2C_SCL_GPIO GPIOC #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_GPIO GPIOC #define I2C_SDA_PIN GPIO_Pin_7 #define SCL_H() GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN) #define SCL_L() GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN) #define SDA_H() GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN) #define SDA_L() GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(I2C_SDA_GPIO, I2C_SDA_PIN) static void i2c_delay(void) { volatile uint32_t i = 20; while (i--); } void i2c_start(void) { SDA_H(); SCL_H(); i2c_delay(); SDA_L(); i2c_delay(); SCL_L(); } void i2c_stop(void) { SCL_L(); SDA_L(); i2c_delay(); SCL_H(); i2c_delay(); SDA_H(); i2c_delay(); } uint8_t i2c_write_byte(uint8_t data) { uint8_t i, nack; for (i = 0; i < 8; i++) { if (data & 0x80) SDA_H(); else SDA_L(); data <<= 1; i2c_delay(); SCL_H(); i2c_delay(); SCL_L(); } SDA_H(); i2c_delay(); SCL_H(); i2c_delay(); nack = SDA_READ(); SCL_L(); return nack; // 返回 0 表示从机应答,返回 1 表示无应答 }这段代码的核心逻辑是把 SCL 当作移位时钟,SDA 上的电平按 MSB first 顺序移出。每一位传输时,必须先保证 SCL 为低电平再改变 SDA,否则会在总线上产生额外的起始/停止条件。发送完 8 位后第 9 个时钟用于读应答,主机释放 SDA,从机拉低表示 ACK。库函数手册里对 GPIO 结构体的描述已经很明确,这里只需要记住引脚模式配成开漏,靠外部上拉电阻提供高电平。
i2c_delay()在 72MHz 主频下会把 I2C 时钟压到 100kHz 左右,对 PCf8563 完全够用。实际项目里如果发现总线波形畸变,优先加长这个延时,不要急着换硬件。
2.3 地址自增与重复起始:一次读回完整时间
PCF8563 内部支持地址自增,连续读时间寄存器可以从 0x02 开始一口气读完 7 个字节,这比读一个字节停一次要高效得多。读取时序的固定套路是“先写地址,再重新发起总线访问读数据”,中间用 repeated start 分隔。
过程拆开是这样的:
- 起始条件
- 发送 0xA2,写入方向
- 发送寄存器地址 0x02
- 再次发送起始条件(repeated start)
- 发送 0xA3,切换为读取方向
- 读取 7 个字节,前 6 个字节回 ACK,最后一个字节回 NACK
- 停止条件
最后一位为什么不回 ACK?因为从机在收到 NACK 后才判断“主机不想再要数据了”,然后释放总线。如果最后一个字节也回 ACK,PCF8563 会继续把下一个寄存器地址的数据推上 SDA,造成一次多余的传输。这个细节在示波器上非常容易看到,表现为停止条件前多出一个字节。
读时间用下面这组代码实现。
typedef struct { uint8_t sec; uint8_t min; uint8_t hour; uint8_t day; uint8_t week; uint8_t month; uint8_t year; } rtc_time_t; 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); } void pcf8563_read_time(rtc_time_t *tm) { uint8_t buf[7]; i2c_start(); i2c_write_byte(0xA2); i2c_write_byte(0x02); i2c_start(); // repeated start i2c_write_byte(0xA3); buf[0] = i2c_read_byte(1); // 秒 buf[1] = i2c_read_byte(1); // 分 buf[2] = i2c_read_byte(1); // 时 buf[3] = i2c_read_byte(1); // 日 buf[4] = i2c_read_byte(1); // 星期 buf[5] = i2c_read_byte(1); // 月 buf[6] = i2c_read_byte(0); // 年,NACK i2c_stop(); tm->sec = bcd2dec(buf[0] & 0x7F); tm->min = bcd2dec(buf[1] & 0x7F); tm->hour = bcd2dec(buf[2] & 0x3F); tm->day = bcd2dec(buf[3] & 0x3F); tm->week = bcd2dec(buf[4] & 0x07); tm->month = bcd2dec(buf[5] & 0x1F); tm->year = bcd2dec(buf[6]); }掩码值对应前面表格里的有效位:秒和分保留低 7 位,小时保留低 6 位,月份保留低 5 位。世纪位 C 在 buf[5] 的 bit7,如果产品生命周期不会跨世纪,可以先忽略;要做 2100 年问题就得单独保存这个标志位。
3. STM32 库函数工程里的 pcf8563 驱动封装
3.1 引脚开漏配置与 SWD 冲突
选引脚有个容易被忽略的坑:很多人喜欢把 I2C 放在 PB3、PB4 上,但这两个引脚在系统复位后默认是 JTAG 的 JTDO 和 NJTRST。如果只调用 GPIO_Init 把它们配置成 I2C 功能而没关闭 JTAG 复用,调试器会直接失联,甚至出现类似 “error: no stm32 target found” 的报错,程序烧进去后一运行就断连。
我一般把 PC6、PC7 留给软件 I2C,避开调试口的同时也不会和 USART1 的 PA9/PA10 打架。初始化代码在标准库下是固定套路。
GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); /* 开漏输出没有驱动高电平能力,必须外接上拉电阻 */ GPIO_SetBits(GPIOC, GPIO_Pin_6 | GPIO_Pin_7);为什么必须是开漏而不是推挽?I2C 总线协议要求“线与”,任何设备都可以把 SDA 拉低,多个设备同时输出不同电平时不产生短路。开漏模式下高电平完全依赖外部 4.7kΩ 上拉电阻,总线速度在 100kHz 时噪声容限也更好。如果硬要用推挽模式,从机拉低 SDA 时主机同时在输出高电平,就会形成短暂短路,轻则通信错乱,重则损伤引脚。
初始化完成后两个引脚默认输出高电平,总线处于空闲态。此时可以直接调用 2.2 节里的时序函数,不需要再额外配置复用功能。
3.2 pcf8563_read_time 与连续读实现
上一章已经给出了读时间函数,这里把读字节函数补完整,因为它是反复调用的基础设施。注意读字节时第 8 个时钟结束前要先把 SDA 释放为输入状态,否则主机输出引脚会一直拉高或拉低,读不到从机发来的数据。
uint8_t i2c_read_byte(uint8_t ack) { uint8_t i, data = 0; SDA_H(); // 释放 SDA,准备接收 for (i = 0; i < 8; i++) { data <<= 1; SCL_H(); i2c_delay(); if (SDA_READ()) data |= 0x01; SCL_L(); i2c_delay(); } if (ack) SDA_L(); // 下一字节前回 ACK else SDA_H(); // 最后一字节回 NACK SCL_H(); i2c_delay(); SCL_L(); SDA_H(); return data; }ack参数决定了当前读操作结束后主机怎么应答。连续读 7 个字节时,前 6 次传 1,最后一次传 0。这里还有一个隐含要求:读每个字节期间不能让中断去占用 I2C 引脚,否则 SDA 上的电平变化会被中断打断。多字节读取时如果系统里同时有串口中断、定时器中断,建议在读时间期间用__disable_irq()保护一次,否则偶尔会出现分钟对、秒错位的怪数据。
调用封装后的代码非常短:
rtc_time_t now; pcf8563_read_time(&now); printf("time: %04d-%02d-%02d %02d:%02d:%02d\r\n", 2000 + now.year, now.month, now.day, now.hour, now.min, now.sec);年份这里直接加 2000,是因为 PCF8563 的年寄存器只有 00~99。这个简化在 2099 年之前都成立,工程里写成宏或配置项更合理。
3.3 写时间时对 STOP 位和 VL 位的处理
写时间比读时间多一层考虑:如果在写秒的过程中秒的个位发生进位,时间会出现短暂的不一致。常见做法是先把控制寄存器 0x00 的 STOP 位置 1,让振荡器暂停计数,写入全部 7 个字节后再清 0 恢复走时。对正在对时的设备来说,这 2 毫秒的暂停完全不敏感。
void pcf8563_write_reg(uint8_t reg, uint8_t data) { i2c_start(); i2c_write_byte(0xA2); i2c_write_byte(reg); i2c_write_byte(data); i2c_stop(); } uint8_t pcf8563_read_reg(uint8_t reg) { uint8_t data; i2c_start(); i2c_write_byte(0xA2); i2c_write_byte(reg); i2c_start(); i2c_write_byte(0xA3); data = i2c_read_byte(0); i2c_stop(); return data; } void pcf8563_set_time(rtc_time_t *tm) { uint8_t buf[7]; buf[0] = dec2bcd(tm->sec) & 0x7F; buf[1] = dec2bcd(tm->min) & 0x7F; buf[2] = dec2bcd(tm->hour) & 0x3F; buf[3] = dec2bcd(tm->day) & 0x3F; buf[4] = dec2bcd(tm->week) & 0x07; buf[5] = dec2bcd(tm->month) & 0x1F; buf[6] = dec2bcd(tm->year); pcf8563_write_reg(0x00, 0x20); // STOP=1,暂停走时 i2c_start(); i2c_write_byte(0xA2); i2c_write_byte(0x02); for (uint8_t i = 0; i < 7; i++) { i2c_write_byte(buf[i]); } i2c_stop(); pcf8563_write_reg(0x00, 0x00); // 恢复走时 }写秒寄存器时掩掉 bit7,是为了避免把 VL 位写 1 导致后续读取误判欠压。VL 位本质上是只读状态,写操作里最安全的做法就是保持该位为 0。上电首次读取时如果 VL 为 1,说明芯片经历过低电压,时间可能不可信,程序应在串口日志里提示“RTC 时间已失效,请重新设置”。
3.4 硬件 I2C 和软件 I2C 的取舍
STM32 标准外设库里同样提供了硬件 I2C 相关函数,但 EV5、EV6、EV7 这些事件标志位处理起来比较繁琐,很多新手卡在“总线忙”状态出不来。硬件 I2C 的优势是 CPU 开销低、时序由外设保证,适合高频率读取场景;但它在 STM32F1 上因为时序兼容问题受到过不少争议。软件 I2C 则胜在可移植、时序可控,调试时把i2c_delay调大一点就能观察波形。
对 PCf8563 这种秒级变化的 RTC 芯片,软件 I2C 的 100kHz 速率和几十微秒的读取耗时都不是瓶颈。换到 APM32 或 GD32 的工程里,标准库的 GPIO 和串口寄存器兼容度都很高,软件 I2C 这一层代码几乎不用改。只有产品进入低功耗模式,主频降下来或者要求 CPU 常睡时,才值得花时间迁到硬件 I2C 加 DMA 的写法。
4. 串口通讯协议与命令解析:从字节流到可信应答
4.1 串口初始化与波特率选择
串口部分的目标不是打印调试日志,而是接收上位机的结构化命令,这要求初始化参数固定、中断明确、缓冲清晰。波特率用 115200 比较通用,8 数据位、1 停止位、无校验是当前调试工具和串口屏的默认偏好。RS485 场景下通常也用同样的帧格式,只是多一个方向控制引脚。
USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USART1, ENABLE);如果板载 USB 转串口在设备管理器里出现叹号,先确定是驱动问题还是引脚配置问题。程序跑不通和串口助手连不上经常是两回事,排查时先把 MCU 的 TX/RX 互相短接,发什么收什么,能回环再查外设。
4.2 中断只收数据,解析放主循环
很多初学者喜欢在串口中断里直接调sscanf解析指令,这会让中断处理时间过长,连续字节到达时丢数据。更稳妥的分工是:中断只把字节搬进缓冲,置一个“一帧结束”的标记,主循环看到标记再做解析。这样即便解析逻辑复杂,也不会影响接收时序。
#define RX_BUF_SIZE 64 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint8_t rx_len = 0; volatile uint8_t rx_frame_ok = 0; void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { ch = USART_ReceiveData(USART1); if (rx_frame_ok) { return; // 上一帧还没被消费,丢弃新数据 } if (ch == '\n') { rx_buf[rx_len] = '\0'; rx_frame_ok = 1; } else if (ch != '\r' && rx_len < RX_BUF_SIZE - 1) { rx_buf[rx_len++] = ch; } } }用\n作为帧结束符,同时兼容 Windows 串口助手发来的\r\n。如果半导体厂商的烧写工具或工控屏只发\r不发\n,把触发条件改成“收到\r或\n都收尾”更稳。这里的核心是满缓冲时不做覆盖,保留旧数据供主循环排查,而不是让后续错误数据把有效命令冲掉。
主循环里轮询标志位,处理完后必须同时清零rx_len和rx_frame_ok。漏清零会导致下一帧被永久丢弃,而且现象很隐蔽:设备还能跑,但所有串口指令都失灵。
4.3 GET TIME 与 SET TIME 的解析实现
命令协议不建议做得很复杂,两三条能覆盖需求的命令远比一套花哨的 AT 指令集好维护。下面这个最小协议已经能支撑“查询时间、设置时间”两个核心操作。
| 命令 | 示例 | 返回 |
|---|---|---|
| GET TIME | GET TIME\n | OK TIME 2026-02-14 10:30:00 |
| SET TIME | SET TIME 2026-02-14 10:30:00\n | OK SET |
| 非法命令 | 任意 | ERR |
解析代码用sscanf提取六个整数,年份只取到 9999 以内,再单独做范围检查。** 参数不合法时返回 ERR,设备时间保持原样,不能让一条坏命令把 RTC 打乱。**
void parse_cmd(char *cmd) { int y, mo, d, h, mi, s; if (strncmp(cmd, "GET TIME", 8) == 0) { rtc_time_t now; pcf8563_read_time(&now); printf("OK TIME %04d-%02d-%02d %02d:%02d:%02d\r\n", 2000 + now.year, now.month, now.day, now.hour, now.min, now.sec); return; } if (strncmp(cmd, "SET TIME", 8) == 0) { if (sscanf(cmd + 8, "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &s) == 6) { if (y >= 2000 && y <= 2099 && mo >= 1 && mo <= 12 && d >= 1 && d <= 31 && h >= 0 && h <= 23 && mi >= 0 && mi <= 59 && s >= 0 && s <= 59) { rtc_time_t set = { (uint8_t)s, (uint8_t)mi, (uint8_t)h, (uint8_t)d, 0, (uint8_t)mo, (uint8_t)(y - 2000) }; pcf8563_set_time(&set); printf("OK SET\r\n"); return; } } } printf("ERR\r\n"); }这里有个容易被忽略的问题:sscanf的格式串里空格和-必须与上位机发送的格式完全一致。上位机如果用制表符或全角空格分隔,匹配就失败。我会在协议文档里明确写“日期和时间之间用一个空格分隔”,同时在解析前先做一次strchr(cmd, ' ')检查,找不到空格就直接返回 ERR。
星期字段没有放进命令里,因为 PCf8563 的星期寄存器和日期之间没有严格绑定关系,最稳妥的做法是上位机只传年月日时分秒,MCU 端通过查表法或者蔡勒公式补算星期。
4.4 加一层 RS485 方向控制即可上总线
这套串口协议跑在 RS485 上时,硬件链路变成半双工,MCU 发送期间必须拉高 DE 引脚,发送完成后延时再拉低。软件上只需要把printf的底层改一下,协议本身不用动。
#define RS485_DE_H() GPIO_SetBits(GPIOA, GPIO_Pin_8) #define RS485_DE_L() GPIO_ResetBits(GPIOA, GPIO_Pin_8) void uart_send_string(uint8_t *str) { RS485_DE_H(); while (*str) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, *str++); } while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); RS485_DE_L(); }DE 拉高到第一个起始位之间要留出收发器切换时间,一般延时几十微秒;发完之后等 TC 标志置位再拉低 DE,否则最后一字节会被 RS485 收发器的关断动作截断。如果总线上的设备还用了终端电阻,RS485 方向脚的控制时序是否可靠,直接决定整条链路能不能稳定跑一天。
5. 验证与排错:逻辑分析仪和串口回环把 pcf8563 时序钉死
5.1 四路信号看一眼时序
拿到新板子第一件事是用逻辑分析仪抓四路信号:SCL、SDA、GND、串口 TX,采样率 16MHz 就够。重点看两处:启动条件是否在 SCL 高电平期间发起 SDA 下降沿;读时间结束后停止条件前有没有多出额外的 SCL 时钟。多出的时钟通常是把 NACK 回成了 ACK,从机会继续补发数据,然后在总线上表现为一帧错位的字节。
逻辑分析仪上如果数据看起来全是 0xFF,先检查上拉电阻有没有焊、引脚是不是配成了推挽。软件 I2C 有个好处,把i2c_delay加到 50 个空循环后波形明显变宽,很容易分辨是主机没发时序还是从机没应答。
5.2 用一段 Python 脚本做回归验证
手动在串口助手里敲命令能验功能,验不了边界。写个十几行的脚本,把设置时间和读回时间做成闭环断言,每次改完驱动跑一遍,比用示波器反复抄波形高效。
import serial import time ser = serial.Serial('COM5', 115200, timeout=1) def send(cmd: str) -> str: ser.write((cmd + '\r\n').encode()) time.sleep(0.1) return ser.read_all().decode(errors='ignore') # 1. 设置一个已知时间 resp = send('SET TIME 2026-02-14 10:30:00') assert 'OK SET' in resp, resp # 2. 读回时间,年份日期必须完全一致 resp = send('GET TIME') assert 'OK TIME 2026-02-14 10:30:0' in resp, resp # 3. 故意发非法命令,确认不会误改时间 resp = send('SET TIME 2026-13-40 99:99:99') assert 'ERR' in resp, resp # 4. 再读一次,时间应该还在 10 秒以内,而不是被坏命令打乱 resp = send('GET TIME') assert 'OK TIME 2026-02-14 10:30:' in resp, resp print('pcf8563 roundtrip pass')脚本里第 3 步最有价值:坏命令必须返回 ERR 且不改时间。产线或者上位机联调时,经常有人在日期格式里混入一个前导空格,如果驱动把2026-02-14 10:30:00解析成了非法值却返回 OK SET,问题就很难定位。断言直接把这些风险挡在测试阶段。
5.3 三个容易被误判的坑
第一个是秒寄存器读到 0x80 以上。这不是芯片坏了,而是 VL 位为 1,说明 RTC 曾掉电或电池电压过低。驱动里读取后必须掩码,同时把 VL 状态通过串口上报,让上位机知道“时间可能不准确”,而不是自己悄悄把非法值显示出去。
第二个是晶振不起振。PCF8563 的 CLKOUT 引脚默认输出 32.768kHz 信号,但它可以被寄存器关闭,所以 CLKOUT 没波形不能证明晶振停了。判断振动与否,要用示波器探 OSCO 引脚,而不是依赖 CLKOUT。
第三个是设置时间后过几天又跳回初始值。这类问题往往不是 I2C 写入失败,而是后备电池回路里的二极管压降太大,或者超级电容容量不足。把这个排查思路记在心里,遇到“时间能设能读但掉电即失”的情况,先量 VDD 引脚在断电后的电压,再看 I2C 时序。
回归脚本再往后走一步,可以把 VL 位也做成一个状态字带出来,每次 GET TIME 都额外返回电池健康标志。这样上位机不用猜,驱动也不用吞状态位——让串口把芯片的真实情况吐出来,比在代码里做各种“看起来正常”的补救更可靠。
本文还有配套的精品资源,点击获取