Full picture
7

Memory Management Unit (MMU)

7.1 Overview

As explained in the previous section about the processor, the Z80 presents 16 address lines, in other words, it can handle addresses of 16-bit. This means that the maximum reachable address is 64KB, or 0xFFFF, in hexadecimal. This seems very low compared to modern computers which can address 32-bit or 64-bit addresses, but when the Z80 came out, it was more than enough as RAM and ROM were also very limited in size.

Regarding Zeal 8-bit Computer, 64KB is not enough. Indeed, the RAM itself is as big as 512KB, which already surpasses this limit. The solution to overcome this issue is to add a Memory Management Unit, or MMU, also known as Memory Mapper on some 8-bit computers. Its goal will be to give the CPU access to only a certain 64KB portion of the memory and hide the rest. Of course, the CPU will also be able to tell the MMU which portion of memory should be mapped and made accessible to it.

In our case, the part of the memory that is accessible by the CPU is called virtual memory, whereas the whole memory accessible through the MMU is called physical memory. The first one has a size of 64KB while the latter has a size of 4MB.

7.2 Hardware description

The Memory Management Unit is made out of three chips:

  • Two 4-by-4 register files: 74HC670, referenced U8 and U9 on the board
  • One octal buffer driver: 74HC541, referenced U10

In our case, the register files are connected in parallel to store four 8-bit values. The first register file, referenced U8, manages the upper 4 bits, whereas the second one, U9, manages the lower 4 bits of each byte.

Each 8-bit value represents a virtual page and stores the highest address lines, or extended lines, of it. Thus, at any time, while the CPU is mastering the buses and performing a memory operation, the MMU must be read to get the physical memory address to read or write.

The pinout of the register files is as follows:

Register files
Pinout of the register files

Where the signals are:

  • $DATA\ 0$ to $DATA\ 7$: data bus shared between the CPU and all the other components.
  • $ADDR\ 0$ and $ADDR\ 1$: address lines coming from the CPU. These lines, together, form a 2-bit value used to index the MMU page to write when $\overline{BANK\_WRITE}$ is active.
  • $ADDR\ 14$ to $ADDR\ 21$: output of the MMU, these 8 lines represent the upper 8-bits of the physical address the CPU is currently trying to read or write. These lines are accessible through the extension port, the video card connector and all the components on the board.
  • $CPU\ ADDR\ 14$ and $CPU\ ADDR\ 15$: input lines coming from the Z80 CPU. These lines, together, form a 2-bit value used to index the MMU page to read when $\overline{ENABLE}$ is active. The MMU is the only circuit that makes use of these two CPU lines, in the rest of this document, $ADDR 14$ and $ADDR 15$ refer to the output of the MMU.
  • $\overline{BANK\_WRITE}$: write signal coming from the logic glue. When this signal is low, the page indexed by [ADDR0, ADDR1] is written to the value present on lines $DATA\ 0$ to $DATA\ 7$.
  • $\overline{ENABLE}$: when low, the MMU outputs the current page value, indexed by [ADDR0, ADDR1], on $DATA\ 0$ to $DATA\ 7$ lines. On Zeal 8-bit Computer, this line is only set to high when the Z80 CPU loses bus arbitration. Check Disabling the MMU section for more details.

These two register files are not enough to be able to read back the page configuration from the CPU, this is where the octal buffer driver comes in. When the CPU tries to read the MMU configuration, the buffer will redirect the MMU output to the DATA bus:

MMU buffer
Pinout of the buffer driver

Where the signals are:

  • $DATA\ 0$ to $DATA\ 7$: data bus shared between the CPU and all the other components.
  • $ADDR\ 14$ and $ADDR\ 21$: output lines of the register files above.
  • $\overline{BANK\_READ}$: line coming from the logic glue. Active when the CPU tries to read back the value of a page.

Overall, these components can be seen as interacting as follows:

MMU buffer
Diagram of the MMU

Write MMU configuration

The MMU is also writeable, this is necessary to switch the part of the memory that the CPU wants to access. To do so, both register files write inputs are connected to an active low $\overline{MMU\_WRITE}$ signal, generated by the Logic Glue. This signal will be activated during an IO request when the given I/O address is comprised between 0xF0 and 0xFF.

The index of the page to write is formed by connecting CPU address lines ADDR 1, corresponding to bit 1, and ADDR 0, corresponding to bit 0, to both register files' Wa and Wb inputs respectively.

The address lines comprised between ranges ADDR 15...ADDR 8 and ADDR 3...ADDR 2 will be ignored. As such, writing to I/O address 0xF1, for example, will have the same effect as writing to I/O address 0xF9.

On reset, an MMU_WRITE signal is emitted, initializing the first page, of index 0, with the value 0x00. The CPU will therefore start executing the instruction located at physical address 0x000000 on boot up and resets.

Read MMU configuration

The I/O addresses given in the previous section are not enough to read back the configuration.

Indeed, because of the way the MMU read input pins are connected, reading back the configuration is slightly more complex than writing it. As explained in the section Virtual memory segmentation below, register files read input pins are connected to CPU address lines ADDR 15 and ADDR 14, as such, when reading back the configuration, these lines should also represent the index of the page to retrieve.

Even though the Z80 I/O bus is officially documented and used as an 8-bit bus, the address lines ADDR 15...ADDR 8 are still valid. Check the Z80 16-bit I/O requests table to see the value they take.

When a read MMU I/O request is detected, the value of the requested page is outputted on both register files data lines (Q0...Q4), and the octal buffer driver, referenced U10, is activated. Its role is to redirect these output lines to the CPU data lines. The CPU is then able to get the 8-bit value given by the register files.

For a practical example of how to read the MMU configuration, check the example section.

7.3 Virtual memory segmentation

At runtime, to select which of the four 8-bit values to output, the highest two bits of the CPU address lines, ADDR 14 and ADDR 15 are extracted and interpreted as a 2-bit value. Thus, the pair [ADDR 15, ADDR 14] can take any value between 0 and 3. This is the read entry for both register files.

The remaining 14-bit address is the offset within the virtual memory page. Therefore, a single virtual page has a size of 16KB. We can see [ADDR 15, ADDR 14] as the page index to get data from or write data to.

Virtual address space

After this page index is given to the register files, they output an 8-bit value, representing the highest bits of the physical memory address. We will call this range EXT_ADDR21...EXT_ADDR15. By concatenating the output of the register files and the CPU remaining address lines ADDR13...ADDR0, we get a 22-bit physical address.

Address lines segmentation
Diagram of address lines organization

By programming the content stored in the register files, we can control which 16KB window will be seen by the CPU at a given virtual page.

Symmetrically, by reading the configuration of a virtual page, we can know which physical address is currently being mapped to a virtual address.

7.4 Disabling the MMU

It is important to note that when the $\overline{BUSACK}$ signal is low, the MMU is disabled. In other words, if any other device than the CPU is mastering the buses, it will need to manage all the 22-bit of the address manually. For example, a DMA chip would not be able to interact with or use the MMU, even for reading its configuration. Nevertheless, this also means that such a DMA-capable device would also be able to alter any part of the physical memory, even if it is not mapped on the virtual address space.

7.5 MMU examples

In the following examples, the following macros will be used:

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

They define the I/O address of each page. The following macros define the virtual addresses where each virtual page start:

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

Write configuration

The following example configures the MMU to map a given physical address:

; Map the first 16KB of RAM to the second virtual page. In other words, map physical address 0x80000 to virtual address 0x4000
ld a, 0x80000 >> 14
out (MMU_PAGE1), a
; We can do the same thing using c an another register
ld b, 0x80000 >> 14
ld c, MMU_PAGE1
out (c), b

Read configuration

The following example reads the MMU configuration for the third page:

; Put in A upper 2 bits the page index to read, 0b10 in our case (index 2)
ld a, 0x80 ; = 0b1000_0000 in binary
in a, (MMU_PAGE2) ; In practice, we can use any MMU_PAGEn, but for easier understanding and reading, it is advised to use the actual page to read.
; A register now contains the upper 8 bits of the 22-bit physical address mapped to virtual address 0x8000

It is also possible to perform this operation using c register, but in this case, the page index must be put in b register's upper two bits:

ld b, 0x80
ld c, MMU_PAGE2
; We can then read the 8-bit value of the third page n any register:
in a, (c)
; or E
in e, (c)
; or even B! But in that case, this would destruct the previously written upper bits
in b, (c)

Backup and restore configuration

In some cases, it may be necessary to store the current configuration of a virtual page to modify it freely and restore it before returning.

Let's say for example that a print function needs to map a VRAM area first before actually printing on the screen the characters. Then it needs to restore the original configuration and return it to the caller:

; We suppose that:
;   - SP, Stack Pointer, points to the last page (0xC000-0xFFFF)
;   - PC, Program Counter, is in the first page (0x0000-0x3FFF)
;   - HL contains the string to print
print_screen:
    ; Get the configuration of the second page
    ld a, 0x80 ; 0b1000_0000 in binary
    in a, (MMU_PAGE1)
    ; Store the given value on the stack
    push af
    ; Map the VRAM in the second page
    ld a, VRAM_PHYS_ADDR >> 14 ; we suppose this macro exists
    out (MMU_PAGE1), a
    ; Do what we have to do with VRAM
    [...]
    ; Restore the original configuration
    pop af
    out (MMU_PAGE1), a
    ret