资讯动态

FPGA视音频光端机代码实战:从架构到调试的完整链路解析

发布时间:2026/10/26 11:12:00 来源:尧图企业网站定制
简介面向FPGA开发与光纤通信方向的学习者这份资源提供了一套基于现场可编程门阵列实现的视音频光端机工程代码可用于理解多路模拟视频与音频的采集、编解码、压缩与同步处理以及电话接口、以太网通信和开关量控制等模块在可编程逻辑中的落地方式适合具备一定Verilog与数字电路基础、希望研究光端机系统集成的读者参考。压缩包共198个文件约1.4MB以Verilog源码、VHDL文件、UCF约束、ISE工程文件及综合布局布线报告为主另含比特流、脚本与说明文档完整保留了从设计输入到实现验证的工程痕迹。已有491人学习下载。代码经过实际应用与商业化验证读者可从中获取多业务信号处理的模块划分思路、工程约束与综合实现参考以及排查时序与资源问题的线索。1. 视音频光端机代码一套能跑在 FPGA 上的音视频传输工程第一次拿到这套视音频光端机代码时我以为是几个 Verilog 文件加一份说明结果解压出来是一整条链路视频采集、音频采样、并串转换、光模块接口、时钟域处理全在里面。光端机这东西在安防和广电场景里很常见作用是把多路视频和音频打包成高速串行流通过光纤送到几公里外接收端再还原成原始信号。这套代码适合两类人一类是想搞懂 FPGA 怎么做多路音视频复用的开发者另一类是手里有板子、想跑通一条完整收发链路的工程人员。它不依赖操作系统纯硬件逻辑跑起来就是实打实的时序和带宽问题玄学少、血泪经验多。2. 工程结构与数据通路从摄像头到光模块的完整链路2.1 顶层模块划分与目录组织这套代码的目录结构比较典型按功能分层没有把东西全塞进一个文件里。常见做法是分成rtl/可综合逻辑、sim/仿真激励、constraints/管脚和时序约束、ip/厂商 IP 核四块。顶层模块一般叫video_audio_optical_top下面挂几个子模块视频输入接口、音频采样接口、复用打包模块、串行发送模块、串行接收模块、解复用模块、视频输出接口、音频输出接口。我拆的时候习惯先看顶层端口因为它直接告诉你这块板子对外接什么。典型端口包括视频时钟、视频数据总线、音频位时钟、音频数据、光模块的差分收发对、复位信号以及几个状态指示 LED。端口命名能看出设计者的意图比如vid_clk_in、aud_bclk、gt_tx_p、gt_rx_n这种一看就知道哪路是视频、哪路是音频、哪对是高速串行。module video_audio_optical_top ( input wire sys_clk, // 系统参考时钟通常 50MHz 或 100MHz input wire rst_n, // 低电平复位 // 视频输入侧 input wire vid_clk_in, // 摄像头/解码芯片送来的像素时钟 input wire [15:0] vid_data_in, // YUV422 或 RGB565 数据 input wire vid_hsync, // 行同步 input wire vid_vsync, // 场同步 // 音频输入侧 input wire aud_bclk, // 音频位时钟 input wire aud_lrclk, // 左右声道选择 input wire aud_data_in, // I2S 串行音频数据 // 光模块侧 output wire gt_tx_p, output wire gt_tx_n, input wire gt_rx_p, input wire gt_rx_n, // 状态指示 output wire [3:0] led_status );这段顶层声明里vid_data_in的位宽决定了视频格式16 位对应 YUV42224 位就是 RGB888改格式时这里要同步改。gt_tx_p/n是差分对接光模块的发送端注意极性不能接反接反了链路起不来但不会报错只能靠示波器看眼图。led_status一般用来指示锁相状态、视频锁定、音频锁定和链路误码调试时非常有用。2.2 视频采集与音频采样的时钟域处理这条链路里最容易翻车的地方是时钟域。视频侧有自己的像素时钟音频侧有 I2S 的位时钟光模块侧还有高速串行时钟三个时钟不同源直接跨域传数据必然出亚稳态。代码里的做法是每个输入域先做一级同步再用异步 FIFO 做缓冲。视频采集模块一般这样写在vid_clk_in域里把行场同步和数据打一拍检测到有效行时把像素写进 FIFO。音频侧在aud_bclk域里把 I2S 串行数据移位成并行样本攒够一个采样点后写进另一个 FIFO。两个 FIFO 的读侧都挂在打包模块的时钟域上。// 视频行有效检测与 FIFO 写入简化 always (posedge vid_clk_in or negedge rst_n) begin if (!rst_n) begin wr_en_vid 1b0; end else begin // 行场同步都有效时才认为像素有效 wr_en_vid vid_hsync vid_vsync; if (vid_hsync vid_vsync) vid_fifo_din vid_data_in; end end // 异步 FIFO 例化深度一般取 512 或 1024 async_fifo #( .DATA_WIDTH (16), .ADDR_WIDTH (10) // 深度 1024 ) u_vid_fifo ( .wr_clk (vid_clk_in), .wr_en (wr_en_vid), .din (vid_fifo_din), .rd_clk (pkg_clk), .rd_en (vid_fifo_rd_en), .dout (vid_fifo_dout), .empty (vid_fifo_empty), .full (vid_fifo_full) );这里ADDR_WIDTH取 10 意味着深度 1024够缓冲一行到几行像素。深度太小会在视频分辨率高时溢出表现为画面撕裂或丢行深度太大浪费 Block RAM还会增加延迟。我一般按「一行像素数 × 2」估算1080p 一行 1920 个像素深度取 4096 比较稳。wr_en_vid用行场同步相与是因为很多解码芯片在场消隐期间数据总线是无效的不屏蔽会写进垃圾数据。音频侧要注意 I2S 的时序数据在位时钟下降沿变化、上升沿采样是常见约定代码里移位方向搞反了声音会变成噪声。采样率 48kHz、位宽 24 位时一个声道每秒 48000 个样本打包时按帧组织每帧塞固定数量的音频样本和视频行接收端才能按同样节奏拆开。2.3 复用打包与串行发送打包模块是整个工程的核心它把视频 FIFO 和音频 FIFO 的数据按自定义帧格式拼起来。帧头一般放同步字后面跟帧类型、通道号、长度、载荷、校验。同步字选一个不容易在数据里出现的值比如0xEB90接收端靠它做帧对齐。// 帧打包状态机简化 localparam S_IDLE 2d0; localparam S_HEAD 2d1; localparam S_PAYLOAD 2d2; localparam S_CRC 2d3; always (posedge pkg_clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; pkg_data 16h0; pkg_valid 1b0; end else begin case (state) S_IDLE: begin if (!vid_fifo_empty || !aud_fifo_empty) begin pkg_data 16hEB90; // 同步字 pkg_valid 1b1; state S_HEAD; end end S_HEAD: begin pkg_data {frame_type, channel_id, payload_len}; state S_PAYLOAD; end S_PAYLOAD: begin // 按优先级交替取视频和音频 if (take_video) begin pkg_data vid_fifo_dout; vid_fifo_rd_en 1b1; end else begin pkg_data aud_fifo_dout; aud_fifo_rd_en 1b1; end if (payload_done) state S_CRC; end S_CRC: begin pkg_data crc_result; pkg_valid 1b0; state S_IDLE; end endcase end endframe_type区分纯视频帧、纯音频帧和混合帧channel_id支持多路复用payload_len告诉接收端这一帧有多少个 16 位字。take_video的调度策略直接影响音视频同步常见做法是视频优先但给音频留固定配额否则音频 FIFO 会被视频饿死声音断断续续。CRC 用 16 位或 32 位都行光链路误码率低16 位够用但工业环境建议 32 位。串行发送部分通常调厂商的高速收发器 IP把并行 16 位数据做 8b/10b 或 64b/66b 编码后串出去。编码方式在 IP 核配置里选代码里主要处理的是发送使能和时钟补偿。这里有个坑收发器 IP 的复位时序有严格要求参考时钟稳定后要等若干周期才能释放复位早了链路训练不过。3. 接收端还原与仿真验证怎么确认链路真的通了3.1 解复用与音视频输出接收端是发送端的逆过程串行收、解码、找同步字、拆帧、按类型分发到视频 FIFO 和音频 FIFO再分别用本地时钟读出。找同步字的逻辑一般用移位寄存器滑动比对连续检测到两次同步字才认为对齐防止数据里恰好出现同步字造成误判。// 同步字滑动检测 always (posedge rx_clk or negedge rst_n) begin if (!rst_n) begin sync_shift 16h0; frame_aligned 1b0; end else begin sync_shift {sync_shift[14:0], rx_data}; if (sync_shift 16hEB90) sync_cnt sync_cnt 1b1; else if (frame_gap) sync_cnt 2d0; // 连续两帧同步字命中才锁定 if (sync_cnt 2d2) frame_aligned 1b1; end endsync_cnt连续命中两次才锁定是为了对抗误同步。frame_gap在帧间隔期间清零计数避免跨帧误判。锁定后如果连续多帧找不到同步字要能自动失锁重搜否则一次误码会导致永久失步这个恢复机制很多初版代码都漏掉。视频输出侧把 FIFO 里的像素按本地视频时序读出行场同步由本地计数器生成不依赖发送端的时序。音频输出侧把样本按 I2S 时序移位送出采样率和位宽要和发送端配置一致不一致就是杂音或变调。3.2 仿真平台搭建与关键波形观察上板之前一定要仿真。这套代码的sim/目录里一般有测试激励产生假的视频行场信号和 I2S 音频流跑几百微秒就能看出打包解包对不对。仿真时重点看三个地方FIFO 的 empty/full 有没有异常翻转、同步字检测的锁定时刻、解出的视频数据和原始数据是否逐拍一致。# 用常见仿真器跑回归以 iverilog 为例 iverilog -o tb_top sim/tb_video_audio_optical.v rtl/*.v ip/*.v vvp tb_top # 波形用 gtkwave 看 gtkwave dump.vcd 命令里把rtl和ip都编进去是因为有些 IP 核提供的是行为级模型仿真时必须带上。dump.vcd要在测试激励里用$dumpfile和$dumpvars打开否则没波形可看。仿真通过不代表上板通过但仿真不过一定上不了板这个顺序别省。3.3 上板调试与链路锁定判断上板后先看led_status。正常状态是参考时钟锁定、收发器就绪、帧同步锁定、音频锁定四个灯全亮。如果只有前两个亮说明高速链路通了但帧层没对齐重点查同步字和 CRC如果前两个都不亮查参考时钟和收发器复位时序。用示波器看差分对时注意光模块的发送使能有些模块需要外部拉高才出光。接收端光功率太低会导致误码误码表现为画面马赛克和音频爆音这时候先查光纤接头和光功率计读数别急着改代码。我见过有人把 CRC 校验关了「解决」误码那是把报警器拆了问题还在。4. 避坑与常见问题这几处翻车我踩过4.1 现象画面正常但声音全是噪声原因多半是 I2S 移位方向或左右声道对齐错了。发送端按「高位先出」移位接收端如果按「低位先出」还原样本位序整个反了听起来就是白噪声。解决方法是抓一次 I2S 波形确认数据在哪个时钟沿变化收发两端用同一个约定并在代码注释里写死。4.2 现象跑几分钟后画面卡死复位才能恢复这是 FIFO 溢出后状态机卡在某个状态出不来。视频源时钟和打包时钟有微小频差长时间运行 FIFO 必然慢慢填满或排空。解决方法是加流控FIFO 快满时丢一帧或插入空帧快空时补空闲码别让状态机死等。频差靠 FIFO 缓冲只能撑一段时间根本办法是收发共用同一个时钟参考或者做时钟补偿。4.3 现象仿真通过上板链路训练不过收发器 IP 的复位释放时机不对。参考时钟稳定后需要等锁相环锁定信号有效再等若干周期才能释放收发器复位代码里如果用一个固定计数器代替锁定信号换一批板子就可能不过。解决方法是把 IP 的pll_locked接进复位逻辑锁定后再计数别用拍脑袋的固定值。4.4 现象换一根光纤就误码率飙升光模块的兼容性和光功率预算问题。不同厂商模块的发送光功率和接收灵敏度有差异链路预算不够时误码率随温度漂移。解决方法是先测光功率确认在模块规格范围内再查 CRC 误码计数。代码侧可以加误码统计寄存器通过状态灯或调试接口读出来比盲猜快得多。5. 二次开发改造从能跑到好用这套代码跑通之后二次开发的空间主要在三个方向通道数扩展、帧格式优化、调试手段增强。通道数扩展最直接打包模块里的channel_id位宽加宽FIFO 按通道复制调度器改成轮询或加权轮询。帧格式优化可以把固定长度载荷改成变长视频行数多时一帧塞多行减少帧头开销但接收端的状态机要跟着改变长解析比定长容易出边界错误改完必须跑长时间回归。调试手段增强是我最推荐先做的。加一组只读寄存器记录各 FIFO 的最大水位、CRC 误码计数、失锁次数、音频欠载次数通过一个简单的串口或调试总线读出来。上板出问题时这些计数器比示波器还管用能直接告诉你是带宽不够还是时钟漂移。// 最大水位统计复位后清零运行中只增不减 always (posedge pkg_clk or negedge rst_n) begin if (!rst_n) vid_max_level 10d0; else if (vid_fifo_level vid_max_level) vid_max_level vid_fifo_level; endvid_fifo_level是 FIFO 的实时占用深度vid_max_level记录历史峰值。峰值接近满深度说明带宽吃紧或时钟频差大峰值长期很低说明 FIFO 开太大可以缩。这个计数器成本几乎为零但排查问题时价值很高。改造时有个习惯我一直保持任何改动先在仿真里跑满一万帧比对收发数据逐字节一致再上板。有一次我图省事跳过了长仿真只跑了短激励就上板结果变长帧解析在特定长度下越界现场跑了半小时才复现返工成本远高于仿真时间。从那以后我每次改帧格式或调度逻辑都强制走一遍长回归加逐字节比对确认无误才碰板子。希望这套拆解能帮你少走几步弯路把这条链路真正跑起来、改下去。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑