Skip to content

skx/cpmulator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cpmulator - A CP/M emulator written in golang

This repository contains a CP/M emulator, with integrated CCP, which is primarily designed to launch simple CP/M binaries:

  • The project was created to run a text-based adventure game I wrote a few years ago, to amuse my child.
    • This was written in Z80 assembly language and initially targeted at CP/M, although it was later ported to the ZX Spectrum.
  • Over time it has become more functional:
    • It can now run ZORK 1, 2, & 3, as well as The Hitchhiker's guide to the galaxy and other similar games.

I've implemented enough of the BIOS functions to run simple binaries, including Microsoft BASIC, but I've not implemented any notion of disk-based access. (i.e. 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

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 our release page.

Usage

If you launch cpmulator with no arguments then the integrated CCP ("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 CCP built-in commands:
  • 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.
  • 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.

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]

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 unique 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:.

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>dir
No file

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.

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

There is integrated support for logging the functions that were executed successfully, along with other internal details. To see the log set the environmental variable DEBUG to a non-empty value, which will trigger output to STDERR where you can save it.

The console-I/O is blocking by default, but that can be changed by setting the environmental variable NON_BLOCK to any non-empty value. This will increase CPU load (as it essentially causes the process to run in a busy-loop testing for pending console input).

Here is the complete list of environmental variables which influence behaviour:

Variable Purpose
DEBUG Send a log of CP/M syscalls to STDERR
NON_BLOCK Avoid blocking for console input, instead poll in a busy-loop
SIMPLE_CHAR Avoid the attempted VT52 output conversion.

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

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

Bugs?

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.

Outstanding issues I'm aware of:

  • I don't implement some of the basic BIOS calls that might be useful
    • Get free RAM, etc, etc.
    • These will be added over time as their absence causes program-failures.

Steve