简介面向FPGA视频接口开发与学习的BT656 Verilog工程包聚焦720×576576p标清逐行扫描输出帮助读者理解RGB888到YCbCr色彩空间转换、行场同步、串行时序控制、字节打包及同步字插入等关键环节适合具备一定Verilog基础、希望打通视频采集与显示链路的开发者参考。压缩包共130个文件约4.14MB以Quartus工程编译产生的cdb、hdb、ddb等数据库文件及rpt报告为主同时包含qsf引脚与工程设置、sof下载文件、Verilog源码和readme说明便于对照综合布线结果、时序与资源占用排查设计问题。已有5148人学习下载。工程保留较完整的编译中间产物与报告可用于观察720×576 BT656数据流的模块划分、时序约束与引脚分配思路并借助映射、适配和汇编报告定位拥塞、时序违例或同步异常。对需要快速验证RGB888转BT656输出、复盘FPGA视频接口实现流程的读者具有较强实践参考价值。1. 先看清这项目一条 8bit 数据线怎么变成 720×576 的画面手里那块摄像头模组只给出 8 根数据线和一个 27MHz 时钟上电就有波形可抓下来全是 FF 00 00 打头的字节流像素在哪、行从哪开始、一帧有多长全凭猜——这就是 BT656 接口的日常。这套 720×576 的 Verilog 代码干的事情很具体把 8bit 的 BT656 裸流拆成能看的画面解析 SAV/EAV 时序基准码切出 720×576576p 隔行25fps的有效像素区把 4:2:2 的 YUV 拼成完整像素再转成 RGB 送去显示。它面向的是做 FPGA 视频采集入门、需要把手里的 PAL 模组或 CVBS 解码芯片接上屏的开发者纯 Verilog HDL 实现不依赖厂商 VIP仿真、综合、上板三处都能走一遍。代码量不大但协议细节密集属于那种“看懂了觉得简单没看懂能把人按在地上摩擦”的类型。2. 协议与数据通路一行 1728 字节里的门道BT656 最反直觉的地方在于它没有独立的行场同步线。行从哪开始、场从哪结束全部藏在数据流的特殊字节里。所以任何一份 BT656 解码代码第一件事都不是处理像素而是“找边界”。把一行拆开来看720 个像素的 YUV 4:2:2 数据是 1440 字节加上前导的同步码和中间的消隐填充一行总共 1728 字节。625 行 × 25 帧 × 1728 字节 27,000,000正好对上 27MHz 的像素时钟——这个整除关系不是巧合是标准定死的结果也是后面写状态机时最重要的一个算术校验点。2.1 EAV/SAV 时序基准码与 XY 状态字每一行的开头是 EAVEnd of Active Video有效数据开始前是 SAVStart of Active Video两者都是 4 个字节FF 00 00 XY。前三个字节固定第四个字节 XY 才是信息载体。它拆成两半用高 4 位是标志位低 4 位是保护位。bit7 恒为 1bit6 是场标志 Fbit5 是场消隐 Vbit4 是行消隐 Hbit3~bit0 依次是 P3、P2、P1、P0其中 P3V、P2H、P1F、P0V⊕H⊕F。这个保护位的设计很实在万一数据里恰好出现了FF 00 00靠 P0 的奇偶校验和 bit7 的强制 1基本可以排除误判。八种组合的 XY 值可以直接背下来调试时扫一眼波形就知道当前在哪F场V场消隐H行消隐XY 值含义0000x80第一场有效行SAV0010x9D第一场有效行EAV0100xAB第一场场消隐SAV0110xB6第一场场消隐EAV1000xC7第二场有效行SAV1010xDA第二场有效行EAV1100xEC第二场场消隐SAV1110xF1第二场场消隐EAV凭这张表就能判断H0 说明后面紧跟着的是有效像素H1 说明这一行到此为止V1 说明整行都是消隐一个像素都不用采。实际写代码时不需要穷举八个常量把 F、V、H 三个 bit 提出来做条件判断更省事也更容易在换制式时复用。2.2 4:2:2 字节序与 720 像素的两拍对齐BT656 的 8bit 模式是字节复用一个时钟周期只走一个字节像素数据按Cb Y Cr Y的顺序排列。这意味着一个像素的亮度 Y 每拍都有但色度 U也就是 Cb和 VCr是每两个像素共享一组。720 个像素对应 1440 个字节第 0 字节是 Cb、第 1 字节是 Y0、第 2 字节是 Cr、第 3 字节是 Y1以此类推。这里最容易翻车的是顺序很多人下意识按Y Cb Y Cr去解结果画面能出来但整体偏绿或者偏紫肤色像中毒。原因就在于标准写的是 Cb 在前、Cr 在后而很多图形库、图像格式文档里习惯把 Y 放第一位。血泪经验是拿到任何一份 BT656 源码先看它取色度的两个字节谁先谁后别信注释看代码里第一个色度字节存进了 U 还是 V。再说消隐区。一行的 1728 字节减去 1440 字节有效数据剩 288 字节构成是4 字节 EAV 280 字节填充 4 字节 SAV。填充字节通常是 0x80 或 0x10具体值不重要重要的是解码逻辑必须在 SAV 之后才开始计数、在 EAV 处清零。场结构上625 行里真正带像素的只有 576 行两场各 288 行其余 49 行是场消隐V 位全程为 1。所以一个完整的帧同步逻辑需要两个计数器一个是行内字节计数0~1727一个是帧内行计数0~624两者都靠 SAV/EAV 脉冲复位而不是靠外部 hsync/vsync。3. 核心模块拆解从 8bit 裸流到 RGB 像素整份工程按数据流切成四级流水同步码检测 → 有效数据门控 → 4:2:2 转 4:4:4 → YUV 转 RGB。每一级之间用 valid 信号串起来不用全局使能这样即使某一级因为 FIFO 满而反压前面的逻辑也不会把数据吞掉。下面按模块顺序拆代码都可以直接抠出来单独仿真。3.1 顶层端口与时钟域划分顶层只做两件事例化子模块、定义时钟域。视频输入侧是 27MHz由模组或解码芯片提供这个时钟必须用来采数据不能换成板上的晶振否则采到的字节会整体偏移。输出侧是像素时钟如果直接驱动 VGA 或 RGB 屏通常就是 27MHz如果转 HDMI则是 74.25MHz 之类两者之间必须过异步 FIFO。module bt656_top #( parameter H_ACTIVE 720, // 一行有效像素数 parameter V_ACTIVE 576, // 一帧有效行数两场合计 parameter H_TOTAL 1728 // 一行总字节数含消隐 )( input wire rst_n, // ---- 视频输入侧27MHz 域 ---- input wire vid_clk, // 必须由 BT656 源提供 input wire [7:0] vid_data, // 8bit 复用数据流 input wire vid_de, // 无行场线的模组可恒接 1b1 // ---- 像素输出侧像素时钟域 ---- input wire pix_clk, output reg vs_out, output reg hs_out, output reg de_out, output reg [7:0] r_out, output reg [7:0] g_out, output reg [7:0] b_out ); // 27MHz 域内部信号 wire sav_pulse, eav_pulse; wire f_field, v_blank; wire [7:0] y_pix, u_pix, v_pix; wire pix_valid; // 同步检测找出每行的起止 bt656_sync_detect u_sync ( .clk (vid_clk), .rst_n (rst_n), .din (vid_data), .din_en (vid_de), .sav_pulse (sav_pulse), .eav_pulse (eav_pulse), .f_field (f_field), .v_blank (v_blank) ); // 4:2:2 - 4:4:4输出逐像素的 Y/U/V yuv422_to_444 u_422 ( .clk (vid_clk), .rst_n (rst_n), .din (vid_data), .din_en (vid_de ~v_blank ~sav_pulse ~eav_pulse), .sav_pulse (sav_pulse), .y_out (y_pix), .u_out (u_pix), .v_out (v_pix), .pix_valid (pix_valid) ); // 后面的 YUV-RGB 与 FIFO 跨时钟域见 3.3、3.4 endmodule端口设计上有两个取舍值得说。vid_de这个信号很多模组根本不输出但保留它可以让同一份代码兼容“带 DE 的并行接口”和“纯 BT656 裸流”接法上区别只是恒接 1 还是接真实 DE。f_field单独引出来是因为隔行视频的两场必须分开处理后面做去隔行时它就是关键的场选择信号如果在这里把它丢掉二次开发时就得回头改顶层。3.2 同步码检测四字节滑窗加保护位校验这个模块是整个工程的地基。思路是拿一个 3 级移位寄存器缓存历史字节当前字节din加上d0/d1/d2正好构成 4 字节窗口当窗口内容为FF 00 00 XY且 XY 的保护位校验通过时判定命中。module bt656_sync_detect ( input wire clk, input wire rst_n, input wire [7:0] din, input wire din_en, output reg sav_pulse, // SAV 命中单拍脉冲 output reg eav_pulse, // EAV 命中单拍脉冲 output reg f_field, // 0: 第一场 1: 第二场 output reg v_blank // 1: 当前处于场消隐 ); reg [7:0] d0, d1, d2; // d0 最早din 最新 always (posedge clk or negedge rst_n) begin if (!rst_n) begin d0 8h00; d1 8h00; d2 8h00; end else if (din_en) begin d0 din; d1 d0; d2 d1; end end // 前三个字节必须是 FF 00 00且 XY 的 bit7 恒为 1 wire hdr_hit din_en (d2 8hFF) (d1 8h00) (d0 8h00) din[7]; // 保护位校验P3V P2H P1F P0V^H^F wire chk_ok (din[3:0] {din[5], din[4], din[6], din[5]^din[4]^din[6]}); wire is_sav hdr_hit chk_ok (din[4] 1b0); wire is_eav hdr_hit chk_ok (din[4] 1b1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin sav_pulse 1b0; eav_pulse 1b0; f_field 1b0; v_blank 1b0; end else begin sav_pulse is_sav; eav_pulse is_eav; if (is_sav || is_eav) begin f_field din[6]; // F 位 v_blank din[5]; // V 位 end end end endmodule参数和逻辑说明几点。din_en参与移位寄存器的使能保证在数据间隙不会把旧字节反复移位造出假同步码如果模组始终有数据输出这里恒接 1 也不会出错只是多消耗一点动态功耗。chk_ok用拼接的方式现场算 P3~P0而不是写八个常量比较好处是换 NTSC 或者做 10bit 变体时不用改表。f_field和v_blank只在同步码命中的那一拍更新之后一直保持正好可以当作“当前行、当前场属性”来用。这两个信号是后面所有门控逻辑的依据v_blank为 1 时整行都不该有像素输出f_field变化时表示新的一场开始帧计数器要复位。实际调试时这个模块最值得用在线逻辑分析仪抓来看的就是sav_pulse和eav_pulse的间隔。正常情况下两者之间应该正好是 1440 个din_en有效的时钟周期。如果抓出来是 1438 或者 1442说明数据源的行长度和标准不一致后面的像素计数器会慢慢漂移画面表现为从某一行开始整体左移。3.3 行缓存与 4:2:2 转 4:4:4检测到 SAV 之后接下来的字节按 Cb、Y0、Cr、Y1 循环排列。转 4:4:4 的核心是在 Y 字节到达的那一拍把对应的 U、V 凑齐输出。这里用一个 2bit 的字节序号做状态配合两个保持寄存器。module yuv422_to_444 ( input wire clk, input wire rst_n, input wire [7:0] din, input wire din_en, input wire sav_pulse, output reg [7:0] y_out, output reg [7:0] u_out, output reg [7:0] v_out, output reg pix_valid ); reg [1:0] byte_idx; // 0Cb 1Y0 2Cr 3Y1 reg [7:0] u_hold, v_hold; reg [7:0] v_prev; // 上一像素对的 Cr always (posedge clk or negedge rst_n) begin if (!rst_n) begin byte_idx 2d0; pix_valid 1b0; u_hold 8h80; v_hold 8h80; v_prev 8h80; end else if (sav_pulse) begin byte_idx 2d0; // 每行 SAV 处清零防止漂移累积 pix_valid 1b0; end else if (din_en) begin pix_valid 1b0; // 默认无效只有 Y 到达才拉高 case (byte_idx) 2d0: begin // Cb u_hold din; byte_idx 2d1; end 2d1: begin // Y0 y_out din; u_out u_hold; v_out v_prev; // 用上一对的 Cr 保证对齐 pix_valid 1b1; byte_idx 2d2; end 2d2: begin // Cr v_hold din; byte_idx 2d3; end default: begin // Y1 y_out din; u_out u_hold; v_out v_hold; pix_valid 1b1; v_prev v_hold; byte_idx 2d0; end endcase end end endmodule这里的写法把pix_valid只在 Y 字节处拉高一拍U 和 V 在那一拍已经稳定下游可以直接用pix_valid当时钟使能。sav_pulse复位byte_idx这一步不能省一帧 625 行、每行 1440 字节理论上一定能被 4 整除但如果某次上电时时钟抖动让模组多发了一个字节靠 SAV 复位能保证下一行重新对齐不至于一路错到底。关于v_prev这是一个有意的折中。严格来说 Y0 应该配本像素对的 Cr也就是下一个字节但那需要提前一到两拍拿到数据得再加一级缓存。用上一对的 Cr 带来的效果是色度在水平方向滞后一个像素对 720×576 的 PAL 画面来说肉眼完全看不出换来的是逻辑简单、时序宽松。如果做文字或者细线条画面要求严格对齐可以改成用双口 RAM 预取两个字节代价是多一个读延时。3.4 YUV 转 RGB 的定点乘法BT656 送出来的是 YUV最终上屏或者写显存多半要 RGB。转换公式里带小数系数FPGA 里必须转成定点乘法否则综合工具会直接给你例化一堆浮点 IP。常用的做法是乘 256 再右移 8 位Q8 精度对这一级来说足够了。module yuv2rgb ( input wire clk, input wire rst_n, input wire [7:0] y_in, u_in, v_in, input wire de_in, output reg [7:0] r_out, g_out, b_out, output reg de_out ); // Q8 定点系数真实值 × 256 localparam signed [19:0] C_RV 20sd359; // 1.402 localparam signed [19:0] C_GU 20sd88; // 0.344 localparam signed [19:0] C_GV 20sd183; // 0.714 localparam signed [19:0] C_BU 20sd454; // 1.772 reg signed [19:0] r_acc, g_acc, b_acc; reg signed [19:0] u_off, v_off; reg [7:0] y_d; reg de_d1, de_d2, de_d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin r_acc 20sd0; g_acc 20sd0; b_acc 20sd0; u_off 20sd0; v_off 20sd0; y_d 8d0; de_d1 1b0; de_d2 1b0; de_d3 1b0; end else begin // U/V 去偏置Y 左移 8 位变成同一量纲 u_off $signed({2b00, u_in}) - 20sd128; v_off $signed({2b00, v_in}) - 20sd128; y_d y_in; r_acc $signed({3b000, y_in, 8d0}) v_off * C_RV; g_acc $signed({3b000, y_in, 8d0}) - u_off * C_GU - v_off * C_GV; b_acc $signed({3b000, y_in, 8d0}) u_off * C_BU; // de_out 与三级乘法流水对齐 de_d1 de_in; de_d2 de_d1; de_d3 de_d2; de_out de_d3; end end // 右移 8 位并做 8bit 饱和截断 always (posedge clk or negedge rst_n) begin if (!rst_n) begin r_out 8d0; g_out 8d0; b_out 8d0; end else begin r_out (r_acc 8) 0 ? 8d0 : (r_acc 8) 20sd255 ? 8d255 : r_acc[15:8]; g_out (g_acc 8) 0 ? 8d0 : (g_acc 8) 20sd255 ? 8d255 : g_acc[15:8]; b_out (b_acc 8) 0 ? 8d0 : (b_acc 8) 20sd255 ? 8d255 : b_acc[15:8]; end end endmodule几个参数得解释清楚。y_off那部分直接写成{3b000, y_in, 8d0}等价于y_in × 256符号位补 3 个 0 是因为 Y 只有 8bit 无符号扩到 20bit 才不会在减法里溢出。u_off和v_off的$signed强制转换必须加Verilog 里混合有符号和无符号运算综合工具的行为有时候跟仿真对不上这是经典玄学问题写清楚比事后查波形便宜得多。饱和截断不能省。理论上 YUV 转到 RGB 出来的值应该在 0~255 之间但 BT656 常用的 Y 范围是 16~235、色度是 16~240所谓 studio range超出范围的值如果不截断直接取低 8 位会看到高光处突然变黑或者暗部变白那种“鬼影”现象就是这样来的。如果确实要按 studio range 处理还得再做一次(Y-16) × 255/219的拉伸这个放在二次开发里讲。4. 仿真与上板让这套代码真跑出画面代码写完只是第一步BT656 这种协议密集型的模块仿真跑通和上板出图是两个完全不同的难度等级。很多人卡在仿真全绿、上板黑屏问题往往出在激励不够真实或者观测手段不足而不是逻辑真的错了。这一章按“搭工程 → 造激励 → 上板抓”的顺序走一遍。4.1 工程结构与搭建顺序目录建议按功能分层不要把所有文件堆在一个平铺目录里。BT656 相关的东西复用性强分开放以后换项目直接拷子目录就行。bt656_720x576/ ├── rtl/ │ ├── bt656_top.v # 顶层只做例化和连线 │ ├── bt656_sync_detect.v # EAV/SAV 检测 │ ├── yuv422_to_444.v # 字节解复用 │ ├── yuv2rgb.v # 颜色空间转换 │ ├── timing_gen.v # 输出行场时序VGA/HDMI │ └── async_fifo.v # 27MHz 跨到像素时钟 ├── sim/ │ ├── tb_bt656_top.v # 顶层 testbench │ └── frame_720x576.hex # 一帧裸流脚本生成 ├── script/ │ └── gen_bt656.py # 生成 hex 激励 └── constr/ └── bt656_pin.xdc # 引脚与时钟约束搭建顺序上有讲究先把bt656_sync_detect单独仿真只看sav_pulse、eav_pulse和v_blank对不对再级联yuv422_to_444检查pix_valid每行是不是正好 720 个最后接上yuv2rgb和输出时序。反过来做的话一旦画面不对你没法判断是同步没对齐、像素少了、还是颜色算错了。约束文件里最关键的一条是输入时钟。vid_clk必须约束成 27MHz 并且指定为输入端口综合工具默认不会给它建时钟树不约束的话时序分析直接跳过这一路上板后偶发花屏完全没法查。4.2 用脚本造一帧 720×576 的 BT656 激励手写 testbench 生成 625 行 × 1728 字节不现实正确做法是用脚本生成 hex 文件仿真时$readmemh读进去。下面这个 Python 脚本生成一帧带彩条图案的 PAL 制 BT656 裸流色度用简单的水平渐变方便在波形里分辨行边界。# gen_bt656.py —— 生成一帧 720x576 的 BT656 裸流hex 格式 W_ACTIVE 720 H_TOTAL 1728 H_BLANK H_TOTAL - W_ACTIVE * 2 # 288 字节 V_TOTAL 625 V_BLANK_TOP 23 # 场消隐行数近似值够仿真用 def xy_byte(f, v, h): 按 F/V/H 生成 XY 状态字含保护位 p3, p2, p1 v, h, f p0 p3 ^ p2 ^ p1 return 0x80 | (f 6) | (v 5) | (h 4) | (p3 3) | (p2 2) | (p1 1) | p0 def yuv422_line(y_val, base_u, base_v): 生成一行 1440 字节的 Cb Y Cr Y 数据 out [] for x in range(W_ACTIVE): # 每两个像素共享一组色度这里用轻微渐变模拟彩条 if x % 2 0: out.append((base_u x // 8) 0xFF) # Cb p a hrefhttps://download.csdn.net/download/u013867758/10814466 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p