ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA高速串行接口SRIO工程实战:Verilog代码与Vivado配置

FPGA高速串行接口SRIO工程实战:Verilog代码与Vivado配置

搞FPGA这几年,高速串行接口这块我接触过不少。从PCIe到JESD204B,再到Serial RapidIO,SRIO这个协议在嵌入式高性能计算、基站信号处理、军工电子领域用得是真不少。最近把手上的一套Verilog写的SRIO源代码工程重新整理了一遍,里面维护请求、读写请求、门铃中断这些常用事务都有完整实现,拿来就能用。这里把整个工程的核心设计和实操经验做个总结,给正在折腾Xilinx Vivado加SRIO的朋友一点参考。

先说清楚这个工程能干什么。它是一套基于Xilinx 7系列FPGA、Vivado开发环境的Serial RapidIO Gen2 IP核用户逻辑工程,用Verilog实现。里面包含了维护包(Maintenance)构造与解析、NWRITE存储映射写、NREAD读请求产生与响应处理、门铃(Doorbell)发送接收、链路初始化状态机、逻辑与物理层错误处理等功能模块。做信号处理板卡、多板互联系统或者想学SRIO协议栈的朋友,可以直接拿这套逻辑当基础框架,省去从协议手册抠时序的大把时间。

1. 工程整体设计与代码架构解析

1.1 为什么用Xilinx官方SRIO IP核,而不是完全自己写协议栈

这是很多新手最先问的问题。为什么不直接用RTL代码硬撸SRIO物理层?因为SRIO协议栈的复杂性摆在那里,尤其是高速SerDes物理自适应、链路初始化训练、8b/10b编码、错误重传机制、流控这些底层细节,想用纯逻辑实现还做到可靠稳定,工作量巨大,调试周期更是漫长。Xilinx的SRIO IP核实际上把GTX/GTH高速收发器、物理层、传输层大部分协议处理都封装好了,对用户呈现的是基于HELLO(High-speed Electrically Link Layer Operation)格式的包接口,我们只需要关注transaction层的包构造与解析,这大大降低了开发门槛。

这个思路本质上就是用成熟的IP核做协议底层,把精力放在用户逻辑和应用事务实现上。FPGA开发讲究的是“能买的不自己造”,IP核如果你有授权、有licence,那这层成本远远低于自己研发协议栈的时间成本。尤其SRIO这种成熟的、业界统一的高速互联总线,直接用合规的IP核方案,稳定性、兼容性都有保障,后续升级维护也方便。

1.2 事务类型划分:维护包、读写包、门铃包如何各司其职

工程里的事务处理逻辑,说白了就是对三类包的处理:维护类、数据读写类、门铃类。我按功能划分模块,每个模块各管一摊,结构很清晰。

维护包(Maintenance):SRIO协议里的“管理面”,用来对端设备的能力寄存器、状态寄存器进行读改写访问。比如可以读远端节点的组件ID、读取链路状态、写配置信息。工程里实现了对维护读请求的响应,以及对维护写请求的处理,格式严格按RapidIO Interconnect Specification(Rev 2.2)的维护包格式来构造。

数据读写包(NWRITE/NREAD):高性能数据通路。NWRITE是写请求,不需要响应(posted),适合数据搬运;NWRITE_R是带响应的写,保证数据到达;NREAD是读请求,对端需要返回READ响应包。这个工程把SWRITE也兼容进去了,三十二位数据位宽下,单包最大有效载荷是256字节,实测带宽能跑到链路速率的百分之八十以上,这个后面详细说。

门铃包(Doorbell):高效的事件通知机制,十六位信息字段,专门用来传递短小的控制信息和中断通知。FPGA之间互发门铃,比拿GPIO拉线靠谱多了——走高速串行链路,延迟低,还能跨背板长距离传输。工程里实现了门铃发送接口和门铃接收检测逻辑,收到门铃会置位中断标志,方便上报处理器。

1.3 代码模块划分与数据通路设计

整个工程的代码组织,从顶层依次往下,大致是:

模块功能定位
srio_顶层例化SRIO IP核、用户逻辑、时钟复位管理、ILA调试接口
srio_user_rx接收HELLO包,解析出事务类型,分发到维护、读写、门铃子模块
srio_user_tx包发送通道,处理本地请求,支持维护读写、NWRITE、NREAD、门铃
srio_req_gen用户接口控制状态机,接收外部控制信号,生成对应事务请求
srio_resp_gen响应通道,处理对端发来的读请求和写请求,维护共享寄存器文件
srio_reg_file寄存器映射区,用于访问控制、状态上报、门铃字存储
srio_init_monitor链路初始化状态机,监控IP核的port_initialized信号,确保链路就绪后才允许用户事务发包

数据通路设计上,发送方向:用户逻辑需要发数据时,先把载荷写入发送FIFO,然后给srio_req_gen模块一个发起请求的信号,状态机根据请求类型组装HELLO包头,再按IP核的接口时序把包体逐拍送入TX通道。接收方向:IP核把收到的HELLO包按拍流递交给用户逻辑,我这边先做包头解析,判断事务类型和目的地址,然后分流到对应的处理分支。整个设计用AXI4-Stream接口和IP核对齐,握手信号都用TVALID/TREADY这套,时序干净,不会出现跨时钟域那种莫名其妙的问题。

2. Vivado工程搭建与SRIO IP核配置要点

2.1 IP核参数配置项解析:链路速率、链路宽度、参考时钟怎么选

用Vivado生成SRIO IP核的时候,界面里有几个关键参数得先想清楚,这里逐个说下我的配置经验。

链路速率(Line Rate):SRIO Gen2支持1.25G、2.5G、3.125G、5G、6.25G这几个档位。我工程里默认配的5Gbps,原因是5G速率下GTX收发器工作在比较舒服的频率区间,眼图裕量足够,均衡(LPM/DFE)参数也好调。如果你板子的光模块或背板走线质量一般,建议先降到3.125G把链路功能调通,再往上提速率,千万别一上来就冲6.25G,信号完整性不过关的时候排查问题非常痛苦。

链路宽度(Link Width):SRIO Gen2支持1x和4x两种。1x简单,一个GTX通道搞定;4x理论上四倍带宽,但要求四根差分对等长、等延迟,PCB设计要求高很多。我做第一版工程用的是1x模式跑通全流程,等后续做高速互联板卡再专门优化4x。如果你的板子已经按4x layout了,那IP核设置里选4x就行,逻辑部分基本不用改,收发数据通路的位宽会自动调整。

参考时钟(Reference Clock):GTX需要外部参考时钟,一般选125MHz。要注意SRIO IP核里有两个参考时钟来源选项:一个是shared logic(共享逻辑),就是参考时钟由IP核外部输入后,在IP核内部管理;另一个是separate(独立),不同通道可以给不同时钟源。工程里用的shared logic + 内部时钟分频方案,省事,逻辑层接口时钟由IP核自动生成。

RapidIO Gen2 PHY选项:IP核配置界面有Link Initialization相关的参数,比如acknowledge ID宽度、MAC模式下的事务类型支持。这些保持默认就能跑通,具体含义后面讲初始化流程时再展开。

2.2 HELLO包发送逻辑设计与本地地址映射

**HELLO(High-speed Electrically Link Layer Operation)**格式是SRIO IP核呈现出来的事务接口。它是SRIO协议的一个简化操作子集,负责把各种事务打包成统一的包结构。工程里的多个模块都基于HELLO格式操作,通过以下核心字段来路由:

HELLO字段含义作用
Ftype事务类型区分维护包、数据包、门铃、响应等
Ttype事务子类型区分NWRITE/NWRITE_R/READ等具体操作
DestID / SrcID目的ID / 源ID多节点路由的关键字段
Transaction ID事务ID匹配请求与响应,NREAD主要依赖此字段做关联
Address目标地址访问对端存储空间的偏移地址
Payload数据载荷写事务的数据内容

地址映射这块,我定义了Base Address + Offset的寻址方式。比如维护寄存器组的基地址从0x00000000开始,数据缓冲区的地址映射到一个较大的偏移区间,门铃信息字段单独映射。对端节点发起访问时,本地接收到读请求后在响应路径里根据地址判断访问的是哪个资源,再返回对应的数据。这样双方约定好内存map,不用动态协商,工程简单可靠。

2.3 初始化流程与链路状态判断

SRIO链路建立是个多层握手过程,从物理层开始逐步向上。物理层链路训练完成后,会进入RapidIO Link Initialization状态机,这个状态机会交换SRIO Maintenance LTE(Link Training Error)状态、设置acknowledge ID、确定操作模式。IP核把整个初始化的状态打包在几个用户可读的信号里。工程里我用port_initialized作为核心状态标志,这个信号拉高,才认为链路好了,用户事务逻辑才允许启动。

always @(posedge srio_user_clk) begin if (link_init_done) begin if (!ip_port_initialized && !link_lost_sticky) link_lost_sticky <= 1'b1; end end

这里有个关键细节:链路初始化完成信号拉高之后,链路还可能因为对端复位、板卡插拔等原因掉链子。因此工程里专门实现了一个链路状态监视模块,port_initialized一旦从高变低,就锁存一个sticky错误标志位,同时把错误状态上报到状态寄存器,软件层可以通过维护读请求读取这个状态。另外,发送侧在链路未初始化完成时,要堵塞所有用户事务发包请求,保证状态机不出乱。

3. 核心事务(维护/读写/门铃)实现原理与Verilog关键代码

3.1 维护包(Maintenance)怎么构造与解析

维护包的包头结构和普通数据包不太一样。它没有payload数据区(携带数据的维护写除外),主要是配置信息的传输,所以包格式上维护包的控制字段更紧凑。我工程里维护读响应包的主要字段组装逻辑是这样的:

// 维护读请求发起 if (req_type == MAINT_READ_REQ) begin hec_fifo_tdata <= { 8'h00, 8'h20, src_id, dst_id, 16'h0000, 8'h00, maint_rd_addr, 48'h000000000000 }; end

第一个字节是Ftype(8’h08表示维护类),第二字节包含Ttype和请求类型标志,接着是源ID和目的ID,再往后是事务ID和地址字段。对端设备收到这个包以后,响应端会构造一个维护读响应包,把寄存器值放在payload字段里返回。工程里专门的维护响应模块就是干这个事儿的,它会根据访问地址索引本地的寄存器数组,从reg_file里面取数据,组装成响应包发回去。寄存器地址我做了宽度可配置的设计,访问地址高8位、寄存器索引、字节偏移,软件工程师使用的时候按头文件里定义的宏来访问就行,不用关心bit位具体排列。

3.2 NREAD / NWRITE 数据包的数据载荷与方向

数据读写是SRIO工程里最核心的数据通路。NWRITE(非posted写事务)走的是不需要响应的事务方式,对端收到写包后直接写入本地存储,不返回响应包。这样节省了响应通道开销,适合数据连续下发、实时采样数据回传这类场景。NREAD则是读请求事务,请求方构造读包发送给对端,对端必须返回一个带数据的响应包。

读写数据包的处理,重点在payload的组织上。工程里发送侧维护了一个写数据FIFO,用户逻辑把要发送的数据写进FIFO,发送状态机从FIFO里读出数据,按照双字(8字节)粒度做数据宽度匹配,再填入HELLO包。一个时钟拍能推64bit或者32bit看IP核的位宽配置。载荷边界都用双字对齐,这样对端的DMA逻辑就好处理了。接收侧,收到写事务后,解析出目标地址和payload,然后按字(32bit)宽度写入本地缓冲区,同时生成一个写完成标志,如果关掉了中断屏蔽位,还可以触发一次本地中断通知CPU来搬运数据。这个数据路径用乒乓缓冲区处理,可以做到边收边用,吞吐不卡顿。

这里特别说明一个容易踩坑的点:NREAD读请求发出后,对端返回响应是一个异步、不确定延迟的过程。发起方必须管理好事务ID(TID)的生命周期。工程里用一个简单的TID池,发出读请求时分配TID,收到对应响应并匹配到TID后释放。如果软件做了超时控制,硬件侧在计数器溢出后要主动丢弃这个TID并标记超时错误,不然TID耗尽后read请求就发不出去了。

3.3 门铃包处理机制与事件中断

门铃事务是SRIO里最精巧的控制消息机制,它没有payload,就靠一个16位的info字段干活。工程里发送门铃的接口信号设计为doorbell_req和doorbell_info,用户逻辑置位doorbell_req同时把16位信息放到总线上,发送模块就会自动组包发出。

wire [15:0] doorbell_info = host_cmd_reg[15:0]; wire doorbell_pulse = host_cmd_wr_strobe;

收端呢,收到门铃包之后,把门铃信息字段写入本地的doorbell_reg,同时置位doorbell_intr信号。这个中断信号既可以直接上报到嵌入式处理器核(比如MicroBlaze或Zynq的ARM核),也可以输出到板级外部中断控制器。中断处理完成后,软件读走门铃值、写清除寄存器来解除中断状态。这整套机制非常符合嵌入式系统的中断处理习惯。

工程里还实现了一个门铃应用的典型场景:多板卡间的心跳机制。每块板卡定时通过门铃向主控板发一个固定模式的心跳包,超时未收到心跳则主控板判定对应板卡掉线。相比引脚电平检测,SRIO门铃方式可以在背板走线很少的情况下实现多节点状态监控,板卡位置还能和DestID绑定,代码里配置一下ID就能识别报文从哪块板来,非常实用。

4. 仿真与板级调试经验实录

4.1 用Vivado自带的SRIO example design做验证

SRIO IP核生成的时候,Vivado会附带一个example design工程,一定要先用这个工程打通链路。Example工程里包含了回环测试(loopback)逻辑,你把内部生成器的数据包配置成回环接收模式,可以直接验证物理层和链路层有没有问题。我的习惯是先在example design上做仿真实测验证,确认IP核配置没问题后,再把用户逻辑挂上去替换掉example里的示例用户逻辑。

仿真那步也非常重要,别想着直接上板盲调。我写好了一个简单的测试平台,用task封装了发起维护读、NWRITE、门铃发送的操作,然后给SRIO IP核模型里挂一个remote device的模拟响应端。对于sent transaction,检查expect response包的类型、TID和数据内容,能做到比较系统的回归验证。跑仿真的时候记得把IP核的随机延迟打开,验证带随机延迟的事务时序比不带严格握手跑下来的代码可靠得多。

4.2 常见问题排查:链路没起来、门铃对不上、读写超时

干活多了总会遇到这样那样的问题,每个主流坎儿基本都趟过一遍,整理了下面这个简易速查表,希望能帮你少走弯路。

问题现象可能原因排查方法
port_initialized一直拉不高参考时钟没锁定 / GTX没有复位完成 / 对端SRIO未使能链路ILA抓GTX reset、CPLL锁定信号;查看IP核的链路状态寄存器;确认对端链路速率和宽度配置一致
维护读返回超时目的ID映射错误 / 包被对端丢弃 / 本地响应模块没有使能用VIO强制修改目的ID走完一轮;测试对端交换芯片的维护寄存器;检查响应背压信号
NWRITE数据对不上地址偏移在HELLO包里错位 / 字节序未对齐用ILA抓发送FIFO读出的数据和HELLO包的地址字段,确认是按双字对齐规则填的
门铃收到但中断不触发中断使能位没置1 / 门铃信息被响应通道误当作数据响应处理检查门铃接收分支的使能寄存器,在仿真平台里加定向测试
链路建起来后跑一阵子就断了信号完整性问题 / 时序收不了 / 收到了error recovery包降到低速率测试;检查时序报告critical path;用IBERT例程调收发器均衡参数

这里面我想重点展开两个高频问题。一个是链路起了又断,多数情况是GTX的RX端均衡参数和工作协议模式不匹配,工程里用Vitesse那种背板通道时,需要调LPM值和DFE开关。直接在Vivado里例化IBERT核去获取不同参数组合下的误码率,效果比盲调好太多。

另一个是门铃收到值总是不变。排查过几次发现,都是因为软件读走了门铃寄存器之后没有执行清除动作,导致门铃中断被反复触发。这属于硬件外联设计时把门铃的“事件触发”语义理解错了,门铃寄存器应该是写1清除(W1C)或者读清除(RC),我用的是W1C方式,软件处理中断的时候写清除命令即可。把这个交互协议在头文件里注释清楚,联调的时候能省一大堆口舌。

4.3 避坑清单

整理这个工程的过程中,做了个重要的避坑清单,血的教训总结起来的几条:

  1. 不要跳过SRIO IP核的example design验证步骤。这一步能帮你剥离开GTX物理层、链路初始化、用户逻辑三层问题,分层排查永远是最高效的调试策略。
  2. 时钟复位按IP核手册来释放。SRIO IP核的复位信号有严格的时序要求,如mmcm_lock先等GTX复位完成,再释放逻辑复位。这里图省事手动拉高复位,各种怪毛病都会接踵而至。
  3. 小心大端小端转换。RapidIO标准里是用大端还是小端在不同IP、不同处理器平台之间会有差异,尤其做数据字节序对不上的问题排查时,一定要把HELLO包的字节序看清楚。Xilinx IP输出是按大端序的,和本地寄存器数据总线对接时要做转换,工程里我专门写了一个bswap转换模块,从源头上保证了正确性。
  4. 多关注链路错误统计寄存器。SRIO IP核里其实有很详细的链路状态和错误统计寄存器,比如CRC错误计数、包错误类型、acknowledge ID异常计数。没事看几眼,比凭空猜要准十倍。

5. 后续还能怎么扩展

这套源代码工程目前支持了维护事务、读写事务和门铃中断,其实SRIO还有很多可玩的高级特性,比如数据流控制(Data Streaming)、原子操作(Atomic Operations)、Type 9/11扩展事务。如果要做高性能的分布式DSP算法或者数据加速卡集群,可以基于这个工程把原子操作加上去,对多节点并发时分发任务特别有用。另外目前链路默认1x模式,如果想体验更极致的带宽表现,IP核换成4x模式,顶层逻辑的收发位宽适配一下就能复用,数据带宽轻松再翻几倍。

最后说点个人体会:SRIO这套工程的调试过程,跟我最早做PCIe时的过程很像。高速接口这东西,看着协议手册觉得哎这不就逻辑加减法吗,真到了板上跑起来,物理特性、时序收敛乱七八糟的因素全都涌上来了。所以拿到这种参考工程,最要紧的是建立自己的调试节奏——能分层的先分层,能从寄存器里读状态就不靠猜,一切以链路建立为优先,功能扩展留到后面。顺着这套方法论去调,SRIO没想象中那么难,运营起来也会很顺手。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进