mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
gb: added banking support in sdcc 3.5
This commit is contained in:
1 parent
d21d112fa2
commit
937f6dbf84
8 files changed
+294
-17
No files matched your search
@@ -0,0 +1,18 @@
|
||||
// Everything in this file goes in ROM bank 1.
|
||||
#pragma bank 1
|
||||
|
||||
#include <stdint.h>
|
||||
#include "gb/types.h"
|
||||
#include "gb/hardware.h"
|
||||
#include "gb/gb.h"
|
||||
|
||||
const char message1[] = "HELLO FROM BANK 1";
|
||||
|
||||
void get_message1(char* buf) __banked {
|
||||
const char* p = message1;
|
||||
while ((*buf++ = *p++));
|
||||
}
|
||||
|
||||
uint8_t which_bank1(void) __banked {
|
||||
return _current_bank;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Everything in this file goes in ROM bank 2.
|
||||
#pragma bank 2
|
||||
|
||||
#include <stdint.h>
|
||||
#include "gb/types.h"
|
||||
#include "gb/hardware.h"
|
||||
#include "gb/gb.h"
|
||||
|
||||
const char message2[] = "HELLO FROM BANK 2";
|
||||
|
||||
void get_message2(char* buf) __banked {
|
||||
const char* p = message2;
|
||||
while ((*buf++ = *p++));
|
||||
}
|
||||
|
||||
uint8_t which_bank2(void) __banked {
|
||||
return _current_bank;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
ROM bank switching for Game Boy.
|
||||
Code and data in bank1.c and bank2.c live in switchable
|
||||
ROM banks, mapped at 0x4000-0x7FFF one bank at a time.
|
||||
A __banked function call switches banks, runs the function,
|
||||
and switches back.
|
||||
|
||||
The ROM grows to fit the highest bank, and the build sets
|
||||
the ROM size in the header. We pick an MBC5 mapper here;
|
||||
the build defaults to MBC5 when a ROM has banks.
|
||||
*/
|
||||
|
||||
//#link "gb/sfr.sgb"
|
||||
//#link "gb/crt0.sgb"
|
||||
//#link "bank1.c"
|
||||
//#link "bank2.c"
|
||||
//#resource "gb/global.sgb"
|
||||
//#symbol ld GB_MAPPER=0x19
|
||||
|
||||
#include <stdint.h>
|
||||
#include "gb/types.h"
|
||||
#include "gb/hardware.h"
|
||||
#include "gb/gb.h"
|
||||
#include "gbtext.h"
|
||||
|
||||
// defined in bank1.c and bank2.c
|
||||
void get_message1(char* buf) __banked;
|
||||
void get_message2(char* buf) __banked;
|
||||
uint8_t which_bank1(void) __banked;
|
||||
uint8_t which_bank2(void) __banked;
|
||||
|
||||
// banked data is only readable while its bank is mapped,
|
||||
// so banked functions copy it into RAM for us
|
||||
char buf[20];
|
||||
|
||||
void main(void) {
|
||||
DISPLAY_OFF;
|
||||
BGP_REG = 0xE4;
|
||||
font_init();
|
||||
|
||||
put_str(2, 2, "BANK SWITCHING");
|
||||
|
||||
get_message1(buf);
|
||||
put_str(2, 5, buf);
|
||||
|
||||
get_message2(buf);
|
||||
put_str(2, 7, buf);
|
||||
|
||||
// _current_bank tracks the mapped bank
|
||||
put_str(2, 10, "BANK 1 SAYS");
|
||||
put_char(15, 10, '0' + which_bank1());
|
||||
put_str(2, 12, "BANK 2 SAYS");
|
||||
put_char(15, 12, '0' + which_bank2());
|
||||
|
||||
SHOW_BKG;
|
||||
DISPLAY_ON;
|
||||
while (1) {
|
||||
wait_vbl_done();
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,30 @@ the top of the file:
|
||||
|
||||
Either pragma slows builds but produces faster and smaller code.
|
||||
|
||||
### Game Boy ROM banks
|
||||
|
||||
On the Game Boy, `#pragma bank N` at the top of a file puts its code and
|
||||
constant data in ROM bank *N*, which the CPU sees at `$4000-$7FFF`. Link
|
||||
the file from your main file, and call its functions through `__banked`
|
||||
prototypes. Each call switches to the function's bank and back again:
|
||||
|
||||
~~~c
|
||||
// bank1.c
|
||||
#pragma bank 1
|
||||
void say_hello(char* buf) __banked { ... }
|
||||
|
||||
// main.c
|
||||
//#link "bank1.c"
|
||||
//#symbol ld GB_MAPPER=0x19
|
||||
void say_hello(char* buf) __banked;
|
||||
~~~
|
||||
|
||||
The ROM grows to fit the highest bank, and the build writes the ROM size
|
||||
into the header. If `GB_MAPPER` is still 0 (ROM only), the build selects
|
||||
MBC5. A bank holds 16 KB; overflowing it is a build error. Banked data
|
||||
can only be read while its bank is mapped, so copy it out from a
|
||||
`__banked` function. See the *ROM Bank Switching* preset.
|
||||
|
||||
## File extensions
|
||||
|
||||
The extension of your source file determines the tool used to assemble
|
||||
|
||||
@@ -16,6 +16,7 @@ const GB_PRESETS: Preset[] = [
|
||||
{ id: 'galaxy.c', name: 'Window + Sprites' },
|
||||
{ id: 'gbdecompress.c', name: 'Decompression' },
|
||||
{ id: 'music.c', name: 'Music Player' },
|
||||
{ id: 'banking.c', name: 'ROM Bank Switching' },
|
||||
{ id: 'siegegame.c', name: 'Siege Game' },
|
||||
{ id: 'climber.c', name: 'Climber Game' },
|
||||
{ id: 'chase.c', name: "Shiru's Chase Game" },
|
||||
|
||||
@@ -391,7 +391,8 @@ export var PLATFORM_PARAMS = {
|
||||
arch: 'gbz80',
|
||||
code_start: 0x0, // ROM starts @ 0x0, header @ 0x100, etc.
|
||||
codeseg_start: 0x200, // _CODE area starts here
|
||||
rom_size: 0x8000,
|
||||
rom_size: 0x8000, // grows to fit the highest bank
|
||||
rom_banking: { size: 0x4000, window: 0x4000 }, // #pragma bank N -> _CODE_N @ 0xN4000
|
||||
data_start: 0xc0a0,
|
||||
data_size: 0x1f60,
|
||||
stack_end: 0xe000,
|
||||
|
||||
+106
-16
@@ -15,45 +15,88 @@ function hexToArray(s, ofs) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
function parseIHX(ihx: string, rom_start: number, rom_size: number, errors: WorkerError[]) {
|
||||
/**
|
||||
* Banked ROM layout, following the GBDK/devkitSMS convention: the linker places
|
||||
* bank N's area at the virtual address (N << 16) | window, and the ROM image
|
||||
* holds bank N at file offset N * size.
|
||||
*/
|
||||
export interface ROMBanking {
|
||||
size: number; // bytes per bank (power of 2)
|
||||
window: number; // CPU address the switchable bank is mapped at
|
||||
}
|
||||
|
||||
/** Linker `-b` args for each `_CODE_N` area (N > 0) the object files declare. */
|
||||
export function bankedAreaArgs(rels: string[], banking: ROMBanking): string[] {
|
||||
let banks = new Set<number>();
|
||||
for (let rel of rels) {
|
||||
let re = /^A _CODE_(\d+) /gm, m;
|
||||
while ((m = re.exec(rel))) {
|
||||
let n = parseInt(m[1]);
|
||||
if (n > 0) banks.add(n);
|
||||
}
|
||||
}
|
||||
let args = [];
|
||||
for (let n of Array.from(banks).sort((a, b) => a - b))
|
||||
args.push('-b', `_CODE_${n}=0x${((n << 16) | banking.window).toString(16)}`);
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Intel HEX to a ROM image of rom_size bytes. With banking, records
|
||||
* above 64 KB are placed by bank, and the image grows to the next power of 2
|
||||
* that holds the highest bank.
|
||||
*/
|
||||
export function parseIHX(ihx: string, rom_start: number, rom_size: number, errors: WorkerError[], banking?: ROMBanking) {
|
||||
var output = new Uint8Array(new ArrayBuffer(rom_size));
|
||||
var high_size = 0;
|
||||
var upper = 0; // from type 04 (extended linear address) records
|
||||
for (var s of ihx.split("\n")) {
|
||||
if (s[0] == ':') {
|
||||
var arr = hexToArray(s, 1);
|
||||
var count = arr[0];
|
||||
var offset = (arr[1] << 8) + arr[2] - rom_start;
|
||||
var address = upper + (arr[1] << 8) + arr[2];
|
||||
var offset = address - rom_start;
|
||||
var rectype = arr[3];
|
||||
//console.log(rectype,address.toString(16),count,arr);
|
||||
if (rectype == 0) {
|
||||
if (banking && address >= 0x10000) {
|
||||
let bank = address >>> 16;
|
||||
let low = address & 0xffff;
|
||||
if (low < banking.window || low + count > banking.window + banking.size) {
|
||||
errors.push({line:0, msg:`Bank ${bank} overflows its 0x${banking.size.toString(16)}-byte window at 0x${low.toString(16)}`});
|
||||
continue;
|
||||
}
|
||||
offset = bank * banking.size + low - banking.window;
|
||||
if (offset + count > output.length) {
|
||||
let newsize = output.length;
|
||||
while (newsize < offset + count) newsize *= 2;
|
||||
let grown = new Uint8Array(newsize);
|
||||
grown.set(output);
|
||||
output = grown;
|
||||
}
|
||||
}
|
||||
// The linker also emits records for whatever it placed outside
|
||||
// the ROM -- initialized data, or a GSINIT area that landed in
|
||||
// the _DATA area's RAM. Those bytes are not part of the ROM
|
||||
// image; reading past the end of output to check them yields
|
||||
// undefined, which used to be reported as an overlap.
|
||||
if (offset < 0 || offset + count > rom_size) {
|
||||
console.log(`skipping IHX record outside ROM: 0x${offset.toString(16)} +${count}`);
|
||||
if (offset < 0 || offset + count > output.length) {
|
||||
console.log(`skipping IHX record outside ROM: 0x${address.toString(16)} +${count}`);
|
||||
continue;
|
||||
}
|
||||
if (output[offset] !== 0) {
|
||||
errors.push({line:0,msg:`IHX overlap offset 0x${(offset).toString(16)}`});
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
var b = arr[4 + i];
|
||||
output[i + offset] = b;
|
||||
output[i + offset] = arr[4 + i];
|
||||
}
|
||||
if (i + offset > high_size) high_size = i + offset;
|
||||
} else if (rectype == 1) {
|
||||
break;
|
||||
} else if (rectype == 4) {
|
||||
upper = ((arr[4] << 8) | arr[5]) << 16;
|
||||
} else {
|
||||
console.log(s); // unknown record type
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: return ROM anyway?
|
||||
if (high_size > rom_size) {
|
||||
//errors.push({line:0, msg:"ROM size too large: 0x" + high_size.toString(16) + " > 0x" + rom_size.toString(16)});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -177,7 +220,27 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
// //#symbol ld (sdldz80 uses -g sym=expr) and //#flag ld
|
||||
args.push.apply(args, linkSymbolArgs('sdldz80', params.symbols && params.symbols.linker));
|
||||
args.push.apply(args, extraArgsFor('sdldz80', params.buildArgs));
|
||||
args.push.apply(args, step.args);
|
||||
var objargs = step.args;
|
||||
if (params.rom_banking) {
|
||||
// place each #pragma bank N area, unless a //#flag ld already did
|
||||
let banked = objargs.filter((fn) => fn.endsWith('.rel') && bankedAreaArgs([getWorkFileAsString(fn)], params.rom_banking).length);
|
||||
let bargs = bankedAreaArgs(banked.map(getWorkFileAsString), params.rom_banking);
|
||||
for (let i = 0; i < bargs.length; i += 2) {
|
||||
let area = bargs[i + 1].split('=')[0] + '=';
|
||||
if (!args.some((a) => a.startsWith(area)))
|
||||
args.push(bargs[i], bargs[i + 1]);
|
||||
}
|
||||
// Link banked objects just before the last bank 0 object. The
|
||||
// linker writes an IHX extended address record when it flushes
|
||||
// buffered data, using the address width of the module it is
|
||||
// reading at that moment; the XL2 (16-bit) libraries come last, so
|
||||
// the last object's upper address is lost -- harmless for bank 0.
|
||||
// The first objects stay first: they set the area order crt0 needs.
|
||||
let rest = objargs.filter((fn) => !banked.includes(fn));
|
||||
if (banked.length && rest.length)
|
||||
objargs = rest.slice(0, -1).concat(banked, rest.slice(-1));
|
||||
}
|
||||
args.push.apply(args, objargs);
|
||||
//console.log(args);
|
||||
execMain(step, LDZ80, args);
|
||||
if (errors.length) {
|
||||
@@ -191,7 +254,7 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
if (!anyTargetChanged(step, ["main.ihx", "main.noi"]))
|
||||
return;
|
||||
// parse binary file
|
||||
var binout = parseIHX(hexout, params.rom_start !== undefined ? params.rom_start : params.code_start, params.rom_size, errors);
|
||||
var binout = parseIHX(hexout, params.rom_start !== undefined ? params.rom_start : params.code_start, params.rom_size, errors, params.rom_banking);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
}
|
||||
@@ -240,8 +303,14 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// gameboy: compute checksum
|
||||
// gameboy: fix up header for the final ROM size, compute checksum
|
||||
if (step.params.arch === 'gbz80') {
|
||||
if (binout.length > 0x8000) {
|
||||
// ROM size code n means 32 KB << n
|
||||
binout[0x148] = Math.log2(binout.length / 0x8000);
|
||||
// a ROM-only cart can't switch banks; MBC5 accepts MBC1-style bank writes
|
||||
if (binout[0x147] === 0) binout[0x147] = 0x19;
|
||||
}
|
||||
var checksum = 0;
|
||||
for (var address = 0x0134; address <= 0x014C; address++) {
|
||||
checksum = checksum - binout[address] - 1;
|
||||
@@ -258,6 +327,26 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sdcc 3.6.5 compiles a __banked call to `call banked_call; .dw _fn; .dw 0`,
|
||||
* with a "PENDING: bank support" comment where the bank number belongs. Supply
|
||||
* it the way later sdcc versions do: a file with `#pragma bank N` defines
|
||||
* `b_fn == N` for each function, and call sites reference `b_fn`.
|
||||
*/
|
||||
export function fixBankedCalls(asm: string): string {
|
||||
asm = asm.replace(/^(\s*\.dw\s+(_\w+)\s*\n\s*\.dw\s+)0\s*; PENDING: bank support/gm,
|
||||
(_m, head, fn) => head + 'b' + fn);
|
||||
let m = /^\s*\.area\s+_CODE_(\d+)\b/m.exec(asm);
|
||||
if (m && parseInt(m[1]) > 0) {
|
||||
let defs = [];
|
||||
let re = /^(_\w+)::/gm, f;
|
||||
while ((f = re.exec(asm)))
|
||||
defs.push(`\t.globl b${f[1]}\nb${f[1]} == ${m[1]}`);
|
||||
if (defs.length) asm += '\n' + defs.join('\n') + '\n';
|
||||
}
|
||||
return asm;
|
||||
}
|
||||
|
||||
export function compileSDCC(step: BuildStep): BuildStepResult {
|
||||
|
||||
gatherFiles(step, {
|
||||
@@ -332,6 +421,7 @@ export function compileSDCC(step: BuildStep): BuildStepResult {
|
||||
// massage the asm output
|
||||
var asmout = FS.readFile(outpath, { encoding: 'utf8' });
|
||||
asmout = " .area _HOME\n .area _CODE\n .area _INITIALIZER\n .area _DATA\n .area _INITIALIZED\n .area _BSEG\n .area _BSS\n .area _HEAP\n" + asmout;
|
||||
if (isGBZ80) asmout = fixBankedCalls(asmout);
|
||||
putWorkFile(outpath, asmout);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import { parseIHX, bankedAreaArgs, fixBankedCalls } from "../../src/worker/tools/sdcc";
|
||||
|
||||
// Intel HEX record with checksum
|
||||
function rec(type: number, addr: number, data: number[]): string {
|
||||
let bytes = [data.length, addr >> 8, addr & 0xff, type, ...data];
|
||||
let sum = bytes.reduce((a, b) => a + b, 0);
|
||||
bytes.push((-sum) & 0xff);
|
||||
return ':' + bytes.map((b) => b.toString(16).padStart(2, '0').toUpperCase()).join('');
|
||||
}
|
||||
const GB_BANKING = { size: 0x4000, window: 0x4000 };
|
||||
|
||||
describe("sdcc ROM banking", function () {
|
||||
|
||||
it("places type 04 records by bank and grows the ROM", function () {
|
||||
const ihx = [
|
||||
rec(0, 0x0100, [0x11]),
|
||||
rec(4, 0, [0x00, 0x01]), rec(0, 0x4000, [0x22]),
|
||||
rec(4, 0, [0x00, 0x05]), rec(0, 0x4001, [0x55]),
|
||||
rec(4, 0, [0x00, 0x00]), rec(0, 0x0200, [0x33]),
|
||||
rec(1, 0, []),
|
||||
].join('\n');
|
||||
const errors = [];
|
||||
const rom = parseIHX(ihx, 0, 0x8000, errors, GB_BANKING);
|
||||
assert.deepStrictEqual(errors, []);
|
||||
assert.strictEqual(rom.length, 0x20000); // bank 5 -> 0x14001, rounded up to 128 KB
|
||||
assert.strictEqual(rom[0x100], 0x11);
|
||||
assert.strictEqual(rom[0x4000], 0x22);
|
||||
assert.strictEqual(rom[0x14001], 0x55);
|
||||
assert.strictEqual(rom[0x200], 0x33);
|
||||
});
|
||||
|
||||
it("reports a bank that overflows its window", function () {
|
||||
const ihx = [rec(4, 0, [0x00, 0x01]), rec(0, 0x8000, [0x22]), rec(1, 0, [])].join('\n');
|
||||
const errors = [];
|
||||
parseIHX(ihx, 0, 0x8000, errors, GB_BANKING);
|
||||
assert.strictEqual(errors.length, 1);
|
||||
assert.match(errors[0].msg, /Bank 1 overflows/);
|
||||
});
|
||||
|
||||
it("keeps the fixed size without banking", function () {
|
||||
const ihx = [rec(0, 0x0000, [0x01]), rec(4, 0, [0x00, 0x01]), rec(0, 0x0000, [0x02]), rec(1, 0, [])].join('\n');
|
||||
const rom = parseIHX(ihx, 0, 0x8000, [], undefined);
|
||||
assert.strictEqual(rom.length, 0x8000);
|
||||
assert.strictEqual(rom[0], 0x01); // record above 64 KB is skipped, not folded onto 0
|
||||
});
|
||||
|
||||
it("generates -b args for #pragma bank areas", function () {
|
||||
const rels = ["XL3\nA _CODE size 0 flags 0\nA _CODE_2 size 1F flags 0\n", "A _CODE_1 size 4 flags 0\nA _CODE_0 size 0 flags 0\n"];
|
||||
assert.deepStrictEqual(bankedAreaArgs(rels, GB_BANKING),
|
||||
['-b', '_CODE_1=0x14000', '-b', '_CODE_2=0x24000']);
|
||||
});
|
||||
|
||||
it("fills in the bank of a __banked call and defines b_ symbols", function () {
|
||||
const caller = "\tcall\tbanked_call\n\t.dw\t_foo\n\t.dw 0 ; PENDING: bank support\n";
|
||||
assert.ok(fixBankedCalls(caller).includes("\t.dw b_foo"));
|
||||
const callee = "\t.area _CODE_3\n_foo::\n\tret\n_bar::\n\tret\n";
|
||||
const out = fixBankedCalls(callee);
|
||||
assert.ok(out.includes("b_foo == 3"));
|
||||
assert.ok(out.includes("b_bar == 3"));
|
||||
// bank 0 code gets no b_ symbols
|
||||
assert.ok(!fixBankedCalls("\t.area _CODE\n_foo::\n").includes("b_foo"));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user