This document covers the BDOS, BIOS, terminal, and program conventions for writing .COM utilities on this system. All information is specific to this implementation.
- Program Startup & Memory Map
- BDOS Calling Convention
- BDOS Functions
- FCB Structure
- BIOS Functions
- Terminal (Screen & Keyboard)
- Disk Parameter Block (DPB)
- Useful Recipes
0000h JP warm_boot ; WBOOT vector
0001h dw bios_base ; BIOS jump table address - 3
0005h JP bdos ; BDOS entry point
0006h dw top_of_tpa ; high byte = top of usable RAM
0038h EI / RETI ; IM 1 interrupt vector
005Ch FCB1 (36 bytes) ; first filename argument
006Ch FCB2 (36 bytes) ; second filename argument (overlaps FCB1 AL)
0080h db cmd_tail_len ; command tail length
0081h ds 127 ; command tail text (default DMA buffer)
0100h ...program code... ; entry point
...
F600h common memory starts ; system stubs, not usable by programs
When a .COM program starts executing at 0100h:
- SP = address of 0006h (top of TPA, use as initial stack)
- C = current default drive (0=A)
- Z80 interrupts: Enabled, IM 1
- Page zero: WBOOT at 0000h, BDOS at 0005h
- DMA address: 0080h (default)
- FCB1 (005Ch): parsed first filename argument from command line
- FCB2 (006Ch): parsed second filename argument
- Command tail: length byte at 0080h, ASCII text at 0081h (no trailing CR)
- Both FCBs are cleared (drive=0, name=spaces) if no argument was given
Return to the CCP by jumping to 0000h (warm boot). This reloads the CCP and redisplays the prompt. You can also call BDOS function 0 (system reset).
Programs should close any open files before exiting (BDOS F16).
~61K available (0100h to ~F600h). The exact top is in the word at 0006h.
| Register | Direction | Purpose |
|---|---|---|
| C | In | Function number |
| DE | In | Parameter (address, value, etc.) |
| E | In | Low-byte parameter for some functions |
| A | Out | Return value (low byte) |
| HL | Out | Return value (16-bit) |
ld c, func_num
ld de, parameter
call 5
; A and HL contain return values- Most functions return a status in A (0=success, 0xFF=error).
- HL usually matches A (L = A, H = 0) for 8-bit returns.
- 16-bit returns use HL directly.
- Other registers (BC, DE, IX, IY) are NOT preserved across BDOS calls unless the function docs say so.
- In: C=0
- Effect: Warm boot; does not return. Reloads CCP.
- In: C=1
- Out: A=character (with echo)
- Note: Waits for keypress. Echoes the character.
- In: C=2, E=character
- Out: A=E
- Note: Outputs one character to the screen.
- Out: A=0x1A (EOF)
- Note: Stub — no auxiliary device.
- In: E=character
- Note: Stub — discarded.
- In: E=character
- Note: Stub — discarded (no printer).
- In: C=6, E=command
- Out: varies
- E=0xFF: Read a character (non-blocking). Returns 0 if none ready.
- E=0xFE: Status check. Returns 0xFF if char ready, 0x00 if not.
- E=other: Output character E. No echo processing.
- Out: A=IOBYTE value
- In: E=value
- In: C=9, DE=address of '$'-terminated string
- Out: A=0x24 ('$')
- Note: Outputs characters until '$' is found. The '$' is NOT printed.
- In: C=10, DE=buffer address
- Out: A=0x0D
- Buffer format:
+0: max chars (set by caller, e.g. 126) +1: actual len (filled by BDOS) +2: data bytes (characters, no CR) - Line editing: BS (0x08) / DEL (0x7F) = destructive backspace. Ctrl-C = abort + warm boot. CR = submit.
- Note: No cursor movement, insert mode, or history.
- Out: A=0xFF (char ready) or 0x00 (not ready)
- Out: HL=0x0031 (CP/M 3.1)
- Effect: Resets to drive A, clears login/RO vectors, sets DMA=0080h.
- In: E=drive (0=A, 1=B, ...)
- Note: Sets default drive for subsequent file operations.
- Out: HL=16-bit bitmap (bit N = drive N logged in)
- Out: A=drive number (0=A)
- In: DE=DMA address
- Note: Sets the 128-byte buffer address for disk reads/writes. Default 0080h.
- Out: HL=address of allocation bitvector for current drive
- Effect: Marks current drive as read-only in software.
- Out: HL=16-bit bitmap (bit N = drive N is read-only)
- Out: HL=address of Disk Parameter Block for current drive
- In: DE=16-bit drive bitmap
- Effect: Clears login for specified drives.
- Out: A=0
- Note: Writes any dirty deblock buffer to disk.
- In: DE=FCB address
- Out: A=0 (found) or 0xFF (not found)
- Note: Sets CR=0. FCB must have drive and filename filled in.
- In: DE=FCB address
- Out: A=0 or 0xFF
- Note: Flushes directory entry. Required to save written data!
- In: DE=FCB address (supports '?' wildcards in name/type)
- Out: A=entry index (0-3) or 0xFF (not found)
- Note: Copies matching 128-byte directory sector to DMA. Return value 0-3 = offset into DMA (entry = DMA + index*32).
- Out: A=entry index (0-3) or 0xFF (no more matches)
- Note: Continues from previous F17. Same FCB must remain at 005Ch.
- In: DE=FCB address (supports wildcards)
- Out: A=0 (deleted) or 0xFF (none found)
- In: DE=FCB address
- Out: A=0 (OK), 1 (EOF), 0xFF (error)
- Note: Reads 128 bytes to DMA. Advances CR. Auto-extends to next extent.
- In: DE=FCB address
- Out: A=0 (OK), 1 (disk full), 0xFF (error)
- Note: Writes 128 bytes from DMA. Advances CR. Allocates blocks as needed.
- In: DE=FCB address
- Out: A=0 (created) or 0xFF (directory full)
- Note: Creates new directory entry. FCB must have drive and filename. Should be followed by writes then close.
- In: DE=FCB address
- Note: Bytes 1-11 = old name, bytes 17-27 = new name. Supports '?' in old.
- In: DE=FCB address
- Note: Copies attribute bits (bit 7 of name/type bytes) to directory.
- In: DE=FCB address (R0/R1/R2 = record number)
- Out: A=0 (OK), 1 (unwritten), 4 (no extent), 6 (random record overflow)
- Note: Reads 128 bytes from arbitrary record position to DMA.
- In: DE=FCB address (R0/R1/R2 = record number)
- Out: A=0 (OK), 6 (overflow)
- Note: Writes 128 bytes from DMA to arbitrary record position.
- In: DE=FCB address
- Out: FCB[33-35] = random record count (virtual file size)
- Note: Scans all extents. Returns highest record + 1.
- In: DE=FCB address
- Out: FCB[33-35] = current sequential position as random record
- In: DE=FCB (R0/R1/R2 set)
- Out: A=0 or 6
- Note: Same as F34 in this implementation.
- In: E=count (1-128)
- Out: A=0 (OK) or 0xFF (invalid)
- In: E=mode
- In: E=drive (0=default, 1=A, ...)
- Out: DMA[0..2] = 24-bit little-endian free record count
- Note: Free records = free_blocks × records_per_block. To get free K:
free_K = (DMA[0] | DMA[1]<<8 | DMA[2]<<16) / 8(for 1K blocks).
- In: DE=PB (parameter block address)
- PB format:
[offset, operation, value_lo, value_hi]- operation 0x00: get word → returns SCB word at offset
- operation 0xFE: set byte
- operation 0xFF: set word
- In: DE=PB (parameter block address)
- PB format:
[func, A, C, B, E, D, L, H](8 bytes) - Out: A, HL = BIOS return values
- Note: Calls BIOS function number
funcwith specified registers. - Note: F50 follows the public BIOS numbering. Function 30 (
USERF) is not supported through F50 becauseUSERFrequires an inlineDWafterCALL, which the 8-byte F50 parameter block cannot express.
- Out: A=0
- In: DE=address of 4-byte date/time:
[days_lo, days_hi, hour_BCD, min_BCD]
- Out: DMA gets 5 bytes:
[days_lo, days_hi, hour, min, sec] - Out: A=seconds, HL=day counter
- Out: DMA gets 6 bytes:
"CPM31\0"
- In: DE=0xFFFF → get; DE=value → set
- Out: HL=current return code (when getting)
- In: DE=0xFFFF → get; DE=value → set
- In: E=0xFF → get; E=char → set
- Note: Default delimiter is '$' (0x24).
- In: DE=CCB address
- CCB format:
[addr_lo, addr_hi, len_lo, len_hi] - Note: Outputs
lengthcharacters fromaddrto console.
Offset Size Field Description
------ ---- ------- -----------
0 1 DR Drive: 0=default, 1=A, 2=B
1-8 8 F1-F8 Filename (space-padded, ASCII uppercase)
9-11 3 T1-T3 Filetype (space-padded, ASCII uppercase)
T1 bit 7 = Read-Only attribute
T2 bit 7 = System attribute
T3 bit 7 = Archive attribute
12 1 EX Extent number (low)
13 1 S1 Reserved
14 1 S2 Extent number (high)
15 1 RC Record count in this extent (0-128)
16-31 16 AL0-ALF Allocation block numbers (disk block list)
32 1 CR Current record (sequential access, 0-127)
33 1 R0 Random record number (low byte)
34 1 R1 Random record number (mid byte)
35 1 R2 Random record number (high byte)
- Each extent holds up to 128 records (16K of data on 1K-block disks).
CRis the sequential record pointer within the current extent (0-127).RCis the number of records written in this extent (set by BDOS).- For random access, the 24-bit record number in R0/R1/R2 addresses any 128-byte record in the file.
- '?' (0x3F) in any name/type position matches any character.
- Used with F17 (Search First) and F19 (Delete File).
; Open a file
fcb: db 0 ; default drive
db 'HELLO ' ; filename (8 chars, space padded)
db 'TXT' ; extension (3 chars, space padded)
ds 24 ; rest zeroed
ld de, fcb
ld c, 15 ; open file
call 5
cp 0xFF
jr z, not_found
; Read a record
ld de, fcb
ld c, 20 ; read sequential
call 5
; data is now at DMA address (default 0080h)Use BDOS F50 (Direct BIOS Call) with an 8-byte parameter block:
bios_pb:
db func_num ; BIOS function number
db 0 ; A
db 0 ; C
db 0 ; B
db char ; E
dw 0 ; D, L (or DE as word)
dw 0 ; H (or HL as word)
ld de, bios_pb
ld c, 50
call 5Alternatively, get the BIOS base address from 0001h and call through the jump table directly:
ld a, (1) ; low byte of bios base (normally 0x??00)
; BIOS function N is at bios_base + N*3| Fn | Name | In | Out | Description |
|---|---|---|---|---|
| 0 | BOOT | — | — | Cold boot. Never called by programs. |
| 1 | WBOOT | — | — | Warm boot. Use JP 0000h instead. |
| 2 | CONST | — | A=0xFF/0x00 | Console input status. |
| 3 | CONIN | — | A=char | Console input (blocking). |
| 4 | CONOUT | C=char | — | Console output. |
| 5 | LIST | C=char | — | Printer output (stub). |
| 6 | AUXOUT | C=char | — | Aux output (stub). |
| 7 | AUXIN | — | A=0x1A | Aux input (stub, returns EOF). |
| 8 | HOME | — | — | Home disk to track 0. |
| 9 | SELDSK | C=drive, E=login | HL=DPH or 0 | Select disk drive (0=A, 1=B). |
| 10 | SETTRK | BC=track | — | Set track for next I/O. |
| 11 | SETSEC | BC=sector | — | Set sector for next I/O. |
| 12 | SETDMA | BC=address | — | Set DMA address for next I/O. |
| 13 | READ | — | A=0/1 | Read 128 bytes to DMA. |
| 14 | WRITE | C=type | A=0/1/2 | Write 128 bytes from DMA. |
| 15 | LISTST | — | A=0 | List device status (stub). |
| 16 | SECTRN | BC=sector, DE=table | HL=physical | Sector translate (no-op). |
| 17 | CONOST | — | A=0xFF | Console output status (always ready). |
| 18 | AUXIST | — | A=0 | Aux input status (stub). |
| 19 | AUXOST | — | A=0 | Aux output status (stub). |
| 20 | DEVTBL | — | HL=table | Character device table. |
| 21 | DEVINIT | — | — | Device init (stub). |
| 22 | DRVTBL | — | HL=table | Drive table (16 pointers). |
| 23 | MULTIO | A=count | — | Set multi-sector count. |
| 24 | FLUSH | — | A=0 | Flush disk buffers (stub). |
| 25 | MOVE | HL=dst,DE=src,BC=len | — | Block move (LDIR). |
| 26 | TIME | C=0/0xFF | — | Get/set time (stub). |
| 27 | BNKSEL | A=bank | — | Select bank (0=TPA, else system). |
| 28 | SETBNK | A=bank | — | Set DMA bank for next I/O. |
| 29 | XMOVE | B=dst,C=src | — | Set up cross-bank move. |
| 30 | USERF | inline DW | varies | XBIOS dispatch (Amstrad extensions). |
| 31 | — | — | — | Reserved (JP 0). |
| 32 | SCRMODE | A=32 or 51 | — | Set screen column count (clears screen). |
| Mode | Columns | Rows | Char Width | Font |
|---|---|---|---|---|
| 51-col | 51 | 24 | 5px | 5px non-byte-aligned |
| 32-col | 32 | 24 | 8px | 8px byte-aligned |
Switch modes: SCREEN 51 or SCREEN 32 from the CCP, or BIOS fn 32.
Default: 51 columns.
The current column count minus 1 is stored in the SCB at offset 0x1A
(conwidth).
| Code | Name | Effect |
|---|---|---|
| 0x07 | BEL | Ignored (no beep) |
| 0x08 | BS | Move cursor left, erase character. Wraps to previous line. |
| 0x0A | LF | Cursor down one row. Scrolls if at bottom. |
| 0x0D | CR | Cursor to column 0. |
| 0x1B | ESC | Start escape sequence. |
Send these via BDOS F2 (one character at a time).
| Sequence | Name | Effect |
|---|---|---|
ESC A |
CUU | Cursor up (stop at row 0) |
ESC B |
CUD | Cursor down (stop at row 23) |
ESC C |
CUF | Cursor right (stop at last column) |
ESC D |
CUB | Cursor left (stop at column 0) |
ESC H |
HOME | Cursor to (0, 0) |
ESC Y r c |
CUP | Absolute position. r = row+32, c = col+32. Row 0-23, col 0 to cols-1. |
ESC j |
SCP | Save cursor position |
ESC k |
RCP | Restore cursor position |
| Sequence | Name | Effect |
|---|---|---|
ESC E |
CEL | Clear entire screen (cursor stays) |
ESC J |
ED0 | Erase from cursor to end of screen |
ESC K |
EL0 | Erase from cursor to end of line |
ESC d |
ED1 | Erase from top of screen to cursor |
ESC l |
EL1 | Erase entire current line |
ESC o |
EL0d | Erase from start of line to cursor |
| Sequence | Name | Effect |
|---|---|---|
ESC I |
RI | Reverse index: cursor up; scroll down if at row 0 |
ESC L |
IL | Insert blank line at cursor row |
ESC M |
DL | Delete cursor row (pull up) |
ESC N |
DCH | Delete character at cursor (shift left) |
These are consumed without error but have no effect:
ESC 0 ESC 1 (status line), ESC e ESC f (cursor on/off),
ESC p ESC q (reverse), ESC r (underline), ESC v ESC w (wrap),
ESC x ESC y (80-col mode).
Sequences with a consumed parameter: ESC 2 p ESC 3 p ESC 4 p ESC 5 p
ESC b col ESC c col ESC X tr lc h w.
Row 0: [CS] z x c v
Row 1: a s d f g
Row 2: q w e r t
Row 3: 1 2 3 4 5
Row 4: 0 9 8 7 6
Row 5: p o i u y
Row 6: [Enter] l k j h
Row 7: [Space] [SS] m n b
| Mode | Keys | Characters |
|---|---|---|
| Unshifted | — | lowercase letters, digits, space, enter |
| Caps Shift | Caps Shift | UPPERCASE letters, BS (with 0 or 5) |
| Symbol Shift | Symbol Shift | punctuation: ! @ # $ % & * ( ) - = + etc. |
| Control | Caps+Symbol | Ctrl-A through Ctrl-Z (0x01-0x1A) |
| Ctrl-C | Caps+Sym+Space | 0x03 |
- Initial delay: ~500ms
- Repeat rate: ~17 chars/sec
; Print a string
print_msg:
ld de, msg
ld c, 9
call 5
ret
msg: db 'Hello, world!', 13, 10, '$'
; Print a single character
ld e, 'A'
ld c, 2
call 5
; Print a newline
print_crlf:
ld e, 13 ; CR
ld c, 2
call 5
ld e, 10 ; LF
ld c, 2
call 5
ret
; Read a line of input
input_buf:
db 80 ; max 80 characters
ds 81 ; len byte + 80 data bytes
ld de, input_buf
ld c, 10
call 5
; input_buf+1 = number of chars typed
; input_buf+2 = start of character data
; Check for keypress (non-blocking)
ld c, 11
call 5
cp 0xFF
jr z, key_available
; Direct console I/O — read without echo
ld e, 0xFF
ld c, 6
call 5
; A=character or 0 if none readySPT=36 BSH=3 BLM=7 EXM=0 DSM=174 DRM=63
AL0=C0h AL1=00h CKS=16 OFF=1 PSH=2 PHM=3
| Offset | Size | Field | Value | Meaning |
|---|---|---|---|---|
| 0 | 2 | SPT | 36 | Sectors per track (physical, 512-byte) |
| 2 | 1 | BSH | 3 | Block shift (log2 of block size - 7) |
| 3 | 1 | BLM | 7 | Block mask (2^BSH - 1) |
| 4 | 1 | EXM | 0 | Extent mask |
| 5 | 2 | DSM | 174 | Maximum block number (total blocks - 1) |
| 7 | 2 | DRM | 63 | Maximum directory entry number |
| 9 | 1 | AL0 | 0xC0 | Allocation vector byte 0 (first 2 blocks = directory) |
| 10 | 1 | AL1 | 0x00 | Allocation vector byte 1 |
| 11 | 2 | CKS | 16 | Directory checksum bytes |
| 13 | 2 | OFF | 1 | Reserved tracks (track 0 = boot) |
| 15 | 1 | PSH | 2 | Physical sector shift (log2 of phys/128) |
| 16 | 1 | PHM | 3 | Physical sector mask (2^PSH - 1) |
- Block size: 128 << BSH = 128 << 3 = 1024 bytes (1K)
- Records per block: BLM + 1 = 8
- Disk capacity: (DSM + 1) × block_size = 175 × 1K = 175K
- Directory entries: DRM + 1 = 64
- Directory blocks: 2 (from AL0=C0h = bits 7,6 set = blocks 0,1)
- Physical sector size: 128 << PSH = 128 << 2 = 512 bytes
- Records per physical sector: PHM + 1 = 4
The CCP puts everything after the command name into the buffer at 0080h:
; Get command tail
ld a, (0x80) ; length byte
or a
jr z, no_args ; no arguments
ld hl, 0x81 ; start of text
; process HL[0..length-1]; FCB at 005Ch has the parsed filename
; Copy "NAME.EXT" to a display buffer
print_fcb_name:
ld hl, 0x5C + 1 ; point to filename field
ld b, 8
call copy_spaces ; copy 8 chars of name
ld e, '.'
ld c, 2
call 5 ; print dot
ld hl, 0x5C + 9 ; point to extension field
ld b, 3
call copy_spaces ; copy 3 chars of ext
ret
copy_spaces:
ld e, (hl)
ld c, 2
call 5
inc hl
djnz copy_spaces
ret; List files matching pattern
search_fcb:
db 0 ; default drive
db '????????' ; wildcard name
db '???' ; wildcard extension (or 'COM' for .COM files)
ds 24 ; zeroed
ld de, search_fcb
ld c, 17 ; search first
call 5
cp 0xFF
jr z, no_files
show_entry:
; Directory data is at DMA (0080h)
; Entry at DMA + A*32
; Bytes 1-11 are the filename
; Print it...
ld de, search_fcb
ld c, 18 ; search next
call 5
cp 0xFF
jr nz, show_entryclear_screen:
ld e, 0x1B ; ESC
ld c, 2
call 5
ld e, 'E' ; ESC E = clear screen
ld c, 2
call 5
ret; Move cursor to row 5, column 10
cursor_xy:
ld e, 0x1B ; ESC
ld c, 2
call 5
ld e, 'Y' ; ESC Y = cursor position
ld c, 2
call 5
ld e, 5 + 32 ; row + 32 = 37
ld c, 2
call 5
ld e, 10 + 32 ; col + 32 = 42
ld c, 2
call 5
retcheck_free:
ld e, 0 ; default drive
ld c, 46 ; get free space
call 5
; DMA[0..2] = 24-bit LE free record count
; Each record = 128 bytes, 8 records per 1K block
ld hl, (0x80) ; low 16 bits
ld a, (0x82) ; high byte
; Divide HL by 8 to get free K (for 1K blocks)
srl a
rr h
srl a
rr h
srl a
rr h
; HL = free K
; Print HL as decimal...set_51col:
ld de, bios_pb
ld c, 50
call 5
ret
bios_pb:
db 32 ; BIOS fn 32 = SCRMODE
db 51 ; A = 51 columns
ds 6 ; remaining params zeroed ; Set return code to 1
ld de, 1
ld c, 108
call 5
; Exit to CCP
jp 0 ; warm boot; Convert HL to decimal ASCII at DE, return length in B
; Destroys AF, BC, HL
hl_to_dec:
ld b, 0
ld c, 10
.divide:
call div_hl_c ; HL = HL/10, A = HL%10
add '0'
push af
inc b
ld a, h
or l
jr nz, .divide
.store:
pop af
ld (de), a
inc de
djnz .store
ret
; Divide HL by C, quotient in HL, remainder in A
div_hl_c:
ld a, 0
ld b, 16
.shift:
sla l
rl h
rla
cp c
jr c, .skip
sub c
inc l
.skip:
djnz .shift
retBDOS Entry: CALL 5 (C=function, DE=param, returns A/HL)
Warm Boot: JP 0
BDOS Address: word at 0006h (also = top of TPA address)
Console:
F1 = read char (echo) F2 = write char
F6 = direct I/O F9 = print $-string
F10 = read line F11 = key status
Files:
F15 = open F16 = close F17 = search first
F18 = search next F19 = delete
F20 = read seq F21 = write seq
F22 = make F23 = rename
F33 = read rand F34 = write rand
F35 = file size F36 = set random
Disk:
F13 = reset F14 = select drive F25 = current drive
F26 = set DMA F31 = get DPB F46 = free space
Screen:
51x24 or 32x24 (BIOS fn 32 to switch)
ESC E = clear ESC H = home
ESC Y r c = position (+32 offset to both)
ESC J = erase to end of screen
ESC K = erase to end of line
FCB at 005Ch (36 bytes):
[DR, F1-F8, T1-T3, EX, S1, S2, RC, AL0-ALF, CR, R0-R2]
Command tail: length at 0080h, text at 0081h