Apple IIc Plus, ROM 5X, and Merlin

Written on 2017/12/14.

A Wild Bug Appears

Recently, “Apple II Enthusiasts” Facebook group members Skander Ben Abdelkrim, Eric Leung, and Wyatt Wong reported a bug, seemingly in ROM 5X for the Apple IIc Plus: The Merlin assembler, specifically the 128K versions 2.58 and 2.59, would crash at the main menu when ROM 5X was installed, but worked fine with the stock Apple ROM.

This had me a bit perplexed, as I ran through possibilities in my head. After booting a copy of Merlin 2.58 in an emulator and looking at its normal behavior, I had a place to start - the crash was happening immediately after the prompt was displayed at the main menu.

Using the Source

The source code for Merlin 2.59 is available. So naturally this would provide a good place to start. In the Merlin code in the file Macexec.S we find the menu display/prompt/input code:

         JSR   MENU

WNDUP    PRINT 81;"%"
         JSR   JCLOSE
:BL      JSR   BELL
         JSR   RDKEY

Now it is a fact that the % prompt appears before the crash, so it has to be after the % is printed on the screen. The main thing that sticks out here is the JSR BELL. That's suspicious because in the affected versions of ROM 5X, the main hook into the ROM 5X code is in the BELL1 routine that actually sounds the system bell.

Now, there's nothing special about the ROM 5X BELL1 code, it looks like this (ca65-format):

        .org    $FBDD
BELL1:  lda     #$40
        sta     $C028   ; original:  jsr     WAIT
        ldy     #$C0    ; "BELL1.2" in the IIc Technical ref 2nd ed.
:       lda     #$0C
        jsr     WAIT
        lda     SPEAKER
        dey
        bne     :-
        rts

The first two instructions starting at BELL1 produce a short delay before the bell is sounded, so that multiple bells are distinct. The remaining code starting at ldy #$C0 makes the actual beep sound.

The sta $c028 switches to the aux firmware bank to the ROM 5X dispatcher, which does one of a few things depending on the contents of the A register. Two of them, if the A register contains $A9 or $EA, are to dispatch the reset or boot hooks that provide for the main ROM 5X features. It would be obvious if we were accidentally hitting these. One is to sound the classic “air raid beep” if the A register contains $40, like it would if someone were calling BELL1 to sound the speaker. Finally, if the A register is not one of those three values, the dispatcher calls the WAIT routine and exits back to the main firmware at the ldy #$C0 and falls through to the original BELL sounder. We'd know if we were hitting that if we heard the “IIc Plus beep”. But we don't actually hear the beep that Merlin is attempting to make near where it crashes. We crash into the monitor and that is what causes the beep we actually hear.

I hadn't really tested this fall-through much and Occam's Razor suggested that Merlin was using its own delay value and calling BELL1 at the jsr WAIT/sta $C028 instruction, and that I was doing something like trashing the stack or returning an unexpected value.

At that point I spent an hour looking at my code, testing all of the possibilities, and could not replicate the issue. I did confirm that the BELL1 patch was causing the Merlin crash, though.

So I was back to square one.

Disassembling the ROM, redux

When I implemented ROM 5X, I patched the beep code out of convenience. I hadn't originally set out to make the IIc Plus beep sound like the other Apple IIs. Its different sound wasn't a bug to my ears, any more than the distinctive “bonk” of the Apple IIgs.

However, the popularity of Quinn Dunki's beep patch was brought to my attention via the requests of several people interested in my ROM projects. The fact that I had already hijacked the BELL1 routine was mighty convenient.

At any rate, I had to see what the Apple IIc Plus BELL1 routine was doing in full, because whatever it was, it wasn't triggering the Merlin bug.

Quinn's excellent work already showed why the the beep was different based on the IIc Plus's aux-ROM WAIT implementation, and there is no need to do that work again here.

However, there's one little tidbit that wasn't important to the sound of the beep, but was important to the behavior of the system. The IIc Plus main-ROM WAIT routine is implemented thusly:

        .org    $FCA8
WAIT:   nop
        jmp     $C2EE    ; what's this?
LFCAC:  sta     $C028    ; bank switch
        nop
        nop
        nop
        nop
        rts

The label LFCAC is important, because it's right after we jumped away from the original routine. We eventually come back here, which we will get to momentarily. Importantly, as Quinn covered, the sta $C028 goes to the aux ROM bank to the new IIc Plus speed-compensated WAIT routine.

So what does the code at $C2EE do?

        .org    $C2EE
        phx
        jsr     $CFE5
        jsr     LFCAC
        inc     $C000,x
        plx
        lda     #$00
        rts

Okay, we save the X register, call whatever's at $CFE5, and then call the WAIT routine in the aux bank, back at $FCAC. Finally, we access something in the I/O page, indexed by the X register which must be set by the $CFE5 routine, because it's certainly not set here. We restore X and zero the accumulator, and we are done.

The accumulator is zeroed because WAIT always returns with it zeroed according to Apple documentation. So that's an easy one. What happens at $CFE5 and why do we hit the I/O page?

Well here is what's at $CFE5:

        .org    $CFE5
        ldx     #$81    ; $C081 = ROMIN
        bit     $C012   ; RDLCRAM - state of LC RAM
        bpl     LCFF8   ; if off
        ldx     #$8B    ; $C08B = LCBANK1
        bit     $C011   ; RDLCBNK2 - state of LC RAM bank 1/2
        bpl     LCFF5   ; if off
        ldx     #$83    ; $C083 = LCBANK2
LCFF5:  sta     $C081   ; ROMIN
LCFF8:  rts

Aha! This code saves the language card state in the X register, and enables the ROM. $C000+X is the soft switch needed to restore the language card state to what it was while we are accessing this routine.

There's just one problem.

This Code *is* in the ROM!

This code saves the language card state and switches to the ROM. But we just found this code in the ROM. What gives?

Initially I had assumed the IIc Plus had some weird thing happening that would put it at odds with the other Apple IIs with regard to the memory select soft switches.

But, then I had another idea.

Or is it?

What would happen if this code wasn't in the ROM. Say, perhaps, executing out of the language card RAM?

Pause for a moment and think about that.

Well, the original code works fine because it makes sure the ROM is present and then restores whatever language card/ROM state was when it was called. Save language card state, put the ROM in, switch to aux firmware, do WAIT, switch to main firmware, restore language card state, voila! So that carries on to the BELL routine, nice and easy.

So what if the ROM 5X BELL code is running out of the language card RAM?

Well, we switch to the aux ROM, but nothing happens to our code because the ROM bank toggle doesn't affect the RAM. The code falls through to the “BELL1.2” code which should sound a standard Apple IIc Plus beep. The beep code calls WAIT, which as we see at the $CFE5 routine saves the language card state and turns on the ROM…

Crash!

What happened?

Well… … … We had already toggled the firmware bank once. So the aux ROM was already switched in when the $CFE5 code enabled the ROM.

The next instruction was not what it was supposed to be.

Could that be our problem?

It sure can!

Going back to the Merlin code, this time in Interpreter.S which is the source to MERLIN.SYSTEM, we find this little tidbit.

:NOX     BIT   CONTROL!BANK1!ROMREAD!ENABLE
         BIT   CONTROL!BANK1!ROMREAD!ENABLE
         LDA   #$FF
         STA   ENDMOVE
         STA   ENDMOVE+1
         STY   FROM
         STY   DEST
         LDA   #$F8
         STA   FROM+1
         STA   DEST+1
         JSR   MOVE       ;Move monitor

One comment says it all: “Move monitor”. That code copies everything from $F800 to $FFFF right back on top of $F800 to $FFFF… reading from ROM and writing the aux language card space.

So it turns out that ROM 5X was running from the RAM after all.

Fixing ROM 5X

The fix is easy… either don't mess with the BELL routine, or make sure we get back to the right place if we are running out of RAM, and the latter is exactly what happens in the 12/10/2017 update of ROM 5X.

While there are other miscellaneous changes and code reorganizations around fixing this problem (they can be seen in the commit), they are not centrally important to the problem we are fixing.

The main parts of the fix are as follows: (Important note: The ROM in $Cxxx is always visible no matter what, there's no way to switch it out for RAM.)

Move the dispatcher bank switch out of the BELL1 routine to a few free bytes at $C7FC in the main bank and $C7FF of the aux bank:

; Main bank:
	.org  $c7fc
	sta   $c028
; Aux bank:
        .org  $c7ff
        jmp   $fb3c     ; to 5X dispatch

Now, to do the beep fix, we patch BELL1:

        .org  $fbe6
        jmp   $c2fc        ; to 5X beep with merlin fix

This replaces the jsr WAIT in the “BELL1.2” portion to trigger the “air raid beep” sound via the ROM 5X dispatcher, using the preceding lda #$0c as the function code.

Now, we know that we have to jump through the language card hoops in order to run out of RAM, so that's what happens at $C2FC:

        .org  $c2fc
        phx
        jmp   $c9a1     ; next step

There's not a ton of space, so we have just enough room to save the X register and jump to some more code:

        .org  $c9a1
        jsr   $cfe5   ; get memory config we need to fix
        jsr   $c7fc   ; call ROM 5X dispatch
        jmp   $c2f5   ; fix memory, restore x, a=$00

We make use of the IIc Plus's existing routines to save and restore the language card state, and in between we call the ROM 5X dispatch, which executes the classic “air raid beep” that the Apple II line is known for. In fact, this can be used to call any of the ROM 5X functions, now or in the future, and preserve the RAM state, provided we don't care about the contents of the A register.

Conclusion

Looking into the ROM further, the only time that the stock IIc Plus monitor ($F800-$FFFF) switches into the aux ROM is for the WAIT routine.

There are plenty of programs that copy the ROM into RAM in order to modify it or make use of the routines in a non-standard memory configuration. That's what's going on here.

Apple's engineers probably hit this problem early on in testing the IIc Plus, perhaps noting an odd crash in a popular assembler.