第9章 EBASIC脚本语言及应用
到目前为止,我们设计并实现了一个完整的计算机系统,包括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脚本语言及应用相关推荐
- 第15章 使用DOM的脚本语言
1. 大多数浏览器已经支持DOM1规范,虽然他们可能采取不同的实现方式. 2. 优雅脚本:把行为从Web开发的结构和表现中分开来,它涉及用于增强一个已经是基于语义和可访问性的标记结构,并能够在用户或者 ...
- 设计一种面向对象脚本语言
有没有感觉设计一门语言实在是太有意思了,可以自定义语法规则,我的"地盘听我的". 脚本语言的功能 本书设计一门纯粹的面向对象脚本语言,任何语言都有个名词,这里给这个语言起个名字-- ...
- 脚本语言和工程语言_语言工程中有趣的事情
脚本语言和工程语言 如果您阅读此博客,您将知道我坚信语言的力量. 所以,我当然有很大的偏见,但是我感觉语言工程社区正在增长,并且越来越有趣的东西正在涌现. 为此,我认为通过查看社区中正在发生的事情并列 ...
- linux脚本语言求累加和,Linux Shell脚本语言与数学表达式
当你理解了Shell脚本,每当需要时都能流畅编写时,那种感觉很爽的.本章中,我们将教你用脚本语言进行比较复杂的数学运算. 让我们从斐波那契数列开始吧. 斐波那契数列,又称黄金分割数列,指的是这样一个数 ...
- php属于脚本,php是脚本语言吗
PHP即"超文本预处理器",是一种通用开源脚本语言.PHP是在服务器端执行的脚本语言,与C语言类似,是常用的网站编程语言.PHP独特的语法混合了C.Java.Perl以及 PHP ...
- html支持的脚本语言,能不能让日志内容在支持html语言的同时支持一下脚本语言,拜托!拜托!...
日志里经常引用一个网站的歌,他们的歌曲网址有部分常改变,比如说dm1.hting.com/ht//部分,没准哪天就变成dm1.hting.com/as//. 所以我想在内容模板里声明一个变量.比如说用 ...
- 视频教程-Kali Linux渗透测试全程课与脚本语言编程系列课程-渗透测试
Kali Linux渗透测试全程课与脚本语言编程系列课程 本人有多年的服务器高级运维与开发经验,擅长计算机与服务器攻防及网络攻防技术!对网络安全领域有持续的关注和研究! 林晓炜 ¥899.00 立即订 ...
- JavaScript基础知识与脚本语言总结
1 Aptana插件安装 1.Aptana插件安装 <1>Aptana是一个非常强大,开源,JavaScript-focused的AJAX开发IDE. <2>它的特点包括: J ...
- 学习笔记之编程达到一个高的境界就是自制脚本语言(图)
学习笔记之编程达到一个高的境界就是自制脚本语言(图) 编程达到一个高的境界就是自制脚本语言,通过这可以精通编程里面的高深的技术,如编译原理.语言处理器.编译器与解释器,这些都是代表一个程序员实力的技术 ...
最新文章
- 【蓝桥java】进制与整除之天平秤重
- python可以做什么有趣的东西-您用python做过什么有趣的事?(什么事python)
- const、static、const staic理解
- java强引用软引用深刻理解_Java-强引用、软引用、弱引用、虚引用
- HDU 4990 Ordered Subsequence --数据结构优化DP
- 【转】刨根究底字符编码之零——前言
- 常见的getchar 与EOF的问题
- hdu4521 小明系列的问题——小明序列(LIS变种 (段树+单点更新解决方案))
- PowerDesigner(六)-物理数据模型(PDM逆向工程)
- 设为首页+加入收藏夹+打印网页
- Docker学习总结(14)——从代码到上线, 云端Docker化持续交付实践
- Introduction to SQL Server 2005 XML
- Leetcode 1218.最长定差子序列
- warcraft3Viewer模型导入到3dsmax到Unity
- 微信投票python脚本_微信刷票Python脚本教程
- 经典企业文化书籍推荐,有了这6本书企业文化落地不再是难事
- 命令行编译工具NMAKE
- 【Matlab】使用Matlab运行Windows命令行命令+实例
- [APK签名] jarsigner APK V1签名
- java blackjack card game_Java BlackJack Game Ace值