第1章 PCIe总线概述:从并行到串行的蜕变

做嵌入式或FPGA开发的朋友,对PCIe应该不陌生。但要说清楚它到底怎么来的、跟老一代PCI有什么区别,很多人可能就含糊了。这一章,我们先把这个基础框架搭好。

先问一个问题:为什么当年PCI如日中天,还要搞出PCI Express?说白了——速度追不上了。PCI是并行总线,32根数据线一起跑,频率一高,信号歪斜(skew)就大到没法忍。我记得第一次调一个PCIe Gen2的链路时,差点被电气复杂度搞崩溃,后来才明白,非要从并行转串行不可。

1.1 PCIe发展历程:踩过的坑和爬过的坡

PCIe的历史不长,但迭代很快。2003年PCI-SIG发布1.0a规范,单通道2.5GT/s。那时候大家觉得挺快,其实现在看就是小水管。后来2.0翻倍到5GT/s,3.0把编码从8b/10b改成了128b/130b,效率大涨。4.0到16GT/s,5.0到32GT/s,6.0更是跳到了64GT/s,还用了PAM4。

我个人习惯把这几代分成两个阶段:编码时代(1.0/2.0)和效率时代(3.0及以后)。

版本发布年份单通道速率编码方式有效带宽
PCIe 1.x20032.5 GT/s8b/10b2.0 Gb/s
PCIe 2.x20075.0 GT/s8b/10b4.0 Gb/s
PCIe 3.x20108.0 GT/s128b/130b7.88 Gb/s
PCIe 4.x201716.0 GT/s128b/130b15.76 Gb/s
PCIe 5.x201932.0 GT/s128b/130b31.52 Gb/s
避坑指南:我曾经在项目里用Gen3的卡插在Gen2的槽上,以为能自动协商到Gen2最高速率,结果因为链路均衡没做对,一直降速到Gen1才稳定。所以,速率协商不是简单的“向下兼容”四个字能概括的,电平参数、相位裕量都得考虑。

1.2 PCIe架构层次:三层结构别搞混

PCIe的协议栈分为三层:事务层、数据链路层、物理层。你想想看,跟网络OSI模型有点像,但更精简。

  • 事务层(Transaction Layer):负责生成和解析TLP(事务层包)。比如你要读一个设备的配置空间,事务层就组装一个Memory Read Request TLP往下送。
  • 数据链路层(Data Link Layer):在TLP外面加序列号、CRC校验,还有ACK/NAK重传机制。说白了,保证数据不丢、不错。
  • 物理层(Physical Layer):最底层,含电气子层和逻辑子层。并串转换、时钟恢复、差分信号的发送接收都在这里。

很多工程师只关心事务层,因为驱动开发主要跟TLP打交道。但调试高速信号时,数据链路层的错误计数、物理层的眼睛图,才是关键。我记得有一次查链路不稳定,查了半天TLP重传,最后发现是物理层的高频去耦电容没焊好。

三层协作流程:上层应用发出请求 → 事务层封装成TLP → 数据链路层加序列号和LCRC → 物理层进行并串转换发送。接收方按相反顺序剥离层次。
PCIe 三层协议栈架构图 PCIe 协议栈三层架构 事务层 (Transaction Layer) 生成/解析 TLP • 处理订购与流量控制 数据链路层 (Data Link Layer) DLLP • 序列号+CRC • ACK/NAK重传 物理层 (Physical Layer) MIPI / 并串转换 • 差分信号 • 链路训练 数据流向 注意:每一层都会在包头上添加或剥离自己的前缀/校验信息,不能跳过任意一层

1.3 与PCI/PCI-X的差异:从并行到点对点

PCI和PCI-X是老前辈了。PCI是33/66MHz的并行共享总线,所有设备挂在同一组信号线上。你想想看,一个设备占用总线,其他都得等着。PCI-X提高了频率到133/266MHz,但本质还是并行,skew和负载效应是硬伤。

PCIe完全不同:点对点串行差分链路,每个设备有自己的独立通道(Lane),全双工同时收发。而且支持多通道绑定(x1, x4, x8, x16),带宽线性叠加。

核心差异一览:
特性PCI / PCI-XPCIe
总线拓扑共享并行总线点对点交换架构
信号方式单端,多地线差分对(Tx+, Tx- / Rx+, Rx-)
时钟同步公共时钟内嵌时钟(CDR恢复)
带宽扩展提高频率很难增加通道数或提高速率
热插拔软件支持差原生硬件支持
配置空间256字节4096字节(增强型)

嗯,这里要注意:PCIe的配置空间扩展到了4KB,支持更多的Capability结构,比如电源管理、MSI中断等。你在写驱动时要遍历这些Capabilities,不能用PCI的老方法。

我曾经踩过的坑:在移植旧PCI驱动到PCIe时,直接用了in/out指令访问I/O空间,结果发现PCIe原生不推荐I/O事务(除非RC明确支持)。正确做法是使用MMIO(Memory-Mapped I/O)读写BAR空间。所以,代码里看到inl() outl()的地方,一定要改成ioread32() iowrite32()。

1.4 小结:从架构角度看PCIe调试

搞懂这三层架构,对后面的解码和触发配置特别重要。你想想看,触发条件是在事务层拦截TLP包,还是数据链路层看CRC错误?解码的时候,你要解析的是TLP头还是物理层的Ordered Set?没有这个框架,配置逻辑根本写不出来。

我个人习惯在调链路时,先看物理层的链路训练状态机(LTSSM),确定是否进入了L0。然后再看数据链路层的DL_Up状态。最后才在事务层抓包。这个顺序,一步错,调试效率至少降一半。