Bagpuss wrote: ↑
Sun May 13, 2018 1:19 pm
From what I have read using zcc to compile banked c code isn't the way to go.
When you start banking, it's important to keep in mind:
Where the stack is. Are you banking it out? See the REGISTER_SP pragma for setting the initial stack location of a program.
When your banked code runs, can it see the variables or data it operates on in the current 64k configuration?
If you need to be compatible with NextOS, stay away from NextOS's memory in 16k banks 0-8, except for regions of memory traditionally available on the spectrum. Even without NextOS compatibility, you can still use the esxdos api for disk functions.
In C, the assignment to sections (banks, pages, divmmc memory, resources, etc) happens on a per-file basis since you give the section assignments on the compile line, just as you are doing. In asm, there is a directive "SECTION" that allows you to switch sections within a single file.
The C compiler generates code and data into four sections with these as defaults:
The code section holds read-only code. All compiled code goes here.
Constant data. These are variables declared as const, eg "const int a = 5;", or the string part of 'unsigned char *msg = "hello world";'. For programs stored in rom, this stuff will also be placed in rom.
The data section. This holds initially non-zero ram variables, eg "int a = 2;" and self-modifying code. For programs stored in rom, the crt must keep a copy in rom that is copied into ram before the program starts.
The bss section. This holds initially zero or uninitialized variables, eg "int a;". For programs stored in rom, the crt will zero this block of ram before starting the program, using something like an LDIR.
For the zx next, you are compiling a ram model program so the crt will not initialize the data or bss sections by default - instead the produced binary will contain the right initial data when it is loaded into ram. You may want to change this if your program would like to restart from scratch without re-loading. Using a rom model compile you can get the crt to re-initialize all variables in memory so that your program can run multiple times with a clean initial state.
Anyway I list these sections because in order to change what banks stuff is placed in you have to re-assign the section the compiler uses for one or more of the destination sections listed above. Those default sections are placed in the "main binary" by the memory map z88dk supplies. What I mean by "main binary" will be described below.
So to change where code goes, you have to change the section from "code_compiler" to something else with "--codesegNAME" on the zcc compile line, eg "--codesegBANK_6"
To change where the constant data goes, you have to change the section from "rodata_compiler" to something else with "--constsegNAME" on the zcc compile line, eg "--constsegBANK_6"
The data and bss sections can only be changed when using sccz80 (-clib=new). zsdcc cannot change those sections yet (-clib=sdcc_iy or -clib=sdcc_ix). For now, I'd recommend defining bss or data variables in assembler so that you can place them where you want.
Here's an example:
C program contains this:
Code: Select all
extern unsigned int a;
extern int b;
The "extern" says this variable is defined somewhere else. That somewhere else will be assembler:
Code: Select all
_a: defw 10
_b: defw -10
The extra leading underscore is necessary because the c compilers add it during translation to asm.
The program is compiled by listing both files on the compile line:
zcc +zxn .... foo.c foo.asm ...
The SECTION directive tells the assembler where something is going in the memory map, the PUBLIC directive makes the symbol visible to all files (if they import it with EXTERN). The "extern" in the c program locates this symbol from the global namespace.
Now something about the memory spaces in the zx next.
z88dk defines four main memory spaces:
This maps to the 8k memory pages in divmmc memory.
This holds resources, which is stuff you only want available from disk. We will be using this to add data to a single executable file which your program would read as needed from disk.
These are different views of the same ram available on the zx next. BANK divides all the memory into 16k chunks and is consistent with the 128k spectrum's banking model. Eg, ports 7ffd/dffd allow you to place a 16k memory bank in the top 16k of memory from 0xc000 to 0xffff. PAGE divides the same
memory into 8k pages. This is consistent with how the zx next's mmu paging works which allows you to put any page into any of the z80's eight physical 8k slots. Because they refer to the same memory PAGE_N, corresponding to 8k page N, refers to the same physical region in memory as 16k BANK_N/2 (N divided by 2, odd page number means upper half of 16k bank).
Although BANK and PAGE refer to the same memory, z88dk treats them differently. z88dk enforces 16k limits on BANKs. So if you put anything into BANK_6, it is not allowed to overflow the 16k boundary. This is consistent with 128k spectrum banking. Only one bank can be seen at a time in address range 0xc000-0xffff and it cannot exceed 16k. Because the zx next memory space is actually divided into 8k slots, z88dk also allows the 16k BANKs to be split into 8k halves called low (L) and high (H). So BANK_6 is a 16k bank with BANK_6_L and BANK_6_H corresponding to the bottom 8k of BANK_6 and the top 8k of BANK_6 respectively. Again, z88dk will enforce 8k sizes on BANK_6_L and BANK_6_H.
PAGEs have no restrictions applied to them at all. If you assign to PAGE_100 and you exceed 8k, any spillover will continue into PAGE_101.
So it is up to you to use BANK or PAGE depending on your needs. Choose BANK to enforce page boundaries and PAGE if you don't care about page boundaries.
z88dk will notify you if the stuff you place in memory overlaps.
Lastly, there is the question of ORG. By default, z88dk sets up the 128k memory model. That is all banks are ORGed to 0xc000 and pages, which divide the banks into 8k halves, are assigned ORG of 0xc000 for even pages and 0xe000 for odd pages.
However because of the zx next's mmu, there is a difference between placing stuff into a physical memory bank and where you might see that stuff in the z80's 64k memory space. So you may want to put items A & B into PAGE_50 but you may want to see item A in mmu0 (address 0-0x1fff) and you may want to see item B in mmu6 (48k-56k). The org for item A will be somewhere in 0-0x1fff but the org for item B will be somewhere in 48k-56k and yet both will be physically placed in the same 8k PAGE_50.
z88dk can handle this scenario but a more common concrete one is you want everything in PAGE_50 to org at 0 so that your program can place page 50 into mmu 0 before accessing the page. You would do this by changing the ORG address of PAGE_50 away from the default. This is done with a pragma "#pragma output CRT_ORG_PAGE_50 = 0". Then anything placed in that page will have org 0 and your program can put page 50 into mmu0 to run that code or read data from there.
Note that for data, the exact org address may not matter. There are functions in the library that will take an assemble address and relocate it into a different mmu slot. So if you have org-agnostic data assembled for mmu6, you can still access it from mmu0 if you modify the address to look at the same offset in mmu0 as originally orged in mmu6. This is what zxn_addr_in_mmu(mmu,addr)
does. Place your 8k page containing data into any mmu and then change the address you have for it to point into that mmu with this function.
I think I've gone far enough here and probably further than you wanted to because I think you're still thinking in terms of 128k banking? In that case, simply assigning to BANK_6 or BANK_6_L/BANK_6_H for 8k splits of BANK_6 will org your code in the top 16k and is ready for banking using legacy port 7ffd/dffd or mmu6,mmu7.
There is much more flexibility if you use 8k pages and mmu to place pages in any part of the z80's address space.