I3C通信流程:动态地址分配与公共命令代码(CCC)
好,这一章咱们来聊聊I3C通信里两个最核心的实操环节——动态地址分配和公共命令代码(CCC)。说白了,这俩东西就是I3C总线能灵活组网、高效通信的「地基」。
先问个问题。你想想看,一条I2C总线上挂一堆从设备,最烦人的是什么?地址冲突,对吧?要么靠硬件引脚硬编码,要么厂商预置地址就那么几个,稍微复杂点的系统就打架。I3C把这事彻底改了。
核心思想:I3C主设备在初始化时,会给每个连接的从设备动态分配一个唯一的7位地址。没有固定地址,就没有冲突。
动态地址分配(Dynamic Address Assignment, DAA)
我个人习惯把DAA理解成「开班报到」。主设备在总线上广播:所有新设备,听令,报数!每个从设备会用自己的48位临时ID(或者更长的动态ID)来响应。主设备收到后,按顺序给它们发一个7位地址。
具体流程大概是这样:
- 广播进入DAA模式:主设备发送
ENTDAA公共命令(0x01)。 - 从设备仲裁:所有未分配地址的设备开始逐位发送临时ID。位与位之间,如果某个从设备发0,另一个发1,那么发0的胜出(I3C支持开漏仲裁,这里和I2C有点像)。
- 主设备确认胜出者:仲裁胜出的从设备,主设备会分配一个地址。然后主设备再继续,直到所有设备都分到地址。
- 特殊地址:动态地址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子系统就初始化完成了。
下面这张图把整个流程串起来了:
个人实战建议:在写固件实现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命令的语义和总线上的时序对应清楚。