321 lines
13 KiB
C

/**
* PAT80 Emulator
*
* Emulates a PAT80.
* Based on https://github.com/redcode/Z80
*/
#include <Z/constants/pointer.h> /* Z_NULL */
#include <Z80.h>
#include <string.h>
#include <stdlib.h>
#include <ncurses.h>
#define ROM_SIZE 0x8000 /* 32 KiB */
#define MEMORY_SIZE 0xFFFF /* 64 KiB */
#define TERMINAL_WIDTH 60
#define TERMINAL_HEIGHT 25
#define SPACING_BETWEEN_WINDOWS 3
#define INSTRUCTION_WINDOW_HEIGHT 3
typedef struct {
void* context;
zuint8 (* read)(void *context);
void (* write)(void *context, zuint8 value);
} Device;
typedef struct {
zusize cycles;
zuint8 memory[65536];
Z80 cpu;
WINDOW *terminal_win;
WINDOW *status_win;
WINDOW *lcd_top_win; // 40x4 LCD, top two lines
WINDOW *lcd_bottom_win; // 40x4 LCD, bottom two lines
} Machine;
static zuint8 machine_cpu_read(Machine *self, zuint16 address) {
return address < MEMORY_SIZE ? self->memory[address] : 0xFF;
}
static void machine_cpu_write(Machine *self, zuint16 address, zuint8 value) {
if (address >= ROM_SIZE && address < MEMORY_SIZE)
self->memory[address] = value;
}
static zuint8 machine_cpu_in(Machine *self, zuint16 port) {
// Pat80 has 8 devices, decoded based on the 3 most significant IO addr bits.
// Note the Z80 has 16 bit address bus, but only the first 8 are used as IO addr,
// so the 3 most significant IO addr bits in this case are A7, A6, A5. The bits
// A4-A0 may be used by the single device, at its own discretion.
zuint16 bitmask = 7; // 0000000000000111
int ioDevice = port & bitmask;
if (ioDevice <= 0x1F) {
// Port 0 (0x00 to 0x1F): terminal
// Read char from stin
char c = getch();
// Intercept emulator commands
switch (c) {
case 27:
// ESC: shutdown emulator
exit(0); // TODO: Shutdown ncurses and emulator cleanly
return 0;
default:
// Deliver keypress to pat80
return c;
}
}
if (ioDevice <= 0x3F) {
// Port 1 (0x20 to 0x3F): sound card (sn76489)
wprintw(self->status_win, "sound_cmd[IN]: Not supported!\n");
return 0x00;
}
if (ioDevice <= 0x5F) {
// Port 2 (0x40 to 0x5F): LCD top 2 lines
if (port == 0x40 || port == 0x60) {
// TODO: Simulate busy flag and cursor position
return 0x00; // Busy flag clear, for now
}
wprintw(self->status_win, "IO_ERROR_IN: No device at port 2\n");
return 0x00;
}
if (ioDevice <= 0x7F) {
// Port 3 (0x60 to 0x7F): LCD bottom 2 lines
if (port == 0x0) {
// TODO: Simulate busy flag and cursor position
return 0x00;
}
wprintw(self->status_win, "IO_ERROR_IN: No device at port 3\n");
return 0x00;
}
if (ioDevice <= 0x9F) {
// Port 4 (0x80 to 0x9F)
wprintw(self->status_win, "IO_ERROR_IN: No device at port 4\n");
return 0x00;
}
if (ioDevice <= 0x5F) {
// Port 5 (0xA0 to 0xBF)
wprintw(self->status_win, "IO_ERROR_IN: No device at port 5\n");
return 0x00;
}
if (ioDevice <= 0x5F) {
// Port 6 (0xC0 to 0xDF)
wprintw(self->status_win, "IO_ERROR_IN: No device at port 6\n");
return 0x00;
}
if (ioDevice <= 0x5F) {
// Port 7 (0xE0 to 0xFF)
wprintw(self->status_win, "IO_ERROR_IN: No device at port 7\n");
} else {
wprintw(self->status_win, "IO_ERROR_IN: Invalid port address: %#04x\n", port);
}
// TODO: place this in a refresh cycle between CPU instructions
refresh();
wrefresh(self->terminal_win);
wrefresh(self->status_win);
wrefresh(self->lcd_top_win);
wrefresh(self->lcd_bottom_win);
}
static void machine_cpu_out(Machine *self, zuint16 port, zuint8 value) {
// Pat80 has 8 devices, decoded based on the 3 most significant IO addr bits.
// Note the Z80 has 16 bit address bus, but only the first 8 are used as IO addr,
// so the 3 most significant IO addr bits in this case are A7, A6, A5. The bits
// A4-A0 may be used by the single device, at its own discretion.
zuint16 bitmask = 0xE0; // 0000000011100000
int ioDevice = port & bitmask;
bitmask = 0x1F; // 0000000000011111
int ioAddrInsideDevice = port & bitmask;
wprintw(self->status_win, "[%#06x]DECODED[%#04x] - ", port, ioDevice);
if (ioDevice <= 0x1F) {
// Port 0 (0x00 to 0x1F): terminal
wprintw(self->terminal_win, "%c", value);
} else if (ioDevice <= 0x3F) {
// Port 1 (0x20 to 0x3F): sound card (sn76489)
wprintw(self->status_win, "sound_cmd[%#04x]\n", value);
} else if (ioDevice <= 0x5F) {
// Port 2 (0x40 to 0x5F) and Port 3 (0x60 to 0x7F):
// lcd display 40x4 (mod. TM404A, based on 2 KS0066 chips, each one controlling 2 rows,
// top controller at port 2 and bottom at port 3).
// Instruction register at each port first address, data register at second address
if (ioAddrInsideDevice == 0) {
// Port 2, A4 LOW = talking to LCD top 2 lines instruction register
wprintw(self->status_win, "lcd_top_cmd[%#04x]\n", value);
} else if (ioAddrInsideDevice == 1) {
// Port 2, A4 HIGH = talking to LCD top 2 lines data register (writing text to screen)
wprintw(self->status_win, "lcd_top_data[%#04x](%c)\n", value, value);
wprintw(self->lcd_top_win, "%c", value);
} else {
wprintw(self->status_win, "IO_ERROR_OUT: lcd (top controller) does not listen at addr %#04x\n", ioAddrInsideDevice);
}
} else if (ioDevice <= 0x7F) {
// Port 2 (0x40 to 0x5F) and Port 3 (0x60 to 0x7F):
// lcd display 40x4 (mod. TM404A, based on 2 KS0066 chips, each one controlling 2 rows,
// top controller at port 2 and bottom at port 3).
// Instruction register at each port first address, data register at second address
if (ioAddrInsideDevice == 0) {
// Port 3, A4 LOW = talking to LCD bottom 2 lines instruction register
wprintw(self->status_win, "lcd_bottom_cmd[%#04x]\n", value);
} else if (ioAddrInsideDevice == 1) {
// Port 3, A4 HIGH = talking to LCD bottom 2 lines data register (writing text to screen)
wprintw(self->status_win, "lcd_bottom_data[%#04x](%c)\n", value, value);
wprintw(self->lcd_bottom_win, "%c", value);
} else {
wprintw(self->status_win, "IO_ERROR_OUT: lcd (bottom controller) does not listen at addr %#04x\n", ioAddrInsideDevice);
}
} else if (ioDevice <= 0x9F) {
// Port 4 (0x80 to 0x9F)
wprintw(self->status_win, "IO_ERROR_OUT: No device at port 4\n");
} else if (ioDevice <= 0xBF) {
// Port 5 (0xA0 to 0xBF)
wprintw(self->status_win, "IO_ERROR_OUT: No device at port 5\n");
} else if (ioDevice <= 0xDF) {
// Port 6 (0xC0 to 0xDF)
wprintw(self->status_win, "IO_ERROR_OUT: No device at port 6\n");
} else if (ioDevice <= 0xFF) {
// Port 7 (0xE0 to 0xFF)
wprintw(self->status_win, "IO_ERROR_OUT: No device at port 7\n");
} else {
wprintw(self->status_win, "IO_ERROR_OUT: Invalid port address: %#04x\n", port);
}
// TODO: place this in a refresh cycle between CPU instructions
refresh();
wrefresh(self->terminal_win);
wrefresh(self->status_win);
wrefresh(self->lcd_top_win);
wrefresh(self->lcd_bottom_win);
}
static void machine_cpu_halt(Machine *self, unsigned char signal) {
wprintw(self->status_win, "HALTED (%d)\n", signal);
// Refresh all windows
refresh();
wrefresh(self->terminal_win);
wrefresh(self->status_win);
wrefresh(self->lcd_top_win);
wrefresh(self->lcd_bottom_win);
// Wait an ESC before exiting
while (getch() != 27) {}
exit(0);
}
void machine_initialize(Machine *self) {
self->cpu.context = self;
self->cpu.fetch_opcode =
self->cpu.fetch =
self->cpu.nop =
self->cpu.read = (Z80Read )machine_cpu_read;
self->cpu.write = (Z80Write)machine_cpu_write;
self->cpu.in = (Z80Read )machine_cpu_in;
self->cpu.out = (Z80Write)machine_cpu_out;
self->cpu.halt = (Z80Halt)machine_cpu_halt;
self->cpu.nmia = Z_NULL;
self->cpu.inta = Z_NULL;
self->cpu.int_fetch = Z_NULL;
self->cpu.ld_i_a = Z_NULL;
self->cpu.ld_r_a = Z_NULL;
self->cpu.reti = Z_NULL;
self->cpu.retn = Z_NULL;
self->cpu.hook = Z_NULL;
self->cpu.illegal = Z_NULL;
self->cpu.options = Z80_MODEL_ZILOG_NMOS;
}
void machine_power(Machine *self, zbool state) {
if (state)
{
self->cycles = 0;
memset(self->memory, 0, MEMORY_SIZE);
}
z80_power(&self->cpu, state);
}
void machine_reset(Machine *self) {
z80_instant_reset(&self->cpu);
}
void machine_run(Machine *self) {
z80_run(&self->cpu, Z80_MAXIMUM_CYCLES);
}
int main(int argc, char *argv[]) {
// Parse arguments
if (argc < 2) {
printf("Usage: %s [romFile]\n", argv[0]);
exit(0);
}
char* romFilePath = argv[1];
// Init ncurses
initscr(); // Start curses mode
raw(); // Line buffering disabled (get character without waiting for ENTER key)
keypad(stdscr, TRUE); // We get F1, F2 etc..
noecho(); // Don't echo() while we do getch
start_color(); // Use colors
init_pair(1, COLOR_WHITE, COLOR_BLUE); // Terminal window color
init_pair(2, COLOR_YELLOW, COLOR_BLACK); // Status window color
init_pair(3, COLOR_BLACK, COLOR_GREEN); // LCD window color
int x,y;
getmaxyx(stdscr, y,x);
// Setup virtual Pat80 computer
Z80 pat80Cpu = {};
Machine pat80 = {
/*zusize*/ .cycles = 0,
/*Z80*/ .cpu = pat80Cpu,
.terminal_win = newwin(TERMINAL_HEIGHT, TERMINAL_WIDTH, INSTRUCTION_WINDOW_HEIGHT, 0),
.status_win = newwin(y, x - TERMINAL_WIDTH - SPACING_BETWEEN_WINDOWS, INSTRUCTION_WINDOW_HEIGHT, TERMINAL_WIDTH + SPACING_BETWEEN_WINDOWS), // To right of terminal window
.lcd_top_win = newwin(2, 40, INSTRUCTION_WINDOW_HEIGHT + TERMINAL_HEIGHT + SPACING_BETWEEN_WINDOWS, 0), // Below terminal window
.lcd_bottom_win = newwin(2, 40, INSTRUCTION_WINDOW_HEIGHT + TERMINAL_HEIGHT + SPACING_BETWEEN_WINDOWS + 2, 0)
};
wbkgd(pat80.terminal_win, COLOR_PAIR(1)); // Ncurses: set terminal window color
wbkgd(pat80.status_win, COLOR_PAIR(2));
wbkgd(pat80.lcd_top_win, COLOR_PAIR(3));
wbkgd(pat80.lcd_bottom_win, COLOR_PAIR(3));
scrollok(pat80.terminal_win, TRUE); // Ncurses: Allow scrolling when reached end of window
scrollok(pat80.status_win, TRUE);
scrollok(pat80.lcd_top_win, FALSE);
scrollok(pat80.lcd_bottom_win, FALSE);
attron(A_BOLD); // Print instructions
printw("Emulator commands\n");
attroff(A_BOLD);
printw("ESC: Exit");
machine_initialize(&pat80);
machine_power(&pat80, Z_TRUE);
// Load ROM into memory
FILE *romFile;
romFile = fopen(romFilePath,"rb");
if (romFile == NULL) {
printf("Unable to open rom file at %s", romFilePath);
exit(1);
}
fread(&pat80.memory,ROM_SIZE,1,romFile); // load rom from file into memory, up >
fclose(romFile);
// Start emulated computer
machine_reset(&pat80);
machine_run(&pat80);
// Stop ncurses
delwin(pat80.terminal_win);
delwin(pat80.status_win);
delwin(pat80.lcd_top_win);
delwin(pat80.lcd_bottom_win);
endwin();
return 0;
}