基于FPGA实现PCIe H2C过程中TAG的一种管理方法
PCIe DMA · Tag 管理 · 顺序队列 · 纯 RTL 实现
一、背景
在基于 Xilinx 7 系列 PCIe 硬核做 DMA 传输时,H2C(Host to Card,主机到板卡)方向本质上是一连串存储器读请求:板卡上的 DMA 引擎向主机发出读请求,主机通过**带数据的完成包(CplD)**把数据送回来。
PCIe 协议规定,每个读请求必须携带一个 Tag ,主机的完成包会原封不动地把这个 Tag 带回来,发送端正是靠它来区分"这包数据是哪个请求的响应"。协议同时规定:同一个 Tag 必须等它的完成包全部返回后才能复用。也就是说,Tag 是一种"借出---归还"型的有限资源。
一个 DMA 系统往往不只有一个数据流。以本文讨论的设计为例,H2C 方向同时存在四类请求源:
- 数据通道的正常 DMA 读;
- 描述符预取通道的读;
- SGL 链表(散列-聚集列表)的读取;
- C2H 方向列表信息的读取。
这些通道共享同一个 PCIe IP 的请求接口,也就共享同一个 Tag 空间。若没有统一管理,会出现两类致命问题:
- Tag 冲突:两个通道拿到同一个 Tag 发请求,完成数据回来后无法归属;
- 数据错路由:完成包回来时,不知道该送到哪个用户通道,也不知道该有多长。
因此需要一个集中式的 TAG 管理模块,完成申请、登记、路由、释放四件事。本文介绍一种基于"环形指针 + 顺序释放 + 并行匹配"的实现方法。
二、总体思路
核心思想一句话概括:
把 32 个 Tag 当作一个环形缓冲区来用------申请时顺序发放,释放时也必须按申请顺序释放。
这样做的依据来自 PCIe 协议本身:
- 读请求按序发出后,完成包的返回顺序与请求顺序一致(同一流量类别下,后发的读请求,其完成包不会越过先发的);
- 每个请求占用一个 Tag,其完成包结束后该 Tag 即可回收。
既然"借出顺序"和"归还顺序"天然一致,就不需要为每个 Tag 维护集中式查找表,只需两个指针:写指针 指向下一个待发放的 Tag,读指针 指向最早发出、等待归还的 Tag。两者之间夹着的,就是全部"在途"请求。

三、申请侧:仲裁与发放

3.1 请求仲裁
四个请求源可能同时发起 Tag 申请。设计采用轮询仲裁:
- 只有一个请求源在申请时,直接放行,不浪费节拍;
- 多个请求源同时申请时,按一个循环移位的优先级序列依次裁决,保证各通道长期获得均等机会,避免高吞吐通道"饿死"其他通道。
一轮仲裁完成后,胜出的通道拿到授权,进入发放流程。
3.2 Tag 发放与信息登记
发放动作由写指针完成:
- 当前写指针的值就是要发放的 Tag ID,直接回给胜出的请求源,后者随即带着这个 Tag 组装读请求;
- 与此同时,把本次请求的三个关键属性写入 TAG 属性存储 (深度 32,位置即 Tag 编号):总长度 (以双字为单位)、是否为最后一个包 的标志、来源通道号------数据最终送回哪个用户通道,全靠它。
发放之后写指针加一,回到环形下一位。

3.3 流量控制
Tag 是有限资源(本设计为 32 个,与 PCIe 典型的在途请求配额匹配)。用申请计数减去释放计数得到当前在途数量,并衍生出四种状态:
| 状态 | 含义 | 用途 |
|---|---|---|
| 空 | 在途数为 0 | 系统空闲指示 |
| 将空 | 在途数 ≤ 1 | 预警 |
| 将满 | 在途数 ≥ 31 | 提前反压,留余量给流水线 |
| 满 | 在途数为 32 | 硬反压,拒绝一切新申请 |
"将满"阈值比"满"提前一拍,是给仲裁和发放流水线留出反应时间------若等到满才反压,已进入发放流水线的请求会造成 Tag 超发。
四、完成接收侧(上):并行匹配与私有缓存
完成数据从 PCIe 硬核返回时携带 Tag 编号,且数据流是"广播"性质------任何一个在途请求的数据都可能出现在其中。如何把广播数据流准确分发到 32 个在途请求?本设计没有采用"先解码、再查表、后路由"的集中式方案,而是反过来:为每个 Tag 配一个独立的控制器,让广播数据流同时到达所有控制器,由各控制器自行比对编号------编号命中且本 Tag 处于占用状态的那一个接收数据,其余 31 个视而不见。
这是典型的"以面积换简单性":
- 匹配逻辑从 1 份变成 32 份,但每份只是一个等值比较;
- 每个 Tag 拥有一条私有数据缓存,彻底消除多请求争用同一缓存的冲突;
- 32 份控制器结构完全相同,仅编号参数不同,代码高度规整,时序收敛容易。
为驱动 32 份逻辑,广播数据流、属性写入、读指针三组输入各自复制出多组寄存器副本,以降低扇出、改善时序------这是 FPGA 工程中大扇出信号的标准处理手法。

单个 Tag 控制器内部完成四件事:
- 登记:属性写入且地址命中自己的编号时,锁定本次传输的长度、包尾标志和来源通道号,同时置起占用标志;
- 匹配接收:监视广播数据流,编号命中且占用时,将数据压入私有 FIFO。由于字节有效以字节为单位而长度以双字为单位,每 4 位取 1 位折算出每拍的双字数并累加;
- 收齐上报:累计双字数达到登记长度时,发出"收齐"脉冲------注意此时数据只是收进了私有 FIFO,还没有交给用户;
- 按需排空:上层读指针指向自己且下游就绪时,从私有 FIFO 读出数据,一边读一边对剩余长度递减计数,并按剩余长度重组每拍的字节有效与包尾标志;剩余长度减到最后一拍时,发出"排空完成"脉冲------这才是 Tag 真正释放的时刻。

五、完成接收侧(下):按序汇聚与 Tag 释放
32 个控制器的输出汇聚回一条数据流:用读指针做 32 选 1 ,任何时刻只放行"最早在途"的那个 Tag 的私有 FIFO------即便后面的 Tag 早已收齐数据在排队,也必须等它前面的依次排空。这一步把 PCIe 返回数据重整为严格按申请顺序的数据流,四个用户通道看到的顺序从此有了保证。
32 个"收齐"脉冲经两级或树(8→4→1,打两拍)合并成一个"读请求完成"信号,通知上游某次读事务结束;"排空完成"则驱动上层做三件事:在途计数减一、读指针加一、产生通道完成脉冲。接收节奏由主机决定、用户通道各自的就绪状态相互独立,因此数据面统一经过输出 FIFO 缓冲后再按属性中的来源通道号分发,管理逻辑只关心顺序和计数,数据面只关心握手。

六、设计要点与取舍
1. 为什么敢用"顺序释放"?
这是整个方案成立的前提。PCIe 规范保证读完成按请求顺序返回,只要 DMA 引擎按序发请求(本设计中请求经统一仲裁后串行发出,天然有序),完成顺序就有保障。省掉按 Tag 索引的集中式随机查找,用两个指针加一个计数器就完成全部管理,资源开销极小。
2. 属性随 Tag 走,而不是随数据走。
请求发出后,长度、包尾、通道号这些信息就"挂"在 Tag 上了。本设计把属性表展开成 32 份"活动"的控制器:登记、匹配、缓存合为一体,读指针推进到哪,数据与属性就从哪份控制器中取出。比在每个完成包上粘贴旁路元数据稳健得多。
3. "收齐"与"排空"双脉冲分离。
"收齐"是事务级事件,通知 DMA 引擎本次读请求的数据已全部返回;"排空"是数据级事件,代表数据已按序转交、Tag 可回收。两者分离让接收与转发彻底解耦:主机可以尽快归还完成包、腾出 PCIe 流水线,而数据可以按用户侧的节奏慢慢消费。
4. 水线反压是稳定性的关键。
32 个 Tag 一旦超发,会出现两个请求共用一个 Tag 的灾难场景。将满/将空两级水线让上下游都能提前减速,实测中可以把 Tag 用满而不错发。
5. 可扩展性。
将来若增加新的请求源(比如更多 DMA 通道),只需扩展仲裁器的输入位宽和属性中的通道号字段;Tag 深度变化也只是复制更多相同结构的控制器,环形管理的骨架完全不用动。
七、结语
PCIe DMA 中的 Tag 管理,本质上是一个资源分配 + 顺序队列问题。利用 PCIe 读完成天然有序的特性,把 32 个 Tag 组织成"写指针发放、读指针回收"的环形结构;完成侧用 32 份并行匹配的单 Tag 控制器替代集中查表,私有缓存消除冲突,读指针汇聚强制按序,收齐/排空双脉冲解耦接收与转发。整套方法不依赖专用 IP,纯 RTL 即可实现,用很小的资源代价同时支撑多个并发 DMA 通道的正确性与高吞吐,适合作为自研 DMA 引擎中请求管理子系统的参考骨架。