基于GDB的Arduino远程调试器的研究与实现
2020-07-11平震宇李培峰
平震宇,李培峰,孟 帆
(1.江苏信息职业技术学院物联网工程学院,江苏无锡214101;2.苏州大学计算机科学与技术学院,江苏苏州215006)
Arduino是开放源码的软硬件平台,其硬件原理图、核心库文件都是开源的,它是基于单片机系统开发的,具有集成开发环境(Integrated Development Environment,IDE)。Arduino是硬件开发的趋势,其简单的开发方式使开发者可以更快速地完成自己的创意。Arduino逐渐发展成为一个廉价的易于使用的实验平台,被广泛应用于各领域。由于Arduino 入门要求低,不乏文科背景的爱好者、艺术家和设计师。Arduino也可以用于设计语音控制小车设计[1],书画机械手臂[2],迈克尔逊干涉仪测量的改进[3],制作体感机械手[4],汽车并线辅助系统[5],移动机器人控制系统[6]这样复杂的应用系统。近年来Arduino 被应用到物联网工程专业实践教学中,利用Arduino 平台的优势,创新物联网工程专业实践教学,并在物联网专业教学的软硬件整合方面进行深入的探索和改进[7]。通过简便、灵活的开发平台,进行物联网感知、识别和控制中外围功能模块的讲解,让学生在有限的基础知识上提高学习效率、快速进行实验开发。
1 背景与相关工作
Arduino支持使用各种电子元件、传感器、通信组件、控制输入输出设备,Arduino开发板种类很多,常用的有Arduino UNO、Due、Yun、Mega、Zero、Robot、Leonardo、Nano等,Arduino UNO是USB接口系列的常用版本。Arduino简化了硬件接口操作,它为软件开发者设计的硬件开发平台。但是调试Arduino 程序只能通过串口回传一些信息或者通过开发板上的LED 灯显示程序运行情况。这两种方法只适用于几十行代码的小程序,对于复杂的程序或初学用户就不能满足要求了。物联网专业课程通过若干Arduino 项目提高学生的动手、编程和创新能力,课程的实践教学也需要Arduino具有调试功能。
Arduino项目开发过程中需要确定程序错误的位置以及错误发生的内在原因,Arduino IDE 却没有提供调试功能。Arduino UNO开发板使用的ATmega328 芯片拥有一个片上的调试模块(on-chip debug),可以实现对微控制单元(Micro Controller Unit,MCU)的运行过程进行单步调试、设置硬件断点等调试操作,可查看或修改数据存储区,为调试者提供了访问MCU运行时状态以及控制MCU 运行过程的方法和途径。Visual Micro开发了集成于Microsoft Visual Studio 和Atmel Studio IDE 的插件(http://www. visualmicro. com)[8],但是Visual Micro的调试功能是收费的。
因此本文设计与实现Eclipse 集成开发环境下基于GDB 的Arduino 远程调试器。Arduino 使用的是Atmel AVR系列微控制单元,编译器是使用了GNU开发工具系统的avr-gcc,调试工具是avr-gdb。GDB是一个功能强大的调试器,支持多种硬件平台与多种程序语言,可以用于本地调试,也可以用于远程调试[8]。由于Arduino开发板资源受限等原因,不可能直接在Arduino开发板上运行调试器,通常的做法是采用宿主机(host)加目标机(target)的远程调试方式。在宿主机上运行GDB调试器,目标机上运行被调试程序及调试插桩(GDB stub),目标机上的GDB stub和宿主机上的GDB通过串口,遵循GDB的远程串行协议(Remote Serial Protocol,RSP),由宿主机上的GDB 获取对应用程序的控制权,对应用程序进行源码级调试。GDB stub是一段代码,需要把GDB stub和被调试程序编译链接成一个可执行文件在目标机上运行。
Arduino远程调试器系统模型如图1 所示,这个模型无须额外的硬件支持。GDB 是一个强大且免费的命令行调试工具,GDB远程调试功能已成功运用于多种嵌入式平台[9-10],但并没有在Arduino 开发板上实现。Roman Pen实现了Atmel AVR系列微控制单元的调试服务[11](Embedded GDB server for AVR MCU)对本项目有参考价值。
图1 Arduino远程调试器系统模型
2 实现Arduino UNO调试器GDB stub
GDB 远程调试有两种方法GDB stub 与GDB Server。GDB Server 是个调试代理程序,宿主机上的GDB程序可以通过运行于目标机的GDBserver对嵌入式系统软件与应用程序进行调试。GDB Server是运行于目标机上的一个进程,需要通过Linux 系统提供的ptrace()系统调用实现对被调试进程的访问与控制[12]。GDB Server是非侵入式的调试代理,适用于有操作系统支持的嵌入式系统[13]。GDB stub 则是侵入式的调试代理,需要通过链接器把GDB stub和被调试程序链接成一个可执行文件,对于没有操作系统支持的Arduino UNO 平台,必须使用GDB stub 方式实现GDB远程调试。
GDB stub可以通过TCP/IP 协议、并行接口或者串口与宿主机GDB 进行通信[14-15]。Arduino UNO 开发板上的USB 采用ATMega8U2 芯片模拟串口,用于Arduino供电、下载程序。本文通过这个虚拟串口与宿主机建立物理连接,由RSP协议分析模块来处理宿主机发生过来的调试命令。
2.1 RSP协议分析模块
GDB定义了RSP协议用于与目标机GDB stub 之间的通信,GDB 是请求方,GDB stub 是调试应答方。例如GDB接收到用户继续执行命令(continue),GDB就遵循RSP协议规定向GDB stub发送(c#63),GDB stub接收到这条命令后继续调试程序运行并向GDB返回(+)。RSP协议使用ASCII 字符来描述消息,消息由“”符号开始,以“#”符号结束,另外带8 bit 校验。如果接收消息校验正确,并准备接收下一个消息则返回“+”符号,如果校验错误需要重新传输则返回-符号[16-17]。
RSP协议定义的命令非常多,只有需要实现其中部分命令即可。
?获取最后的信号
H 设置线程,T 查看线程是否存活。直接回复OK字符串。
g 读寄存器命令,返回所有目标寄存器数值,G 写寄存器命令,返回OK确认数据已经写入寄存器。
m 读取内存命令,返回要求读取的制定内存地址的数值,M 写内存命令,返回OK 确认数据已经写入内存。
c 继续执行命令,s 单步执行命令。调用gdb_update_breakpoints()函数处理断点。
Z 插入断点,z 删除断点。调用gdb_insert_remove_breakpoint(gdb_ctx-> buff)插入,删除断点。
2.2 断点机制处理模块
断点机制是GDB stub需要实现的核心功能,当进程执行到用户设置断点的时候需要让程序暂停,另外当用户需要单步调试程序时,GDB 也是通过设置断点来获取控制权。
GDB通过内存的读写来实现设置断点,它使用一个trap指令来替换原有指令,当被调试的程序运行到断点的时候产生单步调试中断信号(SIGTRAP)。该信号被GDB捕获并进行断点命中判定,当GDB 判断出这次SIGTRAP是断点命中之后就会转入等待用户输入进行下一步处理,否则继续。ATmega328p 拥有32KB的Flash程序存储器空间,用于存放程序指令代码[18]。使用trap指令来替换原有指令就需要频繁地擦写Flash 程序存储器,ATmega328p 用户手册表示Flash程序存储器至少可以擦写10,000 次。Flash 程序存储器空间分为两个区:引导程序区(BootLoader)和应用程序区,两个区有专门的锁定位以实现读和读/写保护[18]。用于写应用程序区的SPM指令必须位于引导程序区。GDB stub必须实现擦写Flash 程序存储器的功能dboot_safe_pgm_write(),并且将代码存放在引导程序区,也就是需要重新下载BootLoader程序。
另外一种实现断点的方法是把所有的断点位置都存放在一个链表中(gdb_ctx-> breaks[ind_bks]),命中判定即把被调试程序当前的位置(gdb_ctx->pc)和链表中的断点位置进行比较。如果当前位置在断点链表中则发送GDB_SIGTRAP信号,并调用函数handle_exception(),控制权就交给GDB。为了让GDB stub比较当前运行指令,就需要在执行用户程序每个指令后产生一个中断信号,在中断处理程序中让GDB stub获得程序的控制权。ATmega328p 在退出中断后总是回到主程序并至少执行一条指令后才可以去执行其他被挂起的中断。进入中断服务程序时状态寄存器不会被保存,中断返回时也不会自动恢复,需要用户通过软件来完成。
通过gdb_enable_swinterrupt()函数设置外部中断INT0 来产生中断请求,由中断处理程序ISR(INT0_vect,ISR_BLOCK ISR_NAKED)来处理这个中断请求。
GDB单步执行有step 与next 两种方法。next 命令在处理过程是在该行语句处设置断点,然后运行continue命令,让程序继续执行,当执行到下一个断点停下。step命令需要跟踪到函数的内部,若执行语句不包括函数则与next 命令一样处理。如果包含函数调用,在当单步运行遇到call指令时,找到被调用函数的第一条指令并设置断点。由gdb_update_breakpoints()函数完成应用程序Flash的断点写入与删除操作。
GDB stub使用链表gdb_ctx->breaks[]来管理用户设置的所有断点。当程序在断点处停下或者单步执行一步后,GDB会删除所有的断点,在运行continue命令之前需要设置好下一个断点,由gdb_insert_breakpoint()与gdb_remove_breakpoint()函数完成链表gdb_ctx->breaks[]的插入和删除操作。
GDB stub完整的代码已共享在arduino 中文社区的综合讨论区。GDB stub 分别在windows7、windows10、ubuntu 16.04 系统中测试成功,Eclipse IDE for C/C++ Developers 版本是4. 7. 3a,AVR Eclipse plugin版本是2.5.0,Arduino IDE版本是1.8.5。GDB stub也可以在其他支持GDB调试的IDE环境中使用。由于Arduino IDE没有实现调试配置与用户交互界面,所有不能与Arduino IDE配合使用。
3 使用Eclipse开发Arduino程序
Arduino IDE功能简捷易用,非常适合不熟悉编程环境的用户,但是对于较复杂的Arduino 项目开发,Arduino IDE功能就显得过于简单。本文使用Eclipse的C/C++开发环境(C/C++ Development Tooling,CDT),CDT Debug MI提供了Eclipse与GDB的通信机制。宿主机调试器使用Arduino 工具链提供的avrgdb,版本信息为GNU gdb7. 8 (AVR_8bit_GNU_Toolchain_3.5.4)。
Eclipse还有一个支持Arduino 开发的插件(AVR Eclipse plugin,AVR),Eclipse 通过CDT 插件与AVR插件构建一个Arduino集成开发环境。AVR插件集成了对GNU toolchain的支持,通过设置工具链路径就可以直接在Eclipse 中进行Arduino 项目代码的编辑、编译。
3.1 下载软件
需要下载的相关软件见表1。其中Arduino IDE是Arduino的集成开发环境,将会使用它的烧写程序AVRDude以及Arduino 的核心库文件。Eclipse IDE for C/C++是Eclipse的C/C++集成开发环境,在官方网站下载。AVR Eclipse plugin 是Eclipse 的AVR 插件,通过Eclipse 的菜单项Help->Install New Software直接安装。Atmel AVR Toolchain 是Atmel 的工具链,将会使用其工具链中的avr-gdb。Mingw tools 是一套完整的开源编译工具集,将使用它的项目管理工具Make。
表1 相关软件下载地址
3.2 配置Eclipse的AVR插件
Eclipse的AVR 插件需要配置AVRDude 下载程序设置以及配置开发工具链。
(1)AVRDude 下载程序设置。选择Project->Properties菜单项,弹出工程设置对话框如图2 所示,在左侧窗口点击“AVR”前面的“+”号,展开子项,点击选中“AVRDude”子项,右侧窗口将出现对应的设置。
图2 AVRDude下载程序设置
AVRDude的用户配置参数选择Arduino IDE安装目录下的avrdude. conf 文件(C:\ arduino-1. 8. 5 \hardware\tools\avr\etc\avrdude.conf)。
在程序配置(Program Configuration)选择项,选择“Add..”新建一个下载配置。在这配置界面里硬件类型(Programmer hardware)选择Arduino,下载默认串口(Override default port)中输入串口号,串口号与Arduino IDE 中配置保持一致,字符串格式为(/ /. /COM3),其余参数保持不变。
(2)配置开发工具链。Eclipse 的AVR 插件需要配置开发工具链,包括编译器、项目管理工具、头文件目录、下载程序,见表2。其中AVR-GCC 可以选择Arduino IDE的工具链,由于Arduino IDE的AVR-GDB无法使用,所以就统一使用Atmel的工具链。
表2 开发工具链配置
3.3 配置Arduino的库文件
Arduino 库有标准库与扩展库,标准库是指Arduino IDE 自带的一些常用功能,它预装在Arduino安装文件夹的Libraries 文件夹中。Arduino 作为一个开源平台,全世界的开发者都可以共享自己编写的扩展库,其他开发者可以免费引用这些库,也可以对它们进行扩充和完善。如果Arduino 项目开发时使用了Arduino的标准库,在链接阶段就需要有Arduino 的标准库的静态库文件,并在项目Properties菜单项中设置AVR C/C++ Linker-> Libraries。
另一种方法是将Arduino 的标准库通过创建链接资源(linked resources)的方式导入到工程中。首选创建一个新的Arduino UNO(AVR Cross Target Application)工程,选择CPU型号为ATmega328p,频率为16 MHz。选择File -> New -> Folder 菜单项打开新建文件夹对话框,单击"高级(Advanced)"按钮,选择"链接文件夹(Link to alternate location)",文件夹路径选择Arduino 标准库"C:\arduino-1.8.5\hardware\arduino\avr\cores\arduino"。
最后设置项目属性,右键单击项目,打开属性对话框,选择"C/C++ Bulid -> Settings",在"AVR C++Complier -> Directories"选项中添加上一步添加的Arduino标准库文件夹。现在编译器会搜索选项指定的目录中的头文件,并且将所需要的函数库一起编译到项目中。
4 调试器GDB stub测试与性能评估
调试器GDB stub 性能评估分为功能可靠性和调试性能两个方面。其中功能可靠性是对RSP 协议解析的准确性以及调试器GDB stub与Eclipse 配合与准确性验证,调试性能统计了GDB stub 代码的大小,内存使用情况,程序运行效率等数据。
功能可靠性评估以Arduino 集成开发环境中自带的示例程序为测试用例。将GDB stub 的源码文件加入到Blink项目中与其一起编译,需要在Blink 项目的setup()函数中调用初始化函数debug_init()。将程序编译下载到开发板后开始调试,Eclipse IDE 负责前端界面显示,avr-gdb 将与运行在开发板的GDB stub 通过串口链接完成调试工作。通过eclipse 来进行远程调试,需要在调试配置中设置远程目标(Remote Target)的串口号和波特率。
Blink程序Eclipse 调试界面如图3 所示,可如同调试本地程序方便的设置断点,在代码里需要调试的地方,鼠标双击代码行号的左边。Debug 窗口显示当前线程方法调用栈及方法执行到第几行,图3 所示Blink程序单步执行到digitalWrite函数,digitalWrite函数位于Arduino 标准库中的wiring_digital. c 文件153行。函数变量显示当前函数的局部变量,非静态变量等。可以给一个变量或表达式添加永久观察点,当程序在调试时观察点会在表达式视图(Expression view)中显示出来,也可以变量视图(Variables view)查看与修改制定变量值,根据变量类型在其对应的Value 列里输入值即可。断点(Breakpoints)视图中可用来新增和删除断点等,控制台(Console)用于查看打印的日志信息。在代码视图显示当前运行的代码,可以单步跟踪,进入当前函数内一步一步执行或者全速运行等。功能可靠性评估显示可以提供所有标准的调试功能,包括设置断点、单步执行、查看修改变量和寄存器等功能。
图3 Blink程序调试界面
调试性能采用DF 创客社区DFR0100 Arduino 入门套件的示例代码作为测试用例。统计了测试用例程序代码大小,内存使用情况以及加入调试代码以后的程序代码大小,内存使用情况,如表3 所示。结果显示调试代码大概占用4. 7KB Flash 程序存储器空间,ATmega328p具有32KB Flash 程序存储器空间,调试代码占用14%的程序存储器空间。调试代码使用了277Byte内存,ATmega328p具有2KB 内存。这结果对于大多数Arduino项目是可以接受的。
表3 代码大小以及内存使用情况
当测试用例设置断点后,程序的运行速度会显著的下降。如果测试用例程序中有延时函数(delay),延时函数是通过轮询硬件时间实现延时的,加入调试代码后可能导致延时被延长大约350 倍,100 ms的延时在调试阶段可能是延时了35 s。Arduino 项目的程序不是很复杂,对实时性要求也不高,在调试阶段这种情况完全可以满足用户使用需求。可以通过在合适的地方设置断点或者修改延时函数来减少调试代码对程序的影响。调试结束后把调试代码从项目中移除,不要再编译在项目中。
5 结 语
本文介绍了Arduino 的远程调试器的研究与实现,调试器是基于开源调试器GDB 的,并在Eclipse IDE下搭建了源码级调试环境,为Arduino开发平台构建了完整的开发环境。解决方案是实现在目标机运行的远程调试器GDB stub,通过串口接收宿主机调试器发来的调试命令,根据调试命令来控制程序的运行过程,并返回调试结果。GDB stub 需要与被调试的程序一起编译后运行于目标机,宿主机上运行GDB调试器通过串口与目标机的GDB stub连接,通过简单的设置即可对运行在目标机上的程序进行调试。它提供了调试器所期望的设置和删除断点,逐步执行代码和检查变量等常用功能。大幅提高了Arduino 项目的调试效率,有效地为Arduino 开发程序,缩短了开发周期。在物联网专业教学可以作为一个实验平台,使用Arduino教授嵌入式编程、传感器应用等课程。