ARM-DOS 4.00 Play it

Technical reference

The ARM/AT

What is emulated, and the agreement that turns an ARM processor into an IBM PC compatible.

PC software leaned less on the processor than on a pile of agreements: the interrupt table sits at address zero, the text screen lives at B800h, INT 21h with AH=3Dh opens a file, and a failed call sets the carry flag. Programs were written against those agreements, and the chip under them changed several times without most programs noticing. The ARM/AT keeps the agreements and swaps the chip.

1The ARM PC/AT design

Every design decision came back to one question: what would IBM and Microsoft have done in 1988 if the PC had shipped with an ARM? Nearly always the answer was to keep what they had, so every PC and DOS interface that makes sense on an ARM stays. The only changes are the ones the processor forces, and there are four of them.

INT n becomes SVC #n

On a PC, INT 21h makes the processor fetch a handler address from the vector table at 0000:0084. Here a program executes SVC #0x21. The ARM traps into the ROM, the ROM reads the number out of the instruction itself and fetches the handler from the same table at the same address: 256 vectors of four bytes each, vector n at n*4. Programs hook interrupts with INT 21h AH=25h and chain to the old handler, exactly as on a PC. Hardware interrupts arrive the same way, as INT 08h-0Fh for IRQ 0-7 and INT 70h-77h for IRQ 8-15, and handlers send their own end-of-interrupt to port 20h.

x86 registers live in ARM registers

x86ARMNotes
AX, AH, ALr0, bits 15-8, bits 7-0AH=09h is mov r0, #0x900
BX, CX, DXr1, r2, r3
SI, DI, BPr4, r5, r6
DS, ES (as values)r7, r8only where DOS takes a segment number, such as freeing memory
carry flagthe ARM's C flagafter a DOS call, bcs error is the translation of jc error
zero flagthe ARM's Z flag

Segments become flat pointers

An ARM has one flat address space, so wherever DOS expects a DS:DX pair, the ARM version passes a single 32-bit pointer in the offset register. Where DOS hands out or takes a segment (memory blocks, PSPs, environments), ARM-DOS still uses a paragraph number, the address divided by 16, just as DOS does. DOS's own memory all sits in the first 640 KB, so every segment fits in 16 bits and MEM looks the way it did in 1989.

EXE files hold ARM code

Every .EXE on this machine starts with a proper DOS MZ header and a tiny 8086 program. Copy one to a real 1989 PC and it will run, just long enough to print This program requires an ARM processor. At the usual offset 3Ch, the header points to an ARM header (signature AR1) that describes the ARM image, its size, stack, entry point and relocation table. Programs are linked at address zero and relocated when they load, at PSP + 100h, the same place a .COM file goes. A plain .COM is a flat ARM image loaded at the same spot.

Even the floppy boot sector follows the rule. On a PC its first bytes jump over the BIOS parameter block; here they are an ARM branch over it, because the parameter block must stay at offset 0Bh.

2The processor and its JIT

The processor is an ARM926EJ-S: the ARMv5TE instruction set in both ARM and Thumb states, with the DSP multiplies and saturating arithmetic, and a VFP9-S floating-point coprocessor (VFPv2) that programs can opt into. There is no MMU, as on the simple ARM systems of the time; DOS and its programs run in a privileged mode with no protection, just like real mode. Jazelle is left out. When DOS has nothing to do, it executes the ARM's wait-for-interrupt instruction, so the processor sleeps until the next interrupt and your browser tab stays cool.

The emulator, written in JavaScript, has two tiers. An interpreter runs everything, counting how often each entry address is reached. After sixteen visits, the block from there (up to 64 instructions, continuing past conditional branches as side exits) is compiled into a JavaScript function, with the guest registers in local variables, constants folded, memory accesses inlined and a branch back to the block's start turned into a native loop. The browser's JavaScript engine then compiles that into machine code. Stores into memory that holds compiled code throw exactly the affected functions away, so self-modifying code and program loading just work.

Benchmark (Chromium)JITInterpreter
mixed C benchmark, ARM, bare processor~880 MIPS~155 MIPS

On a modern computer that is several times what a 100 MHz machine needs, so the real-time driver holds it back to its clock and lets the rest of the time go idle. Every instruction takes one cycle, which keeps emulated time exact: DOOM's timedemo gives the same 77 frames per second at 100 MHz on every computer.

Busy-wait loops (waiting for the vertical retrace, polling the BIOS tick) are fast-forwarded. The emulator single-steps one iteration of a suspected loop, proves it changes nothing but time, and then charges the skipped iterations up to the next moment anything could change, which gives exactly the result of running every one.

How we know it is right. The processor was checked against QEMU's ARM926 with more than half a million random instructions across every instruction class, with and without the JIT, and the floating-point unit with over 150,000 more cases covering rounding modes, NaNs, denormals and infinities. Every result matched.

3The memory map

AddressWhat is there
00000000the interrupt vector table, 256 four-byte vectors
00000400the BIOS data area, at the PC's offsets (equipment word, keyboard buffer, video mode, tick count ...)
00000500-0009FFFF640 KB of conventional memory: DOS, its buffers, the memory control blocks and programs
000A0000-000BFFFFvideo memory: VGA graphics at A0000h, text and CGA at B8000h
000C0000-000FFFFFthe adapter and ROM hole, empty (except in video mode 62h)
00100000-0010FFFFthe first 64 KB of extended memory, used by the BIOS for its data and stacks
00110000-00FFFFFFextended memory, handed out by HIMEM.SYS through XMS
10000000-1000FFFFthe ISA I/O space: x86 port p is at 10000000h + p
FFF00000-FFFFFFFFthe BIOS ROM, with the ARM exception vectors at the top

ARM programs have no IN and OUT instructions, so the I/O ports are memory-mapped: outb(0x20, 0x20) is a byte store to 10000020h. Every port number is the PC's.

4The devices

PortDevice
20h, A0hinterrupt controller, 16 lines (timer, keyboard, serial, floppy, Sound Blaster, clock, MIDI, mouse, disks)
40h-43h, 61h8254 timer at 1.193182 MHz, driving the timer tick and the PC speaker
60h, 64h8042 keyboard controller, scan code set 1, with a PS/2 mouse
70h, 71hCMOS clock and battery RAM (kept in your browser) for BIOS SETUP
00h-0Fh, C0h-DFhthe AT's pair of 8237 DMA controllers, with page registers
1F0h, 170hIDE: the hard disks (ATA, master and slave) and the CD-ROM drive (ATAPI)
300h-307hthe floppy controller: a simplified ARM-PC design with its own DMA, on the port range of IBM's old prototype card
3C0h-3DFhVGA (or a Hercules card at 3B0h-3BFh)
220h, 388hSound Blaster 16 with its OPL3 FM chip
330hMPU-401 MIDI interface with a General MIDI synthesiser behind it
201hgame port for two joysticks
3F8hCOM1, a serial console shown on the page
2F8hCOM2, a 16550A with an internal Hayes modem
LPT1an Epson FX-80 style dot-matrix printer
F0h-FFhthe ARM-PC system board: board ID, clock speed, TURBO, the X86 lamp, and small mailboxes the page reads

Each device is modelled closely enough that DOS programs of the time can drive the hardware directly, as they always did. The timer can be reprogrammed and counted, the keyboard controller can be hooked at INT 9, and the game port's one-shots take real (emulated) time to settle, so the counting loops old games used give the numbers they expect at any clock speed.

The game port takes real game controllers through the browser's Gamepad API, or the on-screen pad's JOY mode, as analogue PC joysticks.

Opening the case lets you change the hardware: VGA or Hercules, Sound Blaster 16 or an AdLib-only card or nothing, 1 to 16 MB of SIMMs, the modem card, the CD-ROM drive, the MIDI daughterboard, the mouse and the clock jumper. The BIOS finds out what is fitted at the next power-on, as a real one did.

5Video

The VGA draws the screen straight from the machine's memory, 70 frames a second. It supports:

Register writes are placed on the scan line they happen on, so palette changes per scan line (the "copper" effects of the demo scene) are displayed as they would be on a real card.

A Hercules Graphics Card with a green, amber or paper-white monochrome monitor can be fitted instead, with MDA text and 720x348 graphics. On the page, a WebGL shader draws the picture tube: curvature, scan lines, a shadow mask, bloom, warm-up, the dot when it switches off, and the afterglow of the monochrome phosphors.

6Sound

All of the machine's audio is rendered in emulated time and played through an AudioWorklet in the browser.

7The modem and the phone exchange

COM2 is a full 16550A UART with FIFOs and interrupts, and behind it sits a Hayes-compatible modem with the AT command set, S-registers, result codes and the +++ escape with its guard time. The speed switch on its front sets the card to 2400, 14,400, 33,600 or 56,000 bits per second, and the data is paced to that rate. The modem's speaker plays the dial tone, the touch-tone digits, the ringback and the handshake of each modulation, generated from real carrier signals. The 56K handshake was tuned against a real call.

The page runs a small telephone exchange that the modem dials into:

8Disks

9The page around it

The whole computer is drawn in CSS and a few small SVGs, and scales as one object. The page is a progressive web app: it can be installed to a home screen and works offline once loaded. A new release downloads in the background, complete or not at all, and takes over the next time the page loads, never under a running machine. Nothing runs until you press the power switch, and nothing plays a sound before you have touched the page.

The ARM inspector beside the machine is a real debugger. It shows the registers under their x86 names, disassembles around the program counter, pauses and single-steps, sets breakpoints (which turn the JIT off while they exist), logs every interrupt with its DOS function, and dumps memory. The memory map draws all 16 MB as a heat map of execution, reads and writes, with each DOS memory block named after its owner; it costs only a percent or two while open and nothing while closed. The ELBOW view is described on the ELBOW page.

The ARM-DOS machine just after booting: the monitor lists HIMEM, the CD-ROM driver and ARM-DOS Version 4.00 messages, and the ARM inspector beside it shows the registers under their x86 names, the flags, a disassembly and a log of INT 21h calls.
Just after boot. The inspector shows the registers under their x86 names and the last few INT 21h calls.