65.9K
CodeProject 正在变化。 阅读更多。
Home

Arduino 的调试器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2015 年 10 月 9 日

CPOL

22分钟阅读

viewsIcon

178049

downloadIcon

3987

适用于 Arduino 的源代码级别调试器,使用 GDB 和 Eclipse。

引言

本文介绍用于 Arduino 的源代码级调试器。此调试器可用于单步执行代码、设置断点、查看变量等。它不需要对 Arduino 板进行任何修改或外部硬件。它只是添加到您的 Arduino 程序中的一段代码。它适用于基于 ATmega328 微控制器(经 Arduino Uno 测试)的 Arduino,也适用于带有 ATmega2560 或 ATmega1280(Arduino Mega)的 Arduino。此调试器有一些限制,正如文章末尾所描述的那样,但我相信它会对许多人有用。

当一个有其他计算机平台编程经验的人开始使用 Arduino 时,他或她通常会惊奇地发现没有调试器。运行程序后,除了通过串行监视器打印消息和/或闪烁 LED 之外,无法查看内部发生的情况。诚然,可以通过这种方式调试程序,但在某些情况下,单步执行代码或在特定时刻查看变量可以节省大量时间。
您可能会想:“好吧,这是一个微控制器;我必须接受这个。” 但那不是真的。如今,微控制器通常具有与“大型计算机”类似的调试功能。只是 Arduino 平台在设计时没有调试器——IDE 中没有调试器接口,板上(硬件中)也没有直接的硬件支持。这并不是说 Arduino 不好。省略调试器是一个有效的设计决策,而且对于预期的“非程序员”受众来说,调试器可能过于复杂。但 Arduino 变得如此受欢迎,以至于现在几乎所有需要使用微控制器的人,包括程序员,都在使用它。对于这些人来说,调试器是一个有用的东西。

关于 Arduino 调试有一个相当普遍的误解——如果您使用真正的 IDE,例如 Eclipse 或 Atmel Studio,来开发您的 Arduino 程序,您也能够调试它。诚然,这些 IDE 包含调试器,但是您的程序无法与调试器通信。实际上,在 Arduino 上获得调试器功能的方法有:

  1. 购买外部调试设备(例如 AVR Dragon)并将其连接到您的 Arduino 板。您需要对您的 Arduino 板进行小幅修改。
  2. Atmel Studio 的 Visual Micro 调试器插件。我一年多以前尝试过它,所以如有疑问,请自行检查,但当时我认为这并不是一个真正的调试器。它不支持单步执行代码,并且它基于在构建之前插入到您的程序中的隐藏代码来与调试器通信。
  3. 这个调试器

有什么新功能?

2019 年 6 月:此调试器也可用于 Visual Studio Code IDE。有关说明,请参阅此处。我还添加了将其用作 Arduino 库的选项——请参阅此项目GitHub 存储库中的 arduino/library 子文件夹,或从此处的 VS Code 文章下载。

本文的 2018 年 1 月修订版增加了对两个新功能的描述,这些功能极大地改进了此调试器:

  • 将断点写入闪存
  • 通过调试器加载程序

有了这两个功能,调试体验与任何标准基于硬件的调试器都非常相似。通过闪存中的断点,调试器的主要限制得以克服——现在程序可以以全速运行并插入断点。在调试器的旧版本中,程序运行速度明显较慢。仍然可以使用原始的所谓 RAM 断点来避免闪存磨损。

当使用通过调试器加载程序的功能时,您可以一键加载程序并开始调试。无需先用 AVR dude 上传,然后开始调试会话。

2017 年 1 月,添加了对 Arduino Mega(ATmega2560 和 ATmega1280)的支持。我还发现有时可以使用直接串行连接,而无需 TCP 到串行转换器(代理)。这似乎适用于 Windows 10 上的所有板,在 Windows 7 上仅适用于 Arduino Mega。下面的教程也已更新,以描述直接连接。

背景

本节提供有关调试器工作原理的一些信息。如果您只想尽快开始调试,请随意跳到下一节。

现代微控制器包含支持调试其内部程序的电路。在 Arduino Uno 中使用的 ATmega328 微控制器的情况下,此电路称为 debugWire。如上所述,问题在于 Arduino 平台并非设计用于使用它,而且您需要一块额外的硬件才能通过 debugWire 与微控制器通信,这会增加成本并使事情复杂化。

如果我们不想(或不能)使用 debugWire,我们可以使用串口进行调试。为此,MCU 中的程序必须与调试器“通信”——我们的程序必须包含一些处理此通信的代码。原则上,当您使用 Serial.print() 输出一些信息来调试您的程序时,这就是您所做的。但是用 Serial.print() 命令填充您的代码并不舒服。最好有一段代码在后台与调试器通信,这样您就不需要担心它。这样一段代码很早就被发明出来了,在串口是与 MCU 通信的唯一方式的时代。它通常被称为远程 stub。这个 stub 必须能够控制程序——在某些点停止它,读写内存等。
那另一边呢——您的 IDE 安装在的 PC 电脑呢?您可能会想象为 PC 编写一个程序来与调试器 stub 通信。但没必要重复造轮子。GDB——GNU 调试器——可以处理这个问题,并且它受到许多 IDE 的支持,包括 Eclipse。

总而言之,本文描述的调试器是用于 ATmega328 微控制器的 GDB stub。它能够通过 Arduino 板提供的虚拟串口与 PC 上的 GDB 调试器通信。通过这种方式,您可以使用 GDB 调试 Arduino 中的程序,并(或不)使用一些图形前端(调试器 GUI),例如 Eclipse IDE 中包含的那个。
  

如何使用调试器

请注意,在 Eclipse 中设置调试程序所需的所有内容可能看起来相当复杂,特别是如果您只有 Arduino IDE 的经验并且从头开始。另一方面,如果您是经验丰富的程序员,这应该不难。

在本文中,我将尝试给您一些关于如何设置和使用调试器的想法,但没有足够的空间涵盖所有细节。下载包中提供了包含分步说明的完整文档——请参阅doc子文件夹中的 avr_debug.pdf
以下是需要完成的工作概述:

  1. 设置 Eclipse IDE,使其能够为 Arduino 构建(和调试)您的程序。我建议阅读上面提到的文档中的教程,或者我之前在 codeproject 上的文章:在 Eclipse 中创建 Arduino 程序
  2. 将调试器库添加到您的 Arduino 程序中 - 此库(驱动程序)随本文提供。
  3. 在 Eclipse 中设置调试器 (GDB)。

 

步骤 1 - 设置 Eclipse IDE

此步骤的目标是能够在 Eclipse IDE 中构建您的 Arduino 程序。如上所述,本调试器的文档或我的早期文章中提供了分步说明。您还可以使用其他处理在 Eclipse 中构建 Arduino 程序的教程。

此类教程中未涵盖的一个重要步骤向 Eclipse 添加 GDB 硬件调试启动配置。要执行此操作:

在 Eclipse 中,转到菜单“帮助”>“安装新软件…”

在“使用”框中输入以下更新站点地址,然后按 Enter 键

http://download.eclipse.org/tools/cdt/releases/8.6

稍后,窗口中的列表将显示一些项目。

展开 CDT 可选功能类别并选择 C/C++ GDB 硬件调试。

按照向导安装此功能,并在提示时重新启动 Eclipse。

Arduino Mega 注意事项

配置 AVR Eclipse 插件以上传您的程序(使用 avrdude)时,请使用

  • 对于 ATmega2560 配置文件“Wiring”和波特率 115200。
  • 对于 ATmega1280 配置文件“Arduino”和波特率 57600。

步骤 2 - 向程序添加调试器支持

首先,将随附的 zip 文件解压到计算机上的某个文件夹中,例如 c:\avr_debug。最好路径中没有空格和/或特殊字符。

接下来,将 avr8-stub.cavr8-stub.h 文件添加到 Eclipse 中的项目中。
这些文件位于 avr_debug 文件夹中的 avr8-stub 文件夹中。
您可以从文件管理器中拖动文件,然后将它们放到 Eclipse 的项目资源管理器视图中的项目上。
在拖放文件后出现的“文件操作”窗口中选择“复制文件”选项。

打开您的项目属性(Alt + Enter)并转到 C/C++ 构建 > 设置。

选择 AVR C++ 编译器 > 调试。在“调试信息格式”中选择 dwarf-2。

 

对 AVR C 编译器执行相同的操作。

单击“确定”关闭“首选项”窗口。

在您的源文件中包含调试器头文件 avr8-stub.h

#include "avr8-stub.h"

并在 main() 的开头调用

debug_init();

提示:您也可以在代码中插入对 `breakpoint()` 函数的调用,程序将在该行停止。但您也可以在调试时“动态”插入断点。

这是一个可以尝试的示例程序

#include "arduino.h"
#include "avr8-stub.h"

void setup(void)
{
    debug_init();
    pinMode(13, OUTPUT);
}

void loop(void)
{
    breakpoint();
    digitalWrite(13, HIGH);
    delay(200);
    digitalWrite(13, LOW);
    delay(500);
}

尝试构建程序。

会出现一些构建错误。链接器抱怨“__vector_1 和 vector 18 的多重定义”。这些是 INT0 外部中断(在引脚 2 上)和 UART 模块中断的向量,用于指示通过串行线接收到字符。这两个中断都是调试驱动程序工作所需的,但也由 Arduino 软件库使用。

要解决此问题

在 Eclipse 的项目资源管理器中展开项目中的 Arduino 文件夹,并找到 HardwareSerial0.cpp 文件。

右键单击此文件,然后从上下文菜单中选择“资源配置”>“从构建中排除…”

在打开的窗口中,选择“调试”和“发布”配置,然后单击“确定”。

现在,HardwareSerial0.cpp 文件将不会与您的程序一起构建。这将解决向量 18(UART)的多重定义问题,但这也意味着 Arduino 串行功能将无法工作。请注意,这仅适用于此程序(项目),而不适用于您在 Eclipse 或 Arduino IDE 中创建的其他程序。您没有修改 Arduino 安装中的任何内容。

对 WInterrupts.c 文件重复相同的过程。也就是说,也将此文件排除在构建之外。

这解决了向量 1 的多重定义问题,但通过将 WInterrupts.c 从构建中排除,您的程序根本无法使用 `attachInterrupt` Arduino 函数。如果您需要在程序中使用 `attachInterrupt`,您可以在此文件中仅排除向量 1 的定义。最简单的方法是用本文在 avr_debug/arduino 中提供的 WInterrupts.c 文件替换原始文件——有关详细信息,请参阅 readme.txt 文件。它不会影响您的其他 Arduino 程序。或者,您可以按照 avr_debug/doc/avr_debug.pdf 中的描述自行修改文件。

构建项目。现在应该没有错误了。

将程序上传到您的 Arduino 板。
 

步骤 3 - 在 Eclipse 中配置调试器

本节假定您已在 Arduino 板中加载了带有调试驱动程序(stub)的程序。换句话说,您已完成本教程的前一部分。

在 Eclipse 的项目资源管理器中右键单击您的项目。从上下文菜单中选择调试方式>调试配置...

在“调试配置”窗口中,选择“GDB 硬件调试”项,然后单击窗口左上角的“新建启动配置”按钮。

这将在 GDB 硬件调试项下创建新的启动配置。
注意:如果您在列表中看不到 GDB 硬件调试,则可能尚未安装此类型的配置。请参阅上面的“设置 Eclipse IDE”部分。

选择 GDB 硬件调试下的新配置以配置其属性。配置名称基于您的项目名称。在下图中为 test1 Debug。


 

在右侧选择启动选项卡。

取消选中(清除)所有框(重置和延迟、暂停、加载映像和加载符号)。

 

切换到调试器选项卡。

在“GDB 命令”字段中输入(或浏览到)GDB 可执行文件 avr-gdb.exe 的路径,然后是您的“可执行文件”(.elf) 文件的路径。

GDB 的路径是 [arduino location]\hardware\tools\avr\bin\avr-gdb.exe。

您的文件路径可以使用 eclipse 变量。
这是我在此字段中的示例

c:\Programs\arduino-1.6.5-r2\hardware\tools\avr\bin\avr-gdb.exe "${project_loc}/Debug/${project_name}.elf"


提示:使用浏览按钮选择 avr-gdb.exe。然后输入空格并粘贴以下行
"${project_loc}/Debug/${project_name}.elf"。


选中“使用远程目标”框。

现在有两种连接到被调试程序的选项。

  • 直接串口连接
  • 通过 TCP-to-serial 端口转换器(代理服务器)连接

直接串行连接更易于使用,因此我建议首先尝试此方法。我能够在 Windows 10 上同时用于 Arduino Uno 和 Mega;在 Windows 7 上仅用于 Mega。如果不起作用,请使用通过 TCP-to-Serial 代理的连接。

请注意,在 Linux 上,您始终可以使用直接串行连接;只需输入设备名称而不是 COMx,例如 /dev/ttyACM0。

通过直接串行连接进行调试...

在“JTAG 设备”中选择“通用串行”。

在“GDB 连接字符串”中输入您的 Arduino 板连接的 COM 端口号,例如 COM5。

您现在已准备好进行调试。单击“调试配置”窗口底部的调试按钮,然后继续阅读“调试会话”一章(跳过下面的“TCP 到串行”部分)。

通过 TCP-to-Serial 代理进行调试...

使用 TCP 到串行代理不如直接串行连接方便,因此仅当上述直接串行连接不起作用时才使用此选项。

在“JTAG 设备”中选择“通用 TCP/IP”并输入
主机名或 IP 地址:localhost
端口号:11000。请注意,默认端口号不同,请将其更改为 11000

单击“应用”按钮保存更改,但暂时不要关闭“调试配置”窗口。

现在使用您的文件管理器(例如 Windows 资源管理器)打开本文提供的源代码包所在的文件夹。例如,c:\avr_debug。
您应该在此文件夹中看到一个 start_proxy.bat 文件。

在记事本或其他文本编辑器中打开 start_proxy.bat 文件(右键单击 + 编辑或打开方式...)。

更改此文件中的 COM 端口号。有这样一行

hub4com-2.1.0.0-386\com2tcp --baud 115200 \\.\COM15 11000

只需将 COM 后面的数字从 15 更改为您的计算机上连接 Arduino 板的 COM 端口号即可。

保存并关闭 start_proxy.bat 文件。

运行 start_proxy.bat 文件(双击它)。

这将启动一个转换器,在 GDB 调试器(我们上面配置的)使用的 TCP/IP 端口 11000 和连接 Arduino 的串行端口之间进行转换。您应该会看到一个带有某些信息的控制台窗口。此窗口在调试期间将始终打开。

Windows 防火墙可能会提示您解除阻止该端口,请允许此操作。


现在返回 Eclipse。我们仍然打开着调试配置窗口。

单击此窗口右下角的调试按钮。

 

调试会话

当您单击“调试”按钮时,Eclipse 应该会询问您是否要切换到调试视图(透视图)。回答“是”。

您应该会看到程序在调试器中停止,如下图所示。

您现在可以单步执行代码(工具栏中的“步过”按钮),查看 LED 亮起等。

请注意,从循环结束处单步跳出后,您将发现自己身处 Arduino 库的 main.cpp 文件中。如果您继续单步执行,您将再次进入您的循环。此外,看起来 setup 函数再次被调用,但这只是您在 C 语言中看到的代码与编译器生成的实际代码之间的差异;setup 并没有真正再次执行。

您可以使用“恢复”按钮让程序运行,直到它遇到我们在循环开头“硬编码”的断点。

当然,您也可以通过在左边距右键单击并从上下文菜单中选择“切换断点”来设置断点。

如果您想让程序全速运行,您需要编辑代码以删除对 breakpoint() 函数的调用。为此,您需要首先终止调试会话。然后重新构建并重新上传程序到板上,然后再次连接调试器。更改程序的步骤如下:

  • 使用红色方块“终止”按钮终止调试会话。
  • 关闭带有 tcp2com 代理的命令提示符窗口(需要释放 COM 端口)。
  • 更改您的程序,构建它并上传到 Arduino 板。
  • 再次启动 start_proxy.bat 脚本。
  • 在 Eclipse 中开始调试(展开工具栏中的“调试”按钮并单击您的调试配置名称)。
  • 如果启动时收到错误,可能是因为 Eclipse 项目资源管理器中未选择项目。只需单击左侧窗口中的项目,然后重试。或者右键单击项目,选择“调试方式”>“调试配置”,然后从那里启动调试会话。


如果您没有在程序中放置对 breakpoint() 函数的调用,它将在上传后立即运行(LED 闪烁)。当您连接调试器时,它将停止在随机行;最有可能在 delay() 代码的某个位置。您可能会看到类似以下内容:


在上面的窗口(调试)中,有所谓的调用堆栈——导致程序到达当前位置的调用“链”。程序停止在 micros() 函数内部,该函数是从 delay() 函数调用的,而 delay() 函数本身又从 loop() 函数调用,依此类推。

要快速进入您自己的代码,请在“调试”窗口中单击 loop()(选择 loop 函数)。这将在下部窗口中显示 loop 函数的代码。现在您可以设置一个断点,例如,在 digitalWrite(13, HIGH) 行,然后恢复程序。它将停止在断点处。

闪存断点和通过调试器加载

截至 2018 年 1 月,调试器新增了两个功能——将断点写入闪存和将程序上传到 MCU 的选项。

闪存断点

此调试驱动程序有两种实现断点的方法

  • 选项 1 - 将程序应停止的地址存储在一个变量中。然后,在执行程序的每条指令后,将此变量与程序的当前位置(程序计数器寄存器,PC)进行比较。如果找到匹配项,则停止并通知调试器 - 让用户知道程序在断点处停止。
  • 选项 2 – 将程序应停止的地址处的指令替换为一条特殊指令,该指令导致“跳转”到调试驱动程序,以便它可以通知调试器。

在整个文档中,我将选项 1 称为 RAM 断点,选项 2 称为闪存断点。在此调试器的早期版本中,仅提供 RAM 选项。在当前版本中,两个选项都可用。默认情况下使用 RAM 断点。这可以在 avr8-stub.h 文件中更改,请参见 `AVR8_BREAKPOINT_MODE` 常量。

RAM 断点的问题在于,程序在执行每条指令后都必须停止,以将 PC 与所需断点的地址进行比较。这会大大降低被调试程序的运行速度。但实际上,除非您调试带有通过递增/递减计数器实现的较长延迟的代码,否则几乎不会注意到这一点。

闪存断点不会降低程序速度。程序会在断点处自行停止。缺点是,要使用闪存断点,您需要替换 Arduino 中的引导加载程序,因为调试器需要与引导加载程序通信才能修改闪存。这将在下面描述。

无论如何,在任何情况下,我都建议首先使用 RAM 断点。只有当您能够使用 RAM 断点调试程序并发现使用闪存断点可以受益时,才采取额外的步骤来启用它们。

通过调试器加载程序

调试器现在可以接收新版本的程序并将其存储到 MCU 的内存中。此功能允许您一键开始调试。通常,您的程序使用名为 AVRDude 的独立工具上传到 Arduino,典型的程序调试工作流程如下:

  • 编辑代码
  • 构建代码
  • 上传代码(通过 AVRDude)
  • 点击调试按钮
  • 调试程序

如果您启用通过调试器上传的选项,工作流程可能如下:

  • 编辑代码
  • 点击调试按钮(您的代码在加载前会自动构建)。

问题是您需要替换 Arduino 中的引导加载程序。这也是使用闪存断点所必需的,因此如果您替换引导加载程序,您将获得两个功能——您可以使用闪存断点,还可以通过调试器加载程序。
要启用通过调试器加载,请在 avr8-stub.h 中设置常量 `AVR8_LOAD_SUPPORT`。

更换引导加载程序

更换引导加载程序可能看起来很复杂,但实际上它非常简单并且在互联网上有很多文档。但您确实需要一个 ICSP 编程器或另一个 Arduino 板来完成此操作。调试器文档 avr_debug.pdf 中有完整的说明。

闪存断点和加载程序所需的引导加载程序包含在此调试驱动程序的包中——请参阅 avr_debug/bootloader/optiboot/debug/optiboot.hex。它只是标准 Arduino 引导加载程序——optiboot 的修改版本。

请注意,此引导加载程序仅适用于 ATmega328(Arduino Uno 和其他使用此 MCU 的变体)。在 Arduino Mega 和其他非基于 ATmega328 的变体上,无法使用闪存断点并通过调试器加载。不过我正在努力解决这个问题…

对于高级用户,这里是修改后的引导加载程序所需的熔丝设置。引导加载程序的大小需要更改为 1024 字(将 BOOTSZ 设置为 1024 字 - 引导加载程序起始地址 0x3c00)。有关详细说明,请参阅 avr_debug.pdf。

结论

希望本文能让您对这款 Arduino 调试器的功能和使用有一个大致的了解。如果您是 Eclipse 和微控制器编程的新手,设置环境可能会显得很困难。您可以在下载包中提供的文档中找到详细的分步教程,请参阅 doc 子文件夹中的 avr_debug.pdf。

最好循序渐进,先从用纯 C 语言编写的简单程序开始,成功后再尝试包含 Arduino 软件库的程序。PDF 文档中的教程就是这样组织的。

最新版本的代码和文档可在 GitHub 存储库中找到:https://github.com/jdolinay/avr_debug。

调试器的局限性

我认为当前版本的调试器可以在不修改 Arduino 板和购买硬件调试器的情况下提供与硬件调试器类似的功能和用户体验。但是,您应该注意一些限制。

Arduino Serial 类不能在您的程序中与调试器一起使用。调试器使用串行线与 Eclipse IDE 通信。如果您以“Arduino 方式”编写程序,即向串行线打印调试消息,这可能看起来是一个大问题。但是当您使用调试器调试时,您通常不需要打印此类消息。如果您确实需要,有一个 `debug_message` 函数可以用于将消息发送到 Eclipse 中的调试控制台。如果您需要从程序发送数据(用于正常操作,而不是用于调试),那么您必须首先在没有串行输出的情况下调试程序,然后启用串行输出并禁用调试器。或者您可以在 Arduino Uno 上使用 SoftwareSerial,在 Arduino Mega 上也可以使用其他硬件串行接口 Serial1、2 等。

一个具有外部中断功能(INT0、INT1 等)的引脚必须保留给调试器。对于 Arduino Uno,这可以是数字引脚 2 或 3(MCU 的 PD2 或 PD3 引脚)。对于 Arduino Mega,有更多选项。默认情况下,使用 INT0 引脚(Uno 引脚 2,Mega 引脚 21),但您可以通过 avr8-stub.h 文件中的 `AVR8_SWINT_SOURCE` 常量来更改此设置。

如上所述,在默认的 RAM 断点配置下,当程序中设置了断点时,程序的执行速度会大大降低。这是因为断点是利用 Atmel AVR 架构的一个稍微奇怪的特性实现的——在中断服务例程 (ISR) 返回后,总是执行一条指令,然后才能再次进入相同或其他 ISR。由于这个特性,可以单步执行程序并将当前程序计数器与所需的断点地址进行比较。但是,每次指令后触发中断会大大降低程序速度。在许多情况下,这种缓慢不是问题,但如果您发现它限制了您,您可以切换到闪存断点。

使用闪存断点时无法使用看门狗。Arduino 库不使用看门狗,所以这通常不是问题。如果您需要在应用程序中使用看门狗,请仅在应用程序调试完成后启用与看门狗相关的代码,或使用 RAM 断点配置。

致谢

我应该提到,如果没有一些处理 Atmel AVR 的 GDB stub 的旧项目,开发这个调试器将花费更长时间。有关更多信息和链接,请参阅 avr8-stub.h 文件的头部。


本文的代码也可以在 github.com 上找到:https://github.com/jdolinay/avr_debug。

历史

2015 年 10 月 9 日 - 第一个版本。

2017 年 1 月 30 日 - 更新版本,支持 Arduino Mega。

2018 年 1 月 18 日 – 更新版本,包含闪存断点和调试器中的加载支持。

2018 年 7 月 12 日 - 更新了带有错误修复的 zip 存档。

2019 年 6 月 27 日 - 添加了关于在 Visual Studio Code 和 Arduino 库中使用的说明。

© . All rights reserved.