title 'Boot loader module for CP/M 3.0' maclib options.lib public ?init,?ldccp,?rlccp,?time public @bootdu,@bootsl extrn ?pmsg,?conin extrn ?mvinit,?bnkxlt,?xmove,?move extrn @civec,@covec,@aivec,@aovec,@lovec extrn @cbnk,?bnksl,?bank extrn @sysdr,@ccpdr extrn dph0 extrn @dtbl,@ctbl extrn @date,@hour,@min,@sec extrn @srch1 extrn @hbbio extrn addhla extrn phex16, phex8 extrn cin, cout extrn crlf, crlf2 extrn bcd2bin, bin2bcd include c:ver.lib bdos equ 5 if banked tpa$bank equ 1 else tpa$bank equ 0 endif dseg ; init done from banked memory ?init: call ?mvinit ; Install RomWBW CBIOS stamp in page zero ld hl,stpimg ld de,stploc ld bc,stpsiz ldir if banked ; Clone page zero from bank 0 to additional banks ld b,2 ; last bank ld c,0 ; src bank init$2: push bc ; save bank id's call ?xmove ; set src/dest banks ld bc,0100h ; size is one page ld hl,0 ; dest adr is 0 ld de,0 ; src adr is 0 call ?move ; do it pop bc ; restore bank id's djnz init$2 ; loop till done endif call cinit ; char device init ld hl,signon$msg ; signon message call ?pmsg ; print it ; Check for HBIOS/CBIOS mismatch ld b,0F1h ; hbios version rst 08 ; do it, de=maj/min/up/pat ld a,d ; a := maj/min cp ((rmj << 4) | rmn) ; match? jr nz,init$3 ; handle ver mismatch ld a,e ; a := os up/pat and 0F0h ; pat not included in match cp (rup << 4) ; match? jr nz,init$3 ; handle ver mismatch jr init$4 ; all good, continue init$3: ; display version mismatch ld hl,vermis$msg ; version mismatch call ?pmsg ; display it init$4: ; Get boot disk unit and save it ld bc,0F8E0h ; HBIOS func: get boot info rst 08 ; do it, D := boot unit, E: := boot slice ld a,d ; move boot unit to A ld (@bootdu),a ; save it ld a,e ; move boot slice to A ld (@bootsl),a ; save it call dinit call clrram ret cinit: ; Setup CON: I/O vector based on HBIOS console device ld b,0FAh ; HBIOS Peek Function ld a,(@hbbio) ; HBIOS bank id ld d,a ; ... goes in D ld hl,112h ; Offset 112h is current console device rst 08 ; Call HBIOS, value in E push de ; save console unit value ld b,e ; Use as loop counter inc b ; ... but loop 1 extra time ld hl,0 ; Clear vector bitmap scf ; Set carry cinit$1: rr h ; Rotate carry flag rr l ; ... into correct vector position djnz cinit$1 ; loop as needed ld (@civec),hl ; assign to console input ld (@covec),hl ; assign to console output ; Setup AUX: I/O vector if there are 2+ char devices in system ld bc,0F800h ; HBIOS GET Character Device Count rst 08 ; do it, count in E ld a,e ; device count to accum pop de ; recover console unit num to E push af ; save device count cp 2 ; check for 2+ char devices jr c,cinit$3 ; if not, skip aux assignment ld a,e ; console unit num to A or a ; check for zero ld hl,4000h ; assume aux on second char device jr z,cinit$2 ; if console on unit 0, assumption good ld hl,8000h ; otherwise, aux goes to first char device cinit$2: ld (@aivec),hl ; assign to aux input ld (@aovec),hl ; assign to aux output cinit$3: pop af ; recover device count ; Truncate char table based on actual num of char devices rlca ; A still has char device count rlca ; * 8 for ctbl entry size rlca ; " ld hl,@ctbl ; Start of char table call addhla ; Skip used entries xor a ; Zero to accum ld (hl),0 ; Set table terminator ret ; done dinit: ; loop through all disk devices to count hard disk units ld b,0F8h ; SYS GET ld c,010h ; Disk Drive Unit Count rst 08 ; e := disk unit count ld b,e ; count to b ld a,b ; count to a or a ; set flags ret z ; !!! handle zero devices (albeit poorly) !!! ; loop thru devices to count total hard disk volumes ld c,0 ; init c as device list index ld d,0 ; init d as total device count ld e,0 ; init e for hard disk device count ld hl,drvlst ; init hl ptr to drive list ; dinit2: push bc ; save loop cnt & unit call dinit3 ; check drive pop bc ; recover loop cnt & unit inc c ; next unit djnz dinit2 ; loop ld a,d ; total device count to d ld (drvlstc),a ; save the count jr dinit4 ; continue dinit3: push de ; save de (hard disk volume counter) push hl ; save drive list ptr push bc ; save loop control ld b,17h ; hbios func: report device info rst 08 ; call hbios, unit to c ld a,c ; device attributes to a pop bc ; restore loop control pop hl ; restore drive list ptr pop de ; restore de ld b,a ; attributes to b ; ; if device is not removable, then check to ensure it is ; actually online. if not, skip unit entirely. bit 6,b ; removable? jr nz,dinit3a ; if so, skip media check push de ; save de (hard disk volume counter) push hl ; save drive list ptr push bc ; save loop control ld b,18h ; hbios func: sense media ld e,1 ; perform media discovery rst 08 ; do it pop bc ; restore loop control pop hl ; restore drive list ptr pop de ; restore de ret nz ; offline, skip entire unit ; dinit3a: ld (hl),c ; save unit num in list inc hl ; bump ptr inc d ; inc total device count bit 5,b ; high capacity? ret z ; done if not inc e ; increment hard disk count ret ; done dinit4: ; set slices per volume (hdspv) based on hard disk volume count ld a,e ; hard disk volume count to a ld e,8 ; assume 8 slices per volume dec a ; dec accum to check for count = 1 jr z,dinit5 ; yes, skip ahead to implement 8 hdspv ld e,4 ; now assume 4 slices per volume dec a ; dec accum to check for count = 2 jr z,dinit5 ; yes, skip ahead to implement 4 hdspv ld e,2 ; in all other cases, we use 2 hdspv dinit5: ld a,e ; slices per volume value to accum ld (hdspv),a ; save it ld hl,0 ; dph index ld a,(@bootdu) ; boot disk unit ld d,a ; ... to d ld a,(@bootsl) ; boot slice ld e,a ; ... to e ld b,1 ; one slice please call dinit8a ; make DPH for A: ld a,(drvlstc) ; active drive list count to accum ld b,a ; ... and move to b for loop counter ld de,drvlst ; de is ptr to active drive list dinit6: ; loop thru all units available push de ; preserve drive list ptr ex de,hl ; list ptr to hl ld c,(hl) ; get unit num from list ex de,hl ; list ptr back to de push bc ; preserve loop control push hl ; preserve dph pointer ld b,17h ; hbios func: report device info rst 08 ; call hbios, a := device attributes ld a,c ; device attributes to a pop hl ; restore dph pointer pop bc ; get unit index back in c push bc ; resave loop control call dinit7 ; make drive map entry(s) pop bc ; restore loop control inc c ; increment list index pop de ; restore drive list ptr inc de ; increment active drive list ptr djnz dinit6 ; loop as needed ret dinit7: ; process a unit (all slices) ld e,0 ; initialize slice index ld b,1 ; default loop counter ld d,c ; unit number to d bit 5,a ; high capacity device? jr z,dinit8 ; nope, leave loop count at 1 ld a,(hdspv) ; get slices per volume to accum ld b,a ; move to b for loop counter dinit8: ; test to avoid reallocating boot disk unit/slice ld a,(@bootdu) ; boot disk unit to accum cp d ; compare to cur unit jr nz,dinit8a ; if ne, ok to continue ld a,(@bootsl) ; boot slice to accum cp e ; compare to cur slice jr nz,dinit8a ; if ne, ok to continue inc e ; is boot du/slice, skip it djnz dinit8 ; loop till done with unit ret dinit8a: ; d=unit, e=slice, l=dph#, b=slice cnt ld a,l ; dph # to accum cp 16 ; dph table size ret z ; bail out if overflow push hl ; save dph # rlca ; *2 for adr entry ld hl,@dtbl ; dph table start call addhla ; offset hl to desired entry ld a,(hl) ; dereference inc hl ld h,(hl) ld l,a dec hl ; backup to slice field ld (hl),e ; update slice number dec hl ; backup to unit number ld (hl),d ; update unit number pop hl ; restore dph # inc hl ; next dph # inc e ; next slice djnz dinit8 ; loop till done with unit ret ; RomWBW CBIOS page zero stamp starts at $40 ; $40-$41: Marker ('W', ~'W') ; $42-$43: Version bytes: major/minor, update/patch ; $44-$45: CBIOS Extension Info address stploc equ 40h stpimg db 'W',~'W' ; marker db rmj << 4 | rmn ; first byte of version info db rup << 4 | rtp ; second byte of version info dw cbx ; address of cbios ext data stpsiz equ $ - stpimg cseg ; ram disk init must be done from resident memory ; ; Initialize ram disk by filling directory with 'e5' bytes ; Fill first 8k of ram disk track 1 with 'e5' ; clrram: di ; no interrupts ld a,(0FFE0h) ; get current bank push af ; save it ld b,0FAh ; HBIOS Peek Function ld a,(@hbbio) ; HBIOS bank id ld d,a ; ... goes in D ld hl,1DCh ; Offset 1DCh is ram disk bank 0 rst 08 ; Call HBIOS, value in E ld a,e ; move to A for bank sel cp 0FFh jr z,clrram3 ;call hb_bnksel ; select bank call 0FFF3h ; select bank ; Check the first sector (512 bytes) for all zeroes. If so, ; it implies the RAM is uninitialized. ld hl,0 ; start at begining of ram disk ld bc,512 ; compare 512 bytes xor a ; compare to zero clrram000: cpi ; a - (hl), hl++, bc-- jr nz,clrram00 ; if not zero, go to next test jp pe,clrram000 ; loop thru all bytes jr clrram2 ; all zeroes, jump to init clrram00: ; Check first 32 directory entries. If any start with an invalid ; value, init the ram disk. Valid entries are e5 (empty entry) or ; 0-15 (user number). ld hl,0 ld de,32 ld b,32 clrram0: ld a,(hl) cp 0E5h jr z,clrram1 ; e5 is valid cp 16 jr c,clrram1 ; 0-15 is also valid jr clrram2 ; invalid entry! jump to init clrram1: add hl,de ; loop for 32 entries djnz clrram0 ; jr clrram2 ; *debug* jr clrram3 ; all entries valid, bypass init clrram2: ld hl,0 ; source adr for fill ld bc,2000h ; length of fill is 8k ld a,0E5h ; fill value call fill ; do it or 0ffh ; flag value for cleared ld (clrflg),a ; save it clrram3: pop af ; recover original bank ;call hb_bnksel ; select bank call 0FFF3h ; select bank ei ; resume interrupts ld a,(clrflg) ; get cleared flag or a ; set flags ld hl,clr$msg ; clear ram disk message call nz,?pmsg ; print msg if true ret ; ; Fill memory at hl with value a, length in bc. All regs used. ; Length *must* be greater than 1 for proper operation!!! ; fill: ld d,h ; set de to hl ld e,l ; so destination equals source ld (hl),a ; fill the first byte with desired value inc de ; increment destination dec bc ; decrement the count ldir ; do the rest ret ; return cseg ; boot loading most be done from resident memory ; This version of the boot loader loads the CCP from a file ; called CCP.COM on the system drive. ?ldccp: ; Force CCP to use system boot drive as initial default ;ld a,(@sysdr) ; get system boot drive ;ld (@ccpdr),a ; set CCP current drive ; First time, load the CCP.COM file into TPA ;ld a,(@sysdr) ; get system boot drive ;;ld (4),a ; save in page zero??? ;inc a ; drive + 1 for FCB ;ld (ccp$fcb),a ; stuff into FCB ;add 'A' - 1 ; drive letter ;ld (ccp$msg$drv),a ; save for load msg ;xor a ;ld (ccp$fcb+15),a ld hl,0 ld (fcb$nr),hl ld de,ccp$fcb call open inc a jr z,no$CCP ld de,0100H call setdma ld de,128 call setmulti ld de,ccp$fcb call read if banked ld hl,0100h ; clone 3K, just in case ld bc,0C80h ld a,(@cbnk) ; save current bank push af ld$1: ld a,tpa$bank ; select TPA call ?bnksl ld a,(hl) ; get a byte push af ld a,2 ; select extra bank call ?bnksl pop af ; save the byte ld (hl),a inc hl ; bump pointer, drop count dec bc ld a,b ; test for done or c jr nz,ld$1 pop af ; restore original bank call ?bnksl endif ret no$CCP: ; here if we couldn't find the file ld hl,ccp$msg call ?pmsg call ?conin jp ?ldccp ?rlccp: if banked ld hl,0100h ; clone 3K ld bc,0C80h rl$1: ld a,2 ; select extra bank call ?bnksl ld a,(hl) ; get a byte push af ld a,tpa$bank ; select TPA call ?bnksl pop af ; save the byte ld (hl),a inc hl ; bump pointer, drop count dec bc ld a,b ; test for done or c jr nz,rl$1 ret else jr ?ldccp endif ?time: ; per CP/M 3 docs, *must* preserve HL, DE push hl push de ; force return through time$ret ld hl,time$ret push hl ; branch to get or set routine ld a,c ; get switch value or a ; test for zero jr z,time$get ; 0 means get time jr time$set ; else set time time$ret: ; restore HL, DE pop de pop hl ret time$get: ; RTC -> cpm date/time in SCB ; read time from RTC ld b,020h ; HBIOS func: get time ld hl,tim$buf ; time buffer rst 08 ; do it ret nz ; bail out on error ; convert yymmss in time buffer -> cpm3 epoch date offset call date2cpm ; time buf (yr, mon, day) -> SCB (@date) time$get1: ; set time fields in SCB ld a,(tim$hr) ; get hour from time buf ld (@hour),a ; ... and put in SCB ld a,(tim$min) ; get minute from time buf ld (@min),a ; ... and put in SCB ld a,(tim$sec) ; get second from time buf ld (@sec),a ; ... and put in SCB ret time$set: ; CPM date/time in SCB -> RTC ; convert CPM3 epoch date offset in SCB -> yymmss in time buffer call cpm2date ; SCB (@date) -> time buf (yr, mon, day) ; copy CPM3 time values from SCB -> time buffer ld a,(@hour) ; get hour from SCB ld (tim$hr),a ; ... and put in tim$hr ld a,(@min) ; get minute from SCB ld (tim$min),a ; ... and put in tim$min ld a,(@sec) ; get second from SCB ld (tim$sec),a ; ... and put in tim$sec ; send time to RTC ld b,021h ; HBIOS func: set time ld hl,tim$buf ; ... from time buffer rst 08 ; do it ret date2cpm: ; Convert YYMMSS from time buffer at HL ; into offset from CPM epoch and store ; result in SCB. ld hl,0 ; initialize day counter ; Add in days for elapsed years ld a,(tim$yr) ; get current year call bcd2bin ; convert to binary sub 78 ; epoch year jr nc,d2c1 ; if not negative, good to go add a,100 ; else, adjust for Y2K wrap d2c1: ld b,a ; loop counter ld c,3 ; leap counter, 78->79->80, so 3 ld de,365 ; days in non-leap year or a ; check for zero jr z,d2c10 ; skip if zero d2c2: add hl,de ; add non-leap days dec c ; dec leap counter jr nz,d2c3 ; if not leap, bypass leap inc inc hl ; add leap day ld c,4 ; reset leap year counter d2c3: djnz d2c2 ; loop for all years d2c10: ; Add in days for elapsed months ld de,daysmon ; point to start of days per mon tbl ld a,(tim$mon) ; get current month call bcd2bin ; convert to binary dec a ; don't include cur mon jr z,d2c20 ; done if Jan ld b,a ; save as loop counter cp 2 ; Mar = 2 jr c,d2c11 ; bypass if < Mar (no leap month) dec c ; C still has leap year counter jr nz,d2c11 ; skip if not leap year inc hl ; add in the leap day d2c11: ld a,(de) ; get days for cur mon call addhla ; add to running count inc de ; bump to next mon ptr djnz d2c11 ; loop for # of months d2c20: ; Add in days elapsed within month ; Note that we don't adjust the date to be a zero ; offset which seems wrong. From what I can tell ; the CP/M epoch is really 1/0/1978 rather than the ; 1/1/1978 that the documentation claims. Below seems ; to work correctly. ld a,(tim$day) ; get day call bcd2bin ; make binary call addhla ; add in days ld (@date),hl ; store in SCB ;call phex16 ; *debug* ;call crlf ; *debug* ret cpm2date: ; Convert CPM epoch date offset in SCB ; into YYMMSS values and store result in ; time buffer at HL. ; We start by subtracting years keeping a count ; of the number of years. Every fourth year is a leap ; year, so we account for that as we go. ld hl,(@date) ; get the count of days since epoch dec hl ; because we want 1/1/78 to be offset 0 ld c,78 ; init the years value c2d1: ld de,365 ; normal number of days per year ld a,c ld b,0 ; init leap year flag and 03h ; check for leap year jr nz,c2d2 ; if not zero, no need to adjust inc de ; add a day for leap year ld b,1 ; leap year flag for later c2d2: or a ; clear carry sbc hl,de ; subtract jr c,c2d3 ; get out if we went too far inc c ; add a year to year value ld a,c ; to accum cp 100 ; century rollover? jr nz,c2d1 ; nope, loop ld c,0 ; reset for start of century jr c2d1 ; loop c2d3: ld a,c ; years to accum call bin2bcd ; convert to bcd ld (tim$yr),a ; ... and save it ;call phex8 ; *debug* ; ; Now we use the days per month table to find the ; month. add hl,de ; restore days remaining ld c,0 ; init month value (zero indexed) c2d4: ld a,c ; get month value push hl ; save hl (days remaining) ld hl,daysmon ; point to start of table call addhla ; point to month entry ld e,(hl) ; get months day count to E ld d,0 ; zero msb, DE is days in month pop hl ; recover hl (days remaining) ld a,c ; month value to accum cp 1 ; leap month? check for Feb jr nz,c2d5 ; no, leave alone ld a,b ; get leap year flag (set above) or a ; leap year? jr z,c2d5 ; if not, skip ahead inc de ; account for leap year c2d5: or a ; clear carry sbc hl,de ; subtract days for the month jr c,c2d6 ; get out if we went too far inc c ; next month jr c2d4 ; continue c2d6: inc c ; switch from 0 to 1 offset ld a,c ; move to accum call bin2bcd ; convert to bcd ld (tim$mon),a ; save it ;call phex8 ; *debug* ; ; Leftover days is day value add hl,de ; restore days remaining ld a,l ; only need lsb inc a ; switch from 0 to 1 offset call bin2bcd ; convert to bcd ld (tim$day),a ; save it ;call phex8 ; *debug* ret daysmon: ; days per month db 31 ; January db 28 ; February (non-leap) db 31 ; March db 30 ; April db 31 ; May db 30 ; June db 31 ; July db 31 ; August db 30 ; September db 31 ; October db 30 ; November db 31 ; December ; RTC time buffer (all values packed bcd) tim$buf: tim$yr db 80h tim$mon db 05h tim$day db 10h tim$hr db 01h tim$min db 02h tim$sec db 03h open: ld c,15 jp bdos setdma: ld c,26 jp bdos setmulti: ld c,44 jp bdos read: ld c,20 jp bdos clrflg db 0 ; RAM disk cleared flag clr$msg db 'RAM Disk Initialized',13,10,13,10,0 vermis$msg db 7,'*** WARNING: HBIOS/CBIOS Version Mismatch ***',13,10,13,10,0 if zpm signon$msg db 13,10,'ZPM3' if banked db ' [BANKED]' endif db ' for HBIOS v' biosver db 13,10,13,10,0 ccp$msg db 13,10,'BIOS Err on ' ccp$msg$drv db 'A' db ': No ZCCP.COM file',0 ccp$fcb db 1,'ZCCP ','COM',0,0,0,0 ds 16 fcb$nr db 0,0,0 else signon$msg db 13,10,'CP/M v3.0' if banked db ' [BANKED]' endif db ' for HBIOS v' biosver db 13,10,13,10,0 ccp$msg db 13,10,'BIOS Err on ' ccp$msg$drv db 'A' db ': No CCP.COM file',0 ccp$fcb db 1,'CCP ','COM',0,0,0,0 ds 16 fcb$nr db 0,0,0 endif @bootdu db 0 ; boot disk unit @bootsl db 0 ; boot slice hdspv db 2 ; slices per volume for hard disks (must be >= 1) drvlst ds 32 ; active drive list used durint drv_init drvlstc db 0 ; entry count for active drive list ; The following section contains key information and addresses for the ; RomWBW CBIOS. A pointer to the start of this section is stored with ; with the CBX data in page zero at $44 (see above). cbx: devmapadr dw 0 ; device map address drvtbladr dw @dtbl ; drive map address (filled in later) dphtbladr dw dph0 ; dpb map address cbxsiz equ $ - cbx end