esp32目录和CMAKE

ESP-IDF 项目结构与 CMake 讲解


一、ESP-IDF 项目目录结构

key 项目为例:

key/                          ← 项目根目录(相当于 STM32 的一个 Keil/CubeIDE 工程)
├── CMakeLists.txt            ← 顶层构建文件(相当于 Keil 的 .uvprojx 工程文件)
├── sdkconfig                 ← 芯片配置文件(相当于 CubeMX 生成的配置)
├── main/                     ← 主程序目录
│   ├── CMakeLists.txt        ← main 组件的构建文件
│   └── main.c                ← 程序入口,app_main() 在这里(相当于 STM32 的 main.c)
└── components/               ← 自定义组件目录(相当于你在 STM32 里写的 BSP 驱动库)
    ├── key/                  ← 按键驱动组件
    │   ├── CMakeLists.txt
    │   ├── key_app.c
    │   └── key_app.h
    └── led/                  ← LED 驱动组件
        ├── CMakeLists.txt
        ├── led_app.c
        └── led_app.h

和 STM32 的关键对比

概念 STM32 (Keil/CubeIDE) ESP-IDF
程序入口 main() app_main()(被 FreeRTOS 调用)
工程配置 .uvprojx / .ioc CMakeLists.txt + sdkconfig
外设驱动 HAL 库 / LL 库 ESP-IDF 的 driver 组件
自己写的驱动 手动加 .c/.h 到工程 放到 components/ 下,每个文件夹是一个"组件"
依赖管理 手动管理头文件路径 CMakeLists.txt 里用 REQUIRES 声明

核心概念:组件(Component)

ESP-IDF 的核心思想是一切皆组件

  • main/ 是一个特殊组件(程序入口)
  • components/led/components/key/ 是你自己写的组件
  • ESP-IDF 自带几百个组件(driver、freertos、esp_wifi 等),放在 ESP-IDF 安装目录的 components/

组件名字就是它所在文件夹的名字:

  • components/led/ → 组件名叫 led
  • components/key/ → 组件名叫 key
  • $IDF_PATH/components/driver/ → 组件名叫 driver

组件之间通过 REQUIRES 声明依赖关系,构建系统自动处理头文件路径和链接。

sdkconfig 是什么?

类似 STM32 CubeMX 里配置时钟、外设的界面。在终端运行 idf.py menuconfig 可以打开图形化配置界面,配置内容保存在 sdkconfig 里。常见配置项:

  • Flash 大小、分区表
  • WiFi/蓝牙开关
  • FreeRTOS tick 频率
  • 日志级别

二、CMake 是什么?解决什么问题?

从编译说起

假设你有一个 main.c,手动编译:

gcc main.c -o main

如果项目有 10 个 .c 文件:

gcc main.c led.c key.c uart.c spi.c i2c.c wifi.c http.c json.c utils.c -o app

问题:每次编译都要手敲这么长一串,慢且容易出错。

Makefile 解决了"手敲命令"的问题

把编译命令写成文件,用 make 一键执行。

但新问题:不同平台的编译器不一样(Linux 用 gcc,Windows 用 msvc,ESP32 用 xtensa-esp32-elf-gcc),Makefile 换个平台就废了。

CMake 解决了"跨平台"的问题

CMake 的思路:你只需要告诉我三件事:

  1. 编译哪些源文件?(SRCS)
  2. 头文件在哪里找?(INCLUDE_DIRS)
  3. 依赖哪些库?(REQUIRES)

然后 CMake 根据当前平台,自动生成对应的编译命令。

CMake 本身不编译代码,它是一个"生成构建脚本的工具"。

ESP-IDF 对 CMake 的封装

标准 CMake 要写三行:

add_library(led led_app.c)
target_include_directories(led PUBLIC .)
target_link_libraries(led driver)

ESP-IDF 封装了一个函数 idf_component_register,一行搞定:

idf_component_register(SRCS "led_app.c"
                    INCLUDE_DIRS "."
                    REQUIRES driver)

idf_component_register 不是 CMake 自带的,是 ESP-IDF 提供的快捷方式。ESP-IDF 开发只需要记住这一个函数。

本质上,CMake 就是用文本文件做了 Keil 图形界面里点点点做的事。


三、idf_component_register 三个参数详解

SRCS — 要编译的源文件

列出本组件的所有 .c 文件。不管别人用不用,只要是这个组件的 .c 文件就全部加进去。

不加到 SRCS 的 .c 文件不会被编译,里面的函数就不存在,谁也用不了。

SRCS "key_app.c" "key_init.c" "key_debounce.c"   # 3个 .c 文件全部列上

INCLUDE_DIRS — 头文件搜索目录

告诉编译器去哪些目录找 .h 文件

"." 表示当前目录。比如 components/led/CMakeLists.txt 里写 INCLUDE_DIRS ".",就是把 components/led/ 这个目录暴露为头文件搜索路径。

这个路径对所有人生效,没有"自己"和"别人"的区分:

  • 组件自己的 .c 文件编译时,能从这些目录找 .h
  • 别的组件 REQUIRES 你之后,也从这些目录找 .h

如果头文件分布在多个子目录,空格隔开列出所有目录:

INCLUDE_DIRS "." "pwm" "effect"

注意:INCLUDE_DIRS 和 SRCS 互不对应,各管各的。 一个组件可能有 5 个 .c 文件,但只有 1 个目录需要暴露。

REQUIRES — 依赖哪些组件

声明本组件依赖谁。写了 REQUIRES led,就能在代码里 #include "led_app.h" 并调用 led 的函数。

三者总结

参数 作用 类比 Keil
SRCS 本组件所有 .c 文件,全部列上去编译 把 .c 文件拖进工程树
INCLUDE_DIRS 头文件所在目录,暴露出去让所有人能找到 .h Options → C/C++ → Include Paths
REQUIRES 本组件依赖哪些其他组件 勾选要用的库

三者各管各的,互不对应。


四、三个 CMakeLists.txt 各自的作用

1. 顶层 key/CMakeLists.txt — 定义项目名称

cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)  # 引入 ESP-IDF 构建系统
project(key)                                         # 项目名叫 "key"

基本不用改,每个新项目复制一份改个项目名就行。

2. main/CMakeLists.txt — 注册 main 组件

idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS ""
                    REQUIRES led key freertos)

3. components/led/CMakeLists.txt — 注册 led 组件

idf_component_register(SRCS "led_app.c"
                    INCLUDE_DIRS "."
                    REQUIRES driver)

五、完整构建流程(核心!)

执行 idf.py build 后发生的事:

第一阶段:CMake 登场(调度员)

CMake 读取所有 CMakeLists.txt,搞清楚谁需要谁,把路径整理好。

第1步:读 components/led/CMakeLists.txt
       → 记录:led 组件,头文件目录是 components/led/

第2步:读 components/key/CMakeLists.txt
       → 记录:key 组件,头文件目录是 components/key/

第3步:读 main/CMakeLists.txt
       → 发现 REQUIRES led key
       → 查表:led 的头文件目录是 components/led/
       → 查表:key 的头文件目录是 components/key/
       → 结论:编译 main.c 时需要搜索这两个目录

第4步:生成编译命令:
       gcc main.c -I components/led/ -I components/key/

CMake 工作结束,退场。从此不再参与。

第二阶段:编译器登场(工人)

编译器拿到的东西很简单:

gcc main.c -I components/led/ -I components/key/

它不知道什么 CMake,什么组件,什么 REQUIRES。它只知道:

  • 要编译 main.c
  • 找头文件时去 components/led/components/key/ 这两个目录找

编译器读 main.c,遇到 #include "led_app.h"

去 components/led/ 找 led_app.h → 找到了 → 搞定

编译器眼里没有"组件"的概念,没有 CMakeLists.txt 的概念,它只有一堆目录路径和一个文件名。

如果 INCLUDE_DIRS 列了多个目录,编译器就挨个目录试,先找到谁就用谁。跟你在电脑上找文件一样——一个文件夹一个文件夹打开看。

流程一句话总结

你写 CMakeLists.txt(声明依赖关系)
        ↓
CMake 读它们,把路径整理成编译命令(-I 参数)
        ↓
编译器拿着命令干活,按 -I 路径找头文件,编译代码
角色 干什么 知道什么
写 CMakeLists.txt 和代码 知道谁依赖谁
CMake 读 CMakeLists.txt,生成编译命令 知道所有组件的路径和依赖关系
编译器 按命令编译代码 只知道"一堆目录路径 + 文件名"

六、实际开发顺序

先写 CMakeLists.txt,再写 #include。 CMakeLists.txt 是基础设施,#include 建立在它之上。先修路,再通车。

第1步:你想在 main.c 里用 led 的功能
        ↓
第2步:在 main/CMakeLists.txt 里写 REQUIRES led
       (告诉 CMake:我需要 led 组件)
        ↓
第3步:在 main.c 里写 #include "led_app.h"
       (因为第2步已经让编译器能找到这个头文件了)
        ↓
第4步:在 main.c 里调用 led 的函数

如果跳过第2步直接写第3步,编译报错:

fatal error: led_app.h: No such file or directory

因为没人告诉编译器去哪里找这个文件。


七、常见场景

组件有多个 .c 文件

idf_component_register(SRCS "led_app.c" "led_pwm.c" "led_effect.c"
                    INCLUDE_DIRS "."
                    REQUIRES driver)

SRCS 后面列出所有 .c 文件。如果文件都在同一个目录下,INCLUDE_DIRS "." 不用改。

组件有子目录

components/led/
├── CMakeLists.txt
├── led_app.c
├── led_app.h
├── pwm/
│   ├── led_pwm.c
│   └── led_pwm.h
└── effect/
    ├── led_effect.c
    └── led_effect.h
idf_component_register(SRCS "led_app.c" "pwm/led_pwm.c" "effect/led_effect.c"
                    INCLUDE_DIRS "." "pwm" "effect"
                    REQUIRES driver)

SRCSINCLUDE_DIRS 的顺序不需要对应,各管各的。

不过实际开发中,一个组件的文件通常放在同一个目录下。如果复杂到需要分很多子目录,通常说明应该拆成多个组件了。

组件之间互相调用

key 组件要控制 LED:

# components/key/CMakeLists.txt
idf_component_register(SRCS "key_app.c"
                    INCLUDE_DIRS "."
                    REQUIRES driver led)   # ← 加上 led

加了 REQUIRES led 后,key_app.c 里就可以直接 #include "led_app.h"