☰
FPGA视音频光端机架构与工程实践全解析
2026/10/17 8:49:54 网站建设 项目流程

简介:基于FPGA的视音频光端机完整工程代码,面向嵌入式硬件工程师、FPGA开发者和高校相关专业学生,适用于监控、广播、教育等场景下多路视频音频信号的长距离光纤传输与综合业务接入。资源共198个文件,RAR压缩包仅1.4MB,包含ISE工程文件、Verilog/VHDL源码、UCF约束文件、bit/mcs配置比特流以及网表和编译报告;其中源码可学习逻辑设计,约束文件对应引脚分配,比特流可直接烧录,编译报告便于排查时序布局问题。代码实现了多路模拟视频输入、音频数字化处理、电话接口、以太网通信和开关量控制等核心功能,内部涉及帧同步、加扰解扰、编解码及主从收发等逻辑,设计层次完整;工程以成熟版本形式发布,稳定性和兼容性经过实际商业化验证。已有491人学习,适合需要快速理解光端机FPGA实现方案,或在该工程基础上进行功能扩展与移植的开发者。

1. 一句话讲清这是干什么的:视音频光端机的 FPGA 核心

我拆过一个实际跑在广电/安防项目里的视音频光端机工程,说是“代码”,其实是一整套完整方案:发送端把 SDI 或模拟视音频信号采进来,做并串转换、加扰、上光模块,跑几十公里光纤;接收端再把光信号恢复成像素时钟、还原成 SDI 输出给监视器或编码器。中间还夹着 RTP 封装、UART 配置、误码统计这些外围逻辑。大多数开发者第一次拿到这类工程会懵——全是 Verilog,又是 GTX、又是 Aurora,不知道从哪看起。这篇就把这套 FPGA 光端机代码从架构到跑通、再到改造成自己的传输方案,完整讲一遍。适合正在做视频传输设备、安防监控集成、广电信号延长项目的开发者。

2. 系统架构:发送端和接收端各自在做什么

2.1 整体链路:从电信号到光信号再到电信号

整套光端机代码在顶层分两大块:发送端(TX)和接收端(RX)。发送端做的事情可以拆成四步:先接收外部进来的视音频信号,做格式解析和时钟恢复;然后把并行像素数据打包成适合光纤传输的帧格式;再做并串转换,把高速串行数据送给光模块;最后光模块把电信号转成光信号射出去。接收端顺序反过来,先由光模块把光转回电,再做串并转换、解帧、恢复像素时钟,最后并行输出给 SDI 驱动器或视频解码芯片。

顶层模块的典型例化关系是这样的,top_tx和top_rx各自独立,中间只有光纤连着,两边没有共享时钟也没有共享复位——这是光端机设计和普通板级设计最大的区别。发送端的本地晶振(比如 148.5MHz 用于 1080p)负责把所有像素数据打进去,接收端的恢复时钟是直接从串行数据流里提取出来的,两边频率哪怕差几个 ppm,靠 FIFO 和帧同步机制也能兜住。

// top 层例化示意 wire [19:0] tx_pixel_data; wire tx_pixel_clk, tx_pixel_valid; tx_engine u_tx ( .rst_n (sys_rst_n), .ref_clk (148_500_000), // 1080p50/59.94 常用像素时钟 .pixel_data_in (sdififo_dout), .pixel_valid_in (sdififo_valid), .sfp_txp (sfp_txp), .sfp_txn (sfp_txn), .tx_status (tx_status_reg) ); rx_engine u_rx ( .rst_n (sys_rst_n), .sfp_rxp (sfp_rxp), .sfp_rxn (sfp_rxn), .vid_clk_out (rx_pixel_clk), // 恢复出的时钟 .vid_data_out(rx_pixel_data), .vid_valid (rx_pixel_valid) );

这里最值得留意的是rx_pixel_clk,它不是板上晶振产生的,而是 CDR 模块从光纤数据流里恢复出来的。后续 SDI 输出、音频时序全都跟着这个时钟走,才能保证接收端画面和声音不撕裂、不同步。

2.2 发送端关键模块:SDI 解串与像素时钟域

发送端入口先接 SDI 信号。SDI 进来是串行的,速率根据格式不同,HD-SDI 是 1.485Gbps,3G-SDI 是 2.97Gbps,看似差异不大但处理逻辑差别很大。工程里一般会用 FPGA 自带的 SDI IP 或自己写解串逻辑,从串行码流里恢复出 20bit 并行像素数据和对应的像素时钟,然后送进异步 FIFO。

关键点是这个 FIFO 解决的是两个时钟域的问题:SDI 恢复出来的时钟和本地球(发送引擎参考时钟ref_clk)不是同源的。很多新手直接拿 SDI 恢复时钟去驱动整个发送链路,结果换一个输入源就花屏,就是因为没做时钟域隔离。工程里最常见的做法是 FIFO 读写两侧完全独立,读侧用本地参考时钟,空满标志留足裕量。

// SDI 解串后进入 FIFO 的典型处理 always @(posedge sdi_rx_clk) begin if (sdi_rst_n == 1'b0) begin fifo_wr_cnt <= 20'd0; sdi_fifo_wr <= 1'b0; end else if (sdi_valid) begin fifo_wr_cnt <= fifo_wr_cnt + 1'b1; sdi_fifo_wr <= 1'b1; sdi_fifo_din <= {sdi_stuff_bits, sdi_pixel_data}; end else begin sdi_fifo_wr <= 1'b0; end end

这段代码里sdi_valid由 SDI IP 给出,高电平表示当前有有效像素数据;sdi_stuff_bits是用来向接收端传递辅助信息的随路位。重点说明一下:光端机传输的不是压缩视频,而是原始无压缩像素流,所以每一个时钟周期都要尽量传满数据,FIFO 深度不够或读写速率不匹配,丢的是实实在在的像素。

2.3 接收端关键模块:串化输出与时钟重建

接收端拿到的是光模块出来的高速差分信号,先进 GTX/SerDes 的 CDR 恢复数据和时钟。恢复出来的并行数据先解帧,把插入的同步头、校验位拿掉,再拼成 20bit 像素数据。但这里有个细节:恢复出的数据时钟只是“跟数据同频”,相位关系不一定适合直接抓数据,所以接收端一般会再接一个弹性缓冲。

弹性缓冲的经典用法是:写入侧用恢复时钟,读出侧再用一个 PLL 把恢复时钟整形成干净的像素时钟输出。这个 PLL 的反馈要做成“跟踪模式”,频率锁定在恢复时钟上,而不是锁在一个固定晶振频率上,否则输入源稍微偏一点,缓冲就会溢出或下溢。

// 接收端像素时钟恢复的典型结构 rx_clk_pll u_rx_pll ( .clkin (cdr_recovered_clk), // CDR 恢复的串行时钟 .clkout (rx_pixel_clk), // 补偿后干净的像素时钟 .locked (rx_pll_locked) ); always @(posedge rx_pixel_clk) begin if (!rx_buf_rst_n) begin rx_fifo_rd_en <= 1'b0; rx_fifo_rd_cnt <= 4'd0; end else begin // 读侧比写侧滞后一拍,避免发生亚稳态 if (rx_fifo_valid && rx_fifo_rd_cnt >= 4'd2) begin rx_fifo_rd_en <= 1'b1; rx_pixel_data <= rx_fifo_dout; end end end

rx_fifo_rd_cnt >= 4'd2这个条件,是让读侧等写侧先写了两个数据再开始读,防止一开始空读。实际工程里要根据 FIFO 写延迟和 PLL 锁定时间调这个值,调不好就是首帧黑屏或者底部滚动条。

3. 为什么用 FPGA 而不是专用芯片来做这项任务

3.1 视频链路对延迟的容忍度极低

很多第一次接触光端机的开发者会问:现在 DSP、编解码芯片那么多,一个视频延长器为什么还非得用 FPGA?最直接的原因是延迟。视音频光端机走的是无压缩传输,端到端延迟要求通常在几毫秒以内,这在专用 ASIC 和 FPGA 之外很难保证。软件编码压缩动不动几十毫秒延迟,对广电的演播室返送、手术室视频传输这种场景就是不可接受的。

FPGA 的优势在于它的并行数据通路完全是硬件化的:像素数据从进入 FPGA 到从光纤出去,延迟只由几级寄存器和 FIFO 决定,通常能做到几十个像素时钟周期以内。工程里我特意用 ILA 抓过实际延迟,从 SDI 输入到光模块输出,大概是 12 个左右ref_clk周期,约 80ns 级别——这在其他方案里想都不敢想。

3.2 带宽要算清楚:无压缩像素流有多恐怖

做光端机绕不开带宽计算。以 1080p60 为例,像素时钟 148.5MHz,每个像素 20bit(YCbCr 4:2:2),一路裸视频就是 2.97Gbps,加上音频辅助数据、同步头、纠错位,实际光纤速率要做到 3.1872Gbps 甚至更高。这个速率下,信号完整性、时钟抖动、CDR 抖动容限全都成了硬指标,用 FPGA 的高速收发器可以精确控制每一段链路的端到端参数,这是普通芯片方案给不了的。

这里还要顺带提一句:光端机传的不是单纯的视频,工程里普遍会复用 SDI 标准的行消隐区来塞音频和辅助数据。所以计算带宽时不能只看像素数据,要把 16 通道音频、ANC 数据、时序信息全算进去,否则到了实际对接时发现带宽不够,再回头改帧格式就麻烦了。

3.3 FPGA 方案和 ASIC 专用方案的权衡

现实地说,如果只做一种固定格式、固定距离的光端机,用专用 SDI 光收发芯片更省事——功耗低、外围少、代码量小。但这类方案的问题是格式升级就废掉,比如从 3G-SDI 升级到 6G-SDI 或 12G-SDI,芯片得换,PCB 得改。FPGA 方案里,只要高速收发器速率够,升级通常是改代码、改配置,板卡不用动。

工程里这套设计走的是“FPGA + 可重构收发器”路线。代码里模块之间靠 AXI-Lite 寄存器总线连接配置通道,换分辨率、换光纤速率、换音频路数,大部分时候只是改寄存器配置,不用动 RTL。这个架构选型对做产品的人来说太重要了:同一块板卡,既能跑 1080p 光端机,改配置又能跑 4K 光模块——前提是收发器速率和光模块带宽够。

4. 工程代码怎么读:目录结构、核心模块和配置通道

4.1 工程目录:从顶层到 IP 的完整梳理

拿到工程先别急着开 Vivado,先扫一遍目录。一套合格的光端机工程,目录结构是有规律的,通常分成rtl、ip、sim、constraints四块。rtl里按发送、接收、公共逻辑分目录;ip里放 SDI IP、GTX IP、FIFO IP;sim里至少有发送端仿真和接收端仿真两个测试平台;constraints里除了引脚约束,还要有专门的时序约束文件。

我这里展示一个典型目录结构,你拿到手的工程大致就是这个骨架。

project_root/ ├── rtl/ │ ├── tx/ │ │ ├── top_tx.v │ │ ├── sdi_rx_wrapper.v // SDI 解串 + 时钟恢复 │ │ ├── video_fifo_wr.v │ │ ├── audio_mux.v // 音频插入辅助数据区 │ │ ├── framer.v // 成帧:同步头 + 校验 │ │ └── serdes_tx_wrapper.v // GTX 发送 │ ├── rx/ │ │ ├── top_rx.v │ │ ├── serdes_rx_wrapper.v // GTX 接收 + CDR │ │ ├── deframer.v // 解帧 │ │ ├── video_fifo_rd.v │ │ ├── sdi_tx_wrapper.v // SDI 输出串化 │ │ └── audio_demux.v │ └── common/ │ ├── reg_bus.v // AXI-Lite 寄存器总线解析 │ ├── status_reg.v // 状态寄存器 │ └── crc32.v ├── ip/ // Xilinx IP 核 ├── sim/ │ ├── tb_tx_top.v │ └── tb_rx_top.v └── constraints/ ├── pin.xdc └── timing.xdc

framer.v是整个发送端最核心的模块,它决定每一帧怎么组织;deframer.v是它的逆过程。读懂这两个文件,光端机原理就通了七成。

4.2 核心代码逐段讲:framer 成帧与数据封装

framer.v做的事情可以用三句话概括:在每一行有效视频数据前面插入同步头,在后面插入校验字,在行消隐期间插入音频和辅助数据。看代码时先找两个计数器:一个是行计数line_cnt,一个是像素计数pix_cnt,这两个计数器锁定了所有数据放哪个位置。

// framer 核心逻辑(简化版) always @(posedge tx_clk) begin if (!rst_n) begin line_cnt <= 12'd0; pix_cnt <= 11'd0; end else if (tx_valid && tx_eol) begin line_cnt <= line_cnt + 1'b1; pix_cnt <= 11'd0; end else if (tx_valid) begin pix_cnt <= pix_cnt + 1'b1; end end // 每行开头 4 个像素插入同步头 always @(posedge tx_clk) begin if (pix_cnt < 4) framer_out_data <= {SYNC_HEADER, crc_inline}; else if (pix_cnt >= total_pix - 4) framer_out_data <= {CRC_TAIL, audio_payload}; else framer_out_data <= pixel_data; // 有效像素直通 end

这段代码里SYNC_HEADER是自定义的同步字,接收端靠它找到行起点;total_pix是每一行的总像素数,这个值跟分辨率绑定,1080p 是 2200,720p 是 1650;audio_payload是把 16 路音频按帧塞进行消隐区的数据。留心一个点:这里的crc_inline是对上一行像素算出来的 CRC 值,插在当前行的开头,这样接收端拿到一行就能立刻校验,不用缓存整行再算,节省不少 BRAM。

4.3 配置通道:UART 与寄存器总线是怎么回事

光端机作为设备,必须能改配置、查状态。工程里最常见的做法是一颗 MCU 通过 UART 和 FPGA 通信,FPGA 内用一个寄存器总线模块把 UART 协议转成寄存器读写操作。

寄存器总线模块的逻辑各家不同,但接口基本一致:地址、数据、读使能、写使能、读数据。代码里会有一个大的case语句,按地址分发到各个子模块的寄存器。比如地址0x00是版本号,0x04是链路状态,0x10是分辨率配置,0x14是音频使能掩码。

// 寄存器总线读操作示例 always @(*) begin case (reg_addr) 12'h000: reg_rdata = version_reg; // 固件版本 12'h004: reg_rdata = link_status; // 光纤链路锁定状态 12'h008: reg_rdata = ber_counter; // 误码计数 12'h010: reg_rdata = video_mode_cfg; // 分辨率配置 12'h014: reg_rdata = audio_en_mask; // 音频通道使能 12'h018: reg_rdata = clk_offset_cfg; // 时钟微调偏移 default: reg_rdata = 32'h0; endcase end

这个地址表就是设备的手册。二次开发时你想加一个功能,比如加一种新的分辨率支持,就在地址表里加一个寄存器位,然后让video_mode_cfg的解析逻辑多处理一种枚举值就行。注意ber_counter是只读寄存器,读它不影响计数,但可以让它带“自清零”属性:读一次自动清零,方便软件连续监测误码率。

5. 避坑指南:光端机调试中常见的 6 类问题

5.1 现象:光纤插上完全没有图像

刚上电时经常会遇到这种情况,第一步先别急着看 RTL,先用光功率计测光纤接收端光功率。常见原因是光模块的 TX Disable 引脚被拉高了,光模块根本没有发光。

原因:光模块的 TX Disable 是低有效,很多开发板的默认状态把它拉高了,导致光模块不工作。解决:在发送端顶层模块里显式把 TX Disable 拉到低电平,并且加上电延时——上电后等 100ms 再释放,因为部分光模块上电需要时间初始化内部电路。代码里加一个简单的延时计数器即可。

5.2 现象:画面有横条纹,图像能出但不停滚动

原因:接收端恢复的像素时钟和发送端不一致,慢了几十个 ppm。横条纹滚动的频率恰好对应两边时钟频率差。解决:检查接收端 PLL 的反馈配置,如果 PLL 是锁定在固定频率而不是跟踪 CDR 恢复时钟,就会出这个问题。参考 2.3 节的做法,把 PLL 改成跟踪模式,或者在弹性缓冲上做“自适应读写指针调整”,让缓冲周期性微调读指针,吸收时钟漂移。

5.3 现象:图像正常,但音频有杂音或丢字

原因:音频数据是放在行消隐区里的,行消隐区像素数有限,而每一行能塞的音频字节数受像素时钟和音频采样率的比值约束。配置了过多数量的音频通道,或者音频采样率和视频场频不匹配,就会丢音频包。解决:重算行消隐余量。1080p60 每行有 280 个消隐像素,每像素传 20bit,理论上能塞 700bit 辅助数据,减去同步头、CRC、ANC 校验,实际可用的大约 600bit 左右;16 通道 48kHz 音频每行需要约 128bit,留的余量要至少 30%,否则突发状况下必丢。

5.4 现象:传输距离一远就闪断,近距离没事

原因:光模块发射光功率和接收灵敏度不匹配。近距离能通是因为光功率虽然偏大或偏小但还在接收端动态范围内,距离一拉长,衰减变大就超出了。解决:换模块或者调整发送端的预加重参数。FPGA 高速收发器的 TX 预加重不是为长距离光纤设计的,但如果光模块驱动能力偏弱,可以通过调节 TXEQ 参数稍微补偿。要注意这是模拟参数,改完必须重新跑眼图测试,不能靠肉眼判断。

5.5 现象:同一套代码,换一批板卡效果不一样

原因:时序约束没做对。很多工程直接拿默认引脚约束,没有给高速收发器的时钟加create_clock约束,导致综合布局布线时时钟路径是随机优化的。这批板卡体质好还能跑,换一批就出问题。解决:在timing.xdc里至少为三类时钟加约束:参考时钟、恢复时钟、像素时钟,每一条都标注来源。并且跑完布局布线后看时序报告,setup和hold都要过,同时检查 CDR 的抖动容限仿真。

5.6 现象:误码率测试通过不了,BER 在 1e-9 附近徘徊

原因:发送端数据位宽和 GTX 的用户接口位宽不一致。比如 GTX 配置成 20bit 接口,但 framer 输出的是 32bit 数据,中间交叉拼接位序错了,导致偶尔误码。解决:先做 Loopback 测试。把 GTX 配置成近端 PCS 环回,跑 PRBS 测试确认 GTX 本身无误码,然后把环回点往两端扩——先跳过 framer,再连上 framer,逐段缩小问题范围。经验上 80% 的“误码”不是光链路的问题,而是数字逻辑位序问题。

6. 二次开发:怎样改成自己的音视频传输方案

拿到这套代码做二次开发,我建议按三个步骤走:先跑通收发环回,再改自己需要的功能,最后做整机验证。

跑通环回是第一优先级。把发送端和接收端放在同一块板子上,用光纤短接,先用 ILA 观察接收端的rx_pixel_valid信号是否稳定拉高。这一步能确认时钟恢复和弹性缓冲是否正常。然后跑一个纯色测试图发送,接收端接一个 SDI 监视器,纯色不花不闪,基本盘就稳了。

改分辨率是二次开发里最常见的需求。工程默认支持 1080p60 和 720p60,要加一个 1080p30,需要改的点有四个:像素时钟频率(148.5MHz 变成 74.25MHz)、total_pix和total_lines、GTX 的线速率配置、SDI IP 的输出格式配置。不要只改 framer 里的计数器,因为 GTX 线速率是独立配置的,改漏了就会出现“图像能出但大小不对”的怪问题。

改音频通道数相对简单。audio_mux.v里把audio_ch_sel向量扩展几位,然后在寄存器地址表里加一个音频通道掩码位,MCU 侧配置就可以。但要注意行消隐容量上限,不能无限加,算好每一行的辅助数据余量再动手。

验证阶段最值得做的两个动作:一是长时间老化测试,至少运行 72 小时,每 10 分钟记录一次误码计数,确认 BER 稳定在 1e-12 以下;二是源端兼容性测试,把不同品牌的 SDI 输出设备接进来,因为不同设备的时钟偏移和抖动特性差异很大,只测一个源会给你留下隐患。

这套代码我拆完最大的体会是:光端机工程里,RTL 逻辑只占一半工作量,另一半是时序约束、时钟架构和信号完整性设计。从那以后我每次拿到新的 FPGA 传输工程,都强制自己先画一张时钟树——列出所有时钟的来源、频率、域间关系,再开始读 RTL。这个习惯帮我少踩了很多坑,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询