Gerätetreiber unter Linux

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 Adams
Die 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]).
[2]Die Anzahl der virtuellen Konsolen wird durch die in der Datei <linux/tty.h> befindliche Konstante NR_CONSOLES auf 8 festgelegt.
[3]Derzeit ist das RICK MILLER (rick@ee.uwm.edu).

7.1 Zeichen- und Blockgeräte

Grundsätzlich gibt es zwei Arten von Geräten, Blockgeräte und zeichenorientierte Geräte.

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 minor
mit 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.

7.2 Polling- und Interruptbetrieb

Für die Synchronisation von Prozessor und Hardware gibt es mehrere Anforderungen. Zum einen ist die Hardware im allgemeinen im Vergleich zum Prozessor sehr langsam. Andererseits müssen bei bestimmten Geräten gewisse Zugriffszeiten eingehalten werden. Um dies zu realisieren, gibt es grundsätzlich zwei Möglichkeiten.

7.2.1 Polling

Im Pollingbetrieb fragt der Treiber die Hardware ständig ab. Dadurch wird sinnlos Prozessorzeit verschwendet; manchmal ist es aber die schnellste Möglichkeit, mit der Hardware zu kommunizieren.

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.

7.2.2 Interruptbetrieb

Ein Interruptbetrieb hingegen ist nur möglich, wenn die Hardware diesen auch unterstützt. Dabei benachrichtigt das Gerät die CPU über einen Interruptkanal (IRQ), wenn es eine Operation beendet hat. Diese unterbricht den laufenden Betrieb und führt eine Interruptserviceroutine (ISR) aus. Innerhalb der ISR erfolgt dann die weitere Kommunikation mit dem Gerät.

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.

7.2.3 Bottom Halfs - Die unteren Interrupthälften

Es gibt jedoch oft den Fall, daß nach Auftreten eines Interrupts nicht alle Funktionen sofort ausgeführt werden müssen, sondern daß man "wichtige" Aktionen sofort erledigen muß, andere auch später noch erledigt werden können bzw. vergleichsweise lange dauern würden und man den Interrupt nicht blockieren will. Für diesen Fall wurden die Bottom Halfs (untere Hälften) geschaffen. Nach jedem Sprung durch ret_from_syscall, also auch nach jedem langsamen Interrupt, wird eine Liste von maximal 32 Bottom Halfs durchsucht, wenn derzeit kein weiterer Interrupt läuft[8]. Sind sie als aktiv markiert, werden sie der Reihe nach einmal ausgeführt und dann automatisch wieder als inaktiv gekennzeichnet. Dabei sind die Bottom-Halfs atomar, d.h. solange ein Bottom-Half aktiv ist, kann kein anderer ausgeführt werden. Man muß sich also nicht mittels cli() vor Unterbrechungen schützen.

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-Treiber gezeigt.

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-Interrupts, im Idealfall gleich danach. Im Bottom Half werden sowohl die Anzeigen der Keyboard-LEDs auf den aktuellen Stand gebracht als auch kontrolliert, ob das letzte eingegebene Zeichen ein Break war. In diesem Fall müssen Ein- und Ausgabewarteschiange des Geräts gelöscht sowie weitere Operationen, die relativ viel Zeit benötigen, durchgeführt werden. Deshalb ist diese Funktionalität aus dem eigentlichen Interrupt ausgelagert.

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.

7.2.4 DMA-Betrieb

Sollen besonders viele Daten kontinuierlich von bzw. zu einem Gerät transportiert werden, bietet sich der DMA-Betrieb an. In dieser Betriebsart transferiert der DMA-Controller Daten ohne Mithilfe des Prozessors direkt aus dem Speicher zu einem Gerät. Normalerweise löst das Gerät danach einen IRQ aus, so daß in der behandelnden ISR der nächste DMA-Transfer vorbereitet werden kann. Leider gibt es auch Beispiele für DMA-fähige Geräte, die keinen IRQ unterstützen. Viele Handscanner fallen in diese Kategorie. Will man einen Gerätetreiber für diese Klasse schreiben, muß man den DMA-Controller pollen, um das Ende eines Transfers festzustellen.

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.

Um diese Probleme zu lösen, existiert die Funktion sound_mem_init(), die Puffer für den DMA-Transfer zur Soundkarte alloziert und dabei auf 64-K-Grenzen achtet. Leider wird im Protected Mode das DMA-Konzept durch die notwendigen physischen Adressen sehr gestört, so daß der Soundtreiber die Daten erst mit Hilfe des Prozessors in die DMA-Puffer kopiert und dann mittels DMA zur Soundkarte transferiert. Dieses Vorgehen widerspricht eigentlich der Idee, Daten ohne Hilfe des Prozessors zu übertragen, ist aber trotzdem sinnvoll, da man sich nicht um ein Timing bei der Datenübertragung zur Soundkarte kümmern muß.

7.3 Die Hardware

Wollen wir nun einen Gerätetreiber für den internen Lautsprecher schreiben, kommen wir nicht umhin, uns genauer mit dieser Hardware und ihrer Steuerung zu beschäftigen.

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-Speakers
Der 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-Längen-Modulation benutzt. Durch sehr schnelles Umschalten zwischen verschieden langen Ein- und Aus-Phasen, deren Verhältnis gerade dem auszugebenden Analogwert entspricht, wird durch die mechanische Trägheit des Lautsprechers eine analoge Ausgabe erzeugt. Leider ist die Puls-Längen-Modulation auch sehr empfindlich. Bereits das Fehien eines Samples äußert sich in einem störenden Knacken im Lautsprecher[9].

Als das eigentliche Problem bei der Verwendung der Puls-Längen-Modulation stellt sich das Bestimmen der benötigten Zeitintervalle und ihre Erzeugung heraus. Die erste Möglichkeit besteht darin, Timer 2 nicht zu benutzten und die Ausgabe völlig mit Hilfe des Bit 1 des System Control Latches zu steuern. Die Zeitintervalle können durch Warteschleifen erzeugt werden. Dieses Vorgehen ist am einfachsten zu realisieren, bietet aber zwei entscheidende Nachteile:

Die zweite Möglichkeit besteht darin, den Timer 2 als Retriggerable Oneshot zu programmieren. Durch Anlegen einer 1 am Restart Gate wird der Timer gestartet und gibt eine 0 aus. Nach dem Herunterzählen der Timerkonstante wird wieder eine 1 ausgegeben. Nach einer gewissen Zeit, die dem maximalen Samplewert entspricht, wird eine neue Konstante in den Timer 2 übertragen und dieser wieder gestartet. Diese konstante Zeit kann wiederum mit Hilfe einer Zeitschleife oder mit dem Timer 0 erzeugt werden. Timer 0 läuft normalerweise im Teilermodus und generiert nach jedem Herunterzähien der Timerkonstante den IRQ0. Diese vom Timer 0 erzeugte Frequenz ist gleichzeitig die Samplerate, mit der die Samples abgespielt werden können. Sie wird im folgenden Reale Samplerate genannt. In der Interruptbehandlungsroutine muß Timer 2 dann neu initialisiert werden. Dieses Vorgehen zeigt Abbildung 7.2.

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 2
Um z.B. einen Ton mit 10000Hz zu erzeugen, sind folgende Schritte notwendig: Das "Stillegen" erfolgt einfach durch outb_p(inb_p(0x61)&0xFC,0x61). Dadurch wird sowohl der Lautsprecher abgeschaltet als auch der Timer gestoppt.

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-kommandos
Kommen 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.

7.3.1 Hardwareerkennung

Obwohl der Lautsprecher des PCs sich stets auf denselben Portadressen befindet, muß dies nicht auch bei jeder Zusatzhardware so sein. Da leider durch das Design des ISA-Busses für die möglichen Portadressen Grenzen gesetzt sind[12], kann es zu Adreßüberschneidungen kommen. Das wohl geläufigste Beispiel ist die Belegung der I/O-Adresse der COM4-Schnittstelle durch ISA-Karten mit S3-Chip.

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, 0x10
wird 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.

7.4 Die Implementation eines Treibers

Nach detailierter Erklärung der Hardware des internen Lautsprechers drängt sich nun die Frage auf, warum extra ein Gerätetreiber gebraucht wird, um das Schreiben und Lesen einiger I/O-Ports zu erledigen.

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: Das Schreiben eines "Audio-Dämons" der aus einer Named Pipe die Sampledaten liest und beim Hochfahren des Systems über die Datei rc.local gestartet wird, hilft dabei nur bedingt. Das Problem der Zeitabstimmung bleibt auf jeden Fall erhalten.

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.

7.4.1 Die Setup-Funktion

Manchmal möchte man einem Gerätetreiber oder allgemein dem LINUX-Kern Parameter übergeben. Dies kann notwendig sein, wenn eine automatische Erkennung von Hardware nicht möglich ist oder zum Konflikt mit anderer Hardware führt. Dazu bietet sich die Nutzung der LlNUX-Bootparameter an, die dem Kernel während des Bootvorgangs übergeben werden können. Im allgemeinen werden diese Parameter in Form einer Kommandozeile vom LINUX-Lader LILO kommen (siehe Abschnitt D.2.5).

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,...,paramn
Die 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.

7.4.2 Init

init() wird nur während der Kern-Initialisierung aufgerufen, erfüllt jedoch wichtige Aufgaben. Diese Funktion dient der Überprüfung des Vorhandenseins eines Geräts, dem Aufbau interner Strukturen des Gerätetreibers sowie der Anmeldung des Geräts.

Der Aufruf der init-Funktion muß in einer der folgenden Funktionen[15], je nach Art des Gerätetreibers, stattfinden:

drivers/char/mem.c: chr_dev_init() übernimmt die Initialisierung der Geräte- treiber (z.B. Terminals, parallele Schnittstellen, Erstinitialisierung der Sound- karten usw.).
drivers/block/ll_rw_bl.c: Für die Initialisierung der Blocktreiber gibt es die Funk- tion blk_dev_init().
dnvers/net/net_init.c: Die Initialisierung spezieller "exotischer" Netzwerkgeräte findet in net_dev_init() statt.
drivers/scsi/scsi.c: scsi_dev_init() Initialisierung der SCSI-Geräte und ihrer DMA-Puffer.
Die Funktion pcsp_init() wird demnach in unserem Fall von der Funktion chr_dev_init() aufgerufen.

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 Eine zurückgelieferte Null signalisiert, daß der neue Treiber registriert ist.

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.

7.4.3 Open und Release

Die open-Funktion ist verantwortlich für die Verwaltung aller Geräte. open() wird aufgerufen, sobald ein Prozeß eine Gerätedatei öffnet. Falls, wie in unserem Fall, nur ein Prozeß mit einem Gerät arbeiten kann, muß open() als Rückgabewert -EBUSY zurückgeben. Kann ein Gerät von mehreren Prozessen gleichzeitig benutzt werden, sollte open() die dafür notwendigen Warteschlangen einrichten, falls diese nicht in read() oder write() eingerichtet werden können. Falls kein Gerät existiert (z.B. wenn ein Treiber mehrere Geräte unterstützt, aber nur eines vorhanden ist), sollte er -ENODEV zurückgeben. Ansonsten ist open() der richtige Platz, um für den Treiber notwendige Standardeinstellungen zu initialisieren. Der PC-Speaker-Treiber setzt in der open-Funktion beide Puffer auf die Länge 0 und verriegelt den Treiber gegen einen späteren Zugriff durch einen anderen Prozeß. Ein gelungenes Öffnen ist durch eine 0 als Rückgabewert anzuzeigen.

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.

7.4.4 Read und Write

read() und write() sind vom Prinzip her zueinander symmetrische Funktionen. Da man vom internen Lautsprecher keine Daten lesen kann, ist im PC-Speaker-Treiber nur write() implementiert. Der Einfachheit halber soll jedoch zunächst der Aufbau einer write-Funktion für Treiber im Pollingbetrieb betrachtet werden. Hier wird wieder der Druckertreiber bemüht.

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.

7.4.5 IOCTL

Obwohi ein Gerätetreiber versucht, die Bedienung von Geräten nach außen hin möglichst zu abstrahieren, hat doch jedes Gerät seine speziellen Eigenschaften. Dazu können verschiedene Operationsmodi ebenso wie gewisse Grundeinstellungen gehören. Auch eine Einstellung von Geräteparametern zur Laufzeit, wie IRQs, I/O-Adresse usw., ist denkbar.

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.

7.4.6 Select

Obwohl select() nicht implementiert ist, soll doch hier die Funktionsweise beschrieben werden, da diese Funktion insbesondere für Zeichengeräte sinnvoll ist. Als Beispiel sei die select-Implementierung des ATI-Busmaus-Treibers betrachtet:

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);
 }
  ...
}

7.4.7 Lseek, Readdir, MMap und Fsync

Diese Funktionen sind im PC-Speaker-Treiber nicht implementiert. Da die Struktur file_operations nicht nur für Geräte, im besonderen Zeichengeräte, verwendet wird, enthält sie von Gerätetreibern nicht genutzte Funktionen. So ist readdir() und fsync()[17] für Geräte sinnlos.

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.