🔢 第28课:BASIC运算与IO

📖 本课目标

扩展Tiny BASIC解释器,添加完整的运算能力(比较、逻辑、内置函数)和IO系统(INPUT、键盘读取、VGA输出)。让BASIC真正成为8位电脑的"操作系统"——不仅能做数学计算,还能与用户交互、在屏幕上画图。

🧠 比较运算与逻辑运算

上一课的表达式求值器只支持算术运算。要让IF-THEN生效,我们需要比较运算符和逻辑运算符:

运算符优先级(从低到高) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 优先级1: OR (逻辑或) 优先级2: AND (逻辑与) 优先级3: NOT (逻辑非) 优先级4: = < > <= >= <> (比较) 优先级5: + - (加减) 优先级6: * / (乘除) 优先级7: - (一元) (取负) 优先级8: () (括号,最高) 比较运算返回值: 真 = -1 (所有位为1, 即 $FFFF) 假 = 0 ($0000) 逻辑运算: AND: 位与 (非0为真) OR: 位或 (非0为真) NOT: 位取反 示例: IF A > 5 AND B < 10 THEN 100 等价于: IF (A>5) AND (B<10) THEN 100

扩展的表达式求值器

def _comparison(self, tokens, pos):
    """比较运算层"""
    left, pos = self._expr(tokens, pos)
    while pos < len(tokens) and \
          tokens[pos][0] == 'OP' and \
          tokens[pos][1] in ('=','<','>','<=','>=','<>'):
        op = tokens[pos][1]
        pos += 1
        right, pos = self._expr(tokens, pos)
        if op == '=':    result = (left == right)
        elif op == '<':  result = (left < right)
        elif op == '>':  result = (left > right)
        elif op == '<=': result = (left <= right)
        elif op == '>=': result = (left >= right)
        elif op == '<>': result = (left != right)
        left = -1 if result else 0
    return left, pos

def _and_op(self, tokens, pos):
    """AND运算层"""
    left, pos = self._comparison(tokens, pos)
    while pos < len(tokens) and \
          tokens[pos] == ('KEYWORD', 'AND'):
        pos += 1
        right, pos = self._comparison(tokens, pos)
        left = left & right  # 位与
    return left, pos

def _or_op(self, tokens, pos):
    """OR运算层(最顶层)"""
    left, pos = self._and_op(tokens, pos)
    while pos < len(tokens) and \
          tokens[pos] == ('KEYWORD', 'OR'):
        pos += 1
        right, pos = self._and_op(tokens, pos)
        left = left | right  # 位或
    return left, pos

📋 内置函数

Tiny BASIC提供几个关键的内置函数:

函数语法功能实现说明
RNDRND(X)返回0到X-1的随机整数线性同余伪随机数生成器
ABSABS(X)绝对值负数取反
SGNSGN(X)符号函数(-1/0/1)判断正负零
PEEKPEEK(addr)读取内存地址直接访问8位内存空间
LENLEN(A$)字符串长度未来扩展

PEEK/POKE:BASIC直接访问硬件

PEEK和POKE是8位BASIC最强大的特性——它们让BASIC程序直接读写内存映射I/O,控制硬件:

; PEEK函数实现 - 读取内存地址
; 在8位汇编中的实现

PEEK_FUNC:
    ; 参数: 16位地址在$24/$25
    ; 返回: 8位值在A寄存器
    LDX #$00
    LDA ($24,X)      ; 读取地址处的值
    RTS

; POKE语句实现 - 写入内存地址
; POKE addr, value

POKE_STMT:
    JSR PARSE_ADDR   ; 解析地址到$24/$25
    JSR PARSE_COMMA  ; 跳过逗号
    JSR EXPR         ; 计算值 → A
    LDX #$00
    STA ($24,X)      ; 写入内存
    RTS

BASIC硬件控制示例

; 用POKE控制VGA屏幕颜色
10 REM 改变屏幕背景色
20 POKE &H7800, 65   : REM 第一个字符='A'
30 POKE &H7801, &HF1 : REM 属性=白字蓝底
40 END

; 用PEEK读取键盘
10 REM 等待按键
20 IF PEEK(&HFF03) AND 1 = 0 THEN 20
30 LET K = PEEK(&HFF02)
40 PRINT "KEY CODE: "; K
50 GOTO 20

; 声音输出 - 通过定时器产生频率
10 REM 播放音阶
20 FOR I = 1 TO 8
30 READ F
40 POKE &HFF04, F AND 255
50 POKE &HFF05, F / 256
60 POKE &HFF06, 11
70 FOR D = 1 TO 500 : NEXT D
80 NEXT I
90 DATA 262,294,330,349,392,440,494,523

🔧 INPUT语句实现

INPUT让用户在程序运行时输入数据:

def _exec_input(self, tokens):
    """INPUT语句执行"""
    i = 0
    # 可选的提示字符串
    if tokens[i][0] == 'STRING':
        print(tokens[i][1], end='')
        i += 1
        if i < len(tokens) and tokens[i] == ('OP', ';'):
            i += 1
    else:
        print("?", end=' ')

    # 读取变量名列表
    while i < len(tokens):
        if tokens[i][0] == 'VAR':
            var_name = tokens[i][1]
            value = input()  # 在8位上替换为键盘读取
            try:
                self.variables[var_name] = int(value)
            except ValueError:
                print("?REDO FROM START")
                continue
        i += 1

8位汇编版INPUT

; INPUT语句 - 8位汇编实现
INPUT_STMT:
    ; 显示提示符"? "
    LDA #'?'
    JSR PUTCHAR
    LDA #' '
    JSR PUTCHAR

    ; 读取一行输入到缓冲区
    JSR READ_LINE

    ; 解析输入为数字
    JSR PARSE_NUMBER   ; 结果在$24/$25 (16位)

    ; 存入变量
    ; 变量地址 = VAR_BASE + var_index * 2
    LDA $24
    STA $0200,X        ; 存入变量表
    LDA $25
    STA $0201,X
    RTS

📋 完整的BASIC程序示例

示例1:猜数字游戏

10 REM 猜数字游戏
20 LET N = RND(100) + 1
30 PRINT "I'm thinking of a number 1-100"
40 INPUT "Your guess? "; G
50 IF G = N THEN 90
60 IF G < N THEN PRINT "Too low!"
70 IF G > N THEN PRINT "Too high!"
80 GOTO 40
90 PRINT "You got it!"
100 END

示例2:乘法口诀表

10 REM 9×9乘法口诀表
20 FOR I = 1 TO 9
30 FOR J = 1 TO I
40 LET P = I * J
50 IF P < 10 THEN PRINT " ";
60 PRINT P; " ";
70 NEXT J
80 PRINT
90 NEXT I
100 END

示例3:屏幕画图(POKE VGA缓冲)

10 REM 在VGA屏幕上画对角线
20 FOR I = 0 TO 24
30 LET ADDR = &H7800 + I * 80 + I
40 POKE ADDR, 42   : REM '*' 的ASCII码
50 POKE ADDR+1, &HE4 : REM 黄色亮红底
60 NEXT I
70 END

示例4:音乐播放器

10 REM 简易音乐播放器
20 READ N
30 IF N = 0 THEN END
40 POKE &HFF04, N AND 255
50 POKE &HFF05, N / 256
60 POKE &HFF06, 11    : REM 使能+循环+/8预分频
70 FOR D = 1 TO 300 : NEXT D
80 POKE &HFF06, 0     : REM 关闭定时器
90 GOTO 20
100 DATA 262,262,294,330,330,294,262,262
110 DATA 247,220,220,247,262,262,247,247
120 DATA 0

🎯 练习

练习1:字符串变量

扩展BASIC支持字符串变量(A$)和字符串操作:字符串赋值(LET A$="HELLO")、连接(A$+B$)、比较(A$=B$)、LEN(A$)。在8位实现中,字符串存储在专用的字符串池中,变量表存储字符串指针和长度。

练习2:ON...GOTO/GOSUB

实现ON X GOTO 100,200,300语句:根据X的值跳转到不同的行号。这是BASIC的"switch"语句。在8位汇编中,行号列表存储为连续的16位地址,用X作为变址。

练习3:DATA/READ/RESTORE

实现数据语句:DATA定义常量列表,READ逐个读取,RESTORE重置读取指针。这是BASIC特有的初始化机制——程序中内嵌数据,运行时读取。注意DATA语句不执行,只在程序区中占位。

练习4:错误处理

为解释器添加完善的错误处理:语法错误(?SYNTAX ERROR)、除零错误(?DIVISION BY ZERO)、行号不存在(?UNDEF'D STATEMENT)、栈溢出(?OUT OF MEMORY)。每个错误显示错误类型和出错的行号。

🏆 成就解锁:交互设计师

你扩展了BASIC的运算和IO能力!这包括:

BASIC现在可以做真正的交互式程序了——游戏、工具、甚至简单的应用!