一、简介
如果想要新建一个ESP32项目,需要包含很多其他的文件夹和文件,如果对ESP32的勾线系统原理不理解,就会产生出很多编译不通过的问题。这里就对ESP-IDF构建系统的实现原理做一个简单的总结。
测试环境:Ubuntu18.4
ESP-IDF:V5.0
官方参考链接:构建系统 - ESP32 - — ESP-IDF 编程指南 v5.0.1 文档
二、概念
项目
项目特指一个目录,其中包含了构建可执行应用程序所需的全部文件和配置,以及其他支持型文件,例如分区表、数据、文件系统分区和引导程序等等。通俗一点说,就是项目文件夹。
项目配置
项目配置保存在项目根目录下的sdkconfig文件中。可以通过idf.py menuconfig指令进行修改,且一个项目只能包含一个项目配置。
应用程序
应用程序是由ESP-IDF构建得到的可执行文件。一个项目通常会构建两个应用程序:项目应用程序(可执行的主文件,即用户自定义的固件)和引导程序(启动并初始化项目应用程序)。
组件
组件是模块化且独立的代码,会被编译成静态库(.a文件),并链接到应用程序。部分组件由ESP-IDF官方提供,其他组件则来源于其他开源项目。
目标
特指运行构建后应用程序的硬件设备。运行idf.py --list-targets可以查看当前ESP-IDF版本中支持目标的完整列表。
注:以下部分不属于项目的组成部分。
- ESP-IDF。其并不是项目的一部分,它独立于项目,通过IDF-PATH环境变量(保存esp-idf目录的路径)链接到项目,从而将IDF架构与项目分离。
- 交叉编译工具链。其应该被安装在系统PATH环境变量中。
示例项目的项目树:
- DemoProject/
- build
- main/
- CMakeLists.txt
- main.c
- components/
- CMakeLists.txt
- test.c
- CMakeLists.txt
- sdkconfig
项目包含了以下组成部分:
build目录:该目录是存放构建输出的地方,如果没有此目录,idf.py会自动构建。CMake会配置项目,并在此目录下生成临时的构建文件。随后,在主构建进程的运行期间,该目录还会保存临时目标文件、库文件以及最终输出的二进制文件。次目录通常不会添加到项目的源码管理系统中,也不会随项目源码一同发布。
main目录:该目录是一个特殊的组件,它包含项目本身的源代码。main是默认名称,CMake变量COMPONENT_DIRS默认包含此组件,可以自行修改此变量。有关详细信息,请参阅 重命名 main 组件。如果项目中源文件较多,建议将其归于组件中,而不是全部放在 “main” 中。
components目录:该目录是可选的,其中包含了项目的部分自定义组件,并不是每个项目都需要这种自定义组件,但它有助于构建可复用的代码或者导入第三方(不属于ESP-IDF)的组件。或者,也可以在顶层CMakeLists.txt中设置EXTRA_COMPONENT_DIRS变量以查找其他指定位置处的组件。
顶层项目CMakeLists.txt文件:该文件是CMake用于学习如何构建项目的主要文件,可以在这个文件中设置项目全局的CMake变量。顶层项目CMakeLists.txt文件会导入esp-idf/tools/cmake/project.cmake文件,由它负责实现构建系统的其余部分。该文件最后会设置项目的名称,并定义该项目。
sdkconfig文件:为项目配置文件,执行idf.py menuconfig时会创建或更新此文件,文件中保存了项目中所有组件(包括ESP-IDF本身)的配置信息。sdkconfig文件可能会也可能不会被添加到项目的源码管理系统中。
注:每个组件目录都包含一个CMakeLists.txt文件,里面会定义一些变量以控制该组件的创建过程,以及其与整个项目的继承。
项目CMakeLists文件
每个项目都会有一个顶层CMakeLists.txt文件,包含整个项目的构建设置。默认情况下,项目CMakeLists文件会非常小。
最小CMakeLists文件示例
# The following 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.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_project)
必要部分
每个项目都要按照上面显示的顺序添加上述三行代码:
- cmake_minimum_required(VERSION 3.16)必须放在CMakeLists.txt文件的第一行,它会告诉CMake构建该项目所需要的最小版本号。ESP-IDF支持CMake3.16或更高的版本。
- include($ENV{IDF_PATH}/tools/cmake/project.cmake)会导入CMake的其余功能,来完成配置项目、检索组件等任务。
- project(test_project)会创建项目本身,并指定项目名称。该名称会作为最终输出的二进制文件的名字,即test_project.bin。每个CMakeLists文件只能定义一个项目。
可选的项目变量
以下这些变量都有默认值,用户可以覆盖这些变量值来自定义构建。更多实现细节,请参阅 /tools/cmake/project.cmake 文件。
- COMPONENT_DIRS:组件的搜索目录,默认为IDF_PATH/components、PROJECT_DIR/components和EXTRA_COMPONENT_DIRS。如果不想在这些位置搜索组件,请覆盖此变量。
- EXTRA_COMPONENT_DIRS:用于搜索组件的其他可选目录列表。路径可以是相对于项目目录的相对路径,也可以是绝对路径。
- COMPONENTS:要构建进项目中的组件名称列表,默认为COMPONENT_DIRS目录下检索的所有组件。使用此变量可以“精简”项目以缩短构建时间。请注意,如果一个组件通过COMPONENT_REQUIRES执行了它依赖的另一个组件,则会自动将其添加到COMPONENTS中,所以COMPONENTS列表可能会非常短。
以上变量中的路径可以是绝对路径,或者是相对于项目目录的相对路径。
请使用 cmake 中的 set 命令 来设置这些变量,如set(EXTRA_COMPONENT_DIRS "./src")。请注意,set()命令需放在include(...)之前,cmake minimum(...)之后。
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS path)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_project)
组件CMakeLists文件
每个项目都包含一个或多个组件,这些组件可以是ESP-IDF的一部分,可以是项目自身组件目录的一部分,也可以从自定义组件目录添加。
组件是COMPONENT_DIRS列表中包含CMakeLists.txt文件的任何目录。也就是说,组件目录下,必须包含CMakeLists.txt文件。
搜索组件
搜索COMPONENTS_DIRS中的目录列表以查找项目的组件,此列表中的目录可以是组件自身(即包含CMakeList.txt文件的目录),也可以是子目录是组件顶级目录的目录。
当CMake运行项目配置时,它会记录本次构建包含的组件列表,它可用于调试某些组件的天加/排除。
同名组件
ESP-IDF在搜索所有待构建的组件时,会按照COMPONENT_DIRS指定的顺序依次进行,这意味着在默认情况下,首先搜索ESP-IDF内部组件(IDF-PATH/components),然后是EXTRA_COMPONENT_DIRS中的组件,最后是项目组件(PROJECT_DIR/components)。如果这些目录中的两个或多个包含具有相同名字的组件,则使用搜索到的最后一个位置的组件。这就允许将组件赋值到项目目录中再修改以覆盖ESP-IDF组件。如果使用这种方式,ESP-IDF目录本身可以保持不变。
注:如果在现有项目中通过将组件移动到一个新位置来覆盖它,项目不会自动看到新组建的路径。请运行idf.py reconfigure命令后再重新构建。
最小组件CMakeLists文件
最小组件CMakeLists.txt文件通过使用idf_component_register将组件添加到构建系统中。
idf_component_register(SRCS "test.c"
INCLUDE_DIRS "."
REQUIRES src)
- SRCS:是源文件列表(*.c、*.cpp、*.cs、*.s),里面所有的源文件都将会编译进组件库中。
- INCLUDEDIRS:是目录列表,里面的路径会被添加到所有需要该组件的组件(包括main组件)全局include搜索路径中。
- REQUIRES:可选。通常需要它来声明该组件需要使用哪些其他组件。参考 组件依赖。
上述命令会构建生成与组件同名的库,并最终被链接到应用程序中。
idf_component_register的其他参数可以参考here。
有关更完整的 CMakeLists.txt
示例,请参阅 组件依赖示例 和 组件 CMakeLists 示例。
组件依赖
编译各个组件时,ESP-IDF系统会递归评估其依赖项。这意味着每个组件都需要声明它所依赖的组件,即"requires"。
编写组件
idf_component_register(...
REQUIRES mbedtls
PRIV_REQUIRES console spiffs)
- REQUIRES:需要包含所有在当前组件的公共头文件里#include的头文件所在的组件。
- PRIV_REQUIRES:需要包含被当前组件的源文件#include的头文件所在的组件(除非已经被设置在REQUIRES中)。以及是当前组件正常工作必须要链接的组件。
- REQUIRES和PRIV_REQUIRES的值不能依赖于任何配置选项(CONFIG_xxx宏)。这是因为在配置加载之前,依赖关系就已经被展开了。其他组件变量(比如包含路径或源文件)可以依赖配置选择。
- 如果当前组件除了通用组件依赖项中设置的通用组件(比如RTOS、libc等)外,并不依赖其它组件,那么对于上述两个REQUIRES变量,可以选择其中一个或者两个都不设置。
注:通俗一点说,如果头文件(.h)里边include了其他组件,需要使用REQUIRES包含被需要的组件。如果源文件(.c)里边include了其他组件,需要使用PRIV_REQUIRES包含被需要的组件。
如果组件仅支持某些硬件目标(IDF_TARGET的值),则可以在idf_component_register中指定REQUIRED_IDF_TARGETS来声明这个需求。在这种情况下,如果构建系统导入了不支持当前硬件目标的组件时就会报错。
注:在CMake中,REQUIRES和PRIV_REQUIRES是CMake函数target_link_libraries(...PUBLIC...)和target_link_libraries(...PRIVATE...)的近似包装。
组件依赖示例
假设现在有一个car组件,它需要使用engine组件,而engine组件需要使用spark_plug组件:
- autoProject/
- CMakeLists.txt
- components/ - car/ - CMakeLists.txt
- car.c
- car.h
- engine/ - CMakeLists.txt
- engine.c
- include/ - engine.h
- spark_plug/ - CMakeLists.txt
- spark_plug.c
- spark_plug.h
Car组件
car.h头文件是car组件的公共接口。该头文件因为需要使用engine,h中的一些声明,直接包含了engine.h。
/* car.h */
#include "engine.h"
#ifdef ENGINE_IS_HYBRID
#define CAR_MODEL "Hybrid"
#endif
同时car.c也包含了car,h
/* car.c */
#include "car.h"
这代表文件car/CMakeLists.txt需要声明car需要engine:
idf_component_register(SRCS "car.c"
INCLUDE_DIRS "."
REQUIRES engine)
- SRCS:提供car组件中源文件列表
- INCLUDE_DIRS:提供该组件公共头文件目录列表,由于car.h是公共接口,所以这里列出了所有包含了car.h的目录
- REQUIRES:给出该组件的公共接口所需的组件列表。由于car.h是一个公共头文件并且包含了来自engine的头文件,所以这里engine文件夹。这样可以确保任何包含了car.h的其他组件也能递归地包含所需的engine.h
Engine组件
engine组件也有一个公共头文件include/engine.h,但这个头文件更简单。
/* engine.h */
#define ENGINE_IS_HYBRID
void engine_start(void);
在engine.c中执行
/* engine.c */
#include "engine.h"
#include "spark_plug.h"
...
在该组件中,engine依赖于spart_plug,但这是私有依赖关系。编译engine.c需要spart_plug.h但不需要包含engine.h。
这代表文件engine/CMakeLists.txt可以使用PRIV_REQUIRES:
idf_component_register(SRCS "engine.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES spark_plug)
因此,car组件中的源文件不需要在编译器搜索路径中添加spart_plug的inclulde目录。这可以加快编译速度,避免编译器命令行过于冗长。
Spart Plug组件
spart_plug组件没有依赖项,它有一个公共头文件spark_plug.h,但不包含其他组件的头文件。这代表spark_plug/CMakeLists.txt文件不要任何REQUIRES或PRIV_REQUIRES:
idf_component_register(SRCS "spark_plug.c"
INCLUDE_DIRS ".")
三、实战
项目名修改
我们创建一个简单的项目,先看项目树:
- DemoProject/
- main/
- CMakeLists.txt
- main.c
- CMakeLists.txt
- README.md
项目结构很简单,只有一个main文件夹。这里为什么没有sdkconfig文件呢?是因为我们还没有配置。此时我们在终端中输入idf.py menuconfig指令。
cd demo_project/
idf.py menuconfig
可以看到,当我们运行idf.py menuconfig指令后,系统就在项目目录下自动创建了sdkconfig。
此外,此时也还没有build文件夹。运行一下idf.py build.
编译成功后,项目中就自动生成了build文件夹。我们打开该文件夹。
可以看到该文件夹中,已经输出了项目bin文件。名字为“main”。根据我们在上文中介绍的:
说明项目名是在顶层CMakeLists.txt文件中设定的。打开这个文件。
project(项目名) ,此时修改项目名为“demo_project”。并重新运行idf.py build指令。并查看Build文件夹。
可以看到,这里已经生成了名为demo_project.bin的文件。但是main.bin还是存在的。这是因为每次编译,系统只会做增量的操作。如果我们不希望有上一个项目名的痕迹,可以运行如下指令:
idf.py fullclean //清除所有编译
运行结束后,可以看到build文件夹已经空了。
重新运行idf,py build编译。
此时,build文件夹中就只有修改后的项目文件了。
添加components组件
当需要添加components组件时,可以在项目中添加components文件夹,然后在该文件夹中添加自己的文件。
比如现在添加test.c自定义文件。
然后,我们在main.c中添加代码
#include "test.h"
编译。
系统报错,提示找不到test.h文件。奇怪,为什么呢? 查看概念:
对了,忘了添加CMakeLists.txt文件了。
再次提醒,组件目录下,必须包含CMakeLists.txt文件。
添加CMakeLists.txt文件,并修改为如下:
这样应该就没问题了吧。再次编译。
还报错!!!!为什么??目前来看,项目下各个文件应该都是正常的才对呀。为什么还是报错呢?再查概念。
原来如此,因为新建了components文件夹,而Cmake在我们第一次idf.py build的时候链接了所有组件,并没有发现项目下有components组件,就没有链接进来。那运行一下idf.py reconfigure指令重新链接。 可以看到,重新链接了库,再编译。
成功!!!
这里解释一下,为什么我们在项目中添加components文件夹就直接能添加自定义文件。
概念上有说,系统默认会去链接项目/comnponents文件夹。如果有的话,就自动添加到链接库中。
添加自定义组件
如果我们不想用components组件,就想使用自定义组件要怎么操作呢?比如我们要添加一个src的组件。(我们把刚才的components文件夹改名为src).
在main.c中还是#include "test.h"。
方法一、修改顶层CMakeLists.txt
此时,我们编译一下,试试。
报错!对了,是不是要重新链接一下呢?
再编译。
还是报错!!!查资料。
原来是针对自定义的组件需要添加到顶层CMakeLists.txt中。好,修改文件。
重新编译。
成功!!
方法二、通过main组件链接
因为main组件是系统默认链接的,那自然也就可以通过main组件来链接自定义组件。修改main组件的CMakeLists.txt文件。
上述语句看不懂的请查看资料。
编译。
成功!!!