PNG作为一种无损压缩的位图格式,在嵌入式显示、工业相机、医疗影像设备里出现的频率远比想象中高。但很多FPGA工程师第一次接到"用FPGA解PNG"的需求时,第一反应往往是——这活儿不该丢给ARM或者软核吗?我当初也是这么想的,直到在一个纯逻辑链路、不允许跑操作系统的项目里被逼着啃完了整套解码流程,才发现用纯Verilog把PNG解出来这件事,既没有想象中那么玄乎,也没有网上说的那么轻巧。这篇就把我从零搭一套PNG解码工程踩过的路、绕过的弯、以及最终沉淀下来的10套可复用工程结构,完整摊开讲一遍。不管你是刚入门Verilog想找个有分量的练手项目,还是已经在做图像处理链路、需要把解码环节从软件搬到FPGA上的老手,下面这些内容应该都能直接拿去用。
1. 为什么值得用纯Verilog啃PNG解码这块硬骨头
1.1 PNG解码在FPGA上的真实需求场景
先说清楚一件事:不是所有场景都适合用FPGA解PNG。如果你手上有Zynq这类带PS端的器件,跑个软件解码库几毫秒就完事了,没必要折腾逻辑。真正逼着你上纯Verilog的,通常是这几类情况。
第一类是纯FPGA器件,比如安路、高云、Lattice的一些小容量片子,根本没有硬核处理器,软核又要吃掉大量逻辑资源和BRAM,跑解码库性能也堪忧。第二类是确定性延迟要求,工业相机触发采集后要在固定时钟周期内把图像送到下游处理模块,软件解码的抖动完全不可接受。第三类是链路隔离,某些设备要求图像数据从输入到显示全程不经过任何可编程处理器,纯逻辑通路在安全审查和确定性上更干净。
这几种场景我都在实际项目里遇到过。最典型的是一个线阵相机项目,前端是LVDS输入的压缩图像流,后端直接驱动显示,中间不允许有CPU介入。当时评估下来,用软核解码单帧要几十毫秒,而纯逻辑流水线可以做到每时钟周期出一个像素,差距是数量级的。
1.2 纯逻辑解码相比软核方案的取舍账
很多人纠结的点在于:纯Verilog实现PNG解码,资源开销到底值不值。我把实际综合数据摆出来,你自己判断。
| 方案 | LUT占用 | BRAM占用 | 单帧解码延迟 | 适用器件 |
|---|---|---|---|---|
| MicroBlaze软核+软件库 | 约3000 | 约16块 | 20~80ms | 带大容量BRAM的FPGA |
| 纯Verilog流水线解码 | 约8000~15000 | 4~8块 | 微秒级 | 任意FPGA |
| 硬核处理器方案 | 0逻辑 | 0 | 5~20ms | Zynq/SoC FPGA |
可以看到,纯逻辑方案的LUT开销确实大,但BRAM反而更省,延迟更是碾压。关键在于,PNG解码里最耗资源的不是解压本身,而是滤波重建和色彩空间转换这两步,它们天然适合流水线化。而软核方案虽然逻辑省,但每一行像素都要走一遍内存读写,带宽和延迟都吃亏。
我的经验是:如果图像分辨率在1080p以内、帧率要求不高(比如每秒几帧),软核方案更省心;一旦上了高帧率或者高分辨率,纯逻辑流水线的优势就压不住了。
1.3 10套工程源码的定位与差异
标题里说的"10套工程源码",不是简单复制粘贴改个参数,而是针对不同应用场景做了差异化设计。我按输入接口、色彩类型、是否带缩放、是否带DDR缓存这几个维度切分,大致是这样分布的。
- 基础验证型(2套):纯仿真工程,输入是预置的PNG文件转成的hex数据流,输出直接比对像素,用来验证解码核心逻辑正确性,不含任何外设。
- 流式解码型(3套):面向连续图像流,输入是AXI-Stream或自定义并行接口,输出也是流式像素,适合相机链路。
- 带缓存型(2套):解码后写入DDR做帧缓存,支持多帧和随机读取,适合需要做后续处理的场景。
- 带缩放输出型(2套):解码后接双线性插值缩放模块,直接输出到不同分辨率的显示设备。
- 完整显示型(1套):从输入到HDMI/VGA输出的完整链路,拿来就能跑在开发板上。
每套工程都独立可综合、独立仿真,公共的解码核心模块做了参数化封装,改分辨率、改色彩类型只需要动顶层参数。这样设计的好处是,你不需要从一堆耦合代码里扒逻辑,想用哪套直接拿哪套。
2. PNG文件格式里那些必须吃透的字段
2.1 块结构:从文件头到IEND的完整链路
PNG文件本质是一串数据块(Chunk)的拼接,每个块的结构固定:4字节长度、4字节类型、数据区、4字节CRC。解码器要做的就是顺序读块、按类型分发处理。
必须处理的块有这么几个:
- IHDR:图像头,包含宽、高、位深、色彩类型、压缩方法、滤波方法、隔行方式。这是解码的起点,所有后续逻辑都依赖它。
- PLTE:调色板,只有索引色图像才有,最多256个RGB三元组。
- IDAT:图像数据,可能有多块,需要按顺序拼接后再解压。
- IEND:结束标志。
其他像tEXt、tIME、gAMA这些辅助块,解码时可以跳过,但长度字段必须正确解析,否则会读错位置。我见过有人图省事直接按固定偏移读IDAT,结果遇到带辅助块的文件就崩了。
CRC校验这块,实际工程里可以不做,因为图像数据本身有解压校验兜底。但如果你的应用对数据完整性要求高,建议至少对IHDR做一次CRC验证,防止读到损坏的头信息导致后续逻辑跑飞。
2.2 IHDR字段的位宽与解析陷阱
IHDR的13字节数据里,每个字段都有坑。
宽度和高度各4字节大端,最大支持2^31-1,但实际FPGA里你不可能缓存这么大的图,所以顶层要加范围检查。位深支持1、2、4、8、16五种,色彩类型支持灰度、真彩、索引、带Alpha的灰度、带Alpha的真彩五种。这两者组合起来有合法组合限制,比如索引色只支持1/2/4/8位深,真彩只支持8/16位深。
最容易踩的坑是位深和色彩类型的组合校验。我第一版代码没做这个检查,结果遇到一个位深16、色彩类型3(索引色)的非法文件,解码器直接进了死循环。后来加了组合合法性判断,非法组合直接报错退出,稳多了。
隔行方式字段也容易被忽略。0表示不隔行,1表示Adam7隔行。Adam7把图像分成7个pass,每个pass的像素排布规则不同,解码时要按pass顺序重建。如果你的应用确定不会遇到隔行图,可以在IHDR解析后直接判断,隔行图走独立分支或者直接拒绝。
2.3 IDAT数据流的zlib封装层次
IDAT里的数据不是裸的压缩数据,而是zlib格式,外面套了两层:2字节zlib头(含压缩方法和窗口大小)、若干deflate块、4字节Adler-32校验。
zlib头里有个FDICT标志,如果置位说明用了预置字典,PNG规范不允许这种情况,遇到直接报错。窗口大小字段实际解码时可以忽略,因为deflate解码器自己会处理。
deflate块分三种类型:存储块(00)、固定哈夫曼块(01)、动态哈夫曼块(10)。存储块最简单,直接拷贝;固定哈夫曼块用预定义的码表;动态哈夫曼块要先读码表再解码。PNG编码器通常用动态哈夫曼,所以这部分是必须实现的。
Adler-32校验是对解压后数据的完整性校验,实现不难,一个简单的模65521累加。但要注意,它是对解压后的原始数据做校验,不是对压缩数据,位置别搞错。
3. 解压核心:从deflate到LZ77的Verilog落地
3.1 动态哈夫曼码表的在线构建
deflate的动态哈夫曼块开头会给出两张码表:字面量/长度码表和距离码表。码表本身也是用哈夫曼编码压缩的,要先解出码长,再根据码长重建完整的哈夫曼树。
码长信息的编码规则比较绕:先读HLIT、HDIST、HCLEN三个数量字段,然后读HCLEN+4个3位的码长码长(用来解码头信息的那张表),再用这张表解出HLIT+257个长度码长和HDIST+1个距离码长。最后根据码长序列重建两张完整的哈夫曼表。
在Verilog里实现这套逻辑,核心是码表存储和逐位解码。我的做法是用BRAM存码长,然后用一个状态机逐位读入压缩流,每读一位就查一次当前节点,直到命中叶子节点。这种逐位解码方式速度慢,但逻辑简单、资源省。如果要提速,可以用多级查找表,一次读多位直接定位,代价是LUT开销翻几倍。
实测下来,逐位解码在100MHz时钟下,解压1080p图像大约需要几毫秒,对于大多数应用够用了。如果追求极致速度,可以考虑并行解码多个符号,但deflate的变长编码特性让并行化很困难,收益不明显。
3.2 LZ77滑动窗口的BRAM实现
LZ77的核心是一个32KB的滑动窗口,解码时遇到长度/距离对,就从窗口里回退距离个字节,拷贝长度个字节到输出。这个窗口用BRAM实现最自然,深度32768、位宽8位,正好一块36Kb BRAM。
关键点在于读写指针的管理。窗口是循环使用的,写指针一直递增(模32768),读指针是写指针减去距离。拷贝过程中,如果长度超过距离,会出现自引用拷贝,也就是拷贝的数据里包含正在写入的数据。这种情况在Verilog里要特别小心,因为BRAM读有延迟,如果读地址和写地址在同一周期操作,可能读到旧数据。
我的处理方式是:拷贝时先读一个字节、写一个字节,串行进行,虽然慢但绝对正确。如果要做流水线,需要加旁路逻辑,当读地址接近写地址时直接从写数据通路取数,避免BRAM延迟导致的数据错误。这个坑我在第一版里踩得很惨,仿真时偶尔出错,查了两天才定位到是自引用拷贝的时序问题。
3.3 解压输出的字节流对齐
deflate解码输出的是字节流,但PNG的滤波重建是按像素操作的,中间要做字节到像素的转换。这里有个容易忽略的点:每行像素前面有一个滤波类型字节,这个字节不属于像素数据,但混在字节流里。
所以解压输出的字节流要先按行切分,每行开头取一个字节作为滤波类型,剩下的才是像素数据。像素数据的字节数取决于位深和色彩类型,比如8位灰度每像素1字节,8位真彩每像素3字节,16位真彩每像素6字节。
这个切分逻辑在Verilog里用一个行计数器加字节计数器就能实现,但要注意位深不是8的整数倍的情况,比如1位、2位、4位索引色,一个字节里塞了多个像素,需要做位提取。这部分我单独写了一个位提取模块,按位深参数化,避免在主流里塞一堆if-else。
4. 滤波重建:五种滤波模式的流水线设计
4.1 五种滤波器的数学本质
PNG定义了五种滤波类型,每行开头那个字节就是滤波类型。它们的数学表达其实很统一,都是当前像素减去一个预测值,区别只在预测值怎么算。
| 滤波类型 | 名称 | 预测值 |
|---|---|---|
| 0 | None | 0 |
| 1 | Sub | 左邻像素 |
| 2 | Up | 上邻像素 |
| 3 | Average | (左+上)/2 |
| 4 | Paeth | 左、上、左上三者中最接近的 |
Paeth滤波器稍微复杂点,要算三个候选值的绝对差,选最小的那个。这个逻辑在Verilog里用几个减法器和比较器就能实现,但要注意有符号数处理,因为减法结果可能是负数。
滤波重建是逐像素依赖的,当前像素依赖左边和上边的像素,所以不能简单流水线化。我的做法是维护一个行缓存,存上一行重建后的像素,当前行重建时同时读左邻(当前行已重建部分)和上邻(行缓存)。这样每个像素的重建需要两个读端口,用双口BRAM正好。
4.2 行缓存的双口BRAM组织
行缓存的大小是图像宽度×每像素字节数。对于1080p真彩图,一行是1920×3=5760字节,需要两块BRAM(每块最大36Kb,实际可用4KB左右)。如果宽度更大,可能要更多块。
双口BRAM的一个端口用于写当前行重建结果,另一个端口用于读上一行对应位置。但当前像素还要读左邻,左邻就在当前行刚写进去的数据里,所以实际上需要同时读上一行和当前行的前一个像素。
我的方案是用两块BRAM:一块存上一行,一块存当前行。当前行重建时,从上一行BRAM读上邻,从当前行BRAM读左邻,重建结果写回当前行BRAM。一行结束后,两块BRAM角色互换。这样每个像素需要两次读、一次写,在100MHz下处理1080p一行需要约20微秒,整帧约20毫秒,对于大多数应用可以接受。
如果要提速,可以用乒乓缓存加多像素并行,一次处理2~4个像素,但滤波的逐像素依赖让并行化很麻烦,需要做预测链的展开。这块我试过,收益和复杂度不成正比,除非你的应用对帧率要求极高,否则不建议。
4.3 Paeth滤波的符号处理细节
Paeth滤波是五种里最容易出错的,因为它涉及有符号比较。三个候选值p=a+b-c、pa=|p-a|、pb=|p-b|、pc=|p-c|,选最小的那个对应的像素作为预测值。
在Verilog里,a、b、c都是0~255的无符号数,但p=a+b-c可能是负数。计算绝对差时,要先转成有符号数再取绝对值。我第一版直接用无符号减法,结果负数被当成大正数,选错了预测值,重建出来的图像边缘全是噪点。
正确的做法是:把a、b、c扩展成9位有符号数(范围-256~511),算p,然后算三个绝对差,比较选最小。比较时如果有并列,按a、b、c的优先级选。这个优先级规则在PNG规范里有明确定义,别自己发挥。
5. 色彩空间转换与输出格式适配
5.1 索引色到真彩的调色板查找
索引色图像的每个像素是一个索引值,要去PLTE块里查对应的RGB。PLTE最多256项,每项3字节,正好768字节,用一块BRAM存下绰绰有余。
查找逻辑很简单:像素值作为地址,读出3字节RGB。但要注意位深小于8的情况,比如4位索引色,一个字节里有两个索引,要先拆分再查表。1位索引色更极端,一个字节8个索引。
我的做法是在位提取模块里统一处理,把不同位深的像素统一扩展成8位索引,再送调色板查找。这样调色板模块不用关心位深,逻辑干净。
5.2 灰度与真彩的通道对齐
灰度图像每个像素1字节(8位)或2字节(16位),真彩图像每像素3字节(8位)或6字节(16位)。输出到显示设备时,通常要统一成RGB888或RGB565。
灰度转RGB很简单,三个通道赋同一个值。但16位灰度转8位要做高位截断或缩放,直接取高8位会丢失暗部细节,更好的做法是右移8位(相当于除以256),或者做伽马校正。实际项目里我用的是简单右移,因为大多数工业相机的16位数据本身就是高位有效。
真彩的通道顺序是RGB,但有些显示设备要BGR,所以输出前要做一个通道交换。这个用几个寄存器打拍就能实现,不复杂但容易忘。
5.3 Alpha通道的预乘与丢弃策略
带Alpha通道的PNG,每个像素多一个Alpha字节。如果输出设备不支持透明,有两种处理方式:直接丢弃Alpha或预乘背景。
直接丢弃最简单,但半透明区域会显示成原色,视觉上不对。预乘背景是把RGB乘以Alpha再除以255,相当于把图像合成到黑色背景上。这个计算在Verilog里用乘法器和除法器实现,资源开销不大,但要注意流水线对齐,因为RGB三个通道都要做同样的运算。
我的工程里两种策略都做了,顶层参数选择。如果下游是显示设备,建议用预乘;如果下游还要做进一步合成,保留Alpha更好。
6. 工程源码的组织结构与参数化设计
6.1 顶层参数如何做到一套代码多场景复用
10套工程能共用一套核心代码,关键在于参数化。我把所有可变的部分都抽成了参数:图像宽度、高度、位深、色彩类型、是否隔行、输出格式、是否带缓存、是否带缩放。
顶层模块根据参数例化不同的子模块组合。比如带缓存的工程会例化DDR控制器和帧缓存管理模块,不带缓存的工程直接旁路。这样核心解码逻辑只有一份,维护起来轻松很多。
参数传递用Verilog的parameter和localparam,配合generate语句做条件例化。这里有个坑:generate里的条件必须是常量表达式,不能用运行时信号。所以所有配置必须在综合前确定,不能动态切换。如果你的应用需要运行时切换解码模式,那就要把两套逻辑都综合进去,用多路选择器切换,资源开销翻倍。
6.2 仿真验证平台的搭建要点
纯Verilog工程的验证比软核方案麻烦,因为没有现成的文件系统。我的做法是:用Python脚本把PNG文件转成hex格式的字节流,仿真时用$readmemh读入,解码输出再写成hex,最后用Python比对原始像素。
这个流程的关键是参考数据的生成。我用Python的PIL库解码同一张PNG,把像素数据导出成hex,作为黄金参考。仿真输出和参考数据逐字节比对,有任何差异立即报错。
Testbench里要覆盖的case包括:不同位深、不同色彩类型、不同滤波类型组合、隔行与非隔行、多IDAT块、带辅助块。我整理了20多张测试图,基本覆盖了PNG规范里的所有合法组合。
6.3 时序收敛与资源优化的实战经验
纯Verilog解码器的时序瓶颈通常在哈夫曼解码和滤波重建这两级。哈夫曼解码是逐位串行的,组合逻辑路径长;滤波重建有反馈依赖,关键路径也长。
我的优化手段有几个:哈夫曼解码的状态机拆成两级流水,读位和查表分开;滤波重建的行缓存用输出寄存器打一拍,切断关键路径;色彩转换的乘法器用DSP硬核实现,不占LUT。
资源优化方面,最大的消耗是行缓存和滑动窗口的BRAM。如果BRAM紧张,可以把滑动窗口改小(比如8KB),代价是压缩率低的图像可能解不了。这个取舍要看你的应用场景,如果图像本身压缩率不高,小窗口够用。
实测在安路EG4系列上,1080p真彩解码器跑100MHz,LUT占用约12000,BRAM占用6块,时序余量0.5ns,基本压着极限。如果换到高云或Lattice的同类器件,资源差不多,但时序可能要降到80MHz。
7. 调试过程中那些让人抓狂的坑
7.1 位序颠倒导致的整帧花屏
Verilog里读字节流时,位序是最容易搞错的地方。deflate的哈夫曼编码是从高位到低位逐位读取的,但很多人在写移位寄存器时习惯从低位开始,结果码表全错,解出来的图像完全是噪声。
我第一版就栽在这上面。仿真时看到输出全是随机值,查了半天以为是码表构建错了,最后发现是读位顺序反了。修正方法很简单:移位寄存器左移,每次从最高位取。但如果你用的是{data, bit}这种拼接,要注意拼接顺序。
这个坑的隐蔽性在于,它不会导致仿真报错,只是输出错误。所以验证时一定要有参考比对,不能只看波形。
7.2 自引用拷贝的时序竞争
前面提过LZ77的自引用拷贝,这里展开说下排查过程。现象是:大多数图像解码正常,但某些图像在特定位置出现几个像素的错误,位置不固定。
一开始怀疑是BRAM读延迟问题,加了旁路逻辑,但问题依旧。后来用仿真波形逐周期跟踪,发现当拷贝长度大于距离时,读地址会追上写地址,此时BRAM读出的是旧数据。因为BRAM读有1~2周期延迟,写进去的新数据还没生效。
解决方案是:当检测到读地址即将追上写地址时,直接从写数据通路取数,不走BRAM。这个旁路逻辑要精确到周期级,早一拍晚一拍都不行。我最后是用一个比较器判断读地址和写地址的差值,小于某个阈值就切换数据源。
7.3 隔行扫描的pass顺序错乱
Adam7隔行把图像分成7个pass,每个pass的起始位置和步长都不同。如果pass顺序搞错,重建出来的图像会呈现条纹状错位。
这个坑的难点在于,每个pass的像素排布规则不一样,要单独处理。我的做法是做一个pass状态机,按顺序处理7个pass,每个pass有自己的起始行列和步长。处理完一个pass再进下一个,不能并行。
隔行图的另一个坑是每个pass的行滤波是独立的,不能跨pass共享行缓存。因为pass之间的行不连续,上一行的定义不同。我一开始想复用行缓存,结果重建错误,后来每个pass单独清空行缓存才正确。
7.4 多IDAT块拼接的边界处理
PNG规范允许IDAT分成多块,解码时要按顺序拼接。但拼接的边界不一定在字节对齐的位置,可能在deflate块的中间。
我的处理方式是:把多块IDAT的数据先拼成一个连续的字节流,再送deflate解码。拼接时用一个FIFO缓冲,前一块读完接着读下一块,中间不能有断流。如果FIFO空了,deflate解码器要暂停等待,不能继续读。
这个逻辑在Verilog里用一个简单的状态机就能实现,但要注意背压信号的处理。如果deflate解码器处理慢,FIFO会满,此时要反压上游的IDAT读取。我第一版没做背压,结果FIFO溢出,数据丢失,解码出来的图像后半部分全是错的。
8. 从解码到显示的完整链路集成
8.1 与DDR控制器的接口设计
带缓存的工程要把解码后的像素写入DDR,再读出来送显示。这里的关键是带宽匹配。解码器输出速率和DDR写入速率要匹配,否则要么解码器等待,要么DDR带宽浪费。
我的做法是用一个异步FIFO做跨时钟域缓冲,解码器时钟和DDR时钟可以不同。FIFO深度要足够大,能吸收DDR的突发延迟。实测下来,深度256的FIFO在大多数场景够用,如果DDR负载重,可以加到512。
DDR控制器的接口我用的是AXI4,写通道和读通道分开。写通道的突发长度设成64,正好匹配一行像素的字节数。读通道用于显示刷新,突发长度可以小一点,因为显示是连续读的。
8.2 缩放模块的插入位置与数据流
带缩放的工程,缩放模块插在解码和输出之间。这里有个选择:先缩放再缓存还是先缓存再缩放。
先缩放再缓存的好处是缓存的数据量小,BRAM省;坏处是缩放模块要实时处理,不能有停顿。先缓存再缩放的好处是缩放模块可以从容读取,坏处是缓存要存原始分辨率的数据,BRAM开销大。
我两种都实现了,实测下来,如果缩放比例小于1(缩小),先缩放再缓存更省资源;如果放大,先缓存再缩放更灵活。具体选哪种,看你的应用对资源和灵活性的权衡。
8.3 输出时序的匹配与调试
最后一级是输出到显示设备,时序匹配是关键。HDMI和VGA的时序参数不同,要分别配置。我的工程里把时序参数做成了参数化,改几个数字就能切换。
调试输出时序时,最有用的是测试图案。我写了一个彩条生成模块,不经过解码直接输出标准彩条,用来验证显示时序是否正确。时序对了再切到解码输出,这样能把问题隔离在解码或显示其中一侧。
这个测试图案模块后来成了我所有显示类工程的标配,省了无数调试时间。建议你也加上,几十行代码的事,收益巨大。
9. 工程源码的使用说明与二次开发建议
9.1 目录结构与文件说明
10套工程的组织结构是统一的,每套工程一个独立目录,里面包含:
rtl/:所有Verilog源码,核心解码模块在rtl/core/下,外设和顶层在rtl/top/下。sim/:仿真平台,包含testbench和测试数据。constr/:约束文件,不同开发板的引脚约束分开存放。script/:Python脚本,用于生成测试数据和比对结果。doc/:简要说明文档,包含参数配置和移植指南。
核心解码模块是参数化的,移植到新平台只需要改顶层参数和约束文件,核心逻辑不用动。
9.2 移植到不同FPGA平台的注意事项
移植时要注意几个平台差异点。BRAM的初始化方式不同,Xilinx用COE文件,Altera用MIF,安路和高云各有各的格式。我的工程里把初始化数据做成了通用的hex格式,移植时用平台工具转一下。
DSP硬核的例化方式也不同,Xilinx用DSP48,Altera用DSP Block,其他平台可能没有硬核乘法器。如果没有DSP,乘法器会用LUT实现,资源开销大但功能一样。
时钟管理模块(PLL/MMCM)的例化方式各平台不同,我的工程里把时钟管理单独放在一个文件里,移植时只改这个文件。
9.3 常见问题的快速定位方法
用这套工程时,如果遇到问题,可以按这个顺序排查:
- 仿真不过:先检查测试数据是否正确,用Python脚本重新生成一遍。然后看波形,定位到出错的具体模块。
- 综合报错:多半是参数配置不合法,比如位深和色彩类型组合非法。检查顶层参数。
- 时序不收敛:先看关键路径报告,通常是哈夫曼解码或滤波重建。尝试降低时钟频率或加流水线。
- 上板无输出:先检查时钟和复位,再用测试图案验证显示链路,最后切到解码输出。
这套排查流程是我踩了无数坑之后总结的,能覆盖90%以上的问题。剩下的10%通常是平台相关的怪问题,那就只能具体分析了。
10. 关于这套工程后续可以怎么玩
这套解码器的核心逻辑是通用的,但外围可以玩出很多花样。我自己后续做了几个扩展,供你参考。
一个是多路解码并行,把解码器例化多份,同时解多张图,适合多相机同步采集的场景。代价是资源翻倍,但延迟不变。
另一个是解码加编码,解码后接一个PNG编码器,实现图像的无损转码。这个在图像存档场景有用,比如把一批图统一转成特定参数。
还有一个是部分解码,只解图像的某个区域,适合大图预览。这个需要改IDAT的解压逻辑,跳过不需要的行,实现起来有点绕但可行。
最后说个实际体会:纯Verilog做PNG解码,最难的不是某个模块的实现,而是整体数据流的调度。解压、滤波、色彩转换这几级之间的握手和背压,如果设计不好,要么死锁要么丢数据。我的建议是先把数据流图画清楚,每一级的输入输出速率算明白,再动手写代码。这样能省掉大量调试时间。