/* czx81 - ZX81 CURSES emulator Copyright (C) 2020 Ian Cowburn This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "wide_curses.h" #include #include #include #include #include #include #include "z80.h" #include "zx81rom.h" /* -------------------------------------------------- MACROS */ #define ROMLEN 0x2000 #define ROM_SAVE 0x2fc #define ROM_LOAD 0x347 #define ED_SAVE 0xf0 #define ED_LOAD 0xf1 #define ED_WAITKEY 0xf2 #define ED_ENDWAITKEY 0xf3 #define ED_PAUSE 0xf4 #define SLOW_TSTATES 16000 #define FAST_TSTATES 64000 #define E_LINE 16404 #define LASTK1 16421 #define LASTK2 16422 #define MARGIN 16424 #define FRAMES 16436 #define CDFLAG 16443 #define DFILE 0x400c #define PEEKW(addr) (mem[addr] | (Z80Word)mem[addr+1]<<8) #define POKEW(addr,val) do \ { \ Z80Word wa = addr; \ Z80Word wv = val; \ mem[wa] = wv; \ mem[wa+1] = wv>>8; \ } while(0) #define TXT_W 32 #define TXT_H 24 /* -------------------------------------------------- GLOBALS */ static int quit = FALSE; static Z80Val FRAME_TSTATES = FAST_TSTATES; static Z80Byte mem[0x10000]; static Z80Word RAMBOT; static Z80Word RAMTOP; static int waitkey = FALSE; static int started = FALSE; static unsigned prev_lk1; static unsigned prev_lk2; static Z80 *z80; static Z80Byte matrix[8]; static wchar_t scrcode[0x40] = { L' ', L'\u2598', L'\u259d', L'\u2580', /* 00 - 03 */ L'\u2596', L'\u258c', L'\u259e', L'\u259b', /* 04 - 07 */ L'\u2592', L'\u2584', L'\u2580', L'"', /* 08 - 0b */ L'\u00a3', L'$', L':', L'?', /* 0c - 0f */ L'(', L')', L'>', L'<', /* 10 - 13 */ L'=', L'+', L'-', L'*', /* 14 - 17 */ L'/', L';', L',', L'.', /* 18 - 1b */ L'0', L'1', L'2', L'3', /* 1c - 1f */ L'4', L'5', L'6', L'7', /* 20 - 23 */ L'8', L'9', L'A', L'B', /* 24 - 27 */ L'C', L'D', L'E', L'F', /* 28 - 2b */ L'G', L'H', L'I', L'J', /* 2c - 2f */ L'K', L'L', L'M', L'N', /* 30 - 33 */ L'O', L'P', L'Q', L'R', /* 34 - 37 */ L'S', L'T', L'U', L'V', /* 38 - 3b */ L'W', L'X', L'Y', L'Z', /* 3c - 3f */ }; static int key_press_frames = 1; static int current_key_press_frame = 0; static int current_key = -1; static int trace = FALSE; static char snappath[4096] = "."; static int allow_save = FALSE; static struct { int key; /* The curses keycode */ int no; /* The number of keys to press */ int row[5]; /* The rows for each keypress */ int bit[5]; /* The bit number for each keypress */ } keymap[] = { {'1', 1, {3}, {0x01}}, {'2', 1, {3}, {0x02}}, {'3', 1, {3}, {0x04}}, {'4', 1, {3}, {0x08}}, {'5', 1, {3}, {0x10}}, {'6', 1, {4}, {0x10}}, {'7', 1, {4}, {0x08}}, {'8', 1, {4}, {0x04}}, {'9', 1, {4}, {0x02}}, {'0', 1, {4}, {0x01}}, {'q', 1, {2}, {0x01}}, {'w', 1, {2}, {0x02}}, {'e', 1, {2}, {0x04}}, {'r', 1, {2}, {0x08}}, {'t', 1, {2}, {0x10}}, {'y', 1, {5}, {0x10}}, {'u', 1, {5}, {0x08}}, {'i', 1, {5}, {0x04}}, {'o', 1, {5}, {0x02}}, {'p', 1, {5}, {0x01}}, {'a', 1, {1}, {0x01}}, {'s', 1, {1}, {0x02}}, {'d', 1, {1}, {0x04}}, {'f', 1, {1}, {0x08}}, {'g', 1, {1}, {0x10}}, {'h', 1, {6}, {0x10}}, {'j', 1, {6}, {0x08}}, {'k', 1, {6}, {0x04}}, {'l', 1, {6}, {0x02}}, {'\n', 1, {6}, {0x01}}, /* Shift key skipped */ /* {'', 1, {0}, {0x01}}, */ {'z', 1, {0}, {0x02}}, {'x', 1, {0}, {0x04}}, {'c', 1, {0}, {0x08}}, {'v', 1, {0}, {0x10}}, {'b', 1, {7}, {0x10}}, {'n', 1, {7}, {0x08}}, {'m', 1, {7}, {0x04}}, {'.', 1, {7}, {0x02}}, {' ', 1, {7}, {0x01}}, /* Shifted alpha */ {'Q', 2, {0, 2}, {0x01, 0x01}}, {'W', 2, {0, 2}, {0x01, 0x02}}, {'E', 2, {0, 2}, {0x01, 0x04}}, {'R', 2, {0, 2}, {0x01, 0x08}}, {'T', 2, {0, 2}, {0x01, 0x10}}, {'Y', 2, {0, 5}, {0x01, 0x10}}, {'U', 2, {0, 5}, {0x01, 0x08}}, {'I', 2, {0, 5}, {0x01, 0x04}}, {'O', 2, {0, 5}, {0x01, 0x02}}, {'P', 2, {0, 5}, {0x01, 0x01}}, {'A', 2, {0, 1}, {0x01, 0x01}}, {'S', 2, {0, 1}, {0x01, 0x02}}, {'D', 2, {0, 1}, {0x01, 0x04}}, {'F', 2, {0, 1}, {0x01, 0x08}}, {'G', 2, {0, 1}, {0x01, 0x10}}, {'H', 2, {0, 6}, {0x01, 0x10}}, {'J', 2, {0, 6}, {0x01, 0x08}}, {'K', 2, {0, 6}, {0x01, 0x04}}, {'L', 2, {0, 6}, {0x01, 0x02}}, {'Z', 2, {0, 0}, {0x01, 0x02}}, {'X', 2, {0, 0}, {0x01, 0x04}}, {'C', 2, {0, 0}, {0x01, 0x08}}, {'V', 2, {0, 0}, {0x01, 0x10}}, {'B', 2, {0, 7}, {0x01, 0x10}}, {'N', 2, {0, 7}, {0x01, 0x08}}, {'M', 2, {0, 7}, {0x01, 0x04}}, /* Shifted alpha shortcuts */ {'\'', 2, {0, 2}, {0x01, 0x01}}, {'$', 2, {0, 5}, {0x01, 0x08}}, {'(', 2, {0, 5}, {0x01, 0x04}}, {')', 2, {0, 5}, {0x01, 0x02}}, {'"', 2, {0, 5}, {0x01, 0x01}}, {'^', 2, {0, 6}, {0x01, 0x10}}, {'-', 2, {0, 6}, {0x01, 0x08}}, {'+', 2, {0, 6}, {0x01, 0x04}}, {'=', 2, {0, 6}, {0x01, 0x02}}, {':', 2, {0, 0}, {0x01, 0x02}}, {';', 2, {0, 0}, {0x01, 0x04}}, {'?', 2, {0, 0}, {0x01, 0x08}}, {'/', 2, {0, 0}, {0x01, 0x10}}, {'*', 2, {0, 7}, {0x01, 0x10}}, {'<', 2, {0, 7}, {0x01, 0x08}}, {'>', 2, {0, 7}, {0x01, 0x04}}, {',', 2, {0, 7}, {0x01, 0x02}}, /* Cursor keys */ {KEY_LEFT, 2, {0, 3}, {0x01, 0x10}}, {KEY_DOWN, 2, {0, 4}, {0x01, 0x10}}, {KEY_UP, 2, {0, 4}, {0x01, 0x08}}, {KEY_RIGHT, 2, {0, 4}, {0x01, 0x04}}, /* Function keys */ {KEY_F(1), 2, {0, 3}, {0x01, 0x01}}, {KEY_F(2), 2, {0, 3}, {0x01, 0x02}}, {KEY_F(3), 2, {0, 3}, {0x01, 0x04}}, {KEY_F(4), 2, {0, 3}, {0x01, 0x08}}, {KEY_F(5), 2, {0, 4}, {0x01, 0x02}}, {KEY_F(6), 2, {0, 6}, {0x01, 0x01}}, {KEY_F(7), 2, {0, 7}, {0x01, 0x01}}, /* Backspace/delete */ {KEY_BACKSPACE,2, {0, 4}, {0x01, 0x01}}, {KEY_DC, 2, {0, 4}, {0x01, 0x01}}, {127, 2, {0, 4}, {0x01, 0x01}}, {ERR, 0, {0}, {0}} }; static char message[1024]; /* -------------------------------------------------- UTILS */ static long TimeDiff(const struct timespec *start, const struct timespec *end) { long diff; diff = (end->tv_nsec - start->tv_nsec) + (end->tv_sec - start->tv_sec) * 1000000000L; return diff; } static void ClearScreen(void) { clear(); } static void RenderScreen(void) { static int message_delay = 0; mvprintw(0, 40, "Message:"); if (message[0] && message_delay == 0) { message_delay = 250; } if (message_delay > 0) { mvprintw(1, 40, message); if (--message_delay == 0) { message[0] = 0; } } refresh(); } static FILE *OpenTapeFile(Z80Word addr, const char *mode) { static const char zx_chars[] = "\"#$:?()><=+-*/;,." "0123456789" "abcdefghijklmnopqrstuvwxyz"; char full_fn[4096]; char fn[4096]; int f; int done; done = FALSE; f = 0; while(!done) { int ch; ch = mem[addr++]; if (ch & 0x80) { done = TRUE; ch &= 0x7f; } if (ch >= 11 && ch <=63) { fn[f++] = zx_chars[ch - 11]; } } fn[f] = 0; sprintf(full_fn, "%s/%s", snappath, fn); return fopen(full_fn, mode); } static void ReadConfig(void) { char fn[4096] = {0}; char *env; FILE *fp; if ((env = getenv("HOME"))) { strcpy(fn, env); strcat(fn, "/"); } strcat(fn, ".czx81rc"); if ((fp = fopen(fn, "r"))) { char s[4096]; while(fgets(s, sizeof s, fp)) { size_t l; char *tok1 = NULL; char *tok2 = NULL; l = strlen(s); while(l && s[l - 1] == '\n') { s[--l] = 0; } tok1 = strtok(s, " \t"); if (tok1) { tok2 = strtok(NULL, " \t"); } if (strcmp(tok1, "snappath") == 0) { if (tok2) { strcpy(snappath, tok2); } } else if (strcmp(tok1, "allowsave") == 0) { allow_save = TRUE; } } fclose(fp); } } /* -------------------------------------------------- ZX81 CODE */ static void RomPatch(const Z80Byte patch[], Z80Word addr) { int f; for(f = 0; patch[f] != 0xff; f++) { mem[addr++] = patch[f]; } } static void ZX81HouseKeeping(void) { unsigned row; unsigned lastk1; unsigned lastk2; /* British ZX81 */ mem[MARGIN]=55; /* Update FRAMES */ if (FRAME_TSTATES == SLOW_TSTATES) { Z80Word frame = PEEKW(FRAMES) & 0x7fff; if (frame) { frame--; } POKEW(FRAMES, frame|0x8000); } if (!started) { prev_lk1 = 0; prev_lk2 = 0; return; } /* Update LASTK */ lastk1 = 0; lastk2 = 0; for(row = 0; row < 8; row++) { unsigned b; b = (~matrix[row]&0x1f)<<1; if (row == 0) { unsigned shift; shift = b&2; b &= ~2; b |= (shift>>1); } if (b) { if (b > 1) { lastk1 |= (1<PC; int f; for(f = 3; f < 24; f++) { mvprintw(f, 40, "%4.4x:", addr); mvprintw(f, 45, "%-35.35s", Z80Disassemble(z80, &addr)); } f = 26; if (f < LINES) mvprintw(f++, 0, "AF = %4.4x", z80->AF.w); if (f < LINES) mvprintw(f++, 0, "BC = %4.4x", z80->BC.w); if (f < LINES) mvprintw(f++, 0, "DE = %4.4x", z80->DE.w); if (f < LINES) mvprintw(f++, 0, "HL = %4.4x", z80->HL.w); if (f < LINES) mvprintw(f++, 0, "IX = %4.4x", z80->IX.w); if (f < LINES) mvprintw(f++, 0, "IY = %4.4x", z80->IY.w); if (f < LINES) mvprintw(f++, 0, "SP = %4.4x", z80->SP); if (f < LINES) mvprintw(f++, 0, "MATRIX = %2.2x/%2.2x/%2.2x/%2.2x/%2.2x" "/%2.2x/%2.2x/%2.2x", matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6], matrix[7]); refresh(); } static int CheckTimers(Z80 *z80, Z80Val val) { if (trace) { Trace(); } if (val >= FRAME_TSTATES) { Z80ResetCycles(z80, val - FRAME_TSTATES); if (started && ((mem[CDFLAG] & 0x80) || waitkey)) { FRAME_TSTATES = SLOW_TSTATES; } else { FRAME_TSTATES = FAST_TSTATES; } DrawScreen(); if (z80->SP < 0x8000) { ZX81HouseKeeping(); } return FALSE; } else { return TRUE; } } static void PressKey(int key) { int f; if (current_key_press_frame > 0) { if (--current_key_press_frame <= 0) { current_key_press_frame = 0; for(f = 0; f < keymap[current_key].no; f++) { matrix[keymap[current_key].row[f]] |= keymap[current_key].bit[f]; } current_key = -1; } } else { if (key != ERR) { int n; for(n = 0; keymap[n].key != ERR && current_key == -1; n++) { if (keymap[n].key == key) { current_key_press_frame = key_press_frames; current_key = n; for(f = 0; f < keymap[current_key].no; f++) { matrix[keymap[current_key].row[f]] &= ~keymap[current_key].bit[f]; } } } } } } static int EDCallback(Z80 *z80, Z80Val data) { switch((Z80Byte)data) { case ED_SAVE: if (allow_save && z80->DE.w < 0x8000) { FILE *fp; fp = OpenTapeFile(z80->HL.w, "wb"); if (fp) { int f; int end; f = 0x4009; end = PEEKW(E_LINE); while(f <= end) { fputc(mem[f++], fp); } fclose(fp); } else { sprintf(message, "Failed to open file"); } } break; case ED_LOAD: if (z80->DE.w < 0x8000) { FILE *fp; fp = OpenTapeFile(z80->DE.w, "rb"); if (fp) { int c; Z80Byte *a; a = mem + 0x4009; while((c = getc(fp)) != EOF) { *a++ = c; } fclose(fp); } else { sprintf(message, "Failed to open file"); } } mem[CDFLAG] = 0xc0; break; case ED_WAITKEY: waitkey = TRUE; started = TRUE; break; case ED_ENDWAITKEY: waitkey = FALSE; break; case ED_PAUSE: { Z80Word pause; struct timespec start; waitkey = TRUE; pause = z80->BC.w; while(pause-- && !(mem[CDFLAG]&1)) { struct timespec end; struct timespec pause = {0}; ClearScreen(); PressKey(getch()); CheckTimers(z80, FRAME_TSTATES); RenderScreen(); clock_gettime(CLOCK_MONOTONIC, &end); start = end; pause.tv_nsec = 20000000 - TimeDiff(&start, &end); if (pause.tv_nsec > 0 && FRAME_TSTATES != FAST_TSTATES) { nanosleep(&pause, NULL); } } waitkey = FALSE; break; } } return TRUE; } static Z80Byte ZX81ReadMem(Z80 *z80, Z80Word addr) { return mem[addr]; } static void ZX81WriteMem(Z80 *z80, Z80Word addr, Z80Byte val) { if (addr >= RAMBOT && addr <= RAMTOP) { mem[addr] = val; } } static Z80Byte ZX81ReadPort(Z80 *z80, Z80Word port) { Z80Byte b = 0; switch(port & 0xff) { case 0xfe: switch(port & 0xff00) { case 0xfe00: b = matrix[0]; break; case 0xfd00: b = matrix[1]; break; case 0xfb00: b = matrix[2]; break; case 0xf700: b = matrix[3]; break; case 0xef00: b = matrix[4]; break; case 0xdf00: b = matrix[5]; break; case 0xbf00: b = matrix[6]; break; case 0x7f00: b = matrix[7]; break; } /* Some code expects some of the top bits set. Whether this is a good idea or not is unsure. */ b |= 0x60; break; default: b = 0xff; break; } return b; } static void ZX81WritePort(Z80 *z80, Z80Word port, Z80Byte val) { } static void ZX81Reset(void) { int f; Z80Reset(z80); FRAME_TSTATES = FAST_TSTATES; /* 16K of memory */ RAMBOT = 0x4000; RAMTOP = RAMBOT + 0x4000; for(f = RAMBOT; f <= RAMTOP; f++) { mem[f] = 0; } /* Keyboard matrix */ for(f = 0; f < 8; f++) { matrix[f] = 0x1f; } /* Fill the upper 32K with RET opcodes for ULA reads */ for(f = 0x8000; f < 0x10000; f++) { mem[f] = 0xc9; } } static void ZX81Init(void) { static const Z80Byte save[]= { 0xed, ED_SAVE, /* (SAVE) */ 0xc3, 0x07, 0x02, /* JP $0207 */ 0xff /* End of patch */ }; static const Z80Byte load[]= { 0xed, ED_LOAD, /* (LOAD) */ 0xc3, 0x07, 0x02, /* JP $0207 */ 0xff /* End of patch */ }; static const Z80Byte fast_hack[]= { 0xed, ED_WAITKEY, /* (START KEY WAIT) */ 0xcb,0x46, /* L: bit 0,(hl) */ 0x28,0xfc, /* jr z,L */ 0xed, ED_ENDWAITKEY, /* (END KEY WAIT) */ 0x00, /* nop */ 0xff /* End of patch */ }; static const Z80Byte kbd_hack[]= { 0x2a,0x25,0x40, /* ld hl,(LASTK) */ 0xc9, /* ret */ 0xff /* End of patch */ }; static const Z80Byte pause_hack[]= { 0xed, ED_PAUSE, /* (PAUSE) */ 0x00, /* nop */ 0xff /* End of patch */ }; /* Load and patch ROM */ memcpy(mem, ZX81ROM, ROMLEN); RomPatch(save, ROM_SAVE); RomPatch(load, ROM_LOAD); RomPatch(fast_hack, 0x4ca); RomPatch(kbd_hack, 0x2bb); RomPatch(pause_hack, 0xf3a); mem[0x21c] = 0x00; mem[0x21d] = 0x00; mem[0x0079] = 0; mem[0x02ec] = 0; /* Lodge Z80 callbacks */ Z80LodgeCallback(z80, eZ80_EDHook, EDCallback); Z80LodgeCallback(z80, eZ80_Instruction, CheckTimers); /* Mirror the ROM */ memcpy(mem + ROMLEN, mem, ROMLEN); ZX81Reset(); } /* -------------------------------------------------- MENU */ static void Menu() { int done = FALSE; erase(); while(!done) { int ch; mvprintw(0, 0, "F1 .. Tracing %s", trace ? "ON ":"OFF"); mvprintw(2, 0, "F2 .. Frames per key press: %2d", key_press_frames); mvprintw(4, 0, "F3 .. Reset ZX81"); mvprintw(6, 0, "F4 .. Quit"); mvprintw(8, 0, "Press ESCAPE to resume"); refresh(); ch = getch(); switch(ch) { case KEY_F(1): trace = !trace; break; case KEY_F(2): if (++key_press_frames == 10) { key_press_frames = 1; } break; case KEY_F(3): ZX81Reset(); break; case KEY_F(4): quit = TRUE; done = TRUE; break; case 27: done = TRUE; break; default: break; } } } /* -------------------------------------------------- MAIN */ int main(void) { struct timespec start; setlocale(LC_ALL, ""); ReadConfig(); z80 = Z80Init(ZX81ReadMem, ZX81WriteMem, ZX81ReadPort, ZX81WritePort, ZX81ReadMem); if (!z80) { fprintf(stderr, "Failed to initialise the Z80 CPU\n"); return EXIT_FAILURE; } ZX81Init(); initscr(); raw(); noecho(); keypad(stdscr, TRUE); nodelay(stdscr, TRUE); clock_gettime(CLOCK_MONOTONIC, &start); while(!quit) { struct timespec end; struct timespec pause = {0}; int ch; ClearScreen(); Z80Exec(z80); ch = getch(); switch(ch) { case 3: quit = TRUE; break; case 27: nodelay(stdscr, FALSE); Menu(); ClearScreen(); nodelay(stdscr, TRUE); break; default: PressKey(ch); break; } RenderScreen(); clock_gettime(CLOCK_MONOTONIC, &end); start = end; pause.tv_nsec = 20000000 - TimeDiff(&start, &end); if (pause.tv_nsec > 0 && FRAME_TSTATES != FAST_TSTATES) { nanosleep(&pause, NULL); } } endwin(); return EXIT_SUCCESS; }