3. 从设备地址冲突检测与处理
动态地址分配(Dynamic Address Assignment,DAA)跑完之后,I3C总线上每个从设备都应该有一个独一无二的动态地址(Dynamic Address,DA)。
但现实往往不那么完美。我做了这么多年调试,遇到过好几次这样的情况:明明DAA流程走完了,总线却莫名其妙地出问题——要么应答乱掉,要么数据被篡改。查到最后,发现两个从设备居然拿到了相同的动态地址。
为什么会这样?说白了,就是地址冲突。这节我们就来聊聊怎么发现它,怎么处理它。
3.1 地址冲突的本质
在I3C协议里,动态地址是7位的,取值范围从0x08到0x7F。理论上最多支持120个设备,但实际项目中很少会接满。不过,一旦某个地址被重复分配,总线上的信号就会乱套。
举个例子:两个从设备都响应0x1A这个地址。主设备发送“0x1A + 写”的时候,两个设备都会拉低SDA做应答。应答信号本来是一对一的,现在变成了“多对一”,主设备根本分不清谁是谁。
| 冲突现象 | 主设备感知 | 直接后果 |
|---|---|---|
| 应答阶段总线异常 | SDA在ACK窗口内既不是低也不是高 | 主设备认为NACK,丢弃当前事务 |
| 数据读取错乱 | 读到的字节包含两个设备的混合输出 | 数据校验失败,系统重启DAA |
| 广播地址多设备同时应答 | 无法区分哪个设备回应了哪个命令 | 整个I3C总线进入未知状态 |
3.2 检测机制——分层排查
我个人习惯把检测分成三层:软件层、协议层和物理层。从软件层开始往下查,效率最高。
3.2.1 软件层:主设备维护DA分配表
主设备在做DAA的时候,内部应该维护一张已分配动态地址的列表。每次给新的从设备分配地址前,先查查这个地址有没有被用过。
/* 伪代码:主设备动态地址分配时的冲突预检 */
uint8_t assigned_da_pool[120];
uint8_t pool_size = 0;
bool is_address_used(uint8_t da) {
for (int i = 0; i < pool_size; i++) {
if (assigned_da_pool[i] == da) {
return true;
}
}
return false;
}
uint8_t allocate_dynamic_address(void) {
uint8_t candidate = 0x08 + rand() % 0x78;
while (is_address_used(candidate)) {
candidate = 0x08 + rand() % 0x78;
}
assigned_da_pool[pool_size++] = candidate;
return candidate;
}
3.2.2 协议层:利用SDR CCC进行主动探测
软件层的预检不能完全避免冲突,因为有些冲突是硬件时序导致的。这时候需要用协议层的办法。
主设备可以在DAA完成后,发送一个 GET_XXXX 类的SDR CCC(比如 GET_ACCCR 或 GET_SETDASA),带上目标动态地址。如果多个设备响应同一个地址,它们的应答会冲突。
我怎么检测这个冲突?
- 主设备发送SDR CCC,目标地址为目标DA。
- 在ACK阶段,拉低SDA表示应答。
- 如果多个设备同时拉低,SDA电平正常(低)——但这种情况下,后续数据阶段就会出问题。
- 更有效的办法:发送GET_SETDASA命令。这个命令会让从设备返回自己的静态地址。如果总线上一堆0x00、0xFF或者乱码,基本可以断定地址冲突了。
我曾经用这个办法查过一个智能家居网关。DAA看起来跑完了,但每次读取传感器数据时,温度值偶尔会变成湿度值。发送GET_SETDASA之后,两个传感器居然返回了相同的静态地址类型——嗯,它们的Provisional ID虽然不同,但动态地址冲突了。
3.2.3 物理层:示波器抓取ACK行为
如果软件层和协议层都没发现问题,那就得上示波器了。我一般会看两个关键点:
- ACK/NACK窗口的SDA电平:正常ACK窗口SDA被拉低;如果SDA在窗口内呈现“中间电平”或随机跳变,那肯定有冲突。
- SCL时钟后的数据字节:地址冲突后,数据阶段会收到两个设备混合输出的字节。比如一个设备想发0xA5,另一个想发0x5A,主设备读到的可能是乱码。
3.3 处理策略——冲突发生后怎么办?
冲突已经发生了,别慌。I3C协议提供了一种优雅的恢复机制:重新进行动态地址分配(RSTDDA)。
3.3.1 主设备主导:发起RSTDDA
RSTDDA(Restart Dynamic Address Assignment)是一个广播命令。主设备发送这个命令后,所有支持DAA的从设备都会把自己的动态地址清空,回到初始状态。
/* 主设备发起RSTDDA广播 */
/* I3C总线事务示例(C伪代码) */
void i3c_enter_daa_mode(void) {
// 1. 发送START条件
i3c_start();
// 2. 发送广播地址0x7E + 写位(0)
i3c_send_byte(0x7E << 1);
// 3. 发送ENTDAA CCC(0x01)
i3c_send_byte(0x01);
// 4. 进入DAA流程,主设备开始逐个分配地址
// ...(后续DAA逻辑)
}
void i3c_handle_address_conflict(void) {
// 发现冲突 -> 先发RSTDDA
i3c_send_ccc(RSTDDA);
delay_ms(1); // 等待从设备复位
// 重新执行完整的DAA流程
i3c_enter_daa_mode();
}
3.3.2 从设备配合:支持SETDASA和RSTDDA
作为从设备的开发者,你的固件必须正确响应这两个CCC:
- RSTDDA:清空动态地址寄存器,回到未分配状态。
- SETDASA:允许主设备强制将一个静态地址转为动态地址。
我记得有一次,供应商的从设备芯片根本不支持RSTDDA。这就尴尬了。主设备发完RSTDDA后,从设备还赖在原地不动。最后只能物理断电再上电——相当于给总线来个“硬复位”。从那以后,我每次选型都会确认从设备是否完整支持RSTDDA和SETDASA。
3.4 综合排查流程图
我把这些步骤总结成一张图。你调试的时候可以按这个流程走,少走弯路。
3.5 完整异常处理代码示例
最后,我给大家一个实战中完整的异常处理代码片段。它综合了前面的检测和处理策略。
/* i3c_address_conflict_handler.c */
/* 实际项目中直接用这个函数解决地址冲突 */
void i3c_detect_and_resolve_conflict(void) {
uint8_t da_test;
uint8_t response;
// 第一步:遍历所有已分配地址,发送GET_SETDASA验证唯一性
for (int i = 0; i < pool_size; i++) {
da_test = assigned_da_pool[i];
// 发送SDR CCC:GET_SETDASA (0x0C)
i3c_start();
i3c_send_byte((da_test << 1) | 0); // 写入,目标DA
if (i3c_get_ack() == I3C_ACK) {
i3c_send_byte(0x0C); // GET_SETDASA
i3c_get_ack();
response = i3c_read_byte(1); // 读返回的静态地址高字节
i3c_send_nack();
i3c_stop();
// 检查返回的静态地址是否有重复
if (check_static_addr_duplicate(response)) {
// 发现冲突!
goto conflict_found;
}
} else {
// 地址无人应答,可能之前的分配有问题
// 记录下来,后面重新分配
}
}
// 没有发现冲突,正常返回
return;
conflict_found:
// 第二步:先发RSTDDA重置所有从设备
i3c_start();
i3c_send_byte(0x7E << 1);
i3c_get_ack();
i3c_send_byte(0x20); // RSTDDA CCC码(示例值,实际请查协议)
i3c_get_ack();
i3c_stop();
// 等从设备反应
delay_ms(2);
// 第三步:重新执行DAA
i3c_enter_daa_mode();
// 第四步:再次验证
i3c_detect_and_resolve_conflict();
}