Banked memory support¶
This appendix covers DCC's optional fixed-address bank-1 libraries for the
Z80 ROM-less SBC. These features
depend on that project's banked SRAM, resident loader and BANKSDK runtime;
they are not part of standard CP/M 2.2 and are not needed for ordinary
single-bank .COM applications. Generating Z80 code alone does not provide
cross-bank calls.
Use 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.
The memory layouts and diagrams below are adapted from the SBC project's CP/M execution reference. The SVG assets are included locally so this appendix can be viewed without loading images from another site. Consult the upstream reference and bank-1 library specification for the current firmware, loader and hardware details. For ordinary builds, see Building and linking.
Two banks, one address window¶
Region heights are schematic. Addresses are hexadecimal and ranges inclusive
unless labelled otherwise; # means active LOW.
The SBC's 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 the 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. Each bank's heap_base
follows its own linked 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. 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. The resident gateway saves and restores the caller's
bank and stack. See the SBC's
stack switch and return reference
for the saved state and call sequence.
CP/M memory map reference¶
These addresses describe the SBC's banked CP/M implementation, not a universal CP/M memory map. Bank 0's TPA excludes the CCP; all resident workspace stays above it:
| Region | Address range | Role |
|---|---|---|
| Page zero | 0000h-00FFh |
CP/M vectors, default FCBs, DMA buffer and command tail |
| Transient Program Area (TPA) | 0100h-E5FFh |
.COM image, BSS, malloc heap and call stack |
| CCP | E600h-EDFFh |
Console Command Processor, reloaded after a warm boot |
| BDOS shim and tables | EE00h-FB3Fh |
Bank-0 console and disk/file proxy; entry at EE06h |
| BDOS shim private stack | F600h-F7FFh |
512-byte stack inside the BDOS slot |
| Mirrored trampoline | FB40h-FB7Fh |
Bank-1 warm-boot return and checked-transfer extent lookup |
| Reserved BDOS padding | FB80h-FBFFh |
128-byte BIOS directory buffer |
| BIOS core | FC00h-FEFDh |
Boot, console, disk, translation and startup-report routines |
| Spare padding | FEFEh-FEFFh |
2 bytes |
| BIOS bank extension | FF00h-FFFFh |
Mirrored bank-switch code and private workspace |
Bank 1 reserves DE00h and above for system code:
| Bank-1 address | Role |
|---|---|
0000h-00FFh |
CP/M page zero |
0100h-(heap_base - 1) |
Fixed .LIB image and BSS |
heap_base-(C800h - stack - 1) |
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 |
From a CP/M call to physical I/O¶
Arrows show logical call/data flow, not PCB wires.
- Bank-0
CALL 0005henters the BDOS shim atEE06h. - Console calls stay local; disk/file calls copy File Control Blocks (FCBs)
and software DMA buffers to bank 1 using the private
F600h-F7FFhstack. - Bank-1 BDOS selects files and records, then calls BIOS at
FC00h. Libraries reach it 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.
The Z80 executes CP/M directly from SRAM; the Pico supervises boot and I/O rather than emulating the CPU. See the SBC's PIO I/O engine reference for hardware-specific timing and device details.
Composite projects¶
A parent dccmake.txt can build an ordered collection of child projects:
# LIBDEMO/dccmake.txt
dcc-projects=lib,host
Run dccmake in the parent directory. Each listed directory must contain
its own dccmake.txt; paths resolve relative to the containing parent.
The library builds first and generates the typed host wrappers before the
host compiles. Each child retains its own inputs, output format, tools,
and build directory. Nested composite projects are supported, with cycle
detection and a maximum of 16 composite levels. A failed child stops the
remaining builds and makes the root invocation fail.
Parent configurations contain only dcc-projects and an optional
dcc-library-linker. Keep compilation settings in children; common
command-line overrides are forwarded to each child. dcc-input and
dcc-output cannot be overridden on a composite invocation.
dccmake dcc-library-linker=/path/to/dcc/l80c
The library-specific linker override applies only to LIB children, leaving COM children on their ordinary linker. It can also be used for a standalone library build. A composite builds outputs in their child build directories; it does not install files or make disk updates transactional.
Fixed-address bank-1 libraries¶
dcc-output-format=lib builds a fixed-address CP/M bank-1 library, not a
LINK-80 object archive. DCC's native l80c implements this format through
its --lib mode; emulated L80.COM does not.
Keep the host and library in separate directories, each with a
dccmake.txt and build/ directory. For example, the library configuration
can contain:
dcc-input=LIB.C
dcc-output=SPREADS
dcc-output-format=lib
dcc-exports=auto
dcc-library-api=auto
dcc-library-host-dir=../host/generated
dcc-stack-bytes=512
dcc-runtime=${DCC_DIR}/DCCRTL.MAC
dcc-include-directory=${DCC_DIR}
l80c-tool=${DCC_DIR}/l80c
Run dccmake in that directory to produce build/SPREADS.LIB and
build/SPREADS.SYM. Configure compiler/assembler tools as for ordinary
application builds. The host's own configuration keeps the default com
format and uses the same native linker without --lib.
The library pipeline uses DCC's linked BSS/heap symbols, omits transient
startup, and leaves heap initialization to the matching bank-1 loader.
Library output requires dcc-exports=auto: the 1–8 exports are the public C
definitions discovered by the compiler, and each one is linked with its
canonical signature (see Automatic typed libraries).
Export names are at most eight characters. All application functions are
retained so externally called exports cannot be stripped as unreachable.
Runtime stripping and optional peephole optimization still apply.
Library stacks must be 256–2,048 bytes in 256-byte units. If DCC raises the
first translation unit's stack requirement, dccmake rejects the mismatch
instead of publishing a smaller stack in the header; increase the configured
reserve to an aligned value. Choose sufficient stack space for helper
modules and the complete library call chain as well.
The linked image, BSS and at least one heap byte must end below the reserved
stack, which ends at C7FFh. With the default 2 KiB stack an image without
BSS may therefore extend through BFFEh. Native l80c enforces the same
image, heap and stack limits as the resident bank-1 loader.
The linker records the heap base after the linked image/BSS, and the loader
bounds the heap below the reserved stack. Library main, process
exit/chaining, and re-entry into the bank-switch gateway are not supported.
The .LIB file (library format version 1) is a header, the export table,
then the image bytes, padded to a 128-byte record multiple. All multi-byte
fields are little-endian:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Magic B1LB |
| 4 | 1 | Library format version, 1 |
| 5 | 1 | ABI major, 1 |
| 6 | 1 | ABI minor, 0 |
| 7 | 1 | Call revision, 1 |
| 8 | 2 | Origin, 0100h |
| 10 | 2 | Image length |
| 12 | 2 | Export count, 1–8 |
| 14 | 2 | Reserved, zero |
| 16 | 2 | Stack bytes |
| 18 | 2 | Header length, 26 + 12 × exports |
| 20 | 2 | __brk image offset, or FFFFh |
| 22 | 2 | __hlimit image offset, or FFFFh |
| 24 | 2 | Absolute heap base |
Each 12-byte export entry holds a zero-padded 8-byte name, the adapter's image-relative offset, and the image-relative offset of its NUL-terminated canonical signature string. Every export has a signature.
Automatic typed libraries¶
Library interfaces are generated from compiler type information; there is no export list or separate interface manifest to maintain:
# lib/dccmake.txt (run dccmake here before building the host)
dcc-input=DATA.C
dcc-output=DATA
dcc-output-format=lib
dcc-exports=auto
dcc-library-api=auto
dcc-library-host-dir=../host/generated
dcc-stack-bytes=512
dcc-runtime=${DCC_DIR}/DCCRTL.MAC
dcc-include-directory=${DCC_DIR}
l80c-tool=${DCC_DIR}/l80c
With dcc-library-api=auto, library source is the single source of truth.
DCC compiles every configured library source with its normal include paths,
defines and compiler settings, emitting resolved type metadata with -bank-api.
It generates host/generated/API.H, retaining parameter names and resolving
typedefs to primitive C types, plus the client wrappers. Do not include this
generated header from library source: generation must work without prior
outputs. No source-text C parser or Python generator is used.
Every non-static definition must have a supported signature; public helpers
must either join the API or become static. Unsupported definitions fail
instead of disappearing.
Alternatively, dcc-library-api=../API.H selects a handwritten header: the
library and host share that header, and every public definition must match a
declaration there. The header is validated, never overwritten. Exports are
still discovered from definitions and linked with generated signatures.
Assembly definitions, unresolved prototypes, public data, static helpers,
and functions owned by the separately linked runtime are not auto exports.
There must be 1–8 discovered functions, and names are uppercased for lookup
and limited to eight characters. M80 external-name collisions and reserved
generated symbol names are rejected.
The build produces:
lib/build/B1ADAPT.C: bank-1 dispatch adapters and canonical signature strings.host/generated/API.H: public prototypes and buffer annotations in auto mode.host/generated/B1HOST.C: client implementations of the public C API.lib/build/B1M01.APIand later: resolved source metadata, including definition parameter names. Handwritten-header mode also producesB1M00.APIfor the selected header. Metadata-pass.MACfiles are build intermediates.lib/build/DATA.LIB: the library image with its typed export table; no hand-maintained manifest.
Generate the contract before client integration, without an assembler, linker or runtime file:
# In the library directory, or the composite root containing lib and host:
dccmake dcc-contract-only=true
This still compiles the library sources to validate types and obtain metadata, but does not assemble, link or install anything. Composite COM children are skipped. A previously linked LIB is left untouched. Normal builds refresh the contract automatically. Contract-only mode requires LIB output. Failed generation invalidates generated interfaces instead of leaving stale headers for clients. Existing handwritten headers in the generated location are protected against accidental overwriting.
The host includes API.H and retains an ordinary COM configuration, adding
generated/B1HOST.C to dcc-input, both generated and the directory containing
the common BANKLIB.H to dcc-include-directory, and compiling BANKLIB/BIOSBANK runtime
sources. Auto-generated client wrappers include their adjacent API header;
bank adapters and handwritten-header wrappers use a build-time absolute path,
so regenerate them after moving the source tree. Missing directories beneath
dcc-library-host-dir are created automatically. Full automatic rebuilds also
invalidate the previous LIB before compiling.
Transfer types and annotations. The supported set is void
results, signed/unsigned 8-, 16-, and 32-bit integers, opaque unsigned int
handles, explicitly counted byte buffers, and annotated pointers to single
named flat records. Typedefs resolve through the
compiler: a handle typedef remains canonical u16, not a raw pointer.
Plain char is signed on this target. Pointer results, unannotated pointers,
pointer graphs, function pointers, varargs, old-style/unprototyped functions,
floats, and by-value structure/union arguments/results are rejected explicitly.
Records have a checked target-layout contract, not implicit pointer or
deep-object serialization.
For example:
typedef unsigned int handle;
unsigned int onload(void);
unsigned int onunload(void);
unsigned int add16(unsigned int left, unsigned int right);
unsigned int upper(unsigned char *data, unsigned int count);
unsigned int filerd(unsigned char *data, unsigned int count);
struct BlockSpec { unsigned int seed; unsigned char label[8]; };
struct BlockInfo {
unsigned int handle, size;
unsigned long byte_sum;
signed char state;
unsigned char label[8];
};
handle blknew(const struct BlockSpec *spec, struct BlockInfo *info);
unsigned int blkread(handle block, unsigned int offset,
unsigned char *data, unsigned int count);
unsigned int blkfree(struct BlockInfo *info);
#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
The maintained
LIBDEMO example
builds this data service from lib/DATA.C and its eight-step client/test
program from host/DATATEST.C. Its outputs are DATA.LIB and DATATEST.COM.
The shared BANKLIB/BIOSBANK client support lives in
examples/BANKSDK,
not inside another runnable example.
Its public C API uses descriptive bank_library_*, bank_memory_* and
bank_bios_* names, aliased to the short linker symbols, so they add no
runtime wrapper overhead.
The generated wrappers make ordinary C calls possible; only the deliberate
ABI-rejection checks use low-level call frames.
The pragma syntax is
#pragma dcc bank_buffer FUNCTION BUFFER_ARG in|out|inout COUNT_ARG MAX_BYTES.
Argument positions are 1-based; the count must resolve to unsigned int
(u16); the maximum is a literal decimal integer from 1 to 256. Annotate
each byte pointer once (identical repeated annotations are harmless).
In auto-header mode place pragmas in library sources or headers they include;
annotations are collected across configured modules. In handwritten-header mode
they must occur in the selected API header.
const byte pointers support in only. Zero count permits NULL; nonzero
count with NULL is invalid. No pointers or pointer graphs cross banks;
buffers are independent flat snapshots, without pointer-alias preservation.
The sum of result, scalar arguments and maximum buffer sizes must fit 256
bytes. These are byte lengths, never implicit NUL-terminated strings.
For a single record use
#pragma dcc bank_record FUNCTION RECORD_ARG in|out|inout.
The 1-based argument must point to a complete named struct with
unqualified signed/unsigned 8-, 16- or 32-bit integer fields or fixed
one-dimensional arrays of those types. Pointer members, nested/anonymous
aggregates, unions, bitfields, const/volatile members, and flexible arrays
are not supported. Record pointers cannot be NULL; const record pointers
permit only IN. Records passed by value or returned by value remain
unsupported.
The compiler emits version-2 bank metadata with resolved tags, member
names/types, sizes, offsets and array lengths. This is obtained from the
type model, never by parsing source text. Automatic contracts use
#pragma once instead of macro include guards and contain
complete struct TAG definitions, annotations and compile-time target-layout
checks; original typedef aliases are resolved, not reproduced. Handwritten
contracts are checked against the same schemas. Conflicting definitions
across source modules are rejected, including same-size field changes.
Use matching updated dcc and dccmake executables.
VS Code's C/C++ IntelliSense uses native host widths and alignment, not
DCC's 16-bit packed target model. Generated layout assertions are skipped
only while __INTELLISENSE__ is defined by the editor; struct definitions
and prototypes remain visible. Real compilation, including DCC and native
compiler validation, retains all assertions. Do not define this editor
macro in build settings to bypass incompatible-layout failures.
Record copies use the verified DCC packed little-endian representation;
these headers are not a portable native-host ABI. A compiler with different
widths/padding fails the generated static assertions. Wrappers copy complete
records with bounded memcpy; no heap allocation, per-field serialization
or runtime layout discovery is needed. A fixed-size call uses constant frame
offsets, an exact-size host array, and one adapter body-length check.
Only return/OUT regions are zeroed, rather than clearing inputs before
overwriting them. Record and byte-buffer arguments may be mixed.
OUT records start zero-filled. IN/OUT/INOUT direction and successful-dispatch copy-back rules are the same as for byte buffers; a zero C result with a successful transport still publishes outputs. Copied arguments are independent snapshots; overlapping arguments involving a record are rejected if either is writable. The callee must not retain its temporary record pointer. Large record arrays should remain bank-owned, with bounded slices exposed separately.
Canonical signatures and framing. Type names are void, i8, u8,
i16, u16, i32, and u32. Functions use RESULT(ARG,...), with no
whitespace: u16(), void(), u16(u16), and void(u16). Byte buffers
replace the pointer type with DIRECTION:u8[COUNT_ARG<=MAX_BYTES], regardless
of signedness of the byte pointer. The example objread has exactly
u16(u16,u16,out:u8[4<=128],u16). Full NUL-terminated ASCII strings
(maximum 255 characters excluding NUL) are linked at b1s00, b1s01, etc.;
adapters use b1e00, b1e01, etc. The linker receives both
--export OBJREAD=_b1e05 and --signature OBJREAD=_b1s05, avoiding M80's
six-significant-character limit and signature-hash collisions. Export
entries are 12 bytes: name[8], image-relative adapter offset, image-relative
signature-string offset (both offsets little-endian uint16).
The packed body is result bytes first, then scalar arguments in
declaration order (pointer arguments consume no pointer slot), then copied
buffers and records in declaration order. Integers use the target's little-endian representation.
An OUT buffer begins zero-filled; IN copies only into bank 1; OUT/INOUT copy
back only after successful dispatch. Results and count scalars occupy their
normal 1/2/4-byte widths, without alignment padding. For objread, the body
is result2, handle2, offset2, count2, then count bytes: 8–136 bytes.
Records occupy one fixed-size region each among the copied arguments, in
declaration order, and do not consume pointer slots. blknew uses 29 body
bytes and blkfree 19. Record signatures use
DIRECTION:rec1le:TAG[SIZE]{FIELD:TYPE@OFFSET;ARRAY:TYPE[COUNT]@OFFSET}.
The grammar version and le specify packed little-endian layout. Full
field names/order, widths/signedness, array bounds, offsets and size are
included, so equal names or sizes alone never establish compatibility.
The 255-character signature and 256-byte body limits apply;
overflow is a generation error, not a truncated signature or short hash.
Host wrappers keep one four-byte static bank_library_binding_t and call
bank_library_call_bound(&binding, name, signature, body, body_size) from
BANKLIB.H. BANKSDK compares the full signature once per module generation,
then reuses the bound export index. Open reserves a 512-byte bank-1 frame
extent; steady-state calls do not allocate/free. Module generation and pool
epoch checks reject stale bindings/ownership before frame writes and again at
dispatch. Raw blcall() is also available, with full signature validation on
every call. Clients and libraries use BIOS ABI 1.0, call revision 1.
On the bank wire,
the runtime prepends a little-endian uint16 body-size prefix and passes a
pointer to that prefix to unsigned int b1e00(unsigned char *wire).
The adapter checks the prefix against the minimum fixed size before
reading scalars, checks each count/bound and the 256-byte cap, then requires
an exact body-size match before rebasing pointers or invoking C. It returns
1 for dispatch or 0 for invalid framing. The common runtime must ensure the
allocation really contains the claimed body bytes; the prefix itself is
outside the 256-byte body limit.
On local NULL/count/bounds validation failures wrappers call
blfail(BL_ARGUMENT) or blfail(BL_BOUNDS) before any copying or transport.
On transport failure they preserve the runtime's blerr(), do not copy out,
and return deterministic zero (or return from a void function). Callers
must check blerr(), since a legitimate result can also be zero.
Optional onload(void)/onunload(void) lifecycle functions are ordinary
u16() exports; the common runtime owns their automatic invocation.