第2章 开始进行

C语言并不难,但它是低级语言,你需要适应使用它来与硬件打交道。软件部分可能很简单,但要习惯与硬件交互所涉及的各种理念则是另一回事——你得换种思路思考。简单来说,时间很重要。这一点在本书的其余部分会变得清晰,而在这类硬件编程中,事情发生的具体时间和顺序是核心问题,这通常意味着需要尽可能高效的编程语言——因此C语言是完美的选择。

ESP-IDF

乐鑫(Espressif)为ESP32推出的官方软件开发工具包(SDK)是其物联网开发框架ESP-IDF,该框架可在Windows、Linux和macOS系统上运行,支持C语言和C++,是目前使用C语言为ESP32编程的最佳方式。

开始使用一款新的处理器及其相关的软件开发工具包(SDK)通常是一件耗时又令人沮丧的事情。一般来说,你必须搭建一个工具链,并让它能与你选择的编辑器协同工作。这包括找到并安装编译器和构建系统。尽管编译器通常属于GCC家族,构建系统也多以CMake为基础,但这并不意味着只要你有相关使用经验,整个过程就会一帆风顺。每个SDK通常都会对编译器和构建系统进行调整,这是你必须学习甚至可能需要修改的部分。

学习曲线可能会比较陡峭,但ESP-IDF的安装很简单,而且只要你使用VS Code,用起来会更简单。它也支持Eclipse,当然,你也可以随意将它与任何开发系统搭配使用,但本书的其余部分会使用VS Code,并且强烈推荐使用它。

安装VS Code

开始使用没有任何先决条件,你只需要一台能运行VS Code的电脑。如果你还没有使用过VS Code,首先要做的就是安装它。由于其安装步骤经常变动,最好的建议是按照官网上的最新说明进行操作:https://code.visualstudio.com/ 。除非有特殊原因,否则接受所有默认设置即可。

安装好 VS Code 后,你需要添加 C/C++ 扩展:

安装C/C++扩展

如果你安装了C/C++扩展包,系统会推荐另外两个C/C++扩展,并且会自动安装CMake和CMake工具。对于大型多文件项目,CMake是管控项目的最佳方式,但对于像“Hello World”这样的小型单文件示例,你并不需要它。如果VS Code提示要为你配置CMake,选择“忽略CMake”选项。ESP-IDF确实会用到CMake,但它的使用方式能让你在大多数情况下不用特意去管它。

安装IDF

安装好 VS Code 后,你可以搜索 ESP-IDF 扩展:

安装IDF扩展

这个扩展的安装过程不会太久,安装完成后,你会在左侧边栏看到ESP的标志:

边条

选中它,欢迎界面就会出现。你可以通过这个界面安装SDK,此时最简单的做法是选择“快速设置”(Express setup),除非你已经安装过SDK了。

idf插件欢迎页面

如果你选择“快速设置”,会弹出另一个对话框,让你选择要安装的SDK版本以及安装位置。默认设置通常是可以接受的。

快速安装页面

点击“安装”后,包含工具、编译器、链接器等的完整SDK将被下载并安装。这一过程需要数十分钟,具体取决于你的网络连接速度。

安装完成后,你会看到一个新页面,上面有创建或导入项目的选项。你可以使用这些选项,也可以使用左侧扩展栏中的对应菜单项:

安装完成

第一个项目

开始操作前,你需要一块ESP32或ESP32 S3(如果你愿意对操作步骤做些小修改,其他型号也可以)。对于初次操作,最简单的做法是用USB线将开发板连接到开发电脑上。如果你使用的是S3开发板,连接哪个USB接口都可以,但直接连接USB端口的速度会更快。

创建新项目的方法有很多,包括选取一个现有的示例并对其进行修改,但大多数情况下,你会希望从一个最小化项目开始。要做到这一点,首先选择“新建项目向导”,然后等待它加载完成。

创建项目

当“新建项目”标签页出现时,输入项目名称和要使用的目录。较难的是选择开发板。除非你使用的是其他型号,否则选择ESP-WROVER-KIT 3.3V。其他选项的主要区别在于开发板的编程方式。这一点以及所使用的ESP32型号后续都可以更改。如果你已经连接了ESP32,应该能从下拉列表中选择它的COM端口。如果端口没有显示,你可能需要在主机上安装USB转串口驱动程序。

新项目页面

接下来选择“Choose Template”(选择模板)按钮,页面显示后,选择“template-app”选项——还有许多其他模板,你可以之后再探索:

选择项目模板

“template-app”是一个极简项目,其main.c文件中没有任何代码。点击“Create Project”(创建项目),然后使用“File”(文件)菜单中的“Open Folder”(打开文件夹)命令打开“HelloWorld”文件夹。系统会自动创建几个子文件夹,不过目前我们只需要关注“main”文件夹,在那里你会找到C主程序文件main.c

main.c文件

在 VS Code 编辑器中,将以下代码输入到该文件中:

#include <stdio.h>

void app_main(void)
{
    printf("Hello World\n");
}

现在我们需要编译并运行这个程序。不要使用VS Code的标准选项,而要使用IDF扩展提供的新选项。

首先我们需要为编译器设置目标芯片,在我们的例子中,可以是esp32或esp32s3,不过对于其他型号的芯片,操作流程完全相同。

目标选择

选好正确的处理器后,下一步是选择闪存和调试方式。由于我们尚未设置调试功能,这一步目前意义不大——详情见后文。选择任意选项即可:

openocd调试选择

现在我们已经准备好对程序进行编译、烧录和监控了。你可以通过ESP-IDF扩展菜单栏中的命令来完成这些操作:

边条工具栏

“编译”(Build)命令会针对你所选的目标芯片构建项目。首次编译程序时,所有使程序能运行的文件都会被编译——这个过程可能需要很长时间。通常,一次完整的全新编译可能需要10多分钟,但后续的编译会快得多,因为只有修改过的文件才会被重新编译。

然后你可以使用“烧录”(Flash)命令将其安装到目标芯片中。此时会提示你选择烧录设备的方式:

烧写方式选择

三个选项分别是:

  • JTAG(联合测试行动组)
  • DFU(设备固件更新,仅在S3型号上可用)
  • UART(通用异步收发传输器)

选择UART,程序就可以顺利下载到ESP32中。

程序下载完成后会立即运行,但由于没有终端连接到串口,我们无法查看运行结果。不必单独连接串口终端,最简单的方法是使用“Monitor”(监视器)命令运行监控程序。这会重启你刚刚烧录的程序,此时你应该会看到:

I (304) main_task: Started on CPU0
I (314) main_task: Calling app_main()
Hello World
I (314) main_task: Returned from app_main()

在“Hello World”之前还会出现很多信息。SDK包含大量日志消息,这些消息在你创建新应用时有时会很有用。在生产环境的应用中,你可以关闭这些消息。

这就完成了你的第一个应用程序。每次开始一个新应用时,你只需重复这些步骤即可。注意,如果你对配置进行了重大更改(例如更换目标芯片),则会执行一次完整的全新编译,所需时间会更长。

图标和面板命令

到目前为止,我们已经使用了左侧边栏中的扩展命令,不过窗口底部的状态栏也包含了大部分你需要的操作和信息:

面板图表

在大多数情况下,你可以通过点击图标来设置数量或运行任务。例如,点击目标芯片图标可以设置目标芯片型号。注意,“编译/烧录/监控”是一个复合命令,它会先编译项目、进行烧录,然后通过运行监控程序让你查看结果。

调试选项无法正常工作,除非你将开发板设置为支持调试模式,详情见后文。

通过ESP-IDF扩展的左侧边栏和状态栏可使用的命令只是最常用的那些。完整的命令列表可通过以下方式找到:使用快捷键CTRL+SHIFT+P打开命令面板,然后输入“esp”,即可筛选出以“ESP”开头的命令。

命令栏

配置SDK

大多数时候,你只需按照上述方法编写程序并运行即可,但偶尔也需要进行一些调整。ESP-IDF扩展提供了SDK配置编辑器,用于更改各项功能的运行方式。使用该编辑器的唯一问题是,其中的选项过多,导致查找所需内容并不容易。使用它的最佳方法是在搜索框中输入合适的关键词。例如,如果你想更改编译器的配置方式,可以通过下拉列表进行相关设置。

SDK配置

如果你在搜索框中输入“compiler”(编译器),会找到相同的一组选项。

一个ESP-IDF项目由多个组件构成。每个组件都可以单独编译,并且拥有自己的CMake文件来指定编译方式。这些文件可以通过SDK配置编辑器进行配置。大多数情况下,你无需为此操心,因为构建系统会在初始构建时包含标准组件。大多数硬件外设(如SPI、PWM、GPIO等)都有相应的组件,这些组件会自动包含在你的项目中,并且可以通过SDK配置编辑器进行配置。

这一点与大多数SDK不同,你无需费心配置想要使用系统的哪些部分。所有标准组件都会包含在初始构建中,而最终可执行文件只会链接你实际使用到的组件。这种方式的唯一缺点是首次构建时间较长,而且如果更改了项目配置,就需要重新进行完整构建。

你可以使用“创建ESP-IDF组件”命令创建自己的组件来扩展系统。通常情况下,除非你打算将新组件分发给其他用户,否则无需这样做。ESP-IDF有一个组件注册表,你可以在其中查找并安装组件。具体操作是:打开VS Code的命令面板,找到“ESP-IDF 显示组件注册表”命令。通过该命令,你可以浏览可用的组件,并将任何你想要使用的组件添加到自己的项目中。

还需要记住的是,项目是从主组件(main component)开始的。只要主组件位于项目目录的“main”文件夹中,它就会被自动添加到构建过程中。由于所有其他组件都作为依赖项添加到主组件中,因此整个项目会连同主组件一起被构建。你可以通过更改主组件所在文件夹的名称来修改主组件的名称,但之后必须在其CMake文件中指定所有你想要使用的组件。

监视器

在ESP32运行程序时,与其进行交互的标准方式是使用监视器程序。这是一个Python应用程序,它实现了一个定制的串口终端。在调试构建后,当ESP32运行你的应用时,监视器会接收并显示其生成的众多系统消息。

关于监视器程序,最重要的一点是它在加载时会重启ESP32。比如说,如果你正在用逻辑分析仪捕获输出,这可能会让你感到意外。你会看到程序在首次烧录后运行,然后在监视器启动时再次重启。这并非故障。

监视器会显示并解析ESP32通过串口发送的日志消息。例如,当循环中不断生成警告和错误消息时,日志输出可能会非常繁多。你可以使用其中一个监视器命令来停止输出。

要向监视器发送命令,你首先需要按下CTRL+T,然后再按下某个标准命令:

快捷键 命令
CTRL+] 退出程序
CTRL+T 菜单退出键,重复按可发送CTRL+T
CTRL+] 向远程设备发送退出字符
CTRL+P 通过RTS和DTR线将目标设备重置到引导加载程序模式以暂停应用程序
CTRL+R 通过RTS线重置目标板
CTRL+F 编译并烧录项目
CTRL+A (or A) 仅编译并烧录应用程序
CTRL+Y 停止/恢复在屏幕上打印日志输出
CTRL+L 停止/恢复将日志输出保存到文件
CTRL+I (or I) 停止/恢复打印时间戳
CTRL+H (or H) 显示所有键盘快捷键
CTRL+X (or X) 退出程序
CTRL+C 中断正在运行的应用程序

你可以使用SDK配置编辑器关闭所有日志记录:

log配置

如果你想阻止监视器重置ESP32,似乎没有简单的方法来修改内置命令,但你可以打开一个ESP32终端并输入以下命令:

idf.py monitor --no-reset -p COM3

其中,你需要将COM3替换为ESP32所连接的串口。

一个常见的错误是,由于串口正被占用,命令无法执行完成。这通常是因为有一个打开的终端仍在使用该串口。解决此问题最简单的方法是关闭所有终端窗口:

windows列表

使用JTAG进行调试

为ESP32设置调试可能有些棘手,因为这需要使用JTAG(联合测试行动小组)适配器。有些ESP32开发板(包括ESP32 S3)内置了JTAG适配器,这样就无需再使用外接适配器了。这是一个显著的优势,因为它不仅省去了购买外部JTAG适配器的麻烦,还能释放外部适配器所需的四个GPIO引脚。

所有ESP32开发板都支持JTAG总线,但它会使用四个GPIO线路,而这些线路有时会被分配给其他用途。在这种情况下,除非禁用正在使用这些引脚的功能,否则无法设置JTAG。

在学习后续章节中的示例时,你无需设置调试功能,实际上,许多程序员都依靠监视器和日志消息来调试自己的程序。你可以稍后再着手处理调试相关的事情,但在某个阶段进行调试设置是非常值得的,因为从长远来看,这会为你节省大量时间。可能出现的问题有很多,关于这个主题,有一个专门的常见问题解答(FAQ):https://github.com/espressif/openocd-esp32/wiki/Troubleshooting-FAQ

你的第一个问题是选择一款JTAG适配器,市面上有很多种。唯一需要注意的是,ESP32不支持SWD(串行线调试),SWD是ARM处理器对JTAG的一种改进。不过,ESP32可以与同时支持JTAG和SWD的适配器配合使用。一个安全且价格合理的选择是使用ESP-Prog,这是乐鑫(Espressif)设计的一款JTAG和串口适配器:

jtag

这款适配器由多家厂商生产,售价不到20美元。它唯一的缺点是有些功能你可能用不上。例如,它有一个串口,可用于给ESP32烧录程序和进行监视,还有一个非标准的带状电缆连接器,大多数开发板都无法使用。它还支持3.3V(常规电压)和5V。

与其尝试使用ESP-Prog的所有功能,不如直接使用开发板上的USB连接来进行烧录(Flash)和监视(Monitor),这样更为简便。另一种选择是,使用JTAG适配器给开发板烧录程序,而仅用USB连接来进行监视:

jtag link

假设你有一块ESP-Prog板,将其连接到ESP32相对容易。你需要将大型JTAG连接器上的四个引脚以及地线连接到ESP32的GPIO线路上:

jtag pin

ESP32 Pin ESP32 S3 Pin JTAG Signal
MTDO / GPIO15 MTDO / GPIO40 TDO
MTDI / GPIO12 MTDI / GPIO41 TDI
MTCK / GPIO13 MTCK / GPIO39 TCK
MTMS / GPIO14 MTMS / GPIO42 TMS

请注意,ESP32 S3的引脚连接仅在通过烧写电子熔丝(eFuse)激活后才生效。烧写DIS_USB_JTAG电子熔丝将永久禁用USB_SERIAL_JTAG与ESP32-S3的JTAG端口之间的连接。

烧写STRAP_JTAG_SEL电子熔丝(eFuse)后,将能够通过一个栓锁引脚(GPIO3)来选择JTAG接口。当该引脚设为低电平时,将使用GPIO线路;设为高电平时,将使用USB。实际上,最好使用内置的JTAG USB端口,这样可以让GPIO线路保留下来供通用用途使用。

ESP32 DevC开发板的连接方式如下:

jtag link esp32

GPIO12是一个栓锁引脚,用于控制SPI闪存端口的电压。如果在启动时该引脚为低电平,则会使用3.3V进行闪存操作。理论上,ESP-Prog应通过设置延迟来处理这一问题,确保在尝试连接前ESP32已启动,但有时这一机制会出现问题。实际操作中,这意味着你应在ESP32启动后再将GPIO12连接到ESP-Prog,或者先给ESP32通电,再给ESP-Prog连接USB电源。另外,你也可以通过以下方式设置闪存电压,从而禁用该栓锁引脚:

espefuse.py set_flash_voltage 3.3V

请注意,这是一项不可逆的更改。在撰写本文时,无法通过VS Code的IDF终端完成此操作。

一旦你将ESP-Prog连接好并接上线缆,最大的问题就是确保安装了正确的驱动程序。在Windows系统下,这可以通过Zadig工具来完成。从https://zadig.akeo.ie/下载该工具,并用它将“Dual RS232-HS”设备的驱动程序设置为WinUSB。你只需为接口0执行此操作,但为两个接口都设置也没有坏处,因为你可以将ESP32提供的USB连接用作串口。要查看所有设备,你必须选择“选项”中的“列出所有设备”。设置完成后,你需要重启电脑,并检查驱动程序是否已更改。

Zadig

现在你可以使用调试命令来设置断点并单步执行程序。

Debug

请注意,在撰写本文时,重启命令无法正常工作。若要重新运行程序,只需断开连接并再次使用调试命令即可。

JTAG烧录功能可用,且可能比UART烧录更快。

如前所述,ESP32 S3具有内置的JTAG适配器,它通过USB C接口进行连接。通常情况下,你只需将两个USB C接口都连接到开发机器即可:

deboule-typec

使用内置JTAG的一大问题还是在于USB端口的驱动程序。在撰写本文时,SDK并未为Windows系统安装正确的驱动。你需要再次使用Zadig来安装驱动,其中重要的是JTAG/串行调试单元(接口2),它需要安装libusbK驱动:

zadig-usb-jtag

接口1只是一个标准的USB串行端口。

在Linux系统下,需要添加OpenOCD的udev规则,具体操作是将位于https://github.com/espressif/openocd-esp32/blob/master/contrib/60-openocd.rules的udev规则文件复制到/etc/udev/rules.d目录中。

完成这一更改并重启后,JTAG调试功能应该就能正常工作了,但在撰写本文时,仍存在一些问题。首先,你需要使用JTAG USB设备提供的COM端口,这样才能进行烧录和监视操作。如果想要通过JTAG对ESP32 S3进行烧录,需要先启动OpenOCD,再选择烧录选项,因为让烧录命令自动启动会失败。你可以使用调试命令,它能让你单步执行程序,还会显示局部变量等值。不过,重启命令无法使用,若要重新运行程序,你需要先断开与OpenOCD的连接,再重新使用调试命令。

如果想要查看调试输出,你需要在开始调试前打开一个监视器终端。

如果系统陷入混乱,且尝试运行调试时出现超时,那么请停止OpenOCD,重置ESP32 S3,然后让调试命令自动加载OpenOCD。

提示

VS Code的ESP扩展运行良好,但在使用过程中你会学到一些快捷操作和“重置”方法,以下是一些能帮助你快速上手的技巧:

  • 如果你看到错误消息提示由于无法访问串口而导致某些操作失败,原因通常是有一个终端正在占用该串口。只需通过右侧的列表关闭所有终端即可:

提示图1

  • 对目标进行修改,以及对程序进行许多其他重大修改时,会导致整个SDK重新编译。你可以在“构建任务”终端窗口中跟踪其进度,在该窗口中可以看到已编译组件数/总组件数的统计。

提示图2

  • ESP-IDF终端可用于输入大多数SDK命令,因为它已设置好正确的路径。
  • 你可以使用监视器(Monitor)进行大量调试工作,但它存在一个问题:会自动重启程序。为避免这种情况,可使用ESP-IDF终端并输入:idf.py monitor --no-reset -p port 其中,“端口”需替换为连接ESP32所用的串口。
  • 若要在调试时查看程序输出,可在开始调试前启动监视器。你可以使用–no-reset选项,以避免在调试开始前必须运行程序。
  • “调试单步跳过”(Debug Step Over)命令并非总能正常工作,对于任何被调用的函数,你可能需要逐句单步执行。解决办法是在函数后面设置一个断点。
  • 如果你发现断点和单步执行无法正常工作,请检查是否记得将当前构建的版本烧录到设备中。有可能出现使用更新后的源代码但设备中仍是过时机器码的情况。
  • 查看当前主程序的汇编清单通常很有用。最简单的方法是启动一个ESP-IDF终端,然后使用以下命令:xtensa-esp32-elf-gdb path to .elf file 接着使用命令:disassemble /m function。其中,“函数名”是你想要反汇编的函数的名称。

摘要

  • 你可以通过命令行或Eclipse IDE对ESP32进行编程,但最简便的入门方式是使用VS Code及其ESP-IDF扩展。
  • 安装好VS Code、IDF扩展和ESP-IDF SDK后,只需通过USB将开发板连接到开发机器,你就能快速创建第一个程序。
  • 每当开始新的开发任务时,可使用“新建项目”向导和“template-app”模板创建一个空项目。
  • IDF扩展为VS Code添加了许多命令,但大多数情况下,你可以通过扩展菜单或底部工具栏找到所需功能。
  • 首次编译程序时,由于要编译整个SDK,可能需要数十分钟。后续编译会更快,因为只需重新编译必要的部分。对项目的重大修改可能会触发完全重新编译。
  • 你可以使用监视器(Monitor)查看应用程序的输出。
  • 对于简单调试,监视器通常足够用,但迟早你会需要基于JTAG的调试。
  • 对于ESP32,你需要一个JTAG适配器,如ESP-Prog。而ESP32 S3内置了适配器,只需再进行一次USB连接即可。
  • 让JTAG正常工作的主要问题在于为适配器安装正确的驱动程序。