到目前为止,我们设计并实现了一个完整的计算机系统,包括8051计算机硬件、51DOS磁盘操作系统和基本命令程序,但为其开发应用程序只能在PC上进行,还需要Keil开发环境下和C语言,能不能直接在我们自己设计的计算机系统上开发程序呢?

程序大体可以分成两大类,一类是由二进制机器码组成的,计算机硬件直接识别执行;另一类是由脚本语言组成,这类程序就是使用脚本语言写成的文本文件,由一个脚本解释程序一边解释一边执行。二进制机器码的程序可以由高级语言(例如C语言)经编译器生成,也可以由汇编语言经汇编器生成,甚至也可以直接用二进制指令写,因为是硬件执行,所以速度相对较快。脚本语言程序是由脚本解释器一行一行的解析并处理,解释器本身是一个计算机程序,脚本就是一个文本文件,可以理解为脚本解释器处理脚本文件,根据脚本文件的内容作出各种响应,比如脚本文件需要输出“hello world”,那么脚本解释器就输出个“hello wolrd”,脚本文件需要往P0口输出高电平,脚本解释器就往P0口输出个高电平,这种软件解释的方式,速度相对比较慢。

如果想在我们设计的计算机系统上写程序,可以使用哪种方式呢?C语言等高级语言这种方式可以排除,因为高级语言编译器需要内存比较大、CPU比较快,8051单片机无法满足。汇编语言程序可以考虑,汇编语言和机器语言有严格对应关系,汇编器所需要的内存和CPU资源有限,8051单片机基本可以满足,笔者也做了一个简单的汇编器,可以实现在8051计算机上写汇编程序,由汇编器汇编成二进制程序,然后直接执行,但汇编语言可读性太差,这种方式我们也不准备采用。脚本语言有高级语言一样的可读性和可移植性,而且8051单片机的资源也能运行简单的脚本解释器程序,所以脚本语言程序是不错的选择。

9.1 EBASIC语言语法设计

我们选择BASIC语言作为开发语言,但完整版的BASIC语言语法也非常复杂,解释器做起来也很困难,所以要对BASIC语法做最大限度的精简,然后再为其做一个简单的BASIC解释器,语法简化后的BASIC语言,就叫EBASIC语言吧,容易的BASIC语言。

1. EBASIC语言基本语法

l 每一行语句要有行号,程序结尾要有end;

l 变量名只能用26个小写字母,变量类型是32位整形有符号数,输入的常数最长10位,常量可以是负数;

l 支持变量赋值语句;

l 支持print语句,可以打印输出字符串、变量、算数表达式,多个输出用逗号隔开;

l 支持for循环,for循环可以多层嵌套;

l 支持if else判断语句,但要写在一行,不支持嵌套;

l 支持加、减、乘、除、取余、逻辑与、逻辑或运算;

l 支持等于、大于、小于关系运算;

l 支持带括号的算数表达式;

l 支持子程序调用;

l 支持延时语句。

2. EBASIC语言扩充语句

l in 通道号 to 变量,用于读取P1端口的值到变量,通道号0至7,通道号可以是常数,也可以是变量;

l out 变量 to 通道号,用于把变量的值写入P1端口,通道号0至7,通道号可以是常数,也可以是变量。

9.2 EBASIC语言示例

例子1 计算并输出1加到100之和

10 s=0

20 for i=0 to 100

30 s=s+i

40 if i=100 then print s

50 next i

60 end

在8051计算机上使用行文本编辑器ed创建123.txt文件,输入上面的例子并保存退出,使用EBASIC的解释器执行“basic 123.txt”,运行结果如图9-1所示。

图9-1  1到100之和执行情况

例子2 打印输出乘法口诀表

10 for i=1 to 9

20 for j=1 to 9

30 print i,”x”,j,”=”,i*j

40 next j

50 next i

60 end

 创建文件并输入程序,运行结果片段如图9-2所示。

 

图9-2 乘法口诀表片段

例子3 计算圆周率的近似值

莱布尼茨公式:

这个圆周率计算公式比较有规律,分母是顺序的奇数,分子是1,正负符号一项变化一次。

EBASIC语言只支持整数,所以公式可以简单变化一下:

这样可以计算出圆周率1000000倍的近似值,每一项都是近似值,累加的项数越多,精度就越高,计算前1000项的代码如下:

10 p=0

20 for i=1 to 1000

30 if i%2=1 then p=p+4000000/(2*i-1) else p=p-4000000/(2*i-1)

40 print p

50 next i

60 end

读者创建脚本文件并输入代码,使用basic程序解释执行代码,会看到圆周率精度逐步提高,计算1000项后的结果如图9-3所示,中间任意时刻可以输入q退出。

图9-3  近似计算圆周率结果

例子4 一筐鸡蛋,1个1个拿能拿完,2个2个拿剩1个,3个3个拿剩1个,4个4个拿剩1个,5个5个拿剩1个,6个6个拿剩1个,7个7个拿能拿完,这筐鸡蛋最少有多少个?

10 for i=1 to 1000

20 a=(i%2-1)|(i%3-1)|(i%4-1)|(i%5-1)|(i%6-1)|(i%7)

30 if a=0 then print i

40 next i

50 end

创建脚本文件,并执行之后如图9-4所示。

图9-4  例子3执行情况

例子5  K1~K4四个按键分别控制D1~D4四个LED灯的亮灭。

10 for i=0 to 1000

20 for j=4 to 7

30 in j to a

40 out a to j-4

50 next j

60 next i

70 end

创建脚本文件并执行,是不是按键按下时,相应的LED等熄灭?键盘输入q可以退出程序。

例子6  LED灯轮流亮灭

10 for i=0 to 1000

20 gosub 100

30 next i

40 end

100 delay 1000

110 out 0 to i%4

120 delay 1000

130 out 1 to i%4

140 return

“gosub 100”表示跳转到子程序100行处执行,140行处的“return”表示返回主程序,也就是继续执行第30行。另外,EBASIC中的变量都是全局变量。

创建脚本文件并执行,会看到四个LED灯轮流亮灭,键盘输入q可退出。

实际生产生活中,懂计算机编程的人往往对各个具体的行业不太熟悉,而熟悉本行业的业内人士又对计算机编程不太熟悉,如果我们能有一种语法及其简单的语言,再配合一些本行业相关的语句,让业内人士也能轻松学会编程,应该也是一件有意义的事情。或许,EBASIC语言再加上行业语句,可以让不懂计算机的行业人士用上编程。

9.3 EBASIC解释器原理

EBASIC解释器是在一个叫做“ubasic”的开源软件基础上修改扩充而成。这里我简单描述一下解释器的实现原理。

解释器可以分成两大部分,第一部分完成词法分析,第二部分完成语法分析。

9.3.1 词法分析

把EBASIC脚本分割成一个一个的可识别的单词元素(Token),EBASIC语言支持的单词元素如下:

关键字类

print、if、then、else、for、to、next、gosub、return、in、out、delay、end。

单字符类

\n、,、+、-、*、/、%、&、|、(、)、<、>、=。

杂项类

文本结束标志0、数字常量、字符串、变量、无法识别。

9.3.2 语法分析

分析由单词元素构成的语句,我们这里就是分析并处理一行语句。(可以贴一点核心代码)

1. 变量赋值语句

使用有26个元素的数组来存储26个变量值,a对应数组的第一个元素,z对应最后一个。那怎样解析赋值语句呢?

赋值语句的第一个单词必定是一个变量,把变量名换算成数组序号,越过变量,再越过等号,计算等号后面的算数表达式的值,把这个值写入变量名对应的数组元素,赋值语句解析完成。

static void let_statement(void)

{

int var;

var = tokenizer_variable_num();

accept(TOKENIZER_VARIABLE);

accept(TOKENIZER_EQ);

ubasic_set_variable(var, expr());

while(tokenizer_token() != TOKENIZER_CR)

tokenizer_next();

accept(TOKENIZER_CR);

}

算数表达式涉及加、减、乘、除、取余、逻辑与、逻辑或和括号的排列组合,处理起来比较复杂,按照从左至右,先算乘除,后算加减,如遇括号,先算括号,往前累计的方式递归运算,具体实现方法,请读者直接阅读源代码。

static long expr(void) compact reentrant

{

long t1, t2;

long op;

t1 = term();

op = tokenizer_token();

while(op == TOKENIZER_PLUS ||

op == TOKENIZER_MINUS ||

op == TOKENIZER_AND ||

op == TOKENIZER_OR) {

tokenizer_next();

t2 = term();

switch(op) {

case TOKENIZER_PLUS:

t1 = t1 + t2;

break;

case TOKENIZER_MINUS:

t1 = t1 - t2;

break;

case TOKENIZER_AND:

t1 = t1 && t2;

break;

case TOKENIZER_OR:

t1 = t1 || t2;

break;

}

op = tokenizer_token();

}

return t1;

}

2. print语句

越过print单词,如果下一个单词是字符串,那么打印输出这个字符串,如果是逗号,那么输出空格,如果是算数表达式(可以只是一个变量或一个常数),那么计算出算数表达式的值并输出,继续分析后面的单词,直到本行语句结束。

static void

print_statement(void)

{

accept(TOKENIZER_PRINT);

do {

if(tokenizer_token() == TOKENIZER_STRING) {

tokenizer_string(string, sizeof(string));

printf("%s", string);

tokenizer_next();

} else if(tokenizer_token() == TOKENIZER_COMMA) {

printf(" ");

tokenizer_next();

} else if(tokenizer_token() == TOKENIZER_SEMICOLON) {

tokenizer_next();

} else if(tokenizer_token() == TOKENIZER_VARIABLE ||

tokenizer_token() == TOKENIZER_MINUS ||

tokenizer_token() == TOKENIZER_NUMBER) {

printf("%ld", expr());

} else {

break;

}

} while(tokenizer_token() != TOKENIZER_CR &&

tokenizer_token() != TOKENIZER_ENDOFINPUT);

printf("\r\n");

while(tokenizer_token() != TOKENIZER_CR)

tokenizer_next();

tokenizer_next();

}

3. if语句

越过if单词,下一个肯定是关系表达式,计算关系表达式的值(先计算关系符两侧的算数表达式的值,再计算最终的关系值,结果是0或1)。越过then,如果前面表达式的值为真,则继续解析then后的语句(递归解析),如果为假,则判断后面是否有else,有else则解析else后语句(递归解析),无else则本行解析结束。

static void if_statement(void) compact reentrant

{

int r;

accept(TOKENIZER_IF);

r = relation();

accept(TOKENIZER_THEN);

if(r) {

statement();

} else {

do {

tokenizer_next();

} while(tokenizer_token() != TOKENIZER_ELSE &&

tokenizer_token() != TOKENIZER_CR &&

tokenizer_token() != TOKENIZER_ENDOFINPUT);

if(tokenizer_token() == TOKENIZER_ELSE) {

tokenizer_next();

statement();

} else if(tokenizer_token() == TOKENIZER_CR) {

tokenizer_next();

}

}

}

4. gosub语句

越过gosub单词,取出gosub后面的跳转行号,把下一行的行号压入调用栈保存,然后跳转到跳转行号处。

static void gosub_statement(void)

{

int linenum;

accept(TOKENIZER_GOSUB);

linenum = tokenizer_num();

accept(TOKENIZER_NUMBER);

while(tokenizer_token() != TOKENIZER_CR)

tokenizer_next();

accept(TOKENIZER_CR);

if(gosub_stack_ptr < MAX_GOSUB_STACK_DEPTH) {

gosub_stack[gosub_stack_ptr] = tokenizer_num();

gosub_stack_ptr++;

jump_linenum(linenum);

} else {

}

}

跳转到具体行号的方式是:从前往后检索行号,如果行号匹配,就把这一行作为下一步解析的开始。

static void jump_linenum(int linenum)

{

tokenizer_init(program_ptr);

while(tokenizer_num() != linenum) {

do {

do {

tokenizer_next();

} while(tokenizer_token() != TOKENIZER_CR &&

tokenizer_token() != TOKENIZER_ENDOFINPUT);

if(tokenizer_token() == TOKENIZER_CR) {

tokenizer_next();

}

} while(tokenizer_token() != TOKENIZER_NUMBER);

}

}

5. return语句

从调用栈里弹出返回行号,跳转到返回行,跳转方法如前面所述。

static void

return_statement(void)

{

accept(TOKENIZER_RETURN);

if(gosub_stack_ptr > 0) {

gosub_stack_ptr--;

jump_linenum(gosub_stack[gosub_stack_ptr]);

} else {

}

}

6. for语句

越过for单词,计算初始值并写入循环变量,越过to单词,计算循环终止值,把for语句下一行的行号、循环变量名和循环终止值压入循环栈保存,本行语句解析完成。

static void for_statement(void)

{

int for_variable, to;

accept(TOKENIZER_FOR);

for_variable = tokenizer_variable_num();

accept(TOKENIZER_VARIABLE);

accept(TOKENIZER_EQ);

ubasic_set_variable(for_variable, expr());

accept(TOKENIZER_TO);

to = expr();

accept(TOKENIZER_CR);

if(for_stack_ptr < MAX_FOR_STACK_DEPTH) {

for_stack[for_stack_ptr].line_after_for = tokenizer_num();

for_stack[for_stack_ptr].for_variable = for_variable;

for_stack[for_stack_ptr].to = to;

for_stack_ptr++;

} else {

}

}

7. next语句

越过next单词,把循环变量值加1,然后判断是否小于等于循环栈里保存的循环终止值,如果小于等于,那么调转到循环栈里保存的行号(就是for语句的下一行),如果大于,那么把循环栈保存的本循环数据释放,执行next语句的下一行,也就是循环结束了。

static void

next_statement(void)

{

int var;

accept(TOKENIZER_NEXT);

var = tokenizer_variable_num();

accept(TOKENIZER_VARIABLE);

if(for_stack_ptr > 0 &&

var == for_stack[for_stack_ptr - 1].for_variable) {

ubasic_set_variable(var,

ubasic_get_variable(var) + 1);

if(ubasic_get_variable(var) <= for_stack[for_stack_ptr - 1].to) {

jump_linenum(for_stack[for_stack_ptr - 1].line_after_for);

} else {

for_stack_ptr--;

accept(TOKENIZER_CR);

}

} else {

accept(TOKENIZER_CR);

}

}

8. in语句

越过in单词,计算算数表达式值作为通道号,越过to单词,取出变量名,读取P1端口对应通道号的值,把值写入变量,本行解析完成。

static void in_statement(void)

{

int var, channel;

accept(TOKENIZER_IN);

channel = expr();

accept(TOKENIZER_TO);

var = tokenizer_variable_num();

accept(TOKENIZER_VARIABLE);

P1 = P1 | (1 << channel);

ubasic_set_variable(var, (P1 >> channel) & 1);

accept(TOKENIZER_CR);

}

9. out语句

越过out单词,计算算数表达式的值作为输出值,越过to单词,计算算数表达式的值作为通道号,把输出值输出到P1口对应的通道,本行解析结束。

static void out_statement(void)

{

int value, channel;

accept(TOKENIZER_OUT);

value = expr();

accept(TOKENIZER_TO);

channel = expr();

if(value == 1)

P1 = (1 << channel) | P1;

else P1 = ~(1 << channel) & P1;

accept(TOKENIZER_CR);

}

10. delay语句

越过delay单词,计算算数表达式的值作为延时的毫秒值,通过循环模拟经过的毫秒数(不准确,只是近似值),本行解析完成。

static void delay_statement(void)

{

int ms;

char t;

accept(TOKENIZER_DELAY);

ms = expr();

while(ms--)  //about

for(t = 0; t<65; t++);

accept(TOKENIZER_CR);

}

11. end语句

置位退出标志,结束解析,返回操作系统。

static void end_statement(void)

{

accept(TOKENIZER_END);

ended = 1;

}

12.关键字的判断

static void  statement(void)  compact reentrant

{

int token;

token = tokenizer_token();

switch(token) {

case TOKENIZER_PRINT:

print_statement();

break;

case TOKENIZER_IF:

if_statement();

break;

case TOKENIZER_GOTO:

goto_statement();

break;

case TOKENIZER_GOSUB:

gosub_statement();

break;

case TOKENIZER_RETURN:

return_statement();

break;

case TOKENIZER_FOR:

for_statement();

break;

case TOKENIZER_NEXT:

next_statement();

break;

case TOKENIZER_IN:

in_statement();

break;

case TOKENIZER_OUT:

out_statement();

break;

case TOKENIZER_DELAY:

delay_statement();

break;

case TOKENIZER_END:

end_statement();

break;

case TOKENIZER_LET:

accept(TOKENIZER_LET);

/* Fall through. */

case TOKENIZER_VARIABLE:

let_statement();

break;

default:

printf("error statement!\n");

}

}

9.3.3 关键字扩充

首先,在tokenizer.h头文件的枚举类型里添加关键字标识,比如“out”关键字添加“TOKENIZER_OUT”。其次,在tokenizer.c文件的“keywords[]”结构体里添加关键字对应的字符串,比如out关键字就添加{"out", TOKENIZER_OUT}。再次,完成关键字功能的函数实现,如前面所述的out_statement()。最后,在关键字判断函数statement()中添加实现的函数。这样,新添加的关键字就可以在EBASIC程序中使用了。

第9章 EBASIC脚本语言及应用相关推荐

  1. 第15章 使用DOM的脚本语言

    1. 大多数浏览器已经支持DOM1规范,虽然他们可能采取不同的实现方式. 2. 优雅脚本:把行为从Web开发的结构和表现中分开来,它涉及用于增强一个已经是基于语义和可访问性的标记结构,并能够在用户或者 ...

  2. 设计一种面向对象脚本语言

    有没有感觉设计一门语言实在是太有意思了,可以自定义语法规则,我的"地盘听我的". 脚本语言的功能 本书设计一门纯粹的面向对象脚本语言,任何语言都有个名词,这里给这个语言起个名字-- ...

  3. 脚本语言和工程语言_语言工程中有趣的事情

    脚本语言和工程语言 如果您阅读此博客,您将知道我坚信语言的力量. 所以,我当然有很大的偏见,但是我感觉语言工程社区正在增长,并且越来越有趣的东西正在涌现. 为此,我认为通过查看社区中正在发生的事情并列 ...

  4. linux脚本语言求累加和,Linux Shell脚本语言与数学表达式

    当你理解了Shell脚本,每当需要时都能流畅编写时,那种感觉很爽的.本章中,我们将教你用脚本语言进行比较复杂的数学运算. 让我们从斐波那契数列开始吧. 斐波那契数列,又称黄金分割数列,指的是这样一个数 ...

  5. php属于脚本,php是脚本语言吗

    PHP即"超文本预处理器",是一种通用开源脚本语言.PHP是在服务器端执行的脚本语言,与C语言类似,是常用的网站编程语言.PHP独特的语法混合了C.Java.Perl以及 PHP ...

  6. html支持的脚本语言,能不能让日志内容在支持html语言的同时支持一下脚本语言,拜托!拜托!...

    日志里经常引用一个网站的歌,他们的歌曲网址有部分常改变,比如说dm1.hting.com/ht//部分,没准哪天就变成dm1.hting.com/as//. 所以我想在内容模板里声明一个变量.比如说用 ...

  7. 视频教程-Kali Linux渗透测试全程课与脚本语言编程系列课程-渗透测试

    Kali Linux渗透测试全程课与脚本语言编程系列课程 本人有多年的服务器高级运维与开发经验,擅长计算机与服务器攻防及网络攻防技术!对网络安全领域有持续的关注和研究! 林晓炜 ¥899.00 立即订 ...

  8. JavaScript基础知识与脚本语言总结

    1 Aptana插件安装 1.Aptana插件安装 <1>Aptana是一个非常强大,开源,JavaScript-focused的AJAX开发IDE. <2>它的特点包括: J ...

  9. 学习笔记之编程达到一个高的境界就是自制脚本语言(图)

    学习笔记之编程达到一个高的境界就是自制脚本语言(图) 编程达到一个高的境界就是自制脚本语言,通过这可以精通编程里面的高深的技术,如编译原理.语言处理器.编译器与解释器,这些都是代表一个程序员实力的技术 ...

最新文章

  1. 【蓝桥java】进制与整除之天平秤重
  2. python可以做什么有趣的东西-您用python做过什么有趣的事?(什么事python)
  3. const、static、const staic理解
  4. java强引用软引用深刻理解_Java-强引用、软引用、弱引用、虚引用
  5. HDU 4990 Ordered Subsequence --数据结构优化DP
  6. 【转】刨根究底字符编码之零——前言
  7. 常见的getchar 与EOF的问题
  8. hdu4521 小明系列的问题——小明序列(LIS变种 (段树+单点更新解决方案))
  9. PowerDesigner(六)-物理数据模型(PDM逆向工程)
  10. 设为首页+加入收藏夹+打印网页
  11. Docker学习总结(14)——从代码到上线, 云端Docker化持续交付实践
  12. Introduction to SQL Server 2005 XML
  13. Leetcode 1218.最长定差子序列
  14. warcraft3Viewer模型导入到3dsmax到Unity
  15. 微信投票python脚本_微信刷票Python脚本教程
  16. 经典企业文化书籍推荐,有了这6本书企业文化落地不再是难事
  17. 命令行编译工具NMAKE
  18. 【Matlab】使用Matlab运行Windows命令行命令+实例
  19. [APK签名] jarsigner APK V1签名
  20. java blackjack card game_Java BlackJack Game Ace值

热门文章

  1. 实验2.1:交换机VLAN配置
  2. 已知: 每个飞机只有一个油箱, 飞机之间可以相互加油(注意是相互,没有加油机) 一箱油可供一架飞机绕地球飞半圈, 问题:为使至少一架飞机绕地球一圈回到起飞时的飞机场,至少需要出动几架飞机?(所有飞机从
  3. 开源矿工的备用矿池设计与实现
  4. VS配置使用NUnits
  5. 短距离无线电测向的基本方法和基本技术,可归纳为下列几个方面:
  6. JAVAEE用户管理系统
  7. 《智慧书》格言11¬20
  8. 荣耀笔记本Linux刷win10,笔者亲自带你体验,荣耀笔记本该怎么刷Win10系统?
  9. 微信小程序 滑块验证小demo
  10. 新的一年,莫把青山当柴烧