很多人都玩过乐高解模方机器人,不知道大家注意过没有,在乐高官方提供的解魔方的方案中,解魔方的程序和执行操作的程序是各自独立的,为什么执行程序不能直接控制解魔方程序呢?
有谁能够写出一套完整图形化的解魔方案吗?很遗憾,到目前为止,我发现所有的解魔方案都是高级语言写的。使用图形化编程根本无法完成解魔方这么复杂的算法。图形化编程语言的特点是简单易懂,缺点是稍微复杂一点的算法都要整好几页。就拿那个简单的PID算法来说吧,C语言应该不超过10行代码,但是图形化语言要写好多页。那么可不可将复杂的算法写用高级语言写,让后通过图形化编程去调用呢?很遗憾,乐高图形化编程没有提供这样的接口。
我通过对乐高内核的研究,要使用图形化编程调用高级语言写的模块,至少涉及到以下三种编程方式:
1. C语言编程,内核是用这个语言写的,在后面我称之为C语言编程
2. EV3-block编程,EV3的图形化编程界面的下面,大家会看到有个动作、流程控制、传感器等等模块。就是用Block写的,在后面我称之为B语言编程。
3. 另一个就是大家用的LEGO MINDSTORMS Education EV3 图形化编程,在后面我称之为G语言编程。
上面三种编程的关系是:G语言通过调用B语言的模块编写程序,然后编译称二进制文件,此二进制文件的类型为*.rbf,然后被C编程写程序调用。注意,*.rbf 尽管是二进制文件,但还不是可执行的机器语言文件,它和我们在Linux上C语言编译出来的可运行程序是有差别的。
如何将这三种语言混合使用呢?用G语言写流程,用B语言写模块,用C语言写算法,不是很完美么?前面说了乐高没有提供这三种语言交互使用的接口,不过提供了大量的说明书,包括软件的,硬件的!我通过阅读这些说明书,终于将这条路走通了。在接下来的文章中我将逐步讲解如何将这三种语言编程语言串联起来使用,为了避免和网上的其它资料重复,在此我借用陈寅恪老人家的那句话:前人讲过的,我不讲;近日讲过的,我不讲;外国人讲过的,我不讲,这里我只讲大家都没有讲过的。由于涉及的内容太多了,如果从每一个知识点都讲到,不是三五篇文章能够说完的。因此我会在如下的文章中尽量注重解决问题的思路,而不是某个细节。
当然,如果只有思路,没有具体的成果,有人又会说我在这里讲毫无用处屠龙之术,在这里我先自我介绍一下。毕业于某九八五,混迹于自动化行业20年有余,在某乐高培训机构兼职10年有余,身无半截功名,空有雕虫小技。
为了说明我的方案是成功的,我提供了一下示例:在示例中,我在内核中融合了PID算法和卡尔曼滤波等算法。比用G语言直接写这些算法简单多了。详情见另一篇文章《自定义算法内核1.10F 》
内核文件、block文件、示例程序、示例程序说明见百度网盘
链接:https://pan.baidu.com/s/1BVcrYhNGAu_e8lTAO00FFQ
提取码:euhg
EV3 Firmware V1.10F.bin 内核程序:高级语言写的算法融合到内核中
HaoQiCal.ev3b B语言编程,调用高级语言写的算法
HQDemo.ev3 算发调用G语言示例。
大家可以先用内核EV3 Firmware V1.10F.bin更新主机,再导入HaoQiCal.ev3b,然后打开HQDemo.ev3程序测试。由于HaoQiCal.ev3b是和上面的内核对应,不跟新内核只导入此模块应该不能获得正确结果。
测试小车如下:

B语言编程
LEGO MINDSTORMS Education EV3 软件其实是具备B语言编程功能的,不过一般都不会用它。因为里面DeveloperMode="False"。
打开BLOCK编译模式
查看MindstormsEV3.exe 所在的目录是否存在
MindstormsEV3.ini 文件,没有就新建一个,在里面输入内容:
<?xml version="1.0" encoding="utf-8" ?>
<Tokens
DeveloperMode="True"
/>
有就设置DeveloperMode="True"
打开上的设置后重启。新建一个工程,拖入一个电机模块双击就可以看到如下界面:
注意这里说的Block创建和使用“我的模块创建器”创建的模块是不一样的。使用我的模块创建器创建的模块和EV3程序有关,当程序关闭了,这个模块就没了。
Block创建的模块是需要通过模块导入进行管理的,并且和EV3程序无关,当程序关闭了模块还在。
EV3是基于WebUI Builder,用于为基于网络的数据源创建界面的来自于NI的产品。它与实验界面有点相似,像调色板,连接线,块数据图等等。
Block创建的模块是以*.ev3b命名的,实际上,这个一个压缩包,你可以直接将*.ev3b文件直接重命名为*.zip,然后解压,解压后的文件,你可以看到
每个*.ev3b解压后的目录结构都是一样的,根目录下有个blocks.xml主要是模块说明。help目录为各类语言的帮助目录,images为图标文件目录,strings 目录为各种语言的模块命名目录,VIs 为 NXT和PRB两种主机对应的程序目录。在进行Block编程时,注意images中的图形文件命名与block的命名有关。根目录下的blocks.xml与strings 目录blocks.xml的命名要对应,并且两个文件中的变量命名要与VIs 为 目录文件中的变量命名对应。
详细模块创建说明可见
http://www.mindsensors.com/content/66-ev3-blocks-and-sample-programs
但是好像被墙了。如有需要,可联系作者获取。
要了解G语言写的程序如何编译执行就必须对编译后的二进制文件 *.rbf 有一个详细的了解,这一节我就专门讲这个文件。为了更好的理解这个文件,我还专门编写了一个二进制文件的解析文件,不过不具备反编译的功能。
编译后的*.rbf文件为二进制文件,该文件可以使用linux命名vim打开
Vim 可以用来查看和编辑二进制文件
vim -b filename 加上-b参数,以二进制打开
然后输入命令 :%!xxd -g 1 切换到十六进制模式显示
rbf文件的头由16个字节组成
/* 0 */ PROGRAMHeader(size=317, VersionInfo=0.57,NumberOfObjects=2,
GlobalBytes=57),
/* 0 */ Sign = 4C 45 47 4F
/* 4 */ Image size = 3D 01 00 00
/* 8 */ Version info = 39 00
/* 10 */ Number of objects = 02 00
/* 12 */ Number of global bytes = 39 00 00 00
0-3 四个字节为字母LEGO
4-7 四个字节为rbf文件的大小
8-9 两个字节为版本号*100
10-11 两个字节 object的个数,两个字节
12-15 四个字节为全局变量所占的字节数,全局变量用于各个Object之间交换数据。
在Image Header后面接着就是定义每个Object Header
object至少为1个,有多少个obejct就排多少个。
/* 16 */ VMTHREADHeader(OffsetToInstructions=40,LocalBytes=35), // Object 1
/* 16 */ Offset to instructions len=4 Code= 28 00 00 00
/* 20 */ Owner object id len=2 Code= 00 00
/* 22 */ Trigger count len=2 Code= 00 00
/* 24 */ Local bytes len=4 Code= 23 00 00 00
Object Header 的第一个四字节(16-19字节)为Object对应的指令偏移码,即执行此Object时,跳转到何处开始执行。
Ower Object Id(20-21字节) 依据主Object,子Object,程序调用等进行设置
Trigger count(22-23字节) 为触发器设置。
Local Bytes(24-27字节)为定义局部变量的字节数,用于Object内部交换数据。
在Object 后面接着就是程序指令,此指令和内核中定义的指令函数相对应。
在内核中,共定义了255个主函数(实际有些是空的),每个函数都定义了对应的参数个数和参数类型,所有主函数参数个数不超过8个;部分主函数又定义了子函数。
通过每个函数对应的指令和参数即可执行*.rbf对应的二进制文件。每个函数对应的代码在lms2012.c文件中定义
如: 假说 Object 1 对应的命令信息为40-127处的字节表示
40位置的字节为
A2 00 0F
第40个字节处 A2 表示调用函数, opOUTPUT_RESET, 函数对应的属性Pars=0x00000088 表示这个函数的参数类型,为两个 DATA8L类型。
00 0F 表示opOUTPUT_RESET, 的两个输入参数。
/* 43 */ OpCodes[99]=opINPUT_DEVICE, .Pars=0x00000B18
SubCodes[0B][0A]=LC0(CLR_ALL),LC0(FFFFFFFF),
99 0A 3F
第43个字节处99 表示调用函数 opINPUT_DEVICE,函数对应的属性Pars=0x00000B18 ,该函数三个参数,并且调用了子函数 CLR_ALL , opINPUT_DEVICE函数对应的属性Pars=0x00000B18 中的0B 表示子函数的类,第44 字节 0A 表示 子函数对应的类中的第几个函数,所有的自函数在SubCodes[0B][0A] 自函数表中, SubCodes[0B][0A] 表示的是子函数 CLR_ALL。
关于各个函数的说明可以参见文档
LEGO MINDSTORMS EV3 Firmware Developer Kit.pdf
此文档在乐高网站上可下载。
在第三节讲了二进制文件*.rbf的结构,在本节中,我主要想讲一下此文件的执行,从这里开始,就和内核文件密切相关关了。在内核代码中,*.rbf 主要是由lms2012工程相关的程序处理。在lms2012工程的bytecodes.h中共定义了5个执行槽,分别执行界面程序、用户程序,用户命令,终端命令,调试程序。界面程序就是启动EV3后看到的界面,用户程序就是通过LEGO MINDSTORMS Education EV3 运行按钮或者界面运行按钮启动的用户程序。
当用户程序被启动后,每个执行槽同时只能载入一个可执行程序*.rbf,各个执行槽之间的程序可以进行运行或者阻塞状态切换,也就是说最多同时只能有五个程序处于运行或者等待运行之中。在EV3操作系统启动后,自动启动lms程序,该程序还是二进制的机器语言程序,由lms2012编译生成,有点像一个微型的操作系统,它负责管理上面的五个执行槽中的程序运行。在该程序启动,就启动一个叫UI.rbf的程序,该程序就是我们在LED屏上看到的界面。该程序在GUI_SLOT执行槽中执行。该UI程序又可以载入用户程序*.rbf程序到USER_SLOT执行槽中执行。
在装在用户程序后(载入用户程序一般是UI.rbf执行),lms调用ProgramReset()函数对程序进程进行初始化。初始首先是CMomory 分配内存。内存大小由GetAmountOfRamForImage()函数计算。一个用户程序需要的内存的大小是由三部分组成的,
a 全局变量的大小(四字节对齐)
b 每个Object的表头的链表表头(表头占用的字节本身是4字节对齐的)
c 每个Object的局部变量的大小(四字节对齐)
内存分配成功后,由MemoryInstance.pPoolList 进行管理
MemoryInstance.pPoolList[PrgId][TmpHandle].pPool;
MemoryInstance.pPoolList[PrgId][TmpHandle].Type = Type;
MemoryInstance.pPoolList[PrgId][TmpHandle].Size = Size;
该结构体分别记下了申请的内存的类型、大小、和起始位置。
每个进程槽可管理MAX_HANDLES(500)个以内的内存缓冲区。
cMemoryOpen(PrgId,RamSize,(void**)&pData); 最后一个参数传回的是申请到的缓冲区的首地址MemoryInstance.pPoolList[PrgId][TmpHandle].pPool。
在ProgramReset()初始成功后即可载入将该进程的切入运行态,运行的起始地址为第一个Object的偏移地址Offset to instructions。
在进程执行时,PrimParPointer()函数实现了类似于CPU程序计数器PC的功能,它自动实现一个函数一个函数的访问。访问的方法是通过第一个DATA8数据定位到具体的函数,有了函数就能知道这个函数的具体参数个数,然后依据后面接着的DATA8的数据位去访问具体的参数。访问参数的规则是依据下图自动从局部变量或者全局变量或者常量位置中提取参数。
通过对*.rbf文件的分析,我们就可以看出B语言中的模块调用对应的内核中的函数了,我们就可以依据这些函数,对内核进行改写。LEGO公开的文档和帮助算是比较完整的了,软硬件基本都能够找到源代码。我唯一没有找到源码的是LEGO MINDSTORMS Education EV3软件的源代码,听说这是第三方开发的。我也不知道 *.ev3文件是如何编译成*.rbf文件的,如果谁有这方面的资料,望不吝赐教。不过我们依据对*.rbf的分析基本够看出*.vix文件对应内核中的函数。
工作做到这一步,已经和内核密切相关了。在这一节我主要说一下内核是如何编译的,关于内核的编译配置,详细的说明在如下网址
https://github.com/mindboards/ev3sources/wiki/Getting-Started
我这里主要说一下注意事项。
1. 安装ubuntu一定要安装32位的,不要安装64位的,ubuntu16即可,高版本除了界面炫酷一点没有任何好处,我第一次就是安装了64位的系统,然后在上面编译32的程序,各种各样的坑,让我填了半年,最后终于编译出来,还是各种问题不断。后来换成32位的系统立马问题消失了。
2. 如果不想改你电脑的系统,你可以安装一个U盘系统,也挺好用的,不过U盘一定要选3.0接口的U盘,2.0接口的U盘系统慢的不能忍受。
上面两步走对了了之后,基本你就可以愉快的编译了,但是并不代表没有问题。
/projects/lms2012/open_first/执行make如下目标
Execute "make TARGET" where TARGET is:
lms2012: to build the lms2012 program and its libraries
modules: to build lms2012 kernel modules
programs: to build bytecode programs and their data files
kernel: to build Linux kernel
u-boot: to build u-Boot
doc: to build documentation
make doc 编译生成帮助说明,你编译时碰到的问题一般能在这里找到答案。
建议首先阅读readme.txt文件
make u-boot 编译嵌入式内核加载程序
make kernel 编译嵌入式linux内核。
make programs 这个我真不知道编译出什么有用的东西,应该是操作系统用的基本程序吧。
make modules 编译驱动,要是想制作乐高兼容设备,可以在这个上面做工作,乐高的硬件定义。当然,自定义算法程序也可以在这里嵌入。
make lms2012 编译主程序,这就是前面说的那个微操作系统了,所有的*.rbf通过它处理。
驱动、LINUX内核,lms2012工程,以及运行程序*.rbf的关系如下图:
在make kernel时可能会报出如下错误
Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.
起初一头雾水,仔细看了错误提示后发现问题出现在如下文件中,估计是遗传问题/home/book/projects/extra/linux-03.20.00.13/kernel/timeconst.pl
在文件中 373行的
if (!defined(@val)) {
改为if (!@val) {
即可编译通过。
编译后执行文件组装脚本即可生成对应的内核二进制文件。组装脚本主要是将以下三个二进制文件组装成一个二进制文件。
0x0 uBoot
0x50000 uImage linux内核文件
0x250000 EV3.cramfs.bin cramfs映像
组装完成之后的文件即为你经常更新的固件,命名为 EV3-image.*.bin,其中的*为一个时间标签,此内核即可下装到EV3主机当中,当然此固件也可以分解为上面三个文件,并且我已经提供了分解固件的程序和源代码Extract_uImage.zip,当然需要在linux操作系统上运行和编译。编译软eclipse。
将固件下载到EV3主机当中,开机运行,听到嘀的一声,是不是突然有种做上帝的感觉。当然也有可能更新固件失败,导致破坏了一个旧世界,又没有创造新世界。不过也不要着急急,网上有很详细的解决方案,照着做一下,你就会有拯救整个世界的感觉。
EV3从图形化编程到内核编译的思路到这里基本就讲的差不多了,关键是要实践,实践过程中肯定会碰到各种各样的问题,不要担心,因为问题使人进步,但我还是祝你好运!
附固件分解程序下载地址
链接:https://pan.baidu.com/s/1kas53edBp44qB_wgrzqGDw?pwd=euhg
来源:https://bbs.cmnxt.com/thread-65166-1-1.html