Add cfg/compiler.cfg, the production mmu.s, c128.inc, and the test mailbox, proven by a capacity-checking skeleton in x128: - 8K bottom common RAM, shared code at $1300, private zero page at bank 0 page $1A via P0, bank-1 tables at $2000-$BFFF, test mailbox at $1BF0 - ld65 rejects overflows of the ZP, COMMON, MAIN, SWSTACK, and TABLES areas - a CIA #2 timer NMI taken under bank 1 with the private zero page returns cleanly through the NMI guard (closes the E01-S03 NMI risk) - decision D117 records the map; RESTORE is ignored while compiling Also pin newLang/ to LF in .gitattributes. A branch switch under core.autocrlf had rewritten the byte-exact fixtures with CRLF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
| @@ -7,3 +7,7 @@ | |||
| *.d64 binary | |||
| *.d71 binary | |||
| *.d81 binary | |||
| # Nova128 sources and fixtures are byte-exact test inputs (PETSCII conversion, | |||
| # validators anchored on line ends). Keep them LF on every platform. | |||
| newLang/** text=auto eol=lf | |||
| @@ -0,0 +1,54 @@ | |||
| # Nova128 compiler memory map (E01-S05). docs/memory-map.md explains each | |||
| # region and the evidence behind it. ld65 refuses any segment that overflows | |||
| # its area, so this layout is also the compiler's capacity check. | |||
| # | |||
| # Bank 0 (CR=$0E) Bank 1 (CR=$4E) | |||
| # $0000-$12FF system (BASIC/KERNAL) $0000-$1FFF hidden by common RAM | |||
| # $1300-$18FF COMMON (copied at init) $2000-$BFFF BANK1 compiler tables | |||
| # $1900-$19FF DISKBUF $C000-$FFFF system ROM (unmapped RAM) | |||
| # $1A00-$1AFF private zero page (P0) | |||
| # $1B00-$1BEF SCRATCH (common) | |||
| # $1BF0-$1BFF MAILBOX | |||
| # $1C01-$B7FF MAIN (stub, code, data) | |||
| # $B800-$BFFF SWSTACK | |||
| # $C000-$FFFF system ROM | |||
| MEMORY { | |||
| # Logical zero page. With P0 -> $1A00 these are physical $1A02-$1AFF. | |||
| # $00/$01 are the 8502 port and are never allocated. | |||
| ZP: file = "", start = $0002, size = $00FE, type = rw, define = yes; | |||
| LOADADDR: file = %O, start = $1BFF, size = $0002; | |||
| HEADER: file = %O, start = $1C01, size = $000C; | |||
| MAIN: file = %O, start = $1C0D, size = $9BF3, define = yes; # to $B7FF | |||
| COMMONRAM: file = "", start = $1300, size = $0600, define = yes; # to $18FF | |||
| DISKBUF: file = "", start = $1900, size = $0100, define = yes; | |||
| SCRATCH: file = "", start = $1B00, size = $00F0, define = yes; | |||
| MAILBOX: file = "", start = $1BF0, size = $0010, define = yes; | |||
| SWSTACK: file = "", start = $B800, size = $0800, define = yes; # to $BFFF | |||
| BANK1: file = "", start = $2000, size = $A000, define = yes; # to $BFFF | |||
| } | |||
| SEGMENTS { | |||
| ZEROPAGE: load = ZP, type = zp, optional = yes; | |||
| LOADADDR: load = LOADADDR, type = ro; | |||
| EXEHDR: load = HEADER, type = ro; | |||
| STARTUP: load = MAIN, type = ro; # first: SYS 7181 enters at $1C0D | |||
| CODE: load = MAIN, type = ro; | |||
| RODATA: load = MAIN, type = ro, optional = yes; | |||
| DATA: load = MAIN, type = rw, optional = yes; | |||
| COMMON: load = MAIN, run = COMMONRAM, type = rw, define = yes; | |||
| BSS: load = MAIN, type = bss, optional = yes, define = yes; | |||
| DISKBUF: load = DISKBUF, type = bss, optional = yes; | |||
| SCRATCH: load = SCRATCH, type = bss, optional = yes; | |||
| MAILBOX: load = MAILBOX, type = bss, optional = yes; | |||
| SWSTACK: load = SWSTACK, type = bss, optional = yes; | |||
| TABLES: load = BANK1, type = bss, optional = yes, define = yes; | |||
| } | |||
| SYMBOLS { | |||
| # Physical bank-0 page that P0 maps to logical zero page in compiler states. | |||
| __COMPILER_ZP_PAGE__: type = export, value = $1A; | |||
| } | |||
| @@ -7,9 +7,9 @@ version: 0.1-draft | |||
| # Nova128 Compiler and Runtime Architecture | |||
| Status: approved version 1 baseline. Exact addresses are deliberately absent | |||
| until the platform-probe epic validates them in VICE and, when available, on | |||
| hardware. | |||
| Status: approved version 1 baseline. The compiler memory map is frozen by | |||
| E01-S05 (section 4.4, `docs/memory-map.md`). Generated-program addresses are | |||
| still deliberately absent until the E04 stories validate them. | |||
| ## 1. System Boundary | |||
| @@ -131,6 +131,26 @@ bank 0 and bank 1 compiler mappings, CHROUT, a drive-8 status read, and a | |||
| device-not-present failure all work, with A/X/Y/C/ST preserved and no channels | |||
| left open. | |||
| ### 4.4 Frozen compiler memory map (E01-S05) | |||
| The gate above is closed for the compiler. `cfg/compiler.cfg` is authoritative | |||
| and `docs/memory-map.md` explains it and gives the evidence. In summary: | |||
| - **Compiler states:** CR `$0E` (bank 0) or `$4E` (bank 1), 8K bottom common | |||
| RAM, P0 relocated to bank 0 page `$1A`, IRQs masked. | |||
| - **Bank 0:** COMMON `$1300-$18FF` (copied at init), DISKBUF `$1900`, private | |||
| zero page `$1A00`, SCRATCH `$1B00-$1BEF`, MAILBOX `$1BF0-$1BFF`, MAIN | |||
| `$1C0D-$B7FF` (STARTUP first), SWSTACK `$B800-$BFFF`. | |||
| - **Bank 1:** compiler tables `$2000-$BFFF` (40 KB). | |||
| - **Zero page:** the compiler owns logical `$02-$FF` via P0; KERNAL calls run | |||
| on the system zero page. | |||
| - **NMI:** guarded by `mmu_nmi_guard` through `$0318`; RESTORE is ignored | |||
| during compilation. | |||
| - **Test mailbox:** 16 bytes at `$1BF0`, shared by platform tests, the | |||
| compiler, and generated programs. | |||
| The generated-program memory map remains open (E04-S01/E04-S02). | |||
| ### 4.3 Display and clock | |||
| Compiler text uses KERNAL-compatible character output on the display already | |||
| @@ -337,8 +357,8 @@ Testing is layered: | |||
| 2. Compiler fixtures for lexer, parser, symbols, types, class layout, interface | |||
| conformance, and diagnostics. | |||
| 3. Golden PRG bytes and disassembly for small generated routines. | |||
| 4. End-to-end x128 runs that write a result signature to a fixed test mailbox | |||
| chosen during E01. | |||
| 4. End-to-end x128 runs that write a result signature to the fixed test | |||
| mailbox at `$1BF0` (`src/common/mailbox.inc`, `docs/memory-map.md`). | |||
| 5. Monitor scripts with MMU, stack, heap, and dispatch break/watch points. | |||
| 6. Editor round-trip: Vim saves source, C128 compiler consumes it without host | |||
| conversion, and Vim reopens unchanged source. | |||
| @@ -363,7 +383,7 @@ features depend on it. | |||
| ## Open Questions | |||
| - Exact bank layouts, zero-page reservations, and common-RAM window | |||
| - Generated-program memory map (the compiler map is frozen in section 4.4) | |||
| - Final interface-result convention | |||
| - Compiler table capacities and their user-visible resource limits | |||
| - Whether to cache the complete source in banked RAM when it fits | |||
| @@ -38,6 +38,7 @@ documented default. `Proposed` means a later change still needs approval. | |||
| | D114 | Version 1 has no interface-to-class down-cast operation. | Programs express polymorphism through interfaces without runtime type reflection. | | |||
| | D115 | Character literals use single quotes and contain exactly one PETSCII character or supported escape. | The `char` type has a direct, unambiguous literal form while `//` remains the comment syntax. | | |||
| | D116 | Runtime faults report a stable code and the best available map-resolvable program address. | Rich stack traces are deferred; faults remain diagnosable within the runtime budget. | | |||
| | D117 | Freeze the compiler memory map in `cfg/compiler.cfg`: 8K bottom common RAM, COMMON at `$1300`, private zero page at bank 0 page `$1A` via P0, bank-1 tables at `$2000-$BFFF`, IRQs masked and NMIs guarded in compiler states, test mailbox at `$1BF0`. | Internal contract backed by E01-S05 x128 evidence (`docs/memory-map.md`); no user-visible behavior except that RESTORE is ignored while compiling. Generated-program maps remain open. | | |||
| ## Proposed | |||
| @@ -58,6 +59,8 @@ No proposed product changes are currently pending. | |||
| ## Open Questions | |||
| 1. What exact compiler and generated-program memory maps pass the E01 probes? | |||
| 2. What compiler table capacities fit after the verified memory map is frozen? | |||
| 1. What generated-program memory map should E04-S01/E04-S02 adopt? (The | |||
| compiler map is D117.) | |||
| 2. What compiler table capacities fit in the 40 KB bank-1 window and 254-byte | |||
| zero page now that the compiler map is frozen? | |||
| @@ -0,0 +1,126 @@ | |||
| # Nova128 Compiler Memory Map | |||
| Story: E01-S05 | |||
| Status: frozen for the compiler (D117); verified in VICE x128 3.10 | |||
| Authoritative source: `cfg/compiler.cfg`. This document explains it. | |||
| The generated-program memory map is **not** frozen here. It belongs to E04-S01 | |||
| and E04-S02, which must reuse the test mailbox below. | |||
| ## Mappings | |||
| | State | CR | RCR (low bits) | P0 | P1 | IRQs | | |||
| |---|---|---|---|---|---| | |||
| | System | as BASIC left it (`$00`) | as BASIC left it | `$0000` | `$0001` | caller's | | |||
| | Compiler bank 0 | `$0E` | `%0110` 8K bottom common | bank 0 page `$1A` | `$0001` | masked | | |||
| | Compiler bank 1 | `$4E` | `%0110` 8K bottom common | bank 0 page `$1A` | `$0001` | masked | | |||
| RCR bits 6–7 (VIC bank) are always carried over from the system value. | |||
| `src/compiler/mmu.s` is the only module that writes MMU registers. | |||
| ## Bank 0 | |||
| | Range | Size | Name | Contents | | |||
| |---|---|---|---| | |||
| | `$0000-$12FF` | | system | BASIC 7, KERNAL, editor, screen. Off limits except `NMINV` (`$0318`), which the compiler saves and restores | | |||
| | `$1300-$18FF` | 1536 | COMMON | Copied from the PRG by `mmu_init`: MMU switches, KERNAL wrapper, NMI guard, and any code that runs while bank 1 is mapped | | |||
| | `$1900-$19FF` | 256 | DISKBUF | Disk transfer buffer, visible from both banks | | |||
| | `$1A00-$1AFF` | 256 | private ZP | Physical page behind logical zero page in compiler states. Not a linker area: nothing else may live here | | |||
| | `$1B00-$1BEF` | 240 | SCRATCH | Shared state needed from both banks | | |||
| | `$1BF0-$1BFF` | 16 | MAILBOX | Fixed test mailbox (below) | | |||
| | `$1C01-$1C0C` | 12 | header | BASIC 7 stub `10 SYS 7181` | | |||
| | `$1C0D-$B7FF` | 39923 | MAIN | `STARTUP` (always first, entry at `$1C0D`), CODE, RODATA, DATA, COMMON load image, BSS | | |||
| | `$B800-$BFFF` | 2048 | SWSTACK | Compiler software stack | | |||
| | `$C000-$FFFF` | | system ROM | KERNAL/editor in both compiler states | | |||
| `$1300-$1BFF` is the C128's free application area: after boot x128 shows only | |||
| the power-on fill pattern there. All of `$0000-$1FFF` is bottom common RAM, so | |||
| COMMON, DISKBUF, private ZP, SCRATCH, and MAILBOX are visible from bank 1. | |||
| ## Bank 1 | |||
| | Range | Size | Name | Contents | | |||
| |---|---|---|---| | |||
| | `$0000-$1FFF` | | hidden | Covered by bottom common RAM in compiler states | | |||
| | `$2000-$BFFF` | 40960 | BANK1 / `TABLES` | Names, symbols, class/interface/type records, pass workspaces | | |||
| | `$C000-$FFFF` | | unmapped | System ROM is visible here instead; BASIC string space at the top of bank 1 is untouched | | |||
| Bank-1 data is reached only through `mmu.s` helpers (`mmu_peek1`/`mmu_poke1` | |||
| with the zero-page pointer `mmu_ptr`, and later block-copy routines), or by | |||
| code in COMMON. MAIN code never runs while bank 1 is mapped, because MAIN above | |||
| `$1FFF` is not visible then. | |||
| BASIC 7 keeps variables in bank 1 from `$0400`. The compiler is started with | |||
| `RUN`, which clears variables, so overwriting `$2000-$BFFF` loses no BASIC | |||
| state. | |||
| ## Zero Page | |||
| In compiler states P0 points at bank 0 page `$1A`, so the compiler owns the | |||
| whole logical zero page `$02-$FF` (linker area `ZP`, 254 bytes). `$00/$01` are | |||
| the 8502 port and are never allocated. Modules allocate zero page only through | |||
| the `ZEROPAGE` segment. Current users: `mmu_ptr` (2 bytes). | |||
| System zero page stays intact. KERNAL calls switch P0 back to `$0000` for the | |||
| call. While P0 is relocated, the system zero page is still readable at | |||
| `$1A00-$1AFF` (the MMU swaps the two pages), e.g. `ST` at `$1A90`. | |||
| ## Interrupts | |||
| - **IRQ:** masked in both compiler states. The ROM IRQ path (`$FF17`) jumps via | |||
| `$0314` into BASIC's handler, which uses system zero page and would corrupt | |||
| the private page. `kcall` enables IRQs only for the duration of a KERNAL call, | |||
| in system state. | |||
| - **NMI:** cannot be masked. The ROM entry `$FF05` uses no zero page: it pushes | |||
| A/X/Y/CR, sets CR=0, and jumps via `$0318`. `mmu_init` points `$0318` at | |||
| `mmu_nmi_guard` (in COMMON), which reads the CIA #2 ICR, counts the event, | |||
| and exits through `$FF33` (pull CR/Y/X/A, RTI). RESTORE is therefore ignored | |||
| while the compiler runs. `mmu_shutdown` restores `$0318`. | |||
| ## Test Mailbox | |||
| Defined in `src/common/mailbox.inc`. Used by platform tests, the compiler, and | |||
| generated programs, and read by the harness after the program returns. | |||
| | Address | Field | Meaning | | |||
| |---|---|---| | |||
| | `$1BF0` | magic 0 | `$4E` (`N`) once initialized | | |||
| | `$1BF1` | magic 1 | `$38` (`8`) | | |||
| | `$1BF2` | producer | `$01` platform test, `$02` compiler, `$03` generated program | | |||
| | `$1BF3` | status | `$01` running, `$A5` pass, `$FA` fail | | |||
| | `$1BF4-$1BF5` | code | Little-endian diagnostic, fault, or failed-check code; 0 on pass | | |||
| | `$1BF6-$1BF7` | address | Little-endian fault or completion address | | |||
| | `$1BF8-$1BFF` | data | Eight producer-defined bytes | | |||
| Protocol: | |||
| 1. The producer clears all 16 bytes, writes the magic and producer, then sets | |||
| status `$01` before doing any work. | |||
| 2. It writes code, address, and data, then writes the final status **last**. | |||
| 3. The harness polls until the magic matches and status is `$A5` or `$FA`. A | |||
| status still `$01` at the timeout means the producer hung or crashed. A | |||
| missing magic means it never started. | |||
| ## Evidence | |||
| `scripts/verify-map-skeleton.ps1` links `src/compiler/mmu.s` with | |||
| `tests/platform/map-skeleton.s` using `cfg/compiler.cfg`, then: | |||
| 1. Builds five variants that overflow ZP, COMMON, MAIN, SWSTACK, and TABLES by | |||
| one region's worth. ld65 rejects each in the expected memory area. | |||
| 2. Checks fixed placements: STARTUP and `skel_entry` at `$1C0D`, MAILBOX at | |||
| `$1BF0`, COMMON within `$1300-$18FF`, TABLES within `$2000-$BFFF`. | |||
| 3. Runs the skeleton in x128 and reads the mailbox: | |||
| `4E 38 01 A5 00 00 … 01 4E 1A C5`. All six checks pass: | |||
| | Check | What it proves | | |||
| |---|---| | |||
| | ZP | A store to logical ZP lands at `$1Axx`; system ZP is unchanged | | |||
| | KCALL | CHROUT via `kcall_inline` from both states; private ZP and CR/P0 intact afterwards | | |||
| | BANK1 | First and last TABLES bytes hold bank-1 values; bank 0 at the same addresses unchanged | | |||
| | COMMON | DISKBUF/SCRATCH written in bank 0 read back in bank 1; mailbox written from bank 1 | | |||
| | NMI | A CIA #2 timer NMI taken in the bank-1 state returns with CR `$4E`, P0 `$1A`, sentinel `$C5`, and the same SP | | |||
| | RESTORE | After `mmu_shutdown`: CR/RCR/P0/P1/`$0318` and SP equal their entry values | | |||
| Negative control: forcing P0L to `$00` in `mmu_bank0/1` makes the skeleton | |||
| report FAIL with code `$000F` (ZP, KCALL, BANK1, NMI). | |||
| @@ -0,0 +1,92 @@ | |||
| --- | |||
| story: E01-S05 | |||
| owner: e01_memory_map | |||
| status: done | |||
| started: 2026-11-07 | |||
| depends-on: [E01-S02, E01-S03, E01-S04] | |||
| --- | |||
| # E01-S05 Claim | |||
| ## Intended change | |||
| Freeze the compiler memory map from E01 evidence: common-RAM placement, a | |||
| private compiler zero page via P0, bank-1 table space, and a fixed test | |||
| mailbox. Provide `cfg/compiler.cfg`, `src/common/c128.inc`, | |||
| `src/common/mailbox.inc`, and the production `src/compiler/mmu.s` (bank | |||
| switching, KERNAL wrapper, NMI guard), then prove them with a linked skeleton | |||
| in x128. | |||
| ## File area | |||
| - `cfg/compiler.cfg` | |||
| - `src/common/c128.inc`, `src/common/mailbox.inc` | |||
| - `src/compiler/mmu.s` | |||
| - `tests/platform/map-skeleton.s` | |||
| - `scripts/build-map-skeleton.ps1`, `scripts/verify-map-skeleton.ps1` | |||
| - `docs/architecture.md` section 4, `docs/decisions.md` | |||
| - `planning/claims/E01-S05.md` | |||
| - generated files under `build/` | |||
| ## Acceptance evidence | |||
| - [x] `docs/architecture.md` lists verified regions and reservations | |||
| (section 4.4; full table and evidence in `docs/memory-map.md`). | |||
| - [x] `cfg/compiler.cfg` can link a capacity-checking skeleton (normal link | |||
| passes; ZP, COMMON, MAIN, SWSTACK, and TABLES overflow builds are each | |||
| rejected by ld65 in the expected memory area). | |||
| - [x] `src/common/c128.inc` defines observed hardware/KERNAL constants, each | |||
| annotated with the document holding its evidence. | |||
| - [x] The test mailbox address and protocol are documented and exercised | |||
| (`$1BF0`, `src/common/mailbox.inc`; x128 run reads | |||
| `4E 38 01 A5 00 00 ...`). | |||
| ## Coordination notes | |||
| Absorbs the E01-S03 open NMI risk: the skeleton takes a CIA #2 timer NMI while | |||
| bank 1 and the private zero page are active. | |||
| ## Work log | |||
| - Read from x128 ROM: `$FFFA` -> `$FF05` (SEI, push A/X/Y/CR, CR=0, | |||
| `JMP ($0318)`); `$FF33` pops CR/Y/X/A and RTIs; `$FF17` IRQ path jumps via | |||
| `$0314` into BASIC, which uses system zero page. `$1300-$1BFF` holds only | |||
| the power-on fill pattern after boot. | |||
| - First x128 run never touched the mailbox: `mmu.s` linked first, so | |||
| `mmu_init` sat at the `SYS 7181` target. Added a `STARTUP` segment placed | |||
| first and a build check that the entry label is at `$1C0D`. | |||
| - Memory-area and segment `define` symbols collided (`__DISKBUF_SIZE__`); | |||
| only the memory areas define symbols now. | |||
| - A CIA #2 timer NMI taken in the bank-1 state with the private zero page | |||
| returns cleanly through ROM `$FF05` -> `mmu_nmi_guard` -> `$FF33`, which | |||
| closes the E01-S03 NMI risk. | |||
| - Negative control: forcing P0L to `$00` in `mmu_bank0/1` gives mailbox | |||
| FAIL code `$000F` (ZP, KCALL, BANK1, NMI). Source restored afterwards. | |||
| - Found that the earlier branch switch/merge had rewritten all committed | |||
| `newLang/` text files with CRLF (`core.autocrlf=true`), breaking the grammar | |||
| validator. Added `newLang/** text=auto eol=lf` to `.gitattributes` and | |||
| converted the 79 files back to LF; they match the committed blobs again. | |||
| ## Completion | |||
| - Tests run: `scripts/verify-map-skeleton.ps1` (includes five overflow | |||
| builds), mutation run, `tests/grammar/validate.ps1`, | |||
| `tests/disk/prepare-disk.tests.ps1`, `scripts/verify-minimal.ps1`, | |||
| `scripts/verify-display-clock.ps1`, `scripts/verify-mmu-probe.ps1`, | |||
| `scripts/verify-kernal-probe.ps1`. | |||
| - Results: skeleton PASS with code `$0000`; one NMI under CR `$4E` / P0 `$1A` | |||
| with sentinel `$C5` intact; screenshot shows both KCALL lines and | |||
| `NOVA128 MAP SKELETON OK`; all earlier regressions pass, and the disk | |||
| builder still produces SHA-256 `208EF297...`. | |||
| - Files changed: `cfg/compiler.cfg`, `src/common/c128.inc`, | |||
| `src/common/mailbox.inc`, `src/compiler/mmu.s`, | |||
| `tests/platform/map-skeleton.s`, `scripts/build-map-skeleton.ps1`, | |||
| `scripts/verify-map-skeleton.ps1`, `docs/memory-map.md`, | |||
| `docs/architecture.md`, `docs/decisions.md` (D117), this claim, and the | |||
| repository `.gitattributes`. | |||
| - Remaining risks: generated-program map still open (E04); bank-1 block-copy | |||
| routines not yet written (only byte peek/poke); RESTORE is ignored during | |||
| compilation (documented in D117); IRQs stay masked, so long compiler phases | |||
| freeze the jiffy clock and keyboard except during KERNAL calls; VICE-only | |||
| evidence. | |||
| @@ -0,0 +1,105 @@ | |||
| # E01-S05: link the memory-map skeleton against cfg/compiler.cfg. | |||
| # build-map-skeleton.ps1 normal build, prints segment usage | |||
| # build-map-skeleton.ps1 -Overflow ZP build that must fail in ld65 | |||
| [CmdletBinding()] | |||
| param( | |||
| [ValidateSet("", "ZP", "COMMON", "MAIN", "SWSTACK", "TABLES")] | |||
| [string]$Overflow = "" | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $config = Join-Path $project "cfg\compiler.cfg" | |||
| $sources = @( | |||
| (Join-Path $project "src\compiler\mmu.s"), | |||
| (Join-Path $project "tests\platform\map-skeleton.s") | |||
| ) | |||
| $suffix = if ($Overflow) { "-overflow-" + $Overflow.ToLowerInvariant() } else { "" } | |||
| $stem = Join-Path $build ("map-skeleton" + $suffix) | |||
| $program = "$stem.prg" | |||
| $map = "$stem.map" | |||
| $labels = "$stem.lbl" | |||
| $ca65 = (Get-Command ca65 -ErrorAction Stop).Source | |||
| $ld65 = (Get-Command ld65 -ErrorAction Stop).Source | |||
| New-Item -ItemType Directory -Force -Path $build | Out-Null | |||
| foreach ($path in @($program, $map, $labels)) { | |||
| if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force } | |||
| } | |||
| $objects = @() | |||
| foreach ($source in $sources) { | |||
| $object = Join-Path $build (([IO.Path]::GetFileNameWithoutExtension($source)) + $suffix + ".o") | |||
| $defines = @() | |||
| if ($Overflow) { $defines = @("-D", "OVERFLOW_$Overflow=1") } | |||
| & $ca65 --cpu 6502 @defines -o $object $source | |||
| if ($LASTEXITCODE -ne 0) { throw "ca65 failed on $source" } | |||
| $objects += $object | |||
| } | |||
| $linkOutput = & $ld65 -C $config -o $program -m $map -Ln $labels @objects 2>&1 | |||
| $linkExit = $LASTEXITCODE | |||
| if ($Overflow) { | |||
| $text = ($linkOutput | Out-String) | |||
| if ($linkExit -eq 0) { throw "OVERFLOW_$Overflow linked; capacity check failed to fire" } | |||
| if ($text -notmatch "(?i)overflows memory area") { | |||
| throw "OVERFLOW_$Overflow failed for an unexpected reason: $text" | |||
| } | |||
| $area = [regex]::Match($text, "(?i)overflows memory area\W+(\w+)").Groups[1].Value | |||
| Write-Host ("overflow {0}: ld65 rejected it (memory area {1})" -f $Overflow, $area) | |||
| return | |||
| } | |||
| if ($linkExit -ne 0) { throw ("ld65 failed:`n" + ($linkOutput | Out-String)) } | |||
| # Segment usage from the map file: name, start, end, size. | |||
| $segments = @{} | |||
| $mapText = Get-Content -Raw -LiteralPath $map | |||
| $block = [regex]::Match($mapText, "(?s)Segment list:.*?-{5,}\r?\n(.*?)\r?\n\r?\n").Groups[1].Value | |||
| foreach ($line in ($block -split "\r?\n")) { | |||
| $m = [regex]::Match($line, "^(\w+)\s+([0-9A-F]{6})\s+([0-9A-F]{6})\s+([0-9A-F]{6})") | |||
| if ($m.Success) { | |||
| $segments[$m.Groups[1].Value] = @{ | |||
| Start = [Convert]::ToInt32($m.Groups[2].Value, 16) | |||
| End = [Convert]::ToInt32($m.Groups[3].Value, 16) | |||
| Size = [Convert]::ToInt32($m.Groups[4].Value, 16) | |||
| } | |||
| } | |||
| } | |||
| # Contract checks that ld65 cannot express: fixed placements. | |||
| function Require-Segment([string]$name, [int]$lo, [int]$hi) { | |||
| if (-not $segments.ContainsKey($name)) { throw "segment $name missing from map" } | |||
| $seg = $segments[$name] | |||
| if ($seg.Size -gt 0 -and ($seg.Start -lt $lo -or $seg.End -gt $hi)) { | |||
| throw ("segment {0} `${1:X4}-`${2:X4} outside `${3:X4}-`${4:X4}" -f | |||
| $name, $seg.Start, $seg.End, $lo, $hi) | |||
| } | |||
| } | |||
| Require-Segment "ZEROPAGE" 0x0002 0x00FF | |||
| Require-Segment "COMMON" 0x1300 0x18FF | |||
| Require-Segment "MAILBOX" 0x1BF0 0x1BFF | |||
| Require-Segment "STARTUP" 0x1C0D 0xB7FF | |||
| Require-Segment "CODE" 0x1C0D 0xB7FF | |||
| if ($segments["STARTUP"].Start -ne 0x1C0D) { throw "STARTUP does not begin at `$1C0D" } | |||
| $labelText = Get-Content -Raw -LiteralPath $labels | |||
| if ($labelText -notmatch "(?im)^al\s+001C0D\s+\.skel_entry\s*$") { | |||
| throw "skel_entry is not at `$1C0D (the SYS 7181 target)" | |||
| } | |||
| Require-Segment "SWSTACK" 0xB800 0xBFFF | |||
| Require-Segment "TABLES" 0x2000 0xBFFF | |||
| if ($segments["MAILBOX"].Start -ne 0x1BF0) { throw "mailbox is not at `$1BF0" } | |||
| $bytes = [IO.File]::ReadAllBytes($program) | |||
| if ($bytes[0] -ne 0x01 -or $bytes[1] -ne 0x1C) { throw "load address is not `$1C01" } | |||
| Write-Host ("built {0} ({1} bytes)" -f $program, $bytes.Length) | |||
| $limits = [ordered]@{ | |||
| ZEROPAGE = 0x00FE; COMMON = 0x0600; SWSTACK = 0x0800; TABLES = 0xA000 | |||
| } | |||
| foreach ($name in ($segments.Keys | Sort-Object { $segments[$_].Start })) { | |||
| $seg = $segments[$name] | |||
| $limit = if ($limits.Contains($name)) { " of {0}" -f $limits[$name] } else { "" } | |||
| Write-Host (" {0,-9} `${1:X4}-`${2:X4} {3,6} bytes{4}" -f | |||
| $name, $seg.Start, $seg.End, $seg.Size, $limit) | |||
| } | |||
| @@ -0,0 +1,130 @@ | |||
| # E01-S05: prove cfg/compiler.cfg capacity checks, then run the map skeleton | |||
| # in VICE x128 and verify the fixed test mailbox at $1BF0. | |||
| [CmdletBinding()] | |||
| param( | |||
| [int]$RemotePort = 6513, | |||
| [int]$TimeoutSeconds = 30 | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| $project = Split-Path $PSScriptRoot -Parent | |||
| $build = Join-Path $project "build" | |||
| $program = Join-Path $build "map-skeleton.prg" | |||
| $screenshot = Join-Path $build "map-skeleton-vicii.png" | |||
| $x128 = "C:\Program Files\GTK3VICE-3.10-win64\bin\x128.exe" | |||
| $mailbox = 0x1BF0 | |||
| # 1. Every bounded region must reject an overflow at link time. | |||
| foreach ($region in "ZP", "COMMON", "MAIN", "SWSTACK", "TABLES") { | |||
| & (Join-Path $PSScriptRoot "build-map-skeleton.ps1") -Overflow $region | |||
| } | |||
| # 2. The real skeleton. | |||
| & (Join-Path $PSScriptRoot "build-map-skeleton.ps1") | |||
| if (-not (Test-Path -LiteralPath $x128)) { throw "x128.exe not found at $x128" } | |||
| if (Test-Path -LiteralPath $screenshot) { Remove-Item -LiteralPath $screenshot -Force } | |||
| $arguments = @( | |||
| "-autostart", ('"' + $program + '"'), | |||
| "-autostart-warp", "-warp", "-sounddev", "dummy", | |||
| "-exitscreenshotvicii", ('"' + $screenshot + '"'), | |||
| "-remotemonitor", "-remotemonitoraddress", ("ip4://127.0.0.1:{0}" -f $RemotePort) | |||
| ) -join ' ' | |||
| $psi = [Diagnostics.ProcessStartInfo]::new() | |||
| $psi.FileName = $x128 | |||
| $psi.Arguments = $arguments | |||
| $psi.UseShellExecute = $true | |||
| $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden | |||
| $process = [Diagnostics.Process]::Start($psi) | |||
| $client = $null | |||
| function Read-Available($stream) { | |||
| $buffer = New-Object byte[] 16384 | |||
| $text = "" | |||
| Start-Sleep -Milliseconds 300 | |||
| while ($stream.DataAvailable) { | |||
| $count = $stream.Read($buffer, 0, $buffer.Length) | |||
| if ($count -le 0) { break } | |||
| $text += [Text.Encoding]::ASCII.GetString($buffer, 0, $count) | |||
| Start-Sleep -Milliseconds 50 | |||
| } | |||
| return $text | |||
| } | |||
| function Convert-Dump([string]$text) { | |||
| $memory = @{} | |||
| $rows = [regex]::Matches($text, "(?i)C:([0-9a-f]{4})((?:[ ]{1,2}[0-9a-f]{2}){1,16})") | |||
| foreach ($row in $rows) { | |||
| $address = [Convert]::ToInt32($row.Groups[1].Value, 16) | |||
| foreach ($hex in ($row.Groups[2].Value.Trim() -split '\s+')) { | |||
| $memory[$address] = [Convert]::ToInt32($hex, 16) | |||
| $address++ | |||
| } | |||
| } | |||
| return $memory | |||
| } | |||
| try { | |||
| $deadline = [DateTime]::UtcNow.AddSeconds(10) | |||
| while ([DateTime]::UtcNow -lt $deadline) { | |||
| if ($process.HasExited) { throw "x128 exited before remote verification" } | |||
| try { | |||
| $client = [Net.Sockets.TcpClient]::new() | |||
| $client.Connect("127.0.0.1", $RemotePort) | |||
| break | |||
| } catch { | |||
| if ($client) { $client.Dispose(); $client = $null } | |||
| Start-Sleep -Milliseconds 200 | |||
| } | |||
| } | |||
| if (-not $client -or -not $client.Connected) { throw "could not connect to VICE remote monitor" } | |||
| $stream = $client.GetStream() | |||
| $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true) | |||
| $writer.NewLine = "`n" | |||
| $writer.AutoFlush = $true | |||
| # Mailbox protocol: magic "N8", then status leaves RUNNING ($01). | |||
| $memory = @{} | |||
| $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) | |||
| while ($true) { | |||
| Start-Sleep -Seconds 1 | |||
| $writer.WriteLine(("m `${0:X4} `${1:X4}" -f $mailbox, ($mailbox + 15))) | |||
| $chunk = Read-Available $stream | |||
| $memory = Convert-Dump $chunk | |||
| $magic = ($memory[$mailbox] -eq 0x4E -and $memory[$mailbox + 1] -eq 0x38) | |||
| if ($magic -and $memory[$mailbox + 3] -in 0xA5, 0xFA) { break } | |||
| if ([DateTime]::UtcNow -ge $deadline) { | |||
| throw "mailbox never reached a final status; last response: $chunk" | |||
| } | |||
| $writer.WriteLine("x") | |||
| Read-Available $stream | Out-Null | |||
| } | |||
| $writer.WriteLine("x") | |||
| Start-Sleep -Seconds 2 # let the OK line print | |||
| $writer.WriteLine("m 0000 0000") | |||
| Read-Available $stream | Out-Null | |||
| $bytes = 0..15 | ForEach-Object { "{0:X2}" -f $memory[$mailbox + $_] } | |||
| $code = $memory[$mailbox + 4] + 256 * $memory[$mailbox + 5] | |||
| $writer.WriteLine("quit") | |||
| $writer.Dispose(); $client.Dispose(); $client = $null | |||
| if (-not $process.WaitForExit(10000)) { throw "x128 did not exit after monitor quit" } | |||
| Write-Host ("mailbox `$1BF0: {0}" -f ($bytes -join ' ')) | |||
| $checks = @(@(0x01, "ZP"), @(0x02, "KCALL"), @(0x04, "BANK1"), @(0x08, "NMI"), | |||
| @(0x10, "COMMON"), @(0x20, "RESTORE")) | |||
| $failed = @($checks | Where-Object { $code -band $_[0] } | ForEach-Object { $_[1] }) | |||
| if ($memory[$mailbox + 2] -ne 0x01) { throw "producer is not the platform skeleton" } | |||
| if ($memory[$mailbox + 3] -ne 0xA5 -or $code -ne 0) { | |||
| throw ("skeleton FAILED checks: {0} (code `${1:X4})" -f ($failed -join ', '), $code) | |||
| } | |||
| $nmis = $memory[$mailbox + 8] | |||
| if ($nmis -lt 1) { throw "no NMI was counted" } | |||
| Write-Host ("verified mailbox PASS, code `$0000, {0} NMI(s) under CR `${1:X2} / P0 page `${2:X2}, ZP sentinel `${3:X2}" -f | |||
| $nmis, $memory[$mailbox + 9], $memory[$mailbox + 10], $memory[$mailbox + 11]) | |||
| if (Test-Path -LiteralPath $screenshot) { Write-Host ("screenshot: {0}" -f $screenshot) } | |||
| } finally { | |||
| if ($client) { $client.Dispose() } | |||
| if ($process -and -not $process.HasExited) { $process.Kill(); $process.WaitForExit() } | |||
| } | |||
| @@ -0,0 +1,70 @@ | |||
| ; c128.inc - C128 hardware and KERNAL constants used by Nova128. | |||
| ; | |||
| ; Every value here was exercised in VICE x128 3.10 by an E01 probe; the | |||
| ; evidence column names the document. Add a constant only with evidence. | |||
| .ifndef C128_INC | |||
| C128_INC = 1 | |||
| ; ------------------------------------------------------------------ MMU | |||
| MMU_CR = $FF00 ; configuration register, visible in all maps | |||
| MMU_RCR = $D506 ; RAM/common config (I/O must be mapped) | |||
| MMU_P0L = $D507 ; page-zero pointer, low commits (write H first) | |||
| MMU_P0H = $D508 | |||
| MMU_P1L = $D509 ; page-one pointer, low commits (write H first) | |||
| MMU_P1H = $D50A | |||
| CR_BANK15 = $00 ; BASIC+KERNAL+I/O, RAM bank 0 (BASIC default) | |||
| CR_RAM0_KERNAL = $0E ; RAM bank 0 $0000-$BFFF, system ROM, I/O | |||
| CR_RAM1_KERNAL = $4E ; same with RAM bank 1 | |||
| CR_BANK1_BIT = $40 | |||
| RCR_VIC_MASK = $C0 ; RCR bits 6-7: VIC bank, always preserved | |||
| RCR_COMMON_8K_BOTTOM = $06 ; 8K common $0000-$1FFF (bank 0) | |||
| ; docs/mmu-probe.md, docs/kernal-wrapper.md | |||
| ; ------------------------------------------------- interrupt entry (ROM) | |||
| VEC_NMI_HW = $FFFA ; -> $FF05 in system ROM | |||
| ROM_NMI_ENTRY = $FF05 ; SEI; push A,X,Y,CR; CR=0; JMP (NMINV) | |||
| ROM_IRQ_ENTRY = $FF17 ; push A,X,Y,CR; CR=0; JMP (CINV/CBINV) | |||
| ROM_INT_EXIT = $FF33 ; pull CR,Y,X,A; RTI | |||
| CINV = $0314 ; IRQ vector (BASIC's handler uses system ZP) | |||
| NMINV = $0318 ; NMI RAM vector | |||
| ; docs/memory-map.md (read from x128 ROM) | |||
| ; -------------------------------------------------------------- CIA #2 | |||
| CIA2_TALO = $DD04 | |||
| CIA2_TAHI = $DD05 | |||
| CIA2_ICR = $DD0D ; NMI source; read acknowledges | |||
| CIA2_CRA = $DD0E | |||
| CIA_ICR_SET = $80 | |||
| CIA_ICR_TA = $01 | |||
| CIA_CRA_START = $01 | |||
| CIA_CRA_ONESHOT = $08 | |||
| CIA_CRA_LOAD = $10 | |||
| ; ------------------------------------------------------ video and clock | |||
| MODE = $D7 ; bit 7: $80 = 80-column editor active | |||
| VIC_CLKRATE = $D030 ; bit 0: 2 MHz (VIC display off) | |||
| ; docs/platform-display-clock.md | |||
| ; --------------------------------------------------- KERNAL zero page | |||
| STATUS = $90 ; ST, as returned by READST | |||
| LDTND = $98 ; number of open logical files | |||
| ; ---------------------------------------------------------- KERNAL calls | |||
| SETBNK = $FF68 ; A = data bank, X = filename bank | |||
| READST = $FFB7 | |||
| SETLFS = $FFBA | |||
| SETNAM = $FFBD | |||
| OPEN = $FFC0 ; no-name OPEN sends nothing on the bus | |||
| CLOSE = $FFC3 | |||
| CHKIN = $FFC6 ; absent device: C=1, A=5, ST=$80 | |||
| CLRCHN = $FFCC | |||
| CHRIN = $FFCF | |||
| CHROUT = $FFD2 | |||
| ; docs/kernal-wrapper.md | |||
| KERNAL_ERR_DEVICE_NOT_PRESENT = 5 | |||
| .endif | |||
| @@ -0,0 +1,31 @@ | |||
| ; mailbox.inc - fixed test mailbox shared by the compiler, generated | |||
| ; programs, and the x128 harness. Protocol: docs/memory-map.md. | |||
| ; | |||
| ; The mailbox is in bank 0 inside bottom common RAM, so it is readable and | |||
| ; writable from every Nova128 mapping and from the VICE monitor after exit. | |||
| .ifndef MAILBOX_INC | |||
| MAILBOX_INC = 1 | |||
| MB_BASE = $1BF0 | |||
| MB_MAGIC0 = MB_BASE + 0 ; 'N' ($4E) once the mailbox is initialized | |||
| MB_MAGIC1 = MB_BASE + 1 ; '8' ($38) | |||
| MB_PRODUCER = MB_BASE + 2 ; who wrote it (MB_PROD_*) | |||
| MB_STATUS = MB_BASE + 3 ; MB_ST_* | |||
| MB_CODE = MB_BASE + 4 ; 2 bytes LE: diagnostic/fault/check code | |||
| MB_ADDR = MB_BASE + 6 ; 2 bytes LE: fault or check address | |||
| MB_DATA = MB_BASE + 8 ; 8 bytes producer-defined payload | |||
| MB_SIZE = 16 | |||
| MB_MAGIC0_VALUE = $4E | |||
| MB_MAGIC1_VALUE = $38 | |||
| MB_PROD_PLATFORM = $01 ; platform probe/skeleton | |||
| MB_PROD_COMPILER = $02 ; N128C | |||
| MB_PROD_PROGRAM = $03 ; generated Nova128 program | |||
| MB_ST_RUNNING = $01 ; written first; still set => hung or crashed | |||
| MB_ST_PASS = $A5 | |||
| MB_ST_FAIL = $FA | |||
| .endif | |||
| @@ -0,0 +1,286 @@ | |||
| ; mmu.s - the only Nova128 compiler module that changes the C128 MMU. | |||
| ; | |||
| ; Compiler mappings (docs/memory-map.md): | |||
| ; bank-0 state: CR=$0E, RCR=8K bottom common, P0 -> private page | |||
| ; bank-1 state: CR=$4E, otherwise identical | |||
| ; system state: whatever BASIC had at entry (saved by mmu_init) | |||
| ; | |||
| ; Rules every caller relies on: | |||
| ; - IRQs stay masked in both compiler states. The ROM IRQ path jumps into | |||
| ; BASIC's handler, which uses system zero page; with P0 relocated it would | |||
| ; corrupt the compiler's private zero page. | |||
| ; - KERNAL routines run only through kcall/kcall_inline, which switch to the | |||
| ; system state (including system zero page) and back. | |||
| ; - NMIs (RESTORE, CIA #2) are acknowledged and ignored by mmu_nmi_guard until | |||
| ; mmu_shutdown restores the system NMI vector. | |||
| ; - Everything in the COMMON segment runs from bottom common RAM, so it can be | |||
| ; called from either compiler state. mmu_init (CODE) runs in system state. | |||
| ; | |||
| ; P0H is always 0: E01-S02 showed that under bottom common the P0 bank bit is | |||
| ; forced to bank 0 anyway, and the private page lives in bank 0. | |||
| .setcpu "6502" | |||
| .include "../common/c128.inc" | |||
| .import __COMMON_LOAD__, __COMMON_RUN__, __COMMON_SIZE__ | |||
| .import __COMPILER_ZP_PAGE__ | |||
| .export mmu_init, mmu_bank0, mmu_bank1, mmu_system, mmu_shutdown | |||
| .export kcall, kcall_inline, kc_target | |||
| .export mmu_nmi_guard, mmu_nmi_count | |||
| .export mmu_peek1, mmu_poke1 | |||
| .exportzp mmu_ptr | |||
| .export mmu_sys_cr, mmu_sys_rcr, mmu_sys_p0l, mmu_sys_p0h | |||
| .export mmu_sys_p1l, mmu_sys_p1h, mmu_sys_nmi, mmu_comp_rcr | |||
| .segment "ZEROPAGE" | |||
| mmu_ptr: .res 2 ; bank-1 access pointer (private page) | |||
| ; ------------------------------------------------------------------ CODE | |||
| .segment "CODE" | |||
| ; Call in system state with IRQs masked, before any other mmu routine. | |||
| ; Copies the COMMON segment to its run address, records the system state, | |||
| ; and installs the NMI guard. Clobbers A/X/Y. Leaves system state mapped. | |||
| mmu_init: | |||
| ldx #<__COMMON_SIZE__ ; whole pages first, then the remainder | |||
| ldy #>__COMMON_SIZE__ | |||
| lda #<__COMMON_LOAD__ | |||
| sta @src+1 | |||
| lda #>__COMMON_LOAD__ | |||
| sta @src+2 | |||
| lda #<__COMMON_RUN__ | |||
| sta @dst+1 | |||
| lda #>__COMMON_RUN__ | |||
| sta @dst+2 | |||
| cpy #$00 | |||
| beq @tail | |||
| @page: jsr @copy256 | |||
| inc @src+2 | |||
| inc @dst+2 | |||
| dey | |||
| bne @page | |||
| @tail: cpx #$00 | |||
| beq @copied | |||
| ldy #$00 | |||
| @last: jsr @one | |||
| iny | |||
| dex | |||
| bne @last | |||
| beq @copied | |||
| @copy256: | |||
| sty @savey | |||
| ldy #$00 | |||
| @full: jsr @one | |||
| iny | |||
| bne @full | |||
| ldy @savey | |||
| rts | |||
| @one: | |||
| @src: lda $FFFF,y ; patched | |||
| @dst: sta $FFFF,y ; patched | |||
| rts | |||
| @savey: .byte $00 | |||
| @copied: | |||
| lda MMU_CR | |||
| sta mmu_sys_cr | |||
| lda MMU_RCR | |||
| sta mmu_sys_rcr | |||
| and #RCR_VIC_MASK | |||
| ora #RCR_COMMON_8K_BOTTOM | |||
| sta mmu_comp_rcr | |||
| lda MMU_P0L | |||
| sta mmu_sys_p0l | |||
| lda MMU_P0H | |||
| sta mmu_sys_p0h | |||
| lda MMU_P1L | |||
| sta mmu_sys_p1l | |||
| lda MMU_P1H | |||
| sta mmu_sys_p1h | |||
| lda NMINV | |||
| sta mmu_sys_nmi | |||
| lda NMINV+1 | |||
| sta mmu_sys_nmi+1 | |||
| lda #$00 | |||
| sta mmu_nmi_count | |||
| lda #<mmu_nmi_guard | |||
| sta NMINV | |||
| lda #>mmu_nmi_guard | |||
| sta NMINV+1 | |||
| rts | |||
| ; ---------------------------------------------------------------- COMMON | |||
| .segment "COMMON" | |||
| ; Enter a compiler state from any state. Clobbers A only; flags undefined. | |||
| ; Order: common RAM first, so the code stays visible when CR selects bank 1. | |||
| mmu_bank0: | |||
| lda #CR_RAM0_KERNAL | |||
| .byte $2C ; BIT abs: skip the next LDA | |||
| mmu_bank1: | |||
| lda #CR_RAM1_KERNAL | |||
| pha | |||
| lda mmu_comp_rcr | |||
| sta MMU_RCR | |||
| lda #$00 | |||
| sta MMU_P0H ; high first, low commits | |||
| lda #<__COMPILER_ZP_PAGE__ | |||
| sta MMU_P0L | |||
| pla | |||
| sta MMU_CR | |||
| rts | |||
| ; Return to the saved system state, NMI guard still installed. Clobbers A. | |||
| ; Order: CR first (system CR selects bank 0, where this code also lives). | |||
| mmu_system: | |||
| lda mmu_sys_cr | |||
| sta MMU_CR | |||
| lda mmu_sys_rcr | |||
| sta MMU_RCR | |||
| lda mmu_sys_p0h | |||
| sta MMU_P0H | |||
| lda mmu_sys_p0l | |||
| sta MMU_P0L | |||
| lda mmu_sys_p1h | |||
| sta MMU_P1H | |||
| lda mmu_sys_p1l | |||
| sta MMU_P1L | |||
| rts | |||
| ; Final exit: system state plus the system NMI vector. Clobbers A. | |||
| mmu_shutdown: | |||
| jsr mmu_system | |||
| lda mmu_sys_nmi | |||
| sta NMINV | |||
| lda mmu_sys_nmi+1 | |||
| sta NMINV+1 | |||
| rts | |||
| ; KERNAL wrapper (contract proven by E01-S03, extended with P0): | |||
| ; in: kc_target = KERNAL entry; A/X/Y = routine arguments | |||
| ; out: A/X/Y and N/V/Z/C as the routine returned them, caller's I flag, | |||
| ; CR/RCR/P0 exactly as at the call | |||
| ; Runs the routine in system state with IRQs enabled. | |||
| kcall: | |||
| php | |||
| sei | |||
| sta kc_a | |||
| pla | |||
| sta kc_caller_p | |||
| lda MMU_CR | |||
| sta kc_saved_cr | |||
| lda MMU_RCR | |||
| sta kc_saved_rcr | |||
| lda MMU_P0L | |||
| sta kc_saved_p0l | |||
| lda mmu_sys_cr ; bank 0 first: kcall lives in bank 0 | |||
| sta MMU_CR | |||
| lda mmu_sys_rcr | |||
| sta MMU_RCR | |||
| lda mmu_sys_p0h | |||
| sta MMU_P0H | |||
| lda mmu_sys_p0l ; system zero page for the KERNAL | |||
| sta MMU_P0L | |||
| cli | |||
| lda kc_a | |||
| jsr @jump | |||
| php | |||
| sei | |||
| sta kc_a | |||
| pla | |||
| sta kc_result_p | |||
| lda #$00 | |||
| sta MMU_P0H | |||
| lda kc_saved_p0l | |||
| sta MMU_P0L | |||
| lda kc_saved_rcr ; common before a possible bank-1 CR | |||
| sta MMU_RCR | |||
| lda kc_saved_cr | |||
| sta MMU_CR | |||
| lda kc_caller_p ; result flags with the caller's I flag | |||
| and #$04 | |||
| sta kc_i_bit | |||
| lda kc_result_p | |||
| and #$FB | |||
| ora kc_i_bit | |||
| pha | |||
| lda kc_a | |||
| plp | |||
| rts | |||
| @jump: jmp (kc_target) | |||
| ; Bank-1 byte access for code running in the bank-0 state. The pointer is in | |||
| ; the private zero page, which is visible in both states. | |||
| ; mmu_peek1: A = bank1[(mmu_ptr),Y]; N/Z reflect A | |||
| ; mmu_poke1: bank1[(mmu_ptr),Y] = A | |||
| ; Both preserve X and Y and return in the bank-0 state. | |||
| mmu_peek1: | |||
| jsr mmu_bank1 | |||
| lda (mmu_ptr),y | |||
| pha | |||
| jsr mmu_bank0 | |||
| pla | |||
| rts | |||
| mmu_poke1: | |||
| pha | |||
| jsr mmu_bank1 | |||
| pla | |||
| sta (mmu_ptr),y | |||
| jmp mmu_bank0 | |||
| ; Compact form: JSR kcall_inline / .word target. Preserves A/X/Y in, then | |||
| ; behaves exactly like kcall. | |||
| kcall_inline: | |||
| sta kc_inl_a | |||
| sty kc_inl_y | |||
| pla | |||
| sta @src+1 ; JSR pushed the address of its last byte | |||
| pla | |||
| sta @src+2 | |||
| ldy #1 | |||
| @src: lda $FFFF,y ; patched | |||
| sta kc_target-1,y | |||
| iny | |||
| cpy #3 | |||
| bne @src | |||
| lda @src+1 ; return past the inline word | |||
| clc | |||
| adc #2 | |||
| tay | |||
| lda @src+2 | |||
| adc #0 | |||
| pha | |||
| tya | |||
| pha | |||
| ldy kc_inl_y | |||
| lda kc_inl_a | |||
| jmp kcall | |||
| ; Reached from ROM $FF05 with CR=0 (I/O mapped) and A/X/Y/CR on the stack. | |||
| ; Reading the CIA #2 ICR acknowledges a timer NMI; RESTORE needs no ack. | |||
| mmu_nmi_guard: | |||
| bit CIA2_ICR | |||
| inc mmu_nmi_count | |||
| jmp ROM_INT_EXIT | |||
| kc_target: .word $0000 | |||
| .assert <kc_target <> $FF, error, "kc_target must not straddle a page (JMP bug)" | |||
| kc_a: .byte $00 | |||
| kc_caller_p: .byte $00 | |||
| kc_result_p: .byte $00 | |||
| kc_i_bit: .byte $00 | |||
| kc_saved_cr: .byte $00 | |||
| kc_saved_rcr: .byte $00 | |||
| kc_saved_p0l: .byte $00 | |||
| kc_inl_a: .byte $00 | |||
| kc_inl_y: .byte $00 | |||
| mmu_nmi_count: .byte $00 | |||
| mmu_comp_rcr: .byte $00 | |||
| mmu_sys_cr: .byte $00 | |||
| mmu_sys_rcr: .byte $00 | |||
| mmu_sys_p0l: .byte $00 | |||
| mmu_sys_p0h: .byte $00 | |||
| mmu_sys_p1l: .byte $00 | |||
| mmu_sys_p1h: .byte $00 | |||
| mmu_sys_nmi: .word $0000 | |||
| @@ -0,0 +1,429 @@ | |||
| ; E01-S05: capacity-checking skeleton for cfg/compiler.cfg. | |||
| ; | |||
| ; Links the production src/compiler/mmu.s with the frozen memory map and | |||
| ; proves, in x128, every region and rule the compiler will depend on. Results | |||
| ; go to the fixed test mailbox (src/common/mailbox.inc): | |||
| ; MB_STATUS $A5 pass / $FA fail | |||
| ; MB_CODE 16-bit mask of FAILED checks (CHECK_*), 0 on pass | |||
| ; MB_DATA+0 NMIs counted by mmu_nmi_guard | |||
| ; MB_DATA+1 CR seen right after the NMI (expect $4E) | |||
| ; MB_DATA+2 P0L seen right after the NMI (expect the private page) | |||
| ; MB_DATA+3 private zero-page sentinel after the NMI (expect $C5) | |||
| ; | |||
| ; Assemble with -D OVERFLOW_<REGION>=1 to prove ld65 rejects an overflow of | |||
| ; ZP, COMMON, MAIN, SWSTACK, or TABLES. | |||
| .setcpu "6502" | |||
| .include "../../src/common/c128.inc" | |||
| .include "../../src/common/mailbox.inc" | |||
| .import mmu_init, mmu_bank0, mmu_bank1, mmu_system, mmu_shutdown | |||
| .import mmu_peek1, mmu_poke1, kcall_inline, mmu_nmi_count | |||
| .importzp mmu_ptr | |||
| .import __COMPILER_ZP_PAGE__ | |||
| CHECK_ZP = $0001 ; private zero page isolated from system ZP | |||
| CHECK_KCALL = $0002 ; KERNAL calls from both states keep ZP/MMU | |||
| CHECK_BANK1 = $0004 ; first/last table bytes are bank-1 only | |||
| CHECK_NMI = $0008 ; NMI under bank 1 + private ZP is harmless | |||
| CHECK_COMMON = $0010 ; COMMON/DISKBUF/SCRATCH/MAILBOX shared | |||
| CHECK_RESTORE = $0020 ; exact system state after mmu_shutdown | |||
| CHECK_ALL = $003F | |||
| ZP_SENTINEL = $C5 | |||
| .segment "LOADADDR" | |||
| .word $1C01 | |||
| .segment "EXEHDR" | |||
| .word basic_end | |||
| .word 10 | |||
| .byte $9E | |||
| .byte "7181" ; SYS $1C0D | |||
| .byte $00 | |||
| basic_end: | |||
| .word $0000 | |||
| .segment "ZEROPAGE" | |||
| zp_probe: .res 1 | |||
| .ifdef OVERFLOW_ZP | |||
| .res $FE | |||
| .endif | |||
| .segment "MAILBOX" | |||
| mailbox: .res MB_SIZE | |||
| .assert mailbox = MB_BASE, error, "MAILBOX segment must sit at MB_BASE" | |||
| .segment "DISKBUF" | |||
| diskbuf: .res $100 | |||
| .segment "SCRATCH" | |||
| scratch: .res 1 | |||
| .segment "SWSTACK" | |||
| swstack_first: .res 1 | |||
| .res $0800 - 2 | |||
| swstack_last: .res 1 | |||
| .ifdef OVERFLOW_SWSTACK | |||
| .res 1 | |||
| .endif | |||
| .segment "TABLES" | |||
| tables_first: .res 1 | |||
| .res $A000 - 2 | |||
| tables_last: .res 1 | |||
| .ifdef OVERFLOW_TABLES | |||
| .res 1 | |||
| .endif | |||
| ; --------------------------------------------------------------- STARTUP | |||
| .segment "STARTUP" | |||
| .export skel_entry | |||
| skel_entry: | |||
| php | |||
| sei | |||
| tsx | |||
| stx entry_sp | |||
| ldx #MB_SIZE - 1 ; mailbox: initialized and running | |||
| lda #$00 | |||
| @clr: sta MB_BASE,x | |||
| dex | |||
| bpl @clr | |||
| lda #MB_MAGIC0_VALUE | |||
| sta MB_MAGIC0 | |||
| lda #MB_MAGIC1_VALUE | |||
| sta MB_MAGIC1 | |||
| lda #MB_PROD_PLATFORM | |||
| sta MB_PRODUCER | |||
| lda #MB_ST_RUNNING | |||
| sta MB_STATUS | |||
| lda #<CHECK_ALL ; every check starts failed | |||
| sta fail_mask | |||
| lda #>CHECK_ALL | |||
| sta fail_mask+1 | |||
| lda MMU_CR ; system state for CHECK_RESTORE | |||
| sta entry_state+0 | |||
| ldx #4 | |||
| @save: lda MMU_RCR,x | |||
| sta entry_state+1,x | |||
| dex | |||
| bpl @save | |||
| lda NMINV | |||
| sta entry_state+6 | |||
| lda NMINV+1 | |||
| sta entry_state+7 | |||
| lda zp_probe ; system ZP byte at the probe's offset | |||
| sta sys_zp_before | |||
| jsr mmu_init | |||
| jsr mmu_bank0 | |||
| ; ------------------------------------------- private zero page | |||
| lda #ZP_SENTINEL | |||
| sta zp_probe ; logical ZP -> physical $1Axx | |||
| jsr mmu_system | |||
| lda zp_probe ; system ZP again | |||
| cmp sys_zp_before | |||
| bne @zp_done | |||
| lda __COMPILER_ZP_PAGE__ * 256 + zp_probe | |||
| cmp #ZP_SENTINEL | |||
| bne @zp_done | |||
| lda #<CHECK_ZP | |||
| jsr pass | |||
| @zp_done: | |||
| jsr mmu_bank0 | |||
| ; ---------------------------------- KERNAL calls from both states | |||
| ldy #$00 | |||
| @say0: lda bank0_msg,y | |||
| beq @said0 | |||
| sty print_y | |||
| jsr kcall_inline | |||
| .word CHROUT | |||
| ldy print_y | |||
| iny | |||
| bne @say0 | |||
| @said0: jsr common_say_bank1 ; same from the bank-1 state | |||
| lda zp_probe | |||
| cmp #ZP_SENTINEL | |||
| bne @kc_done | |||
| jsr state_is_bank0 | |||
| bne @kc_done | |||
| lda kc1_ok | |||
| cmp #$01 | |||
| bne @kc_done | |||
| lda #<CHECK_KCALL | |||
| jsr pass | |||
| @kc_done: | |||
| ; ------------------------------------------- bank-1 table window | |||
| lda tables_first ; bank-0 bytes at the same addresses | |||
| sta b0_first | |||
| lda tables_last | |||
| sta b0_last | |||
| lda #<tables_first | |||
| sta mmu_ptr | |||
| lda #>tables_first | |||
| sta mmu_ptr+1 | |||
| ldy #$00 | |||
| lda #$5A | |||
| jsr mmu_poke1 | |||
| lda #<tables_last | |||
| sta mmu_ptr | |||
| lda #>tables_last | |||
| sta mmu_ptr+1 | |||
| lda #$A5 | |||
| jsr mmu_poke1 | |||
| jsr mmu_peek1 | |||
| cmp #$A5 | |||
| bne @b1_done | |||
| lda #<tables_first | |||
| sta mmu_ptr | |||
| lda #>tables_first | |||
| sta mmu_ptr+1 | |||
| jsr mmu_peek1 | |||
| cmp #$5A | |||
| bne @b1_done | |||
| lda tables_first | |||
| cmp b0_first | |||
| bne @b1_done | |||
| lda tables_last | |||
| cmp b0_last | |||
| bne @b1_done | |||
| jsr state_is_bank0 | |||
| bne @b1_done | |||
| lda #<CHECK_BANK1 | |||
| jsr pass | |||
| @b1_done: | |||
| ; ----------------------------------------- shared common regions | |||
| lda #$3C | |||
| sta diskbuf | |||
| lda #$C3 | |||
| sta scratch | |||
| jsr common_shared_test | |||
| lda shared_ok | |||
| cmp #$01 | |||
| bne @sh_done | |||
| lda MB_DATA+7 ; written from the bank-1 state | |||
| cmp #$7E | |||
| bne @sh_done | |||
| lda #<CHECK_COMMON | |||
| jsr pass | |||
| @sh_done: | |||
| lda #$00 | |||
| sta MB_DATA+7 | |||
| ; -------------------------------------- NMI under bank 1 + ZP | |||
| jsr common_nmi_test | |||
| lda MB_DATA+0 | |||
| beq @nmi_done ; the NMI never arrived | |||
| lda MB_DATA+1 | |||
| cmp #CR_RAM1_KERNAL | |||
| bne @nmi_done | |||
| lda MB_DATA+2 | |||
| cmp #<__COMPILER_ZP_PAGE__ | |||
| bne @nmi_done | |||
| lda MB_DATA+3 | |||
| cmp #ZP_SENTINEL | |||
| bne @nmi_done | |||
| lda nmi_sp | |||
| cmp entry_sp_inner | |||
| bne @nmi_done | |||
| lda #<CHECK_NMI | |||
| jsr pass | |||
| @nmi_done: | |||
| ; ------------------------------------------------- full restore | |||
| jsr mmu_shutdown | |||
| tsx | |||
| cpx entry_sp | |||
| bne @rs_done | |||
| lda MMU_CR | |||
| cmp entry_state+0 | |||
| bne @rs_done | |||
| ldx #4 | |||
| @rs: lda MMU_RCR,x | |||
| cmp entry_state+1,x | |||
| bne @rs_done | |||
| dex | |||
| bpl @rs | |||
| lda NMINV | |||
| cmp entry_state+6 | |||
| bne @rs_done | |||
| lda NMINV+1 | |||
| cmp entry_state+7 | |||
| bne @rs_done | |||
| lda #<CHECK_RESTORE | |||
| jsr pass | |||
| @rs_done: | |||
| ; ------------------------------------------------ report | |||
| lda fail_mask | |||
| sta MB_CODE | |||
| lda fail_mask+1 | |||
| sta MB_CODE+1 | |||
| lda #<skel_done | |||
| sta MB_ADDR | |||
| lda #>skel_done | |||
| sta MB_ADDR+1 | |||
| ldy #ok_msg - messages | |||
| lda fail_mask | |||
| ora fail_mask+1 | |||
| bne @fail | |||
| lda #MB_ST_PASS | |||
| bne @report | |||
| @fail: ldy #fail_msg - messages | |||
| lda #MB_ST_FAIL | |||
| @report: | |||
| sta MB_STATUS | |||
| skel_done: | |||
| plp | |||
| @print: lda messages,y | |||
| beq @end | |||
| sty print_y | |||
| jsr CHROUT | |||
| ldy print_y | |||
| iny | |||
| bne @print | |||
| @end: rts | |||
| .segment "CODE" | |||
| ; Clear a passed check (A = low-byte mask; all checks fit in the low byte). | |||
| pass: | |||
| eor #$FF | |||
| and fail_mask | |||
| sta fail_mask | |||
| rts | |||
| ; Z=1 when the MMU shows the exact bank-0 compiler state. | |||
| state_is_bank0: | |||
| lda MMU_CR | |||
| cmp #CR_RAM0_KERNAL | |||
| bne @no | |||
| lda MMU_P0L | |||
| cmp #<__COMPILER_ZP_PAGE__ | |||
| @no: rts | |||
| .ifdef OVERFLOW_MAIN | |||
| .res $9BF3 | |||
| .endif | |||
| .segment "RODATA" | |||
| bank0_msg: | |||
| .byte "KCALL FROM BANK 0", $0D, $00 | |||
| messages: | |||
| ok_msg: | |||
| .byte "NOVA128 MAP SKELETON OK", $0D, $00 | |||
| fail_msg: | |||
| .byte "NOVA128 MAP SKELETON FAIL", $0D, $00 | |||
| .segment "BSS" | |||
| entry_sp: .res 1 | |||
| entry_state: .res 8 ; CR, RCR, P0L, P0H, P1L, P1H, NMINV | |||
| sys_zp_before: .res 1 | |||
| fail_mask: .res 2 | |||
| b0_first: .res 1 | |||
| b0_last: .res 1 | |||
| print_y: .res 1 | |||
| ; ---------------------------------------------------------------- COMMON | |||
| ; These routines run with bank 1 mapped, so they must be in common RAM. | |||
| .segment "COMMON" | |||
| common_say_bank1: | |||
| lda #$00 | |||
| sta kc1_ok | |||
| jsr mmu_bank1 | |||
| ldy #$00 | |||
| @say: lda bank1_msg,y | |||
| beq @said | |||
| sty say_y | |||
| jsr kcall_inline | |||
| .word CHROUT | |||
| ldy say_y | |||
| iny | |||
| bne @say | |||
| @said: lda MMU_CR ; still bank 1 after the KERNAL calls? | |||
| cmp #CR_RAM1_KERNAL | |||
| bne @out | |||
| lda MMU_P0L | |||
| cmp #<__COMPILER_ZP_PAGE__ | |||
| bne @out | |||
| inc kc1_ok | |||
| @out: jmp mmu_bank0 | |||
| common_shared_test: | |||
| lda #$00 | |||
| sta shared_ok | |||
| jsr mmu_bank1 | |||
| lda diskbuf | |||
| cmp #$3C | |||
| bne @out | |||
| lda scratch | |||
| cmp #$C3 | |||
| bne @out | |||
| lda #$7E | |||
| sta MB_DATA+7 | |||
| inc shared_ok | |||
| @out: jmp mmu_bank0 | |||
| common_nmi_test: | |||
| tsx | |||
| stx entry_sp_inner | |||
| jsr mmu_bank1 | |||
| lda #$7F ; disable all CIA #2 sources, clear flags | |||
| sta CIA2_ICR | |||
| bit CIA2_ICR | |||
| lda #$40 | |||
| sta CIA2_TALO | |||
| lda #$00 | |||
| sta CIA2_TAHI | |||
| lda mmu_nmi_count | |||
| sta nmi_before | |||
| lda #CIA_ICR_SET | CIA_ICR_TA | |||
| sta CIA2_ICR | |||
| lda #CIA_CRA_START | CIA_CRA_ONESHOT | CIA_CRA_LOAD | |||
| sta CIA2_CRA | |||
| ldx #$00 ; wait up to ~65K iterations | |||
| ldy #$00 | |||
| @wait: lda mmu_nmi_count | |||
| cmp nmi_before | |||
| bne @hit | |||
| dex | |||
| bne @wait | |||
| dey | |||
| bne @wait | |||
| @hit: lda MMU_CR ; state right after the NMI returned | |||
| sta MB_DATA+1 | |||
| lda MMU_P0L | |||
| sta MB_DATA+2 | |||
| lda zp_probe | |||
| sta MB_DATA+3 | |||
| tsx ; same depth as at routine entry? | |||
| stx nmi_sp | |||
| lda #$7F | |||
| sta CIA2_ICR | |||
| lda #$00 | |||
| sta CIA2_CRA | |||
| bit CIA2_ICR | |||
| lda mmu_nmi_count | |||
| sec | |||
| sbc nmi_before | |||
| sta MB_DATA+0 | |||
| jmp mmu_bank0 | |||
| bank1_msg: .byte "KCALL FROM BANK 1", $0D, $00 | |||
| kc1_ok: .byte $00 | |||
| say_y: .byte $00 | |||
| shared_ok: .byte $00 | |||
| nmi_before: .byte $00 | |||
| nmi_sp: .byte $00 | |||
| entry_sp_inner: .byte $00 | |||
| .ifdef OVERFLOW_COMMON | |||
| .res $0600 | |||
| .endif | |||
Powered by TurnKey Linux.