diff --git a/emulator.pas b/emulator.pas index 6aeed38..fd418c7 100644 --- a/emulator.pas +++ b/emulator.pas @@ -16,6 +16,7 @@ type const IO = $ffff; + LastRAM = $ffef; var Hlt, ASCII, Verbose: boolean; //Halt, ASCII, and verbose flags @@ -23,7 +24,7 @@ var X, Y: 0 .. 3; //Register arguments Addr, IP, RP: word; //Immediate or address argument and instruction and return pointers R: array [0 .. 3] of byte; //General-purpose registers - Mem: array [0 .. $ffef] of byte; //Memory + Mem: array [0 .. LastRAM] of byte; //Random access memory Prog{$ifdef printer}, Prn{$endif}{$ifdef tape}, TapeIn, TapeOut{$endif}: file of byte; //Program file, line printer, and tape reader and punch tapes {$ifdef tape} Reader, Punch: Tape; //States of the tape reader and punch @@ -31,6 +32,7 @@ var {$endif} Ch, Scan: ansichar; //Character for input and output and scancode for non-ASCII keys IC, LFX: integer; //Instruction counter for CPU speed + Fetched: byte; //Fetched byte //Terminal output procedure Output; @@ -60,12 +62,204 @@ begin IC := 0; end; +//Load a byte from memory +function LoadByte (W: word): byte; +var + B: byte; +begin + //Terminal input + if W = IO then begin + wait; + //Read a keypress + repeat + Ch := ReadKey; + //Check for non-ASCII keys + if Ch = ansichar (0) then begin + //Non-ASCII + if keypressed then begin + Scan := ReadKey; + //The delete key inserts the delete character + if Scan = ansichar ($53) then begin + Ch := ansichar ($7f); + ASCII := true; + end + //Unused function keys insert a null + else ASCII := true; + end + //Null + else ASCII := true; + end + //Other ASCII + else ASCII := true; + until ASCII = true; + //Bodge for the home and end keys + if Ch = ansichar ($37) then begin + if keypressed then begin + Scan := ReadKey; + Scan := ReadKey; + Scan := ReadKey; + Ch := ansichar (0); + end; + end + else if Ch = ansichar ($38) then begin + if keypressed then begin + Scan := ReadKey; + Scan := ReadKey; + Scan := ReadKey; + Ch := ansichar (0); + end; + end; + //Process the keypress + Output; //Local echo + B := byte (Ch); + end + //Tape reader + {$ifdef tape} + else if W = $fffd then begin + wait; + assign (State, ExpandFileName ('~/.tapes.thingamajig')); + //Check the reader state + if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin + try + reset (State); + read (State, Reader); + read (State, Punch); + close (State); + except + end; + end; + //Read + assign (TapeIn, Reader.Path); + try + reset (TapeIn); + seek (TapeIn, Reader.Pos); + read (TapeIn, B); + close (TapeIn); + Reader.Pos := Reader.Pos + 1; + except + B := $ff; + end; + //Save the reader state + if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin + try + rewrite (State); + write (State, Reader); + write (State, Punch); + close (State); + except + end; + end; + end + {$endif} + //Unused addresses + else if W > LastRAM then B := 0 + //Regular load + else B := Mem [W]; + //Result + LoadByte := B; +end; + +procedure StoreByte (W: word; B: byte); +begin + //Terminal output + if W = IO then begin + wait; + Ch := ansichar (B); + Output; + end + //Printer + {$ifdef printer} + else if W = $fffe then begin + wait; + assign (Prn, '/dev/usb/lp0'); + try + rewrite (Prn); + write (Prn, B); + close (Prn); + except + end; + end + {$endif} + //Tape punch + {$ifdef tape} + else if W = $fffd then begin + wait; + assign (State, ExpandFileName ('~/.tapes.thingamajig')); + //Check the punch state + if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin + try + reset (State); + read (State, Reader); + read (State, Punch); + close (State); + except + end; + end; + //Punch + if Punch.Path <> '' then begin + assign (TapeOut, Punch.Path); + if FileExists (Punch.Path) = false then begin + try + rewrite (TapeOut); + write (TapeOut, B); + close (TapeOut); + Punch.Reset := false; + except + end; + end + else if Punch.Reset then begin + try + rewrite (TapeOut); + write (TapeOut, B); + close (TapeOut); + Punch.Reset := false; + except + end; + end + else begin + try + reset (TapeOut); + seek (TapeOut, FileSize (TapeOut)); + write (TapeOut, B); + close (TapeOut); + except + end; + end; + end; + //Save the punch state + if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin + try + rewrite (State); + write (State, Reader); + write (State, Punch); + close (State); + except + end; + end; + end + {$endif} + //Regular store + else if W <= LastRAM then Mem [W] := B; +end; + +procedure Call; +begin + //Low byte of the return address + RP := RP - 1; + StoreByte (RP, IP and $ff); + //High byte of the return address + RP := RP - 1; + StoreByte (RP, IP shr 8); + //Call + IP := Addr; +end; + begin //Initialise the halt flag, the pointers, and the instruction counter Hlt := false; IP := 0; - RP := $fff0; + RP := LastRAM + 1; IC := 0; //Initialise the tape reader and punch @@ -103,6 +297,10 @@ begin if Verbose = true then assign (Prog, ParamStr (2)) else assign (Prog, ParamStr (1)); reset (Prog); + if FileSize (Prog) > LastRAM + 1 then begin + writeln ('Error: program size cannot exceed ', LastRam + 1, ' bytes'); + halt; + end; {$i+} if IOResult <> 0 then begin writeln ('Error: program file cannot be read from'); @@ -111,11 +309,7 @@ begin repeat read (Prog, Mem [IP]); IP := IP + 1; - until (eof (Prog)) or (IP = $fff0); - if IP = $fff0 then begin - writeln ('Error: memory overflow'); - halt; - end; + until (eof (Prog)); //Reinitialise the instruction pointer IP := 0; @@ -127,45 +321,34 @@ begin if Verbose = true then writeln (StdErr, 'IR: ', IntToHex (Op, 1), IntToHex (Regs, 1), IntToHex (Addr, 4), '; IP: ', IntToHex (IP, 4), ', RP: ', IntToHex (RP, 4), '; R0: ', IntToHex (R[0], 2), ', R1: ', IntToHex (R[1], 2), ', R2: ', IntToHex (R[2], 2), ', R3: ', IntToHex (R[3], 2), ansichar ($d)); //Fetch the instruction and increment the instruction pointer - //Opcode - Op := Mem [IP] and $f0 shr 4; - //Register arguments - Regs := Mem [IP] and $f; - X := Mem [IP] and $c shr 2; - Y := Mem [IP] and 3; + //Fetch the opcode and register arguments + Fetched := LoadByte (IP); + //Decode the opcode + Op := Fetched and $f0 shr 4; + //Decode the register arguments + Regs := Fetched and $f; + X := Fetched and $c shr 2; + Y := Fetched and 3; IP := IP + 1; - if IP > $ffef then begin - writeln ('Error: illegal instruction pointer value'); - halt; - end; //Immediate or address argument if Op >= $a then begin //Immediate or high byte of address - Addr := Mem [IP]; + Fetched := LoadByte (IP); + Addr := Fetched; Addr := Addr shl 8; IP := IP + 1; - if IP > $ffef then begin - writeln ('Error: illegal instruction pointer value'); - halt; - end; //Low byte of address if Op = $a then begin if Y = 0 then begin - Addr := Addr + Mem [IP]; + Fetched := LoadByte (IP); + Addr := Addr + Fetched; IP := IP + 1; - if IP > $ffef then begin - writeln ('Error: illegal instruction pointer value'); - halt; - end; end; end else begin - Addr := Addr + Mem [IP]; + Fetched := LoadByte (IP); + Addr := Addr + Fetched; IP := IP + 1; - if IP > $ffef then begin - writeln ('Error: illegal instruction pointer value'); - halt; - end; end; end else Addr := 0; @@ -176,20 +359,12 @@ begin //Ret else if Op = 1 then begin //High byte of the return address - IP := Mem [RP]; + IP := LoadByte (RP); IP := IP shl 8; RP := RP + 1; - if RP > $fff0 then begin - writeln ('Error: illegal return pointer value'); - halt; - end; //Low byte of the return address - IP := IP + Mem [RP]; + IP := IP + LoadByte (RP); RP := RP + 1; - if RP > $fff0 then begin - writeln ('Error: illegal return pointer value'); - halt; - end; end //Shl else if Op = 2 then R [X] := R [X] shl 1 @@ -212,177 +387,10 @@ begin //Immediate if Y <> 0 then R [X] := Addr shr 8 //Address - else begin - //Terminal input - if Addr = IO then begin - wait; - //Read a keypress - repeat - Ch := ReadKey; - //Check for non-ASCII keys - if Ch = ansichar (0) then begin - //Non-ASCII - if keypressed then begin - Scan := ReadKey; - //The delete key inserts the delete character - if Scan = ansichar ($53) then begin - Ch := ansichar ($7f); - ASCII := true; - end - //Unused function keys insert a null - else ASCII := true; - end - //Null - else ASCII := true; - end - //Other ASCII - else ASCII := true; - until ASCII = true; - //Bodge for the home and end keys - if Ch = ansichar ($37) then begin - if keypressed then begin - Scan := ReadKey; - Scan := ReadKey; - Scan := ReadKey; - Ch := ansichar (0); - end; - end - else if Ch = ansichar ($38) then begin - if keypressed then begin - Scan := ReadKey; - Scan := ReadKey; - Scan := ReadKey; - Ch := ansichar (0); - end; - end; - //Process the keypress - Output; //Local echo - R [X] := byte (Ch); - end - //Tape reader - {$ifdef tape} - else if Addr = $fffd then begin - wait; - assign (State, ExpandFileName ('~/.tapes.thingamajig')); - //Check the reader state - if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin - try - reset (State); - read (State, Reader); - read (State, Punch); - close (State); - except - end; - end; - //Read - assign (TapeIn, Reader.Path); - try - reset (TapeIn); - seek (TapeIn, Reader.Pos); - read (TapeIn, R [X]); - close (TapeIn); - Reader.Pos := Reader.Pos + 1; - except - R [X] := $ff; - end; - //Save the reader state - if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin - try - rewrite (State); - write (State, Reader); - write (State, Punch); - close (State); - except - end; - end; - end - {$endif} - //Regular load - else R [X] := Mem [Addr]; - end; + else R [X] := LoadByte (Addr); end //Store - else if Op = $b then begin - //Terminal output - if Addr = IO then begin - wait; - Ch := ansichar (R [Y]); - Output; - end - //Printer - {$ifdef printer} - else if Addr = $fffe then begin - wait; - assign (Prn, '/dev/usb/lp0'); - try - rewrite (Prn); - write (Prn, R [Y]); - close (Prn); - except - end; - end - {$endif} - //Tape punch - {$ifdef tape} - else if Addr = $fffd then begin - wait; - assign (State, ExpandFileName ('~/.tapes.thingamajig')); - //Check the punch state - if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin - try - reset (State); - read (State, Reader); - read (State, Punch); - close (State); - except - end; - end; - //Punch - if Punch.Path <> '' then begin - assign (TapeOut, Punch.Path); - if FileExists (Punch.Path) = false then begin - try - rewrite (TapeOut); - write (TapeOut, R [Y]); - close (TapeOut); - Punch.Reset := false; - except - end; - end - else if Punch.Reset then begin - try - rewrite (TapeOut); - write (TapeOut, R [Y]); - close (TapeOut); - Punch.Reset := false; - except - end; - end - else begin - try - reset (TapeOut); - seek (TapeOut, FileSize (TapeOut)); - write (TapeOut, R [Y]); - close (TapeOut); - except - end; - end; - end; - //Save the punch state - if FileExists (ExpandFileName ('~/.tapes.thingamajig')) then begin - try - rewrite (State); - write (State, Reader); - write (State, Punch); - close (State); - except - end; - end; - end - {$endif} - //Regular store - else Mem [Addr] := R [Y]; - end + else if Op = $b then StoreByte (Addr, R [Y]) //Breq else if Op = $c then begin if R [X] = R [Y] then IP := Addr; @@ -393,53 +401,11 @@ begin end //Cleq else if Op = $e then begin - if R [X] = R [Y] then begin - //Low byte of the return address - RP := RP - 1; - if RP > $fff0 then begin - writeln ('Error: illegal return pointer value'); - halt; - end; - Mem [RP] := IP and $ff; - //High byte of the return address - RP := RP - 1; - if RP > $fff0 then begin - writeln ('Error: illegal return pointer value'); - halt; - end; - Mem [RP] := IP shr 8; - //Call - IP := Addr; - if IP > $ffef then begin - writeln ('Error: illegal instruction pointer value'); - halt; - end; - end; + if R [X] = R [Y] then Call; end //Clneq else if Op = $f then begin - if R [X] <> R [Y] then begin - //Low byte of the return address - RP := RP - 1; - if RP > $fff0 then begin - writeln ('Error: illegal return pointer value'); - halt; - end; - Mem [RP] := IP and $ff; - //High byte of the return address - RP := RP - 1; - if RP > $fff0 then begin - writeln ('Error: illegal return pointer value'); - halt; - end; - Mem [RP] := IP shr 8; - //Call - IP := Addr; - if IP > $ffef then begin - writeln ('Error: illegal instruction pointer value'); - halt; - end; - end; + if R [X] <> R [Y] then Call; end; //Increment the instruction counter diff --git a/readme.md b/readme.md index ccf4dcf..2aed37d 100644 --- a/readme.md +++ b/readme.md @@ -14,12 +14,6 @@ assembler and a disassembler, all written in FreePascal. It also includes couple of simple example programs for Thingamajig written in Assembly. -Speed ------ - -Thingamajig does not have a prescribed speed. The emulator runs at -roughly 500 KIPS. - Registers and Memory -------------------- @@ -28,11 +22,10 @@ Registers and Memory * 8-bit general-purpose registers R0-R3 * 8-bit memory locations 0-FFFF -Multi-byte values are big-endian. Memory addresses FFF0-FFFF are -reserved for memory mapped devices; the instruction and return pointers -cannot have values higher than FFEF and FFF0 respectively to avoid them. -The instruction and return pointers are initialised as 0 and FFF0 -respectively, while other registers and memory are unitialised. +Multi-byte values are big-endian. Memory locations 0-FFEF are used for +RAM while FFF0-FFFF are reserved for memory mapped devices. Input and +output are mapped to address FFFF, while arbitrary devices can be mapped +to the other reserved addresses. Instructions ------------ @@ -78,8 +71,7 @@ For the arguments of each instruction see the previous section. Address arguments can be either absolute addresses or references to or relative to a label. Relative references are of the form LABEL +/- N, -the spacing being optional. Note that the assembler does not check for -addresses or references to reserved addresses. +the spacing being optional. In addition to the true instructions there are three pseudo-instructions. ORG defines the starting address of the program: it @@ -88,26 +80,34 @@ not required if the starting address is 0. DATA introduces a byte of data. ADDR introduces two bytes of data containing the address of a reference to or relative to a label. -Memory-Mapped Devices ---------------------- +Boot +---- -Input and output are mapped to address FFFF; the emulator emulates a -roughly 1000 CPS glass teletype with local echo. +At boot the initial program loader (IPL) loads a program to the RAM +starting at address 0 after which is cedes control to the CPU. The +instruction and return pointers are initialised as 0 and the first +address after RAM respectively, while other registers and RAM are +uninitialised. -Arbitrary devices can be mapped to the other reserved addresses. +Emulator +-------- + +The emulator runs at roughly 500 KIPS and has the full 65520 bytes of +RAM. + +Input and output are handled by an emulated roughly 1000 CPS glass +teletype terminal with local echo. Note that of the control characters +only bell, backspace, line feed, and carriage return are used by the +terminal, and the backspace and delete keys are tied to their respective +characters and non-character keys to null. In Linux the emulator can be compiled with support for a character printer and an emulated punched tape reader and punch with the arguments -dprinter and -dtape respectively. The printer is mapped to address FFFE and the tape reader and punch to FFFD. The printer prints into /dev/usb/lp0 and the tape files read from and punched to are (re)set -using the program tapectl. If you wish to use a different setup you have -to modify the code yourself. +using the program tapectl. -Initial Program Loader ----------------------- - -At boot the initial program loader (IPL) loads a program to the memory -starting from address 0 after which is cedes control to the CPU. The -emulator loads the program from a ROM file. +The IPL loads a program from a file specified when launching the +emulator.  \ No newline at end of file