FPGA纯Verilog实现PNG硬解码:10套工程源码与实操避坑指南

发布时间:2026/9/30 1:32:06
FPGA纯Verilog实现PNG硬解码:10套工程源码与实操避坑指南 PNG 这种格式在 PC 端属于“呼吸一样自然”的存在浏览器、图片查看器、甚至记事本都能把它打开。但一旦把场景挪到 FPGA 上事情就完全变了味没有操作系统、没有 zlib 库、没有现成的解码器只有一块逻辑资源和片上存储都极其有限的芯片。你要做的是从一串字节流里把压缩过的图像数据一点点“抠”出来还原成像素再送到显示器或者后续的图像处理流水线里。这件事听起来像是给自己找麻烦但它恰恰是很多嵌入式视觉项目绕不开的一环——摄像头采集、图像缓存、界面显示中间往往就需要一个能独立跑起来的 PNG 解码模块。我接触这个方向最初是因为一个工业检测的小项目上位机把标注好的参考图以 PNG 格式下发到板子上板子需要把这张图解码后叠加到实时画面上做比对。用软核跑解码速度跟不上还占 CPU。用现成的 IP要么收费要么不开放源码改都改不动。最后只能自己用纯 Verilog 撸一套。前后折腾了好几版踩过的坑从 zlib 的 Huffman 树重建到滤波器类型的边界处理再到 BRAM 的位宽对齐几乎每一处都能让人卡上半天。这篇文章就把这套东西完整拆开讲一遍包括 10 套不同定位的工程源码是怎么组织的、各自适合什么场景、以及那些文档里不会写的实操细节。1. 为什么要在 FPGA 里硬解 PNG而不是换个格式1.1 PNG 的压缩链路决定了它“不好惹”先把 PNG 的文件结构捋清楚不然后面写解码器就是盲人摸象。一个标准 PNG 文件由若干 chunk 组成每个 chunk 有长度、类型、数据和 CRC 四部分。真正跟解码相关的是三类IHDR 描述图像宽高、位深、颜色类型IDAT 存放经过 zlib 压缩的图像数据IEND 收尾。关键就在 IDAT 里——它不是简单的游程编码而是先做行滤波Filter再用 DEFLATE 算法压缩。DEFLATE 本身又分两层LZ77 做字典匹配Huffman 做熵编码。Huffman 还分固定表和动态表两种动态表需要在解码前先把码长信息读出来、重建整棵 Huffman 树。这意味着解码器不能“边读边解”必须先缓存一段数据、建树、再回过来解码。这个“先建树后解码”的特性直接决定了 FPGA 里必须有一块缓冲来暂存压缩数据也决定了状态机的复杂度。很多人第一反应是既然这么麻烦为什么不直接用 BMPBMP 几乎不压缩解码就是搬数据。问题在于体积。一张 1920×1080 的 24 位 BMP 差不多 6MB而同样内容的 PNG 可能只有几百 KB。在带宽受限、Flash 容量有限的板子上这个差距是致命的。所以不是我们想硬解 PNG而是存储和传输成本逼着我们必须解 PNG。1.2 纯 Verilog 实现的价值到底在哪用 HLS 或者软核能不能做能但有代价。HLS 生成的电路在时序收敛和资源利用上往往不够精细尤其是 Huffman 解码这种带大量分支和查表的逻辑综合出来的结果可能比手写 Verilog 大一圈。软核就更不用说了解码一张图动辄几十毫秒实时性根本谈不上。纯 Verilog 的好处是每一拍时钟都在你的掌控之中。你可以精确设计流水线让 Huffman 解码、反向滤波、像素输出三级并行起来你可以把 Huffman 查找表塞进分布式 RAM 而不是 BRAM省下宝贵的大块存储你还可以针对特定图像尺寸做参数化把不需要的逻辑直接裁掉。这些优化空间是高层综合工具给不了的。提示纯 Verilog 不等于“全部手写”。Huffman 码表、CRC 校验这类规整逻辑完全可以用脚本生成 Verilog 查找表既保证正确性又省人力。关键是核心状态机和数据通路要自己掌控。1.3 10 套工程源码的定位差异之所以整理出 10 套工程是因为不同阶段、不同板子、不同需求对解码器的要求差别很大。有的是为了教学代码要清晰、注释要全、方便单步仿真有的是为了上板跑通追求资源占用和时序余量还有的是为了集成到完整图像处理链路里需要 AXI-Stream 接口和 DDR 缓存配合。这 10 套大致可以分成几个梯队入门验证型侧重仿真和波形观察、资源优化型针对小容量 FPGA、高性能流水线型面向高分辨率实时解码、以及系统集成型带 DDR 读写和显示输出。每一套的顶层结构和参数配置都不一样但底层解码核心是共用的。下面会逐层拆解。2. 解码核心的状态机该怎么划分2.1 从字节流到像素四个必须串行的阶段不管哪套工程解码核心都逃不开四个阶段chunk 解析、zlib 解压、反向滤波、像素重组。这四个阶段有严格的先后依赖不能随意打乱。chunk 解析负责从文件头开始扫描找到 IHDR 拿到图像参数找到 IDAT 把压缩数据搬进缓冲区。zlib 解压把 IDAT 数据还原成“滤波后的原始字节”。反向滤波根据每行开头的滤波器类型字节把滤波后的字节还原成真实像素字节。像素重组再根据颜色类型和位深把字节流拼成 RGB 或灰度像素。这里最容易出问题的是阶段之间的握手。比如 zlib 解压输出的字节流是连续的但反向滤波需要按“行”来处理每行开头有一个滤波器类型字节。如果解压模块不知道行边界在哪就会把滤波器字节当成像素数据处理。解决办法是在解压输出侧维护一个行计数器每输出一行像素字节数就插入一个行结束标志通知滤波模块切换行。2.2 状态机的粒度选择粗粒度还是细粒度状态机划分有两种思路。粗粒度是把四个阶段各做一个大状态每个大状态内部再用计数器控制子步骤。细粒度是把每个字节的处理都拆成一个状态。粗粒度的好处是状态数少、综合后面积小但调试时波形不够直观细粒度的好处是每一拍在干什么一目了然但状态数可能上百时序压力大。我实测下来比较稳妥的做法是“中等粒度”chunk 解析用粗粒度因为它的分支不多zlib 解压用细粒度因为 Huffman 解码本身就是逐位进行的反向滤波和像素重组用中等粒度按行或按像素块推进。这样既保证了可调试性又不至于让状态机爆炸。2.3 一个容易被忽略的细节位序和字节序PNG 规定所有多字节整数都是大端序Big-Endian而 DEFLATE 里的 Huffman 码是“从高位到低位”逐位读取的。这两点如果搞反解码结果会完全错乱而且症状很隐蔽——可能前几行正常后面突然花屏。我在第一版里就栽在这上面chunk 长度字段读反了导致 IDAT 数据偏移了整整两个字节解出来的图像整体错位。处理办法很直接在读取多字节字段时用一个移位寄存器把字节按大端顺序拼起来在 Huffman 解码时明确约定“先读入的位放在高位”。这两处都要在代码里写清楚注释否则过两个月自己都看不懂。3. Huffman 解码整个工程里最硬的一块骨头3.1 动态 Huffman 树的码长重建过程固定 Huffman 表是写死的直接查表就行。动态 Huffman 表才是麻烦所在。DEFLATE 的动态块开头会给出三组码长字面量/长度码的码长、距离码的码长、以及码长码本身的码长。码长码用固定表解码解出来的结果再用来构建前两组码长序列。这里有个“重复码”机制码长 16 表示重复前一个码长 3 到 6 次17 表示重复 0 共 3 到 10 次18 表示重复 0 共 11 到 138 次。这个机制是为了压缩码长序列本身。在 FPGA 里实现这段逻辑核心是一个“码长缓冲区”和一套重复展开逻辑。缓冲区大小要按最大码长数量来定字面量/长度码最多 286 个距离码最多 30 个。展开逻辑要能处理连续多个重复码不能只处理一个。我见过有人只处理单次重复结果遇到长串零就崩了。3.2 用查找表还是用树遍历Huffman 解码有两种实现方式一种是构建二叉树从根节点逐位下降另一种是构建查找表用固定位宽直接索引。树遍历的优点是逻辑简单、资源少缺点是每解码一个符号要走多拍速度慢。查找表的优点是一拍出结果缺点是表的大小随码长指数增长。实际工程里我推荐“分级查找表”先用一个较小的表处理短码比如 9 位以内命中就直接输出没命中再用第二级表处理长码。这样大部分符号一拍就能解出来只有少数长码需要两拍。资源占用比全表小很多速度又比纯树遍历快得多。这个思路在 DEFLATE 解码里非常经典值得花时间实现。3.3 位流读取器的设计要点Huffman 解码是逐位进行的所以需要一个位流读取器把字节流转换成连续的位流。这个模块看似简单实则暗藏玄机。首先它要能跨字节边界读取比如当前需要 7 位但当前字节只剩 3 位就得从下一个字节借 4 位。其次它要能预读因为 Huffman 解码需要先看若干位才能决定码长。第三它要能回退因为分级查找表可能先读了 9 位但实际码只有 5 位多读的 4 位要还回去。我的做法是维护一个位宽足够的移位寄存器比如 32 位每次从输入 FIFO 补充数据解码时从高位取。回退操作就是调整一个“已消费位数”的指针。这个设计在仿真里跑得很稳综合后也能满足时序。注意位流读取器的位宽不能太小。如果只用 16 位遇到长码加跨边界的情况就会频繁补充数据吞吐率掉得厉害。32 位是比较平衡的选择。4. 反向滤波与像素重组细节决定成败4.1 五种滤波器类型的处理差异PNG 定义了五种滤波器None、Sub、Up、Average、Paeth。每行开头一个字节指明用哪种。None 就是原样输出Sub 是当前像素减去左边像素Up 是减去上边像素Average 是减去左边和上边的平均值Paeth 最复杂要根据左边、上边、左上三个像素做一个预测再减去预测值。这五种滤波器的共同点是都需要“上一行”的数据。这意味着解码器必须缓存至少一整行的像素。对于 1920 宽的图像一行就是 1920 字节灰度或 5760 字节RGB这个缓存必须用 BRAM 实现。而且滤波是逐像素进行的Sub 和 Paeth 还需要“当前行已解码的左边像素”所以缓存要支持同时读上一行和写当前行。Paeth 预测器的实现是重点。它的逻辑是计算左边、上边、左上三个值的和减去其中最大者和最小者剩下的就是预测值。这个逻辑用组合电路实现不难但要注意位宽和符号处理。我见过有人直接用有符号数算结果在边界处溢出图像边缘出现异常条纹。4.2 位深与颜色类型的组合处理PNG 支持多种位深1、2、4、8、16和颜色类型灰度、真彩、索引、带 Alpha。不同组合下像素字节的排列方式完全不同。比如 8 位灰度是每字节一个像素8 位 RGB 是每三字节一个像素而 1 位灰度是每字节八个像素需要逐位拆分。在 FPGA 里最省事的做法是先把所有格式统一转换成 8 位 RGB 或 8 位灰度再做后续处理。转换逻辑放在像素重组阶段。对于低位深需要做位拆分和查表索引色对于 16 位需要做高低字节合并和截断。这些逻辑虽然琐碎但都是纯组合或简单时序实现起来不难难的是把所有分支都覆盖到。4.3 行缓存的组织方式行缓存是反向滤波的核心存储。它的组织方式直接影响 BRAM 的利用率和访问冲突。我的做法是用一个双口 BRAM一个口专门读上一行一个口专门写当前行。读地址和写地址同步推进这样每拍可以同时读一个旧像素、写一个新像素效率最高。但这里有个陷阱当处理到行首时上一行的数据还没准备好因为上一行可能刚写完。解决办法是在行与行之间插入一个“行切换”周期等上一行完全写入后再开始下一行。这个周期虽然会损失一点吞吐率但保证了正确性。如果追求极致速度可以用乒乓缓存两行交替读写但 BRAM 占用翻倍。5. 10 套工程源码的组织与选型建议5.1 入门验证型先跑通仿真再谈上板前两套工程定位是入门验证特点是代码结构清晰、注释详尽、附带完整的 testbench。它们不追求资源优化甚至故意把一些逻辑展开写方便在波形里观察每一步的变化。适合刚接触 FPGA 图像处理、想先理解 PNG 解码流程的人。这两套的顶层只包含解码核心和一个简单的输出 FIFO不涉及 DDR 和显示。仿真时用 Icarus Verilog 或商业仿真器都可以testbench 会读入一个小的 PNG 文件比如 64×64把解码结果和参考像素逐点比对。跑通这两套基本就掌握了 PNG 解码的主干。5.2 资源优化型给小容量 FPGA 留出空间第三到第五套针对的是逻辑资源紧张的板子比如一些入门级 FPGA。优化手段包括Huffman 查找表用分布式 RAM 而非 BRAM行缓存用单口 BRAM 加时分复用状态机合并以减少寄存器位流读取器位宽降到 24 位以省触发器。这些优化会牺牲一些吞吐率但对于小图比如 800×600 以下完全够用。实测下来优化后的版本在同等器件上能省下约 30% 的 LUT 和 20% 的 BRAM。代价是解码一张 800×600 的图从 2ms 变成 3ms 左右对于非实时场景可以接受。5.3 高性能流水线型面向高分辨率实时解码第六到第八套是给高分辨率场景准备的比如 1080p 甚至更高。核心思路是把解码流程做成深度流水线Huffman 解码、反向滤波、像素重组三级并行每级之间用 FIFO 缓冲。这样虽然单级延迟没变但整体吞吐率大幅提升。流水线的难点在于反压处理。如果后级 FIFO 满了前级必须暂停但 Huffman 解码是逐位进行的暂停后位流指针要能正确恢复。我的做法是在每级 FIFO 上设置水位线接近满时提前拉低前级使能留出足够的余量。这个余量要根据最坏情况下的分支延迟来估算不能拍脑袋定。5.4 系统集成型带 DDR 和显示输出的完整链路最后两套是完整系统包含 DDR 读写控制器、显示时序生成、以及 AXI-Stream 接口。解码后的像素先写入 DDR 帧缓存再由显示模块读出送到屏幕。这种结构适合把 PNG 解码嵌入到更大的图像处理系统里。DDR 读写是这里的另一个坑。DDR 的突发长度、地址映射、刷新时序都要仔细配置否则会出现读写冲突导致画面撕裂。我的经验是给解码写入和显示读取分配不同的 Bank并且用独立的仲裁器管理访问优先级。显示读取优先级要高于解码写入因为显示断流是肉眼可见的而解码慢一点只是加载时间变长。工程编号定位适用场景资源侧重吞吐率参考1-2入门验证学习、仿真可读性优先低3-5资源优化小容量 FPGALUT/BRAM 节省中6-8高性能流水线1080p 实时吞吐率优先高9-10系统集成完整图像链路接口与缓存高6. 实操中那些文档不会告诉你的坑6.1 CRC 校验到底要不要做PNG 每个 chunk 后面都有 CRC 校验。理论上应该校验但实际工程里如果数据来源可靠比如从 Flash 读取固定图片CRC 校验可以省掉省下的逻辑和周期相当可观。我的做法是提供一个参数默认关闭 CRC需要时再打开。这样既保证了灵活性又不拖累常规性能。但要注意如果关闭 CRCchunk 解析时仍然要正确跳过 CRC 字段的四个字节不能因为不校验就不读。这个偏移量算错后面全乱。6.2 图像宽度不是 8 的倍数时怎么办这个问题在低位深图像里特别常见。比如 1 位灰度、宽度 100 的图像每行实际占 13 个字节100 位需要 12.5 字节向上取整到 13。但滤波器是按字节处理的最后那个字节里有 4 位是填充位不是有效像素。如果像素重组时不把这 4 位剔除就会多出 4 个像素导致图像错位。处理办法是在像素重组阶段维护一个像素计数器达到实际宽度就停止输出忽略填充位。这个计数器要跟行同步每行重置。6.3 多 IDAT chunk 的拼接大图的 IDAT 往往分成多个 chunk每个 chunk 的压缩数据是连续的但 chunk 之间有长度和 CRC 字段隔开。解码器必须把这些 chunk 的数据无缝拼接起来不能因为 chunk 边界就重置 zlib 状态。我的做法是在 chunk 解析阶段就把所有 IDAT 数据搬进一个连续的缓冲区解码时只面对连续数据流不感知 chunk 边界。这个缓冲区的深度要按最大图像来估算。如果图像太大放不下就得做流式处理边解析 chunk 边解码。流式处理的复杂度高很多一般工程里用连续缓冲区就够了。6.4 仿真通过但上板花屏的排查思路这是最让人头疼的情况。仿真里像素完全正确上板后却花屏或者颜色不对。常见原因有几个一是时序约束没做好跨时钟域信号没同步二是 BRAM 初始化问题仿真时 BRAM 初值是 X但实际电路是 0如果逻辑依赖初值就会不一致三是输入数据源的字节序和仿真模型不同。排查时我一般先抓 IL A 看关键信号chunk 解析出来的宽高对不对、Huffman 解码输出的符号数对不对、行缓存的读写地址是否匹配。从这些中间信号往回推通常能定位到问题所在。最怕的是直接看最终像素那样信息量太少根本无从下手。7. 把解码器接进你自己的项目7.1 接口设计FIFO 还是 AXI-Stream解码器的输入是一个字节流输出是像素流。输入侧我建议用标准 FIFO 接口因为数据来源可能是 Flash 控制器、DDR 读出模块或者网络接收模块FIFO 的通用性最好。输出侧如果接图像处理流水线用 AXI-Stream 更规范方便跟其他 IP 对接。如果只是简单显示输出侧用“像素有效 行有效 场有效”三信号也够用。这种接口虽然不标准但胜在简单调试时一眼就能看懂。7.2 参数化配置的边界工程里的参数包括最大图像宽度、最大高度、是否支持 Alpha、是否支持索引色等。参数化做得好可以按需裁剪逻辑做得不好会导致综合时生成大量冗余逻辑。我的原则是只参数化那些真正影响资源的结构比如行缓存深度、Huffman 表大小对于颜色类型这种分支用参数控制是否例化对应模块而不是在模块内部用 if 生成。7.3 和 DDR 控制器配合时的注意事项如果解码后的像素要写入 DDR要注意写地址的生成。DDR 的突发写入要求地址对齐所以像素数据要先攒够一个突发长度再发起写请求。我的做法是在解码输出侧加一个小的打包 FIFO攒够 64 字节或 128 字节再触发 DDR 写。这样既满足了突发要求又不会因为频繁小写而浪费带宽。另外DDR 的刷新周期会影响写入延迟如果解码器对延迟敏感要在写请求和实际写入之间做好缓冲。这个缓冲深度要根据 DDR 控制器的刷新间隔来算不能凭感觉。8. 关于这套源码后续还能怎么用这套解码器的核心其实不局限于 PNG。DEFLATE 解压部分稍作修改就能用于 gzip 或者 zlib 数据流Huffman 解码模块可以复用到其他熵编码场景行缓存和滤波结构也能迁移到其他图像格式的处理里。我自己的做法是把这些模块拆成独立的 IP每个都带完整的仿真和综合脚本这样在新项目里直接例化就行不用每次重头写。如果你打算在这套基础上做扩展我建议先从仿真入手把 testbench 改成读入你自己的图片观察中间信号。确认解码正确后再逐步接入 DDR 和显示。不要一上来就上板那样出了问题很难定位。另外源码里的参数注释要仔细看有些参数改了之后需要同步调整其他模块不是孤立的。最后分享一个我踩过的坑有一次为了省 BRAM把行缓存深度设成了刚好等于图像宽度结果遇到宽度不是 2 的幂次时地址回绕逻辑出错图像每隔几行就错位一次。后来把深度设成比最大宽度多几个字节问题就消失了。这个“多几个字节”的余量看起来浪费实际上是给地址计算留的缓冲非常值得。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询