Verilog实现BT656 720×576视频解码与同步时序解析

发布时间:2026/10/17 14:06:34
Verilog实现BT656 720×576视频解码与同步时序解析
简介这份BT656 Verilog实现以720×576分辨率视频输出为目标面向需要在FPGA上完成RGB888到YCbCr色彩空间转换的开发者也适合入门数字视频接口的硬件工程师。资源围绕完整的Quartus工程展开包含可直接查看的Verilog源码和综合、适配、时序报告核心知识点涉及行/场同步、字节打包、同步字插入和边沿检测等BT656实现细节。压缩包共130个文件约4.14MB主要文件类型包括Verilog源码(.v)、Quartus工程配置(.qpf/.qsf)、编译生成的设计数据库(.cdb/.hdb)、综合报告(.rpt/.html)以及可编程下载的.sof比特流readme则提供基本使用说明便于快速定位工程结构和运行结果。目前已有5145人学习下载。通过这套工程读者可以对照FPGA设计流程检查关键报告理解576p标清视频输出的完整数据通路工程内各子模块划分清晰也可作为后续自研视频处理模块、开展色彩空间转换实验的参考起点。1. 这块 Verilog 把 720×576 的 BT656 码流变成了可以落地的视频帧做视频采集或者显示驱动时720×576 这个分辨率你会经常碰到它是 PAL 制 D1 格式的有效像素区老式模拟摄像头、SDI 转换板、还有不少图像处理板卡往外送数据用的都是 BT656 接口。很多开发者第一次拿到“bt656 verilog 代码 720*576”这套工程时容易误判以为 BT656 就是同步信号加像素数据拆开波形才发现里面还有 EAV、SAV、水平消隐区、隔行场标志直接用错位的字节去拼 RGB画面要么花屏要么上下场对不上。这套代码解决的正是这个问题把 BT656 4:2:2 的视频流按 720×576 的时序完整地解析出来或者按同样的时序生成标准 BT656 信号。无论你是接模拟视频解码芯片做图像采集还是想在 FPGA 板卡上输出 BT656 测试图它都能直接作为前端 IP 来用。2. BT656 协议拆解720×576 的行场时序与同步基准码2.1 行场参数从 27MHz 像素时钟反推整行结构BT656 的全名是 ITU-R BT.656它定义的是数字视频的并行或串行传输接口。并行形式下每个时钟沿传一个字节数据按 YCbCr 4:2:2 的次序排列也就是 Cb、Y、Cr、Y 这样循环。对 720×576 来说一行里真正送出的有效像素字节数是 1440 个——对应 720 个亮度样本和 720 个色度样本Cb、Cr 各 360 个交替存放。但 BT656 不是每一行都从第一个像素开始它在一行的开头和结尾各插入了一段时序基准码分别叫 SAV有效视频开始和 EAV有效视频结束。这里有一组数字需要烂熟于心才能理解整个工程为什么这么计数参数数值说明像素时钟27 MHz标准 BT.601 采样时钟行周期64 μs一行总时长单行总字节数172827M × 64μs有效像素字节1440720 亮度 720 色度消隐与同步字节288EAV 4 消隐 280 SAV 4有效行数/帧576两场各 288 行帧总行数625PAL 制 D1 标称值单行结构从前往后依次是 EAV4 字节、水平消隐数据280 字节、SAV4 字节、有效像素区1440 字节。代码里 sync_gen 模块的计数器上限就是 1728这个数字是整个时序的根基——如果你把 1728 改成 1729画面会斜着走每一行都比上一行偏移一个字节。这个坑我见人踩过好几次所以拿到代码第一件事先确认行计数器是不是 1728而不是纠结消隐区填什么值。标题里写“576p”严格讲 BT656 传的是 576i隔行两场拼一帧但行业里说 576p 说习惯了这套代码按 BT656 标准处理隔行标志不影响使用。2.2 EAV/SAV 与 XY 字节同步是怎么锁定的EAV 和 SAV 的格式固定是 FF 00 00 XY 四个字节。前三个字节是为了自同步BT656 在 8 位模式下约定亮度信号只使用 16235色度信号使用 16240所以 0x00 和 0xFF 不会出现在有效视频数据里接收端看到连续三个字节是 FF 00 00就认为下一个字节是 XY 状态字。剩下关键全在 XY 上。XY 字节的高 3 位分别是 F场标志、V垂直消隐标志、H水平消隐标志低 5 位是保护位和固定位。保护位按下面规则生成P3 V XOR HP2 F XOR HP1 F XOR VP0 F XOR V XOR H接收端解析到 XY 后要重新算一遍保护位对不上就说明这一次同步头不可信需要回到 FF 00 00 搜索状态重新锁定。这在实际工程里不是可有可无的校验——板级信号抖动、跨时钟域采样毛刺都可能让 XY 字节被采错。如果丢了对保护位的检查偶发花屏会被当成玄学问题排查半天最后发现是代码直接忽略了低 5 位。F、V、H 三个标志的含义以及在代码里的用法可以用一张表说清楚标志位含义取值F当前是第几场0 表示第一场1 表示第二场V是否处于垂直消隐1 为消隐区0 为有效视频区H同步类型1 为 EAV0 为 SAV一行里 EAV 的 H1SAV 的 H0。垂直消隐期间 H1 的 EAV 照常出现但 SAV 后面不跟有效像素接收端借此把 V 标志同步到本地寄存器里。这套代码的 sync_gen 模块做的就是这个从比特流里连续识别 FF 00 00提取 XY更新 F/V/H 状态再输出给像素解析模块决定什么时候放行数据。没有这个同步过程后面所有像素对齐都是空中楼阁。3. Verilog 核心模块拆解接收、解析与发送的完整链路3.1 工程结构与顶层例化这套代码是典型的可综合 Verilog 写法没有厂商专用原语放进 Vivado、Quartus 或者 Yosys 都能编译。目录结构大致是这样sim/ bt656_tb.v // 仿真脚本发包 接收 自动统计 src/ bt656_top.v // 顶层例化接收与发送通路 bt656_sync_gen.v // 同步时序生成搜索 EAV/SAV、解析 XY bt656_pixel_parser.v// 像素解析把码流拆成 Y/Cb/Cr bt656_packet_gen.v // 发送端把像素打包成 BT656 码流顶层模块把发送端和接收端放在一起好处是两边可以互相打回环仿真时不需要外部信号源。核心代码如下module bt656_top #( parameter DATA_WIDTH 8, parameter ACTIVE_LINE 720, parameter ACTIVE_FRAME 576 )( input wire rst_n, input wire clk_27m, // 接收方向 input wire [DATA_WIDTH-1:0] bt656_in, output reg [7:0] y_out, output reg [7:0] cb_out, output reg [7:0] cr_out, output wire frame_vsync, output wire line_hsync, // 发送方向 input wire [7:0] y_in, input wire [7:0] cb_in, input wire [7:0] cr_in, input wire tx_req, output reg [DATA_WIDTH-1:0] bt656_out ); wire f_field; wire v_blank; wire v_active; bt656_sync_gen sync_inst ( .clk_27m(clk_27m), .rst_n(rst_n), .bt656_in(bt656_in), .f_field(f_field), .v_blank(v_blank), .v_active(v_active) ); bt656_pixel_parser parser_inst ( .clk_27m(clk_27m), .rst_n(rst_n), .bt656_in(bt656_in), .v_blank(v_blank), .y_data(y_out), .cb_data(cb_out), .cr_data(cr_out) ); // 发送通路见 bt656_packet_gen 模块 endmodule第一个要注意的是 DATA_WIDTH 参数被单独提出来。很多人写 BT656 直接把位宽写死成 8但广播级设备不少走 10 位模式10 位下同步码是 0x3FF、0x000、0x000消隐电平是 0x200和 8 位完全不同。把位宽参数化换 10 位时只需要改顶层参数和消隐默认值不用动时序状态机。第二个要注意的是 v_active 由 sync_gen 输出parser 模块只使用 v_blank 做数据有效判断。不要小看这一层隔离BT656 的 SAV/EAV 是脉冲式出现的如果直接用组合逻辑去判断像素有效高电平期间的一个毛刺就会丢一整行把状态判断收进 sync_gen用寄存器打拍输出毛刺最多影响一个周期下一拍数据重新锁定不会造成整行错乱。3.2 同步头检测与 XY 字节解码接收方向最核心的是 bt656_sync_gen它完成三件事搜索 FF 00 00 序列锁定并提取 XY 字节根据 XY 更新 F/V/H 状态并给出有效视频窗口。代码核心片段如下module bt656_sync_gen #( parameter ACTIVE_LINE 720 )( input wire clk_27m, input wire rst_n, input wire [7:0] bt656_in, output reg f_field, output reg v_blank, output reg v_active ); reg [7:0] delay0, delay1, delay2; reg [7:0] xy_byte; reg sync_locked; // 三级移位寄存器捕捉连续出现的 FF 00 00 always (posedge clk_27m or negedge rst_n) begin if (!rst_n) begin delay0 8h00; delay1 8h00; delay2 8h00; end else begin delay0 bt656_in; delay1 delay0; delay2 delay1; end end wire sync_candidate (delay2 8hFF) (delay1 8h00) (delay0 8h00); // 第四个字节就是 XY锁进来并解析 F/V/H always (posedge clk_27m or negedge rst_n) begin if (!rst_n) begin xy_byte 8h00; f_field 1b0; v_blank 1b1; sync_locked 1b0; end else if (sync_candidate) begin xy_byte bt656_in; f_field bt656_in[7]; v_blank bt656_in[6]; sync_locked 1b1; end end reg [10:0] pixel_cnt; wire is_sav sync_locked (xy_byte[5] 1b0); wire is_eav sync_locked (xy_byte[5] 1b1); always (posedge clk_27m or negedge rst_n) begin if (!rst_n) begin pixel_cnt 11d0; end else if (is_sav) begin pixel_cnt 11d0; end else if (pixel_cnt ACTIVE_LINE * 2 - 1) begin pixel_cnt pixel_cnt 1b1; end end assign v_active sync_locked !v_blank !is_eav (pixel_cnt ACTIVE_LINE * 2); endmodulesync_candidate 在 delay0 等于 0x00 的周期拉高下一个时钟边沿读到的 bt656_in 就是 XY 字节。这里用三级寄存器是有讲究的如果直接用组合逻辑比较输入线上的 FF 00 00在输入信号发生翻转的瞬间组合逻辑输出会有一小段毛刺而寄存器打拍把比较结果推迟了一个完整时钟周期毛刺被自然滤掉。pixel_cnt 的行计数以 SAV 为基准SAV 到来时清零之后每个有效字节加 1。为什么是 720 乘 2因为 4:2:2 格式下 720 个像素对应 1440 字节。v_active 的下降沿由 is_eav 控制这样即使前一行的数据还没走完EAV 一到就立刻切断输出防止把消隐区的杂散数据当成像素送出去。至于 F 字段它不开辟独立接口而是在模块内部被 f_field 锁存送帧缓存时用来计算隔行行号。3.3 发送方向从像素打包成 BT656 码流发包模块 bt656_packet_gen 的逻辑正好是接收的逆过程用行计数和场计数控制输出在每一行的 1728 个周期里依次输出 EAV、消隐数据、SAV、有效像素。核心框架如下module bt656_packet_gen #( parameter H_TOTAL 1728, parameter H_ACTIVE 1440 )( input wire clk_27m, input wire rst_n, input wire [7:0] y_in, input wire [7:0] cb_in, input wire [7:0] cr_in, output reg [7:0] bt656_out ); reg [10:0] h_cnt; reg [9:0] v_cnt; always (posedge clk_27m or negedge rst_n) begin if (!rst_n) begin h_cnt 11d0; v_cnt 10d0; end else if (h_cnt H_TOTAL - 1) begin h_cnt 11d0; if (v_cnt 10d624) v_cnt 10d0; else v_cnt v_cnt 1b1; end else begin h_cnt h_cnt 1b1; end end reg [7:0] y_d1; reg [7:0] cb_d1; reg [7:0] cr_d1; always (posedge clk_27m or negedge rst_n) begin if (!rst_n) begin y_d1 8h10; cb_d1 8h80; cr_d1 8h80; end else begin y_d1 y_in; cb_d1 cb_in; cr_d1 cr_in; end end always (posedge clk_27m or negedge rst_n) begin if (!rst_n) begin bt656_out 8h80; end else begin if (h_cnt 0) bt656_out 8hFF; else if (h_cnt 1) bt656_out 8h00; else if (h_cnt 2) bt656_out 8h00; else if (h_cnt 3) bt656_out 8h80; // EAV 的 XYH1 else if (h_cnt 4 h_cnt 284) bt656_out (h_cnt[0]) ? 8h80 : 8h10; // 消隐区 else if (h_cnt 284) bt656_out 8hFF; else if (h_cnt 285) bt656_out 8h00; else if (h_cnt 286) bt656_out 8h00; else if (h_cnt 287) bt656_out 8h1D; // SAV 的 XYH0 else begin // 有效像素区按 Cb/Y/Cr/Y 顺序输出 case (h_cnt[1:0]) 2d0: bt656_out cb_d1; 2d1: bt656_out y_d1; 2d2: bt656_out cr_d1; 2d3: bt656_out y_d1; endcase end end end endmodule这里最难理解的是把 h_cnt 的计数范围和 BT656 行的段对齐。EAV 在 03 周期消隐在 4283 周期SAV 在 284287 周期有效像素从 288 一直走到 1727。278 个消隐周期里填 0x80 和 0x10 交替注意不要填 0x00——如果一个长段全部填 0接收端的同步搜索器会看到一长串 00虽然不会立刻错锁但增加了误判概率。这属于工程习惯后面避坑章节会展开。XY 字节在第 3 章代码里简化成了常量实际项目中应该用 F/V/H 标志动态生成EAV 的 H1SAV 的 H0垂直消隐区 V1第二场 F1。这一层的 XY 值如果写错接收端解析出来的场标志会乱隔行拼接图像直接碎掉。我一般会把 XY 生成单独写成一个函数接收端和发送端共用同一个生成逻辑避免两边不一致。4. 仿真与上板从 testbench 到 27MHz 实物信号的完整验证4.1 搭一个能自检的 testbench拿到代码第一个动作不是上板而是先把 testbench 跑通。工程自带 sim/bt656_tb.v思路是发包模块生成一段模拟 BT656 码流喂给接收模块解析再用计数器统计有效像素总数和理论值比对。核心骨架如下module bt656_tb; reg clk_27m; reg rst_n; wire [7:0] bt656_intf; wire [7:0] y, cb, cr; wire frame_vsync, line_hsync; initial begin clk_27m 0; forever #18.518 clk_27m ~clk_27m; end initial begin rst_n 0; #100; rst_n 1; end bt656_packet_gen u_gen ( .clk_27m(clk_27m), .rst_n(rst_n), .y_in(8h10), .cb_in(8h80), .cr_in(8h80), .bt656_out(bt656_intf) ); bt656_top u_top ( .clk_27m(clk_27m), .rst_n(rst_n), .bt656_in(bt656_intf), .y_out(y), .cb_out(cb), .cr_out(cr) ); integer valid_cnt 0; always (posedge clk_27m) begin if (line_hsync) valid_cnt valid_cnt 1; end initial begin $dumpfile(bt656_tb.vcd); $dumpvars(0, bt656_tb); #(37.037 * 1728 * 625 * 2); // 跑两帧即四场 $display(valid pixel count: %0d, valid_cnt); $display(valid_cnt 414720 ? TEST PASS : TEST FAIL); $finish; end endmodule#(37.037 * 1728 * 625 * 2) 这句需要解释27MHz 的时钟周期约 37.037ns一行 1728 个周期PAL 一帧 625 行乘 2 表示连续跑两帧四场。为什么不跑一帧就结束因为 sync_gen 模块依赖 SAV 对齐仿真刚开始时状态机可能正好落在同步头半路第一场的头几个同步会不稳定完整跑完一场后才进入稳态。如果你只看前几百个周期就判断模块没输出那是误杀。valid_cnt 的理论值是 720×576414720 个像素时钟周期。testbench 通过判断计数是否等于这个数来决定 PASS/FAIL不需要人工盯波形。这个方法比肉眼检查波形高效得多——一旦你改过代码导致像素区宽度变化计数器立刻报警。4.2 波形里盯住这几个信号跑完仿真打开 VCD 文件后第一件事是看四个信号bt656_in、sync_candidate、v_blank、v_active。sync_candidate 应该每个行周期精确出现两次一次是行末的 EAV一次是行尾的 SAV。如果 sync_candidate 出现频率不稳定说明移位寄存器或者输入时钟有问题。最常见的原因是 clk_27m 不是真正的 27MHz而是开发板上的 25MHz 晶振分频而来两者差 7.4%同步码间隔会逐行漂移最终导致 sync_locked 周期性失锁。仿真阶段用的时钟一定要和目标板卡一致不然仿真通过也说明不了问题。v_blank 信号在正确波形里应该是一个较宽的脉冲PAL 制每场大约有 2425 行垂直消隐v_blank 拉高期间不输出任何有效数据。如果 v_blank 每场只拉高一次或者从不拉高问题大概率在 sync_gen 的场计数逻辑而不是像素解析模块。最后一个值得关注的是时序对齐y_out、cb_out、cr_out 与 line_hsync 之间不应在同一拍翻转。解析器内部有寄存器延迟hsync 拉高后数据要再等 12 拍才真正有效。写外部 FIFO 时不能用 hsync 当写使能而要用 hsync 延迟两拍后的信号否则存入 FIFO 的第一个数据会是无效字节。这是 BT656 接收端最常见的隐性 bug仿真波形上看着一切正常接到真实像素数据后一帧图像首行错位。4.3 上板验证的检查点仿真通过后上板我习惯按这个顺序检查先查时钟。多数 FPGA 开发板没有 27MHz 晶振需要由 PLL 从 50MHz 或 100MHz 倍频分频得到 27MHz例如 50MHz 乘 27 再除 50。PLL 锁定需要时间上电后要等 locked 信号拉高再释放复位否则锁相环还没稳定前端采样进来的第一个码流就是乱的。再查电平标准。BT656 并行接口通常走 LVCMOS 3.3V如果板卡上视频解码芯片输出的是 1.8V 电平而 FPGA 引脚约束配成了 3.3V高电平 1.8V 低于 VIH 阈值采到的全是 0。这个错误在仿真阶段完全看不出来属于典型的“仿真通过上板翻车”。最后查行场计数。把 v_active 接到一个 GPIO 上用示波器看正常应该是行频 15.625kHz、脉冲宽度约 64μs 的信号如果看不到或者频率偏差超过 5%大概率是同步丢失。实物信号一旦出现间歇性丢同步优先怀疑输入时钟的质量其次再怀疑代码逻辑。5. 避坑指南BT656 联调里最容易翻车的 5 个环节5.1 现象隔行图像上下错位静止画面出现锯齿边现象视频源是隔行摄像头采集画面里竖线条出现横向交错锯齿像被梳子梳过一样。原因FPGA 内部把 F0 第一场和 F1 第二场的行顺序搞反了。720×576 是隔行格式第一场包含第 1、3、5 行第二场包含第 2、4、6 行。如果解析模块拿到 F 标志后没有把它参与行号计算而是简单地把两场数据连续拼接显示时奇数行和偶数行的位置互换锯齿就出现了。解决解析模块里加一个行号累加器F 字段作为行号的高位。帧缓存写入时行地址按 line_num {f_field, line_cnt_within_field} 生成。这样第一场从 0 开始写偶数行第二场从 1 开始写奇数行拆开的场又重新合回正确的帧。5.2 现象画面整体偏绿或偏品红现象图像能出来但颜色完全不对偏绿或偏品红黑白场景尤其明显。原因Cb 和 Cr 的通道互换或者 Y 被当成 Cb 送去做了色度。BT656 的字节顺序是 Cb Y Cr Y如果 seq_cnt 清零时机不对新场第一个字节被当成 Y整个场色度通道偏一个字节。解决确认 seq_cnt 的复位点是 SAV 而不是 EAV。在垂直消隐期间要强制清零保证下一场第一个取到的字节就是 Cb。如果 sinal 丢了一拍把解析器的 seq_cnt 改到和 SAV 对齐的位置重新仿真一次色度就回来了。5.3 现象同步头频繁丢锁图像周期性撕裂现象波形上 sync_candidate 出现频率不稳定图像每隔一段时间撕裂一次。原因输入数据没有同步到 FPGA 内部的 27MHz 时钟域。BT656 并行接口的信号和时钟在外部是同步的但经过 PCB 走线和引脚延迟后数据相对时钟会有偏移。如果直接用系统时钟采样数据在建立保持时间边缘翻转时会被采到不确定值。解决在 bt656_in 输入处打两拍第一拍跨时钟域第二拍消除亚稳态。如果偏移较大可以用 FPGA 的 IDELAY 或引脚逻辑延迟做相位调整。另外如果视频源芯片和 FPGA 不是同一个参考时钟供电建议用一个异步 FIFO 把两个时钟域彻底隔离。5.4 现象消隐区全填 0x00偶发整行丢失现象图像大多数时候正常但偶发一整行变黑或者一行错位频率没有规律。原因发送端在水平消隐区填了一长串 0x00。BT656 的同步搜索器靠 FF 00 00 识别同步头如果消隐区出现连续 0x00搜索器会在一串 00 里反复尝试匹配消耗掉状态机的回锁时间等到真正的同步头来临时可能刚好错过。解决消隐区填充 0x80 和 0x10 交替不要全填 0。0x80 在 8 位模式下是色度消隐电平0x10 是亮度黑电平既符合 BT.601 的消隐规范又不会干扰同步码识别。如果你从别的工程复制代码务必检查发包模块消隐区的填充值。5.5 现象仿真全部通过上板后没有任何输出现象testbench 里 PASS下载到板卡后 bt656_out 一点波形都没有。原因不是代码问题是复位时序问题。很多开发板用按键产生复位信号按键松开是低电平复位但 PLL 的输出时钟在锁定前是不稳定的。如果复位释放得太早同步状态机在乱时钟下初始化到一个错误状态之后再也回不来。解决用 PLL 的 locked 信号参与复位逻辑写成 rst_n button_rst_n pll_locked。同时确认仿真里也是这么写的不能仿真里直接拉高复位、上板才用 locked两者不一致时仿真通过没有参考意义。6. 二次开发把 BT656 前端接进你自己的图像链路6.1 从 Y/Cb/Cr 到 AXI-Stream 的适配这套代码输出的是 Y/Cb/Cr 三路 8 位数据和行场标志。如果你的下游是 DDR 帧缓存控制器通常需要把并行视频转成 AXI-Stream 接口配合 VDMA 使用。转换思路不难还是按照 BT656 的字节顺序把 Cb Y Cr Y 四个字节打包成 32 位 AXI-Stream 的 tdata行尾拉高 tlast场起始拉高 tuser。// 伪代码示意把 Cb/Y/Cr/Y 打包成 AXI-Stream reg [31:0] axis_tdata; reg axis_tvalid; reg axis_tlast; always (posedge clk_27m) begin if (v_active) begin case (pixel_cnt[2:0]) 3d0: axis_tdata[31:24] cb; 3d1: axis_tdata[23:16] y; 3d2: axis_tdata[15:8] cr; 3d3: axis_tdata[7:0] y; endcase end end关键点在于 tlast 与字节计数对齐。当 pixel_cnt 走到 1438 时数据是每行最后一个像素应该让 tlast 在这个周期置位。和 4.2 节说的一样tvalid 不要直接用 v_active而是用延迟两拍后的版本保证数据先稳定再拉高。6.2 一个值得保留的扩展点把位宽和行场做成参数我拿到别人的视频前端代码第一件事就是看参数列表。这套代码把 DATA_WIDTH、ACTIVE_LINE、ACTIVE_FRAME 全部参数化这点做得比大多数网上开源工程好。做二次开发时我习惯再加一个参数叫 LINE_TOTAL用来适配不同制式PAL 是 625NTSC 是 525有效区 720×480。改参数比改代码安全得多改完只需重新跑一遍 testbenchvalid_cnt 理论值跟着改一下就能验证。如果做 10 位扩展记得把消隐默认值从 8h80/8h10 改成 10h200/10h040同步码也要从 8hFF 改为 10h3FF。这个坑在 3.1 节提过实际改起来还有一处容易漏packet_gen 里 h_cnt[1:0] 的位宽匹配没问题但接收端的三级移位寄存器比较也要改成 10 位不然 10 位数据比较 8 位常量永远匹配不上。从那以后我每接到一个新的视频前端 IP都会先强制走一遍“打两拍 检查 sync_candidate 间隔 跑完整两帧仿真”的流程再连下游模块省掉了无数上板后抓头发的夜晚。希望帮到你。本文还有配套的精品资源点击获取