/* 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 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 */ }; /* -------------------------------------------------- ZX81 CODE */ static void RomPatch(const Z80Byte patch[], Z80Word addr) { int f; for(f = 0; patch[f] != 0xff; f++) { mem[addr++] = patch[f]; } } static int EDCallback(Z80 *z80, Z80Val data) { Z80Word pause; switch((Z80Byte)data) { case ED_SAVE: if (z80->DE.w < 0x8000) { /* TODO: Save */ } break; case ED_LOAD: if (z80->DE.w < 0x8000) { /* TODO: Load */ } mem[CDFLAG] = 0xc0; break; case ED_WAITKEY: waitkey = TRUE; started = TRUE; break; case ED_ENDWAITKEY: waitkey = FALSE; break; case ED_PAUSE: waitkey = TRUE; pause = z80->BC.w; while(pause-- && !(mem[CDFLAG]&1)) { /* TODO: Pause code */ } waitkey = FALSE; break; } return TRUE; } 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<= FRAME_TSTATES) { Z80ResetCycles(z80, val - FRAME_TSTATES); if (started && ((mem[CDFLAG] & 0x80) || waitkey)) { FRAME_TSTATES = SLOW_TSTATES; } else { FRAME_TSTATES = FAST_TSTATES; } if (z80->SP < 0x8000) { ZX81HouseKeeping(); } return FALSE; } else { 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; } void ZX81WritePort(Z80 *z80, Z80Word port, Z80Byte val) { } 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 */ }; int f; /* 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); /* 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 DrawScreen(void) { int fast; int x,y; fast = (FRAME_TSTATES == FAST_TSTATES); if (fast) { for(y = 0; y < TXT_H; y++) { for(x = 0; x < TXT_W; x++) { mvprintw(y, x, "%lc", L'\u2592'); } } } else { Z80Byte *scr; scr = mem + PEEKW(DFILE); y = 0; while(y < TXT_H) { scr++; x = 0; while(*scr != 118 && x < TXT_W) { Z80Byte ch = *scr++; if (ch & 0x80) { attron(A_REVERSE); } mvprintw(y, x, "%lc", scrcode[ch & 0x3f]); if (ch & 0x80) { attroff(A_REVERSE); } x++; } y++; } } } /* -------------------------------------------------- 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; } /* -------------------------------------------------- MAIN */ int main(void) { struct timespec start; int quit = FALSE; setlocale(LC_ALL, ""); 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; Z80Exec(z80); clear(); DrawScreen(); ch = getch(); if (ch == 3) { quit = TRUE; } refresh(); 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; }