返回首页

FBASIC 教程

FBASIC 教程 — 从 BASIC 语句到 6502 汇编

FBASIC 是一个 Family BASIC 风格的 BASIC 方言,有三个实现版本。
本教程介绍语言本身,并深入讲解 IF 和 FOR 结构对应的 6502 汇编实现——这是通往 NES 版解释器的必经之路。


目录

  1. FBASIC 简介
  2. 变量与赋值
  3. 算术表达式与优先级
  4. 逻辑表达式与 IF 结构
  5. 循环与 FOR 结构
  6. 函数
  7. 数组
  8. 完整示例程序
  9. 语句与版本支持总表

1. FBASIC 简介

版本 语言 平台 状态
JS 版 JavaScript 浏览器(本站) ✅ 功能最全
C 版 C99 PC 终端 ✅ 完整表达式求值
NES 版 6502 汇编 红白机 ROM 📐 设计中

程序格式:每行以行号开头,按行号升序执行。

1
2
10 PRINT "HELLO"
20 GOTO 10

直接命令(不写行号立即执行):

命令 作用
RUN 运行程序
LIST 列出程序
NEW 清空程序
CLS 清屏

2. 变量与赋值

2.1 数值变量

1
2
3
4
10 A = 10            ; 赋值 (LET 可省略)
20 LET B = 25 ; 显式 LET, 效果相同
30 C = A ; 变量间赋值
40 PRINT A; B; C ; 输出: 10 25 10

变量名:字母开头,可含数字(如 COUNT、X1)。未赋值的数值变量默认为 0。

2.2 字符串变量

变量名以 $ 结尾即为字符串类型:

1
2
3
10 N$ = "FAMILY BASIC"
20 PRINT N$ ; 输出: FAMILY BASIC
30 PRINT LEN(N$) ; 输出: 12

未赋值的字符串变量默认为空串 ""。

2.3 INPUT 输入赋值

1
2
10 INPUT "YOUR NAME"; N$
20 PRINT "HELLO, "; N$

2.4 对应的 6502 汇编

A = 10 在 6502 上就是两条指令:

1
2
3
4
VAR_A = $10          ; 变量A = 零页地址 $10

lda #$0A ; 累加器 = 10 (立即寻址)
sta VAR_A ; 内存$10 = 累加器 (零页寻址) → A = 10 完成

NES 版变量表设计:每个变量 3 字节 [变量名][值高][值低],16 位有符号整数,26 个变量(A-Z)放在 $0600。赋值由 var_set 例程完成。


3. 算术表达式与优先级

3.1 运算符与优先级

优先级 运算符 含义 示例
1(最高) ( ) 括号 (2+3)*4 = 20
2 -(一元) 负号 -5 + 3 = -2
3 * / 乘、除 2+3*4 = 14
4(最低) + - 加、减 10-4-3 = 3

同级运算从左到右(左结合):

1
2
3
4
10 PRINT 2 + 3 * 4       ; 14  (先算 3*4)
20 PRINT (2 + 3) * 4 ; 20 (括号优先)
30 PRINT 20 / 5 / 2 ; 2 (从左到右: 20/5=4, 4/2=2)
40 PRINT 10 - 4 - 3 ; 3 (从左到右: 10-4=6, 6-3=3)

3.2 变量参与运算

1
2
3
4
10 A = 10
20 B = A * 2 + 5 ; B = 25
30 C = (A + B) / 7 ; C = 5
40 PRINT B; C

3.3 对应的 6502 汇编

6502 没有乘除指令,加减用 ADC/SBC,乘除用移位实现:

1
B = 2 + 3 * 4            ; 结果 14
1
2
3
4
5
6
7
8
9
10
11
; --- 按优先级求值: 先算 3*4, 再加 2 ---
; 第1步: 3 * 4 = 12 (乘法用移位: ×4 = 左移2次)
lda #$03 ; 累加器A = 3 (立即寻址, 装入被乘数)
asl ; A 左移1位 = 6 (×2)
asl ; A 左移1位 = 12 (×4) → 3*4 完成
sta tmp1 ; 临时变量 tmp1 = 12 (保存中间结果)
; 第2步: 2 + 12 = 14
lda #$02 ; 累加器A = 2 (装入被加数)
clc ; 清进位标志 (6502加法带进位, 加法前必须清0!)
adc tmp1 ; A = A + tmp1 + 0 = 2 + 12 = 14
sta VAR_B ; 变量B = 14 (存回结果)

关键点:高级语言的”优先级”在汇编层体现为求值顺序——解释器(或编译器)按优先级规则安排指令的先后次序,用临时变量(tmp1)保存中间结果。

版本说明:JS 版与 C 版均完整支持算术表达式(含括号、一元负号、\ 整除、MOD 取余),内置函数可出现在任意表达式中(如 LEN(A$) * 2、CHR$(65) + "B");NES 版按设计将实现 16 位整数的 + - * /。


4. 逻辑表达式与 IF 结构

4.1 比较运算符

运算符 含义 示例
= 等于 A = 5
<> 不等于 A <> 5
< 小于 A < 5
> 大于 A > 5
<= 小于等于 A <= 5
>= 大于等于 A >= 5

4.2 IF/THEN 语法

1
2
IF 条件 THEN 语句
IF 条件 THEN 行号 ; 条件成立则跳转到指定行
1
2
3
4
5
6
7
10 A = 10
20 IF A > 5 THEN PRINT "BIG" ; 输出: BIG
30 IF A = 5 THEN PRINT "FIVE" ; 不成立, 跳过
40 IF A < 20 THEN 60 ; 成立 → 跳到行60
50 PRINT "SKIPPED"
60 PRINT "DONE" ; 输出: DONE
70 END

字符串也能比较(按字典序):

1
2
10 N$ = "APPLE"
20 IF N$ = "APPLE" THEN PRINT "MATCH"

注:AND/OR/NOT 逻辑连接符当前版本未实现(规划中)。复合条件可用嵌套 IF 代替:

1
10 IF A > 5 THEN IF A < 10 THEN PRINT "IN RANGE"

4.3 ★ IF 结构的 6502 汇编实现

BASIC 程序:

1
2
10 IF A > 5 THEN PRINT "BIG"
20 PRINT "DONE"

6502 汇编(条件跳转 = 比较指令 + 分支指令):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; --- 行10: IF A > 5 THEN PRINT "BIG" ---
lda VAR_A ; ① 取变量A的值到累加器 (零页寻址)
cmp #6 ; ② A - 6, 只设标志不存结果 (A>5 等价于 A>=6)
bcc @line20 ; ③ 进位C=0 说明 A<6 → 条件假, 跳过THEN部分

; ④ 条件真: 执行 THEN 后面的语句 PRINT "BIG"
lda #<msg_big ; A = 字符串地址低字节 (#< 取16位地址的低8位)
ldy #>msg_big ; Y = 字符串地址高字节 (#> 取16位地址的高8位)
jsr print_string ; 调用打印例程 (约定: A/Y = 字符串地址)

; --- 行20: PRINT "DONE" ---
@line20: ; ⑤ 条件假从这里继续 (顺序执行下一行)
lda #<msg_done ; A = "DONE"字符串地址低字节
ldy #>msg_done ; Y = "DONE"字符串地址高字节
jsr print_string ; 打印 DONE

执行流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────┐
│ lda VAR_A │ 取A
└──────┬──────┘
┌──────▼──────┐
│ cmp #6 │ A - 6 (只设标志, 不存结果)
└──────┬──────┘
│
进位C=0?──── 是 ──► @line20 (跳过THEN)
│
否 (A>=6)
┌──────▼──────┐
│ PRINT "BIG" │ THEN 部分
└──────┬──────┘
┌──────▼──────┐
│PRINT "DONE" │ 行20
└─────────────┘

六种比较运算的汇编对照表(无符号数):

BASIC 条件 6502 实现 分支指令 跳转含义
A = B lda A / cmp B beq 相等跳
A <> B lda A / cmp B bne 不等跳
A < B lda A / cmp B bcc 小于跳
A >= B lda A / cmp B bcs 大于等于跳
A > B lda A / cmp #B+1 bcs 大于跳(技巧:比B+1)
A <= B lda A / cmp #B+1 bcc 小于等于跳

原理:CMP 执行减法 A - B 但不保存结果,只设置标志位。BCC(进位清零跳转)= 有借位 = A < B;BEQ(零标志)= A = B。


5. 循环与 FOR 结构

5.1 FOR/NEXT 语法

1
2
3
FOR 变量 = 初值 TO 终值 [STEP 步长]
...循环体...
NEXT 变量
1
2
3
4
10 FOR I = 1 TO 5
20 PRINT "I="; I
30 NEXT I
40 PRINT "DONE"

输出:

1
2
3
4
5
6
I=1
I=2
I=3
I=4
I=5
DONE

5.2 STEP 步长

1
2
3
10 FOR I = 10 TO 1 STEP -2     ; 倒数, 步长-2
20 PRINT I;
30 NEXT I

输出:10 8 6 4 2

步长为正:变量 ≤ 终值时继续;步长为负:变量 ≥ 终值时继续。

5.3 循环嵌套

1
2
3
4
5
10 FOR I = 1 TO 3
20 FOR J = 1 TO 2
30 PRINT I; ","; J
40 NEXT J
50 NEXT I

输出:1,1 1,2 2,1 2,2 3,1 3,2

5.4 GOTO 无限循环

1
2
3
10 C = C + 1
20 PRINT C;
30 GOTO 10

5.5 ★ FOR 结构的 6502 汇编实现

BASIC 程序:

1
2
3
4
10 FOR I = 1 TO 10
20 PRINT I
30 NEXT I
40 PRINT "DONE"

6502 汇编(初始化 + 循环体 + 步进判断三段式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; --- 行10: FOR I = 1 TO 10 (初始化段) ---
lda #$01 ; ① 累加器A = 1 (FOR的初值, 立即寻址)
sta VAR_I ; 变量I = 1 (存入循环变量)
; (解释器还会把 终值10/步长1/循环体地址 压入FOR栈)

@loop: ; ② 循环体入口标号 (NEXT 判断后跳回这里)
; --- 行20: PRINT I (循环体) ---
lda VAR_I ; A = 循环变量I的当前值
jsr print_number ; 调用打印例程, 显示 I

; --- 行30: NEXT I (步进+判断段) ---
inc VAR_I ; ③ 内存VAR_I 直接+1 (I = I + 1, 步长1)
lda VAR_I ; A = 步进后的新 I 值
cmp #$0B ; ④ A - 11, 只设标志 (I>10 等价于 I>=11)
bcc @loop ; ⑤ 进位=0 说明 I<11 (即I<=10) → 跳回②继续

; --- 行40: PRINT "DONE" (循环出口) ---
lda #<msg_done ; A = "DONE"字符串地址低字节
ldy #>msg_done ; Y = "DONE"字符串地址高字节
jsr print_string ; 打印 DONE, 循环结束

执行流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    ┌──────────────┐
│ I = 1 (初始化)│
└──────┬───────┘
┌──────▼───────┐
┌──►│ 循环体 │ PRINT I
│ └──────┬───────┘
│ ┌──────▼───────┐
│ │ I = I + 1 │ inc (步进)
│ └──────┬───────┘
│ ┌──────▼───────┐
│ │ I <= 10 ? │ cmp + bcc
│ └──┬───────┬───┘
│ 是 │ │ 否
└──────┘ ▼
┌──────────────┐
│ 循环出口 │ PRINT "DONE"
└──────────────┘

负步长版本(FOR I = 10 TO 1 STEP -1):

1
2
3
4
5
6
7
8
9
    lda #$0A            ; 累加器A = 10 (FOR初值, 立即寻址)
sta VAR_I ; 变量I = 10
@loop: ; 循环体入口
; ...循环体... ; (每轮执行的语句)
dec VAR_I ; 内存VAR_I 直接-1 (负步长-1用dec, 与inc对应)
lda VAR_I ; A = 步进后的新 I 值
cmp #$01 ; A - 1, 只设标志 (与终值1比较)
bcs @loop ; 进位=1 说明 I>=1 → 未超终值, 跳回继续
; (负步长的继续条件是>=, 与正步长相反)

5.6 解释器的 FOR 栈机制

FBASIC 解释器(JS/C 版)用栈保存每个活跃循环的状态,支持嵌套:

1
2
3
4
5
6
7
8
9
10
执行 FOR 时压栈:
┌─────────────────────────────┐
│ 变量名 │ 终值 │ 步长 │ 返回行号 │ ← 一个栈帧
└─────────────────────────────┘

执行 NEXT 时:
1. 取栈顶帧
2. 变量 += 步长
3. 未超终值 → 跳回返回行号 (继续循环)
4. 超过终值 → 弹栈 (循环结束)

NES 版设计:FOR 栈帧 7 字节 [变量名][终值高][终值低][步长高][步长低][返回地址高][返回地址低],最多 4 层嵌套,放在 $0700-$077F。

为什么需要栈:嵌套循环 FOR I ... FOR J ... NEXT J ... NEXT I 时,内层 NEXT 只操作栈顶的 J 帧,外层 I 帧在下面等着——后进先出正好匹配循环的嵌套结构。


6. 函数

所有内置函数都可以用在任意表达式中,支持嵌套调用(如 LEN(STR$(123))、CHR$(65) + "B"、INT(RND * 6) + 1)。

6.1 内置数值函数

函数 作用 示例 结果
ABS(x) 绝对值 ABS(-5) 5
INT(x) 向下取整 INT(3.7) 3
RND 随机数 0~1 INT(RND * 6) + 1 1~6 随机
SQR(x) 平方根 SQR(16) 4
SGN(x) 符号函数 SGN(-5) -1
SIN(x) 正弦 SIN(0) 0
COS(x) 余弦 COS(0) 1
TAN(x) 正切 TAN(0) 0
ATN(x) 反正切 ATN(1) 0.785…
LOG(x) 自然对数 LOG(1) 0
EXP(x) e 的 x 次方 EXP(0) 1
ASC(s) 首字符 ASCII 码 ASC("A") 65
LEN(s) 字符串长度 LEN("ABC") 3
VAL(s) 字符串转数值 VAL("42") 42
INSTR(a,b) 查找子串位置(从 1 起) INSTR("HELLO","LL") 3

6.2 内置字符串函数

函数 作用 示例 结果
CHR$(n) ASCII 码转字符 CHR$(65) “A”
STR$(n) 数值转字符串 STR$(42) “42”
LEFT$(s,n) 左取 n 字符 LEFT$("HELLO",2) “HE”
RIGHT$(s,n) 右取 n 字符 RIGHT$("HELLO",2) “LO”
MID$(s,start[,len]) 中间取子串 MID$("HELLO",2,3) “ELL”
STRING$(n,c) 字符重复 n 次 STRING$(5,42) “*****”
SPACE$(n) n 个空格 "[" + SPACE$(3) + "]" “[ ]”
HEX$(n) 转十六进制字符串 HEX$(255) “FF”
OCT$(n) 转八进制字符串 OCT$(8) “10”

6.3 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
10 S$ = "FAMILY BASIC"
20 PRINT LEN(S$) ; 12
30 PRINT LEFT$(S$, 6) ; FAMILY
40 PRINT RIGHT$(S$, 5) ; BASIC
50 PRINT MID$(S$, 8, 3) ; BAS
60 PRINT ASC("A") ; 65
70 PRINT CHR$(66) ; B
80 PRINT ABS(-7) + INT(2.9) ; 9
90 PRINT CHR$(65) + "B" ; AB (函数用于拼接表达式)
100 PRINT LEN(S$) * 2 ; 24 (函数用于算术表达式)
110 PRINT STRING$(5, "*") ; *****
120 PRINT INSTR(S$, "BASIC") ; 8

6.4 用户子程序:GOSUB/RETURN

BASIC 没有 def 函数,用 GOSUB(跳转子程序)+ RETURN(返回)实现可复用代码块:

1
2
3
4
5
10 GOSUB 100              ; 调用子程序 (行100)
20 PRINT "BACK AT 20"
30 END
100 PRINT "IN SUB" ; 子程序开始
110 RETURN ; 返回调用处的下一行 (行20)

输出:

1
2
IN SUB
BACK AT 20

6.5 对应的 6502 汇编

GOSUB/RETURN 直接对应 6502 的 JSR/RTS:

1
2
3
4
5
100 GOSUB 500
110 PRINT "BACK"
...
500 PRINT "SUB"
510 RETURN
1
2
3
4
5
6
7
8
9
10
11
    jsr sub_500         ; GOSUB 500: 返回地址(行110的地址)压入硬件栈, 再跳转
; --- 行110: PRINT "BACK" --- ← RTS 后回到这里继续
lda #<msg_back ; A = "BACK"字符串地址低字节
ldy #>msg_back ; Y = "BACK"字符串地址高字节
jsr print_string ; 打印 BACK
sub_500: ; 子程序入口 (对应 BASIC 行500)
; --- 行500: PRINT "SUB" ---
lda #<msg_sub ; A = "SUB"字符串地址低字节
ldy #>msg_sub ; Y = "SUB"字符串地址高字节
jsr print_string ; 打印 SUB
rts ; RETURN: 从硬件栈弹出返回地址 → 跳回行110

原理:JSR 把”下一条指令的地址”压入 $0100-$01FF 的硬件栈再跳转;RTS 弹出该地址返回。这与 BASIC 解释器用软件栈(gosub_stack)保存”下一行行号”是同一思想——调用者位置入栈,返回时出栈。

内置函数在 NES 版的实现则是查表 + 跳转:函数名 → 函数表索引 → jmp (函数表) 间接跳转到对应例程。


7. 数组

状态:数组(DIM 语句)JS 版已支持(现代编辑器模式与真实 FBASIC 模式均可运行),C 版与 NES 版规划中。可前往 冒泡排序可视化 或 选择排序可视化 页面观看数组排序的动画演示。

7.1 DIM 声明与使用

1
2
3
4
5
10 DIM A(5)              ; 声明数组 A, 下标 0~5
20 FOR I = 1 TO 5
30 A(I) = I * 10 ; 写数组元素
40 NEXT I
50 PRINT A(3) ; 读数组元素 → 30

7.2 数组求和示例

1
2
3
4
5
6
7
10 DIM S(4)
20 S(1) = 95: S(2) = 88: S(3) = 76: S(4) = 92
30 T = 0
40 FOR I = 1 TO 4
50 T = T + S(I)
60 NEXT I
70 PRINT "AVG ="; T / 4 ; 87.75

7.3 对应的 6502 汇编

数组的本质是连续内存 + 下标计算,6502 用变址寻址直接支持:

1
2
A(I) = I * 10        ; 写数组
X = A(3) ; 读数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; --- 写: A(I) = I * 10 (8位元素版) ---
lda VAR_I ; A = 下标I的值 (零页寻址取变量)
asl ; A 左移 = I×2
sta tmp1 ; tmp1 = I×2 (暂存, 后面还要用)
asl ; A 左移 = I×4
asl ; A 左移 = I×8
clc ; 清进位 (加法前必做)
adc tmp1 ; A = I×8 + I×2 = I×10 (乘10 = ×8+×2 分解)
ldx VAR_I ; X = 下标 I (变址寄存器装下标)
sta ARRAY,X ; 内存[ARRAY+I] = A ← 变址寻址一步到位!

; --- 读: X = A(3) ---
ldx #$03 ; X = 下标 3 (立即寻址)
lda ARRAY,X ; A = 内存[ARRAY+3] = A[3] (变址读)
sta VAR_X ; 变量X = A[3] (存结果)

关键指令 sta ARRAY,X(绝对变址寻址):有效地址 = 数组基址 + X 寄存器。CPU 硬件完成”基址+下标”的加法,这正是数组下标访问的汇编本质。

NES 版数组设计:数组区放在变量表之后,元素为 16 位整数(2 字节),访问时下标 ×2 后变址:

1
2
3
4
5
6
7
8
; 16 位数组: A(I) 的地址 = ARRAY_BASE + I*2
lda VAR_I ; A = 下标 I
asl ; A 左移 = I×2 (16位元素占2字节)
tax ; X = I×2 (下标换算成字节偏移)
lda ARRAY_BASE,X ; A = A[I]的低字节 (基址+偏移)
sta tmp1 ; tmp1 = 低字节 (暂存)
lda ARRAY_BASE+1,X ; A = A[I]的高字节 (基址+1+偏移)
sta tmp2 ; tmp2 = 高字节 (16位值凑齐)

8. 完整示例程序

综合运用本教程所有特性(可在 JS 版直接运行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
10 CLS
20 PRINT "=== FBASIC DEMO ==="
30 LET A = 10
40 PRINT "A ="; A
50 FOR I = 1 TO 5
60 PRINT "I="; I; " A+I="; A + I
70 NEXT I
80 IF A > 5 THEN PRINT "A IS BIG"
90 IF A < 5 THEN PRINT "A IS SMALL"
100 GOSUB 500
110 N$ = "FAMILY"
120 PRINT "LEN("; N$; ") ="; LEN(N$)
130 PRINT "LEFT$ ="; LEFT$(N$, 3)
140 PRINT "DONE"
150 END
500 PRINT "--- SUBROUTINE ---"
510 RETURN

输出:

1
2
3
4
5
6
7
8
9
10
11
12
=== FBASIC DEMO ===
A = 10
I=1 A+I=11
I=2 A+I=12
I=3 A+I=13
I=4 A+I=14
I=5 A+I=15
A IS BIG
--- SUBROUTINE ---
LEN(FAMILY) = 6
LEFT$ = FAM
DONE

运行方式:

  • JS 版:本站 FBASIC 页面粘贴后点 RUN
  • C 版:cd fbasic-c && ./fbasic,逐行输入后 RUN

9. 语句与版本支持总表

语句

语句 语法 JS 版 C 版 NES 版(计划)
赋值 A = expr / LET A = expr ✅ ✅ ✅
输出 PRINT expr[; expr...] ✅ ✅ ✅
输入 INPUT ["提示";] var ✅ ✅ 阶段5
跳转 GOTO line ✅ ✅ ✅
子程序 GOSUB line / RETURN ✅ ✅ ✅
条件 IF cond THEN stmt/line ✅ ✅ ✅
循环 FOR v = a TO b [STEP s] / NEXT v ✅ ✅ ✅
结束 END / STOP ✅ ✅ ✅
注释 REM text ✅ ✅ ✅
清屏 CLS ✅ ✅ ✅
数组 DIM A(n) ✅ ❌ 规划

表达式

特性 JS 版 C 版 NES 版(计划)
算术 + - * / ✅ ✅ ✅ 16位整数
整除 \ / 取余 MOD ✅ ✅ 规划
括号 ( ) ✅ ✅ ✅
比较 = <> < > <= >= ✅ ✅ ✅
逻辑 AND OR NOT ❌ ❌ 规划
数值函数 (ABS/INT/RND/SQR/SIN…) ✅ ✅ 部分
字符串函数 (CHR$/LEFT$/STRING$/HEX$…) ✅ ✅ 阶段2
函数用于复合表达式 ✅ ✅ 阶段2

相关文档

文档 内容
6502 指令速查手册 6502 常用指令、寻址方式、代码模板
6502 寻址方式详解 12 种寻址方式原理与场景选择
FBASIC 模拟器 在浏览器中直接运行 BASIC 程序