ESP-IDF 构建系统 v2:从单个项目构建多个独立固件二进制(Building Multiple Binaries)

ESP-IDF 构建系统 v2:从单个项目构建多个独立固件二进制(Building Multiple Binaries) ESP-IDF 构建系统 v2从单个项目构建多个独立固件二进制Building Multiple Binaries【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idfESP-IDF 构建系统 v2cmakev2默认从一套源码树构建单一应用但它同样支持在一个项目内构建多个相互独立的固件二进制每个二进制可以有自己的入口组件和组件集合而共享组件只编译一次。本篇指南完整讲解这一多二进制工作流的 CMake 写法idf_project_init 多次idf_build_executable 各自的idf_build_binary/idf_flash_binary/idf_create_menuconfig目标并结合 ESP-IDF 仓库中的参考示例examples/build_system/cmakev2/features/multi_binary与tools/cmakev2下的构建系统源码说明共享组件、单一sdkconfig、可选依赖optional requires模式等关键机制背后的实现细节与限制。读完后你可以把同一套共享组件库打包成若干功能/产品变体固件并理解多二进制项目在烧写、配置管理上的行为边界。一、适用场景多个二进制 vs 多个配置构建系统 v2 提供了两个方向上的扩展能力二者解决的问题不同参见 multiple-binaries.rst 与 multiple-configurations.rst需求方案机制同一套共享组件基础产出多个不同的应用如若干功能/产品变体固件仅组件取舍不同多二进制本文主题一个项目、一份sdkconfigidf_build_executable调用多次同一个应用需要多套彼此独立的配置如 dev/prod1/prod2多配置CMake presets每个 preset 独立的sdkconfig.defaults与构建目录官方文档给出的判断原则是如果几个应用共享组件基础与同一份配置只是包含的组件不同用多二进制——共享组件只编译一次且在各二进制间天然保持一致如果每个应用需要各自独立的配置就应该拆成多个项目或用多配置 preset。二、核心工作流一次idf_project_init多次idf_build_executable多二进制项目的骨架只有两步idf_project_init()只调用一次完成项目级初始化构建属性、默认编译/链接选项、组件发现、全局烧写目标等之后为每个二进制各调用一次idf_build_executable并在COMPONENTS参数中给出该二进制各自的组件集合。组件目标component target在整个项目范围内是共享的被多个可执行文件用到的组件只会被构建一次。官方文档中的最小示例如下include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) project(multi_binary C CXX ASM) idf_project_init() idf_build_executable(app1.elf COMPONENTS app1_main component1 component2) idf_build_executable(app2.elf COMPONENTS app2_main component1 component2 component3)从源码看idf_build_executable的内部行为印证了共享组件只构建一次这一点。在 tools/cmakev2/build.cmake#L669-L744 中该函数会先为每个可执行文件创建一个专属库目标library_executable例如library_app1.elf再通过idf_build_library()把COMPONENTS列表中各组件的接口链接进这个库随后生成一个 stub 源文件executable_executable.c用add_executable建出真正的可执行目标并链接该库最后把库目标记录到可执行目标的LIBRARY_INTERFACE属性上。由于组件层目标在 tools/cmakev2/project.cmake#L637-L669 的idf_project_init中只初始化一次两次idf_build_executable引用的component1、component2自然是同一份编译产物——这就是共享组件编译一次、跨二进制一致的实现基础。idf_build_executable还支持NAME生成的二进制名称、SUFFIX与MAPFILE_TARGET为指定可执行文件生成链接 map 文件见 tools/cmakev2/build.cmake#L651-L659 的 API 文档等可选项。三、为每个二进制生成烧写与配置目标每个可执行文件随后拥有互不冲突的独立目标名二进制文件目标、烧写目标和 menuconfig 目标。文档给出的写法为idf_build_binary(app1.elf OUTPUT_FILE ${CMAKE_BINARY_DIR}/app1.bin TARGET app1_binary) idf_flash_binary(app1_binary TARGET app1-flash NAME app1 FLASH) idf_create_menuconfig(app1.elf TARGET app1-menuconfig) idf_build_binary(app2.elf OUTPUT_FILE ${CMAKE_BINARY_DIR}/app2.bin TARGET app2_binary) idf_flash_binary(app2_binary TARGET app2-flash NAME app2) add_custom_target(app ALL DEPENDS app1.bin app2.bin)各函数的作用与关键细节idf_build_binarytools/cmakev2/build.cmake#L1039 起从.elf生成应用镜像.binTARGET指定生成目标名。idf_flash_binarytools/cmakev2/build.cmake#L1268-L1311为二进制注册 esptool 烧写目标TARGET指定烧写目标名如app1-flashNAME指定烧写元数据中的应用名偏移量自动取自分区表中第一个 app 分区--partition-boot-default的 offset。FLASH选项是可选的传入时该二进制会同时被挂到项目全局的flash目标上。从源码看build.cmake L1307-L1310FLASH选项最终把该镜像追加注册到全局flash目标——因此多个二进制若都传FLASH会注册到相同偏移默认 0x10000在全局flash目标中产生重复键。这也是官方示例中 app2 故意不传FLASH的原因示例注释明确指出两个应用共用同一偏移都带FLASH会破坏flasher_args.json。idf_create_menuconfig/idf_create_confservertools/cmakev2/kconfig.cmake#L880、#L1110分别为每个可执行文件创建配置编辑目标。add_custom_target(app ALL DEPENDS app1.bin app2.bin)让默认构建同时产出两个.bin。这样一次构建即可同时产出app1.bin和app2.bin并分别拥有appN-flash与appN-menuconfig目标可在不重新构建的情况下切换烧写哪个应用。四、完整参考示例multi_binary项目仓库中的 multi_binary 示例CMakeLists.txt、README.md是上述工作流的完整落地支持 ESP32 / C2 / C3 / C5 / C6 / C61 / H2 / H21 / H4 / P4 / S2 / S3 / S31 等目标芯片。4.1 项目结构所有组件放在components/下每个idf_build_executable只挑选自己需要的子集用入口组件显式声明依赖的方式避免复杂的条件逻辑multi_binary/ ├── CMakeLists.txt └── components/ ├── app1_main/ # app1 的入口组件 │ ├── CMakeLists.txt │ └── app1_main.c ├── app2_main/ # app2 的入口组件 │ ├── CMakeLists.txt │ └── app2_main.c ├── component1/ # 共享组件 ├── component2/ # 共享组件 └── component3/ # 仅链接进 app24.2 完整 CMakeLists.txt示例的完整写法在文档骨架之外多了两处工程细节生成flasher_args.json与元数据以及为两个应用分别创建 confserver 目标cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) project(multi_binary C CXX ASM) # Manual initialization for fine-grained control over component inclusion idf_project_init() # Build the first executable: app1 # Links: app1_main, component1, component2 idf_build_executable(app1.elf COMPONENTS app1_main component1 component2) # Build the second executable: app2 # Links: app2_main, component1, component2, component3 idf_build_executable(app2.elf COMPONENTS app2_main component1 component2 component3) # Generate binaries and flash targets # App1 binary idf_build_binary(app1.elf OUTPUT_FILE ${CMAKE_BINARY_DIR}/app1.bin TARGET app1_binary) idf_flash_binary(app1_binary TARGET app1-flash NAME app1 FLASH) # Generate flasher_args.json and metadata for the primary app1 binary. # app1 is the only app registered in the global flash target (app2 omits FLASH), # so flasher_args.json correctly contains app1 as the sole app binary. idf_build_generate_flasher_args() idf_build_generate_metadata(BINARY app1_binary) # Create menuconfig and confserver targets for app1 binary idf_create_menuconfig(app1.elf TARGET app1-menuconfig) idf_create_confserver(app1.elf TARGET app1-confserver) # App2 binary idf_build_binary(app2.elf OUTPUT_FILE ${CMAKE_BINARY_DIR}/app2.bin TARGET app2_binary) # Do NOT pass FLASH here: both apps share the same 0x10000 offset, which would # create a duplicate key in the global flash target and break flasher_args.json. # app1 (with FLASH) is used as the default app in flasher_args.json; the pytest # test swaps in app2.bin when needed. idf_flash_binary(app2_binary TARGET app2-flash NAME app2) # Create menuconfig and confserver targets for app2 binary idf_create_menuconfig(app2.elf TARGET app2-menuconfig) idf_create_confserver(app2.elf TARGET app2-confserver) # Make both binaries part of the default app target add_custom_target(app ALL DEPENDS app1.bin app2.bin)要点解读idf_build_generate_flasher_args()定义于 tools/cmakev2/project.cmake#L765生成flasher_args.json它是idf.py flash/ 自动化烧写时确定烧哪些文件、到哪个偏移的依据。由于只有 app1 注册进了全局flash目标该 JSON 中以 app1 为唯一应用镜像app2 通过独立的app2-flash目标按需烧写。若两个应用需要部署到不同的 app 分区例如配合 A/B 分区的APP_BUILD_TYPE_APP_2NDBOOT则二者可以分别使用不同偏移并各自带FLASH但共用同一默认偏移时只能让默认应用带FLASH。4.3 构建、烧写与配置构建一条命令产出两个二进制cd examples/build_system/cmakev2/features/multi_binary idf.py set-target target idf.py build烧写无需重新构建即可切换应用idf.py app1-flash monitor idf.py app2-flash monitor配置idf.py app1-menuconfig --no-hints注意通过idf.py调用自定义 menuconfig 目标时必须加--no-hintsidf.py默认会重定向 stdout而 menuconfig 是基于 curses 的终端界面重定向会破坏交互见示例 README.md。预期串口输出README 与测试用例均可验证# app1 I (xxx) component1: Hello from component1! I (xxx) component2: Hello from component2! # app2 I (xxx) component1: Hello from component1! I (xxx) component2: Hello from component2! I (xxx) component3: Hello from component3!4.4 测试用例如何验证两个二进制pytest_cmakev2_multi_binary.py 的测试逻辑与多二进制语义完全对应DUT 固件先依据flasher_args.json自动烧入 app1校验输出中只有 component1/component2 的日志然后把 app 分区从flash_args[app1][offset]取偏移只重写为app2.bin再校验输出多出 component3 的日志。这说明两个镜像复用同一分区表、同一偏移差异只在链接的组件集合。五、两个必须知道的行为约束5.1 单一sdkconfig跨可执行文件共享配置多二进制项目只有一份项目级sdkconfig被多个可执行文件共享的组件是单实例、单配置。因此所有appN-menuconfig目标编辑的都是同一份sdkconfig通过其中任意一个改动了共享组件的选项会影响所有使用该组件的可执行文件。示例 README 对此的表述是每个组件只会用当前配置求值一次若某组件需要不同的配置必须另建项目不能在同一项目里让同一组件拥有两种配置。如果目标是同一应用、多套真正独立的配置应改用 Building Multiple Configurations通过CMakePresets.json的 configurePresets 为每套配置指定独立的SDKCONFIG_DEFAULTS与独立构建目录配合set(SDKCONFIG ${CMAKE_BINARY_DIR}/sdkconfig)再用idf.py --preset prod1 build分别构建。5.2 必须使用 IMMEDIATE 可选依赖模式多二进制项目要构建多个库每个可执行文件对应一个library_*.elf因此必须使用IMMEDIATE可选依赖解析模式IDF_COMPONENT_OPTIONAL_REQUIRES_MODE默认值。不要将其设为DEFERRED否则构建会直接报错。源码层面的证据链单应用默认路径 idf_project_default 之所以能启用DEFERRED正是因为它只会产生唯一的一个库——源码注释写明Use DEFERRED optional-requires resolution only when this will be the sole library being builtproject.cmake L958-L960。而 tools/cmakev2/compat.cmake#L285-L292 中__idf_component_process_optional_requires在检测到LIBRARY_INTERFACES数量大于 1 时直接idf_dieDEFERRED optional requires mode cannot be used when building multiple libraries (detected N libraries). Set IDF_COMPONENT_OPTIONAL_REQUIRES_MODE to IMMEDIATE for multi-library projects.两种模式的语义差异compat.cmake L376-L397 的 API 注释IMMEDIATE默认在idf_component_optional_requires调用处立即判断并把可选依赖纳入并链接DEFERRED只记录请求留待库构建末期基于当前库内已有的组件集合统一解析——这套延迟解析依赖单一库内组件集合已定型的前提多个库并存时该前提不成立。换言之多二进制项目保持IMMEDIATE即默认值手动idf_project_init后不改动即可就是正确做法DEFERRED只服务于单应用场景idf_project_default自动开启。六、总结与实践建议何时用多二进制多个应用共享组件基础与同一份配置、仅组件取舍不同功能/产品变体。共享组件只编译一次配置天然一致。最小骨架idf_project_init()一次 → 每应用一次idf_build_executable(name.elf COMPONENTS ...)→ 每应用idf_build_binary/idf_flash_binary目标名加前缀避免冲突/idf_create_menuconfig→add_custom_target(app ALL DEPENDS ...)让默认构建产出全部二进制。全局flash目标只能有一个默认应用携带FLASH选项避免同一偏移产生重复键破坏flasher_args.json其余应用通过专属appN-flash目标烧写。配置边界一份sdkconfig管所有二进制需要独立配置时改用 presets 多配置方案或拆项目。可选依赖模式保持默认 IMMEDIATE切勿对多库项目设置DEFERRED。继续深入可阅读docs/en/api-guides/build-system-v2/index.rst构建系统 v2 文档总览、examples/build_system/cmakev2/features/multi_config多配置对照示例、tools/cmakev2/build.cmake 与 tools/cmakev2/project.cmake 中各idf_*函数的完整 API 注释。【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考