CP/M Emulator Reference
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:
| Feature | What it is | Where |
|---|---|---|
| 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/A … P 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.
All cpm_emu_* keys live in egateway.conf and are editable from all three config UIs (telnet, web, GUI).
| Key | Default | Meaning |
|---|---|---|
cpm_emu_enabled | true | Whether the CP/M System menu item exists at all. Set false to refuse guest code entirely. |
cpm_screen_input | true | May the web UI's VDM / Dazzler page type at a booted disk? The screen is readable either way. See Screens. |
cpm_emu_max_minstr | 2000 | Instruction 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_machine | auto | Which 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_backspace | backspace | What 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_writable | true | May 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_cpu | z80 | Which 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_printer | text | Where 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_port | altair_c | Which 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_uart | rc2014_1b | How the guest reaches the virtual modem — a machine port, the BDOS AUX: device, or RomWBW HBIOS. See UART Profiles. |
cpm_emu_echo | true | Virtual modem ATE — echo command-mode characters. |
cpm_emu_verbose | true | Virtual modem ATV — word result codes rather than numeric. |
cpm_emu_quiet | false | Virtual modem ATQ — suppress result codes. |
cpm_emu_x_code | 4 | Virtual modem ATX result-code level (0–4). |
cpm_emu_dcd_mode | 1 | Virtual modem AT&C — 0 forces DCD on, 1 tracks real carrier. |
cpm_emu_s_regs | (empty) | Saved S-register list (S0…S27) — 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.
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.
| Command | Does |
|---|---|
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 namealso DEL | Erase file(s); wildcards accepted. |
REN new=oldalso RENAME | Rename a file. |
TYPE file | Show a text file. |
SAVE n file | Save n pages of the TPA to a file. |
USER n | Select user area. |
A: … P: | Change drive. All sixteen CP/M 2.2 drives exist, each a folder under transfer_dir/CPM. |
VERalso VERSION | Emulator version and TPA size. |
HELLO | BDOS print-string demo (function 9). |
ECHO | Interactive 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. |
HELPalso ? | This command list. |
EXITalso 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. |
| name | Run 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.
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.
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.
| Token | Format | Size |
|---|---|---|
ibm3740 | IBM 3740 8" SSSD — the closest thing CP/M had to a universal disk. Tarbell, Cromemco SD, IMSAI / z80pack. | 256,256 |
altair8 | Altair 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 |
altairhd | Altair 88-HDSK hard disk (the Altair-Duino set). 256-byte sectors, two CP/M records in each. | 4,988,928 |
cromemcodd | Cromemco 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 |
cromemcodsdd | Cromemco 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 |
z80packhd | z80pack 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.
.dsk is a CP/M diskAltair 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.
| Where | How |
|---|---|
| Telnet / SSH | Settings → CP/M Emulator → I. A short wizard: pick an image, pick a drive, confirm. |
| Web | AI, Browser, Weather & CP/M → More → Mount CP/M drives. |
| Desktop | Same 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.
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.
| Mount | Boot | |
|---|---|---|
| Scope | One drive of sixteen | The whole machine |
| Filesystem | Ours | The guest's |
| Other drives | Folders and images alongside | Mounted images come along, at the board slot their letter names; folders do not |
Our A>, EXIT, EGT8080 | Yes | No — the guest talks to hardware |
| Getting out | EXIT, or ESC twice | Press ESC twice |
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.
| System | Sign-on |
|---|---|
| Altair CP/M 2.2 | 63K CP/M / Version 2.2mits → A> |
| CP/M 3.0 | CP/M V3.0 Loader, Copyright (C) 1982, Digital Research |
| CP/M 2.2AT | For Altair 8" Floppy and Tarbell SDC |
| Altair DOS | MEMORY SIZE? |
| Altair Disk Extended BASIC | ALTAIR DISK EXTENDED BASIC, VERSION 300-5-F |
| Time Sharing BASIC | ALTAIR TIMESHARING BASIC → RECONFIGURE (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.
| Where | How |
|---|---|
| Telnet / SSH | Settings → CP/M Emulator → B (Boot settings) → R, which opens a list of the emulator and every disk that boots. |
| Web | AI, Browser, Weather & CP/M → More → CP/M runs. |
| Desktop | Same 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.
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.
| Value | Console | Notes |
|---|---|---|
auto | — | The default. Read from the disk's own boot code, falling back to altair_2sio when the disk does not say plainly. |
altair_2sio | Altair 88-2SIO, 10h/11h | What auto falls back to. Every Altair disk boots because of it. |
altair_sio | Altair 88-SIO, 00h/01h | Active-low status. |
console_04 | 04h/05h | Ready when the status bit is clear. |
console_04_cuter | 04h/05h in, ROM out | The guest prints by calling a Processor Technology CUTER ROM, which we synthesise at C019. Reaches Tarbell 48K CPM 2.2. |
z80pack | 00h/01h, whole-byte | A 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. |
cromemco | Cromemco TU-ART, 00h/01h | Bit 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 0Ah–11h,
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.
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:
| Guest | BS 08h | DEL 7Fh | Wants |
|---|---|---|---|
| MITS CP/M 2.2, Altair Disk Extended BASIC, Altair Hard Disk BASIC | 08 20 08 — erases | G / \G — rubout | backspace |
| Digital Research CP/M 2.2, MP/M, UCSD p-System IV | 08 20 08 — erases | 08 20 08 — erases | either |
| CP/M 1.3, 1.4, 1975 | ^H printed literally | G — rubout | rubout |
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.
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:
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.
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:
| Profile | Why not |
|---|---|
aux | The AUX: device belongs to our BDOS; a booted disk brings its own. |
hbios_1, hbios_2 | HBIOS is RomWBW firmware, which an Altair disk does not have. |
altair_2sio1 | 0x10/0x11 is the console on this machine — on a real Altair the console is 2SIO port A. |
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.
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.
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:
| Card | Disk | What it is |
|---|---|---|
| VDM-1 64×16 characters |
TDISK04 | CP/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 |
TDISK05 | CP/M 2.2 for the VDM-1 | |
DISK11 | VDM-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 | |
TDISK06 | VDM-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-stuff | z80pack’s altairsim library — two systems and a programs disk | |
| Dazzler colour, 1976 |
DISK10 | Dazzler programs, and boots CP/M — the library disk, around two dozen programs, and the usual place to start |
DISK15 | the Felix animation system for the Dazzler | |
dazzler, dazzlerII, dazzler_graphics, dazzler_stuff | z80pack’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.
web_enabled, default port
8080) — it is off by default.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.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 see | What it means |
|---|---|
(VDM-1) beside a session | That 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 rising | The 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. |
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.
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 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.
| Format | Picture | Colour |
|---|---|---|
| Normal, 512 bytes | 32×32 | Four 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 KB | 64×64 | |
| Resolution ×4, 512 bytes | 64×64 | One 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 KB | 128×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 DISK13–DISK16
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 18h–1Ch — 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.
cpm_printerCP/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.
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.
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.cpm_printer_port; today there is one, the Altair line printer with
its data register at 03h.
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.
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.
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.
cpm_emu_uartThree 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.
| Value | Access | Addresses / unit | Matches |
|---|---|---|---|
off | — | — | No virtual modem; guest code cannot open a socket. |
rc2014_1a | Port (Z80 SIO/2) | status 0x80 / data 0x81 | RC2014 SIO/2 board 1, channel A |
rc2014_1b (default) | Port (Z80 SIO/2) | status 0x82 / data 0x83 | RC2014 SIO/2 board 1, channel B — the usual AUX: port; qterm82 |
rc2014_2a | Port (Z80 SIO/2) | status 0x84 / data 0x85 | RC2014 SIO/2 board 2, channel A — qterm84 |
rc2014_2b | Port (Z80 SIO/2) | status 0x86 / data 0x87 | RC2014 SIO/2 board 2, channel B |
altair_2sio1 | Port (6850 ACIA) | status 0x10 / data 0x11 | Altair 88-2SIO port 1 — IMP8 |
altair_2sio2 | Port (6850 ACIA) | status 0x12 / data 0x13 | Altair 88-2SIO port 2 |
altair_sio | Port (88-SIO) | status 0x00 / data 0x01 | Altair 88-SIO, the original MITS board |
aux | BDOS AUX: | functions 3 / 4 | Hardware-independent; SC126 / RomWBW |
hbios_1 | RomWBW HBIOS | unit 1, via RST 8 | qtermh1 and RomWBW software generally |
hbios_2 | RomWBW HBIOS | unit 2, via RST 8 | qtermh2 |
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 (“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.
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:
JR→JP, 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.
| Key | Does |
|---|---|
T | Terminal mode — the wire, with the menu key to get back. |
S | Settings: serial port, line settings, display. |
U | Upload a file to the far end (XMODEM). |
D | Download a file from the far end (XMODEM). |
H | Help. |
V | Save settings into EGT8080.COM itself. |
X | Exit to CP/M. |
^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.
| Key | Does |
|---|---|
E | Exit terminal mode, back to the main menu. |
H | Help. |
U | Upload a file to the far end (XMODEM), without leaving the session. |
D | Download a file from the far end (XMODEM), without leaving the session. |
| the menu key again | Sends the menu key itself to the far end. |
| anything else | Returns 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.
S → PThe 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 profile | Press | EGT8080 then reports |
|---|---|---|
rc2014_1b (default) | 1 | Z80 SIO/2 at 82/83 |
rc2014_1a | 5 1 1 | Z80 SIO/2 at 80/81 |
rc2014_2a | 5 1 3 | Z80 SIO/2 at 84/85 |
rc2014_2b | 5 1 4 | Z80 SIO/2 at 86/87 |
altair_2sio1 | 3 1 | 6850 ACIA at 10/11 |
altair_2sio2 | 3 2 | 6850 ACIA at 12/13 |
altair_sio | 4 1 | Altair 88-SIO at 00/01 |
aux | 6 then any key | CP/M AUX: device (BDOS 3/4) |
hbios_1 | 2 1 RETURN | RomWBW HBIOS unit 1 |
hbios_2 | 2 2 RETURN | RomWBW 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 AT →
OK 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.
EGT8080's five port families, what selects each, and how far each one has been proven.
| Machine | EGT8080 driver | Emulator profile | Verified |
|---|---|---|---|
| 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_1a … rc2014_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.
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) | Name | Behaviour here |
|---|---|---|
0x00 | CIOIN 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. |
0x01 | CIOOUT 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. |
0x02 | CIOIST input status | Count of waiting characters, in both A and E, flags following, so Z means “nothing waiting”. Capped at 0x7F. |
0x03 | CIOOST output status | Free output space, same convention, also capped at 0x7F. |
0x04 | CIOINIT 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”. |
0x05 | CIOQUERY | Reports the stored line characteristics in DE, with no terminal type. |
0x06 | CIODEVICE | Describes the unit: RS-232 attribute, this unit number, no device type / mode / base. |
0xF1 | SYSVER | Firmware version, returned in DE with the platform id in L. |
0xF8 | SYSGET | Sub-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”. |
0x20 | RTCGETTIM 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. |
0x21 | RTCSETTIM | Refused. 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.
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.
| Fn (C) | Name | Notes |
|---|---|---|
0 | System Reset | Warm boot — returns to the A> prompt. |
1 | Console Input | Blocking read, echoed. |
2 | Console Output | Character in E. |
3 | Reader Input | AUX: in — the virtual modem. |
4 | Punch Output | AUX: out — the virtual modem. |
5 | List Output | Printer, routed to the console. |
6 | Direct Console I/O | Non-blocking read / write. |
7 / 8 | Get / Set IOBYTE | Backed by the page-zero IOBYTE. |
9 | Print String | $-terminated string at DE. |
10 | Read Console Buffer | Line input with editing. |
11 | Console Status | Non-blocking “is a key waiting”. |
12 | Return Version | 0x22 — CP/M 2.2. |
32 | Get / Set User | E = 0xFF gets, otherwise sets. |
| Fn (C) | Name | Notes |
|---|---|---|
13 | Reset Disk System | Back 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. |
14 | Select Disk | Also maintains the page-zero CDISK byte, which some transients read directly to find their login drive. |
15 / 16 | Open / Close File | Writes 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 / 18 | Search First / Next | Wildcard directory search. |
19 | Delete File | |
20 / 21 | Read / Write Sequential | |
22 | Make File | |
23 | Rename File | |
24 | Return Login Vector | All sixteen bits set — every drive A:–P: exists, since its folder is auto-created. Returning 0 here used to confuse drive-enumeration utilities. |
25 | Return Current Disk | |
26 | Set DMA Address | Reset 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. |
27 | Get Allocation Vector | A real bitmap — reserved directory blocks plus the blocks this drive's files occupy — so STAT's free count reflects actual usage. |
28 | Write Protect Disk | Software write-protects the current drive until the next disk reset. Enforced in all four mutating paths — write, make, erase and rename all refuse. |
29 | Get R/O Vector | The real bitmap of write-protected drives, so a program can see the protection it just asked 28 for. |
30 | Set File Attributes | The 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. |
31 | Get Disk Parameters | Returns a synthesised DPB. |
33 | Read Random | |
34 / 40 | Write Random / with Zero Fill | 40 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. |
35 | Compute File Size | Sets R0–R2 to the record count. |
36 | Set Random Record | From the current sequential position. |
37 | Reset Drive | Releases 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. | ||
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.
| Vector | Name | Behaviour |
|---|---|---|
1 | WBOOT | Warm boot to the A> prompt. |
2 | CONST | 0xFF if a key is buffered, else 0. |
3 | CONIN | Blocking keyboard read, no echo. |
4 / 5 | CONOUT / LIST | Character in C to the console. |
6 | PUNCH | Character in C to the virtual modem. |
7 | READER | Next modem byte, or ^Z if none. |
15 | LISTST | List 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.
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.
| Command | Does |
|---|---|
AT | Attention — answers OK. |
ATDtarget | Dial. See the target table below. |
ATA | Answer a ringing inbound call. |
ATH | Hang up. |
ATO | Return online after a +++ escape. |
ATEn | Command echo off / on. |
ATQn | Suppress result codes. |
ATVn | Word (1) or numeric (0) result codes. |
ATXn | Result-code level 0–4. Low levels collapse BUSY / NO ANSWER to NO CARRIER, as a modem without call-progress detection does. |
ATIn | Identify. |
ATZ | Reset to the saved profile — this modem's NVRAM, not the file, as a real modem does. |
ATSr=n, ATSr? | Set / query S-register. |
AT&Cn | DCD handling: 0 forces it on, 1 tracks carrier. |
AT&Dn | DTR handling — accepted, not modelled. |
AT&F | Factory reset. |
AT&W | Write 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.
| Target | Reaches |
|---|---|
ATD A / ATD B | This gateway's own serial Port A / Port B. |
ATD A@host | A port on another gateway, via the master/slave crossbar. Requires allow_peer_dial. |
ATDT host[:port] | A TCP host; port 23 by default. |
ATDT ethernetgateway | This 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.
Deep-dive reference pages for every protocol and interface, plus the character-set tables and ANSI escape-sequence reference.
UART profiles, EGT8080, HBIOS calls, BDOS/BIOS coverage.
128-byte blocks; CRC-16 / checksum negotiation.
Block-0 metadata, batch, exact size truncation.
Streaming; ZDLE, CRC-32, autostart, resume.
Send-Init negotiation; F / A / D / Z / B transfer.
C1 dual checksum, two-phase, GOO/BAD/ACK.
Measured Altair / IBM 3740 layouts, skew, checksums, EXM.
Hayes command set, S-registers, +++ escape.
IAC negotiation, every option, the NVT data phase.
Server, gateway, host keys, TOFU, auth modes.
Hex tables for every encoding: ASCII, ANSI, PETSCII, ATASCII, Baudot/ITA2, ZX Spectrum, TRS-80.
Cursor, colour/SGR, erase, and screen-mode escape codes, with the raw hex bytes.