简介:一份基于 Verilog 语言在 FPGA 平台上实现 JPEG 编码器的完整源代码工程,面向数字 IC、FPGA 开发者和图像处理方向学生,覆盖颜色空间转换、8×8 块划分、离散余弦变换(DCT)、量化与哈夫曼熵编码等硬件实现要点,适用于实时图像压缩、低延迟视频传输等参考设计。压缩包共 52 个文件,约 166KB,其中 Verilog 源文件有 38 个,是整个工程的核心;另含 chm 帮助文档、txt 说明、ModelSim 工程与仿真记录。目录按功能子模块划分,从 DCT 蝶形运算、zigzag 扫描到游程编码均有对应实现,便于按需深入。已有 591 人学习/下载。通过源码可掌握 Verilog 模块化设计、并行流水线优化与 FPGA 仿真测试流程;压缩包还提供测试平台、仿真波形记录及开发者讨论链接,可对照学习如何搭建验证环境、模拟输入图像并检查编码结果。对于希望从算法走向硬件实现、从事嵌入式图像处理的开发者,这套代码提供了完整的实践路径和二次开发基础。
1. FPGA JPEG Verilog 源代码:拿到这份源码,先想清楚三件事
FPGA JPEG Verilog 源代码这个标题,乍看是「一份源码」,实际它是一个完整编码器方案落地的起点。把 FPGA 和 JPEG 编码器放到一起,核心诉求通常是:在实时视频通路里做硬压缩,省带宽、省存储,同时还能把码流喂给后端解码显示。真正要解决的不是看懂每一行代码,而是确认数据通路怎么走、量化表和 Huffman 表怎么配、能不能在目标板卡上闭环输出一张可解码的 JPEG 图片。这篇文章按自己摸排过多次这类工程的经验来写,适合拿着源码却不知道先看哪里的工程师,也适合想评估这个方向值不值得投入的团队。先想清楚这三件事,再往下翻源码不迟。
2. JPEG 编码器里到底有什么:模块拆分与数据流
2.1 从 RGB 到 JPEG 码流:一条数据通路看清源码骨架
拿到任何一份 FPGA JPEG Verilog 源码,我做的第一件事不是打开顶层文件从头看,而是按 JPEG 的编码链路反向找模块。标准编码链是这样的:RGB 转 YCbCr 色彩转换、按 8x8 分块、二维 DCT、量化、Zigzag 扫描、DC 差分编码与 AC 游程编码、Huffman 熵编码,最后是码流组装与 marker 写入。源码里只要这七段都有对应模块,这份代码就是完整的;缺了任何一段,都需要自己补。
实际工程里,模块命名千奇百怪,但数据通路很好认:找信号名里带pixel、dct、quant、ziczac、huff、bitstream的那条线,从输入到输出串起来,就是源码的骨架。以下是我一般会对照的模块清单:
| 模块职责 | 输入信号特征 | 输出信号特征 | 常见位宽 |
|---|---|---|---|
| 色彩转换 | RGB 24bit 像素流 | YCbCr 三个分量 | 各 8bit |
| 8x8 分块与行缓冲 | 像素流 + 行有效标志 | 8x8 数据块 | 多行缓存 |
| 二维 DCT | 8x8 像素块 | 8x8 频域系数 | 中间 12~16bit |
| 量化 | DCT 系数 + 量化表 | 量化后系数 | 8~12bit |
| Zigzag 扫描 | 按行序系数 | 按 zigzag 序系数 | 与量化输出同宽 |
| 熵编码 | 量化系数 | 编码码字 | 可变长度 |
| 码流组装 | 码字 + 长度 | JPEG 字节流 | 8bit |
最后一项容易被忽略。很多源码实现了 DCT、量化、Huffman,但把码流组装写得很简陋,甚至直接输出原始码字让外部去拼。判断源码能不能用,先看 bitstream 组装处有没有写 SOI、EOI、量化表段、Huffman 表段这些 marker。没有 marker 的 JPEG 编码器,输出只能叫「熵编码数据」,不是合法 JPEG 文件。
2.2 DCT 怎么在 FPGA 里做:行-列分解与流水线结构
FPGA 上做二维 DCT,没有人会真按二维 8x8 的矩阵展开算,那样乘加器数量爆炸。最常见做法是行-列分解:先对每行做一维 DCT,结果存入转置缓冲,再对每列做一维 DCT,两次一维变换拼出二维结果。源码里找dct_row、dct_col、transpose这类信号,基本就能确认架构。
一维 8 点 DCT 在硬件里又分两条路:直接乘加阵列和蝶形快速算法。乘加阵列的思路是 8 个输入分别乘以余弦系数再累加,代码直观但乘法器多。蝶形 FCT 结构复用中间结果,乘法器省一半,但代码绕,信号依赖关系复杂,新人很容易改错。我一般建议先按乘加阵列理解数据流,跑通后再评估要不要换蝶形。
下面这段伪代码风格的一维 DCT 模块,展示的是最接近数学定义的实现方式,方便对照源码理解系数是怎么用的:
module dct_1d_8( input wire clk, input wire rst_n, input wire [7:0] xin[0:7], // 8 个输入像素,注意只有 8bit input wire xin_valid, output reg [11:0] y[0:7], // 8 个输出系数,预留进位 output reg y_valid ); // 余弦系数按 Q5 定点格式预存,避免浮点 // C(k)=1/sqrt(2) 的 k=0 系数单独处理 localparam signed [7:0] COS[0:7][0:7] = '{ '{64, 64, 64, 64, 64, 64, 64, 64}, '{89, 75, 50, 18, -18, -50, -75, -89}, '{83, 36, -36, -83, -83, -36, 36, 83}, '{75, -18, -89, -50, 50, 89, 18, -75}, '{64, -64, -64, 64, 64, -64, -64, 64}, '{50, -89, 18, 75, -75, -18, 89, -50}, '{36, -83, 83, -36, -36, 83, -83, 36}, '{18, -50, 75, -89, 89, -75, 50, -18} }; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin y_valid <= 1'b0; end else begin for (int k = 0; k < 8; k++) begin y[k] <= 0; for (int n = 0; n < 8; n++) begin y[k] <= y[k] + xin[n] * COS[k][n]; end end y_valid <= xin_valid; end end endmodule逻辑说明:COS数组存的是余弦变换系数的定点近似,每个系数量化到 Q5 格式。输入 8bit,系数 8bit,乘积 16bit,8 个乘积累加,最大值约255*64*8,13bit 能放下,但我这里输出给了 12bit,实际源码里通常会给 14~16bit 保险,因为列变换那级还要再累加一次。参数说明:xin_valid是输入侧握手,y_valid输出延迟一拍对齐,这套 valid 打拍逻辑在整条链上都要遵守,否则后面数据错位很难查。这里的xin[0:7]数组写法在部分仿真工具里需要开多维数组支持,编译报错的话换成 8 个独立端口就行。
2.3 量化表与 Huffman 表:源码里最容易改错的一组常量
量化表和 Huffman 表是 JPEG 编码器的「配置文件」。量化表决定压缩比和画质,Huffman 表决定码长分布。两份表在源码里通常以parameter、localparam或 ROM 初始化文件形式存在。找量化表有个技巧:标准 JPEG 的亮度量化表左上角是 16,色度表左上角是 17,表格按 Zigzag 序存储时这俩数字非常显眼。Huffman 表则长得很像两级数组:码长集合加码值集合。
改表最容易踩的坑是顺序。数学上量化表是按行序定义 8x8 的,但很多源码为了后续 Zigzag 方便,直接存 Zigzag 序。如果只改了数值没改顺序,编出来的图会出斜纹状的噪声,而且解码端不报错,极其隐蔽。我拿到源码会先做一个测试:把量化表全部改成 1,编一张纯色图,输出应当接近无损;再把表全改成 255,输出应当非常模糊但颜色不花。两道测试能立刻暴露表序对不对。
Huffman 表的初始化,源码里常见两种做法:直接写case语句逐个给码字,或用$readmemh从外部文件加载。前者好查错但代码长,后者灵活但路径问题多。下面这段展示用函数包初始化一张简化 DC 表的方式:
function automatic [15:0] get_dc_table( input integer tbl_id, input integer idx ); // 标准亮度 DC 表:码长集合为 {0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0} // idx 0 表示 category,1 表示码值 case (tbl_id) 0: begin if (idx == 0) get_dc_table = 16'h0001; // category 1 占 1 bit else get_dc_table = 16'h0000; // 码值从 0 开始 end default: get_dc_table = 16'h0000; endcase endfunction逻辑说明:这个函数是「查表接口」的示意,真正源码里会写成 16 项 category 数组加 16 项码值数组。DC 表按 category 区分码长,编码时要先算差分值的 bit 位数来确定 category,再查码值,最后拼上差分值本身。参数说明:tbl_id用于区分亮度表和色度表,idx的 0/1 两个维度分别取码长和码值。这类函数在综合时会被展开成常量,不会占用额外硬件。改 Huffman 表前先确认编码端和解码端用的是同一套表,很多「自己编的图自己都解不开」的翻车,根因就是编解码表不一致。
3. 把 Verilog 源码跑起来:仿真环境与最小操作
3.1 先别上板:建立一个可对比的仿真基准
拿到源码的第一反应往往是赶紧综合上板。我的习惯是反过来,先在仿真环境里把编码器跑起来,而且要用「可对比」的输入输出。什么叫可对比?输入用程序生成的规则图像而不是照片,输出直接落成 JPEG 文件或中间码流,然后拿参考实现解码对比。
规则图像首选灰度渐变图和正弦条纹图。渐变图能暴露 DCT 边界处理问题,正弦条纹图能暴露频域系数量化问题。最省事的输入是 64x64 灰度图,正好 8 个 8x8 块,调试时可以逐个 block 查。写测试平台时,把像素数据打平成数组,按行序送进编码器,同时打印每个 block 的边界:
module tb_jpeg_encoder; reg clk; reg rst_n; reg [7:0] pixel_data; reg pixel_valid; wire out_byte; wire out_valid; // 64x64 灰度渐变图:每行亮度递增 4 // 预先把整张图放进内存,仿真时按行取出 reg [7:0] frame [0:4095]; integer i; initial begin for (i = 0; i < 4096; i = i + 1) begin frame[i] = (i / 64) * 4; // 纵向渐变 end end // block 边界打印:每 64 像素是一个 8x8 块 always @(posedge clk) begin if (pixel_valid && (i % 64) == 0) $display("block start at pixel index %0d", i); end // 主循环驱动代码省略,按标准 valid/ready 时序发送 endmodule逻辑说明:frame数组用initial块初始化,渐变值(i / 64) * 4让每行像素亮度递增 4,这样每个 8x8 block 内部有梯度,DCT 能产生非零高频系数,适合检查量化路径。$display打印 block 边界是给调试用的标记,综合时会被忽略。参数说明:4096是 64x64 的总像素数,i / 64得到当前行号。这里用i % 64 == 0判断 block 起始,实际源码如果做了行缓冲延迟,打印点要对齐编码器内部的有效信号,否则对不上。
这套基准跑通后,编码器输出应该是一个合法的 JPEG 码流,用任何标准解码器能解出 64x64 渐变图。解出来的图应当没有明显条纹和偏色。达不到这个标准,先别往后走。
3.2 最小编译与波形,一条命令看清码流
仿真跑不起来,九成是编译选项和文件顺序问题。JPEG 编码器源码文件多,依赖关系密,常见做法是先写一个filelist.f汇总所有.v文件,再写一个仿真脚本统一编译运行。文件顺序上,包定义、函数定义文件放最前,顶层模块最后编译。
下面是我常用的最小命令流(仿真器和综合工具按你环境里的对应命令替换):
# filelist.f 内容示例 # 注意:包和头文件必须先于使用它的模块 # ./rtl/jpeg_pkg.v # ./rtl/color_convert.v # ./rtl/dct_2d.v # ./rtl/quantize.v # ./rtl/huffman_enc.v # ./rtl/bitstream_pack.v # ./rtl/jpeg_encoder_top.v # ./tb/tb_jpeg_encoder.v # 编译:打开多维数组支持,设置 timescale $> sim_tool -f filelist.f -timescale "1ns/1ps" -o sim_v0 # 运行并导出波形 $> sim_v0 +vcd +wave_form=fsdb # 检查输出 jpeg 文件是否生成 $> ls -l out.jpeg逻辑说明:-timescale统一到 1ns/1ps,避免不同文件里仿真精度不一致导致握手信号毛刺;+wave_form=fsdb是导出波形的通用参数,不同工具写法略有差异。跑完先看out.jpeg在不在,文件大小是不是在预期范围。64x64 渐变图质量因子 50 左右,码流应该在几百字节级别,如果输出几千字节,量化表大概率没生效;如果只有几十字节,Huffman 编码可能丢了数据。参数说明:sim_v0是输出可执行文件的名字,每次改代码建议换_v1、_v2后缀,方便回退。
波形查看这一步很多人跳过,我建议至少看两层信号:顶层的数据有效握手,和dct模块输出前的中间系数。valid信号变形、系数全为 0、Huffman 输出为 X 态,这三种异常通过波形几秒钟就能定位,盲猜代码则可能耗掉半天。
3.3 用一张真实图片驱动编码器
规则图跑通后,要换真实图片验证。真实图片的动态范围更大、高频细节多,能压出编码器在边界情况下的问题。但仿真器没法直接读 BMP/JPG,需要先转成 RGB 序列。常见做法是用脚本把图片转成 Verilog 可用的$readmemh文件。
# 把 BMP 转成 RTL 可读的 hex 序列 # 输出格式:每行一个像素,RGB 三个字节按顺序展开 from PIL import Image import sys img = Image.open("test.bmp").convert("RGB") w, h = img.size with open("frame_hex.txt", "w") as f: for y in range(h): for x in range(w): r, g, b = img.getpixel((x, y)) # 按内部接口位宽输出,这里是 8bit x3 f.write(f"{r:02x}{g:02x}{b:02x}\n")逻辑说明:这段脚本把所有像素按行序写成一个 hex 文件,Verilog 测试平台用$readmemh加载后就可以按像素索引取数。参数说明:convert("RGB")强制统一通道顺序,避免源图带 Alpha 通道时错位;输出每行 6 个 hex 字符,正好 24bit 一个像素。现实里我还会把图像尺寸打出来存进define,在仿真时用来校验总像素数是否读齐全。
真实图片仿真的关键检查点有两个:一是码流文件能被解码器正常打开,二是解码后的 PSNR 比参考软件编码低 1~2dB 以内。超过这个差距,先查量化表实现,再查 DCT 定点精度,最后查 Huffman 表。顺序基本是按对画质影响从大到小排的。
4. 改参数而不是改架构:量化、位宽与码率控制
4.1 质量因子怎么映射到量化表
JPEG 的「质量」本质上不是编码器参数,而是量化表的缩放因子。标准 JPEG 参考表给出 50 质量档的量化值,质量因子大于 50 时按(100 - q) / 50缩放,小于 50 时按50 / q缩放。FPGA 源码里一般不会内置整套缩放逻辑,而是由上位机算好量化表,通过寄存器或文件下发。
我见过不少直接改编码器源码里量化表常量的做法,不推荐。更好的方式是保留一个量化表 RAM 接口,让 CPU 在初始化时写入所需表格。下面展示量化系数缩放的参考片段,这个逻辑常用在表格生成脚本侧,不在 FPGA 内部:
# 标准亮度量化表(8x8 行序) base_quant = [ 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99 ] def scale_quant(q): if q <= 0: q = 1 if q >= 100: q = 100 if q < 50: s = 5000 // q else: s = 200 - 2 * q return [max(1, (v * s + 50) // 100) for v in base_quant]逻辑说明:q < 50时用5000 // q,q >= 50时用200 - 2*q,这是业界通用的独立 JPEG 组缩放公式。生成的结果取整后最小值钳到 1,防止量化系数为 0 导致除零。参数说明:base_quant是标准亮度表,色度表需要单独传。实际源码里量化表的位宽多为 8bit,q=10时缩放后的系数可能超过 255,这时要么钳位,要么扩位宽,两个方案视觉差异不大。
回读量化表是排查参数生效与否的好方法:上电完成配置后,把所有量化表寄存器读回来,和生成表逐项对比。有一次排查花了两小时,最后发现是 CPU 写寄存器时地址偏移了一位,表整体错位,回读机制能立刻暴露这类问题。
4.2 位宽选择:DCT 中间位宽、行缓冲、码流 FIFO
位宽是 FPGA JPEG 里最影响资源和隐藏正确性的参数。DCT 中间位宽不够,高对比度图片会出现条纹;位宽太宽,DSP 和寄存器浪费严重。按我常用的经验,输入像素 8bit,一维 DCT 累加后给 12bit 是底线,二维再累加给 14bit 比较稳妥,量化后回到 8~12bit。
位宽设计可以按最坏情况推算:8bit 输入最大值 255,DCT 系数和绝对值上限约 2040(255 乘 8 个 1/√2 近似值),加符号位需要 12bit。二维后再乘一轮,上限到 16bit 级。源码如果只给到 12bit,大概率做了截位,这时要关注截位策略是舍入还是直接截断。直接截断会产生直流偏置,灰度渐变图上会看到一层淡淡的横纹。
行缓冲和码流 FIFO 的位宽相对直觉。行缓冲存的是像素或 DCT 中间结果,按数据位宽加 1bit 有效标志位;码流 FIFO 存的是打包后的字节,宽度 8bit 或 16bit,深度取决于码率波动范围。一张 1080p 的帧内编码,码流 FIFO 至少要到 16KB,否则高细节区域容易溢写,溢出表现是码流丢字节,解码端对应区域花屏但文件头尾完好。
4.3 码率控制管不管得住
JPEG 是固定量化、可变码长的编码,码率波动是天然的。FPGA 实现里「码率控制」通常指两种:通过量化表改变整体码率,以及通过输出 FIFO 加反压防止溢出。源码里如果带rate_control或quality_scale信号,多半是后者。
实际调参时,我一般给三组量化表做三档预设,用最小硬件代价覆盖场景:
| 场景 | 质量因子 | 量化表来源 | 典型压缩比 | 硬件改动量 |
|---|---|---|---|---|
| 预览缩略图 | 40~50 | 标准表缩放 | 10:1~15:1 | 无,写入表即可 |
| 普通存储 | 65~75 | 标准表缩放 | 6:1~10:1 | 无 |
| 医疗/检测原图 | 90+ | 全 1 表或接近无损 | 2:1~3:1 | 检查 DCT 位宽是否够 |
从这张表能看出来,换档位本质是换表,不碰架构。真正需要改硬件的场景只有无损档,那时要确认 DCT 和量化整条路径的精度是否足够,否则无损档会编出「视觉无损但数值有损」的结果。数值有损在图像保存场景能用,在特征提取或测量场景就是事故。
5. 移植到板卡前,这些坑足以让编码器翻车:避坑记录
5.1 渐变图出现规则横纹,查 DCT 中间位宽
现象:用灰度渐变图仿真,解码出来的图在亮度跳变处出现等间距横纹,像梯田一样分层。原因:DCT 中间累加器位宽不足或截位过狠,导致高频分量的舍入误差累积成可见条纹。解决:把一维 DCT 输出从 12bit 扩到 14bit,并把截位策略从直接截断改成四舍五入。验证方法是编一张 256 级灰度渐变图,解码后逐行统计亮度值,应当每个灰度级都出现,且相邻级差为 1。只要发现某个灰度级整体缺失,就是精度问题。这个坑在实物上最容易翻车,因为屏幕显示动态范围大,横纹非常显眼。
5.2 码流能解码但画面花,优先查 Huffman 表与 Zigzag 顺序
现象:码流文件头尾完整,标准解码器能打开,但画面是大面积彩色雪花。原因:这类现象九成是 Zigzag 扫描序和 Huffman 编码期望的顺序不一致,或者 AC 系数中游程长度和幅值分组的拼接顺序反了。解决:找源码里的 zigzag 索引表,确认它是按 JPEG 标准序0,1,5,6,14,15,27,28,...排列的;再打印每个 block 的 DCT 系数序列,对照标准序人工验证一遍。曾经遇到过一个源码,Zigzag 表本身对,但量化后系数写入顺序沿用了行序,等于绕过了扫描模块,表现就是每隔 64 个系数花一块。这类问题用固定测试图挨个 block 比最快。
5.3 valid/data 错一位,握手逻辑没对齐
现象:简单图编码正常,复杂图偶尔丢行,解码出来每隔几行有一条错位横带。原因:valid信号和data处理路径差了拍。典型场景是 DCT 内部流水线打了两拍,但后续模块的输入整理逻辑只打了一拍。解决:在仿真波形里同时拉出pixel_valid、pixel_data、编码器输出out_valid、out_data,核对每个有效沿上数据是否对应正确的像素索引。排查时有血泪经验:别盯着always块里的赋值语句数拍,直接在 tb 里给每个输入像素编号打印,输出端就能精确对齐。这条坑花过我一整个下午,最后发现是流水线里valid用组合逻辑传递、数据用寄存器传递造成的错拍。
5.4 量化表 ROM 全零,查初始化文件路径
现象:上板后无论怎么调质量因子,输出图片都极度模糊,像蒙了层纱。回读量化表寄存器全部是 0。原因:量化表用$readmemh从文件加载,综合后的实现里文件路径是相对路径,板卡运行时找不到文件,或者 ROM 初始化过程被综合工具优化掉了。解决:改用initial块里的case语句直接赋值量化表,规避文件路径问题;如果必须用文件,把初始化文件路径写成仿真和综合都能找到的绝对路径,并确认综合报告里 ROM 有 initial 内容。验证方法是在仿真里强制把$readmemh文件改成错误路径,看是否报出 warning,如果仿真不报错、综合也不报错,说明加载逻辑可能被优化了,要用(* ramstyle = "block" *)这类属性强制保留。
5.5 图像宽度不是 8 的倍数,行错位不报错
现象:用 640x480 图像测试正常,换成 1920x1080 就出现整块图像上下错位,但解码不报错。原因:图像宽度必须是 8 的倍数才能在行缓冲里对齐 block。1080 不是 8 的倍数,实际块数是 135 个整块加 0.75 个残块,源码没做边界 padding,导致下一行取数错位。解决:编码器前端加一个 padding 逻辑,行末补像素到 8 对齐,同时把补齐的尺寸写进 JPEG 的 SOF marker。这个坑属于「原理没问题、集成就出事」的典型,标准码流不报错是因为解码端按 SOF 里的尺寸解码,尺寸错了画面就跟着错。验证时先用 1920x1088 的尺寸仿真,确认码流正常,再打开 padding 对比 1920x1080 是否一致。
6. 上板前最后一步:用 C model 生成黄金数据逐块比对
6.1 block 级比对的流程
上板前的验证,我不推荐只做「整图解码看效果」。整图比对能发现错误,但定位慢。更可靠的做法是逐 block 比对:用参考 C 模型生成每个 8x8 块的 DCT 系数、量化系数和 Huffman 码字,RTL 仿真时在每个 block 结束处比对一次,任何差异立刻知道是哪个模块出的问题。这个流程不复杂,但耗时主要在搭黄金数据生成脚本,收益是上板后基本一次点亮。
6.2 黄金数据生成与 RTL 端读取
黄金数据用脚本在 PC 端生成,格式简单到 RTL 测试平台能直接读。我一般生成三个文件:DCT 系数表、量化系数表、期望输出码流,每行一个 block 的 64 个系数。
# 生成 DCT 黄金数据,按 block 组织 # 这里用 numpy 加速;输出文本给 tb 读取 import numpy as np def dct2(block): # 标准二维 DCT,结果按行序展平 return np.rint( np.fft.fft2(block) # 示意,实际请用标准 DCT 矩阵 ).astype(np.int16) img = np.array(frame_data).reshape(64, 64) with open("golden_dct.txt", "w") as f: for by in range(0, 64, 8): for bx in range(0, 64, 8): block = img[by:by+8, bx:bx+8] coefs = dct2(block) f.write(" ".join(str(v) for v in coefs.flatten()) + "\n")// RTL 测试平台读取黄金数据,逐个 block 比对 reg [11:0] golden_dct [0:63]; integer file_h; integer blk_idx; initial begin file_h = $fopen("golden_dct.txt", "r"); for (blk_idx = 0; blk_idx < 64; blk_idx = blk_idx + 1) begin for (int c = 0; c < 64; c = c + 1) begin void'($fscanf(file_h, "%d", golden_dct[c])); end // 触发编码器完成当前 block,然后比对 wait (dct_done); for (int c = 0; c < 64; c = c + 1) begin if (golden_dct[c] !== dct_out[c]) begin $display("MISMATCH block %0d coeff %0d: golden %0d rtl %0d", blk_idx, c, golden_dct[c], dct_out[c]); end end end $fclose(file_h); end逻辑说明:脚本把每个 8x8 block 的 DCT 结果写一行,RTL tb 用$fscanf逐行读入,等dct_done信号拉高后逐系数比对。dct_done需要从被验证模块引出,如果源码没这个信号,就用输出 valid 打拍生成。参数说明:比对允许误差通常设为 ±1 LSB,因为 C 模型浮点和 RTL 定点存在舍入差异;如果差异超过 ±1,优先查定点格式是否一致,再查量化舍入方向。
6.3 最后一拍:把自己的检查固化成一个脚本
经历过几次翻车后,我现在每个 JPEG 编码器项目都会维护一个compare_all.py脚本,输入源图,自动完成黄金数据生成、RTL 仿真、解码对比一条链。这个脚本平时跑回归,改动任何参数或位宽后先跑它,全绿才动板卡。做这个脚本的一点体会是:仿真阶段的认真程度,决定了上板后排错的痛苦程度。只要你坚持每次改代码都跑 block 级比对,DCT 精度、Huffman 表、握手对齐这些坑基本都能拦在进板之前;跳过中间验证直接上板的人,最后基本会在逻辑分析仪上重新补这一课。希望帮到你。
本文还有配套的精品资源,点击获取