Everything you wanted to know about z88dk paging (segmented memory addresses) but were afraid to ask.
Recently, on discord, RCL asked how one goes about positioning specific files in a fixed location in memory with C, and Allan was kind enough to explain it better and fuller than I’ve ever seen before, so I’ve taken the liberty of transcribing it here – and, thus, from hereon, the words are his, so over to you Allan…
You can place anything anywhere in z88dk. Here are some choices that include more pedestrian ones:
- Keep data in separate files. Load them at runtime. You’ll see many do it this way and it has to do with unfamiliarity with the Next and the tools. That doesn’t always mean it’s a bad choice but you can end up with a lot of files.
- Append all your data to your nex file and load on demand at runtime. Better than (1) in the sense your entire program is now in one file including extra data you may not want in memory.
- Place your data into memory.
z88dk defines sections that correspond to the Next’s (and the 128K’s) memory space. It names the memory in two ways using either BANK or PAGE. BANK is numbered in 16K units like the 128K did. The 128K had BANKS 0-7 and if you are familiar with that, you’ll know that BANK 7 is where the second display file is, BANK 5 is where the first display file is and the initial memory space of a 128K program consists of (in 16K units) ROM, BANK 5, BANK 2, BANK 0. The Next has more memory so it has BANKS 0-7 but also 8-47 (1 MB Next) and 48-111 (2 MB Next). The other view of the same memory is PAGE which maps to how the Next does bankswitching. The same memory space is divided into 8K units. A 16K BANK N corresponds to two 8K PAGES 2N, 2N+1. The Next is able to place individual 8K PAGEs into any of the 8K slots in the Z80’s 64K space. I call those MMU 0 (0-8K), MMU 1 (8K-16K), MMU 2 (16K-24K), … MMU 7 (56K – 64K). The 128K spectrum could only place BANKS 0-7 into the top 16K (MMU 6, MMU 7).
One further meta that z88dk places on this is that BANKs are not allowed to exceed 16K (or 8K) in size. The tools will enforce this by informing you of spillage. Why? The 128K can only place 16K banks in the top 16K of memory. If there is spillage, your program doesn’t see it. The data must be confined to 16K. z88dk can also optionally do 8K checks and for the same reason, this time because the unit of memory is 8K in Next pages. If you want your data to fit into a single 8K page so that you see it all in a single 8K chunk in the 64K space, you can use this to get the tools to tell you that everything fits and is ok.
PAGEs have no checks. You can add as much as you want to a PAGE and the data will just spill to following pages. Clearly if you place 20K into PAGE 10, it will occupy pages 10/11/ part of 12. If you place something else into PAGE 12, there is a conflict and the tool will tell you that. If you place something into the last 12K of PAGE 12 there is no conflict and the tooling is fine with that too.
I’m going to build this from the bottom up to convey some insight into segmented address spaces and how it is solved by z88dk.
This solution applies to all bankswitched targets that z88dk targets, with some additions for the Next specifically.
If you are starting with binary data, it’s much easier to just include the binary into the appropriate memory spot using asm:
In one single asm file:
SECTION BANK_20 ;; place in 16k bank 20, must fit in 16K
;; SECTION BANK_20_H = place in upper 8K of 16K bank 20, must fit in 8K
;; SECTION BANK_20_L = place in lower 8K of 16K bank 20, must fit in 8K
;; SECTION PAGE_40 = place starting in lower 8K of 16K bank 20
;; SECTION PAGE_41 = place starting in upper 8K of 16K bank 20
PUBLIC _hereitis ;; export the label into the global scope
;; leading underscore makes it accessible from C as well because
;; C adds a leading underscore to names
_hereitis:
BINARY "...." ;; the name of the binary file - I think INCBIN works too
Add the asm file to your compile line and z88dk will make a NEX with your stuff in the right place. The name “_hereitis” is globally exported by the PUBLIC
directive so you can declare a pointer to it in C:
extern unsigned char hereitis[]; // the name is "_hereitis", C treats data as a char array per the
// declaration and the name is an alias for the memory address due
// to how C works
If you want to refer to it from asm from any asm file:
EXTERN _hereitis ;; refer to name in the global scope not necessarily this file
ld hl,_hereitis
Of course any C or asm code referring to hereitis
must ensure that that memory bank is paged in before accessing the data.
You can add to the same banks in multiple files. The tool will merge it all together and let you know if there are conflicts. z88dk uses a linking assembler, as you’d expect, so it just packs it all together into the indicated bank without overlap.
What is the memory address? That depends on the ORG of the BANK. The default for BANK_N is 0xc000, BANK_N_L is 0xc000 (the first 8K of the bank), BANK_N_H is 0xe000 (the second 8K of the bank), PAGE_N is 0xc000 for even N and 0xe000 for odd N. This keeps BANK and PAGE org consistent and sensible for a 128K Spectrum. So the _hereitis
label as defined above will be in the top 16K of the address range and your program would have to page that stuff into the top 16K before using it.
You can change the default org of any banks with a pragma (but there is a much better way):
#pragma output CRT_ORG_BANK_20 = 0x2000
Now anything assigned to BANK_20 will have addresses starting in the 0x2000 range so your program should bank this memory into MMU 1 / MMU 2 to address it properly. This org change does not apply to the other sections mentioned :- BANK_20_L, BANK_20_H, PAGE_40, PAGE_41
they have their own ORG which you must change individually, also via pragma if you want to.
This pragma can appear on the compile line but that is the wrong way to go. If you use pragmas, collect them in a single file. I usually call it “zpragma.inc” and then put that file on the compile line. You can create a long list of pragmas in such a file and comment them so you know what they are doing.
Since a BANK is 16K in size, the memory placement within the 16K bank only depends on the bottom 14 bits of the ORG address. If your ORG is 0x4200 then your data appears at offset 0x200 in the 16K bank but memory address labels will be at 0x4200. Your program is expected to place that memory at address 0x4000 before accessing it to make the label address valid. PAGEs are 8K in size so the bottom 13 bits of the ORG tell what the offset into the page is for the data. It is possible to have many org for a particular bank and the lower bits will determine where stuff goes in the bank/page. The tool will tell you if there is conflict. The memory address labels will reflect the full 16-bits of the ORG address so your program must ensure the bank/page is mapped into the correct slots in the 64K space before accessing. I went into a lot of detail there to help you understand the underlying banking mechanism. It reflects exactly how bankswitching is done in a segmented memory space like the Next’s (or the 128K’s). The z80 has a 64K address space and addresses are 16-bit. All labels generated by the compiler or assembler are 16-bit and refer to an address in that 64K space. Your BANK or PAGE data is a 16K or 8K container with an ORG address. The lower bits define where within that 16K / 8K container that data is placed. The rest of the bits decide which memory slot in the z80’s 64k space that bank or page should be placed with MMU before accessing.
And lastly I’ll describe the recommended way to precisely handle placement of stuff in memory. Make your own memory map. The names BANK_20, BANK_20_L, BANK_20_H, PAGE_40, PAGE_41
are special reserved works in the z88dk toolchain for the Next target. You can use them as a prefix to define your own memory section. Eg, BANK_20_MYSTUFF
will be understood by z88dk to mean this is your own section name but it’s in BANK 20. Each section can have its own org.
Here’s an example memory map which would be placed in a fixed file mmap.inc
. A pragma tells z88dk to use this file to augment its memory map:
SECTION BANK_20_MAIN
org 0x2000
SECTION BANK_20_WHY_I_DO_THIS
org 0xff00
You can add as many sections as you want.
The section BANK_20_MAIN
has org at 0x2000 so it’s expecting BANK 20 to be paged into the z80 memory space at 0x2000 (mmu 1, maybe mmu 2 if more than 8K data) before accessing stuff placed there. That stuff is placed at offset 0x0000 in the bank, determined by the lower 14 bits of the org address. Section BANK_20_WHY_I_DO_THIS
will be stored inside BANK 20 starting at offset 0x3f00 (only 256 bytes are left before spilling outside the bank which will be caught by the toolchain). Its org indicates that address labels expect BANK 20 to be paged into the top 16K of the z80’s address space before accessing. The offset is in the top 8K of BANK 20 so you could get away with paging in PAGE 41 into mmu 7 before accessing.
On the spectrum next you can do this mapping simultaneously. MMU1 = PAGE 40, MMU 2 = PAGE 41 (for accessing BANK_20_MAIN
stuff). MMU 7 = PAGE 41 (for accessing BANK_20_WHY_I_DO_THIS
stuff).
Compile C to an object file (-c), one compile line per set of C code going to the same banks. Then build the result in a final compile that gathers the object files (which include bank assignment), asm, whatever else for creating a nex (or whatever). For your specific question about having your data in a C file (and again, I recommend just doing it in asm with a binary include): If your declaration is const unsigned char mypic[] = ...
then it’s destined for an rodata segment (read only data, safe for rom). If your declaration is unsigned char mypic[] = ...
then it’s destined for a data segment (read/write, must be ram). You would place it in a specific memory bank by compiling to an object file specifying the correct section names:
zcc +zxn -c -clib=sdcc_iy -SO3 --max-allocs-per-node200000 --datasegBANK_20 --constsegBANK_20 mypic.c
(the code segment and bss segment are not redirected so anything there is headed for the main binary) Then you add that object file to the final compile line that builds the nex:
zcc +zxn -startup=31 -clib=sdcc_iy -SO3 --max-allocs-per-node200000 main.c mypic.o other.asm -subtype=nex -Cl"--clean" -create-app
Your build script (whether batch file or makefile) would go through these steps. If you have a long list of files to add to a compile line, use a .lst file which contains a list of files to compile. A more than trivial example (but not too much more) is the playvid dot command:
https://gitlab.com/thesmog358/tbblue/-/tree/master/src/c/DotCommands/playvid?ref_type=heads
The build script is a windows bat: https://gitlab.com/thesmog358/tbblue/-/blob/master/src/c/DotCommands/playvid/build.bat?ref_type=heads
The first step compiles some code destined to main memory at 0x8000 (the main binary of a dot program is normally located at 0x2000).
The second step makes the final dot command taking the object file output from the first step and combining with more c and asm files going to the main binary at 0x2000.
About the binary output – this type of output is a dotn, something defined by z88dk as a dot command larger than 8K. What z88dk does is generate the first 8K as normal and then it appends extra banks in 8K (or 16K? can’t recall) blocks. So the dot command itself will be larger than 8K. NextZXOS only loads the first 8K but the crt code that z88dk provides will load the rest into memory banks from the stuff appended to the 8K.
The next steps are building the asm players individually. These players are appended to the dot command and the dot command determines which player to use, then loads it into memory before playing the video.
Thanks! This is incredibly useful. And rather timely. I was just wrapping my head around banking and this filled in a bunch of gaps. I am now happily swapping code and data in and out of pages and it all works beautifully.
I do have one question though. The article says “PAGEs have no checks. You can add as much as you want to a PAGE and the data will just spill to following pages. “.
I am trying to do exactly this. I have a big chunk of data that would span multiple consecutive pages, I want to address this at 0xe000, and page in the required block of 8k myself when required. I tried just including the big binary file in my .asm. The compiler complains about “segment overflow” though.
My current workaround is to split the big blob into 8k pages, each with its own .asm file. But this is not ideal. Can anyone say if I can have one big lump of data, included by one .asm file that takes advantage of this “spill” when using PAGE?
Hi,
I just tried modifying an .asm file in my project to temporarily include a 30kb binary file (.\bin\temp.bin) and the project compiled and ran okay. Was unable to recreate the “segment overflow” error.
— .asm file
SECTION PAGE_81
org 0xe000
PUBLIC _reached2
; Path finding map – 50 * 50 array of 3 bytes
._reached2
ds 50*50*3
BINARY “.\bin\temp.bin”
Hi, thanks for the great article. Having all this information in one place is very helpful and has validated a lot of cobbled togther assumptions from various sources that I have been using.
The one thing I really wish I could do is statically initialise a global C struct to define (lots of complex) reference data and place that struct/data in a specific bank. My understanding is this is not possible with sdcc as all global variables end up in the main bank i.e. the BSS segment limitation mentioned in the article ?
My workaround is to define and export an asm variable, place that .asm in the required bank and then use banked C code to create a pointer to a struct at the address of the variable and to set the struct data. Which is horrible as I end up writing a lot of code to set the data values but I can’t think of a more elegant way to do it. I cant (easily) define the data in a binary file as it is relies on enums etc.
Am I correct in my understanding it is not possible to place a statically initialised global struct variable in a specific bank using z88dk sdcc?
Thanks!