在开始今天的水文前,老周先要奉劝一下国内某些嵌入式砖家和穴者,不要看不起 Arduino,它不是一种开发板,而是一种规范。Arduino 的思想是正确的,把各种开发板封装为统一的 API,让许多开源库共享相同的基础代码,严重降低了移植成本。Arduino 本质上使用的是 C/C++ 开发,只是统一了 API,对性能其实没多大影响。论"套娃"的程度,至少比 STM32 的 HAL 库要好。其实 ST 真不如把精力放在写一套 Arduino API 和开发 VS Code 插件上更有价值。虽然某 hub 上有 stm32 的 Arduino 库,但好像不是官方的。ST 的库代码还有一个缺点:你用 CudeMX 下载SDK时会发现,你选择的每个 STM 型号都要单独下载一份,假如每个型号的SDK要2个G,那么,你用了三个型号,就要下载6G的东西。这玩意儿我就觉得很奇怪,难道各个型号之间就没有共同的代码?
相比而言,老周觉得乐鑫的封装更科学。虽然 esp32 也有N多型号,可人家都共用一个 idf 库,只是通过宏定义来选择有型号差异的代码,其他部分的代码是共享的。esp32 idf 的封装很 *nix 风格,更加应用化的结构。老周是相当认同这种思维的,嵌入式步入物联网时代,就应该以"应用开发"的视角去对待,统一化是很有必要的,就像跨平台一样。
乐鑫在 Arduino(哦,顺便提一下,这个读"阿嘟伊诺",重音落在"诺"上,以后不要乱读了,就像现在很多人连 Python 的名字都读错,小心被大蟒蛇咬你,网络时代那么方便,你都不去查查字典。C#是不是还有人读"C井"?)方面的贡献还是很积极的,估计也是这个原因让 esp32 火遍全球。Arduino 和 idf 构成了 esp32 开发的主流框架,基于.NET 的 NanoFramework 也是基于 idf 的,就是体积有些大,而且多线程方面还没优化好,很容易崩掉。
因为 Arduino 也是乐鑫自己维护的,所以,它和 idf 框架是可以同时使用的。其好处很明显:arduino 的开源库多,这么一来,等于把两大框架的资源都整合了,咱们在用的时候也可以少造些轮子了。
等等,老周,你好像离题了。没有的,只是咱们要一些准备知识。esp32 的代码是由组件构成的,idf 本身也是这样,在 components 目录下就是基本组件,由于 idf 太大了,后来又拆出一个扩展组件库(https://github.com/espressif/idf-extra-components)。组件多了,相互之间的依赖关系就很烦人,于是,乐鑫提供了一个组件存储库(ESP Component Registry),可以通过命令行在项目中安装组件,它会自动下载组件代码,包括存在依赖关系的组件,有点 Nuget 的感觉了,属于是自动化处理了。下载的组件会放在项目目录下的 managed_components 子目录下。假设,我要语音识别的 esp_sr 组件,那么,打开 component registry 主页,搜索 esp_sr。点击进入详细页,在页面右侧,它会告诉你要执行的命令,直接复制命令,到 VS Code 中点击"打开idf终端",然后粘贴执行就行(前提 VS Code 要装esp idf 插件)。当然,在组件详细页面的右侧,还有下载链接,你可以直接下载代码,作为本地组件使用。

组件目录下会有个名为 idf_component.yml 的文件,用来描述当前组件依赖哪些组件。这个文件结构不复杂,不过咱们目前不需要了解。
esp 组件的名字就是其目录的名字,比如:
abc
|---include
| |---dot.h
|---dot.c
|---CMakeLists.txt
那个这个组件就叫 abc,组件的目录下必须有 CMakeLists.txt 文件,该文件中会调用 idf 定义的 cmake 函数 idf_component_register 函数来注册组件。
有了上面的认识后,咱们定制本地组件就容易多了。虽然用组件管理器可以自动下载组件,但它也是有缺点的:
1、组件下载时和 Nuget 一样,会有缓存的(位于用户目录下,如 C:\Users\Who\AppData\Local\Espressif\ComponentManager\Cache\XXXX);
2、每个项目都会存一份,如果多个项目都用到某些组件,那么就会出现很多重复文件。如果你喜欢用 Arduino,那么你建15个项目,就会有15个重复的 arduino 组件目录;
3、由于新旧版本的过渡,有些 API 会用不了,比如你要把 esp32 变成USB鼠标,那么 USBHIDMouse 类现在是用不了的,因为 tinny_usb 库和版本的原因,有些组件的引用需要手动改。
打造本地组件虽然麻烦,但,你只需干一次的活,而且 99.9 % 的情况下不用改代码,尽量只改 CMake 文件(语音识别API除外,因为这个目前只能用英文识别,要识别中文,就得改掉源代码)。Arduino 的 API 一般都封装得很好,调用起来就是几行代码的事,很难不让人喜欢。当然了,过度封装有时候也会带来麻烦,尤其是需要指定自定义参数的时候,所以,打造本地化组件,就可以根据咱们的需求改。就算库升级也不影响,替换下文件就好了。
说了那么多热身知识,现在可以动手了。首先,到 ESP Component Registry 或 Github 上下载 arduino-esp32 的源代码。
1、espressif/arduino-esp32 • v3.2.0-rc2 • ESP Component Registry
2、https://github.com/espressif/arduino-esp32(如果打不开,自己找镜像加速)
不管你用哪种方式,搞到源代码就行。把它解压出来,放到一个你喜欢的目录中(其实路径含有中文也没报错,貌似有的机器会报错),记得把目录改为 arduino-esp32,这是组件名称,后面要用。你放组件的目录下应该是这样的:
├─arduino-esp32
│ ├─.github
│ │ ├─ISSUE_TEMPLATE
│ │ ├─pytools
│ │ ├─scripts
│ │ └─workflows
│ ├─cores
│ │ └─esp32
│ │ ├─apps
│ │ │ └─sntp
│ │ └─libb64
......
│ ├─libraries
│ │ ├─ArduinoOTA
│ │ │ ├─examples
│ │ │ │ └─BasicOTA
│ │ │ └─src
│ │ ├─AsyncUDP
│ │ │ ├─examples
│ │ │ │ ├─AsyncUDPClient
│ │ │ │ ├─AsyncUDPMulticastServer
│ │ │ │ └─AsyncUDPServer
│ │ │ └─src
│ │ ├─BLE
......
│ │ ├─BluetoothSerial
│ │ │ ├─examples
│ │ │ │ ├─bt_classic_device_discovery
│ │ │ │ ├─bt_remove_paired_devices
│ │ │ │ ├─DiscoverConnect
│ │ │ │ ├─GetLocalMAC
│ │ │ │ ├─SerialToSerialBT
│ │ │ │ ├─SerialToSerialBTM
│ │ │ │ ├─SerialToSerialBT_Legacy
│ │ │ │ └─SerialToSerialBT_SSP
│ │ │ └─src
│ │ ├─DNSServer
│ │ │ ├─examples
│ │ │ │ └─CaptivePortal
│ │ │ └─src
│ │ ├─EEPROM
│ │ │ ├─examples
│ │ │ │ ├─eeprom_class
│ │ │ │ ├─eeprom_extra
│ │ │ │ └─eeprom_write
│ │ │ └─src
......
│ │ ├─ESPmDNS
│ │ │ ├─examples
│ │ │ │ ├─mDNS-SD_Extended
│ │ │ │ └─mDNS_Web_Server
│ │ │ └─src
│ │ ├─ESP_I2S
│ │ │ ├─examples
│ │ │ │ ├─ES8388_loopback
│ │ │ │ ├─Record_to_WAV
│ │ │ │ └─Simple_tone
│ │ │ └─src
│ │ ├─ESP_NOW
│ │ │ ├─examples
│ │ │ │ ├─ESP_NOW_Broadcast_Master
│ │ │ │ ├─ESP_NOW_Broadcast_Slave
│ │ │ │ ├─ESP_NOW_Network
│ │ │ │ └─ESP_NOW_Serial
│ │ │ └─src
......
│ │ ├─HTTPClient
│ │ │ ├─examples
......
│ │ │ └─src
前面讲过了,组件名称就是包含代码的目录,所以要保留 arduino-esp32 目录,假设你把这个目录放到 D:\my_libs 目录下。
现在开始改,尽量只改 CMakeLists.txt 文件,不要动配置文件(Kconfig.projbuild)和源代码。打开 arduino-esp32 目录下的 CMakeLists.txt 文件,注意,开头有一段判断 idf 版本的代码,看看你安装的 idf 版本达不达标。
set(min_supported_idf_version "5.3.0")
set(max_supported_idf_version "5.4.99")
set(idf_version "${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}.${IDF_VERSION_PATCH}")
if ("${idf_version}" AND NOT "$ENV{ARDUINO_SKIP_IDF_VERSION_CHECK}")
if (idf_version VERSION_LESS min_supported_idf_version)
message(FATAL_ERROR "Arduino-esp32 can be used with ESP-IDF versions "
"between ${min_supported_idf_version} and ${max_supported_idf_version}, "
"but an older version is detected: ${idf_version}.")
endif()
if (idf_version VERSION_GREATER max_supported_idf_version)
message(FATAL_ERROR "Arduino-esp32 can be used with ESP-IDF versions "
"between ${min_supported_idf_version} and ${max_supported_idf_version}, "
"but a newer version is detected: ${idf_version}.")
endif()
endif()
要是你有把握不会有兼容性问题的话,你可以把以上这一段注释掉。
接下来是 CORE_SRCS 变量,它包含封装好的 arduino 基础库 API,这个不要去改,不用改,不用改!
set(CORE_SRCS
cores/esp32/base64.cpp
cores/esp32/cbuf.cpp
cores/esp32/ColorFormat.c
cores/esp32/chip-debug-report.cpp
cores/esp32/esp32-hal-adc.c
cores/esp32/esp32-hal-bt.c
......
cores/esp32/FirmwareMSC.cpp
cores/esp32/firmware_msc_fat.c
cores/esp32/wiring_pulse.c
cores/esp32/wiring_shift.c
cores/esp32/WMath.cpp
cores/esp32/WString.cpp
)
然后,重点来了,ARDUINO_ALL_LIBRARIES 变量表示 esp32 封装的所有库。这里咱们可以注释掉一些不常用的,保留一些常用的。
set(ARDUINO_ALL_LIBRARIES
# ArduinoOTA
AsyncUDP
BLE
BluetoothSerial
DNSServer
EEPROM
ESP_I2S
ESP_NOW
# ESP_SR
ESPmDNS
Ethernet
FFat
FS
HTTPClient
# HTTPUpdate
# Insights
# LittleFS
# Matter
# NetBIOS
Network
# OpenThread
# PPP
Preferences
# RainMaker
SD_MMC
SD
SimpleBLE
SPIFFS
SPI
Ticker
# Update
USB
WebServer
NetworkClientSecure
WiFi
# WiFiProv
Wire
Zigbee
)
这里头有些库是有依赖关系的,比如 LittleFS。当你要用到有依赖的库时,你只要下载它,放到你的组件目录下即可,比如
my_libs
|--- arduino-esp32
|--- littlefs
你只要告诉 idf 去 my_libs 目录下搜索组件就行,编译时会自动添加进去。后面咱们改好文件后再讨论这个。
继续,在设置 ARDUINO_ALL_LIBRARIES 变量后,就是一串变量,组织各个库的源代码,我们不用改。一直把文件拉到最后,把最后这一段全部注释掉。
# if(IDF_TARGET MATCHES "esp32s2|esp32s3|esp32p4" AND CONFIG_TINYUSB_ENABLED)
# maybe_add_component(arduino_tinyusb)
# endif()
# if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_ArduinoOTA)
# maybe_add_component(esp_https_ota)
# endif()
# if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_ESP_SR)
# maybe_add_component(espressif__esp_sr)
# endif()
# if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_Matter)
# maybe_add_component(espressif__esp_matter)
# endif()
# if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_LittleFS)
# maybe_add_component(joltwallet__littlefs)
# endif()
# if(NOT CONFIG_ARDUINO_SELECTIVE_COMPILATION OR CONFIG_ARDUINO_SELECTIVE_WiFiProv)
# maybe_add_component(espressif__network_provisioning)
# endif()
这些都是要依赖的组件,咱们暂时不需要。我们要保证常用的库能编译通过,以后用到哪个再手动配置。而且,这些库里面有个 arduino_tinnyusb 是找不到的。这个库藏得很深,在 GitHub - espressif/esp32-arduino-lib-builder 这个库里面。遗憾的是这个库只是包含了配置文件,实际上是没有 tiny_usb 的源代码的。实际上,在 idf 中用到的 arduino 库也不需要 arduino-lib-bulder 库,这个是给 Arduino IDE 用的(需要先编译)。
好了。把 CMakeLists.txt 文件保存,咱们改完了,现在到 VS Code 中新建一个空项目。
1、在左侧工具栏找到 ESP 插件的图标。点击"新项目向导"。

2、设置项目名、存放路径,至于开发板和串口,可以随便,反正后面可以改的。

3、点击"Choose Template" 按钮,进入下一页。模板选"template-app"就好了。

4、点击右上角的"Create project using <你选的模板>"按钮,这样项目就创建了。
此时,VS Code 右下角会弹出提示,问你要不要打开新项目,如果是,就会打开新的 vs code 窗口;选择否,就留在当前 vs code 窗口,然后我们手动打开项目目录。
这时候,可以先选好开发板型号,不然后面一改动又要重新配置了。当然,你玩熟悉了的话也没啥。点击状态栏中的"设置乐鑫设备目标"。

比如,老周用来测试的板子是 LilyGO 的,型号是 esp32 S3,然后烧录方式选 builtin USB JTAG。

这个你得看一下你买的开发板的原理图,USB是不是与 ESP32 的USB引脚直接连接的,即没有使用 USB 转串口芯片。Esp32 S3 和 P4 这些是可以直接连 USB 引脚的。如果使用了像 CH34XXX 这些转换芯片,就要选另外两个(这两个可以任选,反正没啥影响)。实际上,如果只是考虑烧录的话,这里三个选项是可以随便选(即使没有 USB 转换串口芯片也能以 UART 方式烧录),但,如果要考用 Arduino 库的 Serial 对象从 USB 接口输出,就得看有没有 USB 转串口芯片了。
其实,VS Code 中你不装 idf 插件也能玩的,有 cmake 插件就行,毕竟其本质上就是用 cmake 来构建的。只是有 idf 插件能节省很多事情。
注意,项目里面有两个 CMakeLists.txt 文件,一个位于项目目录下,一个位于 main 目录下。如果你还记得老周前文中说的内容,你会猜到,其实位于 main 目录下的 CMakeLists.txt 文件是描述 idf 组件的。没错,main 组件,默认会把项目目录下名为 main 的组件添加到构建列表中。所以:
1、位于项目目录下的 CMakeLists.txt 文件是配置整个项目的;
2、位于 main 目录下的 CMakeLists.txt 文件只配置 main 组件。
Arduino 库会验证 FreeRTOS 的 tick 频率是否为 1000 Hz,默认是 100,不符合要求,生成的时候会报错的。所以,咱们要先改一下,点击 VS Code 状态栏上的"SDK配置编辑器(menuconfig)"按钮,找到 FreeRTOS 节点,然后把"TICK_RATE_HZ"改为 1000。然后点击右上角的"保存"按钮,关闭配置页。

当然,你还可以直接把 arduino-esp32 组件中的验证关闭,方法是打开 arduino-esp32 组件的 CMakeListst.txt 文件,把以下片段注释掉。
# if(NOT CONFIG_FREERTOS_HZ EQUAL 1000 AND NOT "$ENV{ARDUINO_SKIP_TICK_CHECK}")
# # See delay() in cores/esp32/esp32-hal-misc.c.
# message(FATAL_ERROR "esp32-arduino requires CONFIG_FREERTOS_HZ=1000 "
# "(currently ${CONFIG_FREERTOS_HZ})")
# endif()
但是,老周建议不要注释掉这段代码,最好还是把 TICK_RATE 设置为 1000 hz,或者定义 ARDUINO_SKIP_TICK_CHECK 环境变量来跳过。
咱们要先告诉 idf,arduino-esp32 在哪里找,打开项目级别的 CMakeLists.txt 文件,在包含 project.cmake 文件之前设置 EXTRA_COMPONENT_DIRS 变量。这个变量是可以设置多个值的,如果你希望 idf 从多个目录查找组件,可以指定多个路径,路径之间用空格或换行来隔开就行了。
cmake_minimum_required(VERSION 3.5)
# 添加查找组件目录
set(EXTRA_COMPONENT_DIRS
"D:\\my_libs"
)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_arxj pp)
一定要在 include 指令之前设置,否则就无法配置了。
为了防止组件管理器自动下载依赖的组件,需要设置 IDF_COMPONENT_MANAGER 环境变量来禁用组件管理器。
cmake_minimum_required(VERSION 3.5)
# 禁用组件管理器
set(ENV{IDF_COMPONENT_MANAGER} 0)
# 添加查找组件目录
set(EXTRA_COMPONENT_DIRS
"D:\\my_libs"
)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_app)
如果你想减少编译的代码,还可以加上这一行:
cmake_minimum_required(VERSION 3.5)
# 禁用组件管理器
set(ENV{IDF_COMPONENT_MANAGER} 0)
# 添加查找组件目录
set(EXTRA_COMPONENT_DIRS
"D:\\my_libs"
)
# 设置要编译的组件
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_app)
COMPONENTS 变量列出要编译的组件,只指定 main 就是只编译 main 组件。但你不用担心,main 所依赖的组件会被编译。这样可以减少一些不需要的代码(实际上也少不了多少)。
项目级别咱们配置完了,随后要配置 main 组件。打开 main 目录下的 CMakeLists.txt 文件,在 idf_component_register 调用时添加对 arduino-esp32 的依赖。顺便把 SRCS 参数(源代码文件)中的 main.c 改为 main.cpp。毕竟 arduino 用的是 C++。
idf_component_register(SRCS "main.cpp"
INCLUDE_DIRS "."
REQUIRES arduino-esp32)
如果你刚才解压缩 arduino 组件时,把目录重命名为其他,那么 REQUIRES 参数后面的组件名称也要对应,比如,你把目录重命名为 esp-arduino,那么这里就要写上 esp-arduino。
保存并关闭 CMakeLists.txt 文件。这样,整个项目就改好了。
接下来把 main.c 文件重命名为 main.cpp。代码如下:
#include <stdio.h>
#ifdef __cplusplus
extern "C"
{
#endif
void app_main(void)
{
// 此处是你的代码
}
#ifdef __cplusplus
}
#endif
这里你不需要判断 __cplusplus 宏也行,直接写 extern "C" 也可以,反正文件后缀就是 .cpp 了。因为 app_main 函数在 idf 源代码中已定义(app_startup.c)。
static void main_task(void* args)
{
ESP_LOGI(MAIN_TAG, "Started on CPU%d", (int)xPortGetCoreID());
#if !CONFIG_FREERTOS_UNICORE
// Wait for FreeRTOS initialization to finish on other core, before replacing its startup stack
esp_register_freertos_idle_hook_for_cpu(other_cpu_startup_idle_hook_cb, !xPortGetCoreID());
while (!s_other_cpu_startup_done) {
;
}
// 此处省略1033个字
extern void app_main(void**)**;
app_main();
ESP_LOGI(MAIN_TAG, "Returned from app_main()");
vTaskDelete(NULL);
}
app_main 用 extern 关键字定义,这允许它可在项目的 main.cpp 文件中去实现,编译时编译器会自己去找 app_main 函数的实现代码。也就是说,esp32 应用程序默认是启动了一个作为主任务的 task,并运行它。若运行完就删除该任务。不过,现在 main.c 被咱们改为 main.cpp 了,由于 C++ 导出函数名的时候会在名称上加一些序号(C++支持重载,加序号为了针对函数重载),导致 app_startup.c 中的代码链接不到 app_main 函数的代码,加上 extern "C" 就是让它以 C 语言的方式导出函数名。防止 C 代码找不到 app_main。
咱们把 arduino.h 头文件包含进来。
#include <stdio.h>
#include "Arduino.h"
#ifdef __cplusplus
extern "C"
{
#endif
void app_main(void)
{
// 此处是你的代码
}
#ifdef __cplusplus
}
#endif
然后,点击 VS Code 状态栏上的"打开 ESP-IDF 终端"按钮,输入 idf.py reconfigure,执行,看看,如果能生成配置,说明前面的修改没有问题了(尽管这里面埋了个深雷,但炸不死人)。

最后,咱们写点代码试试。
#include <stdio.h>
#include "Arduino.h"
// 设置/初始化
void setup()
{
Serial.begin(115200);
}
// 在循环中调用的函数
void loop()
{
Serial.println("Hello !!!");
}
#ifdef __cplusplus
extern "C"
{
#endif
void app_main(void)
{
// 1、调用初始化函数
initArduino();
// 2、配置/初始化相关外设
setup();
// 3、进入循环
while (1)
{
loop();
}
}
#ifdef __cplusplus
}
#endif
是不是跟 Arduino 程序很像了?哦,记住要调用 initArduino 函数进行初始化。现在点击"构建项目"按钮,看看能不能编译。耐心等吧,要编译 1000 多个对象呢。
毫无意外的,埋的雷终于爆了(没有找到 esp_timer 组件)。
fatal error: esp_timer.h: No such file or directory
22 | #include "esp_timer.h"
| ^~~~~~~~~~~~~
咱们重新打开 arduino-esp32 组件的 CMakeLists.txt 文件看看它所设置的依赖列表。
set(priv_includes cores/esp32/libb64)
set(requires spi_flash esp_partition mbedtls wpa_supplicant esp_adc esp_eth http_parser esp_ringbuf esp_driver_gptimer esp_driver_usb_serial_jtag driver)
set(priv_requires fatfs nvs_flash app_update spiffs bootloader_support bt esp_hid usb esp_psram ${ARDUINO_LIBRARIES_REQUIRES})
依赖组件列表中没有直接指出 esp_timer 组件,由于咱们注释掉了一些库,导致 esp_timer 组件没有被包含进去。所以,咱们要在依赖列表中显式地指定 esp_timer,不过,我们可以不改这里,而是在 main 组件中设置依赖,也能达到同样效果。
把 arduino 组件的 CMakeLists.txt 关闭,打开 main 组件下的 CMakeLists.txt,在 idf_component_register 调用中,REQUIRES 参数中加上 esp_timer 和 esp_netif 即可(这里就连 esp_netif 组件一起加了,反正需要的,因为咱们编译了网口和 Wifi 相关的类)。
idf_component_register(SRCS "main.cpp"
INCLUDE_DIRS "."
REQUIRES
esp_timer
esp_netif
arduino-esp32)
然后,再编译一次。这一次可以成功编译了。
这时,大伙伴们会说:还不够像,Arduino 主文件一个只有 setup 和 loop 函数。可以的,现在你点击 VS Code 状态栏上的 "SDK配置编辑器"按钮,打开配置页,找到"Arduino Configuration"节点,勾选上"Auto start Arduino setup and loop on boot",然后点击"保存"按钮并关闭配置页面。

接着,main.cpp 文件只保留 setup 和 loop 函数即可。
#include "Arduino.h"
// 设置/初始化
void setup()
{
Serial.begin(115200);
}
// 在循环中调用的函数
void loop()
{
Serial.println("Hello !!!");
}
现在像了吧。不是很像,简直一模一样。
确认改好后,删除 build 目录,把项目压缩到一个 .zip 文件中,以后用 Arduino 项目,你就直接用它来创建新项目。嵌入式项目经常这么搞的,嵌入式应用的库没有一般软件开发那么好摆弄,经常需要整理、移植和订制。
-------------------------------------------------------------- 可能出现的问题 ----------------------------------------------------------
有大伙伴烧录并重置开发板后,用串口工具打开发现串口无输出。也就是 Serial 对象不工作,这是什么原回呢?这个还得看你的开发板原理图,图可以找卖家要,如果卖家不给,直接给他差评。看看你的板子是不是有 USB 转串口,并且 USB 是不是连在转换芯片上,只有这样连上串口才能输出。如果 esp32 引脚是直接连到 USB 接口(不管是不是C口都一样)的,那么,Serial 对象不能是 UART(HardwareSerial 类),而应该是 HWCDC 类。
要解决这个问题,先来看看源代码:
#if ARDUINO_USB_CDC_ON_BOOT //Serial used from Native_USB_CDC | HW_CDC_JTAG
#if ARDUINO_USB_MODE // Hardware CDC mode
// Arduino Serial is the HW JTAG CDC device
#define Serial HWCDCSerial
#else // !ARDUINO_USB_MODE -- Native USB Mode
// Arduino Serial is the Native USB CDC device
#define Serial USBSerial
#endif // ARDUINO_USB_MODE
#else // !ARDUINO_USB_CDC_ON_BOOT -- Serial is used from UART0
// if not using CDC on Boot, Arduino Serial is the UART0 device
#define Serial Serial0
#endif // ARDUINO_USB_CDC_ON_BOOT
// There is always Seria0 for UART0
extern HardwareSerial Serial0;
#if SOC_UART_NUM > 1
extern HardwareSerial Serial1;
#endif
#if SOC_UART_NUM > 2
extern HardwareSerial Serial2;
#endif
#if SOC_UART_NUM > 3
extern HardwareSerial Serial3;
#endif
#if SOC_UART_NUM > 4
extern HardwareSerial Serial4;
#endif
#if SOC_UART_NUM > 5
extern HardwareSerial Serial5;
#endif
从上面代码来看,要使用 USB JTAG 从 USB 接口直接输出,需要定义两个宏:ARDUINO_USB_CDC_ON_BOOT 和 ARDUINO_USB_MODE。
为了方便配置,这个宏咱们不在代码中定义,而是在项目级别的 CMakeLists.txt 文件中定义。
cmake_minimum_required(VERSION 3.20)
# 此处省略2125个字
# 定义开启 USB 串口的宏
*add_compile_definitions( ARDUINO_USB_CDC_ON_BOOT*=1 ARDUINO_USB_MODE=1*)*
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_app)
再次编译,烧录。打开串口监视工具,就能看到输出了。
还有一个问题:程序运行几秒钟后,开发板就会重启,报错如下:
E (10346) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
E (10346) task_wdt: - IDLE1 (CPU 1)
E (10346) task_wdt: Tasks currently running:
E (10346) task_wdt: CPU 0: IDLE0
E (10346) task_wdt: CPU 1: loopTask
E (10346) task_wdt: Print CPU 1 backtrace
原因是触发了任务看门狗。看门狗你可以理解为一条带着定时器的狗,假设它的过期时间是 5 秒,你必须在 5 秒内喂狗。狗吃饱了就会把定时器清零,重新计数,然后你也要在 5 秒内喂狗,不然狗饿了会咬人。一旦你超过 5 秒没有喂狗,就认为开发板死机了,强制重启。
灵长目动物都知道,RTOS 是基于多任务的轻量级系统。在启动的时候,esp32 程序会为每个 CPU 核创建一个"空闲"任务。0 核上的叫 IDLE0,1 核上的叫 IDLE1。这两个任务之所以闲着,就因为它们的职责只是喂狗。而我们的 loop 函数(在名为 loopTask 的任务上)是在一个死循环中执行的,并且程序没有让出 CPU 时间片,这导致空闲任务动不了,就喂不了狗,然后就超时了。
最简单的解决方法就是禁止任务看门狗(task_wdt 取消任务订阅),这样就不会触发了。做法是在 setup 函数中调用 disableCoreXWDT 函数,其中,X表示 CPU 的核,即 loop 函数所在核,这个通过 ARDUINO_RUNNING_CORE 选项可以配置,默认是第二核,即 Core1,如下图所示。

故,setup 函数中应加入以下代码:
void setup()
{
#if ARDUINO_RUNNING_CORE == 0
disableCore0WDT();
#else
disableCore1WDT();
#endif
Serial.begin(115200);
}
毕竟配置文件可能把 ARDUINO_RUNNING_CORE 配置为 Core0 或 Core1,所以这里要判断一下,再确认调用 disableCore0WDT 还是 disableCore1WDT 函数。
这样修改后,就不会出现任务超时重启了。不过,这样改是有风险的。这样就等于说 loop 函数所在的任务霸占了所有 CPU 时间,如果应用程序是多任务的话,那其他任务就无法执行了。所以,禁用任务看门狗的做法仅适用于单任务应用程序(你整个项目只用一个 task);如果是多任务的项目,就应该在 loop 函数中调用 delay 函数来延时一下,让出 CPU 时间给其他任务运行。
好了,今天就水到这里了,信息量太大了怕大伙伴们看着不爽。下一篇水文老周就说说自制 USB 鼠标键盘的 Arduino 修改方案。