I3C通信流程:动态地址分配与公共命令代码(CCC)

好,这一章咱们来聊聊I3C通信里两个最核心的实操环节——动态地址分配和公共命令代码(CCC)。说白了,这俩东西就是I3C总线能灵活组网、高效通信的「地基」。

先问个问题。你想想看,一条I2C总线上挂一堆从设备,最烦人的是什么?地址冲突,对吧?要么靠硬件引脚硬编码,要么厂商预置地址就那么几个,稍微复杂点的系统就打架。I3C把这事彻底改了。

核心思想:I3C主设备在初始化时,会给每个连接的从设备动态分配一个唯一的7位地址。没有固定地址,就没有冲突。

动态地址分配(Dynamic Address Assignment, DAA)

我个人习惯把DAA理解成「开班报到」。主设备在总线上广播:所有新设备,听令,报数!每个从设备会用自己的48位临时ID(或者更长的动态ID)来响应。主设备收到后,按顺序给它们发一个7位地址。

具体流程大概是这样:

  1. 广播进入DAA模式:主设备发送 ENTDAA 公共命令(0x01)。
  2. 从设备仲裁:所有未分配地址的设备开始逐位发送临时ID。位与位之间,如果某个从设备发0,另一个发1,那么发0的胜出(I3C支持开漏仲裁,这里和I2C有点像)。
  3. 主设备确认胜出者:仲裁胜出的从设备,主设备会分配一个地址。然后主设备再继续,直到所有设备都分到地址。
  4. 特殊地址:动态地址0x7E:所有未分配地址的从设备,默认监听这个地址。主设备向0x7E发DAA命令,所有没地址的设备都必须响应。

小经验:我在项目中遇到过,有些从设备的临时ID是随机生成的。如果刚好两个设备生成的ID一样,那怎么办?虽然概率极低,但I3C标准提供了用 SETNEWDA 命令更换地址的机制。嗯,设计系统时你得留好这个后门。

公共命令代码(CCC)归纳总结

CCC是I3C协议里最灵活的部分。你可以把它看成主设备给所有从设备(或者一组从设备)下的「全局指令」。不需要每次通信都走完整的读/写流程,效率高很多。

CCC分为两大类:

  • 直接命令(Direct CCC):只发给某个特定从设备。格式是:命令 + 目标地址 + 数据。
  • 广播命令(Broadcast CCC):发给总线上的所有从设备。格式是:命令 + 0x7E(广播地址) + 数据。

我整理了一下最常用的CCC命令,做成表格你一眼就看清:

CCC命令 方向 类型 十六进制值 功能描述
ENEC 主→从 广播 0x00 使能所有从设备的事件检测
DISEC 主→从 广播 0x01 禁用所有从设备的事件检测
ENTDAA 主→从 广播 0x02 进入动态地址分配模式
SETDASA 主→从 直接 0x03 为指定从设备分配静态地址
RSTDAA 主→从 广播 0x04 重置所有从设备的动态地址
GETPID 主→从 直接 0x10 读取从设备的48位产品ID
GETBCR 主→从 直接 0x11 读取从设备的总线特性寄存器
GETDCR 主→从 直接 0x12 读取从设备的设备特性寄存器
GETSTATUS 主→从 直接 0x14 查询从设备的故障/状态信息

避坑提醒:我曾经写错过 ENEC 和 DISEC 的ATTR字段(属性字节)。记住,ATTR的bit[1:0]定义:00表示广播,01表示直接。你发广播命令时如果ATTR写错了,从设备可能根本不响应你。

你可能会问,这些CCC命令在实际中怎么用?举个例子。

假设你做的是一个传感器集线器。上电后,主设备先发 RSTDAA 重置所有地址,再发 ENTDAA 进入DAA流程,给每个传感器分配地址。接着用 GETPID 确认每个传感器是什么型号,用 GETBCR 和 GETDCR 获取它们的能力(是否支持IBI中断、是I3C还是I2C设备等)。最后,用 ENEC 使能所有传感器的事件检测。这样,一个完整的I3C子系统就初始化完成了。

下面这张图把整个流程串起来了:

I3C通信流程:动态地址分配与CCC分类 I3C主设备 CCC广播命令(RSTDAA / ENTDAA) 动态地址分配(DAA):逐位仲裁 + 分配地址 设备识别(GETPID / GETBCR / GETDCR) 使能事件(ENEC):开启IBI / 中断检测 CCC命令分类 • 广播(Broadcast): 0x7E作为目标,所有从设备 必须监听(如 ENTDAA)。 • 直接(Direct): 指定从设备地址,一对一。 ATTR字段区分方向与数据。 避坑要点 • 地址0x7E是特殊广播地址, 不要在DAA之外使用。 • 动态地址分配后,一定要 调用GETDCR确认设备类型 (比如是I3C还是I2C桥接)。 • 我自己踩过坑:位仲裁时 如果SDA和SCL时序不精确, 胜出者判断会出错。

个人实战建议:在写固件实现DAA时,不要把所有CCC命令都堆在一个大函数里。我通常把它们拆成几个独立的小模块:发送模块(只负责组包、发数据)、仲裁模块(处理位仲裁逻辑)、分配模块(管理地址池)。这样调试起来,3分钟就能定位问题。

嗯,还有一点。CCC命令其实有两种数据格式:

  • 无数据命令:比如 RSTDAA,只发命令字节,后面没有数据。
  • 带数据命令:比如 SETDASA,发命令字节后,还要跟一个字节的静态地址数据。

你在写驱动时,注意区分这两种情况。否则多发了数据,从设备就懵了。

最后分享个代码片段。这是我常用的一个CCC命令解释器入口:

def send_ccc_command(bus, command, target_addr=0x7E, data=None):
    """
    发送CCC命令。
    :param bus: I3C总线实例
    :param command: CCC命令字节(如 0x02 表示 ENTDAA)
    :param target_addr: 0x7E为广播,其他为直接
    :param data: 数据字节列表,如果无数据则为None
    """
    if target_addr == 0x7E:
        # 广播命令:先发目标地址0x7E,再发命令
        bus.start()
        bus.write_byte(0x7E)   # 广播地址
        bus.write_byte(command)
        if data:
            for byte in data:
                bus.write_byte(byte)
        bus.stop()
        print(f"广播命令 {hex(command)} 发送成功")
    else:
        # 直接命令:先发目标地址,再发命令
        bus.start()
        bus.write_byte(target_addr)
        bus.write_byte(command)
        if data:
            for byte in data:
                bus.write_byte(byte)
        bus.stop()
        print(f"直接命令 {hex(command)} 发送给 {hex(target_addr)}")

你看,结构其实不复杂。关键是把CCC命令的语义和总线上的时序对应清楚。