第二章 PCIe DMA 基础与框架
各位同学,今天我们来聊聊PCIe DMA。说实话,这个主题是FPGA交易网卡开发中最核心的部分之一。我刚开始接触这个领域时,也被PCIe的复杂协议栈搞得晕头转向。但别担心,我会用最接地气的方式,把这块硬骨头啃下来。
2.1 PCIe协议概述
PCIe,全称是Peripheral Component Interconnect Express。说白了,就是电脑内部的高速总线。为什么叫Express?因为它快啊!
我个人习惯把PCIe理解成一个高速公路系统。传统的PCI是并行总线,就像一条多车道的老路,所有设备共享带宽。而PCIe是串行总线,每个设备都有自己的专用车道,叫Lane。一条Lane是一对差分信号,一个发送一个接收。
常见的配置有x1、x4、x8、x16。x16就是16条Lane,带宽最大。我在项目中用过x8的配置,跑10Gbps的网卡数据,绰绰有余。
PCIe的关键特性:
- 点对点连接:每个设备直接连到Root Complex,不共享总线
- 差分信号:抗干扰能力强,频率可以跑得很高
- 分层协议:事务层、数据链路层、物理层,各司其职
- 流量控制:基于信用机制,防止数据溢出
为什么会设计成分层协议?你想想看,如果所有功能都揉在一起,调试起来得多痛苦。分层的好处是,每一层只关心自己的事。物理层只管信号的收发,数据链路层保证数据不出错,事务层处理读写请求。
这里我画了一张PCIe协议栈的结构图,帮你理清层次关系:
嗯,这里要注意,事务层包(TLP)是我们FPGA开发者最常打交道的。所有的DMA传输,本质上就是在构造和解析TLP。
2.2 DMA传输原理
DMA,Direct Memory Access,直接内存访问。为什么需要DMA?
想象一下,如果没有DMA,CPU要从网卡收一个数据包,得先读网卡的寄存器,再把数据搬到内存。每搬一个字节,CPU就得停下手里的活。这效率,你想想看,得多低。
DMA的核心理念是:让设备自己把数据搬到内存,搬完了通知CPU一声就行。CPU只需要在开始的时候告诉设备「数据放哪里」,然后就可以去干别的事了。
我在项目中遇到过这样的情况:刚开始用PIO(Programmed IO)方式,CPU占用率直接飙到90%以上。换成DMA后,CPU占用率降到5%以下。差距就是这么明显。
DMA传输的关键要素:
- 源地址:数据从哪里来(比如网卡的FIFO)
- 目的地址:数据放到哪里(主机内存地址)
- 传输长度:要搬多少数据
- 描述符:描述一次DMA传输的控制信息
- 完成通知:传输完成后如何告诉CPU
DMA传输一般分为两种模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 寄存器模式 | CPU直接读写DMA控制寄存器 | 小数据量、控制类操作 |
| 描述符模式 | CPU在内存中构建描述符链表,DMA控制器自动取指执行 | 大数据量、高性能场景 |
交易网卡通常使用描述符模式。为什么?因为网卡的数据包是连续不断的,用描述符链表可以做到流水线处理。CPU准备好一批描述符,DMA控制器一个一个执行,执行完了再通知CPU。这样CPU和DMA可以并行工作。
避坑指南:我曾经在描述符对齐问题上吃过亏。PCIe要求描述符地址必须按16字节对齐,否则会触发Unsupported Request。后来我养成了习惯,所有描述符都用__attribute__((aligned(16)))来声明。
2.3 XDMA IP核配置
Xilinx的XDMA IP核,是我们做PCIe DMA开发的好帮手。它把复杂的PCIe协议封装成了简单的AXI接口。说白了,你不需要关心TLP怎么构造,只需要往AXI接口上读写数据就行。
XDMA IP核支持两种接口:
- AXI4 Memory Map:带地址的总线接口,适合随机访问
- AXI4-Stream:流式接口,适合连续数据流
我个人习惯在交易网卡中使用AXI4-Stream接口。为什么?因为网卡的数据是流式的,一个包接一个包,用Stream接口天然匹配。而且Stream接口延迟更低,吞吐量更高。
配置XDMA IP核时,有几个关键参数需要注意:
| 参数 | 说明 | 我的建议 |
|---|---|---|
| Number of DMA Channels | DMA通道数量 | 至少2个,一个读一个写 |
| AXI Data Width | 数据位宽 | 256位(64字节)性能最好 |
| AXI Clock | AXI接口时钟频率 | 250MHz以上,看你的FPGA等级 |
| Max Payload Size | 最大TLP负载 | 512字节,兼容性最好 |
| Descriptor Bypass | 是否绕过描述符 | 建议开启,减少延迟 |
嗯,这里要注意,Descriptor Bypass这个选项很多人会忽略。开启后,CPU可以直接指定DMA传输的源地址、目的地址和长度,不需要构建描述符。对于小数据量的控制命令,非常方便。
2.4 AXI4-Stream接口时序
AXI4-Stream接口,是XDMA IP核和用户逻辑之间的桥梁。它的时序其实很简单,核心就是一对握手信号:
- TVALID:发送方说「我有数据要发」
- TREADY:接收方说「我准备好收了」
- TDATA:数据总线
- TLAST:最后一个数据(包结束标志)
- TKEEP:哪些字节是有效的
数据传输发生在TVALID和TREADY同时为高的时钟沿。这个规则很简单,但实际项目中容易出问题。
我曾经遇到过一个bug:发送方把TVALID拉高后,不等TREADY就改变了TDATA。结果接收方在TREADY拉高时采到的数据是错的。后来我加了一个状态机,确保TVALID拉高后,TDATA保持不变直到传输完成。
下面是一个标准的AXI4-Stream写时序示例:
// AXI4-Stream 发送状态机示例
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tvalid <= 1'b0;
tdata <= 64'b0;
tlast <= 1'b0;
state <= IDLE;
end else begin
case (state)
IDLE: begin
if (fifo_empty) begin
state <= IDLE;
end else begin
tdata <= fifo_rdata; // 从FIFO读取数据
tvalid <= 1'b1;
if (fifo_last) begin
tlast <= 1'b1;
state <= LAST;
end else begin
state <= SEND;
end
end
end
SEND: begin
if (tvalid && tready) begin
// 数据被成功接收
tvalid <= 1'b0;
tlast <= 1'b0;
state <= IDLE;
end
end
LAST: begin
if (tvalid && tready) begin
// 最后一个数据被接收
tvalid <= 1'b0;
tlast <= 1'b0;
state <= IDLE;
end
end
endcase
end
end
AXI4-Stream时序要点:
- TVALID不能依赖TREADY,发送方必须主动拉高
- TREADY可以随时拉高或拉低,接收方有流量控制权
- TLAST必须在最后一个数据时拉高,且与TVALID同步
- TKEEP用于指示哪些字节有效,比如64位数据中只有低32位有效
你想想看,如果接收方处理不过来,可以把TREADY拉低,发送方就会暂停发送。这种背压机制,保证了数据不会丢失。
最后,我画了一张AXI4-Stream的时序图,帮你直观理解:
好了,这一章的内容就到这里。PCIe协议的分层设计、DMA的传输原理、XDMA IP核的配置要点、AXI4-Stream的握手时序,这些都是后续开发的基础。把这些搞懂了,后面写驱动代码就会顺手很多。
公众号:蓝海资料掘金营,微信deep3321