diff --git a/source/Makefile b/source/Makefile index 9ad8f828..60b7da44 100644 --- a/source/Makefile +++ b/source/Makefile @@ -21,7 +21,6 @@ ASFLAGS += '-D MONITOR_ADDRESS=$(DOLLAR)$(LMONITOR)' '-D LOCKOUT_ADDRESS=$(DOLLA -L $(BUILDDIR)debug$(S)basic.lst -l $(BUILDDIR)debug$(S)basic.lbl MODULES = +hardware +tokeniser +graphics +kernel:2 +sound:2 -BUILD_OUT = $(BUILDDIR)basic.rom SCRIPTDIR = scripts$(S) MAME_PATH=../../mame KERNEL_PATH=../../F256_MicroKernel @@ -74,10 +73,20 @@ build: prelim $(call cleandir,$(BUILDDIR)debug$(S)) @echo "Compiling assembly" - $(Q)$(ASM) $(ASFLAGS) _basic.asm -o $(BUILD_OUT) + $(Q)$(ASM) -b $(ASFLAGS) _basic.asm \ + --output-section boot -o $(BUILDDIR)sb01.bin \ + --output-section resident_shared -o $(BUILDDIR)sb02.bin \ + --output-section resident_swappable -o $(BUILDDIR)sb03.bin \ + --output-section page1 -o $(BUILDDIR)sb04.bin \ + --output-section page2 -o $(BUILDDIR)sb05.bin \ + --output-section zeropage -o $(BUILDDIR)zeropage.ram \ + --output-section zeropref -o $(BUILDDIR)zeropref.ram \ + --output-section arguments -o $(BUILDDIR)arguments.ram \ + --output-section storage -o $(BUILDDIR)storage.ram + + @echo "Checking constraints" + $(Q)$(PYTHON) $(SCRIPTDIR)verifyblank.py $(BUILDDIR) - @echo "Splitting ROM" - $(Q)$(PYTHON) $(SCRIPTDIR)splitrom.py $(BUILDDIR) # # Scripts run in advance generating tables etc. @@ -123,7 +132,7 @@ build_release: $(call cleandir,$(OUTPUTDIR)debug$(S)) @echo "Copying build artifacts" - $(Q)$(CCOPY) $(BUILDDIR)*.* $(OUTPUTDIR) + $(Q)$(CCOPY) $(BUILDDIR)*.bin $(OUTPUTDIR) $(Q)$(CCOPY) $(BUILDDIR)debug$(S)*.* $(OUTPUTDIR)debug # @@ -170,9 +179,6 @@ paging: # # Test on real hardware # -run: build - foenixmgr binary --address 5000 $(BUILD_OUT) - flash: build foenixmgr flash-bulk build/basic.csv diff --git a/source/common/aa.system/00start.asm b/source/common/aa.system/00start.asm index dc1dd806..23f952a6 100644 --- a/source/common/aa.system/00start.asm +++ b/source/common/aa.system/00start.asm @@ -2,6 +2,7 @@ ; Start up code ;; +; ---------------------------------------------------------------------------------------------- ; .section boot ;; @@ -25,7 +26,7 @@ KernelHeader: .send boot - +; ---------------------------------------------------------------------------------------------- ; .section code ;; diff --git a/source/common/aa.system/01common.inc b/source/common/aa.system/01common.inc index 706cf773..a592481f 100644 --- a/source/common/aa.system/01common.inc +++ b/source/common/aa.system/01common.inc @@ -51,11 +51,17 @@ MaxPages = 32 ; max logical pages (256KB program space) ArrayStart = $4000 ArrayEnd = $8000 ; -; Build address of ROM (slots 4-5, $8000-$BFFF) ; Boot section at $6000 (slot 3, remapped to RAM after boot) ; BootStart = $6000 -CodeStart = $8000 +; +; Resident shared code (slot 4, $8000-$9FFF) +; +ResidentSharedStart = $8000 +; +; Resident swappable code (slot 5, $A000-$BFFF) +; +ResidentSwappableStart = $A000 ; ; Start of variable/string space. This is a fixed location in memory. ; @@ -84,6 +90,9 @@ MaxLineSize = 80 ; ; ************************************************************************************************ +;; +; RAM storage, declarations/dynamic data only, cannot contain statically initialized data +;; * = ZeroPageMandatory ; *must* be in zero page .dsection zeropage @@ -96,11 +105,91 @@ MaxLineSize = 80 * = MemoryStorage ; doesn't matter if zero page or not .dsection storage - * = $4000 ; page 2 modules at $4000 (output before boot) + +; ROM image sections: code and static data +; +; $4000-$5FFF: module page 2 code (`page2` section) +; $6000-$7FFF: KernelHeader + headerdata (`boot` section) +; $8000-$9FFF: resident code 1 (`resident_shared` section) +; $A000-$BFFF: resident code 2 (`resident_swappable` section) +; $C000-$DFFF: module page 1 code (`page1` section) +; +; Banking/visibility rules: +; +; - `resident_shared` is always visible. Banking thunks, interrupt handlers, +; and code/data needed by paged modules live here. +; +; - `resident_swappable` is unavailable while `page1` is mapped. +; +; - `page2` hides array RAM at $6000-$7FFF while executing. +; +; - `page1` has the full array storage ($4000-$7FFF) visible, but only if it +; doesn't have `page2` code in its call stack. +; +; - Calls from outside `page1` or `page2` must go through the corresponding +; banking thunk. +; +; - A `page1` thunk may only be entered while `resident_swappable` is mapped. +; Consequently, `page1` cannot be re-entered through a thunk, including +; through a `page1` -> `page2` -> `page1` call chain. +; +; - `page2` may call `page1` through a thunk iff `page1` is not already +; mapped. The called code cannot access array RAM at $6000-$7FFF because +; `page2` still occupies slot 3. + + + ; Page 2 section at $4000 + ; + ; Mapped to `BootStart` (slot 3) when called and therefore cannot + ; directly access higher half of the RAM/array data at $6000-$7FFF + + * = $4000 + .logical BootStart .dsection page2 + .endlogical + + ; define the corresponding namespace +Page2 .namespace + .endnamespace - * = BootStart ; boot section at $6000 (KernelHeader + headerdata) + + ; Boot section at $6000 (KernelHeader + headerdata) + ; + ; RAM (array/`alloc` storage in slot 3) is mapped here after boot and + ; page 2 code is mapped here when called + + * = BootStart .dsection boot - * = CodeStart ; main code at $8000 - .dsection code + + ; Shared resident code at $8000 + ; + ; The only code section that stays resident in CPU memory throughout + ; the lifetime of the interpreter + + * = ResidentSharedStart ; shared resident code at $8000 + .dsection resident_shared + + + ; Swappable resident code at $A000 + ; + ; Page 1 modules are mapped here on demand + + * = ResidentSwappableStart + .dsection resident_swappable + + + ; Page 1 section at $C000 + ; + ; Mapped to `ResidentSwappableStart` when called and therefore cannot + ; depend on `resident_swappable` routines + + * = $C000 + .logical ResidentSwappableStart + .dsection page1 + .endlogical + + ; define the corresponding namespace +Page1 .namespace + .endnamespace + diff --git a/source/common/aa.system/_dataoverflowchecks.asm b/source/common/aa.system/_dataoverflowchecks.asm index 493ce7e7..6b9a4a9a 100644 --- a/source/common/aa.system/_dataoverflowchecks.asm +++ b/source/common/aa.system/_dataoverflowchecks.asm @@ -18,5 +18,5 @@ .send storage .section boot - .cwarn * > CodeStart, "BROKEN BUILD: boot section overflowed into code by ", * - CodeStart," bytes" + .cwarn * > ResidentSharedStart, "BROKEN BUILD: boot section overflowed into resident code by ", * - ResidentSharedStart," bytes" .send boot diff --git a/source/common/aa.system/_slot3banking.asm b/source/common/aa.system/_slot3banking.asm index ce7e2b32..496b5f19 100644 --- a/source/common/aa.system/_slot3banking.asm +++ b/source/common/aa.system/_slot3banking.asm @@ -9,6 +9,10 @@ ; buffer overflows). ;; +; Page 1 modules use slot 5 (inc/dec 8+5). +; Page 2 modules use slot 3 (save/restore 8+3 with depth counter). + + .if PagingEnabled==1 && HasPage2==1 Slot3Init: diff --git a/source/modules/Makefile b/source/modules/Makefile index 9f15fc43..0a3f0b51 100644 --- a/source/modules/Makefile +++ b/source/modules/Makefile @@ -21,14 +21,14 @@ all: $(Q)$(MAKE) -B -C hardware$(S)startup $(Q)$(CCOPY) ..$(S)common$(S)generated$(S)kwdtext.dat tokeniser$(S)__kwdtext.asm - $(Q)$(PYTHON) _scripts$(S)makebuild.py tokeniser + $(Q)$(PYTHON) _scripts$(S)makebuild.py tokeniser $(BUILD_DIR) $(Q)$(PYTHON) _scripts$(S)makebuild.py graphics $(BUILD_DIR) $(Q)$(PYTHON) _scripts$(S)makebuild.py hardware $(BUILD_DIR) $(Q)$(PYTHON) _scripts$(S)makebuild.py kernel $(BUILD_DIR) --page 2 $(Q)$(PYTHON) _scripts$(S)makebuild.py sound $(BUILD_DIR) --page 2 - $(Q)$(PYTHON) _scripts$(S)makeexport.py >$(BUILD_DIR)$(S)_exports.module.asm + $(Q)$(PYTHON) _scripts$(S)makeexport.py $(BUILD_DIR) clean: $(Q)$(MAKE) -C graphics clean diff --git a/source/modules/_scripts/makebuild.py b/source/modules/_scripts/makebuild.py index dc859c3e..b3ab60b5 100644 --- a/source/modules/_scripts/makebuild.py +++ b/source/modules/_scripts/makebuild.py @@ -14,21 +14,12 @@ from pathlib import Path from typing import TextIO +from pages import Page, PAGE1, PAGE2 + # regex for exported labels export_re = re.compile(r"^\s*Export_(\w+)\s*:\s*(;.*)?$") -PAGE2_BEGIN = """ -.section page2 -.logical * + $2000 -""" - -PAGE2_END = """ -.endlogical -.send page2 -""" - - def main(*, module_name: str, build_dir: Path, page: int = 1) -> None: """Generate a single assembly build file for the specified module.""" if not build_dir.exists(): @@ -36,10 +27,9 @@ def main(*, module_name: str, build_dir: Path, page: int = 1) -> None: sources = collect_sources(module_name) - if page == 2: - output_path = build_dir / (module_name + "_p2.module.asm") - else: - output_path = build_dir / (module_name + ".module.asm") + output_page = PAGE2 if page == 2 else PAGE1 + output_path = build_dir / output_page.name / (module_name + ".module.asm") + output_path.parent.mkdir(exist_ok=True) with open(output_path, "w", encoding="utf-8") as out: # Write the output assembly file header @@ -50,14 +40,14 @@ def main(*, module_name: str, build_dir: Path, page: int = 1) -> None: for src in sources: # Scan each source file line-by-line, accumulating its exports # and writing its contents to the output. - file_exports = process_source(src, out, page=page) + file_exports = process_source(src, out, page=output_page) module_exports.extend(file_exports) # Dump the collected exports to a file - dump_exports(build_dir, module_name, module_exports, page=page) + dump_exports(build_dir, module_name, module_exports, page=output_page) -def process_source(path: Path, out: TextIO, *, page: int = 1) -> list[str]: +def process_source(path: Path, out: TextIO, *, page: Page) -> list[str]: """Process a single source file, extracting exports and writing to output.""" exports: list[str] = [] @@ -66,14 +56,13 @@ def process_source(path: Path, out: TextIO, *, page: int = 1) -> list[str]: if m := export_re.match(line): exports.append(m.group(1)) - if page == 2: - normalized = " ".join(line.split()) - if normalized == ".section code": - out.write(PAGE2_BEGIN) - continue - elif normalized == ".send code": - out.write(PAGE2_END) - continue + normalized = " ".join(line.split()) + if normalized == ".section code": + out.write(page.begin()) + continue + elif normalized == ".send code": + out.write(page.end()) + continue out.write(f"{line.rstrip()}\n") @@ -81,15 +70,14 @@ def process_source(path: Path, out: TextIO, *, page: int = 1) -> list[str]: def dump_exports( - build_dir: Path, module_name: str, exports: list[str], *, page: int = 1 + build_dir: Path, module_name: str, exports: list[str], *, page: Page ) -> None: """Save module exports to the corresponding `.exports` file.""" if not exports: return - suffix = "_p2" if page == 2 else "" with open( - build_dir / (module_name + suffix + ".exports"), "w", encoding="utf-8" + build_dir / page.name / (module_name + ".exports"), "w", encoding="utf-8" ) as out: for export in exports: out.write(f"{export}\n") diff --git a/source/modules/_scripts/makeexport.py b/source/modules/_scripts/makeexport.py index e9735a80..5e59b153 100644 --- a/source/modules/_scripts/makeexport.py +++ b/source/modules/_scripts/makeexport.py @@ -4,75 +4,60 @@ and generates corresponding entry points. If paging is enabled, includes the necessary assembly code to handle memory bank switching. -Page 1 modules use slot 5 (inc/dec 8+5). -Page 2 modules use slot 3 (save/restore 8+3 with depth counter). - Outputs the entry points code to standard output. """ import sys from pathlib import Path +from dataclasses import dataclass + +from pages import Page, PAGES + + +@dataclass +class ModuleExports: + module_name: str + exports: list[str] def main(*, build_dir: Path) -> None: - """Generate module entry points and write to standard output.""" - exports = read_exports(build_dir) paging = True + if paging: + for page_name, page in PAGES.items(): + generate_exports(build_dir=build_dir / page_name, page=page) + else: + generate_exports(build_dir=build_dir) - print(f"PagingEnabled = {1 if paging else 0}") - - has_page2 = any(exports[m]["page"] == 2 for m in exports) - print(f"HasPage2 = {1 if has_page2 else 0}") - - for module in exports: - page = exports[module]["page"] - routines = exports[module]["routines"] - module_name = exports[module]["name"] - print(f"\t.if {module_name}Integrated == 1") - for routine in routines: - print(f"{routine}:") - if paging: - if page == 1: - print(page1_thunk(routine)) - elif page == 2: - print(page2_thunk(routine)) - else: - raise RuntimeError(f"Unknown page #{page}") - else: - print(f"\tjmp\tExport_{routine}") - print("\t.endif") +def generate_exports(*, build_dir: Path, page: Page | None = None) -> None: + """Collect module entry points in the specified dir and write them to _exports.asm""" + exports = read_exports(build_dir) + with open(build_dir / "_exports.asm", "w") as out: + if page: + out.write(f"{page.thunk_namespace}\t.namespace\n") -def page1_thunk(routine: str) -> str: - return f""" - inc 8+5 - jsr Export_{routine} - php - dec 8+5 - plp - rts -""" + for module in exports: + module_name = exports[module].module_name + out.write(f".if {module_name}Integrated == 1\n") + routines = exports[module].exports + for routine in routines: + out.write(f"\n{routine}:\n") + if page: + out.write(page.thunk(routine)) + else: + out.write(f"\tjmp\tExport_{routine}\n") -def page2_thunk(routine: str) -> str: - return f""" - jsr Slot3BankIn - jsr Export_{routine} - php - jsr Slot3BankOut - plp - rts -""" + out.write(".endif\n") + if page: + out.write(".endnamespace\n") -def read_exports(build_dir: Path) -> dict[str, dict]: - """Read all `.exports` files from the build directory. - Files named `_p2.exports` are treated as page 2 exports - (slot 3 via save/restore 8+3). All others are page 1 (slot 5 via inc/dec 8+5). - """ - all_exports: dict[str, dict] = {} +def read_exports(build_dir: Path) -> dict[str, ModuleExports]: + """Read all `.exports` files from the build directory.""" + all_exports: dict[str, ModuleExports] = {} if not build_dir.exists(): return all_exports @@ -80,19 +65,13 @@ def read_exports(build_dir: Path) -> dict[str, dict]: # Find all .exports files in the build directory for file_path in build_dir.glob("*.exports"): if module_exports := read_module_exports(file_path): - stem = file_path.stem # filename without extension - if stem.endswith("_p2"): - module_name = stem[:-3] # strip _p2 suffix - page = 2 - else: - module_name = stem - page = 1 + module_name = file_path.stem # filename without extension + module_exports.sort() - all_exports[stem] = { - "name": module_name, - "page": page, - "routines": module_exports, - } + all_exports[module_name] = ModuleExports( + module_name=module_name, + exports=module_exports, + ) return all_exports diff --git a/source/modules/_scripts/pages.py b/source/modules/_scripts/pages.py new file mode 100644 index 00000000..b7706a30 --- /dev/null +++ b/source/modules/_scripts/pages.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass + + +@dataclass +class Page: + name: str + + @property + def namespace(self) -> str: + return self.name.capitalize() + + @property + def thunk_namespace(self) -> str: + return f"{self.name.capitalize()}Switch" + + def begin(self) -> str: + return f""" +.section {self.name} +.namespace {self.namespace} +""" + + def end(self) -> str: + return f""" +.endnamespace +.send {self.name} +""" + + def thunk(self, routine: str) -> str: + return f""" + .{self.name}_bank_in + jsr {self.namespace}.Export_{routine} + php + .{self.name}_bank_out + plp + rts +""" + + +PAGE1 = Page("page1") +PAGE2 = Page("page2") +PAGES = dict((page.name, page) for page in [PAGE1, PAGE2]) diff --git a/source/modules/hardware/charout.asm b/source/modules/hardware/charout.asm index 52859f04..fe03b553 100644 --- a/source/modules/hardware/charout.asm +++ b/source/modules/hardware/charout.asm @@ -49,7 +49,6 @@ Export_EXTPrintNoControl: ; ************************************************************************************************ Export_EXTPrintCharacter: -PAGEDPrintCharacter: pha phx phy diff --git a/source/modules/sound/update.asm b/source/modules/sound/update.asm index 80b50ccd..75963e2e 100644 --- a/source/modules/sound/update.asm +++ b/source/modules/sound/update.asm @@ -19,7 +19,6 @@ ; ************************************************************************************************ Export_SNDUpdate: -PagedSNDUpdate: lda SNDTimeLeft+0 ; look at time remaining beq _SNDUNot0 ; not playing ldx #0 ; so we know which channel to update @@ -55,7 +54,7 @@ SNDUpdateChannel: cmp #$FF ; sound $FF play forever until turned off manually beq _SNDUCExit dec a ; decrement and update timer - sta SNDTimeLeft,x + sta SNDTimeLeft,x beq _SNDUCUpdate ; if zero, silence channel ; lda SNDAdjustLow,x ; adjust ? @@ -78,7 +77,7 @@ _SNDUCUpdate: pla jsr SNDCheckChannel ; more to do ? _SNDUCExit: - rts + rts .send code ; ************************************************************************************************ diff --git a/source/scripts/splitrom.py b/source/scripts/splitrom.py deleted file mode 100644 index 18ce26a9..00000000 --- a/source/scripts/splitrom.py +++ /dev/null @@ -1,60 +0,0 @@ -# Split the SuperBASIC ROM file into 8192-byte binary chunks -# -# The assembler produces a binary spanning $4000-$DFFF: -# $4000-$5FFF: module page 2 code (page2 section) -# $6000-$7FFF: KernelHeader + headerdata (boot section) -# $8000-$9FFF: main code block 1 (code section) -# $A000-$BFFF: main code block 2 (code section) -# $C000-$DFFF: module page 1 (code section, paged via .offs) -# -# Output block order: -# sb01 = boot + headerdata (N+0, slot 3, remapped to RAM after boot) -# sb02 = main code block 1 (N+1, slot 4) -# sb03 = main code block 2 (N+2, slot 5 default) -# sb04 = module page 1 (N+3, inc 8+5 once) -# sb05 = module page 2 (N+4, inc 8+5 twice) - -import sys -from pathlib import Path - - -PAGE_SIZE = 8192 # size of each page in bytes - - -def main(*, build_dir: Path): - with open(build_dir / "basic.rom", "rb") as f: - data = f.read() - - # Binary layout starts at $4000: - # offset $0000 = $4000 (page2 section) - # offset $2000 = $6000 (boot section) - # offset $4000 = $8000 (main code block 1) - # offset $6000 = $A000 (main code block 2) - # offset $8000 = $C000 (module page 1) - page2_offset = 0x0000 - boot_offset = 0x2000 - main_offset = 0x4000 - mod1_offset = 0x8000 - - pages = [ - data[boot_offset : boot_offset + PAGE_SIZE], # sb01: boot + headerdata - data[main_offset + 0 * PAGE_SIZE : main_offset + 1 * PAGE_SIZE], # sb02: main block 1 - data[main_offset + 1 * PAGE_SIZE : main_offset + 2 * PAGE_SIZE], # sb03: main block 2 - data[mod1_offset : mod1_offset + PAGE_SIZE], # sb04: module page 1 - data[page2_offset : page2_offset + PAGE_SIZE], # sb05: module page 2 - ] - - for i, chunk in enumerate(pages): - chunk = chunk.ljust(PAGE_SIZE, b"\xff") - with open(build_dir / "sb{:02x}.bin".format(i + 1), "wb") as out: - out.write(chunk) - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python splitrom.py ") - sys.exit(1) - - main( - build_dir=Path(sys.argv[1]), - ) diff --git a/source/scripts/verifyblank.py b/source/scripts/verifyblank.py new file mode 100644 index 00000000..fe8fd4c3 --- /dev/null +++ b/source/scripts/verifyblank.py @@ -0,0 +1,28 @@ +# Verify that RAM sections in the build dir don't contain static data + +import sys +from pathlib import Path + + +def main(*, build_dir: Path): + ram_files = list(build_dir.glob("*.ram")) + + if not ram_files: + raise SystemExit("BROKEN BUILD: no .ram files found") + + nonempty = [path for path in ram_files if path.stat().st_size] + + if nonempty: + for path in nonempty: + print(f"BROKEN BUILD: initialized data found in {path}") + raise SystemExit(1) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python verifyblank.py ") + sys.exit(1) + + main( + build_dir=Path(sys.argv[1]), + )