	title	CPUID - Determine CPU & NDP Type
	page	58,122
	name	CPUID

;
;	CPUID uniquely identifies each NEC & Intel CPU & NDP.
;
; Notes on program structure:
;
;	This program uses four segments, two classes, and one group.
;	It demonstrates a useful technique for programmers who generate
;	.COM programs.  In particular, it shows how to use segment
;	classes to re-order segments, and how to eliminate the linker's
;	warning message about the absence of a stack segment.
;
;	The correspondence between segments and classes is as follows:
;
;			Segment		Class
;			-------		-----
;			STACK		prog
;			DATA		data
;			MDATA		data
;			CODE		prog
;
;	The segments apprear in the above order in the program source
;	to avoid forward references in the CODE segment to labels in
;	the DATA/MDATA segments.  However, because the STACK segment
;	appears first in the file, it and all segments in the same
;	class are made contiguous by the linker.  Thus they precede
;	the DATA/MDATA segments in the resulting .COM file because
;	the latter are in a different class.  In this manner, although
;	DATA and MDATA precede CODE in the source file, their order
;	is swapped in the .COM file.  That way there is no need for
;	an initial skip over the data areas to get to the CODE
;	segment.  As a side benefit, declaring a STACK segment (as
;	the first segment in the source) also eliminates the linker's
;	warning about that segment missing.  Finally, all segments
;	are declared to be in the same group so the linker can properly
;	resolve offsets.
;
;	Note that if you re-assemble the code for any reason, it is
;	important to use an assembler later than the IBM version 1.0.
;	That version has a number of bugs including an annoying habit
;	of alphabetizing segment names in the .OBJ file.  If you use
;	IBM MASM 2.0, be sure to specify /S to order the segments
;	properly.
;
;	If the program reports results at variance with your knowledge
;	of the system, please contact the author.
;
; Environments tested in:
;
;			CPU Speed
;	System		 in MHz		CPU		NDP
;	------		---------	---		---
;	IBM PC AT	6		Intel 80286	Intel 80287
;	IBM PC AT	9		Intel 80286	Intel 80287
;	IBM PC AT	6		Intel 80286	none
;	IBM PC AT	8.5		Intel 80286	none
;	IBM PC		4.77		Intel 8088	Intel 8087-3
;	IBM PC		4.77		Intel 8088*	Intel 8087-3
;	IBM PC XT	4.77		Intel 8088	none
;	IBM PC XT	4.77		Intel 8088	Intel 8087-3
;	IBM PC Portable	4.77		NEC V20		none
;	COMPAQ		4.77		Intel 8088	none
;	COMPAQ		4.77		NEC V20		none
;	AT&T PC 6300	8		Intel 8086	Intel 8087-2
;	AT&T PC 6300	8		NEC V30		Intel 8087-2
;	Tandy 2000	8		Intel 80186	none
;
;	* = faulty CPU
;
; Program structure:
;
;	Group PGROUP:
;	Stack   segment STACK, byte-aligned, stack,  class 'prog'
;	Program segment CODE,  byte-aligned, public, class 'prog'
;	Data    segment DATA,  byte-aligned, public, class 'data'
;	Data    segment MDATA, byte-aligned, public, class 'data'
;
; Assembly requirements:
;
;	Use MASM 1.25 or later.
;	With IBM's MASM 2.0 only, use /S to avoid alphabetizing the
;		segment names.
;	Use /r option to generate real NDP code.
;
;	MASM CPUID/r;			to convert .ASM to .OBJ
;	LINK CPUID;			to convert .OBJ to .EXE
;	EXE2BIN CPUID CPUID.COM		to convert .EXE to .COM
;	ERASE CPUID.EXE			to avoid executing .EXE
;
;	Note that the linker doesn't warn about a missing stack segment.
;
; Author:
;
;	Original code by:	Bob Smith	May 1985
;				Qualitas, Inc.
;				8314 Thoreau Dr.
;				Bethesda, MD   20817
;
;	Arthur Zachai suggested the technique to distinguish within the
;	808x and 8018x families by exploiting the difference in the
;	length of their pre-fetch instruction queues.
;
;	Published in PC Tech Journal - April 1986 - Vol 4 No 4

	subttl	Structures, Records, Equates, & Macros
	page
ARG_STR	struc
	dw	?			; caller's bp
ARG_OFF	dw	?			; caller's offset
ARG_SEG	dw	?			;          segment
ARG_FLG	dw	?			;          flags
ARG_STR	ends

; Record to define bits in the CPU's & NDP's flags' registers

CPUFLAGS record	RO:1,NT:1,IOPL:2,OF:1,DF:1,IF:1,TF:1,SF:1,ZF:1,R1:1,AF:1,R2:1,PF:1,R3:1,CF:1

NDPFLAGS record	R4:3,IC:1,RC:2,PC:2,IEM:1,R5:1,PM:1,UM:1,OM:1,ZM:1,DM:1,IM:1

;	FLG_PIQL	Pre-fetch instruction queue length, 0 => 4-byte
;							    1 => 6-byte
;	FLG_08		Intel 808x
;	FLG_NEC		NEC V20 or V30
;	FLG_18		Intel 8018x
;	FLG_28		Intel 8028x
;	FLG_87		Intel 8087
;	FLG_287		Intel 80287
;
;	FLG_CERR	Faulty CPU
;	FLG_NERR	Faulty NDP switch setting

FLG	record	RSVD:9,FLG_NERR:1,FLG_CERR:1,FLG_NDP:2,FLG_CPU:3

; CPU-related flags

FLG_PIQL	equ	001b shl FLG_CPU
FLG_08		equ	000b shl FLG_CPU
FLG_NEC		equ	010b shl FLG_CPU
FLG_18		equ	100b shl FLG_CPU
FLG_28		equ	110b shl FLG_CPU

FLG_8088	equ	FLG_08
FLG_8086	equ	FLG_08 or FLG_PIQL
FLG_V20		equ	FLG_NEC
FLG_v30		equ	FLG_NEC or FLG_PIQL
FLG_80188	equ	FLG_18
FLG_80186	equ	FLG_18 or FLG_PIQL
FLG_80286	equ	FLG_28 or FLG_PIQL

; NDP-related flags

;			00b shl FLG_NDP		Not Present
FLG_87		equ	01b shl FLG_NDP
FLG_287	equ	10b shl FLG_NDP
BEL		equ	07h
LF		equ	0ah
CR		equ	0dh
EOS		equ	'$'

POPFF	macro
	local	L1,L2
	jmp	short L2		; skip over IRET
L1:
	iret				; pop the cs & ip pushed below along
					; with the flags, our original purpose
L2:
	push	cs			; prepare for IRET by pushing cs
	call	L1			; push ip, jump to IRET
	endm				; POPFF macro

TAB	macro	TYP
	push	bx			; save for a moment
	and	bx,mask FLG_&TYP	; isolate flags
	mov	cl,FLG_&TYP		; shift amount
	shr	bx,cl			; shift to low-order
	shl	bx,1			; times two to index table of words
	mov	dx,TYP&MSG_TAB[bx]	; ds:dx => descriptive message
	pop	bx			; restore
	mov	ah,09h			; function code to display string
	int	21h			; request dos service
	endm				; TAB macro
	page
INT_VEC	segment at 0			; start INT_VEC segment
		dd	?		; pointer to INT 00h
INT01_OFF	dw	?		; pointer to INT 01h
INT01_SEG	dw	?
INT_VEC	ends				; end INT_VEC segment

PGROUP	group	STACK,CODE,DATA,MDATA

; The following segment both positions class 'prog' segments lower in
; memory than others so the first byte of the resulting .COM file is
; in the CODE segment, as well as satisfies the LINKer's need to have
; a stack segment.

STACK	segment	byte stack 'prog'	; start STACK segment
STACK	ends				; end STACK segment

I11_REC	record	I11_PRN:2,I11_RSV1:2,I11_COM:3,I11_RSV2:1,I11_DISK:2,I11_VID:2,I11_RSV3:2,I11_NDP:1,I11_IPL:1

DATA	segment	byte public 'data'	; start DATA segment
	assume	ds:PGROUP

OLDINT01_VEC	label	dword		; save area for original INT 01h handler
OLDINT01_OFF	dw	?
OLDINT01_SEG	dw	?

NDP_CW		label	word		; save area for NDP control word
		db	?
NDP_CW_HI	db	0		; high byte of control word
NDP_ENV		dw	7 dup(?)	; save area for NDP environment

DATA	ends
	subttl	Message Data Area
	page
MDATA	segment	byte public 'data'	; start MDATA segment
	assume	ds:PGROUP

MSG_START	db	'CPUID -- Version 1.0'
		db	CR,LF,CR,LF,EOS
MSG_8088	db	'CPU is an Intel 8088.'
		db	CR,LF,EOS
MSG_8086	db	'CPU is an Intel 8086.'
		db	CR,LF,EOS
MSG_V20		db	'CPU is an NEC V20.'
		db	CR,LF,EOS
MSG_V30		db	'CPU is an NEC V30.'
		db	CR,LF,EOS
MSG_80188	db	'CPU is an Intel 80188.'
		db	CR,LF,EOS
MSG_80186	db	'CPU is an Intel 80186.'
		db	CR,LF,EOS
MSG_UNK		db	'CPU is a maverick -- 80288??.'
		db	CR,LF,EOS
MSG_80286	db	'CPU is an Intel 80286.'
		db	CR,LF,EOS

CPUMSG_TAB	label	word
	dw	PGROUP:MSG_8088		; 000 = Intel 8088
	dw	PGROUP:MSG_8086		; 001 = Intel 8086
	dw	PGROUP:MSG_V20		; 010 = NEC V20
	dw	PGROUP:MSG_V30		; 011 = NEC V30
	dw	PGROUP:MSG_80188	; 100 = Intel 80188
	dw	PGROUP:MSG_80186	; 101 = Intel 80186
	dw	PGROUP:MSG_UNK		; 110 = ?
	dw	PGROUP:MSG_80286	; 111 = Intel 80286

NDPMSG_TAB	label	word
	dw	PGROUP:MSG_NDPX		; 00 = No NDP
	dw	PGROUP:MSG_8087		; 01 = Intel 8087
	dw	PGROUP:MSG_80287	; 10 = Intel 80287

MSG_NDPX	db	'NDP is not present.'
		db	CR,LF,EOS
MSG_8087	db	'NDP is an Intel 8087.'
		db	CR,LF,EOS
MSG_80287	db	'NDP is an Intel 80287.'
		db	CR,LF,EOS

CERRMSG_TAB	label	word
	dw	PGROUP:MSG_CPUOK	; 0 = CPU healthy
	dw	PGROUP:MSG_CPUBAD	; 1 = CPU faulty

MSG_CPUOK	db	'CPU appears to be healthy.'
		db	CR,LF,EOS
MSG_CPUBAD	label	byte
		db	BEL,'*** CPU incorrectly allows interrupts '
		db	'after a change to SS ***',CR,LF
		db	'It should be replaced with a more recent '
		db	'version as it could crash the',CR,LF
		db	'system at seemingly random times.',CR,LF,EOS

NERRMSG_TAB	label	word
	dw	PGROUP:MSG_NDPSWOK	; 0 = NDP switch set correctly
	dw	PGROUP:MSG_NDPSWERR	; 1 = NDP switch set incorrectly

MSG_NDPSWOK	db	EOS		; no message
MSG_NDPSWERR	label	byte
		db	'*** Although there is an NDP installed '
		db	'on this sytem, the corresponding',CR,LF
		db	'system board switch is not properly set.  '
		db	'To correct this, flip switch 2 of',CR,LF
		db	'switch block 1 on the system board.',CR,LF,EOS

MDATA	ends				; end MDATA segment
	subttl	Main Routine
	page
CODE	segment	byte public 'prog'	; start CODE segment
	assume	cs:PGROUP,ds:PGROUP,es:PGROUP
	org	100h			; skip over PSP

INITIAL	proc	near
	mov	dx,offset ds:MSG_START	; starting message
	mov	ah,09h			; function code to display string
	int	21h			; request DOS service

	call	CPUID			; check the CPU's identity

	TAB	CPU			; display CPU results
	TAB	NDP			; display NDP results
	TAB	CERR			; display CPU ERR results
	TAB	NERR			; display NDP ERR results

	ret				; return to DOS
INITIAL	endp				; end INITIAL procedure
	subttl	CPUID Procedure
	page
CPUID	proc	near			; start CPUID procedure
	assume	cs:PGROUP,ds:PGROUP,es:PGROUP

; This procedure determines the type of CPU and NDP (if any) in use.
;
; The possibilities include:
;
;		Intel 8086
;		Intel 8088
;		NEC V20
;		NEC V30
;		Intel 80186
;		Intel 80188
;		Intel 80286
;		Intel 8087
;		Intel 80287
;
; Also checked is whether or not the CPU allows interrupts after
; changing the SS register segment.  If the CPU does, it is faulty
; and should be replaced.
;
; Further, if an NDP is installed, non-AT machines should have a
; system board switch set.  Such a discrepancy is reported.
;
; On exit, BX contains flag settings (as defined in FLG record) which
; the caller can check.  For example, to test for an Intel 80286, use
;
;		and	bx,mask FLAG_CPU
;		cmp	bx,FLG_80286
;		je	ITSA286

	irp	XX,<ax,cx,di,ds,es>	; save registers
	push	XX
	endm

; test for 80286 -- this CPU executes PUSH SP by first storing SP on
; stack, then decrementing it.  earlier CPU's decrement, THEN store.

	mov	bx,FLG_28		; assume it's a 286
	push	sp			; only 286 pushes pre-push SP
	pop	ax			; get it back
	cmp	ax,sp			; check for same
	je	CHECK_PIQL		; they are, so it's a 286

; test for 80186/80188 -- 18xx and 286 CPU's mask shift/rotate
; operations mod 32; earlier CPUs use all 8 bits of CL.

	mov	bx,FLG_18		; assume it's an 8018x
	mov	cl,32+1			; 18x masks shift counts mod 32
					; note we can't use just 32 in CL
	mov	al,0ffh			; start with all bits set

	shl	al,cl			; shift one position if 18x
	jnz	CHECK_PIQL		; some bits still on,
					; so its a 18x, check PIQL

; test for V20

	mov	bx,FLG_NEC		; assume it's an NEC V-series CPU
	call	CHECK_NEC		; see if it's an NEC chip
	jcxz	CHECK_PIQL		; good guess, check PIQL

	mov	bx,FLG_08		; it's an 808x
	subttl	Check Length of Pre-Fetch Instruction Queue
	page
; Check the length of the pre-fetch instruction queue (PIQ).
;
; xxxx6 CPUs have a PIQ length of 6 bytes,
; xxxx8 CPUs have a PIQ length of 4 bytes
;
; Self-modifying code is used to distinguish the two PIQ lengths.

CHECK_PIQL:
	call	PIQL_SUB		; handle via subroutine
	jcxz	CHECK_ERR		; if CX is 0, INC was not executed,
					; hence PIQ length is 4
	or	bx,FLG_PIQL		; PIQ length is 6
	subttl	Check for Allowing Interrupts After POP SS
	page
; Test for faulty chip (allows interrupts after change to SS register)

CHECK_ERR:
	xor	ax,ax			; prepare to address
					; interrupt vector segment
	mov	ds,ax			; DS points to segment 0
	assume	ds:INT_VEC		; tell the assembler

	cli				; nobody move while we swap

	mov	ax,offset cs:INT01	; point to our own handler
	xchg	ax,INT01_OFF		; get and swap offset
	mov	OLDINT01_OFF,ax		; save to restore later

	mov	ax,cs			; our handler's segment
	xchg	ax,INT01_SEG		; get and swap segment
	mov	OLDINT01_SEG,ax		; save to restore later

; note we continue with interrupts disabled to avoid
; an external interrupt occuring during this test

	mov	cx,1			; initialize a register
	push	ss			; save ss to store back into itself
	pushf				; move flags
	pop	ax			; ... into ax
	or	ax,mask TF		; set trap flag
	push	ax			; place onto stack
	POPFF				; ... and then into effect
					; some CPUs effect the trap flag
					; immediately, some
					; wait one instruction
	nop				; allow interrupt to take effect

POST_NOP:
	pop	ss			; change the stack segment register
					; (to itself)
	dec	cx			; normal cpu's execute this instruction
					; before recognizing the single-step
					; interrupt
	hlt				; we never get here

INT01:

; Note: IF=TF=0
; If we're stopped at or before POST_NOP, continue on

	push	bp			; prepare to address the stack
	mov	bp,sp			; hello, Mr. stack

	cmp	[bp].ARG_OFF,offset cs:POST_NOP	; check offset
	pop	bp			; restore
	ja	INTO1_DONE		; we're done

	iret				; return to caller

INTO1_DONE:

; restore old INT 01h handler

	les	ax,OLDINT01_VEC	; ES:AX ==> old INT 01h handler
	assume	es:nothing		; tell the assembler
	mov	INT01_OFF,ax		; restore offset
	mov	INT01_SEG,es		; ... and segment
	sti				; allow interrupts again (IF=1)

	add	sp,3*2			; strip ip, cs, and flags from stack

	push	cs			; setup ds for code below
	pop	ds
	assume	ds:PGROUP		; tell the assembler

	jcxz	CHECK_NDP		; if cx is 0, the dec cx was executed,
					; and the cpu is ok
	or	bx,mask FLG_CERR	; it's a faulty chip
	subttl	Check For Numeric Data Processor
	page
; Test for a Numeric Data Processor -- Intel 8087 or 80287.  The
; technique used is passive -- it leaves the NDP in the same state in
; which it is found.

CHECK_NDP:
	cli				; protect FNSTENV
	fnstenv NDP_ENV			; if NDP present, save
					; current environment,
					; otherwise, this instruction
					; is ignored
	mov	cx,50/7			; cycle this many times
	loop	$			; wait for result to be stored
	sti				; allow interrupts
	fninit				; initialize processor to known state
	jmp	short $+2		; wait for initialization

	fnstcw	NDP_CW			; save control word
	jmp	short $+2		; wait for result to be stored
	jmp	short $+2
	cmp	NDP_CW_HI,03h		; check for NDP initial control word
	jne	CPUID_EXIT		; no NDP installed
	int	11h			; get equipment flags into ax
	test	ax,mask I11_NDP		; check NDP-installed bit
	jnz	CHECK_NDP1		; it's correctly set
	or	bx,mask FLG_NERR	; mark as in error
CHECK_NDP1:
	and	NDP_CW,not mask IEM	; enable interrupts
					; (IEM=0, 8087 only)
	fldcw	NDP_CW			; reload control word
	fdisi				; disable interrupts (IEM=1) on 8087,
					; ignored by 80287
	fstcw	NDP_CW			; save control word
	fldenv	NDP_ENV			; restore original NDP environment
					; no need to wait
					; for environment to be loaded
	test	NDP_CW,mask IEM		; check interrupt enable mask
					; (8087 only)
	jnz	CPUID_8087		; it changed, hence NDP is an 8087
	or	bx,FLG_287		; NDP is an 80287
	jmp	short CPUID_EXIT	; exit with falgs in BX
CPUID_8087:
	or	bx,FLG_87		; NDP is an 8087
CPUID_EXIT:
	irp	XX,<es,ds,di,cx,ax>	; restore registers
	pop	XX
	endm
	assume	ds:nothing,es:nothing
	ret				; return to caller
CPUID	endp				; end CPUID procedure
	subttl	Check For NEC V20/V30
	page
CHECK_NEC	proc	near

; The NEC V20/V30 are very compatible with the Intel 8086/8088.
; The only point of "incompatibility" is that they do not contain
; a bug found in the Intel CPU's.  Specifically, the NEC CPU's
; correctly restart an interrupted multi-prefix string instruction
; at the start of the instruction.  The Intel CPU's incorrectly
; restart in the middle of the instruction.  This routine tests
; for that situation by executing such an instruction for a
; sufficiently long period of time for a timer interrupt to occur.
; If at the end of the instruction, CX is zero, it must be an NEC
; CPU; if not, it's an Intel CPU.
;
; Note that we're counting on the timer interrupt to do its thing
; every 18.2 times per second.
;
; Here's a worst case analysis: An Intel 8088/8086 executes 65535
; iterations of LODSB ES[SI] in 2+9+13*65535 = 851,966 clock ticks.
; If the Intel 8088/8086 is running at 10 MHz, each clock tick is
; 100 nanoseconds, hence the entire operation takes 85 milliseconds.
; If the timer is running at normal speed, it interrupts the CPU every
; 55ms and so should interrupt the repeated string instruction at least
; once.

	mov	cx,0ffffh		; move a lot of data
	sti				; ensure timer enabled

; execute multi-prefix instruction.  note that the value of ES as
; well as the direction flag setting is irrelevant.

	push	ax			; save registers
	push	si
	rep	lods	byte ptr es:[si]
	pop	si			; restore
	pop	ax

; on exit: if cx is zero, it's an NEC CPU, otherwise it's an Intel CPU

	ret				; return to caller
CHECK_NEC	endp
	subttl	Pre-Fetch Instruction Queue Subroutine
	page
PIQL_SUB	proc	near

; This subroutine discerns the length of the CPU's pre-fetch
; instruction queue (PIQ).
;
; The technique used is to first ensure that the PIQ is full, then
; change an instruction which should be in a 6-byte PIQ but not in a
; 4-byte PIQ.  Then, if the original instruction is executed, the PIQ
; is 6-bytes long; if the new instruction is executed, PIQ length is 4.
;
; We ensure the PIQ is full be executing an instruction which takes
; long enough so that the Bus Interface Unit (BIU) can fill the PIQ
; while the instruction is executing.
;
; Specifically, for all byt the last STOSB, we're simple marking time
; waiting for the BIU to fill the PIQ.  The last STOSB actually changes
; the instruction.  By that time, the orignial instruction should be in
; a six-byte PIQ byt not a four-byte PIQ.

	assume	cs:PGROUP,es:PGROUP
@REP	equ	3			; repeat the store this many times
	std				; store backwards
	mov	di,offset es:LAB_INC+@REP-1	; change the instructions
					; at ES:DI
					; and preceding
	mov	al,ds:LAB_STI		; change to a sti
	mov	cx,@REP			; give the BIU time
					; to pre-fetch instructions
	cli				; ensure interrupts are disabled,
					; otherwise a timer tick
					; could change the PIQ filling
	rep	stosb			; change the instruction
					; during execution of this instruction
					; the BIU is refilling the PIQ.  The
					; current instruction is no longer
					; in the PIQ.
					; Note at end, CX is 0.

; The PIQ begins filling here

	cld				; restore direction flag
	nop				; PIQ fillers
	nop
	nop

; The following instruction is beyond a four-byte-PIQ CPU's reach,
; but within that of a six-byte-PIQ CPU.

LAB_INC		label	byte
	inc	cx			; executed only if PIQ length is 6

LAB_STI	label 	byte
	rept	@REP-1
	sti				; restore interrupts
	endm
	ret				; return to caller
	assume	ds:nothing,es:nothing
PIQL_SUB	endp			; end PIQL_SUB procedure

CODE	ends				; end code segment

	if1
%OUT	Pass 1 Complete
	else
%OUT	Pass 2 Complete
	endif

	end	INITIAL			; end CPUID module
