esp-iot-solution 的 cmake_utilities:从 relinker 到压缩 OTA 的 ESP-IDF 构建工具链解析
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
导读
cmake_utilities是 esp-iot-solution 仓库中一个专门面向 ESP-IDF 生态的 CMake 工具组件,它并不直接提供某个设备驱动,而是为工程构建阶段注入一系列能力:把 SRAM 中的函数重新链接到 Flash 以释放堆内存、通过 menuconfig 开启 GCC LTO 与字符串 1 字节对齐以压缩固件体积、生成 xz 压缩 OTA 镜像、合并 app/bootloader/分区表为单一 bin,以及为组件提供版本宏注入与诊断着色控制。本文以该组件的 CHANGELOG.md 为时间线索,结合其 README.md、使用文档 与各 CMake 脚本源码,逐一拆解每个子功能的原理、配置项与实操命令,让读者能够在自己的 ESP-IDF 工程中直接复用这套构建期优化方案。
一、组件定位与功能全景
cmake_utilities的官方定位是“提供 ESP-IDF 之外的实用 CMake 工具”(A collection of useful cmake utilities),其版本信息在 idf_component.yml 中声明为1.1.1,依赖idf: ">=4.1",意味着它兼容从 ESP-IDF 4.1 起的较宽版本范围,同时内部又针对 IDF 5.0/5.3 等版本做了针对性适配。
从 README.md 与仓库目录结构看,它由 6 个可独立选用的 CMake 模块构成:
| CMake 模块 | 核心能力 | 关键开关(menuconfig) |
|---|---|---|
project_include.cmake | 自动向工程注入 GCC 诊断着色参数 | CU_DIAGNOSTICS_COLOR(never/always/auto) |
package_manager.cmake | 解析idf_component.yml版本号并注入编译宏 | cu_pkg_get_version/cu_pkg_define_version |
gcc.cmake | 对指定组件/依赖开启 GCC LTO 与字符串 1 字节对齐 | CU_GCC_LTO_ENABLE、CU_GCC_STRING_1BYTE_ALIGN |
relinker.cmake | 在链接阶段将 IRAM/SRAM 函数与 Flash 互迁,节省 RAM | CU_RELINKER_ENABLE及下属子开关 |
gen_compressed_ota.cmake | 新增idf.py gen_compressed_ota命令生成 xz 压缩 OTA 镜像 | 无(与安全启动配置联动) |
gen_single_bin.cmake | 新增idf.py gen_single_bin/flash_single_bin命令合并并烧录单一 bin | 无 |
其中project_include.cmake会被 ESP-IDF 构建系统在工程目录下自动解析,因此诊断着色无需手动include;其余模块则需要在工程CMakeLists.txt的project(XXXX)之后显式include。下文的版本演进与各模块细节,正是 CHANGELOG.md 逐条记录的内容。
二、版本演进脉络(CHANGELOG 主线)
CHANGELOG.md 完整记录了该组件从 2023 年 1 月到 2025 年 2 月的迭代轨迹,核心脉络如下:
- v0.1.0(2023-01-12):组件诞生,提供
cu_pkg_get_version函数、cu_pkg_define_version宏,并将自身 cmake 脚本加入CMAKE_MODULE_PATH。 - v0.2.0(2023-02-23):新增 relinker 功能。
- v0.2.1(2023-03-09):修复组件名含
-(如esp-xxx)时的编译问题。 - v0.3.0(2023-03-10):新增
gen_compressed_ota功能。 - v0.4.0(2023-03-13):新增
idf.py gen_single_bin与idf.py flash_single_bin命令,新增Color in diagnostics配置项。 - v0.4.1(2023-03-15):relinker 支持自定义配置文件路径、支持缺失函数时打印错误信息代替抛异常,并细化对 SPI flash 与 esp_timer 相关函数迁移的编译宏约束。
- v0.4.3~v0.4.6:relinker 支持解码获取 IRAM 排除库、支持同名对象、抑制
__pycache__生成,并加入 IDF v4.3.x 支持。 - v0.4.7~v0.4.9:
gen_compressed_ota支持 v2 压缩 OTA 头、修复 v3 头部字节数与 MD5 长度定义、去除脚本中的/用法以兼容 Windows cmd。 - v0.5.0~v0.5.3:新增 GCC LTO 支持、字符串 1 字节对齐支持、兼容旧版 ESP-IDF 4.3.x;修复
relinker.cmake中add_dependencies called with incorrect number of arguments,并废弃直接include(cmake_utilities)的用法以避免依赖问题。 - v1.1.0(2025-01-16):relinker 支持 ESP32-C3 与面向 flash-suspend 的 SRAM 优化,新增 IDF v5.3.x 支持。
- v1.1.1(2025-02-25):修复 relinker 脚本中
relinker.py:76的无效转义序列告警、IDF 5.0 下IDF_VERSION识别错误,并为 relinker 补充 CI。
这一演进过程反映出一个清晰的工程策略:先是解决构建期的通用能力(版本管理),再逐步加入“内存优化”(relinker、LTO)与“发布产物优化”(压缩 OTA、合并 bin)两大主题。下面按功能模块深入展开。
三、relinker:链接阶段的内存腾挪术
3.1 原理
在 ESP-IDF 中,部分关键函数(如中断处理、缓存禁用期间仍需执行的代码)在链接阶段被强制放入 SRAM(IRAM),以提升执行速度或保证 cache 失效时的可执行性。但并非所有位于 SRAM 的函数都真正“关键”,于是 relinker 提供了一条后处理通道:在链接阶段由脚本读取三份 CSV 配置,把用户指定的函数在 Flash 与 IRAM 之间重新归位,从而释放 SRAM 供堆(heap)使用。这正是 relinker.md 开篇所述的核心思路,也是它被称为 relinker 的原因——发生在 linker 阶段之后的二次“重链接”。
此外,ESP32-C2 与 ESP32-C3 等芯片支持硬件级自动 flash-suspend:当 Flash 处于读、写、擦除状态时,CPU 硬件可自动无缝切回执行 Flash 中的代码,无需软件干预。这为“把更多 IRAM 函数下沉到 Flash”提供了执行层面的安全保障。
3.2 接入与启用
在工程CMakeLists.txt中于project(XXXX)之后引入:
project(XXXX) include(relinker)功能默认关闭,需在 menuconfig 中开启CMake Utilities → Enable relinker(即CU_RELINKER_ENABLE)。当 relinker 未启用时,relinker.cmake 会直接打印Relinker isn't enabled.并跳过全部逻辑。
3.3 配置文件机制
relinker 依赖三份 CSV 文件(详见 docs/relinker.md):
- function.csv:声明要迁移的函数及其所属库、目标文件、生效条件;
- object.csv:声明目标文件在
build目录下的相对路径; - library.csv:声明库文件在
build目录下的相对路径。
仓库在 scripts/relinker/examples 下按“动作 × 芯片 × IDF 版本”组织了默认配置,例如flash_suspend/esp32c2/5.3/与iram_strip/esp32c3/5.0/各含三份 CSV。这两种动作对应两种典型场景:
iram_strip(默认动作):开启
CU_RELINKER_ENABLE但关闭CU_RELINKER_LINK_SPECIFIC_FUNCTIONS_TO_IRAM时使用。它把配置文件中列出的函数从 SRAM 迁往 Flash。例如想把 FreeRTOS 的__getreent移到 Flash,在function.csv中加入:libfreertos.a,tasks.c.obj,__getreent,flash_suspend(进阶动作):同时开启
SPI_FLASH_AUTO_SUSPEND、CU_RELINKER_ENABLE、CU_RELINKER_LINK_SPECIFIC_FUNCTIONS_TO_IRAM时使用。此时即使函数带有IRAM_ATTR或由link.lf配置为 noflash,也只有配置文件中显式列出的函数才会被链入 IRAM,其余 IRAM 函数可放心下沉到 Flash,换取更大的 SRAM 收益。
CSV 每行的语义为库名,目标文件,函数名[,生效的menuconfig条件]:
若函数无条件生效,第四列为空,如
libfreertos.a,tasks.c.obj,__getreent,;若函数仅在某个 menuconfig 选项开启时才迁移,则在第四列写明选项名。例如
__getreent依赖FREERTOS_PLACE_FUNCTIONS_INTO_FLASH时写为:libfreertos.a,tasks.c.obj,__getreent,CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH
配套地,在object.csv中声明对象文件位置(相对build):
libfreertos.a,tasks.c.obj,esp-idf/freertos/CMakeFiles/__idf_freertos.dir/FreeRTOS-Kernel/tasks.c.obj在library.csv中声明库位置(相对build):
libfreertos.a,./esp-idf/freertos/libfreertos.a需要注意:若对应条目已存在于仓库默认配置中,请勿重复添加。
3.4 使用自有配置
若想使用自己的配置文件,在 menuconfig 中开启CU_RELINKER_ENABLE_CUSTOMIZED_CONFIGURATION_FILES,并设置CU_RELINKER_CUSTOMIZED_CONFIGURATION_FILES_PATH为配置文件目录路径,该路径相对工程根目录解析:
[*] Enable customized relinker configuration files (path of your configuration files) Customized relinker configuration files path从 relinker.cmake 的源码可以确认这一行为:开启自定义路径时,脚本通过idf_build_get_property(project_dir PROJECT_DIR)拿到工程根目录,再把配置路径get_filename_component(... ABSOLUTE BASE_DIR "${project_dir}")解析为绝对路径;若目录不存在会直接FATAL_ERROR。未开启自定义时,它会优先查找工程内relinker/${target}目录,找不到再回退到组件内置的scripts/relinker/examples/.../${idf版本}示例配置——目前内置配置只覆盖 IDF 5.0 与 5.3,其他版本会提示去提交 GitHub issue。
3.5 底层实现:一次完整的 relinker 构建
relinker.cmake 展示了完整的集成方式:
- 仅支持
esp32c2与esp32c3两个目标,其他 SoC 直接FATAL_ERROR; - 依据
IDF_VERSION提取形如5.0、5.3的主次版本前缀; - 拼装
relinker.py的参数:--input sections.ld、--output customer_sections.ld、三份 CSV、--sdkconfig、--target、--version、--objdump; - 若开启
CU_RELINKER_ENABLE_PRINT_ERROR_INFO_WHEN_MISSING_FUNCTION(默认开启),追加--missing_function_info——对应 CHANGELOG v0.4.1 中“缺失函数时打印错误信息而非抛异常”的能力; - 若开启
CU_RELINKER_LINK_SPECIFIC_FUNCTIONS_TO_IRAM,追加--link_to_iram切换为 flash-suspend 模式; - 通过
add_custom_command生成customer_sections.ld,再拷贝覆盖sections.ld,并以add_dependencies(${project_elf} customer_sections)挂接到工程链接目标之前。
整个过程中脚本使用-B参数抑制__pycache__生成(对应 CHANGELOG v0.4.4)。Kconfig 中该功能还有一条易被忽略的约束:CU_RELINKER_LINK_SPECIFIC_FUNCTIONS_TO_IRAM的生效依赖SPI_FLASH_AUTO_SUSPEND,即必须先开启 Flash 自动挂起能力才能进入 flash-suspend 迁移模式。
3.6 实测收益参考
docs/relinker.md 给出基于 ESP-IDF v5.3.2 与 cmake_utilities v1.1.0 的power_save示例实测数据(仅供量化参考,可用idf.py size自行核对):
| 芯片 | 默认选项 | 开启 relinker | relinker + flash-suspend |
|---|---|---|---|
| ESP32-C2 | 101408 | 91728 | 51360 |
| ESP32-C3 | 118728 | 99864 | 58312 |
可见在 ESP32-C3 上叠加 flash-suspend 后,SRAM 占用可从约 118 KB 降到约 58 KB,收益接近减半。这也是 CHANGELOG.md 中 v1.1.0 将“ESP32-C3 与 flash-suspend SRAM 优化”列为重点的原因。
四、GCC LTO 与字符串 1 字节对齐:固件体积压缩组合拳
4.1 接入方式
gcc.cmake 提供链接期优化能力,接入方式同样是project(XXXX)之后include(gcc):
include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(XXXX) include(gcc)LTO 默认关闭,需在 menuconfig 开启CU_GCC_LTO_ENABLE。开启后,通过两个宏指定优化范围:
include(gcc) cu_gcc_lto_set(COMPONENTS component_a component_b DEPENDS dependence_a dependence_b) cu_gcc_string_1byte_align(COMPONENTS component_c component_d DEPENDS dependence_c dependence_d)COMPONENTS面向用户自己的组件,DEPENDS面向依赖目标;两个宏都支持cu_gcc_lto_set与cu_gcc_string_1byte_align的叠加使用。
4.2 底层编译选项
从 gcc.cmake 源码可以看到:
- 启用 LTO 前先通过
check_ipo_supported检测工具链支持性,不支持则FATAL_ERROR; - 将归档工具替换为带 LTO 插件的
gcc-ar与gcc-ranlib; - 编译期参数:
-flto=auto -ffat-lto-objects -flto-compression-level=9(auto模式提升编译速度,压缩等级最高 9); - 链接期参数:
-flto -fuse-linker-plugin -ffat-lto-objects -flto-partition=max(max分区可更彻底地清除无用符号); - 对 IDF ≥ 4.4 使用
target_link_libraries(${project_elf} PRIVATE ...),更早版本退化为非 PRIVATE 写法。
字符串 1 字节对齐(CU_GCC_STRING_1BYTE_ALIGN)则通过-malign-data=natural实现,把组件内字符串从默认的 4 字节对齐降为 1 字节对齐,消除填充字节,进一步压缩固件,代价是字符串处理速度略有下降。
4.3 工程示例与实测数据
docs/gcc.md 给出了 esp-matterlight示例中的用法——应用代码体积大,是最适合 LTO 的场景:
project(light) include(gcc) set(app_lto_components main chip esp_matter) set(idf_lto_components lwip wpa_supplicant nvs_flash) set(lto_depends mbedcrypto) cu_gcc_lto_set(COMPONENTS ${app_lto_components} ${idf_lto_components} DEPENDS ${lto_depends})以 ESP32-C2 为目标、开启CU_GCC_LTO_ENABLE并关闭断言、COMPILER_OPTIMIZATION设为-Os、ESP_MAIN_TASK_STACK_SIZE提到 5120 后,实测(引自文档,供参考):
| 选项 | 固件体积 | 栈开销 |
|---|---|---|
| -Os | 1,113,376 | 2508 |
| -Os + LTO | 1,020,640 | 4204 |
再叠加cu_gcc_string_1byte_align后固件进一步降至 1,018,340。文档同时给出 5 条重要权衡提醒:
- 减小固件体积可能降低性能;
- 提升性能可能增大固件体积;
- 开启 LTO 会显著增加编译时间;
- 开启 LTO 可能增大任务栈开销(上表 2508 → 4204 即为实证);
- 字符串 1 字节对齐会降低字符串处理速度。
4.4 与 relinker 的联动限制
LTO 在链接阶段会用新的函数索引代替文件路径,例如(见 docs/gcc.md):
.text 0x00000000420016f4 0x6 /tmp/ccdjwYMH.ltrans51.ltrans.o 0x00000000420016f4 app_main而未开 LTO 时是:
.text.app_main 0x00000000420016f4 0x6 esp-idf/main/libmain.a(app_main.c.obj) 0x00000000420016f4 app_main这意味着一旦对某组件开启 LTO,relinker 便无法再基于.obj路径定位并迁移其中的函数。因此文档建议:优先对应用组件和依赖做 LTO 优化,而不要对内核与硬件驱动类组件开启,以免两者相互冲突。
五、gen_compressed_ota:生成 xz 压缩 OTA 镜像
5.1 命令接入与产物
在工程CMakeLists.txt中加入:
project(XXXX) include(gen_compressed_ota)然后在工程目录运行:
idf.py gen_compressed_ota该命令会先完成整体编译,再调用 gen_custom_ota.py 生成压缩固件。以simple_ota_examples工程为例,成功后会在工程下生成custom_ota_binaries目录,包含:
simple_ota.bin.xz simple_ota.bin.xz.packed其中simple_ota.bin.xz.packed才是真正需要传输的压缩固件。若开启了 Secure Boot v2(或CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT),还会额外生成已签名的:
simple_ota.bin.xz.packed.signed这一签名行为由 gen_compressed_ota.cmake 根据CONFIG_SECURE_BOOT_V2_ENABLED或CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT自动追加--sign_key ${PROJECT_DIR}/${CONFIG_SECURE_BOOT_SIGNING_KEY}参数实现。
5.2 直接调用脚本
也可不经过idf.py,直接对任意 app bin 压缩(-hv v3指定 v3 压缩头格式,--add_app_header附加 app 头):
python3 gen_custom_ota.py -hv v3 -i simple_ota.bin --add_app_header若目标是与 esp-bootloader-plus 配套的压缩固件(不要求新增 app 头),则直接:
python3 gen_custom_ota.py -i simple_ota.bin所有可用参数可用python3 gen_custom_ota.py -h查看。压缩所用的 xz 实现来自仓库的 components/utilities/xz,而压缩 OTA 的引导侧能力对应 components/bootloader_support_plus。
5.3 CHANGELOG 中的相关演进
该模块在 CHANGELOG.md 中多次出现:
- v0.4.2:修复 v3 压缩镜像头保留字节数与不同版本下 MD5 长度定义;
- v0.4.5:去除
gen_custom_ota.py中的/用法,使其可在 Windows cmd 终端使用; - v0.4.7:支持为压缩固件附加 v2 压缩 OTA 头;
- v0.4.9:文档改用默认的 V3 版本压缩格式。
这些记录提示用户:压缩头存在 v2/v3 之分,选择哪个版本应与目标引导程序(esp-bootloader-plus 还是 bootloader_support_plus)的能力对齐。
六、gen_single_bin:单 bin 合并与一键烧录
gen_single_bin.cmake 扩展出两个idf.py命令(对应 CHANGELOG v0.4.0):
idf.py gen_single_bin:将 app、bootloader、分区表等合并为${CMAKE_PROJECT_NAME}_merged.bin。底层通过esptool.py merge_bin -o xxx_merged.bin @flash_args实现,@flash_args复用构建系统已生成的烧录参数文件;idf.py flash_single_bin:依赖gen_single_bin,将合并后的 bin 从地址0x0整包烧录到目标芯片,适合产线批量烧录或简化多文件烧录流程。
两者均挂接在gen_project_binary与bootloader目标之后,保证合并前已具备完整产物。
七、package manager 与诊断着色:两个轻量实用件
7.1 组件版本宏注入
package_manager.cmake 提供两个工具(对应 CHANGELOG v0.1.0 的最初功能):
cu_pkg_get_version(pkg_path ver_major ver_minor ver_patch):读取指定路径下idf_component.yml的version字段,解析出主、次、修订号;cu_pkg_define_version(pkg_path):在cu_pkg_get_version基础上,把组件名转换为大写并注入-D${NAME}_VER_MAJOR/-D${NAME}_VER_MINOR/-D${NAME}_VER_PATCH三个编译宏。组件名中的espressif__前缀会被剥除,-会转为_,因此espressif__usb_stream与usb_stream会生成相同的USB_STREAM_VER_MAJOR等宏(对应 v0.2.1 对含-组件名的修复)。
7.2 诊断着色
project_include.cmake 被构建系统自动解析,它把CMAKE_MODULE_PATH指向本目录,并根据 menuconfig 的CU_DIAGNOSTICS_COLOR选项注入-fdiagnostics-color=always/auto/never。其中auto仅在 stderr 为终端且非 emacs shell 环境下输出颜色(详见 Kconfig 的说明),默认值为always。这让用户在 CI 日志与本地终端之间可以灵活切换编译错误/警告的颜色输出。
八、质量保障:单元测试与 CI
组件带有独立的测试工程 tools/cmake_utilities/test_apps:main/test_cmake_utilities.c对版本宏注入等行为做单元验证,pytest_cmake_utilities.py提供 pytest 级别的集成验证(对应 CHANGELOG v0.4.8 的“Add unit test app”)。CHANGELOG v1.1.1 还专门为 relinker 补充了 CI 覆盖,进一步说明 relinker 的 CSV 配置与不同 IDF 版本、不同芯片的组合是需要持续回归的关键路径。开发者若为 relinker 编写自定义配置,建议参照 scripts/relinker/examples 中flash_suspend与iram_strip两种动作、esp32c2/esp32c3双芯片、5.0/5.3双版本的组织方式维护矩阵,避免配置漂移。
九、使用建议与注意事项汇总
- 接入顺序:除
project_include.cmake外,其余模块一律在project(XXXX)之后include(...);旧版文档中直接include(cmake_utilities)的写法已不推荐(见 CHANGELOG v0.5.3),应改为按需引入具体模块文件。 - relinker 的适用范围:当前仅支持 ESP32-C2 / ESP32-C3;
CU_RELINKER_LINK_SPECIFIC_FUNCTIONS_TO_IRAM依赖SPI_FLASH_AUTO_SUSPEND;启用 flash-suspend 前务必确认所用 Flash 型号被 ESP-IDF 支持。 - LTO 与 relinker 互斥:LTO 会让 relinker 失去函数定位能力,建议只对应用层组件开 LTO;同时留意 LTO 带来的编译时间、任务栈开销增长。
- 压缩 OTA 的版本对齐:
gen_custom_ota.py的-hv版本(v2/v3)要与目标引导程序支持的头格式匹配;默认推荐 V3。 - 配置矩阵化:relinker 的 CSV 随芯片与 IDF 版本变化,升级 IDF 或换芯片后应重新核对 scripts/relinker/examples 下对应目录的条目,或启用自定义配置并纳入版本管理。
十、小结
从 CHANGELOG.md 的时间线可以看到,cmake_utilities的每一次版本跃迁都对应一个可落地的构建期优化手段:v0.2.0 的 relinker 解决 SRAM 紧张、v0.3.0 的压缩 OTA 解决升级带宽、v0.4.0 的单 bin 合并解决产线效率、v0.5.0 的 LTO 与字符串对齐解决固件体积。它们在构建期工作,不增加任何运行时依赖,却能显著改变最终固件的内存布局与体积。对于内存敏感的 ESP32-C2/C3 产品,或对 OTA 镜像大小有苛刻要求的场景,这套组件是目前 esp-iot-solution 仓库中最直接的构建期优化工具箱,值得结合本文所引的 使用文档、LTO 文档、压缩 OTA 文档 与实际 CMake 源码逐一实践验证。
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考