1. PCIe基础回顾:从体系结构到实战配置

各位同学,咱们今天聊聊PCIe的基础。说实话,很多工程师做了好几年PCIe设计,但对事务层和数据链路层的理解还是模模糊糊的。我个人习惯是,先把地基打牢,再往上盖楼。这一章,我们就从PCIe的体系结构开始,一步步深入到TLP、DLLP,最后把配置空间和BAR空间讲透。

1.1 PCIe体系结构概述

PCIe本质上是一个高速串行总线。它取代了老旧的PCI并行总线。为什么?因为并行总线在高速下信号同步太难了。你想想看,几百根线要同时到达,频率一高就出问题。

PCIe采用点对点连接。每个设备都有自己的专用通道。这跟PCI的共享总线完全不同。共享总线就像一条单车道,所有车挤在一起。PCIe呢?每个设备都有自己的高速公路。

PCIe的层次结构分为三层:

  • 事务层(Transaction Layer):负责生成和解析TLP包。说白了,就是处理读写请求和完成包。
  • 数据链路层(Data Link Layer):负责可靠传输。加CRC、做重传。嗯,这里要注意,DLLP是这一层的管理报文。
  • 物理层(Physical Layer):负责串并转换、编码解码。8b/10b编码(Gen1/Gen2)或128b/130b编码(Gen3+)。

我在项目中遇到过一个问题:某次调试时,链路一直训练不成功。查了半天,发现是物理层的参考时钟抖动太大。所以啊,物理层的问题往往最隐蔽。

1.2 事务层(TLP)原理

TLP是PCIe通信的核心。所有的数据交换,最终都体现在TLP上。TLP分为几种类型:

TLP类型 用途 方向
MRd(Memory Read) 读取内存空间 发起者→目标
MWr(Memory Write) 写入内存空间 发起者→目标
Cpl(Completion) 读请求的返回数据 目标→发起者
CplD(Completion with Data) 带数据的完成包 目标→发起者
Msg(Message) 中断、错误通知等 发起者→目标

TLP的格式,我给大家一个简单的例子。一个32位内存写TLP:

// TLP头部格式(Memory Write 32-bit)
Byte 0: [Fmt=2'b10][Type=5'b00000]  // 4DW头部,Memory类型
Byte 1: [TC=3'b000][Reserved][Attr=2'b00][TH=0][TD=0][EP=0][Attr2=1'b0]
Byte 2-3: [Length=16'h0001]         // 1个DW的数据
Byte 4-7: [Requester ID]            // 总线号:设备号:功能号
Byte 8-11: [Tag]                    // 标签,用于匹配请求和完成
Byte 12-15: [Address[31:2]]         // 32位地址,低2位为0

这里有个坑。我曾经调试一个DMA引擎,发现写TLP总是发不出去。后来发现是Length字段填错了。Length是以DW(4字节)为单位的,不是字节。你填1表示4字节,填2表示8字节。这个细节,很多新手会搞错。

关键点:TLP的Length字段单位是DW(32位),不是字节。写操作时,数据载荷必须与Length一致。

1.3 数据链路层(DLLP)原理

数据链路层在事务层下面。它的主要工作是保证TLP可靠传输。DLLP是链路层自己的报文,不向上层传递。

DLLP的类型包括:

  • Ack/Nak:确认或否定确认。收到TLP后,接收方发Ack告诉发送方“我收到了”。如果CRC校验失败,发Nak要求重传。
  • InitFC1/InitFC2:初始化流量控制。链路训练完成后,双方交换缓冲区信息。
  • UpdateFC:更新流量控制信用。接收方处理完TLP后,释放缓冲区,通知发送方可以继续发。
  • PM_*:电源管理相关的DLLP。

我个人习惯,在调试PCIe链路时,先看DLLP是否正常。如果Ack/Nak交互不正常,那TLP肯定传不过去。我曾经遇到一个案例:某FPGA实现的PCIe端点,总是出现链路超时。抓包发现,接收方发了Nak后,发送方没有重传。原因是重传缓冲区被写满了。嗯,数据链路层的流控机制,一定要仔细实现。

调试技巧:使用PCIe协议分析仪抓取DLLP。如果看到连续的Nak,说明链路质量有问题或接收缓冲区不足。

1.4 PCIe配置空间与BAR空间详解

配置空间是PCIe设备的“身份证”。每个PCIe设备都有256字节的标准配置空间(Type 0)。如果是桥设备,则是Type 1配置空间。

配置空间的关键字段:

偏移地址 字段 说明
0x00-0x01 Vendor ID 厂商ID,由PCI-SIG分配
0x02-0x03 Device ID 设备ID,厂商自定义
0x04-0x05 Command 控制命令,如I/O空间使能、内存空间使能、总线主控使能
0x06-0x07 Status 状态寄存器
0x10-0x24 BAR0-BAR5 基地址寄存器,共6个,每个32位
0x3C Interrupt Line 中断线号
0x3D Interrupt Pin 中断引脚(INTA#-INTD#)

BAR空间是设备暴露给主机的内存或I/O地址范围。主机通过配置空间中的BAR寄存器,获知设备需要多大的地址空间,然后分配物理地址。

BAR空间的配置流程:

  1. 主机读取BAR寄存器,得到全1值(比如0xFFFF_FF00)。
  2. 主机根据读回的值,判断BAR空间大小。低12位为0,说明大小是4KB(2^12)。
  3. 主机写入分配的基地址。比如分配0x8000_0000,则写入0x8000_0000。
  4. 设备收到写操作后,将BAR寄存器更新为0x8000_0000。后续对该地址的访问,设备会响应。

这里有个实战经验。我设计DMA引擎时,BAR0通常用来映射控制寄存器。BAR1用来映射DMA描述符环。BAR2用来映射数据缓冲区。这样分层清晰,调试也方便。

注意:BAR空间的大小必须是2的幂次,且对齐到大小边界。比如4KB的BAR,基地址的低12位必须为0。如果BAR空间需要64位地址,使用BAR对(如BAR0和BAR1组成64位地址)。

配置空间的访问,通过配置读写TLP完成。Type 0配置请求的目标是端点设备。Type 1配置请求的目标是桥设备,桥设备会转发到下游总线。

举个例子,一个典型的配置读TLP:

// 配置读TLP(Type 0)
Byte 0: [Fmt=2'b00][Type=5'b00100]  // 3DW头部,配置读
Byte 1: [Reserved]
Byte 2-3: [Length=16'h0001]         // 返回1个DW
Byte 4-7: [Requester ID]
Byte 8-11: [Tag]
Byte 12-15: [Bus Number][Device Number][Function Number][Register Number]

我记得有一次,客户反映DMA传输偶尔失败。排查后发现,是配置空间中的Bus Master Enable位被意外清除了。这个位在Command寄存器的第2位。如果它被清零,设备就不能发起内存读写TLP。所以,DMA引擎初始化时,一定要检查这个位是否置位。

避坑指南:我曾经在配置空间初始化时,忘记设置Command寄存器的Bus Master Enable位。结果DMA引擎一直发不出TLP。调试了两天才发现。所以,初始化代码中一定要显式设置:config_space[0x04] |= 0x04;

好了,这一章的内容就到这里。PCIe的基础知识,是后续DMA引擎设计的前提。下一章,我们会深入DMA引擎的架构设计。到时候,我会结合一个实际的DMA控制器代码,带大家一步步实现。

记住,PCIe调试没有捷径。多抓包、多分析、多总结。嗯,就这样。