ZurückZum InhaltVorwärts

Maschinen und C

C ist eine Hochsprache. Das beinhaltet, daß ein in ANSI-C geschriebenes Programm ohne Probleme auf eine andere Maschine gebracht werden kann, die ebenfalls einen ANSI-C Compiler hat. Programme können in Hochsprachen ohne Kenntnis des verwendeten Prozessors geschrieben werden.

In manchen Fällen benötigen wir trotzdem die Möglichkeit, auf ganz spezielle Eigenschaften der Maschinen zuzugreifen. Dies gilt besonders für Programmierer, die Treiber schreiben. Im folgenden wollen wir uns einige Möglichkeiten ansehen, direkt auf Speicher, Register und E/A-Adressen sowie Unterbrechungen zuzugreifen.

Dabei verlassen wir den Boden des ANSI-C-Standards. Die hier vorgestellten Verfahren und Möglichkeiten sind nicht standardisiert und können daher auf Ihrem Compiler und auf Ihrem Betriebssystem unterschiedlich gehandhabt werden. Im folgenden werden hauptsächlich Hinweise zur Programmierung unter DOS gegeben, da hier der Zugriff auf den Rechner immer möglich ist. Bitte versuchen Sie also nicht, unter UNIX/Linux, Windows 9x oder NT diese Programmbeispiele zu übernehmen.

Bei anderen Betriebssystemen, wie UNIX, ist der Umgang mit dem Rechner aus gutem Grund bedeutend schwieriger, da hier der Schutzmechanismus für den Speicher und für die CPU berücksichtigt werden muß. Bei näherem Interesse lohnt sich hier ein Buch über die Treiberprogrammierung. Und gereade bei Linux würden sich ein paar Mark oder Euro in eine der Distributionen zu investieren und den Quellcode zu studieren.

Ein Hinweis: Am Ende des Kapitels finden Sie eine Reihe von Links auf Programmdateien. Für eine Bearbeitung des empfiehlt es sich, diese Programme zu holen und parallel zu lesen. Die Unterteilung in einzelne Funktionen, die im ursprünglichen Buch gemacht wurde, war in der WWW-Version schwer handhabbar.

Zugriff auf Adressen

Im Gegensatz zu anderen Sprachen hat C die Möglichkeit eingebaut, auf die Inhalte von Speicheradressen zuzugreifen. Das Mittel dazu sind die Zeiger. Der Inhalt des Zeigers gibt dabei die Adresse an, auf die wir zugreifen wollen, und der Typ des Zeigers gibt an, wie groß das Datum ist, das an dieser Stelle erwartet wird.

Eigentlich dürfen wir an Zeiger nur Adressen anderer Variablen des gleichen Datentyps zuweisen. Wenn man das aber ignoriert, kann man an Zeiger auch absolute Adressen als Zahlenkonstanten zuweisen und dann damit zugreifen. (Dieses Verfahren ist einfach, schnell - und nicht zu empfehlen.)

Bild 10-1: Speicherzugriff mit absoluter Adresse

Je nach Betriebssystem sind unterschiedliche Adressen möglich. Ein Standard-UNIX-System wird immer mit Speicherschutz arbeiten. Daher kann kein Programm auf Daten zugreifen, die ihm nicht gehören. Versucht man es trotzdem, dann wird das eigene Programm vom Betriebssystem beendet und man erhält die Fehlermeldung "Memory fault - core dumped" (Speicherfehler, Programm auf Platte abgelegt).

Bild 10-2: BIOS-Zugriff mit absoluter Adresse

Unter DOS wird kein Speicherschutz verwendet, auch wenn alle 80286- und 80386-Rechner ihn eingebaut haben. Daher kann man mit beliebigen Adressen zugreifen und auch jeden möglichen Schaden anrichten. In den Beispielen werden wir daher nur lesend zugreifen.

Im ersten Beispiel greifen wir auf die Tabelle der Unterbrechungsfunktionen zu. Bei den Prozessoren der DOS-Maschinen liegen ab der physikalischen Adresse "0" die 256 Adressen der Bedienfunktionen für Unterbrechungen ("interrupt service routines"). Jede Adresse ist vier Byte groß. Sie besteht aus einer Segmentnummer und der Adresse im Segment.

Die Prozessoren der Intel 80X86-Reihe sind 16-Bit-Rechner (oder verhalten sich so), die aber einen großen Speicherbereich adressieren können. Daher teilt man die Adressierung in zwei Stufen. Der Speicher von 1MB-Größe (8086) wird dabei in Segmente unterteilt, die ihrerseits maximal 64k-Bytes groß sein können. Ein zweiter Adreßteil, der Offset, wird beim Zugriff versetzt addiert, um die benötigten 20 Bit für die physikalische Adresse zu gewinnen.

Die Adressierung soll hier nur kurz erwähnt werden, um die auftauchenden Begriffe "Segment" und "Offset" zu erläutern. Ein weiterer Begriff taucht in den Beispielen in Zusammenhang mit der Adressierung bei DOS-Rechnern immer wieder auf: "near" und "far" Zeiger bzw. Adressen. Wird ein Zeiger, eine Funktion oder eine Adresse als "near" bezeichnet, dann ist die verwendete Adressse nur 16 Bit groß und besteht nur aus dem Offset. Das Gegenstück dazu ist "far", das immer die volle 32-Bit Adresse mit Segmentkennung und Offset beinhaltet.

[8086 Adressierung]

Bild 10-3: Adressierungsverfahren bei Intel CPU's

Mit den sogenannten "Speichermodellen" der DOS-Compiler kann man die maximale Größe der Daten und des Programms einstellen. Diese Modelle handhaben dann die Zeiger unterschiedlich. Um sicher zu sein, daß ein bestimmter Zeiger die gewünschte Größe besitzt, definieren wir ihn mit dem (nicht standardisierten) Schlüsselwort far.

Neben der Möglichkeit, absolute Adressen (auch als Kombination von Segment und Offset) in Zeiger zu laden, haben viele Compiler Funktionen oder Makros definiert, die Namen wie "peek" und "poke" tragen. Basic-Programmierern werden diese Namen bekannt vorkommen.

Mit peek() greift man auf eine absolute Speicheradresse lesend zu, mit poke() schreibend.

Muß man in einem Programm auf absolute Adressen zugreifen, ist es sehr viel sinnvoller, Makros oder Funktionen wie peek() oder poke() zu verwenden. Sollte das Programm auf eine andere Maschine gebracht werden, dann ist die Portierung einfacher, da dann nur diese Funktionen ausgetauscht werden müssen.

Und natürlich sind solche Funktionen leichter lesbar, wenn man selbst nach einer gewissen Zeit das eigene Programm noch einmal überarbeiten muß. Am besten schreibt man alle Programme so sorgfältig und einfach, als müßte man sie selbst in drei Jahren wieder ändern.

In vielen Programmen faßt man alle maschinenabhängigen Teile, wie etwa ein selbst geschriebenes peek() oder poke(), in einer Datei mit dem Namen system.c zusammen. Dann ist es für alle Beteiligten klar, wo im Falle einer Umstellung zuerst geändert werden muß. Die Funktionen liefern Worte zurück. peekb() und pokeb() arbeiten mit char.

Bild 10-4: Zugriff auf absolute Adresse mit peek()

Zugriff auf Ein- und Ausgabeadressen

Beim Zugriff auf Peripheriegeräte gelten sinngemäß die Bemerkungen zu den Speicherzugriffen. Es gibt Maschinen, die zwischen Speicherzugriffen und E/A-Zugriffen keinen Unterschied machen. Hier sind die Register der Peripheriegeräte über normale Speicheradressen ansprechbar. Man nennt dies MMIO (memory mapped io - E/A im Speicherbereich). In diesem Fall gilt auch für Registerzugriffe der vorhergegangene Abschnitt.

Die meisten Rechner haben einen getrennten Adreßraum für Speicher und Peripherie. In diesen Fällen hat die CPU eigene Befehle für die Bedienung der Peripheriechips (in,out). Man kann nun bei vielen CPUs zwischen einer Benutzer- und einer Systembetriebsart umschalten. Als Benutzer kann man nur einen Teil der CPU-Befehle verwenden, als Betriebssystem alle. Unter Standard-UNIX wird diese Unterscheidung verwendet, unter DOS nicht. Daher kann ein DOS-Programmierer während der Programmlaufzeit auf alle Peripheriechips zugreifen und für jede mögliche Verwirrung sorgen. Die UNIX-Programme haben keinen Zugriff auf die E/A-Befehle, da das Betriebssystem in die Systembetriebsart schaltet und so allen normalen Benutzern die E/A-Befehle verbietet.

[Registersatz SIO]

Bild 10-5: Registerbank der seriellen Schnittstelle

Programmierer, die auf Peripheriechips zugreifen möchten, können bei DOS- Compilern spezielle Makros oder Bibliotheksfunktionen verwenden. Bei UNIX und anderen geschützten Betriebsystemen muß für die Ein- und Ausgabe auf Registerebene ein spezieller Betriebssystemauffruf erfolgen, der zumeist ein Teil der Treiber-Schnittstelle des Betriebssystems ist.

Bild 10-6: Zugriff auf SIO-Register

Sehen wir uns als Beispiel den Zugriff auf die Registerbank eines seriellen E/A-Chips im IBM-PC an. Als SIO (serieller I/O-Chip) wird ein Chip mit der Bezeichnung 8250 oder ein dazu kompatibler verwendet. Aus Programmierersicht besteht der Chip aus acht Registern. Die Register dienen dem Datenaustausch, der Steuerung des Chips und der Steuerung aller Leitungen, die auf den Stecker der seriellen Verbindung geschaltet sind. Diese Leitungen heißen auch Modem- oder Handshake-Steuerleitungen.

Wir wollen einmal alle acht Registerinhalte anzeigen. Wegen der extrem simplen Hardware sind nur vier Schnittstellen unterstützt, die unter den Namen COMx bekannt sind. Das "X" steht hier für eine Ziffer zwischen 1 und 4.

Im Programmbeispiel übergeben wir als Parameter eine Nummer zwischen 1 und 4 für die DOS-Bezeichnung COM1 bis COM4. Diese Nummer wird als Index in eine Adreßtabelle "sioadr" benutzt, die die Basisadressen enthält. Schließlich wird in einer Schleife auf alle acht Register (Index 0-7) mit Hilfe des Makros inportb() zugegriffen.

Programme dieser Art sind eine Domäne ungeschützter Betriebssysteme. Neben dem Eingabebefehl inportb() für byteweises Einlesen gibt es meist noch drei weitere für die byteweise Ausgabe und für Wortoperationen. Leider gehören solche Spracherweiterungen in den Bereich, in dem sich die Hersteller der Compiler einen Kampf um Marktanteile liefern. Sie sind nicht immer gleich benannt. Sehen Sie daher bei Bedarf im Manual nach. Die hier verwendeten Namen sind die der TurboC/C++- Compilerfamilie. Zumeist klingen die Namen aber ähnlich (z. B. inb/outb).

Programmierung von Unterbrechungen

Wenden wir uns nun dem dritten großen Bereich der maschinennahen Programmierung zu. Dieser Bereich behandelt die Unterbrechungen. Eine Unterbrechung ist ein elektrisches Signal eines Peripheriegerätes, das damit eine Bedienung anfordert.

Ein Drucker ohne Papier, ein empfangenes Byte an der seriellen Schnittstelle oder das Ende eines Datentransfers mit DMA-Hilfe melden ihren Zustand mit Hilfe einer Unterbrechungsanforderung.

Diese Anforderungen werden von einem Controllerchip (8259A) gesammelt, nach Prioritäten geordnet und dann an die CPU weitergereicht. Für die CPU bedeutet ein Unterbrechungssignal ein Umschalten vom laufenden Programm auf ein anderes.

Da es bis zu 256 verschiedene Unterbrechungsquellen geben kann, liefert der Controller eine Nummer von 0 bis 255 an die CPU, um ihr zu sagen, welche Unterbrechung vorliegt.

Diese Nummer (engl: interrupt vector number / Unterbrechungszeiger Nummer) wird intern als Index für eine Adreßtabelle benutzt, die die Adressen von maximal 256 Funktionen beinhaltet, die die jeweilige Anforderung bearbeiten.

Neben den außerhalb der CPU erzeugten Unterbrechungsanforderungen, kann jede CPU auch interne Zustände als Auslöser für eine Unterbrechungsanforderung verwenden. Fehler in mathematischen Berechnungen lösen Unterbrechungen aus oder aber unbekannte Bitmuster im gerade gelesenen Code.

Um intern erzeugte Unterbrechungen von den externen zu unterscheiden, nennt man die internen oft Ausnahmen (exceptions). Die Datenblätter für die Intel-Prozessoren haben von Anfang an festgelegt, daß die ersten 32 Unterbrechungen ausschließlich für die Ausnahmen der CPUs freizuhalten sind.

Der große Schrittmacher für PCs hat anscheinend dem Datenblatt nicht geglaubt und sich nicht daran gehalten. So liegen heute im reservierten Bereich BIOS-Unterbrechungen, die den Übergang zu den neueren Prozessoren durchaus behindern.

Im Programmbeispiel haben wir die dritte Form eines Unterbrechungsaufrufs verwendet. Hier handelt es sich nicht um eine Unterbrechung im eigentlichen Sinn, sondern um einen speziellen Funktionsaufruf. Mit Hilfe eines besonderen Assemblerbefehls oder einer besonderen C-Funktion rufen wir indirekt eine Funktion auf, deren Adresse in der Unterbrechungstabelle liegt. Bei Aufruf müssen wir dabei nicht die Adresse selbst kennen, sondern nur die Indexnummer in der Tabelle. Dies ist sehr hilfreich, da ein Autor eines Programms nicht den Ort der Funktion wissen muß. Dieser Mechanismus wird auch für alle Betriebssystemaufrufe unter DOS verwendet (int 21h).

Die CPU benötigt noch eine Tabelle mit 256 Adressen der Unterbrechungsprogramme, um mit Hilfe der Unterbrechungsnummer des Controllers das zugehörige Unterprogramm zu finden. Diese Tabelle liegt bei den Intel-CPUs an der niedrigsten Adresse (bei DOS-Maschinen).

Eigene Unterbrechungsroutinen

Mit DOS-Compilern lassen sich auch Funktionen schreiben, die direkt (also ohne ein dazwischengeschaltetes Betriebssystem) von der Hardware gestartet werden können. Schaltet der Prozessor mit Hilfe eines Unterbrechungssignals auf ein anderes Programm um, dann darf das gerufene Unterprogramm dem unterbrochenen Programm nichts zerstören. Daher sichern die Unterbrechungsfunktionen alle verwendeten Register.

Viele DOS-Compiler kennen das Schlüsselwort interrupt, das eine Übersetzung mit Registersicherung erzwingt. Je nachdem, für welche der drei möglichen Verwendungen, Hardware-Unterbrechung, Prozessorausnahme oder indírekter Funktionsaufruf, die Funktion geschrieben wird, kann sie auf Peripheriechips, Prozessorregister oder allgemeine C-Funktionen zugreifen.

In unserem einfachen Beispiel soll die Verwendung als indirekt aufgerufene Funktion gezeigt werden. Um die Arbeitsweise vorzustellen, genügt es, einen kleinen Meldetext auszugeben. Die Funktion meldet, daß die ISR-255 gerufen wurde. ISR steht für die englische Bezeichnung: interrupt service routine / Unterbrechungs-Bedienungs-Funktion.

Bild 10-7: Aufbau einer Unterbrechungsfunktion

Im aufrufenden Programm müssen wir zuerst einen Eintrag in die Unterbrechungstabelle vornehmen lassen. Sicherheitshalber sollte aber vorher der dort gespeicherte Eintrag gerettet und am Programmende wieder restauriert werden. Für die beiden Vorgänge gibt es Spezialfunktionen getvect() und setvect(). Wiederum gibt es ähnlich heißende Funktionen in der Treiberschnittstelle bei UNIX.

Im auffrufenden Programm wird eine Zeigervariable alterint angelegt, die als Typ Zeiger auf Interrupt-Funktion ohne Parameter und ohne Rückgabe hat. In dieser Variablen halten wir die alte Unterbrechungsadresse (oder: interrupt vector), die bisher in der Tabelle unter dem Index 255 stand. Mit getvect() wird die Hilfsvariable alterint geladen.

Mit setvect() wird der neue Eintrag gesetzt. Der Aufruf kann danach mit int86() erfolgen. Hier werden neben der gewünschten Unterbrechungsnummer noch zwei Zeiger auf sogenannte Spiegelregistersätze übergeben.

Ein Spiegelregistersatz ist eine strukturierte Variable, die den Registersatz der CPU nachbildet. In unserem Fall könnte man im Spiegelregistersatz r einzelne Variable setzen. Es ist dann Teil der Funktion int86(), daß die Prozessorregister tatsächlich aus dem Spiegelregistersatz geladen werden, bevor der Aufruf der Unterbrechungsroutine erfolgt.

10-8: Installation und Aufruf einer Interrupt-Funktion

Nach Ablauf der Unterbrechungsroutine werden die Register dann in einem möglicherweise neuen Spiegelregistersatz geschrieben. Der C-Programmierer kann damit auch BIOS- und DOS-Aufrufe ansetzen, die eine bestimmte Vorbesetzung der Register erwarten.

Diesen Aufruf einer Unterbrechungsbedienroutine gibt es noch in einer erweiterten Fassung für DOS- und BIOS- Aufrufe, die zusätzlich zu den Registern der CPU die Segmentregister der Speicherverwaltung benutzen (engl. MMU- memory management unit / Speicherverwaltungseinheit). Die Funktion heißt int86x().

Bild 10-9: Aufruf eines BIOS-Interrupts

Hier wird neben dem Spiegelregistersatz für die CPU-Register eine eigene Struktur für die Segmentregister mit übergeben. Sowohl Spiegelregister als auch Segmentregisterstruktur übergibt man mit Hilfe der Startadresse. Damit können dann die Funktionen int86() und int86x() ihre Ergebnisse in den übergebenen Strukturen zurückliefern.

Im Beispiel definieren wir einen far- Zeiger auf char mit dem Namen fcp. Dieser Zeiger soll zum Zugriff auf die BIOS-Parametertabelle benutzt werden. Diese Tabelle, die es erst ab späteren AT-BIOS-Versionen gibt, beinhaltet Modell und Versionsnummern des BIOS.

Mit dem erweiterten Unterbrechungsaufruf können wir die vollständige Adresse dieser Tabelle vom BIOS erhalten. Dazu wird die Unterbrechung "0x15" mit dem Befehl "0xc0" benutzt. Das Ergebnis des Aufrufs liegt in den Registern BX und ES (Extrasegment).

Zum Aufbau der vollständigen Adresse wird ein Makro MK_FP (make far pointer / mache vollständige Adresse) benutzt. Falls es dieses Makro nicht gibt, kann die Adresse auch aus ihren Teilen durch Schieben und Verodern aufgebaut werden. Wie immer, ist ein Makro oder eine Funktion für nicht standardisierte Vorgänge vorzuziehen.

Der Zugriff auf die Spiegelregister geschieht hier als 16-Bit-Zugriff. In der Überlagerung (union) REGS liegen zwei Strukturen übereinander. Die eine heißt "x" und beschreibt die acht 16-Bit-Register, die zweite heißt "h" und beschreibt die acht 8-Bit-Register. Die einzelnen Register werden mit ihrem gewohnten Namen angesprochen.

Treiber als bessere Lösung

In diesem Kapitel wurden viele Beispiele gegeben für Dinge, die man eigentlich gar nicht in einem Programm erledigen sollte. In der Theorie ist in einem Programm jeder Zugriff auf den Speicher oder E/A-Adressen zu unterlassen. Zur Bedienung aller Geräte sollten Treiber eingesetzt werden. Nun liegt aber (leider) eine umfassende Darstellung von Treibern außerhalb des Themas dieses Buches.

Treiber sollten für jedes Gerät erstellt werden, die an einen Rechner angeschlossen werden. Und nur Treiber sollten dann die notwendigen Zufgriffe auf die Elemente der maschinennahen Programmierung vornehmen. Dem C-Programmierer wird mit einem Treiber die Möglichkeit geboten, wieder mit fopen(), fread() und anderen Bibliotheksfunktionen auch auf bisher unbekannte Geräte zuzugreifen.

Aufbau einer Interruptbedienung

Für den Programmierer ist die Erstellung einer Anwendung mit Hardware-Interrupts eine heikle Angelegenheit. Dies bedeutet nicht etwa, daß es sehr schwierig ist, solche Funktionen zu schreiben. Alledings ist es ziemlich schwierig, Bedienfunktionen für Interrupts zu testen. Sie treten asynchron und außerhalb des Programmes auf. Ein richtiger Test müßte daher mit einem Hardware-Testgerät (einem sogenannten ICE/in circuit emulator) erfolgen.

In den folgenden Beispielen wollen wir einmal eine Bedienfunktion für die serielle Schnittstelle Schritt für Schritt aufbauen. In verschiedenen Versionen des gleichen Programmes analysieren wir dabei die einzelnen Schritte und Randbedingungen. Neben den Kenntnissen über die Programmierung werden wir auch die verwendeten Controllerbausteine und sogar IBM-PC-spezifische Spezialitäten wissen müssen. Soweit dies in einem Buch über C möglich ist, werden sie innerhalb der einzelnen Funktionen erläutert.

Das erste Beispiel definiert nur eine leere Bedienfunktion. Sie wird mit dem speziellen Schlüsselwort interrupt als besondere Funktion gekennzeichnet. Für derartige Funktionen erzeugt der Compiler speziellen Code am Beginn und am Ende der Funktion. Damit wird sichergestellt, daß die Funktion zur Laufzeit kein Prozessorregister verändert und auch den Prozessorstatus (das Flag-Register) am Ende wieder restauriert. Dies ist notwendig, da beim Aufruf einer solchen Funktion neben der Rückkehradresse auch der Prozessorstatus am Stack gesichert wird.

Mit getvect() holt das Programm (im Bild 10) den momentanen Eintrag aus der Unterbrechungstabelle und speichert ihn für die Dauer des Programmablaufes in einer Hilfsvariablen. Um die Hilfsvariable leicher definieren zu können, wurde ein eigener Datentyp definiert. isra ist ein Zeiger auf eine Interruptfunktion ohne Parameterprüfung und mit ungültiger Rückgabe.

Die Adresse der eigenen Bedienfunktion wird mit setvect() eingetragen. Die Nummer setzt sich aus der Unterbrechungsnummer an der Unterbrechungssteuerung (PIC) und dessen Basisnummer (8) zusammen.

Um den Erfolg zu überprüfen wird ein Hilfszeiger definiert, mit dem man auf den gesamten Adreßbreich zugreifen kann (far-pointer). Mit dem Makro MK_FP() kann dieser Zeiger dann vorbesetzt werden. Das Makro akzeptiert dabei die beiden Bestandteile der Adresse einer Intel-CPU, Segment und Offset, und bildet daraus eine 32-Bit Adresse.

Die sleep()-Funktion wartet die angebene Zahl von Sekunden. sleep() ist keine ANSI-Funktion, ist aber unter UNIX und DOS weit verbreitet.

Bild 10-10: Setzen von neuen Funktionsadressen

Am Schluß stellt das Programm den alten Zustand wieder her.

Das folgenden Beispiel (Bild 11) zeigt die nächste Stufe. Es enthält eine Bedienfunktion. Diese Bedienfunktion soll mit Hilfe eines sogenannten Softwareinterrupts aufgerufen werden. Ein Softwareinterrupt ist ein indirekter Funktionsaufruf mit Hilfe der Unterbrechungstabelle, der sich aber ansonsten wie ein Hardwareinterrupt verhält.

Die einzige Aufgabe, die die Bedienfunktion erledigt, ist das Inkrementieren eines globalen Zählers. Der kann dann im Hauptprogramm abgefragt werden. In diesem Fall wird noch kein echter Interrupt bearbeitet. Der Aufruf der Bedienfunktion geschieht daher wie bei einer normalen Funktion im Ablauf des Programmes.

Bild 10-11: Test des Handleraufrufes mit SW-Interrupt

Die Funktion delay() (im Bild 11) versucht, die angebene Anzahl von Millisekunden zu warten. Je nach der Genauigkeit der internen Uhr kann aber der Wert nicht immer genau eingehalten werden. Die Funktion ist wieder bei DOS und UNIX wohlbekannt, aber nicht bei ANSI.

Bild 10-12: Zählen der Mausinterrupts

Das dritte Beispiel (im Bild 12) realisiert einen Zähler für die Bytes, die von der Maus angekommen sind. Dabei setzt das Programm voraus, daß die gesamte Einstellung der Hardware bereits vom Maustreiber vorgenommen wurde und ersetzt nur die Behandlungsfunktion des Maustreibers durch die eigene. Es wird dabei angenommen, daß die Maus über eine serielle Schnittstelle angeschlossen ist.

Im Hauptprogramm (Zeile 30) wird zuerst festgelegt, welche Schnittstelle verwendet wird. Die COM1 würde durch einen Index "0" beschrieben, die COM2 durch "1". Danach installiert installint() den neuen Vektor. Solange nun keine Taste vom Benutzer gedrückt wird, prüft das Hauptprogramm eine Statusvariable (oder: ein Flag), die einen eingetroffenen Interrupt anzeigt. In diesem Fall wird der Zähler ausgegeben. Am Bildschirm wird somit fortlaufend die Anzahl der eingetroffenen Unterbrechungen angezeigt.

Die Bedienfunktion der Unterbrechung mußte nun stark geändert und dem Hardwareaufruf angepaßt werden. Die Bedienfunktion inkrementiert einen Aufruzähler und setzt eine Flagvariable, um ddie Bearbeitung einer Unterbrechung anzuzeigen. Sowohl der Zähler als auch das Flag wurden als volatile definiert. Damit zeigen wir dem Compiler an, daß er keine Optimierungen beim Zugriff durchführen soll. Dies ist für Variable üblich, die aus einer Unterbrechungsfunktion heraus verändert werden.

Der Grund für die Unterbrechung ist vermutlich der Empfang eines Bytes von der Maus. Um die Anforderung der Unterbrechungsbedienung wieder zurückzusetzen, liest die Unterbrechungsbedienung das Datenempfangsregister der seriellen Schnittstelle. Da die serielle Schnittstelle verschiedene Unterbrechungen generieren kann, ist dies eine nicht immer richtige Annahme. Im nächsten Beispiel werden wir die korrekte Bedienung einführen.

Am Ende einer Unterbrechungsbedienung muß die Bedienfunktion dem Unterbrechungscontroller das Ende anzeigen. Dies ist notwendig, da der Controller immer über die höchstwertige, momentan bediente, Unterbrechungsanforderung informiert sein muß.

Betätigt man eine Taste, wird der alte Unterbrechungsvektor wieder in die Tabelle eingesetzt und das Programm beendet. Sollten Sie eine Busmaus benutzen oder nicht unter DOS arbeiten, können Sie das Beispiel leider nicht ablaufen lassen.

Die Unterbrechungsfunktion

Die Bedienfunktion für den Interrupt wird ab dem dritten Beispiel (im Bild 12) durch die Hardware aufgerufen,. Sehen wir uns daher die notwendigen Voraussetzungen der Hardware an.

Die serielle Maus enthält einen sogennten Mikrocontroller. Das ist ein kompletter Rechner mit CPU, Speicher und Peripherie in einem einzigen Baustein. Dieser Mikrocontroller fühlt über Lichtschranken (oder andere Sensoren) jede Bewegung der Maus. Die Tastendrücke werden über kleine Schalter gemeldet. Der Mikrocontroller betrachtet jede Änderung der Position oder einer Taste als Ereignis und sendet ein Datenpaket seriell zur Schnittstelle des PCs.

Hier kommen bei jeder Mausbewegung Daten an. Jedes einzelne Byte führt zu einem Unterbrechungssignal der seriellen Schnittstelle (SIO), das an der Interruptcontroler (PIC-programmable interrupt controller / programmierbarer Unterbrechunsgcontroller) weitergeleitet wird. Von den acht möglichen Eingängen benutzt COM1 den Eingang 4 und COM2 den Eingang 3.

[Interrupt Controller]

Bild 10-13: Der Aufbau des Unterbrechungssystems

Falls einer der Eingänge aktiv ist, sendet der PIC ein Signal zum Prozessor (INTR- interrupt request / Anforderung der Unterbrechung). Kann der Prozessor die Unterbrechung bedienen, sendet er ein INTA-Signal (interrupt acknowledgement / Bestätigung). Daraufhin sendet der PIC ein Nummer zum Prozessor, die sich aus seiner eigenen Idendifikation (mit 5 Bits) und der Nummer des aktiven Eingangs (mit 3 Bits) zusammensetzt.

Mit dieser Nummer wählt der Prozessor dann eine Adresse aus der Vektortabelle und ruft die damit bestimmte Funktion auf. Dies geschieht ähnlich einem Aufruf einer Interruptfunktion mit int86().

Bild 10-14: Auswertungen der Unterbrechungen der SIO

[Interrupt Tabelle]

Bild 10-15: Aufbau der Unterbrechungstabelle <$&inttab1.gem[v]>

Sehen wir uns die Unterbrechungsbedienung nun in der vollständigen Form an. Um die Darstellung zu erleichtern, werden eine Informationsdatei und zwei Implementierungsdateien eingeführt.

Bild 10-16: Informationsdatei zum Beispiel sioint3.c

Im Hauptprogramm wird der Unterbrechungsvektor gesetzt und wie bisher auf das Eintreffen einer Unterbrechung gewartet. Die Bedienfunktion wird, wie bisher auch, im Index 8 + Unterbrechungsnummer eingesetzt. Der Grund ist, daß der Unterbrechungscontroller seine Bedienfunktionen ab dem Index 8 erwartet.

Die Informationsdatei sio1.h enthält die #include-Anweisungen für die benötigten weiteren Informationsdateien, zwei Aufzählungstypen und die Typdefinition für die Bedienfunktion sowie schließlich die Deklaration der Bedienfunktion. Aufzählungstypen definieren konstante Namen für ihre Mitglieder. Jedem Mitglied wird, beginnend mit 0, ein Wert zugewiesen. In unserem Fall erhalten die Namen der Register die Werte von 0 bis 7. Die Konstanten können daher später bei Zugriffen auf die Register verwendet werden und die Adressierung verständlicher gestalten.

Bild 10-17: Die verbesserte Interruptbedienung

Die verbesserte Unterbrechungsbedienung liest in einer Schleife das Unterbrechungsidendifizierungsregister ein.

[Interrupt Auswertung]

Bild 10-18: Aufbau der Unterbrechungserkennung

Ein Register (das IIR) liefert die Information, ob tatsächlich eine Unterbrechung vorliegt und in zwei Bits, welche der vier möglichen Unterbrechungen die momentan wichtigste ist. Die Schleife in der Unterbrechungsfunktion liest und bearbeitet möglicherweise mehrere Anforderungen nacheinander.

Ist das erste Bit gesetzt, kann die Funktion die Schleife mit break verlassen und die Bedienung als beendet melden. Momentan soll die Funktion nur für jeden der möglichen Unterbrechungsquellen einen Zähler mitführen. Nur die leseunterbrechung wird bedient.

Beispiel mit vollständiger Unterbrechungseinstellung

In der letzten Ausbaustufe soll dann die die gesamte Handhabung der Unterbrechunge vom eigenen Programm übernommen werden. Noch wurde ja die serielle Schnittstelle vom Maustreiber gesetzt.

Vielleicht fragen Sie sich, warum ausgerechnet der Maustreiber hier als Beispiel verwendet wurde. Die Antwort ist einfach. Die Maus ist eines der wenigen Geräte am PC, die normalerweise vorhanden sind und mit Hilfe der Unterbrechungssteuerung arbeiten. Ohne die Unterstützung durch die Hardware wäre es nicht möglich, einen lebendigen Mauszeiger über den Bildschirm zu führen. Schließlich läuft ja primär ein anderes Programm.

Im abschließenden Beispiel soll nun auch die SIO in die Initialisierung einbezogen werden. Um die Diskussion zu erleichtern, wurden die einzelnen Funktionen in eigene Rahmen gesetzt. Im Quellcode stehen sie jedoch zusammen in einer Datei.

Es wurde zur Beschreibung der SIO eine Struktur eingeführt, die im Bild 23 beschrieben wird.

Die Installationsfunktion (Bild 19) schaltet zuerst am Interrupt-Controller die eigene Unterbrechung ab. Dies ist eine übliche Vorsichtsmaßnahme. Dazu wird das entsprechende Bit im IMR (interrupt mask register) des i8259A gesetzt. Die folgende Zuweisung hat nur den Sinn, etwas Zeit zu gewinnen, da die Controller zwischen Zugriffen etwas Zeit benötigen. Die nächste Aktion löscht einen möglicherweise anstehende Unterbrechungsanforderung. Auch dies ist wieder eine Vorsichtsmaßnahme, die im Normalfall nie benötigt wird.

Dazu schreibt das Programm eine Leseanforderung (Zeile 9). Falls in der eingelesenen Bitmaske das Bit gesetzt ist, das eine aktive Unterbrechungsbedienung anzeigt, dann wird es durch einen speziellen Endebefehl mit Angabe der eigenen Unterbrechungsnummer gelöscht.

Der bisher gesetzte Unterbrechungsvektor ( sprich Funktionsadresse) wird in geholt und in der Variablen oldint gerettet. Die Adresse (oder Vektor) der eigenen Bedienfunktion schreibt setvect() in den richtigen Tabelleneintrag. Nun setzt sioSetParam() die Schnittstelle auf die Parameter, die eine normale Microsoft-Maus erwartet.

In der seriellen Schnittstelle werden zwei Unterbrechungsquellen freigegeben. Dies sind der Empfangsinterrupt und der Empfängerstatus-Interrupt. Der letztere tritt im Falle von Übertragungsfehlern auf.

Die letzte Tätigkeit der Installationsfunktion schaltet die Unterbrechungsbedienung am Interrupt-Controller wieder ein. Nun kann die Hardware Unterbrechungen erzeugen und der Prozessor mit der entsprechenden Unterstützung durch den Controller die Unterbrechungen durch den Aufruf der passenden Bedienfunktion bearbeiten.

Das Gegenstück zur Installation am Beginn ist die Restaurierung der alten Umgebung am Ende. Dies erledigt die Funktion restoreint(). Sie schaltet die Unterbrechungen wieder ab und setzt den geretteten Unterbrechungsvektor wieder ein. Danach bleibt die Unterbrechung abgeschaltet. Soll das Beispiel mit einem Maustreiber funktionieren, der in anderen Programmen weiter benutzt wird, dann muß am Ende von restoreint() der Mausinterrupt wie in der Installationsfunktion wieder freigegeben werden.

Die Bedienfunktion ist wieder etwas umfangreicher geworden. Zuerst wird wieder der allgemeine Unterbrechungszähler count erhöht und das Unterbrechungsflag gesetzt.

In der schon bekannten Schleife werden nun die möglichen Anforderungen abgearbeitet. Hier werden alle Quellen berücksichtigt und die zugehörigen Anforderungen durch das lesen der entsprechenden Register zurückgesetzt.

Die vollständige Behandlung dieser Details muß dem Datenblatt vorbehalten bleiben. Im Falle eines fehlerhaft empfangenen Bytes wird das Empfangsregister gelesen, um dieses Byte aus der Eingabe zu entfernen. Am Ende der Bedienung steht wieder die allgemeine Endekennung. Diese Endekennung kann der Interrupt-Controller selber auswerten. Im Gegensatz dazu wäre auch eine spezielle Endekennung möglich, die genau angibt, welche Bedienung beeendet wurde.

Das Hauptprogramm wurde im Vergleich zu den vorhergegangenen etwas verändert.

Die Beschreibung einer seriellen Schnittstelle wurde in einer strukturierten Variablen zusammmengefaßt. Dieses Vorgehen unterstützt eine klare Gliederung von Programmen. Das Arbeiten mit Strukturen wird in der objektorientierten Programmierung zum Bearbeiten von Objekten ausgebaut.

Das beschriebene Programm wurde an einem Rechner getestet, der eine Busmaus besitzt. Die Busmaus wird vom normalen Maustreiber bedient. Zusätzlich wurde eine serielle Maus während des Betriebs an die im Programm benutzte serielle Schnittstelle gesteckt und danach das Zählprogramm gestartet.

So blieb die normale Funktion der Maus intakt und die zweite Maus spielte den Erzeuger von Unterbrechungen, die im Zählprogrammbehandelt wurden.

Grenzen der Darstellung

Die Darstellung einer so Hardware-nahen Programmierung birgt sicher Probleme in sich. Den Anwendungsprogrammierern mag die Darstellung zu technisch, den Entwicklern von Hardware zu oberflächlich erscheinen. Trotzdem bietet die Programmierung eines seriellen Maustreibers ein interessantes und oft nachvollziehbares Beispiel für die Leistungsfähigkeit von C.


Anhang zur WWW-Version:

In den folgenden Links wurden die verwendeten Programme jeweils als Ganzes zusammengefaßt, nicht als Teile, wie ursprünglich im Buch. Zur Benutzung möchte ich vorschlagen, die folgenden Programme zu laden, auszudrucken und zusammen mit dem Text zu verfolgen.

Programmversion 0

Programmversion 1

Programmversion 2

Programmversion 3

Programmversion 4

Programmversion 5


Zum Inhalt