第十七讲:Z80汇编语言(任天堂游戏机和德州仪器计算器)
第十七讲:Z80汇编语言(任天堂游戏机和德州仪器计算器)
本节我们将学习另外一个系列的CPU,Z80。Z80 CPU起源于8080CPU, 8080CPU是现在Intel CPU的鼻祖。
Z80 CPU最著名的用途就是任天堂Gameboy和TI图形计算器。 Z80的目的是与Intel 8080二进制兼容,即操作码相同。这意味着为Intel 8080编写的软件可以在Z80上运行而无需重新编译),但由于版权问题,寄存器和指令名称不同。
Z80
寄存器
Z80 有 8 个或 5 个通用寄存器,具体取决于您如何看待事物:
- 寄存器
A
、F
、B
、C
、D
、E
、H
和L
可以单独用作 8 位寄存器 - 寄存器对
BC
、DE
和HL
可用作16位寄存器,其中B
是BC
的高字节,而C
是低字节。 - 有点奇怪的是,
A
寄存器可以与标志寄存器F
组合起来作为 16 位寄存器AF
。 - 寄存器
IX
和IY
是 16 位索引寄存器,与内存操作数一起使用。HL
类似地用于内存操作数。
与 x86
一样,SP
寄存器指向堆栈顶部,PC
寄存器指向当前指令(请注意,这与 x86 的 IP 寄存器指向下一条指令不同)。F
寄存器包含标志:符号、零、奇偶校验/溢出、满进位。A
和 F
寄存器可以通过几条指令组合成一个 16 位寄存器 AF
。
Z80有一个中断向量寄存器I,其中包含中断向量表的地址,这个设计很有先见之明;在这个时代的Intel CPU中,中断向量的地址是固定在内存中的。
与 x86 不同,Z80 有一组影子寄存器;这是一组附加寄存器 A'
、F'
、B'
、C'
、D'
、E'
、H'
和 L'
。
您无法直接访问这些寄存器,但可以使用一条指令交换主寄存器组和影子寄存器组。
这提供了一种简单但有限的方法来保存所有寄存器的值,然后在以后恢复它们(但是,与堆栈不同,您只能保存一组寄存器值)。
指令集
大多数 Z80 汇编器使用的语法更像 AT&T 语法而不是 Intel 语法。这意味着指令具有以下形式
INSTRUCTION DESTINATION, SOURCE
内存的操作写在括号中。
你可以在这里查找到完整的Z80指令集: Z80指令集
mov
已重命名为 ld
("加载"),但它可用于将值写入寄存器或内存。例如:
ld a, 5 ; Set register A = 5
ld b, 7 ; Set register B = 7
add a, b ; Set A = A + B
ld [Output], a ; Write A into address Output
(一些 Z80 汇编器将内存操作数写在括号 () 中,而其他汇编器(例如我们将要使用的汇编器)则使用更熟悉的 [])。
add
指令只能使用 a
、hl
、ix
或 iy
作为其目标。内存操作数可以使用立即地址,或者您可以执行类似的操作
ld [hl], a ; Write A into the address contained in HL
如果要在内存操作数中使用立即地址加寄存器,则寄存器必须是 IX
或 IY
:
ld [Array + ix], a ; Write A into Array[ix]
与 x86 一样,ld
的两个操作数必须具有相同的大小:均为 8 位或均为 16 位。根据需要,立即操作数将提升为 16 位。
Z80 指令的大小为 1 到 3 个字节。
条件跳转
有两种条件跳转指令:
jp CC, Target ; 条件绝对跳转
jr CC, Target ; 条件相对跳转
其中 CC 是条件代码。两者的主要区别是jp
更灵活(可以使用更多条件,速度更快),而jr
更有限(条件更少,速度更慢,只能在程序内跳转±128字节)但只占用2个字节程序而不是 jp
的 3 个字节。
更专门的条件跳转指令是 djnz
,类似于 x86 中的loop
,它代表"递减,如果非零则跳转"
djnz Target ; Decrement B, jump to Target if B ≠ 0
函数
call
与 x86 一样,用于调用函数,它压入 PC+3
,然后跳转到函数地址。 (请记住,PC
包含当前指令的地址;即 call
指令本身。call
指令占用 3 个字节,因此下一条指令是 PC+3
。)类似地,ret
通过弹出 PC
和跳向它。
与 x86 不同,Z80 有条件返回指令:
ret p ; Return if positive (sign flag = 0)
可用条件有 C/NC(进位设置/未设置)、M/P(符号 = 1/0)、Z/NZ(零 = 1/0)、PE/PO(奇偶校验 = 1/2)。这些与条件跳转指令 jp CC 所使用的条件相同.
push
和 pop
可用于将寄存器压入堆栈或从堆栈中弹出。只有16位寄存器可以压入/弹出,所以如果你想压入A,你必须结合标志寄存器来压入它:
push af
块传输
Intel的处理还没有字符串的指令,也没有字符串重复前缀。但Z80在器块传输操作码中确实有这些指令的最小形式。这些允许将数据块从 I/O 端口或内存复制到 I/O 端口或内存(允许内存到内存传输)。
块传输函数使用 HL 作为要读/写的内存地址,使用 B 作为计数。 INI 指令(输入和增量)从给定的 I/O 端口读取 16 位,将其复制到 HL 指向的地址,递增 HL,并递减 B。所有块传输指令都遵循以下模式:
RGBDS 汇编器工具
我们将使用RGBDS 工具来为GameBoy汇编代码。
虽然架构不同,但 RGBASM 支持的汇编语法与我们熟悉的 YASM/Intel 语法非常相似。主要区别是:
- 十六进制值以
$
开头,而不是0x
- 二进制值以
%
开头,而不是0b
- GameBoy图形化可以直接使用符号嵌入由于GameBoy上的每个像素都可以是四种灰度之一,因此使用值0到3。每个值都被编码为位对 (00, 01, 10, 11),然后打包成一个字节。因此,上面对应于字节
`0123
%00011011
或$0f55
。 - 与YASM一样,RGBASM允许对涉及常量和算术运算符的表达式进行汇编时计算。
SECTION
伪指令存在,但唯一有效的部分是ROM0
(盒式 ROM 组 0)和 ROMx(盒式 ROM 组 x)。如果您需要更多控制,SECTION 指令支持用于指定节地址的附加选项。- 与 YASM 中一样,标签从行首开始,以 : 结束。局部标签以 . 开头。标签必须以一行的第一个字符开始。
- 由于标签必须从一行的第一列开始,因此指令必须缩进至少一个空格或制表符。
- 与 YASM 中一样,equ 可用于定义常量。但是,符号名称后面不能跟 : (在 YASM 中, : 是可选的):
Addr equ 0xff40
- Z80 汇编传统上在 () 中写入内存操作数,而 RGBASM 使用 Intel 风格的 []。
- 对于 ldi 系列指令(加载和增量),RGBASM 支持两种语法。通常的方式:和替代语法
ldi [hl], 0 ; Write 0 into address hl, increment hl
您可以在此处找到有关 RGBASM 语法的完整文档ld [hli], 0 ; Write 0 into address hl, increment hl
GameBoy模拟器的HelloWorld程序
GameBoy 硬件缺乏任何类型的文本输出到屏幕,因此我们实际上无法打印“Hello, world!”。此外,它甚至不允许直接访问屏幕上的各个像素;相反,它使用平铺屏幕模式:屏幕分为多个图块,每个图块为 8x8 像素。图块集存储在内存中,屏幕上的每个图块都映射到该集合中的一个图块。这比存储屏幕的整个(像素)内容节省了大量的内存:
160 × 144 × 2 = 46080 bits = 5760 bytes
(160/8) × (144/8) × 8 = 2880 bits = 360 bytes
TI-83 图形计算器模拟器上的 Hello World
许多 TI 图形计算器运行 Z80 处理器,因此我们可以为它们编写 Z80 程序集。与 GameBoy 不同,TI 计算器运行的是原厂Z80,没有任何修改。
我们将使用 Pasmo 汇编器和 Oysterpac 工具将原始二进制数据“打包”到 TI 计算器可执行文件中。两步过程只是首先将我们的源代码组装成二进制文件:
pasmo file.asm file.bin
然后在其上运行 Oysterpac 以生成打包的可执行文件: oysterpac file.bin file.83p
附录
原文链接:
https://staffwww.fullcoll.edu/aclifton/cs241/lecture-z80-assembly.html