Following on from the previous part in this series I now turn to /ROMEN and /RAMRD signals, collectively called Memory Read Select.
When the CPU wants to read from memory the gate array has to decide whether that read should come from RAM or ROM1. This decision is based on the memory bank being accessed and the current ROM enable states.
The memory bank being read from is determined from the states of address lines A15 and A142. The ROM enable states are latched into the gate array when the appropriate register is written to by the Z80. The gate array also needs to sample /MREQ and /RD to determine when a memory read is taking place3.
I haven’t yet implemented the /IORQ functionality required for the gate array to update the ROM enable states so I’ll leave placeholders for that to be added later. For now I’ll default to having both ROMs permanently enabled. This is the default state at reset, and is required for the system to be able to boot (think about what would happen if the lower ROM was not enabled at start up) so there’s enough functionality here for the computer to run.
A Jump Table in a PIO?
Implementing this functionality within an RP PIO gives an interesting challenge. We need to wait for an appropriate state on two pins (/MREQ low, /RD high) then set two pins (/ROMEN, /RAMRD) dependent on the state to two pins (A15, A14) and two internal latches (upper ROM enable, lower ROM enable). We also need the gate array to respond very quickly to the input states.
At first this seems like way too much for a PIO to be able to handle but the PIOs have a neat little trick which can help here: the program counter of a PIO can be set from a register. Thus I can read the state of the pins onto the ISR (input shift register), then move that value into PC (the program counter). The PIO will then start executing code at the address read from the pins.
The code at the each target address needs to set the output pins as required for the appropriate combination of input pins and ROM enable states. When the ROM enable states change this can be handled by writing new instructions into the jump table at the relevant address. The new instructions will write the appropriate values to /ROMEN and /RAMRD when executed. This updating of instructions happens when the CPU writes to the gate array, ie. during an /IORQ, and is not highly time dependent. Thus there is plenty of time for this to be handled by main core code.
The Code
Starting at the end of this process with the jump table gives the following code
;addr a15 a14 rd
PUBLIC opcode_lorom:
jmp loop side 0b10 ;0 0 0 0 &0000-&3fff - To be MODIFIED
jmp loop side 0b11 ;1 0 0 1
PUBLIC opcode_ramrd:
jmp loop side 0b01 ;2 0 1 0 &4000-&7fff - Always RAM
jmp loop side 0b11 ;3 0 1 1
jmp loop side 0b01 ;4 1 0 0 &8000-&bfff - Always RAM
jmp loop side 0b11 ;5 1 0 1
PUBLIC opcode_hirom:
jmp loop side 0b10 ;6 1 1 0 &c000-&ffff - To be MODIFIED
jmp loop side 0b11 ;7 1 1 1
Each instruction is a jump back to the main loop (which we’ll see later). Each instruction also has a sideset
to set the ROMEN and RAMRD pins. The code above shows which target addresses always affect RAM, which are dependent on ROM enable states, and which correspond to a write and send neither output signal. The default states here correspond to the default described earlier, ie. both ROMs enabled4.
The public labels in the above code will be used by the main core to read and write the instructions and I’ll describe them below.
The side setting here is configured to use two pins/bits, and to be optional. Being optional means that not every instruction needs to perform a sideset
. This is required so the state of the output pins can be preserved by other instructions.
.side_set 2 opt
The Loop
loop:
mov isr null ;Clear ISR and counter
wait 1 gpio mreq_pin ;Wait for end of memory cycle
wait 0 gpio mreq_pin side 0b11 [2] ;Wait for memory cycle…
;and reset out pins
;Delay [] because /RD goes low
;about 10ns after /MREQ does.
in pins, 3 ;Read pins (A15, A14, /RD). Left shifts them into
;high bits of ISR
mov pc :: isr ;Jump to address read in. Reverse bit order, which puts
;data in low three bits
The main loop is fairly simple and begins by clearing the ISR. This is necessary because we’ll only be reading three bits into it and any old data would cause the jump to fail.
Next the PIO waits for the end of the current memory cycle (as indicated by /MREQ), then waits for the start of the next memory cycle. This instruction also sidesets the output pins to cancel the previous memory cycle – sidesets take place as the instruction is executed. The wait (for the pin) happens as a result of the instruction being executed. Thus we need to add the sideset
to the WAIT 0
, not the previous WAIT 1
. Finally this instruction (the WAIT 0
) waits for two cycles (the [2]). This gives time for the Z80 to update both the /MREQ and /RD pins. In my experience there can be a few nanoseconds difference between the two signals being asserted by a 4MHz NMOS Z80, and the RP2350 is running so fast that it’s wise to give the old chip some leg room here.
Once the WAIT 0
has finished executing the IN PINS,3
reads the states of A15, A14 and /RD into the ISR, which is followed by the MOV
that copies this value to the program counter. However the IN instruction shifts the pin values into the most significant bits of the ISR. We need those values in the least significant bits of the program counter to use them as an address. The MOV
instruction can perform this bit reversal using the :: operator5.
Initialisation
The configuration function for the PIO is pretty standard so I’ll move on to the initialisation function.
void init_romen_ramrd() {
uint offset = 0; //Program must be loaded at offset zero
pio_add_program_at_offset(ROMEN_PIO, &romen_ramrd_program, offset);
printf("ROMEN_RAMRD program loaded at %d\n", offset);
romen_ramrd_program_init(ROMEN_PIO, ROMEN_SM, offset, A15_PIN, ROMEN_PIN);
//Data we need later when settings are changed.
//Grab the opcodes we'll need to poke in when settings are changed
rom_enable_opcode =
romen_ramrd_program_instructions[romen_ramrd_offset_opcode_lorom];
rom_disable_opcode =
romen_ramrd_program_instructions[romen_ramrd_offset_opcode_ramrd];
pio_sm_set_enabled(ROMEN_PIO, ROMEN_SM, true);
}
The usual way to add a PIO program uses the pio_add_program
function and this function selects an offset address (ie. an address in the PIOs address space) at which to load the program6. However, using the jump table means this PIO must be loaded at offset zero in order for the jumps to work properly. Therefore I’m using the pio_add_program_at_offset
function which loads the program at the specified offset.
The other non-standard piece of code is the two lines to set the rom_enable_opcode
and rom_disable_opcode
variables (declared as ints). These lines read opcodes from the instruction address space of the PIO. The PIOs instruction space appears as an array within the address space of the main core which may seem confusing but these are just memory mapped registers of the PIO7. The indexes used here are the public labels that were declared in the PIO code. Note how the compiler (or is it the assembler?) prepends the PIO program name to them to help ensure uniqueness.
Testing
With all of that done it’s onto testing. For the tests below I’ve hooked /MREQ up to /CPU for ease of testing and the other inputs are just pulled to 5V or ground on a breadboard. The scope traces should speak for themselves.





Footnotes
- Memory writes always go to RAM, even if a ROM has been enabled ‘above’ that memory region. Clearly writing to ROM wouldn’t make any sense, and this design means that video memory can always be written to without the penalty of having to disable – and re-enable – the upper ROM.
- Note here that the port address of the Gate Array is dependent on address lines A15 being low. Using A15 as the ‘chip select’ in this way allowed them to save a pin on the gate array in a design which already maxes out the number of available pins on the gate array. (The gate array also verifies that A14 is high but this is probably more to do with the pin already being available to the gate array).
- And another pin saving: the gate array has access to /RD but not /WR. The state of /WR being implied by the states of /MREQ and /RD.
- When reading the binary values for each
sideset
bear in mind that the LSB is on the right and the MSB on the left. My mental image of the GPIOs puts the lowest bit to the left (or above) – the opposite to my mental image of bits in a byte. This reversing of directions between hardware and software has caused me far more debugging time that I care to admit. - You can configure that
IN
instruction to shift data into the least significant bits which might obviate the need for this bit reversal. However that might (I’m not sure offhand) result in the bit being in the opposite order and, thus, affect that ordering of the jump table. The :: operator does not slow down theMOV
instruction so there’s no benefit to such a rewrite - You might assume, as I did when I first wrote this code, that
pio_add_program
would load programs to the lowest available address with the first program thereby being loaded to address zero. However the routine actually loads programs at the highest available address. Beware of assumptions! - If you wonder where these memory mapped addresses (and related data structures and other constants) come from: the library source code will have been installed with the Pico SDK and you can find them by reading it. It can take a bit of drilling to find the appropriate files. The Raspberry Pi documentation is excellent but it doesn’t tell you everything, and it’s not always correct (or up to date).