Full picture
7

内存管理单元 (MMU)

7.1 概览

如前一节关于处理器的说明所述,Z80 具有 16 条地址线,换句话说,它可以处理 16 位地址。这意味着最大可寻址地址为 64KB(十六进制 0xFFFF)。与现代可以寻址 32 位或 64 位地址的计算机相比,这似乎非常低,但在 Z80 问世时,由于 RAM 和 ROM 的容量也非常有限,64KB 已经足够了。

对于 Zeal 8 位计算机而言,64KB 是不够的。事实上,RAM 本身就有 512KB,已经超过了这个限制。解决此问题的方法是添加一个内存管理单元(MMU),在某些 8 位计算机上也称为内存映射器。其目标是让 CPU 只能访问内存的某个 64KB 部分,而隐藏其余部分。当然,CPU 也能够告诉 MMU 应该映射哪个内存部分并使其可访问。

在我们的案例中,CPU 可访问的内存部分称为虚拟内存,而通过 MMU 可访问的整个内存称为物理内存。前者大小为 64KB,后者大小为 4MB。

7.2 硬件描述

内存管理单元由三个芯片组成:

  • 两个 4×4 寄存器文件:74HC670,在主板上标记为 U8U9
  • 一个八路缓冲驱动器:74HC541,标记为 U10

在我们的案例中,寄存器文件并联连接,用于存储四个 8 位值。第一个寄存器文件(标记为 U8)管理每个字节的高 4 位,而第二个寄存器文件(U9)管理每个字节的低 4 位。

每个 8 位值代表一个虚拟页面,并存储其最高地址线(即扩展地址线)。因此,在任何时候,当 CPU 掌握总线并执行内存操作时,必须读取 MMU 以获得要读取或写入的物理内存地址。

寄存器文件的引脚排列如下:

寄存器文件
寄存器文件的引脚排列

其中各信号的含义为:

  • $DATA\ 0$ 到 $DATA\ 7$:数据总线,由 CPU 和所有其他组件共享。
  • $ADDR\ 0$ 和 $ADDR\ 1$:来自 CPU 的地址线。当 $\overline{BANK\_WRITE}$ 有效时,这两条线一起构成一个 2 位值,用于索引要写入的 MMU 页面。
  • $ADDR\ 14$ 到 $ADDR\ 21$:MMU 的输出,这 8 条线表示 CPU 当前试图读取或写入的物理地址的高 8 位。这些线可通过扩展端口、视频卡连接器和板上的所有组件访问。
  • $CPU\ ADDR\ 14$ 和 $CPU\ ADDR\ 15$:来自 Z80 CPU 的输入线。当 $\overline{ENABLE}$ 有效时,这两条线一起构成一个 2 位值,用于索引要读取的 MMU 页面。MMU 是唯一使用这两条 CPU 线的电路,在本文档的其余部分,$ADDR 14$ 和 $ADDR 15$ 均指 MMU 的输出。
  • $\overline{BANK\_WRITE}$:来自逻辑胶合的写入信号。当此信号为低电平时,由 [ADDR0, ADDR1] 索引的页面被写入到 $DATA\ 0$ 到 $DATA\ 7$ 线上的值。
  • $\overline{ENABLE}$:当为低电平时,MMU 将当前由 [ADDR0, ADDR1] 索引的页面值输出到 $DATA\ 0$ 到 $DATA\ 7$ 线上。在 Zeal 8 位计算机上,此线仅在 Z80 CPU 失去总线仲裁时置为高电平。更多详情请查看禁用 MMU 部分。

仅靠这两个寄存器文件不足以让 CPU 读回页面配置,这就是引入八路缓冲驱动器的原因。当 CPU 尝试读取 MMU 配置时,缓冲器将 MMU 输出重定向到 DATA 总线:

MMU 缓冲器
缓冲驱动器的引脚排列

其中各信号的含义为:

  • $DATA\ 0$ 到 $DATA\ 7$:数据总线,由 CPU 和所有其他组件共享。
  • $ADDR\ 14$ 和 $ADDR\ 21$:上述寄存器文件的输出线。
  • $\overline{BANK\_READ}$:来自逻辑胶合的线。当 CPU 尝试读回页面值时有效。

总的来说,这些组件的交互方式如下图所示:

MMU 示意图
MMU 结构示意图

写入 MMU 配置

MMU 也是可写的,这对于切换 CPU 想要访问的内存部分是必要的。为此,两个寄存器文件的 write 输入都连接到一个低电平有效的 $\overline{MMU\_WRITE}$ 信号,该信号由逻辑胶合生成。当 I/O 请求中给定的 I/O 地址在 0xF00xFF 之间时,该信号将被激活。

要写入的页面索引由 CPU 地址线 ADDR 1(对应位 1)和 ADDR 0(对应位 0)分别连接到两个寄存器文件的 WaWb 输入构成。

地址线范围 ADDR 15...ADDR 8ADDR 3...ADDR 2 将被忽略。因此,例如写入 I/O 地址 0xF1 与写入 I/O 地址 0xF9 效果相同。

复位时,会发出一个 MMU_WRITE 信号,将索引 0 的第一个页面初始化为值 0x00。因此,CPU 在启动和复位时将从物理地址 0x000000 开始执行指令。

读取 MMU 配置

上一节中给出的 I/O 地址不足以读回配置。

实际上,由于 MMU 的 read 输入引脚的连接方式,读回配置比写入稍微复杂一些。如下面的虚拟内存分段所述,寄存器文件的 read 输入引脚连接到 CPU 地址线 ADDR 15ADDR 14,因此,在读回配置时,这些线也应该代表要获取的页面索引。

尽管 Z80 I/O 总线在官方文档中被定义和使用为 8 位总线,但地址线 ADDR 15...ADDR 8 仍然是有效的。请查看 Z80 16 位 I/O 请求 表以了解它们所取的值。

当检测到读取 MMU 的 I/O 请求时,所请求页面的值被输出到两个寄存器文件的数据线(Q0...Q4)上,同时八路缓冲驱动器(标记为 U10)被激活。其作用是将这些输出线重定向到 CPU 数据线。然后 CPU 就能获取寄存器文件给出的 8 位值。

有关如何读取 MMU 配置的实际示例,请查看示例部分

7.3 虚拟内存分段

在运行时,要选择输出四个 8 位值中的哪一个,会提取 CPU 地址线的最高两位 ADDR 14ADDR 15 并将其解释为一个 2 位值。因此,[ADDR 15, ADDR 14] 可以取 03 之间的任何值。这就是两个寄存器文件的读取入口。

剩余的 14 位地址是虚拟内存页面内的偏移量。因此,单个虚拟页面的大小为 16KB。我们可以将 [ADDR 15, ADDR 14] 视为从中获取数据或写入数据的页面索引。

虚拟地址空间

在此页面索引被提供给寄存器文件后,它们输出一个 8 位值,表示物理内存地址的最高位。我们将此范围称为 EXT_ADDR21...EXT_ADDR14。通过将寄存器文件的输出与 CPU 剩余的地址线 ADDR13...ADDR0 拼接,我们得到一个 22 位物理地址。

地址线分段
地址线组织结构图

通过编程寄存器文件中存储的内容,我们可以控制 CPU 在给定虚拟页面上看到哪个 16KB 窗口。

对称地,通过读取虚拟页面的配置,我们可以知道当前哪个物理地址被映射到虚拟地址。

7.4 禁用 MMU

值得注意的是,当 $\overline{BUSACK}$ 信号为低电平时,MMU 被禁用。换句话说,如果任何其他设备(而非 CPU)正在掌握总线,它将需要手动管理所有 22 位地址。例如,DMA 芯片将无法与 MMU 交互或使用 MMU,即使只是读取其配置。不过,这也意味着这样的 DMA 设备也能够改变物理内存的任何部分,即使它没有被映射到虚拟地址空间。

7.5 MMU 示例

在以下示例中,将使用以下宏:

DEFC MMU_PAGE0    0xF0
DEFC MMU_PAGE1    0xF1
DEFC MMU_PAGE2    0xF2
DEFC MMU_PAGE3    0xF3

它们定义了每个页面的 I/O 地址。以下宏定义了每个虚拟页面的起始虚拟地址:

DEFC VIRT_ADDR_PAGE0    0x0000
DEFC VIRT_ADDR_PAGE1    0x4000
DEFC VIRT_ADDR_PAGE2    0x8000
DEFC VIRT_ADDR_PAGE3    0xC000

写入配置

以下示例配置 MMU 以映射给定的物理地址:

; 将 RAM 的前 16KB 映射到第二个虚拟页面。换句话说,将物理地址 0x80000 映射到虚拟地址 0x4000
ld a, 0x80000 >> 14
out (MMU_PAGE1), a
; 我们也可以使用 c 和另一个寄存器实现相同的效果
ld b, 0x80000 >> 14
ld c, MMU_PAGE1
out (c), b

读取配置

以下示例读取第三页的 MMU 配置:

; 将页面索引(本例中为 0b10,即索引 2)放在 A 的高 2 位
ld a, 0x80 ; = 0b1000_0000 二进制
in a, (MMU_PAGE2) ; 实际上,我们可以使用任何 MMU_PAGEn,但为了便于理解和阅读,建议使用实际要读取的页面。
; A 寄存器现在包含映射到虚拟地址 0x8000 的 22 位物理地址的高 8 位

也可以使用 c 寄存器执行此操作,但在这种情况下,页面索引必须放在 b 寄存器的高两位:

ld b, 0x80
ld c, MMU_PAGE2
; 然后我们可以读取第三页的 8 位值到任何寄存器:
in a, (c)
; 或 E
in e, (c)
; 甚至 B!但在这种情况下,会破坏之前写入的高位
in b, (c)

备份和恢复配置

在某些情况下,可能需要先存储当前虚拟页面的配置,以便自由修改它,然后在返回之前恢复它。

例如,假设一个打印函数需要先映射 VRAM 区域,然后才能在屏幕上实际打印字符。然后它需要恢复原始配置并返回给调用者:

; 我们假设:
;   - SP(堆栈指针)指向最后一页(0xC000-0xFFFF)
;   - PC(程序计数器)在第一页(0x0000-0x3FFF)
;   - HL 包含要打印的字符串
print_screen:
    ; 获取第二页的配置
    ld a, 0x80 ; 0b1000_0000 二进制
    in a, (MMU_PAGE1)
    ; 将获取的值存储在堆栈上
    push af
    ; 将 VRAM 映射到第二页
    ld a, VRAM_PHYS_ADDR >> 14 ; 我们假设此宏存在
    out (MMU_PAGE1), a
    ; 执行与 VRAM 相关的操作
    [...]
    ; 恢复原始配置
    pop af
    out (MMU_PAGE1), a
    ret
EN | 中文Beta