简介此压缩包提供 zlib 1.2.2 完整开源库面向嵌入式、单片机及硬件编程场景适用于 C/C 开发者实现轻量级数据压缩、解压与流式传输。库基于 DEFLATE 算法LZ77 与霍夫曼编码结合提供跨平台 C API、CRC32/Adler-32 完整性校验及内存缓冲分块处理可显著优化存储占用、降低窄带环境下的传输耗时并简化固件升级流程。包内共 176 个文件以 c/h 核心源码和 makefile、vcproj、configure 等构建脚本为主还包含 readme、示例程序、平台适配文件及多套编译工程结构清晰便于移植到不同 MCU 开发环境。压缩包仅 506KB轻量易部署。已有 328 人学习下载适合需要在资源受限设备上实现可靠压缩功能的嵌入式开发人员参考使用。1. 内存只有 32 KB 的单片机也能剪出一份可用的 zlib一枚 Cortex-M 核心的单片机RAM 往往只有 32 KB 甚至更少大部分读者听到 zlib 第一反应是“PC 端库”。zlib 1.2.2 的 DEFLATE 实现并不依赖大内存真正占内存的是 LZ77 窗口和哈希表这两块可以通过 windowBits 和 memLevel 压到几 KB。我用它在 STM32 这类板子上做过两件常见事日志压缩和固件解压。压缩 1 KB 左右的传感器记录CPU 占用不高Flash 里的存储体积却能明显降下来。这个 zip 包里还混着一组 Ada 绑定zlib.adb、zlib-thin.adb但压缩核心永远是 C API嵌入式 C/C 开发者只需要盯住 deflate/inflate 这组接口。下文先拆内存边界再讲裁剪和集成最后给一个可以抄的流式解压状态机。2. zlib 1.2.2 的 API 分层compress2 与流式 deflate 的选型边界zlib 对外有两套完全不同的调用方式。compress/uncompress是黑盒式你给一块内存它返回一块内存。deflate/inflate是状态机式通过z_stream控制输入和输出游标可以分几百次把一个大对象处理完。单片机上真正值得优先讨论的不是压缩算法本身而是你愿不愿意让库在内部调用 malloc。2.1 compress2适合小数据块但要预留出整个输出缓冲compress2是一步到位的简化封装。下面这段代码把日志片段压缩到目标数组里#include zlib.h int compress_log(uint8_t *dst, size_t dst_cap, const uint8_t *src, size_t src_len) { uLongf out_len dst_cap; int ret compress2(dst, out_len, src, src_len, Z_BEST_SPEED); if (ret ! Z_OK) return -1; return (int)out_len; }out_len既是输入参数告诉 zlib 输出缓冲有多大也是输出参数返回实际压缩后的大小。Z_BEST_SPEED是级别 1压缩率最低但最省 CPU想压得更小再用Z_BEST_COMPRESSION级别 9单片机一般不建议一上来就用 9。compress2内部会调用deflateInit如果 zlib 编译时开着默认malloc每次压缩都会动态分配一块内部状态。对 RAM 紧的硬件这个行为很危险内存碎片、分配失败都没法在表面看到。如果单条日志只有几百字节并且你在启动时就把目标缓冲按compressBound(src_len)预留好用compress2是没问题的。真正要避免的是大文件一次性塞进去这时输出缓冲会占到原始数据加窗口状态两倍以上的空间。2.2 deflateInit2用 windowBits 和 memLevel 换内存一旦想控内存就要切换到deflate流式接口。初始化时两个参数决定了大部分内存消耗z_stream strm {0}; strm.zalloc Z_NULL; /* 裸机下换成自研池化分配器 */ strm.zfree Z_NULL; int ret deflateInit2(strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); if (ret ! Z_OK) return -1;windowBits传入负数表示输出裸的 DEFLATE 流不包 zlib 头适合自制固件升级容器。memLevel范围是 1 到 9控制内部哈希表大小默认 8在 32 KB RAM 的板子上我一般会先试 5压缩率大约只掉几个百分点内存能省下一大截。参数常用取值嵌入式影响windowBits15、-15、31151615 有 2 字节 zlib 头-15 是裸流31 输出 gzipmemLevel1~9越小哈希表越稀疏内存占用降低压缩率略降strategyZ_DEFAULT / Z_FILTERED / Z_HUFFMAN_ONLY传感器数据用 Z_FILTERED 可能比默认更好注意deflateInit2中的zalloc/zfree如果不设置内部会用zcalloc调 C 库malloc。裸机上请务必替换否则第一次压缩就卡死。2.3 inflateInit2 区分 zlib、gzip 与 raw 流解压端同样要指定格式。windowBits为正数 15 时inflate会解析 zlib 头并校验 Adler-32传入-MAX_WBITS则跳过头部把后面的字节当作裸 DEFLATE。PNG 图片内部用的是 zlib 包格式.gz文件则用 gzip 包格式如果你在 OTA 升级时自己构造固件容器裸 DEFLATE 能省下包头同时把完整性校验交给你自己定义的包格式。下面这张表可以直接当速查包格式inflateInit2 参数适用场景zlib 头MAX_WBITS15PNG、HTTP 压缩传输gzipMAX_WBITS 1631读取 .gz 文件raw DEFLATE-MAX_WBITS自制固件、私有协议实际调试时最容易踩的坑是格式不匹配用 -15 初始化解压器却喂给带 zlib 头的文件inflate会返回 Z_DATA_ERROR而且是在消费掉两个头字节之后才报错。所以日志里看到 Z_DATA_ERROR先检查 data 开头是 0x78 还是空。3. 从 zip 包走到链接器zlib 1.2.2 在嵌入式工程里的裁剪与 C 桥接拿到 zip 包后第一眼看到的文件除去 README、man 手册还有一串.adb文件。这是 zlib 的 Ada 绑定不是给 C/C 用的。把它们放在一边真正需要进入编译的是 C 实现。3.1 先分清 zip 里的 C 源文件和 Ada 绑定zlib.adb、zlib-thin.adb、zlib-streams.adb这些文件是另一个世界的东西如果项目只用 C/C别把它们加进构建。Ada 绑定里有一件事值得参考zlib-thin.adb直接映射了z_stream的字段和deflate/inflate函数地址能帮你确认当前头文件版本里每个成员的确切位置和尺寸。C 侧要裁剪的就是下面这几类文件源文件作用只用解压时可裁否adler32.cAdler-32 校验保留deflate 输出要用crc32.cCRC-32 校验不用 gzip 格式可裁deflate.c / trees.c压缩端只需要解压时可裁inflate.c / inftrees.c / inffast.c解压端只需要压缩时可裁zutil.c内部内存与错误处理必须保留在固件 OTA 的设备上我通常只保留 inflate 组在需要记录日志的设备上只保留 deflate 组。配合链接器--gc-sectionsFlash 占用可以压到十几 KB 级别。3.2 用 CMake 生成最小编译单元下面这份 CMake 片段把压缩和解压都编进去但是靠编译器优化把没引用的函数丢掉。如果你确定只要解压直接从列表里删掉deflate.c和trees.cadd_library(zlib_core STATIC adler32.c crc32.c zutil.c deflate.c trees.c inflate.c inftrees.c inffast.c) target_include_directories(zlib_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_options(zlib_core PRIVATE -Os -ffunction-sections -fdata-sections) target_compile_definitions(zlib_core PUBLIC Z_PREFIX)加上Z_PREFIX后zlib 的公开函数名会被统一加上z_前缀比如z_deflate、z_inflate。这能避免 zlib 和 TCP/IP 协议栈、加密库里的同名compress冲突。注意这是个 PUBLIC 宏凡是想调用 zlib API 的源码都必须带上该定义。如果你在 VS Code 里用 C/C 插件调试记得把它同步进c_cpp_properties.json的 defines 列表否则 IntelliSense 和交叉编译器看到的头文件内容不一致。3.3 C 工程接 zlib 的几个身份问题zlib 头文件本身有extern C保护但老版本或自定义裁剪头不一定带。保险做法是在引用处再包一层extern C { #include zlib.h }另一个身份问题是内存所有权。C 里new出来的对象和 zlib 内部从malloc拿到的指针不能混在一起释放。裸机项目通常没有堆直接给z_stream挂一个静态内存池static uint8_t pool[8 * 1024]; static uint32_t pool_pos; void *pool_zalloc(void *opaque, uInt items, uInt size) { uint32_t need items * size; need (need 3u) ~3u; /* 4 字节对齐 */ if (pool_pos need sizeof(pool)) return Z_NULL; void *p pool[pool_pos]; pool_pos need; return p; }zalloc回调里的opaque可以用来传递池控制块不能在回调里递归调用 zlib API否则同一把内存很快会被覆盖。检查返回值为Z_NULL时应直接放弃本轮压缩而不是让 zlib 继续推进。4. 在 MCU 上做流式固件解压inflate 状态机、落盘和格式选择实际嵌入式场景很少是一次性拿到整个压缩文件。OTA 升级包从网络分片到达日志也是边产生边压缩。下面这套状态机是我在 RAM 只有 8 KB 的板子上用过的结构只要求驱动把数据按块喂进来。4.1 从数据流中一点点解出固件先定义上下文把z_stream和三块缓冲摊在结构体里typedef struct { z_stream strm; uint8_t in[256]; uint8_t out[256]; int done; } inf_ctx; int inf_feed(inf_ctx *ctx, const uint8_t *seg, size_t len) { ctx-strm.next_in (Bytef *)seg; ctx-strm.avail_in (uInt)len; while (ctx-strm.avail_in 0 !ctx-done) { ctx-strm.next_out ctx-out; ctx-strm.avail_out sizeof(ctx-out); int ret inflate(ctx-strm, Z_NO_FLUSH); if (ret ! Z_OK ret ! Z_STREAM_END) return -1; size_t have sizeof(ctx-out) - ctx-strm.avail_out; if (have) flash_write(ctx-out, have); if (ret Z_STREAM_END) { ctx-done 1; break; } } return ctx-done ? 1 : 0; }inflate消费avail_in并产生avail_out。每次循环重新铺设输出游标have就是本次调用真正解出来的字节数必须立刻搬走否则下一轮会覆盖。返回Z_STREAM_END时说明 DEFLATE 流已经结束avail_in里可能还剩几个后续包的字节驱动层要把这些余数交还给网络缓冲。初始化时用inflateInit2(ctx-strm, -MAX_WBITS)。4.2 压缩日志写 FlashZ_SYNC_FLUSH 是最好的断点日志和固件包不同写入过程中设备随时可能断电。DEFLATE 的 LZ77 窗口依赖前文如果不主动打断丢掉尾巴会让后续数据无法接续。常见做法是攒够一块数据然后带Z_SYNC_FLUSH压一帧int log_append(log_ctx *log, const uint8_t *line, size_t len) { log-strm.next_in (Bytef *)line; log-strm.avail_in (uInt)len; while (log-strm.avail_in 0) { log-strm.next_out log-block; log-strm.avail_out sizeof(log-block); deflate(log-strm, Z_SYNC_FLUSH); flash_append(log-block, sizeof(log-block) - log-strm.avail_out); } return 0; }Z_SYNC_FLUSH会把当前 LZ77 缓冲区对齐到字节边界同时不结束压缩流即使设备在下一帧前掉电已经落盘的那些帧也能从最近对齐点继续追加而不会被后面未写的数据拖累。下表是deflate的几种 flush 模式MCU 上最常用的是中间两行flush 模式解压侧看到的行为典型用途Z_NO_FLUSH攒着不解出批量压缩一整块文件Z_SYNC_FLUSH立刻能在输出侧读到边界日志落盘、分包发送Z_FULL_FLUSH清空窗口字典强制重建匹配跳播、重传Z_FINISH结束并写 Adler-32 尾部文件收尾注意每次Z_SYNC_FLUSH都会破坏一点压缩率。日志行很短时不要一行一 flush否则压缩后可能比原文更长我一般攒 1~4 KB 再 flush。4.3 zlib 头、gzip 头还是 raw DEFLATE同样一段压缩数据第一字节是否带 0x78 或 0x1f 决定了解压器该用什么参数。嵌入式通信中最好统一约定。格式windowBits优点注意zlib15有 Adler-32 完整性开头 2 字节长度计算要少 6 字节gzip31与 PC 端 .gz 兼容头部复杂裁剪后要保留 crc32.craw DEFLATE-15包结构最简洁校验必须自己加到外层协议如果升级包从某个网页服务器下拉服务器端可能直接返回 gzip这时解压端要用 31。如果是设备与设备之间点对点传raw 再套一层自己的 CRC32 更直观。5. 上线前最后检查inflateSync、deflateParams 与符号管理最后分享三个在真实板子上用得到的小工具。5.1 inflateSync 只恢复位置不恢复完整性inflateSync会在受损的 DEFLATE 流里查找下一个可能的同步点跳过损坏字节后继续解压。听上去很美但它不能恢复前面已经丢掉的数据。在无线升级场景里我更愿意在每个分包上带序号和 CRC32哪一包校验失败就重传哪一包而不是依赖inflateSync把流救回来。只有在 TF 卡 / Flash 里读到一段已经损坏、又不想放弃剩余帧的数据时inflateSync才是最后的抢救手段。5.2 用 deflateParams 动态调整压缩档位压缩日志时CPU 占用率在实时任务高发期可能突然飙升。deflateParams允许在两个deflate调用之间修改压缩级别和策略if (cpu_load() 30) { deflateParams(log-strm, Z_DEFAULT_COMPRESSION, Z_DEFAULT_STRATEGY); } else { deflateParams(log-strm, Z_BEST_SPEED, Z_DEFAULT_STRATEGY); }这个函数必须在一次deflate返回后、下一次喂数据前调用在deflate尚未返回时调用会得到 Z_STREAM_ERROR。它能让“CPU 空闲时压得更紧任务繁忙时快速落盘”属于不改变缓冲区就能平滑换挡的技巧。注意它只能改 level 和 strategy改不了 windowBits 和 memLevel。5.3 用 nm 检查符号和 Flash 占用裁剪完成后用交叉编译器的nm或size验证链接结果arm-none-eabi-size bld/zlib_core.a arm-none-eabi-nm --size-sort --radixd bld/zlib_core.a | tail -20如果你看到inflate_fast、deflate_slow这些符号还在说明对应模块确实进了链接脚本。如果只想保留解压在源码里把deflate入口全部注释掉后重新编译观察 Flash 占用是否降下来。烧板前先在宿主机上用同样的windowBits和memLevel参数组合跑一遍确认 CRC/Adler-32 结果能对上再信任这段状态机。这比在目标板上单步断点来得快得多。本文还有配套的精品资源点击获取