cpmulator - A CP/M emulator written in golang
This repository contains a CP/M emulator, with integrated CCP, which is designed to run CP/M binaries:
The project was initially created to run my text-based adventure game, which I wrote a few years ago, to amuse my child. That was written in Z80 assembly language and initially targeted CP/M, although it was later ported to the ZX Spectrum.
Over time this project has become more complete, and more complex, and I've implemented enough functionity to run simple binaries and many of the well-known CP/M programs:
The Aztec C-Compiler. You can edit and compile C code within the emulator, then run it!
Borland's Turbo Pascal You can edit and compile Pascal code within the emulator, then run it!
Many early Infocom games: Zork 1, 2, & 3. Planetfall. etc.
Microsoft BASIC
Wordstar
The biggest caveat is that I've not implemented any notion of disk-based access. This means that, for example, opening, reading/writing, and closing files is absolutely fine, but any API call that refers to tracks, sectors, or disks will fail.
A companion repository contains a collection of vintage CP/M software you can use with this, or any other, emulator:
Installation & Versioning
This emulator is written using golang, so if you have a working golang toolchain you can install in the standard way:
go install github.com/skx/cpmulator@latest
If you were to clone this repository to your local system you could then build and install by running:
go install .
If neither of these options are suitable you may download the latest binary from the release page.
Releases will be made as/when features seem to justify it, but it should be noted that I consider the CLI tool, and the emulator itself, the "product". That means that the internal APIs will change around as/when necessary.
If you wish to import any of the internal APIs you do so at the risk of changes happening at any time - although so far these have been minor I'm not averse to changing parameters to internal packages, or adding/renaming/removing methods as necessary without any regard for external users.
Portability
The CP/M input handlers need to disable echoing when reading (single) characters from STDIN. There isn't a simple and portable solution for this in golang - so I've resorted to the naive approach of executing stty when necessary.
This means the code in this repository isn't 100% portable; it will work on Linux and MacOS hosts, but not Windows.
There is code to set a console into RAW mode, and disable echoing input, for example you can consider the code in the readPassword function in x/term . Unfortunately the facilities there are only sufficient for reading a line, not a character which means we'll need to essentially copy and paste their implementations inline to take advantage of this, and restore portability.
This is tracked in #65.
Usage
If you launch cpmulator with no arguments then one of the integrated CCPs ("console command processor") will be launched, dropping you into a familiar shell:
$ cpmulator A > dir A: LICENSE . | README .MD | CPMULATO. | GO .MOD A: GO .SUM | MAIN .GO | RET .COM A > TYPE LICENSE The MIT License (MIT) .. A >
You can terminate the CCP by pressing Ctrl-C, or typing EXIT . The following built-in commands are available:
Show the built-in commands of the default CCP: CLS Clear the screen.
DIR List files, by default this uses " *.* ". Try " DIR *.COM " if you want to see something more specific, for example.
EXIT / HALT / QUIT Terminate the CCP.
/ / ERA Erase the named files, wildcards are permitted.
TYPE View the contents of the named file - wildcards are not permitted.
REN Rename files, so " REN NEW=OLD " - again note that wildcards are not permitted, nor is cross-drive renaming.
There are currently a pair of CCP implementations included within the emulator, and they can be selected via the -ccp command-line flag:
"ccp" This is the default, but you can choose it explicitly via cpmulator -ccp=ccp .. . The original/default one, from Digital Research
"ccpz" Launch this via cpmulate -ccp=ccpz .. An enhanced one with extra built-in commands. Notably "GET 0100 FOO.COM" will load a binary into RAM, at address 0x100. Then "JMP 0100" will launch it. The prompt changes to show user-number, for example if you run "USER 3". If a command isn't found in the current drive A: will be searched instead, which is handy.
You can also launch a binary directly by specifying it's path upon the command-line, followed by any optional arguments that the binary accepts or requires:
$ cpmulator /path/to/binary [optional-args]
Other options are shown in the output of cpmulator -help , but in brief:
-cd /path/to/directory Change to the given directory before running.
-directories Use directories on the host for drive-contents, discussed later in this document.
-log-path /path/to/file Output debug-logs to the given file, creating it if necessary.
-prn-path /path/to/file All output which CP/M sends to the "printer" will be written to the given file.
Sample Binaries
I've placed some games within the dist/ directory, to make it easier for you to get started:
$ cd dist/ $ cpmulator ZORK1.COM ZORK I: The Great Underground Empire Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights reserved. ZORK is a registered trademark of Infocom, Inc. Revision 88 / Serial number 840726 West of House You are standing in an open field west of a white house, with a boarded front door. There is a small mailbox here. >
A companion repository contains a larger collection of vintage CP/M software you can use with this emulator:
Drives vs. Directories
By default when you launch cpmulator with no arguments you'll be presented with the CCP interface, with A: as the current drive. In this mode A:, B:, C:, and all other drives, will refer to the current-working directory where you launched the emulator from (i.e. they have the same view of files). This is perhaps the most practical way to get started, but it means that files are repeated across drives:
i.e. " A:FOO " is the same as " B:FOO ", and if you delete " C:FOO " you'll find it has vanished from all drives. In short " FOO " will exist on drives A: all the way through to P: .
" is the same as " ", and if you delete " " you'll find it has vanished from all drives.
If you prefer you may configure drives to be distinct, each drive referring to a distinct sub-directory upon the host system (i.e. the machine you're running on):
$ mkdir A/ ; touch A/LS.COM ; touch A/FOO.COM $ mkdir B/ ; touch B/DU.COM ; touch B/BAR.COM $ mkdir G/ ; touch G/ME.COM ; touch G/BAZ.COM
Now if you launch the emulator you'll see only the files which should be visible on the appropriate drive:
$ cpmulator -directories A > DIR A: A: FOO .COM | LS .COM A > DIR B: B: BAR .COM | DU .COM A > DIR G: G: BAZ .COM | ME .COM A > DIR E: No file
A companion repository contains a larger collection of vintage CP/M software you can use with this emulator:
This is arranged into subdirectories, on the assumption you'll run with the -directories flag, and the drives are thus used as a means of organization. For example you might want to look at games, on the G: drive, or the BASIC interpreters on the B: drive:
frodo ~/Repos/github.com/skx/cpm-dist $ cpmulator -directories A>g: G>dir *.com G: HITCH .COM | LEATHER .COM | LIHOUSE .COM | PLANET .COM G: ZORK1 .COM | ZORK2 .COM | ZORK3 .COM G>dir b:*.com B: MBASIC .COM | OBASIC .COM | TBASIC .COM
Note that it isn't currently possibly to point different drives to arbitrary paths on your computer, but that might be considered if you have a use-case for it.
Implemented Syscalls
You can see the list of implemented syscalls, along with a mention of how complete their implementation is, by running:
$ cpmulator -syscalls BDOS 00 P_TERMCPM 01 C_READ 02 C_WRITE 03 A_READ ..snip.. BIOS 00 BOOT 01 WBOOT ..snip..
Items marked "FAKE" return "appropriate" values, rather than real values. Or are otherwise incomplete.
The only functions with significantly different behaviour are those which should send a single character to the printer (BDOS "L_WRITE" / BIOS "LIST"), they actually send their output to the file print.log in the current-directory, creating it if necessary. (The path may be altered via the -prn-path command-line argument.)
The implementation of the syscalls is the core of our emulator, and they can be found here:
Debugging Failures & Tweaking Behaviour
When an unimplemented BIOS call is attempted the program it will abort with a fatal error, for example:
$ ./cpmulator FOO.COM {"time":"2024-04-14T15:39:34.560609302+03:00", "level":"ERROR", "msg":"Unimplemented syscall", "syscall":255, "syscallHex":"0xFF"} Error running FOO.COM: UNIMPLEMENTED
If things are mostly working, but something is not quite producing the correct result then we have some notes on debugging:
The following environmental variables influence runtime behaviour:
Variable Purpose SIMPLE_CHAR Avoid the attempted VT52 output conversion.
For reference the memory map of our CP/M looks like this:
0x0000 - Start of RAM
0xDE00 - The CCP
0xF000 - The BDOS (fake)
0xFE00 - The BIOS (fake)
Sample Programs
You'll see some Z80 assembly programs beneath samples which are used to check my understanding. If you have the pasmo compiler enabled you can build them all by running "make", in case you don't I've also committed the generated binaries.
Credits
Much of the functionality of this repository comes from the excellent Z80 emulator library it is using, written by @koron-go.
The CCP comes from my fork of the original cpm-fat However this is largely unchanged from the original CCP from Digital Research, although I did add the CLS , EXIT , HALT & QUIT built-in commands.
When I was uncertain of how to implement a specific system call the following two emulators were also useful:
https://github.com/ivanizag/iz-cpm Portable CP/M emulation to run CP/M 2.2 binaries for Z80. Has a handy "download" script to fetch some CP/M binaries, including BASIC, Turbo Pascal, and WordStar. Written in Rust.
https://github.com/jhallen/cpm Run CP/M commands in Linux/Cygwin with this Z80 / BDOS / ADM-3A emulator. Written in C.
References
Release Checklist
The testing that I should do before a release:
Play lighthouse of doom to completion, either victory or death.
Play lighthouse of doom to completion, either victory or death. Compile a program with ASM & LOAD. Confirm it runs.
Compile a program with ASM & LOAD. Confirm it runs. Compile HELLO.C and ECHO.C with Aztec C Compiler. Confirm the generated binaries run.
Compile HELLO.C and ECHO.C with Aztec C Compiler. Run BBC Basic, and play a game. Test "SAVE" and "LOAD" commands. Test saving tokenized AND raw versions. (i.e SAVE "FOO" , and SAVE "FOO", A .)
Run BBC Basic, and play a game. Compile a program with Turbo Pascal. Confirm the generated binary runs.
Compile a program with Turbo Pascal. Play Zork1 for a few turns. Test SAVE and RESTORE commands, and confirm they work.
Play Zork1 for a few turns. Test BE.COM
Test BE.COM Test STAT.COM
Test STAT.COM Test some built-in shell-commands; ERA TYPE, and EXIT.
Test some built-in shell-commands; ERA TYPE, and EXIT. Test samples/INTEST.COM samples/READ.COM , samples/WRITE.COM .
Let me know by filing an issue. If your program is "real" then it is highly likely it will try to invoke an unimplemented BIOS function.
Known issues:
Wordstar is broken. Don't care, because I have no strong attachment to it. Seems to be related to console I/O via " RST xx " instructions.
SUBMIT.COM doesn't work. I suspect at least part of this is CCP-related. It can print the last record of a file, but not execute it. I think it also doesn't work in reverse like it is supposed to.
Steve