设计并实现一个交互式监控程序(Monitor),这是8位电脑最基础的系统软件——它让你可以通过串口或键盘输入命令,检查内存、修改数据、运行程序。监控程序是操作系统最简单的形态,也是所有更复杂软件的起点。
监控程序这个名字来自早期大型机的"控制台监控器"——操作员坐在终端前,通过命令控制整台机器。Apple II的System Monitor、Commodore 64的内置监控器、ZX Spectrum的ROM BASIC——都是这种理念的产物。
| 系统 | 监控名称 | 入口方式 | 特色 |
|---|---|---|---|
| Apple II | System Monitor | Call -151 | 迷你反汇编器 |
| Commodore 64 | M64 | SYS 8*4096 | 磁带操作 |
| ZX Spectrum | ROM Monitor | 内置 | 与BASIC集成 |
| 6502 SBC | WOZ Monitor | 上电启动 | 最精简实现(256字节!) |
; monitor.asm - Retro8 监控程序
; 存放于ROM $8000-$F9FF
.ORG $8000
; ===== 入口点 =====
MONITOR:
JSR INIT_UART ; 初始化串口
JSR PRINT_BANNER ; 显示欢迎信息
MON_LOOP:
LDA #'>' ; 显示提示符
JSR PUTCHAR
JSR READ_LINE ; 读取一行命令
JSR PARSE_CMD ; 解析命令
JMP MON_LOOP ; 循环
; ===== 显示欢迎信息 =====
PRINT_BANNER:
LDA #; 字符串指针低字节
LDA #>BANNER
STA $21 ; 字符串指针高字节
BANNER_LOOP:
LDY #$00
LDA ($20),Y
BEQ BANNER_DONE
JSR PUTCHAR
INC $20
BNE BANNER_LOOP
INC $21
JMP BANNER_LOOP
BANNER_DONE:
RTS
BANNER:
.DB $0D,$0A ; CR LF
.DB "Retro8 Monitor v1.0",$0D,$0A
.DB "8-bit Computer System",$0D,$0A
.DB "Type ? for help",$0D,$0A
.DB $00 ; NUL结束
; 命令解析器
; 输入缓冲区在 $0100-$017F
; 命令格式: CMD [ADDR] [VALUE]
PARSE_CMD:
LDX #$00
LDA $0100,X ; 取第一个字符
; 'D' - 显示内存
CMP #'D'
BNE NOT_D
JSR CMD_DUMP
RTS
NOT_D:
; 'M' - 修改内存
CMP #'M'
BNE NOT_M
JSR CMD_MODIFY
RTS
NOT_M:
; 'G' - 执行程序
CMP #'G'
BNE NOT_G
JSR CMD_GO
RTS
NOT_G:
; 'R' - 显示寄存器
CMP #'R'
BNE NOT_R
JSR CMD_REGS
RTS
NOT_R:
; 'L' - 通过UART加载程序
CMP #'L'
BNE NOT_L
JSR CMD_LOAD
RTS
NOT_L:
; '?' - 帮助
CMP #'?'
BNE NOT_HELP
JSR CMD_HELP
RTS
NOT_HELP:
; 未知命令
LDA #'?'
JSR PUTCHAR
LDA #$0D
JSR PUTCHAR
LDA #$0A
JSR PUTCHAR
RTS
; D addr - 显示16字节内存
; 格式: XXXX: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ................
CMD_DUMP:
JSR PARSE_ADDR ; 解析地址到$22/$23
LDY #$00 ; 16行
DUMP_ROW:
; 打印地址
LDA $23
JSR PRINT_HEX8
LDA $22
JSR PRINT_HEX8
LDA #':'
JSR PUTCHAR
LDA #' '
JSR PUTCHAR
; 保存起始地址
LDA $22
PHA
LDA $23
PHA
; 打印16个字节的十六进制值
LDX #$00
DUMP_HEX:
LDA ($22),Y
JSR PRINT_HEX8
LDA #' '
JSR PUTCHAR
INY
CPY #$10
BNE DUMP_HEX
; 打印ASCII字符
LDA #' '
JSR PUTCHAR
PLA
STA $23
PLA
STA $22
LDY #$00
DUMP_ASCII:
LDA ($22),Y
CMP #$20 ; 小于空格的不可打印
BCC DUMP_DOT
CMP #$7F ; 大于DEL的不可打印
BCS DUMP_DOT
JSR PUTCHAR
JMP DUMP_NEXT
DUMP_DOT:
LDA #'.'
JSR PUTCHAR
DUMP_NEXT:
INY
CPY #$10
BNE DUMP_ASCII
; 换行
LDA #$0D
JSR PUTCHAR
LDA #$0A
JSR PUTCHAR
; 地址+16
LDA $22
CLC
ADC #$10
STA $22
LDA $23
ADC #$00
STA $23
RTS
; G addr - 跳转到指定地址执行
CMD_GO:
JSR PARSE_ADDR ; 解析地址到$22/$23
; 保存监控程序返回地址
LDA #>MON_LOOP
PHA
LDA #; 间接跳转到目标地址
JMP ($0022) ; 通过零页间接跳转
; 注意:如果目标程序执行RTS,会返回MON_LOOP
; 目标程序应该以JMP MONITOR结束
; L - 通过UART接收Intel HEX格式文件
; Intel HEX格式:
; :LLAAAATT[DD...]CC
; LL=数据长度, AAAA=地址, TT=类型, DD=数据, CC=校验和
CMD_LOAD:
JSR PRINT_MSG
.DB "Waiting for HEX data...",$0D,$0A,$00
LOAD_LOOP:
JSR GETCHAR ; 等待起始字符':'
CMP #':'
BNE LOAD_LOOP
; 读取记录头
JSR READ_HEX_BYTE ; 字节数 → $24
STA $24
JSR READ_HEX_BYTE ; 地址高字节 → $23
STA $23
JSR READ_HEX_BYTE ; 地址低字节 → $22
STA $22
JSR READ_HEX_BYTE ; 记录类型 → A
CMP #$01 ; 类型01=文件结束
BEQ LOAD_DONE
; 读取数据字节
LDX #$00
LOAD_DATA:
JSR READ_HEX_BYTE
STA ($22,X) ; 写入内存(零页变址)
; 注意:实际应使用绝对变址
INX
CPX $24
BNE LOAD_DATA
; 读取校验和(忽略验证)
JSR READ_HEX_BYTE
LDA #'.' ; 进度指示
JSR PUTCHAR
JMP LOAD_LOOP
LOAD_DONE:
JSR PRINT_MSG
.DB $0D,$0A,"Load complete!",$0D,$0A,$00
RTS
; ===== 串口IO子程序 =====
PUTCHAR:
PHA ; 保存A
WAIT_TX:
LDA $FF01 ; UART状态
AND #$80 ; bit7=TX_BUSY
BNE WAIT_TX ; 等待发送器空闲
PLA
STA $FF00 ; 写入发送数据
RTS
GETCHAR:
LDA $FF01 ; UART状态
AND #$20 ; bit5=RX_READY
BEQ GETCHAR ; 等待接收数据
LDA $FF00 ; 读取接收数据
RTS
; ===== 十六进制输出 =====
PRINT_HEX8:
PHA
LSR ; 高4位
LSR
LSR
LSR
JSR PRINT_NIBBLE
PLA
AND #$0F ; 低4位
PRINT_NIBBLE:
CMP #$0A
BCC NIBBLE_NUM
ADC #$06 ; 'A'-':'-1=6
NIBBLE_NUM:
ADC #$30 ; +'0'
JMP PUTCHAR
; ===== 读取一行输入 =====
READ_LINE:
LDX #$00
READ_LOOP:
JSR GETCHAR
CMP #$0D ; 回车=结束
BEQ READ_DONE
CMP #$08 ; 退格
BNE READ_STORE
CPX #$00 ; 已经在行首?
BEQ READ_LOOP
DEX
LDA #$08
JSR PUTCHAR ; 回显退格
LDA #' '
JSR PUTCHAR
LDA #$08
JSR PUTCHAR
JMP READ_LOOP
READ_STORE:
STA $0100,X
JSR PUTCHAR ; 回显字符
INX
CPX #$7F ; 最大127字符
BNE READ_LOOP
READ_DONE:
LDA #$00
STA $0100,X ; NUL结束
LDA #$0D
JSR PUTCHAR
LDA #$0A
JSR PUTCHAR
RTS
; ===== 解析十六进制地址 =====
PARSE_ADDR:
LDX #$02 ; 跳过命令字符和空格
LDA #$00
STA $22
STA $23
PARSE_LOOP:
LDA $0100,X
BEQ PARSE_DONE
CMP #' '
BEQ PARSE_SKIP
JSR HEX_CHAR_VAL ; 将ASCII十六进制转为数值
LDA $23 ; 结果*16+新数字
ASL
ASL
ASL
ASL
STA $23
LDA $0100,X
JSR HEX_CHAR_VAL
ORA $23
STA $23
INX
JMP PARSE_LOOP
PARSE_SKIP:
INX
JMP PARSE_LOOP
PARSE_DONE:
RTS
HEX_CHAR_VAL:
CMP #'0'
BCC HCV_ERR
CMP #'9'+1
BCS HCV_ALPHA
SEC
SBC #'0'
RTS
HCV_ALPHA:
CMP #'A'
BCC HCV_ERR
CMP #'F'+1
BCS HCV_LOWER
SEC
SBC #'A'-10
RTS
HCV_LOWER:
CMP #'a'
BCC HCV_ERR
CMP #'f'+1
BCS HCV_ERR
SEC
SBC #'a'-10
RTS
HCV_ERR:
LDA #$00
RTS
为监控程序添加软件断点:B addr命令在指定地址插入BRK指令($00),保存原指令。当CPU执行到BRK时触发中断,ISR恢复原指令并回到监控程序,显示寄存器值。这是最基础的调试功能。
实现单步调试(Step):执行一条指令后立即回到监控程序。提示:利用CPU的trace模式——在每条指令后强制产生中断。需要在CPU中加入trace_enable控制位。
除了Intel HEX,Motorola S-Record也是常用的机器码传输格式。实现S1/S9记录的解析和加载。S-Record与Intel HEX的主要区别是什么?各有什么优缺点?
为监控程序添加命令历史功能:用上/下箭头键浏览之前输入的命令。需要维护一个环形缓冲区存储历史命令。这是一个很好的内存管理练习。
你实现了完整的交互式监控程序!这包括:
监控程序是最小的操作系统——有了它,你的8位电脑真正成为一台可交互的机器!