Today, I was able to use the Z80 I built last week to boot Grant Searle’s Z80 BASIC ROM without modifications, with access to both banks of SRAM. You can download this ROM file here.
Several core pieces of functionality had to be implemented to enable full operation, which will be described here.
“UART” from Z80
If you have been reviewing datasheets, you will note that unlike modern microcontrollers, there is no hardware serial RX/TX from the Z80. Instead, this function can be implemented in myriad different ways, depending on agreement between the BIOS (a handful of interrupt vectors counts right?) and the system designer – anything from memory mapped IO to in/out on special ports was OK.
In this case, as I wanted to run Grant Searle’s BASIC ROM, my implemention had to be compatible with what the ROM was expecting. The ROM implemented IO using IOREQ access to ports 0x80 (Control) and 0x81. Both are implemented by the ATMega.
Truth told, I didn’t understand the control register implementation, but I didn’t need to – the BASIC ROM only used this register to check for readiness to send and receive, so I faked it with fixed values, which was good enough to support the minimal requirement of allowing the Z80 to send and receive characters.
This leads us to our next problem…
Send and receive functionality is enough for an extremely primitive terminal, but is not enough for the BASIC console to work. The interrupt stub for the BASIC rom reveals why:
In truth, incoming characters were handled by a reset vector at 0x38 (no equivalent poll loops were present in BASIC.ASM).
I handled this by first converting the AVR’s UART to generate interrupts. This can be done by:
- Setting the RXCIE bit in the UART control register.
- At build/flash-time, registering the UART Receive Complete interrupt vector
- After enabling hardware UART at runtime, enable global interrupts with sei()
This done, I then modified the interrupt handler to buffer the stored character and set a global “interrupt pending” flag, such that the interrupt line could be asserted low at the next available clock cycle. A bit of trickery here – as the Z80 does not execute one instruction per cycle, we must hold the interrupt line until the interrupt is acknowledged, requiring the IOREQ and M1 lines to be held low, as per the following diagram:
To do this, I carved out another line from the AVR-Z80 address bus (remember: we need to pull each “unmanaged” address line to 0 so it doesn’t fragment our memory accesses) for M1. I wrote some handler code to assert an interrupt until acknowledgement, then insert a fake clock cycle to return an interrupt vector, incase I want Mode 2 interrupts in future.
By this point, I had enough for the BASIC interpreter to function, which leads us to our next problem…
SRAM / Bus Width
The BASIC ROM (interrupt stub + BASIC interpreter) was all of 8KB. Our newly shrunken address bus simply was not able to write 8KB to memory by itself – nor did I want to forcibly assert control of the WR/RD lines while the Z80 was held in reset.
Instead, I relied on using a small bootloader from the PSoC project, here. I modified this slightly to complete more quickly (as I only needed 0x2000, not 0x4000 bytes), and hard-coded this into the AVR program.
I then wrote a fake IO port at 0xFF as the original author did, serving up the BASIC ROM. Unfortunately, the BASIC ROM was too large to fit into the AVR’s SRAM, so I modified the code further to place both the bootloader and BASIC ROM into program memory, carefully refactoring code as necessary to support the change.
With all this done, BASIC successfully launches:
As an added bonus, the memory-selftest function indicates that both banks of SRAM are recognized by the Z80, and accessible to the user transparently. Hooray!