简介:基于FPGA的视音频光端机完整工程代码,面向监控、广播、教育等长距离光纤传输场景,解决多路模拟视频、音频、电话、以太网及开关量信号的多业务复用与实时传输问题,适合通信与FPGA方向的开发者学习、移植或二次开发。压缩包内共198个文件(RAR格式,约1.4MB),以Verilog/VHDL源码、ucf约束文件和xise/prj工程文件为核心,辅以bit/bin/mcs配置、ngc/ncd网表、twr/mrp报告、txt说明及IP核配置等,覆盖发送/接收通路、扰码解扰、DCM时钟管理、电话与以太网接口适配、开关量控制等模块,目录结构完整清晰。已有491人浏览学习,工程经过商业化验证,包含接收端主机/从机、发射端等关键逻辑,以及加扰器、解扰器、DCM配置等可直接复用的代码,可帮助快速理解整机数据流与接口时序,并在此基础上定制功能或移植到其他FPGA平台。
1. 视音频光端机代码:一套基于FPGA的视音频光纤传输完整工程
前两年朋友公司接了一个跨园区安防项目,监控中心到前端摄像点直线距离超过4公里,用网络延长器加交换机做了两级中继,结果画面时不时卡顿、音频对不上口型。最后换了一套视音频光端机方案,一根光纤把视频、音频、控制信号全部串过去,问题直接消失。当时我就想,这种设备的核心到底是怎么实现的?于是花了两周把一套基于FPGA的视音频光端机源码完整拆了一遍。这套代码覆盖了从SDI视频接入、音频提取、自定义帧组装到高速SerDes光口发送的全链路逻辑,适合正在做视频传输设备、监控系统或广播级传输方案的开发者参考。它不是什么黑匣子,就是一段段能够跑仿真、能上板调通的Verilog工程。
2. 技术选型与整体架构:为什么FPGA是光端机主控的最优解
2.1 从输入到光纤:一条完整的信号链路
先看这套光端机代码最核心的数据通路。单看发送端,信号流是这样的:外部SDI或者模拟视频信号进来后,先经过电缆均衡器完成预加重补偿,再进入FPGA内部做解串行化,恢复出并行视频数据和时钟。视频数据在当前工程里以YCbCr 4:2:2格式处理,每像素20比特。与此同时,SDI流里内嵌的音频数据需要单独提取出来,通过ANC辅助数据解析电路分离出AES/EBU音频包,缓存到异步FIFO中。视频和音频两路数据最后汇聚到帧组装模块,按行交织的方式打包成自定义的光传输帧,再交给高速收发器做并串转换,最后通过SFP光模块发送到光纤上。
接收端本质上是发送端的逆过程:光模块接收信号,高速收发器完成CDR时钟恢复与串并转换,帧解包模块分离视频和音频,视频经过FIFO做时钟域跨越恢复出SDI时序,音频则重新嵌入到SDI的辅助数据区。整个架构是典型的对称式设计,一个FPGA器件同时承担发送和接收两套逻辑,这在声道方向可配置的单芯光端机里是常见做法。
了解完数据走向,再看这份工程代码目录,结构是这样组织的:
fpga_av_optical_transceiver/ ├── src/ │ ├── top/ │ │ └── top_av_transceiver.v # 顶层模块,例化收发两端 │ ├── video/ │ │ ├── sdi_rx_wrapper.v # SDI解串器封装与时序对齐 │ │ ├── pixel_align.v # 像素对齐和行场消隐生成 │ │ └── video_fifo.v # 跨时钟域视频缓存 │ ├── audio/ │ │ ├── anc_decoder.v # SDI辅助数据包解析 │ │ └── audio_fifo.v # 音频样本异步FIFO │ ├── link/ │ │ ├── frame_assembler.v # 发送端帧组装 │ │ ├── frame_disassembler.v # 接收端帧解包 │ │ └── gearbox.v # 位宽变换适配收发器 │ └── phy/ │ └── serdes_wrapper.v # 高速收发器例化封装 ├── ip/ │ ├── pll_core/ # 时钟管理 │ └── gt_serdes/ # 高速收发器IP核 ├── constraint/ │ ├── pins.xdc # 引脚约束 │ └── timing.xdc # 时序约束 └── sim/ ├── tb_top.v # 顶层测试平台 └── coef/ # 测试用的视频音频样本2.2 为什么选FPGA而不是DSP或ASIC
做视音频光端机,方案其实就三条路:DSP加外部串行器、专用ASIC光端机芯片、FPGA自行逻辑实现。前两条路在市面上成品芯片里很常见,但做产品的开发者选FPGA的原因不外乎三点。
第一是接口匹配灵活。SDI接口的标准从SD-SDI到3G-SDI再到6G-SDI,速率逐代翻倍,使用ASIC芯片基本是被芯片规格绑死的。而FPGA内的高速收发器可以配置成不同的线速率,同时兼容多种视频格式,换一个器件或者改几行约束就能支持新规格。第二是系统集成度高。光端机不只是传视频,往往还要同时传音频、RS232/RS485控制信号、甚至以太网数据。这些在FPGA里面都是现成的逻辑资源,不需要额外挂MCU做协议转换。第三是延迟可控。广播级传输对端到端延迟要求是小于一帧,FPGA全硬件流水线处理能做到微秒级固定延迟,而DSP方案受制于软件处理节奏,通常很难做到。
当然FPGA也有代价,开发周期比用现成芯片长得多,时序收敛和高速收发器调试都需要经验。如果产品只做一个固定格式、固定方向的光端机,用ASIC方案更快;需要灵活配置或者多业务复用的,FPGA几乎是最优解。这份代码用的是中端主流FPGA器件,资源和性能刚好卡在3G-SDI单芯双向这个档位上。
2.3 工程内各模块的分工协作
把顶层打开来看,发送端和接收端的信号走向非常清晰。发送部分从SDI_RX进来,依次经过解串、像素对齐、锁存到视频FIFO,同时音频走ANC解析后进音频FIFO。两路数据在帧组装模块里按固定的字格式排布,然后进入gearbox把位宽从20比特扩展到32比特,适配高速收发器的数据接口。接收部分相反,收发器出来32比特数据先经过comma对齐和字对齐,然后送帧解包模块按帧头查找边界,拆出视频和音频分别送入对应的FIFO,最后视频经过一个重建模块恢复出行场同步时序,音频则按SDI协议嵌入到辅助数据区,随SDI输出一起发送出去。
这里有两个值得注意的设计点。一个是在视频FIFO和音频FIFO都使用了独立读写时钟的异步FIFO,因为视频时钟和光口恢复时钟来自不同的PLL域,不做跨时钟域处理必然出现偶发花屏。另一个是帧组装模块内部有一层轻量级的剝离保护,每个帧都有帧头、行号和校验字,接收端找不到帧头时不会盲目输出数据,而是保持静默直到重新锁定,避免丢帧时输出乱码。
3. 核心模块拆解:从SDI解串到光口发送的完整链路
3.1 SDI视频解串与像素对齐:把串行比特流变成标准时序
SDI信号进来是一路串行差分信号,首先要经过均衡器和FPGA的高速收发器将其恢复成并行的数字视频流。在当前代码里,解串器输出的数据宽度是20比特,对应YCbCr 4:2:2格式的单个像素。但这个并行数据并不是按像素整齐对齐的,因为SDI传输过程中字边界可能处于任意位置,所以需要做像素对齐处理。
module pixel_align ( input wire clk_hd, // 74.25MHz 像素时钟 input wire rst_n, input wire [19:0] sdi_data_in, // 来自解串器的20bit并行数据 input wire data_valid_in, // 解串器数据有效标志 output reg [19:0] pix_data_out, // 对齐后的像素数据 output reg pix_valid_out, // 像素有效 output reg hsync_out, // 行同步 output reg vsync_out // 场同步 ); // SDI的行基准码EAV/SAV是三字节固定序列:0x000 0x3FF 0x3FF // 检测到该序列后,调整数据对齐窗口 reg [19:0] shift_reg [0:2]; wire is_trs = (shift_reg[0] == 20'h00000) && (shift_reg[1] == 20'h3FFFF) && (shift_reg[2] == 20'h3FFFF); always @(posedge clk_hd or negedge rst_n) begin if (!rst_n) begin pix_valid_out <= 1'b0; pix_data_out <= 20'd0; end else if (data_valid_in) begin shift_reg[0] <= sdi_data_in; shift_reg[1] <= shift_reg[0]; shift_reg[2] <= shift_reg[1]; // 检测到TRS基准码后,下一拍进入有效视频区 if (is_trs) begin pix_valid_out <= 1'b1; end // 根据TRS中的第4个字判断行场消隐状态 // bit5=场标志,bit4=行标志,bit3=有效视频标志 if (shift_reg[2][3] == 1'b1) begin pix_valid_out <= 1'b0; end pix_data_out <= sdi_data_in; end end // hsync/vsync由TRS字中的状态位解码得出 // 这里简化为从对齐后的数据中提取消隐标志 always @(posedge clk_hd or negedge rst_n) begin if (!rst_n) begin hsync_out <= 1'b1; vsync_out <= 1'b1; end else if (is_trs) begin hsync_out <= ~shift_reg[2][4]; // 行标志位 vsync_out <= ~shift_reg[2][5]; // 场标志位 end end endmodule这段代码做的事情,通俗讲就是把解串器出来的乱序数据找到正确的“字边界”。SDI标准中每行数据都以TRS基准码开头,只要连续识别到三个特征字0x000、0x3FF、0x3FF,就说明当前已经处在一个已知位置上,后面数据可以按像素边界切分。参数上,clk_hd是74.25MHz,对应1080p30或720p60这档HD-SDI格式,如果你的输入是3G-SDI的1080p60,时钟就应该配成148.5MHz。data_valid_in来自解串器,是数据有效的门控信号,实际工程里还需要配合data_valid_in做不同步时的自动重对齐,传统做法是连续丢失TRS计数超过阈值就强制触发重同步。
这里有一个非常容易踩的坑:直接用解串器的并行输出当作像素数据送给后级逻辑,会发现画面出现规律的斜纹,这是因为字边界差了一个bit或者几个bit。所以做完解串后一定要有像素对齐环节,并且要对TRS的检测加一个“连续N次未检测到才重同步”的迟滞,防止单次误码导致频繁重同步、画面频繁闪断。
3.2 音频提取模块:从SDI辅助数据包中还原AES/EBU音频
SDI内嵌音频的承载方式是辅助数据包,位于行消隐区的特定位置。每个音频包携带一组48kHz采样的音频数据,按3字节一组存放。解析这个数据包是音频提取模块的核心工作。
module anc_decoder ( input wire clk_audio, // 音频时钟 48kHz*512 input wire rst_n, input wire [9:0] anc_data_in, // ANC通道数据 input wire anc_valid_in, // ANC数据有效 output reg [23:0] audio_sample_out, // 24bit音频样本 output reg audio_valid_out // 音频样本有效 ); // ANC包格式:0x000 + 0x3FF + 0x3FF + DID + SDID + DC + 数据... localparam ANC_HEAD1 = 10'h000; localparam ANC_HEAD2 = 10'h3FF; localparam ANC_HEAD3 = 10'h3FF; reg [4:0] state; reg [7:0] data_count; reg [9:0] current_byte; reg [23:0] sample_buf; localparam IDLE = 5'd0; localparam WAIT_HEAD2 = 5'd1; localparam WAIT_HEAD3 = 5'd2; localparam WAIT_DID = 5'd3; localparam WAIT_SDID = 5'd4; localparam WAIT_DC = 5'd5; localparam DATA_BYTE = 5'd6; always @(posedge clk_audio or negedge rst_n) begin if (!rst_n) begin state <= IDLE; data_count <= 8'd0; audio_valid_out <= 1'b0; end else if (anc_valid_in) begin case (state) IDLE: begin audio_valid_out <= 1'b0; if (anc_data_in == ANC_HEAD1) state <= WAIT_HEAD2; end WAIT_HEAD2: begin if (anc_data_in == ANC_HEAD2) state <= WAIT_HEAD3; else state <= IDLE; end WAIT_HEAD3: begin if (anc_data_in == ANC_HEAD3) state <= WAIT_DID; else state <= IDLE; end WAIT_DID: begin // DID=0x2F表示音频数据包;0x1F表示保留 if (anc_data_in[9:2] == 8'h2F) state <= WAIT_SDID; else state <= IDLE; current_byte <= anc_data_in; end WAIT_SDID: begin // SDID低5位表示音频组编号和扬声器通道 state <= WAIT_DC; end WAIT_DC: begin data_count <= anc_data_in[7:0]; state <= DATA_BYTE; end DATA_BYTE: begin if (data_count == 8'd0) begin state <= IDLE; audio_valid_out <= 1'b1; audio_sample_out <= sample_buf; end else begin data_count <= data_count - 8'd1; // 每3字节组成一个24bit音频样本 case (data_count % 3) 2'b00: sample_buf[23:16] <= anc_data_in[7:0]; 2'b01: sample_buf[15:8] <= anc_data_in[7:0]; 2'b10: sample_buf[7:0] <= anc_data_in[7:0]; endcase end end endcase end end endmodule这段状态机看着简单,实际项目里音频提取的坑都在细节上。ANC包头的检测必须严格匹配连续三个特征字,少一个都不能进入后续解析,现实传输中偶尔会出现单个bit错误,可以考虑加一个容错状态,允许其中一个字有一比特翻转,代价是有极小概率误触发。数据计数data_count取到的是数据区字节数,按照SDI标准通常是音频块大小减去头部的固定值,这里是按实际字节数逐字搬运。DID和SDID两个字段决定了这个包是不是音频包以及对应哪一组的哪两个声道,解析出来以后要跟后续的声道映射表一起用。
音频样本的输出来到audio_fifo时,写时钟是clk_audio这一路,读时钟则是光口发帧的BIT时钟,两者完全异步。这种异步FIFO在FPGA工程里是标准做法,但还有一点需要留意:48kHz音频时钟和视频时钟之间的频率误差会造成FIFO水位缓慢漂移。常见的应对是在帧组装时定期插入或删除音频样本,或者在FIFO水位超过阈值时做静音补偿。代码里没有做这个复杂的速率适配,而是通过控制帧结构里的音频样本数量来吸收抖动,对广播级应用来说这个精度够用。
3.3 自定义光传输帧:把音视频装进一个固定帧结构里
视频是逐行扫描的,音频是固定采样率的,两者要复用到一根光纤上,必须有统一的帧格式。代码采用的做法是把一帧视频按行拆分,每行插入固定数量的音频样本,再加上帧头、行号、校验字,组成一个定长的传输帧。
module frame_assembler ( input wire clk_tx, // 光口发送时钟 input wire rst_n, input wire [19:0] video_data_in, // 视频像素数据 input wire video_valid_in, // 视频有效标志 input wire [23:0] audio_data_in, // 音频样本 input wire audio_valid_in, // 音频样本有效 output reg [31:0] frame_data_out, // 32bit光口数据 output reg frame_valid_out // 数据有效 ); localparam FRAME_SYNC = 32'hA5C3_5A3C; // 帧同步字 localparam FRAME_RESV = 32'h0000_0000; // 保留字 reg [7:0] line_num; reg [15:0] video_word_count; reg [15:0] audio_word_count; reg in_video_region; reg [31:0] tx_buf; // 每行最多携带16个音频样本,之后全部填视频数据 always @(posedge clk_tx or negedge rst_n) begin if (!rst_n) begin frame_data_out <= 32'd0; frame_valid_out <= 1'b0; in_video_region <= 1'b0; line_num <= 8'd0; end else begin // 行起始:发送帧头 + 行号 + 音频数据区 if (video_valid_in && !in_video_region) begin in_video_region <= 1'b1; video_word_count <= 16'd0; audio_word_count <= 16'd0; line_num <= line_num + 8'd1; frame_data_out <= FRAME_SYNC; frame_valid_out <= 1'b1; end // 音频数据区:先发音频样本 else if (in_video_region && audio_valid_in && audio_word_count < 16'd16) begin frame_data_out <= {8'h00, audio_data_in}; frame_valid_out <= 1'b1; audio_word_count <= audio_word_count + 16'd1; end // 视频数据区:填满视频像素 else if (in_video_region) begin if (video_valid_in) begin frame_data_out <= {12'h000, video_data_in}; frame_valid_out <= 1'b1; video_word_count <= video_word_count + 16'd1; // 每行视频数据量为2200字(1080p30一行) if (video_word_count >= 16'd2200) in_video_region <= 1'b0; end else begin frame_valid_out <= 1'b0; end end else begin frame_valid_out <= 1'b0; end end end endmodule这个帧结构设计有一个关键取舍:音频优先还是视频优先?代码里在每行开始先把最多16个音频样本插入到视频数据前面,保证音频不会被大流量视频数据饿死。因为音频样本率是48kHz,每帧视频(50/60帧)大约要传800个音频样本,分摊到每行约15到16个,所以这里固定预留16个位置。如果你改成了8路音频通道,每行的音频样本数要翻倍,帧结构里的FRAME_RESV保留字就是给后续扩展预留的。接收端解帧时按帧头和行号来恢复行结构,与发送端的行号必须严格对应。
有一点需要注意:代码中的video_word_count上限2200是基于1080p的每行总样本数,换720p格式要改成1650,换1080p60要改成2640。这个参数属于格式相关参数,工程里个别的做法是放在头文件里用宏定义统一管理,避免散落在各模块里不好改。
3.4 高速收发器与Gearbox:把并行数据搬上光纤
光口发送的最后一公里是把32比特的并行数据通过高速收发器变成串行比特流。FPGA内的收发器一般支持多种协议模式,代码里切换到了自定义原始模式,直接使用transceiver的复位、TX数据接口和状态指示信号。
module serdes_wrapper ( input wire clk_ref, // 参考时钟 148.5MHz input wire rst_n, input wire [31:0] tx_data_in, // 待发送的32bit并行数据 input wire tx_valid_in, // 发送数据有效 output wire tx_p, // 差分发送正端 output wire tx_n, // 差分发送负端 output wire tx_ready, // 发送通路就绪 output wire rx_ready, // 接收通路就绪 output reg [31:0] rx_data_out, // 接收到的并行数据 output reg rx_valid_out, // 接收数据有效 input wire rx_p, // 差分接收正端 input wire rx_n // 差分接收负端 ); // 收发器IP核实例化(具体名称按厂商核而定) // 参数:线速率3Gbps,参考时钟148.5MHz,数据宽度32bit wire tx_rst_done, rx_rst_done; wire [31:0] rx_data_raw; wire rx_slip; // 实例化厂商收发器IP // 拿到tx_rst_done和rx_rst_done后组合出ready信号 assign tx_ready = tx_rst_done; assign rx_ready = rx_rst_done; // 接收数据直接送出,字对齐交给gearbox模块处理 always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) begin rx_data_out <= 32'd0; rx_valid_out <= 1'b0; end else begin rx_data_out <= rx_data_raw; rx_valid_out <= 1'b1; end end endmodule高速收发器的调试向来是光端机项目里最玄学的环节。现象往往是复位时序不对,表现为收发器偶尔能起来偶尔起不来,或者起来后传输几分钟就掉链子。需要强调的复位顺序是:先给收发器参考时钟,等到PLL锁定后再释放收发器自身的复位,最后等tx_rst_done和rx_rst_done拉高。工程里常见做法是在复位模块里维护一个状态机,按参考时钟锁定、收发器复位、弹性缓冲使能、用户逻辑复位这样的顺序依次释放。如果把用户逻辑复位跟收发器复位绑在一起,大概率会碰到初始化竞争问题。
收发器接口的数据宽度和帧组装模块的32比特是匹配的,但实际传输中收发器内部还有8B/10B编码选项。这份代码没有启用8B/10B,直接传原始数据,好处是带宽利用率100%,代价是DC平衡变差,对光模块的抗扰要求更高。如果传输距离超过20公里,更稳妥的做法是启用8B/10B或者64B/66B编码,代价是有效带宽下降20%或3.125%,但长期可靠性明显更好。这个取舍看你产品的传输距离定位。
4. 运行与调试:从仿真验证到上板调通的全过程
4.1 搭建仿真环境:用测试平台验证核心逻辑
拿到代码后第一步不是急着上板,而是先在仿真环境里把发送端和接收端的逻辑跑通。工程sim目录下已经有完整的测试平台,但还是要根据自己的EDA工具调整一下文件路径。测试平台做的事情很简单:产生一路带TRS基准码的模拟SDI视频流和一路生成的音频数据,送入顶层,再把发送端输出的串行数据直接回环到接收端,验证数据经过完整链路后端到端是否一致。
module tb_top; reg clk_ref; reg rst_n; wire tx_p, tx_n; wire rx_p, rx_n; wire [19:0] video_out; wire audio_valid_out; wire [23:0] audio_out; // 时钟:148.5MHz视频时钟 initial begin clk_ref = 1'b0; forever #3.367ns clk_ref = ~clk_ref; end // 复位:上电后延迟100ns释放 initial begin rst_n = 1'b0; #100ns rst_n = 1'b1; end // 光口回环:发送端直接连接接收端 assign rx_p = tx_p; assign rx_n = tx_n; // 待测顶层 top_av_transceiver dut ( .clk_ref(clk_ref), .rst_n(rst_n), .tx_p(tx_p), .tx_n(tx_n), .rx_p(rx_p), .rx_n(rx_n), .video_out(video_out), .audio_valid_out(audio_valid_out), .audio_out(audio_out) ); // 数据校验:计数视频像素和音频样本 integer video_count, audio_count; always @(posedge clk_ref) begin if (dut.frame_assembler_inst.frame_valid_out) video_count = video_count + 1; if (audio_valid_out) audio_count = audio_count + 1; end // 打印结果 initial begin #200us; $display("Video words: %0d, Audio samples: %0d", video_count, audio_count); if (video_count > 0 && audio_count > 0) $display("Loopback test PASSED"); else $display("Loopback test FAILED"); $finish; end endmodule仿真跑之前要确认两件事。第一,厂商收发器IP核的仿真模型是否存在,通常需要在EDA工具里额外编译一遍secureip或者unisim库才可以在仿真中例化。第二,代码里所有异步FIFO用的是不是标准库,如果是厂商的原语,仿真也需要做库映射。这些准备工作不做,一上来跑仿真大概率报一堆找不到模块的错误。
仿真结果的判断指标有三项:一是视频像素计数是否等于预期值,比如1080p一帧画面的像素总数约207万个,发送和接收的值应该完全一致;二是音频样本数量是否稳定,48kHz采样率下每帧视频对应约800个样本;三是帧头同步状态机是否能锁定,观察接收端frame_lock信号是否拉高。如果回环测试三项都通过,可以认为核心逻辑功能上是通的。
4.2 引脚约束与时钟规划:别在综合环节留下暗坑
仿真通过后进入综合和实现阶段。这个环节最常见的灾难是把pins.xdc里的引脚约束写错,导致布线后信号质量不过关。约束文件的重点有三个区域:SDI输入引脚需要约束为差分对并注意是否经过均衡器,光模块的收发引脚要约束到高速收发器的专用引脚上,而时钟引脚必须进入全局时钟网络。
# 引脚约束示例 set_property PACKAGE_PIN AA4 [get_ports sdio_rx_p] set_property PACKAGE_PIN AB4 [get_ports sdio_rx_n] set_property IOSTANDARD LVDS [get_ports sdio_rx_p] set_property PACKAGE_PIN AA6 [get_ports sfp_tx_p] set_property PACKAGE_PIN AB6 [get_ports sfp_tx_n] set_property IOSTANDARD LVDS [get_ports sfp_tx_p] # 时钟约束:参考时钟必须有对应的create_clock create_clock -name clk_ref -period 6.734ns [get_ports clk_ref] # 收发器时钟组约束示例 set_clock_groups -asynchronous \ -group [get_clocks -include_generated_clocks clk_ref] \ -group [get_clocks -include_generated_clocks gt_rx_clk]一个需要重点解释的约束是set_clock_groups -asynchronous,它告诉时序分析工具视频时钟域和光口恢复时钟域不需要做时序收敛。因为这两个时钟本来就不同源,接收端的视频输出时钟是从光口数据里恢复出来的,与本地视频时钟毫无相位关系。如果你不设置这一条,综合工具会对这两个时钟域做跨时钟路径分析,出来一堆时序违例,而实际上这些违例在功能上是无害的。
时钟规划上最值得注意的一点是:高速收发器的参考时钟必须使用高精度晶振,不能用普通有源晶振随便替代。收发器内部的PLL对参考时钟的抖动非常敏感,参考时钟抖动过大的话,光口的眼图质量会明显劣化。工程里常见的做法是选用压控晶振配合时钟芯片实现频率微调,或者至少使用温补晶振。频率准确度不够时,接收端CDR的追踪范围会吃紧,长时间运行容易出现偶发误码。
4.3 上板调试:看什么信号、抓什么波形
上板调试的步骤一般按电源、时钟、复位、数据通路的顺序来做。上电后先用示波器确认各路电源电压和纹波在额定范围内,然后看全局时钟输出是否正常。时钟没问题后就检查复位信号时序,特别是收发器复位完成信号是否已经拉高。
这时候真正好用的调试手段是插入ILA逻辑分析仪探针。在serdes_wrapper模块的输出端、frame_disassembler的帧锁定信号、以及视频FIFO的空满标志上各挂一组探针,然后触发条件设为“帧锁定信号下降沿”,这样一旦出现传输异常就能抓到帧锁定丢失时刻附近的关键波形。用这个方式排查问题,比从头到尾翻代码效率高很多。常见的情况是,抓到的波形显示帧锁定信号周期性跌落,说明接收端字对齐逻辑在持续滑动,问题大概率出在comma检测或gearbox逻辑上;如果帧锁定稳定但视频花屏,就需要看视频FIFO是否频繁出现空满翻转,那说明读写时钟频率失配。
调试光端机时还有一个细微但高频出现的问题:视频信号源校准时,需要在TX端发送彩条信号而不是真实摄像头画面。彩条信号的结构是已知的,波形看起来更直观,更容易定位像素错位的位置。等彩条完全通过后,再切换到实际摄像头信号测链路稳定性。这一先后顺序千万不要反过来,否则一边看图像一边猜问题,会浪费大量时间。
5. 避坑与常见问题排查:光端机FPGA工程里的5个翻车现场
5.1 画面花屏但同步正常
现象:接收端图像能出画面,但画面像被斜着切了一刀,颜色位置错乱,而行场同步是稳定的。
原因:视频数据在像素对齐模块中没有对准字边界。TRS检测成功只代表进入了同步状态,不代表数据从下一个时钟开始就是像素边界对齐的。如果字边界差异了2个比特,就会出现这种斜切式花屏。
解决:在pixel_align模块中加入一个可调延迟链窗口,让数据流在多组延迟选项中动态切换。实现方式是在检测到TRS后,比较当前数据与期望的0x3FF序列的匹配位数,选取匹配度最高的延迟档位并锁定。锁定后的延迟值要保持到下一次TRS丢失为止,不能每个时钟都重新选择。这类问题用仿真一般很难触发,因为仿真模型的时序是理想化的,上板调试时配合ILA抓TRS前后的数据相对位置,一步就能定位。
5.2 音频间歇性爆音
现象:画面一切正常,音频低频段偶发“咔咔”的爆音,时间间隔没有规律。
原因:ANC解码器的状态机对包头的检测过于严格,传输中出现单比特误码就完全丢弃整个音频包,反映到听感上就是样本序列出现缺口。还有一重原因是异步FIFO水位管理不当,当读侧速度快于写侧时,FIFO被读空,输出端补了非音频数据进去。
解决:ANC解码器增加容错逻辑,允许包头三个字的匹配中有一个字存在单比特翻转,这样大部分误码可以被吸收而不是丢包。对于FIFO水位问题,把audio_fifo的读侧加上一个“低于低水位才暂停输出”的枢纽逻辑,避免在临界水位反复横跳。血泪经验是:音频爆音这类问题,不要只盯解码逻辑,把FIFO水位的波形一起抓出来看,大概率是水位锯齿状波动导致的。
5.3 光口TX信号不亮
现象:SFP光模块的发送光功率为零,用光功率计测试完全没有输出,但FPGA侧的数据通路看起来在工作。
原因:高速收发器没有完成初始化,或者参考时钟未锁定。比较隐蔽的一个场景是收发器的参考时钟引脚约束到了普通IO上,导致参考时钟根本进不了收发器内部的PLL。
解决:先检查约束文件里参考时钟是否约束到了MGTREFCLK专用引脚上,再看参考时钟的PLL锁定信号txpll_lock是否拉高。如果锁定信号未拉高,用示波器量参考时钟的波形和频率准确度。很多工程翻车在这一步只是因为晶振焊错或者频率选错,比如原本需要148.5MHz结果贴了个125MHz。锁定之后,还要按先PLL后收发器复位的顺序检查复位逻辑。特别提醒:不同厂商对收发器复位顺序的要求有差异,不要跨厂商照搬代码。
5.4 长时间运行后光纤链路掉线
现象:设备刚上电工作正常,运行几小时或几天后随机出现链路中断,重启后恢复,然后过一段时间又复现。
原因:通常是收发器的CDR追踪范围裕量不足,叠加环境温度变化导致参考时钟频率漂移,最终超出CDR的锁定范围。另一种高频因素是收发器复位逻辑里缺少“自动复位”机制,瞬态干扰导致收发器进入异常状态后无法自愈。
解决:在顶层增加一个链路监视模块,周期性检查rx_rst_done和帧同步状态。如果发现异常,先软复位接收通路,再不行就整体复位收发器。这个自愈机制对无人值守的设备特别重要。光模块的选型也值得复查,不同厂家的SFP模块在灵敏度、功耗和CDR配合上有细微差异,项目定型前要把可能用到的模块型号都做一轮连续72小时老化测试。这种玄学问题,往往最后查出来是模块兼容性而不是FPGA逻辑。
5.5 仿真全部通过,上板一测完全不出图
现象:Testbench回环测试全绿,一上板接真实SDI信号就完全无图,接收端任何信号都没有。
原因:仿真环境里信号源是理想的,没有模拟真实SDI信号的电缆衰减、直流偏置和抖动。上板后最常见的问题是SDI均衡器芯片的配置不对,或均衡器输出没有连接到FPGA的正确引脚。如果接的是3G-SDI信号,而均衡器配置成了HD-SDI模式,信号在进入FPGA前已经失真了。
解决:先用示波器确认均衡器芯片输出端的信号眼图是否清晰,眼图张不开就先查均衡器配置和电源。然后在FPGA内部用一个简单的计数器,确认SDI解串器是否产生了data_valid信号。如果解串器工作正常但无像素输出,再怀疑像素对齐模块的初始状态。调试上板问题时,最重要的习惯是逐个模块排查,不要一上来就改逻辑,先确认每个子模块的输入信号是符合预期的。
6. 二次开发思路:把这套光端机代码改成你想要的形态
拿到这份代码,最常见的改造方向有三个。第一个是把SDI输入换成本地并行视频接口,比如接HDMI前端芯片。这个改造相对直接,把video/目录下pixel_align模块整体替换成HDMI接收芯片输出的并行视频时序即可,重点是把HDMI的DE、HSYNC、VSYNC信号转换成帧组装模块期望的video_valid_in形式。需要注意的是HDMI输入是RGB格式,而帧组装模块是按YCbCr 4:2:2设计的,需要加一个色彩空间转换模块,否则图像颜色会异常偏色。I2C配置、EDID读取这些芯片设置也属于新增工作,难度不大但工作量琐碎。
第二个方向是把单向传输改成双向复用,在一根光纤上同时传视频和以太网数据。这需要重新设计帧结构,把原来预留的保留字段扩展成以太网数据的封装区,并在交织策略上做带宽分配。以太网数据对实时性不敏感,但要求误码后能够重传,所以在解帧端需要加一个轻量级的重传机制。帧结构设计建议从“每行固定插入16个音频样本”改成“按时间片分配带宽”,把链路带宽划分成视频、音频、以太网三个逻辑通道,每个通道有独立的优先级和缓存队列。这也是光端机产品从“传输设备”走向“业务平台”的常见演进路线。
第三个方向是向低成本平台迁移,把代码换到资源更少的中低端FPGA上。这份工程用了高速收发器和较大的FIFO资源,低成本器件往往没有高速收发器,替换方案是外挂独立的SerDes芯片,把光口数据通路改为“FPGA并行数据 + 外部SerDes并串转换”的形态。改造重点是serdes_wrapper模块的接口适配,以及把收发器相关的复位时序改成外挂芯片的控制时序。FIFO资源紧张时,可以把视频FIFO的深度从整帧改成若干行,配合帧同步策略减少缓存压力。
调试上板的时候,我自己的习惯是每改一个模块就重新跑一遍回环测试,再上板验证一次,绝不攒上好几个改动一起验证。因为同时动了几个地方,出了问题根本不知道是哪一个引入的,排查时间会翻倍。还有一个坚持了很久的习惯:每次改完代码都看一眼综合报告里资源使用量和时序裕量的变化,只要某个信号路径的时序裕量下降超过5%,即使当前没有报错也要查清楚原因,多数隐蔽问题这时候就已经露出苗头了。这个习惯帮我挡住了不少后面会反复发作的疑难杂症。希望这些拆解和调试经验能帮到你,让你在自己的光端机项目里少走一些弯路。
本文还有配套的精品资源,点击获取