Output Formats

The same source assembles to four different files. Which one you want depends on what is going to load it — an emulator, a real Next, a flashing tool, or something of your own.

FormatIsTypical use
binThe assembled bytes and nothing elseincbin into another build, a loader you wrote
hexIntel HEX, as textEPROM burners and flashing tools
snaA whole-machine snapshotEmulators, and the 48K/128K machines
nexThe Next executableA real Next, or an emulator of one

bin is the default.

The Format

Three places say which format you want, strongest first:

  1. format as a directive in the sourceformat nex. The source is read after the options are, so it beats both of the others.
  2. -f/--format on the command line.
  3. format under [assembler] in fantasm.toml.

If none of them says, the output filename’s extension decides — game.nex gets a NEX without being asked.

A directive left in a source overrides the flag a build script passes, and nothing says so. That holds for device as well as format!device zxnext assembles Z80N however -d zx48 was meant. A source that must build both ways names neither.

An output whose extension names a different format is renamed. Ask for nex with an output called game.bin and you get game.nex — nothing is left at the name you gave, so a build script looking for it finds nothing. It is the format that decides, however it was chosen; -f and a format directive both do this.

An extension that names no format is left alone. game.out stays game.out whatever the format is, which is the way to hold a name the build must not change.

bin

fantasm build main.asm game.bin

No header, no padding, no record of where the code was meant to live. A program that orgs at $8000 and emits three bytes produces a three-byte file; whatever loads it has to know that $8000 is where those bytes belong.

That makes it the format to use when something else is doing the loading — a incbin in another target (Building Several Images), a custom loader, or a tape image built by another tool.

Your code has to be contiguous, whatever the format. An ORG that skips forward over addresses nothing writes, or a second one that puts code where code already is, is refused with E1085. The image is built in the order bytes are emitted and holds no addresses of its own, so a skipped range would leave every byte after it at the wrong offset — the byte a label points at would be somewhere else entirely.

The gap is not padded, deliberately: padding back from a higher ORG gives a file the size of the address span rather than the size of the program, so five bytes between $9000 and $8000 would be a 4099-byte file.

Assemble each region separately and combine them — a [[target]] per region and an incbin is the usual shape. A MODULE body included above your program is not a gap; it is placed after the program and the result is one unbroken range.

Relocation

--reloc, or reloc under [artifacts], writes a companion file saying what a loader must patch to run the image at an address not known when it was assembled:

fantasm build main.asm game.bin --reloc game.reloc

It is a companion to bin and to nothing else. Every other format writes the same buffer through a transformation that breaks the correspondence a table of offsets depends on — hex is text records carrying their own addresses, sna is a whole machine, nex loads banks into fixed slots. Asking for one anywhere else is E1102, as is a bin whose image was not written in address order.

The file is three sections, each a 16-bit little-endian count followed by that many 16-bit little-endian offsets: whole-delta first, then low-byte, then high-byte. Offsets index the file, not the address space — the image is what a loader reads, and the origin is what it is adding. Ascending within each section, so patching is one forward walk.

A program with nothing to patch gets three zero counts and six bytes. That is a position-independent image, which is worth being able to read: an absent file cannot be told from a build that forgot the flag.

What gets an entry: a whole address (dw label, dw label + 4, dw $), or either half of one (label & $FF, label >> 8).

A difference of two labels does not, and that is correct rather than a gap. dw end - start is the same distance wherever the program loads, so patching it would corrupt a length. A constant holding an address is absent for the same reason — SCREEN equ $4000 says where the hardware is, not where your copy landed.

Both halves, or neither. ld a, label & $FF and ld h, label >> 8 are only correct together: the loader adds the delta’s two halves separately and the program puts them back together, so the carry lands where it should. A lone high byte is out by one whenever the low halves carry.

Anything else naming an address is refused with E1092 rather than guessed at — dw label * 2 is twice the adjustment, and db label does not fit.

Not z88dk’s .reloc. The three-section shape follows the reasoning in their issue #13; what they ship is a bare list of 16-bit offsets with no counts and no sections, so it cannot describe a half at all. Nothing that reads theirs will read this.

hex

fantasm build main.asm game.hex -f hex

Text rather than binary, one record per line, each carrying its own load address and checksum:

:038000003E01C975

The same three bytes as above, with 8000 in the record so the address survives. Read by EPROM programmers and by most flashing tools.

sna

A .sna is a picture of the whole machine, not just your program: every byte of RAM, the registers, the interrupt mode and the border. An emulator loads one and is immediately in your program.

MachineSize
zx4849,179 bytes — a 27-byte header and 48 KiB of RAM
zx128131,103 bytes — the 128K variant, with its extra banks

Two directives set what the machine wakes up doing:

    org $8000
start:
    sna pc start        ; where execution begins
    sna sp $BFFF        ; the stack pointer
    ld  a,1
    ret

pc and sp are the only two !sna takes — anything else is E1052 Unknown SNA command.

Both have defaults, so neither is required. Execution begins at your ORG and the stack sits at $FFFE.

A 48K snapshot has no field for the program counter. The entry address is pushed onto the stack and the loader RETs to it, so sna sp moves the entry word with it. If you place the stack over memory you also want to use, the two bytes below SP are not yours.

nex

A NEX carries a 512-byte header and then whole 16 KiB banks, so a program larger than 64 KiB loads in one go without a loader of yours running first.

512 bytes of header  +  16384 bytes per bank

A single-bank program is 16,896 bytes.

FantASM writes the lowest version that carries the fileV1.2 unless something in it needs V1.3. It is not told which to use.

A core older than 3.x refuses a V1.3 file outright. It reads the version string and stops, rather than loading and ignoring the fields it does not know, so the higher tag is not free: a file tagged V1.3 for no reason will not run on those machines.

These raise it, each being a V1.3-only header field: a checksum (nex crc 1), the expansion-bus setting, a CLI buffer, an extended screen, a copper block, a tilemap.

A loading bar does not. nex bar is a V1.2 feature, and the V1.3-only Layer 2 bar position is set alongside it — so that field, and the banks offset, are not read as evidence. Both are cleared when the file is V1.2, the format saying a V1.2 file does not carry them.

nex version V1.2 or V1.3 overrides the choice, spelt as the header spells it. V1.3 tags a file that is 3.x-only by intent; V1.2 is an assertion rather than a downgrade, and is refused with the feature named (E1115) if the file does use one. Anything else is E1114 — write the V.

-vv says which was chosen and why.

The NEX Header

    org $8000
start:
    nex pc start        ; entry point
    nex sp $FF00        ; stack pointer
    nex border 3        ; border while loading
    nex core 2,0,28     ; minimum core version
    ld  a,1
    ret
DirectiveSets
nex pc <addr>Entry point. Defaults to your ORG.
nex sp <addr>Stack pointer. Defaults to $FFFE.
nex border <0-7>Border colour during loading.
nex screen 0Says there is no loading screen. Anything else is E1117 — see below.
nex bar <on>, <colour>, <startDelay>, <loadDelay>The loading bar; every argument after the first is optional.
nex core <major>, <minor>, <subminor>Minimum core version the program needs.
nex entry <bank>The 16 KiB bank mapped at $C000 on entry.
nex preserve <0|1>Keep most NextRegs rather than resetting them.
nex ram <0|1>RAM required: 0 = 768K, 1 = 1792K.
nex crc <0|1>Write a CRC-32C checksum into the header.
nex version V1.2|V1.3Override the version, which is otherwise chosen.
nex cfg <border>, <fileHandle>, <preserve>, <ram>Four of the above at once.

An unrecognised word is E1053 Unknown NEX command, so a typo is refused rather than ignored.

Loading Screens

Not supported, and asking for one is refused (E1117).

The screen flag tells a loader that image data sits between the header and the bank data. FantASM writes none, so setting it would have a loader read the first bytes of your program as a picture and then take the banks from the wrong offset — a file that is valid, loads, and is wrong.

nex screen 0 is accepted: it is the default, and says there is no screen rather than promising one.

Banked Code

The banks in the file are the banks your program actually filled. Select one with a slot, which is what moves assembly into it:

    !device zxnext
    !format nex
    org $8000
    ld  a,1
    ret

    bank 11,3           ; assembly continues at $6000
    db  $AA,$BB

    bank 12,4
    db  $CC

That produces a NEX of 49,664 bytes — the header plus banks 2, 11 and 12.

bank needs a machine that has banks. With no !device the default is a zx48, which has none, so it is E1136 `bank` is not a directive `zx48` has. Set the machine and the directive works.

Bare bank 11 does not do this. It selects the bank but leaves the program counter alone, so the bytes carry on landing where they were going, the bank stays empty, and an empty bank is dropped from the file. See Memory and Banking.

nex bank <n> forces a bank into the file that holds no data, for a program that pages one in and fills it at runtime.

Further Reading

Documentation