CLion开发ESP32
芯片:ESP32C3
版本:2025.3
系统:Windows
一、搭建环境
1,安装ESP-IDF
ESP-IDF是开发ESP项目需要用到的工具链,正如stm32开发中需要用到arm-gnu工具链。ESP-IDF下载
箭头所示方向是仅包含ESP-IDF工具链的,其上方是包含ESP-IDE的(如同stm32的STM32CubeIDE),绿色方框是在线下载。此处应选择箭头所示方向,仅安装ESP-IDF即可。过程是傻瓜式安装,此处不赘述,需要注意整条路径名不要包含中文
2,配置CLion工具链
在CLion的【设置】中找到【构建、执行、部署】下的【工具链】,选择添加MinGW类型的工具链,输入名称后,点击添加环境-来自文件,所需文件为ESP-IDF安装目录下的idf_cmd_init.bat脚本文件
同时修改idf_cmd_init.bat脚本文件,在其开头添加下面语句,路径要根据自己的情况设置。原因如下:cmake加载失败:: 设置示例工程所需的IDF_PATH set IDF_PATH=D:\DevTools\Espressif\frameworks\esp-idf-v5.3.1上面内容没有必要,只需要找到你安装的工具的目录即可,比如下面,其安装的工具目录为:
D:\DevTools\Espressif\frameworks\esp-idf-v5.5.4
那么就把该目录的export脚本填进去(Windows的是ps1脚本,Linux的是sh脚本),原因很简单,环境文件填入的自然是能激活环境的文件
下面的两个编译器需要特别指定,因为它默认使用的是esp-clang,如果是esp32c3的开发,那么就需要用到esp-riscv编译器(目录从下图可以比对出来,此处省略)
![]()
3,创建示例工程
从ESP-IDF目录里,找到示例工程,并复制到你需要创建的新目录里
右键使用CLion打开该目录,选择ESP-IDF工具链
初次构建理应正常,如果出现问题可能是多次尝试的结果,删除cmake-build-xxx缓存目录再重新cmake即可
二、烧录调试
1、烧录
默认运行配置是hello_world.elf,但这只是编译产物,不能直接运行。在运行配置中可以看到,示例工程已经为我们准备了诸如flash、monitor等多种运行配置供驱使
此处我们需要的是调试,因此选择flash。然后还需要做一些配置,原因:monitor错误
①按Win+X,选择设备管理器,插上开发板,查看新出现的端口
②编辑CMake配置文件,添加端口信息
在环境变量一栏添加ESPPORT环境变量,值设置为对应的端口
同时,需要指定自己的芯片,如esp32s3、esp32c2等,默认类型是esp32
在运行配置中指定可执行文件
运行结果如下:
2、监视 & 烧录监视(常用)
初现端倪
与烧录类似,选择monitor目标即可。由于在CLion中直接运行
monitor目标时,标准输入不是TTY(终端设备)。ESP-IDF的监视器(monitor)需要与终端交互,而CLion的CMake构建运行环境并没有提供真正的终端,仍会导致下面错误
解决之道
解决办法:参考Clion配置ESP32开发,一文就够了_clion esp32-CSDN博客中第4节的做法,即再创建一个新的目标来直接运行终端命令。由于芯片型号和串口都已经在CMake配置里设置环境变量了,并且波特率可以自动检测,故此处可以不添加额外实参
monitor$FileDir$
勾选这个模拟终端选项是为了可以在输出控制台中像在终端中那样操作,比如输入一些命令等。下面可以看到正确的运行结果:
需要注意,基于cmake运行程序的这个监视功能只能在调试服务器为原生服务器的使用。此外,前面实现的是flash monitor而非monitor,想要实现monitor,需要自定义一个空白cmake目标,这样想要执行什么命令都可以通过自定义实参实现
终端脚本
前面实现本质上是执行终端命令。这里,提供一种更本质的方法——添加终端命令目标(路径需要自行替换):
cmd /c "D:/DevTools/Espressif/idf_cmd_init.bat && D:\DevTools\Espressif\tools\idf-exe\1.0.3\idf.py.exe monitor"此外,idf.py默认构建目录是build,如果CLion的cmake配置文件的构建目录为空,会默认为cmake-build-xxx目录,那么此处需要使用-B参数指定构建目录
cmd /c "D:/DevTools/Espressif/idf_cmd_init.bat && D:\DevTools\Espressif\tools\idf-exe\1.0.3\idf.py.exe -B cmake-build-debug-esp-idf monitor"
如此一来,运行monitor等命令时,不必切换调试服务器,因为切换配置时不会用到服务器。与下文插件里的终端命令本质上是相同的
完全之体(推荐)
此时,你可能会发现一个问题,那就是刚才关于monitor或者flash monitor的CMake应用程序配置实际上没有指定构建目录,那么就会出现跟终端脚本配置相同的问题。而CLion提供了这样的宏可以很方便地自动指定构建目录
$CMakeCurrentBuildDir$那么monitor就可以这样指定了,不需要依赖具体的路径
-B $CMakeCurrentBuildDir$ monitorflash monitor这个除了目标设置为flash外,也可以设置为empty,让实参设置为flash monitor。但是!设置成flash monitor后,还需要指定工程目录,可参考错误:8、执行monitor命令显示缺少CMakeLists或者报一堆链接缺失错误
-B $CMakeCurrentBuildDir$ -C $ProjectFileDir$ flash monitor此处为错写,工作目录不要使用❌️$FileDir$这个宏,应使用$ProjectFileDir$这个宏!!!
此外!这些命令本身是不包含构建的,这在某些时候就造成了困扰。因此,目标设置为项目可执行文件会更好
因为,点击左边是构建工程,右边是执行命令,也挺符合直觉的
可惜的是,终端脚本配置不能做相似的处理,因为无法插入CLion的宏
3、使用OpenOCD进行单步调试 (进阶,不常用)
想要进行单步调试且不想折腾,那么可以跳过本小节,下文中的插件也会有单步调试的功能。本小节需要对OpenOCD、GDB和可执行文件之间的关系有一定了解
你的CLion IDE ↓ GDB (客户端) ↓ (通过TCP/IP连接,默认端口3333) OpenOCD (GDB服务器) ↓ (通过JTAG/SWD接口) ESP32芯片 ↑ 可执行文件.elf (实际上是经过处理的.bin,烧写到芯片FLASH中)这一部分尝试过许多办法才解决。先说结论,运行/调试配置中的OpenOCD下载并运行目标和嵌入式GDB服务器目标由于高度封装过,不够灵活,其内默认的gdb命令并不适配esp32c3的调试,从而导致OpenOCD与GDB连接失败,具体情况可参考OpenOCD与GDB调试相关问题
所幸CLion之前就提供了一个叫做“调试服务器”的功能,可以更加灵活地指定GDB、OpenOCD的配置情况。调试服务器默认关闭状态,需要在高级设置主动打开
①GDB服务器选项卡
接着在如下位置找到调试服务器,添加一个泛型,在GDB服务器这个选项卡里添加OpenOCD的可执行文件,其在如下所示路径中(与自己的路径对比)
D:\DevTools\Espressif\tools\openocd-esp32\v0.12.0-esp32-20240318\openocd-esp32\bin\openocd.exe
实参需要根据自己的芯片类型进行填写,我的芯片是esp32c3,因此选择esp32c3-builtin,-builtin表示使用内置的USB-JTAG功能。需要确保自己的cfg文件存在
-f board/esp32c3-builtin.cfg
如果不知道自己的配置文件是否存在,那么可以先在如下位置指定前面的OpenOCD可执行文件的路径,点击确定后重启CLion
然后在运行/调试配置中添加一个OpenOCD 下载并运行目标,点击辅助...进行浏览。或者,直接在如下路径进行查找(与自己的路径对比):
D:\DevTools\Espressif\tools\openocd-esp32\v0.12.0-esp32-20240318\openocd-esp32\share\openocd\scripts\board
②设备设置选项卡
设备设置选项卡中没什么需要改变的,只需要知道,所谓文件上传是指程序下载到开发板,monitor reset是复位开发板,monitor reset halt是复位开发板后并暂停运行
不过,重置设备一栏里只能填monitor reset halt,不然会导致错误:断点太多并卡住调试
③调试器选项卡
GDB调试器也要指定为esp32专用调试器,由于我的芯片是esp32c3,riscv32架构,因此需要在这个目录里去寻找。如果是esp32s3需要在xtensa-esp-elf-gdb目录里去找
工作目录使用的是CLion里的宏,实际就是项目根目录。实参填【:3333】,意为本地3333端口,自定义脚本这块输入如下内容,第一行是显示,可有可无;第二行是执行项目里的gdb脚本
echo start to exec gdb script!\n source gdbinit.gdb
输入完成后,在项目根目录里新建一个gdbinit.gdb文件
内容如下,需要注意下面三个路径需要替换成自己的路径,注意自己的项目构建目录是build还是cmake-build-xxx,如果使用的是cmake目标或者插件而非esp-idf命令行且CMake配置中没有修改构建目录,那么应是cmake-build-xxx目录
简陋的gdb脚本(暂不删除,防止后续无法溯源)
set confirm off # 下面三个路径需要替换为自己的路径 add-symbol-file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-debug-esp-idf/bootloader/bootloader.elf" add-symbol-file "D:/DevTools/Espressif/tools/esp-rom-elfs/20240305/esp32c3_rev3_rom.elf" file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-debug-esp-idf/GPSTimeMesh.elf" set confirm on set remotetimeout 10 target remote :3333 monitor reset halt maintenance flush register-cache thbreak app_main包含注释的gdb脚本
# ============================================ # ESP32调试脚本 - 注意:注释必须单独一行,不能写在命令后面 # ============================================ # 1. 关闭确认提示,避免交互中断 set confirm off # 2. 设置远程连接超时时间(秒) set remotetimeout 10 # 3. 连接目标GDB服务器 target extended-remote :3333 # 4. 停止目标CPU # 必须在连接后才能执行monitor命令 monitor reset halt # 5. 加载主程序符号 file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-debug-esp-idf/GPSTimeMesh.elf" # 6. 加载bootloader符号 add-symbol-file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-debug-esp-idf/bootloader/bootloader.elf" # 7. 加载ROM符号 add-symbol-file "D:/DevTools/Espressif/tools/esp-rom-elfs/20240305/esp32c3_rev3_rom.elf" # 8. 刷新寄存器缓存 # 确保读取最新寄存器值 maintenance flush register-cache # 9. 在app_main设置临时硬件断点 thbreak app_main # 这行语句不能加,加了会导致调试卡住,但在命令行里输入却没事。可能是需要一些延迟 # continue # 如果希望在GDB交互中确认操作,可以开启 # set confirm on④开始调试卡
此时CMake目标选择后缀带elf的,其名称应与你的项目名称一致
前面启用调试服务器之后,就会出现左边箭头所指物件,选择为前面创建好的泛型,然后就可以点击调试了
此时可以看到程序能正常调试了,左边的警告无伤大雅
点击这个立方体可以进入控制台
此处实际上是GDB服务器,也就是OpenOCD,左侧是输出,右侧可以输入OpenOCD命令。前面的调试窗口是GDB输出
需要注意的是,停止调试后,GDB服务器也是开着的,从它右上角冒出的小绿点可以看出来
至此,不需要通过插件来完成构建调试,开发体验上更加自然,比如
- 使用插件的调试时它不会自动构建项目,需要主动双击右侧的Build,容易导致调试与烧录不同步,调试时进入汇编文件
- 可以切换cmake目标,自己定义更复杂的cmake目标和执行前操作,以实现嵌套任务和复杂操作。相较于插件来说更加灵活,更加多样
可惜的是,下载功能由于CLion默认行为与esp32期望的不一样,导致无法正常使用,暂未找到解决办法。不过,已经可以摆脱插件了,至于下载等多个功能,完全可以通过切换不同的cmake目标实现,操作仅仅只是展开下拉框切换一下目标,点击构建,而调试服务器不需要改变。需要注意的是,这些CMake目标都只能构建,不能运行,因为运行指的是在你的电脑上运行esp32的可执行文件,对于非原生调试服务器,那么就是上传文件,不过前面提过,这个功能暂不可用
如果强行使用,可能会导致调试时不断复位,详情参考GDB反复复位
4、组合目标
虽然ESP-IDF已经提供了很多cmake目标,但有时候我们需要先后执行一些目标,比如构建后我们更希望能看到内存分布,此时就需要在执行app目标后,再执行size目标。
①依赖
做法有很多种,这里先讲一个最简单的——依赖,让size依赖app,那么就可以在执行size前执行app来构建。即在根目录的CMakeLists里添加下面这行语句(因为size和app目标都是定义在project.cmake里,所以要放在下面)
add_dependencies(size app)
重新cmake后,再点击上方的小锤子,可以发现构建过程后会执行size打印内存布局
优化打印内存布局(推荐)
考虑到size的输出信息还是太抽象了,显示的字节信息不够直观。为此,可以在项目根目录下的CMakeList里的下方添加链接器标志
# 添加链接器内存使用报告 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--print-memory-usage")
虽然这个操作可以在Menu Config里完成,但是在Menu Config里修改后需要编译所有文件,较为繁琐。而在CMakeList里只需要添加一行语句,不需要添加依赖这个操作,并且也更加直观清晰
②自定义目标/命令
想要执行更复杂的操作,在这后面通过add_custom_command或者add_custom_target函数来添加新的目标或命令就行。比如刚才的size输出实际上过于简陋,而idf.py -B cmake-build-debug-esp-idf size这条终端命令可以输出较为详细的内存布局信息,为此我们可以定义这样一条cmake目标
# 添加详细内存分析目标 app_plus add_custom_target(app_plus # 先构建 app 目标 COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target app # 然后执行详细的 size 分析 COMMAND idf.py -B ${CMAKE_BINARY_DIR} size WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Building app and running detailed memory analysis..." USES_TERMINAL VERBATIM )可以看到输出更为详细(只不过容易出现一条缺失错误,不过不影响内存布局打印)
③复合目标
或者直接使用复合目标,可以对原程序做最小的更改
三、开发体验提升
1、插件
在插件市场上esp相关的插件并不多,第一个近乎断更了,第二个文档不是很详细,文档既全功能又丰富的只有第三个了
安装完成后,右侧便会出现
可以查看插件文档ESP-IDF for CLion | ESP-IDF for Clion 文档进行配置,也可以参考下文。
在此之前可以在插件的设置里指定端口(有下拉框可以选择),然后点击保存
①烧录监视
可见红框中提供了三种命令,双击即可,与前文一致
②单步调试
打开运行配置,找到对应目标,Simple Debug和Custom Debug两者理论上随便选一个即可,并且添加后都不需要修改任何东西
但实测发现,Simple Debug显示的总是汇编,且似乎有些水土不服
因此建议使用Custom Debug,使用时可以在代码中设置一个断点。实际运行时会自动在起始地址和一些莫名其妙的地方暂停,继续即可
单步调试出现的常见错误请参考ESP-IDF插件的OpenOCD调试错误
考虑到调试时并不会自动编译工程,为了更好的体验,理应可以添加一个执行前的操作
然而这并没有什么用(悲),因为它会自动删除这个执行前操作,算是一个小bug
所以需要养成调试前进行Build的习惯
③自定义任务
新建项目并无自定义任务,可以通过双击弹出的错误窗口进行模板创建
可以看到默认模板文件如下
关于自定义任务的详细标签属性,请查看快捷命令树 | ESP-IDF for Clion 文档
可惜的是想要先后执行Build任务和Size任务,在自定义任务里没那么容易实现
④总结
这个插件带来的最大便利应该是单步调试功能,可以省去配置OpenOCD的时间和精力耗费,至于构建后显示内存布局等更复杂的操作,完全可以在CMakeLists里定义cmake目标,这样做反而更简单一些,因为Ctrl+C和Ctrl+V即可完成
2、使用git
默认情况下,CLion会自动检索出一个git仓库,然而它与我们项目无关却会影响我们创建git仓库
此时,需要在目录映射里把所有目录全部移除
然后就可以创建一个新的git仓库了,如果出现“与espifv5.3.1关联”类似的语句,请参看本章的第3节的②
同时,需要创建一个.gitignore来排除构建产物
.idea/ build/ cmake-build-*/ sdkconfig sdkconfig.old至于CLion里的git如何使用,可自主学习,此处不赘述
3、重新创建新项目
经过前面步骤,已经基本熟悉使用CLion进行烧录监视和调试的方法。为此可以在此基础上创建一个自定义项目,除了直接修改CMakeLists和文件夹名(不推荐,容易出一些奇奇怪怪的问题),还可参考以下步骤(以GPSTimeMesh项目为例):
①复制此次必要之文件
在工作环境内创建一个新目录GPSTimeMesh
把需要的文件复制过来
删除main目录里的build目录
②启动终将配置的工程
右键GPSTimeMesh目录以使用CLion打开,选择ESP-IDF工具链,把之前的环境变量复制过来。构建目录建议改为build,这会使得需要使用idf.py的命令会简单一些
ESPPORT=COM5;IDF_TARGET=esp32c3
然后等待CMake加载完成。接着,打开设置里的git,把所有目录映射排除,一个不留。并且关闭下方的自动映射检测
此时如果重新启动git可能会出现“与esp32v5.3.1关联”类似语句,可能会导致白忙活
为此,需要点击这里的创建git仓库。如果没有这个功能,可能是旧版,那么点击上方的确定也行
直接点击【选择文件夹】即可
此时就是与ESP-IDF无关的git仓库了
最后,打开CMakeLists,把工程名改为GPSTimeMesh,然后重新CMake。当然,这一步完全可以在复制文件时完成
③烧录调试的再行之路
插上开发板,打开右侧的ESP-IDF插件,先双击Build,进行漫长的等待
双击Monitor,由于ESP-IDF插件可以自动检测串口,在没出问题前不需要主动设置串口,因为这里使用的是idf.py monitor命令,它会自动检索端口
打开运行配置,添加Custom Debug,遵循默认模板即可,然后点击确定,
在main文件里设置一个断点,开始进行调试。此时一般会卡住,停止运行,重新插拔开发板即可(稀奇古怪的问题)
此时可以看到一切正常
以上是插件方案,另一种则是修改Flash、Monitor的CMake目标,并添加Flash Monitor的CMake目标以及OpenOCD的调试服务器,即Flash与Monitor的完全之体(推荐)
④工程开发的远行之旅
接下来就是如同stm32开发的路数,添加文件修改CMakeLists。
右键hello_world_main.c改为main.c
可以观察到CMakeLists.txt里已经自动进行同步修改了,显然,SRCS是资源文件,INCLUDE_DIRS是头文件目录。想要玩转一个cmake工程,除了需要学习一些CMake语法,还需要对C/C++工程的构建有一定了解
至此,基本工作已告一段落……
4、FreeRTOS集成功能
由于esp32默认使用是FreeRTOS,为了更加清晰地调试,可以启用CLion自带的FreeRTOS集成功能
根据文档(详情可见本文末尾的参考文档多线程RTOS的调试),需要进行一些额外配置
有些宏ESP-IDF已经正确设置,同时为了便捷,这里只额外添加一个宏且能正常使用FreeRTOS集成功能,为了确保对ESP-IDF进行最小的无害改动,我们不直接对FreeRTOSConfig.h进行修改。而是在修改sdkconfig,为此有两种常见方法:
①打开menu config进行图形化修改
在项目里添加sdkconfig.defaults文件(空白文件即可,确保menu config能打开),然后双击Menu Config
按照如下路径找到configUSE_TRACE_FACILITY,按回车后再按S,最后按Q退出
可以看到sdkconfig里已经出现了这条配置
或者直接使用插件的Confi UI功能,修改完成后点击停止即可
②通过sdkconfig.defaults(省略)
双击IDF Console打开对应终端
在sdkconfig.defaults文件中添加CONFIG_FREERTOS_USE_TRACE_FACILITY=y内容,然后执行命令
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults" reconfigure此时再重新进行调试,虽然右下角还会出现警告,但已经可以正确显示FreeRTOS的线程了
与不开FreeRTOS集成功能相比,上面多了FreeRTOS对象、堆的视图
不过需要注意,直接在FreeRTOSConfig.h文件中添加该宏后重新编译会出现如下错误,详情见__lock静态断言错误
5、C++的引入
esp对C++组件的支持有限,更倾向于提供C的API,不过这也使其具有更广泛的兼容性,可以被C++、Rust等多种语言轻松调用。同时,得益于C++便捷强大的语法特性,将其应用于ESP32开发同样是一个不错的选择
①注意事项
为此需要注意一些事项,应避免使用虚函数、堆分配和异常处理以及一些标准库
②C/C++混编
在开发过程中,因为主环境变成了C++,想要C++里实现由C声明的函数,那么必须使用extern "C"包含,这是显而易见的。比如应用程序入口点app_main,可以看到它是在app_startup这个c文件声明的,在main_task这个线程函数里调用
现在我们把main.c改为main.cpp,正式开始进入C++环境。需要注意的是在CLion中更改文件名时需要注意,因为CLion提供的重构功能有时会把同名的文件一块改了,所以此时建议勾选这个选项
然后到CMakeLists.txt把对应文件也改了,重新CMake一下
然后以C的形式定义app_main,如果此时"C"出现红色波浪线,说明语言引擎没有重新加载该文件,关闭main.cpp再重新打开就行了
此时构建一切正常,不过搜索main.cpp时可能会搜索不到,因为ESP-IDF这个插件的Build的输出是不完整的,有些构建条目输出会被跳过
使用CMake目标可以清楚的看到main.cpp这个输出条目
此外,需要注意esp-idf默认使用C++23标准
③多线程
ESP对C++的多线程是基于FreeRTOS实现的
官方示例代码如下:
/* pthread/std::thread example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ #include <iostream> #include <thread> #include <chrono> #include <memory> #include <string> #include <sstream> #include <esp_pthread.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <esp_log.h> using namespace std::chrono; const auto sleep_time = seconds { 5 }; void print_thread_info(const char *extra = nullptr) { std::stringstream ss; if (extra) { ss << extra; } ss << "Core id: " << xPortGetCoreID() << ", prio: " << uxTaskPriorityGet(nullptr) << ", minimum free stack: " << uxTaskGetStackHighWaterMark(nullptr) << " bytes."; ESP_LOGI(pcTaskGetName(nullptr), "%s", ss.str().c_str()); } void thread_func_inherited() { while (true) { print_thread_info("This is the INHERITING thread with the same parameters as our parent, including name. "); std::this_thread::sleep_for(sleep_time); } } void spawn_another_thread() { // Create a new thread, it will inherit our configuration std::thread inherits(thread_func_inherited); while (true) { print_thread_info(); std::this_thread::sleep_for(sleep_time); } } void thread_func_any_core() { while (true) { print_thread_info("This thread (with the default name) may run on any core."); std::this_thread::sleep_for(sleep_time); } } void thread_func() { while (true) { print_thread_info(); std::this_thread::sleep_for(sleep_time); } } esp_pthread_cfg_t create_config(const char *name, int core_id, int stack, int prio) { auto cfg = esp_pthread_get_default_config(); cfg.thread_name = name; cfg.pin_to_core = core_id; cfg.stack_size = stack; cfg.prio = prio; return cfg; } extern "C" void app_main(void) { // Create a thread using default values that can run on any core auto cfg = esp_pthread_get_default_config(); esp_pthread_set_cfg(&cfg); std::thread any_core(thread_func_any_core); // Create a thread on core 0 that spawns another thread, they will both have the same name etc. cfg = create_config("Thread 1", 0, 3 * 1024, 5); cfg.inherit_cfg = true; esp_pthread_set_cfg(&cfg); std::thread thread_1(spawn_another_thread); // Create a thread on core 1. cfg = create_config("Thread 2", 1, 3 * 1024, 5); esp_pthread_set_cfg(&cfg); std::thread thread_2(thread_func); // Let the main task do something too while (true) { std::stringstream ss; ss << "core id: " << xPortGetCoreID() << ", prio: " << uxTaskPriorityGet(nullptr) << ", minimum free stack: " << uxTaskGetStackHighWaterMark(nullptr) << " bytes."; ESP_LOGI(pcTaskGetName(nullptr), "%s", ss.str().c_str()); std::this_thread::sleep_for(sleep_time); } }测试一下线程的基本创建
#include <stdio.h> #include <inttypes.h> #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_chip_info.h" #include "esp_flash.h" #include "esp_system.h" // C++ 多线程 #include <esp_log.h> #include <thread> // 简单的线程函数,类似FreeRTOS任务 void task1_function() { int count = 0; while (true) { ESP_LOGI("Task1", "计数: %d, 核心: %d", count++, xPortGetCoreID()); std::this_thread::sleep_for(std::chrono::seconds(1)); // 类似vTaskDelay(1000/portTICK_PERIOD_MS) } } void task2_function() { while (true) { ESP_LOGI("Task2", "正在运行,核心: %d", xPortGetCoreID()); std::this_thread::sleep_for(std::chrono::milliseconds(500)); } } extern "C"{ void app_main(void) { // 创建两个线程 std::thread task1(task1_function); std::thread task2(task2_function); // 主线程继续执行 while (true) { ESP_LOGI("Main", "主线程运行中"); std::this_thread::sleep_for(std::chrono::seconds(2)); } } }可以看到多线程可以正常使用
单步调试中,可以在清楚地看到这两个任务变量
这两个任务实际上就是红框内的这两个FreeRTOS线程
6、加载svd文件
可通过Releases · espressif/svd下载自己芯片的svd文件,以便在调试时可以加载芯片的外设和内存布局,我的芯片是esp32c3,因此下载红色箭头所示
下载完成后可以放到项目的新建目录里,也可以放在其他地方(路径不能包含中文)
接着,启用OpenOCD调试,找到外设,然后加载svd文件
然后勾选全部,点击关闭
下方就是外设情况了,由于展示的外设比较多,可以点击两下折叠,把它们收起来
内存视图,比如随便点击一个任务句柄,就能自动跳转了,也可以自己输入指定地址
7、多个子项目
思路
考虑到这样一种情形,即需要在一个工程里开发多个子项目,比如使用Mesh组网时,难免需要对根节点和叶子节点烧录两种不同的固件。对于根节点和叶子节点这两个子项目来说,创建两个完全独立的工程是不划算的,因为它们之间有很多可以共享的组件。
同时考虑到一个构建目录包含一种固件,需要多个固件就需要多个构建目录,可以说多个构建目录是必不可少的。而不同的cmake配置文件需要不同的构建目录恰好契合这一点,并且不同构建目录具备隔离好、频繁切换不会影响构建等优点。因此,我们需要不同的cmake配置文件来构建不同的子项目,进而可以通过选择不同的cmake配置文件来产生不同的固件。
一个简单的方法就是使用ESP-IDF的自定义组件功能,创建如下的目录组织结构。在不同子目录里的CMakeLists通过idf_component_register来注册组件,在不同的cmake配置文件里为相同的变量赋予不同的值,在根目录的CMakeLists里通过对变量进行条件分支设置不同的子目录为主组件目录。
变量这方面可以通过menu config或者kconfig,但问题是只要修改这种配置就需要重新编译全部文件,会相当麻烦,所以应在cmake配置文件中设置变量
GPSTimeMesh/ ├── components/ │ ├── root_node/ │ │ ├── CMakeLists.txt │ │ └── main_root.c │ └── leaf_node/ │ ├── CMakeLists.txt │ └── main_leaf.c └── CMakeLists.txt但这有些问题,因为在ESP-IDF中自定义组件需要自己填写依赖,只有main组件可以自动解析所有依赖,这样会很麻烦。而且自己填写依赖实际上并没有节省多少编译时间,该编译九百多个文件还是得编译。为此,我们需要确保多个子项目使用同一个main组件,那么我们就需要这样的目录。如果项目更加复杂,完全可以在各自的子项目里配置.cmake文件
GPSTimeMesh/ ├── main/ │ ├── CMakeLists.txt │ ├── root_node/ │ │ └── main.c │ └── leaf_node/ │ └── main.c └── CMakeLists.txt由于idf.py.exe的命令默认构建目录为build,因此对于包含多个子项目的工程来说,需要对不同命令指定不同的构建目录,使用终端脚本配置是很不方便的,而使用CMake应用程序配置很方便,因为ESP-IDF会帮我们处理这些底层(除了monitor目标由于特殊原因不能直接构建目标外)。所以应使用CMake应用程序配置来执行对应操作。这其中需要注意可能出现的错误:执行monitor命令显示缺少CMakeLists或者报一堆链接缺失错误,应参考监视的完全之体配置
创建
那么接下来就根据上述内容来演示,现在我们需要如下三个子项目,为其创建不同的子目录,在main目录里的CMakeLists中,创建if-else语句来区分不同子项目,其NODE_TYPE变量实际上是通过cmake配置传入的,这里设置变量是为了给它一个默认值
# 设置默认目标(可选,方便切换) set(NODE_TYPE "root" CACHE STRING "Node type: root or leaf") if(${NODE_TYPE} STREQUAL "root") set(SRC_FILE "root_node/main.c") message(STATUS "Building root node") elseif(${NODE_TYPE} STREQUAL "leaf") set(SRC_FILE "leaf_node/main.c") message(STATUS "Building leaf node") else() set(SRC_FILE "wifi/main.cpp") message(STATUS "Building main") endif() idf_component_register(SRCS ${SRC_FILE} INCLUDE_DIRS "")
接下来编辑cmake配置文件,先为wifi项目创建一个cmake配置文件,在CMake选项里通过-D来传入这个变量的值。构建目录清空,让它自己生成默认的构建目录。-j16是指使用16线程并行编译,为空时CLion默认使用最大核数-2进行并行构建,比如默认为-j14时,自己主动加2就是使用最大核数进行并行构建
-DNODE_TYPE=wifi
点击复制,然后如法炮制其他cmake配置文件
此时可以看到,三个cmake配置文件同时加载,并且多了三个构建目录
此时可以通过选择不同的配置文件来决定对哪个子项目进行构建
可以正常执行monitor命令,如果出现错误,可参考监视的完全之体
调试
但由于之前的调试服务器中GDB脚本里指定的是具体的路径,并不适合如今的情况。因此调试服务器里也得改,脚本内容如下,其中只有第二个add-symbol-file后面的文件名需要替换成自己的,其他两个文件已经由CLion的宏自动指定了。
只不过因为操蛋的Windows路径中混账的反斜杠,导致原本优雅的宏插入
file "$CMakeCurrentProductFile$" add-symbol-file "$CMakeCurrentBuildDir$/bootloader/bootloader.elf" add-symbol-file "D:/DevTools/Espressif/tools/esp-rom-elfs/20240305/esp32c3_rev3_rom.elf"变成了这副鬼样子,使用$UnixSeparators()$也没用(CLion有一部分锅,不知道未来会不会修复)
file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-$CMakeCurrentProfileName$/$CMakeCurrentTargetName$" add-symbol-file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-$CMakeCurrentProfileName$/bootloader/bootloader.elf" add-symbol-file "D:/DevTools/Espressif/tools/esp-rom-elfs/20240305/esp32c3_rev3_rom.elf"完整内容只能如下了,路径需要自己替换。其中
cmake-build-$CMakeCurrentProfileName$
是个拼好名,指代的是cmake-build-xxx这个构建目录的目录名
set confirm off set remotetimeout 10 target extended-remote :3333 monitor reset halt file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-$CMakeCurrentProfileName$/$CMakeCurrentTargetName$" add-symbol-file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-$CMakeCurrentProfileName$/bootloader/bootloader.elf" add-symbol-file "D:/DevTools/Espressif/tools/esp-rom-elfs/20240305/esp32c3_rev3_rom.elf" maintenance flush register-cache thbreak app_main
总之,调试可行了
此时想要使用插件自带的单步调试不太适合了,因为它现在还不支持插入CLion的宏,不能随时自动切换项目可执行文件,只能创建多个不同的调试配置,分别指定文件
8、单元测试
有些代码不依靠硬件外设,属于纯逻辑代码,如果通过烧录来验证代码情况会相当麻烦。为此需要使用到单元测试来模拟运行情况,即在本机上运行那些纯逻辑代码是否存在逻辑错误等。而CLion提供了 Google Test、Boost.Test、Catch2 和 Doctest 的集成,出于平衡这里选择Google Test
升个配置
使用Google Test需要下载对应的库,使用CMake进行自动下载容易出错,推荐使用vcpkg下载
在左侧找到Vcpkg的窗口
搜索gtest,在三元组里找到x64-mingw-static这个版本(CLion在Windows下默认使用MinGW工具链)。先不要进行安装,因为此时的工具链并非是MinGW,直接安装会失败
为此,需要在把当前配置文件临时改成MinGW,用于下载gtest库,改完之后CMake报错不用管
点击安装后,可以看到已经正确安装了,然后把前面临时修改的工具链重新修改为ESP-IDF
接着在项目根目录下的CMakeLists中添加一个分支用于分流
cmake_minimum_required(VERSION 3.16) option(BUILD_UNIT_TESTS "Build unit tests" OFF) message(STATUS "[Test]:${BUILD_UNIT_TESTS}") # 如果没有定义 IDF_TARGET,说明是在本地环境运行测试(CLion 默认行为) if(BUILD_UNIT_TESTS) # --- 本地 MinGW + vcpkg 单元测试逻辑 --- project(GPSTimeMesh_Test) set(CMAKE_CXX_STANDARD 23) # 自动寻找 vcpkg 提供的 GTest find_package(GTest CONFIG REQUIRED) # 定义测试可执行文件 # 注意:你需要把逻辑代码和测试代码分开管理,或者直接包含头文件目录 file(GLOB SRC_FILES "test/*.cpp" ) add_executable(unit_tests "${SRC_FILES}" ) # 链接 GTest target_link_libraries(unit_tests PRIVATE GTest::gtest_main) # 包含业务代码路径 target_include_directories(unit_tests PRIVATE ./ test/include) else() include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(GPSTimeMesh) # 添加链接器内存使用报告 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--print-memory-usage") ## 查找 idf.py.exe #find_program(IDF_PY_EXE # NAMES idf.py.exe idf.py # HINTS ${IDF_PATH}/tools # PATHS ENV IDF_PATH # DOC "IDF Python tool executable" #) endif()
同时在根目录创建test目录,并且创建一个test_main.cpp文件(空的也行)
// // Created by fairy on 2026/1/2. // #include <gtest/gtest.h> int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }在CMake配置文件中,添加一个名为Debug-Test的配置文件,并且强制指定三元组
-DVCPKG_TARGET_TRIPLET=x64-mingw-static
然后再添加这个选项
-DBUILD_UNIT_TESTS=ON
在Vcpkg里把仓库添加到Debug-Test配置文件中
进行CMake时可能会报如下错误,不知道为什么直接指定路径会报这些错误,使用file函数指定就行了,如
file(GLOB SRC_FILES "test/*.cpp" ) add_executable(unit_tests "${SRC_FILES}" )
在运行/调试配置中添加GTest
然后选择单元测试目标
此时可以看到cmake配置文件已经可以正常加载了
通过切换运行/调试配置,可以自动切换cmake配置文件
如果这其中出现CMake错误,比如出现一堆libusb、clang.exe什么的,请重新加载CMake缓存
进行单元测试(未完)
接下来,需要添加一些文件来模拟esp-idf里的一些依赖具体硬件外设的头文件,如esp_mesh.h等。需要添加单元测试,则直接创建对应的.cpp或者.c,并添加对应的Test函数即可。
下面先以一个简单的文件为例,BaseProtocol.hpp里只有定义的类型,并没有包含esp相关的头文件
// // Created by fairy on 2026/1/1. // #pragma once #include <chrono> #include <concepts> #include <type_traits> #include <cstdint> // 基础协议标签 // 定义 Mesh 数据包的 Concept 约束 template <typename T> concept MeshDataPacket = T::is_protocol == true && // 为防止将一个随机的配置结构体传给发送或者接收数据包函数 std::is_standard_layout_v<T> && // 必须是标准布局(内存连续) std::is_trivially_copyable_v<T>; // 必须可平凡复制(无虚函数、无指针成员) // --- 业务数据结构 --- #pragma pack(push, 1) // 确保字节对齐 struct SensorPacket { static constexpr bool is_protocol = true; uint8_t head = 0xAA; uint8_t node_id{}; std::chrono::milliseconds uptime_ms{}; uint16_t sequence{}; uint8_t tail = 0xFF; }; struct ControlPacket { static constexpr bool is_protocol = true; uint8_t head = 0x55; std::chrono::milliseconds new_interval{}; // 任务四:控制指令 uint8_t tail = 0xEE; }; #pragma pack(pop)为此,我们可以编写这样的一个测试文件protocol_test.cpp
需要注意,既然是测试BaseProtocol.hpp的测试用例,那么就需要在测试用例里包含它的头文件,而这个头文件可以在上图中看出,它并不是在项目根目录的,而是在main/mesh目录里。因此,CMakeLists里需要添加这个头文件目录,确保可以直接通过下面这种方式包含:
#include "BaseProtocol.hpp"如果不添加这个头文件目录,则需要这样添加:
#include "main/mesh/BaseProtocol.hpp"
运行测试,可以看到测试结果,界面与VS的测试资源管理器差不多。想要什么样的测试,就需要使用GTest提供的对应工具
&问题集锦
1、cmake加载失败
原因:idf_cmd_init.bat默认指定的IDF_PATH是ESP-IDF安装目录,但示例工程里的IDF_PATH却应是ESP-IDF安装目录下的示例工程对应版本的目录。想要正确使用ESP-IDF里的cmake构建体系,需要自己指定好IDF_PATH
如下图,左边箭头所示方向是IDF-PATH默认目录,右边箭头是示例工程所需的IDF-PATH目录
解决办法:
①修改idf_cmd_init.bat中的IDF-PATH,将其指定为示例工程所需目录。适合于不常更新ESP-IDF版本的情况
在idf_cmd_init.bat开头添加如下语句,路径替换成自己对应的目录
:: 设置示例工程所需的IDF_PATH set IDF_PATH=D:\DevTools\Espressif\frameworks\esp-idf-v5.3.1②在CMake配置中重新指定IDF-PATH
综合考量:
从便捷角度来说,方法①更优,因为更换ESP-IDF版本的概率不会很高
2、monitor错误
现象:运行monitor配置时出现如下错误
原因:未指定串口
解决办法:详情请参考调试
现象:指定端口后,仍会出现波特率错误
原因:CLion运行目标时并没有提供真正的终端,即使勾选模拟终端也会被识别出来
解决:直接运行终端或者修改idf_monitor.py源码,这里推荐前者。详情请参考调试
或是直接修改monitor目标,其位于xxx\Espressif\frameworks\esp-idf-v5.3.1\components\esptool_py目录下的project_include.cmake文件中,搜索add_custom_target(monitor即可见到
3、ESP-IDF插件的OpenOCD调试错误
现象:Simple Debug出现缺失symbol文件错误(忘记截图了,并且解决后暂时未能复现问题)
原因:可能未构建或者修改过构建目录,也有可能是USB连接相关问题
现象:Custom Debug的调试界面会卡住
在运行里会出现一堆错误
原因:可能未构建或者修改过构建目录,也有可能是USB连接相关问题
解决办法:重新构建 或者 删除运行配置中对应的Simple Debug/Custom Debug目标并重新添加一个新的对应目标(重新添加会自动应用新的构建目录)或者 重新插拔开发板,一般来说,重新插拔开发板即可解决问题
4、__lock静态断言错误
现象:编译报错
D:/DevTools/Espressif/frameworks/esp-idf-v5.3.1/components/newlib/locks.c:240:1: error: static assertion failed: "Incorrect size of struct __lock"
240 | _Static_assert(sizeof(struct __lock) >= sizeof(StaticSemaphore_t),
| ^~~~~~~~~~~~~~表面原因:启用CONFIG_FREERTOS_USE_TRACE_FACILITY宏后,会导致StaticSemaphore_t结构体变大,进而导致静态断言失败
根本原因:未通过Menu Config所代表的sdkconfig机制进行FreeRTOS相关宏设置,而是直接对FreeRTOSConfig进行修改,添加宏configUSE_TRACE_FACILITY,进而导致原始错误
解决方案:
①注释这段代码(不推荐) 因为这可能会导致运行时内存溢出、锁机制失效、系统不稳定或崩溃、难以调试的死锁等问题
②通过Menu Config启用configUSE_TRACE_FACILITY,详情见FreeRTOS集成功能
5、OpenOCD与GDB调试相关问题
①Windows下使用OpenOCD出现libusb_open()错误
现象:libusb_open()属于Linux环境下出现的错误却在Windows出现了
原因:后台已经运行一个OpenOCD了
解决方案:关闭后台的OpenOCD
②GDB主动断开连接
现象:出现Remote replied unexpectedly to 'vMustReplyEmpty': vCont;c;C;s;S
原因:GDB与OpenOCD不兼容,导致协议无法对接;或者GDB命令不适配
解决方案:对于前者,更换适配的GDB和OpenOCD
对于后者,使用适配的GDB命令,对于此处,有两种gdb调试方案:
琪一,在idf环境下运行idf.py gdb,写成bat脚本如下。同时需要注意,CLion的终端默认是powershell而非cmd,因此配置终端目标时,解释器应为powershell而非cmd,否则没有输出。只不过这个方案可能会出现错误③,我还没有找到解决方案
@echo off :: 进入idf环境 call "D:/DevTools/Espressif/idf_cmd_init.bat" timeout /t 1 /nobreak > nul :: 启动gdb idf.py gdb pause其二,使用gdb执行正确的命令,对于riscv32-esp-elf-gdb.exe来说,需要加载两个symbol文件和一个项目可执行文件,具体脚本如下,脚本来源为ESP-IDF插件的源码仓库yunyizhi/ESP-IDF-for-Clion中有关GDB命令序列的代码片段
set confirm off # 需要替换成自己环境下的文件 add-symbol-file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-debug-esp-idf/bootloader/bootloader.elf" add-symbol-file "D:/DevTools/Espressif/tools/esp-rom-elfs/20240305/esp32c3_rev3_rom.elf" file "D:/Projects/MCU/Esp32/GPSTimeMesh/cmake-build-debug-esp-idf/GPSTimeMesh.elf" set confirm on set remotetimeout 10 target remote :3333 monitor reset halt maintenance flush register-cache thbreak app_main执行时,可先启动gdb,然后通过source命令加载gdb脚本文件。大致情况如下:
> xxx\riscv32-esp-elf-gdb.exe > (gdb) source your_script.gdb此外,上述错误如果是使用运行/调试配置中的嵌入式GDB服务器目标或OpenOCD下载并运行目标时出现的,那么请转到使用OpenOCD进行单步调试 (进阶)
③GDB反复复位
现象:一直不停复位,反复出现如下语句:
[esp32c3] Hart unexpectedly reset!
[esp32c3] Reset cause (3) - (Software core reset)
[esp32c3] Hart unexpectedly reset!
[esp32c3] Reset cause (3) - (Software core reset)
[esp32c3] Hart unexpectedly reset!
原因未知,全网仅能搜到为数不多的issue,但搜不到任何解决方案
可能的原因:做过大量尝试,最终发现可能与USB连接、OpenOCD或GDB程序已运行、gdb命令和elf文件有关,情形复现:
①使用调试服务器下载,但是下载失败,之后进行调试时会不断出现此错误。需要重新烧录并插拔开发板
前置准备:仔细检查gdb需要的三个elf文件是否存在,比如bootloader那个,如果不存在需要重新编译。然后仔细检查三个文件路径是否输入正确,因为使用CLion,一般来说默认使用的是cmake-build-xxx构建目录,而ESP-IDF默认使用的是build目录,如果不好区分,那么统一改成build目录。接着再检查gdb命令是否正确,做好这一切后,插拔开发板,重新运行OpenOCD和GDB。至于gdb命令可以查看错误②,正因为这些命令不同于一般gdb的输入(gdb xxx.elf、monitor reset halt等),因此CLion默认的嵌入式GDB服务器目标等都不适合
可能的解决方案:重新烧录,然后插拔开发板,再重新调试
6、flash命令执行失败
现象1:使用CMake目标里的flash,抛出cmake错误,指向串口问题
现象2:使用idf命令行里的flash,出现超时
原因:未知,可能是前面我测试OpenOCD服务器,上传文件之类导致的。虽然显示找不到串口,但实际上设备管理器里是存在的
解决方案:插拔开发板
7、断点太多并卡住调试
错误来源:调试服务器
现象:刚点击爬虫进行调试,会卡住,翻看GDB发现是断点太多,不能插入断点
原因:根本原因未知,但不可能是断点太多导致的,因为调试前断点数量我并未改变。可能原因应是设备运行中不能直接插入断点,必须要停止(按理来说这条命令我在gdb脚本中就完成了,不知道为什么会与设备设置选项卡相关)
解决方案:在调试服务器的设备设置选项卡中添加halt,点击确定
8、执行monitor命令显示缺少CMakeLists或者报一堆链接缺失错误
现象:在main.c所在位置显示缺失CMakeLists.txt
原因:idf.py flash monitor在命令行执行时,如果不指定工程目录,那么就默认当前你在CLion打开并获得焦点的那个文件的所在目录为工程目录(这很阴了)。所以有时候错误能复现,有时候又不能复现,让人还以为是cmake错误呢
解决方案:idf.py -B <构建目录> -C <工程目录> flash monitor
现象:莫名其妙地显示缺失链接库
原因:执行idf.py monitor命令时没有指定构建目录,idf.py默认为build目录,但由于app_main函数所在文件位置没有CMakeLists(比如7、多个子项目中就会出现这种情况)
解决方案:通过-B参数指定构建目录,如idf.py -B xxx monitor
9.烧录错误_空间不足
现象:Error: app partition is too small for binary GPSTimeMesh.bin size 0x113cc0:
- Part 'factory' 0/0 @ 0x10000 size 0x100000 (overflow 0x13cc0)原因:esp32c3默认分区表上限是1MB,体积超了
解决:到menuconfig里调整上限。进入Partition Table里
选择更大的分区(比如第二个,large),或者自定义,最后点击保存
参考:
文档
Clion配置ESP32开发,一文就够了_clion esp32-CSDN博客
Clion OpenOCD ESP32 IDF 实现调试Debug功能 JTAG 调试_esp32 debug-CSDN博客
C++ 支持 - ESP32 - — ESP-IDF 编程指南 v5.5.2 文档
仓库
yunyizhi/ESP-IDF-for-Clion: 为CLion提供IDF支持
视频
更多推荐











































































































































































所有评论(0)