;
;=======================================================================
; HBIOS CPU Speed Selection Tool
;=======================================================================
;
; Simple utility that sets CPU speed on RomWBW systems that support
; software speed selection.
;
;=======================================================================
;
#include "../../HBIOS/hbios.inc"
;
#include "../../ver.inc"
;
; General operational equates (should not requre adjustment)
;
stksiz		.equ	$40		; Working stack size
;
cpumhz		.equ	30		; for time delay calculations (not critical)
;
rtc_port	.equ	$70		; RTC latch port adr
;
restart		.equ	$0000		; CP/M restart vector
bdos		.equ	$0005		; BDOS invocation vector
;
bf_sysreset	.equ	$F0		; restart system
;
bf_sysres_int	.equ	$00		; reset hbios internal
bf_sysres_warm	.equ	$01		; warm start (restart boot loader)
bf_sysres_cold	.equ	$02		; cold start
;
ident		.equ	$FFFE		; loc of RomWBW HBIOS ident ptr
;
;=======================================================================
;
	.org	$100	; standard CP/M executable
;
;
	; setup stack (save old value)
	ld	(stksav),sp		; save stack
	ld	sp,stack		; set new stack
;
	call	crlf
	ld	de,str_banner		; banner
	call	prtstr
;
	; initialization
	call	init			; initialize
	jr	nz,exit			; abort if init fails
;
	call	main			; do the real work
;
exit:
	; clean up and return to command processor
	call	crlf			; formatting
	ld	sp,(stksav)		; restore stack
	jp	restart			; return to CP/M via restart
;
;
;=======================================================================
; Main Program
;=======================================================================
;
;
; Initialization
;
init:
	; check for UNA (UBIOS)
	ld	a,($FFFD)	; fixed location of UNA API vector
	cp	$C3		; jp instruction?
	jr	nz,initwbw	; if not, not UNA
	ld	hl,($FFFE)	; get jp address
	ld	a,(hl)		; get byte at target address
	cp	$FD		; first byte of UNA push ix instruction
	jr	nz,initwbw	; if not, not UNA
	inc	hl		; point to next byte
	ld	a,(hl)		; get next byte
	cp	$E5		; second byte of UNA push ix instruction
	jr	nz,initwbw	; if not, not UNA
	jp	err_una		; UNA not supported
;
initwbw:
	; get location of config data and verify integrity
	ld	hl,(ident)	; HL := adr or RomWBW HBIOS ident
	ld	a,(hl)		; get first byte of RomWBW marker
	cp	'W'		; match?
	jp	nz,err_inv	; abort with invalid config block
	inc	hl		; next byte (marker byte 2)
	ld	a,(hl)		; load it
	cp	~'W'		; match?
	jp	nz,err_inv	; abort with invalid config block
	inc	hl		; next byte (major/minor version)
	ld	a,(hl)		; load it
	cp	rmj << 4 | rmn	; match?
	jp	nz,err_ver	; abort with invalid os version
;
initz:
	; initialization complete
	xor	a		; signal success
	ret			; return
;
;
;
main:
	; skip to start of first parm
	ld	ix,$81		; point to start of parm area (past len byte)
	call	nonblank	; skip to next non-blank char
	jp	z,show_spd	; no parms, show current settings
;
main1:
	; process options (if any)
	cp	'/'		; option prefix?
	jr	nz,main2	; not an option, continue
	call	option		; process option
	ret	nz		; some options mean we are done (e.g., "/?")
	inc	ix		; skip option character
	call 	nonblank	; skip whitespace
	jr	main1		; continue option checking
;
main2:
	ret	z		; if end, nothing to do
	cp	'W'		; warm boot?
	jp	z,wboot		; if so, do it
	cp	'C'		; cold boot?
	jp	z,cboot		; if so, do it
	cp	','		; no new speed?
	jr	z,main2a	; go to wait states
	; parse speed string (half, full, double)
	call	getalpha	; extract speed ("HALF", "FULL", "DOUBLE")
	call	parse_spd	; parse to numeric
	jp	c,err_parm	; if invalid, abort
	ld	(new_cpu_spd),a	; save it
	call	nonblank	; skip whitespace
	jp	z,set_spd	; if nothing else, set new speed
	cp	','		; parm separator
	jp	nz,err_parm	; invalid format, show usage and abort
main2a:
	inc	ix		; pass separator
	call	nonblank	; skip whitespace
	jp	z,set_spd	; if nothing else, set new speed
	call	isnum		; start of parm?
	jr	c,main3		; nope, try skipping this parm
	call	getnum		; get memory wait states
	jp	c,err_parm	; if overflow, show usage and abort
	ld	(new_ws_mem),a	; save memory wait states
;
main3:
	call	nonblank	; skip whitespace
	jp	z,set_spd	; if nothing else, set new speed
	cp	','		; parm separator
	jp	nz,err_parm	; invalid format, show usage and abort
	inc	ix		; pass separator
	call	nonblank	; skip whitespace
	jp	z,set_spd	; if nothing else, set new speed
	call	getnum		; get I/O wait states
	jp	c,err_parm	; if overflow, show usage and abort
	ld	(new_ws_io),a	; save memory wait states
;
	call	nonblank	; skip whitespace
	jp	nz,err_parm	; invalid format, show usage and abort
	jp	set_spd		; set new speed and return
;
parse_spd:
	ld	a,(tmpstr)		; first byte of string
	ld	c,0			; assume half speed
	cp	'H'			; check it
	jr	z,parse_spd1		; if equal, done
	ld	c,1			; assume full speed
	cp	'F'			; check it
	jr	z,parse_spd1		; if equal, done
	ld	c,2			; assume double speed
	cp	'D'			; check it
	jr	z,parse_spd1		; if equal, done
	ld	c,3			; assume quad speed
	cp	'Q'			; check it
	jr	z,parse_spd1		; if equal, done
	or	a			; clear CF
	ccf				; set CF to indicate error
	ret
parse_spd1:
	ld	a,c			; result to a
	or	a			; clear CF
	ret
;
set_spd:
	call	delay
	ld	b,BF_SYSSET
	ld	c,BF_SYSSET_CPUSPD
	ld	a,(new_cpu_spd)
	ld	l,a
	ld	a,(new_ws_mem)
	ld	d,a
	ld	a,(new_ws_io)
	ld	e,a
	rst	08
	jp	nz,err_not_sup
	call	show_spd
	xor	a
	ret
;
show_spd:
	ld	b,BF_SYSGET
	ld	c,BF_SYSGET_CPUINFO
	rst	08
	jp	nz,err_api
	call	crlf2
	push	de			; save CPU speed for now
	push	bc			; Oscillator speed to HL
	pop	hl
	ld	de,str_spacer
	call	prtstr
	call	prtd3m			; print it
	ld	de,str_oscspd
	call	prtstr
	call	crlf
	ld	de,str_cpuspd
	call	prtstr
	pop	hl			; recover CPU speed
	call	prtd3m			; print it
	ld	de,str_mhz
	call	prtstr
;
	ld	b,BF_SYSGET
	ld	c,BF_SYSGET_CPUSPD
	rst	08
	ret	nz			; no CPU speed info, done
	push	de			; save wait states for now
	ld	a,l
	ld	de,str_slow
	cp	0
	jr	z,show_spd1
	ld	de,str_full
	cp	1
	jr	z,show_spd1
	ld	de,str_dbl
	cp	2
	jr	z,show_spd1
	ld	de,str_quad
	cp	3
	jr	z,show_spd1
	jp	err_invalid
show_spd1:
	call	prtstr
	pop	hl
	ld	a,h			; memory wait states
	cp	$FF
	jr	z,show_spd2
	call	crlf
	ld	de,str_spacer
	call	prtstr
	call	prtdecb
	ld	de,str_memws
	call	prtstr
;
show_spd2:
	ld	a,l
	cp	$FF
	jr	z,show_spd3
	call	crlf
	ld	de,str_spacer
	call	prtstr
	call	prtdecb
	ld	de,str_iows
	call	prtstr
;
show_spd3:
	ret
;
; Handle special options
;
option:
;
	inc	ix		; next char
	ld	a,(ix)		; get it
	cp	'?'		; is it a '?' as expected?
	jp	z,usage		; yes, display usage
	jp	err_parm	; anything else is an error

usage:
	call	crlf2
	ld	de,str_usage
	call	prtstr
	or	$FF
	ret
;
; Handle Warm Boot
;
wboot:
	ld	de,str_warmboot		; message
	call	prtstr			; display it
	ld	b,bf_sysreset		; system restart
	ld	c,bf_sysres_warm	; warm start
	call	$fff0			; call hbios
;
; Handle Cold Boot
;
cboot:
	ld	de,str_coldboot		; message
	call	prtstr			; display it
	ld	b,bf_sysreset		; system restart
	ld	c,bf_sysres_cold	; cold start
	call	$fff0			; call hbios
;
; Error Handlers
;
err_una:
	ld	de,str_err_una
	jr	err_ret
err_inv:
	ld	de,str_err_inv
	jr	err_ret
err_ver:
	ld	de,str_err_ver
	jr	err_ret
err_parm:
	ld	de,str_err_parm
	jr	err_ret
err_not_sup:
	ld	de,str_err_not_sup
	jr	err_ret
err_invalid:
	ld	de,str_err_invalid
	jr	err_ret
err_api:
	ld	de,str_err_api
	jr	err_ret
;
err_ret:
	call	crlf2
	call	prtstr
	or	$FF			; signal error
	ret
;
;=======================================================================
; Utility Routines
;=======================================================================
;
;
; Print character in A without destroying any registers
;
prtchr:
	push	af
	push	bc		; save registers
	push	de
	push	hl
	ld	e,a		; character to print in E
	ld	c,$02		; BDOS function to output a character
	call	bdos		; do it
	pop	hl		; restore registers
	pop	de
	pop	bc
	pop	af
	ret
;
; Print a dot character without destroying any registers
;
prtdot:
	; shortcut to print a dot preserving all regs
	push	af		; save af
	ld	a,'.'		; load dot char
	call	prtchr		; print it
	pop	af		; restore af
	ret			; done
;
; Print a zero terminated string at (de) without destroying any registers
;
prtstr:
	push	af
	push	de
;
prtstr1:
	ld	a,(de)		; get next char
	or	a
	jr	z,prtstr2
	call	prtchr
	inc	de
	jr	prtstr1
;
prtstr2:
	pop	de		; restore registers
	pop	af
	ret
;
; Print a hex value prefix "0x"
;
prthexpre:
	push	af
	ld	a,'0'
	call	prtchr
	ld	a,'x'
	call	prtchr
	pop	af
	ret
;
; Print the value in A in hex without destroying any registers
;
prthex:
	call	prthexpre
prthex1:
	push	af		; save AF
	push	de		; save DE
	call	hexascii	; convert value in A to hex chars in DE
	ld	a,d		; get the high order hex char
	call	prtchr		; print it
	ld	a,e		; get the low order hex char
	call	prtchr		; print it
	pop	de		; restore DE
	pop	af		; restore AF
	ret			; done
;
; print the hex word value in hl
;
prthexword:
	call	prthexpre
prthexword1:
	push	af
	ld	a,h
	call	prthex1
	ld	a,l
	call	prthex1 
	pop	af
	ret
;
; print the hex dword value in de:hl
;
prthex32:
	call	prthexpre
	push	bc
	push	de
	pop	bc
	call	prthexword1
	push	hl
	pop	bc
	call	prthexword1
	pop	bc
	ret
;
; Convert binary value in A to ascii hex characters in DE
;
hexascii:
	ld	d,a		; save A in D
	call	hexconv		; convert low nibble of A to hex
	ld	e,a		; save it in E
	ld	a,d		; get original value back
	rlca			; rotate high order nibble to low bits
	rlca
	rlca
	rlca
	call	hexconv		; convert nibble
	ld	d,a		; save it in D
	ret			; done
;
; Convert low nibble of A to ascii hex
;
hexconv:
	and	$0F	     	; low nibble only
	add	a,$90
	daa	
	adc	a,$40
	daa	
	ret
;
; Print value of A or HL in decimal with leading zero suppression
; Use prtdecb for A or prtdecw for HL
;
prtdecb:
	push	hl
	ld	h,0
	ld	l,a
	call	prtdecw		; print it
	pop	hl
	ret
;
prtdecw:
	push	af
	push	bc
	push	de
	push	hl
	call	prtdec0
	pop	hl
	pop	de
	pop	bc
	pop	af
	ret
;
prtdec0:
	ld	e,'0'
	ld	bc,-10000
	call	prtdec1
	ld	bc,-1000
	call	prtdec1
	ld	bc,-100
	call	prtdec1
	ld	c,-10
	call	prtdec1
	ld	e,0
	ld	c,-1
prtdec1:
	ld	a,'0' - 1
prtdec2:
	inc	a
	add	hl,bc
	jr	c,prtdec2
	sbc	hl,bc
	cp	e
	ret	z
	ld	e,0
	call	prtchr
	ret
;
; Print value of HL as thousandths, ie. 0.000
;
prtd3m:
	push	bc
	push	de
	push	hl
	ld	e,'0'
	ld	bc,-10000
	call	prtd3m1
	ld	e,0
	ld	bc,-1000
	call	prtd3m1
	call	prtdot
	ld	bc,-100
	call	prtd3m1
	ld	c,-10
	call	prtd3m1
	ld	c,-1
	call	prtd3m1
	pop	hl
	pop	de
	pop	bc
	ret
prtd3m1:
	ld	a,'0' - 1
prtd3m2:
	inc	a
	add	hl,bc
	jr	c,prtd3m2
	sbc	hl,bc
	cp	e
	jr	z,prtd3m3
	ld	e,0
	call	prtchr
prtd3m3:
	ret
;
; Get the next non-blank character from (HL).
;
nonblank:
	ld	a,(ix)		; load next character
	or	a		; string ends with a null
	ret	z		; if null, return pointing to null
	cp	' '		; check for blank
	ret	nz		; return if not blank
	inc	ix		; if blank, increment character pointer
	jr	nonblank	; and loop
;
; Get alpha chars and save in tmpstr
; Length of string returned in A
;
getalpha:
;
	ld	hl,tmpstr	; location to save chars
	ld	b,8		; length counter (tmpstr max chars)
	ld	c,0		; init character counter
;
getalpha1:
	ld	a,(ix)		; get active char
	call	ucase		; lower case -> uppper case, if needed
	cp	'A'		; check for start of alpha range
	jr	c,getalpha2	; not alpha, get out
	cp	'Z' + 1		; check for end of alpha range
	jr	nc,getalpha2	; not alpha, get out
	; handle alpha char
	ld	(hl),a		; save it
	inc	c		; bump char count
	inc	hl		; inc string pointer
	inc	ix		; increment buffer ptr
	djnz	getalpha1	; if space, loop for more chars
;
getalpha2:	; non-alpha, clean up and return
	ld	(hl),0		; terminate string
	ld	a,c		; string length to A
	or	a		; set flags
	ret			; and return
;
; Determine if byte in A is a numeric '0'-'9'
; Return with CF clear if it is numeric
;
isnum:
	cp	'0'
	jr	c,isnum1	; too low
	cp	'9' + 1
	jr	nc,isnum1	; too high
	or	a		; clear CF
	ret
isnum1:
	or	a		; clear CF
	ccf			; set CF
	ret
	
;
; Get numeric chars and convert to number returned in A
; Carry flag set on overflow
;
getnum:
	ld	c,0		; C is working register
getnum1:
	ld	a,(ix)		; get the active char
	cp	'0'		; compare to ascii '0'
	jr	c,getnum2	; abort if below
	cp	'9' + 1		; compare to ascii '9'
	jr	nc,getnum2	; abort if above
;
	; valid digit, add new digit to C
	ld	a,c		; get working value to A
	rlca			; multiply by 10
	ret	c		; overflow, return with carry set
	rlca			; ...
	ret	c		; overflow, return with carry set
	add	a,c		; ...
	ret	c		; overflow, return with carry set
	rlca			; ...
	ret	c		; overflow, return with carry set
	ld	c,a		; back to C
	ld	a,(ix)		; get new digit
	sub	'0'		; make binary
	add	a,c		; add in working value
	ret	c		; overflow, return with carry set
	ld	c,a		; back to C
;
	inc	ix		; bump to next char
	jr	getnum1		; loop
;
getnum2:	; return result
	ld	a,c		; return result in A
	or	a		; with flags set, CF is cleared
	ret
;
; Start a new line
;
crlf2:
	call	crlf		; two of them
crlf:
	push	af		; preserve AF
	ld	a,13		; <CR>
	call	prtchr		; print it
	ld	a,10		; <LF>
	call	prtchr		; print it
	pop	af		; restore AF
	ret
;
; Convert character in A to uppercase
;
ucase:
	cp	'a'		; if below 'a'
	ret	c		; ... do nothing and return
	cp	'z' + 1		; if above 'z'
	ret	nc		; ... do nothing and return
	res	5,a		; clear bit 5 to make lower case -> upper case
	ret			; and return
;
; Add hl,a
;
;   A register is destroyed!
;
addhla:
	add	a,l
	ld	l,a
	ret	nc
	inc	h
	ret
;
; Delay ~10ms
;
delay:
	push	af
	push	de
	ld	de,625			; 10000us/16us
delay0:
	ld	a,cpumhz - 2
delay1:
	dec	a
	jr	nz,delay1
	dec	de
	ld	a,d
	or	e
	jp	nz,delay0
	pop	de
	pop	af
	ret
;
;
;=======================================================================
; Constants
;=======================================================================
;
str_banner		.db	"RomWBW CPU Speed Selector v1.0, 11-Sep-2024",0
str_spacer		.db	"  ",0
str_oscspd		.db	" MHz Oscillator",0
str_cpuspd		.db	"  CPU speed is ",0
str_mhz			.db	" MHz",0

str_slow		.db	" (Half)",0
str_full		.db	" (Full)",0
str_dbl			.db	" (Double)",0
str_quad		.db	" (Quad)",0
str_memws		.db	" Memory Wait State(s)",0
str_iows		.db	" I/O Wait State(s)",0
str_warmboot		.db	"\r\n\r\nWarm booting...",0
str_coldboot		.db	"\r\n\r\nCold booting...",0
str_err_una		.db	"  ERROR: UNA not supported by application",0
str_err_inv		.db	"  ERROR: Invalid BIOS (signature missing)",0
str_err_ver		.db	"  ERROR: Unexpected HBIOS version",0
str_err_parm		.db	"  ERROR: Parameter error (CPUSPD /? for usage)",0
str_err_not_sup		.db	"  ERROR: Platform or configuration does not support CPU speed configuration!",0
str_err_invalid		.db	"  ERROR: Invalid configuration!",0
str_err_api		.db	"  ERROR: HBIOS API error!",0
str_usage		.db	"  Usage: CPUSPD <cpuspd>,<memws>,<iows>\r\n"
			.db	"         CPUSPD (W)armBoot\r\n"
			.db	"         CPUSPD (C)oldBoot\r\n"
			.db	"\r\n"
			.db	"         <cpuspd>: (H)alf | (F)ull | (D)ouble | (Q)uad\r\n"
			.db	"         <memws>:  Memory wait states\r\n"
			.db	"         <iows>:   I/O wait states\r\n"
			.db	"\r\n"
			.db	"         Any parameter may be omitted\r\n"
			.db	"         Ability to set values varies by system\r\n",0
;
;=======================================================================
; Working data
;=======================================================================
;
stksav		.dw	0		; stack pointer saved at start
		.fill	stksiz,0	; stack
stack		.equ	$		; stack top
;
;
tmpstr		.fill	9,0		; temp string (8 chars, 0 term)
new_cpu_spd	.db	$FF		; new CPU speed
new_ws_mem	.db	$FF		; new memory wait states
new_ws_io	.db	$FF		; new I/O wait states


;
;=======================================================================
;
	.end