第十八章:嵌入式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/目录里。结果项目一膨胀,编译速度慢得像蜗牛,而且代码耦合得一塌糊涂。后来老老实实拆成组件,每个传感器一个独立组件,编译快了,维护也轻松了。

核心原则: 一个组件只做一件事,并且做好。比如WiFi管理、传感器驱动、显示控制,各归各的组件。

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,
注意: 分区表的偏移地址必须对齐到0x10000(64KB)边界。我曾经因为偏移写成了0x9000,结果编译不报错,但烧录后系统死活起不来。排查了半天才发现是分区对齐问题。

18.4 在CMake中指定分区表

分区表文件写好了,怎么告诉CMake用哪个?有两种方式:

  1. 通过menuconfig:运行idf.py menuconfig,在"Partition Table"选项里选择"Custom partition table CSV",然后指定你的partitions.csv路径
  2. 直接在CMake中设置:在项目根CMakeLists.txt里加一行
# 在 project() 之前设置
set(PARTITION_TABLE_CSV "partitions.csv")

我个人更推荐第二种方式。因为这样分区表配置就跟着代码走了,换电脑、换同事都不怕忘记配置。

18.5 知识体系总览

下面这张图,把ESP-IDF的CMake集成、组件管理和分区表配置串起来了。你一看就明白:

ESP-IDF CMake 项目核心结构 项目根 CMakeLists.txt main/ 组件 components/ 组件 partitions.csv CMakeLists.txt (idf_component_register) CMakeLists.txt (idf_component_register) nvs, otadata, phy_init factory, ota_0, ota_1 idf.py build → 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