Ich betrachte hier die parallelen I/O-Busse im AT-PC, die manchmal auch als "local" bezeichnet werden und die im
wesentlichen dem Betrieb von Steckkarten dienen. Neben Adress- und Datenleitungen sind weitere Verknüpfungen
zwischen Geräten und PC Teil der Busse. Sie dienen der zeitlichen Steuerung, der Zuordnung und Meldung von
Unterbrechungen und Steuerung von Zuständen (z.B. Reset) und Vorgängen (z.B. PCI-BIST).
Der Umgang mit den Geräten ist abhängig von den Controllern, die sie für den Programmierer kennzeichnen. Sowohl die
Adressen wie die Bedeutungen dieser Register sind Eigenarten, die ich hier nicht betrachte. Hier geht es nur darum,
wie diese Register und Steuerleitungen mit den Adressbereichen und Steuereingängen der CPU verknüpft werden. Dazu
gehört auch eine Betrachtung dessen, was Programme zur Gerätesteuerung kennzeichnet.
Der ISA-Bus ist nur noch von historischem Interesse, lebt er aber weiter als Interface im PCI-Bus, weil er direkt aus
dem I/O-Bus der Intel-CPU's abgeleitet ist, der durch zwei Eigenheiten gekennzeichnet ist: 8/16-stelliger Datenbus und
eigenem I/O-Adressbereich mit bis zu 20-stelliger Adresse (1M). Dieser Adressbereich ist mit eigenen, für die Intel-CPUs
typischen I/O-Kommandos "in" und "out" zu erreichen. Die Interruptleitungen sind mit dem Interruptsystem der CPUs
ebenfalls eigenartig verknüpft. Einzelheiten habe ich woanders abgehandelt. Hier gebe ich nur die Stichworte "IDT" und
"Interrupt-Controller" an.
Es liegen nämlich auch System-Uhr-, IRQ- und DMA-Steuerleitungen auf diesem Bus - ebenfalls direkt mit der hardware
verknüpft. Im weiteren wichtig ist nur, dass sämtliche IRQs des AT-Interrupt-Controllers an diesem Bus liegen. Sie sind
teilweise standardmässig verdrahtet (und müssen das auch bleiben). Es gibt aber freie Leitungen, die von Karten belegt
werden können. Beim ISA-Bus geht dies nur mit Kurzschlussbrücken (Jumpern) - also einer ebenfalls (fast) festen
Verdrahtung. Nach der gleichen Methode werden auch die Registeradressen angeschlossener Geräte in den Adressbereich
der CPU gebracht.
Daneben sind besondere Adressbereiche abhängig von Geräte-Controllern mit der CPU zu verknüpfen. Dort sind Pufferbereiche
und insbesondere BIOS-Erweiterungen untergebracht.
Weil sowohl die Adressdefinition mittels Brücken, wie auch die Breite der Daten wie der Adressen und auch die nicht
besonders hohe Taktung (ca.8 MHz) als Nachteil empfunden wurden, propagierte die Fa.Intel den PCI-Bus, der zunächst
mal eine Entkopplung des Busgeschehens von Eigenarten der CPU zum Ziel hatte. Damit wurde möglich, Steckkarten, die
bisher die Eigenarten des PC berücksichtigen mussten und deshalb nur dort verwendbar waren, nun zwischen Maschinen
sehr verschiedenen Typs getauscht werden konnten. Allerdings müssen diese Maschinen ein Interface enthalten, das den
verallgemeinerten PCI-Bus zum jeweils "eingeborenen" Bus macht - im PC also wieder zum konkreten ISA-Bus.
Bevor eine Steckkarte aber Programmen wie eine ISA-Bus-Karte erscheint, muss eine Initialisierung stattfinden, die beim
ISA-Bus nicht nötig war.
Im Gegensatz zum ISA-Bus enthält der PCI-Bus einen eigenen Adressbereich für Konfigurationsregister der Geräte am Bus,
die das mittels Programm zu definieren gestatten, was beim ISA-Bus mit Jumpern gemacht werden musste. Diese
Konfigurationsregister liegen in einem besonderen Konfigurationsbereich (=configuration space) und haben dort
genormte Bedeutung. Die Adressen in diesem Bereich sind nicht von Adressbussen der CPU abgeleitet, sondern müssen in ein
eigenes Adressregister in der Busbrücke (=bridge) geschrieben werden, das im I/O-Adressbereich der CPU liegt.
Eine ähnliche Technik wurde bereits bei der CMOS-Uhr oder auch VGA-Video-Karten angewendet. Die PCI-Bus-Adressen sind
allerdings nicht nur Offsetadressen, weshalb gleich zwei verschiedene Adresstypen genormt sein können.
Das Vorhandensein eines PCI-Busses kann sehr einfach durch Lesen des Adressports gefunden werden. Wie auch bei anderen
I/O-Registeradressen sind in allen Stellen Einsen zu finden, wenn ein Register garnicht vorhanden ist (Man liest dann
die pull-up-Widerstände). Ebenso sind vorhandene, aber nicht belegte PCI-Adressen zu selektieren. Nur wenn ein
Konfigurationsbereich zwar definiert ist, ein bestimmtes Register dort aber nicht vorhanden ist, sind Nullen in allen
Stellen zu finden. Das gilt insbesondere für Basis-Adressregister.
Bevor auf irgendwelche Register eines Geräte-Controllers zugegriffen werden kann, müssen diese Register in die
Adressbereiche der CPU gebracht werden. Solche Zuordnungen werden mit dem Ausdruck "mapping" gemeint, den ich im weiteren
ebenfalls verwende, und der die Definition einer Basisadresse betrifft, oberhalb derer Adressen mit Registeradressen belegt
sind. Im AT-PC sind Basisadressen sowohl im I/O- wie im Arbeitsspeicher-Adressbereich möglich, undzwar bedingt durch die
Konstruktion von Geräte-Controllern - und nicht durch Regeln für PCI! Ebenso verhält es sich mit Interruptleitungen,
deren endgültige Zuordnung allerdings in Unterbrechungs-Behandlern festgelegt wird. Am PCI-Bus sind nämlich nur
4 Unterbrechungsleitungen Standard.
Die Busbrücke für den PCI-Bus 0 (oft "north-bridge" genannt) liegt mit zwei 32-stelligen Registern im I/O-Adressbereich
der CPU. Diese sind der Konfigurations-Adress-Port auf CF8h und Konfigurations-Daten-Port auf CFCh
Bei jedem Zugriff auf den Konfigurationsbereich eines Gerätes ist zuerst eine im folgenden "PCI-Adresse" genannte
Adresse zu schreiben, die bewirkt, dass das damit adressierte 32-stellige Register im Konfigurationsbereich auf den
Konfigurations-Daten-Port gemappt wird. Unter der I/O-Adresse dieses Ports können dann Schreib-oder Lese-Zugriffe auf
das Register im Konfigurationsbereich stattfinden, undzwar 32-stellig. Will man Teilbereiche Byte- oder Wort-weise
ausführen, ist nicht etwa eine andere PCI-Adresse zu geben, sondern eine andere I/O-Adresse! Dabei liegen die Bits 0-7
unter CFCh, die weiteren darüber auf CFDh, CFEh und CFFh. Weil oft auch nur Bits zu behandeln sind, ist natürlich zunächst
ein Lese-Zugriff mit anschliesender UNDierung oder ODERierung nötig, bevor der Wert geschrieben wird.
Damit ist also klar, dass die PCI-Adresse das erste ist, was gegeben werden muss. Sie ist nicht abhängig von irgendeiner
Normierung, sondern betrifft einfach den soundsovielten Controller. Irgendwelche Zuordnungen spezifischer Treiber können
deshalb erst nach einer Suche in den Konfigurationsbereichen stattfinden, die nacheinander mit einer PCI-Adresse gegriffen
wurden! Weil viele der möglichen Adressen nicht belegt sein können, ist dabei eine Selektion der nicht definierten
Konfigurationsbereiche erforderlich, die durch den Wert FFFFFFFFh im Register 01 auf Offset 0 im Konfigurationsbereich
gekennzeichnet sind. Auch andere Register enthalten dann diesen Wert, können ihn aber eventuell auch bei sehr wohl
definiertem Konfigurationsbereich enthalten!
Bevor irgendein Betriebssystem das gemacht haben kann, hat es das BIOS im AT-ROM beim Booten gemacht. Insbesondere dann,
wenn ein Bootmedium ausser den klassischen FD- oder IDE-Medien zu finden ist - also z.B. für den USB-Boot. Damit das nicht
allzu abenteuerlich zu programmieren ist, gibt es eine Normung der Register- und Bit-bedeutungen für einige Register im
Konfigurationsbereich, die alle mit einer PCI-Adresse Typ 1 mit folgenden Bit-Bedeutungen zu adressieren sind:
0,1 Adresstyp =1 (Dualziffer!)
2-7 Register-Byteadresse ( aber im DWORD-Raster! = 00h, 04h, 08h ... 3Ch / Inkrement in der PCI-Adresse ist also 4 )
8-10 Funktions-Nr. (Zählung beginnt bei 0 )
11-15 Device-Nr. (Zählung beginnt bei 0 )
16-23 Bus-Nr. (Zählung beginnt bei 0 )
24-30 Reserviert
31 Enable=H (muss bei einer Suche gesetzt sein!)
Der Typ 0 einer PCI-Adresse unterscheidet sich vom Typ 1 nur durch eine 0 in Bits 0,1 und reservierte Bedeutungen in
den Bits 11-31 (also auch =0). Er wird normalerweise im AT-PC nicht gebraucht und erfordert auch besondere Abläufe,
"special cycles", die ich hier nicht betrachte.
Die Funktions-Nr. bezieht sich dabei auf ein "logisches" Gerät, das ein Teilgerät dessen ist, was mit der Device-Nr.
bezeichnet wird. Sind also auf einer Steckkarte verschiedene Funktionsgruppen - z.B. USB und "ethernet" - vorhanden,
dann haben sie zwar die gleiche Device-Nr., über verschiedene Funktions-Nr. aber auch ihre eigenen Registerbereiche,
die in eigenen Konfigurationsbereichen intialisiert werden müssen.
Im mittels PCI-Adresse erreichbaren Konfigurationsbereich können nicht nur Registeradressen, sondern auch IRQ# zugeordnet
werden. Ausserdem können Register oder Speicherbereiche der Geräte (wie z.B. der Bildwiederholspeicher bei Videokarten)
mittels Definition von Basisadressen in den Adressbereich der CPU gebracht bzw. dort verschoben werden. Das kann neben
dem I/O-Adressbereich auch der Arbeitsspeicher sein. Allerdings sind dabei gewisse Beschränkungen zu beachten, die zum
einen durch Standards des AT-PC sowie aktuelle Speicherbestückung, Betriebs-Modus (real oder protected mode) und
natürlich Karteneigenschaften gegeben sind.
Konfigurationsbereiche bestehen grundsätzlich aus 64 8-stelligen Registern, von denen jeweils 4 gleichzeitig und
parallel erreichbar sind - oder unter vier verschiedenen I/O-Portadressen übereinander. Die Normung der Bedeutung
wird über einen "header-ID" noch variiert, der deshalb bei unbekannter Bestückung als erstes erfasst werden muss.
Ich stelle zunächst die Bedeutungen vor, die immer die gleichen sind (dezimale Registernr./ hex Byteadresse):
Register 01/00h:
Bits 0-15 =Hersteller-ID (="vendor-ID" / Nur lesen ), Bits 16-31 =Geräte-ID (="device-ID" / Nur lesen )
Dieses Register ist nach dem Register 04 das wichtigste, wenn nach einem Gerät gesucht wird, um es einem bestimmten
Treiber zuzuordnen. Dann ist zunächst der Hersteller-ID das Kriterium für eine bestimmte Registerausstattung des
Controllers. Es kann damit auch das Vorhandensein bestimmter BIOS-Erweiterungen gegeben sein (vor allem bei
Video-Karten). Welcher ID welchen Hersteller bezeichnet, ist Listen zu entnehmen, die frei verfügbar im Internet zu
finden sind (oder in meinen Quellen). Das gilt auch für den Geräte-ID, der ebenso global einzigartig festgelegt ist.
Der Geräte-ID ist schliesslich strengstens von der oben genannten Gerätenummer als Teil der PCI-Adresse zu
unterscheiden, die nur aktuell durch die Belegung von Steckplätzen zustande kommt, also keineswegs einzigartig ist!
Bei einer Suche am PCI-Bus kann man auf Konfigurationsbereiche stossen, die den Wert =0 in diesem Register enthalten.
Dieser Wert kennzeichnet zwar ein nicht definiertes Gerät, kommt allerdings dennoch mit Werten in anderen Registern
vor. Ich fand solche Zustände bei Funktions-Nummern grösser 0 und kann das nur so erklären, dass Hersteller Bereiche
reserviert haben, im aktuellen Gerät aber nicht realisierten...
Register 02/04h:
Bits 0-15 =Kommando mit folgenden obligatorischen Bitbedeutungen:
Bit 0 : Enable I/O =H / Erreichbarkeit eines Gerätes über den I/O-Adressbereich schalten.
Bit 1 : Enable memory =H / Dieses Bit schaltet analog zu Bit 0 die Verknüpfung mit dem Arbeitsspeicherbereich.
Da das Bit gleichzeitig mit Bit 0 gesetzt sein kann, können folglich die gleichen Register-Adressen sowohl im I/O-Bereich
wie im Arbeitsspeicher liegen - oder auch teils teils. Auch die Basisadressen der hier nur freigeschalteten Bereiche sind
evtl. über die Controller zu machen!
Andernfalls ist über die Register 05/10h - 10/24h zu mappen wie weiter unten beschrieben.
Genormte Bereiche wie die der VGA-Register erscheinen aber meist garnicht im Konfigurationsbereich.
WICHTIG: Hier können NICHT Ein- und Ausgänge auf Steckern ausgeschaltet werden!
Bit 2 : Enable master behavior =H / Die Möglichkeit, als Busmaster zu agieren, wird hier eingeschaltet - natürlich nur,
wenn das Gerät auf der Steckkarte dazu imstande ist!
Bit 3: Enable special cycles =H / Ebenfalls abhängig vom Gerät!
Bit 4: Enable invalidate =H / Falls dieses Bit =L, muss ein busmaster "memory write" benutzen ( also nur im Zusammenhang
mit mehreren Busmastern auf dem PCI-Bus interessant )
Bit 5: Enable VGA palette snooping =H / Muss für diese spezielle Operation von VGA-Karten gesetzt sein!
Bit 6: Enable PERR# =H / Damit wird die Abhandlung eines parity-Fehlers im Gerät eingeschaltet (falls möglich!).
Bit 7: Enable adress/data-stepping =H / Seit neuestem reserviert!
Bit 8: Enable SERR# =H / Adress-parity-Fehler werden nur gemeldet, wenn dieses Bit 8 und Bit 6 gesetzt sind.
Bit 9: Enable back-to-back =H / bei mehreren busmastern wichtig...
Bit 10: Einst reserviert, seit neuestem aber Interrupt disable =H (unbedingt setzen, solange kein Unterbrechungs-Behandler
etabliert ist !)
Bits 11-15 sind reserviert ( Weil diese Bits =L sind, kann auch ein nicht existentes Gerät über FFFFFFFFh in diesem
Register erkennt werden)
Ein Kommando =0 macht alle Register des Gerätes unerreichbar, abgesehen von denen, die im Konfigurationsbereich liegen!
Bei Umdefinieren von Basis-Adressen sollten vorher Bits 0,1 =0 gesetzt werden.
Bits 16-31 =Status mit folgenden obligatorischen Bitbedeutungen (bezogen auf Bit 16 entsprechend Bit 0):
Schreiben an dieses Register kann Bits nur rücksetzen und nicht setzen!
Bits 0-2 sind reserviert
Bit 3 ist ursprünglich ebenfalls reserviert, seit PCI-Version 2.3 aber der Interrupt-Status
Bit 4: "capabilities pointer" =H / Damit wird ein Wert in Register 14, Bits 0-7 als gültig gekennzeichnet.
Bit 5: 66 MHz Bus-Betrieb möglich =H / 33 MHz Bus-Betrieb möglich =L
Bit 6 ist reserviert
Bit 7: Falls das Gerät zu "back-to-back"-Aktionen fähig ist =H
Bit 8: Wird nur von Busmastern gesetzt, falls die PERR#-Leitungen des PCI-Bus benutzt werden (Parityfehler)
Bits 9-10: Dualziffer für (DEV-select-)timing: LL=schnell, LH=mittel, HL=langsam, HH=reserviert
Bit 11: Abbruch durch Zielgerät passiert =H ("target-abort")
Bit 12: Wie Bit 11, aber von einem Busmaster für sein Zielgerät gesetzt
Bit 13: Wie Bit 12, aber für den Abbruch durch den Meister selbst =H gesetzt
Bit 14: SERR# erfasst =H (Systemfehler festgestellt, z.B. beim BIST)
Bit 15: PERR# parity-Fehler erfasst =H
Sowohl SERR# wie PERR# können in einem AT-PC ohne schreckliche Folgen ignoriert werden, bzw. garnicht erst im
oben erklärten Kommando-Register eingerichtet werden. Nur bei rauher Umgebung oder extremen Sicherheitsanforderungen
ist wirklich nötig, auch das PCI-Bus-Geschehen unter Kontrolle zu halten. Parity-Fehler bei Transfers zu
Speichermedien z.B. werden in Statusregistern von Geräten gemeldet. Und diese Fehler treten deutlich
wahrscheinlicher auf als auf dem PCI-Bus!
Register 03/08h ist Byte-orientiert und nur zu lesen:
LL-Byte: =Revisions-Nr.
Die übrigen drei Byte stellen den "Class-Code" dar, bestehend aus:
LH-Byte: "register-level-programming-interface", falls vorhanden (z.B.f.VGA),
HL-Byte: Sub-Klasse
HH-Byte: Basis-Klasse
Der Class-Code erlaubt, ohne weitere Kenntnisse eines angeschlossenen Gerätes einen Soundchip von einer Videokarte zu
unterscheiden. Sind für solche Klassen Normen für Registerbedeutungen vereinbart, dann können allein damit Treiber zu
Geräten zugeordnet werden. Neben der VGA-Norm sind IDE-, SCSI- und USB-Norm als Beispiele zu nennen...
Register 04/0Ch ist Byte-orientiert und teilweise nur zu lesen:
LL-Byte =cache-Grösse
LH-Byte =Verzögerungszeit
HL-Byte: Header Typ (Nur lesen)
Dieses Byte entscheidet darüber, wie die Bedeutung der weiteren Register ist !
Hier ist die normale Bedeutung vorgestellt, die der Typ =0 kennzeichnet.
Weitere Typen sind wie folgt bezeichnet:
Die Bits 0-6 typisieren mit Dualziffern: 01 = PCI to PCI bridge, 02 = card bus bridge
Weitere Typisierungen in diesen Bits sind reserviert...
Das Bit 7 (=MSB) klassifiziert mit =H das Gerät als "multi function"-Gerät mit evtl. mehr als einem
Interruptausgang. Man hat also normalerweise die Werte 00h oder 80h zu registrieren.
Ebenso wie die Identifikationsnummern muss dieses Byte vor einer Initialisierung ausgewertet werden!
HH-Byte: BIST ("Build In Self Test") Das BIST-Byte hat folgende Bitbedeutungen:
Bits 0-3: fehlerfrei=LLLL / andere Werte bedeuten Fehler
Bits 4-5 sind reserviert
Bit 6 muss =H gesetzt werden, um BIST zu starten. Es wird bei Erfolg vom Gerät zurück gesetzt
Bit 7 ist =H ,falls BIST überhaupt möglich ist - muss also geprüft sein, bevor mit Bit 6 rumgespielt wird!
Die weiteren Register sind in ihrer Bedeutung nicht mehr obligatorisch, sondern bedingt durch den Typ des Headers.
Register 05/10h - 10/24h enthalten Basis-Adressen von Registern, deren Bezug durch das Gerät gegeben ist. Das können
sowohl Register im Controller sein, als auch Speicherbereiche, die zur Pufferung oder als BIOS-Bereich genutzt werden.
Nicht vorhandene Register sind hier durch Null in allen Stellen gekennzeichnet!
Diese Adressregister haben eine bestimmte Bitstruktur, abhängig davon, ob sie als Arbeitsspeicher- oder I/O-Adresse
Bedeutung haben:
Bit 0: =H: kennzeichnet eine Basis-Adresse im I/O-Bereich. In diesem Fall sind Bit 1 reserviert und Bits 2-31 sind die Basis-Adresse,
deren Bits 0,1 dann allerdings =0 sind!
Bit 0: =L: kennzeichnet eine Basis-Adresse im Arbeitsspeicher, die eine absolute, physische ist. Dann sind Bits 4-31 Definition
einer Basis-Adresse, deren Bits 0-3 =0 sind, weil diese Bits dann folgende Bedeutung haben:
Bits 1,2 codieren mit =LL ,dass die Basisadresse im 32-Bit-Adressbereich und mit =HL dass sie im 64-Bit-Adressbereich liegt.
Die Werte =LH und =HH sind reserviert.
Bit 3 =H kennzeichnet den Bereich als "prefetchable"
Diese Basisadressen werden jedenfalls vom AT-BIOS im Zusammenspiel mit möglichen BIOS-Erweiterungen in Geräten festgelegt.
Wer damit rumspielen will, muss unbedingt alle(!) Bereichsdefinitionen aller(!) Geräte berücksichtigen!
Insbesondere Controller-Register werden ja auch vom Gerät aus geschrieben. Ihre Adressen dürfen deshalb nicht identisch zu
denen anderer Register sein. Ein Kurzschluss ist sonst jedenfalls zu fürchten! Vor allem die Bestückung des Arbeitsspeichers
ist bedeutsam. Die regelmässige Einblendung des Bildwiederholspeichers in obere Regionen, falls hoch auflösende Bildschirme
voll genutzt werden sollen, erlaubt bespielsweise nicht(!) die vollständige Bestückung des adressierbaren Bereiches mit realen
Speicherbausteinen. Aber es darf natürlich auch andere Überschneidungen von Adressbereichen nicht geben, weil normalerweise
interne Register nicht kurzschlusssicher sind.
Weil man also tunlichst die Finger von diesen Adressen lässt und sie nur lesend verwertet, will ich nur erwähnt haben, dass noch
ein trickreiches Vorgehen Standard ist, das erlaubt neben den Basisadressen auch die Bereichslänge der definierten Bereiche zu
erfassen. Deshalb sind bestimmte Bits auch überhaupt nicht überschreibbar.
Welche Adresse welchen Bedeutungsbereich meint, ist schliesslich abhängig vom jeweiligen Gerät. Bei Video-Karten beispielsweise
sind meist die ersten drei Register definiert, wobei zwei Register die Basisadressen der gleichen Register im Controller definieren,
einmal aber im I/O-Bereich, das andere Mal im Arbeitsspeicher gemappt.
Register 11/28h enthält den Cardbus CIS-Pointer, der nur von Bedeutung ist, wenn ein PCMCIA-System ebenfalls im Spiel ist.
Register 12/2Ch zeigt in Bits 0-15 den Subsystem-Hersteller-ID und in Bits 16-31 den Subsystem-ID
Dieses Register darf nur gelesen werden.
Register 13/30h enthält die Basisadresse eines möglichen Erweiterungs-ROMs (BIOS) mit folgender Bit-Struktur:
Bit 0: Enable =H
Bits 1-10 sind reserviert
Bits 11-31> sind die Basisadresse, die grundsätzlich im Arbeitsspeicherbereich liegt und in Bits 0-10 =0 sein muss.
Register 14/34h zeigt in Bits 0-7 den "Capabilities-Pointer", der nur bei gesetztem Bit 4 im Status-Register eine
Basis-Adresse im Konfigurationsbereich darstellt. In der nur selten interessanten Struktur werden erweiterte Möglichkeiten
für Fehlerkontrolle als möglich gekennzeichnet. Weitere 24 Bit sind reserviert
Register 15/38h ist reserviert
Register 16/3Ch legt die Zuordnung von Unterbrechungsleitungen in Byte-Registern fest:
LL-Byte =genutzte IRQ-Leitung, die vom BIOS beim POST gesetzt wird.
Die Nummern in diesem Register sind die Nummern für die Eingänge des Interrupt-Controllers (="Interrupt routing").
LH-Byte =IRQ-Pin (Nur lesen), wobei zunächst nur 4 Leitungen wie folgt mit Dualziffern genormt sind:
1=IntA, 2=IntB, 3=IntC, 4=IntD ....eine 0 kennzeichnet ein Gerät, das keine dieser Interruptleitungen benutzt.
Ist das Gerät nicht "multi function", enthält also nicht mehrere Funktionsgruppen (Funktionsnr.=0), dann ist nur der IntA
benutzt. Eventuell wird also eine einzige Unterbrechungsleitung von mehr als einem Gerät oderiert angesteuert!
HL-Byte =Min_Gnt (R-only) zeigt die Länge einer Burst-Periode, die nur auf dem PCI-Bus Bedeutung hat.
HH-Byte =Max_Lat (R-only) zeigt die Häufigkeit des Zugriffs auf den PCI-Bus, die nur dort Bedeutung hat.
Die Verknüpfung von Unterbrechungsleitungen eines Gerätes mit dem Interrupt-Controller ist nicht trivial, weil
der nur 15 Eingänge hat, die zum grössten Teil standardmässig vergeben sind.
Die Busbrücke hat vier Interrupt-Ausgänge, die eventuell von mehreren Geräten am Bus oderiert betätigt werden
und auf vier Interrupt-Eingänge des Interrupt-Controllers wirken.
Es kann deshalb sinnvoll sein, nicht einen Unterbrechungs-Behandler zu schreiben, der zeitaufwendig den Signalgeber
selektiert, sondern die möglichen, aber aktuell unerwünschten Signalgeber als solche auszuschalten (interrupt disable),
und dann einen sehr schnellen Unterbrechungs-Behandler aktuell zu etablieren, der nur den aktuell vorhandenen und nun
einzigen Signalgeber abhandelt. Sowas geht aber nur unter einem Betriebssystem wie ASMOS wirklich einfach.
Das Interruptsystem im PCI-Bus ist zwar so ausgelegt, dass eine Untrbrechungsanforderung (="interrupt request")
solange bestehen bleibt, bis sie ausdrücklich zurück gesetzt wird. Das muss aber nicht ein Gerätetreiber bzw.
ein Unterbrechungs-Behandler machen. Vielmehr reicht der Interrupt-Controller den bei ihm erzeugten "end of
interrupt"-Zustand weiter, macht also die Rücksetzung im PCI-Bus automatisch. Abwärtskompatibilität wäre
anders nicht erreicht...
Alle genannten Register sind ausser den besonders mit "(Nur lesen)" gekennzeichneten auch beschreibbar.
Bei allen Schreibaktionen, die Register mit reservierten Bitbedeutungen betreffen, ist unbedingt der vorhandene Wert
solcher Stellen zu erhalten! Man hat also eventuell erst zu lesen, die Stellen zu maskieren und in den zu schreibenden
Wert zu übernehmen. Reservierte Stellen können nämlich durchaus umdefiniert werden - mit eventuell fatalen Folgen.
Reservierte Register sind dagegen durch Schreiben nicht zu verändern.
Bei vielen Geräten kann ein auf der Karte vorhandenes BIOS in den Arbeitsspeicherbereich eingeblendet werden (oder auch
bereits eingeblendet sein wie bei Videokarten), wo es dann auf klassische Weise mittels INT-Kommando gerufen werden kann.
Im protected mode geht das nicht mehr so einfach, weil dann Segment-Deskriptoren im Spiel sind, die von einem Betriebssystem
regiert werden. Meist müssen spezielle 16-Bit-Deskriptoren vorhanden sein, bevor das BIOS eingeblendet wird!
Jedenfalls sind sehr umständliche Vorbereitungen zu treffen, die die Benutzung eines Karten-BIOS keineswegs
vorteilhaft machen...
Sehr viel brauchbarer ist also der direkte Umgang mit Registern der Controller, die bei Standard-Geräten wie Tastatur,
Timer, Interrupt-Controller, Floppy-Disk oder IDE-Festplatte in der gehabten und unveränderlichen Weise über I/O-Portadressen
gegriffen werden können. Man braucht keinen PCI-Konfigurationsbereich zu lesen, wenn es um solche Standardgeräte geht!
Basisadressen solcher Geräte sind meist noch nichtmal dort zu finden.
Bei anderen Geräten, wie insbesondere Videokarten, kann die Adressierung eines Teils der Register unveränderlich sein
(z.B. VGA-Register), ein anderer Teil kann aber einblendbar sein in den I/O-Adressbereich oder den Arbeitsspeicher. Mal kann
der Controller solche Einblendungen bei beliebigen Adressen erlauben, mal bei besonderen.
Immer ist deshalb die Konstruktion des Controllers eines Gerätes am PCI-Bus letzte Instanz für die Bedeutung der Register
im zugehörigen Konfigurationsbereich. Solche Feinheiten muss man der Dokumentation von Controllern und Geräten entnehmen!
Die dort dokumentierten Basisadressen für die Lage von Erweiterungsregistern oder Pufferbereichen ist allerdings grundsätzlich
falsch! Nur die im Konfigurationsbereich definierten Adressen sind real, wenn es nicht Standardadressen sind!
Für den Umgang mit den PCI-Konfigurationsbereichen gibt es auch ein PCI-BIOS, das im real mode über INT 1Ah verfügbar ist. Im
protected mode ist die Benutzung schon nicht mehr so einfach. Weil der direkte Umgang mit den zwei Registern des PCI-Busses
aber nun wirklich nicht die Welt ist, kann man das PCI-BIOS wirklich vergessen...
"Treiber" (="driver") werden oft auch Programme genannt, die nicht direkt mit Geräten, sondern irgendwelchen
Abstraktionen umgehen, die in anderen Programmstücken definiert werden. Deshalb muss ich ausdrücklich feststellen,
dass ich hier mit diesem Ausdruck nur solche Programme meine, die direkt mit den Registern in Controllern von Geräten
umgehen.
Solche Programme bestehen regelmässig aus zwei unterscheidbaren Schichten. Die eine ist die Konsequenz aus Konstruktion
und Zweck von Controllern, die andere ist die Konsequenz aus dem Betriebssystem, unter dem der Treiber dient.
Wer Treiber für Geräte schreiben will, die im AT-PC mittlerweile regelmässig über den PCI-Bus mit der Maschine verschaltet
sind, hat mit Bezug auf Controller zunächst folgendes zu tun:
Er hat sich mit den Zwecken und Registern zu beschäftigen und davon ausgehend Abläufe für Initialisierung und Betrieb zu
schreiben. Eigentlich immer sind auch Interrupt-Behandlungen zu formulieren, weil fast immer asynchrone, also nicht von der
Uhr der CPU getaktete Ereignisse auftreten. All diese Abläufe sind immer Wertzuweisungen an Register in Controllern, die
oft nur einzelne Bits betreffen und deshalb auch Rechenschritte in der CPU (UNDieren, ODERieren, Schieben) erfordern.
Schliesslich ist meistens mehr als ein Controller im Spiel - z.B. der Timer, der Interrupt-Controller oder auch
der DMA-Controller. Immer ist aber der PCI-Controller (die "bridge") im Spiel.
Ganz entscheidend anders als in Programmen, die Wertzuweisungen innerhalb des Adressbereichs des Arbeitsspeichers abhandeln,
sind stets Adressen zu geben, die nicht beliebig verschieblich sind, sondern absolut entweder im I/O- oder
Speicher-Adressbereich liegen. Abgesehen von genormten Basis-Adressen sind Adressen durch die jeweiligen
Controller-Konstruktionen gegeben, die diese im PCI-Konfigurationsbereich mittels Basis-Adressen verschieblich präsentieren.
Was da verschoben werden kann oder muss, handelt das BIOS im AT-PC ab. Daran muss man eigentlich nicht mehr rühren. Nur
die Werte der Basis-Adressen muss man in den Zusammenhang des Treiberprogramms bringen, und natürlich auch die
Interruptnummern, falls gegeben. Dazu hat man wie oben beschrieben, mit dem PCI-Controller und den Konfigurationsbereichen
umzugehen.
Weil Treiber mit Adressen umgehen, die a priori feststehen, muss ein Betriebssystem nicht mehr dazu tun, als einen
Deskriptor für den gesamten Arbeitsspeicherbereich, falls dorthin gemappte Register oder Puffer benutzt werden sollen.
Ausserdem muss die Basisadresse der IDT irgendwie gegeben werden, damit Unterbrechungsabhandlungen etabliert werden können -
am besten auf absoluter Adresse lesbar.
Ein Datentransfer, den der Treiber bewältigen soll, kann im Prinzip über Register der CPU beim Ruf von Treiberprozeduren
abgewickelt werden - normalerweise ist die Basisadresse von Quell- oder Ziel-Puffer und die Länge des Datensatzes zu
übergeben. Und natürlich sind ein paar Bits Definition nötig, die festlegen, ob geschrieben oder gelesen werden soll,
oder ob sonst was zu tun ist.
So einfach geht das unter einem Betriebssystem wie ASMOS.
Einfacher geht es garnicht. Aber komplizierter sehr wohl!
Der wichtigste Fehler, den man machen kann bei der Verknüpfung von Treiberprogrammen mit "Anwendungen" ist, Treiber in
"höheren" Programmiersprachen schreiben zu wollen, namentlich in C/C++, das das scheinbar möglich macht.
Keine "höhere" Programmiersprache erlaubt nämlich den Umgang mit Deskriptoren, die im protected mode die Basisadressen
von Adressbereichen geben, in denen Programme arbeiten oder Daten bearbeitbar sind. Der scheinbar pfiffige Ausweg in den
page mode ist damit nämlich diktiert. Dieser Ausweg ist ein Abweg, weil er bei jedem Speicherzugriff zusätzliche
Adresskalkulationen nötig macht, die so viel Zeit kosten (mindestens 50%), dass Vorteile eines mapping von
Controller-Registern in den Arbeitsspeicher mehr als aufgezehrt sind! Kurz gesagt, wird also nicht nur ein erheblicher
Anteil des verfügbaren Arbeitsspeichers für die Seitentabellen, die der page mode nötig macht, verschwendet, sondern
auch genau die Zeit, die nötig ist, um auf dem AT-PC ausreichend schnell Klang oder bewegte Bilder abhandeln zu können
(von anderem ganz zu schweigen...).
Natürlich kann man noch weit mehr Zeit und Arbeitsspeicher verschwenden wollen, indem man multi tasking programmiert.
Die Konsequenz, die aus diesen unbestreitbaren Tatsachen gezogen wurde, ist aber nicht, es sein zu lassen, sondern eine
quasi-religiöse Beschwörung von Vorteilen, die UNIX-C/C++-multi-tasking-supervisor-Systeme haben sollen. Mindestens
wird Maschinen-Unabhängigkeit beschworen, die natürlich gerade dann absurd ist, wenn man mit einer Maschine umgehen
muss und dann nicht anders als Maschinen-abhängig sein kann.
Nun gibt es komischerweise solche Systeme tatsächlich und man kann also sehr genau studieren, wohin die Verblendung führt.
Insbesondere kann festgestellt werden, welche Vorteile eine scheinbar einfachere Formulierung von Quellprogrammen
tatsächlich hat. Auch hier lässt sich kurz sagen: Nicht nur mehr Binärcode, nicht nur mehr Quellcode, nicht nur aus den
Nähten platzende Dateisysteme und Protokollebenen, nicht nur gewaltiger Aufwand beim linking, nicht nur uferlose
Übersetzungszeiten bei Installationen, sondern dankenswert wenig Wiederverwendbarkeit von Code und Anpassbarkeit an die
technische Entwicklung. All das macht fühlbar den geniessbaren Teil des Lebens von damit beschäftigten Programmierern
kürzer.
Es bleibt nur ein Vorteil solcher Systeme unbestreitbar. Sie sind undurchschaubar und deshalb verkäuflich. Vor allem
kann man nötige Dokumentation von Verknüpfungen teuer verkaufen - zu teuer, wie gerade mal wieder die EG-Kommission
angesicht gewisser Produkte feststellte, und deshalb Mikros$ft bestrafte...(Diese Firma macht auch viel Geld mit
C/C++ Compilern)
Was Treiber bei solcher Philosophie sind, kann mittlerweile wenigstens lesbar in offenen Quellen studiert werden, die mit
dem frei verfügbaren GNU-Compiler übersetzbar sind. Man kann insbesondere die Verrenkungen studieren, die nötig sind,
zweierlei oder mehrerlei Dinge unter dem gleichen Namen auftreten zu lassen ("Maschinen-Unabhängigkeit") und zur Domäne
eines allgewaltigen Betriebssystems ("supervisor") zu machen. In erster Linie führt das dazu, dass die besonderen
Vorteile gewisser Maschinen garnicht genutzt werden - allem voran der sehr klug gemachte protected mode der CPU und die
Nutzung von Deskriptoren bzw. ihren Selektoren im Programm.
Da wird jedenfalls klar, dass ungeheuer viel Schreiberei und Sucherei beim Programmieren nötig sind und vor allem mal den
Blick auf das eigentlich Nötige vernebeln. Das macht Verständnis und den Zugang zu Gegebenheiten für Einsteiger so
schwierig, dass ich hier noch ein bisschen Schreiberei zur Entnebelung nötig habe.
Wie gesagt geht es im wesentlichen um Register in Controllern und Bedeutungen von Bits. Das findet man in endlosen Listen
von #define-Anweisungen in header-Dateien (Dateinamenendung ist *.h). Das sieht etwa so aus:
#define CRTC_H_SYNC_STRT_WID 0x0004
Gebraucht wird dabei der bestens aussprechliche Wert 4 als Offset einer Adresse. In einem Assembler-Programm wird dieser
Wert als Immediate-Wert in einem Kommando verwendet, das auch gleich die Wertzuweisung enthält. Ein C/C++-Programmierer
hat dagegen mit dem unaussprechlichen Kürzel "CRTC_H_SYNC_STRT_WID" umzugehen und kann ihn nicht ohne typecasting zu
einer Basisadresse im Speicher addieren. Nur mit einer zeitaufwendigen Suche können Linker- und Präprozessor-Programm
schliesslich die 4 an die richtige Stelle im Programm setzen, wobei der Programmierer auch nicht weniger zu tun, diesen
richtigen Platz durch Verwendung des unaussprechlichen Makros zu kennzeichnen. Und natürlich darf er zwanzig Zeichen statt
einem fehlerfrei schreiben!
Ebenso wie Registeradressen werden Bitbedeutungen virtualisiert. Im günstigen Fall sind es Maskendefinitionen, die
allerdings nicht in der in Assembler üblichen, einfachen Binärdarstellung ausgedrückt sind, sondern hexadezimal, was
einen geeigneten Taschenrechner in Reichweite gebietet. Schlimmstenfalls sind solche Virtualisierungen aber über mehrere
Dateien verteilt, wobei oft auch noch die Namen für ein und dasselbe Ding gewechselt werden.
Ganz toll sind schliesslich die in Assembler einfach zu benutzenden "in" und "out"-Kommandos verkappt. Manchmal sind es
Funktionsaufrufe, die z.B. "inb(Adresse,Wert)" heissen, wobei sowohl der eine wie der andere Parameter ein Makro sein
kann, dessen Definition man in einer anderen Datei suchen darf. Das stattdessen in Assembler zu benutzende Kommando
"in al,dx" folgt auf einen Anlauf, der sofort erkennen lässt, was da wohin geht. Das mag einer für zu kompliziert
halten. Er wird aber im Schlimmstfall durch die Natur der Sache, um die es eigentlich geht, den Controller, belehrt.
Auch wenn eine Funktion "inb(...)" inline übersetzt wird, wird nicht etwa auf den Transfer der Parameter über den Stack
verzichtet, weil das in C normal ist. Deshalb kann man auch Verzweiflungstaten besichtigen, die in der Kapselung von
inline-Assembler in Makros bestehen. Äusserste Umsicht bei Betrachtung solcher Ausdrücke ist schliesslich geboten, weil
auch mal die Bedeutung von Parametern vertauscht sein kann!
Die eigentlichen Abläufe sind meistens als Zuweisungen von Variablen aus structs verkappt. Das sieht z.B. so aus:
u32 ref_clk_per = info->ref_clk_per;
Wer nicht gleich weiss, dass "u32" eine Typdefinition ist, die weiss der Himmel wo definiert ist, aber wohl einen
32-stelligen Wert meint, der kann nichts verstehen, bevor er nicht mindestens ein Dutzend mittels #include eingebundene
Dateien durchstöbert hat, die durchaus die Definition auch nicht enthalten können - man findet sie schliesslich erst
in einer weiteren dort eingebundenen Datei.
Der Teufel geht aber erst richtig los, wenn Treibercode, der nur aus ein paar hundert Byte besteht, mit Protokollen oder
besonderen Dateiformaten verknüpft werden muss - z.B. Ethernet mit TC/IP und HTML. Obgleich das in einem besonderen
Programm zusammen gefasst sein könnte (einem "Browser"), wird es normalerweise über ein ganzes System verschmiert und
ist nur mit "Umgebungsvariablen" überhaupt zum Zusammenspiel zu bringen, die nicht nur beim Übersetzen, sondern auch
jedesmal beim Booten und Linken erneut abgehandelt werden müssen. Enträtseln lässt sich das also nur mit sehr viel
Sucherei.
Treiber, die in Assembler für ein Assembler-Betriebssystem geschrieben werden, bestehen nach meiner Erfahrung aus mindestens
um den Faktor 5 weniger Binärcode und keineswegs mehr Quellcode, weil zahlreiche #Define und #Include völlig überflüssig sind.
Selbstverständlich sind auch Umtriebe eines Linkers bei der Vermittlung der Adressen unnötig, weil die ja absolut sind.
Man muss wohl ein Systemphilosoph statt ein Programmierer sein, um höher hinaus zu wollen statt geradeaus durch.
Wer mir glaubt, dass ein Treiber auch ohne besondere Betriebssystemumgebung genau das tun kann, was er soll, dem
empfehle ich, die Abläufe in Assembler unter ASMOS zu entwickeln. Sie können ohne Verrenkungen sofort unter ASMn oder
ASMat in Sekundenbruchteilen übersetzt werden und gleich im Test laufen. Geht was schief, dauert ein Re-boot 10 Sekunden.
ASMOS erlaubt ausserdem sehr einfach Dateien als Qelle oder Ziel zu verwenden und die dann auf FD oder HD abzuspeichern.
Geht nichts mehr schief, kann immer noch überlegt werden, wie der Code mit dem überflüssigen, aber in höheren Sphären
nötigen Schnickschnack aufgemotzt werden kann. Der NASM-Assemblerdialekt aber kann bleiben (in einem "Modul").
Spätestens dann wird manch einer es nicht mehr anders haben wollen als unter ASMOS...
Wie der Umgang mit dem PCI-Controller unter dem Assembler-Betriebssystem ASMOS zu pflegen ist, geht aus den Quellen des
Programms "DUMP to file" hervor, die ich als Teil eines FD-Dateisystems veröffentliche, das unter ASMOS lesbar und mittels
ASMat übersetzbar ist. Dort findet man auch den Programmcode, der die Benutzung der Prozedur "FP_PCIsearch" in ASMOS
abhandelt und die ebenfalls äusserst einfache Einbindung eines Programms in den Menümode, um Ein- und Ausgabe von Werten
zu realisieren. Unter anderen Betriebssystemen geht das nicht ohne weitere Programmpakete und Bibliotheken.
Nebenbei bemerkt zeigt das Programm, dass man Aufwand bis auf ein Promille schrumpfen kann mittels Assembler und ASMOS.
Der Teil, der den Umgang mit dem PCI-Bus abhandelt, besteht aus ein paar 100 Byte Binärcode im Programm und in ASMOS. Um
die gleiche Aufgabe unter LINUX zu lösen, schrieb Martin Mares die "PCI Utilities". Dieses "Paket" geht kaum durch die Tür.
Es besteht aus mehr als einer halben Million Bytes, während ich kaum tausend nötig hatte (Quellcode).
Das Floppyimage des Dateisystems, das das angesprochene Beispiel enthält, findet man auf meiner Homepage, ebenso die um
einige weitere Einzelheiten zu PCI und anderen Bussen ergänzte Datei "BUSSE", die Teil des FDOS-Archivs ist.