Systems / Under the surface

From CIO to GEM.

The eight-bit family hid powerful device services in ROM; the ST put a graphical desktop on top of a new sixteen-bit foundation.

Atari OS: services in ROM

The Atari eight-bit operating system lives largely in ROM. It initializes hardware, manages input and output through a device-independent interface, supports the screen editor, handles floating-point routines, and coordinates peripherals over the Serial Input/Output bus.

This architecture explains one of the system's most memorable qualities: printers, disk drives, and other intelligent peripherals could be daisy-chained with relatively little configuration.

Revisions and compatibility

The original 400/800 OS and the later XL/XE revisions are closely related but not identical. Some older software relied on undocumented addresses or behavior, so Atari supplied the Translator disk to temporarily load a more compatible environment.

CIO: one interface, many devices

Central Input/Output (CIO) lets a program ask for an operation without knowing how a particular device performs it. An application opens a named device, reads or writes data, checks status, and closes the connection. A device handler supplies the implementation. Changing the device name can redirect output from the screen to a printer or a disk file without rewriting the application’s output logic.

A channel number is a connection, not a hardware address. Channel 1 can refer to the keyboard today and a disk file later. The device specification supplies the destination: in D2:NOTES.TXT, D selects the disk handler, 2 selects the drive, and the rest names the file.

The route from a program to a device
Layer Its job
Application Requests OPEN, GET, PUT, STATUS, CLOSE, or a device-specific operation.
IOCB → CIO An I/O Control Block describes the channel, command, buffer, and options. CIO checks and dispatches the request.
HATABS → handler The RAM Handler Address Table connects a device letter to a table of handler entry points.
Device implementation The handler works with display memory, keyboard services, SIO, or another interface as appropriate.

The common interface does not make all devices identical: a keyboard cannot accept output, a printer cannot supply a line of keyboard input, and disk-specific commands require a disk handler. The handler reports unsupported operations as errors. Atari’s Technical Reference Notes, sections 5 and 9, document both sides of this interface.

The editor, keyboard, screen, and peripherals

Device What you access
E: — screen editor Text output with cursor and editing controls, plus edited keyboard input. A record read lets the user edit a logical line and press RETURN. It is more than a raw stream of key presses.
K: — keyboard Individual translated ATASCII key values, without the editor’s line editing or automatic echo. Useful when your program handles its own input.
S: — screen Display operations in the selected graphics mode. Character or pixel interpretation depends on the mode; screen coordinates and special commands support graphics operations. It does not provide the editor’s keyboard interaction.
P: — printer Buffered printer output through the resident printer handler.
C: — cassette Sequential cassette input and output, with recording details handled below the application.
D: — disk files File operations supplied by a loaded DOS/file-management handler. The stock ROM’s disk boot and sector services alone do not provide a general D: filesystem.
R: — serial interface A handler supplied for an interface such as the Atari 850; it is not one of the five standard resident ROM devices. Configuration and special commands depend on the handler.

In Atari BASIC, plain PRINT and INPUT normally use the editor on channel 0. BASIC manages IOCBs for you through OPEN, CLOSE, GET, PUT, and XIO. For OPEN, auxiliary value 4 requests input, 8 output, and 12 both, where supported. Channel 6 is normally used by BASIC’s graphics commands, while channel 7 is used by several interpreter I/O operations; channel 1 is a convenient choice for these small examples.

Atari BASIC — read one key without editor echo
10 OPEN #1,4,0,"K:"
20 GET #1,K
30 CLOSE #1
40 PRINT "ATASCII VALUE: ";K
Atari BASIC — send a line to a connected printer
10 OPEN #1,8,0,"P:"
20 PRINT #1;"HELLO FROM CIO"
30 CLOSE #1

The first example waits for one key; it does not wait for a complete edited line. The second requires a working printer connection. With DOS loaded, changing "P:" to an unused filename such as "D1:HELLO.TXT" writes a disk file instead. Output mode 8 can replace an existing file, so choose a new name. See Technical Reference Notes, sections 5–6, for device behavior and command parameters.

Calling CIO from 6502 assembly

There are eight 16-byte IOCBs at $0340–$03BF. Use $0340 + channel × 16 for a channel’s base address, and load X with channel × 16 before calling CIOV ($E456). X is an offset: channel 1 uses $10, not 1. Use the published entry vector rather than an internal ROM address that can move between OS revisions.

Offset / name Purpose
+0 ICHID OS-maintained index into HATABS; $FF means closed.
+1 ICDNO Device unit number established by OPEN.
+2 ICCOM Command: 3 OPEN, 5 GET RECORD, 7 GET CHARACTERS, 9 PUT RECORD, 11 PUT CHARACTERS, 12 CLOSE, 13 STATUS.
+3 ICSTA Returned status, also returned in Y. Normal success is 1; values with bit 7 set indicate errors.
+4/+5 ICBAL/H Low/high address of the device specification for OPEN, or of the data buffer for transfers.
+6/+7 ICPTL/H Cached PUT BYTE entry address minus one, established by OPEN.
+8/+9 ICBLL/H Low/high byte count for a buffer transfer.
+10/+11 ICAX1/2 Auxiliary options, including the OPEN access mode.
+12…+15 Additional handler-dependent state.

For a normal OPEN, point the buffer address at an ATASCII device/file specification terminated by $9B (Atari end-of-line). After a successful OPEN, set the transfer command, buffer address, and length before calling CIO again. Record operations recognize end-of-line; character operations transfer bytes without treating $9B as a record boundary. CLOSE lets a handler flush its buffers and release resources.

6502 — print through an already-open editor channel
CIOV  = $E456
ICCOM = $0342
ICBAL = $0344
ICBAH = $0345
ICBLL = $0348
ICBLH = $0349

PrintHello:
    LDX #$00          ; IOCB 0 already open to E:
    LDA #11           ; PUT CHARACTERS
    STA ICCOM,X
    LDA #<Message
    STA ICBAL,X
    LDA #>Message
    STA ICBAH,X
    LDA #MessageEnd-Message
    STA ICBLL,X
    LDA #0
    STA ICBLH,X
    JSR CIOV
    TYA               ; explicitly test returned status
    BMI Failed
    RTS
Failed:
    RTS               ; return error in Y to the caller

Message:
    .BYTE "HELLO FROM CIO",$9B
MessageEnd:

This subroutine assumes the normal editor environment with IOCB 0 already open; it is not a complete boot program or BASIC USR wrapper. It prints one line, including an explicit Atari end-of-line. A caller must handle the returned error status. The assembly snippets use symbolic 6502 notation with .BYTE/.WORD directives; origin, binary-load headers, and invocation depend on your assembler and host program. Reference: section 5 and appendix L, IOCB definitions.

Where CIO ends and SIO begins

CIO deals in logical operations; Serial Input/Output (SIO) deals with transactions on the peripheral bus. A DOS handler can turn a file read into disk-sector requests, then use SIO to exchange those requests with a drive. The drive’s electronics carry out its own part of the protocol. The screen and keyboard do not need to travel over SIO just because an application reaches them through CIO.

To issue a bus transaction directly, a machine-language program fills the Device Control Block at $0300–$030B and calls SIOV ($E459). Its fields specify the device ID, unit, command, data direction, buffer address, timeout, byte count, and two device-specific auxiliary bytes. The direction/status byte DSTATS ($0303) uses $40 for a transfer to the computer and $80 for one from the computer; it holds status on return, also reported in Y.

SIO is useful when you need a peripheral-specific command, but bypassing DOS also bypasses its filenames, allocation, and buffering. Read the device’s command specification before choosing DCB values. A hardware device number in SIO and a channel number in CIO serve different purposes. Reference: section 9, SIO interface and Device Control Block.

Using the floating-point package

The ROM math package is a separate callable library. You do not OPEN a floating-point device or send it a CIO command. Its numbers use a six-byte decimal floating-point representation with a base-100 exponent scheme and packed-BCD mantissa, rather than modern IEEE binary floating point. Operands live in RAM pseudo-registers: FR0 ($00D4) and FR1 ($00E0), each six bytes long.

Routine / entry Calling convention
AFP $D800 Convert numeric text to FR0. Set the text pointer in INBUFF ($F3/$F4) and its starting offset in CIX ($F2). CIX advances past the parsed number; check carry for failure.
FASC $D8E6 Convert FR0 to text. INBUFF returns a pointer into the line buffer. Bit 7 of the final character marks the end; no $9B terminator is appended.
IFP $D9AA Convert an unsigned 16-bit integer in FR0’s first two bytes, low byte first, into floating point in FR0.
FPI $D9D2 Round FR0 to an unsigned 16-bit integer in its first two bytes. Check carry for an invalid or out-of-range result.
FADD $DA66 / FSUB $DA60 FR0 + FR1 or FR0 − FR1 → FR0; FR1 is altered. Carry indicates an out-of-range result.
FMUL $DADB / FDIV $DB28 FR0 × FR1 or FR0 ÷ FR1 → FR0; FR1 is altered. Check carry for range errors, or division by zero.
6502 — convert the integer 123 to printable text
FR0    = $D4
INBUFF = $F3
IFP    = $D9AA
FASC   = $D8E6

MakeNumber:
    LDA #123
    STA FR0
    LDA #0
    STA FR0+1
    JSR IFP
    JSR FASC
    LDY #0
CopyText:
    LDA (INBUFF),Y
    PHA               ; preserve the end-marker bit
    AND #$7F
    STA NumberText,Y
    INY
    PLA
    BPL CopyText
    LDA #$9B
    STA NumberText,Y
    INY
    STY NumberLength  ; count includes the end-of-line
    RTS

NumberText:
    .BYTE 0,0,0,0,0,0,0,0  ; ample space for this example
NumberLength:
    .BYTE 0

After this routine, pass NumberText and NumberLength to CIO PUT CHARACTERS, as in the editor example, to display the number. Copying the text removes the high-bit end marker and adds the editor’s end-of-line character. The eight-byte buffer is for this fixed value; general-purpose formatting needs enough space for signs, decimals, and exponent notation.

The math routines share zero-page workspace and the line buffer around $0580. Save any live host-program state before borrowing those areas, especially when called from BASIC, and copy results before another operation reuses the buffer. Do not assume these routines can be interrupted by another call using the same workspace. Reference: section 8, floating-point routines and calling sequences.

Replacing a handler—including E:

The OS makes its device routing writable even when the original handlers live in ROM. HATABS starts at $031A. Each entry is three bytes: a device letter followed by the low and high bytes of the address of its handler vector table. The standard table has space for twelve entries and two trailing zero bytes. Each of the first six words in a handler vector table stores a routine’s address minus one; the HATABS pointer to that table is an ordinary address.

Table offset Entry and responsibility
+0 OPEN: validate options and prepare the device/channel.
+2 CLOSE: finish buffered output and release resources.
+4 GET BYTE: return one input byte in A.
+6 PUT BYTE: accept one output byte in A.
+8 STATUS: report readiness/device status.
+10 SPECIAL: handle device-specific commands.
+12 A three-byte JMP Init instruction, not another address-minus-one word.

CIO dispatches through the six vectors using the 6502 stack and RTS, which accounts for the minus one. Handler routines return with RTS and a status in Y—normally 1 on success. X identifies the originating IOCB by its byte offset, and CIO supplies working parameters in the zero-page IOCB at $20–$2F. An unsupported operation should report an appropriate error such as $92 (146).

There is an important exception for PUT BYTE: Atari BASIC can call its cached output entry directly without passing through CIO. Such a routine must not assume the zero-page IOCB describes the call. Use the originating IOCB for parameters if needed. Similarly, do not call CIO recursively from inside a handler without explicitly preserving its shared internal state; an output wrapper should chain to the saved handler entry directly.

A small editor extension: uppercase program output

A useful first experiment is to wrap the existing editor instead of implementing every editor function. Keep its OPEN, CLOSE, GET, STATUS, and SPECIAL behavior, and replace just PUT BYTE with a routine that converts ordinary lowercase output to uppercase. Keyboard editing remains with the original editor; this is an output filter, not a complete new text editor.

6502 — editor output wrapper, installed by the steps below
UpperPut:
    CMP #$61          ; ordinary ATASCII lowercase a
    BCC ChainPut
    CMP #$7B          ; one past lowercase z
    BCS ChainPut
    SEC
    SBC #$20          ; a-z becomes A-Z
ChainPut:
    JMP $FFFF         ; installer patches operand with
                      ; saved original PUT address + 1

This is the wrapper routine only: $FFFF must be replaced before it is reachable. Control codes and inverse-video characters pass through unchanged. The original handler returns directly to the caller and supplies its normal status.

  1. Reserve resident RAM. Keep the wrapper, a writable 15-byte handler table, and saved installation state outside memory that BASIC, DOS, display allocation, or your loader will reuse. A DOS resident driver normally participates in its loader’s memory-reservation scheme; an arbitrary fixed address is not a safe permanent home.
  2. Find the active editor entry. Search HATABS in three-byte steps from offset 33 down to 0, matching E. The high-to-low order matters when a replacement is already installed. Save the entry offset and its original table pointer. Do not assume a particular ROM address or a fixed slot.
  3. Prepare the wrapper before publishing it. Copy the original table into your RAM table. Read its PUT word at offsets 6–7; it is an address minus one. Add one with a full 16-bit carry and place the resulting actual address in the operand of ChainPut. Replace the RAM table’s PUT word with UpperPut-1. The other entries continue to refer to the original handler.
  4. Redirect new opens. At a point when no I/O is in progress, replace the saved HATABS entry’s two pointer bytes with the ordinary address of your RAM table. An installer must prevent callbacks from seeing a half-written pointer; simply disabling maskable interrupts does not disable the Atari’s NMI-driven activity.
  5. Update already-open editor channels. Scan IOCBs at offsets $00,$10,…,$70. For each whose ICHID matches the replaced HATABS slot, save ICPTL/H and set it to UpperPut-1. This includes the usual IOCB 0 and catches BASIC’s direct output calls. An application-owned channel can instead be closed and reopened, but casually closing the interpreter’s console can disrupt its editor state.
  6. Test and uninstall deliberately. Output "hello" through CIO and BASIC PRINT, exercise editing and RETURN, and verify that control characters still work. To remove the wrapper, restore the original HATABS pointer and affected channels’ output vectors before releasing RAM. Account for channels opened or closed since installation, and do not overwrite a newer handler installed after yours.

From a wrapper to your own editor

A full replacement provides its own routines for all six operations. Its GET side must handle the input interaction your programs expect; an E:-compatible editor needs cursor motion, insertion/deletion, logical-line handling, RETURN/end-of-line behavior, and BREAK/error handling. Its PUT side must interpret the editor’s ATASCII control characters as well as display ordinary text. Decide how it cooperates with graphics modes, text windows, screen memory, and the OS cursor variables.

To add an unrelated device such as N:, install its letter and vector-table pointer into a free HATABS entry instead. Appending another E entry can override future OPEN searches, but an existing IOCB retains its old HATABS index and cached PUT vector. That is why a controlled replacement of the active entry plus an update of open channels is useful for the console.

Installation and reset survival are separate jobs. The original OS rebuilds HATABS at reset, so the initialization JMP in your table does not by itself make a RAM handler survive. Arrange reinstallation through the appropriate DOS/cartridge initialization path for the target system, initialize private state explicitly, and avoid installing the same wrapper twice. Test the actual 400/800 or XL/XE ROM and DOS combination rather than relying on internal ROM addresses.

The wrapper and installation steps are an implementation guide, not a drop-in resident driver: memory placement, reset integration, and installation/removal code belong to the host environment. The table format and handler contract above follow Atari’s Technical Reference Notes, section 9 (printed pages 134–138). For further explanation, see De Re Atari, chapter 8, and the Atari XL OS addendum.

Disk operating systems

Unlike many later computers, an Atari eight-bit disk operating system is generally loaded from disk. That made DOS replaceable and encouraged alternatives with different command styles, formats, and capabilities.

Atari DOS 2.0S A widely recognized menu-driven standard for single-density disks.
Atari DOS 2.5 Added enhanced-density support for the Atari 1050 while retaining the familiar menu.
MyDOS Popular for subdirectories and larger or nonstandard storage configurations.
DOS XL Optimized Systems Software's flexible DOS offered both a command processor and a loadable menu, along with batch files and single- or double-density support.
SpartaDOS A command-line environment with subdirectories, batch files, date/time support, and a more modern filesystem model.
SpartaDOS X A cartridge-based continuation designed for powerful, expandable systems.
RealDOS An XL/XE-oriented DOS using the SpartaDOS File System, with a command processor largely compatible with SpartaDOS and BeweDOS plus an included menu for menu-oriented operation.
Two layers, two jobs

The ROM OS provides device and machine services. A disk OS adds files, directories, disk commands, and storage formats.

TOS and the ST desktop

On the Atari ST, TOS combines GEMDOS file and process services, BIOS and XBIOS machine interfaces, and GEM's graphical components. Users encounter it through a desktop of windows, menus, icons, drives, and files.

Early machines sometimes loaded TOS from disk; later systems commonly carried it in ROM. Multiple revisions followed as Atari added hardware and improved compatibility. The ST's `.PRG`, `.TOS`, and `.TTP` program types reflect whether an application expects the graphical environment, runs directly, or accepts parameters.

Read the history of the Atari ST operating system for the story of TOS, GEMDOS, GEM, and the teams that brought them together.

Professional GEM by Tim Oren

Read Tim Oren’s 15 Professional GEM columns from ANTIC Publishing, covering windows, dialogs, resources, menus, VDI graphics, events, user interfaces, and GEMDOS. The HTML editions include the original reprint notices and sample code, with links to the unchanged text files.

Beyond stock TOS

Owners can explore replacements and extensions such as EmuTOS, MiNT-family multitasking systems, alternate desktops, hard-disk drivers, and utilities. Compatibility depends on the exact machine and configuration, so changes should be documented and reversible.