简介:面向嵌入式开发者的GD32F103硬件I2C实战资源,围绕SSD1306 OLED显示屏、PCF8563 RTC实时时钟和SHT30温湿度传感器,完整呈现从I2C外设配置、OLED初始化到RTC读写及传感器数据解析的代码实现,并涉及中断/DMA选择、错误处理与低功耗考量,适合需要快速掌握GD32外设驱动的中初级开发者。压缩包为RAR格式,共312个文件,以C源码(80个c)、头文件(99个h)、编译依赖/目标文件以及uvprojx工程文件和hex/axf可执行镜像为主,并附带链接脚本、map文件等调试辅助内容,整体仅5.08MB,结构紧凑便于直接查阅或移植。当前已有1200人学习/下载。读者可从中提取可直接调用的驱动接口与工程配置,稍加修改即用于自己的环境监测、显示终端等嵌入式项目,节省底层调试时间。
1. GD32F103 硬件 I2C:比模拟 I2C 快,也比模拟 I2C 更容易翻车
GD32F103 的硬件 I2C 驱动 SSD1306 OLED、PCF8563 RTC 和 SHT30 温湿度传感器,把三个 I2C 从机挂同一条总线上,是很多温度采集、时间显示小项目的基本盘。但说实话,我第一次从模拟 I2C 切到硬件 I2C 时,在 BUSY 位清不掉这个坑上卡了整整一个下午——硬件 I2C 不占用 CPU、能上 400kHz,却比 GPIO 模拟更讲究初始化顺序、应答时序和错误恢复。这篇笔记就围绕这三颗器件的驱动,把初始化、读写流程、参数取舍和踩坑点讲透,适合刚切到 GD32 或者被硬件 I2C 卡死过的开发者照着复现。
2. 先把时钟和端口配对:GD32F103 硬件 I2C 的初始化与读写流程
2.1 为什么不用 GPIO 模拟 I2C:硬件外设的真正优势
很多开发者从 STM32F103 迁到 GD32F103 之后,第一反应是「I2C 能模拟就模拟」,因为模拟 I2C 的移植成本几乎为零,几个 GPIO 翻转加延时就能跑。这在只挂一个 OLED 的场合确实没问题,但当你把 SSD1306、PCF8563、SHT30 三个器件挂在同一条总线上时,模拟 I2C 的问题就暴露出来了:所有延时都占用 CPU 忙等;400kHz 的时序很难靠 delay 精确控制,SCL 高电平时间稍微偏一点,SHT30 这类对时序敏感的传感器就可能应答异常;主循环里一旦有别的任务插进来,时序被拉长,从机容易误判起始位或停止位。
GD32F103 的硬件 I2C 外设(I2C1/I2C2)自带时钟生成、起始停止条件、应答管理和移位寄存器,配合 DMA 甚至可以在不打断 CPU 的情况下持续刷新 OLED。代价是它的寄存器行为比模拟方式更「死板」:你必须按状态机的顺序去等标志位、清标志位,漏掉一步,总线就卡住。我一般把硬件 I2C 作为默认方案,模拟 I2C 只留作调试期比对波形的参照。
2.2 时钟树与 GPIO 复用:初始化代码与参数说明
GD32 的 I2C1 默认映射在 PB6(SCL)和 PB7(SDA)上,引脚必须配置为开漏复用输出,同时开启 AFIO 时钟和 I2C1 时钟。下面是基于 GD32F10x 标准外设库的最小初始化,注意 GD32 的外设库函数名和 STM32 的 SPL 不一样,别直接照搬 ST 的I2C_Init写法。
#include "gd32f10x.h" void i2c_gd32_init(void) { /* 使能 GPIOB、AFIO 和 I2C1 时钟 */ rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_AF); rcu_periph_clock_enable(RCU_I2C1); /* PB6=SCL, PB7=SDA,开漏复用,50MHz 翻转速度 */ gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7); gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_6 | GPIO_PIN_7); /* 400kHz 快速模式,tlow:thigh = 2:1 */ i2c_clock_config(I2C1, 400000, I2C_DTCY_2); /* 7 位地址模式,主机模式,本机地址随便填一个不冲突的值 */ i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, 0x00); i2c_enable(I2C1); i2c_ack_config(I2C1, I2C_ACK_ENABLE); }这里有两个关键参数。第一个是I2C_DTCY_2,它表示 SCL 低电平和高电平的占空比,快速模式 400kHz 下用 2:1 比较稳;如果总线电容大、从机多,改成I2C_DTCY_16_9或把频率降到 100kHz 会更可靠。第二个是 GPIO 模式,必须是GPIO_MODE_AF_OD开漏复用,不能推挽——I2C 协议本身依赖开漏结构,用推挽会导致多主机冲突时烧引脚。GPIO_AF_4是 GD32F103 系列里 I2C1 引脚复用功能的编号,和 STM32 的GPIO_AF4_I2C1是同一个概念但要写成 GD32 库的枚举。
2.3 读写多字节的完整流程:硬件 I2C 的状态机思维
硬件 I2C 排错难,是因为每一步操作都要等对应的状态位,而且不同库函数对标志位的命名差异很大。GD32 里我把读写流程封装成两个基础函数:写从机寄存器、读从机寄存器。读的时候要用到重复起始位(Restart),很多新人在这里翻车。
static int i2c_wait_flag(I2C_TypeDef *i2c, uint32_t flag, uint32_t timeout_us) { while (!(i2c->STAT0 & flag)) { if (timeout_us-- == 0) { return -1; } } return 0; } int i2c_write_bytes(I2C_TypeDef *i2c, uint8_t dev_addr, uint8_t *buf, uint8_t len) { /* 发送起始条件 */ i2c->CTL0 |= I2C_CTL0_START; if (i2c_wait_flag(i2c, I2C_STAT0_SBSEND, 1000) < 0) goto err; /* 发送从机写地址 */ i2c->DATA = (uint8_t)(dev_addr << 1); if (i2c_wait_flag(i2c, I2C_STAT0_ADDSEND, 1000) < 0) goto err; /* 读 STAT0 和 STAT1 清 ADDSEND 标志 */ (void)i2c->STAT0; (void)i2c->STAT1; /* 逐字节发送数据,等待发送缓冲空 */ for (uint8_t i = 0; i < len; i++) { if (i2c_wait_flag(i2c, I2C_STAT0_TBE, 1000) < 0) goto err; i2c->DATA = buf[i]; } /* 等待最后一个字节发送完成,然后产生停止条件 */ if (i2c_wait_flag(i2c, I2C_STAT0_TBE, 1000) < 0) goto err; i2c->CTL0 |= I2C_CTL0_STOP; return 0; err: /* 失败后发停止条件并复位外设,避免总线挂死 */ i2c->CTL0 |= I2C_CTL0_STOP; i2c->CTL0 |= I2C_CTL0_SWRST; i2c->CTL0 &= ~I2C_CTL0_SWRST; i2c_enable(i2c); return -1; }读多字节的流程要复杂一些:先发从机写地址和寄存器地址,然后发重复起始位,切换为读模式,逐字节接收。最后一个字节接收前要关闭 ACK,否则从机不知道什么时候释放总线。
int i2c_read_bytes(I2C_TypeDef *i2c, uint8_t dev_addr, uint8_t reg, uint8_t *buf, uint8_t len) { /* 第一阶段:发起始位 + 写地址 + 寄存器地址 */ i2c->CTL0 |= I2C_CTL0_START; if (i2c_wait_flag(i2c, I2C_STAT0_SBSEND, 1000) < 0) goto err; i2c->DATA = (uint8_t)(dev_addr << 1); if (i2c_wait_flag(i2c, I2C_STAT0_ADDSEND, 1000) < 0) goto err; (void)i2c->STAT0; (void)i2c->STAT1; i2c->DATA = reg; if (i2c_wait_flag(i2c, I2C_STAT0_TBE, 1000) < 0) goto err; /* 第二阶段:重复起始位 + 读地址 */ i2c->CTL0 |= I2C_CTL0_START; if (i2c_wait_flag(i2c, I2C_STAT0_SBSEND, 1000) < 0) goto err; i2c->DATA = (uint8_t)(dev_addr << 1) | 0x01; if (i2c_wait_flag(i2c, I2C_STAT0_ADDSEND, 1000) < 0) goto err; (void)i2c->STAT0; (void)i2c->STAT1; /* 第三阶段:读 len 个字节,最后一位不回应答 */ for (uint8_t i = 0; i < len; i++) { if (i == len - 1) { i2c_ack_config(i2c, I2C_ACK_DISABLE); } if (i2c_wait_flag(i2c, I2C_STAT0_RBNE, 1000) < 0) goto err; buf[i] = (uint8_t)i2c->DATA; } i2c->CTL0 |= I2C_CTL0_STOP; i2c_ack_config(i2c, I2C_ACK_ENABLE); return 0; err: i2c->CTL0 |= I2C_CTL0_STOP; i2c->CTL0 |= I2C_CTL0_SWRST; i2c->CTL0 &= ~I2C_CTL0_SWRST; i2c_enable(i2c); return -1; }这段代码里的I2C_STAT0_SBSEND对应起始位发送完成,ADDSEND对应地址字节发送且收到从机 ACK,TBE表示发送缓冲空,RBNE表示接收缓冲非空。清ADDSEND必须连续读一次STAT0和STAT1,这是硬件 I2C 最容易漏的步骤。超时值我习惯给到 1000 微秒级别,正常总线操作一次不到 1 毫秒,超时几乎可以断定总线异常。出错分支里的SWRST软件复位是「后悔药」,它能在外设状态机卡死时把 I2C 拉回来,代价是重新初始化要花几个微秒,对这三个从机完全无感。
3. 驱动 SSD1306 OLED:先过 ACK 这一关,再谈显示
3.1 SSD1306 的 I2C 帧格式:地址位与控制字节
SSD1306 在 I2C 总线上的 7 位地址由 SA0 引脚决定,常见模块 SA0 接地,地址是 0x3C,写地址就是 0x78。注意发送数据时,控制字节决定了后续字节是被当作命令还是显存数据:控制字节0x00表示后跟命令,0x40表示后跟数据。OLED 模块不像 RTC 那样有寄存器地址的概念,所有操作都是靠「控制字节 + 内容」的序列完成的,所以不能直接用第 2 章那个i2c_write_bytes(dev_addr, reg, buf, len)去写 OLED,需要先构造一个带控制字节的缓冲。
我一般会在 OLED 驱动里单独做一层封装,避免把控制字节的细节散落到上层业务代码里。
static uint8_t oled_i2c_buf[OLED_BUF_SIZE + 1]; static int oled_write_cmd(uint8_t cmd) { oled_i2c_buf[0] = 0x00; /* 控制字节:命令 */ oled_i2c_buf[1] = cmd; return i2c_write_bytes(I2C1, 0x3C, oled_i2c_buf, 2); } static int oled_write_data(const uint8_t *data, uint16_t len) { if (len > OLED_BUF_SIZE) { return -1; } oled_i2c_buf[0] = 0x40; /* 控制字节:数据 */ memcpy(&oled_i2c_buf[1], data, len); return i2c_write_bytes(I2C1, 0x3C, oled_i2c_buf, len + 1); }这里OLED_BUF_SIZE我一般定义为 128,对应一行 128 字节的显存宽度。0x00和0x40这两个控制字节是 SSD1306 数据手册里的硬性规定,不属于可配置项。封装成oled_write_cmd和oled_write_data之后,上层代码不需要关心 I2C 帧结构,换到模拟 I2C 时只需要改这两个函数的内部实现,显示逻辑一行都不用动。
3.2 初始化序列:上电时序比命令本身更讲究
SSD1306 的初始化命令各家模块大同小异,核心是设置显存寻址模式、显示时钟、电荷泵和扫描方向。我把初始化序列放在数组里,逐条发送,最后留足延时再开显示。
static const uint8_t oled_init_cmds[] = { 0xAE, /* 关闭显示 */ 0x20, 0x02, /* 页寻址模式 */ 0xB0, /* 页起始地址 0 */ 0xC8, /* COM 扫描方向反向 */ 0x00, 0x10, /* 列地址低字节和高字节 */ 0x40, /* 显示起始行 0 */ 0x81, 0xCF, /* 对比度 207 */ 0xA1, /* 段重映射 */ 0xA6, /* 正常显示,不反显 */ 0xA8, 0x3F, /* 复用比 1/64 */ 0xD3, 0x00, /* 显示偏移 0 */ 0xD5, 0x80, /* 时钟分频和振荡频率 */ 0xD9, 0xF1, /* 预充电周期 */ 0xDA, 0x12, /* COM 引脚硬件配置 */ 0xDB, 0x40, /* VCOMH 电平 */ 0x8D, 0x14, /* 开启电荷泵 */ 0xAF /* 打开显示 */ }; int oled_init(void) { for (uint8_t i = 0; i < sizeof(oled_init_cmds); i++) { if (oled_write_cmd(oled_init_cmds[i]) < 0) { return -1; } } /* 电荷泵开启后需要稳定时间,延时后再清屏 */ delay_ms(50); oled_clear(); return 0; }很多人初始化 OLED 失败,问题并不在命令序列,而在上电时序。SSD1306 的 VDD 和 VCC 两个电源域有先后要求,模块刚上电时内部电荷泵还没稳定,立即写入命令会出现「第一次不亮、复位一次就亮」的现象。我一般在0x8D, 0x14开启电荷泵之后延时 50ms,再用0xAF开显示。整屏清屏也是初始化的一部分,因为 OLED 上电后 GDDRAM 内容是随机的,不清理会显示雪花噪点。
3.3 页寻址下画点和字符显示:显存怎么算
页寻址模式下,SSD1306 把显存分成 8 页,每页 128 字节,每字节的 8 个 bit 对应一列像素的 8 行。画一个点实际上是读改写某个页里的一个字节:先定位到页和列,再修改对应的 bit。这套逻辑和常见的 0.96 寸 128x64 模块完全一致。
void oled_set_pixel(uint8_t x, uint8_t y, uint8_t on) { uint8_t page = y / 8; uint8_t bit = y % 8; uint8_t col = x; oled_write_cmd(0xB0 + page); /* 设置页地址 */ oled_write_cmd(0x00 + (col & 0x0F)); /* 列低四位 */ oled_write_cmd(0x10 + (col >> 4)); /* 列高四位 */ /* 读改写:为了不改动同一字节里其他行的点,需要先读回显存 */ /* 这里用缓存数组保存当前页显存,避免频繁读 OLED */ if (on) { oled_cache[page][col] |= (1 << bit); } else { oled_cache[page][col] &= ~(1 << bit); } oled_write_data(&oled_cache[page][col], 1); }这里我引入了一个oled_cache[8][128]的 RAM 缓存,原因是 SSD1306 不支持单点写入,每次写一个字节都会覆盖该字节对应的 8 行像素。如果直接写,不保存原有数据,就会把同列的其它点清掉。全屏缓存占 1KB,GD32F103 有 96KB SRAM,完全不心疼。画字符串就是在像素基础上,从 ASCII 字模表里逐列取出 8x8 的点阵,套用oled_set_pixel或直接按列写缓存字节。初始化 OLED 的整个流程走通之后,显示刷新率能做到 30fps 以上,硬件 I2C 的吞吐量在这里体现得最明显。
4. PCF8563 与 SHT30 的驱动要点:BCD 转换、CRC 校验和数据帧
4.1 PCF8563:读时间之前先看状态,写时间之前先停振
PCF8563 的 7 位 I2C 地址是 0x51,寄存器 0x02 到 0x08 分别对应秒、分、时、日、星期、月、年,全部采用 BCD 编码。这颗芯片有个很容易忽略的位:秒寄存器 0x02 的 bit7 是 VL 位(时钟失效标志),电池掉过电或晶振停过振时它会被置 1。如果不去管它,读出来的时间可能比真实时间慢了几分钟甚至完全错误。
我读时间的做法是先读状态寄存器再判断 VL 位,而不是拿到秒数就直接往上显示。
typedef struct { uint8_t sec; uint8_t min; uint8_t hour; uint8_t day; uint8_t weekday; uint8_t month; uint8_t year; } rtc_time_t; static uint8_t bcd2bin(uint8_t x) { return (x >> 4) * 10 + (x & 0x0F); } static uint8_t bin2bcd(uint8_t x) { return ((x / 10) << 4) | (x % 10); } int pcf8563_read_time(rtc_time_t *t) { uint8_t regs[7] = {0}; /* 从 0x02 开始连续读 7 个字节 */ if (i2c_read_bytes(I2C1, 0x51, 0x02, regs, 7) < 0) { return -1; } /* 秒寄存器 bit7 是 VL 标志,为 1 表示时钟可能走时不准 */ if (regs[0] & 0x80) { return -2; } t->sec = bcd2bin(regs[0] & 0x7F); t->min = bcd2bin(regs[1] & 0x7F); t->hour = bcd2bin(regs[2] & 0x3F); t->day = bcd2bin(regs[3] & 0x3F); t->weekday = bcd2bin(regs[4] & 0x07); t->month = bcd2bin(regs[5] & 0x1F); t->year = bcd2bin(regs[6]); return 0; }写时间时,PCF8563 的寄存器 0x00 的 bit5 是 STOP 位,置 1 会停止振荡。我习惯先把 STOP 置 1,写入全部时间寄存器,再清 STOP 让它重新走时,这样时钟不会在写入过程中跑偏到奇怪的位置。BCD 和二进制之间的转换就是(x >> 4) * 10 + (x & 0x0F)这个公式,注意读秒寄存器时要先& 0x7F把 VL 位滤掉,否则可能出现秒值为 80 之类的异常数据。
4.2 SHT30:测量命令、CRC 校验和数据合成
SHT30 的 7 位地址默认是 0x44,发送测量命令0x2C加一个模式字节0x06(高重复性、单次测量),然后等待测量完成,读回 6 个字节:温度高字节、温度低字节、温度 CRC、湿度高字节、湿度低字节、湿度 CRC。单次测量在高重复性下典型转换时间约 15ms,代码里必须等够这段时间,否则读到的全是旧数据或 0xFF。
CRC 校验是 SHT30 和普通温湿度传感器最大的区别。传感器每个测量值后面跟一个 CRC8 字节,多项式是 0x31,初值 0xFF,校验失败说明数据在传输过程中被干扰,这时候应该丢弃本次结果重新测量。下面是完整的读取流程。
static uint8_t sht3x_crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0xFF; for (uint8_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x31; } else { crc <<= 1; } } } return crc; } int sht30_read(uint16_t *temp_raw, uint16_t *humi_raw) { uint8_t cmd[2] = {0x2C, 0x06}; uint8_t data[6] = {0}; /* 发送单次测量命令 */ if (i2c_write_bytes(I2C1, 0x44, cmd, 2) < 0) { return -1; } /* 等待测量完成,高重复性单次测量典型 15ms */ delay_ms(20); /* 连续读 6 字节 */ if (i2c_read_bytes(I2C1, 0x44, 0xFF, data, 6) < 0) { return -2; } /* 温度数据在 data[0:1],CRC 在 data[2] */ if (sht3x_crc8(data, 2) != data[2]) { return -3; } /* 湿度数据在 data[3:4],CRC 在 data[5] */ if (sht3x_crc8(&data[3], 2) != data[5]) { return -4; } *temp_raw = ((uint16_t)data[0] << 8) | data[1]; *humi_raw = ((uint16_t)data[3] << 8) | data[4]; return 0; }这里我用了i2c_read_bytes(I2C1, 0x44, 0xFF, data, 6)这样的调用方式,实际上 SHT30 在收到测量命令后并不需要指定寄存器地址,直接读就行,所以我传了个占位地址 0xFF,然后让读取函数发重复起始位去读。如果你想更严谨,可以单独写一个i2c_read_raw()函数,只发读地址不加寄存器地址。拿到原始值之后,物理量换算公式是固定的:温度T = -45 + 175 * raw / 65535,相对湿度RH = 100 * raw / 65535。浮点运算在 GD32F103 上没问题,但如果要在中断里频繁计算,建议用定点数或查表法。
5. 硬件 I2C 避坑:BUSY 卡死、上电黑屏和全 0xFF
5.1 BUSY 标志清不掉,整条总线罢工
现象是第一次读写正常,第二次开始i2c_wait_flag全部超时,调试发现 I2C 控制寄存器里的 BUSY 位一直被置 1,发送起始条件没反应。原因大多数是从机在通信中途掉线或 NACK 之后没有正确产生停止条件,外设状态机卡在等待从机释放 SDA 的位置,BUSY 是硬件根据 SDA 电平算出来的,只要 SDA 还处于被拉低的状态,它就永远不会自己变 0。解决办法是触发软件复位:置位I2C_CTL0_SWRST,保持几个时钟周期再清除,重新使能外设,然后让上层调用一次失败重试。如果重试后还是 BUSY,就用万用表量 SDA 电平,确认不是从机自己把总线拉死了。
5.2 OLED 冷启动黑屏,按一下复位才亮
现象是上电后 OLED 全黑,但手动复位 MCU 之后显示正常。原因有两个:一是 SSD1306 的电源域上电时序不满足,电荷泵还没稳定时初始化命令就进去了;二是 MCU 复位时间比 OLED 模块的电源上升时间快,初始化发生在模块还没准备好接收 I2C 数据的时候。解决方法是把初始化拆成两段:先延时 100ms 等电源稳定,再发命令序列;命令序列里0x8D, 0x14开电荷泵后加 50ms 延时,最后才0xAF开显示。如果项目里 OLED 和 MCU 共用一组 LDO,还要检查上电瞬间的压降,压降太大时 OLED 会进入欠压保护。
5.3 PCF8563 时间不走或越走越慢
现象是写入时间后秒数不动,或者一天下来慢了几分钟。原因分两类:寄存器 0x00 的 STOP 位被误置 1,芯片停振;32.768kHz 晶振的两个负载电容没接或取值不对,导致振荡频率偏移。解决方法是写时间流程固定为「读状态寄存器 → 置 STOP → 写全部时间寄存器 → 清 STOP → 读回校验」,另外看 PCB 上晶振引脚有没有对地电容,一般 12.5pF 负载电容对应 6pF 到 8pF 的对地电容,这个值别照搬数据手册,要按实际 PCB 寄生电容调整。软件上可以在每次读时间时检查 VL 位,如果置位,就用一个默认时间重新初始化。
5.4 SHT30 读到全 0xFF 或 CRC 必错
现象是温湿度数据永远是 0xFF,或者 CRC 校验十次有八次失败。0xFF 说明从机根本没应答——地址不对、总线地址冲突或者模块供电异常,先用逻辑分析仪抓一下 ACK 位置,看从机有没有拉低 SDA。CRC 频繁失败常见原因是总线速率太高或线太长,SHT30 本身能跑 1MHz,但杜邦线长到 20cm 以上时信号质量下降,I2C 又没有自动重传机制,数据在传输过程中被破坏只能靠 CRC 发现。解决方法是把 I2C 时钟降到 100kHz 验证,确认是信号问题后,再通过缩短线长、加 2.2k 上拉电阻或者换屏蔽线来恢复 400kHz。
5.5 上拉电阻选错,高低速切换翻车
现象是同一个板子,100kHz 下三个器件都正常,切到 400kHz 后 OLED 偶尔花屏,SHT30 时不时 CRC 错。原因几乎都是上拉电阻太大,SCL/SDA 的上升沿太慢,在 400kHz 下不满足建立时间。I2C 标准里上拉电阻的取值和总线电容相关:总线上三个从机加 PCB 走线,等效电容通常在 50pF 到 100pF,4.7k 上拉在 100kHz 下没问题,但 400kHz 时最好换成 2.2k;如果总线电容再大,2.2k 都不够,建议并联到 1k 并检查从机驱动能力。我现在的做法是原理图上预留 0 欧电阻位置,先按 4.7k 做,调试时根据波形实测换阻值。
6. 把三个设备统一进一个带重试的 I2C 管理器
三个器件各自调通了,紧接着的问题是谁先访问总线、失败了怎么办。我最后一个建议是把第 2 章的读写函数收敛成一个带重试的公共入口,所有从机驱动只调用这个入口,错误处理和总线复位只写一遍。
int i2c_transfer(uint8_t dev_addr, uint8_t reg, uint8_t *buf, uint8_t len, uint8_t is_read) { int ret = is_read ? i2c_read_bytes(I2C1, dev_addr, reg, buf, len) : i2c_write_bytes(I2C1, dev_addr, buf, len); if (ret < 0) { /* 第一次失败多半是瞬时 NACK,复位总线后重试一次 */ delay_ms(1); ret = is_read ? i2c_read_bytes(I2C1, dev_addr, reg, buf, len) : i2c_write_bytes(I2C1, dev_addr, buf, len); } return ret; }重试策略我只做一次,不写死循环,因为重复起始位失败之后从机状态可能已经乱了,无限重试只会让总线一直被占用。更好的验证手段是逻辑分析仪:抓一次sht30_read的完整波形,重点看重复起始位之后有没有出现两次 S 信号、ACK 位置上 SDA 有没有被拉低、最后 STOP 是不是完整的三段式。一个干净的波形比任何调试打印都有说服力。
我现在的习惯是每新接一颗 I2C 从机,都先用逻辑分析仪把它的地址、ACK 序列、数据帧长度抓一遍,确认没问题之后再写驱动。这套流程走下来,SSD1306、PCF8563、SHT30 三个设备在同一条硬件 I2C 总线上共存了很长时间,没有再踩过总线卡死和 CRC 翻车。希望帮到你。
本文还有配套的精品资源,点击获取