This isn't just for C65 compatibility, but it also provides a nice way to integrate microSD storage, because I can override the internal DOS so that it uses the microSD connector instead of the floppy controller.
To do this, I need to implement some of the extra 4510 opcodes, as well as some of the other C65 memory mapper and other control features.
What I didn't know was what extra instructions and features I would need to implement to facilitate this. So I went looking for a disassembly of the C65 ROMs, but couldn't find any. So I set about making my own, using Marko Makela's well known C64 kernel disassembly as the reference, and making note of all the changes.
I could have done it automatically (although the extra opcodes would have complicated this), but I also wanted to gain a clear understanding of how the C65 modified kernel works, and how it intercepts DOS.
The whole process took a few hours to exhaustively map the differences.
The changes basically consisted of throwing out the cassette routines and putting in sufficient intercepts. Other smaller changes include making 8 the default device number:
; set parameters for load/verify/save E1D4 A9 00 LDA #$00 E1D6 20 BD FF JSR $FFBD ; DIFF: C65: Make device 8 the default. ; C64: E1D9 A2 01 LDX #$01 E1D9 A2 08 LDX #$08 E1DB A0 00 LDY #$00 E1DD 20 BA FF JSR $FFBA ... ; get open/close parameters E219 A9 00 LDA #$00 E21B 20 BD FF JSR $FFBD E21E 20 11 E2 JSR $E211 E221 20 9E B7 JSR $B79E E224 86 49 STX $49 E226 8A TXA ; DIFF: C65: Make device 8 the default ; C64 E227 A2 01 LDX #$01 E227 A2 08 LDX #$08 E229 A0 00 LDY #$00 E22B 20 BA FF JSR $FFBA
and changing the shift-RUN/STOP text so that it loads the first file from disk and runs it:
; Fill keyboard buffer with LOAD command when shift-RUNSTOP is pressed E5EE A2 09 LDX #$0A ; C64: LDA #$09 E5F0 78 SEI E5F1 86 C6 STX $C6 ; C64: E5F3 BD E6 EC LDA $ECE6,X ; C65: Copy string L
"0:* R into keyboard buffer E5F3 BD 56 FC LDA $FC56,X ; C65 E5F6 9D 76 02 STA $0276,X E5F9 CA DEX E5FA D0 F7 BNE $E5F3 ... ; C65: key sequence when shift-RUN/STOP is pressed ; L "0:* R FC57 .BY $4C, $CF, $22, $30, $3A, $2A, $0D, $52, $D5, $0D
The DOS routines have been intercepted using an interesting approach. First, $C0 ceases to indicate the tape motor state, and instead indicates whether the current drive is on the serial IEC bus, or handled by the internal 1581 DOS. This is checked using one of the new 4510 opcodes, BBS7, which branches on whether bit 7 is set in a zero-page byte, without having to use the accumulator:
; C65: call DOS routine and return: send secondary address (talk) F7E4 FF C0 09 BBS7 $C0,$F7F0 ; branch based on whether current device is internal 1581 F7E7 20 C7 ED JSR $F72C ; bank in C65 1581 DOS F7EA 22 0A 80 JSR ($800A) F7ED 20 3E F8 JSR $F83E ; C65: return from DOS context and set $90 status F7F0 4C C7 ED JMP $EDC7 ; send secondary address (talk) on serial bus
As can be seen, if the drive is on the IEC bus, then the normal C64 kernel routine is used. However, if the drive is the internal one, then the C65 1581 DOS is banked in to $8000 - $BFFF (conveniently leaving the C64 kernel still in view), and then the appropriate vector in that ROM is called. Notice the use of the new indirect mode of the JSR instruction (opcode $22).
Banking involves the use of the MAP instruction, which sets the memory map. C65 memory mapping is too complex to cover in this post, so I will cover it in a separate post later. The interesting thing for now is to see how NOP is no longer really NOP. The MAP instruction prevents both IRQ and NMI interrupts until a NOP instruction is run. NOP is consequentially also known as End Of Mapping (EOM) on the 4510.
All this and more can be seen in the routine for switching to the C65 1581 DOS memory context.
; C65: switch to C65 1581 DOS context F72C 78 SEI F72D 48 PHA F72E A9 A5 LDA #$A5 ; C65: VIC-III enable sequence F730 8D 2F D0 STA $D02F F733 A9 96 LDA #$96 F735 8D 2F D0 STA $D02F ; C65: VIC-III enabled F739 A9 40 F73A 0C 31 D0 TSB $D031 ; set bit 6 in $D031 to put CPU at 3.5MHz F73D A9 21 F73F 0C 30 D0 TSB $D030 ; bank in $C000 interface ROM and remove CIAs from IO map F742 68 PLA ; store registers F743 8D F6 DF STA $DFF6 F746 8E F7 DF STX $DFF7 F749 8C F8 DF STY $DFF8 F74C 9C F9 DF STZ $DFF9 F74F 68 PLA F750 8D FB DF STA $DFFB F753 68 PLA F754 8D FC DF STA $DFFC F757 BA TSX F758 8E FF DF STX $DFFF ; C65: bank in 1581 DOS F75B A9 00 LDA #$00 F75D A2 11 LDX #$11 ; Map $0000-$1FFF to $10000-$11FFF ($0000+$10000) F75F A0 80 LDY #$80 F761 A3 31 LDZ #$31 ; Map $8000-$BFFF to $20000-$23FFF ($8000+$18000) F763 5C MAP ; activate new map F764 A2 FF LDX #$FF F766 9A TXS F767 AD FC DF LDA $DFFC F76A 48 PHA F76B AD FB DF LDA $DFFB F76E 48 PHA F76F AD F6 DF LDA $DFF6 F772 AE F7 DF LDX $DFF7 F775 AC F8 DF LDY $DFF8 F778 AB FA DF LDZ $DFFA F77B 60 RTS ; RTS (notice how the return address was copied from old stack to new stack)
Notice that there is no NOP or EOM in this routine. This prevents any interrupts occurring while the internal DOS is operating in its special memory map. The EOM appears in the routine for returning from the C65 1581 DOS context:
; C65: return from DOS call and set status in $90 F83E 68 PLA F83F 8D FD DF STA $DFFD F842 68 PLA F843 8D FE DF STA $DFFE F846 20 7C F7 JSR $F77C ; restore C64 memory map F849 77 C0 RMB7 $C0 ; clear bit 7 in $C0 F84B 6B TZA F84C 10 0C BPL $F85A F84E A9 00 LDA #$00 F850 F7 C0 SMB7 $C0 ; set bit 7 in $C0 F852 AE FE DF LDX $DFFE F855 DA PHX F856 AE FD DF LDX $DFFD F859 DA PHX F85A 04 90 TSB $90 ; Set bits in $90 (status) if required F85C AE F7 DF LDX $DFF7 F85F AC F8 DF LDY $DFF8 F862 AB F9 DF LDZ $DFF9 F865 AD F6 DF LDA $DFF6 F868 48 PHA F869 A9 21 LDA #$21 F86B 1C 30 D0 TRB $D030 ; bank out $C000 ROM and bank CIAs back in. F86E A9 40 LDA #$40 F870 1C 31 D0 TRB $D031 ; return CPU to 1MHz. F873 8D 2F D0 STA $D02f ; return to VIC-II mode F876 68 PLA F877 EA EOM ; release IRQ & NMI after MAP change triggered at $F846 F878 58 CLI F879 18 CLC F87A 60 RTS
The $DFFx memory accesses are not to the CIAs, but to the end of screen RAM. Setting bit 0 in $D030 replaces $DC00-$DFFF with an extra 1KB of colour RAM, which is in fact the last 2KB of the 128KB of main RAM of a C65, and hence is 8 bit RAM, unlike the 4-bit colour RAM on the C64.
The $D030 flag is primarily for making the 2KB colour RAM conveniently available to the kernel when working with an 80-column, and hence 2,000 byte screen. Of course this leaves a few bytes spare at the end that are nicely used here to save and restore registers when the stack cannot be used because memory is being remapped.
The last interesting piece is to explore is the reset process. The reset vector has been changed:
FFFA .WD $FE43 ; NMI vector ; C64: FFFC .WD $FCE2 ; RESET vector ; C65 new reset vector FFFC .WD $E4B8 ; RESET vector FFFE .WD $FF48 ; IRQ/BRK vector
Reset proceeds from $E4B8, instead of $FCE2. The $E4B8 routine is quite simple, if a little curious:
; C65: CPU reset entry point. ; Check for cartridge, else normal reset sequence. ; (this is a little strance, since $FCE2 routine also calls $FD02 E4B8 20 02 FD JSR $FD02 ; check for cartridge E4BB D0 03 BNE $E4C0 E4BD 4C E2 FC JMP $FCE2 ; RESET routine ; C65: Enable VIC-III mode, jump to interface E4C0 78 SEI E4C1 A9 A5 LDA #$A5 E4C3 8D 2F D0 STA $D02F E4C6 A9 96 LDA #$96 E4C8 8D 2F D0 STA $D02F E4CB A9 20 LDA #$20 E4CD 8D 30 D0 STA $D030 ; bank interface ROM in @ $C000 E4D0 4C 00 C8 JMP $C800 ; interface ROM entry point E4D3 85 A9 STA $A9 E4D5 A9 01 LDA #$01 E4D7 85 AB STA $AB E4D9 60 RTS
If a C64 cartridge is detected, then the usual C64 reset process is followed. If not, then the machine switches to C65 mode by banking in the interface ROM at $C000-$CFFF and jumping to the entry point there. Also, at another point the C65 1581 ROM is mapped into $8000 - $BFFF and the DOS setup routine is called. This means that I need to disassemble and examine those two ROMs as well I am to fully understand what is going on.
But for now, the complete C65 C64-mode kernel disassembly is available here.