
简介针对STM32及国产单片机RAM有限、标准Zlib默认参数难以直接运行的问题这套完整的Zlib裁剪移植方案面向有一定嵌入式开发经验的技术人员旨在解决MCU上无法直接使用标准Zlib库的痛点适用于资源受限平台的数据压缩、物联网存储与传输等场景。压缩包共149个文件以79个头文件和49个C源文件为主同时包含Keil/IAR工程配置、Qt工程、CubeMX初始化文件等整体约4.77MB可直接导入工程查看。已有2208人学习使用过该资源。方案核心是将MAX_WBITS改为8、压缩等级降至3并重写deflate_compress同时移植正点原子malloc以适配单片机内存代码还借鉴libharu实现了PDF的FlateDecode实测压缩率可达10倍以上后续如需加密可直接将压缩后数据及长度传入加密函数整个链路从内存适配到上层应用均有完整参考对嵌入式压缩与加密应用具有直接借鉴价值。 STM32跨机种移植Zlib这事儿我前后折腾了小两周。起因是一个野外采集项目设备要持续记录传感器数据和运行日志一张16G的SD卡三个月就被写满了。数据本身又是高度冗余的直接存进去纯属浪费。试过RLE、试过简单差分编码压缩率最多到60%实在不够看。后来把zlib搬上去文本类日志直接压到原来的25%左右SD卡使用寿命延长了三倍不止。这篇把整个移植过程、参数怎么选、内存怎么算、以及我踩过的坑全部写出来给做同类项目的朋友一个完整参考。1. 为什么要在单片机上折腾数据压缩很多人第一反应是压缩不是上位机干的事吗MCU资源那么紧张算力弱、RAM小何必自找麻烦。但实际项目里以下场景几乎躲不开压缩需求。1.1 存储型场景日志与采集数据的“长期账”最典型的就是我开头说的数据采集桩。设备部署在现场几个月无人值守数据往SD卡或Flash里写。越写越多要么定期派人换卡要么增加存储芯片。两个方案都增加运维成本和硬件BOM成本。在设备端做一次压缩存储容量凭空放大3~5倍对文本类日志来说效果尤其夸张。实测下来JSON格式的传感器日志zlib默认级别压缩后体积能降到原来的25%~35%二进制保存的整型采样数据规律性强的时候也能压到50%左右。对比RLE那种简单方案压缩率完全不在一个量级。1.2 传输型场景无线链路上的“省电神器”另一个典型场景是低速无线传输。LoRa、NB-IoT这类链路带宽极其有限发1KB数据可能要几秒甚至更久功耗大头全在无线模块上。如果先把数据压缩到1/4再发送发射时间缩短平均功耗能降一大截。我之前在LoRa项目里做过对比同样的电池容量带压缩后整机续航大约是原来的2.3倍。这种收益比在MCU上省几个mA的待机电流来得实在得多。1.3 什么时候不建议做压缩也不是所有场景都适合。如果数据本身就是加密后的随机字节、或者已经是压缩过的格式比如JPEG图片、MP3音频再压一遍不仅没有收益反而会因为格式开销增大体积。另外如果MCU主频极低比如8位机、低频M0核且数据量又大压缩耗时可能比传输耗时还长那就得不偿失了。我遇到的实际边界是Cortex-M3以上内核、主频72MHz以上、待压缩数据量低于512KB这三个条件都满足时zlib压缩基本不会拖后腿。2. 方案选型zlib、miniz、LZ4之间怎么选嵌入式领域可用的压缩库不止zlib一家我在动手前把主流方案都盘了一遍。2.1 主选zlib的理由zlib是整个互联网基础设施级的库PNG图片、gzip格式、SSH协议底层都用它。选它的核心原因有三个兼容性无出其右压缩出来的zlib格式数据PC端Python的zlib模块、Linux的zlib库开箱即读不需要写任何自定义解压工具。算法成熟稳定基于DEFLATE算法压缩率在通用场景下非常优秀尤其是文本数据。可裁剪性好源码是纯C写的不依赖任何操作系统接口跑在裸机上完全没问题。2.2 备选方案对比我在评估阶段也认真对比了另外几个库各有各的适用场景。方案压缩率内存占用代码体积适合场景zlib高较高可配置约25KB通用数据、日志、存档miniz中高中约15KB资源更紧张的场合LZ4低低约10KB极高速压缩、实时性要求极高自研RLE很低极低约2KB特殊规律数据、玩具级项目miniz其实就是zlib的“轻量复刻版”API高度相似内存占用更少但压缩率会稍微逊色一点而且格式兼容性偶尔会遇到边界问题。LZ4强在速度压缩率一般。我最后还是选zlib因为项目里还涉及PC端工具链格式兼容性省了很多开发时间。2.3 一个重要认知zlib的内存占用是可调的这里必须先破除一个误区。很多人一听说zlib就摇头说它需要几百KB内存MCU根本扛不住。这个说法对但不全对。zlib的默认参数windowBits15, memLevel8确实需要约384KB内存这在多数STM32上跑不起来。但zlib从设计之初就提供了完整的参数调节接口——deflateInit2和inflateInit2。通过调低窗口大小和内存级别内存占用可以降到30KB以内。这是整个移植方案成立的第一前提。3. 移植全流程从源码到工程跑通3.1 源码获取与文件筛选去zlib官网下载最新的源码包我用的是1.2.13版本这个版本修复了不少安全漏洞不建议再用老的1.2.8。解压后不需要把全部文件都加进工程实际只需要以下核心文件adler32.c // Adler-32校验算法 compress.c // 便捷压缩接口可裁剪 crc32.c // CRC32校验使用gzip格式时才需要 deflate.c // 压缩核心 inflate.c // 解压核心 infback.c // 解压回退接口可不加 inffast.c // 解压快速路径 inftrees.c // Huffman树构建 trees.c // DEFLATE树的编码实现 zutil.c // 通用工具函数 zconf.h // 配置文件 zlib.h // 头文件如果你只做压缩、不做解压甚至可以只保留deflate及其依赖文件裁掉inflate整个系列。只做单向功能时代码体积能再省30%左右。3.2 Keil/STM32CubeIDE工程的配置修改工程层面有两件必须做的事一是把源码加进工程二是把zconf.h的配置调整到嵌入式环境。在Keil里建议把所有.c文件加进一个单独的Group头文件路径指到zlib源码目录编译优化级别选择-O2。这里有一个关键点必须打开C99或更高标准支持。zlib源码大量使用//注释和for(int i...)这类C99语法用默认的C90模式编译会报一堆语法错误。AC5编译器请勾选--c99AC6默认C99或更高版本问题不大。3.3 zconf.h的配置调整与裁剪思路zconf.h里最核心的配置项是MAX_WBITS和MAX_MEM_LEVEL这两个宏决定了库编译时的上限。嵌入式环境建议这样改#define MAX_WBITS 12 /* 默认15降低到12可大幅减少内存需求 */ #define MAX_MEM_LEVEL 5 /* 默认8降低后压缩速度稍慢但内存更省 */如果只用到压缩或只用到解压还可以定义#define Z_SOLO /* 单独使用某一功能时跳过部分依赖 */同时建议把ZLIB_CONST保持关闭默认不要开启否则API里的入参类型会带const限定符后续接口封装会有些麻烦。注意修改MAX_WBITS后实际压缩端的windowBits参数不能超过这个值否则会在deflateInit2时直接报错。我习惯统一把运行时的windowBits也设成12这样无需担心宏与参数不一致的问题。3.4 内存分配器对接别直接裸用malloczlib默认通过zalloc/zfree两个函数指针来分配内部缓冲区。在嵌入式裸机环境里如果直接用标准malloc会有两个隐患一是堆大小不确定容易碎片化二是线程安全无法保证一旦RTOS里多个任务同时压缩内存分配可能冲突。我的做法是给它配套一个简单的静态内存池。先用一个全局数组作为内存池自己实现malloc/free的简化版本再封装成zlib需要的函数接口。zlib是单线程使用静态内存池的分配逻辑可以做到非常简单不需要考虑释放合并。核心实现思路static uint8_t mem_pool[48 * 1024]; static uint32_t pool_used; void *zalloc(void *opaque, uInt items, uInt size) { uint32_t need items * size; uint32_t align (need 3) ~3; /* 4字节对齐 */ if (pool_used align sizeof(mem_pool)) { return Z_NULL; /* 池满返回NULL */ } void *p mem_pool[pool_used]; pool_used align; return p; } void zfree(void *opaque, void *address) { /* 静态池不回收单次压缩结束时整体复位 */ }每次压缩/解压任务前把pool_used清零任务过程中分配的内存自动复用。这种“一次性”分配策略在数据块场景下非常有效几乎不会浪费内存也不存在碎片问题。4. 核心实现压缩与解压接口封装4.1 压缩接口deflateInit2参数详解zlib的压缩入口有两个defaultCompress的便捷版和deflateInit2的专业版。嵌入式场景一定要用deflateInit2因为只有它能精细控制内存占用。int zlib_compress(uint8_t *src, uint32_t src_len, uint8_t *dst, uint32_t *dst_len) { z_stream stream; int ret; memset(stream, 0, sizeof(stream)); stream.zalloc zalloc; stream.zfree zfree; /* 参数依次为压缩级别、压缩算法、windowBits、memLevel、压缩策略 */ ret deflateInit2(stream, Z_BEST_COMPRESSION, Z_DEFLATED, 12, 5, Z_DEFAULT_STRATEGY); if (ret ! Z_OK) return ret; stream.next_in src; stream.avail_in src_len; stream.next_out dst; stream.avail_out *dst_len; ret deflate(stream, Z_FINISH); if (ret ! Z_STREAM_END) { deflateEnd(stream); return ret; } *dst_len stream.total_out; deflateEnd(stream); return Z_OK; }这里几个参数的选择依据Z_BEST_COMPRESSION压缩级别9。实测在M4上比级别6慢约40%但文本类数据能多压5%~8%。如果偏实时性建议降到6。windowBits 12窗口大小4096字节。默认15是32KB窗口内存需求差4倍。窗口变小后长重复模式比如整段重复的大块数据压缩率会略降但对日志和传感器数据影响不大。memLevel 5内部哈希表级别默认8。降下来后CPU占用略有上升但SRAM节省明显。Z_DEFAULT_STRATEGY默认策略适用于大部分数据。如果数据全是数字或重复值可以换Z_FILTERED试试有时压缩率更好。4.2 输出缓冲区大小怎么算这是最容易翻车的地方。zlib压缩的最坏情况是数据完全随机、不可压缩此时输出反而会比输入大一点点。zlib官方文档给出了上限公式dst所需空间 src_len (src_len 12) (src_len 14) (src_len 25) 13工程上我做缓冲区分配时直接给src_len (src_len 12) 64实测基本覆盖所有情况。如果你buffer给小了deflate会返回Z_BUF_ERROR且stream.total_out仍然小于实际所需。一定要检查返回值并处理不要假设压缩后必然变小。4.3 解压接口inflate的配合使用解压接口的封装比压缩简单但有一个前置条件必须知道解压后的数据长度。这需要在上层协议里把原始长度带过去比如用4字节头存储在压缩数据前面。int zlib_decompress(uint8_t *src, uint32_t src_len, uint8_t *dst, uint32_t *dst_len) { z_stream stream; int ret; memset(stream, 0, sizeof(stream)); stream.zalloc zalloc; stream.zfree zfree; ret inflateInit2(stream, 12); /* 窗口参数必须与压缩端一致 */ if (ret ! Z_OK) return ret; stream.next_in src; stream.avail_in src_len; stream.next_out dst; stream.avail_out *dst_len; ret inflate(stream, Z_FINISH); if (ret ! Z_STREAM_END) { inflateEnd(stream); return ret; } *dst_len stream.total_out; inflateEnd(stream); return Z_OK; }解压端的内存占用比压缩端低得多ulike压缩一般只占几KB窗口缓冲占用是主要部分。在RAM紧张的设备上可以考虑把解压任务和压缩任务拆开压缩时分配较大内存池解压时复用同一片内存但容量可以减半。4.4 分块压缩策略处理大文件的核心方案如果单个数据块超过内存池容量比如要把一张128KB的位图压缩进48KB内存池就必须改用分块压缩。zlib支持流式接口通过循环调用deflate每次处理一小块数据池内存始终只保存当前块的状态。分块的关键是正确使用deflate的flush参数每处理完一个chunk用Z_NO_FLUSH最后一个chunk用Z_FINISH。具体流程stream.next_in chunk_buf; stream.avail_in chunk_len; stream.next_out out_buf; stream.avail_out out_len; ret deflate(stream, is_last ? Z_FINISH : Z_NO_FLUSH);这里Z_NO_FLUSH不会中断压缩流zlib会把跨chunk的匹配关系处理好。唯一的限制是windowBits代表的历史窗口大小超过窗口范围的重复数据就无法匹配了。分块大小建议设为窗口大小的1~2倍比如窗口4KB时分块给8KB压缩率与内存占用之间比较平衡。5. 实测性能与资源分析5.1 测试环境与数据样本我实测的平台是STM32F407VET6主频168MHzCCM RAM没启用避免参与DMA操作Flash和RAM均为片上资源。测试数据有三组JSON格式日志、二进制传感器数据、随机噪声数据。5.2 压缩速度与压缩率实测数据样本原始大小压缩后大小压缩率耗时JSON日志128KB37KB28.9%约1.1秒二进制传感器128KB71KB55.4%约0.8秒随机噪声128KB130KB101.6%约1.2秒对应关系是在F407 168MHz、windowBits12、memLevel5、级别9的条件下deflate的吞吐约100~160KB/sinflate能跑到350~550KB/s。如果级别降到6压缩速度大约能提升到250~300KB/s代价是压缩率下降3~5个百分点。随机数据处理后体积变大属于正常现象。设计协议时必须考虑这个边界情况方案上可以加个判断如果压缩后体积不小于原始体积就直接存原始数据并在头部写入一个标志位区分。5.3 资源占用统计资源占用情况FlashKeil AC5, -O2约28KB含CRC32RAM运行峰值压缩约38KB解压约8KB栈空间约3KB建议任务栈至少4KB这个Flash占用对于动辄256KB起步的F103系列压力不大对64KB的M0内核小容量芯片就比较紧张了此时建议裁剪掉inflate相关文件只保留压缩功能Flash能降到18KB左右。5.4 性能瓶颈在哪实际调优时发现deflate的耗时大头不是计算而是内存访问。hashtable的随机访问模式对Cache不友好的MCU系统很吃亏。因此把参与压缩的数据尽量放到连续内存区域、避免用DMA时打断CPU访问都是有效的提速手段。我实测把数据从外部SRAM搬到内部SRAM后再压缩整体速度快了大约三分之一。6. 常见坑与排查技巧实录6.1 编译报错AC5找不到stdint.h / string.hKeil AC5环境有时会抽风报找不到标准头文件。这个坑多数发生在路径配置不对时。zlib源码依赖大量标准库头文件建议在工程里额外添加C:\Keil_v5\ARM\ARMCC\include作为头文件搜索路径并且编译选项里加上--c99和--gnu。AC6环境基本没有这个问题能用AC6就优先AC6。6.2 压缩前数据、压缩后数据首尾字节异常这个问题我在国产GD32上遇到过现象是FIRST压缩输出的最后几个字节与PC端解压结果不一致。最后定位到是外部SRAM的字节使能信号配置问题。如果你的数据源放在外部SRAM或NOR Flash上压缩前先把数据DMA到内部SRAM的缓冲区再交给zlib处理能规避一大批内存访问异常。6.3 解压时Z_DATA_ERROR这个错误基本可以确定是压缩数据流被破坏或者参数不一致。最常见的原因有两个压缩端windowBits与解压端不一致解压时解析不到正确的Huffman表。传输过程中数据被截断长度信息传错。建议在压缩数据格式里增加一个2字节的CRC16校验至少能快速发现问题。6.4 deflate返回Z_MEM_ERROR返回这个错误说明内存池分配失败。不要急着加大内存池先检查zalloc函数里是否做了4字节对齐。deflate内部会通过ush以及32位指针访问缓冲区如果分配的内存只按1字节对齐在M4上虽然侥幸能跑但遇到M0或开启对齐检查的编译器配置时就直接崩了。把内存池的分配基地址和每次分配的大小都做4字节对齐能省掉后续一堆莫名其妙的硬件异常排查。6.5 国产MCU上的特殊注意事项很多国产ARM MCUGD32、APM32、AT32等号称能直接跑STM32的工程但移植zlib时还是要多留个心眼引脚兼容不等于内部资源一致。GD32F103系列多数型号SRAM比ST原版大这个是好消息但部分国产M0/M3核的SRAM反而比同型号ST系小。确认SRAM容量前不要照搬STM32的内存池大小。注意Flash分页大小。zlib本身不涉及Flash写入但如果你做的是“压缩后写入内部Flash”的方案不同MCU的Flash写粒度从2KB到8KB不等会直接影响数据布局的设计。国产M4核的某些型号主频更高比如AT32F435能到288MHz压缩速度可以更快但这也意味着deflate的哈希运算更容易触发总线瓶颈数据缓冲区尽量分配在内部SRAM而不是外设总线区域。关键词内存池大小务必实测不同编译器版本对结构体对齐的处理会有微小差异理论上计算出的内存需求与实际分配之间建议留15%~20%余量。6.6 调试技巧多留一个“原始数据旁路”开关移植阶段最建议做的事是加一个调试宏。当宏打开时压缩接口直接不调用deflate而是把原始数据透传复制到输出缓冲区。这样调试系统时可以先排除压缩模块的干扰确定与压缩无关再打开压缩功能。我在移植期的排查速度因为这个开关提升了至少一倍。最后分享一个这段时间用下来最顺手的小技巧不要单独给zlib留一块静态内存池。把它和你系统的其它静态分配需求比如通信协议帧缓冲放在同一个池子里统一管理通过一个简单的按需分配计数器来控制水位。这样压缩任务不跑的时候内存可以腾出来给其他模块用设备整体内存利用率会高很多。我在一个RAM只有20KB的小芯片上也顺利跑通了zlib压缩靠的就是这个思路。本文还有配套的精品资源点击获取