资讯详情 FPGA JPEG编码器Verilog源码解析与工程落地
📅 2026/10/13 15:05:24
简介一份基于 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 三个分量各 8bit8x8 分块与行缓冲像素流 行有效标志8x8 数据块多行缓存二维 DCT8x8 像素块8x8 频域系数中间 12~16bit量化DCT 系数 量化表量化后系数8~12bitZigzag 扫描按行序系数按 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) 的 k0 系数单独处理 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 1b0; 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乘积 16bit8 个乘积累加最大值约255*64*813bit 能放下但我这里输出给了 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 表示 category1 表示码值 case (tbl_id) 0: begin if (idx 0) get_dc_table 16h0001; // category 1 占 1 bit else get_dc_table 16h0000; // 码值从 0 开始 end default: get_dc_table 16h0000; 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_formfsdb # 检查输出 jpeg 文件是否生成 $ ls -l out.jpeg逻辑说明-timescale统一到 1ns/1ps避免不同文件里仿真精度不一致导致握手信号毛刺wave_formfsdb是导出波形的通用参数不同工具写法略有差异。跑完先看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 // qq 50时用200 - 2*q这是业界通用的独立 JPEG 组缩放公式。生成的结果取整后最小值钳到 1防止量化系数为 0 导致除零。参数说明base_quant是标准亮度表色度表需要单独传。实际源码里量化表的位宽多为 8bitq10时缩放后的系数可能超过 255这时要么钳位要么扩位宽两个方案视觉差异不大。回读量化表是排查参数生效与否的好方法上电完成配置后把所有量化表寄存器读回来和生成表逐项对比。有一次排查花了两小时最后发现是 CPU 写寄存器时地址偏移了一位表整体错位回读机制能立刻暴露这类问题。4.2 位宽选择DCT 中间位宽、行缓冲、码流 FIFO位宽是 FPGA JPEG 里最影响资源和隐藏正确性的参数。DCT 中间位宽不够高对比度图片会出现条纹位宽太宽DSP 和寄存器浪费严重。按我常用的经验输入像素 8bit一维 DCT 累加后给 12bit 是底线二维再累加给 14bit 比较稳妥量化后回到 8~12bit。位宽设计可以按最坏情况推算8bit 输入最大值 255DCT 系数和绝对值上限约 2040255 乘 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:by8, bx:bx8] 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 表、握手对齐这些坑基本都能拦在进板之前跳过中间验证直接上板的人最后基本会在逻辑分析仪上重新补这一课。希望帮到你。本文还有配套的精品资源点击获取