{"id":15517335,"url":"https://github.com/skx/cpmulator","last_synced_at":"2025-04-11T18:07:57.294Z","repository":{"id":232456132,"uuid":"784388354","full_name":"skx/cpmulator","owner":"skx","description":"Golang CP/M emulator for zork, Microsoft BASIC, Turbo Pascal, Wordstar, lighthouse-of-doom, etc","archived":false,"fork":false,"pushed_at":"2025-02-03T18:15:28.000Z","size":718,"stargazers_count":111,"open_issues_count":0,"forks_count":4,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-11T18:07:36.566Z","etag":null,"topics":["cpm","emulation","golang","golang-application","z80","zork"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"skx","custom":"https://steve.fi/donate/"}},"created_at":"2024-04-09T18:46:49.000Z","updated_at":"2025-03-30T15:35:52.000Z","dependencies_parsed_at":"2024-04-15T10:33:50.680Z","dependency_job_id":"63ff0156-77e1-408d-90e7-c6ba998a1e96","html_url":"https://github.com/skx/cpmulator","commit_stats":null,"previous_names":["skx/go-cpm","skx/cpmulator"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fcpmulator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fcpmulator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fcpmulator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fcpmulator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skx","download_url":"https://codeload.github.com/skx/cpmulator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248456368,"owners_count":21106603,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpm","emulation","golang","golang-application","z80","zork"],"created_at":"2024-10-02T10:12:39.914Z","updated_at":"2025-04-11T18:07:57.260Z","avatar_url":"https://github.com/skx.png","language":"Go","funding_links":["https://github.com/sponsors/skx","https://steve.fi/donate/"],"categories":[],"sub_categories":[],"readme":"# cpmulator - A CP/M emulator written in golang\n\nThis repository contains a CP/M emulator, with integrated CCP (\"Console Command Processor\", i.e. shell), which is designed to run CP/M binaries.  The project was initially created to run [a text-based adventure game](https://github.com/skx/lighthouse-of-doom/) which I wrote a few years ago.  (That game was written in Z80 assembly language targeting CP/M, later it was ported to the ZX Spectrum.)\n\nOver time this project has become more complete, and I've now implemented enough functionity to run many of the well-known CP/M programs:\n\n* The Aztec C-Compiler.\n* Borland's Turbo Pascal.\n* Many early Infocom games:\n  * Zork 1, 2, \u0026 3.\n  * Planetfall.\n  * etc.\n* BBC and Microsoft BASIC.\n* Wordstar.\n\nAs things stand this project is \"complete\".  I'd like to increase the test-coverage for my own reassurance, but I've now reached a point where all the binaries I've tried to execute work as expected.  If you find a program that _doesn't_ work please [open an issue](https://github.com/skx/cpmulator/issues), otherwise I suspect ongoing development will be minimal, and sporadic.\n\n\u003e **NOTE** I've not implemented any notion of disk-support.  This means that opening, reading/writing, and closing files is absolutely fine, but any API call that refers to tracks, sectors, or disks will fail (with an \"unimplemented syscall\" error).\n\nA companion repository contains a collection of vintage CP/M software you can use with this, or any other, CP/M emulator:\n\n* [https://github.com/skx/cpm-dist](https://github.com/skx/cpm-dist)\n\n\n\n\n# Installation \u0026 Versioning\n\nThis emulator is written using golang, so if you have a working golang toolchain you can install in the standard way:\n\n```\ngo install github.com/skx/cpmulator@latest\n```\n\nIf you were to clone this repository to your local system you could build, and install it, by running:\n\n```\ngo build .\ngo install .\n```\n\nIf neither of these options are suitable you may download a binary from [our release page](https://github.com/skx/cpmulator/releases).\n\nReleases 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 - so far changes have been minor, but I'm not averse to changing parameters to internal packages, or adding/renaming/removing methods as necessary without any regard for external users.\n\n\n\n\n# Quick Start\n\n\n\n## Docker-based Quick Start\n\nIf you've got docker installed you can launch things by running:\n\n```sh\ndocker run --pull=always -t -i ghcr.io/skx/cpmulator:master\n```\n\n**NOTE** You must run with the `-t` and `-i` flags, otherwise the console will be broken.\n\nOnce launched you can explore, for example to run Zork type:\n\n* `G:`\n  * This will change to the G:-drive, which is where I've placed games.\n* `ZORK1`\n  * This will launch the Zork1 binary.\n\n\n\n## Local Quick Start\n\n* Build/Install this application.\n* Clone the associated repository of binaries:\n  * `git clone https://github.com/skx/cpm-dist.git /tmp/cpm-dist`\n* Launch the emulator, pointing at the binaries:\n  * `cpmulator -cd /tmp/cpm-dist -directories`\n* Start something:\n  * \"B:\", then \"MBASIC\" - to run BASIC.  (Type \"SYSTEM\" to exit.)\n  * \"G:\", then \"ZORK1\" - to play zork1.\n  * \"P:\", then \"TURBO\" - to run turbo pascal.\n  * \"E:\", then \"WS\" - to run wordstar.\n\n\n\n\n# Usage\n\nIf you launch `cpmulator` with no arguments then the default CCP will be launched, dropping you into a shell:\n\n```sh\n$ cpmulator\nA\u003edir\n\nA: LICENSE .    | README  .MD  | CPMULATO.    | GO      .MOD\nA: GO      .SUM | MAIN    .GO  | RET     .COM\n\nA\u003eTYPE LICENSE\nThe MIT License (MIT)\n..\nA\u003e\n```\n\nYou can terminate the CCP by typing `EXIT`.  The following built-in commands are available:\n\n\u003cdetails\u003e\n\u003csummary\u003eShow the standard built-in commands of the default CCP:\u003c/summary\u003e\n\n* `CLS`\n  * Clear the screen.\n* `DIR`\n  * Try \"`DIR *.COM`\" if you want to see only executables, for example.\n* `EXIT` / `HALT` / `QUIT`\n  * Terminate the CCP.\n* `ERA`\n  * Erase the named files, wildcards are permitted.\n* `TYPE`\n  * View the contents of the named file - wildcards are not permitted.\n* `REN`\n  * Rename files, so \"`REN NEW=OLD`\" - again note that wildcards are not permitted, nor is cross-drive renaming.\n\n\u003c/details\u003e\n\nThere are a pair of CCP implementations included within the emulator, and they can be selected via the `-ccp` command-line flag:\n\n* \"ccp\"\n  * The original CCP from Digital Research.\n  * Launch the emulator via `cpmulator -ccp=ccp ..`, or use `A:!CCP CCP` to change to it at run-time.\n* \"ccpz\"\n  * An enhanced CCP, which is the default\n  * \"`GET 0100 FOO.COM`\" will load a binary into RAM, at address 0x100.  Then \"`JMP 0100`\" will launch it.\n  * There are also built-in `PEEK` and `POKE` commands which can show/set memory contents.\n  * `LIST file.ext` will \"print\" the contents of `file.ext`, but see the note later about printer-output (TLDR; We write it to `print.log`.)\n  * The prompt will show the currently-selected user-number, for example if you run \"USER 3\".\n  * If a command isn't found in the current drive A: will be searched instead, which is handy.\n  * Finally any line beginning with the comment-character (`#`) will be ignored, which is useful for commenting purposes inside SUBMIT files.\n\nYou 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:\n\n```\n$ cpmulator /path/to/binary [optional-args]\n```\n\n\n\n## Command Line Flags\n\nThere are many available command-line options, which are shown in the output of `cpmulator -help`, but the following summary shows the most important/useful options:\n\n* `-cd /path/to/directory`\n  * Change to the given directory before running.\n* `-directories`\n  * Use directories on the host for drive-contents, discussed later in this document.\n* `-embed`\n  * Enable/Disable the embedded binaries we unconditionally add to the A:-drive.  (The utilities to change the output driver, toggle debugging, etc.)\n* `-log-path /path/to/file`\n  * Output debug-logs to the given file, creating it if necessary.\n  * **NOTE**: You can run `A:!DEBUG 1` to enable \"quick debug logging\", and `A:!DEBUG 0` to turn it back off again, at runtime.\n* `-prn-path /path/to/file`\n  * All output which CP/M sends to the \"printer\" will be written to the given file.\n* `-list-syscalls`\n  * Dump the list of implemented BDOS and BIOS syscalls.\n* `-list-input-drivers` and `-list-output-drivers` to see the available I/O driver-names, which may then be selected via the `-input` and `-output` flags.\n* `-version`\n  * Show the version number of the emulator, and exit.\n\n\n\n## Startup Processing\n\nWhen the CCP is launched for interactive execution, we allow commands to be executed at startup:\n\n* If `SUBMIT.COM` **and** `AUTOEXEC.SUB` exist on A:\n* Then the contents of `AUTOEXEC.SUB` will be executed.\n  * We secretly run \"`SUBMIT AUTOEXEC`\" to achieve this.\n\nThis allows you to customize the emulator, or perform other \"one-time\" setup via the options described in the next section.\n\n\n\n## Runtime Behaviour Changes\n\nThere are a small number of [extensions](EXTENSIONS.md) added to the BIOS functionality we provide, and these extensions allow changing some aspects of the emulator at runtime.\n\nThe behaviour changing is achieved by having a small number of .COM files invoke the extension functions, and these binaries are embedded within our emulator to improve ease of use, via the [static/](static/) directory in our source-tree.  This means no matter what you'll always find some binaries installed on A:, despite not being present in reality - you can disable the appearance of these binaries via the `-embed=false` command-line flag.\n\n\u003e **NOTE** To avoid naming collisions all our embedded binaries are named with a `!` prefix, except for `#.COM` which is designed to be used as a comment-binary.\n\n\n### CCP Handling\n\nWe default to loading the enhanced CCP, but allow the original from Digital Research to be used via the `-ccp` command-line flag.   The binary `A:!CCP.COM` lets you change CCP at runtime.\n\n\n### Ctrl-C Handling\n\nTraditionally pressing `Ctrl-C` would reload the CCP, via a soft boot.  I think that combination is likely to be entered by accident, so in `cpmulator` we default to requiring you to press Ctrl-C _twice_ in a row to reboot the CCP.\n\nThe binary `A:!CTRLC.COM` which lets you change this at runtime.  Run `A:!CTRLC 0` to disable the Ctrl-C behaviour, or `A:!CTRLC N` to require N consecutive Ctrl-C keystrokes to trigger the restart-behaviour (max: 9).\n\n\n### Console Input\n\nWe default to using the portable `termbox-go`-based input-handler, this can be changed via the `-input` command-line flag at startup.  Additionally it can be changed at runtime via `A:!INPUT.COM`.\n\nRun `A:!INPUT stty` to use the non-portable Unix-centric approach which provides a scrollback, and uses the system's `stty` binary to enable/disable character echoing.\n\n\n### Console Output\n\nWe default to pretending our output device is an ADM-3A terminal, this can be changed via the `-output` command-line flag at startup.  Additionally it can be changed at runtime via `A:!OUTPUT.COM`.\n\nRun `A:!OUTPUT ansi` to disable the output emulation, or `A:!OUTPUT adm-3a` to restore it.\n\nYou'll see that the [cpm-dist](https://github.com/skx/cpm-dist) repository contains a version of Wordstar, and that behaves differently depending on the selected output handler.  Changing the handler at run-time is a neat bit of behaviour.\n\n\n### Debug Handling\n\nWe expect that all _real_ debugging will involve the comprehensive logfile which is created via the `-log-path` argument to the emulator, however we\nhave a \"quick debug\" option which will merely log the syscalls which are invoked, and this has the advantage that it can be enabled, or disabled, at\nruntime.\n\n`A:!DEBUG.COM` will show the state of the flag, and it can be enabled with `A:!DEBUG 1` or disabled with `!DEBUG 0`.\n\nFinally `A:!VERSION.COM` will show you the version of the emulator you're running.\n\n\n\n\n# Drives vs. Directories\n\nBy 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 from which you launched the emulator.  This is perhaps the most practical way to get started, but it means that files are repeated across drives:\n\n* 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.\n  * In short \"`FOO`\" will exist on drives `A:` all the way through to `P:`.\n\nIf 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):\n\n```sh\n$ mkdir A/  ; touch A/LS.COM ; touch A/FOO.COM\n$ mkdir B/  ; touch B/DU.COM ; touch B/BAR.COM\n$ mkdir G/  ; touch G/ME.COM ; touch G/BAZ.COM\n```\n\nNow if you launch the emulator you'll see only the files which _should_ be visible on the appropriate drive:\n\n\n```sh\n$ cpmulator -directories\nA\u003eDIR A:\nA: FOO     .COM | LS      .COM\n\nA\u003eDIR B:\nB: BAR     .COM | DU      .COM\n\nA\u003eDIR G:\nG: BAZ     .COM | ME      .COM\n\nA\u003eDIR E:\nNo file\n```\n\nA companion repository contains a larger collection of vintage CP/M software you can use with this emulator:\n\n* [https://github.com/skx/cpm-dist](https://github.com/skx/cpm-dist)\n\nThis 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:\n\n```\nfrodo ~/Repos/github.com/skx/cpm-dist $ cpmulator  -directories\nA\u003eg:\nG\u003edir *.com\nG: HITCH   .COM | LEATHER .COM | LIHOUSE .COM | PLANET  .COM\nG: ZORK1   .COM | ZORK2   .COM | ZORK3   .COM\n\nG\u003edir b:*.com\nB: MBASIC  .COM | OBASIC  .COM | TBASIC  .COM\n```\n\nYou can also point specific drives to particular paths via the `-drive-X` command-line arguments.  For example the following would have A: and B: pointed to custom paths, and C:-P: using the current working directory:\n\n```\n$ cpmulator -ccp=ccpz -drive-a /tmp -drive-b ~/Repos/github.com/skx/cpm-dist/G/\n```\n\n\n\n\n# Implemented Syscalls\n\n\nYou can see the list of implemented syscalls, along with a mention of how complete their implementation is, by running:\n\n```\n$ cpmulator -list-syscalls\nBDOS\n\t00 P_TERMCPM\n\t01 C_READ\n\t02 C_WRITE\n\t03 A_READ\n..snip..\nBIOS\n\t00  BOOT\n\t01  WBOOT\n..snip..\n```\n\nItems marked \"FAKE\" return \"appropriate\" values, rather than real values.  Or are otherwise incomplete.\n\n\u003e 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.)\n\nThe implementation of the syscalls is the core of our emulator, and they can be found here:\n\n* [cpm/cpm_bdos.go](cpm/cpm_bdos.go) - BDOS functions.\n  * https://www.seasip.info/Cpm/bdos.html\n* [cpm/cpm_bios.go](cpm/cpm_bios.go) - BIOS functions.\n  * https://www.seasip.info/Cpm/bios.html\n\n\n\n\n# Portability\n\nThe 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, although the appropriate primitives exist so building such support isn't impossible, it just relies upon writing per-environment support, using something like the [ReadPassword](https://pkg.go.dev/golang.org/x/term#ReadPassword) function from the standard-library.\n\nI sidestepped this whole problem initially, just invoking the `stty` binary to enable/disable the echoing of characters on-demand, but that only works on Linux, BSD, and Mac hosts.  To be properly portable I had to use the [termbox](https://github.com/nsf/termbox-go) library for all input, but that means we get no scrollback/history so there's a tradeoff to be made.\n\nBy default input will be read via `termbox` but you may you specify a different driver via the CLI arguments:\n\n* `cpmulator -input xxx`\n  * Use the input-driver named `xxx`.\n* `cpmulator -list-input-drivers`\n  * List all available input-drivers.\n\n\n\n\n# Debugging Failures \u0026 Tweaking Behaviour\n\nWhen an unimplemented BIOS call is attempted the program it will abort with a fatal error, for example:\n\n```\n$ ./cpmulator FOO.COM\n{\"time\":\"2024-04-14T15:39:34.560609302+03:00\",\n  \"level\":\"ERROR\",\n  \"msg\":\"Unimplemented syscall\",\n  \"syscall\":255,\n  \"syscallHex\":\"0xFF\"}\nError running FOO.COM: UNIMPLEMENTED\n```\n\nIf things are _mostly_ working, but something is not quite producing the correct result then we have some notes on debugging:\n\n* [DEBUGGING.md](DEBUGGING.md)\n\nFor reference the memory map of our CP/M looks like this:\n\n* 0x0000 - Start of RAM\n* 0xDE00 - The CCP\n* 0xF000 - The BDOS (fake)\n* 0xFE00 - The BIOS (fake)\n\n\n\n\n# Credits and References\n\n\n* Much of the functionality of this repository comes from the [excellent Z80 emulator library](https://github.com/koron-go/z80) it is using, written by [@koron-go](https://github.com/koron-go).\n* The default CCP comes from [my fork](https://github.com/skx/z80-playground-cpm-fat/) of the original [cpm-fat](https://github.com/z80playground/cpm-fat/)\n  * However this is largely unchanged from the [original CCP](http://www.cpm.z80.de/source.html) from Digital Research, although I did add the `CLS`, `EXIT`, `HALT` \u0026 `QUIT` built-in commands.\n* Reference Documentation\n  * [CP/M BDOS function reference](https://www.seasip.info/Cpm/bdos.html).\n  * [CP/M BIOS function reference](https://www.seasip.info/Cpm/bios.html).\n  * [CP/M Operating System Manual](http://www.gaby.de/cpm/manuals/archive/cpm22htm/)\n    * Contains an assembler reference, DDT commands, etc, etc.\n* Other emulators which were useful resources when some functionality was unclear:\n  * [https://github.com/ivanizag/iz-cpm](https://github.com/ivanizag/iz-cpm)\n    * Portable CP/M emulation to run CP/M 2.2 binaries for Z80.\n    * Has a handy \"download\" script to fetch some CP/M binaries, including BASIC, Turbo Pascal, and WordStar.\n    * Written in Rust.\n  * [https://github.com/jhallen/cpm](https://github.com/jhallen/cpm)\n    * Run CP/M commands in Linux/Cygwin with this Z80 / BDOS / ADM-3A emulator.\n    * Written in C.\n\n\n\n\n# Release Checklist\n\nBefore I make a release I carry out testing of the repository state, to avoid any obvious regressions.  The following checklist is the bare minimum I carry out prior to making a new tagged-release.\n\nNote that the entries \"checked\" here are those entries which are tested automatically via the contents of [test/](test/).  (This automated testing essentially launches the emulator and pipes in fake keyboard input, then looks to see that the expected output is generated.  Scripted automation, in other words.)\n\n* [X] Confirm DDT can be used to trace execution of a simple binary.\n* [X] Confirm the A1 Apple Emulator can be launched, and BASIC will run.\n* [X] Play lighthouse of doom to completion, either victory or death.\n* [X] Compile HELLO.ASM with ASM \u0026 LOAD.  Confirm\n  * [X] Confirm the generated binary executes successfully.\n* [X] Compile ECHO.C with the Aztec C Compiler.\n  * [X] Confirm the generated binary executes successfully.\n* [X] Run BBC Basic, and play a game.\n  * [ ] Test \"SAVE\" and \"LOAD\" commands.\n  * [ ] Test saving tokenized AND raw versions. (i.e `SAVE \"FOO\"`, and `SAVE \"FOO\", A`.)\n* [X] Compile HELLO.PAS with Turbo Pascal.\n  * [X] Confirm the generated binary executes successfully.\n* [X] Launch Zork1 and play for a few turns.\n  * [X] Test SAVE and RESTORE commands, and confirm they work.\n* [ ] Test BE.COM\n* [ ] Test STAT.COM\n* [X] Test some built-in shell-commands; ERA, TYPE, and EXIT.\n\n\n\n## Bugs?\n\nLet me know by filing an issue.\n\nSteve\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Fcpmulator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskx%2Fcpmulator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Fcpmulator/lists"}