; Fully disassembled and analyzed source to AE ; DCLOCK.SYSTEM by M.G. - 04/18/2017 ; Assembles to a binary match for AE code unless ; FIX_BUGS is set. ; speaking of FIX_BUGS, there are critical bugs in the ; original AE code: ; * When driver loader is initially probing it corrupts the ; Apple //c Memory Expansion Card: ; - it saves, but fails to restore, data at address $080000 ; - it fails to reset slinky pointer, and *will* trash $080000-$080007 ; * When the clock is read, it corrupts data at address $08xx01 ; - John Brooks spotted this, I totally missed this. ; Setting FIX_BUGS to 1 will fix these issues. ; other notes: ; * uses direct block access to read volume directory, ; so won't launch from an AppleShare volume. ; Build instructions: ; ca65 dclock.system.s -l dclock.system.lst ; ld65 -t none -o dclock.system dclock.system.o ; put dclock.system as a SYS file on a ProDOS disk. FIX_BUGS := 0 ; set to 1 to fix critical bugs .setcpu "65C02" ; zero page locations SLASHOFFS := $00 ; offset of last '/' in our path MYNAMELEN := $01 ; length of our file name in path FENTPTR := $02 ; directory file entry pointer FENTPTRL := FENTPTR FENTPTRH := FENTPTR+1 CURENT := $04 ; current file entry in block ENTLEN := $05 ; length of a file entry ENTPERBLK := $06 ; number of entries per block DIRBLK := $07 ; directory block to read DIRBLKL := DIRBLK DIRBLKH := DIRBLK+1 CHRPTR := $09 ; character pointer for print routine CHRPTRL := CHRPTR CHRPTRH := CHRPTR+1 SCRATCH := $0B ; scratch value for BCD range checks SAVEBYTE := $0C ; slinky overwritten byte save location BCDTMP := $3A ; location clock driver uses for BCD->Binary ; entry points PRODOS := $BF00 HOME := $FC58 WAIT := $FCA8 COUT1 := $FDF0 ; buffers & other spaces INBUF := $0200 ; input buffer PATHBUF := $0280 ; path buffer CLOCKBUF := $0300 ; clock buffer RELOCT := $1200 ; reloc target BLOCKBUF := $1000 ; buffer for BLOCK_READ READBUF := $1C00 ; I/O buffer for READ SYSEXEC := $2000 ; location of SYS file executable CLKCODE := $D742 ; Clock code location ; global Page entries CLKENTRY := $BF06 ; clock routine entry point DATELO := $BF90 DATEHI := $BF91 TIMELO := $BF92 TIMEHI := $BF93 MACHID := $BF98 ; machine ID ; I/O and hardware ROMIn2 := $C082 ; access to read ROM/no write RAM LCBank1 := $C08B ; Access twice to write bank 1 C8OFF := $CFFF ; C8xx ROM off SLOT4ROM := $C400 ; Slot 4 ROM space SLOT4IO := $C0C0 ; Slot 4 I/O space DPTRL := SLOT4IO+0 ; Slinky data ptr low DPTRM := SLOT4IO+1 ; Slinky data ptr middle DPTRH := SLOT4IO+2 ; Slinky data ptr high DATA := SLOT4IO+3 ; Slinky data byte ; Misc CLKCODEMAX := $7D .org SYSEXEC ; ---------------------------------------------------------------------------- ; relocate code from RELOCS to RELOCT .proc DClockSystem ldy #(rseg1e-rseg1b) : lda RELOCS,y sta RELOCT-1,y dey bne :- ; greetings jsr HOME jsr iprint .byte $07,"DClock",$0d jsr iprint .byte $27,"Copyright (c)1988 Applied Engineering",$0D,$0D jsr ClockRead jsr ValidTime bcc InstallDriver ; clock not found jsr iprint .byte $11,"Can't find clock",$0D jmp NextSys .endproc ; ---------------------------------------------------------------------------- ; Install clock driver .proc InstallDriver ; make language card writable lda LCBank1 lda LCBank1 ; copy clock driver into ProDOS ; should not be hard-coded, Apple says to put it at ; the address pointed to at ($BF07). [PDOS8TRM 6.1.1] ldx #$00 : lda rclkdrv-1,x sta CLKCODE,x inx cpx #CLKCODEMAX bcc :- ; make LC write-protected lda ROMIn2 ; make sure clock vector is preceded by a JMP ; (it is RTS if no clock installed) lda #$4C sta CLKENTRY ; indicate in MACHID that clock is present lda MACHID ora #$01 sta MACHID .endproc ; ---------------------------------------------------------------------------- ; find and launch the next .SYSTEM file .proc NextSys ; check our path for last / (counted string at $0280) ldy PATHBUF : lda PATHBUF,y cmp #'/' beq LSlash dey bne :- LSlash: sty SLASHOFFS ; offset of '/' into $00 lda PATHBUF sec sbc SLASHOFFS ; calculate length of our name sta MYNAMELEN ; and put in $01 jsr ReadVolDir1 ; read first block of volume directory bcs BadSys ; didn't get anything valid ; Now search for our own file name ChkSlf: lda (FENTPTR) ; get length & storage type and #$0F ; mask in length cmp MYNAMELEN ; same as our name? bne INext ; Nope, next entry tay ; Now let's see if name matches ldx PATHBUF : lda (FENTPTR),y cmp PATHBUF,x bne INext ; no match, next entry dex dey bne :- ; ok, found ourself, now get next entry and start looking for *.SYSTEM bra SNext ; Get next entry and start checking for .SYSTEM file INext: jsr NextFileEnt ; Get next entry and check again for ourself bcc ChkSlf bra BadSys ; no more valid entries ; check the current entry for .SYSTEM ChkSys: lda (FENTPTR) ; get storage & length and #$F0 ; mask in storage type beq SNext ; not valid if 0 cmp #$40 bcs SNext ; not valid if >= $40 (not type 1-3) lda (FENTPTR) ; load it again and #$0F ; but mask in length this time cmp #$07 bcc SNext ; not valid if not at least 7 chars long ; now check to see if it ends with .SYSTEM tay ldx #$07 : lda (FENTPTR),y cmp dSYSTEM,x bne SNext ; no match, next! dey dex bne :- ldy #$10 lda (FENTPTR),y ; get file Type cmp #$FF ; is SYS bne SNext ; Nope ; okay we have a .SYSTEM file to load lda (FENTPTR) ; get storage type/name length again and #$0F ; mask in name length tay ; and save for copying ; calculate total length of path and put in length byte at PATHBUF clc adc PATHBUF sec sbc MYNAMELEN sta PATHBUF ; copy filename into path tax : lda (FENTPTR),y sta PATHBUF,x dex dey bne :- jmp LaunchNext ; proceed to launch code ; get next entry for looking for .SYSTEM SNext: jsr NextFileEnt bcc ChkSys ; if we get here, unable to identify next .SYSTEM BadSys: cmp #$00 ; error code in accumulator? beq NoSys ; no, must have not found it pha jsr iprint .byte $18,"Directory ProDOS error #" pla jsr PrHex lda #$8D jsr COUT1 jmp DoQuit ; quit to ProDOS NoSys: jsr iprint .byte $1d,"Can't find next .SYSTEM file",$0d jmp DoQuit ; quit to ProDOS dSYSTEM = * - 1 .byte ".SYSTEM" .endproc ; ---------------------------------------------------------------------------- ; this looks like some unused debugging code to print the filename ; from the current directory entry pointed to at ($02) .proc PrintFN lda (FENTPTR) ; get storage type/name length pha ; save it and #$0F ; mask in name length sta (FENTPTR) ; put in file entry beq done ; if zero, we are done ldy #$01 ; start with byte 1 of entry : lda (FENTPTR),y ; get file name char ora #$80 jsr COUT1 ; print it tya iny cmp (FENTPTR) ; printed all the chars? bcc :- ; nope lda #$8D ; CR jsr COUT1 done: pla ; get type/name length sta (FENTPTR) ; and restore it in file entry rts .endproc ; ---------------------------------------------------------------------------- ; get volume directory first block ($0002) ; and initialize values for processing ; returns carry clear if no error, set otherwise .proc ReadVolDir1 lda #$02 sta DIRBLKL ; initialize block number low byte stz DIRBLKH ; and high byte jsr ReadVolDir ; read volume directory block bcs done ; done if error lda BLOCKBUF+$23 ; get entry length sta ENTLEN ; and save it lda BLOCKBUF+$24 ; get entries per block sta ENTPERBLK ; and save it ; intial values $02/03 = $102B (offset of first file entry in first VD block) lda #<(BLOCKBUF+$2b) sta FENTPTRL lda #>(BLOCKBUF+$2b) sta FENTPTRH lda #$02 ; on first block, start with entry #2 sta CURENT clc done: rts .endproc ; ---------------------------------------------------------------------------- ; Update pointer to the next directory file entry ; read the next block if necessary ; returns carry clear if no error, set otherwise .proc NextFileEnt ; add entry length to current entry pointer at ($02) clc lda FENTPTRL adc ENTLEN sta FENTPTRL lda FENTPTRH adc #$00 sta FENTPTRH inc CURENT ; increment current entry number lda CURENT ; and load it cmp ENTPERBLK ; did the last one in block? bcc Okay ; nope, exit ; check if last block of Volume Directory lda DIRBLKL ; block low byte bne NxtBlk ; another one to read lda DIRBLKH ; block high byte beq Fail ; also zero, exit with error ; read next block of Volume Directory NxtBlk: jsr ReadVolDir ; next volume directory block bcs Fail ; read problem, exit with error lda #<(BLOCKBUF+$04) ; now set entry pointer to the offset of the first sta FENTPTRL ; one in the block lda #>(BLOCKBUF+$04) ; ... sta FENTPTRH ; ... lda #$01 ; and set current entry sta CURENT ; to 1 Okay: clc rts Fail: sec rts .endproc ; ---------------------------------------------------------------------------- ; read volume directory block at ($07) ; and update to point to next block ; returns carry clear if no error, set otherwise .proc ReadVolDir ; most recent accessed device into param list for READ_BLOCK lda $BF30 ; most recent access device sta PL_READ_BLOCK+1 ; into READ_BLOCK parameter list ; copy block number to param list lda DIRBLKL ; copy block number sta PL_READ_BLOCK+4 ; to parameter list lda DIRBLKH ; ... sta PL_READ_BLOCK+5 ; ... jsr PRODOS ; now get the block .byte $80 ; READ_BLOCK .word PL_READ_BLOCK bcs Done ; error, bail out lda BLOCKBUF+$02 ; otherwise get next block number sta DIRBLKL ; from offset $02/$03 lda BLOCKBUF+$03 ; and save it sta DIRBLKH ; ... Done: rts ; Parameter list for READ_BLOCK PL_READ_BLOCK: .byte $03 ; param count .byte $00 ; unit number .word BLOCKBUF ; data buffer .word $0000 ; block number .endproc ; ---------------------------------------------------------------------------- ; enable slinky registers, set adddress and save byte we intend to trash .proc SlinkyEnable lda C8OFF ; not needed on //c, but release $C8xx firmware lda SLOT4ROM ; enable slinky registers lda #$08 ; set addr $080000 sta DPTRH stz DPTRM stz DPTRL lda DATA ; read data byte sta SAVEBYTE ; save it to restore later rts .endproc ; ---------------------------------------------------------------------------- ; Routine to restore trashed byte in slinky RAM ; WARNING: never called by unfixed, so the value is never restored .proc SlinkyRestore lda #$08 ; set adddr $080000 sta DPTRH stz DPTRM stz DPTRL lda SAVEBYTE ; get saved byte sta DATA ; and put it back lda C8OFF ; not needed on //c, but release $C8xx firmware rts .endproc ; ---------------------------------------------------------------------------- ; Write 8 bits to clock ; WARNING: the unfixed code has a bug that trashes 8 bytes in the RAM card at $080000 .proc ClockWrite8b ldx #$08 ; set adddr $080000 .if ::FIX_BUGS stx DPTRH stz DPTRM : stz DPTRL ; restore low byte to 0 sta DATA ; write byte .else stz DPTRL stz DPTRM stx DPTRH : sta DATA ; write byte .endif lsr a ; next bit into 0 position dex bne :- rts .endproc ; ---------------------------------------------------------------------------- ; unlock the clock by writing the magic bit sequence .proc ClockUnlock ldy #$08 : lda unlock,y jsr ClockWrite8b ; write 8 bits dey bne :- rts unlock = * - 1 .byte $5c, $a3, $3a, $c5, $5c, $a3, $3a, $c5 .endproc ; ---------------------------------------------------------------------------- ; Read 8 bits from the clock .proc ClockRead8b ldx #$08 ; set adddr $080000 stz DPTRL stz DPTRM stx DPTRH : pha ; save accumulator lda DATA ; get data byte lsr a ; bit 0 into carry pla ; restore accumulator ror a ; put read bit into position dex bne :- rts .endproc ; ---------------------------------------------------------------------------- ; read the clock data into memory at CLOCKBUF ; WARNING: unfixed code never restores byte we trashed .proc ClockRead jsr SlinkyEnable jsr ClockUnlock ldy #$00 : jsr ClockRead8b sta CLOCKBUF,y iny cpy #$08 ; have we read 8 bytes? bcc :- ; nope .if ::FIX_BUGS jsr SlinkyRestore .endif rts .endproc ; ---------------------------------------------------------------------------- ; validate the DClock data makes sense ; return carry clear if it does, carry set if it does not .proc ValidTime ; validate ms ldx #$00 ldy #$99 lda CLOCKBUF jsr CheckBCD bcs :+ ; validate seconds ldx #$00 ldy #$59 lda CLOCKBUF+$01 jsr CheckBCD bcs :+ ; validate minutes ldx #$00 ldy #$59 lda CLOCKBUF+$02 jsr CheckBCD bcs :+ ; validate hours ldx #$00 ldy #$23 lda CLOCKBUF+$03 jsr CheckBCD bcs :+ ; validate day of week ldx #$01 ldy #$07 lda CLOCKBUF+$04 jsr CheckBCD bcs :+ ; validate day of month ldx #$01 ldy #$31 lda CLOCKBUF+$05 jsr CheckBCD bcs :+ ; validate month ldx #$01 ldy #$12 lda CLOCKBUF+$06 jsr CheckBCD bcs :+ ; validate year ldx #$00 ldy #$99 lda CLOCKBUF+$07 jsr CheckBCD bcs :+ clc ; all good rts : sec ; problem rts .endproc ; ---------------------------------------------------------------------------- ; Check BCD number in range of [x,y] ; return carry clear if it is, carry set if it is not .proc CheckBCD sed ; decimal mode stx SCRATCH ; lower bound into scratch cmp SCRATCH ; compare it bcc :++ ; fail if out of range sty SCRATCH ; upper bound into scratch cmp SCRATCH ; compare it beq :+ ; OK if equal bcs :++ ; fail if out of range : cld ; in range clc rts : cld ; not in range sec rts .endproc ; ---------------------------------------------------------------------------- ; This code segment is relocated to RELOCT at the start of program ; ---------------------------------------------------------------------------- RELOCS = * - 1 rseg1bm = * .org RELOCT rseg1b = * ; ---------------------------------------------------------------------------- ; This routine attempts to execute the next system file that we identified ; in the main code .proc LaunchNext jsr PRODOS .byte $C8 ; OPEN .word PL_OPEN bcs LaunchFail ; if error lda PL_OPEN+5 ; copy reference number sta PL_GET_EOF+1 ; to parm list for GET_EOF sta PL_READ+1 ; and parm list for READ sta PL_CLOSE+1 ; and parm list for CLOSE jsr PRODOS .byte $D1 ; GET_EOF .word PL_GET_EOF bcs LaunchFail ; if error lda PL_GET_EOF+4 ; high byte of length bne LaunchFail ; bigger than 64K... sheesh lda PL_GET_EOF+2 ; copy EOF middle and low sta PL_READ+4 ; to read reqest count lda PL_GET_EOF+3 ; ... sta PL_READ+5 ; ... jsr PRODOS ; and do read .byte $CA ; READ .word PL_READ bcs LaunchFail ; if error jsr PRODOS .byte $CC ; CLOSE .word PL_CLOSE bcs LaunchFail ; if error jmp SYSEXEC ; execute system file LaunchFail: pha ; save accumulator jsr iprint ; because this trashes it .byte $22,"Can't start next SYS file: error #" pla ; restore accumulator jsr PrHex ; print error code lda #$8D jsr COUT1 jmp DoQuit ; quit to ProDOS ; parameter list for OPEN PL_OPEN: .byte $03 ; param count .word PATHBUF ; pathname address .word READBUF ; I/O buffer .byte $00 ; reference number ; parameter list for GET_EOF PL_GET_EOF: .byte $02 ; param count .byte $00 ; ref number .byte $00,$00,$00 ; EOF ; parameter list for READ PL_READ: .byte $04 ; param count .byte $00 ; ref number .word SYSEXEC ; data buffer .word $0000 ; request count .word $0000 ; transfer count ; parameter list for CLOSE PL_CLOSE: .byte $01 ; param count .byte $00 ; ref number .endproc ; ---------------------------------------------------------------------------- ; Routine to print hex number via LUT ; the firmware has a perfectly good routine for this, not sure why ; AE engineers put this in here. Nice wheel, though. .proc PrHex pha lsr a lsr a lsr a lsr a tax lda :+,x jsr COUT1 pla and #$0F tax lda :+,x jsr COUT1 rts : .byte "0123456789ABCDEF" .endproc ; ---------------------------------------------------------------------------- ; Execute QUIT call after long delay, exit via RTS if QUIT fails .proc DoQuit ldx #$0C : lda #$FF jsr WAIT dex bne :- jsr PRODOS .byte $65 .word PL_QUIT rts ; parameter list for QUIT PL_QUIT: .byte $04 ; param count .byte $00 ; quit type - $00 is only type .word $0000 ; reserved .byte $00 ; reserved .word $0000 ; reserved ; unnecessary fill? brk brk .endproc ; ---------------------------------------------------------------------------- ; 'inline print' ; print counted string following JSR .proc iprint ; get pointer into $09/$0a pla clc adc #$01 sta CHRPTRL pla adc #$00 sta CHRPTRH lda (CHRPTR) beq done ; now print the string tay ldy #$01 ploop: lda (CHRPTR),y ora #$80 jsr COUT1 tya iny cmp (CHRPTR) bcc ploop ; done, fix up stack and exit done: clc adc CHRPTRL sta CHRPTRL lda #$00 adc CHRPTRH pha lda CHRPTRL pha rts .endproc ; ---------------------------------------------------------------------------- ; end of relocated code rseg1e = * ; fix up org address so we get a clean ref to rclkdrv .org rseg1bm + (rseg1e - rseg1b + 1) ; ---------------------------------------------------------------------------- ; clock driver code inserted into ProDOS at $D742 - this shouldn't be ; hard-coded but it is. ; critical bug: Corrupts byte in $08xx01 of the //c memory expansion card. rclkdrv = * .org CLKCODE .proc clockdrv begin = * lda #$08 ; useless instruction php sei lda SLOT4ROM ; activate slinky registers ; ($08 from above overwritten) stz DPTRL ; set slinky address to $08xx00 ldy #$08 ; also counter for unlock bytes sty DPTRH lda DATA ; get destroyed byte ; (slinky now at $08xx01) pha ; save value on stack ; unlock dclock registers ubytlp: lda regulk,y ldx #$08 ; bit counter .if ::FIX_BUGS ubitlp: stz DPTRL ; reset pointer to $08xx00 sta DATA ; write to $08xx00 .else ubitlp: sta DATA ; write to $08xx00 ; (!but not on first iteration!) stz DPTRL ; reset pointer to $08xx00 .endif lsr a ; next bit into 0 position dex bne ubitlp dey bne ubytlp ; now read 64 bits (8 bytes) from dclock ldx #$08 ; byte counter rbytlp: ldy #$08 ; bit counter rbitlp: pha lda DATA ; data byte lsr a ; bit 0 into carry pla ror a ; carry into bit 7 dey bne rbitlp ; got 8 bits now, convert from BCD to binary pha and #$0F sta BCDTMP pla and #$F0 lsr a pha adc BCDTMP sta BCDTMP pla lsr a lsr a adc BCDTMP ; place in input buffer, which is OK because the ThunderClock driver does this sta INBUF-1,x dex bne rbytlp ; done copying, now put necessary values into ProDOS time locations ; copy hours to ProDOS hours lda INBUF+4 sta TIMEHI ; copy minutes to ProDOS minutes lda INBUF+5 sta TIMELO ; copy month ... lda INBUF+1 lsr a ror a ror a ror a ; ... and day of month to ProDOS month/day ora INBUF+2 sta DATELO ; copy year and final bit of month to ProDOS year/month lda INBUF rol a sta DATEHI stz DPTRL ; set slinky back to $08xx00 pla ; get saved byte sta DATA ; put it back plp rts ; DS1215 unlock sequence (in reverse) regulk = * - 1 .byte $5C, $A3, $3A, $C5, $5C, $A3, $3A, $C5 end = * .assert (begin - end + 1) < CLKCODEMAX, error, "DCLOCK driver too big" .endproc