第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语言基本语法

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

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

l 支持变量赋值语句;

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

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

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

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

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

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

l 支持子程序调用;

l 支持延时语句。

2. EBASIC语言扩充语句

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

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

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  1100之和执行情况

例子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 一筐鸡蛋,11个拿能拿完,22个拿剩1,33个拿剩1,44个拿剩1,55个拿剩1,66个拿剩1,77个拿能拿完,这筐鸡蛋最少有多少个?

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语言支持的单词元素如下:

关键字类

printifthenelsefortonextgosubreturninoutdelayend

单字符类

\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单词,下一个肯定是关系表达式,计算关系表达式的值(先计算关系符两侧的算数表达式的值,再计算最终的关系值,结果是01)。越过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程序中使用了。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值