|
> 从 TM4C123 移植到 GD32F427 后,外部 EEPROM(CAT24C64)读出来**全是 0**。 > 一路差点被带偏去查"是不是上拉电阻没焊、EEPROM 是不是没供电"—— > 最后发现是 **I2C 器件地址的表示方式差了一位**:旧库要 `0x50`,GD32 库要 `0xA0`。 > 改一个常量,当场读到固件版本号。这篇倒不是炫技,是复盘一个差点甩锅硬件的排查过程。
---
## 一、现象:读到的是 0,不是"读不到"
产品用 ModBus RTU 从站,配置参数存在外部 EEPROM(CAT24C64,I2C 地址 `0x50`)。移植到 GD32 后固件能跑,串口收发也通了(上个坑刚修完 ModBus 帧尾判帧问题)。于是验证 EEPROM,发读参数命令:
``` 发: 01 A0 00 00 00 04 80 10 # 读地址 0,读 4 字节 收: 01 A0 00 00 00 04 00 00 00 00 xx xx # 数据区 = 00 00 00 00 ```
地址 0 是配置结构体的**魔数**字段(健康值 `0xA55A`,小端应是 `5A A5`)。结果读到全 0。
换个地址读固件版本号(偏移 0x12):
``` 发: 01 A0 00 12 00 10 20 1A # 读地址 18,读 16 字节 收: 01 A0 00 12 00 10 00×16 xx xx # 还是全 0 ```
**两个不同地址都读到全 0。** 这时脑子里的第一反应是:空白 CAT24C64 读出来应该是 `0xFF`,不是 `0x00`——读回 0 不对劲,肯定哪儿"读"失败了。但"哪儿"?两个手指头同时指向了"硬件"。
> 这是第一个要纠的念头:**"数据为 0" 不等于 "硬件坏了"。** 它只说明"读回来不是期望值",至于为什么,软件这边有一大堆没查清的地方。
## 二、一个特征值,把"读不到"和"读到 0"分开
要往下查,得先确认我面对的是两种情况里的哪一种:
- **A. I2C 读取失败了**(总线没握上手 / 从机没 ACK),返回的是缓冲区残值; - **B. I2C 读取成功了,但 EEPROM 里真的存着 0**。
这两种在串口上**长得一模一样**(都是 0),必须用工具把它们撕开。
最小改动:临时让 `ExtEEPROMRead` 在**三次重试都失败**时,往结果缓冲区里填一个特征字节 `0xA5`——正常数据区不可能出现 `A5`,一读就知道这是个"人工标记"而不是真数据。
for (retry = 0; retry < 3; retry++) if (HWL_I2C_MemRead(...) == 0) break; // 成功则跳出 if (retry == 3) // 三次全失败 for (i = 0; i < Amount; i++) RData = 0xA5; // 打标
再发读版本号:
``` 发: 01 A0 00 12 00 10 20 1A 收: 01 A0 00 12 00 10 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 DB 21 ```
一锤定音:**是情况 A——读失败了**。因为打进缓冲区的是 `A5`,说明 `HWL_I2C_MemRead` 三次都没成功,落在了超时分支。
到这里为止,方法没问题:**用特征值把两种表象分开,而不是靠猜。**
但**下一个念头又差点错**:既然"读失败",是不是就跳到"电气/器件"(上拉电阻、焊接、供电、地址脚)?——这里才是真正的分水岭,也是最容易翻车的地方。**"读失败"只告诉你"某个函数没返回成功",它不告诉你"为什么失败"。** 而在"为什么"里,软件这一侧还有一大票没证伪的:
- I2C 外设时钟是否真的使能了? - GPIO 复用功能(AF)号配对了没有? - `i2c_clock_config` 依赖 APB1 时钟算分频,APB1 时钟对不对? - 超时判据 `i2c_wait(flag, 0xFFFF)` 是**纯软件轮询计次**,端口/时钟不同会不会"假超时"? - **器件地址到底按库的什么约定传?**
## 三、真正的根因:I2C 地址表示方式差了一位
去读 GD32 库源码的 `i2c_master_addressing`(`gd32f4xx_i2c.c`):
void i2c_master_addressing(uint32_t i2c_periph, uint32_t addr, uint32_t trandirection) { if (I2C_TRANSMITTER == trandirection) addr = addr & 0xFFFFFFFE; // 清最低"方向"位 else addr = addr | 0x00000001; // 置"方向"位 I2C_DATA(i2c_periph) = addr; // 直接写数据寄存器 }
**注意:这个库对地址只做了按位清零/置位,没有任何左移 `<<1`。**
再回想一下 I2C 的地址是怎么在总线上跑的:
> I2C 从机地址是 **7 位**(CAT24C64 固定 `0x50` = `101_0000₂`)。 > 实际传输时把 7 位地址**左移 1 位**,最低位当"方向位"(0=主机写,1=主机读): > - 写地址字节 = `0x50 << 1 | 0` = **`0xA0`** > - 读地址字节 = `0x50 << 1 | 1` = **`0xA1`**
也就是说,往 I2C 数据寄存器里写"地址字节",应该写 `0xA0` / `0xA1` 这种**8 位**的值。
而我们的引脚映射表里定义的是(`gd32_hwl_pinmap.h`):
#define I2C0_EEPROM_ADDR 0x50 /* 7-bit 器件地址 */ 用码道免费领 1 个月 Token cpp 运行 **`0x50` 是 7 位地址,不是 8 位地址字节。** 这个习惯是从原版(TivaWare / STM32 库)照搬来的——那两套库接收的是 7 位地址,内部帮你左移。GD32 库**不帮你左移**。
于是故障过程完全闭环了:
1. 传 `0x50` 进 `i2c_master_addressing`,库清方向位后还是 `0x50`; 2. 硬件把 `0x50` 当作地址字节发到总线上 → 7 位地址 = `0x50 >> 1` = **`0x28`**; 3. EEPROM 实际在 `0x50`,收到的是寻址 `0x28` → **永不应答(无 ACK)**; 4. 主机等 ACK 等不到 → `ADDSEND` 标志一直不置位 → 超时 → 读失败返回 → 缓冲区是残值 0。
**从主机角度看总线"是通的"(START、时序都正常),但从机***不响应。** 这个特征特别阴险——它表现为"读到了空数据",特别容易让人以为"是不是 EEPROM 里没数据 / 是不是芯片坏了",而实际是**主机发错了地址**。
## 四、修复:一个常量
#define I2C0_EEPROM_ADDR 0xA0 /* 8 位地址字节(写, W=0):7 位 = 0x50 */ 用码道免费领 1 个月 Token cpp 运行 写路径库内部 `| 1` 自动变读地址 `0xA1`,所以只要把**写地址字节 `0xA0`** 定义对了,读写两头都齐。
改完重新烧录,再发读版本号:
``` 发: 01 A0 00 12 00 10 20 1A 收: 01 A0 00 12 00 10 56 65 72 20 32 30 32 36 2E 30 33 2E 31 38 00 00 DD 04 ```
`56 65 72 20 32 30 32 36 2E 30 33 2E 31 38` 的 ASCII 就是 **`Ver 2026.03.18`**——固件版本号完整回来了。
**根因:一个常量差了一位(`0x50` vs `0xA0`),表象却是"EEPROM 全读 0"。** 硬件从头到尾没动。
## 五、复盘:这次最该记住的是什么
### 1. "读回 0 / 读回特征值" ≠ 外部原因的证据 `A5` 只证明了"`HWL_I2C_MemRead` 走进了超时分支"。它**证明不了**是硬件。真正该问的是"为什么超时"——答案在自己写的代码里,不在板上。特征值只是把你推到一个更精确的问题,而不是让你直接跳到一个结论。
### 2. 为什么第一感觉总是"硬件"? 因为这行代码的失败从主机端看毫无异常:时序照发、从机"安静"。唯一的破绽是从机不 ACK。可"从机不 ACK"有无数软件原因——地址错了就是其中之一,而且是**最容易踩的那种:它不报错,只是静默失败**。
### 3. 移植的断层,往往藏在"照搬的常量"里 原版 TivaWare / STM32 库与 GD32 库对同一概念可能有**不同的抽象约定**:
| 概念 | 原版库习惯 | GD32 库实际 | |------|-----------|-------------| | I2C 器件地址 | 7 位 `0x50`,库内部左移 | **8 位地址字节 `0xA0`**,库不左移 | | 方向位 | 库内部补 | 库内部补(但前提是你给了 8 位) | | CAN 同步跳转宽 | RJW | **SJW** | | ADC 通道 | REGULAR | **ROUTINE** | | 串口命名 | USART3/4 | **UART3/4** |
所以 **GD32 移植后的第一件事,***是核对从旧代码照搬过来的每一个常量、每一个函数参数约定、每一种字节序习惯**,确认它们在新库里语义一致。这类"断层"才是反复出现的真凶,而不是硬件。
### 4. 一套可复用的排查顺序 1. **现象** → 先分层:物理/驱动 → 协议 → 应用,一次只测一层; 2. 每层用**最小可证伪测试**自证干净(这例是特征值 `0xA5` 区分"读不到"和"读到 0"); 3. 判断靠抓真实变量/源码,不靠推理蒙; 4. **先证明自家代码路径干净,再谈外部**——"要不要打表看线序"排在最后,且通常是可选; 5. 测之前先问:这个测试能**证伪**什么?如果不能,别上。
---
**一句话收尾:** 这一次"EEPROM 全读 0",不是上拉电阻、不是虚焊、不是芯片没供电——是 GD32 库要 `0xA0`,我给了 `0x50`。从"怀疑电气"到"改一个常量定案",中间隔着的是"先查自己代码"这一层。 ———————————————— 版权声明:本文为CSDN博主「高铃」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/April_lie_/article/details/164028228
|