Weet wat je hebt
Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.
In de DOS-tijd wist je niet wat er in je computer zat. Geen automatische herkenning, geen plug & play, geen Windows dat drivers voor je regelt. Je moest het zelf uitzoeken. Hoeveel geheugen? Welke grafische kaart? Welke CPU? En als je software wilde schrijven die slim omging met beschikbare hardware, moest je die vragen zelf kunnen beantwoorden.
Daarom: detectie-code. Code die de machine uitvraagt: wat ben je, wat kun je?
De CPU-familie
De pc draaide op de Intel 80x86 lijn. Begonnen met de 8088 (een goedkopere variant van de 8086) in de eerste IBM PC, daarna de 80286 in de AT, de 80386 die 32-bit rekenen mogelijk maakte, de 80486 met geïntegreerde FPU en cache. Vanaf de Pentium werd de nummering losgelaten.
Ik sla de 80186 over. Die bestond wel, maar zat vrijwel nooit in pc's. Intel maakte hem voor embedded systemen: printers, faxapparaten, controllers. Geen desktop-markt. Dus in detectie-code negeer ik hem. De 80486 en alles daarna pak ik samen als "386 or higher". Waarom? Omdat MS-DOS op dat punt al op zijn retour was. Windows 3.1 en later Windows 95 werden de norm, en die eisten minimaal een 386. Dus voor een DOS-programma dat hardware detecteert, is het onderscheid tussen een 486 en een Pentium niet zo relevant. Ze draaien allemaal dezelfde software en waarschijnlijk geen DOS.
De math coprocessor
Naast de CPU had je de floating point unit, ook wel math coprocessor genoemd. De 8087 bij de 8086, de 80287 bij de 286, de 80387 bij de 386. Losse chips die je in een aparte socket op het moederbord kon plaatsen. Ze handelden floating point berekeningen af: sinus, cosinus, exponenten; al het zware rekenwerk dat de CPU zelf niet (of heel langzaam) deed.
In de praktijk had bijna niemand zo'n coprocessor. Ze waren duur. De 8087 kostte in 1981 meer dan de CPU zelf. En software deed het ook zonder: compilers emuleerden floating point in software, langzamer maar het werkte. Dus waarom zou je als gebruiker honderden dollars uitgeven aan een chip die je toch nooit nodig had? Het klassieke kip-en-ei probleem. Ontwikkelaars programmeerden niet voor de FPU, omdat niemand hem had. Gebruikers kochten geen FPU omdat software hem niet gebruikte.
Ik had in mijn 80286 machine wel een 80287. Niet omdat ik hem nodig had, maar omdat ik een afkeer heb van lege sockets. Net zoals ik in het dashboard van mijn auto een irrationele afkeer heb van plekken waar duidelijk een knopje had kunnen zitten, maar dat er niet zit. Als er ruimte is voor een chip, wil ik die chip er ook in hebben, al doet hij niks. Die 80287 heeft in de jaren dat ik die machine gebruikte waarschijnlijk geen enkele floating point instructie uitgevoerd. Maar de socket was gevuld. Je hebt emotie-eters, ik ben vermoedelijk een emotie-gadgeteer.
Vanaf de 80486 DX was de FPU geïntegreerd in de CPU. Geen aparte chip meer nodig. Maar tegen die tijd was DOS al op sterven na dood.
De GPU-parallel
Hetzelfde patroon zie je later met grafische kaarten. In de jaren negentig kwamen GPU's op: losse kaarten met eigen processors voor 3D-graphics. NVidia, ATI, 3dfx. Voor games waren ze essentieel. Maar voor normale computers? Te duur, niet nodig, niemand had ze. Ik heb nog steeds geen NVidia-monster in mijn computers. Behalve in mijn game-laptop, die heeft een RTX 50-iets. Want games.
Maar nu, plotseling, zijn GPU's het belangrijkste onderdeel van de AI-revolutie. Die massaal parallelle rekenkracht die bedoeld was om pixels te renderen, blijkt perfect voor matrix-vermenigvuldigingen. NVidia beheerst de AI-hardwaremarkt omdat ze toevallig de chips maakten die gamers wilden en die chips blijken precies te doen wat AI-training nodig heeft. Dezelfde cyclus: niemand heeft het, dus niemand programmeert ervoor, dus niemand heeft het. Totdat ineens iemand een toepassing vindt die zo dwingend is dat iedereen het moet hebben. Bij de GPU was dat AI. Bij de FPU is dat moment nooit gekomen.
DetectCPU
Terug naar onze detectie-code. Hoe weet je welke CPU er draait?
Function DetectCPU: String;
Var
CPU: Byte;
Begin
Asm
pushf
pop ax
mov bx, ax
and ax, 0FFFh { probeer bits 12-15 te wissen }
push ax
popf
pushf
pop ax
and ax, 0F000h
cmp ax, 0F000h { bleven ze 1? Dan 8086/8088 }
jne @Not8086
mov CPU, 0
jmp @Done
@Not8086:
or bx, 0F000h { probeer bits 12-15 te zetten }
push bx
popf
pushf
pop ax
and ax, 0F000h
jz @Is286
mov CPU, 3 { 386 or higher }
jmp @Done
@Is286:
mov CPU, 2
@Done:
push bx { herstel originele flags }
popf
End;
Case CPU Of
0: DetectCPU := '8086/8088';
2: DetectCPU := '80286';
3: DetectCPU := '80386 or higher';
Else
DetectCPU := 'Unknown';
End;
End;
Merk op dat ik hier inline assembly gebruik. Inline, omdat dat simpeler is dan in TASM schrijven en dan los te moeten linken. Assembly niet zozeer omdat het een hogere performance geeft, maar omdat het in dit bijzondere geval gewoon veel simpeler is dan Pascal. We manipuleren de CPU op registerniveau en daar is assembly nu eenmaal erg goed in.
Flags manipuleren
Het FLAGS register bevat statusbits die de CPU gebruikt: zero flag, carry flag, overflow flag. De meeste programma's lezen die alleen. Maar sommige bits in FLAGS gedragen zich verschillend afhankelijk van de CPU. Bits 12-15 zijn daar een voorbeeld van:
- Op de 8086/8088 blijven ze altijd 1, wat je ook probeert.
- Op de 80286 kun je ze op 0 zetten.
- Op de 80386 kun je ze op 0 én op 1 zetten naar believen.
Die eigenschap gebruiken we. We proberen bits 12-15 te wissen. Als ze toch 1 blijven, is het een 8086. Als ze 0 worden, proberen we ze weer op 1 te zetten. Lukt dat, dan is het een 386 of hoger. Lukt dat niet, dan is het een 286.
De code stap voor stap
pushf zet het FLAGS register op de stack. pop ax haalt het van de stack in AX. Nu hebben we de huidige flags in een register waar we mee kunnen rekenen.
mov bx, ax bewaart een kopie in BX. Die gebruiken we later om de originele flags te herstellen.
and ax, 0FFFh wist bits 12-15 (de bovenste 4 bits van een 16-bit getal). 0FFFh is binair 0000111111111111, dus een AND met dat masker zet de bovenste bits op 0.
push ax zet de gewijzigde flags terug op de stack. popf laadt ze in het FLAGS register. We hebben nu geprobeerd bits 12-15 te wissen.
pushf / pop ax haalt FLAGS weer op in AX. and ax, 0F000h maskeert alles behalve bits 12-15. Als die bits nog steeds 1 zijn (0F000h), dan is het een 8086. Die CPU weigert die bits te veranderen.
Als het geen 8086 is, doen we de omgekeerde test. We zetten bits 12-15 op 1 met or bx, 0F000h, laden dat in FLAGS, en checken of ze ook echt 1 zijn geworden. Op een 286 lukt dat niet (die bits blijven 0). Op een 386 lukt het wel.
80286 instructies
Bovenaan het programma staat {$G+}. Dat schakelt 80286-instructies in. Standaard compileert Turbo Pascal voor de 8086, maar sommige FPU-instructies die we zo gebruiken, bestaan alleen op de 286 en hoger. {$G+} vertelt de compiler: genereer code voor een 286.
Zonder die directive zou de compiler FPU-instructies weigeren of emuleren, wat niet is wat we willen. We willen de echte FPU bevragen.
DetectFPU
De FPU detecteren is iets anders. We kunnen niet in FLAGS kijken, want de FPU heeft zijn eigen statusregisters. We moeten proberen de FPU te gebruiken en kijken of hij reageert.
Function DetectFPU: String;
Var
HasFPU: Boolean;
ControlWord: Word;
Begin
HasFPU := False;
Asm
fninit
mov byte ptr HasFPU, 0
fnstsw ax
cmp al, 0
jne @NoFPU
fnstcw ControlWord
mov ax, ControlWord
and ax, 103Fh
cmp ax, 003Fh
jne @NoFPU
mov byte ptr HasFPU, 1
@NoFPU:
End;
If HasFPU Then
DetectFPU := 'Present (8087/80287/80387)'
Else
DetectFPU := 'Not detected';
End;
FPU-instructies
De instructies beginnen met fn, wat staat voor "no-wait". Normaal wacht de CPU tot de FPU klaar is met een operatie voordat hij verdergaat. De no-wait versies doen dat niet. Dat is veiliger bij detectie: als er geen FPU zit, hangt de CPU niet.
fninit initialiseert de FPU. Als er geen FPU zit, gebeurt er niks. De instructie wordt genegeerd.
fnstsw ax staat voor "store status word". De FPU heeft een status word dat zijn toestand beschrijft. Dit commando kopieert dat naar AX. Na initialisatie moet de onderste byte (AL) 0 zijn. Als dat niet zo is, is er geen FPU.
Dat is de eerste check. Maar we doen nog een tweede.
fnstcw ControlWord staat voor "store control word". De FPU heeft ook een control word dat bepaalt hoe hij rekent (afronding, precisie, exception handling). Na fninit heeft dat control word een vaste waarde: 003Fh (na maskeren van reserved bits met 103Fh).
Als we die waarde niet terugkrijgen, is het waarschijnlijk geen echte FPU, maar gewoon rommel in het geheugen.
Waarom twee checks?
De eerste check (status word) kijkt of de FPU überhaupt reageert. De tweede check (control word) is extra zekerheid. Soms krijg je toevallig een 0 terug uit het geheugen, ook als er geen FPU zit. Maar de kans dat je zowel een 0 status word als de juiste control word toevallig terugkrijgt, is veel kleiner.
Het is defensief programmeren. Beter te voorzichtig dan ten onrechte melden dat er een FPU zit die er niet is.
Glunderen
Nu hebben we de functies. DetectCPU vertelt ons welke processor er draait. DetectFPU vertelt ons of er een coprocessor zit. We doen er niks mee, behalve het laten zien in een dialoog. En daar was het in de praktijk ook voor: laten zien dat je wist wat er in je machine zat. Een beetje glunderen met je nieuwe 386. Of je teleurstelling verbergen dat je nog steeds een 286 had.
Gerelateerd
Dialogen bouwen
Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.
Menu's en event handling
Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.
Events en de stroom van controle
Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.
Een echt project: SystemInformation
Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.
Turbo Vision, GUI vóór Windows
Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.
