[net.micro.pc] CPUID.ASM source file

schuler@psuvax1.UUCP (David W. Schuler) (04/13/86)

This is the source code for CPUID, a program which will determine the cpu
type and numeric coprocessor type in an IBM-PC (or compatible).  This is
the source code directly from the April 1986 issue of PC Tech Journal.

---
David W. Schuler           {akgua,allegra,ihnp4}!psuvax1!gondor!schuler
Penn State University      schuler%gondor@penn-state.csnet
602 Sproul Bldg.           schuler%psuvaxg.bitnet@wiscvm.arpa
University Park, PA        {akgua,allegra,ihnp4}!psuvax1!tvw@psuvma.bitnet
                           schuler@gondor.uucp : schuler@psuvaxg.bitnet
---
Are you a turtle?
==========================cut here=======================================

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

; :ts=8 /* for MY editor only */
;
;       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