FPGA视音频光端机工程:一芯光纤同时跑视频音频的完整实现 简介基于现场可编程门阵列FPGA的视音频光端机完整工程代码适合FPGA开发工程师、视频监控系统集成人员与通信设备研发者参考用于解决长距离多路视音频信号实时传输和系统集成问题。代码经实际部署验证并成功商业化覆盖多路模拟视频输入适配、多路音频编解码、电话接口、以太网通信及开关量控制等核心功能内部模块划分清晰接口约束、时钟配置与编译脚本一并提供可直接作为二次开发或产品改版的基础。资源共198个文件以HDL源码、Xilinx工程约束与工程文件、综合映射报告、配置固化文件bit/bin/mcs为主压缩包约1.4MB体量紧凑但目录结构完整适合快速导入仿真与上板调试。已有491人学习下载对正在设计光纤视音频传输链路或研究多协议集成的开发者这套代码既能提供稳定的功能框架也能给出芯片级排错思路尤其适合搭建监控级视音频光端机原型。1. 视音频光端机代码这套FPGA工程能让你一芯光纤同时跑视音频某次现场活动机位在舞台最里面导播间在距离两百米外的音控室。HDMI 线最长只能拉十几米用网络延长器又怕延迟和兼容性问题。最后换上基于 FPGA 的视音频光端机代码发送端把摄像机输出的视频和音频打成数据帧混入 RS232 控制通道用一根单模光纤送到接收端画面和声音几乎零延迟。这套代码不是常见的上位机或单片机程序而是一整个可综合、可上板的 FPGA 工程覆盖从视频输入采集、音频嵌入、帧复用、8B/10B 编码到高速收发器输出光信号的完整链路。适合准备做长距离视音频传输设备的硬件工程师也适合想把 FPGA 高速串行接口彻底弄明白的开发者。下面按架构设计、核心模块、踩坑记录和改造方法四个部分拆开讲。2. 系统架构与目录结构一条光纤里怎么装下视频、音频和辅助数据2.1 三层架构物理层、链路层、业务层各管什么这个工程在顶层划分上很有代表性把整个系统拆成业务层、链路层、物理层三层。业务层负责用户能感知的内容视频像素流、音频采样、辅助控制字符。它完全不关心数据是怎么变成光信号的只负责在合适的时钟沿把数据送到下一层的接口上。链路层解决数据和物理传输之间的问题包括 8B/10B 编码、帧同步、空闲码插入、错误检测。物理层直接面对光模块和 FPGA 的高速收发器负责并串转换、时钟恢复、CDR 均衡等。三层之间用固定位宽的接口连接。业务层向链路层提供 28 位并行总线其中高 4 位是类型标识低 24 位是有效负载。链路层把 28 位包拆成三个 8 位字节分别做 8B/10B 编码最终交给物理层。这么分层最直接的收益是「可替换性」如果临时更换光模块或者从一路光口扩成两路物理层的收发器配置要改但业务层和链路层几乎不用动。反过来如果要把视频格式从 RGB 改成 YCbCr只需要改业务层链路层照常工作。分层也带来一个明显代价层与层之间要处理时钟域稍不注意就会引入亚稳态。这个工程的做法是层与层之间尽量用同步 FIFO把绝大多数逻辑统一到主时钟域只有物理层与链路层之间才真正存在异步时钟那个口子上用异步 FIFO 加两级同步器。通信设备里标准做法是让每层各管各的时钟但这个工程为了调试方便选择牺牲一点布局灵活性换来明确的时序关系。对光端机这种数据流向固定的场景这种取舍是合理的。2.2 工程目录结构每个文件负责什么拿到这个工程第一件事先看目录。src/tx 和 src/rx 分别放发送端和接收端逻辑这样的划分比按模块堆在一堆更容易维护因为光端机本质上是两个方向的对称设计改接收端时不会误碰发送端代码。fpga_av_oltx/ ├── src/ │ ├── tx/ │ │ ├── video_capture.v │ │ ├── audio_embed.v │ │ ├── mux_frame.v │ │ ├── encoder_8b10b.v │ │ └── gig_tx.v │ └── rx/ │ ├── gig_rx.v │ ├── decoder_8b10b.v │ ├── demux_frame.v │ └── video_output.v ├── ip/ # 时钟向导、高速收发器IP ├── sim/ │ └── tb_top.sv ├── constraints/ │ ├── top.xdc │ └── timing_ddr.xdc └── scripts/ └── build.tcl发送端的 video_capture.v 负责把外部并行视频数据和数据使能信号打拍对齐并拼上同步标志。audio_embed.v 把来自 I2S 或 PDM 接口的音频采样打包按「行消隐」的时机插入到视频流空隙。mux_frame.v 是组帧核心决定当前总线上传的是视频包、音频包还是空闲同步包。encoder_8b10b.v 实现查表式 8B/10B 编码保证直流平衡和时钟恢复。gig_tx.v 是高速收发器发送端的封装对外只暴露极简的复位和使能接口。接收端对应的模块是 gig_rx.v、decoder_8b10b.v、demux_frame.v 和 video_output.v逻辑上正好是发送端的逆过程。video_output.v 除了恢复视频和音频之外还要按原来的时序输出数据所以里面放了一个输出 FIFO 和一套场同步信号生成器。sim 目录里只有一个 tb_top.sv 测试平台它不是给各个模块单独喂激励而是通过一个参数化的链路模型把发送端和接收端串起来中间可以注入误码、插入毛刺、模拟频差这样在综合之前就能把整条数据通路跑通。这个平台的价值在实际排错时特别明显——上板后遇到问题先回到 tb_top 复现比拿示波器戳引脚快得多。constraints 目录下分两个 xdc 文件top.xdc 里面是管脚位置和电平标准timing_ddr.xdc 名字带 DDR实际是约束工程内部跨时钟域的同步逻辑尤其是异步 FIFO 读写指针路径。新手喜欢把所有约束塞进一个文件工具也认但后期维护会发疯。这个工程拆开是很好的习惯。scripts/build.tcl 负责自动化编译在 CI 里调用综合、布局布线并生成时序报告。脚本里有两个关键参数FPGA 器件型号、工具版本路径。因为不同版本工具工程数据库格式有差异脚本会先清理中间文件再重建避免旧缓存干扰。2.3 关键接口与时序关系发送端和接收端怎么握手业务层和链路层之间的主要接口可以整理成一张表信号名方向位宽说明video_data输入24并行像素数据RGB 或 YCbCrvideo_de输入1像素数据有效使能高有效video_clk输入1像素时钟由视频源提供audio_data输入16音频采样值单通道最多 16 位audio_vld输入1音频采样有效标志aux_data输入8RS232 或 GPIO 辅助数据tx_frame_out输出28链路层打包后的码字tx_frame_vld输出1码字有效使能这个接口设计的核心是 video_de 对齐问题。video_data 和 video_de 必须在 video_clk 的上升沿同时被链路层看到任何一方提前或延后半拍都会被当成不同像素。因此 video_capture 里用了与数据路径完全相同的打拍级数来做 de 延迟这不是随手习惯而是经过时序检查的强制要求。audio_data 与 video_clk 本来是异步来源audio_embed 模块内部先做一个异步 FIFO把音频采样跨到视频像素域再等待行消隐窗口插入。这里不能直接把音频信号接到组帧器否则组合逻辑会稳定地输出竞争值。tx_frame_vld 是链路层的写请求跟 tx_frame_out 同步变化。mux_frame 在每个时钟周期更新 frame_vld不产生毛刺。为了让接收端能定位帧边界链路层每帧开头会插入一个或多个同步码字这样即使发生误码接收端也能在若干时钟周期内重新锁定。3. 核心模块拆解一行视频流水线如何变成一束激光3.1 视频输入采集让并行视频数据稳稳进入内部逻辑发送端最前面的 video_capture.v 做的事情可以概括成一句话外部视频源的数据是自由运行的而内部组帧逻辑需要对齐的、带使能的数据问题在怎么用打拍解决。module video_capture #( parameter DW 24 )( input wire rst_n, input wire video_clk, input wire [DW-1:0] video_data, input wire video_de, output reg [DW:0] mux_wr_data, output reg mux_wr_en ); reg [DW-1:0] data_buf; reg de_dly1; reg de_dly2; always (posedge video_clk or negedge rst_n) begin if (!rst_n) begin data_buf {DW{1b0}}; de_dly1 1b0; de_dly2 1b0; end else begin data_buf video_data; de_dly1 video_de; de_dly2 de_dly1; end end assign mux_wr_data {data_buf, de_dly2}; assign mux_wr_en de_dly2; endmodule为什么要把 de 打两拍因为 data_buf 是 video_data 打一拍后的结果de 信号如果只打一拍与 data_buf 对齐的是「最新的 de」而不是「当时的 de」。这里使用两个触发器 de_dly1 和 de_dly2让 de 比数据晚一个时钟正好和 data_buf 同一拍。mux_wr_data 的最高位就是 de_dly2等于把数据有效标志内嵌进码字下游组帧器看到最高位为 1 就认为当前是视频像素。参数 DW 在工程中使用 24对应 4:4:4 RGB。如果改成 10 位或 12 位采样需要同步调整下游的帧格式以及收发器线速率。有一个容易翻车的地方如果视频源不是像素时钟同步输出而是突发 burst必须在 video_capture 中加一个小 FIFO 做缓冲。这个工程假设视频源来自常见的 HDMI 转换芯片或 SDI 接收芯片它们输出的视频数据和 de 天然与像素时钟对齐所以省掉 FIFO。用寄存器直接采样时一定要做 setup/hold 时序检查像素时钟超过 200MHz 就要改用 IOB 寄存器或原语来采集否则会采到毛刺。3.2 音频嵌入与时分复用组帧数据怎么排进同一个容器音频采样率 48kHz每采样 16bit一秒钟约 1.5Mbit 数据量。相比之下1080p60 的像素数据率是 1.5Gbit 以上。如果把音频单独占一条通道浪费光纤带宽。合理的做法是把音频采样分割成若干小包插进视频行消隐的间隙里。audio_embed.v 先完成时钟域转换再交给 mux_frame.v 复用。mux_frame.v 的核心逻辑是每个时钟周期决定当前总线上放什么内容优先级是视频包优先、音频包次之、最后是空闲同步包。module mux_frame #( parameter PAYLOAD_BITS 28, parameter AUDIO_CH 4 )( input wire clk, input wire rst_n, input wire [PAYLOAD_BITS-1:0] video_in, input wire video_vld, input wire [15:0] audio_in, input wire audio_vld, output reg [PAYLOAD_BITS-1:0] frame_out, output reg frame_vld ); localparam ID_VIDEO 4d1; localparam ID_AUDIO 4d2; localparam ID_SYNC 4d3; reg [15:0] audio_fifo [0:7]; reg [2:0] audio_wr, audio_rd; always (posedge clk or negedge rst_n) begin if (!rst_n) begin audio_wr 0; audio_rd 0; frame_out 0; frame_vld 0; end else begin if (audio_vld) begin audio_fifo[audio_wr] audio_in; audio_wr audio_wr 1b1; end if (video_vld) begin frame_out {ID_VIDEO, video_in[PAYLOAD_BITS-5:0]}; frame_vld 1b1; end else if (audio_wr ! audio_rd) begin frame_out {ID_AUDIO, audio_fifo[audio_rd]}; frame_vld 1b1; audio_rd audio_rd 1b1; end else begin frame_out {ID_SYNC, {PAYLOAD_BITS-4{1b0}}}; frame_vld 1b1; end end end endmodule优先逻辑里video_vld 是真正的「是否传视频」标志。空闲时不发空包而是发 ID_SYNC 同步包并让 frame_vld 保持有效这样接收端在看不到视频和音频时仍能通过同步包维持时钟恢复和帧对齐。audio_wr 和 audio_rd 构成一个 8 深度环形 FIFO这里隐含一个假设音频包写入频率远低于视频行消隐出现的频率FIFO 不会溢满。如果音频通道数增加到 8 路或采样率升到 192kHzFIFO 深度要相应提高否则会丢音频。ID 字段只用了 4 位视频和音频各占一种码型还剩下很多组合。例如需要传时间戳或 Tally 信号时加一个 ID_AUX 即可。但注意加码型时接收端解帧的优先级和路由必须同步更新否则旧帧格式的解码结果会错位。3.3 8B/10B 编码与并串转换信号上光纤前的最后工序8B/10B 编码的意义在于让串行数据流里不出现连续的 0 或 1接收端才能从数据里恢复时钟。这个工程没有重新发明编码算法而是用查表法把输入字节映射成 10 位码字。查表法在 FPGA 里非常高效综合后就是几个 LUT 和 MUX延时稳定。wire [9:0] code_a, code_b, code_c; encoder_8b10b u_enc_a ( .clk(clk), .din(frame_out[7:0]), .dout(code_a) ); encoder_8b10b u_enc_b ( .clk(clk), .din(frame_out[15:8]), .dout(code_b) ); encoder_8b10b u_enc_c ( .clk(clk), .din(frame_out[23:16]), .dout(code_c) );三路编码并行的原因是单路 8B/10B 码表最大吞吐约 1Gbps不能满足 3G 以上视频速率。工程把 28 位包拆成低 24 位有效数据每 8 位一组编码成 10 位共 30 位再通过高速收发器的并行输入端口发送。注意 frame_out 是高 4 位 ID 在前低 24 位在后编码时只编低 24 位ID 不参与编码因为接收端解帧时不需要 ID 的直流平衡保证这样可以减少一次查表延时。线速率的计算对理解工程非常关键。以 1080p60 4:2:2 视频为例像素数据率 148.5M 像素/秒每像素 20 位原始速率 2.97Gbps加上音频、同步和管理开销再乘 8B/10B 的 10/8 系数线速率取 3.75Gbps 是常见选择。这个值由 GTP/GTX 的线速率选项直接生成参考时钟取 150MHzPLL 倍频 25。如果把视频换成 4K60原始速率接近 12Gbps单条 GTH 线速率不够就必须用多条通道并联mux_frame 的并行度也要同步扩大。3.4 接收端反向处理时钟恢复、解码、解帧接收端的高速收发器先做 CDR从恢复的串行比特流中提取时钟和数据再按 10 位对齐解出编码码字。这里最容易忽略的是字节对齐问题——8B/10B 码流没有明确字节界线收发器通过查找特殊 K 码如 K28.5来对齐。工程的 gig_rx.v 启用了接收端的 comma alignment 功能解帧器只要发现 ID_SYNC 就重置帧计数器避免码字错位。之后 decoder_8b10b 按码表恢复数据demux_frame 根据 ID 把数据分别送到视频 FIFO、音频 FIFO 和辅助输出。由于收发器恢复时钟与本地视频时钟存在 PPM 级差异这两个 FIFO 都设计为异步 FIFO。工程里视频 FIFO 深度设为最小两帧用来容忍源端和输出端场同步频率的微小漂移。如果检测到上溢或下溢就直接丢掉当前帧并等待下一个同步头宁可丢一帧也不输出撕裂画面。4. 避坑与排查从丢帧到花屏四个高频问题一次说清4.1 现象光纤接上后接收端偶发无图重新上电又恢复第一次遇到这个问题时第一反应是光模块没插紧或者光纤损耗大但换光模块还是复现。后来用示波器看恢复时钟发现无图时时钟抖动明显偏大同时发送端光模块温度升高后光功率下降。这就是典型的光模块温度特性导致链路劣化激光器发光效率随温度升高而下降如果没有自动光功率控制光信号到接收端会低于灵敏度门限。解决分两步。第一步在发送端读光模块的 DAC 温度寄存器用 PID 调整激光器偏置电流保证光功率恒定第二步在接收端把 CDR 的均衡模式从手动改成自适应以补偿不同光纤长度的衰减。改完做老化测试连续 48 小时没有掉链。日常排查时如果看到接收端指示灯闪烁但没锁住优先用光功率计看接收光功率是否在模块灵敏度范围内再查发送端温度补偿。4.2 现象综合时序全绿上板跑几分钟出现单比特错码这个问题最气人因为工具反馈一切正常。根因在跨时钟域约束缺失。发送端里像素时钟、组帧时钟、编码器输出时钟是三个独立时钟域它们之间通过 FIFO 交换数据。工具默认把所有时钟当同步时钟处理会优化跨时钟路径但这种优化依赖静态时序分析的假设实际工作时跨时钟路径上存在亚稳态风险。解决方法是列出真正的异步时钟域对所有异步 FIFO 中的同步器路径添加 set_max_delay 约束并保证每个同步器是两级触发器结构。set_max_delay -datapath_only 3 [get_cells -hier -filter {NAME ~ *sync_ff*}]这条命令把异步同步器路径上的组合延迟限制在 3ns 以内。加上约束后重新跑布局布线工具会开始诚实报告真实路径红灯可能变多但这才是真实时序。上板跑 24 小时压测后误码彻底消失。经验是听「时钟是异步所以不用约束」这种说法正式因为它猜不出来才需要显式告诉工具哪些路径是异步的、哪些是同步的。4.3 现象图像正常但音频伴随「噼啪」爆音爆音的直接听感是偶发咔嚓声换上耳机更明显。用逻辑分析仪抓音频输出数据发现某一段采样值重复或跳变。原因在于音频采样和视频帧之间没有严格对齐发送端在行消隐里插音频包时如果行消隐窗口因为场同步抖动而变短一个完整音频包可能被拆成两段接收端解包时只读取半个包。针对性解决是给音频通道加「包完整性」检查。发送端把每个音频包加上长度字段和 CRC接收端解帧时若检测到长度不符就丢弃整个包并保持上一次的音频输出电平避免重复采样。这个工程后来又把音频 FIFO 改成双缓冲让读写时钟彻底分离才从根上解决时域碎片化。如果你也遇到爆音先检查音频时钟和视频时钟是否同源不同源时必须用异步 FIFO绝不能靠行消隐时序硬扛。4.4 现象仿真正确但上板显示色彩条纹滚动这个问题经常被归为玄学其实是复位域交叉。仿真里一个全局复位信号能同时在所有寄存器生效但真实器件中同一个 rst_n 到达不同触发器的布线长度不同导致部分模块先退出复位、部分后退出。若某模块先启动开始写总线而接收模块还没复位完成总线上出现半高电平表现出来就是条纹滚动。解决办法是给每个时钟域都做一个独立的复位同步器用同步后的复位去触发该域逻辑。具体做法是两级寄存器级联复位释放时与时钟上升沿对齐。这里强调一点复位同步器的来源不能是按键或 DIP 开关必须经过去抖若使用 MMCM/PLL则等待 locked 信号有效后再释放复位。改完这处色彩条纹再没出现过。从仿真转上板时先检查复位、再检查时钟探测器是否起振能省一半调试时间。5. 二次开发从 1080p 到 4K、从单路到多路怎么改不翻车5.1 分辨率升级把像素时钟、数据位宽和线速率一起改以 720p 升级到 1080p 为例像素时钟从 74.25MHz 提高为 148.5MHzvideo_capture 的 DW 从 20 改 24mux_frame 的 PAYLOAD_BITS 从 24 改成 28编码器并联路数同步调整。线速率跟着带宽公式走LineRate PixelRate x BitsPerPixel / 8 x 10 / LaneCount。所以不能只改一个参数必须通盘检查。改完先编译看资源报告里 LUT 和 FF 是否增长剧烈如果 FF 占用超过 60%要考虑换更大芯片或降低位宽冗余。5.2 增加第二路视频通道ID 扩展和带宽预算多路复用时把 mux_frame 中的 ID_VIDEO 拆成 ID_VIDEO0 和 ID_VIDEO1各自绑定不同的数据来源接收端解帧时按 ID 路由到对应的 FIFO。同时把空闲同步包周期缩短给第二路视频让出时隙。如果两路视频加起来需要的吞吐量超过当前线速率有两条路一是提高收发器线速率比如从 3.75Gbps 升到 6Gbps二是用波分复用把不同视频放到不同光波长上那是成本更高的方案。对四路 SD-SDI 的场景用一条 GTH 线速率的时分复用就足够。5.3 验证流程改动之后必做的三项检查每次改完工程都会强制走一遍完整链路仿真、时序收敛、光纤回环测试。链路仿真用 tb_top 把发送端和接收端串起来注入白噪声和频率偏移时序收敛用 build.tcl 自动跑看 setup/hold 裕量是否大于 100ps回环测试用短光纤把 TX 和 RX 直连发 PRBS 伪随机码流统计误码率再接入真实视频源验证图像和音频。这套流程虽然啰嗦但能过滤掉九成低级错误。从那以后我每次改完工程都强制走这三步不再相信「只改一个参数不会出问题」的侥幸心理。希望帮到你。本文还有配套的精品资源点击获取