summaryrefslogtreecommitdiff
path: root/source/zx81.c
diff options
context:
space:
mode:
Diffstat (limited to 'source/zx81.c')
-rw-r--r--source/zx81.c868
1 files changed, 868 insertions, 0 deletions
diff --git a/source/zx81.c b/source/zx81.c
new file mode 100644
index 0000000..b2d85b1
--- /dev/null
+++ b/source/zx81.c
@@ -0,0 +1,868 @@
+/*
+ 3ds81 - Nintendo 3DS ZX81 emulator
+
+ Copyright (C) 2021 Ian Cowburn <ianc@noddybox.co.uk>
+
+ 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 <http://www.gnu.org/licenses/>
+
+ -------------------------------------------------------------------------
+
+ Provides the emulation for the ZX81
+
+*/
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <3ds.h>
+
+#include "zx81.h"
+#include "gui.h"
+
+#include "stream.h"
+
+#include "config.h"
+
+#include "zx81_bin.h"
+
+#ifndef TRUE
+#define TRUE 1
+#endif
+
+#ifndef FALSE
+#define FALSE 0
+#endif
+
+/* ---------------------------------------- STATICS
+*/
+#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*10)
+
+#define E_LINE 16404
+#define LASTK1 16421
+#define LASTK2 16422
+#define MARGIN 16424
+#define FRAMES 16436
+#define CDFLAG 16443
+
+static Z80Val FRAME_TSTATES=FAST_TSTATES;
+
+/* The ZX81 screen and memory
+*/
+static int waitkey=FALSE;
+static int started=FALSE;
+
+static int hires=FALSE;
+static int hires_dfile;
+static int last_I;
+
+static int fast_mode=FALSE;
+
+static unsigned prev_lk1;
+static unsigned prev_lk2;
+
+#define SCR_W 256
+#define SCR_H 192
+#define TXT_W 32
+#define TXT_H 24
+
+static Z80Byte mem[0x10000];
+
+static Z80Word RAMBOT=0;
+static Z80Word RAMTOP=0;
+
+#define DFILE 0x400c
+
+#define WORD(a) (mem[a] | (Z80Word)mem[a+1]<<8)
+
+/* Tape
+*/
+static int enable_filesystem;
+static int allow_save;
+static const Z80Byte *tape_image;
+static int tape_len;
+
+static char last_dir[FILENAME_MAX] = "/";
+
+/* The keyboard
+*/
+static Z80Byte matrix[8];
+
+static struct
+{
+ int row;
+ int bit;
+} key_matrix[]=
+ {
+ {3,0x01}, {3,0x02}, {3,0x04}, {3,0x08}, {3,0x10}, /* 1 - 5 */
+ {4,0x10}, {4,0x08}, {4,0x04}, {4,0x02}, {4,0x01}, /* 6 - 0 */
+ {2,0x01}, {2,0x02}, {2,0x04}, {2,0x08}, {2,0x10}, /* Q - T */
+ {5,0x10}, {5,0x08}, {5,0x04}, {5,0x02}, {5,0x01}, /* Y - P */
+ {1,0x01}, {1,0x02}, {1,0x04}, {1,0x08}, {1,0x10}, /* A - G */
+ {6,0x10}, {6,0x08}, {6,0x04}, {6,0x02}, {6,0x01}, /* H - NL */
+ {0,0x01}, {0,0x02}, {0,0x04}, {0,0x08}, {0,0x10}, /* CAPS - V */
+ {7,0x10}, {7,0x08}, {7,0x04}, {7,0x02}, {7,0x01} /* B - SPACE */
+ };
+
+
+/* ---------------------------------------- PRIVATE FUNCTIONS
+*/
+#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)
+
+static void RomPatch(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;
+
+ for(f=0;save[f]!=0xff;f++)
+ {
+ mem[ROM_SAVE+f]=save[f];
+ }
+
+ for(f=0;load[f]!=0xff;f++)
+ {
+ mem[ROM_LOAD+f]=load[f];
+ }
+
+ for(f=0;fast_hack[f]!=0xff;f++)
+ {
+ mem[0x4ca+f]=fast_hack[f];
+ }
+
+ for(f=0;kbd_hack[f]!=0xff;f++)
+ {
+ mem[0x2bb+f]=kbd_hack[f];
+ }
+
+ for(f=0;pause_hack[f]!=0xff;f++)
+ {
+ mem[0xf3a+f]=pause_hack[f];
+ }
+
+ /* Trust me, we have a ZX81... Honestly.
+ */
+ mem[0x21c]=0x00;
+ mem[0x21d]=0x00;
+
+ /* Remove HALTs as we don't do interrupts
+ */
+ mem[0x0079]=0;
+ mem[0x02ec]=0;
+}
+
+/* Open a tape file the passed address
+*/
+static FILE *OpenTapeFile(Z80Word addr, int *cancelled, const char *mode)
+{
+ static const char zx_chars[] = "\"#$:?()><=+-*/;,."
+ "0123456789"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ FILE *fp;
+ char full_fn[FILENAME_MAX] = DEFAULT_SNAPDIR;
+ char fn[FILENAME_MAX];
+ int f;
+ int done;
+
+ fp = NULL;
+ f = 0;
+ done = FALSE;
+ *cancelled = FALSE;
+
+ while(f<(FILENAME_MAX-3) && !done)
+ {
+ int ch;
+
+ ch = mem[addr++];
+
+ if (ch&0x80)
+ {
+ done = TRUE;
+ ch &= 0x7f;
+ }
+
+ if (ch>=11 && ch<=63)
+ {
+ fn[f++] = zx_chars[ch-11];
+ }
+ }
+
+ if (fn[0] == '*')
+ {
+ if (GUI_FileSelect(last_dir,fn,".P"))
+ {
+ fp = fopen(fn, mode);
+ }
+ else
+ {
+ *cancelled = TRUE;
+ }
+
+ SK_DisplayKeyboard();
+ }
+ else
+ {
+ fn[f++] = '.';
+ fn[f++] = 'P';
+ fn[f] = 0;
+
+ strcat(full_fn,fn);
+
+ if (!(fp = fopen(full_fn, mode)))
+ {
+ fp = fopen(fn, mode);
+ }
+ }
+
+ return fp;
+}
+
+
+static void LoadInternalTape(Z80 *z80)
+{
+ memcpy(mem+0x4009,tape_image,tape_len);
+}
+
+
+static void LoadExternalTape(FILE *tape, Z80 *z80)
+{
+ int c;
+ Z80Byte *a;
+
+ a=mem+0x4009;
+
+ while((c=getc(tape))!=EOF)
+ {
+ *a++=c;
+ }
+}
+
+
+static void SaveExternalTape(FILE *tape, Z80 *z80)
+{
+ int f;
+ int end;
+
+ f = 0x4009;
+ end = WORD(E_LINE);
+
+ while(f <= end)
+ {
+ fputc(mem[f++], tape);
+ }
+}
+
+
+static void FindHiresDFILE(void)
+{
+ /* Somewhat based on the code from xz81, an X-based ZX81 emulator,
+ (C) 1994 Ian Collier. Search the ZX81's RAM until we find what looks
+ like a hi-res display file.
+
+ Bizarrely the original code used 'f' for a loop counter too... Another
+ poor soul forever damaged by the ZX81's keyword entry system...
+ */
+ int f;
+
+ for(f=0x8000-(33*192); f>0x4000 ; f--)
+ {
+ int v;
+
+ v = mem[f+32];
+
+ if (v&0x40)
+ {
+ int ok = TRUE;
+ int n;
+
+ for(n=0;n<192 && ok;n++)
+ {
+ if (mem[f+33*n]&0x40)
+ {
+ ok = FALSE;
+ }
+
+ if (mem[f+32+33*n] != v)
+ {
+ ok = FALSE;
+ }
+ }
+
+ if (ok)
+ {
+ hires_dfile = f;
+ return;
+ }
+ }
+ }
+
+ /* All else fails, put the hires dfile at 0x4000 -- at least it should be
+ obvious that the hires won't work for whatever is being run.
+ */
+ hires_dfile = 0x4000;
+}
+
+
+/* Perform ZX81 housekeeping functions like updating FRAMES and updating LASTK
+*/
+static void ZX81HouseKeeping(Z80 *z80)
+{
+ 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<<row);
+ }
+
+ lastk2|=b;
+ }
+ }
+
+ if (lastk1 && (lastk1!=prev_lk1 || lastk2!=prev_lk2))
+ {
+ mem[CDFLAG]|=1;
+ }
+ else
+ {
+ mem[CDFLAG]&=~1;
+ }
+
+ mem[LASTK1]=lastk1^0xff;
+ mem[LASTK2]=lastk2^0xff;
+
+ prev_lk1=lastk1;
+ prev_lk2=lastk2;
+}
+
+
+static int CheckTimers(Z80 *z80, Z80Val val)
+{
+ if (val>=FRAME_TSTATES)
+ {
+ /* Check for hi-res modes
+ */
+ if (z80->I && z80->I != last_I)
+ {
+ last_I = z80->I;
+
+ if (z80->I == 0x1e)
+ {
+ hires = FALSE;
+ }
+ else
+ {
+ hires = TRUE;
+ FindHiresDFILE();
+ }
+ }
+
+ Z80ResetCycles(z80,val-FRAME_TSTATES);
+
+ /* Kludge warning - We assume that a hires display will not be in
+ FAST mode!
+ */
+ if (started && ((mem[CDFLAG] & 0x80) || waitkey || hires))
+ {
+ fast_mode = FALSE;
+ FRAME_TSTATES=SLOW_TSTATES;
+ }
+ else
+ {
+ fast_mode = TRUE;
+ FRAME_TSTATES=FAST_TSTATES;
+ }
+
+ /* Update FRAMES (if in SLOW) and scan the keyboard. This only happens
+ once we've got to a decent point in the boot cycle (detected with
+ a valid stack pointer).
+ */
+ if (z80->SP<0x8000)
+ {
+ ZX81HouseKeeping(z80);
+ }
+
+ return FALSE;
+ }
+ else
+ {
+ return TRUE;
+ }
+}
+
+
+static int EDCallback(Z80 *z80, Z80Val data)
+{
+ Z80Word pause;
+
+ switch((Z80Byte)data)
+ {
+ case ED_SAVE:
+ if (allow_save && z80->DE.w<0x8000)
+ {
+ FILE *fp;
+ int cancel;
+
+ if ((fp=OpenTapeFile(z80->HL.w, &cancel, "wb")))
+ {
+ SaveExternalTape(fp,z80);
+ fclose(fp);
+ }
+ }
+ break;
+
+ case ED_LOAD:
+ /* Try and load the external file if a name given. Otherwise, we
+ try the internal one. Some of this is slightly dodgy -- it was
+ never intended for the emulator to be doing any GUI related
+ nonsense (like the alerts) but simply emulating.
+ */
+ if (enable_filesystem && z80->DE.w<0x8000)
+ {
+ FILE *fp;
+ int cancel;
+
+ if ((fp=OpenTapeFile(z80->DE.w, &cancel, "rb")))
+ {
+ LoadExternalTape(fp,z80);
+ fclose(fp);
+ }
+ else
+ {
+ if (!cancel)
+ {
+ GUI_Alert(FALSE,"Couldn't open tape");
+ SK_DisplayKeyboard();
+ }
+ }
+ }
+ else
+ {
+ if (tape_image)
+ {
+ LoadInternalTape(z80);
+ }
+ else
+ {
+ GUI_Alert(FALSE,"No tape image selected");
+ SK_DisplayKeyboard();
+ }
+ }
+
+ 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))
+ {
+ SoftKeyEvent ev;
+
+ while (SK_GetEvent(&ev))
+ {
+ ZX81HandleKey(ev.key,ev.pressed);
+ }
+
+ CheckTimers(z80,FRAME_TSTATES);
+ }
+
+ waitkey=FALSE;
+ break;
+
+ default:
+ break;
+ }
+
+ return TRUE;
+}
+
+
+/* ---------------------------------------- EXPORTED INTERFACES
+*/
+void ZX81Init(Z80 *z80)
+{
+ Z80Word f;
+
+ hires = FALSE;
+ hires_dfile = 0;
+ last_I = 0x1e;
+
+ /* Load the ROM
+ */
+ memcpy(mem,zx81_bin,ROMLEN);
+
+ /* Patch the ROM
+ */
+ RomPatch();
+ Z80LodgeCallback(z80,eZ80_EDHook,EDCallback);
+ Z80LodgeCallback(z80,eZ80_Instruction,CheckTimers);
+
+ /* Mirror the ROM
+ */
+ memcpy(mem+ROMLEN,mem,ROMLEN);
+
+ /* Memory size (16K)
+ */
+ RAMBOT=0x4000;
+ RAMTOP=RAMBOT+0x4000;
+
+ for(f = RAMBOT; f <= RAMTOP; f++)
+ {
+ mem[f] = 0;
+ }
+
+ for(f = 0; f < 8; f++)
+ {
+ matrix[f] = 0x1f;
+ }
+
+ /* Fill the upper 32K with RET opcodes -- hopefully by simply returning
+ RET for ULA reads we save a lot of ROM patching shenanigans.
+
+ Note that this check used to be in ZX81ReadMem, but obviously this
+ should cut down on a *lot* of pointless expression evaluation!
+ */
+ for(f = 0x8000; f <= RAMTOP; f++)
+ {
+ mem[f] = 0xc9;
+ }
+
+}
+
+
+void ZX81RenderDisplay(Framebuffer *fb)
+{
+ FB_Clear(fb, COL_BLACK);
+ FB_Centre(fb, "TODO", 10, COL_WHITE, COL_TRANSPARENT);
+}
+
+
+void ZX81HandleKey(SoftKey key, int is_pressed)
+{
+ if (key<SK_CONFIG)
+ {
+ if (is_pressed)
+ {
+ matrix[key_matrix[key].row]&=~key_matrix[key].bit;
+ }
+ else
+ {
+ matrix[key_matrix[key].row]|=key_matrix[key].bit;
+ }
+ }
+ else
+ {
+ /* TODO: Joysticks? Were there any common ones for the 81? */
+ }
+}
+
+
+Z80Byte ZX81ReadMem(Z80 *z80, Z80Word addr)
+{
+ return mem[addr];
+}
+
+
+void ZX81WriteMem(Z80 *z80, Z80Word addr, Z80Byte val)
+{
+ if (addr>=RAMBOT && addr<=RAMTOP)
+ {
+ mem[addr]=val;
+ }
+}
+
+
+Z80Byte ZX81ReadPort(Z80 *z80, Z80Word port)
+{
+ Z80Byte b=0;
+
+ switch(port&0xff)
+ {
+ case 0xfe: /* ULA */
+ /* Key matrix
+ */
+ 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... Of course, whether
+ or not this may be worse as other code doesn't expect the bits,
+ we shall find out!
+ */
+ b |= 0x60;
+
+ break;
+
+ default:
+ b = 0xff; /* Idle bus */
+ break;
+ }
+
+ return b;
+}
+
+
+void ZX81WritePort(Z80 *z80, Z80Word port, Z80Byte val)
+{
+ switch(port&0xff)
+ {
+ case 0xfd:
+ break;
+
+ case 0xfe:
+ break;
+ }
+}
+
+
+void ZX81Reset(Z80 *z80)
+{
+ int f;
+
+ for(f=0;f<8;f++)
+ matrix[f]=0x1f;
+
+ Z80Reset(z80);
+ Z80ResetCycles(z80,0);
+
+ started=FALSE;
+
+ hires = FALSE;
+ hires_dfile = 0;
+ last_I = 0x1e;
+}
+
+
+void ZX81EnableFileSystem(int enable)
+{
+ enable_filesystem=enable;
+}
+
+
+void ZX81SetTape(const Z80Byte *image, int len)
+{
+ tape_image=image;
+ tape_len=len;
+}
+
+
+void ZX81Reconfigure(void)
+{
+ if (DS81_Config[DS81_STATIC_RAM_AT_0x2000])
+ {
+ RAMBOT = 0x2000;
+ }
+ else
+ {
+ RAMBOT = 0x4000;
+ memcpy(mem+ROMLEN,mem,ROMLEN);
+ }
+
+ allow_save = enable_filesystem && DS81_Config[DS81_ALLOW_TAPE_SAVE];
+}
+
+
+void ZX81SaveSnapshot(FILE *fp)
+{
+ int f;
+
+ for(f=0; f<sizeof mem; f++)
+ {
+ PUT_Byte(fp, mem[f]);
+ }
+
+ for(f=0; f<sizeof matrix; f++)
+ {
+ PUT_Byte(fp, matrix[f]);
+ }
+
+ PUT_Long(fp, waitkey);
+ PUT_Long(fp, started);
+
+ PUT_ULong(fp, RAMBOT);
+ PUT_ULong(fp, RAMTOP);
+
+ PUT_ULong(fp, prev_lk1);
+ PUT_ULong(fp, prev_lk2);
+}
+
+
+void ZX81LoadSnapshot(FILE *fp)
+{
+ int f;
+
+ for(f=0; f<sizeof mem; f++)
+ {
+ mem[f] = GET_Byte(fp);
+ }
+
+ for(f=0; f<sizeof matrix; f++)
+ {
+ matrix[f] = GET_Byte(fp);
+ }
+
+ waitkey = GET_Long(fp);
+ started = GET_Long(fp);
+
+ RAMBOT = GET_ULong(fp);
+ RAMTOP = GET_ULong(fp);
+
+ prev_lk1 = GET_ULong(fp);
+ prev_lk2 = GET_ULong(fp);
+
+ /* Reset last_I to force hi/lo res detection
+ */
+ last_I = 0;
+}
+
+
+/* END OF FILE */