BBC Basic for the Cerberus 2080 (Part 1)
In the previous post I mentioned at the end that the BIOS, running on the ATMega328p, was only capable of communication one way. There are a couple of mailbox memory locations that are used to pass keystrokes to the 8-bit CPUs (Z80 or 6502), yet no way of receiving data back, other than an acknowledgement that the keystroke has been processed.
In order to make the BIOS a bit more BASIC-friendly, I needed to add a sensible way to send commands from the 8-bit CPUs back to the ATMega328p in order to implement commands like LOAD and SAVE.
The standard BIOS
The standard BIOS reserves two memory locations, 0x0200 and 0x201, for sending commands outbound from the ATMega328p. The process goes something like this:
If the ATMega detects a keystroke on the PS/2 port, then:
- Pauses the CPU
- writes the ASCII code of the keystroke to MAILBOX_DATA (memory location0x0201)
- Then writes the value 0x01 to MAILBOX_FLAG (memory location 0x0200)
- Unpauses the CPU
- Triggers an NMI (Non-Maskable Interrupt)
The 8-bit CPU then handles the keystrokes as follows
Read the value MAILBOX_FLAG, possibly in a NMI routine. If the value is set to 1 then:
- Read the keycode value from memory location MAILBOX_DATA
- Acknowledge the keystroke has been processed by setting memory location MAILBOX_FLAG to zero
My proposed BIOS modifications
The challenge here is to retain as much backwards compatibility as possible, whilst being economical on the amount of RAM that needs to be reserved.
In order to do this, I’ve extended the mailboxes as follows:
- 0x0200: OUTBOX_FLAG (byte)
- 0x0201: OUTBOX_DATA (byte)
- 0x0202: INBOX_FLAG (byte)
- 0x0203: INBOX_DATA (word)
OUTBOX_FLAG and OUTBOX_DATA work as per the standard BIOS, and will be used mostly for sending outbound keystrokes to the 8-bit CPUs.
The new INBOX_FLAG and INBOX_DATA memory locations are used for inbound data from the 8-bit CPUs, and the reason for the data being 16-bits rather than 8-bits will become apparent later.
NB: As the mailboxes have grown in size, the new start vector for the CPUs on RUN is now 0x0205.
In addition to the memory map modifications, BBC Basic for Z80 has implemented the following:
- The NMI is now triggered on a 50hz timer, rather than on keypress.
- The main loop has been modified to send all available ASCII keystrokes, not just the ASCII ones.
- The return-to-BIOS key has been remapped to F12, as Escape is needed by BASIC.
I’ve also added some conditional compilation defines near the top of the source code so that I can set the default CPU and speed, enable the 50hz NMI, turn off some output in order to speed up serial transfer of code via the FTDI cable, and disable the startup jingle for when I’m coding late in the evening.
Inbox Protocol
The inbox protocol is fairly straightforward and works along similar lines to the outbound one.
- The 8-bit CPU writes the data to INBOX_DATA. If the data is greater than 2 bytes in length, i.e. a file command, then this is a pointer to that data.
- It then writes the command code out (between 0x01 and 0x7F) to INBOX_FLAG.
- It then waits in a loop until INBOX_FLAG is not the command code written out in the previous step.
- If it is 0x00, then the command succeeded.
- If it is 0x80 or greater, then an error has occurred. The flag is the error code.
The ATMega processes inbound commands every 50ms:
- Pause the CPU
- Read the INBOX_FLAG
- If it is between 0x01 and 0x7F, then process the command.
- The command will write either 0x00 or an error code with a value greater than 0x80 on completion.
- Unpause the CPU
The command processor is a switch/case statement that uses the INBOX_FLAG as the condition input.
There is a slight drawback to this approach; pausing the CPU every 50hz does slow the 8-bit CPU processor down ever so slightly. The BASIC benchmarks indicate that this performance loss is negligible. One approach would be to reduce the frequency. Maybe poll every 25hz or even slower.
The following commands are supported:
Command Byte | Description | Data |
---|---|---|
0x01: Beep | Sound the beeper | Pointer to a two-word buffer: – Frequency (2 bytes) – Duration (2 bytes) |
0x02: Load | Load a file from SD card directly into RAM | Pointer to a file descriptor buffer: – Start address (2 bytes) – Maximum load size (2 bytes) – Filename (\0 terminated ASCII string) |
0x03: Save | Save a file from RAM to the SD card | Pointer to a file descriptor buffer: – Start address (2 bytes) – Length (2 bytes) – Filename (\0 terminated ASCII string) |
0x04: Delete | Delete a file from the SD card | Pointer to a file descriptor buffer: – Filename (\0 terminated ASCII string) |
0x7F: Reset | Return to the BIOS | N/A |
There are a couple of clear advantages to passing a pointer to the data:
- The BIOS only has to reserve 3 bytes for the inbound protocol, so there is minimal overheads for programs that don’t implement the full command set.
- The program running on the 8-bit CPU can determine where it stores the data for the commands.
- For file operations, the BIOS has full access to the SD card and RAM, so can handle the operation efficiently.
The list is not exhaustive and there will be extra commands in for returning a directory of the SD card, and so on.
What’s Next
The Z80 side of the code is almost complete now, with just *. (directory listing) and a couple of tweaks to the graphics library to add.
The BIOS will need standardising, to ensure that any code proposed between the four main coders (Bernardo, Andy, Gordon and myself) is sufficient for our current projects, and in our opinions, reasonably future-proofed. I also need to tweak the interrupt code; currently this is handling inbound commands. I’d like to keep it so that it just triggers the NMI, and move the inbound command handler to the main loop.