☰
BT.656 Verilog 720×576隔行接收:同步码与行场计数避坑
2026/10/26 2:25:08 网站建设 项目流程

简介:这份面向FPGA视频接口开发的Verilog工程,聚焦720×576(576p)分辨率下将RGB888像素转换为符合BT656标准的YCbCr数据流,适合已具备Verilog基础、希望理解数字视频时序与色彩空间转换的开发者用于学习验证和二次开发。压缩包共130个文件、约4.14MB,以Verilog源码、Quartus工程文件(qpf/qsf)、编译数据库(cdb/hdb)、布局布线报告、sof配置文件及readme说明为主,保留了从源码到综合实现的完整工程痕迹。资源围绕行场同步、RGB到YCbCr矩阵运算、串行时序控制、24位到8位字节打包以及同步字插入等关键环节展开,可帮助读者对照工程理解BT656数据流的组织方式,并参考编译报告排查时序与资源问题。工程中的可综合模块、引脚与工程配置、烧录文件相互配套,便于在FPGA平台上复现576p输出,也可为视频采集、监控显示等场景的接口设计提供参考。目前已有5148人学习下载。

1. 一包 BT656 verilog 代码 720*576:先别急着上板,先算清 625 行

拿到“BT656 verilog代码720*576”这类资源,最常见的翻车不是综合不过,而是把 720×576 直接当 576p 逐行送进显示器:图像能出,场序一翻就上下抖。BT.656 的 720×576 在 PAL 制式下是 25fps 隔行,每帧 625 行、两场各 288 个有效行,8bit 数据流里嵌着 EAV/SAV 同步码,没有独立 hsync/vsync。这份 Verilog 代码要解决的事很具体:在 27MHz 像素时钟域里把FF 00 00 XY抠出来,恢复行场有效信号,拆出 YUV 4:2:2,再交给后续去隔行或缩放。它适合做 FPGA 视频采集、老式摄像头接入、视频前端验证的从业者;新手可以按仿真步骤跑通,熟手重点看场计数和跨时钟域边界。关键词里的“576p”要留个心眼,原生 BT.656 不是逐行 576p,代码包按场解析才稳。

2. 720×576 的像素账:一行 1728 个时钟、1440 个有效字节和 625 行怎么对上

2.1 先看一行:EAV、280 字节消隐、SAV、1440 字节数据

BT.656 的 8bit 并行接口只有一根像素时钟、8 根数据线,行同步和场同步都藏在数据里。720×576 的 PAL 制式下,像素时钟常见 27MHz,一行不是 720 个时钟,而是 1728 个时钟。原因在于 YUV 4:2:2:每个像素占 2 个字节,720 个像素就是 1440 个字节,也就是 1440 个时钟;剩下的 288 个时钟是水平消隐,其中 8 个字节给 EAV/SAV 同步码,280 个字节是空白填充。

一行的字节顺序可以拆成四段:EAV 4 字节、水平消隐 280 字节、SAV 4 字节、有效数据 1440 字节。EAV 和 SAV 都是FF 00 00 XY四字节前导,区别在 XY 的 bit4:H=1 是 EAV,H=0 是 SAV。XY 的 bit7 固定为 1,bit6 是 F 场标志,bit5 是 V 垂直消隐标志,bit4 是 H 水平消隐标志,低 4 位是保护位。很多代码包直接拿XY == 8'h80判 SAV,这在某些场、某些消隐行会漏检,因为保护位会随 F/V/H 变化,不能把低 4 位写死。

项目数值说明
像素时钟27MHzPAL 720×576 常用时钟
一行总时钟1728包含消隐
有效像素720每像素 2 字节
有效字节/行1440YUV 4:2:2
水平消隐288 字节含 EAV/SAV 各 4 字节
一帧总行625隔行两场合计
有效行576每场 288 行
帧率25fps50 场/秒

这张表是后面所有计数器的基准。写 Verilog 时不要把 720 当成一行总长度,也不要把 1440 当成像素数;hcnt数到 1727 归零,de只在 1440 个有效字节期间拉高。参数化时至少留H_TOTAL=1728、H_ACTIVE=720、V_TOTAL=625、V_ACTIVE=576四个 localparam,换制式时才不用翻整份代码。

2.2 再看一场:F/V 两位决定 288 行有效区

720×576 是隔行,一帧 625 行,分奇偶两场,每场 312.5 行,实际按整行处理是每场 312 行左右,其中有效行 288 行。第一场有效行通常落在第 23 到 310 行,第二场落在第 336 到 623 行,中间是垂直消隐。不同前端芯片的行号起点可能差一两行,所以代码里不要硬编码“第 23 行开始有效”,而是用 XY 里的 V 位和 F 位判断:V=0 且当前处于有效行区间时,数据才是真正的 YUV;V=1 时即使有数据也要丢弃。

F 位表示场序,V 位表示垂直消隐。常见组合是:F=0、V=0 为第一场有效区;F=1、V=0 为第二场有效区;V=1 时是消隐行。解析时把 F 和 V 一起锁存,别只数行号。行号计数器在每行 EAV 时加一,但场切换时要用 SAV 的 F 位重新对齐,否则自由跑的计数器一旦被干扰就会半场错位。

这里有个容易忽略的细节:BT.656 的“行”是包含消隐的 1728 时钟,不是 720 像素。场计数应该按完整行递增,而不是按有效行递增。也就是说,垂直消隐行也要计数,否则 625 行的帧结构对不上,图像顶部会被裁掉或底部多出黑边。

2.3 同步检测状态机:不要用 hsync,直接用 FF 00 00 XY

资源包里的同步检测模块一般会做滑动窗口。下面这段代码用三个延迟寄存器找FF 00 00 XY,只比较高 4 位,避开保护位带来的误判。din_valid是像素时钟使能,27MHz 连续采样时可直接接1'b1;如果前端是 DDR 或门控时钟,必须用有效的字节使能,否则窗口会错位。

// 8bit BT.656 同步码检测:滑动窗口找 FF 00 00 XY module bt656_sync_detect ( input wire clk, input wire rst_n, input wire [7:0] din, input wire din_valid, output reg sav, // SAV 单周期脉冲 output reg eav, // EAV 单周期脉冲 output reg f_bit, // 场标志 output reg v_bit, // 垂直消隐 output reg h_bit // 水平消隐 ); reg [7:0] b0, b1, b2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin b0 <= 8'h00; b1 <= 8'h00; b2 <= 8'h00; sav <= 1'b0; eav <= 1'b0; f_bit <= 1'b0; v_bit <= 1'b0; h_bit <= 1'b0; end else if (din_valid) begin b0 <= din; b1 <= b0; b2 <= b1; sav <= 1'b0; eav <= 1'b0; // b2=FF, b1=00, b0=00, din=XY,且 XY 最高位为 1 if (b2 == 8'hFF && b1 == 8'h00 && b0 == 8'h00 && din[7]) begin f_bit <= din[6]; v_bit <= din[5]; h_bit <= din[4]; if (din[4]) eav <= 1'b1; // H=1 为 EAV else sav <= 1'b1; // H=0 为 SAV end end end endmodule

这段代码的时序关系要说明白:b0是din延迟一拍,b1延迟两拍,b2延迟三拍,所以当b2/b1/b0/din分别等于FF/00/00/XY时,四个字节刚好凑齐。输出sav/eav是单周期脉冲,后级用它做行场复位或锁存。参数上唯一要调的是din_valid:如果前端数据不是每个时钟都有效,必须把有效标志接进来,否则FF 00 00会被无效字节打断。

提示:不要用XY == 8'h80判断 SAV。低 4 位保护位在 F/V/H 变化时会变,只比较 bit7~bit4 更稳。

2.4 行场计数器:在 EAV 归零,在 SAV 锁存

同步脉冲出来之后,行场计数器就有了锚点。常见做法是:hcnt在 EAV 时归零,数到H_TOTAL-1回绕;vcnt在每行 EAV 的 V=1 区间递增,在场首 SAV 时清零。下面是一个简化片段,重点看复位条件,而不是计数位宽。

localparam H_TOTAL = 1728; localparam V_TOTAL = 625; reg [10:0] hcnt; reg [9:0] vcnt; reg field; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin hcnt <= 11'd0; vcnt <= 10'd0; field <= 1'b0; end else if (din_valid) begin // 行计数:EAV 到达时归零,保证每行从同步码重新对齐 if (eav) hcnt <= 11'd0; else if (hcnt == H_TOTAL - 1) hcnt <= 11'd0; else hcnt <= hcnt + 1'b1; // 场计数:在场首 SAV 锁存 F 位,垂直消隐行也计数 if (sav) begin field <= f_bit; if (v_bit && f_bit != field) vcnt <= 10'd0; // 场切换,行计数回零 end else if (eav && v_bit) begin if (vcnt == V_TOTAL - 1) vcnt <= 10'd0; else vcnt <= vcnt + 1'b1; end end end

逻辑说明:hcnt以 EAV 为绝对锚点,防止自由计数跑偏;vcnt只在 V=1 的消隐行递增,有效行期间保持不变。参数H_TOTAL和V_TOTAL对应 1728 和 625,换 720×480 或 720×576 逐行时只改这两个值还不够,还要改场结构和 F 位处理。field锁存当前场的 F 位,用来判断奇偶场,后级去隔行会直接用它。

3. 拆包看模块:8bit 数据通路、27MHz 时钟和参数化行场计数

3.1 顶层接口怎么定:pclk、din、de、yuv 输出

拿到一份 Verilog 代码包,先看顶层端口,不要一上来翻子模块。BT.656 接收侧的顶层接口通常很朴素:pclk是 27MHz 像素时钟,rst_n是异步复位,din[7:0]是 8bit 数据,输出侧是de、hsync、vsync、y[7:0]、cb[7:0]、cr[7:0],再加一个frame_start或field标志。如果代码包还带发送侧,顶层会多出dout[7:0]和dout_valid,但接收通路的信号名基本绕不开这几个。

接口定得对不对,看两个地方:一是de的宽度,720×576 下每行有效 720 个像素,de必须正好覆盖 720 个像素周期,不能把 SAV/EAV 的 4 个字节算进去;二是y/cb/cr的更新节奏,YUV 4:2:2 是Cb Y Cr Y交替,两个像素共用一组 Cb/Cr,所以色度输出要么每两拍更新一次,要么用de和像素计数配合做保持。很多代码包在这里用pix_cnt[0]做奇偶选择,第一拍取 Cb,第二拍取 Y,第三拍取 Cr,第四拍再取 Y。

我一般会先画一张顶层端口表,把方向、位宽、时钟域写清楚,再对照资源包里的端口名。名字不一样没关系,功能对得上就行。最怕的是de的极性反了,仿真时看着有数据,上板后图像左移或右移半屏。

端口方向位宽说明
pclkinput127MHz 像素时钟
rst_ninput1异步复位,低有效
dininput8BT.656 字节流
din_validinput1字节有效,连续采样可接 1
deoutput1行有效,高有效
hsyncoutput1恢复出的行同步
vsyncoutput1恢复出的场同步
youtput8亮度
cb/croutput8色度
fieldoutput1奇偶场标志

这张表不是让你照抄,而是用来核对资源包里的顶层。如果某个输出缺失,比如没有field,后级去隔行就得自己从 F 位恢复,容易出错。

3.2 模块划分:同步检测、行场恢复、YUV 拆分、跨时钟 FIFO

一份能落地的 BT.656 接收代码,模块不会只有一坨 always。常见拆法是四块:bt656_sync_detect负责找 EAV/SAV;bt656_timing_recover负责行场计数和de/hsync/vsync恢复;bt656_yuv_split负责按 4:2:2 拆 Y/Cb/Cr;async_fifo负责把像素时钟域的数据送到系统时钟域。如果资源包只有前三块,没有 FIFO,那它默认后级和像素时钟同域,或者后级自己处理跨时钟。

同步检测和行场恢复之间是脉冲级联:sav/eav单周期脉冲触发计数器复位或锁存。YUV 拆分依赖de和像素奇偶计数:de为高时,第一个字节是 Cb,第二个是 Y,第三个是 Cr,第四个是 Y,如此循环。注意 BT.656 的字节顺序是 Cb-Y-Cr-Y,不是 Y-Cb-Y-Cr。如果拆反了,图像会变成灰度正常、颜色发绿或发紫,这种翻车在现场很常见。

跨时钟 FIFO 的深度不用太大,一行 720 像素、每像素 2 字节,按 16bit 写、32bit 读,深度 1024 足够缓一行。关键是写使能要跟de走,不要把消隐数据写进去;读侧用frame_start或line_start对齐,否则第一行会错位。异步 FIFO 的指针用格雷码,空满判断按常规做法来,别自己发明“计数器跨域比较”,那是黑匣子。

3.3 参数表:改 720×576 到 625 行时只动哪几个 localparam

资源包里的参数化程度决定了它能不能复用。下面这几个参数是必须露出来的,改制式时只动它们,不要动状态机。

参数720×576 取值作用
PIX_CLK27_000_000像素时钟频率
H_TOTAL1728一行总字节时钟数
H_ACTIVE720一行有效像素
H_BLANK288水平消隐字节数
V_TOTAL625一帧总行数
V_ACTIVE576一帧有效行
FIELD_ACTIVE288单场有效行
XY_PROTECT_MASK8'hF0同步码高 4 位比较掩码

如果代码里把H_ACTIVE写成 1440,说明它把字节数当像素数了;如果V_ACTIVE写成 288,说明它只处理单场,没做帧合并。这两种写法在只看一行或一场时能出图,但接完整帧就会露馅。改参数时先改H_TOTAL和V_TOTAL,再改H_ACTIVE,最后检查de的起止位置和 SAV 的间隔是否还是 1728。

4. 仿真到上板:用 27MHz 时序跑通第一帧

4.1 写一个只发 EAV/SAV 的 TB,先验同步再验像素

仿真不要一上来灌真实图像。先写一个只发同步码和固定像素的 testbench,把FF 00 00 XY的节奏跑对,再换彩条。下面这段 TB 片段每行发 1728 个字节,前 4 个是 EAV,接着 280 个 0x80 消隐,再发 SAV,最后 1440 个像素字节。XY用8'h9D表示 EAV、8'h80表示 SAV,这里只用于仿真激励,DUT 内部仍然按高 4 位比较。

// 简化 TB:一行 1728 字节,先同步后像素 task send_line(input [7:0] xy_eav, input [7:0] xy_sav); integer i; begin send_byte(8'hFF); send_byte(8'h00); send_byte(8'h00); send_byte(xy_eav); for (i = 0; i < 280; i = i + 1) send_byte(8'h80); send_byte(8'hFF); send_byte(8'h00); send_byte(8'h00); send_byte(xy_sav); for (i = 0; i < 1440; i = i + 1) send_byte((i % 4 == 1 || i % 4 == 3) ? 8'h10 : 8'h80); end endtask // send_byte 在 pclk 上升沿驱动 din,并拉高 din_valid 一拍

逻辑说明:send_byte每个pclk上升沿发一个字节,din_valid保持一拍。xy_eav和xy_sav的 bit6/bit5/bit4 要按当前场和消隐状态填,不能全填 0。先跑一行,看sav和eav脉冲间隔是不是 1728 个时钟;再跑一场,看de是不是 720 个像素宽;最后跑两场,看field有没有翻转。参数上把H_TOTAL和V_TOTAL设成和 TB 一致,避免仿真通过、上板不对。

4.2 约束文件只盯三个数:27MHz、输入延时、异步复位

上板前先看约束。27MHz 的周期是 37.037ns,SDC 里写create_clock -period 37.037。输入延时按前端芯片的 Tco 和板级走线估,一般先给set_input_delay -clock pclk 5.000,再根据 ILA 抓到的数据眼图微调。异步复位要加set_false_path,不要让它参与时序分析。

# 27MHz 像素时钟约束,周期单位 ns create_clock -name pclk -period 37.037 [get_ports pclk] # 输入数据相对 pclk 的延时,先给 5ns,上板后按眼图调 set_input_delay -clock pclk -max 5.000 [get_ports {din[*]}] set_input_delay -clock pclk -min 1.000 [get_ports {din[*]}] # 异步复位不做时序检查 set_false_path -from [get_ports rst_n]

这段约束没有写输出延时,因为接收侧主要看输入。如果代码包带发送侧,dout要加set_output_delay,否则外部接收端采样点会偏。参数上-max和-min先给保守值,上板后用 ILA 看din相对pclk的建立保持窗口,再决定收紧还是放松。

4.3 上板抓 ILA:先看 SAV/EAV 间距,再看 de 宽度

ILA 抓信号不要贪多,第一轮只抓pclk、din、sav、eav、de、field、hcnt、vcnt。触发条件设成sav == 1,深度开到能覆盖两行。先看相邻两个 SAV 之间是不是 1728 个pclk,再看de高电平期间是不是 720 个像素周期。如果de只有 360 个周期,多半是把 YUV 4:2:2 的字节数当成了像素数;如果de有 1440 个周期,那是把字节当像素了。

第二轮抓y/cb/cr,用彩条源看颜色顺序。正常彩条下 Y 应该有明显台阶,Cb/Cr 在色块边界跳变。如果整屏偏绿,先查 Cb/Cr 有没有对调;如果颜色整体偏移,查de的第一个像素是不是从 SAV 后第 5 个字节开始。SAV 本身占 4 个字节,有效数据从 SAV 的 XY 之后下一个字节开始,这个偏移错一拍,图像就会整体左移或右移。

5. 避坑与排查:EAV 误判、场序反、跨时钟花屏的 5 个现场

5.1 现象:de 一直为低,帧不同步

现象是仿真里sav/eav有脉冲,但de始终不拉高,或者偶尔拉高一拍就断。原因多数是同步码比较条件太死:代码里写XY == 8'h80或XY == 8'h9D,而实际输入的保护位不是这个值。BT.656 的 XY 低 4 位是保护位,F/V/H 一变,低 4 位就变,硬编码必然漏检。解决方法是只比较 bit7~bit4,或者用掩码(din & 8'hF0) == 8'h80这类形式,把低 4 位放过。改完后再看de是否恢复,如果还不恢复,用 ILA 抓原始din,确认前端是不是真的在发FF 00 00,有些芯片上电后要配置寄存器才出 BT.656。

5.2 现象:图像上下抖,场序反了

现象是静止场景看着正常,一有运动就上下抖,或者图像被切成两半错位。原因是把 720×576 当逐行处理,vcnt按 625 行自由跑,没有用 F 位区分奇偶场。BT.656 的两场是隔行插入的,第一场 288 行、第二场 288 行,场序反了后级去隔行就会把奇偶行拼错。解决办法是在场首 SAV 锁存f_bit,用field标志告诉后级当前是哪一场;如果后级不支持场标志,至少保证vsync的极性跟场首对齐,不要把两场合成一个 576 行的逐行帧直接送显示器。

5.3 现象:颜色错位,Y 和 C 对调

现象是图像亮度正常但颜色发绿、发紫,或者黑白区域出现彩色边缘。原因是 YUV 4:2:2 的字节顺序拆错了。BT.656 的顺序是Cb Y Cr Y,不是Y Cb Y Cr。代码里如果按pix_cnt[0]选择,第一拍应该给 Cb,第二拍给 Y,第三拍给 Cr,第四拍给 Y。解决方法是把de后的第一个字节固定当 Cb,第二个当 Y,后面循环;同时检查de的起始位置是不是从 SAV 后第 5 个字节开始,偏移错一个字节就会把 Cb 当 Y。

5.4 现象:慢速移动时偶尔花屏

现象是静态图像正常,慢速移动或切换场景时偶尔出现一条花屏,重新上电又好了。原因是跨时钟域没处理好:像素时钟域写 FIFO,系统时钟域读 FIFO,读写指针直接比较或者用二进制计数器跨域。解决方法是异步 FIFO 的指针用格雷码,空满判断按标准做法;如果资源包没有 FIFO,后级又不同频,必须补一个。另一个常见原因是de写使能没有跟din_valid对齐,消隐期间的无效字节被写进 FIFO,读出来就多了一行错位。

5.5 现象:换 576p 源后行数不对

现象是接 720×576 隔行源正常,换成 576p 逐行源后图像被压缩或重复。原因是代码里把V_TOTAL=625和两场结构写死了,逐行 576p 的帧结构是 576 行有效、行总数不同,F 位也不再按场翻转。解决方法是把场结构做成参数:INTERLACED为 1 时按 625 行两场处理,为 0 时按逐行总行数处理;field输出在逐行模式下固定为 0。不要指望一份隔行代码不改参数就吃逐行源,这是两套时序。

6. 进阶验证:用 8 场 CRC 和行号快照锁住 720×576 的稳定性

单帧仿真通过不代表稳定。BT.656 的坑往往在第二场、第八场才出现,尤其是场计数和 FIFO 指针回绕。我一般会在验证环境里加两个东西:一个按场统计的 CRC,一个行号快照。CRC 不比对真实图像,只比对连续 8 场的有效像素字节流是否一致;行号快照在每场 SAV 时记录vcnt和hcnt,看 8 场里行号有没有漂移。

// 场级 CRC 统计:de 有效期间逐字节累加,场首清零 reg [15:0] crc_acc; reg [2:0] field_cnt; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin crc_acc <= 16'd0; field_cnt <= 3'd0; end else if (de) begin crc_acc <= crc_acc ^ {8'd0, din}; // 简化 CRC,仅用于一致性比对 end else if (sav && v_bit) begin field_cnt <= field_cnt + 1'b1; if (field_cnt == 3'd7) crc_acc <= 16'd0; // 8 场一轮,重新累计 end end

这段代码的crc_acc只是逐字节异或,不是标准 CRC,但用来抓“同一场景连续 8 场是否一致”足够。field_cnt在垂直消隐的 SAV 递增,数到 7 清零,正好覆盖 8 场。参数上de必须只覆盖有效像素,不能把消隐字节算进去;如果de多包含了 SAV 的 4 个字节,CRC 会在每场同一位置变化,反而误报。

行号快照更直接:在每场第一个 SAV 时把vcnt、hcnt、field存进一个 8 深度的寄存器堆,仿真结束后打印。正常情况 8 场的行号应该按固定序列循环,如果某一场的vcnt突然跳变,说明场同步被干扰或者 EAV 误判。这个办法比看波形快,尤其适合跑长仿真。

验证项观察点正常表现
行周期相邻 SAV 间隔1728 个 pclk
行有效de 高电平宽度720 个像素周期
场有效每场 de 行数288 行
场序field 翻转每场翻转一次
8 场一致性crc_acc同一场景下 8 场一致或按预期变化

血泪经验是:不要等上板才查场序。仿真里把field和vcnt一起打出来,跑够 8 场,比在显示器前猜“为什么上下抖”省一整天。从那以后我每次拿到新的 BT.656 代码包,都强制先跑 8 场 CRC 和行号快照,再谈接摄像头。希望帮到你。

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

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

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

立即咨询