Zilog Z80 处理器
6.1 概览
处理器是计算机的大脑,是整个系统的核心。它提供并控制地址和数据总线,这些总线是主板所有组件之间共享数据的主要硬件通路。它负责发起所有事务,包括从内存和 I/O 读取、执行和写入。
6.2 硬件实现
在 Zeal 8 位计算机上,处理器是 Zilog Z80 CPU Z84C0010VEG,标记为 U2,采用 PLCC44 封装。选择这种封装是因为其外形比传统的 DIP32 更小,同时提供相同的功能并支持 10MHz 的时钟频率。
处理器在主板上的引脚排列如下:
各引脚的含义:
- $CLK$:时钟信号,来自前面介绍的有源振荡器。在 Zeal 8 位计算机上,它是一个稳定的 10MHz 时钟信号。
- $ADDR\ 0$ 到 $ADDR\ 15$:表示 CPU 正在访问的虚拟地址,用于 I/O 和内存总线。
- $DATA\ 0$ 到 $DATA\ 7$:双向总线,表示要写入内存或 I/O 的字节,或从内存或 I/O 读取的字节。
- $\overline{RD}$:当 CPU 执行读操作时有效(低电平)。此时,$DATA$ 总线置为高阻态,以便由 $ADDR\ n$ 线所寻址的组件驱动。
- $\overline{WR}$:当 CPU 执行写操作时有效(低电平)。此时,$DATA$ 总线由 CPU 驱动,包含要写入 $ADDR\ n$ 线所寻址组件的字节。
- $\overline{MREQ}$:当 CPU 执行内存请求操作时有效(低电平)。这在使用内存指令时发生,例如
ld、ldi、ldir、cpi等。更多关于内存总线的信息,请参见内存请求部分。 - $\overline{IORQ}$:当 CPU 执行 I/O 请求操作时有效(低电平)。这在使用 I/O 指令时发生,例如
in、ini、out、outi等。更多关于 I/O 总线的信息,请参见 I/O 请求部分。 - $\overline{M1}$:当 CPU 正在取指下一条要执行的指令时有效(低电平)。该信号在两个 T 状态(时钟周期)内保持有效,在第二个时钟周期内,$\overline{RD}$ 和 $\overline{MREQ}$ 都将有效。更多信息请参见指令取指部分。
- $\overline{WAIT}$:输入信号,用于告诉 CPU 在当前读或写操作中插入一个额外的时钟周期。当设备无法及时响应 CPU 的读写请求时使用。在 Zeal 8 位计算机主板上,没有为任何设备使用它,但在扩展端口上可用。因此,外部组件可以自由使用。此外,该线已经通过 1kΩ 电阻上拉。
- $\overline{INT}$:开漏输入线,用于向 CPU 请求可屏蔽中断。只有在软件通过执行
ei指令启用中断时才会被考虑。它连接到 Z80 PIO、I²C 内部连接器和扩展端口。该线通过 1kΩ 电阻上拉。更多关于中断的信息,请参见下面的中断部分。 - $\overline{NMI}$:输入线,用于向 CPU 请求不可屏蔽中断。此特殊中断的优先级高于通过 $\overline{INT}$ 线请求的中断。该线通过 1kΩ 电阻上拉,并在扩展端口上可用。更多信息请参见下面的中断部分。
- $\overline{BUSREQ}$:输入信号,用于获取地址和数据总线的仲裁权。换句话说,它告诉 CPU 释放所有地址线、数据线、$\overline{WR}$、$\overline{RD}$、$\overline{MREQ}$ 和 $\overline{IORQ}$ 线,将它们置为高阻态。这样,另一个主设备可以使用这些线与计算机上的设备通信。例如,这可用于 DMA 芯片。目前未使用,但已通过 1kΩ 电阻上拉。在扩展端口上可用。
- $\overline{BUSACK}$:当设备将 $\overline{BUSREQ}$ 置低且 CPU 已将内存控制线置为高阻后有效(低电平)。换句话说,CPU 用它来告诉请求总线的主设备现在可以自由使用总线。该线连接到 MMU,以确保仅在 CPU 是总线主设备时才启用。更多信息请参见 MMU 章节。
- $\overline{HALT}$:当 CPU 执行
HALT指令后暂停时有效(低电平)。目前未使用,但在扩展端口上可用。 - $\overline{RFSH}$ / NC:每次指令取指后生成的刷新信号,用于刷新动态 RAM(DRAM)。由于 Zeal 8 位计算机仅使用 SRAM,此信号未使用且未连接。
- Vcc 和 Gnd:5V 电源。
引脚名称上方带横线表示低电平有效,即当电压接近 0V 时视为有效。
6.3 CPU 速度与时钟信号
CPU 速度取决于它每秒可以执行的指令数。Zeal 8 位计算机上使用的 Z80 CPU 是 Z84C0010,标称支持高达 10MHz 的时钟。
该 10MHz 信号来自前面时钟电路部分介绍的有源振荡器。在每个时钟周期(称为 T-State),Z80 改变其内部状态。它经历几个状态(称为机器状态)来执行一条指令,这就是为什么即使是最简单的指令(如 nop!)也需要多个时钟周期才能执行。
因此,它每秒可以执行的指令数远低于时钟频率。就 Zeal 8 位计算机而言,约为 145 万条。
6.4 启动
上电时,Z80 CPU 需要 $\overline{RESET}$ 信号有效几个时钟周期。正如我们在上电与复位电路部分所见,这是通过复位监控电路实现的,它将 $\overline{RESET}$ 线保持超过 100ms。
在 $\overline{RESET}$ 信号释放后,CPU 将从地址 0x0000 开始执行第一条指令。这意味着在启动时,0x0000 虚拟地址必须指向非易失性存储器,例如 ROM。在 Zeal 8 位计算机上正是如此,复位后,ROM 的前 16KB 映射到虚拟地址 0x0000。
6.5 内存请求
官方 Z80 用户手册 提供了丰富的资源和技术细节,介绍了处理器的时序及其对外部组件的请求。因此,在接下来的章节中,我们将重点讨论时序的实际方面及其在 Zeal 8 位计算机上的工作方式。
指令取指
要从内存中执行指令,Z80 首先需要获取(读取)该指令。为此,它将 $\overline{M1}$ 线置为低电平,同时将其程序计数器(PC)寄存器的值置于地址线上。半个时钟周期后,$\overline{MREQ}$ 和 $\overline{RD}$ 同时变为有效。由于这两条线变为低电平时地址已经稳定,因此它们可以用作存储芯片的芯片使能和/或输出使能。这三个信号在 1.5 个时钟周期后取消断言,同时 $\overline{RFSH}$ 被激活。
总体流程如下:
- $\overline{M1}$ 置为低电平,PC 置于地址线
- 0.5 个时钟周期后,$\overline{MREQ}$ 和 $\overline{RD}$ 变为低电平
- 1.5 个时钟周期后,所有三条线置为高电平,$\overline{RFSH}$ 置为低电平
- 2 个时钟周期后,$\overline{RFSH}$ 置为高电平
因此,Z80 上的任何指令至少需要 4 个时钟周期(T 状态),相当于 400ns。
下图显示了实际测量到的时序:
内存读取与写入
在 Z80 获取指令后,即执行该指令。如果指令由多个字节组成(例如 ldir),或者带有参数(例如 ld a, 0x42),或者是内存加载指令(例如 ld a, (hl)),处理器将继续进行另一次内存加载。
执行期间的内存加载与取指过程非常相似,不同之处在于 $\overline{M1}$ 线为高电平,且 $\overline{MREQ}$ 和 $\overline{RD}$ 持续 2 个时钟周期(取指时为 1.5 个时钟周期)。
同样,CPU 在这两条线的上升沿锁存 $DATA$ 总线上的值。
对于内存写入,过程有所不同:$\overline{MREQ}$ 首先变为低电平,以表示要写入内存的字节已在 $DATA$ 总线上就绪。然后,1 个时钟周期后,$\overline{WR}$ 线变为低电平,保持低电平持续一个时钟周期。当然,在整个过程中,$\overline{M1}$ 和 $\overline{RD}$ 始终为高电平。
内存等待周期
正如我们刚才所见,在 Zeal 8 位计算机上,内存读取大约需要 200ns,而内存写入需要 100ns。这意味着需要读取或写入的设备在这两种情况下都必须足够快才能与 Z80 接口。
如果设备无法在此延迟内响应请求,则可以告诉 Z80 等待更长时间。这是通过 $\overline{WAIT}$ 信号实现的,只要设备需要,该信号必须保持有效(低电平)。
在 Zeal 8 位计算机上未使用此功能,因为所有设备都符合 10MHz 时钟要求,但该信号仍然可用并出现在扩展端口上。
6.6 I/O 请求
在内存请求期间,处理器将 $\overline{MREQ}$ 线置为低电平,以表示地址总线上的所有 16 位数据已准备好供设备读取。
Z80 拥有第二个总线,即 I/O 总线,它正式是一个 8 位总线,最多允许 0x00 到 0xFF 的 256 个不同地址。这个辅助总线使得基于 Z80 的计算机可以在主地址空间中只放置 RAM 和/或 ROM,而将负责 I/O 的较慢设备放在这个辅助地址空间中。因此,不需要内存映射 I/O,这通常会使内存映射更加复杂并浪费无法被内存设备使用的字节。
在读取 I/O 请求期间,CPU 在第一个时钟周期输出地址,在第二个时钟周期激活 $\overline{IORQ}$ 和 $\overline{RD}$ 线。此时,CPU 内部插入了一个等待周期。因此,这两条线保持低电平 2.5 个时钟周期:
写入 I/O 请求的过程相同,但 CPU 断言 $\overline{WR}$ 线而不是 $\overline{RD}$ 线:
关于 I/O 请求和地址线的一个小注记。尽管 Zilog 声称 I/O 总线是 8 位总线,但当软件执行 I/O 请求时,地址总线上高 8 位是确定且已有文档记录的:
- 当使用
n作为 I/O 地址时,16 位地址线的高 8 位设置为A寄存器的值,低 8 位设置为n。 - 当使用
C作为 I/O 地址时,16 位地址线的高 8 位设置为B寄存器的值,低 8 位设置为C。
例如,如果软件如下所示:
ld bc, 0x1267 in a, (c)
在 I/O 请求期间放置在地址线上的 16 位地址为 0x1267。
如果软件如下所示:
ld a, 0x42 in a, (0x98)
在 I/O 请求期间放置在地址线上的 16 位地址为 0x4298。
6.7 中断
由于 Zeal 8 位计算机不使用非屏蔽中断线 $\overline{NMI}$,而是使用可屏蔽中断线 $\overline{INT}$,因此我们将只讨论后者。请记住,$\overline{NMI}$ 在官方 Z80 用户手册中有文档记录,并且该引脚在扩展端口上可用,因此可以由外部板使用。
在主板上,$\overline{INT}$ 线同时连接到 Z80 PIO 和内部 I²C 内部连接器。当该线被任何请求设备激活时,Z80 CPU 在当前指令结束时对其进行采样。一旦它确认了 $\overline{INT}$ 线,它将 $\overline{M1}$ 线置为低电平,然后在 2.5 个时钟周期后将 $\overline{IORQ}$ 线置为低电平。从那时起,两者都将保持低电平约 1.5 个时钟周期,数据总线置为高阻态。
总的来说,当 Z80 确认中断时,$\overline{M1}$ 线保持低电平 4 个时钟周期(约 400ns),$\overline{IORQ}$ 保持低电平约 1.5 个时钟周期(150ns)。在此期间,CPU 可能正在等待从发起中断的设备接收一个字节。此行为取决于通过指令 im x(其中 x 为 0、1 或 2)在软件中配置的中断模式。
📝 注意
CPU 仅在软件事先通过指令
ei启用中断后才接受这些中断。此外,中断在启动时是禁用的,这意味着必须至少使用一次ei才能开始接收中断。
模式 0
可以通过执行指令 im 0 选择此模式。在此模式下,在 $\overline{IORQ}$ 线为低电平的时钟周期内,请求中断的设备必须将一个字节放置在数据总线上,该字节将由 CPU 执行。换句话说,请求设备必须向 CPU 提供一条指令。
Zeal 8 位计算机默认不使用此模式。
模式 1
可以通过执行指令 im 1 选择此模式。在此模式下,CPU 不期望在 $\overline{IORQ}$ 线为低电平的时钟周期内接收任何字节。实际上,DATA 总线上的内容将被忽略,CPU 将执行一个重启操作,跳转到虚拟地址 0x0038。换句话说,它将当前的程序计数器保存到堆栈并将其设置为 0x0038,相当于执行 call 0x0038。
Zeal 8 位计算机默认不使用此模式。
模式 2
可以通过执行指令 im 2 选择此模式。在此模式下,在 $\overline{IORQ}$ 线为低电平的时钟周期内,请求中断的设备必须将一个字节放置在数据总线上,该字节表示向量表中的地址索引,向量表的高字节地址存储在 I 寄存器中。然后,CPU 从内存中读取该地址并跳转到其指向的位置。
因此,必须在启用中断之前设置 I 寄存器。以下是一个理解其工作原理的示例,假设我们有以下代码片段:
; 此例程将是我们示例中第一个执行的
_start:
; 将 I 寄存器设置为下面定义的向量表地址。I 只包含高字节
ld a, my_vector_table >> 8
ld i, a
; 切换到中断模式 2 并启用中断
im 2
ei
; 等待中断
wfi:
halt
jp wfi
; 建议将 my_vector_table 对齐到 256 字节边界,使其最低字节为 0
my_vector_table:
DEFW handler_1
DEFW handler_2
; ....
DEFW handler_256
; 设备 1 的中断处理程序
handler_1:
; 执行某些操作...
; 重新启用中断并从中断返回
ei
reti
; 另一个设备的中断处理程序
handler_2:
; 执行某些操作...
; 重新启用中断并从中断返回
ei
reti
假设我们有一个设备 A 配置为在向 Z80 CPU 请求中断时发送字节 0x02。中断 CPU 的流程如下:
- 设备
A将 $\overline{INT}$ 线驱动为低电平以向 CPU 请求中断。该设备现在等待。 - CPU 首先将 $\overline{M1}$ 线置为低电平,然后将 $\overline{IORQ}$ 线置为低电平,等待设备(
A)发送一个字节。 - 设备注意到 $\overline{M1}$ 和 $\overline{IORQ}$ 线都为低电平,它将预配置的字节
0x02输出到 DATA 总线上。 - CPU 将两条线置为高电平,同时从 DATA 总线采样字节
0x02。它还将当前的程序计数器压入堆栈。 - CPU 通过将该新字节与
I寄存器拼接,形成一个 16 位地址,得到VECT_ADDR = (I << 8) | 0x02。 - CPU 从内存地址
VECT_ADDR读取两个字节,找到例程handler_2的地址,即my_vector_table数组中的第二个条目(在 C 语言中的等效表示法为*(my_vector_table + 2) = handler_2)。 - CPU 禁用中断并跳转到
handler_2例程。
📝 注意
向量表中不一定需要有 256 个条目,如果只使用单个设备进行中断,或者已知预配置的索引值,则可以有一个非常小的向量表。
同样,向量表也不必严格对齐到 256 字节边界,但这意味着其地址的低字节需要作为配置给中断设备的索引的一部分。然而,为简单起见,建议将向量表对齐到 256 字节边界(其地址的最低字节等于
0x00)。