简介:一份基于FPGA的Serial RapidIO(SRIO)回环传输例程,面向具备一定数字逻辑基础、希望掌握高速串行接口开发方法的硬件工程师与FPGA学习者。资源以Verilog硬件描述语言源码为主,辅以VHDL等设计文件,覆盖SRIO协议栈配置、收发链路、时钟管理和回环校验机制;配套的编译、仿真脚本,约束文件与综合报告,能够帮助读者在工程环境中快速复现、定位并解决问题。压缩包共452个文件,约80.63MB,除源码外还包含批处理脚本、日志、报告及工程配置文件,目录结构清晰,便于按功能模块检索。目前已有3097人学习下载。通过这套例程,可系统理解SRIO自测试的回环模式,学习在FPGA上实现高速串行通信的完整流程,以及借鉴工程化的调试与排错思路;该例程同时适用于通信、嵌入式、医疗设备与高速数据采集等场景,对开展板级互连验证具有直接参考价值。
1. 为什么说 SRIO 例程是玩 FPGA 高速接口的必修课
如果你做过 FPGA 和 DSP 或者 FPGA 和 FPGA 之间的高速数据交换,大概率绕不开 SRIO(Serial RapidIO)。它不同于 I2C、UART 这类低速总线,也不同于以太网那样需要软件协议栈支撑,SRIO 在硬件层面就把传输、拆包、路由做掉了,延迟低、效率高,在雷达信号处理、图像采集、软件无线电这些场景里几乎是标配。刚接触 SRIO 的开发者,面对那一堆 IP 核配置界面和保护带(keepout)规则,往往一脸茫然,手里又没有一份能直接跑起来的参考工程。这份基于 Verilog 的 FPGA SRIO 例程,正好补上这块短板——它把一个可综合、可仿真的完整链路拆开摆在你面前,从 IP 核参数到数据收发逻辑再到中断上报,照着拉一遍,你就能在自己的板卡上做出第一条像样的 SRIO 通路。适合想做高速传输但缺参考实现的软硬件工程师,也适合给学生做毕业设计打底。
2. 先看懂工程结构:一个 SRIO 例程该包含哪些部分
2.1 目录设计与文件分工
拿到例程,先别急着打开 Vivado,花十分钟把目录结构过一遍。合格的 SRIO 工程一般会分成三个大块:IP 核生成脚本、RTL 源码、仿真与约束。这份工程就是按这个套路组织的:
srio_example/ ├── ip/ # IP 核定义与生成脚本 │ ├── srio_gen2.xci # SRIO IP 核配置文件 │ └── clk_gen.xci # 时钟生成 IP,供参考时钟与用户时钟使用 ├── rtl/ │ ├── top.v # 顶层,例化 IP 核与逻辑模块 │ ├── srio_init.v # 链路初始化状态机 │ ├── srio_write_path.v # 写请求与写数据产生逻辑 │ ├── srio_read_path.v # 读请求产生与读响应接收 │ ├── srio_doorbell.v # Doorbell 中断接收与上报 │ ├── axis_register_slice.v # AXI-Stream 寄存打拍,缓解时序 │ └── srio_common.v # 公共宏定义与常量 ├── sim/ │ ├── tb_top.sv # 顶层测试平台 │ └── rapidio_gen2_model/ # 端点模型,用于环路仿真 ├── constraint/ │ └── top.xdc # 引脚与时钟约束 └── scripts/ ├── create_project.tcl # 一键创建工程脚本 └── run_sim.tcl # 跑仿真脚本文件不算多,但每份都有明确分工。srio_gen2.xci是核心,里面记录了链路宽度、速率、参考时钟频率这些关键参数;srio_init.v负责任务完成前的链路训练;发送和接收路径被拆成独立文件,方便单独仿真和二次修改。仿真目录里的端点模型是 ISE/Vivado 自带的,用来做无硬件环境的环路验证。
2.2 顶层互联:从用户逻辑到 SRIO IP 核
顶层top.v做的事很纯粹:把用户侧产生的 AXI-Stream 数据,通过 SRIO IP 核的 Initiator/Target 接口发出去,同时把远端写过来的数据通过 Target 接口收进来。整体连接关系可以描述成下面的样子:
用户逻辑 (write_path) │ AXI-Stream (tvalid/tready/tlast) ▼ SRIO IP 核的 Initiator 接口 │ RapidIO 串行链路 (4x @ 5.0 Gbps) ▼ 远端设备 (DSP / FPGA / SRIO 交换机) 用户逻辑 (read_path) ▲ AXI-Stream 读响应返回 │ SRIO IP 核的 Target 接口在实际代码里,top.v模块例化时最需要注意的是两个接口的握手信号。SRIO IP 核的 Initiator 发送接口,s_axis_wwr_tready信号由 IP 核拉低表示反压,用户逻辑必须在tready为高且tvalid为高时才认为数据被接收。很多第一次写 SRIO 逻辑的开发者习惯性把tvalid一直拉高,结果遇到反压时丢数据,这属于典型的中断时序问题。例程里write_path的做法是:内部维护一个 FIFO,只有 FIFO 非空才拉高tvalid,同时每个时钟周期检查tready,两者都为高才弹出 FIFO,保证一拍不多、一拍不少。
2.3 关键参数速查:IP 核配置背后的含义
IP 核配置界面有一堆选项,真正决定工程能否正常跑通的是下面这组参数。以 4x 链路、线速率 5.0 Gbps 的典型配置为例:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Protocol Mode | RapidIO Gen2 | 选择 Gen2 才能跑到 5.0 Gbps 以上 |
| Link Width | 4x | 多数板卡走 4x,2x/1x 用于兼容 |
| Line Rate | 5.0 Gbps | 需要确认参考时钟与 GT 引脚分配 |
| Reference Clock | 125 MHz | Gen2 模式常用 125 MHz,需与板卡晶振匹配 |
| Logical Layer | Initiator + Target | 例程同时支持主动写和被动收 |
| Register Space | 0x30 / 0x34 / 0x38 / 0x3C | Doorbell 事件 ID 与中断状态寄存器 |
参考时钟这个坑最多。某些板卡上用的是 156.25 MHz 的差分晶振,如果你在 IP 核里配了 125 MHz,链路训练会一直失败,port_initialized信号永远拉不高。例程里把clk_gen独立出来,目的就是让参考时钟和用户逻辑时钟分开管理,调试时先确认gt_refclk频率,再去看链路状态。
3. 核心代码路径:一个请求从进入到返回的完整链路
3.1 写事务:从 AXI-Stream 到 RapidIO 包
写路径是 SRIO 用得最多的功能,对端设备发来一包数据,FPGA 要把它收下来放进 DDR 或者 BRAM。例程里的srio_write_path.v展示了从 AXI-Stream 接口到 SRIO IP 核 Target 接口的完整处理过程。核心代码不长,但值得逐行读:
// 接收来自 SRIO IP 核 Target 接口的写请求 always @(posedge user_clk or negedge rst_n) begin if (!rst_n) begin wr_addr_q <= 'd0; wr_data_q <= 'd0; wr_be_q <= 'd0; wr_req_valid_q <= 1'b0; end else if (m_axis_wwr_tvalid && m_axis_wwr_tready) begin // tvalid 与 tready 同时为高,说明 IP 核在向用户侧交付写请求 wr_addr_q <= m_axis_wwr_tdata[63:32]; // 高 32 位是目标地址 wr_data_q <= m_axis_wwr_tdata[31:0]; // 低 32 位是写数据 wr_be_q <= m_axis_wwr_tkeep; // 字节使能 wr_req_valid_q <= 1'b1; end else begin wr_req_valid_q <= 1'b0; end end // 向下游 BRAM 发出写请求 always @(posedge user_clk) begin if (wr_req_valid_q) begin bram_we <= wr_be_q; // 写使能,直接透传字节使能 bram_addr <= {wr_addr_q[15:2], 2'b00}; // 地址按 4 字节对齐 bram_wdata <= wr_data_q; end end这段逻辑的核心是理解 SRIO IP 核在 Target 接口上的数据格式:m_axis_wwr_tdata的高 32 位是 SRIO 包里的目标地址,低 32 位才是真正要写入的数据,tkeep信号给出字节使能。例程的做法是先把整包数据暂存一拍,再用独立的写信号向下游存储体发出写操作。这样做的目的是把 SRIO IP 核的时序和用户逻辑的解码逻辑彻底隔离开,避免因为地址解析不完整导致写错位置。实际调试中,用 ILA 抓m_axis_wwr_tvalid和m_axis_wwr_tready,可以看到这对信号像是流水线一样交替拉高,验证阶段重点关注地址是否按预期递增。
3.2 读事务:请求发起与响应返回的时序配合
读路径比写路径复杂,因为它涉及两个方向:先由用户逻辑发起读请求,再由对端返回读响应。例程里srio_read_path.v处理了这两段逻辑。发起阶段,用户逻辑把读地址打包成读请求包发出去;接收阶段,IP 核把响应包送到s_axis_rxd_tdata接口上。核心代码在响应接收部分:
// 响应接收,注意 tdata 的字段布局 always @(posedge user_clk) begin if (s_axis_rxd_tvalid && s_axis_rxd_tready) begin // 读响应包第 1 拍:完成码在高 8 位,事务标签在 [23:16] comp_code <= s_axis_rxd_tdata[31:24]; if (comp_code == 8'h00) begin // 完成码为 0,表示读成功 read_data_q <= s_axis_rxd_tdata[15:0]; // 有效数据在低 16 位,高位是扩展地址 rd_push <= 1'b1; end else begin // 非 0 完成码,按错误处理,丢弃数据 rd_push <= 1'b0; err_cnt <= err_cnt + 1'b1; end end end读响应包和写请求包的数据格式完全不同,写请求的地址和数据各占 32 位,读响应的完成码、事务标签和数据挤在同一条总线上。这里的处理方式是:先抓完成码,如果非零就丢弃,并计数上报;只有完成码为零才把数据推进下游 FIFO。调试时遇到读数据一直为零的情况,别急着怀疑时序,先看完成码是不是出现了非零值。如果是,多半是目标地址越界,或者对端设备根本没有实现读操作。
3.3 Doorbell 中断:最简单的错误状态上报手段
SRIO 协议里,Doorbell 是一种特殊的事务类型,不携带用户数据,只传递一个 16 位的事件 ID。例程里用它做链路状态和错误上报。当对端设备想通知 FPGA 某个事件发生时,会发送一个 Doorbell 包,IP 核解析后把事件 ID 送往m_axis_dbell_tdata。接收逻辑和中断上报逻辑如下:
// Doorbell 接收,事件 ID 在低 16 位 always @(posedge user_clk or negedge rst_n) begin if (!rst_n) begin dbell_event_q <= 16'h0; dbell_pending <= 1'b0; end else if (m_axis_dbell_tvalid) begin dbell_event_q <= m_axis_dbell_tdata[15:0]; dbell_pending <= 1'b1; // 挂起中断,等待软件处理 end else if (dbell_clear) begin dbell_pending <= 1'b0; // 软件写完清除寄存器后拉低 end end这段逻辑的关键在于dbell_clear信号。硬件置位dbell_pending后,软件通过寄存器接口读到事件 ID,处理完再写清除位。例程里把 Doorbell 事件 ID 的低 6 位用来区分错误类型:比如 0x01 表示链路断开,0x02 表示读超时,0x03 表示写数据包 CRC 错误。对端 DSP 侧的程序只做一件事——根据事件 ID 跳转到对应处理函数。理解了这套机制,你在自己的工程里扩展新的错误通知就很简单了。
4. 把例程跑起来:从 Vivado 建工程到片上调试
4.1 一键脚本创建工程并完成综合布线
拿到工程后,第一步不是手动创建 Vivado 工程,而是用仓库里的 Tcl 脚本。很多 SRIO 工程的手动创建过程极其容易出错——IP 核目录找不到、约束文件没加进来、仿真文件类型不对。用脚本可以避免这些问题。打开 Vivado 的 Tcl Console,执行:
cd /your/path/srio_example source ./scripts/create_project.tcl脚本内部做的事按顺序是:创建工程、指定器件型号、添加 RTL 和约束文件、生成并例化所有 IP 核。执行完后工程会自动打开。这里要注意,工程脚本里默认的器件型号是某 7 系列 FPGA,如果你换了型号,需要手动修改脚本里的part变量,再重新执行。综合前建议先跑一遍report_clock_utilization,确认所有时钟频率满足要求。如果出现时序违规,常见原因是 SRIO IP 核的 user_clk 频率配得太高,把 IP 核里用户时钟频率从 125 MHz 降到 100 MHz 往往能立竿见影。
4.2 仿真验证:直接在本地跑环路测试
如果你的硬件还没准备好,可以用仓库里的仿真脚本先验证逻辑正确性。仿真环境的搭建思路是:例化一个 RapidIO 端点模型,把 FPGA 的发送端接到它的接收端,把它的发送端接回 FPGA 的接收端,形成回环。这样不需要两块板子互联就能验证完整的收发链路。运行仿真命令:
cd /your/path/srio_example vivado -mode batch -source ./scripts/run_sim.tcl仿真模型启动后,链路初始化状态机开始训练,大约经过几百微秒后port_initialized拉高。随后测试平台会发起一笔写事务,把 1 KB 数据写入端点模型的内存空间,紧接着发起一笔读事务,把数据读回来做比对。检查仿真波形时,重点看三个信号:port_initialized(链路是否训练成功)、cnt_wr_ok(写事务累加器是否增加)、cmp_err(读回数据比对是否一致)。如果cmp_err一直为高,问题出在数据字节顺序或者地址对齐上,回头检查 3.2 节里wr_addr_q的高低位分离是否匹配。
4.3 上板调试:用 ILA 抓取关键信号
真正上板后,仿真通过并不代表硬件没有玄学问题。例程配套的约束文件里已经把 ILA 核需要的调试信号引出来了,srio_init_dbg、wr_path_dbg、rd_path_dbg三组信号分别对应链路状态、写路径握手和读路径返回。在 Vivado 里设置 debug 标记后重新综合布线,烧录 bit 文件,就可以在 ILA 窗口观察。调试 SRIO 的第一条血泪经验是:先看port_initialized有没有拉高,别急着看数据。如果这个信号一直保持低电平,直接检查参考时钟引脚定义、GT 位置约束和链路速率。
下面是上板后 ILA 抓到的正常波形应当满足的时序关系:
时钟周期 1: port_initialized 从低变高 时钟周期 2: 链路握手信号 trencode 开始切换 时钟周期 3: s_axis_wwr_tvalid 拉高,tready 跟随拉高 时钟周期 4: 数据开始连续传输,每拍一个 64bit 数据如果tvalid拉高了但tready一直为低,说明 IP 核内部的反压机制启动了。此时要检查是不是发送队列没及时排空,或者链路对端的接收缓冲区满了。这种情况在调试多板互联时尤其频繁,处理办法是降低发送速率,或者加大 IP 核内部的 FIFO 深度。
5. 常见问题与排查记录:SRIO 链路联调避坑指南
5.1 链路训练失败,port_initialized 始终拉不高
现象:板卡上电后,无论怎么复位,port_initialized信号一直为低,ILA 里看不到链路握手成功。
原因:多数情况是参考时钟频率不匹配。IP 核里配置的 GT Reference Clock 和板卡实际晶振频率不一致,导致收发器无法锁定。另一种可能是 4x 链路里某一条 lane 的引脚约束写错。
解决:首先用示波器或者 Vivado 里的 IBERT 核测参考时钟频率,确认实际值。如果实际是 156.25 MHz 而 IP 里配了 125 MHz,改 IP 核参数重新生成。确认时钟无误后,检查 XDC 里每个 lane 的引脚位置,特别注意差分对的正负不要搞反。
5.2 数据能发出去,但对端总是收到乱码
现象:写事务的握手信号正常,发送端的tvalid/tready跳变正常,但远端设备读到的数据字节顺序错乱。
原因:这是 SRIO 里最经典的问题——字节顺序不匹配。FPGA 侧默认按照小端序发送 64 位数据,而某个远端 DSP 默认按大端序解析,导致高 32 位和低 32 位互换。
解决:在发送路径里把每个 64 位数据拆成两个 32 位,交换位置后再拼装。或是在 IP 核配置界面的 Logical Layer 选项卡里调整字节顺序模式。例程里默认是小端序,如果对端是大端,需要改srio_write_path.v里的数据拼装逻辑。
5.3 读事务超时,错误计数不断累加
现象:读请求已发出,但读响应迟迟不返回,err_cnt寄存器数值持续增加。
原因:对端设备没有实现读事务功能,或者读地址越界。SRIO 规范里,如果目标器件收到读请求但无法完成,会返回 NAK 或错误完成包。
解决:用 ILA 抓s_axis_rxd_tvalid,如果一直为低,说明对端根本没有发响应。换用一个已知能完成读的端点地址测试。如果是地址越界,检查读请求包里的地址字段是否落在对端允许访问的区域内。
5.4 Doorbell 中断经常丢,或者两个事件并成一个
现象:对端发了两笔 Doorbell,但 FPGA 只上报了一个事件,或者上报的事件 ID 是两次的合并值。
原因:Doorbell 包的接收逻辑里没有做多包排队。如果前一个 Doorbell 还没被软件读走,第二笔到来时就会把状态覆盖掉。
解决:在 Doorbell 接收逻辑里加一层极简 FIFO,至少缓存 4 个 Doorbell 事件。例程里没加,你可以把它作为二次开发的第一步。注意 FIFO 的读指针和写指针位置要单独引出到寄存器,方便软件确认是否有堆积。
5.5 上板跑一段时间后,链路突然断开
现象:系统正常工作几小时后,port_initialized自动拉低,之后无法恢复,只能整板复位。
原因:链路不稳定,常见原因是电源纹波偏大或散热不佳导致 GT 收发器误码率上升。SRIO 协议在误码超过阈值时会主动断开链路。
解决:先看板卡电源模块的纹波是否在 50 mV 以内,超过就要加强滤波。其次检查 GT 参考时钟的相位噪声,如果时钟源质量差,误码率会明显升高。软件侧也需要配合,上层驱动要捕获port_error中断并做有限次重连,不要一断就死等。
6. 改造思路:把这套 SRIO 例程改成你的业务接口
拿到例程,最容易上手的二次开发是把写路径从固定地址改成可配置基址加偏移。做法是加一组寄存器,软件写入基址后,硬件自动累加偏移,每次写事务完成后判断是否超过长度上限。我喜欢在写路径里维持一个计数器,每收到一笔 AXI-Stream 写请求就递增,计数器到达预设值后拉高wr_done中断。上板改过的工程后,我会先用逻辑分析仪抓 10 分钟数据,确认没有毛刺,再对接上层软件联调。从那以后,我每次改 SRIO 工程都会强制走一遍三件套:先跑环路仿真比对数据,再上板抓握手波形核对时序,最后做 8 小时连续读写压测。希望这篇笔记能帮你在 SRIO 调通的路上少踩几个坑。
本文还有配套的精品资源,点击获取