Add full compiler toolchain, libc, examples and reference docs
First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.
What's in here:
bin/sprinter-cc — driver script invoking SDCC + linker + mkexe
libc/ — Sprinter-specific libc layer over ESTEX/BIOS
(conio, gfx, io, mem, stdio + headers)
runtime/ — crt0 variants (default/small/banked/minimal)
+ heap + bank trampolines
toolchain/ — mkexe (SprintEXE packer, C + tests)
examples/ — 30 demo programs (gfx, file I/O, env, time, …)
lib/Makefile — builds the libc archive (sprinter.lib)
docs/ — converted Sprinter manuals + asm reference samples
third_party/ — solid-c reference compiler dump + sdcc setup script
release_docs/ — packaging / release notes
gitignore overhaul:
• Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
on macOS APFS). Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
• Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
INPUT fixtures, not build outputs.
• Collapse the duplicated SDCC/C/Sdcc sections into one section per
concern (build outputs / vendored / OS-junk).
• Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parse an SDCC .map file produced by sdldz80 and verify that every named
|
||||
bank fits inside its 16 KB window.
|
||||
|
||||
The Sprinter toolchain expects each `_BANKn` area (with n >= 1) to occupy
|
||||
at most 16384 bytes — that is the size of CPU window 3 where banked code
|
||||
runs at execution time. The SDCC linker itself does not enforce this
|
||||
limit, so we catch it post-link.
|
||||
|
||||
We also surface the HOME budget: anything left between the end of _CODE
|
||||
and the start of window 2 (0x8000) is leftover space for adding code/data
|
||||
without banking.
|
||||
|
||||
Usage: check_banks.py <path-to.map>
|
||||
|
||||
Exits non-zero with a clear message if any bank is over its limit.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
BANK_LIMIT = 16 * 1024
|
||||
# HOME upper bound depends on layout:
|
||||
# HUGE: CODE at 0x4100 (W1), HOME may spill into W2 → ceiling 0xC000
|
||||
# BIG: CODE at 0x8100 (W2), HOME stays in W2 → ceiling 0xC000
|
||||
# tiny/small: same ceiling — anything in W3 is bank territory.
|
||||
# We pick the ceiling per-image based on where _CODE lives, so we don't
|
||||
# falsely flag W2-resident code as "spilled into stack/heap".
|
||||
HOME_CEILING = 0xC000
|
||||
|
||||
|
||||
def parse_map(path):
|
||||
"""
|
||||
Returns a dict {area_name: (addr, size)} for area lines like:
|
||||
_CODE 00004100 00000313 = ...
|
||||
"""
|
||||
line_re = re.compile(r"^\s*(_\w+)\s+([0-9A-Fa-f]{8})\s+([0-9A-Fa-f]{8})\s*=")
|
||||
areas = {}
|
||||
with open(path) as f:
|
||||
for ln in f:
|
||||
m = line_re.match(ln)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group(1)
|
||||
addr = int(m.group(2), 16)
|
||||
size = int(m.group(3), 16)
|
||||
areas[name] = (addr, size)
|
||||
return areas
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: check_banks.py <path-to.map>")
|
||||
|
||||
areas = parse_map(sys.argv[1])
|
||||
|
||||
fails = []
|
||||
bank_names = sorted(
|
||||
n for n in areas if re.fullmatch(r"_BANK\d+", n)
|
||||
)
|
||||
|
||||
for name in bank_names:
|
||||
addr, size = areas[name]
|
||||
pct = size * 100.0 / BANK_LIMIT
|
||||
marker = "OK"
|
||||
if size > BANK_LIMIT:
|
||||
marker = "OVERFLOW"
|
||||
fails.append((name, size))
|
||||
print(f" {name:<12} @ 0x{addr:08X} size {size:>5} / {BANK_LIMIT} ({pct:5.1f}%) {marker}")
|
||||
|
||||
# HOME budget — _CODE can land in W1 (huge/small) or W2 (big/tiny).
|
||||
if "_CODE" in areas:
|
||||
addr, size = areas["_CODE"]
|
||||
end = addr + size
|
||||
budget_remaining = HOME_CEILING - end
|
||||
which_window = "W2 (0x8000-0xBFFF)" if addr >= 0x8000 else "HOME (0x4100-0xBFFF)"
|
||||
print(f" _CODE @ 0x{addr:08X} size {size:>5} → ends at 0x{end:04X}, "
|
||||
f"{which_window} has {budget_remaining} bytes free before 0x{HOME_CEILING:04X}")
|
||||
if budget_remaining < 0:
|
||||
print(f" ERROR: _CODE extends past 0x{HOME_CEILING:04X} (stack/heap territory)")
|
||||
fails.append(("_CODE", size))
|
||||
|
||||
if fails:
|
||||
print()
|
||||
for name, size in fails:
|
||||
print(f" {name} too big: {size} bytes (limit {BANK_LIMIT})")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# make_release.sh — pack a redistributable Sprinter C Compiler release tarball.
|
||||
#
|
||||
# Whitelist-based: only files explicitly listed below go into the tarball.
|
||||
# Anything else (internal dev tools, research docs, build artefacts, mac
|
||||
# xattr sidecars, .git, .claude memory, etc.) is excluded by construction.
|
||||
#
|
||||
# Output: build/sprinter-c-v<VERSION>-<host>.tar.gz with:
|
||||
# - bin/sprinter-cc : compiler driver
|
||||
# - toolchain/mkexe/ : .ihx → .exe utility (prebuilt for host)
|
||||
# - toolchain/check_banks.py : bank size checker used by sprinter-cc
|
||||
# - runtime/ : crt0 + bank/heap support
|
||||
# - libc/include/ : public headers
|
||||
# - libc/{io,stdio,mem,gfx}/ : libc sources (assembled into sprinter.lib)
|
||||
# - lib/Makefile + lib/sprinter.lib : prebuilt library + recipe to rebuild
|
||||
# - examples/ : 27 ready-to-build programs
|
||||
# - third_party/sdcc/ : vendored SDCC 4.5
|
||||
# - release_docs/ → docs/ : user-facing documentation
|
||||
# - examples.mk-style root Makefile, README, RELEASE_NOTES, LICENSE
|
||||
#
|
||||
# NOT included (developer-only):
|
||||
# - docs/ (research notes, original PetersPlus reference material)
|
||||
# - third_party/solid-c/ (reference for Solid-C compat target)
|
||||
# - mame/ (large emulator + Sprinter HDD/CD images)
|
||||
# - .git/, .claude/, .DS_Store, ._* (macOS xattrs)
|
||||
# - this script itself (toolchain/make_release.sh)
|
||||
# - any build artefacts (.sprinter-cc-*, *.rel, *.ihx, *.lk, *.map, ...)
|
||||
#
|
||||
# Usage: ./toolchain/make_release.sh [VERSION]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-1.0}"
|
||||
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
|
||||
HOST_ARCH="$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m)"
|
||||
NAME="sprinter-c-v${VERSION}-${HOST_ARCH}"
|
||||
STAGE="$ROOT/build/$NAME"
|
||||
TARBALL="$ROOT/build/$NAME.tar.gz"
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
echo ">> verifying build is clean and current"
|
||||
make all >/dev/null 2>&1
|
||||
|
||||
echo ">> staging $STAGE"
|
||||
rm -rf "$STAGE"
|
||||
mkdir -p "$STAGE"
|
||||
|
||||
# --- WHITELIST: only these directories/files end up in the tarball ---
|
||||
|
||||
# Top-level metadata
|
||||
cp README.md RELEASE_NOTES.md LICENSE Makefile "$STAGE/"
|
||||
|
||||
# Toolchain
|
||||
cp -R bin runtime libc examples "$STAGE/"
|
||||
# toolchain/ — only mkexe (prebuilt + sources) and check_banks.py
|
||||
mkdir -p "$STAGE/toolchain/mkexe"
|
||||
cp toolchain/mkexe/mkexe "$STAGE/toolchain/mkexe/"
|
||||
cp toolchain/mkexe/mkexe.c "$STAGE/toolchain/mkexe/"
|
||||
cp toolchain/mkexe/Makefile "$STAGE/toolchain/mkexe/"
|
||||
[[ -f toolchain/mkexe/check.sh ]] && cp toolchain/mkexe/check.sh "$STAGE/toolchain/mkexe/"
|
||||
cp toolchain/check_banks.py "$STAGE/toolchain/"
|
||||
|
||||
# Lib — keep Makefile and the prebuilt archive only; sources are in libc/
|
||||
mkdir -p "$STAGE/lib"
|
||||
cp lib/Makefile "$STAGE/lib/"
|
||||
cp lib/sprinter.lib "$STAGE/lib/"
|
||||
|
||||
# Vendored SDCC — follow symlinks so the real content lands in the tarball
|
||||
mkdir -p "$STAGE/third_party"
|
||||
[[ -d third_party/sdcc ]] && cp -RL third_party/sdcc "$STAGE/third_party/"
|
||||
[[ -f third_party/setup-sdcc.sh ]] && cp third_party/setup-sdcc.sh "$STAGE/third_party/"
|
||||
|
||||
# User-facing docs. Source: release_docs/{en,ru}/ → docs/{en,ru}/ in tarball.
|
||||
# The internal docs/ tree (research notes, original Peters Plus material) is
|
||||
# NOT shipped.
|
||||
[[ -d release_docs ]] && cp -R release_docs "$STAGE/docs"
|
||||
|
||||
# --- Strip build artefacts that crept in via examples/ ---
|
||||
echo ">> stripping build artefacts and macOS metadata"
|
||||
find "$STAGE" \( \
|
||||
-name '.sprinter-cc-*' -o \
|
||||
-name '*.rel' -o -name '*.lst' -o -name '*.sym' -o -name '*.asm' -o \
|
||||
-name '*.ihx' -o -name '*.lk' -o -name '*.map' -o -name '*.noi' -o \
|
||||
-name '*.exe' -o \
|
||||
-name '.DS_Store' -o -name '._*' \
|
||||
\) -print0 | xargs -0 rm -rf
|
||||
|
||||
# Bonus: clean any *_test or scratch files I might have left in libc dirs
|
||||
find "$STAGE/libc" -name 'build' -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# --- Tarball ---
|
||||
echo ">> packaging $TARBALL"
|
||||
cd "$ROOT/build"
|
||||
COPYFILE_DISABLE=1 tar --no-mac-metadata -czf "$TARBALL" "$NAME" 2>/dev/null || \
|
||||
COPYFILE_DISABLE=1 tar -czf "$TARBALL" "$NAME"
|
||||
rm -rf "$NAME"
|
||||
|
||||
ls -lh "$TARBALL"
|
||||
echo
|
||||
echo "Release ready: $TARBALL"
|
||||
echo
|
||||
echo "Try it:"
|
||||
echo " tar xzf $(basename "$TARBALL")"
|
||||
echo " cd $NAME"
|
||||
echo " make all"
|
||||
echo " bin/sprinter-cc -o hello.exe examples/hello/hello.c"
|
||||
@@ -0,0 +1,15 @@
|
||||
CFLAGS ?= -O2 -Wall -Wextra -std=c99 -pedantic
|
||||
CC ?= cc
|
||||
|
||||
all: mkexe
|
||||
|
||||
mkexe: mkexe.c
|
||||
$(CC) $(CFLAGS) -o $@ mkexe.c
|
||||
|
||||
check: mkexe
|
||||
./run-tests.sh
|
||||
|
||||
clean:
|
||||
rm -f mkexe tests/*.actual tests/*.exe
|
||||
|
||||
.PHONY: all check clean
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* mkexe — convert a flat binary or Intel HEX image into a Sprinter ESTEX .EXE.
|
||||
*
|
||||
* Single-image mode (no banks):
|
||||
* +00 3 bytes "EXE"
|
||||
* +03 1 byte version (0)
|
||||
* +04 4 bytes offset to image (0x00000200)
|
||||
* +08 2 bytes loader size (0 — whole file at once)
|
||||
* +0A 6 bytes reserved (0)
|
||||
* +10 2 bytes load address
|
||||
* +12 2 bytes start (PC)
|
||||
* +14 2 bytes initial SP
|
||||
* +16 490 bytes reserved (0)
|
||||
* +200 ... contiguous image bytes
|
||||
*
|
||||
* Multi-bank mode (detected from IHX ELA records):
|
||||
* header.loader = HOME image size (so ESTEX loads only HOME and keeps the
|
||||
* .EXE file open with the handle in IX-3; crt0_banked.s
|
||||
* then reads each bank with ESTEX READ)
|
||||
* file layout: header (512) + HOME image + bank1 (16KB) + bank2 (16KB) + ...
|
||||
*
|
||||
* IHX convention from SDCC `-Wl-b _BANKn=0xNC000`:
|
||||
* ELA = 0x00 → HOME (low16 = 0x4100..0x7FFF range)
|
||||
* ELA = 0x01 → BANK1 (low16 = 0xC000..0xFFFF)
|
||||
* ELA = 0x02 → BANK2
|
||||
* ...
|
||||
*
|
||||
* Build: cc -O2 -Wall -Wextra -std=c99 -o mkexe mkexe.c
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#define EXE_HEADER_SIZE 512
|
||||
#define SEGMENT_SIZE 0x10000u
|
||||
#define BANK_SIZE 0x4000u /* 16 KB per bank */
|
||||
#define DEFAULT_BANK_BASE 0xC000u /* HUGE mode (banks live in window 3)*/
|
||||
#define BIG_BANK_BASE 0x4000u /* BIG mode (banks live in window 1)*/
|
||||
#define MAX_BANKS 15
|
||||
#define HOME_SEG 0
|
||||
|
||||
#define DEFAULT_STACK 0xBFFEu
|
||||
#define DEFAULT_LOAD 0x4100u
|
||||
|
||||
typedef struct {
|
||||
uint8_t bytes[SEGMENT_SIZE];
|
||||
uint8_t present[SEGMENT_SIZE];
|
||||
uint32_t lo;
|
||||
uint32_t hi;
|
||||
int any;
|
||||
} segment_t;
|
||||
|
||||
static segment_t segments[MAX_BANKS + 1];
|
||||
|
||||
static void segments_init(void) {
|
||||
for (int i = 0; i <= MAX_BANKS; i++) {
|
||||
memset(segments[i].bytes, 0, sizeof(segments[i].bytes));
|
||||
memset(segments[i].present, 0, sizeof(segments[i].present));
|
||||
segments[i].lo = 0xFFFFFFFFu;
|
||||
segments[i].hi = 0;
|
||||
segments[i].any = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int segment_set(unsigned seg, uint32_t addr, uint8_t v) {
|
||||
if (seg > MAX_BANKS) {
|
||||
fprintf(stderr, "mkexe: bank id %u exceeds MAX_BANKS=%d\n", seg, MAX_BANKS);
|
||||
return -1;
|
||||
}
|
||||
if (addr >= SEGMENT_SIZE) {
|
||||
fprintf(stderr, "mkexe: address 0x%X outside the 64K Z80 space\n", addr);
|
||||
return -1;
|
||||
}
|
||||
segment_t *s = &segments[seg];
|
||||
s->bytes[addr] = v;
|
||||
s->present[addr] = 1;
|
||||
if (addr < s->lo) s->lo = addr;
|
||||
if (addr > s->hi) s->hi = addr;
|
||||
s->any = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hex_nib(int c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int hex_byte(const char *p) {
|
||||
int h = hex_nib((unsigned char)p[0]);
|
||||
int l = hex_nib((unsigned char)p[1]);
|
||||
if (h < 0 || l < 0) return -1;
|
||||
return (h << 4) | l;
|
||||
}
|
||||
|
||||
static int load_ihx(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "mkexe: cannot open '%s': %s\n", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
char line[1024];
|
||||
int rc = 0, lineno = 0;
|
||||
unsigned seg = 0; /* upper 8 bits of 24-bit virtual addr */
|
||||
uint32_t para_base = 0; /* paragraph base from type-02 records */
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
lineno++;
|
||||
size_t n = strlen(line);
|
||||
while (n > 0 && (line[n-1] == '\n' || line[n-1] == '\r' || line[n-1] == ' ')) {
|
||||
line[--n] = 0;
|
||||
}
|
||||
if (n == 0) continue;
|
||||
if (line[0] != ':') {
|
||||
fprintf(stderr, "mkexe: %s:%d: expected ':' record start\n", path, lineno);
|
||||
rc = -1; break;
|
||||
}
|
||||
if (n < 11) {
|
||||
fprintf(stderr, "mkexe: %s:%d: record too short\n", path, lineno);
|
||||
rc = -1; break;
|
||||
}
|
||||
int count = hex_byte(line + 1);
|
||||
int aHi = hex_byte(line + 3);
|
||||
int aLo = hex_byte(line + 5);
|
||||
int type = hex_byte(line + 7);
|
||||
if (count < 0 || aHi < 0 || aLo < 0 || type < 0) {
|
||||
fprintf(stderr, "mkexe: %s:%d: bad hex in header\n", path, lineno);
|
||||
rc = -1; break;
|
||||
}
|
||||
if (n < (size_t)(11 + 2 * count)) {
|
||||
fprintf(stderr, "mkexe: %s:%d: truncated record\n", path, lineno);
|
||||
rc = -1; break;
|
||||
}
|
||||
unsigned sum = (unsigned)count + (unsigned)aHi + (unsigned)aLo + (unsigned)type;
|
||||
uint16_t addr = (uint16_t)((aHi << 8) | aLo);
|
||||
|
||||
if (type == 0x00) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int b = hex_byte(line + 9 + 2 * i);
|
||||
if (b < 0) { rc = -1; goto done; }
|
||||
sum += (unsigned)b;
|
||||
uint32_t a = para_base + (uint32_t)addr + (uint32_t)i;
|
||||
if (segment_set(seg, a, (uint8_t)b) < 0) { rc = -1; goto done; }
|
||||
}
|
||||
} else if (type == 0x01) {
|
||||
int cc = hex_byte(line + 9);
|
||||
if (cc < 0) { rc = -1; goto done; }
|
||||
sum += (unsigned)cc;
|
||||
if ((sum & 0xFF) != 0) {
|
||||
fprintf(stderr, "mkexe: %s:%d: bad checksum on EOF record\n", path, lineno);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
break;
|
||||
} else if (type == 0x02) {
|
||||
int bHi = hex_byte(line + 9);
|
||||
int bLo = hex_byte(line + 11);
|
||||
if (bHi < 0 || bLo < 0) { rc = -1; goto done; }
|
||||
sum += (unsigned)bHi + (unsigned)bLo;
|
||||
para_base = ((uint32_t)((bHi << 8) | bLo)) << 4;
|
||||
} else if (type == 0x04) {
|
||||
int bHi = hex_byte(line + 9);
|
||||
int bLo = hex_byte(line + 11);
|
||||
if (bHi < 0 || bLo < 0) { rc = -1; goto done; }
|
||||
sum += (unsigned)bHi + (unsigned)bLo;
|
||||
uint32_t high = ((uint32_t)bHi << 8) | (uint32_t)bLo;
|
||||
if (high > MAX_BANKS) {
|
||||
fprintf(stderr, "mkexe: %s:%d: ELA upper16=0x%04X means bank id %u, exceeds MAX_BANKS=%d\n",
|
||||
path, lineno, (unsigned)high, (unsigned)high, MAX_BANKS);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
seg = (unsigned)high;
|
||||
} else if (type == 0x03 || type == 0x05) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int b = hex_byte(line + 9 + 2 * i);
|
||||
if (b < 0) { rc = -1; goto done; }
|
||||
sum += (unsigned)b;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "mkexe: %s:%d: unsupported HEX type 0x%02X\n", path, lineno, type);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
int cc = hex_byte(line + 9 + 2 * count);
|
||||
if (cc < 0) {
|
||||
fprintf(stderr, "mkexe: %s:%d: missing checksum\n", path, lineno);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
sum += (unsigned)cc;
|
||||
if ((sum & 0xFF) != 0) {
|
||||
fprintf(stderr, "mkexe: %s:%d: checksum mismatch (sum=%02X)\n", path, lineno, sum & 0xFF);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
}
|
||||
done:
|
||||
fclose(f);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int load_bin(const char *path, uint32_t load_addr) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "mkexe: cannot open '%s': %s\n", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
uint8_t buf[4096];
|
||||
uint32_t addr = load_addr;
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (segment_set(HOME_SEG, addr++, buf[i]) < 0) { fclose(f); return -1; }
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int has_suffix(const char *s, const char *suf) {
|
||||
size_t ls = strlen(s), lf = strlen(suf);
|
||||
if (lf > ls) return 0;
|
||||
return strcasecmp(s + ls - lf, suf) == 0;
|
||||
}
|
||||
|
||||
static void usage(FILE *o) {
|
||||
fprintf(o,
|
||||
"mkexe — assemble a Sprinter ESTEX .EXE from an IHX or BIN image\n"
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
" mkexe [options] -o OUT.exe INPUT(.ihx|.bin)\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" -o FILE output .exe path (required)\n"
|
||||
" -L ADDR load address (default: lowest in .ihx, or 0x%04X for .bin)\n"
|
||||
" -E ADDR entry point / start PC (default: same as load)\n"
|
||||
" -S ADDR initial SP (default: 0x%04X)\n"
|
||||
" -p PAD padding byte for gaps in image (default: 0xFF)\n"
|
||||
" -B ADDR bank low-16 base (default: 0x%04X HUGE; pass 0x%04X BIG)\n"
|
||||
" -v verbose\n"
|
||||
" -h this help\n"
|
||||
"\n"
|
||||
"If the input .ihx contains ELA records (type 04) with high16 = N where\n"
|
||||
"1 <= N <= %d, those bytes are packed as bank N (16 KB each, base 0x%04X).\n"
|
||||
"The header.loader is then set to the HOME image size so ESTEX keeps the\n"
|
||||
"file open for crt0_banked.s to read each bank with RST 10h C=13h.\n"
|
||||
"\n"
|
||||
"Addresses accept 0x.. / $.. / decimal.\n",
|
||||
DEFAULT_LOAD, DEFAULT_STACK,
|
||||
DEFAULT_BANK_BASE, BIG_BANK_BASE,
|
||||
MAX_BANKS, DEFAULT_BANK_BASE);
|
||||
}
|
||||
|
||||
static int parse_addr(const char *s, uint32_t *out) {
|
||||
if (!s || !*s) return -1;
|
||||
char *end = NULL;
|
||||
int base = 10;
|
||||
if (s[0] == '$') { s++; base = 16; }
|
||||
else if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { s += 2; base = 16; }
|
||||
unsigned long v = strtoul(s, &end, base);
|
||||
if (end == s || (end && *end)) return -1;
|
||||
*out = (uint32_t)v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void put16_le(uint8_t *p, uint16_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFF);
|
||||
p[1] = (uint8_t)(v >> 8);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *out_path = NULL;
|
||||
const char *in_path = NULL;
|
||||
uint32_t load_addr = 0xFFFFFFFFu;
|
||||
uint32_t start_addr = 0xFFFFFFFFu;
|
||||
uint32_t stack_addr = DEFAULT_STACK;
|
||||
uint32_t pad_byte = 0xFF;
|
||||
uint32_t bank_base = DEFAULT_BANK_BASE;
|
||||
int verbose = 0;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *a = argv[i];
|
||||
if (!strcmp(a, "-h") || !strcmp(a, "--help")) { usage(stdout); return 0; }
|
||||
if (!strcmp(a, "-v")) { verbose = 1; continue; }
|
||||
if (!strcmp(a, "-o") && i + 1 < argc) { out_path = argv[++i]; continue; }
|
||||
if (!strcmp(a, "-L") && i + 1 < argc) {
|
||||
if (parse_addr(argv[++i], &load_addr) < 0) { fprintf(stderr, "mkexe: bad -L\n"); return 1; }
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(a, "-E") && i + 1 < argc) {
|
||||
if (parse_addr(argv[++i], &start_addr) < 0) { fprintf(stderr, "mkexe: bad -E\n"); return 1; }
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(a, "-S") && i + 1 < argc) {
|
||||
if (parse_addr(argv[++i], &stack_addr) < 0) { fprintf(stderr, "mkexe: bad -S\n"); return 1; }
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(a, "-p") && i + 1 < argc) {
|
||||
if (parse_addr(argv[++i], &pad_byte) < 0) { fprintf(stderr, "mkexe: bad -p\n"); return 1; }
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(a, "-B") && i + 1 < argc) {
|
||||
if (parse_addr(argv[++i], &bank_base) < 0) { fprintf(stderr, "mkexe: bad -B\n"); return 1; }
|
||||
if (bank_base + BANK_SIZE > SEGMENT_SIZE) {
|
||||
fprintf(stderr, "mkexe: -B 0x%X + 16KB exceeds 0x10000\n", bank_base);
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (a[0] == '-') { fprintf(stderr, "mkexe: unknown option '%s'\n", a); usage(stderr); return 1; }
|
||||
if (in_path) { fprintf(stderr, "mkexe: extra positional '%s'\n", a); return 1; }
|
||||
in_path = a;
|
||||
}
|
||||
|
||||
if (!in_path || !out_path) { usage(stderr); return 1; }
|
||||
|
||||
segments_init();
|
||||
|
||||
if (has_suffix(in_path, ".ihx") || has_suffix(in_path, ".hex")) {
|
||||
if (load_ihx(in_path) < 0) return 2;
|
||||
} else if (has_suffix(in_path, ".bin")) {
|
||||
if (load_addr == 0xFFFFFFFFu) load_addr = DEFAULT_LOAD;
|
||||
if (load_bin(in_path, load_addr) < 0) return 2;
|
||||
} else {
|
||||
fprintf(stderr, "mkexe: input must end with .ihx, .hex, or .bin\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
segment_t *home = &segments[HOME_SEG];
|
||||
if (!home->any) {
|
||||
fprintf(stderr, "mkexe: input has no HOME (segment 0) bytes\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (load_addr == 0xFFFFFFFFu) load_addr = home->lo;
|
||||
if (start_addr == 0xFFFFFFFFu) start_addr = load_addr;
|
||||
|
||||
if (load_addr > 0xFFFFu) { fprintf(stderr, "mkexe: load address 0x%X > 0xFFFF\n", load_addr); return 1; }
|
||||
if (start_addr > 0xFFFFu) { fprintf(stderr, "mkexe: start address 0x%X > 0xFFFF\n", start_addr); return 1; }
|
||||
if (stack_addr > 0xFFFFu) { fprintf(stderr, "mkexe: stack address 0x%X > 0xFFFF\n", stack_addr); return 1; }
|
||||
if (pad_byte > 0xFFu) { fprintf(stderr, "mkexe: pad byte 0x%X > 0xFF\n", pad_byte); return 1; }
|
||||
|
||||
if (home->lo < load_addr) {
|
||||
fprintf(stderr, "mkexe: HOME has bytes at 0x%04X below requested load 0x%04X\n",
|
||||
home->lo, load_addr);
|
||||
return 2;
|
||||
}
|
||||
|
||||
uint32_t home_size = home->hi + 1 - load_addr;
|
||||
if (load_addr + home_size > 0x10000u) {
|
||||
fprintf(stderr, "mkexe: HOME extends past 0xFFFF (load=0x%04X, size=0x%X)\n",
|
||||
load_addr, home_size);
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* Inventory banks. */
|
||||
int max_bank = 0;
|
||||
for (int i = 1; i <= MAX_BANKS; i++) {
|
||||
if (segments[i].any) {
|
||||
if (segments[i].lo < bank_base) {
|
||||
/* Bytes below the bank base inside a BANK segment almost
|
||||
certainly mean the *previous* bank overflowed 16 KB and
|
||||
bled into this 64 KB virtual slot. Tell the user clearly. */
|
||||
int prev = i - 1;
|
||||
if (prev >= 1) {
|
||||
fprintf(stderr,
|
||||
"mkexe: BANK%d appears to overflow its 16 KB limit:\n"
|
||||
" data lands at virtual 0x%X%04X (low16 < 0x%04X)\n"
|
||||
" Move some code from BANK%d's .c files into a new bank.\n",
|
||||
prev, i, segments[i].lo, bank_base, prev);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"mkexe: bank %d has bytes at 0x%04X, below the bank base 0x%04X\n",
|
||||
i, segments[i].lo, bank_base);
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
if (segments[i].hi >= bank_base + BANK_SIZE) {
|
||||
fprintf(stderr,
|
||||
"mkexe: BANK%d overflows its 16 KB limit:\n"
|
||||
" last byte at 0x%04X (limit 0x%04X)\n",
|
||||
i, segments[i].hi, bank_base + BANK_SIZE - 1);
|
||||
return 2;
|
||||
}
|
||||
if (i > max_bank) max_bank = i;
|
||||
}
|
||||
}
|
||||
|
||||
/* HOME must not extend past 0xBFFF (W3 = bank territory in HUGE,
|
||||
free in BIG but still off-limits to HOME). */
|
||||
if (home->hi >= 0xC000u) {
|
||||
fprintf(stderr,
|
||||
"mkexe: HOME image extends to 0x%04X, past window 2 end (0xBFFF).\n"
|
||||
" Code grew too big for HOME — move some .c files into a bank.\n",
|
||||
home->hi);
|
||||
return 2;
|
||||
}
|
||||
/* In BIG mode HOME starts at 0x8100 (W2). Reject if it accidentally
|
||||
overlaps the bank window in W1 (0x4000..0x7FFF) — that means the
|
||||
user passed a bad --code-loc and the image would clobber banks. */
|
||||
if (bank_base == BIG_BANK_BASE && home->lo < 0x8000u) {
|
||||
fprintf(stderr,
|
||||
"mkexe: HOME image starts at 0x%04X in BIG mode, but banks own\n"
|
||||
" 0x4000..0x7FFF — HOME must live in W2 (0x8000..0xBFFF).\n"
|
||||
" Check --code-loc / -L.\n",
|
||||
home->lo);
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* Banks must be densely numbered from 1 so the loader index matches. */
|
||||
for (int i = 1; i <= max_bank; i++) {
|
||||
if (!segments[i].any) {
|
||||
fprintf(stderr, "mkexe: bank %d is empty but bank %d exists — banks must be consecutive from 1\n",
|
||||
i, max_bank);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t loader_size = (max_bank > 0) ? home_size : 0u;
|
||||
|
||||
/* --- Write the EXE. --- */
|
||||
uint8_t header[EXE_HEADER_SIZE];
|
||||
memset(header, 0, sizeof(header));
|
||||
header[0x00] = 'E';
|
||||
header[0x01] = 'X';
|
||||
header[0x02] = 'E';
|
||||
header[0x03] = 0x00;
|
||||
header[0x04] = 0x00;
|
||||
header[0x05] = 0x02;
|
||||
header[0x06] = 0x00;
|
||||
header[0x07] = 0x00;
|
||||
put16_le(header + 0x08, (uint16_t)loader_size);
|
||||
put16_le(header + 0x10, (uint16_t)load_addr);
|
||||
put16_le(header + 0x12, (uint16_t)start_addr);
|
||||
put16_le(header + 0x14, (uint16_t)stack_addr);
|
||||
|
||||
FILE *out = fopen(out_path, "wb");
|
||||
if (!out) {
|
||||
fprintf(stderr, "mkexe: cannot open '%s' for writing: %s\n", out_path, strerror(errno));
|
||||
return 2;
|
||||
}
|
||||
if (fwrite(header, 1, EXE_HEADER_SIZE, out) != EXE_HEADER_SIZE) {
|
||||
fprintf(stderr, "mkexe: short write on header\n"); fclose(out); return 2;
|
||||
}
|
||||
|
||||
/* HOME image */
|
||||
for (uint32_t a = load_addr; a < load_addr + home_size; a++) {
|
||||
uint8_t b = home->present[a] ? home->bytes[a] : (uint8_t)pad_byte;
|
||||
if (fwrite(&b, 1, 1, out) != 1) {
|
||||
fprintf(stderr, "mkexe: short write on HOME at 0x%04X\n", a);
|
||||
fclose(out); return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Banks — full 16 KB each, padding empty cells. */
|
||||
for (int i = 1; i <= max_bank; i++) {
|
||||
segment_t *bnk = &segments[i];
|
||||
for (uint32_t a = bank_base; a < bank_base + BANK_SIZE; a++) {
|
||||
uint8_t b = bnk->present[a] ? bnk->bytes[a] : (uint8_t)pad_byte;
|
||||
if (fwrite(&b, 1, 1, out) != 1) {
|
||||
fprintf(stderr, "mkexe: short write on bank %d at 0x%04X\n", i, a);
|
||||
fclose(out); return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
|
||||
if (verbose) {
|
||||
fprintf(stderr,
|
||||
"mkexe: wrote %s\n"
|
||||
" load=0x%04X start=0x%04X stack=0x%04X loader=0x%04X\n"
|
||||
" HOME 0x%04X..0x%04X (%u bytes)\n",
|
||||
out_path, load_addr, start_addr, stack_addr, loader_size,
|
||||
home->lo, home->hi, home_size);
|
||||
for (int i = 1; i <= max_bank; i++) {
|
||||
fprintf(stderr, " BANK%-2d 0x%04X..0x%04X (%u live bytes, padded to 16 KB)\n",
|
||||
i, segments[i].lo, segments[i].hi,
|
||||
segments[i].hi + 1 - segments[i].lo);
|
||||
}
|
||||
uint32_t total = EXE_HEADER_SIZE + home_size + (uint32_t)max_bank * BANK_SIZE;
|
||||
fprintf(stderr, " total .exe = %u bytes\n", total);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
MKEXE=./mkexe
|
||||
TESTS=tests
|
||||
fail=0
|
||||
total=0
|
||||
|
||||
assert_hex_eq() {
|
||||
local file=$1 offset=$2 expected=$3 label=$4
|
||||
local actual
|
||||
actual=$(xxd -s "$offset" -l $((${#expected} / 2)) -p "$file")
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "FAIL: $label"
|
||||
echo " file: $file"
|
||||
echo " offset: $offset"
|
||||
echo " expected: $expected"
|
||||
echo " actual: $actual"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local name=$1; shift
|
||||
total=$((total + 1))
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
:
|
||||
else
|
||||
echo "FAIL: $name (exit nonzero)"
|
||||
fail=$((fail + 1))
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
expect_failure() {
|
||||
local name=$1; shift
|
||||
total=$((total + 1))
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
echo "FAIL: $name (expected nonzero exit, got success)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Case 1: minimal .ihx with one byte RET at 0x4100
|
||||
run_case "build-single-byte" \
|
||||
$MKEXE -L 0x4100 -E 0x4100 -S 0xBFFE -o $TESTS/case1.exe $TESTS/hello.ihx
|
||||
|
||||
assert_hex_eq $TESTS/case1.exe 0 "455845" "case1: signature EXE"
|
||||
assert_hex_eq $TESTS/case1.exe 3 "00" "case1: version 0"
|
||||
assert_hex_eq $TESTS/case1.exe 4 "00020000" "case1: offset 0x00000200"
|
||||
assert_hex_eq $TESTS/case1.exe 8 "0000" "case1: loader=0"
|
||||
assert_hex_eq $TESTS/case1.exe 0x10 "0041" "case1: load=0x4100"
|
||||
assert_hex_eq $TESTS/case1.exe 0x12 "0041" "case1: start=0x4100"
|
||||
assert_hex_eq $TESTS/case1.exe 0x14 "febf" "case1: stack=0xBFFE"
|
||||
assert_hex_eq $TESTS/case1.exe 0x200 "c9" "case1: image byte = RET (C9)"
|
||||
|
||||
# File size = 512 + 1
|
||||
size=$(stat -f "%z" $TESTS/case1.exe 2>/dev/null || stat -c "%s" $TESTS/case1.exe)
|
||||
if [ "$size" != "513" ]; then
|
||||
echo "FAIL: case1: file size $size != 513"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
total=$((total + 1))
|
||||
|
||||
# Case 2: defaults — load and start auto-derived from .ihx, stack default = 0xBFFE
|
||||
run_case "defaults-from-ihx" \
|
||||
$MKEXE -o $TESTS/case2.exe $TESTS/hello.ihx
|
||||
assert_hex_eq $TESTS/case2.exe 0x10 "0041" "case2: load defaults to 0x4100 (from ihx)"
|
||||
assert_hex_eq $TESTS/case2.exe 0x12 "0041" "case2: start defaults to load"
|
||||
assert_hex_eq $TESTS/case2.exe 0x14 "febf" "case2: stack defaults to 0xBFFE"
|
||||
|
||||
# Case 3: .bin input
|
||||
printf '\xC9' > $TESTS/one.bin
|
||||
run_case "build-from-bin" \
|
||||
$MKEXE -L 0x4100 -o $TESTS/case3.exe $TESTS/one.bin
|
||||
assert_hex_eq $TESTS/case3.exe 0x10 "0041" "case3: bin load=0x4100"
|
||||
assert_hex_eq $TESTS/case3.exe 0x200 "c9" "case3: bin image"
|
||||
|
||||
# Case 4: image extending past 0xFFFF must fail
|
||||
printf '\xFF%.0s' {1..4096} > $TESTS/big.bin
|
||||
expect_failure "reject-past-FFFF" \
|
||||
$MKEXE -L 0xF800 -o $TESTS/case4.exe $TESTS/big.bin
|
||||
|
||||
# Case 5: load above 0xFFFF must fail
|
||||
expect_failure "reject-load-out-of-range" \
|
||||
$MKEXE -L 0x10000 -o $TESTS/case5.exe $TESTS/one.bin
|
||||
|
||||
# Case 6: bad checksum in .ihx must fail
|
||||
cat > $TESTS/bad.ihx <<'EOF'
|
||||
:01410000C900
|
||||
:00000001FF
|
||||
EOF
|
||||
expect_failure "reject-bad-checksum" \
|
||||
$MKEXE -o $TESTS/case6.exe $TESTS/bad.ihx
|
||||
|
||||
# Case 7: multi-bank IHX → loader auto-set to HOME size, bank 1 appended (16 KB)
|
||||
# ELA "0001" puts the next records into bank 1 (virtual 0x1C000+).
|
||||
# Note: Intel HEX address fields are big-endian inside the record
|
||||
# :01 C000 00 77 C8 means addr=0xC000 (record bytes "C000" = high then low)
|
||||
cat > $TESTS/banked.ihx <<'EOF'
|
||||
:01410000C9F5
|
||||
:020000040001F9
|
||||
:01C0000077C8
|
||||
:00000001FF
|
||||
EOF
|
||||
run_case "banked-auto-loader" \
|
||||
$MKEXE -L 0x4100 -o $TESTS/case7.exe $TESTS/banked.ihx
|
||||
assert_hex_eq $TESTS/case7.exe 8 "0100" "case7: loader=HOME size (1 byte)"
|
||||
size=$(stat -f "%z" $TESTS/case7.exe 2>/dev/null || stat -c "%s" $TESTS/case7.exe)
|
||||
if [ "$size" != "16897" ]; then
|
||||
echo "FAIL: case7: file size $size != 16897 (512 hdr + 1 HOME + 16384 bank1)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
# Byte at file offset 0x201 (first byte of bank1) should be the 0x77 we put at bank1's 0xC000
|
||||
assert_hex_eq $TESTS/case7.exe 0x201 "77" "case7: bank1 first byte"
|
||||
|
||||
total=$((total + 9))
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
echo
|
||||
echo "FAILED: $fail of $total assertions/cases"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: $total cases passed"
|
||||
@@ -0,0 +1,2 @@
|
||||
:01410000C900
|
||||
:00000001FF
|
||||
@@ -0,0 +1,4 @@
|
||||
:01410000C9F5
|
||||
:020000040001F9
|
||||
:01C0000077C8
|
||||
:00000001FF
|
||||
@@ -0,0 +1 @@
|
||||
����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
|
||||
@@ -0,0 +1,2 @@
|
||||
:01410000C9F5
|
||||
:00000001FF
|
||||
@@ -0,0 +1 @@
|
||||
�
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sprinter-cc — wrapper around SDCC for building Sprinter .EXE files.
|
||||
#
|
||||
# Usage:
|
||||
# sprinter-cc -o foo.exe foo.c [more.c ...] [options]
|
||||
#
|
||||
# Options:
|
||||
# -o NAME output executable name (required)
|
||||
# --crt0=TYPE crt0 variant: default | minimal | banked (default: default)
|
||||
# -I PATH additional include path (repeatable)
|
||||
# -L 0xADDR code load address (default: 0x4100)
|
||||
# -E 0xADDR entry address (default: same as -L)
|
||||
# -S 0xADDR stack address (default: 0xBFFE)
|
||||
# --code-loc 0xN forwarded to SDCC --code-loc (default: 0x4100)
|
||||
# --data-loc 0xN forwarded to SDCC --data-loc (default: 0x8000)
|
||||
# -Wl FLAG extra linker flag (repeatable)
|
||||
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
|
||||
# automatically and adds -Wl-b_BANKN=0x{N}C000
|
||||
# --mkexe FLAG extra mkexe flag (repeatable; e.g. --mkexe -p --mkexe 0)
|
||||
# -v verbose (echo every command)
|
||||
# -h, --help show this help
|
||||
#
|
||||
# Example:
|
||||
# sprinter-cc -o hello.exe hello.c
|
||||
# sprinter-cc -o app.exe main.c --bank 1=engine.c
|
||||
# sprinter-cc -o tiny.exe tiny.c --crt0=minimal
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
# ------- Locate the toolchain ------------------------------------------------
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJ_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
|
||||
|
||||
SDCC_BIN="$PROJ_ROOT/third_party/sdcc/bin"
|
||||
SDCC="$SDCC_BIN/sdcc"
|
||||
SDASZ80="$SDCC_BIN/sdasz80"
|
||||
MKEXE="$PROJ_ROOT/toolchain/mkexe/mkexe"
|
||||
CHECK_BANKS="$PROJ_ROOT/toolchain/check_banks.py"
|
||||
|
||||
RUNTIME="$PROJ_ROOT/runtime"
|
||||
LIB_DIR="$PROJ_ROOT/lib"
|
||||
INC_DIR="$PROJ_ROOT/libc/include"
|
||||
|
||||
# ------- Defaults ------------------------------------------------------------
|
||||
OUT=""
|
||||
CRT0_TYPE="default"
|
||||
CODE_LOC="0x8100" # TINY mode default — everything in W2 (single page)
|
||||
DATA_LOC="0" # 0 = let linker auto-place after code areas
|
||||
LOAD_ADDR=""
|
||||
ENTRY_ADDR=""
|
||||
STACK_ADDR="0xBFFE"
|
||||
VERBOSE=0
|
||||
USER_INCS=()
|
||||
SOURCES=()
|
||||
LD_EXTRA=()
|
||||
MKEXE_EXTRA=()
|
||||
BANK_SPECS=() # entries like "1=engine.c"
|
||||
|
||||
# ------- Parse args ----------------------------------------------------------
|
||||
usage() {
|
||||
sed -n '3,28p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o) OUT="$2"; shift 2;;
|
||||
--crt0=*) CRT0_TYPE="${1#*=}"; shift;;
|
||||
-I) USER_INCS+=("-I" "$2"); shift 2;;
|
||||
-L) LOAD_ADDR="$2"; shift 2;;
|
||||
-E) ENTRY_ADDR="$2"; shift 2;;
|
||||
-S) STACK_ADDR="$2"; shift 2;;
|
||||
--code-loc) CODE_LOC="$2"; shift 2;;
|
||||
--data-loc) DATA_LOC="$2"; shift 2;;
|
||||
-Wl) LD_EXTRA+=("$2"); shift 2;;
|
||||
--bank) BANK_SPECS+=("$2"); shift 2;;
|
||||
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
|
||||
-v) VERBOSE=1; shift;;
|
||||
-h|--help) usage 0;;
|
||||
-*) echo "sprinter-cc: unknown option: $1" >&2; usage 1;;
|
||||
*) SOURCES+=("$1"); shift;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; }
|
||||
[[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; }
|
||||
|
||||
LOAD_ADDR="${LOAD_ADDR:-$CODE_LOC}"
|
||||
ENTRY_ADDR="${ENTRY_ADDR:-$LOAD_ADDR}"
|
||||
|
||||
# Pick crt0 source.
|
||||
case "$CRT0_TYPE" in
|
||||
default) CRT0_SRC="$RUNTIME/crt0.s"; ;;
|
||||
minimal) CRT0_SRC="$RUNTIME/crt0_minimal.s";;
|
||||
banked) CRT0_SRC="$RUNTIME/crt0_banked.s"; ;;
|
||||
*) echo "sprinter-cc: bad --crt0 type: $CRT0_TYPE" >&2; exit 1;;
|
||||
esac
|
||||
|
||||
# Banked builds always need crt0_banked, regardless of what the user asked.
|
||||
if [[ ${#BANK_SPECS[@]} -gt 0 ]] && [[ "$CRT0_TYPE" != "banked" ]]; then
|
||||
[[ $VERBOSE -eq 1 ]] && echo " --bank present → switching to crt0_banked"
|
||||
CRT0_SRC="$RUNTIME/crt0_banked.s"
|
||||
fi
|
||||
|
||||
# ------- Build in a per-output workdir ---------------------------------------
|
||||
WORK="$(dirname "$OUT")/.sprinter-cc-$(basename "$OUT" .exe)"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
run() {
|
||||
[[ $VERBOSE -eq 1 ]] && echo " $*"
|
||||
"$@"
|
||||
}
|
||||
|
||||
# 1. crt0
|
||||
CRT0_REL="$WORK/$(basename "$CRT0_SRC" .s).rel"
|
||||
run "$SDASZ80" -o "$CRT0_REL" "$CRT0_SRC"
|
||||
|
||||
# 2. user sources → .rel (HOME)
|
||||
USER_RELS=()
|
||||
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" "${USER_INCS[@]}")
|
||||
for src in "${SOURCES[@]}"; do
|
||||
rel="$WORK/$(basename "$src" .c).rel"
|
||||
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$rel" "$src"
|
||||
USER_RELS+=("$rel")
|
||||
done
|
||||
|
||||
# 3. bank sources
|
||||
BANK_RELS=()
|
||||
BANK_LD_FLAGS=()
|
||||
for spec in "${BANK_SPECS[@]}"; do
|
||||
bank_n="${spec%%=*}"
|
||||
bank_src="${spec#*=}"
|
||||
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
|
||||
run "$SDCC" "${CC_FLAGS[@]}" \
|
||||
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \
|
||||
-c -o "$rel" "$bank_src"
|
||||
BANK_RELS+=("$rel")
|
||||
# virtual address: bank_n in upper byte, 0xC000 low
|
||||
addr=$(printf "0x%X" $(( (bank_n << 16) | 0xC000 )))
|
||||
BANK_LD_FLAGS+=("-Wl-b_BANK${bank_n}=${addr}")
|
||||
done
|
||||
|
||||
# 4. link → .ihx
|
||||
IHX="$WORK/$(basename "$OUT" .exe).ihx"
|
||||
LINK_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size
|
||||
--code-loc "$CODE_LOC" --data-loc "$DATA_LOC")
|
||||
LINK_FLAGS+=("${BANK_LD_FLAGS[@]}")
|
||||
for f in "${LD_EXTRA[@]}"; do LINK_FLAGS+=("$f"); done
|
||||
# libsprinter.lib via -l/-L (sdcc passes -lsprinter through to sdldz80)
|
||||
run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \
|
||||
"$CRT0_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \
|
||||
"-L$LIB_DIR" "-lsprinter"
|
||||
|
||||
# Quick bank-size check (only meaningful if there are banks).
|
||||
if [[ ${#BANK_SPECS[@]} -gt 0 ]] && [[ -f "${IHX%.ihx}.map" ]]; then
|
||||
python3 "$CHECK_BANKS" "${IHX%.ihx}.map" || true
|
||||
fi
|
||||
|
||||
# 5. mkexe → .exe
|
||||
MK_FLAGS=(-L "$LOAD_ADDR" -E "$ENTRY_ADDR" -S "$STACK_ADDR" -o "$OUT" "$IHX")
|
||||
[[ $VERBOSE -eq 1 ]] && MK_FLAGS=(-v "${MK_FLAGS[@]}")
|
||||
for f in "${MKEXE_EXTRA[@]}"; do MK_FLAGS=("$f" "${MK_FLAGS[@]}"); done
|
||||
run "$MKEXE" "${MK_FLAGS[@]}"
|
||||
|
||||
echo "sprinter-cc: wrote $OUT"
|
||||
Reference in New Issue
Block a user