Output Formats
The same source assembles to five different files. Which one you want depends on what is going to load it — an emulator, a real Next, a flashing tool, something of your own, or a linker.
| Format | Is | Typical use |
|---|---|---|
bin | The assembled bytes and nothing else | incbin into another build, a loader you wrote |
hex | Intel HEX, as text | EPROM burners and flashing tools |
sna | A whole-machine snapshot | Emulators, and the 48K/128K machines |
nex | The Next executable | A real Next, or an emulator of one |
obj | One piece of a program, with its symbols and relocations | Assembling a program in parts — Building in Pieces |
bin is the default. Four of the five are loadable and obj is not: an object is an input to fantasm link, which joins a set of them into one of the other four — see Building in Pieces.
Format Selection
Three places say which format you want, strongest first:
formatas a directive in the source —format nex. The source is read after the options are, so it beats both of the others.-f/--formaton the command line.formatunder[assembler]in fantasm.toml.
If none of them says, the output filename’s extension decides — game.nex gets a NEX without being asked. 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.
⚠ 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.
bin
Syntax: fantasm build <source> <output>
bin writes the assembled bytes and nothing else: 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.
fantasm build main.asm game.binThat makes it the format to use when something else is doing the loading — an 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, and 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
Syntax: fantasm build <source> <output> --reloc <file>
--reloc writes a companion file saying what a loader must patch to run the image at an address not known when it was assembled. reloc under [artifacts] is the same setting in fantasm.toml.
fantasm build main.asm game.bin --reloc game.relocIt 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. They ascend 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 has no 8-bit field for a 16-bit delta to be added to. db label is recorded in an object, where the section it names can be placed low enough to fit; see Building in Pieces.
This is 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
Syntax: fantasm build <source> <output> -f hex
hex writes Intel HEX: text rather than binary, one record per line, each carrying its own load address and checksum.
fantasm build main.asm game.hex -f hex:038000003E01C975The same three bytes as a bin build, with 8000 in the record so the address survives. Read by EPROM programmers and by most flashing tools.
A hex build is refused where it would write the wrong addresses. A gap or a backward ORG is E1085, as it is for a bin. Bytes emitted out of address order are E1149. A banked program has no hex form at all — use nex or sna.
The records carry their own addresses, which reads as though order could not matter. It did: they were laid down in the order the bytes were emitted, so the file held the right bytes at the wrong addresses, with exit 0 and nothing said.
sna
Syntax: fantasm build <source> <output> -f sna
sna writes a picture of the whole machine rather than 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.
| Machine | Size |
|---|---|
zx48 | 49,179 bytes — a 27-byte header and 48 KiB of RAM |
zx128 | 131,103 bytes — the 128K variant, with its extra banks |
Two directives set what the machine wakes up doing. 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.
org $8000
start:
sna pc start ; where execution begins
sna sp $BFFF ; the stack pointer
ld a,1
ret⚠ 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
Syntax: fantasm build <source> <output> -f nex
nex writes the Next executable: 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 bankA single-bank program is 16,896 bytes.
NEX Version
FantASM writes the lowest version that carries the file — V1.2 unless something in it needs V1.3. It is not told which to use.
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.
⚠ 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.
NEX Header
Syntax: nex <command> [<value>…]
nex sets one field of the header. command is one of the words below; an unrecognised one is E1053 Unknown NEX command, so a typo is refused rather than ignored.
| Directive | Sets |
|---|---|
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 0 | Says there is no loading screen. Anything else is E1117 — see Loading Screens. |
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 bank <n> | Force bank n into the file, whether or not the program filled it. |
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.3 | Override the version, which is otherwise chosen. |
nex cfg <border>, <fileHandle>, <preserve>, <ram> | Four of the above at once. |
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
retLoading Screens
A loading screen is not supported, and asking for one is refused with 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.
If the device is zxnext then:
!device zxnext
!format nex
org $8000
ld a,1
ret
bank 11,3 ; assembly continues at $6000
db $AA,$BB
bank 12,4
db $CCThat 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 supported on `zx48`. 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.
obj
Syntax: fantasm build <source> <output> -f obj
obj writes a relocatable object: the assembled bytes, with enough description for something else to place them and join them to other objects. Every section starts at zero and whoever links it chooses the base, so an org reaches nothing and the same source gives the same object whatever address it was told.
fantasm build kernel.asm kernel.o -f obj
fantasm link kernel.o main.o -o game.bin -e startfantasm dump kernel.o prints what one holds. Building in Pieces is the whole of it — section, extern, what an object build refuses, and what a link does with them.
Further Reading
- Building in Pieces —
-f obj,sectionandextern. - Memory and Banking — what the banks are, and how to know which one you are in.
- Building Several Images — one project producing a payload and the loader that
incbins it. - The Project File —
output,format, and where paths resolve from.