
简介这份基于 Verilog 的 JPEG 编码器源代码面向 FPGA 开发者与图像处理硬件设计人员完整实现从 RGB 转 YCbCr、8x8 分块、DCT/量化到 zigzag 扫描与 Huffman 熵编码的 JPEG 压缩链路适合学习硬件加速与算法硬件化设计。压缩包共 52 个文件以 38 个 .v 源文件为核心覆盖 dct、huffman、rgb2ycrcb、zigzag 等模块另含测试平台、modelsim 工程脚本、chm 帮助文档及说明文件包体仅 166KB便于快速下载分析。资源已有 589 人学习作者为 xinyeyu。通过阅读源码可掌握 JPEG 编码的 RTL 实现思路、模块划分与并行流水设计结合 bench 测试文件可仿真验证编码结果对欲在 FPGA 上实现实时图像压缩的工程师有直接参考价值。 各位做FPGA图像处理的朋友应该都有过这种体会图像采集搞定了显示也搞定了中间那一大坨“处理”却常常卡壳。尤其是JPEG压缩看起来是个标准协议真要在FPGA里用Verilog从零写一套能用的编码器源代码并把它跑在板子上整个过程远比想象中曲折。这篇文章就围绕一套基于Verilog的JPEG硬件压缩实现方案聊聊整体设计、源码框架、关键模块拆解以及我在调试过程中踩过的几个大坑。这套方案解决的核心问题很明确让FPGA实时地把视频帧或单帧图像压缩成标准的JPEG码流不需要外挂DSP或ARM软解压缩后的数据可以直接存SD卡、走以太网或USB上传。适合正在做图像采集系统、视频传输设备或者单纯想深入学习JPEG硬件实现的工程师参考。1. 项目整体设计与源码架构思路1.1 为什么非要在FPGA里做JPEG很多初学者会问JPEG压缩用CPU软算不就行了为什么非要费劲在硬件里实现核心原因是带宽和实时性。假设一帧1080P的RGB888图像原始数据量大约是6MB如果按30fps算每秒要处理180MB数据。这个量级对DDR带宽和CPU资源都是巨大负担。而在FPGA内部数据从Sensor进来之后以流式Pipeline的方式直接走完整个编码流程每一级模块同时在工作整体延时只有几十微秒吞吐量可以做到每时钟周期输出一个像素这是软件方案很难匹敌的。另一个原因是系统集成的便利性。在视频采集板卡上FPGA本来就要做Sensor配置、时序同步、DDR缓存如果压缩功能也集成在FPGA内部整个链路就完全打通了不需要在ARM和FPGA之间来回搬运数据。这里我们讨论的这套源代码方案走的是经典Baseline JPEG流程输出标准JFIF格式字节流可以直接被PC上的图片查看器识别。1.2 编码流水线的模块划分整个JPEG编码器源码顶层架构并不复杂但每个子模块都有很多细节。一条完整的Baseline JPEG流水线包含以下环节颜色空间转换将RGB转成YCbCr同时做下采样通常4:2:08x8分块与数据重排把图像切成8x8的像素块按块送入编码核心DCT离散余弦变换把空间域的像素变换到频率域量化用标准量化表对DCT系数进行有损压缩ZigZag扫描与游程编码把量化后系数按频率从低到高排列再统计零游程Huffman编码对DC系数和AC系数分别编码输出变长码流码流打包把变长码拼接成字节流并插入JPEG头信息我见过很多初版设计喜欢把所有逻辑堆在几个大always块里试图“一步到位”。但那样做基本没法调试。合理的做法是一个模块只干一件事模块之间用valid/ready握手信号对接数据位宽保持一致。顶层就像流水线车间每个模块是一个工位各管一段。源码框架建议按这样的目录组织方便维护和复用src/ top_jpeg_encoder.v // 顶层负责子模块例化与外部FIFO接口 rgb2ycbcr.v // 颜色空间转换 block_reshape.v // 行缓存与8x8分块 dct_2d.v // 二维DCT行列拆分 quant_zigzag.v // 量化与ZigZag重排 huffman_enc.v // Huffman编码查表实现 bitstream_pack.v // 位流打包与头文件拼接 jpeg_rom_tables.v // Huffman码表与量化表ROM sim/ tb_top_jpeg.v // testbench img_to_tb.py // 把bmp转成仿真可读的hex文件这套结构下每个模块都可以独立仿真验证最后再联调。最开始我偷懒跳过block_reshape模块直接用DDR输出的行数据去推DCT结果发现8x8分块的地址计算到处是Bug后来老老实实加了一个专用的重排模块问题才彻底解决。1.3 为什么用Verilog而不用HLS或Vivado IP核市面上有Xilinx和Intel官方的JPEG IP核性能很好但基本都要授权费而且核的内部逻辑是个黑盒出了时序问题你没法排查。HLS工具确实能快速把C代码转成硬件但生成的代码面积和时序往往不可控对于视频这种实时性要求高的场景我还是更倾向于手写Verilog源代码。源码在手想怎么改就怎么改比如把4:2:0下采样改成4:2:2或者把标准Huffman表换成分辨率自适应的定制表这些都是IP核做不到的。2. 核心模块细节与Verilog实现要点2.1 DCT变换行列拆分与定点化DCT是整个编码器里计算量最大的部分。二维DCT如果直接展开每个8x8块要做4096次乘加资源消耗非常吓人。实际工程里几乎都采用行列拆分的方法先对8行数据分别做一维DCT转置后再对8列分别做一维DCT。这样每个8x8块的乘加次数从4096降到1024资源省了四分之三。每一维的8点DCT又可以利用蝶形运算进一步化简。核心原因是DCT变换矩阵本身关于中心对称很多项是重复的合并同类项后只需要21次乘法。别看这个数字不起眼对FPGA来说每次乘法都对应DSP Slice能省一次是一次。关于定点和浮点的问题。JPEG标准里的DCT公式是浮点运算但FPGA里做浮点又慢又费资源所以工程上统一转成定点。具体的做法是把DCT变换矩阵的系数乘以一个缩放因子比如2的13次方也就是8192量化成整数存进ROM。运算结果再右移13位。这样整个DCT模块里全是整数乘加用DSP48或ALM块就能轻松搞定。我实测下来这种定点化方案带来的误差反映到图像上PSNR损失基本可以忽略。DCT模块的另一个关键是流水线设计。8点一维DCT如果所有乘法串联在一个时钟周期内完成组合逻辑路径会非常长比如在100MHz下面很容易时序违例。我的做法是拆成三级流水第一级做输入重排和加减法第二级做乘法第三级做累加和输出。代价是数据有3个周期的延时但对流水线编码器来说这种延时可以完全隐藏。这里有个经验用流水线换时序比强行优化组合逻辑要省心得多这也是视频处理里最常见的设计思路。2.2 量化与ZigZag查表思维的胜利量化本身做的事情很简单就是除法。DCT系数除以量化步长人眼对高频信息不敏感所以高频的量化步长通常取得很大以此丢弃掉大量视觉上不重要的信息。传统做法是用除法器但FPGA里的除法器资源开销大、时序差。更聪明的做法是用查表法或者把除法转成乘法加移位。因为量化步长是固定的由量化表决定我们可以提前算出每个量化步长的倒数放大后存成常数。直接拿DCT系数去乘这个常数再右移相应位数就完成了除法。这样做的误差在1以内对图像质量毫无影响。ZigZag扫描则是一个纯粹的重排操作。量化后一个8x8块内的系数就已经按频率排好了低频频谱在左上角高频率谱在右下角。ZigZag模块做的事情就是按“之”字形顺序把二维矩阵展成一维数组。实现方式是把ZigZag的索引映射关系写成一个8x8的ROM用坐标查ROM得到输出顺序。这个模块的代码量很少但地址映射极易写错检查的方式是仿真时打印坐标对肉眼核对一遍和标准ZigZag顺序是否一致。2.3 Huffman编码查码表比算编码快得多Huffman编码是整个系统中最容易被初学者搞混的部分。很多人一上来就想着怎么动态构建Huffman树进行实时编码这是一个大坑。动态构建Huffman树需要统计图像块的符号出现频率再生成码表这个过程的计算量比编码本身还大而且生成码表后的分发还要额外逻辑。JPEG标准早就规定了一套通用的Huffman码表绝大多数图像用这套码表压缩的效率损失在1%以内谁都能接受。所以这里的源码实现思路非常直接将标准的DC亮度表、DC色度表、AC亮度表、AC色度表以及对应的码长和码值全部存成ROM。编码的时候查输入符号对应的码字和码长输出即可。DC系数和AC系数的编码方式不同这是另一个容易出错的地方。DC系数采用的是差分编码也就是当前8x8块的DC值与上一个块的DC值做差再对这个差值查表编码。AC系数则采用游程编码把非零系数前面连续的零的个数记下来再加上非零系数本身的位宽拼成一个符号查表输出。举个例子一串量化后的频率系数如果是0, 0, 5, -3, 0, 0, 0, 8...那么第一个AC符号就是(2, 3)因为前面有两个零5需要用3位二进制表示。Huffman编码查表索引就是这个(游程, 位宽)的组合。2.4 位流打包变长码怎么拼成字节流Huffman编码输出的码字是变长的有的2位、有的3位、有的16位。而JPEG输出的文件必须是一个字节一个字节的文件中间不能有空洞。位流打包模块要干的事情就是把一串变长的码字按位拼接在一起凑满8位就输出一个字节。我在代码里是这么实现的维护一个64位的移位寄存器也就是Shift Register。每次Huffman模块送来一个码字就把它拼接到当前寄存器的低位同时用一个计数器记录当前有效位的数目。当有效位达到8位以上就从最高位截取一个字节输出剩余部分保留继续参与下一次拼接。这个模块边界情况非常多数据刚好凑满8位、凑满16位、当前剩下的位不够拼下一个码字等等。每一个分支都要测试到位。调这个模块的时候我建议写一个参考的C模型随机生成一个码字序列比对C模型的输出字节流和Verilog仿真的输出字节流能快速定位下午查不出来的毛病。另外还要注意JPEG码流的字节对齐问题在EOI图像结束标志之前如果码流不是字节对齐的需要补1来填充对齐这个细节处理不好生成的图片在PC上可能打不开。3. 基于源码的仿真验证与板级实现流程3.1 Testbench怎么搭更高效拿到底层源代码之后第一步肯定不是直接上板而是先跑仿真。这里跑仿真也不是随便喂几个数据看看波形而要有完整的验证策略。我的做法是准备三张测试图一张纯色图用于检查Huffman码流是否正确一张高细节的纹理图用于检查压缩质量一张标准测试图用于和软件压缩结果做对比。Testbench的结构也不复杂读取一个由Python脚本生成的RGB像素hex文件按像素时钟送入DUT顶层同时模拟Sensor的hsync和vsync时序。DUT输出的是压缩码流Testbench负责把收到的字节写入新的文件。仿真跑完之后用Python把输出的字节流和输入的原始图像做PSNR分析。整个过程不需要打开ModelSim手动拉波形直接跑自动化脚本可以收集所有模块的覆盖率。我写了一个辅助脚本专门把BMP文件转成仿真用的hex格式。原理是读取BMP文件头跳过头54字节的文件信息再读取像素数组。需要注意Windows BMP是自底向上存储的转换的时候要翻转行序。这个脚本代码不长但能省掉大量手工输入仿真数据的时间。具体的实现里可以按标准BMP格式解析各个字段包括biBitCount通常24位、biHeight注意符号位表示是否为自顶向下、biSizeImage像素数据长度等等。3.2 Formality或Vivado综合下来的资源消耗整个编码器用纯Verilog写出来综合资源大概是这样的。以Xilinx Artix-7系列为参考全分辨率1080P30fps的配置下资源消耗如下表所示这一份数据是我在VC707类似的板卡上实测综合的结果不同编译选项可能略有差异资源类型消耗量说明LUT5873主要消耗在Huffman编码和位流打包FF4217主要消耗在流水线寄存器DSP48E118DCT蝶形运算使用BRAM12.5块行缓存、量化表和Huffman表工作频率150MHz流水线优化后的综合结果这个资源量在主流中端FPGA上都跑得起来比如Artix-7 35T或Zynq 7010都有余量。对比直接用IP核的方案我们的源码方案优势是灵活性高可以根据具体场景定制下采样比例和码表。缺点是需要更多验证时间毕竟IP核是厂商调过的。不过对于学习用途来说能把整个JPEG压缩流程的源码跑通对理解视频编码原理帮助巨大比单纯调用IP核要有价值得多。3.3 板级调试时用ILA抓什么信号仿真跑通之后上板调试又是另一批问题。在Vivado里用ILAIntegrated Logic Analyzer抓内部信号是定位问题的核心手段。我最常抓的信号是这几个顶层输入侧的valid和ready握手信号检查数据有没有反压断流block_reshape模块的块计数器看8x8分块有没有错位Huffman编码输出端的码流计数看有没有出现异常跳变外部FIFO的空满标志排查是否存在溢出丢数据的情况上板调试有个容易忽视的问题如果图像尺寸不是8的整数倍最后几行和最后几列的块会缺像素。标准做法是边缘像素填充即用最后一行或最后一列的像素复制填充。很多人在仿真时用规则尺寸的图像测这个问题根本暴露不出来。我建议在Testbench里专门加一个非对齐分辨率的用例把这块逻辑提前验证掉。4. 常见问题与排查技巧实录4.1 打不开生成图片或者图片花屏这是遇到最多的问题。打不开图片基本上可以确定是码流拼接或JPEG头的问题。常见的原因有几个。第一个是头信息中高度和宽度字段没有正确填写某些专业看图软件直接拒绝显示。第二个是EOI标志前缺少字节对齐填充把文件尾部补位的1补成了0。第三个是Huffman码表和实际编码时用的码表不一致这会导致解码器错位。花屏问题的排查思路是先用软件方式生成一张标准的JPEG图片然后把JPEG文件的Huffman段和数据段的字节流和FPGA输出的码流逐字节对比找到第一个不一致的字节再用仿真定位该字节对应的内部状态。这个方法虽然笨但特别管用基本可以准确锁定是哪个模块出了错。4.2 图像有块效应但码流正常块效应有两种一种是整个图像都有方块感另一种是特定区域出现明显的网格。前者大概率是量化表取的步长太大压缩比上去了但画质下来了。后者通常是DCT定点化时精度不够比如缩小因子取太小导致高频系数大量被截断成0。解决方法是调整定点格式。DCT系数在8位像素输入的情况下最大动态范围大概是12位左右如果要用13位定点也就是左移13位那乘法结果需要26位寄存器来存否则就溢出了。这个位宽一定要算清楚不能有侥幸心理。4.3 时序跑不上高频怎么办一套JPEG编码器的组合逻辑最深的地方通常在位流打包模块。因为每次拼接码字时需要依赖当前寄存器状态形成一条较长的组合逻辑链。如果这部分成了时序瓶颈完全可以再多打几拍。一种做法是把64位移位寄存器拆成两级第一级拼接到32位第二级再从32位拼到64位。这样每个周期最多只处理32位的数据组合逻辑深度直接减半代价是无效位需要多留几拍但因为JPEG码流的平均码长较短这个等待周期对吞吐量影响可以忽略。4.4 如何把编码器接到DDR3或AXI总线上实际项目中FPGA前端接的是MIPI或LVDS接口的Sensor后端接DDR缓存编码器只是中间一环。接入总线的关键点是要有异步FIFO做跨时钟域隔离。Sensor的像素时钟和编码器的工作时钟往往不同步中间必须有异步FIFO缓冲。FIFO深度取决于突发处理能力通常保留两行图像数据的空间就够了也就是2乘以行宽乘2字节这样可以在DDR带宽抖动时从容应对。接入AXI总线时编码器的输出端还要加一个大一点的FIFO比如4KB用来凑整burst传输。你不希望每个字节都发起一次AXI写请求那样总线效率会低到无法接受。将编码器输出通过FIFO积累到一定阈值再一次性发起burst写总线利用率能轻松提升到90%以上。5. 一个C参考模型验证环节的照妖镜写硬件的同时我强烈建议保留一份C语言写的JPEG编码参考模型。很多问题比如Huffman编码表不对、位流打包顺序错误用C模型一跑就能快速定位。这个C模型不需要做真正的文件输出只需要输出一个整型数组用来模拟硬件里码流的位拼接过程。调试方法是这样同一张测试图分别喂给FPGA源代码仿真和C参考模型两个输出字节流必须要完全一致。只要能找到一个不一致的字节就用二分法缩小范围比如先检查Huffman输出之前的符号序列是否一致如果符号序列一致再检查位流打包逻辑。这套流程走下来基本可以在一两天内把一个新写的JPEG编码器调试到完全正常。这个C模型的代码不长大概不到200行配合仿真使用是整个验证流程里性价比最高的工具。做FPGA工程的人都清楚调试硬件Bug最怕的就是数据流不可见C模型恰恰是打开黑盒的钥匙。6. 后续扩展思路代码跑通之后这套方案还有很大的扩展空间。目前实现的是静态图像JPEG压缩稍微改一下模块间的控制逻辑就能把连续视频帧压缩成MJPEG视频流。JPEG XS和JPEG 2000等更高压缩率的算法硬件架构和Baseline JPEG完全不同但很多基础模块如色彩空间转换、分块重排、码流打包的思路是相通的可以在现在的源码框架上继续演进。在实际使用中我的体会是FPGA这套JPEG项目技术点非常密集但难点并不在于某一项技术有多深而在于如何把DCT数学原理、量化表的工程选择、变长码流处理的细节还有板上调试的经验串联成一个完整可用的系统。遇到问题先画数据流图再对着C参考模型定位这套方法论比任何单一技巧都重要。如果你也正在做类似的项目建议先不要急着上板把仿真验证环境做到位后面能节省大量时间。本文还有配套的精品资源点击获取