由于焊接工具没还没到位,于是最近几篇文章主要以方案细化与软件开发为主。今天这篇文章主要做一件比较有意义的事情:NDS端BASIC语言解释器的移植与扩展。文章附上了移植后软件的运行截图和实机运行效果,并在文章最后附上工程代码,方便下载。顺便打个广告:有兴趣的朋友可以加入QQ群:362445156 (Arduino极客群)。
一、为什么要移植和扩展BASIC语言解释器?
到目前为止,所有的NDS硬件扩展,包括DS brut在内都只提供SDK软件开发库。这使得你要进行二次开发,必须花大量时间去看SDK的文档甚至代码,工作量开销较大。特别是以下两个问题:(a). 对于想进行快速测试一个外设的开发人员来说,阅读文档,源码,以及在此基础上进行自行开发程序调试程序的时间会比较多。(b). 如果需要控制另一个外设或调整软件功能时,必须重新编译程序,并将程序复制到NDS的烧录卡内。因此,工序多了很多步骤。
BASIC语言在诞生之初就以简单易用为哲学指导。我将秉持该指导思想来做为本方案的Demo技术演示。如果能将BASIC语言的解释器移植到NDS上,将有如下优点:
(1)利用NDS的触屏可以作为字符输入设备,效率很高。
(2)用BASIC语言编写程序,非常容易,几乎不用学习,而且程序一般简短易懂。
(3)通过BASIC语言方便快捷的写程序,可以立杆见影,马上看到执行效果,无需在SDK上进行再开发,明显提高开发效率。这解决了上述的问题(a).
(4)通过扩展BASIC解释器,为其加入对DLDI(在各NDS烧录卡实现统一的文件读写功能)的支持,可以直接在NDS上将编好的程序写入烧录卡的SD卡的文件内,也可以直接从SD卡内将程序文件读入内存。这解决了上述的问题(b).
当然没有一个方案是十全十美的,相比直接在SDK上开发,利用BASIC解释器的缺点是,程序运行速度没有前者快。因此不适合作一些对外设SPI回传数据响应速度要求很高的场合。比如想把NDS做成一个逻辑分析仪。
二、移植BASIC语言解释器
要移植BASIC解释器,那么就得选择一个目标进行移植。早在2007年,就已经有一位网名叫zzo38computer的外国友人做了这个工作,项目名称为DSBasic。他用来移植的BASIC解释器源码用C语言编写,因此比较容易移植,只需要添加了NDS的软键盘等功能。另外,网上流传甚广的开源BASIC解释器源码版本也比较多,比较有名的就是Tiny Basic。这个Tiny Basic说来话长,最早可以说到1975年。这里我们主要讲一下我采用的代码,来自TinyBASIC 2也采用的核心代码BAS-INT.C这个文件。
经过查看源码发现,原来DSBasic也是基于这个版本的代码进行扩展的。而TinyBASIC 2的功能更加强大,还支持画图命令(需SDL库支持,不过SDL库开源且跨平台)。额外一提:自己用C/C++写个BASIC解释器不难,网上也有不少文章介绍,请google之。
下载了TinyBASIC 2源码后,查看BAS-INT.C文件,该文件采用了较早的C语言语法。于是首先修改语法,然后用gcc在我的Mac OS X下顺利编译通过,试着运行了几个附带的BASIC程序例子,一切顺利。
接下来便是将代码移植到NDS上。由于devkitPro并没有提供太多的基于命令行的NDS开发示例程序。因此需要我加一些自己的代码来实现简单的光标、scanf功能等。
整个移植过程就不详述了,具体可以下载后面提供的源码。这里主要讲一下,移植的几个要点:
(1)添加光标。我简单的用"_",即下划线代替光标,该光标很简单,不会闪烁,但基本达到使用的目标,除了一个小BUG:输入文字到行末时,会自动跳到本行行首,而不是下一行。但该Bug不影响输入的代码。
(2)添加int get_input_number()函数实现INPUT命令的移植。因为我使用触屏软键盘后,NDS不支持scanf()函数从屏幕获得输入。
(3)添加"RUN"和"!"两条命令来运行程序。由于BAS-INT.C运行程序是在命令行将需要执行的BASIC程序作为命令行参数进行调用执行的,因此不支持程序编辑功能。而在NDS上我添加了一个非常简单的程序输入功能(包括上面提到的光标)。
图1为移植成功后的运行效果。下文将该移植到NDS的BASIC解释器项目简称:NDSBasic。
图1为最初植移的运行界面,下方为触屏,提供软键盘进行输入。上屏为字符终端,和DOS,以及Terminal类似。
三、扩展BASIC语言解释器
该BASIC解释器 (BAS-INT.C),提供的命令非常有限,因此需要自己扩展添加新的BASIC语言命令。由于BAS-INT.C源码本身编写比较清晰,添加新命令过程非常简单。只需以下几步完成一个新命令添加:
(1)定义新命令宏,如 #define SEND 16
(2)在 struct commands 结构体中添加命令的字符串,以及对应的第(1)步中的宏,如"send", SEND,
(3)添加命令的执行函数声明,以及函数代码,如void exec_send();
(4)在主函数 (main)的switch命令中添加新命令的调用,如:
case SEND:
exec_send();
如果该命令除了一般的逻辑处理外需要用到NDS硬件等功能,则可在第(3)步代码中调用外部函数完成相应的硬件功能。这样的设计代码可移植性较好,逻辑功能代码和硬件相关代码分离。
我主要扩展添加了以下几条命令:
(1)"RUN" 或 "!":如上文所述。
(2)"LIST":打印内存里的BASIC代码到屏幕上。
(3)"NEW":清除内存里的BASIC代码,开始编写新的程序代码。
(4)"?":和PRINT命令一样,用一个简短的符号,减少输入时间。
(5)"SAVE filename":将当前编辑的内存里的代码保存到filename文件中。
(6)"LOAD filename":将filename文件里的代码读入内存。可以直接输入命令"RUN"或"!"运行。
(7)"PSET x,y,clr":画像素。在 (x,y)处像素用clr号颜色点亮。
(8)"LINE x1,y1,x2,y2,clr [,B[F]]":从(x1,y1)到(x2,y2)用clr色号画线、画空心矩形、画实心矩形命令。和QBASIC里的同名命令类似,区别是我为了方便命令的输入,将QBASIC语法中的y1和x2之间的"-"改为了逗号","。另外,clr代表NDSBasic中的颜色号(预定义),"B"代表画空心矩形,"BF"代表画实心矩形。
(9)"CIRCLE x,y,r,clr":画圆命令。x,y代表圆心,clr为颜色号。
(10)"DELAY ms":程序延迟ms毫秒。
(11)"SEND":发送数据到Arduino (Slot 1接口的SPI通道)上。我将该命令的语法设计成和PRINT一致,这样使用起来比较灵活。
(12)"RECV n":从Arduino (Slot 1接口的SPI通道)上读取n个字节的数据,并打印读取到的数据。默认当读取过程中遇到'\0'字符时也会停止读取。
(13)"CLS":清屏命令。执行时将清除屏幕内所有的打印信息。
所有命令不分大小写,解释器能自动识别。实现过程中,
- LINE命令用到了我的开源3D引擎Nomad3D中的画线代码,支持Cohen裁剪功能,且执行性能高效。
- CIRCLE命令则用到了我的另一篇博文:基于NDS/GBA/ARM,从启动到运行你自己的第一行C程序代码(NDS篇)中的画圆算法,效率也很高。
- DELAY命令使用NDS的第0号硬件计时器 (Timer 0)实现,精度达到微秒级。而且每次用完就释放计时器,不占用硬件资源。
- SEND和RECV命令的实现用到了第三方库:libspi-0.2 源码,由于源码对应的devkitPro版本太早,源码中用的很多宏已经不存在或与当前版本(我用的是最新的版本:devkitARM r42,libnds-1.5.8)冲突。因此我重新修改了源码并编译成libspi.a库文件方便以后使用。
void exec_send()
{
//syntax: similar with PRINT
char send_str[256];
char recv_buff[256];
char temp[50];
memset(send_str,0,256);
memset(recv_buff,0,256);
memset(temp,0,50);
sprintf(temp," ");
strcat(send_str,temp);
}
void do_send(char* send_str, char* recv_buff, int max_len)
{
int i=0;
while(send_str[i] && i<= max_len)
{
recv_buff[i] = send_str[i];
i++;
}
recv_buff[i]='\0';
////////
int len=strlen(send_str);
char* p=send_str;
setupConsecutive_cardSPI(len);
while(*p)
writeBlocking_cardSPI(*(p++));
}
RECV命令实现源码:
void exec_recv()
{
int num_recv_byte=0;
int num_byte=0;
get_exp(&num_byte);
memset(recv_buff,0,MAX_RECV_SIZE);
num_recv_byte = do_recv(recv_buff, num_byte, STOP_CHAR);
//the variable Z used to store the number of received byte.
//variables['Z'-'A'] = num_recv_byte;
printf("[received %d byte: %s]\n", num_recv_byte, recv_buff);
}
int do_recv(char* buff, int num_byte, char stop_byte)
{
u8 read_byte=0;
int i=0;
for(i=0; i
{
}
最后,因为添加了画图功能,我将终端窗口从上屏移到下屏,和软键盘放在一个屏内。终端窗口提供14行代码显示,上屏全部用来画图。最后运行效果如图2, 图3, 图4所示。
图2. LINE命令执行效果。
图3. LINE、CIRCLE和DELAY命令在循环中执行的效果。
图4. 实机运行效果。
基于Slot 1接口SPI通信的SEND, RECV命令没能实测,但模拟器上执行来看应该是工作正确的。
代码下载:NDSBasic + libspi
后续将先测试SEND和RECV命令,然后添加以下命令:
- DWRITE pin,value:设置Arduino的第pin数字引脚为值value (1: HIGH高电平,0:LOW低电平)。
- AWRITE pin,value:设置Arduino的第pin (PWM引脚)为值value (0~255之间的值,用于PWM信号)。
- DREAD( pin ):读取第pin数字引脚的电平状态 (1:高电平,0:低电平)。
- AREAD( analogPin ):读取第analogPin模拟引脚的值 (0~1023之间)。
后记:
当时在考虑加画图命令时有两种方案,除了当前使用的方案外,另一种可选方案是:
(1)仍然使用上屏作为主终端屏幕,当执行到画图命令时上屏自动切换到画图状态(由于NDS硬件原因,同一屏幕画图状态和显终端状态不可同时存在)。下屏软键盘上方14行只用于显示与Arduino的SPI通信的数据(发送和接收数据),不作他用。
本篇结束,后面将会涉及Arduino端具体的SPI通信代码设计与编写。