Part 10 - Hijacking the Z80
This article continues on from Part 9 - FBFS File System.
I've been toying with various ideas for giving FPGABee a lightweight way for the user to switch disk images. In FPGABee v1 similar duties for playing cassette files was done by the Peripheral Control Unit (PCU) using a second Z80 instance, included a console mode where commands could be typed, and a key (F12) for switching between the Microbee screen and the PCU.
It worked well but was a bit over-engineered, and was also starting to stretch the capabilities of the Z80 CPU.
This time around I aiming for something a lot simpler - both in implementation and user-interface. After considering a few different options I hit upon the idea of hijacking the Microbee's CPU and having it perform the PCU duties. Since the demands on the PCU are now very minimal - display a simple UI and adjust settings on the disk controller, it really has very little to do. There's no more cassette playback requirement, no disk emulation requirement. I just need to figure out how to get control of it...
Leveraging the NMI
The Z80 CPU supports a non-maskable interrupt, aka NMI. The non-maskable interrupt consists of a single signal on the chip which when asserted causes a call to address 0x0066 at the end of the next instruction. The NMI is currently not-used in FPGABee so it seemed a reasonable option.
I needed this to work in a way that was completely transparent to the Microbee and I figured by performing a bank switch on the entire address space when the NMI is invoked, I should be able to inject code to handle the NMI, perform whatever's needed by the PCU and return to regular execution after switching back to the regular Microbee address space.
Switching into PCU Mode
PCU Mode is a new mode that FPGABee runs in while executing the NMI interrupt handler. It changes the Z80's address decoding to a separate memory/port map. Switching to PCU mode is started by pressing the F12 key. On detecting this, the NMI line is asserted and when the M1 signal is next asserted by the CPU (indicating the start of a new instruction), PCU mode is enabled and the new memory map becomes active.
So, on invoking the NMI, the Z80 saves the current PC address to the Microbee's stack and starts running code at 0x0066 in the PCU address space.
Switching out of PCU Mode
One the PCU code has finished whatever it needs to do, it needs a way to switch back to the Microbee address space. This is a little tricky since the RETN instruction that ends the interrupt handler needs to be read in the PCU's address space, but the return address needs to be popped from the stack in the Microbee's address space. In other words, the switch back to the Microbee's address space needs to be timed to happen mid-instruction.
Instead of having FPGABee's circuitry watch for the RETN instruction, I decided to make it a bit more explicit from the PCU's code. So, first the PCU code must write any value to port 0x80. On seeing the write, FPGABee then watches for the next time a 0x45 byte is read in by the CPU - ie: the second byte of the RETN instruction.
So to switch out of PCU mode, the PCU must execute the following:
OUT (0x80),A ; request exit from disk menu mode RETN ; return to Microbee processing
Tweaking the Boot Sequence
The other thing I've changed is that FPGABee now boots up in PCU mode so that it can perform any startup configuration before handing control to the Microbee. This means a slight tweak when switching from PCU mode to Microbee mode for the first time.
On boot the Z80 starts execution at 0x0000. A normal Microbee on boot returns 0 (nop) for all memory reads until 0x8000 - where the primary ROM is located - is reached. FPGABee calls this startup mode "boot scan".
By starting in PCU mode, I can execute code in the PCU ROM from 0x0000. Once finished it executes the OUT (0x80) to request the exit from PCU mode. If port 0x80 is written to while in the boot scan mode, PCU mode is immediately exited instead of waiting for the RETN. This cause the RETN to never execute and it immediately starts executing the nops up to 0x8000.
To test all this I wrote a NMI handler to toggle some LED's on the Nexys3 board.
The NMI handler switches stacks, saves registers and returns the the point of the last call to YIELD.
ORG 0 JP PCU_MAIN defs 0x66-$ NMI_VECTOR: JP PCU_NMI defs 0x100-$ PCU_NMI: ; Save Microbee state ld (SAVE_MBEE_SP),SP ld SP,SAVE_MBEE_REGS_TOS push AF push BC push DE push HL push IX push IY ; Restore PCU state ld SP,(SAVE_PCU_SP) pop IY pop IX pop HL pop DE pop BC pop AF ; Carry on ret PCU_NMI_END:
YIELD does the opposite - saves the PCU registers, switches stacks and exits back to the Microbee.
YIELD: ; Save PCU state push AF push BC push DE push HL push IX push IY ld (SAVE_PCU_SP),SP ; Restore Microbee state ld SP,SAVE_MBEE_REGS pop IY pop IX pop HL pop DE pop BC pop AF ld SP,(SAVE_MBEE_SP) ; Request PCU exit out (0x80),A retn YIELD_END:
It didn't work at first and some trial and error wasn't really helping. In the end I ran up the T80 core in the simulator to see how it's timing was working. The thing I missed was that the M1 line is actually assert once after the NMI is asserted, but before the jump to 0x0066. To fix it, instead of just watching for the M1 signal - I wait for M1 signal and the address lines to be pointing to 0x0066.
I can now run up the Microbee, run something CPU intensive and press F12 to flash the LEDs - and the Microbee is completely oblivious that tiny slices of time keep disappearing.
Proof of Concept Works
Well it's just a proof on concept, but it proves the concept - that I can hijack the CPU and get it to perform something useful without affecting the Microbee side of things. It sounds overly complex but it actually solves a number of problems:
- I don't need a second micro-controller or CPU - which keeps logic size down (important cause I might try to squeeze this all onto a smaller FPGA chip)
- It simplifies sharing of on board peripherals (RAM/Flash/SD) between the Microbee and the PCU
- Most of the time it has absolutely no impact on the Microbee. It's only on pressing F12 - and even then it's minimal.
- The PCU could be given access to Microbee address space. I could use this to read ROM images from SD card and write them to a block of RAM - RAM that would be read/write for the PCU, but read-only to the Microbee.
That last point is actually quite important as it opens the possibility of running on FPGA boards that don't have parallel flash memory to hold ROM images.
Next is to provide some custom video RAM and controller circuitry for displaying the PCU menu overlaid on the Microbee's display.