A minimal TCP server written in Rust that serves CP/M disk image files over the network to an Arduino Giga R1 WiFi running CP/M 2.2 on a RetroShield Z80.
The server replaces the SD card used in the original Arduino Mega 2560 version of the project. The Arduino connects over WiFi and forwards Z80 BIOS disk I/O operations as TCP messages.
The RetroShield Z80 CP/M system needs disk I/O for booting (loading CPM.SYS) and for all file operations (DIR, loading programs, reading/writing data files). On the Mega 2560, this went through an SD card. On the Giga R1, the SD card is replaced by this server.
The protocol mirrors the Z80 BIOS I/O port commands exactly. The same command bytes the BIOS uses (0x01–0x08, 0x10–0x11) are sent directly over TCP. Block size is 128 bytes — one CP/M sector.
| Command | Byte | Payload | Response |
|---|---|---|---|
| OPEN_READ | 0x01 | filename\0 | status |
| CREATE | 0x02 | filename\0 | status |
| OPEN_APPEND | 0x03 | filename\0 | status |
| SEEK_START | 0x04 | (none) | status |
| CLOSE | 0x05 | (none) | status |
| DIR | 0x06 | (none) | status + listing\0 |
| OPEN_RW | 0x07 | filename\0 | status |
| SEEK | 0x08 | 3 bytes LE offset | status |
| READ_BLOCK | 0x10 | (none) | status + 128 bytes |
| WRITE_BLOCK | 0x11 | 128 bytes | status |
Status is a single byte: 0x00 = OK, 0x01 = error.
- Threaded connections — each client runs in its own thread; the accept loop never blocks
- Read/write timeouts — 30s mid-command, 300s idle; dead connections are dropped automatically
- SO_REUSEADDR — server can restart instantly without port conflicts
- Filename sanitization — rejects path traversal and special characters
- Session metrics — tracks sectors read/written, bytes transferred, command count, errors, and session duration
- Rust (stable toolchain)
The only dependency beyond std is socket2 for SO_REUSEADDR.
cargo build --releaseThe binary is at target/release/sector_server.
sector_server [directory] [port]- directory — path to a folder containing CP/M files (default: current directory)
- port — TCP port to listen on (default: 9000)
Example:
./target/release/sector_server ./cpm_files 9000The directory should contain:
| File | Description |
|---|---|
boot.bin |
Z80 boot loader (loaded first at address 0x0000) |
CPM.SYS |
CP/M system image — CCP + BDOS + BIOS |
A.DSK |
Drive A disk image (256 KB, 77 tracks, 26 sectors/track) |
B.DSK |
Drive B disk image (optional) |
C.DSK |
Drive C disk image (optional) |
D.DSK |
Drive D disk image (optional) |
When a client disconnects, the server prints session metrics:
Session Summary
---------------
Duration: 00:05:23
Commands: 847
Files opened: 12
Seeks: 89
Sectors read: 634
Sectors written: 42
Bytes read: 81,152 (79.2 KB)
Bytes written: 5,376 (5.2 KB)
Errors: 0
- 128 bytes per sector
- 26 sectors per track
- 77 tracks per disk
- 256,256 bytes per disk image (~250 KB)
- 2 reserved tracks for system area
- Up to 4 drives supported (A–D)
- retroshield-z80-cpm-giga — Arduino Giga R1 firmware (the client that connects to this server)
- retroshield-level-shifter-pcb — KiCad design files for the 3.3V-to-5V level converter shield
This project is documented in a three-part series on tinycomputers.io:
- My Experience Using Fiverr for Custom PCB Design: A $468 Arduino Giga Shield
- Porting CP/M to the Arduino Giga R1: When Level Converters Fight Back
- Playing Zork on a Real Z80: From CP/M Boot to the Great Underground Empire
BSD 3-Clause License. See LICENSE.
Alex Jokela — tinycomputers.io