本节目标:补齐环境细节(USB、串口权限、免密登录),看懂 helloworld 工程每个文件的用途,理解 CMake 构建系统,会编译、配置、烧录、监控
一、本节要解决的问题
上一节环境已经能编译,但还有一批"用小毛病换大麻烦"的细节没处理:
- C/C++ 插件在 ubuntu 端占着 CPU 不放,虚拟机越用越卡;
- 开发板插上 Type-C 线后,ubuntu 里经常连不上串口设备;
- 执行
idf.py flash报Permission denied: '/dev/ttyUSB0'; - 每次用 VSCode 远程登录、打开工程都要输两三次密码。
更核心的问题是:helloworld 工程里每个文件是干什么的?编译出来的 bin 放在哪、烧到哪里?不搞清楚这些,就没法自己新建和修改工程------而"从现有工程拷贝来改"正是教程推荐的高效做法。
二、相关知识:环境搭建补充
2.1 降低 C/C++ 插件 CPU 占用
C/C++ 插件安装在 ubuntu 端(不是 Windows 本地那份)。登录远程后,点击插件管理里 ubuntu 端那一份,按教程截图设置,减少 IntelliSense 的后台解析负担。
提示 :仓库自带的
helloworld/.vscode/settings.json里已包含"C_Cpp.intelliSenseEngine": "default"一类配置,就是本地调优后的结果,可作参考。
2.2 虚拟机 USB 设置
开发板通过 Type-C 转 USB 连到电脑,为了下载和调试,我们要把它接到虚拟机上:
- 插上线后虚拟机会弹出提示框,选择"连接到虚拟机",虚拟机右下角出现 USB 已连接图标,也可以手动点该图标连接/断开;
- 按默认设置使用时会经常连不上串口设备,按教程截图在虚拟机 USB 设置里调整,设置完成后重启虚拟机系统。
2.3 USB 串口权限问题
烧录的前提是开发板已连到虚拟机。执行 idf.py flash 时可能报:
Permission denied: '/dev/ttyUSB0'
临时解决(重插或重启后失效,且 777 权限开得过大,仅应急):
bash
sudo chmod 777 /dev/ttyUSB0
一劳永逸(把当前用户加入串口设备所属的 dialout 组,然后重启系统):
bash
sudo usermod -aG dialout 你的用户名
提示 :教程原文命令写的是
usrname,要换成你自己的 ubuntu 用户名。另外,若使用原生 USB 的 ESP32-S3/C3 板子,串口设备名可能是/dev/ttyACM0而不是/dev/ttyUSB0,具体以ls /dev/tty*看到的为准。
2.4 使用证书 SSH 登录 ubuntu
每次远程登录都要输密码很烦,可以改用密钥免密:
- Windows 下打开 PowerShell,执行
ssh-keygen -t ed25519 -C youremail@xxx.com(邮箱随便填,一路回车); - 在
C:\Users\你的用户名\.ssh目录下找到id_ed25519.pub,用文本打开并复制内容; - 用 VSCode 远程登录 ubuntu,打开
.ssh文件夹里的authorized_keys,把公钥内容粘贴进去; - 如仍要输密码,再在 VSCode 的 SSH 配置里加一项
IdentityFile指向密钥,然后重启 VSCode。
注意 :教程原文的
IdentityFile写的是id_ed25519.pub(公钥)。按 OpenSSH 的规范这里通常应指向私钥id_ed25519(不带 .pub);教程也注明"这一步不做也可以",以你本地实际生效的写法为准。
三、知识点:ESP-IDF 的构建系统与工程组成
3.1 两层 CMakeLists 的分工
ESP-IDF 用 CMake 构建,一个工程有两级 CMake 脚本:
- 顶层 CMakeLists.txt(项目级):声明最低 CMake 版本、引入 ESP-IDF 的构建逻辑、指定工程名;
- 组件级 CMakeLists.txt(main/ 目录下):登记本组件有哪些源码文件、头文件目录在哪。
ESP-IDF 以"组件 "为构建单元:esp-idf/components 下每个组件(FreeRTOS、driver、wifi...)独立编译成静态库,最后和你的应用链接成 main.bin。main 是存放用户代码的特殊组件。
3.2 build/ 里的产物与烧录地址
编译后生成的 bin 不止一个。看 build/flasher_args.json 就能知道各自烧到哪里(本仓库实际内容节选):
| 文件 | 烧录地址 | 作用 |
|---|---|---|
bootloader/bootloader.bin |
0x0 | 应用程序启动前运行的一段代码,负责引导 |
partition_table/partition-table.bin |
0x8000 | 分区表,描述各分区分别存放什么 |
main.bin |
0x10000 | 我们的应用代码 |
此外该文件还记录了烧录参数(flash 模式、频率、大小)和目标芯片信息,后续讲分区表时会再回来看它。
3.3 sdkconfig 与 menuconfig
sdkconfig 是工程配置文件,记录所有可配置项的值:
- 如果不存在,idf 会先生成一份默认配置;
- 执行
idf.py menuconfig(图形化配置界面,用键盘上下左右和回车操作),保存后自动写回sdkconfig,旧文件备份为sdkconfig.old; sdkconfig属于自动生成的配置产物,不建议手动编辑。
教程要求先改三项(其余保持默认,以后用到哪个配哪个):
| 配置项 | 教程值 | menuconfig 路径 |
|---|---|---|
| 工作主频 | 160MHz 改为 240MHz | Component config → ESP System Settings → CPU frequency |
| Flash 大小 | 2MB 改为 4MB | Serial flasher config → Flash size |
| PSRAM | 启用 | Component config → ESP PSRAM → Support for external, SPI-connected RAM |
修改后按 s 保存、q 退出,再 idf.py build 重新编译。
3.4 新建工程的两种方式
- 拷贝现有工程 :教程最推荐,把
helloworld整个复制一份改名,改改就能用,效率最高; - 命令行创建 :
idf.py create-project 工程名会自动生成一个最小工程骨架。
四、代码分析
4.1 工程结构
helloworld/
├── CMakeLists.txt # 顶层 CMake 脚本:工程名与构建入口
├── main/
│ ├── CMakeLists.txt # 组件 CMake 脚本:登记源文件
│ └── main.c # 应用入口 app_main()
├── sdkconfig # 工程配置(menuconfig 生成)
├── sdkconfig.old # 上一次保存的配置备份
├── build/ # 编译产物与中间文件(自动生成)
├── .vscode/ # 本地 VSCode 配置(端口、IntelliSense、调试)
├── .clangd # clangd 代码索引配置
└── README.md # ESP-IDF 模板自动生成,无实际内容,可忽略
4.2 顶层 CMakeLists.txt
cmake
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five 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(main)
三句有效代码各有分工:
cmake_minimum_required(VERSION 3.16):要求 CMake 至少 3.16;include($ENV{IDF_PATH}/tools/cmake/project.cmake):引入 ESP-IDF 的构建规则,靠环境变量IDF_PATH定位(这就是必须先source export.sh的原因之一);project(main):声明工程名。要改工程名就改这里的main(注意:它只是工程名,和main/目录名是两回事,模板里两者恰好同名,容易混淆)。
注释原文强调这几行样板代码"必须按此顺序出现",顺序错了 CMake 直接报错。
4.3 main/CMakeLists.txt
cmake
idf_component_register(SRCS "main.c"
INCLUDE_DIRS ".")
SRCS "main.c":本组件的源文件清单。以后新增 .c 文件,必须加到这里,否则不参与编译;INCLUDE_DIRS ".":头文件搜索路径为当前目录。新增头文件目录同样要登记在这里。
main/ 目录是必须存在的,否则编译不过。
4.4 main/main.c
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char* TAG = "main";
void app_main(void)
{
while(1)
{
ESP_LOGI(TAG,"Hello world!");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
app_main()是应用的入口函数,相当于 PC 程序的main;TAG是日志标签,串口打印时用它标识这条日志来自哪个模块;ESP_LOGI(TAG,"Hello world!")以 INFO 级别输出日志,比printf多了时间戳、级别、标签,且能按级别整体过滤,是 ESP-IDF 里推荐的打印方式;vTaskDelay(pdMS_TO_TICKS(1000))延时 1000ms。vTaskDelay的单位是 FreeRTOS 的 tick,pdMS_TO_TICKS负责把毫秒换算成 tick;延时会阻塞当前任务并让出 CPU,空闲任务和其他任务才有机会运行;while(1)让任务常驻循环:app_main返回后会被系统删除,这里不返回,日志便每秒打印一次。
4.5 sdkconfig 关键配置
sdkconfig 有两千多行,初学只需关注开头几项。本仓库当前的实际值(节选):
CONFIG_IDF_TARGET="esp32s3"
CONFIG_IDF_TARGET_ESP32S3=y
CONFIG_IDF_INIT_VERSION="5.3.4"
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="2MB"
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
# CONFIG_SPIRAM is not set
含义:目标芯片是 esp32s3 ,Flash 还保持默认的 2MB ,CPU 主频 160MHz ,PSRAM 未启用。
与教程不一致,务必注意 :教程第 3 节按"原版 ESP32 开发板"讲解------模组为 4MB Flash + 2MB PSRAM、主频可开到 240MHz,所以让你在 menuconfig 里改这三项。但仓库里的
helloworld工程已被切到esp32s3目标,上面的三项配置仍是默认值(build/flasher_args.json里的"chip": "esp32s3"、"flash_size": "2MB"也印证了这一点);而且 sdkconfig 由本地 ESP-IDF 5.3.4 生成,与教程的 v5.2 不完全同源。以自己开发板的原理图和实际工程为准 :若你的板子是原版 ESP32,先idf.py set-target esp32,再按 3.3 配置这三项;换 target 会重置 sdkconfig,配置要重做。
五、关键 API 与命令速查
常用 idf.py 命令:
| 命令 | 作用 |
|---|---|
idf.py create-project 工程名 |
新建一个最小工程骨架 |
idf.py set-target esp32 |
设置目标芯片(会重置 sdkconfig 和 build) |
idf.py build |
编译工程,生成 bin 文件 |
idf.py menuconfig |
图形化配置工程,保存后写回 sdkconfig |
idf.py -p /dev/ttyUSB0 flash |
烧录编译产物到开发板 |
idf.py -p /dev/ttyUSB0 monitor |
打开串口监视器查看日志 |
idf.py flash monitor |
烧录完成后直接进入监视器 |
idf.py fullclean |
清除 build 目录的全部产物 |
本节用到的 API:
| API | 作用 | 所在头文件 |
|---|---|---|
ESP_LOGI(TAG, fmt, ...) |
以 INFO 级别打印日志,自带时间戳与标签 | esp_log.h |
vTaskDelay(ticks) |
阻塞当前任务指定 tick 数并让出 CPU | freertos/task.h |
pdMS_TO_TICKS(ms) |
把毫秒换算成 FreeRTOS tick | freertos/FreeRTOS.h |
关键文件速查:
| 文件/目录 | 作用 |
|---|---|
CMakeLists.txt |
顶层构建脚本,声明工程名与构建入口 |
main/CMakeLists.txt |
组件构建脚本,登记源码文件与头文件目录 |
main/main.c |
应用入口 app_main() |
sdkconfig / sdkconfig.old |
工程配置及其上一次保存的备份 |
build/flasher_args.json |
记录各 bin 文件与烧录地址 |
.vscode/ |
本地 VSCode 配置(串口、IntelliSense、调试) |
六、编译与现象验证
bash
cd ~/esp32/esp32-board/helloworld
idf.py menuconfig # 按需改配置:s 保存、q 退出
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor
- 编译成功后
build/目录下出现main.bin、bootloader/bootloader.bin、partition_table/partition-table.bin; - 烧录:板子通过 Type-C 线连到电脑并选择连接虚拟机,板上有一键下载电路,无需按任何按键;
- 串口现象:每秒打印一行日志,形如
I (1234) main: Hello world!; - 退出串口监视器:按
Ctrl+]。
提示 :仓库
.vscode/settings.json里的"idf.portWin": "COM8"是 Windows 本地 ESP-IDF 插件的端口配置,和 ubuntu 虚拟机里的/dev/ttyUSB0不是一回事,别照着 COM8 去烧录。