EV3运行程序是指下载到EV3编程块上用于执行的一个文件(rbf文件),rbf是Robot Bytecode File的缩写,这个文件是一个字节码文件,需要由EV3固件程序来加载执行,在官方《EV3固件开发者工具包》文档架构图中,把固件程序(lms2012.elf)称为VM(虚拟机)。
那么EV3系统设计为什么使用bytecode做为运行程序呢?
一个可能原因是乐高要在EV3上使用图形化编程,看中了NI的LabVIEW,而LabVIEW在设计上就是采用的bytecode方式,给EV3做一个图形化开发环境只需要实现相应的bytecode指令集和VM运行时(固件)就可以了。
从嵌入式系统设计角度来看,我认为通过VM来隔离应用程序与底层程序(系统程序、硬件驱动等)之间的耦合是一个非常优雅的设计方案,因为这样在开发应用软件时可以不用考虑硬件相关特性,而硬件发生变更时(如更新换代,接口调整等)只需要更新VM即可,对应用软件影响较小,当然代价就是对硬件和系统开发的要求更高,如要能运行Linux,VM的开发要涉及Linux底层的驱动开发等等。
正是因为采用VM系统架构和相关资料的开放,使得EV3除了能面向青少年STEM教育外,还可以供我们系统的学习嵌入式系统与计算机原理相关的知识。
本文主要探究Bytecode指令集、EV3应用开发与编译、VM运行时等相关知识。
EV3的Bytecode指令集采用的是不定长指令设计,指令的第1个字节为操作码(OpCode),共有185条指令。包含:
1.一般操作指令(30条):包含程序、对象、系统等指令
2.数学指令(17条):包含加、减、乘、除等指令
3.逻辑操作指令(11条):包含逻辑与、或、异或、移位等指令
4.内存移动操作指令(18条):包含内存数据移动指令
5.分支操作指令(28条):包含各种分支跳转指令
6.比较操作指令(18条):包含大于、小于、等于、不等于等指令
7.程序选择操作指令(4条)
8.输入端口操作指令(8条):包含对输入端口1-4上设备的操作指令
9.输出端口操作指令(19条):包含对输出端口A-D上设备的操作指令
10.声音操作指令(3条):
11.定时器操作指令(4条)
12.通讯操作指令(12条):包含通讯硬件(USB、蓝牙、WiFi)和MailBox相关的指令
13.内存操作指令(7条):包含对文件和数组进行操作的指令
14.用户界面操作指令(6):包含对LCD显示屏,按钮和指示灯进行操作的指令
各指令的细节请参考《EV3固件开发者工具包》的第4节。
EV3早期指出的应用软件,是集教学、开发调试和EV3编程块管理于一体的软件。应用开发采用的是LabVIEW的Graphicals语言的变体,也被称为EV3-G。
使用EV3-G进行开发,只需将各类程序模块进行连接组合即可完成编程工作。
由EV3-G开发出来的程序文件后缀为ev3,它其实是一个zip文件,包括程序(Program.ev3p)、图像、声音、我的模块和变量。
EV3 Classroom(课堂)是EV3新一代的教学软件,支持Windows, MacOS, 和移动端设备,与Scratch等图形化编程语言类似,程序流为竖向。
除了上面两种图形化开发语言,EV3还支持使用一种LMS专用语言来开发应用程序,EV3启动后的UI程序就是通过这种专用开发语言编写的。
Port View.lms主代码
vmthread MAIN // void MAIN(void)
{ // {
DATA32 Timer //
DATA16 hModes //
DATA8 Run //
DATA8 Selection //
DATA8 Changing //
DATA8 Tmp //
DATA8 Init //
DATA8 ShowVersion //
//
UI_BUTTON(PRESSED,RIGHT_BUTTON,ShowVersion) // UI_BUTTON(PRESSED,RIGHT_BUTTON,ShowVersion)
JR_FALSE(ShowVersion,DontShowVersion) // if (ShowVersion)
// {
UI_DRAW(FILLRECT,BG_COLOR,4,50,170,28) // UI_DRAW(FILLRECT,BG_COLOR,4,50,170,28)
UI_DRAW(RECT,FG_COLOR,6,52,166,24) // UI_DRAW(RECT,FG_COLOR,6,52,166,24)
UI_DRAW(TEXT,FG_COLOR,13,60,appv) // UI_DRAW(TEXT,FG_COLOR,13,60,appv)
UI_DRAW(UPDATE) // UI_DRAW(UPDATE)
//
ShowVersionWait: // do
// {
UI_BUTTON(PRESSED,RIGHT_BUTTON,ShowVersion) // UI_BUTTON(PRESSED,RIGHT_BUTTON,ShowVersion)
// }
JR_TRUE(ShowVersion,ShowVersionWait) // while (ShowVersion)
//
UI_BUTTON(FLUSH) // UI_BUTTON(FLUSH)
DontShowVersion: // }
//
UI_DRAW(RESTORE,0) // UI_DRAW(RESTORE,0)
UI_DRAW(TOPLINE,1) // UI_DRAW(TOPLINE,1)
UI_BUTTON(SET_BACK_BLOCK,1) // UI_BUTTON(SET_BACK_BLOCK,1)
UI_WRITE(LED,LED_GREEN) // UI_WRITE(LED,LED_GREEN)
//
ARRAY(CREATE8,MAX_PORTS,hModes) // ARRAY(CREATE8,MAX_PORTS,hModes)
ARRAY(FILL,hModes,0) // ARRAY(FILL,hModes,0)
//
MOVE8_8(1,Run) // Run = 1
MOVE8_8(0,Selection) // Selection = 0
MOVE8_8(0,Changing) // Changing = 0
MOVE8_8(0,First) // First = 0
MOVE8_8(0,TmpMode) // TmpMode = 0
MOVE8_8(0,Init) // Init = 0
//
Loop: // do
// {
UI_DRAW(FILLWINDOW,BG_COLOR,TOPLINE_HEIGHT,0) // UI_DRAW(FILLWINDOW,BG_COLOR,TOPLINE_HEIGHT,0)
//
CALL(CheckConnections,hModes) // CheckConnections(hModes)
CALL(CheckKeys,Run,Selection,Changing) // CheckKeys(Run,Selection,Changing)
CALL(ShowSelectedConnection,hModes,Selection,Changing) // ShowSelectedConnection(hModes,Selection,Changing)
CALL(ShowAllConnections,hModes,Selection) // ShowAllConnections(hModes,Selection)
//
JR_EQ8(Init,1,Skip) // if (Init != 1)
// {
INPUT_DEVICE(CLR_ALL,0) // INPUT_DEVICE(CLR_ALL,0)
MOVE8_8(1,Init) // Init = 1
Skip: // }
//
UI_DRAW(UPDATE) // UI_DRAW(UPDATE)
//
TIMER_WAIT(UPDATE_TIME,Timer) // TIMER_WAIT(UPDATE_TIME,Timer)
TIMER_READY(Timer) // TIMER_READY(Timer)
// }
JR_TRUE(Run,Loop) // while (Run)
//
UI_BUTTON(SET_BACK_BLOCK,0) // UI_BUTTON(SET_BACK_BLOCK,0)
} // }
左边为专有语句,右边注释为伪高级语句,可以看到主要是把一些结构控制语句换成了JR_xxx跳转语句,另外专有语句名称与Bytecode指令操作码是对应的。
MakeCode
MakeCode for ev3是Microsoft推出的图形化编程软件,支持在线模拟执行。
EV3Basic
EV3Basic是Microsoft推出的基于Small Basic语言的EV3扩展版本。
Small Basic采用基于文本的编程方式,使用简洁的语法和简单直观的命令,使初学者能够快速理解和编写代码。
Robot C
[稍后补充】
编译是指将程序代码转换成计算机指令的过程,是计算机原理的重要组成部分。在EV3系统中,应用程序需要编译成Bytecode指令后,才能被EV3 VM执行。
前文提到EV3-G图形化程序其实是LabVIEW的变体,编译使用的是LabVIEW的编译系统,因为EV3 Lab并没有开放源代码,无法直接学习其编译过程。
不过通过反编译EV3 Lab的相关程序,可以了解编译的大概过程
这里不做展开,有兴趣的同学可以自行反编译后查看。
前文有提到EV3启动后的UI程序是通过LMS专用开发语言编写的,本小节来看下这种专用语言代码(文件后缀lms)的编译。
1. EV3自带的编译程序打开EV3 固件的源代码目录,进入lmssrc/adk/lmsasm目录,找到assembler.jar,这个文件就是编译lms文件的java程序。
新建一个helloworld.lms文件,内容如下:
vmthread MAIN
{
UI_DRAW(FILLWINDOW,0,0,0) // 清屏
UI_DRAW(TEXT,1,10,50,'hello world') // 在10,50 (X,Y)处显示 "hello world"
UI_DRAW(UPDATE) // 更新UI
UI_BUTTON(WAIT_FOR_PRESS) // 等待按钮按下
}
通过执行java -jar assembler.jar helloworld来进行编译,helloworld就是xxx.lms,执行命令里不需要指定.lms后缀,编译若无错误会生成helloworld.rbf文件。
assembler.jar没有开放源码,有兴趣的同学可以自行反编译后查看编译源码。
2. EV3Basic带的编译程序EV3Basic实际上是将Basic语言代码先转换为LMS代码,再使用自带的LMS编译程序将LMS代码编译为RBF,而且这个编译程序还支持将RBF反编译成LMS专用语言代码。
EV3Basic LMS编译程序源码:
https://github.com/c0pperdragon/EV3Basic/tree/master/LMSAssembler
LMS编译涉及的内容较多,这里就不细述了,后面另开一篇文章讲解。
前一小节提到了EV3Basic带的LMS编译程序可以反编译,下面就是Helloworld.rbf反编译的内容,可以看到与helloworld.lms的代码是一致的。
EV3的VM运行时,就是固件程序,它是一个Linux执行程序,EV3程序块系统启动后,这个固件程序就接管了系统,然后等待用户的操作。
1 固件初始化
打开固件源码文件夹的lms2012.c文件,找到入口main函数
int main(int argc,char *argv[])
{
RESULT Result = FAIL;
UBYTE Restart;
do
{
Restart = 0;
Result = mSchedInit(argc,argv);
if (Result == OK)
{
do
{
Result = mSchedCtrl(&Restart);
}
while (Result == OK);
Result = mSchedExit();
}
}
while (Restart);
return ((int)Result);
}
可以看出这是一个较典型的嵌入式系统主程序代码,
2 运行rbf文件
找到mSchedCtrl函数,下面是精简后的代码
RESULT mSchedCtrl(UBYTE *pRestart)
{
RESULT Result = FAIL;
ULONG Time;
IP TmpIp;
if (VMInstance.DispatchStatus != STOPBREAK)
{
ProgramInit();
}
SetDispatchStatus(ObjectInit());
/*** Execute BYTECODES *******************************************************/
#ifndef DISABLE_PREEMPTED_VM
(*VMInstance.pAnalog).PreemptMilliSeconds = 0;
while ((VMInstance.Priority) && ((*VMInstance.pAnalog).PreemptMilliSeconds < 2))
#else
while (VMInstance.Priority)
#endif
{
VMInstance.Priority--;
PrimDispatchTabel[*(VMInstance.ObjectIp++)]();
VMInstance.InstrCnt++;
}
/*****************************************************************************/
VMInstance.NewTime = GetTimeMS();
Time = VMInstance.NewTime - VMInstance.OldTime1;
if (Time >= UPDATE_TIME1)
{
VMInstance.OldTime1 += Time;
cComUpdate();
cSoundUpdate();
}
Time = VMInstance.NewTime - VMInstance.OldTime2;
if (Time >= UPDATE_TIME2)
{
VMInstance.OldTime2 += Time;
usleep(10);
cInputUpdate((UWORD)Time);
cUiUpdate((UWORD)Time);
}
if (VMInstance.DispatchStatus == FAILBREAK)
{
if (VMInstance.ProgramId != GUI_SLOT)
{
if (VMInstance.ProgramId != CMD_SLOT)
{
UiInstance.Warning |= WARNING_DSPSTAT;
}
snprintf(VMInstance.PrintBuffer,PRINTBUFFERSIZE,"}\r\nPROGRAM \"%d\" FAIL BREAK just before %lu!\r\n",VMInstance.ProgramId,(unsigned long)(VMInstance.ObjectIp - VMInstance.Program[VMInstance.ProgramId].pImage));
VmPrint(VMInstance.PrintBuffer);
ProgramEnd(VMInstance.ProgramId);
VMInstance.Program[VMInstance.ProgramId].Result = FAIL;
}
else
{
snprintf(VMInstance.PrintBuffer,PRINTBUFFERSIZE,"UI FAIL BREAK just before %lu!\r\n",(unsigned long)(VMInstance.ObjectIp - VMInstance.Program[VMInstance.ProgramId].pImage));
VmPrint(VMInstance.PrintBuffer);
LogErrorNumber(VM_INTERNAL);
*pRestart = 1;
}
}
else
{
if (VMInstance.DispatchStatus == INSTRBREAK)
{
if (VMInstance.ProgramId != CMD_SLOT)
{
LogErrorNumber(VM_PROGRAM_INSTRUCTION_BREAK);
}
TmpIp = VMInstance.ObjectIp - 1;
snprintf(VMInstance.PrintBuffer,PRINTBUFFERSIZE,"\r\n%4u [%2d] ",(UWORD)(((ULONG)TmpIp) - (ULONG)VMInstance.pImage),VMInstance.ObjectId);
VmPrint(VMInstance.PrintBuffer);
snprintf(VMInstance.PrintBuffer,PRINTBUFFERSIZE,"VM ERROR [0x%02X]\r\n",*TmpIp);
VmPrint(VMInstance.PrintBuffer);
VMInstance.Program[VMInstance.ProgramId].Result = FAIL;
}
ObjectExit();
Result = ObjectExec();
if (Result == STOP)
{
ProgramExit();
ProgramEnd(VMInstance.ProgramId);
VMInstance.DispatchStatus = NOBREAK;
}
else
{
if (VMInstance.DispatchStatus != STOPBREAK)
{
ProgramExit();
}
}
}
if (VMInstance.DispatchStatus != STOPBREAK)
{
Result = ProgramExec();
}
if (*pRestart == 1)
{
Result = FAIL;
}
#ifdef Linux_X86
usleep(1);
#endif
return (Result);
}
其中执行bytecode的代码是这条语句:
PrimDispatchTabel[*(VMInstance.ObjectIp++)]();
显然VMInstance.ObjectIp是当前rbf程序对象的指令指针了,它指向的是指令操作码。
而PrimDispatchTabel是一个函数指针数组,定义如下:
PRIM PrimDispatchTabel[PRIMDISPATHTABLE_SIZE] =
{
[opERROR] = &Error,
[opNOP] = &Nop,
[opPROGRAM_STOP] = &ProgramStop,
[opPROGRAM_START] = &ProgramStart,
[opOBJECT_STOP] = &ObjectStop,
[opOBJECT_START] = &ObjectStart,
[opOBJECT_TRIG] = &ObjectTrig,
[opOBJECT_WAIT] = &ObjectWait,
[opRETURN] = &ObjectReturn,
[opCALL] = &ObjectCall,
[opOBJECT_END] = &ObjectEnd,
[opSLEEP] = &Sleep,
[opPROGRAM_INFO] = &ProgramInfo,
[opLABEL] = &DefLabel,
[opPROBE] = &Probe,
[opDO] = &Do,
[opADD8] = &cMathAdd8,
// 其它指令 ......
}
其中,opXXX是在bytecodes.h中定义的枚举常量
通过与操作码对应的函数指针,VM得以调用执行具体的代码。
传感器是一种检测装置,能感受到被测量的信息,并按一定规律变换成为电信号或其他所需形式的信息输出,以满足信息的传输、处理、存储、显示、记录和控制等要求。
本文介绍如何在Scratch中对EV3机器人进行开发。
在这一章中,你将学习一组传感器,它们被用来执行有根据的动作。
在本章中,您将学习启发式搜索策略背后的基本思想以及如何实现爬山算法,这是 leJOS EV3 中最典型的启发式方法之一。
这一章向你介绍了在莱霍斯 EV3 使用的笛卡尔坐标系的基础知识。它还教你如何在导航课程中应用编程方法来控制轮式车辆,以便在二维平面中用坐标描绘出预定义的路径。
本章提供了如何使用乐高 MindStorm EV3 公司建立 Java 机器人编程环境的分步指南,包括乐高 MindStorm EV3 的基本概述和leJOS-EV3的介绍。