Arduino虚拟机的设计与实现
贡萨洛·扎巴拉、里卡多·莫兰、马蒂亚斯·特拉尼和塞巴斯蒂安·布兰科
摘要
Arduino 已成为构建电子项目的最受欢迎的平台之一,尤其是在初学者中。近年来,为了支持 Arduino,开发了无数的工具、环境和编程语言。其中之一是由作者开发的用于机器人的可视化编程平台 Physical Etoys。Physical Etoys 支持将程序编译到 Arduino 中。为了实现这一点,已经构建了一个 Smalltalk 到 C语言 的翻译器。尽管该翻译器非常有用,但它也带来了一系列新问题。本文将讨论其中一些问题,以及我们如何决定通过开发一个简单的虚拟机来解决这些问题,该虚拟机将作为新一代 Physical Etoys 的基础。
关键词 :Arduino ⋅ 编程语言 ⋅ 虚拟机器 ⋅ 并发 ⋅ Physical Etoys
引言
自从Arduino板问世以来,越来越多没有接受过专业技术培训的人(如艺术家、设计师、业余爱好者)开始探索微控制器编程的世界。教育领域也未能免受这一趋势的影响。随着推广该理念的运动不断发展,在学校的编程教学和计算机科学教育中,机器人技术提供了一种极具吸引力的媒介,可用于引入多个学科的概念。此外,开展中小型机器人项目所需的知识正变得越来越少。Arduino板作为开源硬件且成本低廉,吸引了全球各地的学生投身于教育机器人领域的探索。
Arduino平台提供了一个简化的环境(基于C语言编程语言),其中大多数高级微控制器概念对用户是隐藏的。然而,对于一些最缺乏经验的用户,尤其是年幼的儿童来说,该环境仍然过于复杂。
我们在向高中生教授机器人技术时发现的一个主要问题是,Arduino编程语言对并发的支持有限。机器人竞赛和练习通常需要协调多个并发任务才能实现目标。例如,一种常见的竞赛是机器人相扑,其中两个机器人试图将对方推出圆圈。这项竞赛涉及执行两项同时进行的任务:寻找对手以及避免自己被推出圆圈。其他竞赛则涉及更复杂的任务。大多数参加这类活动的学生在描述并发行为方面存在困难,因此他们的机器人表现不佳。这样一来,本应有趣且富有吸引力的学习体验反而变得令人沮丧。
由于这些原因,加之Arduino是开源的,人们已多次尝试为其提供更适合初学者的编程环境。其中一种尝试是Physical Etoys(PE),它是 Etoys [1]的扩展,旨在为儿童提供易于编程不同硬件平台(包括 Arduino)的工具。Etoys是一个多媒体丰富的创作环境和可视化编程系统,允许各个年龄段的儿童使用基于图块的脚本系统创建模拟程序和小型电子游戏。在PE出现之前,Etoys仅允许操作虚拟对象,例如屏幕上的绘图。通过使用PE,儿童可以像轻松地与Etoys提供的虚拟世界互动一样,通过任何支持的机器人套件与真实世界进行交互。
Etoys(以及由此衍生的 PE)提供了一种非常简单的并发模型,其中所有用户脚本在无限循环中以可配置的时间间隔运行。尽管执行完全是单线程的,但从用户角度来看,所有脚本是同时运行的。对于大多数学生来说,这种简单的模型通常已经足够。
然而,PE要求Arduino板必须持续连接到计算机,而这在某些机器人竞赛中是不允许的。为了解决这个问题,PE现在还可以将其脚本编译,并通过一种特殊的编程模式上传到Arduino。此功能涉及大量复杂性,初学者通常难以处理。而且由于PE固件无法与编译后的程序共存,一旦脚本被上传到 Arduino,PE交互式环境所提供的所有优势都将丧失。
此外,这种“编译”模式存在严重的技术问题。为了使编译能够进行,PE 首先将脚本(自动生成的 Smalltalk代码)转换为 C++,然后使用 avr-gcc 编译 C++ 源代码转换为机器代码,最后使用avrdude将机器代码上传到Arduino板。此过程非常复杂且缓慢。它迫使PE发行版必须包含AVR工具,使其大小增加至原来的5倍(从47 MB增至最新版本的231 MB)。此外,AVR工具是平台相关的,这使得为PE提供单一的跨平台发行版变得困难。
尽管能够根据所进行的项目类型选择编程模式使PE区别于其他类似项目(例如适用于Arduino的Scratch [2]或Minibloq [3]),但上述问题需要采用不同的方法。
需要满足以下要求:
1. 脚本执行必须直接在Arduino板上进行,无需与计算机交互。
2. 如果 Arduino恰好连接到计算机,则必须保留PE提供的所有交互功能。
3. 用户不应被要求指定使用哪种编程模式(编译型或交互型)。
基于这些目标,决定实现一个能够解释非常简单的编程语言字节码的虚拟机。该虚拟机(称为Uzi)将只需上传到Arduino板一次。然后,计算机可以通过USB端口与虚拟机通信,向Arduino发送单条指令或完整程序以运行。
本文将讨论Uzi虚拟机的设计与实现,并将其与其他类似技术进行比较,以突出该解决方案的优势与局限性。
2 相关工作
为小型微控制器开发虚拟机和高级语言并非新事物。人们已进行了大量尝试,旨在为Arduino提供不同的编程环境。其中大多数方案基于已有的通用编程语言,例如Java、Scheme或Python。
HaikuVM 就是此类尝试之一[4]。它是一个基于 leJOS [5]并在 Arduino 上运行的 Java 虚拟机。其编译器会分析 Java 源代码,以生成一个 C 程序,该程序包含用户程序(作为一组 C 结构体存储在闪存中)以及用于解释该程序的虚拟机。然后,用户必须使用 Arduino 工具集将程序上传到开发板上。这种实现方式具有诸多优势,例如通过将用户程序与虚拟机一同存储在闪存中从而降低内存使用量,但其需要使用 Arduino 工具来编译和上传程序。由于它输出的是 C 代码,编译器可以轻松引入特殊结构,使用户能够内联 C 代码,从而根据具体问题选择所需的抽象级别。HaikuVM 支持几乎所有的 Java 语义,包括垃圾回收、线程和异常,但它缺乏支持反射、对象终结、弱引用以及数组的类型信息。为了高效利用 Arduino中的可用内存,编译器执行静态程序分析,从而能够丢弃未使用的类,生成更紧凑的程序。在性能方面,一些基准测试显示其执行速度为“在 8 MHz AVR∼ATmega8上每秒约执行55k个Java操作码”[4]。
Ocamm-pi 是 Ocamm 编程语言的一个变体 [6],支持包括 Arduino [7] 在内的多个平台。Ocamm 特别设计用于编写并发程序,而使用 Arduino 语言难以表达这类程序。它需要至少具有 32 KB 代码空间和 2 KB RAM 的开发板,因此最小支持的 Arduino 开发板是采用 ATmega328 芯片的型号。与 HaikuVM 类似,occam-pi 的字节码与虚拟机一起存储在闪存中。然而,与 HaikuVM 不同的是,字节码可以单独上传。
ocamm-pi 与 HaikuVM 的另一个相似之处在于其静态程序分析,该分析可消除死代码并生成紧凑的程序。此过程不仅作用于用户生成的代码,也作用于 occam-pi 库。Ocamm-pi 拥有一套丰富的运行时库,提供了用于与 Arduino 功能(如串行端口、PWM 和 TWI)交互的函数。这些库中的大多数完全用 occam-pi 实现。这得益于从 occam-pi 代码可以直接访问中断和内存,从而能够在 occam-pi 中直接开发底层库。然而,使用 occam-pi 代码处理中断会带来性能开销,限制了可处理的信息量。例如,在 occam-pi 中处理串行通信时,最多只能以 300 bps 的波特率处理字符。
在性能方面,字节码的执行速度据报道比原生代码慢 100 到 1000 倍。
Splish [8] 是一个有趣的项目,因为它不仅提供了一个虚拟机,还提供了一个可视化编程环境,类似于 PE。所有指令和编程结构都以图标形式表示,这些图标可以相互连接以定义程序流程。使用该可视化环境构建的程序随后会被编译成专为此语言设计的栈式虚拟机的目标代码。通过 USB 将编译后的程序上传到 Arduino 板。Splish 固件包含一个监控程序,负责板子与计算机之间的通信;它监听串行端口中的执行命令,并周期性地回传状态信息。这使得计算机能够监控 Arduino 引脚的状态以及程序的执行情况。
该系统在设计时考虑了调试功能,即使这会对性能产生负面影响。如果 Arduino 板连接到 PC,就可以在“调试模式”下运行程序,实现逐步执行。
PyMite [9](也称为 python-on-a-chip)是一种用于 8 位及更高级微控制器的 Python 解释器。它能够执行 Python 字节码的一个子集,并支持几乎所有 Python 最重要的数据类型(如 32位有符号整数、字符串、元组、列表和字典)以及一些高级特性,例如生成器、类和装饰器。它允许通过使用特殊关键字标记一个 Python 函数,并在该函数的文档字符串中编写 C 代码来开发原生代码,从而便于开发底层库。它支持多个平台,但由于至少需要 64 KB程序内存和 4 KB内存,因此不支持小于 MEGA 的 Arduino开发板。
Scheme编程语言有多种针对基于小型微控制器的嵌入式系统设计的实现。其中两个非常有趣的实现是Microscheme[10]和PICOBIT [11],尽管它们都是同一编程语言的实现,但在方法上却大相径庭。
Microscheme面向大多数Arduino开发板所使用的8位ATmega芯片,而 PICOBIT则面向Microchip PIC18系列微控制器。Microscheme与 PICOBIT的不同之处在于它采用直接编译而非虚拟机。它的编译器用C语言编写,生成AVR汇编代码,然后通过avr-gcc/avrdude工具链进行汇编并上传到开发板。相反,PICOBIT提供了一个用可移植C语言编写的Scheme虚拟机,尽管目前仅针对PIC18微控制器实现,但可以移植到任何具备C编译器的平台。PICOBIT方法另一个值得一提的特点是,它不仅提供了自定义的 Scheme编译器和虚拟机,还提供了一个专为开发虚拟机而设计的自定义C 编译器。该C编译器利用了虚拟机实现中常见的模式,并执行一系列优化,从而显著减少了生成的代码量。这两种实现都支持Scheme的不同子集。
3 设计原则
本项目的主要目标是提供一种工具,使PE等可视化编程环境能够使用该工具来编译和运行其程序。
鉴于PE具有教育目的,Uzi基于以下原则进行设计:
-
简洁性
:应易于理解虚拟机及其工作方式。
-
抽象
:Uzi语言应提供高级函数,隐藏初学者和高级微控制器概念(如定时器、中断、并发、引脚模式等)的部分细节。这些概念可随后根据用户需求以适当的速度逐步引入。
-
监控
:当开发板连接到计算机时,应能够监控开发板状态。
-
自主性
:程序必须能够在无需计算机连接开发板的情况下运行。
-
调试
:Uzi必须提供错误处理和代码逐步执行的机制。如果没有调试工具,缺乏经验的用户在修复错误时可能会感到沮丧。
4 实现
为了简化将PE脚本转换为Uzi程序的过程,Uzi虚拟机的执行模型被设计得尽可能与Etoys模型相似。与Etoys项目一样,一个Uzi程序可以包含多个并发执行的脚本。每个脚本在隐式循环中永久运行,并且其执行速率可以独立配置。这种模型简化了表达并发任务所需的代码,正如本文的示例部分所示。
部分Uzi工具位于计算机上,而另一些则直接在Arduino上运行。计算机具备通过串行端口解析、编译和传输程序到Arduino板所需的所有组件。这些程序均使用Squeak(一种开源的Smalltalk版本)开发。而Arduino则包含执行Uzi程序所需的所有软件,这些工具是用C语言编写的++(图 1)。
UziParser 负责解析用 Uzi 语法编写的字符串并生成解析树。它是使用 PetitParser 实现的,PetitParser 是一个允许你使用 Smalltalk 代码定义解析器的解析框架。尽管 Uzi 语法在很大程度上借鉴了 Smalltalk,但不应将其与 Smalltalk 代码混淆。Uzi 并不遵循任何 Smalltalk 语义,它不支持对象,也不支持动态绑定。Uzi 是一种领域特定语言,其主要目的是在 Arduino 板上运行 PE 脚本,并且它的设计旨在使翻译过程尽可能简单。
下面可以看到一个用Uzi语言编写的示例脚本。这个小程序用于闪烁13号引脚上的 LED:
#BLINK13 滴答 1 / s [切换: 13]
UziCompiler负责遍历解析树并生成包含字节码的已编译程序,这些字节码将由Uzi虚拟机执行。
UziEncoder 负责将程序序列化为专为 Uzi 设计的自定义二进制格式。该格式旨在尽可能紧凑。因为它不仅将用于向Arduino传输程序,还将用于将其存储在空间非常有限的 EEPROM中。
UziSimulator 是 Uzi 虚拟机的 Smalltalk 实现。该工具使我们能够在计算机上运行与 Arduino 将执行的完全相同的过程。目前,这有助于在将更改应用到实际上传到开发板的虚拟机之前,验证新功能的实现。未来, UziSimulator 还可能用于添加调试功能,例如逐步执行。
Uzi协议是PC端的最后一个工具,其他组件使用它与Arduino通信。它既可以发送整个程序,也可以发送Arduino将要执行的特定命令。同时,它还监听Arduino的状态更新。例如,UziSimulator上实现的所有IO原语都使用Uzi协议在开发板上实际执行操作。
在Arduino端,Uzi作为固件安装,该固件包含Uzi虚拟机以及一个通过串行端口与Uzi协议通信的小型监控程序。监控器充当虚拟机与开发工具之间的桥梁。它监听串行端口,等待执行命令,并定期向计算机发送数据。
监控器支持的命令包括IO操作、执行特定程序以及将程序存储到EEPROM内存中。监控器发送的数据包括引脚状态以及虚拟机的状态(全局变量、指令指针、栈和当前脚本)。
VM类负责执行Uzi程序。它主要需要两个属性:指令指针(IP),一个指向下一个将要执行的指令的整数;以及一个指向栈的指针。在每个滴答周期中,虚拟机会遍历所有脚本的列表。对于每个脚本,虚拟机都知道其上一次执行的时间及其滴答速率。如果距离上次执行的时间超过了其滴答速率,虚拟机就会执行该脚本。执行脚本包括重置指令指针,并逐个执行该脚本的每一条字节码。脚本的执行必须保证在执行结束后栈的状态与执行开始前完全一致。字节码的执行由一个简单的switch语句来处理。正如前面所提到的,Uzi编译器优先考虑代码大小而非执行速度,因此Uzi指令集被设计为尽可能节省空间。每条指令占用一个字节,其中最高四位用于表示其操作码,最低四位用于指定其操作数。由于4位最多只能表示16个不同的值,因此使用了一条特殊指令,通过下一个字节作为操作数来扩展特定操作。值0xFF用于标记脚本的结束。由于仅用4位来表示操作码,指令集只包含最常用的操作,例如栈操作、引脚访问、调用原语以及启动/停止脚本。其他操作(如算术或逻辑运算)则作为原语实现。
栈的大小固定为100个元素。如果发生栈溢出,虚拟机会立即停止执行。无效状态将被监控器存储并传输到主机PC(如果已连接)。
5 示例
以下示例虽然确实很简单,但有助于展示在Arduino提供的简化C语言环境(我们将称之为Arduino代码)、使用PE可视化界面构建的脚本以及使用Uzi编程语言编写的Uzi程序之间的差异。
此程序执行四个独立的任务:
1. 每秒闪烁一次LED(BLINK13)。
2. 每秒闪烁另一次LED两次(BLINK12)。
3. 当按下按钮时,点亮第三个LED(BUTTON)。
4. 使用电位器控制第四个LED的亮度(DIMMER)。
这些简单的任务是并发执行的,而在Arduino代码中很难表达这一点。
如下例所示,Arduino代码将执行任务的语句与在正确间隔调度任务所需的代码混合在一起,这使得代码的意图变得不够清晰,因此更难阅读和修改。
Arduino 的引脚概念模型代表了另一个问题。为了进行读取或写入操作,它区分了模拟和数字引脚,迫使用户为不同类型的引脚使用不同的函数。这种抽象甚至不准确:用于“写入”PWM 波形的函数被称为
analogWrite()
,尽管它并不会生成模拟波形,并且与模拟引脚或
analogRead()
函数没有任何关联 [13]。此外,每个引脚只能处于两种模式之一,用户必须显式指定:INPUT 模式用于读取,OUTPUT 模式用于写入。一种更简单的模型可以将对引脚的操作简化为仅“写”和“读”,并在内部处理各种具体情况,而无需向用户暴露细节。这种模型虽然存在缺点,但对于初学者来说比 Arduino 的函数更容易理解。此外,尽管
digitalWrite()
和
digitalRead()
函数的工作范围相同(仅为 0 或 1),
analogWrite()
接受 0 到 255 之间的值,而
analogRead()
返回 0 到 1023 之间的值。这一微小差异迫使用户在尝试将模拟引脚的输入用于输出 PWM 信号时,必须在两个量程之间进行转换,正如 Arduino 示例代码中所示。如果未能正确执行此转换,可能会导致错误行为,这对于缺乏经验的用户来说很难调试。
此外,由于无法读取配置为输出(OUTPUT)的引脚的值(至少在不直接访问寄存器的情况下),为了闪烁LED灯,用户不得不将引脚的状态存储在一个变量中。这段额外的代码增加了解决方案的复杂性。
处理每个LED的闪烁频率还需要额外的代码。在这里不允许使用
delay()
函数(该函数会阻塞处理器一段指定的时间),因为在Arduino示例中常见的这种做法会干扰其他任务的执行(Arduino板只有一个微控制器)。相反,用户被迫调用
millis()
函数,并在每次tick时检查是否到了每个LED闪烁的时间。
这些Arduino代码中的问题大大增加了本应简单的项目的复杂性。
在PE中,由于PE是一个完全的可视化编程环境,这个相同的示例大不相同。首先,用户需要通过单击和拖动图标来指示每个引脚连接的是哪种类型的设备。然后用户必须再次通过单击和拖动不同的指令来构建每个脚本。每个脚本属于一个对象,并且与其他所有脚本并发运行。并发由PE调度器自动处理,这简化了对并发任务执行的描述(图2)。尽管在图中无法看到,每个脚本都配置为以不同的速率运行:第一个为1/s,第二个为2/s,第三和第四个脚本为100/s。这种配置在PE中设置起来比在Arduino代码中简单得多。每个任务被封装在其自己的脚本中,这简化了代码的阅读和理解。
PE提供的可视化界面对于初学者来说更容易理解,因为它杜绝了语法错误,并向用户呈现了一个面向对象API,其中每个图形对象代表一个用户可以直接操作的真实对象。虽然用户不必指定每个引脚模式,但必须告诉PE每个引脚连接了哪些设备,而通过单击和拖动配置设备到其对应的引脚比调用函数更自然和直观。
最后,Uzi程序是三个中最简单的,仅用四行代码描述了四个脚本。每个脚本都可以配置自己的滴答速率,Uzi虚拟机会负责在指定的间隔执行它。如果用户没有指定滴答速率(如“button”和“dimmer”脚本的情况),则虚拟机将在每次滴答时执行它们。现在不再需要手动记住每个引脚的状态来闪烁LED灯,因为在调用“toggle:”原语时,Uzi会自动处理。模拟和数字引脚之间没有区别,唯一可用的操作是“write:value:”和“read:”(以及基于这两个操作构建的其他操作,例如“toggle:”),两者都接受 0到1范围内的值。这一点可以在第3行和第4行中看到,其中脚本本质上具有相同的语句,但参数不同。最后,用户无需显式指定每个引脚模式;Uzi会自动配置引脚。
#BLINK13 滴答 1 / s [切换: D13]
#BLINK12 滴答 2 / s [切换: D12]
#按钮 滴答 [写入: D11 值: (读取: D9)]
#调光器 滴答 [写入: D10 值: (读取: A1)]
6 限制
在实现Uzi过程中做出的一些设计决策导致了某些限制,其中性能是最重要的因素。使用虚拟机使得获得与原生代码相同的性能几乎不可能。尽管尚未进行任何基准测试,但我们预计性能至少会慢100倍。对于用户预期使用 PE编写的大多数程序来说,这可能不是问题,但对于其他程序来说,这可能会施加一个明确的限制。目前正在考虑的解决方案之一是,除了生成 Uzi 字节码外,还自动生成一个 C++ 程序,该程序可以使用 Arduino 软件上传到开发板上。这将使得在需要时能够实现最高效率,同时保留虚拟机方法的优势。
Arduino开发板上可用的内存很小,这带来了一系列限制。目前,EEPROM被专门用于程序存储,这意味着用户无法使用它来存储其程序中的数值。理想情况下,监控器、虚拟机和用户程序都应存储在容量大得多的闪存中,但该方案尚未实现。在此之前,由于字符串和数组占用空间过大,因此不支持它们。
当前的Uzi实现不允许动态内存分配。这一设计决策具有多个优点,例如无需实现垃圾回收器,或允许通过静态分析在编译时确定程序所需的内存数量。然而,这也限制了可以使用Uzi编写的程序类型。
7 未来工作
Uzi 仍处于开发阶段。尽管其大部分设计已完成,但目前仅实现了一小部分基本功能,因此只能编写上述那样简单的程序。一旦实现趋于稳定,它将被集成到 PE 中,从而使得通过图形化脚本编写的 Etoys 项目能够被转换为 Uzi 字节码。
Uzi语言还需要更好的工具支持。尽管调试是该项目的指导原则之一,但尚未实现调试器。未来计划开发一个集成开发环境。
尽管Uzi目前的设计特别关注PE,但作者也感兴趣于评估其作为中间语言的能力,以便在此基础上实现不同的编程模型。
最后,由于Uzi虚拟机体积小且结构简单,将Uzi虚拟机移植到其他教育机器人平台(如乐高Mindstorms Nxt甚至PIC微控制器)也是令人感兴趣的。
8 结论
本文描述了Uzi——一种用于Arduino的虚拟机——的设计与实现。
该虚拟机解决了在使用PE向高中生教授机器人技术时遇到的一个特定问题。
Uzi 相较于传统 Arduino 工具的优势通过用三种不同的编程语言编写相同程序得以体现:Arduino 提供的简化版 C语言、PE 和 Uzi。
鉴于Uzi相较于传统Arduino工具的优势,非常鼓励将其用于教育目的。

被折叠的 条评论
为什么被折叠?



