首页 / 资讯中心 / 文章详情

FPGA高速互联SRIO实战:Vivado下事务级通信平台设计与调试

FPGA高速互联SRIO实战:Vivado下事务级通信平台设计与调试 ★ FEATURED ARTICLE
很多做FPGA高速互联的工程师第一次接触SRIO都是被那堆ireq、iresp、treq、tresp信号劝退的。相比PCIe有成熟的DMA和AXI桥接方案Serial RapidIO在Xilinx Vivado里落地的难度几乎全在用户侧——IP核只保证链路层和逻辑层的协议处理至于怎么把寄存器命令组织成一包合法的请求、怎么处理握手与反压、怎么识别和维护事务的响应全部需要自己写Verilog。这种时候一个包含维护包、读写和门铃功能的事务级源码工程就显得很珍贵了。它不只是一个示例更像一个可以当底座用的SRIO通信平台把协议里最常用的三类事务用可读、可扩展的代码实现出来能帮人省掉大量从零啃手册的时间。这篇内容就从这个视角展开先说清楚SRIO事务模型的底层逻辑再讲Vivado中IP核的配置关键然后拆解工程里用户逻辑的组织方式、三大功能模块的实现细节最后分享我在实测中走过的排查链路。不管你是准备把SRIO引入自己的系统还是想在这个工程基础上做二次开发希望能少踩几个坑。1. 先弄明白SRIO在传什么事务模型决定代码怎么写1.1 一个请求包从产生到落地的完整路径SRIO的体系结构分成逻辑层、传输层和物理层三层。大多数人在调试时关心的其实是逻辑层行为但传输层的destID、物理层的链路协商往往才是问题所在。一个典型的SRIO读请求从本端发起到远端返回数据整个过程是用户逻辑构造一个NREAD请求包包含FTYPE、TTYPE、目标地址、事务ID、目的设备IDIP核把这个包交给传输层加上源设备ID和目的设备ID再往下走物理层会在包前面加物理字和CRC校验通过SerDes发送出去。远端收到后IP核做物理层校验、传输层路由逻辑层再把请求从treq接口递交给用户逻辑。如果目标端决定响应它构造一个带数据的RESPONSE包沿相同路径返回。理解了这条链路就会发现一个关键点本地用户逻辑看到的事务ireq/treq接口上的包和线上真正传输的包是两回事。IP核在中间作了大量协议封装和转换。这也是为什么很多人调试时盯着线上波形却迟迟定位不了逻辑层错误——你要找的信号其实在IP核的用户接口上而不是在GT的收发数据上。1.2 维护、读写、门铃在协议语义上的差别SRIO的事务种类按FTYPE划分每种FTYPE下再按TTYPE区分具体操作。工程里涉及的三大类事务对应关系如下。事务FTYPETTYPE是否需要响应典型用途NREAD0x02请求0x04是返回读数据远端存储器读取NWRITE0x02请求0x05否远端存储器写入NWRITE_R0x02请求0x06是返回完成响应需要确认的写操作SWRITE0x02请求0x07否大数据流写入DOORBELL0x01门铃0x00否事件通知、中断触发维护读0x02请求0x00是读取远端配置寄存器维护写0x02请求0x01是配置交换路由、控制寄存器这张表值得贴在电脑边上看。工程里“读写”通常指的是NREAD/NWRITE/NWRITE_R/SWRITE这一类数据事务“门铃”是FTYPE1的事件通知机制它不携带地址和数据纯粹靠一个包传递信息而“维护”虽然也是读写的语义但它访问的是端点内部的配置寄存器和设备ID寄存器走的是另一套路由寻址方式。这三类事务在源码实现上的差别会在后面的代码拆解里看到但先记住结论读写事务处理的是“地址数据”门铃事务处理的是“信息”维护事务处理的是“配置空间”三者的构造、响应、异常处理逻辑完全不同。2. Vivado里的IP核配置链接参数和逻辑接口的分工2.1 链路参数配置中最容易忽略的地方在Vivado中例化Serial RapidIO Gen2 IP核配置页面第一层是链路参数线速率、通道数、参考时钟。这些参数直接决定了GT的位置和约束上板后想改只能重构bitstream。我自己踩过的一个典型问题是参考时钟选择。SRIO IP核的GT参考时钟通常用125MHz或156.25MHz配置页面选错时钟频率Link Initialization会一直卡在Port OK之前的状态。另一个容易被忽略的是线速率和参考时钟的倍数关系比如3.125Gbps的线速率在某些器件下要求156.25MHz的参考时钟这是由GT的PLL分频结构决定的。配置代码生成后务必检查example design里的时钟约束文件确认IBUFDS_GTE4的引脚和频率注释与配置页面一致。链路宽度方面如果只是做点对点调试1x链路完全够用要跑大数据吞吐至少用2x或4x。要注意的是链路宽度设定为4x不代表逻辑层一次性给你4x64位的数据总线——用户接口的数据宽度默认是64位按ready/valid握手传输。链路宽度影响的是物理层的并行通道数和逻辑侧接口位宽并没有直接等号关系。2.2 ireq/iresp和treq/tresp四个接口的分工SRIO IP核逻辑层给用户提供了四个方向的接口很多初学者会把它们搞混。弄清楚这四个接口就基本读懂了这个工程的代码框架。ireq接口是本地发起请求的出口。本地用户逻辑作为发起端生成NREAD/NWRITE/门铃/维护包从ireq发出去。与ireq对应的iresp接口是本地发起的请求在远端被执行后远端回给本地的响应包入口。也就是说本地请求远端读数据读回来的数据从iresp进来。treq接口是本地作为目标端时接收到的请求入口。远端发起访问如果目标是本地的存储空间或寄存器请求包会从treq交给用户逻辑处理。而tresp接口是本地作为目标端时回给远端的响应出口。本地必须在收到treq后根据请求类型判断是否需要构造RESPONSE包从tresp返回。一句话总结ireq/iresp服务于“本端往外发起的请求”treq/tresp服务于“本端响应远端发来的请求”。源码工程里四个接口分别对应四个独立的处理模块这样调试的时候可以单独拉某一组信号来看不会互相干扰。2.3 握手信号的真实时序rdy、vld、ack谁说了算SRIO逻辑层的用户接口握手方式比较特殊不是简单的valid/ready而是three-way握手。以ireq为例发起端置ireq_vldIP核置ireq_rdy当两者同时为高形成握手发起端在下一拍还要观察ireq_ackack为高才表示IP核真正接受了这个包。这里的坑在于vld和rdy同时为高并不代表当前拍的包头一定被接受必须以ack为准。IP核内部有自己的缓冲队列当队列满时rdy会拉低但有时候rdy为高而ack恰好为低说明IP核正在处理某些内部冲突这一拍的数据不能被当作有效传输。实测中我发现严格按照“vld拉高后等待rdyrdy拉高后等ack再推进状态”的时序写状态机是最稳妥的方式。千万不要只用vld和rdy两信号握手就推进请求包碰上IP核内部重传和重排序非法事务就会从这里产生。3. 用户逻辑的组织方式请求生成、响应配对与异常恢复3.1 请求构造状态机的设计思路工程里对每个事务定义了一组寄存器命令用户通过AXI-Lite写入命令寄存器触发事务执行。核心状态机一般分成IDLE、WAIT_RDY、LATCH、WAIT_ACK、SEND_DATA几个状态。以一次NWRITE_R写事务为例状态机先进入WAIT_RDY检测到ireq_rdy为高后拉高ireq_vld并输出包头信息——控制信号ireq_type置为IO请求ireq_ftype置为0x02ireq_ttype置为0x06同时给出目的设备ID、事务ID、地址。下一拍如果ireq_ack为高则进入SEND_DATA状态按突发长度逐拍发送数据。每拍数据要同时给出ireq_wr_en和ireq_byte_en这里最容易出错的是字节使能如果按64位数据总线算一个地址对应8字节字节使能必须精确到每次写操作实际覆盖的字节范围。对于读请求NREAD构造要简单一些包头之后不需要发送数据但如果使能了load/store扩展还需要处理ireq_wr_be信号表示读操作的字节使能。很多实现里把这个信号固定为全F实际上对于非对齐访问这个信号不能随便填否则读返回的数据顺序会错乱。3.2 响应配对是怎么完成的事务ID管理响应包和请求包通过事务IDtID配对。本地发起多个事务时每个未完成请求占用一个唯一的tID。实际工程里用的是一个简单的tag表——一个tID位宽大小的队列发起请求时从队列头部分配一个空闲tID收到对应的响应包后再将该tID回收。我见过不少实现省掉了这个机制每次只允许一个事务在飞行中发完一个请求必须等响应回来才发下一个。这确实简化了逻辑但吞吐会非常低。在SRIO场景里往返延迟可能达到几百纳秒如果一直单线程跑事务链路的利用率连10%都到不了。源码工程里的做法是维护一个深度为16的tID队列配合一个以tID为索引的完成表每个响应进来后直接用iresp_tid索引完成表将数据写入对应条目同时置完成标志。这个设计有一个隐蔽问题tID复用。响应包在路上延迟比较长时请求端可能已经超重发了一个相同tID的新请求此时晚到的旧响应就会和新请求的响应混淆。解决方案是给每个tID增加一个序列号位或者确保在tID回收后等待足够长的空闲时间再分配。工程里用的是后者——tID从完成队列回收后必须经过一个计数器延迟才能再次分配这个延迟通常设置为最大响应时间的一半。3.3 维护事务响应超时的判断逻辑维护事务最容易出现的情况是发出去的维护读请求石沉大海。比如远端交换芯片的路由表还没配置或者hop_count不对维护请求根本到不了目标设备自然没有响应。工程里的处理方式很典型每个维护事务启动时同时启动一个超时计数器计数器的超时门限由寄存器配置默认值设在几十微秒到几百微秒之间。如果超时计数器溢出时iresp还没有匹配的响应进来状态机会主动向IP核发送一个清除事务的操作并把维护事务挂在错误队列里向上位机报错。这里的关键是SRIO协议本身没有定义“事务超时”机制IP核不会替你判断请求是否超时这个功能完全要靠用户逻辑实现。所以在源码工程中维护事务的超时判断是一个独立的模块不跟读写事务共用一套状态机原因是维护事务经常是用于系统初始化的此时链路上根本没有其他流量单独做判断逻辑会更清晰。4. 模块级拆解门铃、读写和维护的Verilog实现要点4.1 门铃事务最简单但最容易被忽略的细节门铃事务在源码工程里实现起来非常简单一个包长度通常只有4字节没有地址FTYPE为0x01TTYPE为0x00信息字段放在包头的bit15到bit0中。发起门铃时ireq接口只需要发送一拍控制信号、不需要发送任何数据也不需要等待响应。不过有两个细节值得注意。第一门铃事务的优先级。SRIO包头的优先级字段影响交换芯片的处理顺序。如果同时有大批量写事务和门铃事务在飞行中门铃优先级低的话可能被堵在交换芯片的几个时钟周期之外导致延迟抖动变大。工程里把门铃优先级设为最高对应的ireq_prio置为3。第二接收侧的处理。远端通过treq接口收到FTYPE1的包后用户逻辑需要解析出信息字段触发一个中断或者置位某个寄存器。因为门铃不携带地址接收侧不必做地址译码只需在treq_vld和treq_rdy同时有效的那一拍锁存数据。功能上门铃最常用于中断通知。我在一个模拟项目X里用它来同步远端DMA完成状态一个门铃包发出远端立刻拉高中断比轮询读状态寄存器的延迟低了几个数量级。4.2 NREAD/NWRITE/SWRITE的地址对齐与字节使能处理读写事务是源码工程里代码量最大的一部分容易出错的地方集中在地址对齐和字节使能推导上。SRIO要求所有读写地址按32位边界对齐。但用户在寄存器里填的地址不一定是4字节对齐的所以工程里在事务发射前做了地址修正如果地址低两位非零则自动向下取整对应的字节使能保留实际需要访问的字节位。比如一个从地址0x02开始的8字节写操作前4字节只有高2字节有效后4字节全部有效那么第一次发送数据时byte_en为0x30后续数据拍byte_en全为0xFF。这个逻辑写起来不复杂却是最容易引入数据错位的地方。再进一步说64位总线下地址的步进是8字节。NREAD读请求里ireq_addr填的其实是8字节块的起始地址而实际要读的数据可能只占其中的一部分。返回的读数据是完整64位总线用户逻辑必须根据字节使能从返回数据中提取有效字节。工程的读响应模块里专门有一段bit-shift逻辑功能是“根据ireq_wr_be将返回的64位数据对齐到输出总线的低有效位”这样上层拿到的是一个严格对齐后的数据方便直接写入FIFO。SWRITE流写的实现又不一样。SWRITE固定地址连续、固定8字节对齐且比分片的NWRITE效率更高因为它不需要每包都携带地址。在FPGA里做大块DMA搬移SWRITE是首选。工程里把SWRITE单独拆成一个模块用状态机连续发送数据包包与包之间几乎没有间隔配合IP核的acc_features可以把链路利用率推得很高。4.3 维护读写的hop-by-hop与寄存器偏移维护事务访问的是远端设备的配置空间与普通存储器访问最大的差异是维护包的hop_count字段。SRIO网络里交换芯片通过维护包配置路由表。当本地要向远端设备发维护读时这个请求在交换芯片之间每跳一次hop_count减一减到零的那一跳才被当前设备接受用于访问它的配置寄存器。这个机制决定了请求设备必须知道目标设备位于第几跳才能正确设置hop_count。工程里维护事务模块的做法是把hop_count做成可配置寄存器初始化流程里通过逐步增加hop_count的方式扫描整个网络探测哪个设备ID是可达的。扫描到一个端点后就可以用维护读读取设备ID寄存器和扩展特性指针做路由表配置。维护读的寄存器地址由两部分组成维护事务的地址字段用于指定要访问的config空间偏移。例如读一个端点的设备ID寄存器地址填0x60hop_count填1事务发出后响应包的tresp_data会返回该端点的设备ID。维护写用于配置路由表地址指向交换芯片的路由表寄存器数据为被配置设备ID实际上是让交换芯片知道去往某个设备ID的包应该从哪个端口转发。因为维护事务直接面向系统级配置它的实现必须非常谨慎。工程里在维护写之前会先做一次维护读验证确认目标设备存在且状态寄存器正常才发写请求。这个“先读后写”的机制避免了一大堆“看起来写成功了实际上目标不存在”的诡异问题。5. 实测调试验证从Link Up到数据正确的三个关键阶段5.1 链路已连接但事务卡死的排查链路拿到板子先把example design跑起来用ILA抓link_status和port_initialized信号。这两个信号正常后说明物理层和传输层已经初始化完成但此时发出的维护读请求却可能一直没有响应。我遇到过的典型场景是维护请求发给一个尚未配置路由表的交换芯片请求包被发到错误的端口然后丢弃此时illegal transaction错误被记录在端点状态寄存器里后续的维护事务全部被IP核阻塞。排查方法是用维护读去读本端自己的错误状态寄存器看一眼bit14到bit15的type字段就能定位是“packet was discarded”还是“request not accepted”。绝大多数初始化卡死都从这里找到原因。工程里还内置了一个“链路诊断状态机”能按顺序输出本端设备ID、Link Timeout、错误状态字、成功接收计数等关键寄存器。初始化失败时直接把这些寄存器抓上来比对比盲查IP核内部转换消息快得多。维护事务的响应速度本来就不是固定的有的交换芯片响应能拖到几百纳秒状态机里需要留足余量否则很容易把自己误判成超时。5.2 数据错位与端序问题链路通了、事务也有响应但远端读回来的数据和写入的不一致这是最常见的问题。一般情况下先把传出数据设为0x55AA55AA这种特征值再做环回测试。环回正常后接真实远端一旦数据错位优先检查的是地址转字节偏移时端序是否匹配。SRIO本身规定的是大端序而FPGA处理器核普遍按小端序处理数据因此必须在接口处做一次变换。工程的解决方式是对所有写入远端的数据先按字节交换再送入ireq数据总线所有读回的数据先做字节交换再写回AVMM总线或FIFO。具体交换规则是在64位数据总线上最低字节和最高字节互换中间两个字节互换。这个逻辑占的资源不多却几乎决定了通信正确性。如果在调试中懒得做成逻辑也可以在寄存器配置里加一个“端序模式”开关配置为0走直通配置为1走交换调试时通过翻转配置比对数据的差异。多了一个寄存器位整个调试工作量能减少不少。5.3 效率不如预期时的优化方向事务层调通后最关心的就是吞吐了。如果SRIO线速率5Gbps但实际读写吞吐只有两三Gbps先别急着怀疑IP核——问题大概率出在未完成事务数不够。SRIO全双工吞吐要充分发挥必须让多个事务同时在线上飞行也就是保持足够多的未完成读请求。工程里tID队列深度是16如果每次只发一个NREAD往返延迟下吞吐一定上不去。把队列深度提升到16之后同样的链路条件下读吞吐大约能提升到单事务模式的6倍左右。另一个容易被忽略的是包长度。SRIO单包最大有效负载是256字节但IP核默认配置可能把最大负载限制在64字节。如果你一直用默认配置链路上每个包都只有64字节包间隔和包头开销会占掉一大截有效带宽。修改IP核的maximum payload size为256字节配合SWRITE突发传输实测下来吞吐提升非常明显。再有就是中断合并策略。别让每个门铃事务都直接触发一次主机中断中断响应、保存现场、恢复现场的开销会浪费大量时间。工程里让门铃先写进FIFO由中断聚合模块在FIFO达到一定水位或者超时后再统一上报CPU侧处理SRIO事件的效率能提升一个档次。就我个人实际操作中的体会SRIO源码工程调通的标志不光是数据读写得对而是在异常情况下能从维护事务的错误寄存器里快速定位问题能利用未完成事务深度把带宽压榨出来。这三类功能模块合在一起才算是一个真正能落地使用的SRIO通信平台。接下来你可以试着往这个框架里加消息事务或数据流DMA逻辑地址映射和状态机结构都不用推翻重来这也是我建议在这个基础上二次开发而不是另起炉灶的原因。
阅读完成 · 觉得有帮助?
咨询建站