The goal of this project was to get MSBASIC running in an emulated Z80 processor on an Arduino, with no external hardware/components required. Here’s what it looks like when running:
I went with the Arduino Due as it has 96K of RAM, more than enough for a Z80, as well as 512K flash, and a 32-bit ARM core microcontroller running at 84 MHz. Note that the Due uses 3.3V I/O, not 5V as many of the older Arduino boards. Although this will actually be useful for me down the road…
The Due has four serial ports, including two available on USB connectors, the programming port and the native port. I’m using the native port as the terminal interface for the emulated Z80.
For the Z80 emulation I went with z80emu
For MSBASIC itself I went to Grant Searle’s fabulous site, Grant’s 7-chip Z80 computer in particular, and grabbed his ROM files and assembly files.
There’s source code for both a simple monitor that live at 0000H as well as MSBASIC itself which starts at 0150H:
intmini.asm
basic.asm
Also included are the Intel Hex files for both as well as a combined ROM file:
BASIC.HEX
INTMINI.HEX
ROM.HEX
I decided to use the source files and assemble them myself with the asmx multi-CPU assembler, that way I could make changes if I needed to.
I then wrote my own simple little program to take the resulting object code (.bin file) and convert it into Arduino source code that defined a 8K array of PROGMEM prog_uint8_t bytes, which looks like:
const static prog_uint8_t rom_bytes[] PROGMEM = {
0xF3,0xC3,0xB8,0x00,0xFF,0xFF,0xFF,0xFF,0xC3,0x9F,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,
0xC3,0x74,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xC3,0xAA,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x18,0x00,0xF5,0xE5,0xDB,0x80,0xE6,0x01,
0x28,0x2D,0xDB,0x81,0xF5,0x3A,0x43,0x20,0xFE,0x3F,0x20,0x03,0xF1,0x18,0x20,0x2A,
....
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};
This was then added to my Arduino project as another file, rom.h. When the emulator starts up, it’s copied to the memory array used by the emulator for memory access in the 8K 0000-01FF range where the ROM lives. I have another 64K memory array for RAM, right now the lower 8K is never access, but eventually I’ll add the ability for the Z80 to swap out the ROM and swap it in, for a full 64K of RAM (via an IO port access), once I get CP/M running, which is another project.
I have some benchmarking code that estimates the effective CPU speed. It adds some overhead, but even with that I get about 7.4 MHz, which is actually pretty close to ideal for a “real” Z80.
Now my code:
The Z80 sketch itself, which gets everything going:
extern "C"{
#include "MyZ80Emulation.h"
};
#include "rom.h"
#include
void setup() {
Serial.begin(9600);
Serial.println("Serial Open");
SerialUSB.begin(9600);
SerialUSB.println("Terminal Open");
pinMode(LED_BUILTIN, OUTPUT);
int i;
Serial.println("Copy ROM");
for (i=0; i<8192; i++) InitEmulationRom(i,pgm_read_byte_near(rom_bytes+i) );
Serial.println("InitEmulation");
InitEmulation();
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
EmulateInstructions();
digitalWrite(LED_BUILTIN, LOW);
while (SerialUSB.available()>0)
{
int ch;
ch=SerialUSB.read();
if (ch==127) ch=8; // handle delete key
AddToInputBuf(ch);
}
InterruptZ80IfNeeded();
}
void my_putchar(int c)
{
SerialUSB.write(c);
}
MyZ80Emulation.c which interfaces with the actual emulation code:
//
// MyZ80Emulation.c
//
//
#include "MyZ80Emulation.h"
#include
#include
#include "zextest.h"
#define Z80_CPU_SPEED 4000000 // In Hz.
#define CYCLES_PER_STEP (Z80_CPU_SPEED / 40) // let's do 100k cycles
#define MAXIMUM_STRING_LENGTH 100
ZEXTEST context;
void InitEmulationRom(int address, int data)
{
context.rom[address] = data;
}
void InitEmulation(void)
{
context.is_done = 0;
Z80Reset(&context.state);
context.state.pc = 0x0;
}
void EmulateInstructions(void)
{
double totalCycles, totalTime, khz;
unsigned long time1,time2;
time1=micros();
totalCycles += Z80Emulate(&context.state, CYCLES_PER_STEP, &context);
time2=micros();
return; // uncomment if we want to time the system
if (time2>time1) // ignore case where the microseconds timer wrapped around
{
totalTime=time2-time1;
if (totalTime>0) khz = 1000.0 * totalCycles / totalTime; else khz=0;
printf("totalCycles: %6d totalTime: %6d %6d kHz \n",(int)totalCycles,(int)totalTime,(int)khz);
}
}
// Handle user terminal input, characters stored in a circular buffer
inputBuf[256];
inputBufInPtr=0;
inputBufOutPtr=0;
void AddToInputBuf(int c)
{
inputBuf[inputBufInPtr]=c;
inputBufInPtr++;
}
void InterruptZ80IfNeeded(void)
{
if (inputBufInPtr==inputBufOutPtr) return; //only interrupt if there is a received character in the ACIA
int cycles;
cycles=Z80Interrupt (&context.state, 0x38, &context); // interrupt Z80
}
// Emulate 8650 ACIA, port input
int SystemCallPortIn(ZEXTEST *context, int port)
{
int inputReady=0;
if (inputBufInPtr != inputBufOutPtr) inputReady=1;
if (port==0x80) return (0x02+inputReady); // ACIA status register, always ready to transmit, inputReady decdes if we have an input character
if (port==0x81)
{
if (!inputReady) return (inputBuf[inputBufOutPtr]); // no new data, send last character anyway
char c;
c=inputBuf[inputBufOutPtr];
inputBufOutPtr++;
return c;
}
printf("SystemCallPortIn %04x\n", port); // if not handled above, display this diagnostic message
return 0;
}
// Emulate 8650 ACIA, port output
void SystemCallPortOut(ZEXTEST *context, int port, int x)
{
if (port==0x81) // UART write a byte
{
my_putchar(x);
return;
}
if (port==0x80) // ACIA write control register, we never need to handle this
{
return;
}
printf("SystemCallPortOut %04x %02x\n", port, x); // if not handled above, display this diagnostic message
}
\
MyZ80Emulation.h the header for the above:
//
// MyZ80Emulation.h
//
//
#ifndef MyZ80Emulation_h
#define MyZ80Emulation_h
#include
void SetFuncton(int (*f)(void));
void InitEmulationRom(int address, int data);
void InitEmulation(void);
void EmulateInstructions(void);
void my_putchar(int c);
void AddToInputBuf(int c);
void InterruptZ80IfNeeded(void);
#endif /* MyZ80Emulation_h */