;=========================================================================
; cpu.inc - Detect and print CPU type and FPU presence
;-------------------------------------------------------------------------
;
; Compiles with NASM 2.13.02, might work with other versions
;
; Copyright (C) 2010 - 2023 Sergey Kiselev.
; Provided for hobbyist use on the Xi 8088 and Micro 8088 boards.
;
; This program is free software: you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation, either version 3 of the License, or
; (at your option) any later version.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
; GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program.  If not, see <http://www.gnu.org/licenses/>.
;
;=========================================================================

flags_tf	equ	0100h		; TF flag - enables single-step

;=========================================================================
; detect_cpu - detect and print CPU type
; Note:
;	- Designed to detect 8088, 80C88, and V20 CPU. Likely will return
;	  incorrect results if used on other CPU types.
;-------------------------------------------------------------------------
detect_cpu:
	push	ax
	push	si
	mov	al,e_cpu_detect
	out	post_reg,al

	mov	si,msg_cpu
	call	print

; test for NEC V20
	mov	ax,0101h
	aad	10h			; NEC V20 ignores the argument
	cmp	al,0Bh			; and always does AL = AL * 0Ah + AH
	je	.nec_v20

; not NEC V20, test for various 8088 versions
.i8088:					; 8088 - NMOS version of 8088
	cli				; disable interrupts
	push	ds
	xor	ax,ax
	mov	ds,ax			; interrupt table segment
	mov	ax,.int_01		; new int 01h offset
	xchg	ax,word [4]		; switch with original int 1 offset
	push	ax			; save to stack
	mov	ax,cs			; new int 01h segment
	xchg	ax,word [6]		; switch with original int 1 segment
	push	ax			; save to stack
	pushf				; push flags to stack (save)
	pushf				; push flags to stack (modify)
	pop	ax			; and move them to AX
	or	ax,flags_tf		; set TF
	push	ax
	xor	ax,ax			; AX = 0
	popf				; load modified value to FLAGS register
					; enable single-step
	nop
	push	ds			; Harris 80C88 disables interrupts
					; after PUSH SREG instructions
.push_sreg:				; and will not single-step to here
	pop	ds			; 1981 and newer 8088 disable interrupts
					; after modifying segment registers
.pop_sreg:				; and will not single-step to here
	nop
	popf				; restore original FLAGS, disable
					; single-step

	pop	si			; get original int 01h segment
	mov	word [6],si		; save it to interrupt table
	pop	si			; get original int 01h offset
	mov	word [4],si		; save it to interrupt table
	pop	ds
	sti				; enable interrupts
	cmp	ax,0001h		; if AX = 0001h - we have INTEL (C) 1978
	jz	.i8088_78

	cmp	ax,0002h
	jz	.i8088_81

.harris:				; Harris / Intersil 80C88
	mov	si,msg_cpu_harris
	call	print
	jmp	.cpu_freq

.i8088_78:
	mov	si,msg_cpu_8088_78
	call	print
	jmp	.cpu_freq

.i8088_81:
	mov	si,msg_cpu_8088_81
	call	print
	jmp	.cpu_freq

.nec_v20:
%ifdef MACHINE_HOMEBREW8088
; Test for NEC V40
; NEC V40 has on-chip configuration registers at 0FFE0h-0FFFFh I/0 addresses.
; The code below tries to modify the value of 0FFFE0 register and read it back.
; If the value is stored, it is assumed to be a NEC V40 CPU
	push	dx
	mov	dx,0FFE0h	; Use 0FFFE0h register for V40 tests
	in	al,dx		; read the original value
	mov	ah,al		; store the orginal value in AH
	xor	al,0FFh		; invert all the bits
	out	dx,al		; write the inverted value
	in	al,dx		; read it back
	xor	al,0FFh		; invert all the bits again
	cmp	al,ah		; compare with the original value
	jne	.not_v40
	out	dx,al		; write the original value
	in	al,dx		; read it back again
	cmp	al,ah		; compare with the original value
	jne	.not_v40
	pop	dx
	mov	si,msg_cpu_nec_v40
	call	print
	jmp	.cpu_freq

.not_v40:
	pop	dx
%endif ; MACHINE_HOMEBREW8088
	mov	si,msg_cpu_nec_v20
	call	print

.cpu_freq:

%ifdef TURBO_MODE
%ifdef BIOS_SETUP
; print CPU clock frequency

	mov	si,msg_at
	call	print

	push	ax
	call	get_config_a		; read BIOS configuration byte A
	call	print_cpu_clk
	pop	ax

%endif ; BIOS_SETUP
%endif ; TURBO_MODE

	mov	si,msg_crlf
	call	print

	cmp	ax,0001h		; if AX = 0001h - we have INTEL (C) 1978
	jnz	.exit
	mov	si,msg_cpu_bug
	call	print



.exit:
%ifdef MACHINE_HOMEBREW8088
	call	.estimate_clock
%endif ; MACHINE_HOMEBREW8088
	pop	si
	pop	ax
	ret

%ifdef MACHINE_HOMEBREW8088
; Estimate CPU clock frequency
; Algorithm:
; Run a cycle that's setup + a loop of 100 "AAA".
; That takes N ticks of the timer, and it includes N1 of actual operations + N2 wait-states from loading the instructions.
; Then run similar setup + a loop of 100 "AAM".  That takes M ticks, which is M1 actual operations + N2 wait states.
; So M - N will remove the "wait state" factor and be how many ticks it took to do "the difference in work between 100 AAAs and 100 AAMs"
; Since that's a fixed number of CPU cycles, the M-N value is an inverse of the CPU clock and we can divide by a constant (determined experimentally) to get a reasonable MHz figure
; independent of wait-states.  Note this does not compensate for external variations (i. e. DRAM refresh or a device that seizes the bus)
; It's not hyper-accurate-- it's often off by a few hundred KHz, and it can vary from run to run due to timer noise, but it should be enough to differentiate the major speed grades
; A further possible cleanup would be to "bin" the results-- anything below 5MHz as 4.77, anything 7.5-8.5 as 8, etc.
.estimate_clock:
	push	dx
	push	ax
	push	cx
	push	bx
	push	si
	mov	si,msg_est
	cli				; Disable interrupts for better accuracy
	call	print

	call	io_wait_latch
	push	ax
	xor	ax,ax
; Must be unrolled or it behaves inconsistently on different waitstates IME
	%rep 100
	aam
	%endrep
	call	io_wait_latch
	sti
	pop	bx
	cmp	ax,bx
	jae	.second_scan_is_bigger_simple
	sub	bx,ax
	jmp	.sub_done_simple

.second_scan_is_bigger_simple:
	mov	cx,0FFFFh
	sub	cx,ax
	add	bx,cx

.sub_done_simple:
	push	bx	; Save the runtime for the faster instruction
	cli
	call	io_wait_latch
	push	ax
	xor	ax,ax
	%rep 100
	mul	cl
	%endrep
	call	io_wait_latch
	sti
	pop	bx
	cmp	ax,bx
	jae	.second_scan_is_bigger
	sub	bx,ax
	jmp	.sub_done

.second_scan_is_bigger:
	mov	cx,0FFFFh
	sub	cx,ax
	add	bx,cx

.sub_done:
	pop	ax	; pull the count from the simple run
	sub	bx,ax
	; We'd expect BX to be the amount of clock ticks corresponding to (some large number) more cycles


;	mov ax,bx			; Refinement tool:  Display the number of clock ticks the loop generated.
;	call	print_dec		; Uncomment this code and run it on your machine and it will say things like "244 ticks ~ 8.00MHz"
;	mov	si,msg_ticks   ; Calculate (number of ticks) * 100 * (CPU actual MHz)
;	call	print			; and place it in DX:AX as the "clock divider" for the CPU type you're using in the code below
;	mov	ax,0E20h
;	int	10h

	mov	ax,0101h		; Repeat the NEC CPU test
	aad	10h			; NEC V20 ignores the argument
	cmp	al,0Bh			; and always does AL = AL * 0Ah + AH
	jne	.clock_divider_8088
	mov	dx,03h			; "Clock Divider" for NEC CPUs.  Value was based on an 10MHz V40: ~214 ticks, so the expected value is 1000*214 = 214000 = 343F0H
	mov	ax,43F0h		; On the V-series, AAM is faster than MUL (by quite a bit), so a large difference for the clock speed is expected.
	jmp	.clock_divider_set

.clock_divider_8088:
	neg	bx			; "Clock Divider" for 8088 CPUs.  Value was based on a 4.77MHz 8088: It shows as 65186 ticks, but on an 8088, MUL is faster than AAM
	mov	dx,0002h		; This means that BX is negative here, so flip the sign and we get 349 ticks... 349 ticks * 477  = 166473 = 28A49H.
	mov	ax,8A49h

.clock_divider_set:
	div	bx
	mov	bx,100			; Split to integer and fractional parts
	div	bl

	mov	bx,ax
	xor	ah,ah
	call	print_dec		; Integer part
	mov 	ax,0E2Eh		; decimal point
	int	10h
	xor	ah,ah
	mov	al,bh
	cmp	al,10
	jae	.two_digits
	push	ax
	mov	ax,0E30h		; leading zero
	int	10h
	pop	ax

.two_digits:
	call	print_dec
	mov	si,msg_mhz
	call	print
	mov	si,msg_crlf
	call	print
	pop	dx
	pop	si
	pop	bx
	pop	cx
	pop	ax
	ret
%endif

; stack frame after "push bp"
; BP - word [BP]
; IP - word [BP+2]
; CS - word [BP+4]
; FLAGS - word [BP+6]
.int_01:
	push	bp
	mov	bp,sp
	cmp	word [bp+2],.pop_sreg	; if IP = .pop_sreg we have '78 CPU
	jne	.1
	mov	al,01h			; (C) INTEL '79
	jmp	.int_01_exit
.1:
	cmp	word [bp+2],.push_sreg	; if IP = .push_sreg it is not Harris
	jne	.int_01_exit
	mov	al,02h			; not Harris / Intersil 80C88
.int_01_exit:
	pop	bp
	iret

;=========================================================================
; detect_fpu - detect and print FPU (mathematics coprocessor) presence
;-------------------------------------------------------------------------
test_word	equ	03FEh		; right at the end of interrupt table
detect_fpu:
	push	ax
	push	cx
	push	si
	mov	al,e_fpu_detect
	out	post_reg,al

	mov	si,msg_fpu
	call	print
	fninit				; initialize coprocessor
	mov	cx,3
.wait:
	loop	.wait			; wait for coprocessor to initialize
					;   (can't use fwait, since it could
					;   be no coprocessor at all)
	mov	cx,word [test_word]	; save test_word to CX
	mov	word [test_word],0000h	; clear space for control word
	fnstcw	word [test_word]	; store control word
	cmp	word [test_word],03FFh	; 8087 sets control word to 03FFh
	jne	.no_fpu			;   after finit

.fpu:
	mov	si,msg_fpu_present
	call	print
	or	word [equipment_list],equip_fpu ; update equipment word
	jmp	.exit

.no_fpu:

	mov	si,msg_absent
	call	print

.exit:
	mov	word [test_word],cx	; restore initial value of test_word

	pop	si
	pop	cx
	pop	ax
	ret

%ifdef MACHINE_FE2010A
;=========================================================================
; detect_chipset - detect and print chipset type
; Input:
;	none
; Output:
;	none
; Note: This subroutine also disables DRAM refresh on both FE2010A and PT8010AF
;-------------------------------------------------------------------------
detect_chipset:
	push	ax
	push	si
	mov	si,msg_chipset
	call	print
	mov	si,msg_fe2010a
	mov	al,54h			; channel 1, LSB only, mode 2, binary
	out	pit_ctl_reg,al		; used for DRAM refresh on IBM PC/XT/AT
	mov	al,12h			; or for delays (using port_b, bit 4)
        out     pit_ch1_reg,al		; pulse every 15ms
	mov	al,40h			; this disables refresh on FE2010A
	out	pit_ctl_reg,al		; but not on PT8010AF
	in	al,pit_ch1_reg
	cmp	al,12h			; should be 12h on FE2010A
	je	.print_chipset
	mov	si,msg_pt8010af

.print_chipset:
	call	print
	mov	al,54h			; channel 1, LSB only, mode 2, binary
	out	pit_ctl_reg,al		; this disables memory refresh
	pop	ax
	pop	si
	ret

%endif ; MACHINE_FE2010A

%ifdef TURBO_MODE
%ifdef BIOS_SETUP
;=========================================================================
; print_cpu_clk - Print CPU clock frequency
; Input:
;	AL - NVRAM CPU clock frequency configuration (bits 1:0)
;		0 - 4.77 MHz / Normal
;		1 - 7.16 MHz / Turbo
;		2 - 9.55 MHz (FE2010A)
; Output:
;	none
;-------------------------------------------------------------------------
print_cpu_clk:
	push	ax
	push	si
	and	al,nvram_trbo_mask	; get turbo bits
%ifdef MACHINE_FE2010A
	mov	si,msg_clk_7_16mhz
	cmp	al,1
	je	.print
	mov	si,msg_clk_9_55mhz
	cmp	al,2
	je	.print
%endif ; MACHINE_FE2010A
%ifdef MACHINE_XI8088
	mov	si,msg_clk_turbo
	cmp	al,1
	je	.print
%endif ; MACHINE_XI8088
	mov	si,msg_clk_4_77mhz

.print:
	call    print
	pop	si
	pop	ax
	ret
%endif ; BIOS_SETUP

;=========================================================================
; set_cpu_clk - Set CPU clock frequency
; Input:
;	AL - NVRAM CPU clock frequency configuration
;		0 - 4.77 MHz / Normal
;		1 - 7.16 MHz / Turbo
;		2 - 9.55 MHz (FE2010A)
; Output:
;	none
;-------------------------------------------------------------------------
set_cpu_clk:
%ifdef MACHINE_FE2010A
	cmp	al, 1 
	je	set_cpu_clk_7_16mhz
	cmp	al, 2
	je	set_cpu_clk_9_55mhz
	jmp	set_cpu_clk_4_77mhz   
%endif ; MACHINE_FE2010A
%ifdef MACHINE_XI8088
	cmp	al,1
	je	set_cpu_clk_turbo
	jmp	set_cpu_clk_4_77mhz   
%endif ; MACHINE_XI8088
	ret

;=========================================================================
; set_cpu_clk_4_77mhz - Set CPU clock at 4.77 MHz
; set_cpu_clk_7_16mhz - Set CPU clock at 7.16 MHz
; set_cpu_clk_9_55mhz - Set CPU clock at 9.55 MHz
; set_cpu_clk_turbo - Set CPU clock at turbo frequency (system dependent)
; Input:
;	none
; Output:
;	none
;-------------------------------------------------------------------------
%ifdef MACHINE_FE2010A
set_cpu_clk_4_77mhz:
	push ax
	mov	al,byte [equipment_list] ; get lower byte of the equipment list
	and	al,equip_fpu		; isolate FPU installed bit
	or	al,fe_par_disable	; disable memory parity checking
	out	fe_config_reg,al	; clear bits 7-5 - 4.77 MHz CPU clock
	pop ax
	ret

set_cpu_clk_7_16mhz:
	push ax
	call	get_config_a
	mov	ah,al
	mov	al,byte [equipment_list] ; get lower byte of the equipment list
	and	al,equip_fpu		; isolate FPU installed bit
	or	al,fe_par_disable	; disable memory parity checking
	or	al,fe_clk_7_norm	; set bit 6 - 7.15 MHz CPU clock
	test	ah,nvram_fast_ws	; nvram_fast_ws = 1 - Normal wait states
	jnz	.set_config
	or	al,fe_clk_7_fast

.set_config:
	out	fe_config_reg,al
	pop ax
	ret

set_cpu_clk_9_55mhz:
	push ax
	call	get_config_a
	mov	ah,al
	mov	al,byte [equipment_list] ; get lower byte of the equipment list
	and	al,equip_fpu		; isolate FPU installed bit
	or	al,fe_par_disable	; disable memory parity checking
	or	al,fe_clk_9_norm	; set bit 7 - 9.54 MHz CPU clock
	test	ah,nvram_fast_ws
	jnz	.set_config		; nvram_fast_ws = 1 - Normal wait states
	or	al,fe_clk_9_fast

.set_config:
	out	fe_config_reg,al
	pop ax
	ret
%endif ; MACHINE_FE2010A
%ifdef MACHINE_XI8088
set_cpu_clk_4_77mhz:
	push	ax
	in	al,ppi_pb_reg
	and	al,0FBh			; clear bit 2 (turbo enable bit)
	out	ppi_pb_reg,al
	pop	ax
	ret

set_cpu_clk_turbo:
	push	ax
	in	al,ppi_pb_reg
	or	al,04h			; set bit 2 (turbo enable bit)
	out	ppi_pb_reg,al
	pop	ax
	ret
%endif ; MACHINE_XI8088
%endif ; TURBO_MODE
