uCompression:面向MCU的超轻量RLE图像压缩库

张开发
2026/5/23 20:48:20 15 分钟阅读
uCompression:面向MCU的超轻量RLE图像压缩库
1. uCompression 库概述uCompression 是一款专为资源受限型微控制器设计的轻量级、高速数据压缩/解压缩库其核心目标是解决嵌入式图形显示系统中 Flash 存储空间紧张这一典型工程瓶颈。在使用单色monochrome显示屏如 OLED、LCD 段码屏、LED 点阵屏的低端 MCU 平台如 ATtiny85、ATmega328PB —— 即 Arduino UNO 的主控芯片上图像资源尤其是位图字模、图标、启动画面往往占据大量 Flash 空间。以一个 128×64 像素的单色 OLED 屏为例一帧完整图像需占用 1024 字节128 × 64 ÷ 8若需存储 10 张静态界面图即消耗 10 KB Flash —— 这已接近 ATmega328PB 总 Flash 容量32 KB的三分之一。uCompression 通过实现高度优化的游程编码Run-Length Encoding, RLE算法在不引入动态内存分配、不依赖标准 C 库、不使用堆栈递归的前提下达成“代码尺寸极小 执行速度极快”的双重硬性指标。该库采用纯 C 实现但完全规避了面向对象的运行时开销所有接口均定义为static成员函数无需实例化对象无构造/析构开销无虚函数表无隐藏的this指针传递。整个库编译后仅生成数个紧凑的函数体经 GCC-AVR 5.4.0 编译并启用-Os优化尺寸后RLE 压缩与解压缩核心函数合计 ROM 占用低于 320 字节ATmega328P 平台实测值RAM 零静态占用符合裸机Bare-metal及实时操作系统如 FreeRTOS环境下的严苛资源约束。其设计哲学可概括为三点确定性所有函数执行时间可静态分析无分支预测失败惩罚无缓存抖动影响零依赖仅依赖stdint.h和 AVR 特定的pgmspace.h用于PROGMEM访问不调用malloc、memcpy或任何 libc 函数可验证性算法逻辑线性、无状态便于在硬件仿真器如 SimAVR或 ICE 调试器下逐指令验证行为。2. RLE 压缩原理与 uCompression 实现机制2.1 游程编码RLE基础原理RLE 是最古老且最高效的无损压缩算法之一其核心思想是将连续重复的相同字节序列称为“游程”Run替换为一个“计数值”对。例如原始数据流0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x007 字节可编码为0x04, 0x00, 0x02, 0xFF, 0x01, 0x006 字节压缩率约 14%。在单色图像场景中由于大量连续的“黑”0x00或“白”0xFF像素区域普遍存在如文字背景、边框、大面积填充RLE 能取得远超理论平均值的压缩比。实测表明对 128×64 单色 Logo 图像uCompression 的平均压缩比可达 1:2.3即压缩后体积为原图 43%对纯文本界面高比例空白区域压缩比可高达 1:5。2.2 uCompression 的 RLE 编码格式uCompression 采用自定义的紧凑 RLE 格式兼顾解码速度与编码密度。其编码规则如下编码类型字节模式含义示例原始数据编码后短游程Short Run0b0xxxxxxx最高位为 0后续 1 字节为重复值低 7 位表示重复次数1–1270x55重复 5 次 →0b00000101, 0x550x05, 0x55长游程Long Run0b1xxxxxxx最高位为 1后续 2 字节为重复值高位在前低 7 位表示重复次数1–1270x1234重复 10 次 →0b10001010, 0x12, 0x340x8A, 0x12, 0x34原始字节流Literal0b10000000固定标记后续 1 字节为原始未压缩字节0xAA, 0xBB→0b10000000, 0xAA, 0b10000000, 0xBB0x80, 0xAA, 0x80, 0xBB关键设计说明无长度字段溢出保护uCompression 显式要求调用者确保chunkSize数据块长度不超过UINT16_MAX65535且在pgm_RLEdecompress256中进一步限定为uint8_t≤255。这避免了 16 位长度变量的边界检查开销将校验责任移交至应用层 —— 这是嵌入式领域“信任调用者”Trust the Caller的经典实践。双字节游程支持虽主要面向单色图像单字节像素但Long Run模式允许压缩任意uint16_t数据为未来扩展至灰度图如 4-bit或小端序字模数据预留接口。Literal 模式最小化通过将0x80作为唯一控制码确保任何原始数据0x00–0x7F和0x81–0xFF均可无歧义地作为字面值出现仅0x80本身需转义编码为0x80, 0x80极大降低转义开销。2.3 压缩函数接口详解uCompression 提供两套压缩入口适配不同数据源// 压缩 RAM 中的数据 uint16_t uCompression::RLEcompress( const uint8_t* uncompressedData, // 输入原始数据起始地址RAM uint8_t* compressedData, // 输出压缩后数据写入地址RAM uint16_t chunkSize // 输入待压缩数据长度字节 ); // 压缩 FlashPROGMEM中的数据需 pgmspace.h 支持 uint16_t uCompression::pgm_RLEcompress( const uint8_t* uncompressedData, // 输入原始数据在 Flash 中的地址PROGMEM uint8_t* compressedData, // 输出压缩后数据写入地址RAM uint16_t chunkSize // 输入待压缩数据长度字节 );参数深度解析uncompressedData指向原始数据的指针。对pgm_RLEcompress此指针必须为PROGMEM地址如myBitmap[0]函数内部使用pgm_read_byte_near()安全读取。compressedData输出缓冲区首地址必须由调用者预先分配足够空间。uCompression 不进行容量检查其最大压缩后尺寸可按 worst-case 公式估算compressedSize ≤ uncompressedSize (uncompressedSize 62) / 63即每 63 字节最多增加 1 字节开销。chunkSize精确指定待处理字节数。非零终止字符串不依赖\0结束符确保二进制数据安全。返回值实际写入compressedData的字节数。若返回值为0表示输入数据为空或发生不可恢复错误如指针非法但库本身不设错误码 —— 符合“无错误检查”设计原则。3. 解压缩函数接口与性能优化策略3.1 解压缩函数接口解压缩提供三组 API覆盖 RAM/Flash 数据源及尺寸优化场景// 从 RAM 解压到 RAM const uint8_t* uCompression::RLEdecompress( const uint8_t* compressedData, // 输入压缩数据起始地址RAM uint8_t* uncompressedData, // 输出解压后数据写入地址RAM uint16_t chunkSize // 输入期望解压后的原始数据长度字节 ); // 从 FlashPROGMEM解压到 RAM const uint8_t* uCompression::pgm_RLEdecompress( const uint8_t* compressedData, // 输入压缩数据在 Flash 中的地址PROGMEM uint8_t* uncompressedData, // 输出解压后数据写入地址RAM uint16_t chunkSize // 输入期望解压后的原始数据长度字节 ); // 优化版chunkSize ≤ 255 时使用更小更快 const uint8_t* uCompression::pgm_RLEdecompress256( const uint8_t* compressedData, // 输入压缩数据在 Flash 中的地址PROGMEM uint8_t* uncompressedData, // 输出解压后数据写入地址RAM uint8_t chunkSize // 输入期望解压后的原始数据长度字节uint8_t );关键差异与选型指南RLEdecompress与pgm_RLEdecompress的chunkSize均为uint16_t适用于任意大小数据块但需承担 16 位减法与比较的额外指令周期。pgm_RLEdecompress256将chunkSize降为uint8_t编译器可将其映射至 AVR 的r24寄存器循环计数直接使用dec r24; brne loop指令比 16 位版本节省 3–4 个时钟周期/循环且代码体积减少约 12 字节。强烈推荐在单色图像如 128×641024 字节 → 分块为 4×256等已知尺寸场景下使用。返回值语义返回uncompressedData的终址即uncompressedData chunkSize而非成功标志。此设计允许链式调用例如uint8_t frameBuffer[1024]; const uint8_t* ptr frameBuffer; ptr uCompression::pgm_RLEdecompress256(icon1_data, ptr, 256); ptr uCompression::pgm_RLEdecompress256(icon2_data, ptr, 256); // ... 继续填充3.2 汇编级性能优化uCompression 的核心竞争力在于其针对 AVR 架构的深度汇编优化。当检测到目标 MCU 为ATmega328,ATmega32U4,ATtiny85时编译器自动链接手写 AVR 汇编版本位于uCompression_asm.S关键优化点包括零等待状态 Flash 访问pgm_RLEdecompress使用lpmLoad Program Memory指令直接读取 Flash配合预取缓冲实现单周期读取。寄存器直通解码解码循环中游程计数器、当前字节值、输出指针全部驻留在 CPU 寄存器r16-r23避免频繁的 SRAM 读写。分支预测友好采用tstbrne替代cpbrlo利用 AVR 的零标志位快速判断游程是否结束。内联展开对Short Run和Literal等高频路径进行完全展开消除循环跳转开销。性能实测数据Arduino Leonardo, ATmega32U4 16MHz数据源解压算法吞吐量代码来源RAMC700–1000 kB/sRLEdecompressFLASHAssembly1400–2000 kB/spgm_RLEdecompressFLASHAssembly (256)~2200 kB/spgm_RLEdecompress256工程启示2000 kB/s 意味着解压一帧 1024 字节图像仅需512 微秒远低于 OLED SSD1306 的典型刷新间隔10 ms使“按需解压即时显示”成为可能彻底规避大容量显存需求。4. 在嵌入式项目中的集成实践4.1 典型应用场景单色 OLED 动态界面以基于 STM32F030F4P6Cortex-M0, 48MHz驱动 SSD1306 OLED 的项目为例展示 uCompression 与 HAL 库的协同#include uCompression.h #include stm32f0xx_hal.h #include ssd1306.h // 假设 OLED 驱动库 // 定义压缩的图标数据存储于 Flash const uint8_t icon_home_compressed[] PROGMEM { 0x80, 0x00, 0x04, 0xFF, 0x01, 0x00, 0x02, 0xFF, /* ... */ }; // 解压缓冲区位于 RAM大小原始图标尺寸 uint8_t icon_home_decompressed[1024]; void display_home_screen(void) { // 1. 从 Flash 解压到 RAM uCompression::pgm_RLEdecompress256( icon_home_compressed, icon_home_decompressed, sizeof(icon_home_decompressed) // 1024 ); // 2. 使用 HAL_SPI_Transmit 发送至 OLED HAL_SPI_Transmit(hspi1, icon_home_decompressed, sizeof(icon_home_decompressed), HAL_MAX_DELAY); }关键工程考量内存布局icon_home_compressed声明为PROGMEM确保链接器将其置于 Flash 区域icon_home_decompressed为 RAM 数组大小严格匹配原始尺寸。时序保障解压耗时 500 μs远低于 SPI 传输 1024 字节所需时间8MHz SPI ≈ 1024 μsCPU 可在解压后立即发起传输无空闲等待。4.2 与 FreeRTOS 的协同设计在多任务环境中需确保解压操作不阻塞高优先级任务。推荐方案为创建专用解压任务并通过队列传递解压请求// FreeRTOS 队列传递解压任务参数 QueueHandle_t xDecompressQueue; typedef struct { const uint8_t* pCompressed; // Flash 地址 uint8_t* pUncompressed; // RAM 地址 uint16_t size; // 原始尺寸 } DecompressJob_t; void vDecompressTask(void *pvParameters) { DecompressJob_t job; for(;;) { if (xQueueReceive(xDecompressQueue, job, portMAX_DELAY) pdTRUE) { // 执行解压无阻塞纯计算 uCompression::pgm_RLEdecompress( job.pCompressed, job.pUncompressed, job.size ); // 解压完成可通知显示任务 xSemaphoreGive(xDisplayReadySemaphore); } } } // 在应用任务中提交解压请求 void request_icon_display(const uint8_t* pIcon, uint8_t* pBuffer, uint16_t size) { DecompressJob_t job {pIcon, pBuffer, size}; xQueueSend(xDecompressQueue, job, 0); }优势解压任务优先级可设为中等避免抢占实时控制任务队列机制天然提供背压防止请求洪泛。5. 使用限制与工程规避策略5.1 核心限制剖析uCompression 明确声明两大限制其根源在于嵌入式资源约束下的设计取舍无错误检查NO ERROR CHECKING表现若compressedData缓冲区过小解压过程将越界写入相邻内存导致变量覆写、栈破坏最终引发 HardFault 或随机复位。根源添加边界检查如if (output_ptr output_end) return NULL;需额外 4–6 字节代码及 2–3 个时钟周期/次检查违背“代码尺寸与速度优先”原则。RLE 算法固有局限随机数据膨胀对熵值高的数据如加密密文、噪声图像RLE 会因Literal模式开销每字节1字节标记导致输出比输入大 100%。worst-case 公式1 byte per 63 bytes即源于此。非普适性RLE 仅对长游程数据有效无法替代 LZ77/LZ78 等字典类算法。5.2 工程级规避方案问题类型规避策略实施示例缓冲区溢出编译期静态断言static_assert(sizeof(icon_buffer) MAX_COMPRESSED_SIZE, Icon buffer too small!);解压尺寸误判预存尺寸元数据在压缩数据前插入 2 字节原始尺寸Big-Endian解压前先读取uint16_t orig_size (pgm_read_byte_near(data) 8)随机数据误压数据预分析 格式选择对新图像资源先用 PC 端工具计算 RLE 压缩比若 0.95 则改用原始数据存储或切换至其他算法如 uLZSSFlash 寿命担忧只读 Flash 访问pgm_*系列函数仅执行lpm读操作不触发 Flash 写入完全规避擦写寿命问题终极建议在量产固件中所有压缩资源必须经过构建脚本如 Python avr-size的自动化校验确保compressedSize ≤ available_flash_space且decompressedSize ≤ available_ram将风险拦截在编译阶段。6. API 接口速查表函数名输入参数输出典型用途代码尺寸 (ATmega328P)RLEcompressuncompressedData(RAM),compressedData(RAM),chunkSize(uint16_t)压缩后字节数压缩 RAM 中临时生成的图像~180 bytespgm_RLEcompressuncompressedData(PROGMEM),compressedData(RAM),chunkSize(uint16_t)压缩后字节数构建时压缩 Flash 资源~190 bytesRLEdecompresscompressedData(RAM),uncompressedData(RAM),chunkSize(uint16_t)uncompressedData chunkSize解压 RAM 中的动态数据~140 bytespgm_RLEdecompresscompressedData(PROGMEM),uncompressedData(RAM),chunkSize(uint16_t)uncompressedData chunkSize解压 Flash 中的常量资源~150 bytes (C), ~90 bytes (ASM)pgm_RLEdecompress256compressedData(PROGMEM),uncompressedData(RAM),chunkSize(uint8_t)uncompressedData chunkSize解压 ≤255 字节的图标/字符~80 bytes (ASM)注尺寸数据基于 GCC-AVR 5.4.0-Os -mcall-prologues编译不含启动代码。7. 源码关键路径解析以pgm_RLEdecompress256的汇编核心循环为例AVR 汇编伪代码; r24 chunkSize (remaining bytes to decode) ; Z compressedData pointer (r31:r30) ; X uncompressedData pointer (r27:r26) decode_loop: lpm r16, Z ; 读取压缩流首字节到 r16Z 自增 sbrc r16, 7 ; 检查 bit7: 若为0 → Short Run rjmp literal_or_long ; 若为1 → Literal or Long Run short_run: andi r16, 0x7F ; 清除 bit7得 count (1-127) lpm r17, Z ; 读取游程值 mov r18, r17 ; 准备重复写入 short_copy: st X, r18 ; 写入 RAMX 自增 dec r16 ; 计数减1 brne short_copy ; 未完继续 rjmp check_done literal_or_long: cpi r16, 0x80 ; 是否为 Literal 标记 (0x80)? breq literal_byte ; 是 → 处理单字节 ; 否则为 Long Run: r16 低7位为count后续2字节为value andi r16, 0x7F lpm r17, Z ; 读 value high lpm r18, Z ; 读 value low ; [Long Run 复制循环此处省略] rjmp check_done literal_byte: lpm r17, Z ; 读取字面值 st X, r17 ; 写入 RAM check_done: dec r24 ; chunkSize 减1 brne decode_loop ; 未解完继续设计精要sbrcSkip if Bit in Register is Cleared指令实现零开销分支判断st X, r18与lpm r16, Z的自增寻址消除显式指针更新指令dec r24; brne构成最紧凑的 8 位循环仅 2 字节、2 周期。此段汇编是 uCompression “速度”承诺的技术基石也是开发者理解其为何能在 16MHz 下达成 2MB/s 的关键入口。

更多文章