FPGA Serdes SDR模式实现1_to_7串并转换实战解析

发布时间:2026/10/6 1:10:15
FPGA Serdes SDR模式实现1_to_7串并转换实战解析 1. 项目背景与核心思路如果有人问我FPGA里最容易被低估的硬核资源是什么我大概率会回答Serdes。这玩意儿平时藏在高速接口后面大家只把它当“跑高速的通道”用可是当你真的需要做并行数据宽度变换、对接一些非标准的突发协议时Serdes的灵活程度远超你想象。最近我在一个数据采集板卡的项目里就撞上一个需求传感器输出的高速串行数据需要按7个UI一组打包成并行7bit字再交给后级FIFO和DSP处理。翻遍参考设计最后落在Xilinx官方的XAPP585应用笔记上用它的SDR模式把Serdes配成1_to_7串并转换问题迎刃而解。这里把整个踩坑和实现过程掰开揉碎了讲一遍希望能帮到同样被非标准串并比折磨的人。XAPP585是Xilinx提供给Virtex-5等早期FPGA上RocketIO GTP/GTX收发器的一个参考设计原本作用是把千兆以太网的SGMII接口转换成8bit并行数据但它的核心思想——通过Serdes的SDR模式支持任意整数分频的串并转换——完全可以脱离以太网场景使用。我们项目里数据链路的串行速率是2.45Gbps要求逻辑侧得到350Mbps时钟下的7bit并行数据乍一听不是标准的8b/10b、不是4字节对齐常规的Serdes配法也做不到但XAPP585给的正是这个思路不进PCS的8B/10B编解码也不做标准comma对齐直接把串行数据当原始比特流用SDR模式按想要的位宽“切开”。为什么放着DDR模式不用非要选SDR这也是我最初纠结的地方。7bit字宽在Serdes的世界里其实很尴尬因为大多数Serdes的并行因子是4的倍数或者整数可配。Virtex-5的GTP收发器在SDR模式下内部串并转换的工作方式是把输入时钟2分频后再做N分频因此可以支持比较灵活的比值。配合XAPP585里的相位校准逻辑能够把2.45Gbps一个bit一个bit地存入移位链每攒够7个bit输出一拍。这个能力对非标准位宽通信特别珍贵。整个方案的核心有三块一是Serdes模块的初始化与锁定务必保证RX端恢复到正确的时钟二是1_to_7的模式切换必须让Serdes内部的接收数据宽度寄存器按SDR模式正确设置三是从Serdes拿到的7bit数据不能直接丢给后级因为数据在跨时钟域后可能出现比特顺序“错位”需要一层“对齐调整”逻辑XAPP585里叫word alignment这个才是让7bit数据能用的关键。2. Serdes SDR模式原理解读要理解1_to_7怎么实现得先从Serdes的SDR模式原理说起。绝大多数FPGA的Serdes模块都是为高速串行通信设计的发送端把并行数据变成串行比特流接收端把串行比特流恢复成并行数据。这个过程通常会经过一个8B/10B编解码、通道绑定、comma检测等“标准”处理链路。而在XAPP585的设计里这些全被绕开。它走的是FPGA内部原语早期叫ROCKETIO_WRAPPER后来叫GTP/GTXE1最底层的“裸”模式直接把串行比特流送到一个移位寄存器链由接收时钟的若干分频得到并行采样时钟对齐到想要的bit数后并行输出。SDR和DDR的区别DDR模式下上下沿都采样一个GTX时钟周期取2个bit并行输出通常是偶数位宽比如2、4、8SDR模式下只在上沿采样一个GTX时钟周期取1个bit通过数字逻辑可以很灵活地控制并行宽度。对1_to_7这种7为奇数的转换SDR是唯一合理的路径。为什么不能直接用一个7bit的并行因子参数因为很多Serdes原语自带的并行宽度配置并不支持7这个数Xilinx只有1、2、4、8、16等固定档位好在SDR模式下我们可以自己控制上游分频让采样时钟在每7个bit后产生一个并行“有效”脉冲然后把移位寄存器里的7bit一次性取走。在GTP收发器的内部接收路径上RXCLK是高速恢复时钟假设频率为2.45GHz。如果你在SDR模式下把RXOUTDIV配置设成1那么从串行到并行的基本采样周期就是1个bit。接下来要做的是在这个基础上做一个除法器从2.45GHz时钟里每隔7个时钟拉高一次“load_enable”同时将RX_DATAWIDTH设为7bit。XAPP585里有几个关键模块一个叫serial_to_parallel它本质上是一个右移寄存器串行数据不断移入另一个叫bit_align负责检测数据流的“起点”。很多人会问既然移位寄存器是连续移位的怎么保证7bit的边界正好是我们想要的字节边界这就要靠用户在应用层给到对齐标记。XAPP585的典型做法是在发送端周期性发送一个特殊的同步码型接收端通过识别这个码型把移位寄存器里的7个bit对应的位置“拨正”。以太网的SGMII走的是8b/10bcomma码非常好识别但如果我们自定义协议没有comma码那就要自己在发送数据流里插入前导码。我们项目中给传感器输出的数据设计了帧头0x6A在未对齐之前先滑动搜索这个码型一旦找到就锁定输出相位。这个过程要非常小心因为7bit的字位于不同相位看到的帧头字节可能完全不同必须配合滑动窗机制。1_to_7的“1”指的是输入的1位串行比特流“7”则是并行侧的7位总线。也就是说逻辑时钟降到350MHz2.45GHz/7数据总线上每次出现一个7bit向量。从带宽角度看2.45Gbps ÷ 7 350M word/s这个字速率正好可以接到后级AXI-Stream之类的总线上后端处理按350MHz随路时钟采样即可。这里还要顺带提醒一句SDR模式下由于采样边沿较大对PCB走线和参考时钟质量更敏感。别以为SDR比DDR慢就越容易实际上因为每位只靠单沿采样恢复时钟的抖动容忍度更低XAPP585里也专门提到要预留时钟数据恢复的裕量这一点我们在调试时感触很深。3. 1_to_7串并转换的RTL实现细节XAPP585的完整工程在Xilinx官网上能下载但用起来并不像普通IP那样双击生成。它是基于Virtex-5 RocketIO的老工程放到新系列芯片上往往要修改。我这里给出一个基于XAPP585思路、又不依赖具体芯片原语写法差异的伪代码级实现剥掉平台细节这样大家换平台也能看懂。首先是串并转换核心模块。这里的关键是生成一个7分频的“并行使能”信号。我在实际项目中写了一个计数器在高速恢复时钟域里从0数到6然后回到0。每当计数到6时置位一个脉冲enable表示此刻移位寄存器里的数据是一个完整字。同时在enable信号的下一拍将移位寄存器并行读出。reg [6:0] shift_reg; reg [2:0] bit_cnt; wire load_en; always (posedge rx_clk_high or negedge rst_n) begin if (!rst_n) bit_cnt 3d0; else if (bit_cnt 3d6) bit_cnt 3d0; else bit_cnt bit_cnt 1b1; end assign load_en (bit_cnt 3d6); always (posedge rx_clk_high or negedge rst_n) begin if (!rst_n) shift_reg 7d0; else shift_reg {shift_reg[5:0], rx_data_in}; end reg [6:0] parallel_out; always (posedge rx_clk_high or negedge rst_n) begin if (!rst_n) parallel_out 7d0; else if (load_en) parallel_out shift_reg; end这个代码看起来简单但实际操作里有个大坑load_en信号在高速时钟域只持续一个时钟周期你必须保证后续的采集逻辑在这个脉冲处稳定采样。如果你的后级逻辑时钟就是rx_clk_high直接分频出来的那么load_en的建立/保持时序可能特别紧张建议用(* ASYNC_REG TRUE *)打两拍同步同时使用FIFO做跨时钟域缓冲而不是直接让后级逻辑采这个脉冲。再说说7分频计数器。如果你是按照XAPP585原始方案改的会发现它里面用的是rxclk的一个四分频信号再计数的因为SDR模式有时会让RXUSRCLK变成串行时钟的一半。实际上在GTP原语配置里TXOUTCLK和RXOUTCLK的频率设置决定了用户时钟与串行时钟的比例。如果设置RXOUTDIV1那么用户时钟就是2.45GHz逻辑根本跑不动所以通常会把RXOUTDIV设为2即用户时钟1.225GHz再在FPGA逻辑里做3.5分频得到350MHz的并行使能。由于分频值是分数所以要用“7个高速周期内输出一次350MHz字”的时序而不是简单二分频。我的做法是直接用1.225GHz的时钟作为主时钟这样每个高速时钟对应2个串行bit串并转换要攒7个bit就得跨3.5个时钟周期代码里就用一个4bit计数器在每个时钟沿采样2bit攒满7bit后load一次。这样写逻辑其实更贴合XAPP585的推荐。关键的原语配置上收发器的RX_DATA_WIDTH我们必须设为7RX_DFE之类的均衡参数按速率调整。在GTX/GTH上需要关注的还有RXOUT_DIV、RXUSRCLK_DIV等。另外因为不做8B/10BPCS部分要把RX_COMMA_DET_ENABLE关掉否则它会误检测逗号码然后强制对齐破坏我们的自定义边界。除了RTL逻辑时序约束是成败关键。Serdes的用户时钟往往不在系统时钟树上必须明确设置create_generated_clock。我通常这样约束create_generated_clock -name rx_clk_high \ -source [get_pins gt_inst/RXOUTCLK] \ -divide_by 1 [get_pins gt_inst/RXOUTCLK] create_generated_clock -name rx_en_clk \ -source [get_ports custom_clk] \ -divide_by 1 -add \ -master_clock [get_clocks rx_clk_high] [get_pins div_7_reg/C]约束的核心目的是让综合工具知道你所谓350MHz其实是“350MHz的脉冲串”不是真正的均匀时钟所以建议把所有后级逻辑都放在rx_clk_high域只是通过使能门控而不是创建两个物理时钟。如果直接创建350MHz时钟让工具去收敛基本是过不了的。因为数据变化不是均匀分布在每个周期而是在load_en后的那一拍变化。最好的方式是让后级逻辑全部跑在rx_clk_high上用寄存器使能来处理7bit数据的节拍。这样时序约束就是一组普通的单时钟约束DFF的CE脚非常可靠。4. 仿真与上板调试过程我在测试时先写了一个简单的testbench给一个固定串行数据流模拟传感器在连续发送帧头加数据。仿真时最容易犯的错误是直接把并行数据期待成“从移位寄存器并行输出第一个bit为最高位”的形式。但实际上1_to_7串并转换出来后bit顺序和串行流里谁先谁后完全是反着的。比如串行流依次到来是bit0、bit1……bit6如果你期望输出7bit的二进制数值为{bit0,bit1,bit2,bit3,bit4,bit5,bit6}而你代码里写的是new_data {data, new_data}之类那么最终出来的数值位宽顺序就会反转。我建议在仿真阶段专门写一个参考模型把串行比特流一位位送进一个延迟线再按同样的时钟脉冲取出来和RTL输出做比对。一旦不一致先判断是相位问题还是位顺序问题。仿真过程中还有一个典型的“假失败”因为我们在load_en后并行输出而后级FIFO的写使能信号和写数据之间存在一个周期的相位差。如果testbench直接在同一时刻断言FIFO里的数据看起来就是差一拍。我习惯在仿真脚本里把输出打拍后延迟检查或者干脆等数据稳定两拍后再采样。这不是设计bug而是仿真的惯性思维问题。上板调试阶段我建议用ILA观察高速链路的数据。你可以把触发条件设为检测到帧头0x6A然后抓取并行数据和valid信号。注意ILA采样时钟也应该用rx_clk_high而不是某个分频时钟否则会采到毛刺。第一次抓波形时很大概率看到并行数据“缺位”——也就是7bit里可能只出现6个有效bit或者输出数据为0x01、0x03这种明显错位的数值。这通常是相位对齐没完成也就是word alignment没生效。这时候问题不在分频器而在“对齐模块”。XAPP585里的对齐模块是核心中的核心。我在实现时借鉴了它的状态机初始状态为搜索不断从当前的7bit输出中提取一个比较窗和帧头模板比对。如果不等就在移位寄存器层面“跳过”一个bit再试如果连续N次相等就认为对齐并进入锁定状态。在自定义数据格式里必须注意帧头要能够区分相位。如果用0x6A二进制1101010做帧头它需要满足一个条件无论从哪个bit开始截取窗内都不会循环得到相同的无效匹配。比如某些重复码型0x55就不行因为0x55循环移位后仍然在同一个7bit窗内得到0x55这样无法判定边界。我最终选定的0x6A经过验证7种相位下只有正确的相位能匹配上且和后面数据的组合也不会产生伪匹配。基于实际经验帧头长度最好等于并行宽度或者更长但复杂度会高。调试过程中还有一个让我印象极深的坑GTP的RX复位顺序。如果RX复位不完全接收时钟虽然锁定但内部串并转换可能仍然处于错误状态尤其当SDR模式下PMA复位和PCS复位没有按正确顺序释放时输出数据可能“反复跳变”。我用的顺序是先复位整个收发器等待PLL锁定然后释放PMARX复位等待rxbyteisaligned置位最后释放RX复位。在SDR模式下接收字节对齐信号可能不直接反映我们的7bit边界必须用自定义对齐状态机去替代。这是我踩了很久才理解到的点——不要依赖原生信号要自己接管。5. 常见问题与排查技巧实录为了让大家少走弯路我把整个过程中遇到的典型问题和对应解决办法整理成一个排查表。这个表我每次做类似接口都会打开看一眼省了不少时间。现象可能原因排查方法解决手段输出全0或全1Serdes未锁定RX复位未释放检查RXRESETDONE、PLL锁定状态检查参考时钟按正确顺序复位并行数据错位不稳定未做word alignment观察连续帧头是否一致添加滑动对齐模块搜索帧头码输出数据少一个bitSDR模式下用户时钟分频配错用计数器对比每7个bit输出一次修正RXOUT_DIV和用户除法寄存器数据偶发跳变跨时钟域采集导致亚稳态做同步打拍用FIFO缓存插入两级同步器后级全部用使能采样帧头偶发误锁帧头码型不具备唯一性仿真滑动窗检查各相位混淆换用循环移位更不友好的码型高温或长运行后失步时钟恢复单元失锁检查GT参考时钟稳定性减小环路带宽增加复位检测机制后级逻辑时序不过多时钟约束混乱查看时序报告检查分频时钟用单时钟使能设计替代真实分频除了表格里的问题我想再单独说说“SDR模式下的输出数据位序”的困惑。很多官方示例里8bit并行输出的第一个bit往往是串行数据的MSB但在我们自定义7bit时因为时钟分频和移位方向的差异可能恰好相反。我在工程里统一用的准则是“最先进入移位寄存器的bit放在最高位”换句话说在load_data时直接用shift_reg的当前值就能满足这个约定。但到最后发现后级协议需要的是“字节内最低位先发送”这时候还得做一次位序翻转。别把这种翻转丢在Serdes模块里做建议显式在数据通路里加一个组合逻辑方便后面随时调整assign parallel_reversed {parallel_out[0], parallel_out[1], parallel_out[2], parallel_out[3], parallel_out[4], parallel_out[5], parallel_out[6]};从排查经验的整体看问题最集中的还是“对齐”环节。我后来在代码里加了一个看门狗在锁定状态下每隔一段时间检查帧头一旦连续出现几次错误就认为失步自动重新进入搜索状态。这个机制非常实用因为Serdes在真实环境中受到电源噪声、温度漂移的影响偶尔会跳一个bit有了这个自动重同步功能整个链路在长时间压力测试中的可靠性明显提高。6. 收尾与实用建议最后再分享一个小技巧如果你要做1_to_N这类非标准串并转换不要一开始就盯着官方代码里那一堆复杂的状态机。先理清三个问题——数据从哪个时钟域来、字边界用什么码型定义、并行输出送给谁。把这三个问题想清楚再回头去看XAPP585的原始代码就会觉得那些模块其实都围绕这三个问题在转只是它用了以太网场景来包装。读懂以后换到自己场景里无非是把SGMII的数据和comma码换成你的协议帧头而已。我建议任何要做类似功能的人都先在仿真环境里把对齐状态机和相位搜索跑通再上板调GTP。不要为了省时间直接去调硬件那样你很难区分是时序问题还是逻辑问题。同时在逻辑里尽量把收发器的原语包一层自己定义的wrapper把信号名统一改成rx_serdes_data、rx_word_valid、rx_aligned这类语义明确的名称后续维护的人看了不累自己隔一个月再回来看也能快速上手。如果你和我一样手上的串行数据是纯突发、无编码的自定义格式XAPP585的这套SDR自定义对齐方案几乎是最直接有效的手段。它的核心价值不在于“串行转并行”而在于“用任意整数位宽去切任意速率的串行流”。虽然现在新片子的原生支持更强了但我依然觉得把SDR模式的原理吃透能帮你应对未来各种奇奇怪怪的协议需求。