CP/M execution, banked memory and PIO I/O¶
In Phases 9 and 10, the Z80 runs CP/M directly from SRAM; the Pico supervises boot and I/O, rather than emulating the CPU. The diagrams show the architecture; the memory, PIO and BIOS sections provide the implementation reference.
Building applications across two banks¶
Applications can use any compiler targeting the Z80 instruction set, provided its output and runtime are compatible with CP/M. Using both banks also requires the banked-memory interface: generating Z80 code alone does not provide cross-bank calls. DCC has specific compiler and linker support for building fixed-address bank-1 libraries, including generated typed APIs and host wrappers.
Split the application into a bank-0 .COM host for startup and user interaction, and a bank-1 .LIB service for processing and large, service-owned data. Build the library first, then the host with its generated wrappers and BANKSDK. Calls are synchronous and copy bounded arguments rather than sharing pointers; each bank has its own code, data, heap and stack budget. See LIBDEMO for a working example and the library specification for limits.
Two banks, one address window¶
Open or download the full-size memory-map SVG. Region heights are schematic. Addresses are hexadecimal and ranges inclusive unless labelled otherwise; # means active LOW.
The 128 KiB SRAM has two 64 KiB banks, each mapped to 0000h-FFFFh. PIO1 completes OUT 07h by sending D0 to A16: 0 selects bank 0, 1 selects bank 1. This switches code, data, stack and vectors together.
- Bank 0:
.COMprograms start at0100h; the TPA holds 58,624 bytes. - Bank 1: one fixed-address
.LIBuses0100h-C7FFh, 50,944 bytes.
Both budgets include image (code, DCC runtime and initialized data), BSS, malloc heap and call stack—not just free heap. BSS (Block Started by Symbol) holds zero-initialized globals/statics. For example, static unsigned char buffer[1024]; reserves 1,024 bytes; the library loader zeros them without storing those zeros in .LIB. heap_base follows the image and BSS.
The diagram uses 512-byte call stack examples:
| Region | Bank 0 TPA (inclusive) | Bank 1 library (inclusive) |
|---|---|---|
| Image + BSS | .COM: 0100h to heap_base - 1 |
.LIB: 0100h to heap_base - 1 |
malloc heap |
heap_base to E3FFh |
heap_base to C5FFh |
| Call stack (512 bytes) | E400h to E5FFh |
C600h to C7FFh |
Bank-0 placement is illustrative and application-specific. Bank 1's .LIB header sets a 256-2,048-byte stack in 256-byte units below C800h; its last heap byte is C800h - stack size - 1. Each program/library's linked image and BSS determine its own heap_base. The separate 5,632-byte pool at C800h-DDFFh uses checked handles; RESET does not unload the library or reset its malloc heap.
Why switching banks does not lose execution¶
The FF00h gateway and FB40h trampoline have matching code in both banks, so instruction fetching survives an A16 switch. Applications use checked BANKSDK transfers and generated wrappers, not cross-bank pointers or arbitrary bank toggles. See stack switch and return for saved state and library layout for image, BSS and stack budgets.
From a CP/M call to physical I/O¶
Open or download the full-size execution-flow SVG. Arrows show logical call/data flow, not PCB wires.
BDOS selects files; BIOS transfers records¶
- Bank-0
CALL 0005henters the BDOS shim atEE06h. - Console calls stay local; disk/file calls pass their request in registers and copy only File Control Blocks (FCBs) and directory-search records to and from bank 1, using the private
F600h-F7FFhstack. - Bank-1 BDOS selects files and records, then calls BIOS at
FC00h. For a bank-0 read or write, BIOS selects bank 0 around the block transfer so the record moves directly to or from the caller's DMA. Libraries reach BDOS through their own0005hvector. - BIOS emits port I/O; PIO1 completes the bus cycle, core 0 supplies device data, and core 1 handles queued storage/transport work.
Disk BIOS uses ports 10h-14h, computes track * 32 + sector, polls status and transfers 128 bytes with INIR/OTIR. Normal, directory and unallocated-write commands remain distinct; SETDMA sets a software buffer, not hardware DMA. Console I/O uses ports 00h-01h.
A-D are four 512 KiB onboard-flash images: 128 tracks x 32 records x 128 bytes each. System records and filesystem metadata reduce file capacity; no SD card is used.
Boot, warm boot and flash commits¶
- Cold boot: core 0 recovers the journal, validates the image, then injects instructions under slow clocks so the Z80 loads/verifies 64 KiB in bank 0 and 8,960 bytes in bank 1 (page zero plus
DE00h-FFFFh). Owners initialize0100h-DDFFhbefore use. PIO starts only after verification. - CP/M entry: BIOS installs
0000h(warm boot) and0005h(BDOS) vectors, then enters CCP atE600h; programs start at0100h. - Application entry: the banked CCP calls the launch trampoline at
FB68hbefore entering each.COMat0100h. Resident service E=10 reclaims the entire 5,632-byte shared pool, invalidates the previous library, clears its DMA/search context and initializes application-scoped generation/epoch state. Neither abandoned allocations nor exhausted counters carry into the next app, including successive SUBMIT commands and apps that exit withRET. - Warm boot: selects bank 0, flushes writes and reloads 44 system records from Drive A, restoring CCP and the bank-0 shim before the prompt.
- Flash commit: core 0 parks the Z80 with
BUSREQ#/BUSACK#, drains I/O and pauses the engine for core-1 programming, then rearms and returns the bus. The Pico never drives SRAM addresses or write strobes.
Deploy matching firmware, boot package and Drive A system records; rebuild .COM/.LIB clients when their ABI changes. Diagrams and host tests are not hardware timing qualification.
Maintained implementation¶
| Surface | Source |
|---|---|
| Memory layout and boot image | build_images.py, sram.c |
| BDOS forwarding and BIOS | bank0_bdos_shim.asm, z80_bios.asm |
| Mirrored gateway and library control | banked_transfer.asm, banked_extensions.asm |
| PIO timing and ordered FIFO dispatch | banked_io.pio, banked_pio.c |
| Disk geometry and backing storage | flash_layout.h, flash_backend.c |
CP/M memory map reference¶
Bank 0 (TPA excludes CCP; all resident workspace stays above it):
| Region | Address range | Role |
|---|---|---|
| Page zero | 0x0000-0x00FF |
CP/M vectors, default FCBs, DMA buffer, and command tail |
| Transient Program Area (TPA) | 0x0100-0xE5FF |
.COM image (code, DCC runtime, initialized data), BSS, malloc heap and call stack |
| CCP | 0xE600-0xEDFF |
Console Command Processor, reloaded after a warm boot |
| BDOS shim and tables | 0xEE00-0xFB3F |
Bank-0 console and disk/file proxy; entry at 0xEE06 |
| BDOS shim private stack | 0xF600-0xF7FF |
512-byte stack inside the bank-0 BDOS slot; protects the CCP code below its small stack |
| Mirrored trampoline | 0xFB40-0xFB7F |
Bank-1 warm-boot return and checked-transfer extent lookup (0xFB48) |
| Reserved BDOS padding | 0xFB80-0xFBFF |
128-byte BIOS directory buffer |
| BIOS core | 0xFC00-0xFEFD |
766-byte Z80-optimized boot, console, disk, translation and startup-report routines |
| Spare padding | 0xFEFE-0xFEFF |
2 bytes |
| BIOS bank extension | 0xFF00-0xFFFF |
READ/WRITE/INFO/HEAP entry points, mirrored code and private workspace |
CCP occupies 0800h bytes; the BDOS slot occupies 0E00h, including the final 128-byte directory buffer. bios.bin is 1,024 bytes: core, padding and mirrored extension. DCC locates BDOS through JP EE06h at 0005h but must stay below E600h. The shim's private stack protects CCP code during nested transfers.
Bank 1 (DE00h and above is reserved for system code):
| Bank-1 address | Role |
|---|---|
0000h–00FFh |
CP/M page zero |
0100h–(heap_base-1) |
Fixed .LIB image (code, DCC runtime, initialized data) and BSS |
heap_base–(C800h-stack-1) |
DCC library malloc heap, starting after linked BSS |
(C800h-stack)–C7FFh |
Pinned library stack; size comes from the .LIB header |
C800h–DDFFh |
General bmalloc pool, 5,632 bytes |
DE00h–E5FFh |
Resident loader, proxy and service workspace |
E600h–EDFFh |
Extent allocator metadata/code |
EE00h–FB3Fh |
Authoritative CP/M BDOS |
FB40h–FB7Fh |
Mirrored warm-boot return and extent lookup |
FB80h–FEFFh |
BIOS buffer and core |
FF00h–FFFFh |
Mirrored bank-switch gateway |
PIO I/O engine reference¶
Only IN/OUT reach the Pico; normal memory cycles access SRAM directly with the Pico data paths isolated. PIO owns all runtime bus timing.
PIO terminology¶
| Term | Meaning |
|---|---|
| PIO block | A programmable I/O peripheral. The RP2350 has three: PIO0, PIO1 and PIO2. Each has 32 words of instruction memory shared by its state machines. |
| State machine (SM) | A tiny independent processor inside a block (four per block). It executes one PIO instruction per system clock: 8.33 ns at the 120 MHz supervisor clock, so its timing is exact and repeatable. |
| Scratch and shift registers | Each SM has X and Y scratch registers, an output shift register (OSR) for data going to pins, and an input shift register (ISR) for data read from pins. |
| Pin mappings | Each SM is configured with windows of GPIOs: IN reads from them, OUT and SET drive them, side-set drives a pin in the same instruction as other work, and JMP PIN branches on one pin. |
| FIFOs | Two four-word queues per SM. The SM pushes to the RX FIFO for the ARM cores to read; the ARM cores put words in the TX FIFO for the SM to pull. Autopush pushes the ISR automatically when it is full. |
| IRQ flags | Eight flags per block that SMs set, clear and wait on. They can interrupt an ARM core, and on the RP2350 IRQ PREV sets a flag in the neighbouring (previous) block. |
The role of each PIO block¶
| Block | Program | Role |
|---|---|---|
| PIO0 | banked_clock, 4 words |
Generates the 20 MHz Z80 clock (3 ticks HIGH, 3 LOW) on GP2. It stops in a LOW phase whenever PIO0 IRQ flag 1 (I/O hold) or flag 2 (core-0 hold) is set. It shares PIO0 with the status OLED's I²C program. |
| PIO1 | banked_io, 29 words |
The I/O engine. Claimed exclusively; it recognises, completes and times every Z80 IN and OUT, and drives the data bus, buffer enables, WAIT release and SRAM bank select (A16). |
| PIO2 | (not used by the Z80) | Left for the CYW43 Wi-Fi driver, whose SPI bus claims the highest free PIO block. |
PIO1 holds PIO0's clock with IRQ PREV, without ARM timing. The DMA-fed OLED I2C state machine shares PIO0 but uses no IRQ flags.
PIO1 pin windows¶
| Mapping | GPIOs | Signals |
|---|---|---|
IN |
GP10 upwards | D0-D7, A0/A1/A2/A4, RD#, WR# |
OUT (10 pins) |
GP8-GP17 | A16 bank select, IO_RELEASE, D0-D7 |
SET (2 pins) |
GP6-GP7 | UP_OE# (Pico drives the Z80 bus), DOWN_OE# (Pico reads the Z80 bus) |
| Side-set | GP9 | IO_RELEASE: releases the hardware WAIT# |
JMP PIN |
GP27 | RD#: distinguishes IN from OUT |
HCT32 asserts WAIT# as soon as IORQ# falls, before PIO reacts.
How PIO data reaches core 0¶
PIO1 uses 32-bit FIFO words (bank-select OUTs bypass the FIFOs):
| Direction | Instruction | Word contents |
|---|---|---|
| PIO1 → core 0 (RX) | in pins, 32 with autopush |
A snapshot of the pins: data in bits 0-7, A0/A1/A2/A4 in bits 8-11, RD# in bit 17, WR# in bit 18 |
| Core 0 → PIO1 (TX) | pull, out pins, 10, out pindirs, 10 |
The reply for an IN: pin levels for GP8-GP17 (A16 unchanged, IO_RELEASE LOW, D0-D7) in bits 0-9, then the pin directions to use while driving (bits 10-19) and when idle (bits 20-29) |
The RX-not-empty IRQ runs core 0's SRAM-resident handler: apply OUTs in bus order, then reply to IN. Thus each IN observes all earlier OUTs.
An OUT cycle¶
- IORQ# falls; PIO sets the I/O hold so the clock stops at its next LOW phase, and
JMP PINsees RD# HIGH. - PIO enables the receive buffer (DOWN_OE#) and lets it settle for eight ticks.
- For port
07hit drives D0 onto A16, switching the SRAM bank. For any other port it autopushes the snapshot to core 0. - It releases WAIT# while CLK is still LOW, clears the hold so the clock runs freely, waits for IORQ# to rise, then isolates the buffer and rearms WAIT#.
Core 0 applies ordinary writes afterwards. A full four-word RX FIFO holds WAIT# rather than dropping a snapshot.
An IN cycle¶
sequenceDiagram
participant Z as Z80
participant P1 as PIO1 I/O engine
participant P0 as PIO0 clock
participant C0 as Core 0
Z->>P1: IORQ# + RD# LOW (WAIT# already LOW)
P1->>P0: IRQ PREV: hold CLK LOW
P1->>C0: Request snapshot (RX FIFO, IRQ)
C0->>P1: Reply byte + pin directions (TX FIFO)
P1->>Z: Drive D0-D7, enable UP_OE#, release WAIT#
P1->>P0: Run exactly two clocks
Z->>Z: TW sees WAIT# HIGH, T3 samples data
P1->>P1: IORQ# HIGH: isolate bus, data pins to inputs
P1->>P0: Release hold: next instruction fetch
After the reply, PIO runs exactly two clocks: wait release, then T3's falling-edge data sample. It holds CLK LOW and isolates the output buffer before resuming opcode fetching, preventing the Pico from driving the next SRAM read.
Division of work¶
| Owner | Responsibility |
|---|---|
| PIO0 + PIO1 | All Z80 bus timing: the 20 MHz clock, cycle detection, buffer enables, data drive and sampling, WAIT# release and A16 bank select |
| Core 0 | The Z80's supervisor and the device logic behind its I/O ports |
| Core 1 | USB, Wi-Fi and network services, the web terminal, flash programming and the disk read-ahead |
Core 0: PIO1 interrupt¶
Per-port device work (no USB, network or flash programming in the handler):
| Ports | Work |
|---|---|
00h-01h console |
Queue output to the shared ring for core 1 (USB or WebSocket), return input bytes, report status without taking the lock |
10h-14h disk |
Latch drive and LBA, run commands, serve reads from the SRAM read-ahead or flash, collect 128-byte write records and hand them to core 1 |
02h-04h service ABI |
Collect requests for core-1 workers and return their replies |
17h timers |
Countdown timers and stopwatches |
15h-16h HTTP |
Filename and chunk handshakes with the core-1 HTTP worker |
05h, 06h |
Boot markers and disk-diagnostic results |
07h (IN) |
SRAM bank status; OUT 07h completes entirely in PIO |
Core 0: main loop¶
- Services flash grants and bounded core-1 supervisor commands: load/verify, start, status, stop, reboot, fault arming and configuration/web reboots.
- Monitors boot markers, boot timeout and disk diagnostics; publishes CPU state.
- For orderly stop: flush, park, drain OUTs, check disk quiescence, then reset or resume.
Core 0: boot¶
See boot and flash sequencing. Slow stepped clocks are used for loading, not runtime I/O.
Why it is fast¶
PIO steps resolve at 8.33 ns with no software timing delays. Reported core-0 device work costs about 1 µs per IN and 1.2-1.3 µs per OUT, excluding IRQ entry/exit; the measured 20 MHz workload polls 225,000-266,000 INs/s. Queued OUTs and PIO-only bank switching avoid waiting for ARM work when FIFO space is available. Core-1 workers use bounded queues/mailboxes.
Ctrl-] s reports [pio] engine state, [iolat] handler cost and [disk] read-ahead hits. These measurements are workload-specific, not qualification of every network/storage combination.
Safety¶
- Invalid RD#/WR# combinations, a 500 ms stalled cycle or fatal storage error hold the CPU in reset.
- Bus-grant pull-ups keep IORQ#, RD# and WR# inactive; grant/stop paths drain queued I/O before relying on device state.
Device dispatch: io_trap.c. Wiring: PIO acceleration.
Custom BIOS reference¶
The standard CP/M 2.2 17-entry jump table also supports DCC direct-BIOS calls:
| CP/M BIOS operation | Board implementation |
|---|---|
BOOT |
Initialize CP/M and page-zero vectors; enter CCP |
WBOOT |
Flush writes, reload CCP/BDOS from Drive A, and enter CCP |
CONST |
Return 0xFF when terminal status port 0x01, bit 0, reports input ready |
CONIN |
Wait for input on port 0x00; return a 7-bit character |
CONOUT |
Wait for status bit 1 (transmit room); write C to port 0x00 |
READER |
Return text EOF (0x1A) |
LIST / PUNCH |
Discard output |
LISTST |
Return not ready |
HOME |
Set track to zero |
SELDSK |
Return the disk parameter header for A–D, or zero for an invalid drive |
SETTRK / SETSEC / SETDMA |
Store track, sector, and software buffer address; no hardware DMA |
READ / WRITE |
Transfer 128-byte records through ports 0x10–0x14, preserving CP/M write-type semantics |
SECTRAN |
Return the sector unchanged; images use linear sector order |
See the I/O port map for decoding and aliases.
Disk format¶
Drives A–D share one non-standard geometry, defined in
geometry.py.
The same 524,288-byte image is used in onboard flash, by the host emulator and
by cpmtools:
| Property | Value |
|---|---|
| Layout | 128 tracks x 32 records x 128 bytes, sequential, no skew |
| Reserved (system) tracks | 2 |
| Block size | 2 KiB (BSH=4, BLM=15) |
| Blocks | 252 (DSM=251), 502 KiB of files |
| Directory | 64 entries (DRM=63), one block (AL0=0x80) |
| Checksum vector | None (CKS=0, fixed disk) |
This is neither IBM 3740 SSSD nor the Altair 88-DCDD layout, so generic CP/M
disk images must be converted to this layout (for example by copying files
with cpmcp) before use. See the
disk media notes.
Banked-memory BIOS extension (ABI 1.0)¶
Check BMX1 at FF09h, major 1 at FF0Dh, minor 0 at FF0Eh, and INFO success before using the mirrored extension:
| Address | Entry | Contract |
|---|---|---|
FF00h |
READ | HL=bank-0 buffer, DE=bank-1 address, BC=length |
FF03h |
WRITE | HL=bank-0 buffer, DE=bank-1 address, BC=length |
FF06h |
INFO | HL=0100h, DE=C800h, BC=1600h on success |
FF10h |
Service | E selects the operation below |
- Raw READ/WRITE: length must be a multiple of 16; both exclusive range ends must be at or below
FF00h, without wrapping. - Byte helpers:
F800hREAD /F803hWRITE accept arbitrary lengths with the same HL/DE/BC convention andFF00hexclusive range limit, without wrapping. Positive transfers require a bank-0 address at least0100h; raw bank-1 addresses may include page zero. Zero count succeeds without dereferencing. Full-range checks precede copying; neighbouring bytes, IX/IY, SP and interrupt state are preserved. - Checked SDK transfers:
bread/bwriteuseF806h/F809h, adding A=handle page. Live pages areC8h–DDh(pool),01h(library) andC0h(stack). The mirroredFB48hlookup masks interrupts, inspects live extent length/allocation state in bank 1, and returns to bank 0 without stack access. No bounds are cached: invalid/freed handles return A=3/carry set; out-of-extent ranges return A=2/carry set. Valid requests continue to the byte helpers. - Status: copying succeeds with A=0/carry clear; invalid raw ranges return A=2/carry set. Raw resident-memory access is for system code, not applications.
The 237-byte gateway uses two bank-select OUTs per 16-byte burst and bookkeeping per 256 bytes; aligned counts bypass tail storage. A retains low count/direction (only OUT D0 selects the bank), source SP advances through private FFF0h, and carry preserves direction across the payload. INFO delegates to bank-0 FA00h. MEMPERF reports emulator cycles excluding hardware wait time.
Allocation and direct entry calls¶
| E | Operation | Arguments and result |
|---|---|---|
| 0 | ALLOC | HL=requested bytes; returns a 256-byte-aligned handle or zero |
| 1 | FREE | HL=handle; returns 1 on success |
| 2 | SIZE | HL=handle; returns rounded size or zero |
| 3 | AVAILABLE | Returns free bytes, initially 5,632 |
| 4 | RESET | Invalidates general-pool handles and restores the free extent |
| 5 | CALL | HL=live code handle, BC=distinct live stack handle, IX=entry offset, IY=context; returns code's HL |
| 6 | BDOS proxy | Resident bank-0 BDOS shim service: C=function, HL=argument, IY=DMA, B nonzero for a bank-0 record DMA; returns the BDOS HL |
| 7 | CAPABILITIES | Returns bank-1 address of a 48-byte versioned descriptor, or zero |
| 8 | LIB CONTROL | HL=discovered request address; returns status independently of results |
| 9 | LAST STATUS | Returns last resident-service status without clearing it |
| 10 | APPLICATION START | CCP-only lifecycle service: invalidates the previous module, reclaims the pool and begins a fresh application token namespace; returns 1 |
SDK initialization only validates capabilities. It must not invoke APPLICATION START or reset another API's live allocations. Application state does not survive a new CCP transient launch; explicit RESET within an app affects only pool allocations and preserves the loaded module. Direct program chaining that bypasses the CCP must manage its own cleanup.
Capability and library control records¶
After INFO, bank_bios_query() discovers this 24-word little-endian descriptor; clients must not inspect private tables:
| Word | Meaning |
|---|---|
| 0-3 | Size (48), descriptor revision (1), ABI (0100h), feature mask (003Fh) |
| 4-7 | Pool start, capacity, allocation unit, raw burst size |
| 8-11 | Byte READ/WRITE entries, library request address and size |
| 12-15 | Export table address, image origin, pinned code and stack handles |
| 16-19 | Call-token address, maximum body bytes, exports and signature characters |
| 20-23 | Call revision (1), checked READ/WRITE entries, one reserved zero word |
Feature bits 0-5 advertise byte transfers, library control, detailed status, DMA/search contexts, generation/epoch validation and checked transfers. bank_bios_library_control() copies this request to the discovered address, dispatches operation 8 and retrieves the response (16-bit words). bank_bios_library_invoke() does the same for CALL but takes only the status the gateway returns, because CALL results come back in the frame:
| Byte offset | Meaning |
|---|---|
| 0, 2 | Request size (32), revision (1) |
| 4, 6 | Action (0 LOAD, 1 INVALIDATE, 2 QUERY, 3 CALL), output status |
| 8, 10 | Expected/current module generation, zero-based export index |
| 12, 14 | Live FCB staging/frame handle, per-call token |
| 16, 18, 20 | Ready flag, export count, entry size |
| 22, 24, 26, 28 | Image bytes, export table address, image origin, stack handle |
| 30 | Expected/current pool epoch |
- LOAD: copies a padded 48-byte FCB from a live pool allocation.
- QUERY: returns descriptors without changing ownership.
- CALL: requires a live 512-byte frame extent, a body-length prefix in 1..256, a typed export and matching module generation/pool epoch.
- INVALIDATE: makes the module uncallable even at generation exhaustion. Generations/epochs never wrap within an application; LOAD/RESET returns EXHAUSTED rather than reviving stale state. The next CCP application launch begins a new namespace after discarding all prior application state.
BIOS status codes are:
| Code | Status | Meaning |
|---|---|---|
| 0 | OK | The BIOS operation succeeded; an invoked function's result is separate. |
| 1 | UNAVAILABLE | The banked-memory capability or matching resident service is unavailable. |
| 2 | ARGUMENT | An argument, range, length, request field or operation is invalid. |
| 3 | HANDLE | A required allocation handle is not live or does not meet the operation's requirements. |
| 4 | MEMORY | No free extent can satisfy the allocation request. |
| 5 | PINNED | An attempt was made to free a pinned library or stack extent. |
| 6 | DISPATCH | A typed library adapter did not return its required success value of 1. |
| 7 | FORMAT | The library header is invalid. |
| 8 | IO | A library load encountered a file open or read failure. |
| 9 | STALE | The module is not ready, or the expected module generation or pool epoch does not match. |
| 10 | EXHAUSTED | A non-wrapping module-generation or pool-epoch counter cannot advance. |
Function results (including HL=0) are separate from transport status. bank_memory_last_error() and bank_bios_last_error() use the codes above; bank_library_last_error() uses separate BL_* codes in BANKLIB.H; for example, BL_IO is 2 and BL_STALE is 12.
bmalloc splits free extents; bfree coalesces adjacent ones. Four 32-byte metadata tables in pinned E600h–EDFFh leave the full 5,632-byte pool available; handles never point at metadata. SIZE/AVAILABLE inspect live tables, with SIZE validating allocation and length on every call. RESET preserves pinned library, stack, loader and allocator, without reducing the library budget.
Stack switch and return¶
- Gateway saves bank-0 interrupt state, selects bank 1, and resident code saves caller SP before using its private service stack.
- CALL validates distinct live code/stack handles and entry offset, rejects
BC=0, then switches SP to the stack's exclusive end. Pinned library/stack handles are0100h/C000h; an entry offset is not checked as an instruction boundary. IY becomes a 16-bit DCC argument atIX+4after the usual prologue. RETrestores caller SP, bank 0, IX/IY and interrupt state; HL is the result.
Bank-1 code never dereferences the bank-0 stack. Calls are synchronous and non-reentrant; nested gateway calls, interrupt handlers and NMI during switching are unsupported.
Host/library DMA and search continuations are separate; drive/user and disk allocation state are shared. Each private snapshot is nine bytes: DMA plus seven search bytes (FB14h, FB18h–FB1Ah, FB29h–FB2Bh), matching the host proxy layout. Shared geometry scratch is not restored; Search Next refreshes the shared directory buffer with an extra read.
Bank-1 CP/M and C library¶
The loader validates the .LIB header, loads at 0100h, clears BSS, initializes heap bounds from linked __hstart and the stack budget, then exposes exports. Libraries use their local BDOS vector for console/file I/O.
Generated BANKLIB wrappers copy bounded arguments into a length-prefixed frame with bank_bios_write(), invoke LIB CONTROL CALL (E=8, action 3), then retrieve results with bank_bios_read(). Copied pointers are local to the frame, never bank-0 pointers. Typed-export, live-frame and generation/epoch checks precede dispatch; direct CALL does not replace them.
LIBDEMO builds DATATEST.COM and DATA.LIB, with DCC-generated API/wrappers. It tests scalars, buffers, records, files, bank-owned blocks and lifecycle; HOSTCHK.C provides setup/reporting/cleanup. See the library specification for limits.
Bank-0 host: LIBDEMO/host/DATATEST.C
#include <stdio.h>
#include <string.h>
#include "API.H"
#include "BANKLIB.H"
#include "BIOSBANK.H"
#include "HOSTCHK.H"
static unsigned int previous_handle;
static unsigned int check_values(void)
{
unsigned int left = 120, right = 23, result;
result = add16(left, right);
if (result != 143 || bank_library_last_error() != BL_OK || left != 120 || right != 23)
return 0;
printf("add16(120, 23) = %u; caller values unchanged.\n", result);
return add16(65535U, 1) == 0 && bank_library_last_error() == BL_OK;
}
static unsigned int check_text(void)
{
unsigned char text[128];
unsigned int i;
memset(text, 0xA5, sizeof(text));
memcpy(text, "Bank 1", 7);
if (upper(text, 6) != 6 || bank_library_last_error() != BL_OK ||
memcmp(text, "BANK 1", 7))
return 0;
for (i = 7; i < sizeof(text); ++i)
if (text[i] != 0xA5)
return 0;
printf("upper(\"Bank 1\") = \"%s\".\n", text);
memset(text, 'a', sizeof(text));
if (upper(text, sizeof(text)) != sizeof(text) || bank_library_last_error() != BL_OK)
return 0;
for (i = 0; i < sizeof(text); ++i)
if (text[i] != 'A')
return 0;
if (upper(text, 129) != 0 || bank_library_last_error() != BL_BOUNDS)
return 0;
for (i = 0; i < sizeof(text); ++i)
if (text[i] != 'A')
return 0;
return upper(0, 1) == 0 && bank_library_last_error() == BL_ARGUMENT;
}
static unsigned int check_file(void)
{
unsigned char data[130], first;
unsigned int i, checksum = 0;
memset(data, 0xA5, sizeof(data));
if (filerd(data + 1, 128) != 128 || bank_library_last_error() != BL_OK ||
data[0] != 0xA5 || data[129] != 0xA5)
return 0;
first = data[1];
for (i = 1; i <= 128; ++i)
checksum += data[i];
printf("Read 128 bytes from A:AUTORUN.COM in bank 1; byte sum %u.\n",
checksum);
return filerd(data + 1, 1) == 1 && bank_library_last_error() == BL_OK &&
data[1] == first && data[0] == 0xA5 && data[129] == 0xA5;
}
static unsigned int check_exports(void)
{
unsigned int i, j, found;
unsigned char frame[19];
char name[9], signature[BL_SIGNATURE_MAX + 1];
if (bank_library_export_count() != 8)
return 0;
found = 0;
for (i = 0; i < bank_library_export_count(); ++i)
{
if (!bank_library_export_name(i, name) || !bank_library_export_signature(i, signature, sizeof(signature)))
return 0;
printf("Export %s: %s\n", name, signature);
if (!strcmp(name, "ADD16"))
{
found = 1;
memset(frame, 0xA5, sizeof(frame));
if (bank_library_call(name, "void()", frame, sizeof(frame)) ||
bank_library_last_error() != BL_SIGNATURE)
return 0;
if (bank_library_call(name, signature, frame, 1) ||
bank_library_last_error() != BL_DISPATCH)
return 0;
if (bank_library_call(name, signature, frame, BL_FRAME_MAX + 1) ||
bank_library_last_error() != BL_BOUNDS)
return 0;
for (j = 0; j < sizeof(frame); ++j)
if (frame[j] != 0xA5)
return 0;
}
if (!strcmp(name, "BLKFREE"))
{
char *field = strstr(signature, "state:i8@8");
if (!field)
return 0;
memset(frame, 0xA5, sizeof(frame));
*field = 'x';
if (bank_library_call(name, signature, frame, sizeof(frame)) ||
bank_library_last_error() != BL_SIGNATURE)
return 0;
*field = 's';
if (bank_library_call(name, signature, frame, sizeof(frame) - 1) ||
bank_library_last_error() != BL_DISPATCH)
return 0;
for (j = 0; j < sizeof(frame); ++j)
if (frame[j] != 0xA5)
return 0;
}
}
return found;
}
static unsigned int create_block(unsigned int seed, struct BlockInfo *info)
{
struct BlockSpec spec;
unsigned char saved[sizeof(spec)];
unsigned int handle;
spec.seed = seed;
memcpy(spec.label, "BANKDATA", sizeof(spec.label));
memcpy(saved, &spec, sizeof(spec));
handle = blknew(&spec, info);
if (memcmp(saved, &spec, sizeof(spec)) ||
bank_library_last_error() != BL_OK || !handle ||
info->handle != handle || info->size != 4096 ||
info->byte_sum != 522240UL || info->state != 1 ||
memcmp(info->label, "BANKDATA", sizeof(info->label)))
return 0;
return handle;
}
static unsigned int check_blocks(void)
{
unsigned int handle, replacement, i;
unsigned char data[130];
struct {
unsigned char before;
struct BlockInfo info;
unsigned char after;
} guarded;
struct BlockInfo other;
struct BlockSpec spec;
memset(&guarded, 0xA5, sizeof(guarded));
spec.seed = 91;
memcpy(spec.label, "BANKDATA", sizeof(spec.label));
if (blknew(0, &guarded.info) || bank_library_last_error() != BL_ARGUMENT ||
blknew(&spec, 0) || bank_library_last_error() != BL_ARGUMENT ||
blknew((const struct BlockSpec *)&guarded.info, &guarded.info) ||
bank_library_last_error() != BL_ARGUMENT ||
blkfree(0) || bank_library_last_error() != BL_ARGUMENT)
return 0;
for (i = 0; i < sizeof(guarded); ++i)
if (((unsigned char *)&guarded)[i] != 0xA5)
return 0;
handle = create_block(37, &guarded.info);
if (!handle || bank_library_last_error() != BL_OK)
return 0;
memset(&other, 0xA5, sizeof(other));
if (blknew(&spec, &other) != 0 || bank_library_last_error() != BL_OK)
return 0;
for (i = 0; i < sizeof(other); ++i)
if (((unsigned char *)&other)[i] != 0)
return 0;
memset(data, 0xA5, sizeof(data));
if (blkread(handle, 1023, data + 1, 128) != 128 ||
bank_library_last_error() != BL_OK || data[0] != 0xA5 || data[129] != 0xA5)
return 0;
for (i = 0; i < 128; ++i)
if (data[i + 1] != (unsigned char)(37 + 1023 + i))
return 0;
puts("Read a 128-byte slice of a 4096-byte bank-owned block.");
memset(data, 0xA5, sizeof(data));
if (blkread(handle, 0, data + 1, 129) || bank_library_last_error() != BL_BOUNDS)
return 0;
for (i = 0; i < sizeof(data); ++i)
if (data[i] != 0xA5)
return 0;
if (blkread(handle, 4095, data + 1, 2) != 0 || bank_library_last_error() != BL_OK ||
blkread(handle, 4096, data + 1, 1) != 0 || bank_library_last_error() != BL_OK ||
blkread(handle, 65535U, data + 1, 2) != 0 || bank_library_last_error() != BL_OK ||
blkread(0, 0, data + 1, 1) != 0 || bank_library_last_error() != BL_OK ||
!blkfree(&guarded.info) || bank_library_last_error() != BL_OK ||
blkread(handle, 0, data + 1, 1) != 0 || bank_library_last_error() != BL_OK ||
blkfree(&guarded.info) != 0 || bank_library_last_error() != BL_OK ||
guarded.info.handle || guarded.info.size || guarded.info.byte_sum ||
guarded.info.state != -1 ||
memcmp(guarded.info.label, "BANKDATA", sizeof(guarded.info.label)) ||
guarded.before != 0xA5 || guarded.after != 0xA5)
return 0;
puts("PASS: flat record in/out/inout, 32-bit values, arrays and guards.");
replacement = create_block(91, &guarded.info);
if (!replacement || replacement == handle || bank_library_last_error() != BL_OK ||
blkread(handle, 0, data + 1, 1) != 0 || bank_library_last_error() != BL_OK ||
blkread(replacement, 0, data + 1, 1) != 1 || bank_library_last_error() != BL_OK ||
data[1] != 91)
return 0;
/* Unloading, rather than blkfree, must retire this live handle. */
previous_handle = replacement;
return 1;
}
static unsigned int check_reload(void)
{
unsigned int handle, result, cleanup;
unsigned char data[1];
struct BlockInfo info;
if (!hsopen())
return 0;
handle = create_block(12, &info);
result = handle && handle != previous_handle && bank_library_last_error() == BL_OK;
if (result)
result = blkread(previous_handle, 0, data, 1) == 0 &&
bank_library_last_error() == BL_OK &&
blkread(handle, 4095, data, 1) == 1 &&
bank_library_last_error() == BL_OK && data[0] == (unsigned char)(12 + 4095);
/* Leave this block open to exercise the unload hook's cleanup. */
cleanup = hsclos();
return result && cleanup;
}
/* A pool reset hands the library's call frame to the next allocation. The
* following call and close must report BL_STALE and leave that block alone. */
static unsigned int check_pool_reset(void)
{
unsigned char pattern[16], data[16];
unsigned int frame, block, result;
frame = bank_bios_allocate(512);
if (!frame || !bank_bios_free(frame) || !bank_library_open("DATA.LIB"))
return 0;
result = add16(1, 2) == 3 && bank_library_last_error() == BL_OK;
memset(pattern, 0x5A, sizeof(pattern));
block = bank_bios_reset() ? bank_bios_allocate(512) : 0;
result = result && block == frame &&
bank_bios_write(block, pattern, sizeof(pattern)) &&
add16(1, 2) == 0 && bank_library_last_error() == BL_STALE &&
bank_bios_read(block, data, sizeof(data)) &&
!memcmp(data, pattern, sizeof(pattern)) &&
!bank_library_close() && bank_library_last_error() == BL_STALE &&
bank_bios_allocation_size(block) == 512;
return bank_bios_free(block) && result;
}
int main(void)
{
unsigned int step, loaded;
puts("DATATEST: ordinary C calls, separate banks.");
puts("[1/8] Load DATA.LIB and run onload");
loaded = hsopen();
step = 1;
if (!loaded)
goto failed;
puts("PASS: load and lifecycle.");
step = 2;
puts("[2/8] Integer arguments and results");
if (!check_values())
goto failed;
puts("PASS: scalar values and 16-bit wraparound.");
step = 3;
puts("[3/8] Counted text buffer (inout)");
if (!check_text())
goto failed;
puts("PASS: text copy-back, limits and null-pointer rejection.");
step = 4;
puts("[4/8] File I/O in bank 1 (out)");
if (!check_file())
goto failed;
puts("PASS: file read, reopen and buffer guards.");
step = 5;
puts("[5/8] Export discovery and rejected calls");
if (!check_exports())
goto failed;
puts("PASS: eight exports, signatures and malformed-frame rejection.");
step = 6;
puts("[6/8] Flat records, bank-owned blocks and opaque handles");
if (!check_blocks())
goto failed;
puts("PASS: chunk data, bounds, stale handles and double-free rejection.");
step = 7;
puts("[7/8] Unload and check shared runtime state");
loaded = 0;
if (!hsclos())
goto failed;
puts("PASS: directory search preserved and transfer memory recovered.");
step = 8;
puts("[8/8] Reload, reject old handles, clean up an open block, reset the pool");
if (!check_reload() || !check_pool_reset())
goto failed;
puts("PASS: reload isolation, unload cleanup and pool-reset rejection.");
puts("PASS: all 8 bank-1 library checks.");
return 0;
failed:
printf("FAIL: step %u (transport error %u).\n", step, bank_library_last_error());
if (loaded && !hsclos())
puts("FAIL: library cleanup.");
return 1;
}
Bank-1 library: LIBDEMO/lib/DATA.C
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "BANKLIB.H"
struct BlockSpec {
unsigned int seed;
unsigned char label[8];
};
struct BlockInfo {
unsigned int handle;
unsigned int size;
unsigned long byte_sum;
signed char state;
unsigned char label[8];
};
#pragma dcc bank_buffer upper 1 inout 2 128
#pragma dcc bank_buffer filerd 1 out 2 128
#pragma dcc bank_buffer blkread 3 out 4 128
#pragma dcc bank_record blknew 1 in
#pragma dcc bank_record blknew 2 out
#pragma dcc bank_record blkfree 1 inout
static unsigned char *block_data;
static unsigned int block_handle;
#define BLOCK_BYTES 4096
static void library_report_memory(void)
{
meminfo_t info;
meminfo(&info);
printf("Bank 1 DCC heap: %u bytes capacity, %u committed, %u growth; stack: %u bytes\n",
info.heap_capacity, info.heap_committed, info.heap_growth, info.stack_size);
}
unsigned int onload(void)
{
library_report_memory();
return 1;
}
unsigned int onunload(void)
{
meminfo_t info;
if (block_data)
{
free(block_data);
block_data = 0;
block_handle = 0;
}
meminfo(&info);
if (info.heap_committed)
{
printf("Bank 1 cleanup left %u committed heap bytes.\n", info.heap_committed);
return 0;
}
return 1;
}
unsigned int add16(unsigned int left, unsigned int right)
{
return left + right;
}
unsigned int upper(unsigned char *data, unsigned int count)
{
unsigned int i;
if (!data || !count || count > 128)
return 0;
for (i = 0; i < count; ++i)
data[i] = (unsigned char)toupper(data[i]);
return count;
}
unsigned int filerd(unsigned char *data, unsigned int count)
{
FILE *file;
unsigned int result, failed;
if (!data || !count || count > 128)
return 0;
file = fopen("A:AUTORUN.COM", "rb");
if (!file)
return 0;
result = fread(data, 1, count, file);
failed = ferror(file);
if (fclose(file) || failed)
return 0;
return result;
}
unsigned int blknew(const struct BlockSpec *spec, struct BlockInfo *info)
{
unsigned int i;
if (block_data)
return 0;
block_data = (unsigned char *)malloc(BLOCK_BYTES);
if (!block_data)
return 0;
for (i = 0; i < BLOCK_BYTES; ++i)
block_data[i] = (unsigned char)(spec->seed + i);
/* A runtime token is a handle, never a bank-1 pointer. */
block_handle = *(unsigned int *)BANK_LIBRARY_CALL_TOKEN_ADDRESS;
info->handle = block_handle;
info->size = BLOCK_BYTES;
/* Each full 256-byte cycle contains all byte values, regardless of seed. */
info->byte_sum = (BLOCK_BYTES / 256UL) * 32640UL;
info->state = 1;
memcpy(info->label, spec->label, sizeof(info->label));
return block_handle;
}
unsigned int blkread(unsigned int handle, unsigned int offset,
unsigned char *data, unsigned int count)
{
if (!handle || handle != block_handle || !block_data ||
!data || !count || count > 128 || offset >= BLOCK_BYTES ||
count > BLOCK_BYTES - offset)
return 0;
memcpy(data, block_data + offset, count);
return count;
}
unsigned int blkfree(struct BlockInfo *info)
{
if (!info->handle || info->handle != block_handle || !block_data)
return 0;
free(block_data);
block_data = 0;
block_handle = 0;
info->handle = 0;
info->size = 0;
info->byte_sum = 0;
info->state = -1;
return 1;
}
Boot and deployment¶
Libraries use format 1, ABI 1.0 and call revision 1. Follow the boot/deployment rules when updating resident code or clients.