3. I3C数据帧格式:起始条件、地址、读写位、数据字节、ACK/NACK
搞I3C的地址分配,你要是连帧格式都吃不准,那DMA描述符根本没法配。说白了,总线上的每一笔交易,都是靠这个帧结构来定义的。我刚开始接触I3C时,总觉得帧格式跟I2C差不多嘛,结果在地址分配环节踩了个大坑——后面再细说。
一个完整的I3C数据帧,包含以下7个要素:起始条件、7位地址、读写位、ACK、数据字节、ACK/NACK、停止条件。DMA要高效介入,就得在每个环节都清楚谁拉高谁拉低,什么时候触发传输完成。
3.1 起始条件(START)
起始条件长什么样?SCL在高电平时,SDA从高切换到低。就这么一个下降沿,宣告一笔交易的开始。你想想看,总线原本是空闲的,SDA和SCL都被上拉电阻拉高,主机突然拉低SDA——所有从机都得竖起耳朵听。
I3C的起始条件和I2C基本一致,但有一个关键区别:I3C允许"重复起始"(Repeated START),也就是不发送停止条件就再发一次起始。在地址分配场景中,这个特性非常有用。我建议你在DMA描述符里把重复起始当作一个独立的传输段来处理,不要跟前面的数据混在一起。
时序参数方面,I3C要求START保持时间(tHD;STA)最小为0ns(是的,0!),这比I2C宽松多了。但SCL低电平时间有严格要求,一般是100ns到400ns不等,取决于总线模式。
3.2 地址与读写位
起始条件之后,主机发送7位地址 + 1位读写位,总共8位,MSB优先。I3C的动态地址是7位的,范围从0x00到0x7F,但0x7E和0x7F被保留用作特殊用途。
| 地址值 | 用途 | 说明 |
|---|---|---|
| 0x00 – 0x7D | 动态地址 | 主机通过CCC命令分配 |
| 0x7E | 广播地址 | 所有从机必须应答 |
| 0x7F | 虚拟目标地址 | 用于I3C到I2C的桥接 |
读写位跟在地址后面。0表示主机要写数据到从机,1表示主机要从从机读数据。在地址分配场景中,主机先发送广播地址0x7E + 写位,然后发送CCC命令码(比如ENTDAA),接着从机才会回应自己的地址。
我记得第一次调地址分配时,读写位搞反了。我以为主机要读从机的地址,配了个读操作,结果从机根本不搭理我。后来才意识到,地址分配时主机是先写CCC命令,从机再在数据阶段返回地址。这个流程必须用两次不同的传输段来完成。
3.3 数据字节传输
地址发完之后,就是数据字节。每个数据字节8位,同样MSB优先。主机或从机在SCL的驱动下,逐位把数据送到SDA线上。
这里有个重要的细节:数据字节的传输方向取决于之前的读写位。如果是写操作,主机驱动SDA;如果是读操作,从机驱动SDA。DMA设计时,必须根据方向切换数据流向控制。
// I3C数据帧的C结构体表示
typedef struct {
uint8_t start_condition; // 1: START, 0: no
uint8_t address; // 7-bit dynamic address
uint8_t rw; // 0: write, 1: read
uint8_t ack; // ACK from receiver
uint8_t data[MAX_DATA_LEN]; // data bytes
uint16_t data_len; // number of data bytes
uint8_t ack_final; // ACK/NACK for last byte
uint8_t stop_condition; // 1: STOP, 0: no
} i3c_frame_t;
3.4 ACK/NACK应答机制
每个字节(包括地址字节和数据字节)之后,接收方需要在第9个SCL时钟周期给出应答。SDA为低表示ACK,为高表示NACK。
在地址分配场景中,ACK/NACK有几个关键用途:
- 地址阶段:从机收到自己的动态地址后,必须回复ACK。如果地址不匹配,从机必须保持SDA为高(NACK),让主机知道这个地址没人认领。
- 数据阶段:接收方每收一个字节都要回复ACK。如果接收方想终止传输,可以在某个字节后回复NACK,主机收到NACK后通常会发起停止条件。
- 地址分配的特殊流程:在ENTDAA过程中,从机在数据阶段返回自己的地址时,如果多个从机同时响应,会产生总线冲突。I3C通过"仲裁丢失"机制来处理——从机在发送地址时监听SDA,如果发现自己发送的位与总线状态不符,就退出竞争并回复NACK。
我曾经踩过一个坑:DMA在接收从机返回的地址时,把ACK位也当作数据收进来了。结果地址值直接错了一位。解决方案是在DMA描述符里把ACK位单独列出来,不参与数据校验。
3.5 完整帧格式与DMA协同
来,我们把所有东西串起来。一个完整的I3C数据传输帧,从START到STOP,长这样:
| START | 7-bit ADDR | R/W | ACK | 8-bit DATA | ACK | ... | 8-bit DATA | ACK/NACK | STOP |
在地址分配场景中,典型的帧序列是:
- 主机发送:START + 广播地址0x7E + 写位 + ACK
- 主机发送:CCC命令码(如ENTDAA 0x20)+ ACK
- 从机回复:从机发送自己的动态地址 + 主机回复ACK
- 主机发送:STOP
你看,三次独立的帧传输,才完成一次地址分配。DMA要做的就是在这三个帧之间无缝切换,不浪费CPU一个时钟周期。
我个人习惯把地址分配流程拆成3个DMA描述符链:
- 描述符1:写广播地址 + CCC命令。方向:内存→I3C TX FIFO。长度:2字节(地址+命令)。
- 描述符2:读从机地址。方向:I3C RX FIFO→内存。长度:1字节。注意这个描述符要等描述符1完成后自动触发。
- 描述符3:发送STOP条件。没有数据,只配一个命令。
这样配下来,CPU只在三个描述符全部完成时收到一次中断,效率非常高。
下面这张图展示了一个完整I3C数据帧的结构,以及每个字段在地址分配中的角色:
最后,用一个表格总结一下I3C帧格式的每个字段,方便你对照着配DMA描述符:
| 字段 | 位宽 | 驱动方 | DMA注意事项 |
|---|---|---|---|
| START | 1位(时序) | 主机 | DMA不参与,由I3C控制器硬件生成 |
| 地址 | 7位 | 主机 | 放入TX描述符,与R/W位合并为一个字节 |
| R/W# | 1位 | 主机 | 决定DMA方向:0→TX,1→RX |
| ACK | 1位 | 接收方 | DMA计数必须包含此位,用9位模式 |
| 数据字节 | 8位 | 主机/从机 | 根据R/W#方向,放入TX或RX描述符 |
| ACK/NACK | 1位 | 接收方 | NACK作为传输结束信号,触发DMA完成中断 |
| STOP | 1位(时序) | 主机 | 由I3C控制器硬件生成,或通过命令描述符触发 |
搞清楚了帧格式,你才知道DMA描述符该怎么配、传输长度怎么算、方向怎么切。下一层我们要面对的是真正的地址分配协议——ENTDAA命令的完整流程。到时候你会发现,帧格式里每一个ACK/NACK都在关键时刻决定着分配的成败。