
迈克尔·马丁软件实验的综合网站菜单包含以下内容- 主页- 平台指南- 项目- 技术方法ZX Spectrum 系统之旅文本模式既然我们已经了解了 Spectrum 的 BASIC 对硬件的控制方式现在要深入研究机器语言看看它能带来什么。这一领域内容丰富预计会分多篇文章介绍。与其他平台相比这更具挑战性因为 Sinclair 不太关注提供一致且结构化的固件接口。Commodore 的 8 位计算机从最初的 PET 到 C128在其“KERNAL”ROM 末尾都有向后兼容的跳转表很多机器语言代码在进行文件处理和磁盘访问等操作时运行方式相同。MSX 是制造商遵循的行业标准靠近中断向量处有定义明确的跳转表。IBM PC 的 BIOS 也有类似作用但没那么主动。而 Spectrum 只有少数定义好的入口点开发者常直接调用系统 ROM 内部深层代码来实现目标。Timex Sinclair 2068 的 ROM 不兼容几乎没有 Spectrum 软件能在它上面运行这似乎是其在美国市场失败的主要原因。不过MSX、Commodore 16 和 Coleco Adam 在美国市场也惨败它们是 TS2068 的直接竞争对手。鉴于 128 型在 2068 型失败后推出且 128 型谨慎保持其 ROM 相关部分与 48K 版本兼容推测 Sinclair 从中吸取了教训。本系列文章以 48K Spectrum 为目标平台也会确保介绍的技术方法适用于 16K 和 128K 机型。Spectrum 有大量克隆机和后续机型质量不一若遇到相关有趣内容会顺带提及但属于旁支内容。首次探索将研究如何实现像上周展示的纯 BASIC 游戏《Manic Mechanic》那样的显示效果第一篇文章先了解基本的系统组织包括机器语言程序在系统中的存在、加载和运行方式以及它们与 BASIC 和 BIOS 的共存方式。接着深入研究系统中基于文本的显示功能包括《Manic Mechanic》依赖的“用户自定义图形”功能。最后探讨键盘和游戏手柄输入发现这与在 BASIC 中操作相似。基本原则运行代码刚开始探索 ZX Spectrum 时惊喜地发现 BASIC 能与机器代码轻松共存。许多标准 BASIC 命令有额外模式适用于纯机器代码程序和 BASIC/ML 混合程序。磁带上的文件会标识自己是 BASIC 代码、BASIC 数据还是二进制数据。BASIC 的SAVE和LOAD命令有扩展选项可指定加载内容类似其他 BASIC 中的LOAD和BLOAD命令。从磁带上加载机器代码需在文件名后加关键字CODE。BASIC 可通过向CLEAR命令传递参数限制其内存使用。将 BASIC 的内存上限降低到机器代码加载点以下可避免内存冲突。BASIC 使用USR函数调用机器代码子程序。该函数以子程序地址为参数子程序执行完任务后用RET指令返回 BASIC。BC寄存器对的值会作为 16 位无符号整数返回给 BASIC 作为函数值。无有意义返回值的子程序通常会将其传递给随机数生成器处理Sinclair BASIC 中的RANDOMIZE USR与 C64 BASIC 中的SYS作用大致相同。除保存二进制数据范围SAVE函数还可保存 BASIC 程序使其在LOAD后立即开始运行且允许从任意行号开始执行。2017 年为 Spectrum 程序创建了更复杂的 BASIC 存根并沿用至今总体策略如下保存 BASIC 程序使其从第 30 行而非第 10 行开始执行。第 30 行代码用CLEAR命令限制 BASIC 内存然后将磁带上的下一个文件实际程序作为二进制文件加载。加载完成后跳转到程序顶部。第 10 行和第 20 行用RANDOMIZE USR调用加载的机器代码然后跳过加载器逻辑使程序正常退出。最终结果是程序运行时执行其机器代码部分加载时执行所有必要的准备工作。将其转化为可直接使用的打包脚本超出本次探索范围仅提供 关于它的原始文章链接 和 脚本本身的副本链接若需要。机器代码视角下的 SpectrumZ80 有 64KB 的地址空间48K Spectrum 会全部使用。$0000-$3FFF是系统 ROM。$4000-$5AFF是图形 RAM用于存储位图数据和颜色表。其他部分是系统使用的 RAM。16K 和 128K Spectrum 基本类似不过 16K 的 RAM 到$7FFF结束128K 的 ROM 和 RAM 都采用了存储体切换技术。编写汇编语言代码时不能使用系统 ROM 拥有的功能包括与中断、影子寄存器和IY寄存器相关的内容。IY寄存器较特殊虽不能修改但可读取值始终是$5C3A还可用它进行索引。它保存着 Sinclair 所称的“系统变量”的基地址这些变量是关于当前系统状态的数据集合常需读取和写入。这些变量的位置在不兼容的变体之间比 ROM 更一致做第一个项目时大量依赖了这一点。实际上ROM 对此不严格常直接通过地址引用系统变量也会根据需要使用IY进行索引来访问。索引操作成本高但更灵活在明确知道基地址又有索引寄存器的情况下会在使用绝对地址和索引访问之间切换。编写代码时会让所有系统变量访问都通过IY进行但通常并非如此自己在 《Spectrum 平台指南》 中也没这样做。输出文本在 BASIC 探索中可知Spectrum 将屏幕分为 22 行的上窗口用于主显示2 行的下窗口用于输入和状态消息。这并非 BASIC 特性在机器代码层面也存在。要将文本输出到上窗口需配置系统变量告诉其输出例程将文本输出到那里可通过向IY2处的TVFLAG写入 0 实现。完成后可调用地址为$10的 ROM 例程来打印A中的字符。Spectrum 使用 ASCII 编码普通文本很熟悉。这使“Hello World”程序变得简单org $7000ld (iy2),0 ; 写入上屏幕ld hl,msglp: ld a,(hl)inc hlor aret zrst $10jr lpmsg: db Hello, world!,13,0通常将$7000作为起始地址因为它深入到 RAM 中能给 BASIC 留出足够空间也为较小项目留出空间使其能在 16K 机器上运行。使用类似sjasm hello.asm hello.bin的命令来汇编这个程序并用spectralink.py hello.bin 7000进行打包然后将hello.bin.tzx加载到 FUSE 中……成功了定义和使用图形字符使用 BASIC 时有两种图形字符内置的半图形字符和一套可重新编程的字符BASIC 称之为“用户自定义图形”。可通过切换到“图形输入模式”按 1 - 8 键输入半图形字符必要时使用反显模式以获得全部 16 种可能的组合按 A - U 键输入用户自定义字符。在这方面机器语言比 BASIC 稍容易。“图形模式”是输入系统的特性摆脱直接输入所需字符的限制后可直接将扩展字符传递给地址为RST $10的字符打印例程。使用预定义的半图形字符有好处反显问题是因键盘按键不足采取的不太方便的解决办法。实际上在$80-$8F范围内可立即使用全部 16 种可能的半图形字符。它们排列合理第 1、2、4 和 8 位分别对应字符的右上、左上、右下和左下象限。用户自定义图形类似它们占据$90-$A4范围从图形 A 到图形 U128K 机型少两个字符只提供到$A2。字符集其余部分直到$FF是 BASIC 关键字的缩写形式这就是 128K 机型少几个位置的原因SPECTRUM和PLAY命令就出现在那里。用户自定义图形的确切位置因系统而异但在IY65处有一个名为UDG的 16 位指针指向它们。这正是 BASIC 函数USR A返回的值由于 BASIC 本身将其视为一个可POKE的缓冲区在机器代码中也可这样做。下面是一个简单的加载例程假设在gfx处有num_chars个字符的数据ld hl,gfxld e,(iy65)ld d,(iy66)ld bc,num_chars*8ldir这些在 BASIC 中也适用可使用CHR$打印出所有图形字符而无需切换到图形或反显模式。让文本更美观上周研究了七个命令可单独使用也可融入PRINT和DRAW命令中以控制位置、颜色和闪烁文本等行为。字符打印例程会自动处理大部分功能但协议与 BASIC 略有不同。基本思路是通过打印特殊控制代码来重现对文本属性的内联更改。字符代码$10到$17复制了上周看到的文本控制命令依次代表INK、PAPER、FLASH、BRIGHT、INVERSE、OVER、AT和TAB上周没讨论TAB因为它是标准 BASIC 命令并非 Spectrum 特有但实际也是这样实现的。除AT需要两个字节作为参数外其他命令都只需一个字节作为参数。这些命令的工作方式与 BASIC 中完全相同包括PAPER和INK中颜色 8 和 9 的特殊透明/高对比度模式以及AT参数按行、列顺序排列左上角为 (0,0)。与 BASIC 不同的是这些命令在PRINT语句结束后不会重置。即使打印换行符在这个系统中是单纯的回车符$0D也不会恢复初始状态。系统对“永久”和“临时”字符属性有区分但这与 ROM 的内部机制紧密相关。要充分利用这些特性需深入研究 Sinclair 最初未期望关注的内容。更深入地与系统 ROM 协作系统变量中有六个字节与文本显示交互IY85 处的 ATTR-T“临时”文本属性。使用RST $10指令打印字符时该变量中的字节会被复制到颜色内存中的相应单元格以设置其颜色、亮度和闪烁效果。IY86 处的 MASK-T“临时”属性掩码。实际上ATTR-T中的字节并非完全复制。该字节中设置的任何位在复制过程中都不会改变。这就是PAPER 8和INK 8的工作原理它们屏蔽了该变量的相关部分打印例程会保留颜色单元格的这些部分不变。IY83 和 IY84 处的 ATTR-P 和 MASK-P“永久”文本属性和掩码。在 BASIC 中使用INK和PAPER等命令作为独立命令时这些值会更新而PRINT语句首先会将它们复制到临时位置。这就是临时颜色更改具有临时性的原因。清屏指令也会参考ATTR-P来决定将屏幕清除为什么颜色。IY87 处的 P-FLAGS非颜色文本控制。OVER和INVERSE修饰符不属于文本属性特殊的INK 9和PAPER 9模式所需的逻辑也未直接包含在ATTR或MASK中。这些值作为单个位存储在这里临时值存储在偶数位永久值存储在奇数位。这种交错排列允许系统 ROM 通过一些紧凑的位操作将永久颜色复制到临时颜色。IY14 处的 BORDERCR边框颜色。写入该值实际上并不会设置边框颜色边框颜色是通过向 I/O 端口$FE的低三位写入值来控制的但系统 ROM 在重置边框本身时会参考它更重要的是在设置下输入窗口的颜色属性时也会参考它。该字节中的值存储在第 3 - 5 位在存储之前需要将颜色值乘以 8。同样这些都不是任何正式 BIOS 接口的一部分使用系统 ROM 功能在某种程度上假定手头有一份带注释的 ROM 反汇编文件。幸运的是有 skoolkid 网站上的超链接版本能有个大致了解。通过字符打印代码访问的内容永远不会触及永久数据。不过已发布的反汇编文件中提到在$0D4D处有一个内部 ROM 例程TEMPS它会将所有永久值复制到临时位置实现属性重置。该函数无需参数但会破坏A和HL寄存器不过可在需要时随时调用。永久属性主要由清屏函数CLS使用这是一个位于$0D6B的 ROM 例程。这些属性用于填充上窗口的属性单元格下窗口的值来自BORDERCR以及一些基本复制INK 9功能的逻辑。它还会将临时颜色重置为永久颜色。令人恼火的是尽管在这里检查了BORDERCR但它实际上并不会设置边框。更令人恼火的是它会将输出位置重置到下窗口。可通过自己编写一个辅助函数来解决这些问题。这个clrto函数会将A中的属性字节作为参数将整个屏幕清除为该颜色包括上窗口、下窗口和边框。它还会清除可能开启的任何非颜色模式并在结束时将输出恢复到上窗口。可使用TVFLAG来实现但既然直接调用系统 ROM 例程不妨“正确”地操作实际打开上窗口的输出通道。位于$1601的CHAN-OPEN例程用于选择要发送字符的输出设备设备 2 就是上窗口。完整的clrto函数如下clrto: ld (iy83),a ; ATTR-Pand $38ld (iy14),a ; BORDERCRrrcarrcarrcaout ($fe),axor ald (iy84),a ; MASK-Pld (iy87),a ; P-FLAGScall $0d6b ; CLSld a,2jp $1601 ; CHAN-OPENSpectrum 的文本系统还有个小问题。若打印的文本过多导致屏幕需要滚动默认情况下它不会向下滚动。相反它会暂停输出等待用户确认是否真的希望屏幕滚动或者是否希望结束程序。IY82处的系统变量SCR-CT用于衡量在再次提示之前要打印多少行若希望屏幕连续滚动可定期将其重置为一个较大的正数。读取按键和进行 I/O 操作BASIC 提供了两种读取输入的选项INKEY$函数用于检查是否有按键等待处理以及使用IN函数直接与键盘和游戏手柄交互。在机器代码层面也有对应的操作。每一帧都会触发一个中断由固件进行拦截。其中断处理程序会更新一个计时器FRAMES位于IY64处的一个 3 字节值并扫描键盘端口。如果检测到按键按下会将其解码为一个字符将该字符放入IY - 50处的LAST-K中然后设置IY1处FLAGS的第 5 位。以下是一个简单的例程用于等待按键按下然后将其值返回给Agetkey: haltbit 5,(iy1)jr z,getkeyld a,(iy-50)res 5,(iy1)ret在循环中使用HALT是因为只有中断处理程序会实际更新这些标志当可以简单地等待下一次扫描时就无需对这些标志进行自旋锁操作。直接输入更简单因为作为汇编语言程序员可使用IN和OUT直接访问 I/O 总线。不过有个小问题与 BASIC 不同指令集表明 I/O 地址只有 8 位而不是 16 位。这实际上是 Zilog 的一个设计疏忽但却成为了 Sinclair 依赖的一个特性指令IN A,(C)和OUT (C),A会将整个BC寄存器放在地址总线上而不仅仅是C。要检查 A 键是否被按下可这样写ld bc,$fdfe ; BASIC 中的 IN 65022in a,(c)rrajr nc,pressedAKempston 游戏手柄只使用 8 位端口地址所以相关代码更简单。以下是一个完整的读取例程将 x 轴偏移量返回给Ly 轴偏移量返回给H开火按钮状态返回给Crdjoy: ld hl,0ld c,hin a,($1f)rrajr nc,1Finc l1 rrajr nc,1Fdec l1 rrajr nc,1Finc hrrajr nc,1Fdec h1 rraret ncinc cret关于哪些端口对应哪些按键以及各种游戏手柄布局的完整文档可参考 Martin Korth 的 ZX Docs其中有很多不错的表格。游戏手柄操作与 BASIC 基本相同所以上周提到的各种注意事项仍然适用但影响会稍小一些。模拟器在模拟 SHIFT 键时仍然有些不一致但不再需要担心在同时移动和射击时意外触发L BREAK INTO PROGRAM错误。整合所有内容为总结学到的内容将它们整合到一个程序中实现与上次 BASIC 程序相同的横幅显示效果主程序基本上只是依次调用实现的各个功能除了直接的键盘或游戏手柄读取org $7000ld a,$20 ; 清除为绿色背景黑色文字call clrtold hl,gfx ; 将我们的 4 个自定义字符加载到 $90 - $93ld e,(iy65) ; UDGld d,(iy66)ld bc,32ldirld hl,msg ; 打印显示内容call printcall getkey ; 等待按键按下ld a,$38 ; 清除回白色背景黑色文字jp clrto ; 完成后退出clrto和getkey函数与上面列出的完全相同。不过print函数需要与“Hello World”程序中的稍有不同。在之前的程序中使用 0 字节作为字符串结束符现在这样做不太合适因为在显示中会在不同位置将PAPER和BRIGHT都设置为 0这需要通过实际打印CHR$ 0字符来实现。解决方案是将隐式比较A是否为 0 的OR A指令替换为显式比较其是否为 255 的CP $FF函数。这是一个更安全的哨兵代码它对应于 BASIC 关键字COPY而实际上永远不会打印这个关键字。print: ld a,(hl)inc hlcp $ffret zrst $10jr print然后是数据本身要打印的消息和雨伞的图形定义。消息数据大量使用了上面提到的文本控制代码因此基本上难以阅读。图形数据与原始 BASIC 中的DATA语句完全相同只是这次以十六进制形式呈现就像 TI BASIC 中那样msg: db $10,4,$11,0,$16,10,2,$8b, ,$87db $13,1,$10,1,$16,11,2, ,$90,$91,$10,7, BUMBERSHOOT SOFTWARE db $10,1,$16,12,2, ,$92,$10,6,$13,0,$93,$13,1,$10,7,Showing off the Spectrum db $13,0,$10,4,$16,13,2,$8e, ,$8d,$ffgfx: db $03,$0f,$1f,$3f,$7f,$7f,$ff,$ffdb $c0,$f0,$f8,$f8,$f0,$e0,$c0,$80db $ff,$fe,$7c,$78,$30,$00,$00,$00db $80,$c0,$60,$30,$18,$08,$38,$00事实证明这个程序比最初的 BASIC 程序小得多即使考虑到 BASIC 存根和额外的磁带头这个程序的大小也不到原来的一半。本尼会笑到最后吗今天涵盖的内容足以想象一个纯机器代码版本的《Manic Mechanic》类型的游戏上周将其作为示例。现在已经了解了如何重现主游戏显示以及如何处理输入和动画。不过仍然缺少声音功能而且还无法实现标题屏幕像DRAW命令生成的线条图形在机器语言中实现起来比较麻烦但还有一些 BASIC 根本无法比拟的技巧。请继续关注。文章发布于 2026 年 5 月 30 日 [12:30 pm]作者是 迈克尔·马丁分类为 复古编程、Sinclair。文章导航- 给 ZX Spectrum 一个公平的机会 插曲Spectrum 与 C64 BASIC 对决 -