在乐鑫的官方文档中对组件的描述如下:
是模块化且独立的代码,会被编译成静态库(.a 文件)并链接到应用程序。部分组件由 ESP-IDF 官方提供,其他组件则来源于其它开源项目。
可以简单理解为工程下每个文件夹(包含源文件和头文件或仅源文件)都可以看做为一个组件,要添加的用户自己的组件则属于组件中的“其他组件”。相较于KEIL工程添加文件夹和源文件可以直接在IDE中点击添加,ESP32的IDF工程添加文件夹和源文件(组件)则需要在"CMakeLists.txt"
中添加脚本来实现,略微麻烦,这是由于IDF工程是由CMake构建、链接编译的。本文参考乐鑫《ESP-IDF编程指南》中构建系统章节进行组件(包含用户自己的文件夹、源文件与头文件)的添加,并以官方例程"blink"作为示例。
由IDF生成blink工程示例目录树结构如下(其他与“添加组件”无关的文件未展示):
- blink/
- CMakeLists.txt
- sdkconfig
- esp_idf_components/ #此文件夹下为官方组件,不展开
- main/ - CMakeLists.txt
- blink_example_main.c
- build/
在乐鑫《ESP-IDF编程指南》的构建系统内部章节中给出了标准的 ESP-IDF 应用构建过程,构建过程可以大致分为四个阶段:
我们可以从这个过程中去了解组件是如何参与工程编译并总结添加组件的方法。
在将
idf.cmake
导入project.cmake
后,将执行以下步骤:
- 在环境变量中设置
IDF_PATH
或从顶层CMakeLists.txt
中包含的project.cmake
路径推断相对路径。- 将 /tools/cmake 添加到
CMAKE_MODULE_PATH
中,并导入核心模块和各种辅助/第三方脚本。- 设置构建工具/可执行文件,如默认的 Python 解释器。
- 获取 ESP-IDF git 修订版,并存储为
IDF_VER
。- 设置全局构建参数,即编译选项、编译定义、包括所有组件的 include 目录。
- 将 components 中的组件添加到构建中。
自定义 project() 命令的初始部分执行以下步骤:
- 在环境变量或 CMake 缓存中设置
IDF_TARGET
以及设置相应要使用的CMAKE_TOOLCHAIN_FILE
。- 添加
EXTRA_COMPONENTS_DIRS
中的组件至构建中。- 从
COMPONENTS
/EXCLUDE_COMPONENTS
、SDKCONFIG
、SDKCONFIG_DEFAULTS
等变量中为调用命令idf_build_process()
准备参数。调用
idf_build_process()
命令标志着这个阶段的结束。
我们不必完全了解上述的初始化过程,只需要关注与添加组件相关的内容:
- 在环境变量中设置
IDF_PATH
或从顶层CMakeLists.txt
中包含的project.cmake
路径推断相对路径。- 添加
EXTRA_COMPONENTS_DIRS
中的组件至构建中。
从中可以得到的信息是,标准的 ESP-IDF 应用构建是从顶层文件夹(本例为"blink"
文件夹)下"CMakeLists.txt"
开始的,并且在 EXTRA_COMPONENTS_DIRS
中保存有组件的信息,而保存的组件信息又会参与到构建过程中。所以我们需要重点关注"CMakeLists.txt"
和 EXTRA_COMPONENTS_DIRS
,至于在"CMakeLists.txt"
中做了什么和 EXTRA_COMPONENTS_DIRS
保存了什么信息、如何保存信息后文会讲到。
首先来看一下工程顶层文件夹下"CMakeLists.txt"
的内容,因为工程中包含了多个"CMakeLists.txt"
文件,为了方便区分当前所说的"CMakeLists.txt"
为哪一个,后文中提到该文件并需要作区分时加上其上一级文件夹的名称。本例中,顶层文件夹下的"blink/CMakeLists.txt"
内容如下:
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/led_strip)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
project(${ProjectId})
在这个文件中就出现了EXTRA_COMPONENTS_DIRS
,其实从其名字里不难看出EXTRA(外部)、COMPONENTS(组件)、DIRS(文件夹)它是用来保存外部组件文件夹的一个CMake变量,在脚本的第五行 set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/led_strip)
中可以看到对名为"led_strip"组件的路径进行了设置(set),将其保存到了EXTRA_COMPONENTS_DIRS
中,其组件的路经为"$ENV{IDF_PATH}/examples/common_components/led_strip"
。但是在前面给出的目录树中并没有看到组件"led_strip",这是因为在示例工程"blink"中实际上并没有将"led_strip"组件的源文件添加到工程的目录下,而是在编译的时候直接在"$ENV{IDF_PATH}/examples/common_components/led_strip"
路径下寻找源文件。
注:$ENV {IDF_PATH}为环境变量中IDF_PATH的值,即为IDF在本系统中的安装路径,如我的安装路径为"C:/Espressif/frameworks/esp-idf-v4.4/“则"led_strip"的路径为"C:/Espressif/frameworks/esp-idf-v4.4/examples/common_components/led_strip” 。
开头说到工程下每个文件夹(包含源文件和头文件或仅源文件)都可以看做为一个组件,但是这里并没有看到对"main"
文件夹("main"
文件夹下包含了源文件"blink_example_main.c"
,也可以看做是一个组件)的路径进行设置,而在ESP-IDF编程指南中的示例项目中对"main"文件夹有如下描述:
“main” 目录是一个特殊的组件,它包含项目本身的源代码。”main” 是默认名称,CMake 变量 COMPONENT_DIRS* 默认包含此组件,但您可以修改此变量。
也就是说"main"
文件夹是一个默认的路径,并不需要在设置中添加。
*注:这里的COMPONENT_DIRS并不同于EXTRA_COMPONENT_DIRS,没有了EXTRA,是一个内部的组件文件夹路径
上述"blink/CMakeLists.txt"
文件中其他语句在ESP-IDF编程指南中项目 CMakeLists 文件章节也给出了解释:
cmake_minimum_required(VERSION 3.5)
必须放在 CMakeLists.txt 文件的第一行,它会告诉 CMake 构建该项目所需要的最小版本号。ESP-IDF 支持 CMake 3.5 或更高的版本。include($ENV{IDF_PATH}/tools/cmake/project.cmake)
会导入 CMake 的其余功能来完成配置项目、检索组件等任务。project(myProject)
会创建项目本身,并指定项目名称。该名称会作为最终输出的二进制文件的名字,即myProject.elf
和myProject.bin
。每个 CMakeLists 文件只能定义一个项目。
"blink/CMakeLists.txt"
中对组件的路径进行设置并保存在CMake变量EXTRA_COMPONENTS_DIRS
中。注意:在本例程“blink”中已经为EXTRA_COMPONENTS_DIRS
设置了路径,若再次设置会覆盖原来的路径,需要将例程中外部组件"led_strip"复制到新设置的路径下。这个阶段会建立一个需要在构建过程中处理的组件列表,该阶段在 idf_build_process() 的前半部分进行。
- 检索每个组件的公共和私有依赖。创建一个子进程,以脚本模式执行每个组件的 CMakeLists.txt。idf_component_register REQUIRES 和 PRIV_REQUIRES 参数的值会返回给父进程。这就是所谓的早期扩展。在这一步中定义变量 CMAKE_BUILD_EARLY_EXPANSION。
- 导入各组件的 project_include.cmake。
- 根据公共和私有的依赖关系,递归地导入各个组件。
该阶段处理构建中的组件,是 idf_build_process() 的后半部分。
- 从 sdkconfig 文件中加载项目配置,并生成 sdkconfig.cmake 和 sdkconfig.h 头文件。这两个文件分别定义了可以从构建脚本和 C/C++ 源文件/头文件中访问的配置变量/宏。
- 将每个组件添加为一个子目录,处理其 CMakeLists.txt。组件 CMakeLists.txt 调用注册命令 idf_component_register 添加源文件、导入目录、创建组件库、链接依赖关系等。
从枚举的描述中可以了解到每个组件都包含一个"CMakeLists.txt"
文件,并且在构建的过程中会执行每个组件中的"CMakeLists.txt"
文件,可以简单理解为每个组件的文件夹下都存在一个"CMakeLists.txt"
文件,可以将其称为“组件 CMakeLists.txt
”;而在处理的描述中有这样一句话:
组件
CMakeLists.txt
调用注册命令idf_component_register
添加源文件、导入目录、创建组件库、链接依赖关系等
也就是说组件的源文件是通过注册的方式参与到编译的过程中的,而注册的过程需要用到命令idf_component_register
,这个命令写在组件文件夹下"CMakeLists.txt"
文件中。
下面来看一个例子,前文说过 "main"
也是一个组件(内部组件),在前面给出的“blink”例程目录树中可以看到在"main"
文件夹下也有一个"CMakeLists.txt"
文件,打开"main/CMakeLists.txt"
为如下内容:
idf_component_register(SRCS "blink_example_main.c"
INCLUDE_DIRS ".")
可以看到该脚本实现了对"main"组件下的源文件"blink_example_main.c"
进行注册;而INCLUDE_DIRS
表示存放头文件的文件夹,这里对存放头文件的头文夹进行了注册,而不是直接注册头文件。"."
表示头文件的文件夹在当前目录,如果头文件在其他文件夹中,比如下面的目录结构:
main/ - include/ - hander1.h
- hander2.h
- hander3.h
- CMakeLists.txt
- blink_example_main.c
则可以在"main/CMakeLists.txt"
中这样写:
idf_component_register(SRCS "blink_example_main.c"
INCLUDE_DIRS "./include")
如果头文件在上一级文件夹中:
- blink/
- CMakeLists.txt
- sdkconfig
- esp_idf_components/ #此文件夹下为官方组件,不展开
- main/ - CMakeLists.txt
- blink_example_main.c
- build/
- hander.h
则可以这样写(..
表示上一级文件夹):
idf_component_register(SRCS "blink_example_main.c"
INCLUDE_DIRS "../")
通过前文可以知道,要为“blink”工程添加用户自己的"button"组件需要以下几个步骤:
"button"
的文件夹及其源文件添加到工程目录中"led_strip"
组件复制到工程中"blink/CMakeLists.txt"
文件中写入组件路径"button/CMakeLists.txt"
文件"button"
的文件夹及其源文件添加到工程目录中、将"led_strip"组件复制到工程中"user_components"
文件夹(该文件夹名可以为自己定义的任何名字,并在该文件夹下存放自己要添加的组件),在"user_components"
文件夹下再添加"button"
文件夹,"button"
文件夹中包含"button.c"
和"button.h"
,还需要在工程"button"
文件夹下新建一个名为"CMakeLists.txt"
的空文件,再将"led_strip"
组件复制到"user_components"
文件夹下完成后的目录树结构为(其他与“添加组件”无关的文件未展示):- blink/
- CMakeLists.txt
- sdkconfig
- esp_idf_components/ #此文件夹下为官方组件,不展开
- user_components/ - button/ - button.c
- button.h
- CMakeLists.txt
- led_strip/ #无需修改,不展开
- main/ - CMakeLists.txt
- blink_example_main.c
- build/
"blink/CMakeLists.txt"
文件中写入组件路径"blink/CMakeLists.txt"
的原始内容为:# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/led_strip)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
project(${ProjectId})
向文件中添加set(EXTRA_COMPONENT_DIRS "./user_components")
并删除原来的set(...)
,修改后:
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS "./user_components")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
project(${ProjectId})
注:"led_strip"组件的路径为ENV{IDF_PATH}/examples/common_components/led_strip,其中ENV{IDF_PATH}为IDF的安装路径
"button/CMakeLists.txt"
文件"button/CMakeLists.txt"
中写入下面的脚本:idf_component_register(SRCS "button.c"
INCLUDE_DIRS ".")
至此添加用户组件已完成。
"CMakeLists.txt"
文件中写入组件路径。"CMakeLists.txt"
文件,并对组件的源文件和头文件路径进行注册。EXTRA_COMPONENTS_DIRS
只能设置一次,多次设置会将前面设置的内容覆盖,只保留最后一次的设置。以上方法只是添加组件的其中一种方法。
附件:“blink”示例工程 https://gitdep.ml/Nee/blink