;*=*=*
;	xtrshard/dct
;	Emulate hard disk in a Unix file under xtrs
;
;	Copyright (c) 1998, Timothy Mann
;       $Id: xtrshard.z80,v 1.9 2008/06/26 04:39:56 mann Exp $
;
;	This software may be copied, modified, and used for any
;	purpose without fee, provided that (1) the above copyright
;	notice is retained, and (2) modified versions are clearly
;	marked as having been modified, with the modifier's name and
;	the date included.  
;*=*=*


; Number of drives to allow
ndrive	equ	8	


; ASCII chars
LF      equ     10
CR	equ	13
ETX	equ	3

; Model 4 SVC numbers
@high   equ     100
@dsply  equ     10
@flags  equ     101
@logot  equ     12
@gtdcb  equ	82 
@gtmod	equ	83
@div8	equ	93
@mul16	equ	91
@keyin	equ	9

; Model I/III hard addresses
m3flag$ equ	0125h	; 'I' in ROM on Model III
@logot1 equ     447bh
@logot3	equ	428ah
@dsply1	equ	4467h
@dsply3	equ	4467h
high$1  equ     4049h
high$3  equ     4411h
cflag$1 equ     4758h
cflag$3 equ     4758h
@icnfg1	equ	4303h
@icnfg3	equ	421dh
@mult1	equ	4b8fh
@mult3	equ	444eh
@divea1	equ	4b7bh
@divea3	equ	4b7ah
@keyin1	equ	0040h
@keyin3	equ	0040h

; Emulator trap instructions in byte-reversed form
emt_read	equ	32EDH
emt_write	equ	33EDH
emt_lseek	equ	34EDH
emt_strerror	equ	35EDH
emt_ftruncate	equ	3DEDH
emt_opendisk	equ	3EEDH
emt_closedisk	equ	3FEDH

; Constants for emt_opendisk
EO_RDONLY	equ    00o
EO_WRONLY	equ    01o
EO_RDWR		equ    02o
EO_CREAT	equ  0100o
EO_EXCL		equ  0200o
EO_TRUNC	equ 01000o
EO_APPEND	equ 02000o

;*=*=*
;	Set origin to be safe on both LDOS 5 and 6
;*=*=*
	org	6000h

;*=*=*
; 	Relocator for disk driver
;*=*=*
instal:	ld	(dct),de	;Save DCT address
	ld	a,(000ah)	;Determine TRS-80 model
	cp	40h
	jp	nz,lsdos6	;Model 4 (or other LS-DOS, I hope)
	ld	a,(m3flag$)
	cp	'I'
	jp	z,model3	;Go if Model III
;*=*=*
; 	LDOS 5 Model I - See LS-DOS 6 version for comments
;*=*=*
	ld	a,0cdh		;Insert Model I @LOGOT
	ld	(logot),a
	ld	hl,@logot1
	ld	(logot+1),hl
	ld	(domul),a	;Insert Model I MULT
	ld	hl,@mult1
	ld	(domul+1),hl
	ld	(dodiv),a	;Insert Model I DIVEA
	ld	hl,@divea1
	ld	(dodiv+1),hl
	ld	a,'1'		;Modify filename
	ld	(hmod),a
	ld	hl,hello_
	call	@dsply1
	ld      a,(cflag$1)
	bit     3,a		;System request?
	jp      z,viaset
	ld	de,(dct)
	ld	a,d		;DRIVE= must be specified
	or	e
	jp	z,needdr
asku1:	ld	hl,unit_	;Ask which unit number
	call	@dsply1
	ld	hl,unit
	ld	bc,100h
	call	@keyin1
	jp	c,hitbrk
	jp	nz,hitbrk
	ld	a,(unit)
	cp	'0'
	jr	c,asku1
	cp	'0'+ndrive
	jr	nc,asku1
	ld	hl,(high$1)
	call	xgtmod		;Module already loaded?
	jp	z,setdct
	ld      hl,(high$1)
	ld      (newend),hl
	ld	de,length
	sub	a
	sbc	hl,de
	ld      (high$1),hl
	call	dvrini
	call	relo
	ld	a,(@icnfg1)
	ld	(link),a
	ld	hl,(@icnfg1+1)
        ld      (link+1),hl
	ld	hl,dvrcfg
rx16	equ	$-2
        ld      (@icnfg1+1),hl
	ld	a,0c3h
	ld	(@icnfg1),a
	jp	move
;*=*=*
; 	LDOS 5 Model III
;*=*=*
model3:
	ld	a,0cdh		;Insert Model III @LOGOT
	ld	(logot),a
	ld	hl,@logot3
	ld	(logot+1),hl
	ld	(domul),a	;Insert Model III MULT
	ld	hl,@mult3
	ld	(domul+1),hl
	ld	(dodiv),a	;Insert Model III DIVEA
	ld	hl,@divea3
	ld	(dodiv+1),hl
	ld	a,'3'		;Modify filename
	ld	(hmod),a
	ld	hl,hello_
	call	@dsply3
	ld      a,(cflag$3)
	bit     3,a		;System request?
	jp      z,viaset
	ld	de,(dct)
	ld	a,d		;DRIVE= must be specified
	or	e
	jp	z,needdr
asku3:	ld	hl,unit_	;Ask which unit number
	call	@dsply3
	ld	hl,unit
	ld	bc,100h
	call	@keyin3
	jp	c,hitbrk
	jp	nz,hitbrk
	ld	a,(unit)
	cp	'0'
	jr	c,asku3
	cp	'0'+ndrive
	jr	nc,asku3
	ld	hl,(high$3)
	call	xgtmod		;Module already loaded?
	jp	z,setdct
	ld      hl,(high$3)
	ld      (newend),hl
	ld	de,length
	sub	a
	sbc	hl,de
	ld      (high$3),hl
	call	dvrini
	call	relo
	ld	a,(@icnfg3)
	ld	(link),a
	ld	hl,(@icnfg3+1)
        ld      (link+1),hl
	ld	hl,dvrcfg
rx17	equ	$-2
        ld      (@icnfg3+1),hl
	ld	a,0c3h
	ld	(@icnfg3),a
	jp	move

;*=*=*
; 	LS-DOS 6
;*=*=*
lsdos6:	ld	a,'4'		;Modify filename
	ld	(hmod),a
	ld      hl,hello_
	ld      a,@dsply        ;Display hello
	rst     40
;*=*=*
;       Check if entry from SYSTEM command.
;*=*=*
	ld      a,@flags        ;Get flags pointer into IY
	rst     40
	ld      a,(iy+'C'-'A')  ;Get CFLAG$
	bit     3,a		;System request?
	jp      z,viaset
	ld	de,(dct)
	ld	a,d		;DRIVE= must be specified
	or	e
	jp	z,needdr
;*=*=*
;	Ask which unit number
;*=*=*
asku4:	ld	hl,unit_	;Ask which unit number
	ld	a,@dsply
	rst	40
	ld	hl,unit
	ld	bc,100h
	ld	a,@keyin
	rst	40
	jp	c,hitbrk
	jp	nz,hitbrk
	ld	a,(unit)
	cp	'0'
	jr	c,asku4
	cp	'0'+ndrive
	jr	nc,asku4
;*=*=*
;       Check if driver already loaded
;*=*=*
	ld	de,modnam
	ld	a,@gtmod
	rst	40
	jp	z,setdct	;Already loaded, skip loading
;*=*=*
;       Obtain low memory driver pointer.  Bizarre API here!
;*=*=*
        ld      e,'K'           ;Locate pointer to *KI DCB
	ld	d,'I'		;  via @GTDCB SVC
        ld      a,@gtdcb        
        rst     40
        jp      nz,curdl        ;No error unless KI clobbered!
        dec     hl              ;Decrement to driver pointer
        ld      d,(hl)          ;P/u hi-order of pointer,
        dec     hl              ;  decrement to and p/u
        ld      e,(hl)          ;  lo-order of pointer
;*=*=*
;       Check if driver will fit into [(LCPTR), X'12FF']
;*=*=*
	push	hl		;Save address of pointer
        ld      hl,length	;New pointer will be
        add     hl,de		;  pointer + LENGTH
	ld	d,h		;Save a copy in DE
	ld	e,l
        ld      bc,1301h        ;If > 1300H, driver won't fit
        sub     a               ;Reset carry flag
        sbc     hl,bc
	pop	hl		;Get back address of pointer
        jr      nc,usehi        ;Go if driver won't fit
	ld	(hl),e		;Store new value of pointer
	inc	hl
	ld	(hl),d
	dec	de		;Last byte of driver goes here
	ld      (newend),de
	jr	dorelo
;*=*=*
;       Put in high memory instead.
;*=*=*
usehi:	ld      hl,0            ;Get current HIGH$
	ld      b,l
	ld      a,@high 
	rst     40
	jp      nz,nomem
	ld      (newend),hl	;Last byte of driver goes here
	ld	de,length
	sub	a		;Reset carry flag
	sbc	hl,de		;Compute new HIGH$
	ld      a,@high         ;Set new HIGH$ into the system
	rst     40
;*=*=*
;       Relocate internal references in driver.
;	HL = address for last byte of driver.
;*=*=*
dorelo:	call	dvrini		;Final driver init before move
	call	relo
;*=*=*
;	Link to @ICNFG (must follow address relocation and precede movement)
;*=*=*
	ld      a,@flags        ;Get flags pointer into IY
	rst     40
        ld      a,(iy+28)       ;Copy current @ICNFG into LINK
        ld      l,(iy+29)
        ld      h,(iy+30)
	ld	(link),a
        ld      (link+1),hl
	ld	hl,dvrcfg	;Get relocated init address
rx10	equ	$-2
        ld      (iy+29),l       ;Save in @ICNFG vector
        ld      (iy+30),h
	ld	(iy+28),0c3h	;Insert JP opcode
;*=*=*
;       Move driver into low or high memory.
;*=*=*
move:
	ld      de,(newend)     ;Destination address
	ld      hl,dvrend       ;Last byte of module
	ld      bc,length       ;Length of filter
	lddr
	ex      de,hl
	inc     hl              ;Bump to driver entry
;*=*=*
;	Setup DCT (iy+5 to iy+9 are reset by ckopen if successful)
;*=*=*
setdct:
	ld	iy,(dct)
	ld	(iy),0c3h	  ;JP instruction (enable driver)
	ld      (iy+1),l          ;Driver address
	ld      (iy+2),h
	ld	(iy+3),00001100b  ;Flags: rigid, fixed, step rate 0
	ld	a,(unit)
	and	0fh
	or	00010000b         ;Flags: alien (=no index pulses), unit#
	ld	(iy+4),a
	ld	(iy+5),0	  ;LDOS undefined; we use as sec/cyl (0=256).
	ld	(iy+6),201	  ;high cylinder number
	ld	(iy+7),11111111b  ;high head # (111), high sec/trak (11111)
	ld	(iy+8),11111111b  ;high gran # (111), high sec/gran (11111)
	ld	(iy+9),0ffh	  ;Directory cylinder
;*=*=*
;	Open file now so user can get error if any, and so geometry
;	is established as early as possible.
;*=*=*
	call	ckopen
	ld      hl,0            ;Successful completion
	ret	z		;Fall thru if error
;*=*=*
uerror:	ld	hl,errbuf	;Unix error
	ld	bc,256
	defw	emt_strerror
	defb	0ddh
curdl:	ld	hl,curdl_	;Other error
	defb	0ddh
needdr:	ld	hl,needdr_
	defb	0ddh
viaset: ld      hl,viaset_
	defb    0ddh
nomem:  ld      hl,nomem_
	defb    0ddh
hitbrk:	ld	hl,hitbrk_
logot:	ld	a,@logot 
	rst	40
	ld      hl,-1            ;Unuccessful completion
	ret

;*=*=*
;       Relocate internal references in driver.
;	HL = address for last byte of driver.
;*=*=*
relo:	ld	hl,(newend)
	ld      iy,reltab       ;Point to relocation tbl
	ld      de,dvrend
	sub     a               ;Clear carry flag
	sbc     hl,de
	ld      b,h             ;Move to BC
	ld      c,l
rloop:  ld      l,(iy)          ;Get address to change
	ld      h,(iy+1)
	ld      a,h
	or      l
	ret	z
	ld      e,(hl)          ;P/U address
	inc     hl
	ld      d,(hl)
	ex      de,hl           ;Offset it
	add     hl,bc
	ex      de,hl
	ld      (hl),d          ;And put back
	dec     hl
	ld      (hl),e
	inc     iy
	inc     iy
	jr      rloop           ;Loop till done

;*=*=*
;	Search for existing copy of driver.
;        Rough Model I/III emulation of Model 4 @GTMOD,
;        hardcoded with driver address.
;	Entry: HL holds HIGH$
;	Exit Z: HL holds driver address
;            NZ: driver not found
;*=*=*
xgtmod:	inc	hl
	ld	a,h
	or	l
	jr	nz,xgtm1
	dec	a		;not found
	ret
xgtm1:	ld	a,(hl)
	cp	18h		;unconditional jr?
	ret	nz		;not a module header
	push	hl		;save start address
	inc	hl		;skip jr
	inc	hl		;skip offset
	inc	hl		;skip start address
	inc	hl
	ld	a,(hl)		;compare name length
	cp	modptr-modnam
	jr	nz,nextmd	;different - skip
	ld	b,a		;compare name
	ld	de,modnam
	inc	hl
xgtm2:	ld	a,(de)
	cp	(hl)
	jr	nz,nextmd	;different - skip
	inc	de
	inc	hl
	djnz	xgtm2
	pop	hl		;same - found
	ret
nextmd:	pop	hl		;get back start of module
	inc	hl
	inc	hl
	ld	e,(hl)		;pointer to last byte
	inc	hl
	ld	d,(hl)
	ex	de,hl
	jr	xgtmod

;*=*=*
;	Messages and globals
;*=*=*
hello_: defb    'XTRSHARD - Emulated hard disk driver for xtrs - 5/17/00',CR
curdl_:	defb	'LS-DOS is curdled!',CR
nomem_: defb    'High memory is not available!',CR
viaset_:defb    'Must install via SYSTEM (DRIVE=,DRIVER=)!',CR
needdr_:defb	'DRIVE= must be specified!',CR
unit_:	defb	'Enter unit number ([0]-','0'+ndrive-1,'): ',ETX
hitbrk_:defb	'Aborted!',CR
lcptr:	defw	0
newend:	defw	0
dct:    defw	0
unit:   defs	2
errbuf:	defs	256

;
; Driver - Based on skeletal driver from the Guide
;

entry:  jr      begin           ;The driver starts with the
	defw    dvrend          ;  DOS standard header
rx00	equ	$-2
	defb    modptr-modnam   ;Length of name
modnam:	defb    'xtrshard'      ;Name for @GTMOD requests
modptr: defw    0               ;These pointers are unused
	defw    0
fd:	defs	ndrive*2	;Unix file descriptors
offset:	defw	0,0,0,0		;lseek offset buffer
begin:  
;*=*=*
;	First make sure the file is open and correct the geometry
;       in the DCT if needed.
;*=*=*
	push	ix
	call	ckopen
rx03	equ	$-2
	call	body
rx06	equ	$-2
	pop	ix
	ret
body:	ld	a,32		;"Illegal drive number"
	ret	nz
	ld      a,b             ;The first test will return
	and     a               ;  to the caller on @DCSTAT
	ret	z       	;  and set the Z-flag with A=0
notdcs:	cp      7
	jr      z,rslct         ;Transfer on @RSLCT
	jr      nc,diskio       ;Transfer on physical I/O request
;*=*=*
;	@SLCT, @DCINIT, @DCRES, @RSTOR, @STEPI or @SEEK: no-op
;*=*=*
retzer:	sub	a
	ret
;*=*=*
;       The RSLCT function should return with the hardware
;       write protection status. Set bit 6 of the accumulator
;       to indicate the drive is write-protected
;*=*=*
rslct:  sub	a               ;No emulated hardware WP for now
	ret
;*=*=*
diskio: bit     2,b             ;Test if read or write commands
	jr      nz,wrcmd        ;Transfer if functions <12-15>
	cp	10
	jr	z,vrsec
	jr	nc,rdtrk
	cp	9
	jr	z,rdsec
rdhdr:  ld	a,32		;Not supported ("Illegal drive number")
        and	a            
	ret
;*=*=*
rdsec:				;Read a sector of data
	ld	a,(iy+6)	;Get high cyl #
	cp	d		;At or below it?
	jr	nc,rdok
	ld	a,2		;"Seek error during read"
	ret			;NZ already set
rdok:	push	de
	push	hl
	call	doseek		;Setup and do lseek
rx01	equ	$-2
	pop	hl
	ld	a,5		;"Data record not found during read"
	jr	nz,rddun
	ld	bc,256
	defw	emt_read
	ld	a,4		;"Parity error during read"
rddun:	pop	de
	ret	nz
	ld	a,b		;Check for end of file
	or	c
	jr	nz,rddun2
	push	de
	push	hl		;Return a block full of 0E5H		
	push	bc
	ld	(hl),0e5h
	ld	d,h
	ld	e,l
	inc	de
	ld	bc,0ffh
	ldir
	pop	bc
	pop	hl
	pop	de
	sub	a
	ret
rddun2:	ld	a,d
	sub	(iy+9)
	jr	nz,rddun1
	add	a,6		;"Attempted to read system data record"
	ret
rddun1:	sub	a
	ret
;*=*=*
vrsec:				;Read/verify -- we don't bother reading
	ld	a,(iy+6)	;Get high cyl #
	cp	d		;At or below it?
	jr	nc,rddun2	;Go if so
	ld	a,2		;"Seek error during read"
	ret			;NZ already set
;*=*=*
;       On RDSEC and VRSEC, if the read referenced the
;       directory cylinder and was successful,
;       then you need to return an error code 6. A floppy
;       disk controller will provide the indicated status.
;       Hard disk users may have to compare the requested
;       cylinder to DIRCYL in the DCT.
;*=*=*
rdtrk:  ld	a,32		;Not supported ("Illegal drive number")
        and	a            
	ret
;*=*=*
wrcmd:  bit     7,(iy+3)        ;Check for software write protect
	jr      z,wrcmd1        ;Transfer if no soft WP
	ld      a,15            ;Set "Write protected disk" error
	ret
wrcmd1: cp	14              ;Now parse functions 12-15
	jr	z,wrssc
	jr	nc,wrtrk
	cp	13
	jr	z,wrsec
;*=*=*
hdfmt:                 		;Low-level format (=erase)
	ld	(iy+9),0ffh	;Invalidate directory cylinder
	push	de
	push	hl
	ld	e,(ix)		;Get fd
	ld	d,(ix+1)
exists:	ld	hl,256		;Truncate file to just the header
	ld	(offset+1),hl
rx07	equ	$-2
	ld	hl,offset
rx08	equ	$-2
	defw	emt_ftruncate
creatd:	pop	hl
	pop	de
	ld	a,14		;"Write fault on disk drive"
	ret	nz
	sub	a
	ret
;*=*=*
wrsec:				;Write with X'FB' data address mark
	ld	a,d		;Check if writing track 0, sector 0
	or	e
	call	z,setdir	;Set directory cyl in Reed header
rx20	equ	$-2
;*=*=*
wrssc:				;Write with X'F8' data address mark
	ld	a,(iy+6)	;Get high cyl #
	cp	d		;Beyond it?
	jr	nc,wrok1
	ld	a,10		;"Seek error during write"
	ret			;NZ already set
;*=*=*
wrok1:  push	de
	push	hl
	call	doseek
rx04	equ	$-2
	pop	hl
	ld	a,13		;"Data record not found during write"
	jr	nz,wrdun
	ld	bc,256
	defw	emt_write
	ld	a,12		;"Parity error during write"
wrdun:	pop	de
	ret	nz
	sub	a
	ret
;*=*=*
wrtrk:  ld	a,32		;Write track
        and	a               ;Not supported ("Illegal drive number")
	ret
;*=*=*
;	Perform lseek before r/w
;*=*=*
doseek: sub	a		;sec/cyl to hl, xlate 0 to 256
	ld	h,a
	add	a,(iy+5)
	ld	l,a
	jr	nz,noinc
	inc	h
noinc:	ld	c,d		;cyl# to c
	ld	b,e		;sec# to b
	ld	a,c		;model I/III call uses a, not c
domul:	ld	a,@mul16	;hla = hl * c, smash de
	rst	40
	ld	d,h		;sec# to de (h is 0)
	ld	e,b
	ld	h,l		;product to hl
	ld	l,a
	inc	hl		;add 1 extra for header
	add	hl,de
	ex	de,hl		;offset to de
	ld	hl,offset+2
rx15	equ	$-2
	ld	(hl),d
	dec	hl
	ld	(hl),e
	dec	hl
	ld	bc,0
	ld	(hl),c
	ld	e,(ix)		;Get fd
	ld	d,(ix+1)
	defw	emt_lseek
	ret

;*=*=*
;	Open file and read geometry if needed, and
;       get address of correct fd to ix.
;*=*=*
ckopen: ld	ix,fd		;Compute fd address
rx02	equ	$-2
	push	de
	ld	d,0
	ld	a,(iy+4)
	and	0fh
	rlca
	ld	e,a
	add	ix,de
	pop	de
	ld	a,(ix)		;fd == -1?
	and	(ix+1)
	inc	a
	jr	z,doopen
	sub	a
	ret
doopen:	push	de
	push	bc
	push	hl
	ld	bc,EO_RDWR	;Prepare to open
	ld	de,0666o	;mode
	ld	hl,hard_	;name
rx05	equ	$-2
	ld	a,(iy+4)
	and	0fh
	add	a,'0'
	ld	(hadr),a
rx09	equ	$-2
	defw	emt_opendisk
	ld	(ix),e	
	ld	(ix+1),d
	jr	nz,opnerr
	ld	hl,offset	;Prepare to read geometry
rx13	equ	$-2
	ld	(hl),28		;offset to cyl/sec/gran params
	ld	bc,0
	ld	(offset+1),bc
rx14	equ	$-2
	defw	emt_lseek
	jr	nz,opnerr
	ld	bc,3		;length
	defw	emt_read	;use offset buffer
	jr	nz,opnerr
	ld	a,(hl)		;cyl
	dec	a
	ld	(iy+6),a	;max cylinder
	inc	hl
	ld	b,(hl)		;sec
	ld	(iy+5),b
	inc	hl
	ld	c,(hl)		;gran
	ld	e,b		;compute sec/gran
	ld	a,c		;model I/III call uses a, not c
dodiv:	ld	a,@div8		;a = e / c, e = e % c
	rst	40              ;remainder mbz, but we don't check here
	dec	a
	dec	c
	rrc	c
	rrc	c
	rrc	c
	or	c
	ld	(iy+7),a	;heads, secs per track
	ld	(iy+8),a	;grans, secs per gran
	ld	(iy+9),0ffh	;dircyl unknown
	sub	a		;no error
opnerr:	pop	hl
	pop	bc
	pop	de
	ret
;*=*=*
;	Sleazy trick to update dir cyl in Reed header: do
;	it whenever track 0, sector 0 is written.
;	Only important if sharing images with Reed emulator.
;*=*=*
setdir: push	hl
	push	bc
	push	de
	ld	hl,offset
rx18	equ	$-2
	ld	b,d		;de is known to be 0 here
	ld	c,e
	ld	e,(ix)		;Get fd
	ld	d,(ix+1)
	ld	(hl),31		;offset to dir cyl param
	ld	(offset+1),bc
rx19	equ	$-2
	defw	emt_lseek
	ld	a,(iy+9)	;dir cyl value to write
	ld	(hl),a
	inc	bc
	defw	emt_write
	pop	de		;cheat, ignore errors
	pop	bc
	pop	hl
	ret
;*=*=*
;	Boot-time initialization
;*=*=*
dvrcfg:	ld	de,-1		;@ICNFG chains in here
	defw	emt_closedisk	;close any files left from before reboot
	call	dvrini
rx11	equ	$-2	
link:	defb	'Tim'		;Replaced by next link in @ICNFG chain
dvrini:	ld	hl,fd
rx12	equ	$-2
	ld	d,h
	ld	e,l
	inc	de
	ld	(hl),0ffh
	ld	bc,ndrive*2-1
	ldir
	ret
;*=*=*
;	Disk name: hardM-N for model M (1,3,4), number N (0-7)
;*=*=*
hard_:	defb	'hard'
hmod:   defb	'1-'
hadr:	defb	'0',0

dvrend  equ     $-1
length  equ     $-entry
reltab: defw    rx00,rx01,rx02,rx03,rx04,rx05,rx06,rx07,rx08,rx09
	defw	rx10,rx11,rx12,rx13,rx14,rx15,rx16,rx17,rx18,rx19
	defw	rx20,0
	end     instal
