by Stefan Tramm
There are a lot of web-based computer emulators out there for ancient machines – the Commodore 64 and MSX home computers or the more exotic Space Invaders arcade machines. Such emulators are nice to play with but they are incomplete – they lack an Input/Output subsystem.
Emu8080 is the first known Javascript-based emulator which adds a floppy disk system, a fast paper tape reader/writer, a line printer and a VT100 terminal emulator to the mix. Modern web browsers offer a local WebSQL database, which is used to implement block storage devices. Drag-and-Drop is used to mount a desktop file onto the virtual tape device. Together with CPU and RAM emulation, you have a complete machine inside your web browser…
Want to try out? Use Chrome6+ (or Safari5) and visit http://www.tramm.li/i8080
Now you are working inside a classical machine monitor for an 8080 microcomputer from the 70’s. Available commands are:
- m to modify memory
- d to dump memory on the screen
- l to disassemble a program (in Z80 Syntax)
- g to start execution
- <Enter> to perform a single step
- x to show and modify CPU registers
- h for help
So far this is pretty normal machine emulation.
The first important command is “r”. Without any parameters, it lists the available files on the tramm.li server. “r file” reads an Intel HEX file into memory, for example “r basic” will load the well-known Microsoft 8K BASIC interpreter for the Intel 8080.
But as already mentioned, a disk subsystem is also available. The first step in using it is to either insert a pre-formatted disk or to format an empty disk. To insert a pre-formatted CP/M disk, issue a “r 0 cpma” command.
This command expects the image of an 8-inch IBM formatted SS/SD diskette and writes it sector-wise into the browser-local WebSQL database. This needs to be done only once, causing the disk to now be loaded into your browser as Emu8080 drive 0. With the “b” command, the boot sector is loaded into “main memory”, starting at address 0. Now issue a “g” command to start execution. Ta da!, CP/M 2.2 has been loaded and started!
To go back to the monitor, you have two choices: Ctrl-. or via the “bye” executable.
Emu8080 supports four drives, so you can either format the other drives (“F” command) or search for other images on the internet and load them yourself. But how do we mount these drives? The “dsk” commands comes to rescue. It opens a new window, showing the current status of each drive. Every disk is also a drop-area, so you can mount disk images per drag-and-drop from your desktop into the emulator. Technically the FileReader API of Javascript is used, and this API is currently (November 2010) implemented only by Chrome (or Opera). Yes, you do not need the server anymore to transport data into a Javascript application.
Now you can mount disks and boot CP/M. You can edit files with ED.COM or Wordmaster, or write programs in 8080 assembler. But how to get the modified disk images out of the emulator again? The “dsk [n]” commands opens a new window, which allows to post the binary disk image to an arbitrary web server, e.g. localhost. (btw: there is no FileWriter API in HTML5). Just change the target URL and the form will post it where ever you want.
Sometimes mounting and posting whole images is overkill, transmitting a single file into the CP/M machine or getting it out may be all you want to do. And here comes the emulated paper tape device to rescue. Just drag a file onto the terminal’s main window and it will be mounted. Inside CP/M, just use the PIP command: “pip file.txt=rdr:” and you will have the tape’s content in FILE.TXT. Please keep in mind that CP/M is not 8-bit clean.
To get a single file out of the emulator, the paper tape puncher is used. Inside CP/M just use PIP again: “pip ptp:=rew.asm” to punch the file out. Then enter the monitor again (Ctrl-. or “bye”) and use the “ptp” command, which opens a window, where you either post the tape content to an arbitrary web server or simply copy-paste the content.
The final device is a line printer. It just opens a separate window where every character is shown. No escape sequences are implemented – therefore no bold or underlined characters.
In the unlikely case that the user has no clue about CP/M and its commands, the emulator includes a PDF of the original documentation by Digital Research.
Now, lets take a short look into the technical details:
- WebSQL: The emulator uses the browser local SQL DB as block storage for the four disk drives. The storage is persistent and writable.
- Drag-and-Drop / FileReader API: The emulator uses drag-and-drop and the FileReader API to read paper tape or disc images from your local machine.
- HTML5 manifest: The emulator supports the browser local database and browser local file reading. To make it also runnable without an active internet connection, it is built as a HTML 5 App by providing an emu8080.manifest file. This file tells the browser which resources (HTML, JS, CSS, PNG, HEX, CPM, PDF files) must be cached. This means that if you bookmark http://www.tramm.li/i8080/emu8080.html, you can start the emulator and access the disk drives even without an internet connection — just like a local application.
- ShellInABox: Without the fabulous VT100 emulation of ShellInABox, this emulator would not have been possible. The provided example of a BASIC interpreter was the starting point for the 8080 emulator.
- z80pack: The C based implementation of a Z80 machine was the basis for the IO-subsystem implementation, the custom BIOS, and the boot disk image. The trick for reducing host CPU cycles in tight IO-Loops (eg. when CP/M is waiting for a key press) was taken from this package.
- js8080: The Space Invaders emulator js8080 gifted the core CPU emulation.
One last word on performance. On a usual x86 processor the emulator performs like a 2 MHz 8080. With the monitor command “instr” you can tune the performance, by increasing the number of executed 8080 instructions per Javascript timeslice. With today’s hardware, you can expect the performance of a 8 MHz CPU (which was never available as real silicon).
And for Microsoft BASIC fans, try “r basic” and “g” afterwards instead of booting CP/M – welcome to 8K BASIC. Use “SAVE” to save your programs on the paper tape 🙂 and “LOAD” to read saved programs.
Neozeed provided the Zork adventure game disk image.
And now — happy hacking.
Other links:
- Neozeed blogged about the emulator, he also provides a very clear Howto, please take a look at: http://virtuallyfun.blogspot.com/2010/10/cpm-zork-in-java-script.html
- Emu8080 is also mentioned on the Z80 Info Pages (many thanks to Gaby from gaby.de for the reference!): http://www.z80.info/z80emu.htm#EMU_CPU_VAR
Very excellent work! I have posted this as news on my website.
I don’t know if it’s used in the project but last time I checked js8080 had buggy parity flag implementation.
Checkout http://github.com/begoon/i8080-js and http://github.com/begoon/rk86-rk.
Artur Piwko is correct. The implementation of parity test in js8080 is just plain wrong. Parity indicates if the number of set bits is even or odd. (http://en.wikipedia.org/wiki/Parity_flag). The js8080 implementation indicates if the number itself is even or odd.
Counting bits in JavaScript can be a bit slow. However, since the 8080 parity test operates on an 8-bit value, it’s pretty easy to just create a 256 entry table of true/false values and use the value you’re testing as an index into the table:
var parityTable = [
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
false,true,true,false,true,false,false,true,true,false,false,true,false,true,true,false,
true,false,false,true,false,true,true,false,false,true,true,false,true,false,false,true,
];
// set the Parity flag
function PF(v) {
if( parityTable[ (v & 0xFF) ] )
r8[iF] |= iPF;
else
r8[iF] &= ~(iPF);
}
Can you emulate this and let me know the result?
BDOS EQU 5
WRITESTR EQU 9
READSTR EQU 10
ORG 0100H
JMP PR$LGN
PR$ERR:
LXI D,ERR
MVI C,WRITESTR
CALL BDOS
PR$LGN:
LXI D,LGN
MVI C,WRITESTR
CALL 5
LXI D,BUFFER
MVI C,READSTR
CALL BDOS
MVI C,6
LXI H,BUFFER+2
MVI B,0DFh
UCASE$LOOP:
MOV A,M
ANA B
MOV M,A
INX H
DCR C
JNZ UCASE$LOOP
LDA BUFFER+3
CPI ‘O’
JNZ PR$ERR
LDA BUFFER+1
CPI 6
JNZ PR$ERR
MVI C,6
MVI A,0
LXI H,BUFFER+2
CHK$LOOP:
MOV B,M
ADD B
RLC
INX H
DCR C
JNZ CHK$LOOP
CPI 0B7H
JNZ PR$ERR
MVI A,’8′
MOV M,A
INX H
MVI A,’0′
MOV M,A
LXI H,RCC
MVI C,213
MVI D,0
DEC$LOOP:
PUSH H
LXI H,BUFFER+2
MOV A,L
ADD D
MOV L,A
MVI A,0
ADC H
MOV H,A
MOV A,M
POP H
MOV B,M
XRA B
MOV M,A
INR D
MVI A,7
ANA D
MOV D,A
INX H
DCR C
JNZ DEC$LOOP
LXI D,RCC
MVI C,WRITESTR
CALL BDOS
JMP 0 ;DONE
LGN:
DB 00Dh, 00Ah, 04Ch, 06Fh, 067h, 069h, 06Eh, 03Ah
DB 020h, 024h
ERR:
DB 00Dh, 00Ah, 049h, 044h, 045h, 04Eh, 054h, 049h
DB 046h, 049h, 043h, 041h, 054h, 049h, 04Fh, 04Eh
DB 020h, 04Eh, 04Fh, 054h, 020h, 052h, 045h, 043h
DB 04Fh, 047h, 04Eh, 049h, 053h, 045h, 044h, 020h
DB 042h, 059h, 020h, 053h, 059h, 053h, 054h, 045h
DB 04Dh, 00Dh, 00Ah, 02Dh, 02Dh, 043h, 04Fh, 04Eh
DB 04Eh, 045h, 043h, 054h, 049h, 04Fh, 04Eh, 020h
DB 054h, 045h, 052h, 04Dh, 049h, 04Eh, 041h, 054h
DB 045h, 044h, 02Dh, 02Dh, 024h
RCC:
DB 047h, 045h, 014h, 01Ah, 010h, 004h, 06Ch, 079h
DB 004h, 008h, 000h, 068h, 005h, 013h, 077h, 076h
DB 00Fh, 01Ch, 000h, 007h, 007h, 061h, 07Eh, 071h
DB 006h, 004h, 016h, 006h, 07Bh, 04Ch, 032h, 063h
DB 002h, 00Eh, 01Fh, 004h, 075h, 016h, 07Dh, 010h
DB 01Ah, 003h, 012h, 011h, 075h, 000h, 018h, 077h
DB 00Bh, 002h, 016h, 077h, 058h, 04Bh, 061h, 07Fh
DB 01Fh, 01Dh, 073h, 01Ch, 014h, 013h, 07Fh, 075h
DB 01Eh, 06Fh, 01Ah, 01Bh, 075h, 000h, 06Ch, 010h
DB 019h, 07Ch, 060h, 068h, 061h, 073h, 016h, 005h
DB 07Fh, 078h, 073h, 00Dh, 064h, 074h, 008h, 010h
DB 078h, 07Ah, 07Dh, 07Dh, 064h, 078h, 035h, 03Ah
DB 00Fh, 017h, 007h, 01Ah, 014h, 061h, 070h, 079h
DB 004h, 01Bh, 069h, 068h, 004h, 003h, 072h, 071h
DB 06Ah, 016h, 011h, 002h, 075h, 003h, 079h, 010h
DB 002h, 00Ch, 006h, 01Eh, 00Ch, 018h, 018h, 076h
DB 01Ch, 01Eh, 001h, 045h, 05Fh, 015h, 070h, 075h
DB 06Ah, 00Ch, 012h, 00Bh, 01Dh, 004h, 018h, 073h
DB 005h, 001h, 007h, 009h, 01Ch, 00Fh, 06Bh, 010h
DB 00Bh, 06Fh, 010h, 009h, 007h, 005h, 018h, 067h
DB 003h, 01Bh, 01Bh, 068h, 014h, 061h, 068h, 078h
DB 018h, 00Eh, 000h, 00Dh, 058h, 04Bh, 077h, 07Eh
DB 06Ah, 006h, 007h, 068h, 001h, 009h, 079h, 064h
DB 06Ah, 002h, 01Ah, 00Fh, 01Dh, 015h, 018h, 073h
DB 005h, 002h, 016h, 068h, 01Ch, 00Fh, 018h, 078h
DB 00Bh, 001h, 017h, 011h, 075h, 00Dh, 079h, 064h
DB 00Fh, 01Dh, 05Eh, 042h, 071h
BUFFER: DB 255
DB 0
DS 255
END