Die zwei Schichten des PCI-BIOS (2)

Nachdem wir von Markus Fichtenbauer bereits alle interessanten Fakten zum PCI-Bus im allgemeinen und dem gemeinsamen PCI-BIOS-Standard im besonderen erfahren haben, möchte ich zum Abschluß dieser Reihe auf eine BIOS-Schicht eingehen, die bisher wohlweislich nicht erwähnt wurde, weil sie nach Einführung des gemeinsamen PCI-BIOS-Standards nicht mehr von Anwendungsprogrammen bzw. Treibern verwendet werden sollte (zumindest nicht als alleiniges Interface).

Nun stellt sich natürlich die Frage, wozu das ganze dann überhaupt gut sein soll. Ganz einfach: Das Low-Level BIOS, wie ich die im folgenden beschriebene PCI-BIOS Schicht einmal nennen möchte, übernimmt alle hardwarenahen Aufgaben, d. h. es konfiguriert die vorhandenen Karten und stellt Informationen darüber bereit. Weiterhin liefert es für jede Karte auch einen oder mehrere Interrupt-Handler (in Abhängigkeit von der Anzahl der Funktionseinheiten auf der Karte), da die Interrupt-Behandlung ebenfalls gerätespezifisch ist. Das High-Level BIOS, das in den vorangegangenen Ausgaben beschrieben wurde, liefert eine komfortable Schnittstelle für die einzelnen Gerätetreiber und Anwendungsprogramme.

Historisches

Der Ansatz, das PCI-BIOS in zwei Schichten zu teilen, hat durchaus praktische Gründe: Als ich von MW Computersysteme einen Hades zur Entwicklung meines NE2000-Treibers gestellt bekam, musste ich sehr schnell feststellen, daß das Betriebssystem des Computers (ein gepatchtes TOS 3.06) keinerlei Unterstützung für PCI- oder ISA-Karten bot. Lediglich Grafikkarten wurden so konfiguriert, daß ein Betrieb überhaupt möglich war. Dieser Zustand war höchst unbefriedigend, zwang er doch jeden Entwickler, selbst nach Karten zu suchen, die genaue Konfiguration aller vorhandenen Karten festzustellen und schließlich "seine" Karte konfliktfrei zu konfigurieren. Also wurde der NE2000-Treiber erst einmal zurückgestellt, um ein Programm zur zentralen Verwaltung der Resourcen der installierten Karten zu erstellen. Um mir das Leben etwas zu vereinfachen, faßte ich zu diesem Zeitpunkt (Ostern letzten Jahres) den Entschluss, zuerst lediglich ein Programm zur Konfguration der Karten zu erstellen. Noch bevor allerdings ein eigenes High-Level-BIOS erstellt werden konnte, das seinerseits auf dem Low-Level BIOS aufgesetzt hätte, hatte mich die (Milan-)Entwicklung eingeholt. Gerade noch rechtzeitig (bevor wir schließlich drei konkurrierende BIOS-Versionen bekommen hätten) konnten Michael Schwingen, Markus Fichtenbauer und ich Kontakt aufnehmen. Seitdem herrscht eine rege Kommunikation, deren Ergebnis das in den vorausgegangenen Artikeln beschriebene PCI-BIOS ist.

Weiterhin existiert seit kurzem ein von Markus Fichtenbauer erstelltes High-Level BIOS, das auf das Low-Level BIOS aufsetzt und damit den Hades kompatibel zum gemeinsamen BIOS-Standard macht.
Auch wenn das Low-Level BIOS von Treiber- oder Anwendungs-Software nicht mehr direkt angesprochen werden sollte, bietet es doch dem Entwickler eines neuen Systems deutliche Vorteile (ich denke da z.B. an den Phenix): Das High-Level BIOS ist hardwareunabhängig und muß daher nur einmal programmiert werden. Lediglich das Low-Level BIOS muß neu entwickelt werden.

Die Schnittsteile des Low-Level BIOS

Das Low-Level BIOS stellt seine Dienste ebenfalls über den CookieJar sowie über einen weiteren CookieJar, den ich im folgenden als Sub-CookieJar bezeichne, bereit. Dazu wird der PCI-Cookie verwendet, der auf eine Struktur zeigt, die alle Dienste des BIOS über eine Sprungtabelle bereitstellt. Um allerdings nicht mit dem High-Level BIOS zu kollidieren, das ebenfalls diesen Cookie verwendet, findet sich an erster Stelle dieser Struktur ein Zeiger aufden Sub-CookieJar, den das Low-Level BIOS für seine Zwecke verwendet. Wie man sich die Adresse des SubCookieJars beschafft, kann aus dem Listing pci _bios.c entnommen werden (init_pci(), find _subcookiejar()).
Wie der Name bereits andeutet, ist der Sub-CookieJar ein Gebilde, das in seinem Aufbau dem normalen CookieJar entspricht. Eine Ausnahme bildet aber die Tatache, daß Cookie-Namen mehrfach vorkommen dürfen. Der Sub-Cookie-Jar darf nach seiner Installation nicht mehr verschoben werden. Grund: Es soll verhindert werden, daß Treiber-Software bei jedem Zugriff auf den Sub-CookieJar diesen aufs neue suchen muss.
Im Sub-Cookie-Jar finden sich z. Z. drei Cookie-Typen: SER, PCI und INT.
Grundsätzlich wird für jede gefundene Funktion (NICHT Karte!) eine eigene Endung des Cookienamens erzeugt, angefangen bei ASCII "0", also z.B. "PCI0", "INT0" etc. Dabei ist nicht garantiert, daß diese Numerierung bei "0" anfängt und fortlaufend ist. Wenn der ASCII Zeichenvorrat von "0" bis "9" nicht ausreicht, kann auch einfach weitergezählt werden (z.B. "A", "1311 o. ä.).
Daß hier mit "0" begonnen wird, hat schlicht den Zweck, die Namen etwas lesbarer zu halten. Der SER-Cookie fällt dabei allerdings aus dem Rahmen: Er existiert immer nur in einfacher Ausfertigung in der Form "SER0" und hat nichts mit den Cookies "INT0" oder "PCI0" zu tun.

Cookie-Typen

Der "SER0"-Cookie
Der SER0-Cookie stellt die "Machine ID", also die individuelle Seriennummer des Computers, zur Verfügung. Fehlt der Cookie oder lautet der Wert auf 0x0L, dann besitzt der Computer keine Seriennummer. Dieser Cookie könnte natürlich manipuliert werden, um "dezentralisierte Sicherungskopien" zu nutzen, aber spätestens, wenn zwei serialisierte Treiber für verschiedene Machinen IDs zum Einsatz kommen, sind Hacker-Kenntnisse gefragt. Zur Sicherheit sollte allerdings ein High-Level BIOS, das die Rechner-Hardware kennt, bei der Ermittlung der Machine ID ausnahmsweise nicht den SER0-Cookie abfragen, sondern die Seriennummer selbst ermitteln (natürlich möglichst gut getarnt!). Ein kleines Programm, das die Machine ID über das Low-Level BIOS ermittelt, steht neben der Dokumentation übrigens ebenfalls zur Verfügung. Treiber, die das Low-Level BIOS nutzen wollen, sollten den Zugriff auf den SER0-Cookie und auch die Suche danach gut verschleiern (was ich in pci _bios.c allerdings wieder entfernt habe, um keine eigenen Geheimnisse preiszugeben). Allzuleicht lässt sich das Programm sonst nach "SER0" durchsuchen und dann beliebig manipulieren!

Die "PCIx"-Cookies
Die "PCIx"-Cookies liefern Informationen über die Resourcen einer Karte. "x" ist dabei, wie weiter vorne erwähnt, meist ein ASCII-Zeichen von "0" bis "9".
Alle "PCIx"-Cookies (und auch "INTx") mit demselben Wert für "x" gehören zu einer Funktion. Bei Single Function Karten bedeutet dies, daß alle diese Cookies auch zu einer Karte gehören.
Grundsätzlich sind die Cookies wie folgt aufgebaut:
Bit 0-3: Typ
Bit 4: 1 - 8bittige Zugriffe sind möglich
Bit 5: 1 - 16bittige Zugriffe sind möglich
Bit 6: 1 - 32bittige Zugriffe sind möglich
Bit 7: 1 - Resource liegt im Big Endian Format vor (0 - Little Endian)
Die übrigen Bits enthalten dann die zum Typ gehörenden Daten. Es gibt z.Z. elf Typen, nämlich jeweils Basis/Offset/Größenwerte für Config Space, Memory Space und IO Space, weiterhin noch den Offset für DMA-Transfers (die Adresse 0x00000000 im PCI Adreßraum ist i. d. R. NICHT identisch mit der physischen Adresse 0x00000000 im CPU-Adreßraum). Ich will an dieser Stelle nicht auf alle Details eingehen, sondern lediglich einige Besonderheiten anmerken.

Die Typen 0-8 sind mit wenigen Ausnahmen sehr einheitlich aufgebaut:
Für die Daten stehen maximal 24 Bit zur Verfügung. Fast alle Basis- und Offset-Angaben sind daher auf 256 Byte aligned, d. h. die unteren 8 Bit müssen ausgeblendet werden. Ausnahme sind Offset und Größe von 10Bereichen, da hier eine feinere Granularität erforderlich ist. Hier enthalten die oberen 16 Bit den nicht vorzeichenbehafteten Offset- bzw. Größen-Wert. Die BasisWerte geben jeweils einen Zeiger auf die Adresse 0 des jeweiligen Adreßraums an (z.B. PCI IO oder Speicher). Mehrere Karten, deren Resource-Cookies dieselben Basis-Adressen ausweisen, befinden sich also auf demselben Bussystem (es sind ja z.B. auch Rechner mit zwei unabhängigen PCI-Bussen denkbar). Der Offset gibt dann die Adresse innerhalb des Adreßraums des Bussystems an. Ein Beispiel:
Der Memory Space einer PCI-Karte läge bei 0x00200000 und sei 1 MB groß. Da der Speicherbereich des PCI-Busses des Hades bei 0x80000000 im CPU-Adreßraum liegt, finden wir für den betreffenden Speicherblock im Basis-Cookie den Wert 0x80000000, im Offset-Cookie den Wert 0x00200000 und im Größen-Cookie den Wert 0x00100000. Dabei wurden im Beispiel die zusätzlichen Informationen in den unteren 8 Bit bereits ausgeblendet.
Erwähnenswert ist noch Typ 15 (MISC), der für diverse Flags vorgesehen ist. Hier sind bisher nur die Bits 4 und 5 belegt. Hierüber wird die Allozierung einer Funktionseinheit vorgenommen.
Die Bedeutung der Bits für die Busbreite ist eigentlich klar. Wenn die Hardware die entsprechenden Zugriffe nicht unterstützt, dann sind die entsprechenden Bits auch nicht gesetzt. Beispielsweise ist beim Milan der Zugriff auf den Config-Space nur langwortweise möglich - eine Einschränkung der verwendeten PCI-Bridge.
Kritischer ist da schon das Big Endian Flag. Genaugenommen reicht ein Bit nicht aus, um alle möglichen Verdrehungen von Adress- und Datenleitungen darzustellen.
Im Low-Level BIOS sind nur zwei Möglichkeiten vorgesehen.

1. Big Endian
Die Daten werden ohne irgendwelche "Dreher" und Manipulationen von Datenund Adreßbits an die CPU weitergereicht. Ein Beispiel: Die PCI-Spec. spezifiziert den Aufbau des Config Space als Little Endian, d. h. Bit 07 eines Langwortes liegen bei Offset 0, Bit 8-15 bei Offset 1 usw. Das bedeutet aber, daß, wenn man mit einer Big Endian CPU auf ein solches Langwort zugreift, man Bit 0-7 des Langwortes aus dem Config Space in Bit 2431 des Registers der Big Endian CPU vorfinden würde usw.
Im Endeffekt bedeutet daher "Big Endian" hier, daß KEINE Anpassung der verschiedenen Darstellungen im Speicher vorgenommen wird, d. h., daß die Resource als Big Endian angesehen wird.
Da das ganze doch etwas verwirrend ist, noch ein zweiter Erklärungsversuch: Würden wir mit einem Little Endian Prozessor (x86) ein Langwort oder Wort, z.B. 0x12345678 oder 0x1234 in den Speicher an Adresse X schreiben, dann würde unser Big Endian Prozessor (M68K) an Adresse X den Wert 0x78563412 bzw. 0x3412 auslesen.

2. Little Endian
Der Bus wird so angepaßt, daß, um bei unserem vorherigen Beispiel zu bleiben, der Big Endian Prozessor wieder die Werte 0x12345678 bzw. 0x1234 lesen würde. Dies ist insbesondere für den Config Space interessant, da darauf meist langwortweise zugegriffen wird und die Daten nach dem Lesen nicht nochmals gedreht werden müßten.

Die "INTx"-Cookies
über die "INTx"-Cookies wird die Verarbeitung von Interrupts ermöglicht. Bit 0 des Cookie-Wertes ist dabei ein Flag:
Bit 0 = 0: Der Cookie-Wert stellt eine Einsprungadresse dar, über die der Interrupt aktiviert werden kann. Vorher muß der Treiber natürlich einen Interrupt-Handler aktiviert haben! Wichtig: Kümmert sich keine Routine um die Verarbeitung eines noch anstehenden Interrupts, dann wird die Interrupt-Kette für den entsprechenden Vektor deaktiviert!
Bit 1=1: In Bit 24-31 findet sich der Interrupt-Vektor für die betreffende Funktionseinheit. Da beim PCI-Bus InterruptSharing möglich ist, ist für den betreffenden Vektor eine XBRA-Kette installiert. Damit der Handler des Low-Level BIOS genügend Kontrollmöglichkeiten hat, hängt er mit zwei Einträgen an erster und letzter Stelle der Kette. Ein Treiber, der einen Interrupt-Handler installiert, muß diesen daher IMMER an zweiter Stelle der Kette einhängen!
Das Low-Level BIOS verwendet übrigens als XBRA-Kennung ebenfalls " PCI", da Seiteneffekte mit den Cookies nicht auftreten können.
Die Details zu den Anforderungen an einen Interrupt-Handler stehen detailliert in der Spezifikation und Anleitung zum Low-Level BIOS, nämlich in der Datei "pci _conf.txt". Was auf jeden Fall noch klar hervorgehoben werden soll: Wenn ein Treiber "seine" Funktionseinheit dazu bringt, Interrupts zu erzeugen, dann MUSS er auch einen Handler installieren, der die Interrupt-Ursache und den Interrupt selbst beseitigt!

Warum kein Low-Level BIOS auf dem Milan?

Das Low-Level BIOS ist aktuell auf dem Hades realisiert und wird dort wohl in Kürze Einzug ins ROM halten, da es mittlerweile sehr stabil arbeitet.
Auf dem Milan ist das Low-Level BIOS aktuell nicht verfügbar und wird voraussichtlich auch nicht implementiert werden. Dafür gibt es auch zumindest einen guten Grund: Meine Konzeption geht davon aus, daß alle Speicherbereiche direkt zugänglich sind. Hier hatte ich leider nicht mit dem "Einfallsreichtum" von PC-Hardware Designern gerechnet! Die PCI-Bridge des Milan blendet die Config-Spaces der installierten Karten nämlich nicht in den Speicher ein. Stattdessen ist ein recht umständlicher Mechanismus erforderlich, der jeweils einen Zugriff auf ein Langwort eines Config Spaces erlaubt - die im Milan verwendete PCI-Bridge lässt grüßen. Die Milan-Entwickler hätten diese Zugriffe nur mit umfangreicher zusätzlicher Logik transparent machen können, was im Sinne eines günstigen Preises für das Gerät nicht zu rechtfertigen gewesen wäre. Hätte man das Low-Level BIOS implementiert und die Config Spaces einfach nicht berücksichtigt, hätte es spätestens dann Probleme gegeben, wenn ein Treiber zwingend darauf zugreifen muß (was momentan allein schon zum Aktivieren der Resourcen einer Funktionseinheit erforderlich ist - das Low-Level BIOS schaltet nämlich Speicher und IO der Funktionseinheiten NICHT frei).

Beispielcode

pci_bios.c und pci_bios.h enthalten ein Beispiel, wie man mit dem Low-Level BIOS arbeiten kann. Dieses Beispiel stammt aus der Praxis und wird in meinem NE2000-Treiber in leicht abgewandelter Form verwendet.

Kontaktaufnahme

Wer sich für Details interessiert, die vor allem für die Entwicklung eines Low-Level BIOS interessant sind (nicht alle Details stehen in der Dokumentation), der kann sich gerne mit mir in Verbindung setzen. Es wurde ja schon verschiedentlich über PCI-Interfaces z.B. für Turbo-Karten wie die PAK/3 diskutiert.
Meine EMail-Adresse: Torsten.Lang@limes.de.


Torsten Lang
Aus: ST-Computer 09 / 1998, Seite 29

Links

Copyright-Bestimmungen: siehe Über diese Seite