第三讲:事务层基础——TLP包结构、类型与寻址
这一讲咱们进入PCIe最核心的部分——事务层。说白了,事务层就是决定“谁给谁发什么数据”的那一层。TLP(Transaction Layer Packet)就是咱们在总线上跑来跑去的消息信封。我个人习惯先把信封拆开看明白,再去理解整个通信流程。
TLP包长什么样?
每个TLP都有固定的头部格式。你想想看,就像快递单子上有发件人、收件人、物品类型、重量一样。TLP头部里最关键的几个字段我列在这里:
| 字段 | 宽度 | 含义 |
|---|---|---|
| Fmt | 2 bit | 头部格式:有无数据载荷、是否带地址 |
| Type | 5 bit | 事务类型:MRd、MWr、Cpl、CplD 等 |
| Length | 10 bit | 数据载荷的DW数(1 DW = 4 字节) |
| Requester ID | 16 bit | 发起者总线号+设备号+功能号 |
| Tag | 8 bit | 事务标签,用于匹配请求与完成 |
| Address/Address+Attr | 32或64 bit | 目标地址+属性(如PCIe存储域控制位) |
我自己记的一个小技巧: 先看Fmt和Type就能知道这是个什么包。比如Fmt=2'b00是没数据的请求,Fmt=2'b01是带数据的请求,Fmt=2'b10是完成包,等等。
四种基本事务类型:MRd、MWr、Cpl、CplD
PCIe的事务类型其实不少,但日常工作里80%的场景就是这四种。我在调设备驱动时几乎天天跟它们打交道。
- MRd(Memory Read):读存储器请求,不带数据载荷。发起者发出去,等着对方回数据。
- MWr(Memory Write):写存储器请求,带数据载荷。写出去后一般不期望回复。
- Cpl(Completion without Data):不带数据的完成包。比如读写完成但不需要回数据的情况,或者异常终止。
- CplD(Completion with Data):带数据的完成包。MRd的响应就靠它了。
嗯,这里要注意一点:MWr虽然不期望回复,但PCIe规范要求接收方必须在内部保证写入顺序(PCIe ordering规则)。我曾经在项目里遇到过接收端缓冲区不够深,MWr包被阻塞导致后续读请求超时……后来加了一级FIFO才解决。
地址与属性
TLP里的地址字段支持32位(传统PCI兼容)和64位(PCIe增强)。属性字段主要控制缓存一致性、no-snoop、relaxed ordering等。
| 属性位 | 名称 | 作用 |
|---|---|---|
| Attr[2] | No Snoop | 1表示不需要缓存一致性探测,适合高性能流式写 |
| Attr[1] | Relaxed Ordering | 1表示允许乱序完成,提升性能但需要软件谨慎使用 |
| Attr[0] | ID-Based Ordering | 用于区分不同Requester ID的事务 |
我曾经踩过的坑: 在某个DMA驱动里,我把No Snoop设成了1,结果CPU写了一遍数据后,DMA读到的是旧值。因为CPU的写缓存还没刷新到内存,而No Snoop跳过了缓存一致性的同步。后来我改成先做写屏障,或者干脆关掉No Snoop才稳定。
事务层整体流程(一张图看懂)
我总结的几个实用点
- MWr无回复,别死等: 你发完MWr,设备端可能因为buffer满而用Flow Control(FC)信用量拒绝,但不会给你回一个包。所以设计上要小心,我曾经在某个FPGA原型里,写寄存器后没等FC就下一笔,结果丢数据了。
- MRd的Tag不能重复: 每个待处理的读请求必须有一个独一无二的Tag,否则返回的CplD你分不清是谁的。我记得有一次代码里Tag循环复用太快,导致匹配错误,数据全乱套了。
- CplD的Length必须和MRd的Length一致: 如果读请求是4DW,那返回的CplD必须刚好4DW,不能多不能少,而且首地址要对应。
核心一句话: TLP就是PCIe通信的基本单元。你只要掌握了MRd/MWr/Cpl/CplD四种类型,加上地址和Tag的匹配规则,就已经能理解90%的事务层行为了。
一点课后小心得
我在做PCIe分析仪解码的时候,经常看到工程师盯着TLP头部的Hex值发呆。其实你只要把Fmt和Type拆出来,剩下的就是读地址、写地址、返回数据而已。别被那一长串16进制吓住,抓住这几个关键字段就通了。