消费电子固件瘦身编程:把智能手表固件体积压缩到512KB以内
智能手表固件压到512KB,不是最后用压缩算法“压一下bin”,而是从构建链到资源表全程做减法。Cortex-M0/M4这类MCU没有多余Flash给你塞全量HAL、完整printf、整本中文字库。正路是:编译器砍死代码、链接器回收段、资源只带用到的、日志和浮点库能省就省。
一、先建体积账单,再动手
别凭感觉优化。每次构建跑:
arm-none-eabi-size build/fw.elf
arm-none-eabi-nm -S --size-sort -r build/fw.elf | head -20
.text看代码体积,.rodata基本是字符串/字体/常量表,.data+.bss看RAM。
找大函数、大表,按占用排个序,先砍最肥的那几个。手环类项目常见大头:日志宏、HAL全集、字库、图片RGB565、printf浮点。
二、编译链四件套必开
Makefile/CMake里固定这套:
CFLAGS += -Os -ffunction-sections -fdata-sections -flto
LDFLAGS += -Wl,--gc-sections -Wl,-Map=build/fw.map
LDFLAGS += --specs=nano.specs
-Os:尺寸优先
-ffunction-sections+-fdata-sections:每个函数/变量独立成段
--gc-sections:没被引用的段直接丢掉
-flto:跨文件内联和死代码消除,再省5%~10%
nano.specs:换newlib-nano,printf瘦一圈
不开function-sections时,一个.c里只要有一个函数被用,整个文件代码都进固件——HAL驱动尤其亏。
三、printf和日志是隐形炸弹
Cortex-M0没FPU,%f一用软浮点库直接涨10KB+。
规矩:
日志宏按等级关:NRF_LOG_ENABLED 0
不用完整printf,换tiny printf或自己写log_raw()
需要浮点再显式加 -u _printf_float
调试字符串别进发布版,#ifdef DEBUG包住
#ifdef DEBUG
#define LOG(...) tiny_printf(__VA_ARGS__)
#else
#define LOG(...)
#endif
四、HAL和外设只留用的
SDK默认全开TWI/SPI/QSPI/PDM,手表只用BLE+屏+心率,其余全关:
#define NRF_DRV_TWIM_ENABLED 0
#define NRF_DRV_QSPI_ENABLED 0
#define PDM_ENABLED 0
BLE也别开满:外设连接数1、MTU默认23,能省十几KB Flash和RAM。
LTO+gc-sections配合下,HAL里没链到的函数会自动消失。
五、字库必须子集化
中文字库全量是几MB,手表界面就那几百个字:“时间、心率、步数、设置、蓝牙、查找手机”。
用fonttools按工程文案抽字符:
pyftsubset full.ttf --text-file=used_chars.txt \
--output-file=sub.ttf
再转点阵或LVGL bin字体进Flash。
规则:
只带界面实际出现的字
小屏12px以下用1~2bpp灰阶
多字号是倍率爆炸源,界面统一1~2种字号
动态内容(通知)走备用小字库或截断
字体从整库到子集,往往能从数百KB掉到几十KB。
六、图片和资源降维
背景图别塞RGB888,用RGB565或PNG驻Flash渲染时解
图标用4bit Alpha够用
大常量表能算就不存:正弦表只存1/4周期
传感器标定系数走压缩或运行时拟合
启动图尽量删,手表开机页几KB也贵
七、链接脚本收尾
发布版丢调试段:
/DISCARD/ : {
*(.debug_*)
*(.comment)
*(.note.gnu.build-id)
}
向量表、启动代码用KEEP()护住,别被gc收掉。
段对齐别乱ALIGN(64),填充字节也是Flash。
八、CI里卡体积红线
写个脚本,超512KB直接构建失败:
flash=$(arm-none-eabi-size fw.elf | tail -1 | awk '{print $1+$2}')
if [ $flash -gt 524288 ]; then
echo "OVER 512KB: $flash"
exit 1
fi
每次合PR都比体积,避免“加了个小功能”慢慢涨爆。
九、落坑清单
只开-Os不开gc-sections→HAL死代码还在
printf留完整newlib→浮点库白吃
字库整包链入→Flash瞬间没
日志宏没条件编译→字符串常驻rodata
图片用RGB888背景→几百KB没必要的
向量表被回收→启动HardFault
只压bin不压elf→执行态没省
没建size基线→不知道谁胖
BLE连接数开8→RAM先爆
动态通知字没兜底→缺字方块
十、落地顺序
size+nm拉出体积榜单
开-Os+function-sections+gc-sections+LTO
换nano.specs,日志按DEBUG关
外设驱动只留用的,BLE砍连接数
字库按界面文案子集化
图片转RGB565/PNG,图标降Alpha
链接脚本丢debug段,KEEP向量表
CI卡512KB红线,每次提交比体积
固件瘦身不是“压缩”,是“别把用不到的东西编进去”:编译器做函数级剔除,链接器做段回收,字库做字符子集,printf做轻量替换。手表512KB预算里,代码通常只占一小半,剩下被字库、日志字符串、HAL全集吃掉的最多。先量再砍,别一上来全局压缩——LZ压完还是得整块解到Flash,纯MCU执行固件基本不划算;真正省的是不让它进.bin。





