☰
FPGA纯Verilog实现PNG解码:10套工程源码与硬件加速实战
2026/9/30 10:37:36 网站建设 项目流程

PNG 这种格式,做图像处理的人都不陌生——无损压缩、带 Alpha 通道、浏览器和上位机通吃。但要把它的解码器塞进 FPGA 里,用纯 Verilog 从零撸出来,这事就没那么轻松了。我前后做过几版 PNG 解码的硬件实现,从最开始的"能出图就行"到后来要求多路视频流实时解码、要跑在低端器件上、要能对接 DDR 做帧缓存,中间踩的坑足够写一本小册子。这篇就围绕"FPGA 纯 Verilog 实现 PNG 解码"这个主题,把整个工程的来龙去脉、核心模块拆解、关键参数计算、以及那 10 套工程源码到底覆盖了哪些场景,一次性讲透。不管你是刚入门想找个完整项目练手,还是已经在做图像链路、需要把 PNG 解码集成进自己的系统,这篇内容都能直接拿去参考。

1. 为什么要在 FPGA 里硬解 PNG,而不是丢给 CPU

1.1 先搞清楚 PNG 解码到底难在哪

很多人第一反应是:PNG 解码不是软件几行代码的事吗,libpng一调就完事了,为什么非要上 FPGA?这个疑问很合理,但要看应用场景。当你面对的是多路高清图像源、要求确定性延迟、或者整个系统里根本没有跑操作系统的 CPU 时,软件解码就成了瓶颈。

PNG 解码的完整流程其实比想象中复杂,它至少包含这几步:

  • 文件头与块解析:PNG 由 8 字节固定签名加一系列 chunk 组成,IHDR 给出宽高、位深、颜色类型,IDAT 存放压缩数据,IEND 结束。硬件要能识别这些块并提取关键参数。
  • Zlib/Deflate 解压:这是最硬的一块。IDAT 里是 zlib 流,包含 2 字节头、Deflate 压缩数据、4 字节 Adler-32 校验。Deflate 又分固定 Huffman 和动态 Huffman 两种编码,动态 Huffman 还需要先解出码长表再重建 Huffman 树。
  • 反滤波(Unfilter):PNG 对每一行做了滤波预处理,有 None、Sub、Up、Average、Paeth 五种类型,解码时必须逐行还原,而且滤波是依赖前一行和左边像素的,天然串行。
  • 像素重组与色彩转换:根据颜色类型(灰度、真彩、索引、带 Alpha 等)把字节流还原成像素,索引色还要查 PLTE 调色板,必要时做 16 位到 8 位的降位深。

软件做这些事靠的是大内存和分支预测,硬件做这些事靠的是流水线和状态机。难点在于 Deflate 的 Huffman 解码是变长码,天然不适合定长流水,必须用查表加逐位推进的方式处理。

1.2 FPGA 硬解的真正价值在哪

我总结下来,FPGA 解 PNG 的价值集中在三个点上。

第一是确定性延迟。软件解码的耗时跟图像内容强相关,压缩率高的图解码慢,遇到复杂图可能抖动几十毫秒。硬件解码一旦流水线跑起来,每行的处理周期基本固定,延迟可预测,这对工业相机、医疗影像、实时拼接这类场景是刚需。

第二是多路并行。一片中端 FPGA 里塞进 4 到 8 路独立的 PNG 解码核完全可行,每路跑自己的状态机,互不干扰。软件方案要开多线程,还要抢内存带宽,扩展性差很多。

第三是系统集成度。很多嵌入式视觉设备的主控就是个 MCU 或者干脆没有 CPU,图像从传感器进来直接要送显示或者做算法。这时候在 FPGA 里顺手把 PNG 解了,省掉一颗处理器,BOM 和功耗都下来了。

需要提醒的是,FPGA 解 PNG 并不是要取代软件方案。如果只是偶尔解一张图、对延迟没要求,那用 CPU 完全够。硬解适合的是"高频、实时、多路、低延迟"这四个条件里至少占两个的场景。

1.3 纯 Verilog 实现意味着什么

标题里强调"纯 Verilog",这点很关键。市面上不少方案是用 HLS 写 C 再综合,或者用厂商的 IP 核拼装。纯 Verilog 的好处是:

  • 可移植:不绑定任何厂商的 HLS 工具链,Xilinx、Altera/Intel、Lattice、安路、紫光同创都能跑,改改约束就能换平台。
  • 资源可控:每一块 BRAM、每一个 LUT 用在哪都清清楚楚,方便做面积优化,往小器件上塞。
  • 时序透明:关键路径在哪、能跑多快,自己心里有数,不像 HLS 生成的逻辑那样黑盒。
  • 便于教学和二次开发:状态机、流水线结构一目了然,适合拿来做数字系统设计的实战案例。

代价就是开发工作量大,Deflate 解码这种逻辑用 Verilog 手写,调试周期比 HLS 长不少。但一旦跑通,这套代码就是你自己的资产,不依赖任何工具链的版本更新。

2. PNG 解码链路的模块划分与数据流设计

2.1 整体架构:从字节流到像素流的四级流水

一套完整的 PNG 硬件解码器,我习惯把它拆成四个大模块,串成一条流水线:

模块职责关键资源
Chunk 解析器识别签名与各 chunk,提取 IHDR 参数、搬运 IDAT 数据小状态机、少量寄存器
Deflate 解压器zlib 头校验、Huffman 解码、LZ77 回溯、Adler-32 校验大 BRAM(滑动窗口)、Huffman 查找表
反滤波器按行做 Sub/Up/Average/Paeth 还原行缓冲 BRAM、像素运算单元
像素重组器色彩类型转换、调色板查表、位深对齐、Alpha 处理调色板 BRAM、输出 FIFO

数据流是单向的:外部把 PNG 字节流喂进 Chunk 解析器,解析器把 IDAT 载荷推给 Deflate 解压器,解压出来的原始扫描行数据进反滤波器,反滤波后的像素经重组器输出成标准像素流(比如 RGB888 或 RGBA8888),再送 DDR 缓存或直接上屏。

这条链路里,Deflate 解压器是绝对的核心,它占了整个解码器 70% 以上的逻辑资源和几乎全部的时序压力。反滤波器次之,因为 Paeth 预测涉及多个加减和绝对值比较。Chunk 解析和像素重组相对轻量。

2.2 为什么 Deflate 要用"滑动窗口 + 回溯"结构

Deflate 的本质是 LZ77 加 Huffman。LZ77 会把重复的字符串用"距离 + 长度"的匹配对来表示,解码时要从已经解出来的历史数据里,往回找对应距离的字节复制过来。这个"历史数据"就是滑动窗口,标准规定窗口大小最大 32KB。

硬件实现滑动窗口,最直接的办法是开一块 32KB 的 BRAM 当环形缓冲。每解出一个字节就写进去,指针循环递增。遇到匹配对时,从当前指针 - 距离的位置开始读,连续读长度个字节,边读边写回缓冲(因为匹配可能重叠,比如距离 1 长度 5 就是重复同一个字节 5 次)。

这里有个容易翻车的点:重叠匹配的读写顺序。如果距离小于长度,读指针会追上写指针,必须保证"读一个写一个"的节奏,不能先批量读完再批量写,否则数据就错了。我在第一版里就是先读后写,结果遇到距离=1的游程编码时整行数据全乱,排查了大半天才定位到这个问题。

2.3 Huffman 解码的查表策略

Deflate 的 Huffman 码是变长的,最短 7 位,最长 15 位(码长码本身最长 7 位)。硬件里没法像软件那样一位一位地走树,得用查表。

我的做法是分级查表:先看前 9 位,如果这 9 位能唯一确定一个符号,就直接出结果;如果落在长码区间,再补看后续位。这样绝大多数符号一次查表就搞定,只有少数长码需要二次查表。查找表放在 BRAM 里,用码长和码值预先算好。

动态 Huffman 的麻烦在于码表是运行时从码长码解出来的。流程是:先解出 HLIT、HDIST、HCLEN 三个数量,再读 HCLEN+4 个 3 位的码长码长度,用这组长度构建第一棵 Huffman 树,然后用它解出字面量/长度码和距离码的码长序列,最后用码长序列重建两棵真正的 Huffman 树。这一串操作在硬件里要用状态机一步步走,中间结果都得存下来。

实测经验:动态 Huffman 的码表重建阶段是整个解码器里状态最多、最容易出 bug 的地方。建议单独做一个仿真激励,把一段真实的动态 Huffman 数据喂进去,逐周期打印状态和中间变量,比在整图里调试高效得多。

3. 关键参数的硬件计算与资源估算

3.1 滑动窗口 BRAM 的容量与位宽取舍

标准 Deflate 窗口是 32KB,但实际很多 PNG 编码器不会用满。如果确定你的图像源都是小图或者压缩时窗口用得小,可以适当缩小 BRAM 省资源。但为了通用性,我建议还是老老实实开 32KB。

BRAM 的位宽选择有讲究。如果按字节(8 位)存,32KB 需要 32768 深度,用 36Kb 的 BRAM 块,深度 1024、位宽 36 的配置,大概要 32 块。如果按 32 位存,深度降到 8192,BRAM 块数能省一些,但读写控制复杂,因为匹配长度不一定是 4 的倍数。我一般选 8 位宽,控制逻辑简单,代价是多用几块 BRAM,中端器件完全扛得住。

3.2 Huffman 查找表的规模估算

字面量/长度码最多 286 个符号,距离码最多 30 个符号。查找表如果按 9 位一级表算,就是 512 项,每项存符号值、码长、类型标志,大概 16 位宽,一块小 BRAM 就够。二级表针对长码,规模更小。

码长码的 Huffman 树只有 19 个符号,码长最长 7 位,直接用一个 128 项的分布式 RAM 就能实现,连 BRAM 都不用占。

3.3 行缓冲与反滤波的时序预算

反滤波要按行处理,每行需要缓存上一行的数据(Up 和 Average、Paeth 都要用)。行缓冲大小等于图像宽度乘以每像素字节数。假设最大支持 1920 像素宽、RGBA 四通道,一行就是 7680 字节,开两块这样的 BRAM 做乒乓。

反滤波的时序压力主要在 Paeth。Paeth 预测器要算p = a + b - c,然后比较|p-a|、|p-b|、|p-c|取最小对应的那个像素。三个绝对值和两次比较,组合逻辑不浅。如果目标频率高,建议把 Paeth 拆成两级流水:第一级算 p 和三个差值,第二级做比较选择。

我做过一个粗略的时序估算:在 100MHz 下,单周期完成 Paeth 的组合逻辑延迟大概在 6 到 8ns,接近临界。如果器件速度等级一般,还是老老实实插一级寄存器,代价是每像素多一个周期,但频率能上到 150MHz 以上,总体吞吐反而更高。

3.4 整体资源占用参考

以支持 1080p、RGBA8888 输出的解码器为例,在中端 FPGA 上的大致占用:

资源类型估算用量说明
LUT8K ~ 12K主要是 Huffman 解码和反滤波
FF6K ~ 9K各级流水寄存器
BRAM40 ~ 60 块滑动窗口占大头
DSP0 ~ 4基本不用,除非做色彩空间转换

这个量级意味着,一片资源在 30K LUT 左右的器件就能放下单路解码器,还有余量做其他逻辑。如果要跑多路,按路数线性叠加即可。

4. 10 套工程源码分别覆盖了哪些场景

标题里说提供 10 套工程源码,这不是凑数,而是针对不同应用场景做了差异化配置。我把它们按用途分成几类来讲,方便你对号入座。

4.1 基础验证类:从单张图到连续流

前几套工程是给入门和验证用的。最基础的一套是"单张 PNG 从 ROM 读入、解码、输出到 VGA 显示",整个链路最短,没有 DDR,没有复杂握手,适合第一次跑通流程、观察波形。ROM 里预存一张小尺寸 PNG(比如 256x256),解码结果直接送显示时序发生器。

再往上一套是"连续多帧 PNG 流解码",用 FIFO 做输入缓冲,支持背靠背地解多张图,用来验证解码器在连续工作时的稳定性,以及输入输出握手协议是否正确。

这类工程的价值在于把问题隔离。当你后面遇到复杂系统里的 bug 时,可以退回到这些最简工程,确认解码核本身没问题,再去查系统集成部分。

4.2 DDR 缓存类:大图与多帧缓存

图像一大,片上 BRAM 就不够存整帧了,必须外挂 DDR。有几套工程专门做这个:解码后的像素流通过 AXI 或原生接口写进 DDR,再由读通道取出来送显示或送算法模块。

这里的关键是跨时钟域和带宽匹配。解码器可能跑在 100MHz,DDR 控制器跑在 200MHz 甚至更高,中间要加异步 FIFO。另外写 DDR 是突发传输效率高,所以要在解码输出侧做打包,攒够一个 burst 长度再发起写请求。

我踩过的一个坑是:解码输出速率和 DDR 写入速率不匹配时,如果 FIFO 深度不够,会出现丢像素。解决办法是加大 FIFO,或者在解码侧做反压,让解码器在 FIFO 快满时暂停。后者更省资源,但要求解码器支持暂停和恢复,状态机设计时要预留这个能力。

4.3 多路并行类:多通道独立解码

针对多路图像源的应用,有工程做了多路解码核的并行实例化。每路有独立的输入 FIFO、独立的滑动窗口 BRAM、独立的输出通道,共享 DDR 带宽但通过仲裁器分配。

多路并行的难点不在解码核本身,而在带宽仲裁和资源分配。如果 4 路同时解码、同时写 DDR,瞬时带宽需求可能是单路的 4 倍。要么提高 DDR 频率,要么做流控让各路错峰写入。工程里用的是轮询仲裁加信用机制,每路维护一个信用计数,写完一批就还信用,避免某一路饿死。

4.4 平台适配类:跨厂商的可移植性

还有几套工程是针对不同 FPGA 平台做的适配版本,覆盖了主流厂商的开发流程。核心 Verilog 代码完全一致,差异只在约束文件、IP 例化方式(比如 BRAM 是用厂商原语还是推断)、以及时钟管理模块。

这种"一套逻辑、多平台约束"的组织方式,是我强烈推荐的。它逼着你把代码写得足够规范,不依赖任何厂商特有的语法糖,可移植性自然就上去了。换平台时只需要重做约束和管脚分配,逻辑部分基本不用动。

5. 从零跑通一套 PNG 解码工程的实操路径

5.1 环境准备与工程导入

拿到源码后,第一步是确认你的工具链。纯 Verilog 的好处是主流工具都认,Vivado、Quartus、Diamond、安路的 TD、紫光同创的 PDS 都能直接建工程。我建议先用仿真工具跑一遍,Icarus Verilog 或者厂商自带的仿真器都行,确认逻辑没问题再上板。

导入时注意几点:源码目录里通常分rtl、sim、constraint、ip几个子目录,建工程时把rtl全部加入,sim里的 testbench 单独建仿真工程,约束文件按你的板子改管脚。IP 目录里如果有厂商原语,需要用对应工具重新生成或直接例化。

5.2 仿真验证:先喂一张小图

不要一上来就上板。先写一个 testbench,把一张小 PNG 的字节流按周期喂进解码器的输入接口,观察输出像素流。testbench 里可以做一个参考模型,用软件解出同样的图,逐像素比对。

仿真的关键是打印中间状态。在 Deflate 解码的状态机里加$display,把每个符号的类型、值、当前窗口指针打出来,跟软件解码的日志对比。第一次跑大概率对不上,但有了逐符号的对比,定位问题很快。

小技巧:把 PNG 用工具重新编码成"固定 Huffman、无滤波、灰度"的最简形式,先让这条最简路径跑通,再逐步增加复杂度(动态 Huffman、各种滤波、彩色、Alpha)。这样每次只面对一个新变量,调试效率高得多。

5.3 上板调试:ILA 是你的好朋友

上板后如果出图不对,别急着改代码,先把 ILA(集成逻辑分析仪)挂上。重点抓这几个信号:输入字节流的有效握手、Deflate 状态机的当前状态、滑动窗口读写指针、反滤波的行号和滤波类型、输出像素的有效标志。

我遇到过一种情况:仿真完全正确,上板后图像上半部分正常、下半部分错位。ILA 一抓发现是行缓冲的乒乓切换在某个边界条件下晚了一个周期,导致某一行用了上一行的旧数据。这种问题仿真时因为激励不够连续,恰好没触发。所以上板测试要用连续多帧、不同尺寸的图去压。

5.4 常见问题速查

现象可能原因排查方向
完全无输出输入握手死锁查 ready/valid 是否互相等待
图像花屏滑动窗口重叠匹配错误查读写指针顺序
颜色偏色彩类型判断错查 IHDR 解析和调色板
边缘错位反滤波行边界处理查每行第一个像素的 left 值
偶发丢帧FIFO 深度不足加大缓冲或加反压

6. 二次开发与性能优化的几个方向

6.1 提高吞吐:从每周期一字节到多字节

基础版解码器通常每周期处理一个输入字节,1080p 的图解码耗时在毫秒级。如果要更高吞吐,可以做多符号并行解码。Deflate 的 Huffman 码虽然是变长的,但可以一次读入多个字节,用更宽的查找表同时解出多个符号。代价是查找表规模指数增长,需要权衡。

另一个方向是提高时钟频率。把关键路径(Huffman 查表、Paeth)拆成多级流水,牺牲一点延迟换频率,整体吞吐反而提升。我做过对比,同样逻辑,插一级流水后频率从 90MHz 提到 140MHz,吞吐提升超过 50%。

6.2 降低资源:面向小器件的裁剪

如果目标是小容量 FPGA,可以做一些裁剪:滑动窗口从 32KB 缩到 8KB(限制压缩时的窗口使用)、只支持固定 Huffman(省掉码表重建逻辑)、只支持灰度或索引色(省掉色彩转换)。这些裁剪能让资源占用降到原来的三分之一,代价是通用性下降。具体裁到什么程度,取决于你的图像源是否可控。

6.3 对接图像处理链路

解码出来的像素流,通常不是终点。后面可能接缩放、色彩空间转换、边缘检测等模块。建议在解码输出和后续处理之间加一个标准化的流接口(比如 AXI4-Stream),这样解码核和处理核解耦,各自独立开发和验证。接口上带tuser标志行首、tlast标志行尾,方便下游做行同步。

6.4 关于技术支持的实际价值

标题里提到提供技术支持,这点对新手很重要。PNG 解码涉及的知识面很宽——文件格式、压缩算法、硬件时序、跨时钟域,任何一个环节卡住都可能让人放弃。有经验的工程师指点一下,往往能省掉几天甚至几周的摸索。我的建议是,遇到问题时先把现象描述清楚,附上波形截图和仿真日志,这样对方能快速定位,沟通效率最高。

7. 我在这个项目里踩过的几个真实坑

第一个坑是Adler-32 校验的位置。zlib 流末尾有 4 字节 Adler-32,我一开始忘了处理,导致解压状态机在数据结束后还在等输入,整个流程卡死。后来加了校验状态,读完 4 字节校验和就正常结束。校验本身可以只做验证不做拦截,但状态机必须走完。

第二个坑是动态 Huffman 的码长码重复。Deflate 里码长序列有一种"重复前一个码长"的编码(16、17、18 三个特殊符号),16 表示重复前一个码长 3 到 6 次,17 表示重复 0 共 3 到 10 次,18 表示重复 0 共 11 到 138 次。我第一版漏了 17 和 18 的处理,遇到用这两个符号的图就解错。补上之后才通。

第三个坑是Paeth 预测器的边界。图像第一行没有上一行,第一列没有左像素,这些边界条件下 a、b、c 的取值要按 0 处理。我一开始没处理,导致第一行和第一列总是偏色。这种边界 bug 在仿真里如果激励图不够大,很容易漏掉。

第四个坑是跨时钟域的握手。解码核和 DDR 控制器时钟不同,中间用异步 FIFO 过渡。有一次 FIFO 的读侧复位没同步好,上电后偶尔出现读指针错乱,图像整体偏移。后来把复位也做了同步处理才稳定。

这些坑的共同点是:都能在仿真里发现,但需要足够丰富的测试激励。所以我现在做这类项目,一定会准备一组覆盖各种情况的测试图:最简灰度、带调色板、带 Alpha、大尺寸、高压缩率、动态 Huffman、各种滤波类型混用。跑通这一组,基本就稳了。

8. 给不同阶段读者的上手建议

如果你是刚接触 FPGA 的学生,建议从最基础的那套工程入手,先把单张图的解码跑通,重点理解状态机和流水线的写法。不要急着改代码,先看懂数据怎么从输入流一步步变成像素。仿真波形是你最好的老师,多花时间看波形,比看代码收获大。

如果你是做图像链路的工程师,可以直接关注 DDR 缓存和多路并行那几套工程,重点看带宽匹配和仲裁逻辑。这部分的设计思路可以直接迁移到其他图像处理项目里。

如果你是要选型做产品的开发者,建议先明确你的图像源特征(尺寸、颜色类型、压缩方式),再决定用哪套配置。如果图像源可控,大胆裁剪;如果来源多样,就保留完整功能,用资源换通用性。

这套纯 Verilog 的 PNG 解码器,从第一版跑通到现在,前后迭代了十几个版本,每一版都是被实际问题逼出来的。硬件解码这条路不好走,但走通之后,你会对数据流、时序、资源这三者的关系有完全不一样的理解。这种理解,是调库调不出来的。

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

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

立即咨询