一、硬件配置

ESP32-CAM:

  • 核心貌似是ESP-32S(ESP32-WROOM-32)模组

  • 两个低功耗 Xtensa® 32-bit LX6 MCU,主频高达 240MHz, 运算能力高达 600 DMIPS,小端序

    • 字节地址 0x0、 0x1、 0x2、 0x3 访问的字节分别是 0x0 访问的 32-bit字中的最低、次低、次高、最高字节
  • RAM:片内520 KB SRAM,片外8MB PSRAM

    • RTC中8 KB SRAM(RTC慢速存储器),Deep-sleep模式下被协处理器访问
    • RTC中8 KB SRAM(RTC快速存储器),Deep-sleep模式下RTC启动时用于数据存储以及被主CPU访问
    • 1kbit eFuse,其中256bit为系统专用(MAC 地址和芯片设置)其余 768bit保留给用户应用,包括Flash加密和芯片ID
  • ROM:片内448 KB ROM(用于程序启动和内核功能调用),片外32Mbit SPI Flash(4MB

  • 支持UART、 SPI、 I2C、 PWM

  • TF卡最大支持4G

  • 供电:4.75-5.25V

  • 购买链接

  • 引脚定义:

    引脚定义

Goouuu-ESP32:

  • ESP32-WROOM-32模组(ESP32-D0WDQ6(revision 1))
  • 两个低功耗 Xtensa® 32-bit LX6 MCU,主频高达 240MHz, 运算能力高达 600 DMIPS,小端序
    • 字节地址 0x0、 0x1、 0x2、 0x3 访问的字节分别是 0x0 访问的 32-bit字中的最低、次低、次高、最高字节
  • 购买链接

ESP32-Audio-kit(安信可官方SDK):

  • ESP32-A1S模组

  • 音频芯片:AC101

  • IIC:SCL(32)、SDA(33)

  • I2S:SCLK(bck 27)、LCLK(ws 26)、DSIN(data_out 25)、DOUT(data_in 35)

  • LED:GREEN(22)

  • SD:INTR_GPIO(GPIO_NUM_34)、INTR_SEL(GPIO_SEL_34)

  • PA:EN(GPIO_NUM_21)、SEL_PA_EN(GPIO_SEL_21)

  • 按键:REC(GPIO_NUM_36)、MODE(GPIO_NUM_13)、SET(GPIO_NUM_19)、PLAY(GPIO_NUM_23)、VOLUP(GPIO_NUM_18)、VOLDOWN(GPIO_NUM_5)

  • 模块内部具体引脚:

    esp_audio_pin

二、软件配置

三、搭建开发环境(Windows10)

官方参考链接

3.1 准备工作

目前,ESP-IDF 仅适用于 Python 2.7

附件:

  • mconf-v4.6.0.0-idf-20190628-win32.zip:mconf-idf
  • ninja-win.zip:ninja
  • cmake-3.17.2-win64-x64.msi:cmake安装包
  • xtensa-esp32-elf-gcc8_2_0-esp-2019r2-win32.zip:编译工具链
  • pyelftools-0.26-py2.py3-none-any.whl:pyelftools包

步骤:

  1. 下载安装Cmake(安装时注意选上Add CMake to the system PATH for all users

  2. 下载安装ninja(手动加到系统环境变量,只支持64位windows,如需其他版本需自己编译)

  3. 下载安装python2.7并通过pip install --user pyserial安装pyserial包(可以通过Anaconda新建环境安装)

  4. 这里下载配置工具mconf-idf(需要手动加到系统环境变量)

  5. 下载交叉编译工具链,解压之后需要添加到系统环境变量,如C:\Program Files\xtensa-esp32-elf\bin

  6. 下载安装Git

  7. 下载ESP-IDF(3.3.2为长期支持版,最新稳定版为4.0)

  8. 设置环境变量:

    1. IDF_PATH 应设置为 ESP-IDF 根目录的路径
    2. PATH 应包括同一 IDF_PATH 目录下的 tools 目录路径(为了使用idf.py 工具,%IDF_PATH%\tools即可)
  9. 根据IDF_PATH/requirements.txt文件安装Python软件包:

    1
    2
    3
    # 注意查询所使用的Python版本(运行命令python --version),并根据查询结果将python替换为python2或python2.7等
    python -m pip install --user -r $IDF_PATH/requirements.txt # 使用pip安装
    # 也可以使用anaconda安装

    安装内容:

    1
    2
    3
    4
    5
    6
    click>=5.0
    pyserial>=3.0
    future>=0.15.2
    cryptography>=2.1.4
    pyparsing>=2.0.3,<2.4.0
    pyelftools>=0.22
    • 其中pyelftools软件包在anaconda中没有,需要手动从这里下载.whl文件,在Anaconda Prompt中通过命令pip install E:\xxx\xxx.whl安装
      • 也可以在conda官网https://anaconda.org/中搜索pyelftools软件包,并通过软件包对应命令进行安装(推荐)

3.2 创建工程

ESP-IDF 的examples目录下有一系列示例工程,可以复制到其他地方进行运行(推荐),也可以直接编译示例,无需进行复制。

注意:ESP-IDF 编译系统不支持带有空格的路径

四、搭建开发环境(ubuntu18.04)

4.1 IDF框架

详见官方安装准备链接快速入门

  1. 安装必备软件:

    1
    2
    sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev
    sudo apt-get install python3 python3-pip python3-setuptools
  2. 获取ESP-IDF

1
2
3
4
#通过git
git clone -b v4.0.1 --recursive https://github.com/espressif/esp-idf.git
#或通过该链接:https://dl.espressif.com/dl/esp-idf/releases/esp-idf-v4.0.1.zip下载压缩包再解压
# unzip esp-idf-v4.0.1.zip
  1. 设置工具(编译器、调试器、Python 包等)

    1
    ./install.sh
    • 默认安装在用户根文件夹中,Linux中为 $HOME/.espressif
  2. 设置环境变量

1
. export.sh
  • 增加至.profile.bash_profile 脚本即可在任何命令窗口使用 ESP-IDF 工具
  • 每次都需要配置,除非添加到.profile.bash_profile 脚本中
  1. 创建工程

    1
    cp -r $IDF_PATH/examples/get-started/hello_world .
  2. 连接设备

    1
    ls /dev/ttyUSB*
    • 需要具有访问串口权限

      1. 方法一:添加串口设备访问规则

        1
        2
        sudo vim /etc/udev/rules.d/70-ttyusb.rules
        KERNEL=="ttyUSB[0-9]*",MODE="0666"
      2. 方法二:将目标用户添加至dialout用户组

        1
        2
        3
        sudo usermod -a -G dialout $USER
        # 检查用户组
        groups $USER
  3. 配置工程

    1
    2
    cd ~/esp/hello_world
    idf.py menuconfig
  4. 编译

    1
    idf.py build
    • 编译应用程序和所有 ESP-IDF 组件,接着生成 bootloader、分区表和应用程序二进制文件
  5. 烧录

    1
    idf.py -p PORT [-b BAUD] flash
    • -p:串口对应的端口
    • -b:烧录波特率,默认460800
    • flash:自动编译并烧录(无需再次执行idf.py build)
  6. 监视器

    1
    idf.py -p PORT monitor
    • -p:串口对应的端口
    • 快捷键 Ctrl+]:退出 IDF 监视器
    • 更多用法详见官方文档

如遇到特殊情况需要擦除flash:

1
2
3
idf.py erase_flash
# 或
esptool.py erase_flash

4.2 ADF框架

详见官方安装准备

  1. 安装好IDF框架

  2. 获取ADF

    1
    2
    cd ~/esp # ADF的安装目录,不支持空格
    git clone --recursive https://github.com/espressif/esp-adf.git
  3. 设置ADF路径ADF_PATH

    1
    2
    export ADF_PATH=~/esp/esp-adf # 设置
    printenv ADF_PATH # 检查
    • 每次打开终端都需要重新设置
  4. 设置环境变量

    1
    . export.sh # 设置IDF的环境变量(需要进入IDF目录)
  5. 创建工程

    1
    2
    cd ~/esp
    cp -r $ADF_PATH/examples/get-started/play_mp3 . # 拷贝demo
  6. 连接设备

  7. 配置项目

    1
    2
    3
    cd ~/esp/play_mp3 # 进入对应项目
    #idf.py set-target esp32 # IDF4.1及之后版本需要选择目标芯片
    idf.py menuconfig # 配置
  8. 构建工程

    1
    idf.py build # 编译所有IDF、ADF组件,并生成BootLoader、分区表和二进制应用文件
  9. 烧写到设备上

    1
    idf.py -p PORT [-b BAUD] flash monitor
    • PORT:目标板卡串口号
    • BAUD:烧写波特率,默认460800
    • flash:会自动构建工程并烧写整个项目,此时可以无需idf.py build命令
    • 烧写时,板卡应该进入上传模式(upload mode):按下boot按钮->按下reset按钮->松开boot按钮
  10. 升级ADF(需要能够正常访问github)

    1
    2
    3
    cd ~/esp/esp-adf # 进入adf安装目录
    git pull # 获取合并和更改
    git submodule update --init --recursive # 更新现有子模块或获取新的子模块的副本

4.3 监视器使用

具体详见官方文档

快捷键 操作 描述
Ctrl+] 退出监视器程序
Ctrl+T 菜单退出键 按下如下给出的任意键,并按指示操作。
Ctrl+T 将菜单字符发送至远程
Ctrl+] 将 exit 字符发送至远程
Ctrl+P 重置目标设备,进入 Bootloader,通过 RTS 线暂停应用程序 重置目标设备,通过 RTS 线(如已连接)进入 Bootloader,此时开发板不运行任何程序。等待其他设备启动时可以使用此操作。
Ctrl+R 通过 RTS 线重置目标设备 重置设备,并通过 RTS 线(如已连接)重新启动应用程序。
Ctrl+F 编译并烧录此项目 暂停 idf_monitor,运行 idf.py flash 目标,然后恢复 idf_monitor。任何改动的源文件都会被重新编译,然后重新烧录。
Ctrl+A (A) 仅编译及烧录应用程序 暂停 idf_monitor,运行 app-flash 目标,然后恢复 idf_monitor。 这与 flash 类似,但只有主应用程序被编译并被重新烧录。
Ctrl+Y 停止/恢复日志输出在屏幕上打印 激活时,会丢弃所有传入的串行数据。允许在不退出监视器的情况下快速暂停和检查日志输出。
Ctrl+L 停止/恢复向文件写入日志输出 在工程目录下创建一个文件,用于写入日志输出。可使用快捷键停止/恢复该功能(退出 IDF 监视器也会终止该功能)
Ctrl+H (H) 显示所有快捷键
  • 除了 Ctrl-]Ctrl-T,其他快捷键信号会通过串口发送到目标设备。

五、一般注意事项

启动日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
ets Jun  8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:6932
load:0x40078000,len:14076
load:0x40080400,len:4304
entry 0x400806e8
I (71) boot: Chip Revision: 1
I (71) boot_comm: chip revision: 1, min. bootloader chip revision: 0
I (39) boot: ESP-IDF v4.0.1-dirty 2nd stage bootloader
I (39) boot: compile time 16:01:45
I (40) boot: Enabling RNG early entropy source...
I (45) boot: SPI Speed : 40MHz
I (49) boot: SPI Mode : DIO
I (53) boot: SPI Flash Size : 2MB
I (57) boot: Partition Table:
I (61) boot: ## Label Usage Type ST Offset Length
I (68) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (75) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (83) boot: 2 factory factory app 00 00 00010000 00100000
I (90) boot: End of partition table
I (94) boot_comm: chip revision: 1, min. application chip revision: 0
I (102) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x059b0 ( 22960) map
I (119) esp_image: segment 1: paddr=0x000159d8 vaddr=0x3ffb0000 size=0x02110 ( 8464) load
I (123) esp_image: segment 2: paddr=0x00017af0 vaddr=0x40080000 size=0x00400 ( 1024) load
0x40080000: _WindowOverflow4 at /home/null/Code/ESP32/esp-idf-4.0.1/components/freertos/xtensa_vectors.S:1778

I (129) esp_image: segment 3: paddr=0x00017ef8 vaddr=0x40080400 size=0x08118 ( 33048) load
I (151) esp_image: segment 4: paddr=0x00020018 vaddr=0x400d0018 size=0x12ca0 ( 76960) map
0x400d0018: _stext at ??:?

I (179) esp_image: segment 5: paddr=0x00032cc0 vaddr=0x40088518 size=0x0156c ( 5484) load
0x40088518: portENTER_CRITICAL_NESTED at /home/null/Code/ESP32/esp-idf-4.0.1/components/freertos/include/freertos/portmacro.h:324
(inlined by) xTaskGetSchedulerState at /home/null/Code/ESP32/esp-idf-4.0.1/components/freertos/tasks.c:3997

I (188) boot: Loaded app from partition at offset 0x10000
I (188) boot: Disabling RNG early entropy source...
I (188) cpu_start: Pro cpu up.
I (192) cpu_start: Application information:
I (197) cpu_start: Project name: hello-world
I (202) cpu_start: App version: 1
I (207) cpu_start: Compile time: Jul 2 2020 16:01:39
I (213) cpu_start: ELF file SHA256: 0d908999c91a5c41...
I (219) cpu_start: ESP-IDF: v4.0.1-dirty
I (224) cpu_start: Starting app cpu, entry point is 0x40081038
0x40081038: call_start_cpu1 at /home/null/Code/ESP32/esp-idf-4.0.1/components/esp32/cpu_start.c:271

I (211) cpu_start: App cpu up.
I (235) heap_init: Initializing. RAM available for dynamic allocation:
I (242) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (248) heap_init: At 3FFB3110 len 0002CEF0 (179 KiB): DRAM
I (254) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (260) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (267) heap_init: At 40089A84 len 0001657C (89 KiB): IRAM
I (273) cpu_start: Pro cpu start user code
I (291) spi_flash: detected chip: generic
I (292) spi_flash: flash io: dio
W (292) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (302) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Hello world!

其中默认分区表(默认位置为Flash上0x8000):

1
2
3
4
5
6
7
8
*******************************************************************************
# Espressif ESP32 Partition Table
# Name, Type, SubType, Offset, Size, Flags
# 标签, 类型 , 子类型 , 加载地址, 大小, 是否加密
nvs,data,nvs,0x9000,24K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
*******************************************************************************
  • data:存储 NVS 库专用分区,flash 的0x9000处,24K大小
    • 存储每台设备的 PHY 校准数据
    • 存储 Wi-Fi 数据
    • 其他应用程序数据
  • data:PHY 初始化数据,flash 的0xf000处,4k大小
  • app:应用程序,flash 的0x10000(64k)处,1M大小(Bootloader 将默认加载这个应用程序,app区始终会加密)
1
2
3
4
5
load:0x3fff0018,len:4
load:0x3fff001c,len:6932
load:0x40078000,len:14076
load:0x40080400,len:4304
entry 0x400806e8

5.1 应用程序启动流程

宏观上,该启动流程可以分为如下 3 个步骤:

  1. 一级引导程序被固化在了 ESP32 内部的 ROM 中,它会从 Flash 的 0x1000 偏移地址处加载二级引导程序至 RAM(IRAM & DRAM) 中
  2. 二级引导程序从 Flash 中加载分区表和主程序镜像至内存中,主程序中包含了 RAM 段和通过 Flash 高速缓存映射的只读段
  3. 主程序运行,这时第二个 CPU 和 RTOS 的调度器可以开始运行

具体如下:

启动流程

5.2 应用程序内存布局方案

ESP32的地址映射结构如下所示:

地址映射结构

  • 小端序:字节地址 0x0、 0x1、 0x2、 0x3 访问的字节分别是 0x0 访问的 32-bit字中的最低、次低、次高、最高字节
  • CPU通过指令总线访问数据必须字对齐,数据总线无要求
总线类型 低位地址 高位地址 容量 目标
0x0000_ 0000 0x3F3F_FFFF 保留
数据 0x3F40_0000 0x3F7F_FFFF 4 MB 片外存储器
数据 0x3F80_0000 0x3FBF_FFFF 4 MB 片外存储器
0x3FC0_0000 0x3FEF_FFFF 3 MB 保留
数据 0x3FF0_0000 0x3FF7_FFFF 512 KB 外设
数据 0x3FF8_0000 Ox3FFF_FFFF 512 KB 片上存储器
指令 0x4000_0000 0x400C_1FFF 776 KB 片上存储器
指令 0x400C_2000 0x40BF_FFFF 11512 KB 片外存储器
0x40C0_0000 0x4FFF_FFFF 244 MB 保留
数据/指令 0x5000_0000 0x5000_1FFF 8 KB 片上存储器
0x5000_2000 0xFFFF_FFFF 保留

对于外部Flash而言(采用默认分区表):

Flash布局(默认分区表)

ESP-IDF 应用程序的代码可以放在:

  • IRAM(指令RAM)

    ESP-IDF 将内部 SRAM0 区域(在技术参考手册中有定义)的一部分分配为指令RAM。除了开始的 64kB 用作 PRO CPU 和 APP CPU 的高速缓存外,剩余内存区域(从 0x400800000x400A0000 )被用来存储应用程序中部分需要在RAM中运行的代码

    一些 ESP-IDF 的组件和 WiFi 协议栈的部分代码通过链接脚本文件被存放到了这块内存区域

    如果一些应用程序的代码需要放在 IRAM 中运行,可以使用 IRAM_ATTR 宏定义进行声明

  • IROM(代码从Flash中运行)

    如果一个函数没有被显式地声明放在 IRAM 或者 RTC 内存中,则将其置于 Flash 中

  • RTC快速内存

    从深度睡眠模式唤醒后必须要运行的代码要放在 RTC 内存中,更多信息请查阅官方文档 深度睡眠

  • DRAM(数据RAM)

    链接器将非常量静态数据和零初始化数据放入 0x3FFB0000 — 0x3FFF0000 这 256kB 的区域。注意,如果使用蓝牙堆栈,此区域会减少 64kB(通过将起始地址移至 0x3FFC0000 )。如果使用了内存跟踪的功能,该区域的长度还要减少 16kB 或者 32kB。放置静态数据后,留在此区域中的剩余空间都用作运行时堆。

    常量数据也可以放在 DRAM 中,需要使用 DRAM_ATTR 宏来声明。

  • DROM(数据存储在Flash中)

    默认情况下,链接器将常量数据放入一个 4MB 区域 (0x3F400000 — 0x3F800000) ,该区域用于通过 Flash MMU 和高速缓存来访问外部 Flash。一种特例情况是,字面量会被编译器嵌入到应用程序代码中。

  • RTC慢速内存

    从 RTC 内存运行的代码(例如深度睡眠模块的代码)使用的全局和静态变量必须要放在 RTC 慢速内存中。更多详细说明请查看官方文档 深度睡眠

    RTC_NOINIT_ATTR 用来声明将数据放入 RTC 慢速内存中,该数据在深度睡眠唤醒后将保持不变。

DMA能力要求

大多数的 DMA 控制器(比如 SPI,SDMMC 等)都要求发送/接收缓冲区放在 DRAM 中,并且按字对齐。建议将 DMA 缓冲区放在静态变量中而不是堆栈中。使用 DMA_ATTR 宏可以声明该全局/本地的静态变量具备 DMA 能力(推荐全局)

六、构建系统(CMake)

6.1 基本概念

  • 项目 特指一个目录,其中包含了构建可执行应用程序所需的全部文件和配置,以及其他支持型文件,例如分区表、数据/文件系统分区和引导程序。
  • 项目配置 保存在项目根目录下名为 sdkconfig 的文件中,可以通过 idf.py menuconfig 进行修改,且一个项目只能包含一个项目配置。
  • 应用程序 是由 ESP-IDF 构建得到的可执行文件。一个项目通常会构建两个应用程序:项目应用程序(可执行的主文件,即用户自定义的固件)和引导程序(启动并初始化项目应用程序)。
  • 组件 是模块化且独立的代码,会被编译成静态库(.a 文件)并链接到应用程序。部分组件由 ESP-IDF 官方提供,其他组件则来源于其它开源项目。
  • 目标 特指运行构建后应用程序的硬件设备。ESP-IDF 当前仅支持 ESP32 这一个硬件目标。

6.2 idf.py使用

idf.py 命令行工具提供了一个前端,可以帮助您轻松管理项目的构建过程,它管理了以下工具:

  • CMake,配置待构建的系统
  • 命令行构建工具(Ninja 或 GNU Make)
  • esptool.py,烧录 ESP32

idf.py 应运行在 ESP-IDF 的 项目 目录下,即包含 CMakeLists.txt 文件的目录


常用的命令:

  • idf.py menuconfig:运行menuconfig配置项目

  • idf.py build构建在当前目录下的项目,包括:

    • 根据需要创建 build 构建目录,用于保存输出文件,可以使用 -B 选项修改默认的构建目录
    • 根据需要运行 CMake 配置命令,为主构建工具生成构建文件
    • 运行主构建工具(Ninja 或 GNU Make)。默认情况下,构建工具会被自动检测,可以使用 -G 选项显式地指定构建工具

    如果自上次构建以来源文件或项目配置没有发生改变,则不会执行任何操作

  • idf.py clean:把输出文件从构建目录中删除,从而清理整个项目。下次构建时会强制“重新完整构建”这个项目。清理时,不会删除 CMake 配置输出及其他文件

  • idf.py fullclean 会将整个 build 目录下的内容全部删除,包括所有 CMake 的配置输出文件。下次构建项目时,CMake 会从头开始配置项目。请注意,该命令会递归删除构建目录下的 所有文件,请谨慎使用。项目配置文件不会被删除

  • idf.py flash 会在必要时自动构建项目,并将生成的二进制程序烧录进ESP32中。-p-b 选项可分别设置串口的设备名和烧录时的波特率

  • idf.py monitor 用于显示 ESP32 设备的串口输出。-p 选项可用于设置主机端串口的设备名,按下 Ctrl-] 可退出监视器。更多有关监视器的详情,请参阅官方 IDF 监视器

多个 idf.py 命令可合并成一个

环境变量 ESPPORTESPBAUD 可分别用作 -p-b 选项的默认值。在命令行中,重新为这两个选项赋值,会覆盖其默认值


高级命令:

  • idf.py appidf.py bootloaderidf.py partition_table:从项目中构建应用程序、引导程序或分区表
  • idf.py app-flash 等匹配命令:将特定部分烧录至 ESP32
  • idf.py -p PORT erase_flash:使用 esptool.py 擦除整个 Flash
  • idf.py size:打印应用程序相关的大小信息,idf.py size-componentsidf.py size-files 这两个命令分别用于打印每个组件或源文件的详细信息
  • idf.py reconfigure:重新运行CMake(即便无需重新运行)。正常使用时,并不需要运行此命令,但当源码树中添加/删除文件后或更改 CMake cache 变量时,此命令会非常有用,例如,idf.py -DNAME='VALUE' reconfigure 会将 CMake cache 中的变量 NAME 的值设置为 VALUE

同时调用多个 idf.py 命令时,命令的输入顺序并不重要,它们会按照正确的顺序依次执行,并保证每一条命令都生效(即先构建后烧录,先擦除后烧录等)

6.3 CMakeLists文件

6.3.1 项目CMakeLists

每个项目都有一个顶层 CMakeLists.txt 文件,包含整个项目的构建设置。默认情况下,项目 CMakeLists 文件会非常小:

1
2
3
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(myProject)
  • cmake_minimum_required(VERSION 3.5) 必须放在 CMakeLists.txt 文件的第一行,告诉 CMake 构建该项目所需要的最小版本号
  • include($ENV{IDF_PATH}/tools/cmake/project.cmake) 导入 CMake 的其余功能来完成配置项目、检索组件等任务
  • project(myProject) 创建项目本身,并指定项目名称。该名称会作为最终输出的二进制文件的名字,即 myProject.elfmyProject.bin。每个 CMakeLists 文件只能定义一个项目

默认变量(用户可以覆盖这些变量值以自定义构建行为(更多实现细节,请参阅 /tools/cmake/project.cmake 文件)):

  • COMPONENT_DIRS:组件的搜索目录,默认为 ${IDF_PATH}/components${PROJECT_PATH}/componentsEXTRA_COMPONENT_DIRS
  • EXTRA_COMPONENT_DIRS:搜索组件的其它可选目录列表
  • COMPONENTS:要构建进项目中的组件名称列表,默认为 COMPONENT_DIRS 目录下检索到的所有组件。使用此变量可以“精简”项目以缩短构建时间。请注意,如果一个组件通过 COMPONENT_REQUIRES 指定了它依赖的另一个组件,则会自动将其添加到 COMPONENTS 中,所以 COMPONENTS 列表可能会非常短。
  • COMPONENT_REQUIRES_COMMON:每个组件都需要的通用组件列表,这些通用组件会自动添加到每个组件的 COMPONENT_PRIV_REQUIRES 列表中以及项目的 COMPONENTS 列表中。默认情况下,此变量设置为 ESP-IDF 项目所需的最小核心“系统”组件集。通常您无需在项目中更改此变量

以上变量中的路径可以是绝对路径,或者是相对于项目目录的相对路径

请使用 cmake 中的 set 命令 来设置这些变量,即 set(VARIABLE "VALUE")。请注意,set() 命令需放在 include(...) 之前,cmake_minimum(...) 之后

6.3.2 组件CMakeLists

每个组件目录都包含一个 CMakeLists.txt 文件,里面会定义一些变量以控制该组件的构建过程,以及其与整个项目的集成

每个组件还可以包含一个 Kconfig 文件,它用于定义 menuconfig 时展示的 组件配置 选项。某些组件可能还会包含 Kconfig.projbuildproject_include.cmake 特殊文件,它们用于 覆盖项目的部分设置

组件是 COMPONENT_DIRS 列表中包含 CMakeLists.txt 文件的任何目录

ESP-IDF 搜索待构建组件顺序(由COMPONENT_DIRS 指定):

  1. ${IDF_PATH}/components

  2. ${PROJECT_PATH}/components

  3. EXTRA_COMPONENT_DIRS

    如果包含同名组件,则使用最后一个位置的组件

最小组件 CMakeLists.txt 文件:

1
2
3
set(COMPONENT_SRCS "foo.c")
set(COMPONENT_ADD_INCLUDEDIRS "include")
register_component()
  • COMPONENT_SRCS:用空格分隔的源文件列表(*.c*.cpp*.cc*.S),里面所有的源文件都将会编译进组件库中
  • COMPONENT_ADD_INCLUDEDIRS:用空格分隔的目录列表,里面的路径会被添加到所有需要该组件的组件(包括 main 组件)全局 include 搜索路径中。
  • register_component():使用上述设置的变量将组件添加到构建系统中,构建生成与组件同名的库,并最终被链接到应用程序中。如果因为使用了 CMake 中的 if 命令 或类似命令而跳过了这一步,那么该组件将不会被添加到构建系统中

有关更完整的 CMakeLists.txt 示例,请参阅组件 CMakeLists 示例章节

预设变量
  • COMPONENT_PATH:组件目录,即包含 CMakeLists.txt 文件的绝对路径,它与 CMAKE_CURRENT_SOURCE_DIR 变量一样,路径中不能包含空格
  • COMPONENT_NAME:组件名,与组件目录名相同
  • COMPONENT_TARGET:库目标名,它由构建系统在内部为组件创建

可以在组件 CMakeLists 中使用的项目 CMakeLists 预设变量:

  • PROJECT_NAME:项目名,在项目 CMakeLists.txt 文件中设置
  • PROJECT_PATH:项目目录(包含项目 CMakeLists 文件)的绝对路径,与 CMAKE_SOURCE_DIR 变量相同
  • COMPONENTS:此次构建中包含的所有组件的名称,具体格式为用分号隔开的 CMake 列表
  • CONFIG_*:项目配置中的每个值在 cmake 中都对应一个以 CONFIG_ 开头的变量。更多详细信息请参阅 Kconfig
  • IDF_TARGET:项目的硬件目标名称

如果修改以上变量,并不会影响其他组件的构建,但可能会使该组件变得难以构建或调试

  • COMPONENT_ADD_INCLUDEDIRS:相对于组件目录的相对路径,为被添加到所有需要该组件的其他组件的全局 include 搜索路径中。如果某个 include 路径仅仅在编译当前组件时需要,请将其添加到 COMPONENT_PRIV_INCLUDEDIRS 中。

  • COMPONENT_REQUIRES 是一个用空格分隔的组件列表,列出了当前组件依赖的其他组件。如果当前组件有一个头文件位于 COMPONENT_ADD_INCLUDEDIRS 目录下,且该头文件包含了另一个组件的头文件,那么这个被依赖的组件需要在 COMPONENT_REQUIRES 中指出。这种依赖关系可以是递归的。

    COMPONENT_REQUIRES 可以为空,因为所有的组件都需要一些常用的组件(如 newlib 组件提供的 libc 库、freertos 组件提供的 RTOS 功能),这些通用组件已经在项目级变量 COMPONENT_REQUIRES_COMMON 中被设置。

    如果一个组件仅需要额外组件的头文件来编译其源文件(而不是全局引入它们的头文件),则这些被依赖的组件需要在 COMPONENT_PRIV_REQUIRES 中指出。

可选变量
  • COMPONENT_PRIV_INCLUDEDIRS:相对于组件目录的相对路径,仅会被添加到该组件的 include 搜索路径中
  • COMPONENT_PRIV_REQUIRES:以空格分隔的组件列表,用于编译或链接当前组件的源文件。这些组件的头文件路径不会传递给其余需要它的组件,仅用于编译当前组件的源代码。更多详细信息请参阅 组件依赖
  • COMPONENT_SRCS:要编译进当前组件的源文件的路径,推荐使用此方法向构建系统中添加源文件
  • COMPONENT_SRCDIRS:相对于组件目录的源文件目录路径,用于搜索源文件(*.cpp*.c*.S)。匹配成功的源文件会替代 COMPONENT_SRCS 中指定的源文件,进而被编译进组件。即设置 COMPONENT_SRCDIRS 会导致 COMPONENT_SRCS 会被忽略。此方法可以很容易地将源文件整体导入到组件中,但并不推荐使用(使用该方法会导致增量更新编译变得更加麻烦)
  • COMPONENT_SRCEXCLUDE:需要从组件中剔除的源文件路径。当某个目录中有大量的源文件需要被导入组件中,但同时又有个别文件不需要导入时,可以配合 COMPONENT_SRCDIRS 变量一起设置。路径可以是相对于组件目录的相对路径,也可以是绝对路径。
  • COMPONENT_ADD_LDFRAGMENTS:组件使用的链接片段文件的路径,用于自动生成链接器脚本文件。详细信息请参阅 链接脚本生成机制

如果没有设置 COMPONENT_SRCDIRSCOMPONENT_SRCS,组件不会被编译成库文件,但仍可以被添加到 include 路径中,以便在编译其他组件时使用

编译控制

在编译特定组件的源文件时,可以使用 target_compile_options 命令来传递编译器选项:

1
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-unused-variable)

如果给单个源文件指定编译器标志,可以使用 CMake 的 set_source_files_properties 命令:

1
2
3
4
set_source_files_properties(mysrc.c
PROPERTIES COMPILE_FLAGS
-Wno-unused-variable
)

**注意:**上述两条命令只能在组件 CMakeLists 文件的 register_component() 命令之后调用

6.4 组件依赖

编译各个组件时,每个组件的源文件都会使用以下路径中的头文件进行编译:

  • 当前组件的 COMPONENT_ADD_INCLUDEDIRSCOMPONENT_PRIV_INCLUDEDIRS
  • 当前组件的 COMPONENT_REQUIRESCOMPONENT_PRIV_REQUIRES 变量指定的其他组件(即当前组件的所有公共和私有依赖项)所设置的 COMPONENT_ADD_INCLUDEDIRS
  • 所有组件的 COMPONENT_REQUIRES 做递归操作,即该组件递归运算后的所有公共依赖项
6.4.1 编写组件
  • COMPONENT_REQUIRES :包含所有被当前组件的公共头文件 #include 的头文件所在的组件。
  • COMPONENT_PRIV_REQUIRES:包含被当前组件的源文件 #include 的头文件所在的组件(除非已经被设置在了 COMPONENT_PRIV_REQUIRES 中),或者是当前组件正常工作必须要链接的组件
  • COMPONENT_REQUIRESCOMPONENT_PRIV_REQUIRES 需要在调用 register_component() 之前设置
  • COMPONENT_REQUIRESCOMPONENT_PRIV_REQUIRES 的值不能依赖于任何配置选项(CONFIG_xxx),这是因为在配置加载之前,依赖关系就已经被展开。其它组件变量(比如 COMPONENT_SRCSCOMPONENT_ADD_INCLUDEDIRS)可以依赖配置选择
  • 如果当前组件除了 COMPONENT_REQUIRES_COMMON 中设置的通用组件(比如 RTOS、libc 等)外,并不依赖其它组件,那么上述两个 REQUIRES 变量可以为空

如果组件仅支持某些硬件目标(即依赖于特定的 IDF_TARGET),则可以调用 require_idf_targets(NAMES...)来声明这个需求

6.4.2 创建项目
  • 默认情况下,每个组件都会包含在构建系统中
  • 如果将COMPONENTS变量设置为项目直接使用的最小组件列表,那么构建系统会导入:
    • COMPONENTS 中明确提及的组件
    • 这些组件的依赖项(以及递归运算后的组件)
    • 每个组件都依赖的通用组件
  • COMPONENTS 设置为所需组件的最小列表,可以显著减少项目的构建时间
6.4.3 其他细节
  • 在 CMake 配置进程的早期阶段会运行 expand_requirements.cmake 脚本。该脚本会对所有组件的 CMakeLists.txt 文件进行局部的运算,得到一张组件依赖关系图(此图可能会有闭环)。此图用于在构建目录中生成 component_depends.cmake 文件
  • CMake 主进程会导入该文件,并以此来确定要包含到构建系统中的组件列表(内部使用的 BUILD_COMPONENTS 变量)。BUILD_COMPONENTS 变量已排好序,依赖组件会排在前面。由于组件依赖关系图中可能存在闭环,因此不能保证每个组件都满足该排序规则。如果给定相同的组件集和依赖关系,那么最终的排序结果应该是确定的
  • CMake 会将 BUILD_COMPONENTS 的值以 “Component names:” 的形式打印出来
  • 然后执行构建系统中包含的每个组件的配置
  • 每个组件都被正常包含在构建系统中,然后再次执行 CMakeLists.txt 文件,将组件库加入构建系统

BUILD_COMPONENTS 变量中组件的顺序决定了构建过程中的其它顺序:

  • 项目导入 project_include.cmake 文件的顺序
  • 生成用于编译(通过 -I 参数)的头文件路径列表的顺序。请注意,对于给定组件的源文件,仅需将该组件的依赖组件的头文件路径告知编译器

6.5 组件CmakeLists示例

因为构建环境试图设置大多数情况都能工作的合理默认值,所以组件 CMakeLists.txt 文件可能非常小,甚至是空的,请参考组件CMakeLists章节。但有些功能往往需要覆盖预设变量才能实现

6.5.1 条件配置
添加

Kconfig:

1
2
3
4
config FOO_ENABLE_BAR
bool "Enable the BAR feature."
help
This enables the BAR feature of the FOO component.

CMakeLists.txt:

1
2
3
4
5
set(COMPONENT_SRCS "foo.c" "more_foo.c")

if(CONFIG_FOO_ENABLE_BAR)
list(APPEND COMPONENT_SRCS "bar.c")
endif()

上述示例使用了 CMake 的 if 函数和 list APPEND 函数。

选择或删除

Kconfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
config ENABLE_LCD_OUTPUT
bool "Enable LCD output."
help
Select this if your board has a LCD.

config ENABLE_LCD_CONSOLE
bool "Output console text to LCD"
depends on ENABLE_LCD_OUTPUT
help
Select this to output debugging output to the lcd

config ENABLE_LCD_PLOT
bool "Output temperature plots to LCD"
depends on ENABLE_LCD_OUTPUT
help
Select this to output temperature plots

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
if(CONFIG_ENABLE_LCD_OUTPUT)
set(COMPONENT_SRCS lcd-real.c lcd-spi.c)
else()
set(COMPONENT_SRCS lcd-dummy.c)
endif()

# 如果启用了控制台或绘图功能,则需要加入字体
if(CONFIG_ENABLE_LCD_CONSOLE OR CONFIG_ENABLE_LCD_PLOT)
list(APPEND COMPONENT_SRCS "font.c")
endif()
由硬件目标决定

CMake 文件可以使用 IDF_TARGET 变量来获取当前的硬件目标。

此外,如果当前的硬件目标是 xyz(即 IDF_TARGET=xyz),那么 Kconfig 变量 CONFIG_IDF_TARGET_XYZ 同样也会被设置。

请注意,组件可以依赖 IDF_TARGET 变量,但不能依赖这个 Kconfig 变量。同样也不可在 CMake 文件的 include 语句中使用 Kconfig 变量,在这种上下文中可以使用 IDF_TARGET