Memory and Banking
The Z80 sees 64 KiB. Every Spectrum past the 48K has more RAM than that and shows it a window at a time, so writing for one means telling the assembler which physical memory an address currently stands for.
Machine Memory
| Machine | Windows | Page size | Pageable |
|---|---|---|---|
zxnext | 8 | 8 KiB | all eight, over up to 256 pages |
zx128 | 4 | 16 KiB | $C000 only, over banks 0–7 |
zx48 | 4 | 16 KiB | none |
zx16 | — | — | none, and nothing above $8000 |
The 64 KiB space is always a uniform tiling: the window size is the page size, so a Next address divides by 8192 to give its slot and a 128K address by 16384 to give its window.
zx48 and zx16 refuse the paging directives outright rather than accepting them and doing nothing:
Error [E1136]: `bank` is not supported on `zx48`Page Selection
| Directive | |
|---|---|
bank <n> | Select a 16 KiB bank |
bank <n>, <slot> | Select it into a slot, on the Next only |
page <page>, <slot> | Select an 8 KiB page into one of the Next’s eight slots |
slot <page>, <slot> | The same directive under another name |
page ?, <slot> | Mark the slot as holding a page the OS allocates — see Unknown Pages |
page
Syntax: page <page>, <slot>
page selects an 8 KiB page into one of the Next’s eight slots, and moves assembly to that slot’s address. page and slot are expressions, or ? in place of page — see Unknown Pages.
Both arguments are required: page 5 on its own is E1005, which names the form it wants. On a 128K it warns rather than failing, being best-effort mapped so that old source keeps working — the machine has no 8 KiB paging, and the warning is telling you the truth:
Warning [W1130]: `zx128` has 16K banks, not 8K pages
help: use `bank`⚠ slot is an alias for page, and takes the same arguments in the same order — the page first, then the slot. slot 9, 4 puts page 9 into slot 4, exactly as page 9, 4 does. The name reads the other way round and does not mean it.
bank
Syntax: bank <n> [, <slot>]
bank selects a 16 KiB bank. n and slot are expressions.
It is valid on the Next as well as the 128K: a 16 KiB bank is the unit the .nex file format uses, and maps to the slot pair 2N/2N+1, so it is not treated as foreign there. On the Next it takes an optional slot, which a 128K refuses:
Error [E1137]: `bank` takes no slot on `zx128`⚠ Give the slot when you mean to put code in the bank. bank 11, 3 moves assembly to $6000, so the bytes that follow belong to bank 11. Bare bank 11 selects the bank but leaves the program counter where it was, so on a Next the bytes carry on landing wherever they were going and the bank stays empty — which an .nex then drops for holding nothing. On a 128K, bare bank moves assembly to $C000, that machine having only the one pageable window.
Unknown Pages
Some code runs from a page nobody knows at build time. A dot command’s payload is copied into whatever the OS hands over, and a kernel mapped at $0000 is wherever it was put. Debug info then names the page the slot happened to hold while the source was read — page 20, say — and a debugger matching on that page never sees the code run at all.
? where a page goes says so:
!device zxnext
page ?, 0 ; slot 0 holds a page the OS chooses
org $0000
kernel:
!assert a == 0
retEvery SLD record for code in that window then carries page -1 — the label, the trace records, and any !assert or !debug — and a debugger arms them by address alone. See Debugging a Build.
It marks, it does not map. page ?, 0 sets no mapping, allocates no bank and leaves the program counter where it was, which is what separates it from page 20, 0. Where the bytes go is unchanged; only what is recorded about them differs.
A block copied at run time says the same thing as !virtual <addr>, ? — see Virtual Origins. Use page ? for ordinary code at an org, where there is no block to attach the mark to.
Current Mapping
Three read-only values, usable in any expression:
| Written | Is |
|---|---|
_page | The physical 8 KiB page holding the current address |
_bank | Its 16 KiB bank — _page / 2 |
_slot | The 8 KiB slot index, 0–7 |
All three are derived from the live slot map at that point in the source, so they follow your paging directives. On a Next at reset:
!device zxnext
org $8000
db _page, _slot, _bank ; 04 04 02After a page 9, 4 the same three read 09 04 04.
The leading underscore is required. Without it the word is read as a label of that name, so ld hl,page reports an undefined label rather than answering with the page number. A label really called page is reached that way, and is yours.
They cannot be emitted either side of a paging directive into a flat image. Bytes before and after a page belong to different pages, which a bin cannot express, so the pair is E1085 — see Output Formats. Read them into constants, or build a banked format.
They are well defined on every machine — _bank is the useful one on 16 KiB machines, _page and _slot on the Next — so no target branching is needed to use them.
Named Forms
The same three take a name in parentheses and answer about that name instead, and _window joins them:
| Written | Is |
|---|---|
_page(name) | The page holding it |
_bank(name) | That page’s 16 KiB bank |
_slot(name) | The slot its page is mapped through |
_window(name) | The address it has once that page is mapped |
!device zxnext
org $c000
page 20,6
sprites:
ds 256
ld a,_page(sprites) ; 20 — the page to map
nextreg $56,a
ld hl,_window(sprites) ; $C000 — where it appears once mappedThis is the number people maintain by hand. Banked code is full of page this in, then call it, and the page is written at every call site; moving a routine between pages is then an edit in each of them with nothing to catch one that was missed.
A label’s page is recorded where it was written, so a page above it is what these answer from, whatever happens to be mapped by the end of the file. @name is taken too and reaches global scope, as it does everywhere else.
The bare and the called forms are different questions and both stay. _page is the page being assembled into; _page(sprites) is the page sprites is in. _window is the one with no bare form, having no address of its own to answer with.
A page the OS allocates has no number. A body written inside a page ? block is E1160 rather than zero, which would map the ROM over it. _slot(name) and _window(name) still answer, both coming from the address alone — see Diagnostics.
An object records them and --reloc refuses them. In an object each becomes a relocation the linker settles, because a page is decided by the layout it chooses. --reloc moves one image by one delta and has no page to move it between, so the same operand there is E1092 — see Building in Pieces.
RAM Size and --ram
Syntax: fantasm build <source> <output> --ram <size>
--ram says which Next you are targeting, a machine having 1 MiB or 2 MiB. size is 2mb, 1mb, 512k, or a bare number in KiB. ram = "512k" under [assembler] is the same setting — see The Project File.
fantasm build main.asm -d zxnext --ram 512kThis narrows the valid bank range rather than describing the hardware. It defaults to the machine’s maximum, so a Next allows banks 0–127. 512K is 32 banks, and the build tells you when you exceed it:
bank 31 ok
bank 32 Error [E1082]: invalid bank 32A program that must run on a 512K machine then fails at your desk rather than on the desk of whoever owns one.
Paged Code Under !test
!test blocks run on a paged machine that mirrors the assembler’s slot map, so a test that pages memory reads and executes the right physical page. Runtime NEXTREG MMU writes and the 128K paging port remap slots live, as they would on hardware.
For the default layout every slot maps to a different page, so a test that does no paging behaves exactly as it would against a flat 64 KiB. See Testing.
Limitations
⚠ Cross-bank library code is not supported by --gc-modules. A module body placed in a different bank from the code that calls it is untested with elimination on, and is not diagnosed. If you page banks yourself and call across them, leave --gc-modules off — see Modules.
zx16 refuses anything above $8000, which is the machine’s own ceiling rather than a budget you set. -M/max_code_size is the separate limit on how much code there is; see Command Line.