AVR-based bootloader and IO card for a Z80 retrocomputer

Similar projects worth following
z80ctrl is an AVR-based bootloader, debugger, and I/O device for a Z80 retrocomputer with the following features:

- Runs CP/M-based software including WordStar, Zork, Ladder, Turbo Pascal and more!
- Plug-in module for RC2014 retrocomputer replaces the clock, serial, rom, and CF boards
- Reads HEX files and CP/M disk images from a FAT32-formatted SD Card
- Provides Altair and RC2014-compatible serial ports
- Full compatibility with unmodified Altair disk images from the SIMH emulator project
- Monitor with memory inspector and built-in Z80 disassembler
- Single-step debugging, breakpoints, and watchpoints to trace Z80 bus activity in real-time
- Flexible MIT-licensed firmware can be the basis of your own SBC or breadboard computer
- Expandable SPI bus allows bridging other SPI peripherals to the Z80

Visit the GitHub Repo for all project artifacts and the GitHub Wiki for documentation.  For ordering information, refer to the Build Instructions.  

Check out the videos below for an in-depth discussion of the project.

Breadboard Overview

Software Demo

Printed Circuit Board Overview

REV3 gerber files for manufacture

x-zip-compressed - 50.56 kB - 06/22/2018 at 14:57


  • Transparent Paging

    J.B. Langston06/28/2018 at 05:14 0 comments

    I previously added manual paging support using the 512KB RAM/ROM card (either the official or Scott Baker versions), which allowed you to manually specify a set of 4 pages and work with the memory at those pages.  

    I've now added support for 20-bit addresses to z80ctrl with automatic paging of up to 1MB memory (addresses from 0x00000-0xFFFFF). The z80ctrl monitor gives the illusion of one flat address space, paging memory in and out as needed.  The manual "page" command has been replaced by a new command called "base", which allows you to specify any address which falls on a page boundary, from 0x00000 to 0xFC000. Because of the page boundary restriction, the base address must be a multiple of 0x4000. Once set, the base address will be added to any address you specify in a monitor command. This makes it convenient to work within a 64KB block memory without having to specify a long address every time.  The base command also controls the memory that is paged in when the Z80 boots.  Initially, the pages will be set to the contiguous 64KB block of memory starting at the base address. The software running on the Z80 is free to page in different pages using the page registers after it starts.

    Here's an example to help make things clearer.  If you set "base 80000", which is the start of RAM, running dump 0 will actually dump from 80000, and dump 100 will actually dump from 80100 (dump will show this address as well). The four pages starting at base address 80000 will also be loaded when the Z80 is booted. If you set the base address to 80000 and then load a program at address 100, it will actually load the program to 80100, and when you type run 100, the pages starting at address 80000 will be paged in, so the Z80 will see the program at address 100 and run as expected. 

    The flash, fill, loadbin, savebin, disasm, poke, dump, run and debug commands now support 20-bit addresses as parameters, and where appropriate, will display 20-bit addresses when listing memory contents. loadhex and savehex currently do not support 20-bit addresses, but I am working on it.  Until then, you can still use 16-bit addresses with these commands, and setting the base address will transparently adjust their starting address so that they work within the 64KB block starting at the base address.

    Support for 20-bit addresses in the flash command means that z80ctrl is now a fully functional flash programmer, and I have successfully used it to program the flash on the 512KB RAM/ROM board with RomWBW.  Actually running RomWBW with the z80ctrl is currently unsupported.  You will have to remove the z80ctrl module and replace it with the clock module and SIO/2 module in order to use RomWBW for now.  I have, however, confirmed that the image flashes properly and RomWBW boots when using these boards.

  • REV3 Boards Built

    J.B. Langston06/22/2018 at 03:06 0 comments

    The REV3 board is built and working great. No issues discovered so far.  This rev features improved performance, a few fixes, and a two-way IORQ line so that the AVR can control peripherals such as the RomWBW paging registers, or my new TMS9918A board directly.  I've detailed the changes in a previous project log, so I won't rehash them here. Instead I'll just share some pictures of the completed REV3 board.  If you want to make your own REV3 board, you can have it manufactured using the gerbers attached to this project, or from the shared design on OSH Park.

  • Companion Project

    J.B. Langston06/15/2018 at 00:18 0 comments

    I have added a companion project for the z80ctrl, a TMS9918/A-based video card for the RC2014 that I'm calling rc9918.  The TMS9918 was used in the TI-99/4A, MSX, ColecoVision, original Sega SG-1000. Enhanced derivatives were used in later MSX computers, the Sega Master System, and the Sega Genesis. It also influenced the architecture of the PPU in the Nintendo Entertainment System.

    My board is based on a circuit described by Tom LeMense for interfacing the TMS9918A with SRAM. Currently, I have breadboarded the circuit and designed the PCB layout, but I haven't had a PCB manufactured yet.

    I have written a few example Z80 assembly programs, including a Mandelbrot generator and a Nyan Cat.

    I am also adding support for the TMS9918 to the z80ctrl.  z80ctrl will automatically initialize the TMS9918/A in text mode at startup and includes new commands tmsfill and tmsdump, which allows the TMS9918's RAM to be filled or examined, and tmslbin, which allows binary data to be loaded directly into VRAM. Although it's not ready for everyday use yet, I have also tested code that will output the monitor interface from z80ctrl on the TMS9918.

  • Flash ROM Support

    J.B. Langston06/07/2018 at 01:56 0 comments

    I have added built-in Flash ROM support directly to the z80ctrl monitor.  There is a flash command, which takes the same arguments as the loadbin command, and an erase command, which takes either an address less than 7ffff (512K) and erases the 4k sector that it lies within, or "all", which will erase the entire chip. After a region of flash has been written once, it must be erased first to be flashed successfully again. 

    The flash command is currently limited to flashing the first 64K of RAM, but that is a limitation of the UI, not the underlying code.  I plan to make paging transparent from the monitor, so that for any command that takes an address, you can just specify any addresses from 0-fffff (1MB), and it will automatically take care of the paging necessary to access that address.  This will apply to the dump, loadhex, loadbin, flash, and other commands.  

    When this is done, the page command will be relegated to setting the default pages for the Z80 to boot from, not controlling the pages used by the monitor.  For convenience, I may provide a "base" command that will allow specifying a base address to be added to 16-bit addresses, but I haven't yet decided if the convenience is worth the added complexity.  Also, "flash" may not remain a separate command, but instead become an option on the loadbin and loadhex commands.

  • Paging Support

    J.B. Langston06/04/2018 at 00:29 0 comments

    I got Scott Baker's 512K RAM/ROM board built, and started modifying z80ctrl to work with it.  The first step was to modify the z80ctrl firmware to do outbound IO requests.  I added io_in and io_out functions to bus.c, which do IO reads and writes directly from the AVR to other peripherals.   I added in and out monitor commands that use these functions to read or write directly to a specific port. These commands can be used with any other hardware, not just the ROM/RAM board. 

    I also added a page command to the monitor that provides easy control over memory page registers on the RAM/ROM board without having to use the low-level I/O commands.  The page command allows 4 different 16KB pages to be specified or just a single page. If only one page is specified, z80ctrl will assign banks 2-4 to the three contiguous pages immediately following the page specified for page 1.  After a reset, the ROM/RAM board defaults to having paging disabled with all 4 banks pointed to the first 16KB or ROM. Pages 00-1f are ROM, and 20-3f are RAM, so in z80ctrl, running "page 20" switches to the first 64KB in RAM, and then programs can be loaded into RAM from SD card as usual.

    One problem with this is that when z80ctrl resets the Z80 to run a program, the paging registers in the RAM/ROM board also get reset.  To get around the problem, I store the last configured pages and automatically issue the I/O commands to reconfigure the paging registers to their previous values after a reset.  Once the Z80 is running, it can control the paging registers directly.

    Part of the intended design of the z80ctrl is that it doesn't really need a ROM at all since it can load programs into RAM from a file on the SD card.  I initially tried to use 512KB RAM chips in both sockets; however, that doesn't work because three of the pins on the ROM (SST39SF040) chip are different than those on the RAM (AS6C4008). The pins are shuffled around as follows: A14 (RAM) -> A15 (ROM), A15 (RAM) -> WE# (ROM), WE# (RAM) -> A14 (ROM).

    It should be possible to cut the traces to those three pins and run some wires to the correct pins so I can use RAM chips in both sockets.  However, for now, I've decided against this.  Instead, I'm planning to add a switch to the loadbin and loadhex commands that will let the ROM chip be flashed with an image directly from the SD card.  The ROM chip doesn't write simply by asserting the WE# line; there is a special command sequence, so I will have to modify my memory load function to support that.  I may eventually design my own board that contains two 512KB RAM chips instead of 1 ROM and 1 RAM.

    So far, I've gotten altmon, scmon, and CP/M to run successfully with the z80ctrl and 512K RAM/ROM boards. I initially had a few bugs, but these have been found and squashed, and everything appears to be working now.

    In my REV3 board, I addressed the hardware changes necessary to safely support bidirectional IORQ usage.  To avoid unintentional hardware damage, these features are disabled by default on REV1/REV2 boards. You can override this safety on REV1/2 boards by adding -DOUTBOUND_IORQ to your CFLAGS in the Makefile.  It's safe to use bi-directional IORQ as long as the z80ctrl IO block jumper is set to the rightmost position, which connects the AVR's IORQ pin directly to the IORQ bus line.  However, if the IO address selection jumper is accidentally left in one of the other positions, the AVR IORQ pin will be connected directly to VCC or to one of the outputs of the 74HCT139, and one or both chips may be damaged.

  • Another Board Revision

    J.B. Langston05/30/2018 at 00:50 0 comments

    Even though REV2 barely saw the light of day (only one person I'm aware of had it manufactured), I am preparing REV3.  The picture above shows the bodge wires on my REV1 board, which has been modified to implement the changes slated for REV3.  These changes were necessary due to some issues found with previous revisions.  

    BUSRQ Optimization

    First, although using BUSRQ instead of manually clocking when handing the data bus back to the Z80 solved the clock jitter that interfered with the sound card, it introduced a pretty serious performance regression. Non-DMA disk IO was over twice as slow as it was using the manual clocking method.  It took about 6 seconds to load the game Ladder before, and 13 seconds after.  I thought it seemed slower but only after rewatching the video I made demonstrating the software did I realize how bad it was. 

    This regression stems from the fact that BUSRQ is located on the IO expander, so toggling it twice for every single-byte IORQ introduced a lot of additional latency (about 4.8 microseconds per IORQ if I calculated it correctly).  To fix this, I have moved BUSRQ to the local AVR PORTB, replacing the IOACK signal. Since BUSRQ has to be asserted any way to hand control back to the Z80, it can be connected directly to the reset line on the flip-flop to deassert the WAIT line.  This saves a pin on the AVR and a few instructions in the code.

    Other Pin Changes

    I also decided that some of the other tradeoffs I originally made when assigning pins to the AVR or the I/O expander were not optimal. MREQ and BUSACK are used during DMA transfers, and now that DMA is used to service disk IO requests, having them on the I/O expander makes the transfers slower than they need to be.  M1 is only needed during debugging and interrupt response, which are both slow anyway because the other signals they use are already on the IO expander, so it seemed like a good tradeoff.  HALT only needs to be sampled periodically and is not highly latency sensitive so it can be moved to the I/O expander.  Therefore, these signals have been swapped between the AVR and I/O expander: HALT for BUSACK, and MREQ for M1.

    Fixed SPI Address

    The initial board design had SPIA0 and SPIA1 signals crossed between the AVR and the 74HCT139.  This caused the SPI addresses 1 and 2 to be backwards.  This was fixed in software, but in REV3 I have fixed it in hardware so that the SPI addresses are now in the intended order.

    Two-Way IORQ

    Second, because the way I set up the IO address decoding, it was not possible to use IORQ as an output to control other peripherals directly unless the jumper was set to address all IO ports (00-FF), which connects the IORQ pin directly to the IORQ bus line.  If the jumper were set to any of the 4 subsets of IO ports, which come from the 2-to-4 decoder, or to address no IO ports, which ties the IORQ pin directly to VCC, setting the IORQ pin to an output would cause  short circuit and potentially damage both the decoder and/or the AVR.

    I fixed this by directly connecting the IORQ pin on the AVR directly to the bus and using the jumper to select only the IORQ signal that goes to the flip-flop. Therefore, it's still possible to select a subrange of IO ports to trigger wait states for, but the AVR won't burn itself out if you do.  This has the downside that it's now necessary to configure the IO port range that the AVR responds to in software to match those configured by the jumper, since otherwise, the AVR will attempt to respond to IORQs for which the flip flop will not trigger a wait state. This will lead to strange behavior any time the Z80 attempts to access one of these IO ports.

    SD Reset

    Next, I had identified an issue where the SD card would get into an inoperable state if the power to the...

    Read more »

  • DMA Debugged

    J.B. Langston05/26/2018 at 21:36 0 comments

    DMA is fully working and stable now.  I just needed to fix a stupid mistake I made on the sectors per track (had 32 hex when it should have been 32 decimal/20 hex), and I needed to disable interrupts during the timing critical parts where the bus mastering takes place.  Overall it's a little over 2X as fast as the traditional Altair Floppy emulation. Not as much as I had hoped for, but there is quite a bit of overhead in setting up a DMA transfer that I didn't think about.  I think that I could probably squeeze out some additional performance by increasing the amount of data I read in a single DMA transfer from 128 to 256 at least, possibly more.  That would require tweaking the SIMH CP/M BIOS though to do deblocking.

    The debugging features of the z80ctrl really came in handy while debugging the DMA features and it's really validated the effort I put into them.  I set an IO read break point on 0xFD to figure out where in memory the code was that was receiving disk parameters from the DMA port. Then I single stepped until I found where it was copying the data that it read from the port. Then I dumped that data and compared it with data at the same location in the SIMH emulator. That's how I discovered that I had made a mistake with 32 hex vs 32 decimal for the sectors per track in the disk parameter block that my emulation sends to CP/M.  The debugger also came in handy when writing the DMA bootloader and several other times throughout the project.

    Lastly, I've also just implemented binary load and save commands so programs can be loaded in from binary as well as hex files.  I used this to load up the ROM from the RC2014 and got BASIC and the bootloader to display their initial messages, but they use interrupt-driven serial input, so I need to figure out a way to forward the interrupts from the AVR to the Z80 to get that working.  I wanted to get interrupts working anyway in order to support a timer, so I will probably work on that next.

  • DMA Disk Transfers

    J.B. Langston05/24/2018 at 13:25 0 comments

    I got DMA disk transfers partially working last night. The SIMH version of CP/M already had support for DMA access implemented in its BIOS, so I implemented a compatible port interface on the z80ctrl. The same disk images that are mounted on A: through H: will be mirrored to I: through J:. When you use drive letters I through J, the disk images are read via DMA, whereas when you use A through H, they are read using Altair Floppy emulation.  

    This division is a function of the SIMH CP/M BIOS since it sends drive numbers 0 through 8 to the DMA port for drives I through J. My DMA implementation does not modify the disk number it receives from CP/M, so the drives end up being mirrored. The CP/M BIOS could be modified to use DMA for all drives, but as I am currently debugging it, this layout is convenient. It allows me to test the DMA functionality when I want to, or ignore it when I just want a working drive.

    The DMA access only uses a single port: 0xFD.  A READ or WRITE command, followed by 6 bytes specifying the drive, sector, track, and DMA address is written to the port, and then a byte is read from the port to initiate the command. The z80ctrl initiates a DMA transfer at the end of the IORQ cycle using the previously saved command and it's parameters. There is also a PARAM command that allows CP/M to interrogate the drive controller about the characteristics of the drive. The DMA disk emulation code is here and the IORQ handler calls the DMA function here.  

    So far it's working well enough that I can go to the drive and run DIR and get a directory listing, but it's still buggy. I'm getting some duplicate entries in my directory listing and I get a lot of bad sector errors.  I intentionally haven't allowed write support until I get the bugs worked out.  As expected it's considerably faster than the old method that transfers one byte at a time through programmed I/O on the Z80.

    Separately, I have also implemented a bootloader directly in the AVR, which is working very well.  There is a new "boot" command that directly loads the boot sector from the disk mounted on drive 0 into RAM and jumps to it.  I have gone ahead and removed the sboot and dboot commands which utilized the traditional Z80 boot ROMs.  Perhaps removing them so soon was a bit aggressive, but if anyone reports any problems with the new bootloader I can bring them back.

  • New and Improved!

    J.B. Langston05/22/2018 at 00:31 0 comments

    I had a marathon hacking session this weekend and added a lot of improvements and new features to z80ctrl:

    • Massive reduction in memory usage.  FatFS had a default configuration that used a separate buffer for each file object. I had 16 static file objects for the drive emulation. After I found the configuration option to switch to shared buffers, along with some other changes such as moving all the strings in my code to flash, static RAM usage on the AVR went from 77% down to 17%.  Memory usage will still go up and down as variable are allocated on the stack in certain functions, but 17% is now the baseline.  A 77% baseline was dangerously close to running out of memory if a function was run that allocated a big array on the stack.
    • Switch to using BUSRQ to negotiate handing the data bus back to the Z80 after an IORQ instead controlling the clock in software. Manually clocking was interfering with other peripherals like Ed Brindley's sound card that rely on a steady clock rate. This resulted in audible pops when the AVR was answering IORQs. Now it plays without any glitches.  I got the idea to use BUSRQ in this way from Just4fun's AVR/Z80 project.
    • Switch to an interrupt-driven buffered UART driver. This makes it much less likely that the AVR will lose characters when the PC is sending it data quickly. It's still possible to overrun the buffers using PuTTY but it's a lot better than it was. Copy and Pasting via TeraTerm now seems to work pretty well, and XMODEM transfers work as long as the baud rate is 115200 or less. Slower baud rates are less likely to overrun the buffer.  As the AVR doesn't have any hardware flow control, this is probably the best I can do to solve this problem.
    • Change to a columnar directory listing a la CP/M or Unix ls, instead of one file per line used before.
    • Dual UART support as demonstrated in the screenshot. Either physical UART can be assigned to either of two virtual UARTs available to the Z80.  So, for example, you can have debugging output from z80ctrl monitor on UART 0, and interact with the Z80 on UART 1.  The default virtual to physical mapping is 0 to 0 and 1 to 1. This can be changed using the "attach" monitor command.
    • Run-time configurable baud rate. The default rate is still 115200 but it can be changed on the fly using the "baud" command. After you enter the command, it calculates the closest actual baud rate the AVR can achieve and prints it out. It flushes the buffer before switching baud rate so that the output doesn't get corrupted by the baud rate change. You will have to change your terminal's baud rate before you can interact any further.  I've tested baud rates from 600 bps all the way up to 1.25Mbps successfully.  You can also configure a custom baud rate at startup using the autoexec functionality (see below).
    • Figured out how to get libc-avr's stdio library working with fatfs, which gives me the ability to transparently use a file in place of stdin, stdout, or stderr. This lays the groundwork for a lot of nice features, the first of which is batch files. Other potential features are the ability to redirect errors to a separate terminal, log debugging info to a file, redirect command output to a file, and so on.
    • Batch files! You can now place text files on your SD card with one z80ctrl monitor command per line and then execute them via the "submit" monitor command.  This is very simple, just a list of commands to execute verbatim--no flow control, variables, etc.  If you place a file named autoexec.z8c in the root directory of your SD card, z80ctrl will execute it automatically upon startup.  This is a nice way to configure your baud rate, attach virtual UARTs, mount drives, even boot CP/M automatically.
    • Run-time configurable Z80 clock rate via the "clkdiv" monitor command. Any number 1-255 can be specifie for clkdiv. One of the PWM's output compare registers is set to the clkdiv value and the other is set to half...
    Read more »

  • Getting Ready for Prime Time

    J.B. Langston05/20/2018 at 00:32 0 comments

    I think that z80ctrl is almost ready for prime time.  I now have had two additional people build working boards. I have identified several issues with the original revision of the board and figured out workarounds for them. I have also made a second revision of the board that should fix these problems. I have at least one user who has ordered a REV2 board and I am awaiting a report from him.  

    To help get the project ready, I have focused a lot on documentation lately.  Have a look at the GitHub Wiki to get started.  New users want to start with the Board Assembly Instructions and Monitor User Guide. Also, check out the list of Compatible Z80 Software to find things to run on your z80ctrl-powered RC2014.  If you're building a REV1 board, be sure to check out the list of Known Issues as well.

    For those interested in a deeper dive on how the z80ctrl works, check out the Hardware and Software Design Notes.  If you're feeling adventurous and don't want to invest in an RC2014, you can also check out the Breadboard Instructions for the first prototype that I built.

    Finally, for a preview of what I have in mind for the future of z80ctrl, check out the Planned Features page.

    Here's hoping you find the new documentation useful! Let me know if you have any feedback.

View all 10 project logs

View all instructions

Enjoy this project?



villaromba wrote 10/08/2018 at 18:19 point

Fully understand - likewise!!!

At some point it would be good to have (as outlined in your possible expansion)

to add other SPI peripherals and implement I/O port interfaces to them for the Z80. Examples: real-time clock, GPIO, 7-segment display, LCD display, etc.

  Are you sure? yes | no

villaromba wrote 10/08/2018 at 16:08 point


Are you going to be adding any further work to this project?


  Are you sure? yes | no

J.B. Langston wrote 10/08/2018 at 18:04 point

Probably at some point, but I have been focused on other stuff lately. Is there something specific you are hoping I would work on?

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates