Ein Computerterminal ist kein klobiger alter Fernseher, vor dem eine Schreibmaschinentastatur liegt. Es ist eine Schnittstelle, an der sich K�rper und Geist mit dem Universum zusammenschalten und Teile davon durch die Gegend bewegen k�nnen. Douglas AdamsDie verf�gbare Hardware f�r LINUX-Rechner ist vielf�ltig. So wird auch vielf�ltige Software verlangt, um diese Hardware anzusprechen. Ger�tetreiber sind also das Salz in der Suppe eines jeden Betriebssystems.
In UNIX-Systemen soll zudem die Hardware des Rechners vor dem Anwender versteckt werden, ohne da� ihre Funktionalit�t eingeschr�nkt wird. Dies geschieht, indem physische Ger�te durch Dateien repr�sentiert werden. So kann man portable Programme entwickeln, die sowohl auf verschiedene Ger�te als auch auf Dateien mit denselben Systemrufen, wie etwa read() und write(), zugreifen k�nnen. Zu diesem Zweck sind in den LINUX-Kern Ger�tetreiber integriert, die die alleinige Kontrolle �ber die Hardware aus�ben.
Ist demnach ein Ger�tetreiber fehlerfrei implementiert, kann das entsprechende Ger�t durch den Anwender nie falsch benutzt werden. Die Schutzfunktion der Ger�tetreiber ist also nicht zu untersch�tzen.
In diesem Kapitel soll es uns deshalb darum gehen, die Funktionsweise und die richtige Implementation von Ger�tetreibern zu zeigen. Als Beispiel sei der PC- Speaker-Treiber gew�hlt, der die Ausgabe von Sound-Samples auf dem internen Lautsprecher oder einem Digital-Analog-Wandler an der parallelen Schnittstelle unterst�tzt. Zus�tzlich soll er noch zum Soundkartentreiber kompatibel sein.
Da im LINUX-Kern mehrere Ger�tetreiber nebeneinander existieren m�ssen, werden sie anhand der Major-Nummer eindeutig identifiziert. Ein Ger�tetreiber kann mehrere physische und virtuelle Ger�te, zum Beispiel mehrere Festplatten und Partitionen, verwalten. Deshalb wird das einzelne Ger�t durch die Minor-Nummer, eine Zahl zwischen 0 und 255, angesprochen.
Die Ausnahme dieser Regel ist hierbei der Ger�tetreiber f�r Terminals und serielle Schnittstellen. Er nutzt die zwei Major-Nummern 4 und 5. Die Ger�te mit der Major-Nummer 4 sind die virtuellen Konsolen, die einfachen seriellen Schnittstellen (Call-In-Ger�te) und die Pseudoterininals[1]. Dabei besitzen die virtuellen Konsolen die Minor-Nummern 0 f�r tty0 bis 63[2]. Das spezielle Ger�t /dev/tty0 bzw. /dev/console entspricht dabei der jeweils aktuellen virtuellen Konsole.
F�r jede serielle Schnittstelle gibt es zwei logische Ger�te, die Dial-In-Ger�te ttySn und die Call-Out-Ger�te cuan. Ein Proze�, zum Beispiel getty, wird beim �ffnen des Dial-In-Ger�ts solange blockiert, bis die DTR-Leitung der Schnittstelle aktiv wird. Ein das Call-Out-Ger�t �ffnender Proze�, meist ein herausw�hlendes Programm, erh�lt sofort Zugriff auf die serielle Schnittstelle, wenn sie von keinem anderen Proze� genutzt wird. Damit wird ein Proze�, der das Dial-In-Ger�t �ffnen wollte, auch weiterhin blockiert. Die seriellen Dial-In-Ger�te erhalten die Minor-Nummern 64 f�r ttyS0 bis 127.
Die verbleibenden Minor-Nummern von 128 bis 255 werden f�r Pseudoterminals genutzt. Das Master-Pseudoterminal ptyn hat dabei die Minor-Nummer 128+n, w�hrend das zugeh�rige Slave-Terminal ttypn die Minor-Nummer 192+n besitzt.
Die Major-Nummer 5 ist f�r das aktuelle Terminal sowie f�r die Call-Out-Ger�te reserviert. Das Ger�t /dev/tty mit der Minor-Nummer 0 entspricht immer dem dem Proze� zugeh�rigen Terminal. Die Call-Out-Ger�te cuan haben die entsprechenden Minor-Nummem 64+n und unterscheiden sich somit nur durch die Major-Nummer von ihren "Zwillingen".
Die zur Zeit fest vergebenen Major-Nummern k�nnen Tabelle 7.1 entnommen werden. Bei der Entwicklung eigener Treiber sollte man zun�chst eine noch nicht benutzte Major-Nummer ermitteln. Bei einer sp�teren Ver�ffentlichung des eigenen Treibers sollte er beim LlNUX Device Registrar[3] registriert werden. Dieser vergibt dann eine offizielle Major-Nummer, die garantiert von keinem anderen Ger�tetreiber verwendet wird. So erhielt der PC-Speaker-Treiber die Major-Nummer 30.
Major Ger�tetyp 0 unnamed f�r das proc-FS, NFS, usw. 1 b/c Speicherger�te (rd* / mem, null,...) 2 b Disketten(fd*) 3 b IDE-, MFM- und RLE-Festplatten (hd*) 4 c Terminals (pty*, tty?*) 5 c Terminals(tty, cua*) 6 c Parallele Schnittstellen (lp *) 7 b Bandger�te (nicht SCSI) ungenutzt 8 b SCSI-Festplatten (sd*) 9 c SCSI-Tapes (<n>st*) 10 c Busm�use (bin, psaux) 11 b SCSI-CD-ROM (scd*) 12 b QIC02-Tape (rmt*, tape*) 13 b Festplatten an XT-8-Bit-Controler (xd*) 14 c Soundkarten (audio, dsp,...) 15 b/c Cdu31a CD-ROM / Joystick (js*) 16,17,18 c Netzwerk, AF_UNIX, AF_NET 21 b SCSI Generic 22 b 3. und 4. IDE-Festplatte 23 b Mitsumi CD-ROM (mcd*) 24 b Sony535 CD-ROM 25 b Matsushita CD-ROM (sbpcd*) 27 c QIC117-Tape 30 c PC-Speaker-Treiber (pcaudio,pcsp,...) 31 c Link-Interface Tabelle 7.1: Die LINUX-Major-Liste[1]Pseudoterminals sind Ger�tepaare von Master- und Slaveterminals, die zusammen wie eine Terminaleinheit agieren. Das Slave-Terminal ist das Interface, das sich wie eine Terminaleinheit gegen�ber einem Nutzerprogramm verh�lt, w�hrend der Master die Gegenseite (beim Terminal den Nutzer) repr�sentiert (siehe [Ste92b]).
Als Blockger�te werden Ger�te bezeichnet, auf die man wahlfreien Zugriff hat, d.h.von denen beliebige Bl�cke gelesen und geschrieben werden k�nnen. Unter LINUX werden diese Lese- und Schreibzugriffe transparent vom Cache abgewickelt. F�r Dateisysteme ist der wahlfreie Zugriff unbedingt erforderlich. Sie k�nnen deshalb nur auf Blockger�te untergebracht werden.
Zeichenorientierte Ger�te[4] wiederum sind Ger�te, die meist nur sequentiell arbeiten und auf die deshalb ungepuffert zugegriffen wird. In diese Klasse f�llt die gebr�uchlichste Hardware, wie etwa Soundkarten, Scanner, Drucker usw., auch wenn sie intern mit Bl�cken[5] arbeiten. Diese Bl�cke besitzen jedoch sequentielle Natur. Auf sie kann auch nicht wahlfrei zugegriffen werden.
Ansonsten weicht LINUX hier etwas von der allgemeinen UNIX-Philosophie ab, da hier keine so strikte Trennung von Block- und Zeichenger�ten vorgenommen wird. So existieren in anderen UNIX-Systemen zu den Blockger�ten korrespondierende Zeichenger�te, d.h. zeichenorientierte Schnittstellen zu Blockger�ten, die haupts�chlich zur Steuerung[6] des eigentlichen Ger�ts benutzt werden. In LINUX ist die Schnittstelle (VFS) der Block- und Zeichenger�te gleich, weshalb keine zus�tzlichen Zeichenger�te ben�tigt werden.
Ein einzelnes Ger�t ist demnach eindeutig durch den Ger�tetyp (Block- oder Zeichenger�t), die Major-Nummer des Ger�tetreibers und seine Minor-Nummer identifiziert. Das Anlegen eines Ger�ts geschieht deshalb einfach durch:
# mknod /dev/name type major minormit dem Ger�tetyp type b oder c.
Will man zus�tzliche Hardware unter LINUX ansprechen, so wird man demnach in der Regel einen Zeichenger�tetreiber entwickeln, da die zeichenorientierte Hardware in der �berzahl ist.
[4]im folgenden einfach als Zeichenger�te bezeichnet
[5]Sollen gr��ere Datenmengen �bertragen werden,
ist ein Blocktransfer g�nstiger, wie z.B. beim DMA-Betrieb.
[6]Kontrollprogramme von Blockger�ten bei anderen UNIX-Systemen,
wie etwa mkfs oder fsck, operieren auf dem entsprechenden
zeichenorientierten Raw-Device.
Der Ger�tetreiber f�r die parallele Schnittstelle arbeitet standardm��ig im Pollingbetrieb (siehe Abschnitt 2.3). So fragt er die Schnittstelle (in diesem Fall den Statusport der Schnittstelle) solange ab, bis sie ein weiteres Zeichen lpchar entgegennehmen will und �bergibt dann erst das Zeichen an die Schnittstelle. Dieses Vorgehen hat in den Quellen folgendes Aussehen:
#define LP_B(minor) lp_table[(minor)].base /* IO address */ #define LP_S(minor) inbp(LP_B((minor)) + 1) /* status port */ #define LP_CHAR(minor) lp_tab1e[(minor)].chars /* busy timeout */ static int lp_char_polled(char lpchar, int minor){ int status=0; unsigned long count=0; do{ status=LP_S(minor); count++; if (need_resched) schedule(); }while(!(status & LP_PBUSY) && count < LP_CHAR(minor)); ... outb_p(lpchar, LP_B(minor)); ... return 1; }Das Mitz�hlen der Abfragen gilt dem Erkennen eines Fehlers des Datenendger�ts (das ist in den meisten F�llen ein Drucker). Es entspricht dem Timeout und bedeutet, da� das letzte Zeichen nicht gesendet wurde. Die weitere Behandlung dieses Timeouts f�hrt dann zu den Meldungen: "lpn off-line", "lpn out of paper" oder "lpn on fire". Die Anzahl LP_CHAR(minor) ist standardm��ig auf LP_INIT_CHAR (1000) gesetzt und kann mittels ioctl() ge�ndert werden.
So wird ein Proze�, der auf die parallele Schnittstelle im Interruptbetrieb schreiben will, vom Ger�tetreiber im Interruptbetrieb nach dem Schreiben eines Zeichens mit der Funktion
interruptible_sleep_on(&lp->lp_wait_q);angehalten. Kann die parallele Schnittstelle weitere Zeichen entgegennehmen, l�st sie einen IRQ aus. Die behandelnde ISR weckt den Proze� daraufhin wieder und der Vorgang wiederholt sich. Die ISR ist dabei sehr einfach gehalten.
static void lp_interrupt(int irq){ struct lp_struct *lp &lp_table[0]; struct lp_struct *lp_end = &lp_table[LP_NO]; while (irq != lp->irq){ if (++lp >= lp_end) return; } wake_up(&lp->lp_wait_q); }Zuerst wird die Schnittstelle ermittelt, die den Interrupt ausl�ste, und danach der wartende Proze� mit wake_up () "wachgek��t".
Ein weiteres Beispiel ist die serielle Maus, die bei jeder Bewegung Daten an den seriellen Port �bertr�gt, der einen IRQ ausl�st. Erst die behandelnde ISR liest die Daten aus dem seriellen Port aus und stellt sie dem Anwendungsprogramm zur Verf�gung.
Wie in Abschnitt 3.1.10 erw�hnt, gibt es unter LINUX zwei M�glichkeiten der IRQ-Bearbeitung. Langsame IRQs werden mit Hilfe der Funktion
int request_irq(unsigned int irq, void (*handler) (int))installiert. Sie liefert 0 zur�ck, falls der IRQ frei war und belegt werden konnte.
Im Gegensatz zu dieser Definition wird der ISR als Argument jedoch ein Zeiger auf die Struktur pt_regs als Argument �bergeben. Diese Struktur enth�lt alle Register des Prozesses, der durch den IRQ unterbrochen wurde. Auf diese Weise stellt z.B. der Timerinterrupt fest, ob ein Proze� im Kern- oder im Nutzermodus unterbrochen wurde und z�hlt die jeweilige Zeit f�r die Abrechnung hoch. Die Behandlungsroutine eines langsamen IRQs hat also folgendes Aussehen.
void do_irq(struct pt_regs *regs);Langsame Interrupts laufen mit zugeschaltetem Interruptflag, d.h. sie k�nnen wiederum durch andere Interrupts unterbrochen werden. Am Ende eines langsamen Interrupts l�uft derselbe Algorithmus wie beim Beenden eines Systemrufs ab.
Die zweite Art von IRQs sind die schnellen IRQs. Um diese Klasse von ISRs zu installieren, wird die Funktion
int irqaction(unsigned int irq, struct sigaction *new)benutzt. Sie ist eine allgemeinere Variante[7] der Funktion request_irq() und hat ihren Ursprung in der �berlegung, da� IRQs und Signale �hnlich zu behandeln sind. Aus der Struktur
typedef void (*__sighandler_t) (int); struct sigaction{ __sighandler_t sa_handler; sigset_t sa_mask; int sa_flags; void (*sa_restorer) (void); };werden nur die beiden Komponenten sa_handler und sa_mask benutzt. Ist in der Maske sa_mask das Flag SA_INTERRUPT gesetzt, wird die ISR als schneller Interrupt installiert, sonst wie oben beschrieben als langsamer Interrupt.
Einem schnellen Interrupt wird als Integer-Argument beim Aufruf der Funktion sa_handler jeweils die Nummer des IRQs mitgegeben. Man kann also eine ISR f�r mehrere IRQs benutzten. Schnelle Interrupts laufen au�erdem mit abgeschaltetem Interruptflag. Will man in seiner Interruptroutine also auch andere Interrupts zulassen, mu� ein Aufruf des Makros
sti();erfolgen.
Ein Beispiel soll die Installation eines schnellen Interrupts, d.h. des oben beschrieben lp_interrupt, zeigen.
sa.sa_handler = lp_interrupt; sa.sa_flags = SA_INTERRUPT; sa.sa_mask = 0; sa.sa_restorer = NULL; ret = irqaction(irq, &sa); if (ret){ printk("unable to use interrupt %d, error %d\n", irq, ret); return ret; }Normalerweise wird man also f�r die Kommunikation mit der Hardware schnelle Interrupts verwenden.
[7]Genaugenommen ruft request_irq() die Funktion irqaction() auf.
F�r die Installation eines Bottom Halfs existiert keine Funktion im Kern; man mu� sie selbst in die Tabelle bh_base eintragen.
struct bh_struct{ void (*routine) (void *); void *data; }; struct bh_struct bh_base[32]; enum TTMER_BH = 0, CONSCLE_BH, SERIAL_BH TTY_BH, INET_BH, KEYBOARD_BH };Wie aus der Definition der Struktur bh_struct folgt, kann man au�erdem dem Bottom Half als Argument einen Zeiger auf beliebige Daten �bergeben (data). Standardm��ig sind alle Bottom Halfs zugelassen, sie k�nnen aber auch mit den Funktionen
void disable_bh(int nr); void enable_bh(int nr);ab- und wieder zugeschaltet werden. Die Funktion
void markbh(int nr);dient zum Markieren eines Bottom Halfs, d.h. dieser Bottom Half wird zum n�chstm�glichen Zeitpunkt abgearbeitet.
Betrachten wir nun die Verwendung eines Bottom Halfs.
Als Beispiel sei hier der Keyboard-
static void keyboard_interrupt(int int_t_regs){ ... mark_bh(KEYBOARD_BH); /* kbd_bh() wird markiert */ ... } static void khd_bh(void * unused){ ... if (got_break){ /* Test auf Break */ if (tty && !I_IGNBRK(tty)){ if (I_BRKINT(tty)){ flush_input(tty); ... } unsigned long kbd_init(unsigned long kmem_start){ ... /* der Keyboard Bottom Half wird initialisiert */ bh_base[KEYBOARD_BH].routine = kbd_bh; request_irq(KEYBOARD_IRQ,keyboard_interrupt); ... }Die Init-Funktion des Keyboard-Treibers installiert kbd_bh() als Bottom Half und keyboard_interrupt() als langsamen Interrupt. Bei jedem Aufruf des Keyboard-Interrupts wird mark_bh(KEYBOARD_BH) aufgerufen, d.h. der Bottom Half l�uft zum ersten Zeitpunkt nach Beendigung des Keyboard-
Installiert werden Bottom Halfs durch ein schlichtes
bh_base[KRYBOARD_BH].routine = kbd_bh;[8]Das kann durchaus geschehen, wenn z.B. ein langsamer Interrupt von einem anderen unterbrochen wird.
Au�erdem mu� man beim DMA-Betrieb von Ger�ten mit Problemen ganz anderer Art k�mpfen, die zum Teil aus der Kompatibilit�t zum Ur-PC stammen.
Seit den Urzeiten des PCs schon vorhanden, ist der PC-Speaker aufgrund seines Designs nicht gerade gut zur Ausgabe von Samples geeignet. Wie Abbildung 7.1 zeigt, ist sowohl der Aufbau als auch die Programmierung des Speakers sehr einfach.
System Control Latch +---------------+ | | | | | | |1|0|-- +---------------+ | |\ | | | \- +-+ | | | | | |&|------------------ | | | |---| |-- | | | | +-+ | +---------------+ | | /- --|Timerkonstante |-- Restart Gate |/ +---------------+ Abbildung 7.1: Schematischer Anschlu� des PC-SpeakersDer Timerbaustein 8253 besitzt drei interne Timer. Timer 2 ist zur Verwen- dung mit dem PC-Speaker bestimmt. Dazu ist der Ausgang des Timers 2 �ber ein AND-Gatter mit dem Bit 1 des System Control Latches auf I/O-Adresse 0x61 verbunden. Bit 0 dient zum Start bzw. Neustart des Timers 2. Der Speaker kann also nur entweder voll ein- oder ausgeschaltet sein. Normalerweise wird der Timer 2 als Frequenzteiler programmiert (d.h. beide Bits sind gesetzt). Dadurch werden Rechteckwellen erzeugt, die den "typischen" Klang des internen Lautsprechers ausmachen. Die Frequenz entsteht durch Teilung der Timer-Grundfrequenz von 1,193 MHz (=4,77MHz/4) durch die eingestellte Timerkonstante.
Um ein analoges Signal �ber den Speaker �bertragen zu k�nnen, wird die
Puls-
Als das eigentliche Problem bei der Verwendung der Puls-L�ngen-
Der Timerbaustein besitzt 4 I/O-Ports. Port 0x43 ist das Mode Control Register. Die Daten-Ports 0x40 bis 0x42 sind den Timern 0 bis 2 zugeordnet. Um einen Timer zu programmieren, mu� also ein Kommando nach 0x43 und die Timerkonstante in den entsprechen Daten-Port geschrieben werden. Ein Kommando ist sehr einfach aufgebaut. Die Bits 7 und 6 enthalten die Nummer des zu programmierenden Timers, 5 und 4 eine der in Tabelle 7.2 aufgezeigten Zugriffsarten und die Bits 3 bis 1 den Timer-Modus.
Timer- 1|-- -- -- -- Ausgang | _____ _____ _____ ___ 0|__________________________ ***** L�nge des Intervalls, wird durch Timerkonstante 2 festgelegt Reset 1| --- --- --- -- Gate |__ ____ ____ ____ _ 0|__________________________ ******* Konstantes Intervall, wird durch Tirner 0 erzeugt Intervall�nge = Timerkonstante/1193180 sec Abbildung 7.2: Puls-L�ngen-Modulation mit Hilfe der Timer 0 und 2Um z.B. einen Ton mit 10000Hz zu erzeugen, sind folgende Schritte notwendig:
Leider ist im Standard-PC nur der Timer 0 interruptf�hig, so da� die beschriebene zweite M�glichkeit nicht ganz ungef�hrlich ist, wird doch der f�r LlNUX so wichtige Timerinterrupt IRQ 0 ver�ndert. Die neue Interrupt-Routine mu� daf�r sorgen, da� die urspr�ngliche Prozedur in genau denselben Zeitintervallen wieder aufgerufen wird. Au�erdem ben�tigt die Interruptbehandlung im Protected Mode weitaus mehr Zeit als im Real Mode, so da� durch die gr��ere Anzahl ausgel�ster Interrupts die Rechenzeit merklich verbraucht wird.
Bits Mode Erkl�rung 54 00 Latch der Z�hier wird in ein internes Register �bertragen und kann danach ausgelesen werden 01 LSB only nur die unteren 8 Bit des Z�hiers werden �bertragen 10 MSB only nur die oberen 8 Bit des Z�hlers werden �bertragen 11 LSB/MSB zun�chst werden die unteren, danach die oberen 8 Bit �bertragen Tabelle 7.2: Bits 4 und 5 des timer-kommandosKommen wir zur�ck zur Puls-L�ngen-Modulation. Wie bereits erw�hnt, ist die Wahl der Zeitintervalle sehr wichtig. Versuche haben gezeigt, da� f�r eine reale Samplerate zwischen 16000Hz und 18000Hz die besten Resultate erzielt werden.
Je h�her die reale Samplerate, desto besser, da diese als Eigenfrequenz (Pfeifen) zu h�ren ist[10]. Diese Frequenzen ergeben bei Benutzung des Timers 2 m�gliche Timerkonstanten zwischen 1 und 74 (eine 0 w�rde 65536 bedeuten und ist deshalb unzul�ssig). Da die Konstanten direkt mit den Samples zusammenh�ngen, kann man also nur 6 Bit (1-65) ausgeben.
Als Maximalwert f�r die reale Samplerate sind also 18357Hz m�glich (dies entspricht 1,193MHz/65). Dieser Wert ist allerdings nicht sehr gebr�uchlich, deshalb werden mit Hilfe zus�tzlich generierter Samples (Oversampling) auch andere Sampleraten unterst�tzt. Aus Zeitgr�nden sorgt ein einfacher Algorithmus daf�r, da� durch die Wiederholung[11] einzelner Samples die Daten "auseinandergezogen" werden. Soll die Ausgabe z.B. mit 10000Hz erfolgen, mu� jedes Sample im Durchschnitt 1,8mal abgespielt werden.
Die Ausgabe �ber Digital-Analog-Wandler (DAC) hingegen ist sehr einfach. Diese werden einfach an einen Parallelport angeflanscht und wandeln die eingehenden 8 Bit in ein Analogsignal um. Da der Parallelport die eingehenden Werte zwischenpuffert, kann der Aufbau eines solchen DACs sehr einfach sein, im gen�gsamsten Falle handelt es sich einfach um ein Widerstandsnetz. Au�erdem kann der Parallelport die Daten in fast beliebiger Geschwindigkeit ausgeben, Timer 0 kann also mit der wahren Samplerate programmiert werden.
Ebenso entf�llt die Transformation der Samples in eine 6-Bit-Darstellung; die Ausgabe �ber DACs ben�tigt deshalb weniger Prozessorzeit als die �ber den internen Lautsprecher. Letzter Pluspunkt f�r diese L�sung: Fehlende Interrupts �u�ern sich nur durch eine Verlangsamung des abgespielten Sounds und sind innerhalb gewisser Grenzen fast unh�rbar.
[9]Das Nichtbehandeln eines einzigen lnterrupts l��t die Dynamik
des Lautsprechers zusammenbrechen.
Dies ist der Grund f�r die Nebenger�usche bei Diskettenzugriffen
oder sogar bei Mausbewegungen.
[10]Ab welcher Eigenfrequenz dieses Pfeifen h�rbar ist,
h�ngt von der jeweiligen Anatomie ab.
Ich h�re erst ab 14500Hz etwas, andere h�ren auch 17000Hz noch.
[11]Normalerweise w�rden die neuen Samples durch Interpolation berechnet.
Bei der Ausgabe durch den internen Lautsprecher ist damit jedoch
keine Qualit�tsverbesserung zu erzielen.
Zudem hat die Marktentwicklung dazu gef�hrt, das verschiedenste Hardware die gleichen I/O-Adre�r�ume belegt. Meist kann man noch mittels Jumpern verschiedene Basisadressen ausw�hlen. Dies ist zwar oft n�tig, verwirrt aber unbedarfte Nutzer, da sich in Dokumentationen meist nur der Hinweis befindet, man sollte "die Standardbelegung beibehalten und im Falle eines Nichtfunktionierens Jumper XX auf Stellung YY" setzen.
Bei der Entwicklung eines Treibers hat man also zun�chst die M�glichkeit des "sicheren" Weges. S�mtliche Parameter werden vor dem Compilieren fest eingestellt. Das ist zwar sehr sicher, aber nicht sehr komfortabel. Wer will schon jedesmal den Kern neu �bersetzen, wenn er einen Jumper umgesteckt hat?
Es sind also Algorithmen gesucht, die Hardware "erkennen". Im Idealfall m��te eine solche Erkennung allein durch Auslesen von I/O-Ports m�glich sein, aber leider ist das bei der Entwicklung neuer Hardware keine Option. Man ist also gezwungen, ins Blaue hinein Werte zu schreiben, I/O-Ports auszulesen und davon abh�ngig seine Entscheidung zu treffen. Meist werden dabei gewisse Besonderheiten einzelner Chips ausgenutzt (sprich Bugs bzw. "unbenutzte Features"), die dann dazu f�hren k�nnen, da� die kompatible Hardware eines anderen Herstellers nicht erkannt wird.
Das bei weitem unangenehmste Problem ist aber, da� das "Probeschreiben" die Funktionsweise anderer Hardware hemmen bzw. das System zum Absturz bringen kann. Der zweite Fall tritt h�ufig bei der Entwicklung eines Treibers auf, denn meist bemerkt man das Nichtfunktionieren eines anderen Ger�ts erst viel sp�ter...
Unter LINUX lassen sich deshalb I/O-Adre�bereiche sperren. Dazu mu� dem Kern beim Start ein Bootparameter �bergeben werden, der alle gesperrten Bereiche enth�lt. Startet das System nach dem Einbau einer neuen Karte also nicht mehr, sollte man zun�chst versuchen, den Adre�raum dieser Karte auszublenden. Ein fiktives Beispiel soll dies verdeutlichen.
Eine Scanner-Karte belege die Adressen 0x300-0x30F (dort k�nnte sich auch eine Netzwerkkarte befinden). Mit Hilfe des Bootparameters
reserve=0x300, 0x10wird dieser Bereich ausgeschlossen.
Will ein Ger�tetreiber also I/O-Ports (schreibend) testen, sollte zun�chst mit Hilfe der Funktion check_region() die Erlaubnis dazu eingeholt werden. Dazu wollen wir ein Fragment des Skeleton f�r Netzwerktreiber betrachten.
#include <LINUX/ioport.h> netcard_probe(struct device *dev){ ... for (port = &ports[0]; *port; port++){ int ioaddr = *port; if (check_region(ioaddr, ETHERCARD_TOTAL_SIZE)) continue; if (inb(ioaddr) != 0x57) continue; dev->base_addr = ioaddr; if (netcard_probe1(dev,ioaddr) == 0) return 0; } dev->base_addr = base_addr; return ENODEV; }Liefert check_region() also einen Wert ungleich 0, darf auf mindestens einen Port in diesem Bereich nicht zugegriffen werden, und ein Test ist zu unterlassen.
Leider bleibt noch das leidige Problem der Erkennung von IRQ- und DMA-Kan�len. Allerdings ist es prinzipiell einfach zu l�sen. Hat man die Karte eindeutig identifiziert, werden einfach alle m�glichen Kan�le alloziert und ein Interrupt ausgel�st. Man mu� sich lediglich merken, welcher ausgel�st wurde und hoffen, das keine andere Hardware aktiv ist.
Zuletzt sei noch erw�hnt, auf welche Weise der PC-Speaker-Treiber Stereo-on- One's erkennt. Da bereits beim Design darauf Wert gelegt wurde (und die drei m�glichen Parallelports gl�cklicherweise auf festen Adressen liegen), ist dies sehr einfach. Das Datenbit 7 wurde mit dem Steuereingang BUSY verbunden. Da dieses Steuersignal invertiert gelesen wird, ergibt sich folgende Funktion.
/* testet, ob sich ein Stereo-on-One an lp(port) befindet */ inline static int stereo1_detect(unsigned port){ outb(0,LP_B(port)); if(LP_S(port) & 0x80){ outb(0xFF,LP_B(port)); return (LP_S(port) & 0x80) ? 0 : 1; } return 0; }[12]Nur die ersten 10 Bit einer Portadresse liegen auf dem Bus. Das bedeutet, da� alle 65536 m�glichen Portadressen auf den Bereich 0-0x3FF abgebildet werden.
Zur Erzeugung von "Ger�uschen" k�nnte auch ein Programm auplay[13] geschrieben werden, welches mit Hilfe des Systemrufes ioperm() die entsprechenden Ports freigibt
if (ioperm(0x61,1,1) || ioperm(0x42,l,1) || ioperm(0x43,1,1) { printf("can't get I/O permissions for internal speaker\n"); exit(-1) }und die Ausgabe der Samples danach selbst �bernimmt. Dies w�rde aber zu mehreren Nachteilen f�hren:
for ( j = 1; j < DELAY; j++)Dieses Busy Waiting ist nicht akzeptabel, da eine genaue Abstimmung der Samplerate nicht m�glich ist. Die Nutzung des Timer-Interrupts ist daf�r eine wesentlich elegantere Variante, kann aber nur im Kern geschehen.
Ein Ger�tetreiber w�re also doch ratsam. Die eigentliche Implementation des PC-Speaker-Treibers l�uft dabei auf das Ausf�llen der im vorigen Kapitel beschriebenen Struktur file_operations hinaus. Der Programmierer mu� dabei je nach Art des Ger�ts nicht alle Funktionen belegen. Zus�tzlich mu� er eine weitere Prozedur zur Initialisierung des Treibers bereitstellen.
Die Namen dieser C-Funktionen sollten alle nach demselben Schema gebildet sein, um Konflikte mit existierenden Funktionen zu vermeiden. Die sicherste Variante ist, einen kurzen Namen des Treibers den eigentlichen Funktionsnamen voranzustellen. So werden f�r den PC-Speaker-Treiber, oder kurz "pcsp", die Funktionen pcsp_init(), pcsp_read() usw. im folgenden genauer erkl�rt.
Das gleiche Vorgehen sollte auch f�r externe und statische C-Variablen Anwendung finden.
[13]Das Programm auplay von RICK MILLER war der Ansto� zur Implementation
des PC-Speaker-Treiber.
[14]Dies stimmt nur bedingt, da man beim Programm auplay �ber die falsche
Benutzung des Mode Control Registers auf I/O-Adresse 0x43 den Timerinterrupt
durcheinander und den Rechner zum Absturz bringen kann.
Diese Kommandozeile wird von der Funktion parse_options(), die sich in init/main.c befindet, in ihre einzelnen Bestandteile zerlegt. F�r jeden dieser Parameter wird die Funktion checksetup() aufgerufen. Diese Funktion vergleicht den Anfang des Parameters mit den im Feld bootsetups[] gespeicherten Strings und ruft bei �bereinstimmung die jeweilige setup-Funktion auf. Ein Parameter sollte dabei den folgenden Aufbau haben:
name=param1,...,paramnDie ersten zehn Parameter versucht checksetup () noch in Integer-Zahlen um- zuwandeln. Gelingt dies, werden sie in einem Feld abgelegt. Index 0 dieses Feldes enth�lt die Anzahl umgewandelter Parameter. Der Rest der Zeile wird einfach als String weitergereicht. Als Beispiel soll hier die setup-Funktion des PC-Speaker-Treibers dienen.
void pcsp_setup(char *s, int *p){ if (!strcmp(s,"off")){ pcsp_enabled = 0; return; } if (p[0] > 0) pcsp.maxrate = p[1]; pcsp_enabled = 1; }Wie zu sehen ist, testet diese Funktion zun�chst das Vorhandensein des Wortes "off". Der Boot-Parameter "pcsp=off" schaltet den PC-Speaker-Treiber also ab. Ist sonst die Anzahl numerischer Parameter nicht 0, wird der erste Parameter p[1] zur Initialisierung einer globalen Variablen des PC-Speaker-Treibers benutzt. Diese Funktion mu� jetzt noch registriert werden. Dazu tr�gt man sie in das Feld bootsetups[] ein, wie die folgenden Zeilen zeigen.
struct{ char *str; void (*setup_func) (char *, int *); }bootsetups[]={ ... #ifdef CONFIG_PCSP {"pcsp=", pcsp_setup}, #endif {0,0} };Bei der Verwendung einer Setup-Funktion sollte man beachten, da� sie vor der Initialisierung der Ger�tetreiber durch ihre init()-Funktion aufgerufen wird. Man sollte in der Setup-Funktion also nur globale Variablen setzen, die dann von der init-Funktion ausgewertet werden k�nnen.
Der Aufruf der init-Funktion mu� in einer der folgenden Funktionen[15], je nach Art des Ger�tetreibers, stattfinden:
long pcsp_init(long kmem_start){Damit LINUX mit dem Treiber �berhaupt etwas anfangen kann, mu� dieser regi- striert werden. Dazu dient die Funktion register_chrdrv(), die
if (register_chrdev(PCSP_MAJOR,"pcsp",&pcsp_fops)) printk("unable tu get major 30 for pcsp devices\n"); else printk("PCSP-device 0.6 init: \n"); ...init() ist auch der richtige Platz, um zu testen, ob �berhaupt ein Ger�t, welches vom Treiber unterst�tzt wird, vorhanden ist.
Dies gilt besonders f�r Ger�te, die nicht w�hrend des Betriebs gewechselt bzw. angeschlossen werden k�nnen, wie etwa Festplatten. Kann kein Ger�t gefunden werden, sollte jetzt der Treiber eine Meldung ausgeben (das Nichterkennen des Ger�ts k�nnte ja auch ein Hardwarefehler sein) und sicherstellen, da� das Ger�t sp�ter auch nicht angesprochen wird.
Findet z.B. ein CD-ROM-Treiber kein CD-Laufwerk, hat es keinen Sinn, da� der Treiber Speicher f�r Puffer belegt - das Laufwerk kann nicht w�hrend des Betriebes hinzukommen. Anders ist dies bei Ger�ten, die sp�ter zugeschaltet werden k�nnen. Wenn der PC-Speaker-Treiber keinen Stereo-on-One[18] erkennt, l��t der Treiber diesen auch sp�ter noch zu.
Wurden ein oder mehrere Ger�te erkannt, sollten diese innerhalb der init-Funktion initialisiert werden, wenn dies notwendig ist.
Au�erdem kann hier bequem Speicher f�r Puffer alloziert werden. Dazu wird der init-Funktion beim Aufruf das Argument kmem_start mitgegeben, das zwar vom Typ long ist, bei dem es sich aber um die h�chste vom Kern bis jetzt benutzte Adresse handelt (siehe Abschnitt 4.4). Diese mu� man sich einfach nur merken und um die ben�tigte Anzahl von Bytes erh�hen (siehe pc speaker_init()). Als R�ckgabewert mu� init() dann die neue Endadresse zur�ckliefern.
pcsp.buf[0] = (unsigned char *)kmem_start; pcsp.buf[1] = (unsigned char *) (kmem_start + ABLK_STZE); return (kmem_start + 2*ABLK_SIZE) }Der hier allozierte Speicher wird allerdings f�r immer belegt und auch nicht ausgelagert. Er l��t sich also f�r Interrupt-Puffer benutzen, ist aber ansonsten f�r Prozesse unbenutzbar. Man sollte sich also �berlegen, wieviel Speicher auf diese Weise alloziert wird.
[15]Die Funktionen werden in start_kernel (Datei init/main.c) in der
angegebenen Reihenfolge aufgerufen.
[16]Ein Stereo-on-One ist ein von MARK J. Cox entworfener einfacher
Stereo-Digital-Analog-Wandler, der aber nur einen Parallelport belegt
und softwarem��ig erkannt werden kann.
static int pcsp_open(struct inode *inode, struct file *file){ if (pcsp_active) return -EBUSY; pcsp.buffer = pcsp.end = pcsp.buf[0]; pcsp.in[0] = pcsp.in[1] = 0; pcsp.timer_on = 0; pcsp_active = 1; return 0;Die release-Funktion wird im Gegensatz zu open() erst aufgerufen, wenn der Dateideskriptor auf das Ger�t freigegeben wird (siehe Abschnitt 6.2.6). Ihre Aufgabe sind Aufr�umaktionen globaler Natur, u.a. das Leeren von Warteschlangen. Bei bestimmten Ger�ten kann es auch sinnvoll sein, zun�chst alle Daten, die sich noch in Puffern befinden, an das Ger�t weiterzuleiten. Im Falle des PC-Speaker-Treibers bedeutet das, da� die Ger�tedatei schon geschlossen werden kann, bevor alle Daten in den Ausgabepuffern abgespielt sind. Die Funktion pcsp_sync() wartet deshalb darauf, da� beide Puffer geleert wurden.
static void pcsp_release(struct inode *inode, struct file *file){ pcsp_sync(); pcsp_stop_timer(); outb_p(0xb6,0x43); /* binary, mode 2, LSB/MSB, ch 2 */ pcsp_active = 0; }Die release-Funktion ist optional; allerdings ist eine solche Konstellation schwerlich vorstellbar.
static int lp_write_polled(struct inode *inode, struct file *file, char *buf, int count){ int retval; unsigned int minor = MINOR(inode->i_rdev); char c, *temp = buf; temp = buf; while (count > 0){ c = get_fs_byte(temp); retval = lp_char_polled(c, minor); ... if (!retval){ /* Fehlerbehandlung */ } } return temp-buf; }Zu beachten ist, da� sich der Puffer buf im Nutzeradre�raum befindet und deshalb Bytes mit Hilfe von get_fs_byte() gelesen werden m�ssen.
Kann ein Datenbyte f�r eine gewisse Zeitspanne nicht �bertragen werden, sollte der Treiber den Versuch aufgeben (Timeout) bzw. es nach einer weiteren Wartezeit noch einmal versuchen. Daf�r kann man den folgenden Mechanismus verwenden.
if (current->signal & ~current->blocked){ if (temp != buf) return temp-buf; else return -EINTR; } current->state = TASR_INTRRRUPTIBLE; current->timeout = jiffies + LP_TIME(minor); schedule();Zun�chst pr�ft man, ob der aktuelle Proze� Signale empfangen hat. Wenn ja, endet die Funktion und gibt die Anzahl der �bertragenen Bytes zur�ck. Danach wird der Proze� durch die erste Zuweisung in den Zustand TASK_INTERRUPTIBLE versetzt und der Zeitpunkt des "Aufwachens" festgelegt. Dazu mu� zum aktuellen Wert von jiffies die minimale Wartezeit in Ticks hinzugef�gt werden. Der Aufruf von schedule() h�lt den Proze� bis nach dem Verstreichen der Zeit oder bis zum Eintreffen eines Signals an. Danach erst kehrt der Programmablauf aus schedule() zur�ck und current->timeaut ist 0, wenn ein Timeout aufgetreten ist.
Betrachten wir nun die write-Funktion des PC-Speaker-Treibers als Beispiel f�r den Interruptbetrieb.
static int pcsp_write(struct inode *inode, struat file *file, char *buffer, int count){ unsigned long copy_size; unsigned long total_bytes_written = 0; unsigned bytes_written; int i; do{ bytes_written = 0; copy_sIze = (count <= ABLK_SIZE) ? count : ABLK_SIZE; i = pcsp.in[0] ? 1 : 0; if (copy_size && !pcsp.in[i]) { memcpy_fromfs(pcsp.buf[i],buffer,copy_size); pcsp.in[i] = copy_size; if (pcsp.timer_on) pcsp_start_timer(); bytes_written += copy_size; buffer += copy_size; } if (pcsp.in[0] && pcsp.in[i]){ interruptible_sleep_an (&pcsp_sleep); if (current->signal & ~current->blocked){ if (total_bytes_written + bytes_written) return tota1_bytes_written + bytes_written; else return -EINTR; } } total_bytes_written += bytes_written; count -= bytes_written; }while (count > 0); return total_bytes_written;In den ersten freien Puffer werden zun�chst mittels memcpy_fromfs() Daten aus dem Nutzerbereich �bertragen. Dies ist unbedingt notwendig, da der Interrupt unabh�ngig vom aktuellen Proze� auftreten kann und man somit die Daten nicht w�hrend des Interrupts aus dem Nutzerbereich holen kann. Der Zeiger buffer w�rde dann ja in den Nutzeradre�raum des jeweils aktuellen Prozesses zeigen. Sollte der entsprechende Interrupt noch nicht initialisiert sein, wird er jetzt eingeschaltet (pcsp_start_timer()). Da die �bertragung der Daten zum Ger�t in der ISR erfolgt, kann write() den n�chsten Puffer f�llen.
Sind alle Puffer voll, mu� der Proze� angehalten werden, bis zumindest ein Puffer wieder frei ist. Dazu wird die Funktion interruptible_sleep_on() verwendet (siehe Abschnitt 3.1.5). Wurde der Proze� durch ein Signal aufgeweckt, so endet write(), sonst geht der Transfer weiterer Daten in den freigewordenen Puffer weiter.
Betrachten wir nun den prinzipiellen Aufbau der ISR.
int pcsp_do_timer(void){ if (pcsp.index < pcsp.in[pcsp.actual]){ /* Ausgabe eines Bytes */ ... } if (pcsp.index >= pcsp.in[pcsp.actual]){ pcsp.xfer = pcsp.index = 0; pcsp.in[pcsp.actual] = 0; pcsp.actual = 1; pcsp.buffer = pcsp.buf[pcsp.actual] if (pcsp_sleep) wake_up_interruptible(&pcsp_sleep) } ... }Solange sich im aktuellen Puffer noch Daten befinden, werden diese ausgegeben. Ist der Puffer leer, wird auf den zweiten Puffer umgeschaltet und mittels wake_up_interruptible() der Proze� wieder aufgeweckt. Das if vor dem Aufruf der Funktion ist eigentlich unn�tig, da wake_up_interruptible() diesen Test selbst vornimmt. Er geschieht an dieser Stelle lediglich aus Zeitgr�nden.
Wie man sieht, pa�t diese ISR nicht in das zuvor erkl�rte Schema von langsamen und schnellen Interrupts. Das liegt daran, da� der Timerinterrupt in LINUX ein langsamer Interrupt ist, aber f�r den PC-Speaker-Treiber aus Geschwindigkeitsgr�nden ein schneller Interrupt ben�tigt wird. Darum enth�lt der PC-Speaker-Treiber eine "dritte" Art, die gewisserma�en schnelle und langsame Interrupts beinhaltet. Die Routine pcsp_do_timer() wird wie ein schneller Interrupt aufgerufen (allerdings mit gesetztem Interruptflag, d.h. unterbrechbar); gibt sie 0 zur�ck, wird der Interrupt beendet. Anderenfalls wird der urspr�ngliche Timerinterrupt als langsamer Interrupt gestartet. Da der urspr�ngliche Timerinterrupt viel seltener aufgerufen werden mu�, bringt dieses Vorgehen einen gro�en Geschwindigkeitsvorteil.
Die ioctl-Funktion erh�lt als Parameter ein Kommando sowie ein Argument. Da unter LINUX
sizeof(unsigned long) == sizeof(void *)gilt, kann als Argument auch ein Zeiger auf Daten im Nutzeradre�raum �bergeben werden. Normalerweise besteht die ioctl-Funktion deshalb aus einer gro�en switch-Anweisung, in der f�r das Argument eine entsprechende Typumwandlung stattfindet. ioctl-Aufrufe ver�ndern zumeist nur Treiber-globale Variablen oder globale Ger�teeinstellungen.
Betrachten wir ein Fragment der ioctl-Funktion des PC-Speaker-Treibers.
static int pcsp_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg){ unsigned long ret; unsigned long *ptr = (unsigned long *)arg; int i, error; switch (cmd){ case SNDCTL_DSP_SPEED: error = verify_area(VERIFY_READ,ptr,4); if (error) return error; arg = get_fs_long(ptr) if (arg<4000 || arg>44100 || ARG>pcsp.maxrate) return -EINVAL; else /* reset default speed */ pcsp_calc_srate (arg); return 0; ... case SNDCTL_DSP_SYNC: pcsp_sync (); pcsp_stop_timer(); return 0; ... } }Das Kommando SNDCTL_DSP_SPEED formt das Argument arg in einen Zeiger um und liest mit seiner Hilfe die neue Samplerate. Danach berechnet die Funktion pcsp_calc_srate() lediglich einige Zeitkonstanten in Abh�ngigkeit von der neuen Samplerate. SNDCTL_DSP_SYNC hingegen ignoriert das Argument v�llig und ruft die Funktion pcsp_sync () auf. Diese Funktion h�lt den Proze� solange an, bis alle Daten, die sich noch in Puffern befinden, abgespielt wurden. Diese Synchronisation ist z.B. n�tig, wenn w�hrend des Abspielens von Audiodaten die Samplerate oder der Abspielmodus (Mono oder Stereo) ge�ndert wird oder die Ausgabe von Audiodaten mit anderen Ereignissen im Proze� synchronisiert werden soll.
Somit l��t sich die ioctl-Funktion auch dazu verwenden, andere Funktionen innerhalb des Treibers, die nicht vom Virtuellen Dateisystem erfa�t werden, auszuf�hren. Ein weiteres Beispiel f�r dieses Verhalten ist im Treiber f�r die serielle Schnittstelle enthalten. Das Kommando TIOCSERCONFIG startet die automatische Erkennung des UART-Bausteins sowie der benutzten IRQs f�r die Schnittstellen.
static int mouse_select(struct inode *inode, struct file *file, int sel_type, select_table *wait){ if (sel_type != SEL_IN) return 0; if (mouse.ready) return 1; select_wait(&mouse.wait,wait); return 0; }Die Aufgabe der select-Funktion besteht in einer �berpr�fung, ob vom Ger�t gelesen (sel_type == SEL_IN) oder Daten an das Ger�t geschrieben werden k�nnen (SEL_OUT). Mit SEL_EX kann man zudem noch auf das Eintreffen einer Ausnahmebedingung warten. Da fast die gesamte Komplexit�t dieser Aufgabe vom Virtuellen Dateisystem �bernommen wird, ist die Aufgabe der select-Funktion einfach zu beschreiben.
Falls das Argument wait gleich NULL ist, soll das Ger�t nur abgefragt werden. Ist es bereit f�r die abgefragte Funktion, soll select() eine 1 zur�ckliefern, sonst eine 0. F�r wait ungleich NULL mu� der Proze� bis zur Verf�gbarkeit des Ger�ts angehalten werden. Dazu wird jedoch nicht sleep_on() verwendet; diese Aufgabe erledigt die Funktion
void select_wait(struct wait_queue **wait_address, select_table *p)Als Argumente erwartet sie eine Warteschlage sowie das letzte der select-Funktion �bergebene Argument. Da select_wait() sofort zuruckkehrt, wenn dieses Argument NULL ist, kann man sich die Abfrage sparen und bekommt einen Funktionsaufbau, wie in der oben gezeigten Beispielfunktion.
Falls das Ger�t verf�gbar wird (im allgemeinen durch einen Interrupt angezeigt), weckt ein wake_up_interruptible(wait_address) den Proze� wieder auf. Dies zeigt der Maus-Interrupt des Treibers.
void mouse_interrupt(int unused){ if (dx != 0 || dy != 0 || buttons != mouse.latch_buttons){ mouse.latch_buttons |= buttons; mouse.dx += dx; mouse.dy += dy; mouse.ready = 1; wake_up_interruptible(&mouse.wait); } ... }
lseek() ist f�r Zeichenger�te nur bedingt sinnvoll, da sie nicht positionieren k�nnen. Da jedoch die Standardfunktion lseek() im Virtuellen Dateisystem keine Fehlermeldung zur�ckgibt, mu� man explizit eine lseek-Funktion definieren, falls man will, da� der Treiber auf lseek() mit Fehler reagieren soll.
Die mmap-Funktion dient zum Einblenden des Ger�tes in den Nutzeradre�bereich. Diese Funktion ist f�r Zeichenger�te nicht sinnvoll, da sie eine "Adressierung" von Daten innerhalb des Ger�ts voraussetzt. Somit ist mmap() nur von Dateisystemen und h�chstens von Blockger�ten zu verwenden. Eine Ausnahme von dieser Regel ist des Ger�t /dev/mem, da dieses (nat�rlich) keinen unendlichen Datenstrom repr�sentiert, sondern den endlichen und adressierbaren Speicher.
[17]Blockger�te k�nnen zwar eine Fsync-Funktion haben, verwenden dann aber die Funktion
block_fsync () des Caches.