Overview

The CP/M emulator runs real Z80 — or 8080, see cpm_cpu.COM software against an emulated CP/M 2.2, with a virtual Hayes modem the guest can dial out through.

Two separate features in this gateway both say “CP/M”, and it is worth being clear which is which before reading on:

FeatureWhat it isWhere
CP/M emulator
(this page)
A real CPU emulator — a Z80, or an 8080 if cpm_cpu says so — running actual .COM programs against an emulated CP/M 2.2 — BDOS, BIOS, optional RomWBW HBIOS, and a virtual modem. src/cpm/ + telnet/cpm_emu.rs
Gateway Shell A pure-Rust CP/M-styled file manager over the transfer directory. An A> prompt and familiar commands, but no Z80 and no guest code. telnet/kernel.rs

Be sure you trust the CP/M files you run — and the emulator is bounded on three axes regardless. Every file call is jailed under transfer_dir/CPM; a runaway program is stopped by cpm_emu_max_minstr; and a double-ESC always returns you to the A> prompt, even from a program that never reads the console. It services BDOS/BIOS only — there is no path from guest code to a host command.

Reaching it: from the main menu choose K — CP/M System. The emulator is enabled by default (cpm_emu_enabled); the drive folders transfer_dir/CPM/AP are created on first launch, and the bundled EGT8080 terminal is placed on drive A: at the same time — in both its builds, EGT8080.COM and EGT8080.COM.

Getting your own files onto a drive: the drives are ordinary folders, so in the gateway's File Transfer menu change directory to CPM/A and upload there — the file is on drive A: the moment it lands, no import step. EGT8080 can also fetch one over the virtual modem from inside the emulator. The emulator's HELP says both.

The drives are shared between sessions. Each session gets its own CPU and its own 64 KB, but one set of folders serves all of them, so two people in the emulator see the same files. A session entering while another is already inside is told so. Writing is protected: a file one session is writing is claimed until it closes the file or leaves, and another session's write, erase or rename of that file is refused rather than interleaved — which is what would otherwise happen, since the BDOS here opens the host file per record. The refusal reaches the guest as an ordinary CP/M failure, so expect a program to report something like DISK OR DIRECTORY FULL: that is the only code CP/M's create call has. Reads are deliberately not locked, so two people can share a library of .COM files freely.

Performance note. The Z80 core is not the limiting factor — it steps at roughly 33 MIPS (about 65× a 4 MHz Z80) and services around 6.4 million console-output traps per second. What governs perceived speed is the async driver loop, which regains control at every BDOS/BIOS/HBIOS trap. Anything that loop waits on is therefore paid once per emulated character, which is why neither the readiness probe nor the peer poll may sit on a timer.


Configuration Keys

All cpm_emu_* keys live in egateway.conf and are editable from all three config UIs (telnet, web, GUI).

KeyDefaultMeaning
cpm_emu_enabledtrueWhether the CP/M System menu item exists at all. Set false to refuse guest code entirely.
cpm_screen_inputtrueMay the web UI's VDM / Dazzler page type at a booted disk? The screen is readable either way. See Screens.
cpm_emu_max_minstr2000Instruction ceiling in millions before a running program is stopped. The runaway guard. Minimum 1, maximum 1000000 — a larger value is capped rather than refused, so a setting meant as “no limit” is kept as far as it goes. Bounds one transient in the emulator; a booted disk has no ceiling.
cpm_mounts(empty)Images mounted on drives, as A=name.dsk,C=other.dsk. Bare filenames inside CPM/images. See Disk Images.
cpm_boot_image(empty)What the CP/M menu item runs: empty = the CP/M emulator, or a bare filename in CPM/images to cold-boot that disk. A name that is not there falls back to the emulator, shown as (missing). See Booting.
cpm_boot_machineautoWhich machine a booted disk runs on — its console and its disk controllers. auto reads the ports the disk’s own boot loader drives, so ordinarily you set nothing; when a disk does not say plainly the Altair 88-2SIO machine stands, and the boot screen says which happened. Set it explicitly to override. Ignored by the emulator, which has no console to place. See Booting.
cpm_boot_backspacebackspaceWhat a booted disk is handed when you press Backspace — backspace (BS, 08h) or rubout (DEL, 7Fh). Ignored by the emulator, which reads its own console line and accepts either. This is the whole answer: the per-disk question went with the boot picker in 0.9.2. See Booting.
cpm_boot_writabletrueMay a booted disk WRITE to the images it is running? On by default: a vintage operating system saves files, formats disks and updates its own directory, and discarding those writes loses the work silently. A mounted image is read through our own filesystem, which can refuse a request it does not like; a booted disk owns the whole image and rewrites the file when it leaves, so no guard remains that understands what the guest is asking for — turn this off to keep every disk exactly as it is. Covers the boot disk and every image mounted beside it, and it is a standing setting rather than the per-visit question the old boot picker asked. Ignored by the emulator. See Booting.
cpm_cpuz80Which processor both CP/M machines run — the emulator's transient programs and a booted disk's whole operating system. z80 is a strict superset of the 8080, so it runs every disk here; 8080 is the processor the Altair actually shipped with. EGT8080.COM is placed on drive A:: built to the 8080's instruction set, so it runs on either setting. EGT80.COM, the Z80 build, is placed beside it for the Z180 ASCI ports — it is the one to use on an SC126, and the one not to type under 8080, where it stops at its first Z80-only opcode. The hbios_* modem profiles emulate RomWBW — Z80/Z180 firmware — so other people's software that looks for it is generally Z80 code and will not run on an 8080, though our HBIOS itself answers an 8080 perfectly well (RST 8 is an 8080 instruction). See Booting.
cpm_printertextWhere CP/M printer output goes — off (your terminal, as it always has), odt (an OpenDocument text file) or text. Reaches both CP/M machines, by two different routes: in the emulator the printer is an OS service (BDOS function 5 and the BIOS LIST vector), so WordStar, MBASIC's LPRINT and PIP LST:=FILE.TXT all arrive; a booted disk drives the board below. The document lands in a printer folder inside the transfer directory, named PRINT-YYYYMMDD-HHMMSS, 5 seconds after the last character printed — and in the emulator also the moment the program returns to A>, which is exact. See Printing.
cpm_printer_portaltair_cWhich printer board a booted disk finds: altair_c (Altair line printer, data register 03h) or off. Ignored by the emulator, whose printer is a BDOS service with no port at all, and ignored entirely when cpm_printer is off. See Printing.
cpm_emu_uartrc2014_1bHow the guest reaches the virtual modem — a machine port, the BDOS AUX: device, or RomWBW HBIOS. See UART Profiles.
cpm_emu_echotrueVirtual modem ATE — echo command-mode characters.
cpm_emu_verbosetrueVirtual modem ATV — word result codes rather than numeric.
cpm_emu_quietfalseVirtual modem ATQ — suppress result codes.
cpm_emu_x_code4Virtual modem ATX result-code level (0–4).
cpm_emu_dcd_mode1Virtual modem AT&C — 0 forces DCD on, 1 tracks real carrier.
cpm_emu_s_regs(empty)Saved S-register list (S0S27) — empty until something is saved. AT&W writes it; ATZ restores it.

cpm_emu_uart defaults to a live port rather than off because it is the port most CP/M software here expects, and it is what EGT8080 defaults to — so the emulator and its terminal agree out of the box. That is a deliberate widening: with a port selected, guest code can open a socket with ATDT. An operator who does not want that sets cpm_emu_uart = off; peer dialling additionally requires allow_peer_dial.


CCP Commands

The built-in commands at the A> prompt. Anything else is treated as a program name and loaded as name.COM from the current drive.

CommandDoes
DIR [d:][afn]List files: all of the current drive, or only those matching a filespec (DIR *.COM), or another drive without selecting it (DIR B:, DIR B:*.TXT). Same ?/* wildcards as the BDOS search and ERA; a malformed filespec is reported, not read as “everything”.
ERA name
also DEL
Erase file(s); wildcards accepted.
REN new=old
also RENAME
Rename a file.
TYPE fileShow a text file.
SAVE n fileSave n pages of the TPA to a file.
USER nSelect user area.
A:P:Change drive. All sixteen CP/M 2.2 drives exist, each a folder under transfer_dir/CPM.
VER
also VERSION
Emulator version and TPA size.
HELLOBDOS print-string demo (function 9).
ECHOInteractive console demo.
batch
$$$.SUB
While A:$$$.SUB exists the next command is taken from it instead of the keyboard and echoed as it runs; the file is consumed a record at a time and erased when done. Written by CP/M's own SUBMIT.COM. As on real CP/M 2.2: an unrecognised command ends the batch, and batches only work from A: (SUBMIT writes to the current drive, the processor reads A:). CP/M 3's conditionals/parameters and XSUB are not emulated.
HELP
also ?
This command list.
EXIT
also BYE, QUIT
Leave CP/M and return to the gateway menu. Pressing ESC twice at the prompt does the same; a single ESC just cancels the line you are typing.
nameRun name.COM from the drive. A drive prefix works (B:PIP); the extension is always forced to COM, as the real CCP does.

Double-ESC always gets you back. The driver drains the wire between CPU batches, so two ESC keys break out even of a program that never reads the console — a compute-bound loop included. On a PETSCII terminal 0x5F counts as ESC too.


Disk Images

A .dsk image can be mounted on any drive A:–P:. The drive then reads and writes the CP/M filesystem inside the image instead of its folder under CPM/.

Mounting hides a drive folder; it does not touch it. The folder's files are exactly where they were and come back the moment the image is unmounted.

Naming, and why an unnamed image is read-only

Put images in CPM/images inside the transfer directory. Name one <format>_<anything>.dsk — the format token, an underscore, then whatever you like:

ibm3740_cpm22.dsk
altairhd_cobol.dsk

An image named this way mounts read-write without further ado. But you do not have to rename anything. An image with no prefix is identified by inspection and mounts read-write too, provided its CP/M filesystem checks out — because no two formats here are the same size, so the size names the format outright and the only real question is whether the file holds that filesystem at all. The whole directory is read and checked: every allocation block inside the disk, no block claimed by two files, every record count matching the blocks it claims.

An image that does not check out still mounts, but read-only, and says what was wrong. That matters more than it sounds: plenty of disks are 256,256 bytes without being CP/M — a UCSD p-System disk is one. Reading them is safe and usually what you want; writing to them is not. Renaming with a prefix is an override that skips the inspection, not a requirement.

Two things bound that override, because it is the one path that mounts read-write without looking inside. A prefix cannot make a file the wrong size into a disk: the size must still be the format's, and for a while it only had to be at least that, so a 625,920-byte Cromemco double-density image called ibm3740_… was accepted and read as a 256,256-byte single-density one — writable, with its directory landing mid-track. And the size is not required to be exact: anything short of one whole record over is a trailer, which is real — three images in the standard Altair set are their format's size plus 96 bytes, and demanding an exact match refused them on the file length before their directory was ever read.

The read-only rule is the last line of defence in this feature, and it is worth knowing why it is absolute. Every other way a write could go wrong is caught by a check that can see the problem — a block past the end of the disk, a directory entry that did not stick, two files claiming one block. A misidentified format is the one failure none of them can catch: every offset is computed from the wrong geometry, so every check agrees with every other check, and the first write lands in the middle of somebody's files.

What changed is which misidentification is possible. No two formats in the table below are the same size — that was checked, not assumed — so a size never has to choose between two layouts. The danger that remains is a file of the right size holding something else entirely, and that one a consistency check can actually answer. So the rule is no longer “rename it or read it”; it is “prove the filesystem or read it”.

A mount is also forced read-only when the image file is read-only on the host, or when its CP/M directory arrives damaged — an entry naming a block off the end of the disk, one claiming directory space, or two files sharing a block. In that last case the gateway deliberately does not repair anything: guessing which file really owns a shared block is how a backup gets destroyed. The mount screens say which of the three reasons applies.

Formats

TokenFormatSize
ibm3740IBM 3740 8" SSSD — the closest thing CP/M had to a universal disk. Tarbell, Cromemco SD, IMSAI / z80pack.256,256
altair8Altair 88-DCDD 8″ SSSD floppy (MITS). 137-byte sectors, and a layout that changes partway down the disk — see the Disk Format Reference.337,568
altairhdAltair 88-HDSK hard disk (the Altair-Duino set). 256-byte sectors, two CP/M records in each.4,988,928
cromemcoddCromemco 8″ SSDD (MICAH CP/M 2.2). Track 0 is single density so a single-density boot ROM can read it; the rest is 16 sectors of 512 bytes. Declares 254 blocks on a medium that would hold 300.625,920
cromemcodsddCromemco 8″ DSDD (ITC CP/M 2.2). Both sides, a CP/M track being a whole cylinder. Its XLT pointer says it does not translate and its BIOS interleaves by four anyway — believing the pointer gave a format that listed correctly and read scrambled.1,256,704
z80packhdz80pack cpmsim hard disk. 255 tracks × 128 sectors × 128 bytes, 2 KB blocks, 1,024 directory entries, and no reserved tracks — the directory begins at byte zero and the whole 4 MB is data. A simulator's disk rather than a machine's, and it shows.4,177,920

Every entry was measured from a real image rather than transcribed from a table. altair8 was withdrawn for months because its directory read correctly while file content past the first half of a track did not; it is back because a booted Altair was made to read its own disk out over the virtual modem, which gave an exact answer where every earlier check could only score a guess. That measurement, the two skew tables and the sector-checksum formulas are written up in the Disk Format Reference. CPM/images/readme.txt carries the same format list, generated from the same source, along with what to rename an Altair-Duino or IMSAI disk to.

Not every .dsk is a CP/M disk

Altair DOS, Altair Disk BASIC, Time Sharing BASIC and UCSD p-System disks use their own filesystems. They are common in the same collections, and mounting them either fails outright or shows no files — which is correct, not a fault. Those disks are for booting, where the disk's own operating system does its own filesystem work and ours never has to understand the layout.

Mounting

WhereHow
Telnet / SSHSettings → CP/M Emulator → I. A short wizard: pick an image, pick a drive, confirm.
WebAI, Browser, Weather & CP/M → More → Mount CP/M drives.
DesktopSame group → More → Mount CP/M Drives.

Changes take effect immediately, in every session, and are written to cpm_mounts so they survive a restart. A drive somebody is currently using cannot have its disk changed; the screens show which drives are in use and why.

Mounting an image on drive A: hides both terminals, since EGT8080.COM and EGT8080.COM live in the A: folder. Moving files in and out of a mounted image is done with XMODEM from inside the terminal, so mounting over A: costs you the tool you would use to populate the image. Prefer B: or later.


Booting a Disk Image

Mounting gives you one drive with our BDOS underneath. Booting hands the whole machine to the disk: 64 KB of memory, a MITS 88-DCDD floppy controller, a MITS 88-HDSK hard disk controller, an 88-2SIO console and the front-panel sense switches — the disk's size decides which controller it lands on. The disk's own operating system then does everything, including its own filesystem work — which is why booting reaches the disks mounting cannot.

MountBoot
ScopeOne drive of sixteenThe whole machine
FilesystemOursThe guest's
Other drivesFolders and images alongsideMounted images come along, at the board slot their letter names; folders do not
Our A>, EXIT, EGT8080YesNo — the guest talks to hardware
Getting outEXIT, or ESC twicePress ESC twice

What boots

Twenty-four of the images in the Altair-Duino sample set boot, and twenty-two of those print a sign-on straight away. An image may be an 8-inch floppy (337,568 bytes), a minidisk (76,720) or a 4.9 MB 88-HDSK hard disk (4,988,928) — and every one of the eleven hard-disk images in that collection boots. A short trailer past the last sector is accepted, since several images in circulation carry one.

SystemSign-on
Altair CP/M 2.263K CP/M / Version 2.2mitsA>
CP/M 3.0CP/M V3.0 Loader, Copyright (C) 1982, Digital Research
CP/M 2.2ATFor Altair 8" Floppy and Tarbell SDC
Altair DOSMEMORY SIZE?
Altair Disk Extended BASICALTAIR DISK EXTENDED BASIC, VERSION 300-5-F
Time Sharing BASICALTAIR TIMESHARING BASICRECONFIGURE (Y, N, L)?

A programs disk — data rather than a system disk — has no boot sector, and is refused with a message saying so rather than run.

Choosing what CP/M runs

WhereHow
Telnet / SSHSettings → CP/M Emulator → B (Boot settings) → R, which opens a list of the emulator and every disk that boots.
WebAI, Browser, Weather & CP/M → More → CP/M runs.
DesktopSame group → CP/M runs.

The setting is cpm_boot_image: empty (the default) runs the CP/M emulator, a bare filename in CPM/images boots that disk.

If the disk it names is not there, the emulator runs — deleting an image costs you the boot, not the whole CP/M feature — and the gateway says so rather than leaving you to work it out. Every screen that shows the setting marks it (missing), or (invalid name) when the value could never have named a disk at all, and the log records the fallback once per visit. The disk screens follow suit: their rows go back to naming drives A:P: instead of the slots of a board nobody is going to get.

Which machine — where the disk finds its console

A booted disk brings its own operating system, and that operating system was written for a particular machine. Most of these disks expect the Altair's 88-2SIO console at 10h/11h, which is the default and what every disk that boots today uses. Some do not, and the symptom is distinctive: the disk loads, reads its sectors, and then goes completely quiet, because it is printing to hardware that is not in the machine and polling a keyboard port that will never answer.

cpm_boot_machine picks the machine. It is on the same screens as CP/M runs above — on telnet, the CP/M settings screen's B now opens a Boot Settings screen holding both.

ValueConsoleNotes
autoThe default. Read from the disk's own boot code, falling back to altair_2sio when the disk does not say plainly.
altair_2sioAltair 88-2SIO, 10h/11hWhat auto falls back to. Every Altair disk boots because of it.
altair_sioAltair 88-SIO, 00h/01hActive-low status.
console_0404h/05hReady when the status bit is clear.
console_04_cuter04h/05h in, ROM outThe guest prints by calling a Processor Technology CUTER ROM, which we synthesise at C019. Reaches Tarbell 48K CPM 2.2.
z80pack00h/01h, whole-byteA different machine entirely, with z80pack's cpmsim disk device instead of the Altair boards. Reaches CP/M 1.3, 1.4, 2.2 and 3.0, MP/M and UCSD p-System IV.
cromemcoCromemco TU-ART, 00h/01hBit 6 RX, bit 7 TX, active high. Carries the Cromemco 4FDC/16FDC floppy controller instead of the Altair boards. Reaches CDOS and two more CP/M 2.2 systems, on single- and double-density 8″ disks.

The machine picks the disk controllers too, not just the console. That is not tidiness: z80pack's device answers on 0Ah11h, which contains both the 88-DCDD's data register and the 88-2SIO console, and a machine answers disk controllers before its console. Put those boards together and every Altair disk would go silent with its console being answered by a disk controller. It is also what settles a size two boards both claim — 256,256 bytes is an IBM 3740 to the Tarbell and an 8″ SSSD to cpmsim.

auto reads a declaration; it does not guess. A boot loader must drive its own disk controller's registers, so those IN and OUT operands are in the image, put there by whoever built the disk — the same class of evidence as the 88-HDSK volume label or a disk's own DPB. Only ports belonging to exactly one board count, and if the evidence does not name one machine, your setting stands.

It deliberately will not pick a console for the Altair boards, and a real disk is why. MITS software chooses its console from the front-panel sense switches at run time, so its BIOS carries drivers for the 88-SIO, the 88-2SIO, the 4PIO and the ACR all at once; scanning finds consoles the disk will never use. DISK0E was detected as altair_sio on that evidence and went silent, having booted perfectly on the default. Naming a wrong machine breaks a disk that worked, so detection is restricted to what it can actually prove.

The backspace key

Type TESTING into a freshly booted Altair disk, press Backspace four times, and the screen reads TESTINGGNIT. Nothing is broken. Your terminal's Backspace key sends DEL (7Fh), and most of these operating systems read that as a Teletype rubout: they delete the character and then print the character they deleted, so the operator of a printing terminal can see what came off the end of the line. On a screen it just looks wrong.

cpm_boot_backspace decides which byte a booted guest is handed. There is no answer that suits every disk, and this was measured across two whole disk folders rather than reasoned — 38 images that reach a prompt, each booted twice and asked what it echoes:

GuestBS 08hDEL 7FhWants
MITS CP/M 2.2, Altair Disk Extended BASIC, Altair Hard Disk BASIC08 20 08 — erasesG / \G — ruboutbackspace
Digital Research CP/M 2.2, MP/M, UCSD p-System IV08 20 08 — erases08 20 08 — eraseseither
CP/M 1.3, 1.4, 1975^H printed literallyG — ruboutrubout

29 of the 38 erase on BS and 7 on DEL, which is why backspace is the default. The third row is why the setting exists: for CP/M 1.x the rubout is the editing key, and translating breaks something that already worked. A Commodore's own DEL key (PETSCII 14h) is folded to whichever byte the setting names, because no guest in either survey recognises 14h at all — leaving it alone would give a C64 no editing key rather than the disk's own one.

It is a setting, not a question. The right answer is a property of the operating system on the disk rather than of the gateway, and until 0.9.2 the telnet boot picker asked it per disk, seeded from cpm_boot_backspace. That picker was a second way to boot and was removed; the key is now the whole answer, so a CP/M 1.x disk wants rubout set before you boot it. The emulator is unaffected either way: it reads its own console line and has always accepted both bytes.

Both boot lists show only disks that boot. The cpm_boot_image selector on all three configuration screens and the telnet boot picker each run the actual cold start — the same sequence a real boot runs — and offer an image only if it reaches an entry point. This is not a size or a filename test, and it could not be: a companion disk of programs is exactly the size of the system disk it belongs to, which is how all four of the Altair collection's companions came to be listed as bootable and to fail when chosen. They belong on the mount screen, which is deliberately not filtered — mounting one beside its own system disk is the entire point of it.

A disk this machine has no board for stays listed, because that is a cpm_boot_machine setting you can change; a disk that vanished when you changed a setting would be a worse puzzle than a boot that fails naming the boards this machine has.

The other drives

A booted guest is handed every mounted image, each in the controller slot its drive letter names — B: is slot 1, C: is slot 2, F: is slot 5. So you can mount several disks, boot one, and copy between them with the guest's own PIP.

What a slot is belongs to the board. On the floppy controllers it is a drive. On the Altair 88-HDSK it is a platter — a Datakeeper drive carries four of them and addresses each by head number — so slots 0–3 are the first drive's four platters, and the screens name them unit 0.1 rather than with a bare number. That is not a detail: Altair Hard Disk BASIC numbers its disks by platter too, so its MOUNT and these slots line up, and the 88-HDSK CP/M's B: is the fixed platter — slot 1.

How many it can actually reach is the disk's decision, not ours. A drive letter is a CP/M software concept, owned by whichever operating system is running — our BDOS hands out A: to P: because we wrote it, and stock Altair CP/M hands out A: to D: because MITS wrote it. Ask for E: on a booted Altair and its own BDOS answers Bdos Err On E: Select. The controller offers sixteen units; what appears is up to the guest, and lifting that would mean modifying somebody else's BIOS, which is the one thing booting exists to avoid.

Three more rules, each with a reason:

  • The boot disk is always unit 0, so it is the guest's A:. The bootstrap can load a system from any unit — that was measured — but what it loads comes up as A: and reads unit 0 from then on, so a boot disk parked elsewhere runs against whatever is in unit 0. Anything mounted on A: sits behind the boot disk, and the boot banner says so.
  • Writes need two yeses. A mounted disk is writable only if the boot session was opened for writing and the mount itself is writable. The stricter wins.
  • A gap between drives will look like a crash. An empty unit on a real 88-DCDD answers nothing at all, so a guest that selects one waits for a head that never loads. Mount A:, B:, C: and F: and then type STAT D: and the session appears to lock up. That is the hardware's behaviour and it is left alone — the boot banner warns about the gaps, and ESC twice still gets you out.

An image no controller here can carry — a Tarbell or Cromemco disk — mounts perfectly well for the emulator but has no board to sit on, and is named and skipped rather than presented as a drive the guest cannot read. Hard disks are not in that category: they have their own controller and boot on it. Floppies and hard disks can be mixed in one booted machine — each goes on its own board, and a board asked for a slot it was never given refuses rather than reading the other one's disk. What a drive letter cannot be is a slot on both boards at once: B: is slot 1 of whichever board took the disk, so a hard disk at B: — the 88-HDSK's second platter — leaves the floppy board's drive 1 empty. Whether the guest can reach both at all is its own BIOS's business, as ever.

Disks are opened writable and a booted image is held by one session at a time. A booted guest writes raw sectors, and nothing above it understands the format well enough to notice a mistake — but a vintage operating system saves files, formats disks and updates its own directory, and discarding those writes loses the work silently, which is the worse failure. So cpm_boot_writable is on by default, and turning it off is how you keep a disk exactly as it is. It is one decision, and it covers every disk in the machine: the guest can write the disk it booted and every disk mounted beside it, which is what a machine with the write-protect tabs off is. It is also a standing decision now rather than the per-visit question the old boot picker asked, so it applies to everyone who reaches CP/M until you turn it off again. The one thing that still refuses a write is the host refusing the file — an image the operating system will not let us write stays read-only whatever the setting says. There is no instruction ceiling on a booted disk: an operating system sitting at its own prompt is supposed to run indefinitely. An abandoned session is closed by the ordinary idle timeout instead.

The modem inside a booted disk

A booted guest keeps the virtual modem, provided cpm_emu_uart names a pair of ports. A real Altair put its modem on the second port of the 88-2SIO, so altair_2sio2 (0x12/0x13) is the natural choice; comms software under a booted Altair CP/M then finds a UART where it expects one, dials out with ATDT, and can answer an inbound CPM@<ip> call. Three cases cannot work, and the boot banner says which:

ProfileWhy not
auxThe AUX: device belongs to our BDOS; a booted disk brings its own.
hbios_1, hbios_2HBIOS is RomWBW firmware, which an Altair disk does not have.
altair_2sio10x10/0x11 is the console on this machine — on a real Altair the console is 2SIO port A.

The front panel

MITS system software asks the front-panel sense switches on port 0xFF which console board the machine has — 88-SIO, 88-ACR, a 4PIO, or the 88-2SIO. The gateway reports 0x00, which selects the 88-2SIO at 0x10/0x11: the console it actually emulates. This is not a detail. With the port left floating at 0xFF — a valid reading meaning "88-SIO" — Altair DOS booted perfectly and wrote its sign-on to a board that was not there, which looked exactly like a disk that would not boot.


Screens — the VDM-1 and the Dazzler

Some disks do not print to a console at all. Two S-100 video cards of the 1970s put their picture in the computer's own memory and gave the program no data port to write to — the Processor Technology VDM-1, a 64×16 character screen at CC00, and the Cromemco Dazzler, a colour picture the program can put anywhere. Software written for either boots here, takes your keystrokes and leaves the session you started it from completely blank. Both are served in the web UI instead, at /vdm, and one session can have both.

Which disks it applies to

The button is offered for every booted session, not only these — sampling costs the guest nothing, so withholding a screen on a guess would only hide one that works. A disk that drives neither card simply shows a blank grid. These are the ones known to paint something:

CardDiskWhat it is
VDM-1
64×16 characters
TDISK04CP/M 1.4 for the VDM-1 — boots, and prints nothing to a port, so the session it was started from stays blank by design
TDISK05CP/M 2.2 for the VDM-1
DISK11VDM-1 programs, and boots CP/M. The boot banner does not warn for this one — its VDM driver lives in a CUTER ROM rather than on the disk, so there is nothing in the system tracks to find
TDISK06VDM-1 programs — a library, with no boot program of its own. Mount it beside TDISK05; it is not on the boot list and is not in the download offer
cpm14-vdm, cpm22-vdm, vdm-stuffz80pack’s altairsim library — two systems and a programs disk
Dazzler
colour, 1976
DISK10Dazzler programs, and boots CP/M — the library disk, around two dozen programs, and the usual place to start
DISK15the Felix animation system for the Dazzler
dazzler, dazzlerII, dazzler_graphics, dazzler_stuffz80pack’s imsaisim and cromemcosim libraries

Everything above except TDISK06 is in the sample-disk download, so the shortest route to a picture is: fetch the disks, set DISK10 to boot, and press VDM / Dazzler.

Using it

  1. Turn the web server on (web_enabled, default port 8080) — it is off by default.
  2. Boot a disk as usual. If its system tracks drive a VDM-1, the boot banner says so and prints the address to open. There is no such warning for a Dazzler and there cannot be: its two registers are ordinary low port numbers that appear by chance all over a disk's data, so a scan before boot would cry wolf on two images in three. A Dazzler announces itself when the guest switches one on, and the session list marks it then.
  3. Open http://<gateway>:8080/vdm, or press VDM / Dazzler — in the configuration page's AI Chat, Browser, Weather & CP/M frame, or in the desktop UI's CP/M More popup, which opens the same page in your browser — and pick the session from the list.
  4. Keep typing in your telnet or SSH session. The keyboard is a perfectly ordinary port — 04h/05h on the VDM-1 disks, and a normal serial console on the Dazzler ones — because only the display was ever memory-mapped.

Every booted session offers a screen, not only the ones known to be VDM-1 disks, because sampling one costs the guest nothing: it is a read of the guest's own memory, through its own MMU, with no write, no trap and no change in timing. The list marks the sessions that have actually driven the card, and a session showing whatever happens to live at CC00 says so under the screen rather than leaving you to wonder about the noise.

What you seeWhat it means
(VDM-1) beside a sessionThat guest has written the scroll register on port C8h, so it is running a VDM-1 driver.
“waiting for the first frame”The session is registered but has not yet reached the point in its loop where it hands a screen over. The next one publishes.
“That session has ended”The disk was stopped with ESC ESC, or the caller hung up. A blank screen and a finished session look identical, so they are said differently.
The frame number risingThe guest is alive. An unchanging picture with a rising frame number is a guest sitting at its prompt; a frozen number is a session that has stopped.

How much it costs the guest

Nothing, unless somebody is watching. The page asks for a frame roughly every 150 ms and each request produces exactly one snapshot; a session with no viewer checks a single flag as it goes round its loop and copies nothing. There is no timer anywhere, and closing the browser tab ends the cost rather than reducing it.

Who can watch. This page is part of the configuration server, so it authenticates the administrator — while the person typing at the guest is on telnet or SSH. Anyone who can open the configuration page can watch any booted session's screen. On a gateway whose web UI already shows the password and the API key that is not a new privilege, but it is worth knowing before you enable the web server on a network you share.

The cursor needs no special case. On this card the cursor is a cell with bit 7 set — inverse video — rather than an output character, so a literal repaint of the 64×16 window carries it along with everything else. That is the whole argument for repainting rather than reconstructing a character stream from the guest's writes: a picture sampled out of memory cannot be wrong about what the guest meant. The one piece of state that is not in memory is the scroll register, which says which of the sixteen lines is displayed first; without it a guest that has scrolled is shown rotated, with every character present and in the wrong order.

Which disks the VDM-1 reaches: TDISK04 (CP/M 1.4, VDM VERSION) and altairsim's cpm14-vdm. DISK11's VDM driver lives in a CUTER monitor ROM rather than on the disk, and that ROM is not one the gateway has, so it stays dark. See the disk reference for the write-up.

Typing from the browser

The screen page is a keyboard as well as a window. Click the screen and type: the bytes go to the same place the terminal's do, through the same translation, so the backspace key you chose with cpm_boot_backspace behaves identically from either keyboard. Both keyboards work at once — there is one key queue, exactly as two keyboards wired to one port would share one, so the person at the terminal and the person in the browser can both type. If they type at the same moment their characters interleave, which is what a shared terminal is rather than a fault.

Control keys reach the guest: Ctrl+C to interrupt, Ctrl+S to pause — which is what GDEMO asks for by name. What does not reach it is the ESC ESC exit gesture: ending a session somebody else is sitting at is not a keystroke, so a double escape from the browser arrives at the guest as two escapes and nothing more.

Set cpm_screen_input = false to make the page read-only. The screen stays fully readable either way — watching and typing are different acts and get different answers. The setting is live: it is read per keystroke, so turning it off does not need a restart, and a page left open stops offering a keyboard as soon as it is refused once.

This is also what makes the Dazzler disks usable rather than only watchable: LIFE asks ENTER DATA and GDEMO wants Ctrl+S, and neither needs a joystick.

The Cromemco Dazzler

The first colour graphics card for microcomputers, 1976. It reads its picture out of main memory by DMA — so, like the VDM-1, watching it costs the guest nothing — and it has exactly two registers. Port 0Eh carries an on/off bit and the top seven address bits, so the picture sits on a 512-byte boundary wherever the program put it; port 0Fh is the format. Both are read at every frame rather than once, because software animates the format as well as the picture.

FormatPictureColour
Normal, 512 bytes32×32Four bits per element, in memory: red, green, blue and intensity — or one of sixteen greys in black-and-white mode. Two elements per byte.
Normal, 2 KB64×64
Resolution ×4, 512 bytes64×64One bit per element, and the whole picture takes its colour from the format register. Eight elements per byte. Full colour here needs interleaved frames.
Resolution ×4, 2 KB128×128

DISK10.DSK in the Altair collection is the Cromemco software library — two dozen programs including KSCOPE (Kaleidoscope), DMATION (Dazzle-mation), LIFE, XLIFE, GDEMO, DAZZPLOT and COLOR. Twelve more images in the collections carry Dazzler programs.

The gateway can fetch them for you. Every disk screen — telnet, web and desktop — offers to download the sample disks before you mount anything: about 42 MB into CPM/images, pinned to a commit and checked against a recorded SHA-256 so what arrives is what was tested. It offers only the thirty-four disks this gateway is known to run — four data companions that carry no boot program are left out, because downloading a disk that does nothing is not a favour. That is not a remembered list: each candidate is cold-started from the bytes its pinned URL really serves before it earns a place, so the offer is measured every time it is regenerated. Nothing already in the folder is overwritten.

Where the disks come from. They are other people's work and are not shipped with the gateway. The Altair-Duino set — TDISK04 for the VDM-1, DISK10 for the Dazzler — is the disks/ folder of David Hansel's Altair 8800 simulator, github.com/dhansel/Altair8800. Four hard disks more — HDSK04 (Infocom adventures), HDSK05 (BASIC), HDSK06 (COBOL) and HDSK07 (dBase II) — come from Jim McNeely's github.com/jpmcneely/AltairDuino-Disks, which the first collection does not have. Two traps if you browse that one by hand. It carries four files named DISK13DISK16 that are different disks from Hansel's of the same names and undocumented in its own catalogue, so the gateway takes those four from Hansel. And its DISK17 is a name Hansel has no disk for, but its bytes are Hansel's DISK12 exactly — the IMP modem executive under a second number — so it is not offered either. A unique filename is not unique content. cpm14-vdm.dsk and the rest are the <sim>/disks/library folders of Udo Munk's z80pack, github.com/udo-munk/z80pack. Drop the .dsk files into CPM/images. repodisks.txt, written into that folder beside the readme, catalogues every disk in those collections and every file on it. Each one appears once, by name, A to Z, under a one-line summary derived from its own directory, behind an index of the lot, with every collection's address at the head of the file. Every summary opens with boots, mount only or neither, read from the disk's boot sector — so it says which of the two things a disk is for before it says what is on it.

The joystick games are playable from the browser. Cromemco's SPACEWAR, GOTCHA, DOGFIGHT, TANKWAR, CHASE, AMBUSH and TRACK read the D+7A analog board on ports 18h1Ch — a pair of joysticks with four switches and two analog axes each. The board is emulated (cpm_joystick, on by default) and played from the keyboard of the browser watching the screen: player 1 W A S Z with X to fire, player 2 I J K M with N. The page's Joystick switch starts on whenever the board is enabled, so those ten letters drive the sticks rather than typing at the guest. A held key swings — centred when pressed, full deflection half a second later — because these are analog controls and a key has no halfway. Every value was measured against Cromemco's own ADCTEST.COM, which displays all four channels and the switch byte: a centred stick reads 00, right and up read 7F, left and down 81, and the switch byte is active low with player 1's button on bit 0 and player 2's on bit 4.


Printing — cpm_printer

CP/M software prints to a device called LST:, and what it sends is captured and left in your transfer directory as a document you can collect over XMODEM, ZMODEM, Kermit or any of the others — plain text by default, or an OpenDocument file that opens in LibreOffice, Word or Google Docs. Nothing is written until a guest actually prints; cpm_printer = off sends the output to your terminal instead, which is where it used to go and where it cannot be recovered.

Two machines, two routes, one document

This is one of only two settings that reach both CP/M machines — cpm_cpu is the other — and, like that one, they get there by completely different roads.

  • The emulator. Our own BDOS and BIOS sit underneath the guest, so the printer is a service: BDOS function 5 (List Output) and the BIOS LIST vector. Every program that prints through the operating system arrives here — WordStar, MBASIC's LPRINT, PIP LST:=FILE.TXT. No hardware is involved and there is nothing to configure beyond turning it on.
  • A booted disk. The guest owns the machine and drives a printer board, so the gateway has to be one. Which board is cpm_printer_port; today there is one, the Altair line printer with its data register at 03h.

Where the file goes, and when

Into a printer folder inside your transfer directory — its own folder, so a printer left switched on does not scatter documents through your own files — named PRINT-YYYYMMDD-HHMMSS from this machine's clock. Deliberately not onto a CP/M drive: the printout is for you, not for the guest, and putting it on a drive would hand it back to the guest as a file it could open.

A job ends after five seconds of silence. CP/M has no end-of-print signal — a printer is a stream of bytes with no "close", and on real hardware the person standing there decided the job was finished — so silence is the only signal there is. In the emulator there is a second, exact one: returning to the A> prompt ends the job immediately, so a program that prints and exits does not make you wait out the timeout. A booted operating system never "finishes", so there the five seconds are the whole answer.

Merely initialising a printer does not create a document. Altair Hard Disk BASIC answering LINEPRINTER? C writes a handshake byte to the data port before anything is printed; a job with no printable character in it is dropped rather than written out as an empty file.

The auto-line-feed switch

Real Centronics-type interfaces carried a DIP switch for this, and they carried it because the byte stream genuinely cannot say. A carriage return that returns the head without advancing the paper is how overstrike works — it is how period software makes bold and underline. A carriage return that advances is how a great deal of other software ends a line. Same byte, two meanings, and only the hardware settles it.

So it is settled per board, by measurement. Two LPRINTs from Altair Hard Disk BASIC put ALPHA<CR>BETA<CR> on the wire and nothing else — a bare CR, no line feed anywhere. With the switch off, BETA prints on top of ALPHA and an entire report collapses onto a single line, so altair_c has it on. A CR LF pair sent to that board has its line feed absorbed rather than double-spacing the document, which is what the real interfaces did and what made the switch usable by software that sent both.

The emulator's printer has it off, and that was measured to the same standard: PIP LST:=DEMO.ASM run against the CP/M 2.2 distribution disk sent 2745 bytes carrying 65 CR and 65 LF, and the document that came out matched the source file line for line. So CR LF is what arrives through BDOS 5, and overstrike keeps its meaning there.

What it does not do yet

Bold and underline are real. Period software does not ask for them with an escape code, it overstrikes: WordStar prints the line, returns the carriage with a bare CR, and reprints just the emphasised run at the same columns; underline arrives as a pass of _ over the letters. That is recorded as it lands and comes out as real bold and underline in the .odt — verified by printing from WordStar 3.0 on a booted disk and opening the result in LibreOffice. It is also why cpm_printer_autolf exists: with the switch on, each overstrike pass lands on a line of its own instead of on top of the text. Plain text gains no markup at all.


UART Profiles — cpm_emu_uart

Three ways a guest can reach the virtual modem, all selected by this one key. Software that pokes a UART wants a port; software that asks the OS wants AUX:; software that asks the firmware wants HBIOS.

ValueAccessAddresses / unitMatches
offNo virtual modem; guest code cannot open a socket.
rc2014_1aPort (Z80 SIO/2)status 0x80 / data 0x81RC2014 SIO/2 board 1, channel A
rc2014_1b (default)Port (Z80 SIO/2)status 0x82 / data 0x83RC2014 SIO/2 board 1, channel B — the usual AUX: port; qterm82
rc2014_2aPort (Z80 SIO/2)status 0x84 / data 0x85RC2014 SIO/2 board 2, channel A — qterm84
rc2014_2bPort (Z80 SIO/2)status 0x86 / data 0x87RC2014 SIO/2 board 2, channel B
altair_2sio1Port (6850 ACIA)status 0x10 / data 0x11Altair 88-2SIO port 1 — IMP8
altair_2sio2Port (6850 ACIA)status 0x12 / data 0x13Altair 88-2SIO port 2
altair_sioPort (88-SIO)status 0x00 / data 0x01Altair 88-SIO, the original MITS board
auxBDOS AUX:functions 3 / 4Hardware-independent; SC126 / RomWBW
hbios_1RomWBW HBIOSunit 1, via RST 8qtermh1 and RomWBW software generally
hbios_2RomWBW HBIOSunit 2, via RST 8qtermh2

The HBIOS entries exist because a good deal of RomWBW software — the QTERM h builds among them — never touches a port at all, so no port address could ever have served it. HBIOS answers only when an hbios_* profile is selected: on any other setting the guest sees the page zero of a plain CP/M 2.2 machine, with no RST 8 vector, and well-behaved software detects its absence and says so rather than hanging.

The aux caveat. CP/M 2.2 has no way to ask whether an AUX: byte is waiting, so this family must read ahead and cannot receive a literal ^Z. Prefer a chip family or HBIOS when one fits the machine.

Single source of truth: src/cpm/uart.rs — config validation and all three UIs enumerate that same list, so this table cannot drift from one UI to another.


EGT8080 — the bundled CP/M terminal

EGT8080 (“Ethernet Gateway Terminal”) is this project's own CP/M terminal, written in period assembly. It asks which port it is on instead of shipping one build per machine — and it ships in two builds, one per processor: EGT8080.COM, which runs anywhere, and EGT80.COM, which adds the Z180 ASCI ports an 8080 binary cannot hold.

Every period terminal we tried was built for one machine's serial port — QTERM ships a separate binary per port, IMP8 is an Altair 2SIO build, and KERCPM22's generic overlay has no serial driver at all. Get the pairing wrong and the program is simply silent. EGT8080 asks instead: pick the port from a menu, and it remembers.

It ships inside the gateway binary and is written out whenever it is missing — at every start-up, and again when a session enters the emulator, so a folder deleted while the gateway is running is repaired without a restart. It is then never overwritten — it saves its settings inside its own .COM, so refreshing it would discard your configuration. Deleting the file restores the shipped copy on the next launch, which is the documented way back to a known state.

This used to happen only once somebody entered the emulator. Erasing the transfer directory and restarting therefore recreated the sixteen drive folders with no terminal in any of them — and the loose copies in the transfer directory, whose whole purpose is to reach a terminal without starting the emulator, appeared only after you had started the emulator. Fixed in 0.9.3. Set place_bundled_terminals = false if you keep your own build and would rather a file you deleted stayed deleted.

Two builds, and which one to run

EGT8080.COM is the one to reach for. It runs under cpm_cpu = z80 and = 8080 alike, and on any machine either imitates, because it is written to the 8080's instruction set and the Z80's is a strict superset of it. Start here unless your console is a Z180 ASCI port.

EGT80.COM is the Z80 build, and it exists for one family of ports. A Z180 board — an SC126, and many a RomWBW machine — drives its console from the ASCI channels inside the processor. Reaching them needs IN0/OUT0, and knowing a Z180 is there at all needs MLT BC; all three are ED-prefixed instructions, and on a true 8080 an ED byte is an undocumented CALL — such a probe would not fail, it would jump into the weeds. So those bytes cannot be in a binary that must also run on an 8080, and no amount of care in EGT8080 could serve a Z180 console. That is the whole reason there are two.

Both are placed on drive A: and in the transfer directory — four files, and the reason the second pair exists is so the file-transfer menus can send one to real hardware without starting the emulator. The port menu is the difference: EGT8080 offers the Z80 SIO/2, the 6850 ACIA, the Altair 88-SIO, RomWBW HBIOS and CP/M AUX:; EGT80 offers those and the Z180 ASCI. The 6850 ACIA is in both — if your board has one at a port address, EGT8080 reaches it.

The cost, and why EGT8080 leads. EGT80 is Z80 code: run it with cpm_cpu = 8080 and the machine stops at its first Z80-only opcode. The emulator's sign-on says 8080 selected.  Run EGT8080. under that setting for exactly this reason. On the 8080 build the ASCI entry is still on the port menu and answers “This processor is not a Z180, so it has no ASCI ports” — listing it and refusing beats hiding it, because the operator of a Z180 board learns the family exists and that this is the wrong build for it.

They were one build for part of 0.9.2's cycle. The Z80 build was retired on the reasoning that one binary running everywhere beats two that need choosing between — true of every machine except the Z180, which is the one an SC126 owner has. It came back before 0.9.2 shipped. EGT80.Z80 is the source; EGT8080.Z80 is derived from it by tools/port8080.py, so a change is made once. If you upgraded from an earlier release, your own EGT80.COM is still on drive A: with its settings — placement never overwrites — which is the right outcome: it is the same program.

What the porter does. Everything mechanical: JRJP, two BIT 7,A, one LDIR, the Z180 probe answering “no” without asking, and the port drivers, which on an 8080 must patch the port address into their own IN and OUT because the 8080 has no register-indirect I/O at all.

What holds the line is tools/check8080.py. The 8080 source is still written in Zilog mnemonics and assembled by the same Z80 assembler — what has to be in the 8080's set is the opcodes emitted, not the words — so the assembler would happily accept a JR and say nothing. The checker reads every instruction and matches it against the 8080's set by form, not by mnemonic, because half the Z80-only instructions share a mnemonic with an 8080 one. That is not theoretical: a census by mnemonic reported “312 JR, two BIT, one LDIR, nothing else” and had missed LD (nn),DE and sixteen IN A,(C)/OUT (C),A. The first build ran perfectly on a Z80 and, on an 8080, executed ED as an undocumented CALL straight into a string constant.

It starts in ASCII mode. An ANSI terminal displays plain ASCII perfectly, so defaulting to ASCII costs that user only colour — Settings → A turns ANSI (and with it colour) on. A plain or PETSCII terminal shows ANSI as litter printed over every screen, which costs that user a program too garbled to find the setting that would fix it. Note this governs escape sequences and colour; the clear-screen dialect is a separate setting (Settings → C), and it defaults to the ADM-3A ^Z to match — the gateway translates that for whichever client is connected, so an ASCII terminal receives no escape byte at all. Pick the ANSI ESC [ 2 J there if you are on real hardware with a modern terminal.

Main menu

KeyDoes
TTerminal mode — the wire, with the menu key to get back.
SSettings: serial port, line settings, display.
UUpload a file to the far end (XMODEM).
DDownload a file from the far end (XMODEM).
HHelp.
VSave settings into EGT8080.COM itself.
XExit to CP/M.

Terminal-mode menu — the menu key (^Y by default)

Pressing the menu key inside terminal mode does not hang up, so a transfer can be started mid-call: dial, ask the far end for a file, then fetch it.

KeyDoes
EExit terminal mode, back to the main menu.
HHelp.
UUpload a file to the far end (XMODEM), without leaving the session.
DDownload a file from the far end (XMODEM), without leaving the session.
the menu key againSends the menu key itself to the far end.
anything elseReturns to the session.

Settings is deliberately not on this menu. It used to be, and it did not work: the settings screen drew, but keystrokes were still going to the remote, so nothing could be selected. Press E first — Settings works properly from the main menu, and leaving terminal mode does not drop the line.

Selecting a port — keys from SP

The top level names machines, not chips, because which chip a board uses is a question only someone who already knows the board can answer. “Other hardware” is the old chip list, one level down, with the free-form address prompts intact.

To match this profilePressEGT8080 then reports
rc2014_1b (default)1Z80 SIO/2 at 82/83
rc2014_1a5 1 1Z80 SIO/2 at 80/81
rc2014_2a5 1 3Z80 SIO/2 at 84/85
rc2014_2b5 1 4Z80 SIO/2 at 86/87
altair_2sio13 16850 ACIA at 10/11
altair_2sio23 26850 ACIA at 12/13
altair_sio4 1Altair 88-SIO at 00/01
aux6 then any keyCP/M AUX: device (BDOS 3/4)
hbios_12 1 RETURNRomWBW HBIOS unit 1
hbios_22 2 RETURNRomWBW HBIOS unit 2

6 (AUX:) pauses on a “Press any key” note about the missing status call before the pick completes. D sets the gateway default and clears the rest; N selects no port; Q leaves the port unchanged.

All ten profiles verified against EGT8080 in the emulator: each pick was confirmed by the Port: line EGT8080 redraws, then ATOK through that port, then a real burst dialled with ATDT ethernetgateway. Throughput came out even across all ten (≈1.4–1.5 KB/s), which is the point — no family is a slow path.

EGT8080.COM is committed and CI cannot rebuild it. It is built by running the real period assembler (SLR Z80ASM) under zxcc via EGT8080/Makefile, with M80+L80 and ZMAC as portability gates. Do not edit EGT8080.Z80 without re-running make — the compiled-in copy is what ships.


Which Driver for Which Machine

EGT8080's five port families, what selects each, and how far each one has been proven.

MachineEGT8080 driverEmulator profileVerified
This gateway's emulator Z80 SIO/2 — 82/83 rc2014_1b yes — the default pairing, works untouched
RC2014 and most Z80 boards Z80 SIO/2 — four channels (0x80/0x82/0x84/0x86) or any address rc2014_1arc2014_2b yes
Any RomWBW machine (incl. SC126) RomWBW HBIOS — unit 0–3 via RST 8 hbios_1, hbios_2 yes
Altair 88-2SIO, RC2014 ACIA 6850 ACIA — 0x10 / 0x12, or any address altair_2sio1, altair_2sio2 yes
Altair 88-SIO (original MITS board) Altair 88-SIO — 0x00/0x01, or any address altair_sio yes
Z180 boards without RomWBW Z180 ASCI — channel 0/1, internal I/O base not reachable here real iron only — our Z80 core has no IN0/OUT0
Portable fallback, any CP/M CP/M BDOS AUX: — functions 3/4, no parameters aux yes — but cannot receive a literal ^Z

Why the 88-SIO is its own item rather than an address on the 6850 screen: it is a different board. It reports ready by pulling a bit low, so an 88-2SIO driver aimed at it reads every test inverted and the port looks permanently busy and permanently empty at once. The addresses were never the only difference.

Why the firmware is asked rather than guessed on a Z180: RomWBW relocates the Z180's internal I/O block, so a fixed guess addresses nothing on some boards. EGT8080 asks the firmware where the block is and offers C0 by name for Small Computer Central boards (SC126 and friends), where RomWBW puts it.

One EGT8080 path is reasoned, not run, because nothing here can reach it: the CP/M 3 console path. It is documented as such in EGT8080/README.md.

The Z180 ASCI ports are not offered at all. Detecting a Z180 needs MLT BC, laid down as DB 0EDH,4CH because no Z80 assembler encodes it — and on a true 8080 an ED byte is an undocumented CALL, so the probe would not fail, it would jump into the weeds. The bytes cannot be in an 8080 binary at all. Restoring the ASCI ports needs an 8080/Z80 flag test, using no ED byte, ahead of any Z180 probe.


RomWBW HBIOS Calls

A clean-room implementation of the RomWBW HBIOS serial group, written from the published API. Reached by RST 8 with the function in B and the unit in C.

Only present when an hbios_* profile is selected. On any other cpm_emu_uart setting there is no RST 8 vector at all, and every function is refused — so software that checks for RomWBW correctly concludes this is not a RomWBW machine.

Fn (B)NameBehaviour here
0x00CIOIN
serial input
Returns the next byte in E. Blocking: with nothing waiting the call is left unanswered and the guest's PC stays parked on the trap, so it is re-reported next batch — one blocking call to the guest, a responsive host.
0x01CIOOUT
serial output
Sends the byte in E. Parks the same way when the TX ring is full, rather than dropping the byte — which is the whole point of a blocking send.
0x02CIOIST
input status
Count of waiting characters, in both A and E, flags following, so Z means “nothing waiting”. Capped at 0x7F.
0x03CIOOST
output status
Free output space, same convention, also capped at 0x7F.
0x04CIOINIT
initialise
Accepts the line characteristics in DE and remembers them so QUERY reports them back. There is no UART to program — a TCP connection has no baud rate — but refusing would stop software before it starts; this is the first call QTERM's overlay makes. DE = 0xFFFF means “reset, keep the current line”.
0x05CIOQUERYReports the stored line characteristics in DE, with no terminal type.
0x06CIODEVICEDescribes the unit: RS-232 attribute, this unit number, no device type / mode / base.
0xF1SYSVERFirmware version, returned in DE with the platform id in L.
0xF8SYSGETSub-function in C. CIOCNT (0x00) reports the serial-unit count and RTCCNT (0x20) reports one clock; the disk, video, sound and DSKY counts are refused rather than answered with zero, which a caller would read as a successful “none”.
0x20RTCGETTIM
read the clock
Fills the six-byte buffer at HL with year, month, day, hour, minute, second — each BCD encoded, as the published interface specifies. The time is the host's, in local time where the platform can report it (UTC on Windows). This is the only clock an emulated program can have: CP/M 2.2 has none of its own.
0x21RTCSETTIMRefused. The clock is the host's and a guest cannot set it — and answering “done” before reading back a time it never set would be worse than saying no.
Every other serial function, the RTC NVRAM calls, and every other HBIOS group — disk, video, sound, DSKY, bank management — returns a failure result (0xFF). The clock is only there under an hbios_* profile: on a port profile the machine is a plain CP/M 2.2 one, and that has no clock either.

Two deliberate details that bit us. First, the status calls return the count in A as well as E: the API labels A a result code, but RomWBW's own drivers put the count there and callers rely on it — QTERM's overlay hands A straight to a JR Z. Second, that count is capped at 0x7F rather than 0xFF, because the API reserves bit 7 for an error result; a larger count reads as a failure to any guest that checks, and one did hang on exactly that ambiguity.

The disk / RTC / video counts under SYSGET are refused rather than answered with zero, since a caller would read a successful zero as “none present” and a refusal as “not that kind of machine” — which is the truth here.


BDOS and BIOS Coverage

What the emulated CP/M 2.2 answers. Guest calls reach the live telnet/SSH session for console work, and a directory-backed filesystem for disk work.

BDOS — console and system

Fn (C)NameNotes
0System ResetWarm boot — returns to the A> prompt.
1Console InputBlocking read, echoed.
2Console OutputCharacter in E.
3Reader InputAUX: in — the virtual modem.
4Punch OutputAUX: out — the virtual modem.
5List OutputPrinter, routed to the console.
6Direct Console I/ONon-blocking read / write.
7 / 8Get / Set IOBYTEBacked by the page-zero IOBYTE.
9Print String$-terminated string at DE.
10Read Console BufferLine input with editing.
11Console StatusNon-blocking “is a key waiting”.
12Return Version0x22 — CP/M 2.2.
32Get / Set UserE = 0xFF gets, otherwise sets.

BDOS — disk and file

Fn (C)NameNotes
13Reset Disk SystemBack to drive A:, DMA 0x0080, and every software write-protect released. Returns 0FFH in A when the drive it logs in (A:) holds a temporary file whose name starts with $ — any such name, as the real BDOS tests only the first byte — otherwise 0. That flag is how a command processor discovers a SUBMIT batch is already running.
14Select DiskAlso maintains the page-zero CDISK byte, which some transients read directly to find their login drive.
15 / 16Open / Close FileWrites are write-through, so close has no directory to rewrite — but it still answers like the real thing: 0 when the file is there, 0FFH when the name is not in the directory. A software write-protected drive, and an FCB whose byte 14 has its high bit set, both return success without looking, as CP/M’s own CLOSEF short-circuits.
17 / 18Search First / NextWildcard directory search.
19Delete File
20 / 21Read / Write Sequential
22Make File
23Rename File
24Return Login VectorAll sixteen bits set — every drive A:–P: exists, since its folder is auto-created. Returning 0 here used to confuse drive-enumeration utilities.
25Return Current Disk
26Set DMA AddressReset to 0x0080 before every program is run, exactly where CP/M's own CCP does it (CCP22.ASM calls SETDMA on the line before it enters the TPA). Without that reset a program inherited the DMA the last one left: PIP moves it to its own buffer, so a following DUMP read the stale command tail instead of the file, silently.
27Get Allocation VectorA real bitmap — reserved directory blocks plus the blocks this drive's files occupy — so STAT's free count reflects actual usage.
28Write Protect DiskSoftware write-protects the current drive until the next disk reset. Enforced in all four mutating paths — write, make, erase and rename all refuse.
29Get R/O VectorThe real bitmap of write-protected drives, so a program can see the protection it just asked 28 for.
30Set File AttributesThe R/O attribute (t1') maps onto the host file's read-only permission, so it means the same thing to a host user as to the guest. System (t2') and Archive (t3') are accepted and ignored — see the note below.
31Get Disk ParametersReturns a synthesised DPB.
33Read Random
34 / 40Write Random / with Zero Fill40 is serviced by the same code as 34: drives are byte-exact host files, and a write past the end is zero-filled by the host either way, so the two are genuinely identical here. Errors use CP/M's documented codes — 0x06 when the record is past the per-file size cap, 0x05 for any other write failure.
35Compute File SizeSets R0R2 to the record count.
36Set Random RecordFrom the current sequential position.
37Reset DriveReleases the write-protect on the drives named in the DE bitmap. The "log the drive out" half is a genuine no-op rather than a stub: drives are host folders that are always present, and the directory is synthesised live on every search, so there is no cached state to invalidate.
Accepted and ignored: the System (t2') and Archive (t3') file attributes. A host directory has nowhere to keep them, and inventing a sidecar file would litter the very folders you drop your own files into — so they are honestly dropped rather than faked.

BIOS jump table

A real jump table is installed, and both the ways a guest reaches it work: calling a table slot, and calling the extracted operand address directly — which is what MBASIC does.

VectorNameBehaviour
1WBOOTWarm boot to the A> prompt.
2CONST0xFF if a key is buffered, else 0.
3CONINBlocking keyboard read, no echo.
4 / 5CONOUT / LISTCharacter in C to the console.
6PUNCHCharacter in C to the virtual modem.
7READERNext modem byte, or ^Z if none.
15LISTSTList device always ready.
BOOT, HOME, SELDSK, SETTRK, SETSEC, SETDMA, READ, WRITE and SECTRAN stub to 0 — there is no raw-sector disk emulation, because the drives are host directories rather than disk images.

Every file call is jailed. Drives A:–P: are folders under transfer_dir/CPM, and no FCB can name a path outside it. There is no BDOS function that reaches a host command.

Making a file read-only on the host protects it from the guest. A chmod -w file (or one the guest itself marked R/O via BDOS 30) shows as R/O in STAT, and ERA and REN refuse it with CP/M's File R/O rather than deleting it. This needs enforcing in the emulator, not just on the host: a Unix unlink is governed by the directory's write bit, not the file's, so the host permission alone would not have stopped an erase.


Virtual Modem — AT Commands

The guest talks to a Hayes-compatible modem through whichever cpm_emu_uart access mode is selected. Both directions work: it dials out, and it can be dialled.

CommandDoes
ATAttention — answers OK.
ATDtargetDial. See the target table below.
ATAAnswer a ringing inbound call.
ATHHang up.
ATOReturn online after a +++ escape.
ATEnCommand echo off / on.
ATQnSuppress result codes.
ATVnWord (1) or numeric (0) result codes.
ATXnResult-code level 0–4. Low levels collapse BUSY / NO ANSWER to NO CARRIER, as a modem without call-progress detection does.
ATInIdentify.
ATZReset to the saved profile — this modem's NVRAM, not the file, as a real modem does.
ATSr=n, ATSr?Set / query S-register.
AT&CnDCD handling: 0 forces it on, 1 tracks carrier.
AT&DnDTR handling — accepted, not modelled.
AT&FFactory reset.
AT&WWrite the current settings to the cpm_emu_* keys.
+++Escape to command mode. Guarded by an idle gap (S12) so a +++ inside binary data is not mistaken for it.

Commands chain left to right, so ATE0Q0V1X4S0=1 works as one line.

Dial targets

TargetReaches
ATD A / ATD BThis gateway's own serial Port A / Port B.
ATD A@hostA port on another gateway, via the master/slave crossbar. Requires allow_peer_dial.
ATDT host[:port]A TCP host; port 23 by default.
ATDT ethernetgatewayThis gateway's own menu, as though you had telnetted in. Handy for testing without leaving CP/M.

Inbound: the emulator is dialable as CPM@ip. The guest sees RING and answers with ATA, or automatically once S0 rings have passed. Only one session claims each call — the take is atomic, so there is no double-answer.

These are the same dial targets the physical serial modem answers; for the full Hayes command set and S-register detail see the AT Command Reference.


References

Deep-dive reference pages for every protocol and interface, plus the character-set tables and ANSI escape-sequence reference.

CP/M Emulator (this page)

UART profiles, EGT8080, HBIOS calls, BDOS/BIOS coverage.

XMODEM

128-byte blocks; CRC-16 / checksum negotiation.

YMODEM

Block-0 metadata, batch, exact size truncation.

ZMODEM

Streaming; ZDLE, CRC-32, autostart, resume.

Kermit

Send-Init negotiation; F / A / D / Z / B transfer.

Punter

C1 dual checksum, two-phase, GOO/BAD/ACK.

Disk Formats

Measured Altair / IBM 3740 layouts, skew, checksums, EXM.

AT Commands

Hayes command set, S-registers, +++ escape.

Telnet

IAC negotiation, every option, the NVT data phase.

SSH

Server, gateway, host keys, TOFU, auth modes.

Character Code Tables

Hex tables for every encoding: ASCII, ANSI, PETSCII, ATASCII, Baudot/ITA2, ZX Spectrum, TRS-80.

ANSI Escape Sequences

Cursor, colour/SGR, erase, and screen-mode escape codes, with the raw hex bytes.