Cartridges
Every Container so far has been something a machine loads: a DOS reads a
.xex into memory, a boot record reads sectors off a diskette. A cartridge is
not loaded at all. Its ROM is decoded onto the bus by the board, the CPU reads
it where it lies, and the only thing software can do is choose which part of a
large ROM stands in a window.
That changes what a Container is for. There is no loader to write and no order to load things in; there is an image whose offsets are the hardware's, and the board does the rest.
The same program, another machine
Chapter six's program, unchanged. print.asm, intro.asm and level.asm are
the same files, the Phases are the same, the Pane is the same and so is the
Trampoline that draws the map. Three lines of the Project differ:
include "atari/xegs128.ngp"
modules { "print.asm" "intro.asm" "level.asm" }
panes in switched { level }
resident { print }
phase intro { needs intro then level entry introStart }
phase level { needs level entry levelStart }
entry intro
container car "xegs128"
include names a machine holding a 128 KB cartridge; the Pane's Window is
called switched on this board rather than ext; and the Container is a
.car, which names the board it is an image of:
container car "xegs128"
The board is quoted for the reason a processor is — 8k begins with a digit,
and no identifier of this language may — and it is not decoration. A board
decides how large the image is, where in it each bank stands, and what number a
reader has to find in the header to know which of the hundred-odd cartridge
mappers this one is. Eight boards are written: "8k" and "16k", which have no
banks at all, and the six of the XEGS family from "xegs32" to "xegs1024".
The machine
; An Atari 800XL holding a 128 KB XEGS cartridge: sixteen banks of eight
; kilobytes, of which the last is the fixed part the CPU always sees at
; $A000-$BFFF and the other fifteen are storage, brought into $8000-$9FFF one at
; a time by the byte written to $D5FF. A cartridge is ROM the CPU reads where it
; lies, so nothing is loaded and there is no DOS: the machine's whole RAM below
; the cartridge is the program's, $0700 included.
target {
cpu "6502"
; The one Container a machine holding a cartridge takes. A `.xex` needs a DOS
; to load it, and this machine has none.
containers car
; Fifteen and not sixteen: the sixteenth bank is the fixed part under another
; name, and one set of bytes with two names is one too many.
units banks 15
window switched $8000 .. $9FFF views banks
}
storage { units banks }
target {
region ram $0000 .. $7FFF ram
region stack $0100 .. $01FF reserved
region io $D000 .. $D7FF register
; The part of the cartridge no switch takes away, which is where the solver
; puts everything nothing writes. $8000-$9FFF is in no Region at all: it is
; the window, it shows one Bank at a time, and only a Pane or storage stands
; there — as the boot record's page is in no Region of the diskette's variant.
region cart $A000 .. $BFFF rom
; What the driver writes. Any address of $D500-$D5FF is the same register.
register CARTSEL $D5FF
; The hardware that holds a Section while a program has handed it one, by
; `.root NAME` before the taking of its address, until it hands another or
; `.release NAME`: what it holds stays where it is for that long. See
; docs/decisions/0247-the-hardware-holds-a-section-for-as-long-as-it-is-given-it.md.
holder dlist ; ANTIC's display list: SDLSTL, or DLISTL
holder dli ; the display list interrupt: VDSLST
holder vbi ; the immediate vertical blank: VVBLKI
holder vbd ; the deferred vertical blank: VVBLKD
holder nmi ; $FFFA, where the OS ROM is switched out
holder irq ; $FFFE, where the OS ROM is switched out
holder chbase ; the character set: CHBAS, or CHBASE
holder pmbase ; the player-missile memory: PMBASE
holder dosini ; DOSINI: called once a boot returns, and on every reset
holder cartinit ; $BFFE: called in the middle of the cold start, and on every reset
}
; The OS, the driver for this storage and the decoders the tool ships. Every
; Transition calls the driver and the decoders, so they are resident; a decoder
; nothing uses is dropped.
modules {
"atari/osram.asm" "atari/osrom.asm" "atari/mathpack.asm"
"atari/charsets.asm" "atari/cart.asm" "stream/zx0.asm"
}
resident { osram, osrom, mathpack, charsets, cart, zx0 }
Three things in it are worth stopping on.
The fixed part is a rom Region. That is a fourth thing a Region can be,
beside ram, reserved and register: memory the solver places in and no code
writes. Everything the program never writes goes there — every Proc, every table
of constants — and the solver puts it there without being told, because it can
see which Sections nothing writes. Where it cannot see, the source says
readonly; that is the whole of chapter thirteen's problem seen from the other
side, and the next section shows it.
$8000-$9FFF is in no Region at all. It is the window, it shows one bank at
a time, and the only things that stand there are a Pane's Sections and storage.
A range in no Region is in no pool, so the solver never allocates there — the
same trick a diskette's variant uses to keep the boot record's page to itself.
A .with switches the bank and leaves it switched, as it does on the
130XE. What the window shows outside a .with is undefined, and the tool
refuses every way of reading it. On a cartridge that is plain from the hardware
alone: under the window there is no memory, only the next bank.
Which is how one thinks about banks anyway: not what is in the window by default but the thing I want is in bank n. It costs two instructions and no exit, and it is what makes a Pane on a cartridge reachable at all.
What it costs, and what it gives back
The map, in one Phase:
phase level (1)
zero page: 143 of 256 bytes
memory: 15617 of 32256 bytes
...
$0083-$0084 level.drawMap.at temporary level shares with cartDst ngaDst ngaPtr
...
$0085-$0085 level.drawMap.left temporary level shares with cartSize ngaOffset ngaValue
...
$8000-$8171 level.levelMap section kept intro..level in pane level (state 14)
...
$A0B0-$A0C8 print.printLine proc intro..level
$A0DB-$A0FF level.drawMap proc level
$A100-$A10A level.levelStart proc level
...
$8000-$BFFF is gone for good. A plain XEGS cartridge cannot be switched
off, so 16 KB of the address space is the board's for the whole run — of which
8 KB is the window and 8 KB the fixed part. That is the rent.
The RAM a DOS would have held is the program's. There is no DOS: the machine
boots the cartridge, so $0700 upwards is free, and the variant's region ram
runs from zero. Chapter four's program had to leave $0700-$1FFF to a DOS of
the 2.x family.
The ROM is as large as the board. 128 KB of it here: eight for the fixed
part and fifteen banks of eight for storage and for Panes. A .xex this program
would fit in twice over.
The cold start
One thing a cartridge cannot do is put a byte in RAM. intro.asm holds a
string, introText, that the program prints — and a string is bytes the program
was given, standing at an address in RAM. On a .xex the DOS puts it there. Here
nobody does.
So the tool does it the way it does everything else about a Phase's memory: the
cold start is an edge into the entry Phase from nowhere. The string waits in
a bank as any Payload does, the run vector points at a stub, and the stub hands
the Frame of that edge to the Transition routine, which copies the blocks,
writes the Cell, and jumps to the entry Label — exactly what it does when a
.transition is taken. Storage says so:
storage
bank 0: 30 of 8192 bytes
$0000-$000F frame into intro live intro..level
$0010-$0013 frame intro -> level live intro
$0014-$001A intro.introText copy (7 bytes) live intro
$001B-$001D nga.cell.ngaCurrentPhase copy (3 bytes) live intro
bank 1: 0 of 8192 bytes
bank 2: 0 of 8192 bytes
bank 3: 0 of 8192 bytes
bank 4: 0 of 8192 bytes
bank 5: 0 of 8192 bytes
bank 6: 0 of 8192 bytes
bank 7: 0 of 8192 bytes
bank 8: 0 of 8192 bytes
bank 9: 0 of 8192 bytes
bank 10: 0 of 8192 bytes
bank 11: 0 of 8192 bytes
bank 12: 0 of 8192 bytes
bank 13: 0 of 8192 bytes
bank 14: 0 of 8192 bytes
frame into intro is the cold start's; frame intro -> level is the edge the
program takes itself. Both are read by the same routine, and the second Phase's
map is not there at all — a Pane's Sections are written into their Bank by the
Container and no edge copies them, which is chapter six's rule and does not
change here.
The price is the routine: some four hundred bytes of the fixed part, in every
program on a banked board, whether or not it takes a .transition of its own.
The alternative was a copier of its own for the cold start, and it was refused
because a Payload belongs to a Section and a Frame to an edge: a program with an
edge back into its entry Phase would have carried the same bytes twice, and
nothing would have related the two.
How it starts
Six bytes at the top of the image are the tool's, and they are what the OS reads before it runs anything of a cartridge:
| Address | Holds |
|---|---|
$BFFA |
the program's start — the cold start's stub, or the entry Label where there is none |
$BFFC |
zero, which is what says a cartridge is there |
$BFFD |
$04: started through $BFFA, no disk boot, not the diagnostic cartridge |
$BFFE |
the init vector, pointing at an rts |
The init vector does nothing on purpose. The OS calls it in the middle of its
own cold start, before E: exists, so anything run there would run in a machine
half set up. The jump through $BFFA comes at the very end, after E: is
open on channel 0 — which is why print.asm works on a cartridge exactly as
it works on a diskette.
All of it runs again on RESET, so a cartridge program restarts rather than
resuming.
Running it
A cartridge is the one Container besides the .xex that the suite can put in a
machine and run: an image is memory, and the register that switches its banks is
one store. But the boot protocol above is the OS's, not the suite's, so this
chapter's program was booted on an emulated Atari with the machine's own ROM,
the way the tutorial's programs are:
$ scripts/run-on-atari.py tut.car --frames 200
tut.car on 130XE, Atari XL/XE OS ver.2, 200 frames after boot
screen memory at $7C40
| INTRO
| #####################################
| #...................................#
| #...................................#
| #......########.....................#
| #......#......#.....................#
| #......#......#.....................#
| #......########.....................#
| #...................................#
| #...................................#
| #####################################
INTRO came out of a string the cold start copied from bank 0; the map was
drawn out of bank 14 without being copied anywhere at all.
What is not written
Of the cartridge mappers that exist, this tool writes eight. The reason is one line of hardware: a program of this tool has code that must be reachable while a bank is switched — the Transition routine, the driver, the decoders — so it needs a part of the ROM that no switch takes away. MegaCart, AtariMax, Williams and SIC! switch the whole of the cartridge's address space, so on those the resident code would have to be replicated in every bank or copied into RAM before any switch happened. Switchable XEGS, whose bit 7 reveals the RAM beneath the whole cartridge, is the one that would come first: the RAM would be one more state of the window, as the machine's own memory is the fifth Bank of a 130XE's.