2、PCIe体系结构:分层架构与核心协议
好,咱们今天来聊聊PCIe最核心的东西——它的分层架构。说实话,我刚接触PCIe那会儿,看着那一堆层啊、包啊的,头都大了。但后来我发现,只要把这三层的关系理清楚,后面写驱动、调bug就会顺很多。
PCIe采用了一种分层设计,从上到下分别是:事务层(Transaction Layer)、数据链路层(Data Link Layer)和物理层(Physical Layer)。每一层各司其职,上层只管发命令,下层只管传数据。这种设计的好处是——你写驱动时,大部分时间只需要跟事务层打交道。
核心观点:PCIe的分层架构,本质上是一种"关注点分离"的设计哲学。事务层负责"做什么",数据链路层负责"怎么传得可靠",物理层负责"怎么传得过去"。
2.1 分层架构总览
我们先看一张整体结构图,把这三层的关系搞清楚。
这张图我建议你多看两眼。每次调PCIe驱动时,脑子里要有这个分层模型。我曾经遇到过一个bug,TLP死活发不出去,查了半天发现是数据链路层的流控机制卡住了——这就是没把分层关系搞清楚的教训。
2.2 事务层与TLP包格式
事务层是离软件最近的一层。说白了,你的驱动发一个读请求,事务层就把它封装成一个TLP(Transaction Layer Packet),然后往下扔。
TLP的格式,我习惯把它分成三部分:头部(Header)、数据负载(Data Payload)和摘要(Digest,可选)。其中头部是必须的,数据负载看情况。
| 字段 | 长度 | 说明 |
|---|---|---|
| Fmt + Type | 1 DW(4字节) | 包格式与类型:读、写、完成、消息等 |
| TC / Attr / TH / TD / EP / Attr2 / AT | 1 DW | 流量类别、属性、地址类型等控制信息 |
| Length | 1 DW | 数据负载长度(以DW为单位) |
| Requester ID / Tag / Last DW BE / 1st DW BE | 1 DW | 请求者标识、标签、字节使能 |
| Address / 其他 | 1~2 DW | 目标地址(32位或64位) |
| Data Payload | 0~1024 DW | 实际读写数据(可选) |
| ECRC(可选) | 1 DW | 端到端CRC校验 |
个人经验:调试时,我经常用逻辑分析仪抓TLP。重点关注Fmt+Type字段和Length字段。有一次我发现Length字段写成了0,结果对端直接返回了一个"不支持的请求"完成包。嗯,这种低级错误,犯过一次就不会再犯了。
TLP的类型主要有这么几类:
- Memory Read/Write:最常用的,读写内存空间
- IO Read/Write:访问IO空间,新设备基本不用了
- Configuration Read/Write:配置空间访问,枚举设备时用
- Completion:完成包,读请求的响应
- Message:消息包,中断、错误通知等
你想想看,写驱动时,你调用的readl()、writel()最终都会变成Memory Read TLP和Memory Write TLP。所以理解TLP结构,就是理解你的代码到底在总线上干了什么。
2.3 数据链路层与DLLP包格式
数据链路层夹在中间,干的是"脏活累活"。它负责保证TLP能可靠地传到对端。怎么保证?靠DLLP(Data Link Layer Packet)。
DLLP和TLP不一样。DLLP是数据链路层自己生成的,软件看不到。它的主要作用有三个:
- ACK/NAK:告诉对端"我收到了"或"我没收到,重传"
- 流控(Flow Control):告诉对端"我还能收多少"
- 电源管理:链路状态切换的通知
DLLP的格式比TLP简单得多:
| 字段 | 长度 | 说明 |
|---|---|---|
| SOP / EOP | 1字节 | 包起始/结束定界符 |
| Sequence Number | 2字节 | TLP序列号,用于重传和去重 |
| TLP数据 | 可变 | 来自事务层的TLP |
| LCRC | 4字节 | 链路层CRC,校验TLP完整性 |
注意:DLLP的ACK/NAK机制是PCIe可靠传输的基石。我曾经在一个项目中,发现链路偶尔丢包,但驱动层面没做重传——因为PCIe硬件自己会重传。但如果你把数据链路层的流控窗口调得太小,性能会急剧下降。这个坑,我踩过。
2.4 流控机制
流控,说白了就是"你发慢点,我快撑不住了"。PCIe的流控是基于信用(Credit)的,不是基于停等协议的。
具体怎么工作的?我给你捋一捋:
- 每个设备在初始化时,告诉对端自己有多少个接收缓冲区(也就是信用)
- 发送方每发一个TLP,就消耗一个信用
- 接收方处理完TLP后,通过DLLP把信用还回来
- 发送方如果信用用完了,就必须等,不能继续发
流控分为两类:
- Posted(发布型):写请求、消息。不需要响应,发出去就不管了
- Non-Posted(非发布型):读请求。需要等待完成包回来
这两类流控的信用池是分开的。为什么?你想想看,如果读请求和写请求共用信用,写请求把信用全占了,读请求就发不出去了——那整个系统就死锁了。
避坑指南:我曾经调试过一个NVMe SSD的驱动,发现顺序写性能上不去。抓包一看,Posted信用经常为0,发送方在等信用。后来调整了驱动中的请求合并策略,减少了TLP数量,性能就上来了。所以,流控不只是硬件的事,驱动也要配合。
2.5 小结
这一章我们聊了PCIe的分层架构、TLP和DLLP的包格式,还有流控机制。记住几个关键点:
- 事务层生成TLP,数据链路层保证TLP可靠传输,物理层负责比特流
- TLP头部是关键,调试时多看看Fmt+Type和Length
- 流控是基于信用的,Posted和Non-Posted分开管理
- 驱动开发主要跟事务层打交道,但理解下层机制能帮你快速定位问题
嗯,这些概念看起来有点抽象,但等你真正开始写驱动、调bug时,就会体会到它们的重要性了。