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总线进入未知状态
警告: 有次调试一个AI相机项目,我发现从设备偶尔回复乱码。示波器一抓,ACK窗口的SDA信号是“中间电平”——既不是0也不是1。两个从设备一个拉高、一个拉低,信号互相抵消了。这就是最典型的地址冲突物理表现。

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;
}
小技巧: 我建议你在分配表中额外记录从设备的 Provisional ID,就算地址冲突了,也能通过PID反查到底是哪个设备出了问题。

3.2.2 协议层:利用SDR CCC进行主动探测

软件层的预检不能完全避免冲突,因为有些冲突是硬件时序导致的。这时候需要用协议层的办法。

主设备可以在DAA完成后,发送一个 GET_XXXX 类的SDR CCC(比如 GET_ACCCR 或 GET_SETDASA),带上目标动态地址。如果多个设备响应同一个地址,它们的应答会冲突。

我怎么检测这个冲突?

  1. 主设备发送SDR CCC,目标地址为目标DA。
  2. 在ACK阶段,拉低SDA表示应答。
  3. 如果多个设备同时拉低,SDA电平正常(低)——但这种情况下,后续数据阶段就会出问题。
  4. 更有效的办法:发送GET_SETDASA命令。这个命令会让从设备返回自己的静态地址。如果总线上一堆0x00、0xFF或者乱码,基本可以断定地址冲突了。

我曾经用这个办法查过一个智能家居网关。DAA看起来跑完了,但每次读取传感器数据时,温度值偶尔会变成湿度值。发送GET_SETDASA之后,两个传感器居然返回了相同的静态地址类型——嗯,它们的Provisional ID虽然不同,但动态地址冲突了。

3.2.3 物理层:示波器抓取ACK行为

如果软件层和协议层都没发现问题,那就得上示波器了。我一般会看两个关键点:

  • ACK/NACK窗口的SDA电平:正常ACK窗口SDA被拉低;如果SDA在窗口内呈现“中间电平”或随机跳变,那肯定有冲突。
  • SCL时钟后的数据字节:地址冲突后,数据阶段会收到两个设备混合输出的字节。比如一个设备想发0xA5,另一个想发0x5A,主设备读到的可能是乱码。
经验之谈: 示波器抓这种信号,我建议用“正常触发模式”,触发电平设在0.8V左右,触发边沿选SCL的下降沿。然后观察ACK窗口(第9个SCL)上的SDA状态。一把就能抓到证据。

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();
}
注意: 有些从设备的设计比较糟糕,收到RSTDDA后不会立即清空动态地址。我建议在RSTDDA之后加一点延时(1~2ms),然后发送一个“虚拟访问”来验证所有设备是否真的回归了初始状态。如果某个设备还在响应旧地址,强制给它发SETDASA重置。

3.3.2 从设备配合:支持SETDASA和RSTDDA

作为从设备的开发者,你的固件必须正确响应这两个CCC:

  • RSTDDA:清空动态地址寄存器,回到未分配状态。
  • SETDASA:允许主设备强制将一个静态地址转为动态地址。

我记得有一次,供应商的从设备芯片根本不支持RSTDDA。这就尴尬了。主设备发完RSTDDA后,从设备还赖在原地不动。最后只能物理断电再上电——相当于给总线来个“硬复位”。从那以后,我每次选型都会确认从设备是否完整支持RSTDDA和SETDASA。

3.4 综合排查流程图

我把这些步骤总结成一张图。你调试的时候可以按这个流程走,少走弯路。

从设备地址冲突检测与处理流程 I3C从设备地址冲突检测与处理流程图 1. 地址冲突被报告(应答异常/数据乱码) 2. 主设备检查DA分配表,发现重复 3. 发送GET_SETDASA等CCC,验证多个设备响应同一地址 4. 示波器抓取ACK窗口,确认冲突电平 5. 主设备发起RSTDDA,重新执行DAA 6. 验证每个从设备是否有唯一动态地址 避坑提醒 支持RSTDDA / SETDASA是从设备 选型时的基本要求。

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();
}
再次强调: 地址冲突不可怕,可怕的是没有检测机制。我建议在你的主设备固件中加入一个定时任务,每隔几秒或者每次读写前做一次快速地址唯一性检查。开销不大,但能提前发现隐患。