2、PCIe体系结构:分层架构与核心协议

好,咱们今天来聊聊PCIe最核心的东西——它的分层架构。说实话,我刚接触PCIe那会儿,看着那一堆层啊、包啊的,头都大了。但后来我发现,只要把这三层的关系理清楚,后面写驱动、调bug就会顺很多。

PCIe采用了一种分层设计,从上到下分别是:事务层(Transaction Layer)、数据链路层(Data Link Layer)和物理层(Physical Layer)。每一层各司其职,上层只管发命令,下层只管传数据。这种设计的好处是——你写驱动时,大部分时间只需要跟事务层打交道。

核心观点:PCIe的分层架构,本质上是一种"关注点分离"的设计哲学。事务层负责"做什么",数据链路层负责"怎么传得可靠",物理层负责"怎么传得过去"。

2.1 分层架构总览

我们先看一张整体结构图,把这三层的关系搞清楚。

PCIe 分层架构与数据流 事务层(Transaction Layer) 生成/解析 TLP(事务层包) 负责:读写请求、完成、消息 数据链路层(Data Link Layer) 生成/解析 DLLP(数据链路层包) 负责:CRC校验、重传、流控 物理层(Physical Layer) 编码/解码、串并转换、电气特性 负责:位传输、链路训练、速率协商 TLP 下行 TLP 上行 数据流:软件 → 事务层(TLP) → 数据链路层(加DLLP) → 物理层(串行化) → 对端 接收方向则相反:物理层 → 数据链路层(校验) → 事务层(解析TLP) → 软件

这张图我建议你多看两眼。每次调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)的,不是基于停等协议的。

具体怎么工作的?我给你捋一捋:

  1. 每个设备在初始化时,告诉对端自己有多少个接收缓冲区(也就是信用)
  2. 发送方每发一个TLP,就消耗一个信用
  3. 接收方处理完TLP后,通过DLLP把信用还回来
  4. 发送方如果信用用完了,就必须等,不能继续发

流控分为两类:

  • Posted(发布型):写请求、消息。不需要响应,发出去就不管了
  • Non-Posted(非发布型):读请求。需要等待完成包回来

这两类流控的信用池是分开的。为什么?你想想看,如果读请求和写请求共用信用,写请求把信用全占了,读请求就发不出去了——那整个系统就死锁了。

避坑指南:我曾经调试过一个NVMe SSD的驱动,发现顺序写性能上不去。抓包一看,Posted信用经常为0,发送方在等信用。后来调整了驱动中的请求合并策略,减少了TLP数量,性能就上来了。所以,流控不只是硬件的事,驱动也要配合。

2.5 小结

这一章我们聊了PCIe的分层架构、TLP和DLLP的包格式,还有流控机制。记住几个关键点:

  • 事务层生成TLP,数据链路层保证TLP可靠传输,物理层负责比特流
  • TLP头部是关键,调试时多看看Fmt+Type和Length
  • 流控是基于信用的,Posted和Non-Posted分开管理
  • 驱动开发主要跟事务层打交道,但理解下层机制能帮你快速定位问题

嗯,这些概念看起来有点抽象,但等你真正开始写驱动、调bug时,就会体会到它们的重要性了。