/*
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;
}