At the end of my last post about Big-80, I said this:
At this point it's tempting to start thinking about disk drives, high-res graphics etc... but I've decided to resist and keep the project as simple as possible. The code base is reasonably simple and elegant and to enhance further would complicate it - and to complicate it would spoil it.
Of course, I can never leave well enough alone... and I've been working on some improvements.
The thing that was bugging me most was the need for a specially formatted SD card to hold the cassette tape images: it worked but the selection mechanism (a tape slot number) made it difficult to remember which tape was on which slot and getting images onto and off the SD card was difficult requiring the use of scripts and special tools.
So I've been working on FAT file system support to allow direct copying of files to the SD card using any modern operating system.
Unfortunately this has complicated the implementation project somewhat. To preserve some of its original simplicity I've kept separate "V1" branch. It's here if you're looking for it.
See It In Action
For a better idea of what this post is describing, check out this video which shows the new on-screen menus and FAT file system in action:
The FAT file system is too complicated to work with directly from FPGA hardware. FatFs is an excellent pure C FAT file system library I've used before and knew it would be a good fit for this project.
But where to run it?
Hijacking the Z-80
My initial thought was to run the FatFs code on a second soft processor core on the FPGA. I looked into using Moxie but it was a bit big for the FPGA and I'd prefer use that space for other things down the track (like a disk controller).
Instead, I reverted to a trick I used in FPGABee - hijacking the Z-80. The idea is pretty simple:
- The system runs in one of two modes: Normal (TRS-80) mode or Hijacked mode.
- In hijacked mode, the TRS-80's memory and port mappings are switched out for a separate bank of memory and port mappings.
- When something of interest happens the Z-80's non-maskable interrupt (NMI) is raised and hijacked mode is entered.
- Once the NMI handler has finished, everything switches back and the TRS-80 is blissfully unaware (mostly) that a small slice of time disappeared.
The trick to making this work is the precise timing of the switch between hijacked and normal mode:
- After raising the NMI, the FPGA hardware watches for the Z-80 about to execute an instruction at address 0x0066 (the NMI handler vector). That's the signal to enter hijack mode.
- To exit hijack mode, the NMI handler outputs to a special port that signifies a request to "exit hijack mode". The hardware then watches for a
RETN(return from non-maskable interrupt) or a
JP (HL)instruction. That's the signal to leave hijack mode.
Entering and leaving hijack mode is simply a matter of flipping a register that controls which the address and port mappings are active.
Need More RAM
The Spartan FPGA LX9 FPGA chip has about 72KB of on-chip block RAM. Given the TRS-80 needs 48KB for RAM, 12KB for ROM and 1KB for video RAM that doesn't leave much room.
The Mimas V2 board also has 64MB of LPDDR RAM and the Spartan has a built-in memory controller for working with it.
The simple interface I wrote to hook the LPDDR to the Z-80 is fast enough when running at TRS-80 speeds (1.774 MHz) but requires the occasional wait state when running at the faster 40 MHz "turbo-tape" mode.
It took a lot of work to get this working but I eventually figured it out and the LPDDR RAM is now used for the TRS-80's RAM and ROM as well as for the FatFs code. (Video memory still uses the FPGA block RAMs).
New Boot Procedure
Although the point of all this was to eventually load .cas files from a FAT formatted SD card, a simpler place to start seemed to be just getting the TRS-80 ROM image to load from FAT.
For the first pass at this I wrote a boot loader in C that uses FatFs to load the TRS-80 BASIC ROM image from the SD card, writes it to the area of LPDDR RAM that maps to TRS-80 ROM address and then switches out of hijack mode and jumps to the ROM's entry point.
Compiled with SDCC and embedded into the firmware of the FPGA design it worked as good proof of concept that the FatFs code was working.
Next, realizing I had a lot more code to write for handling the cassette images and since the FPGA project takes about 15 minutes to build I didn't want to have to rebuild the FPGA project for each test run. So the boot procedure now works like this:
- The machine starts in hijacked mode running the embedded firmware.
- The embedded firmware includes a smaller read-only version of FatFs with just enough code to load a file called BIG80.SYS from the SD card.
- Once loaded, the firmware is paged out and execution of BIG80.SYS starts.
- BIG80.SYS loads the TRS-80 BASIC ROM and sets up NMI interrupt handlers.
- Hijack mode is exited and execution of the TRS-80 ROM starts.
This approach let's me quickly update the BIG80.SYS system software on the SD card instead of rebuilding the FPGA project.
But what exactly is BIG80.SYS?
Big-80's System Control Software
BIG80.SYS is the system control software that runs all the background duties required to support the FAT file system and other features such as loading the BASIC ROM during boot up, on-screen menus for selecting files and changing options, transferring data between the SD card and the cassette controller etc...
In the current code base this software is called "SysCon". (I don't really like the name and might change it at some point).
The SysCon software required a fair bit of additional hardware in the FPGA design:
- An Interrupt Controller
- A Small on-screen video overlay (32 x 16 characters, 16 colors)
- The SD Card Controller
- A Keyboard Controller
- A Serial Port Controller
- Address Page Mapping logic
- An Options Control Port (eg: ability to enable/disable things like scan lines)
Aside from the video overlay, these are accessed via Z-80 ports and all were developed in parallel to the SysCon software itself.
A Mini Operating System
The SysCon software is like a miniature operating system and includes things like:
- Support for simple co-operative multi-threading (aka: fibers), including signals and mutexes.
- A memory heap using SDCC's malloc/free implementation
- FAT file system support via FatFs
- A messaging sub-system for dispatching keyboard and other events
- Video drawing functions (text, boxes etc...)
- A simple windowing system
- Window classes for list boxes, message boxes and text entry prompts
- Serial Port I/O functions
- Utility functions like linked lists, string allocations, keyboard scan code to ASCII translation etc...
All of the above works together to run the SysCon software. After loading the BASIC ROM, but before jumping to its entry point it also creates three fibers to perform various tasks:
- UI Fiber - runs the on-screen menus and handles keyboard IRQs.
- Serial Port I/O Fiber - listens for commands on the serial port. Currently supports uploading a file to the SD card and resetting the machine (used during development to upload new builds of big80.sys and saves having to move the SD card between a dev machine and FPGA).
- Cassette Controller Fiber - handles requests from the cassette controller for SD card block numbers for read/write operations.
The Cassette Controller
Finally we get to the whole point of this project - loading cassette images from the SD card.
As mentioned, in version 1 the virtual cassette player uses a custom file system where each cassette image was placed in a well defined consecutive sequence of blocks on the SD card. Slot 1 was blocks 0-31, Slot 2 was blocks 32-63 etc... The hardware has a starting block number and simply moves to the next block when more data is needed.
That approach has now be replaced with a mechanism that works as follows:
- whenever the cassette controller needs to read or write a .cas image it raises an IRQ.
- The cassette fiber receives the IRQ and uses FatFs to seek to the correct place in the .cas image file and then translates the position back to a block number on the SD card.
- The block number is passed back to the cassette controller which it uses to access the the SD card directly (like DMA that doesn't use system bus).
(A priority arbiter co-ordinates the cassette controller and the SysCon software so they can share access to the SD card).
I had to write a couple of additional helper functions in FatFs to translate the position in a FAT file back to the raw block number on the SD card but this hybrid approach of the SysCon software decoding the FAT file system and informing the hardware where to read/write seems to work well.
Aside from the block number look-ups the cassette controller is essentially the same as before - it renders audio from binary data during playback and parses audio back to binary data when recording.
Other Notes of Interest
A couple of interesting notes about all this:
- Although the SysCon software runs on the same Z-80 as the TRS-80 it's automatically overclocked to 40 MHz when in hijack mode (similar to the overclocking used for turbo-tape mode). This minimizes impact on the running TRS-80.
- The SysCon software never blocks in such a way that it stalls the TRS-80 - whenever all active fibers are blocked, hijack mode is exited and the TRS-80 continues to run. Combined with the overclocking, the on-screen menus and all background operations are for all intents and purposes completely invisible to the TRS-80. ie: you can navigate around in the on-screen menus and a TRS-80 game will continue to run unaffected in the background.
- In order to not affect the timing of the TRS80 cassette save and load operations, the cassette audio render and parser components are suspended when in hijack mode.
- The over-clocked turbo-tape mode continues to work as before but required one small tweak. The wait states introduced by the LPDDR RAM when running at 40 MHz slightly affected the timing while loading cassette tapes. Strangely machine code system tapes continued to load fine, but BASIC seemed to be more sensitive and wouldn't load. This was rectified by also stalling the cassette audio render/parse during LPDRR wait states.
I Said It Would Get Complicated
As mentioned at the top, I was reluctant to go down this path because of the complexity it would introduce - and it's certainly more complex now than it was before. But I also enjoyed the challenge and I think it was worth it:
- The on-screen menus are much easier to use than the FPGA board buttons and seven-segment display
- Getting files onto and off the SD card is much, much easier
- It should make porting to another FPGA board simpler since it no longer requires a certain configuration of buttons and LEDs to control it.
- It sets the stage for other types of virtual files such as disk images
- It all still fits on a tiny $50 FPGA board
Out of Room
Although the FPGA board has 64MB RAM, the Z-80 address space is limited to 64KB (note "MB" vs "KB"). All the code I've described above compiles to just under 48KB. Throw in a 8KB heap, 1KB for video memory, a bunch of global variables and buffers and things are getting very tight.
I'm really not going to be able to progress this much further without a bit of re-engineering. The FatFs library compiles to about 28KB so I'm thinking I should be able to move that to a separate overlay and page it in/out as necessary, This would free up a big chunk of space for other things like the code required to handle virtual disk images (DMK, JV1, JV3 etc...).
Version 2 Done
I'm calling that version 2 done. It's a definitely more complex than version 1 but is easier to use and works surprisingly well.
Have thoughts on this? Leave a comment on Twitter.