Skip to content

firmware: run C128 BASIC programs in 128 mode on a C128, and add Exit to C128 BASIC - #96

Merged
SensoriumEmbedded merged 3 commits into
SensoriumEmbedded:mainfrom
kfox:c128-native-prg
Nov 11, 2026
Merged

SensoriumEmbedded merged 3 commits into
SensoriumEmbedded:mainfrom
kfox:c128-native-prg

Conversation

@kfox

@kfox kfox commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

On a C128, a C128 BASIC program launched from the TeensyROM now runs in 128 mode instead of loading into C64 mode, where it cannot run. A C64 program launches as before. The Utilities menu also gets Exit to C128 BASIC, which restarts a C128 in 128 mode with no cartridge, as a power-on would.

How a program is recognized

A PRG is treated as C128 BASIC when it loads at $1C01 and its first line link points just past a zero byte inside the file, which is how a tokenized BASIC 7.0 program starts. Anything else, including a $1C01 file whose link doesn't check out, takes the existing C64 path. The menu reports whether the host is a C128, so a C64 still gets the C64 path even for a $1C01 program. An empty program (link $0000) means "just start C128 BASIC": that's the Exit to C128 BASIC menu item, a 4-byte c128basic.prg. On a C64 it is refused with "C128 BASIC needs a C128".

This runs where a .prg launch is already decided (DriveDirLoad.ino), so it covers every entry point: the TR menu, remote launch, the host app's file list, NFC, hot keys and autolaunch.

How it runs

StartC128Prg() resets the C128 into a 1.25 KB function ROM (Source/C64/C128PrgBoot/) that the C128 KERNAL finds at power-on through its ID byte $FF, the one that runs after BASIC's own cold start. The ROM copies a small loader to $1300. The loader streams the program from the TeensyROM's stream register to $1C01, switching the MMU between I/O and all-RAM for each byte. It then sets the end of BASIC text and puts RUN and a return in the keyboard buffer. Before restarting BASIC it tells the firmware to drop the ROM (rCtlVanishROM), so a later reset gives a plain C128. Programs up to $FF00 fit, so the end of BASIC text goes as high as 59392.

The boot ROM is copied out of flash through a pointer the optimizer cannot see through. A plain memcpy of the whole PROGMEM array made GCC keep a second copy in .rodata, which on Teensy is DTCM, and on the TR+ that cost a 4 KiB step of free-for-locals.

Verification

  • npm test: 364 pass. New host tests cover the classifier and the launch path (Source/Teensy/tests/c128-prg.test.js compiles the real C128Prg.ino against stubs), and the loader in a 6502 model (tools/c128/c128prgboot-run.test.mjs).
  • Hardware: TR+ and TR (Fab 0.2) in a flat NTSC C128, full main image of this branch, launched from SD over the remote protocol:
    • a 40-column program runs in 128 mode with sound (PLAY);
    • an 80-column program (GRAPHIC 5) runs on the VDC;
    • a 52,223-byte program loads with its checksum intact and TOP 59392;
    • Exit to C128 BASIC restarts in 128 mode with no cartridge, from SD and from the TR+ menu row;
    • a C64 PRG still runs in C64 mode.
  • Reset after a launch: on the TR and on the TR+ with External Reset Detect off, the C128 comes back to C128 BASIC. With it on, the TR+ comes back to the TR menu, as it does for any program.
  • Not tested: PAL, and the menu row on the TR (no DMA there to drive keys).

Memory

Main image, Docker linux/amd64, Teensyduino 1.61.0, origin/main (ac24701) vs this branch:

TR+ base TR+ branch TR base TR branch
FLASH code 285884 286364 252940 253484
FLASH data 1455196 1456548 1442976 1444328
RAM1 variables 215044 215044 204932 204932
RAM1 code 203576 203576 190472 190536
RAM1 padding 25800 25800 6136 6072
RAM1 free for local variables 79868 79868 122748 122748
RAM2 unchanged unchanged

The TR's +64 bytes of RAM1 code are a codegen change in LoadFile. It doesn't cross an ITCM bank, so free-for-locals is unchanged.

Review

Each commit was reviewed at medium or low, and the branch got a final high pass. That pass found the DTCM copy described above, and its fix is folded into the commit it reviewed.

kfox added 3 commits November 10, 2026 09:23
…xit to C128 BASIC

On a C128 the menu runs in C64 mode, so a C128 BASIC program loaded today lands in C64 RAM and cannot run. A small
function ROM, polled by the C128 kernal, now streams the program to $1C01 and types RUN. Its ID asks for the late
call from PHOENIX, which BASIC's cold start makes after its NEW, so the program survives. A PRG is taken for one
when it loads at $1C01 and its first line link points just past that line's end; an empty one returns to C128 BASIC.
@kfox
kfox marked this pull request as ready for review November 10, 2026 15:36

@SensoriumEmbedded SensoriumEmbedded left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the ability to run C128 BASIC programs in 128 mode, and for Exit to C128 BASIC, it's great to have C128 programs launch the way a C128 owner expects! :)

I reviewed this merged onto current main (ac24701) and built both targets. There's nothing I'd like you to change before merging.

Memory: no RAM1 code (ITCM) on either target, and free for local variables is unchanged on both. DTCM content grows 48 bytes on TR+ and 56 on TR (the new Utilities entry and its name), inside the current alignment step, and flash grows about 2 KB.

Hardware tested

On a TR+ with this PR merged onto main, along with your TR+ and TR tests on an NTSC C128. All passed:

  • NTSC C64:
    • Exit to C128 BASIC refuses with "C128 BASIC needs a C128" and stays in the menu.
    • $1C01 programs (plain BASIC, a SYS line with machine code after it, and a .P00) still load in C64 mode.
    • A $0801 program still runs.
  • NTSC C128, 40 columns:
    • The same $1C01 files, launched from SD, reset into 128 mode, land at $1C01 and RUN.
    • Exit to C128 BASIC comes up in empty BASIC 7.0.
    • The $0801 program still runs in C64 mode.
    • A reset returns to the TR menu each time.
  • NTSC C128, other launch paths:
    • A hot key.
    • Power-up autolaunch from autolaunch.txt, with the 40/80 key up and down.
  • NTSC C128, 40/80 key down:
    • Exit to C128 BASIC, and a remote launch, both come up on the 80-column screen and RUN.

Hardware testing: Just to document. I'll hit these at some point, not a request or requirement! :)

  • PAL C128
  • Exit to C128 BASIC from the TR menu on a TR
  • A $1C01 program extracted from a D64
  • C128D, or a C128 with JiffyDOS or an internal function ROM

Thank you!

@SensoriumEmbedded
SensoriumEmbedded merged commit d1a7ab7 into SensoriumEmbedded:main Nov 11, 2026
4 checks passed
@wesbiggs

Copy link
Copy Markdown
Contributor

Coming in a little late with this review, but there's a case I think might need considering in a next update. Lightly edited from Claude...

After the boot ROM writes rCtlRunningPRG, IOHandlerSelectInit() picks the default Special IO. If that is Kernal Replace and an earlier C64-mode launch loaded a kernal, KernalBin is still allocated, and fBusSnoop is HandshakeSnoop at that point. So InitHndlr_KernalReplace() stages KernalCheckArm(), and KernalCheckDMA ends up hooked while the C128 runs in native 128 mode. GAME/EXROM don’t map the internal ROM out there, so the Teensy would drive $E000-$FFFF against the C128’s own KERNAL, and the DMA HIRAM sense would fire on $00/$01 accesses.

Could the 128-mode launch skip handlers that only make sense in C64 mode (Kernal Replace, TR BASIC), for example by selecting IOH_None for them, or by a per-handler “OK in 128 mode” flag? Alternatively, InitHndlr_KernalReplace() could refuse when the launch went to 128 mode.

For vdrive, I will look into a C128-mode-aware version in due course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants