第十八章:嵌入式MCU项目实战(ESP32):ESP-IDF与CMake集成、组件管理、分区表配置
说实话,ESP32是我这几年用得最多的MCU之一。它的双核架构、WiFi/BLE集成,再加上乐鑫官方力推的ESP-IDF框架,让嵌入式开发变得相当现代化。而ESP-IDF从4.0版本开始全面拥抱CMake,这对我来说简直是福音——终于不用再跟那些晦涩的Makefile死磕了。
这一章,我就带你手把手过一遍ESP-IDF的CMake集成、组件管理,还有那个容易踩坑的分区表配置。嗯,都是实战干货。
18.1 ESP-IDF的CMake项目结构
先看看一个典型的ESP-IDF项目长什么样:
my_esp_project/
├── CMakeLists.txt # 项目级CMake文件
├── main/
│ ├── CMakeLists.txt # 主组件CMake文件
│ ├── app_main.c
│ └── app_main.h
├── components/ # 自定义组件目录
│ ├── my_sensor/
│ │ ├── CMakeLists.txt
│ │ ├── sensor.c
│ │ └── sensor.h
│ └── my_display/
│ ├── CMakeLists.txt
│ ├── display.c
│ └── display.h
├── partitions.csv # 分区表文件
└── sdkconfig # 配置缓存
我个人习惯把项目根目录的CMakeLists.txt写得尽量简洁。它其实就干三件事:指定CMake最低版本、引入ESP-IDF框架、设置项目名。
# 项目根 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_esp_project)
你看,就这么几行。真正的魔法都在project.cmake里——它会自动扫描main/和components/目录下的所有组件,并帮你处理依赖关系。
IDF_PATH,指向你安装的ESP-IDF路径。不然CMake会报错说找不到 project.cmake。
18.2 组件管理:模块化你的代码
ESP-IDF的组件(component)机制,说白了就是CMake的add_subdirectory加上一些自动化扫描。每个组件都是一个独立的模块,有自己的CMakeLists.txt和源文件。
来看一个组件的CMake文件怎么写:
# components/my_sensor/CMakeLists.txt
idf_component_register(
SRCS "sensor.c"
INCLUDE_DIRS "."
REQUIRES driver
PRIV_REQUIRES esp_timer
)
这里有几个关键点:
- SRCS:列出组件的源文件
- INCLUDE_DIRS:头文件搜索路径,通常就是当前目录
- REQUIRES:公开依赖,其他组件也能用这些依赖的头文件
- PRIV_REQUIRES:私有依赖,只有本组件内部能用
我曾经在一个项目中,把所有传感器驱动都塞进main/目录里。结果项目一膨胀,编译速度慢得像蜗牛,而且代码耦合得一塌糊涂。后来老老实实拆成组件,每个传感器一个独立组件,编译快了,维护也轻松了。
18.3 分区表配置:别让Flash空间浪费了
分区表是ESP32项目里最容易忽略、也最容易出问题的地方。它决定了Flash的布局:代码放哪、文件系统放哪、OTA升级怎么搞。
默认的分区表长这样:
# ESP-IDF 默认分区表 (4MB Flash)
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000,1M,
ota_1, app, ota_1, 0x210000,1M,
但如果你不需要OTA,或者Flash只有2MB,就得自己定制了。我建议你根据实际需求来:
- 只需要固件更新:保留factory分区,去掉ota_0和ota_1,省出2MB空间
- 需要OTA:至少保留两个OTA分区,大小要能装下最大固件
- 需要文件系统:在末尾加一个spiffs或fat分区
举个例子,一个2MB Flash、带文件系统的分区表:
# 2MB Flash 定制分区表
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0xE0000,
storage, data, spiffs, 0xF0000, 0x100000,
18.4 在CMake中指定分区表
分区表文件写好了,怎么告诉CMake用哪个?有两种方式:
- 通过menuconfig:运行
idf.py menuconfig,在"Partition Table"选项里选择"Custom partition table CSV",然后指定你的partitions.csv路径 - 直接在CMake中设置:在项目根
CMakeLists.txt里加一行
# 在 project() 之前设置
set(PARTITION_TABLE_CSV "partitions.csv")
我个人更推荐第二种方式。因为这样分区表配置就跟着代码走了,换电脑、换同事都不怕忘记配置。
18.5 知识体系总览
下面这张图,把ESP-IDF的CMake集成、组件管理和分区表配置串起来了。你一看就明白:
18.6 实战:从零搭建一个带OTA的ESP32项目
好了,理论说完了,咱们来点实际的。假设你要做一个远程升级的传感器节点,需要OTA功能,还要存一些校准数据。我会这么做:
第一步:创建项目结构
mkdir esp_sensor_node
cd esp_sensor_node
touch CMakeLists.txt
mkdir main components partitions
touch main/CMakeLists.txt main/app_main.c
touch partitions/partitions.csv
第二步:写分区表
# partitions/partitions.csv (4MB Flash, 带OTA)
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000,1M,
ota_1, app, ota_1, 0x210000,1M,
calib, data, nvs, 0x310000,0x10000,
注意我加了一个calib分区,专门存校准数据。这样OTA升级时不会把校准数据冲掉。
第三步:配置CMake
# 根 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(PARTITION_TABLE_CSV "partitions/partitions.csv")
project(esp_sensor_node)
第四步:写主程序
// main/app_main.c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_ota_ops.h"
void app_main(void)
{
// 初始化NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
const esp_partition_t* partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, NULL);
if (partition != NULL) {
ESP_ERROR_CHECK(esp_partition_erase_range(partition, 0, partition->size));
ret = nvs_flash_init();
}
}
ESP_ERROR_CHECK(ret);
printf("系统启动成功!\n");
printf("当前运行分区: %s\n", esp_ota_get_running_partition()->label);
while (1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
esp_ota_get_running_partition() 可以知道当前跑的是哪个分区。这在调试OTA时特别有用。
18.7 避坑指南
最后,分享几个我踩过的坑:
- 分区表大小别算错:所有分区大小加起来不能超过Flash总容量。我见过有人把4MB Flash的分区表加起来算了4.5MB,结果编译通过,烧录后直接变砖。
- 组件依赖别循环:组件A依赖B,B又依赖A,CMake会报循环依赖错误。解决办法是拆组件,或者把公共部分抽成第三个组件。
- 头文件路径别漏:如果组件A要引用组件B的头文件,B的
INCLUDE_DIRS必须包含那个头文件目录,并且A的REQUIRES要写上B。
嗯,这一章的内容就到这。ESP-IDF的CMake集成其实不复杂,但细节不少。你只要把组件结构搭好、分区表算清楚,后面开发就会顺畅很多。
公众号:蓝海资料掘金营,微信deep3321