1. PCIe基础回顾:从拓扑到信号
做PCIe驱动,说白了就是跟三样东西打交道:设备怎么连的、系统怎么找到它的、数据怎么传的。这三样搞不清楚,后面调DMA肯定踩坑。我见过不少同事,上来就啃DMA寄存器,结果连配置空间都没读对,折腾了两天才发现是链路没训练好。
所以咱别急,先把地基打牢。这一章我带你把PCIe的五大核心模块过一遍——链路拓扑、配置空间、事务层、数据链路层、物理层。嗯,都是干货,也是后面所有故障排查的出发点。
1.1 链路拓扑——谁和谁连在一起
PCIe的拓扑结构,说白了就是一棵树。树根是RC(Root Complex),树干是Switch,树叶就是各种Endpoint。
- RC(Root Complex)—— 系统的“大脑”,连接CPU和内存,所有事务的源头和终点。我习惯把它理解成“PCIe世界里的内存代理”。
- Switch—— 相当于一个智能交换机,把来自RC的一条链路扩展成多条。它不产生事务,只负责转发。
- Endpoint(EP)—— 真正的功能设备。GPU、NVMe SSD、网卡,都是Endpoint。
你想想看,传统PCI是共享总线,所有设备挤在同一条线上说话。PCIe改成了点对点串行连接,每个设备都有自己的专用通道,速度和并发能力一下就上去了。
我在项目中遇到过一个问题:一块FPGA加速卡在RC直连的槽位上工作正常,但插到Switch下面就不认了。排查了半天,发现是Switch的端口虚拟通道配置不对。所以,拓扑结构不只是画图好看,它直接影响你的驱动怎么枚举、怎么发中断。
核心要点: PCIe拓扑是树形,RC是根,Switch是中间节点,Endpoint是叶子。每个链路都是点对点的串行通道。
1.2 配置空间——设备的身分证
每个PCIe设备都有配置空间,这是固件和驱动识别设备的唯一依据。没有它,系统连设备叫什么名字都不知道,更别提驱动加载了。
标准配置空间共256字节(0x00–0xFF),PCIe还扩展到了4KB(0x000–0xFFF)。但驱动最常用的还是前256字节,也就是PCI兼容区域。
| 偏移地址 | 寄存器名称 | 作用 |
|---|---|---|
| 0x00 – 0x01 | Vendor ID | 厂商编号(如 NVIDIA 是 0x10DE) |
| 0x02 – 0x03 | Device ID | 设备型号编号 |
| 0x04 – 0x05 | Command | 控制设备响应(I/O、Memory、Bus Master等) |
| 0x06 – 0x07 | Status | 设备状态(是否有能力、是否中断挂起等) |
| 0x10 – 0x27 | BAR (0–5) | 基地址寄存器,定义设备占用的地址空间 |
| 0x34 | Capabilities Pointer | 指向PCIe能力结构链的起点 |
| 0x3C | Interrupt Line | 中断号(由BIOS/固件填写) |
调试小技巧: 我每次拿到一块新板子,第一件事就是读Vendor ID和Device ID。如果读出来全是0xFF或者0x00,大概率是链路没训练好,或者设备没上电。别急着查驱动,先拿示波器看物理层信号。
读配置空间的方法很简单——通过I/O端口 CF8/CFC(x86平台)或者MMIO方式(Memory Mapped Configuration Space,简称MCFG)。现代系统基本都用MMIO了,效率高很多。
// 伪代码:通过MMIO读取配置空间(x86为例)
uint32_t pcie_read_config(uint8_t bus, uint8_t dev, uint8_t func, uint16_t offset) {
// 计算MMIO基地址(由BIOS在系统启动时设置)
uint64_t mmio_base = get_mcfg_base();
uint64_t addr = mmio_base + (bus << 20) | (dev << 15) | (func << 12) | offset;
return *(volatile uint32_t *)addr;
}
嗯,这里要注意:BAR寄存器是驱动和硬件沟通的桥梁。我遇到过好几次,设备枚举正常,但驱动加载后无法访问设备内存,最后发现是BAR解析错了——64位BAR占了两个槽位,驱动只读了一个。
1.3 事务层——TLP的"快递"规则
事务层是整个PCIe协议的灵魂。它负责把CPU或驱动的读写请求,打包成TLP(Transaction Layer Packet),然后往下扔给数据链路层。
TLP的格式很简单:报头(Header) + 数据负载(Data Payload,可选) + CRC(可选)。报头里包含了事务类型、地址、长度、标签等信息。
PCIe定义了四种地址空间,每种空间对应不同的事务类型:
| 地址空间 | 事务类型 | 典型用途 |
|---|---|---|
| Memory(内存空间) | MRd, MWr | DMA读写、寄存器访问(最常用) |
| I/O(IO空间) | IORd, IOWr | 传统设备兼容,新设备已很少使用 |
| Configuration(配置空间) | CfgRd, CfgWr | 枚举和配置设备 |
| Message(消息空间) | Msg | 中断(MSI/MSI-X)、错误报告、电源管理等 |
做DMA驱动,你打交道最多的就是Memory Write(MWr)和Memory Read(MRd)。说白了,DMA就是设备自己发起MWr把数据写进内存,或者发MRd从内存读数据。
踩坑警示: 我曾经遇到过一块DMA卡,在Gen2速率下读写都正常,切换到Gen3就开始丢数据。最后定位到是事务层Max Payload Size(MPS)不匹配。发送端和接收端的MPS必须一致,不然TLP会被当成错误包扔掉。切记,驱动初始化时一定要协商好MPS。
1.4 数据链路层——丢了包怎么办
数据链路层夹在事务层和物理层中间,干的是"快递员"的活。它给每个TLP加上序列号(Sequence Number)和CRC校验码,然后发出去。接收端收到后,会回复一个Ack或Nak。
- Ack —— 收到正确,确认。
- Nak —— 收到有误,请求重传。
如果发送端收到Nak,或者在一定时间内没收到Ack,就会自动重传。这个机制叫ACK/NAK协议,是PCIe可靠传输的根基。
我记得有一次调一个高速数据采集卡,发现DMA传输偶尔会卡死。读链路状态寄存器,发现Data Link Layer Protocol(DLLP)错误计数一直在涨。后来换了更高质量的线缆,问题就消失了。嗯,物理层的问题,有时候会在数据链路层暴露出来。
排查思路: 如果你发现DMA传输超时或数据错乱,先看数据链路层的错误计数器。如果错误计数持续增加,大概率是信号完整性或链路稳定性的问题,别急着改驱动逻辑。
1.5 物理层——信号怎么跑
物理层是PCIe的"路基",负责把比特流从A点搬到B点。它用的是差分信号对——Tx+ / Tx- 和 Rx+ / Rx-,一对线发,一对线收,全双工工作。
PCIe的速率代代升级,向下兼容:
| 代次 | 单通道速率 |
|---|