Treiber und Getriebene Entwicklung eines PCI-Kartentreibers für Windows Embedded CE 6.0

Einen PCI-Gerätetreiber für Windows CE 6.0 zu schreiben, ist eine echte Herausforderung. Der Treiber soll im Kernel- oder User-Mode gleichermaßen funktionieren. Externe Ereignisse, signalisiert durch Interrupts, sollen durch einen eigenen, dynamisch ladbaren ISR-Handler (Interrupt Service Routine) bedient werden, und für die Hardware-Ereignisse am PCI-Bus müssen sich alle PCI-Karten einen Interrupt teilen.

Ein PCI-Gerätetreiber ist eine Software-Komponente, über die das Betriebssystem und die Anwendungen mit einem Peripheriegerät kommunizieren, das am PCI-Bus (Peripheral Component Interconnect) angeschlossen ist. Anstatt direkt auf die Hardware zuzugreifen, lädt das Betriebssystem den Gerätetreiber und spricht das Gerät mittels Funktionen und E/A-Diensten an. Der Gerätetreiber enthält den hardwarespezifischen Code.

Um Windows CE auf einem Computersystem zu installieren, nutzt man den „Windows CE Platform Builder“, eine grafisch gesteuerte Anwendung, in der alle Komponenten für das Zielsystem ausgewählt und zu einem „Image“ zusammengefasst werden. Das Image wird anschließend auf die Ziel-Hardware überspielt. Im Standard-Lieferumfang von Windows CE sind bereits Treiber für gängige PCI-Geräte enthalten. Zusätzlich zu den Standardtreibern in CE können benutzerdefinierte Treiber implementiert werden, um weitere Peripheriegeräte anzusteuern.

Beim Entwickeln von Gerätetreibern muss man sich an strikte Programmierverfahren halten und die Komponenten umfassend in verschiedenen Systemkonfigurationen testen. Fehlerhafte Kernel-Mode-Treiber führen sofort zu einem Systemabsturz. User-Mode-Treiber können dagegen höchstens den User-Prozess zum Absturz bringen.

Ein Gerätetreiber in Windows Embedded CE ist eine DLL (Dynamic-Link Library), die eine Abstraktionsschicht zwischen der Hardware und dem Betriebssystem sowie den Anwendungen bereitstellt (Bild 1). Die Treiber umfassen Funktionen und Programmlogik zum Initialisieren und Kommunizieren mit der Hardware. Anwendungen können dann die Windows-API-Standardfunktionen aufrufen, beispielsweise ReadFile oder WriteFile, um auf das Peripheriegerät zuzugreifen, unabhängig von den Informationen über die physikalische Hardware.

Vorbereitung der PCI-Karte und Laden des Treibers

Nachdem die PCI-Karte eingebaut wurde, wird mit dem Platform Builder ein Debug-Image für die Target-Hardware erstellt und über den „KITL Transport“ heruntergeladen. Der Ladevorgang wird mit dem Starten von Windows CE auf dem Target-System abgeschlossen. Im Debug-Fenster des Platform Builders kann man den kompletten Startvorgang der Treiber anhand der Debug-Messages mitverfolgen. Dabei scannt der PCI-Bus-Enumerator (busenum.dll) den kompletten PCI-Bus und liest von jeder PCI-Karte die kartenspezifischen Infos aus, die im PCI-Header stehen. Diese Infos erscheinen dann im Debug-Fenster.

Beispiel: VendorID = 0x9710, DeviceID = 0x9815, InterruptLine = 0x0A usw.

Aus den Debug-Infos wird ein Registrierungsschlüssel (..\PCI\Template\..) zusammengestellt und in die „project.reg“ eingetragen. Nach dem erneuten „Builden“ und Laden des Images erstellt der PCI-Bus-Emulator dann einen [..\PCI\Instance\..]-Eintrag in der Registry.

Dieser „Instance“-Eintrag enthält alle Informationen, um die PCI-Karte für eigene Anwendungen zu nutzen. Der hier beschriebene Treiber (PCI_ ParallelPort_Driver.DLL) wird benutzt, um mit der PCI-Hardware zu kommunizieren. Ein Anwenderprogramm (PCI_ ParallelPort_Client.exe) bedient den Treiber (Bild 2).

Um einen Treiber zu laden, gibt es in Windows CE zwei Möglichkeiten: In der Startphase des Systems lädt der Gerätemanager alle Treiber, die in der Registry unter [HKLM\Drivers\BuiltIn] eingetragen sind. Oder ein Anwenderprogramm verwendet das API ActivateDeviceEx um den Treiber zu laden.

Erfolgreich geladene Treiber werden durch den Gerätemanager unter [HKLM\Drivers\Active] gemäß der Ladereihenfolge (Order) eingetragen (Bild 3).

Alle dort eingetragene Treiber sind systemweit bekannt und könnten z.B. auch vom PowerManager (PM) zum Powermanagement verwendet werden. Unter dem Active-Eintrag befindet sich auch das Handle des Treibers. Sollte es einmal vorkommen, dass eine Anwendung unsachgemäß beendet wird, so kann man hier das Handle neu laden. Denn ein Treiber kann nicht mehrmals geladen werden.

Die Interrupt-Verarbeitung von Windows CE

Die von einer Hardware ausgelösten Ereignisse werden als Interrupt bezeichnet. Bild 4 zeigt die komplette Interrupt-Schnittstelle zwischen der PCI-Karte, der Interrupt Service Routine (ISR) und die Verbindung zum Interrupt Service Thread (IST).

Ein durch die Hardware ausgelöster Interrupt wird vom „Kernel Interrupt Handler (KIH)“ entgegengenommen und an die ISR weitergeleitet. Das Programm der ISR wird mit einer Verzögerung (latency) von etwa 1 μs ausgeführt. Eine belgische Firma hat für alle Echtzeit-Betriebssysteme derartige Messungen durchgeführt (www.dedicated-systems.com). Die Interrupt-Hardware sperrt alle Interrupts mit einer niedrigeren Priorität. Diejenigen höherer Priorität sind weiterhin verfügbar.

Durch diesen Mechanismus ist es auch möglich, dass ein aktiver Interrupt durch einen Interrupt mit einer höheren Priorität unterbrochen werden kann (Nested-Interrupt). Die ISR setzt den anstehenden Interrupt im Interrupt-Register (Bild 4) zurück und liefert an den KIH eine Interrupt-ID. Diese ID repräsentiert den Hardware-Interrupt (IRQ 10). Der Interrupt Service Thread (Listing 1) führt in der while-Schleife immer den Befehl „WaitForSingleObject()“ aus und wartet darauf, dass eine Signalisierung „PulseEvent()“ vom KIH kommt. Der Programmcode des IST wird nach einer weiteren Verzögerung von etwa 10 μs ausgeführt. Eine komplette Interrupt-Verarbeitung wird mit dem API-Aufruf „InterruptDone()“ abgeschlossen.

Unter Windows CE besteht die Möglichkeit, jedem Thread eine Priorität von 0 bis 255 zuzuordnen. Dabei hat die kleinste Nummer die höchste Priorität. Die zur Verfügung stehenden Prioritäten werden in Bereiche eingeteilt. Die Prioritäten von 0 bis 96 sind für Echtzeit-Treiber reserviert. Der IST ist immer aktiv und muss beim Entfernen des Treibers aus dem Speicher beendet werden. Ein Thread, der den Befehl „return 0“ ausführt, befindet sich dann im Zustand „signalisiert“.

Datenaustausch zwischen Interrupt Service Routine und Interrupt Service Thread

Wie in Bild 4 zu erkennen ist, befindet sich die ISR im OEM Adaption Layer (OAL) und der IST im Treiber. Bei der Datenübergabe ist immer darauf zu achten, ob es sich um eine virtuelle oder eine physikalische Adresse des Buffers handelt. Die beste Methode zur Übertragung von Daten ist das Reservieren von Speicher in der Datei config.bib. Mit der Funktion OALPAtoVA wird die virtuelle Adresse in eine physikalische Adresse des reservierten Buffers umgewandelt. Mit den Funktionen VirtualAlloc und Virtual- Copy lässt sich ein physikalischer Speicher auf einen virtuellen Speicher abbilden.

Eine weitere Methode, die Daten von einer ISR an einen IST zu übergeben, ist das dynamische Zuordnen von physikalischem Speicher im RAM unter Verwendung der Funktionen im Gerätetreiber. Diese Vorgehensweise ist insbesondere für installierbare ISRs nützlich, die bei Bedarf in den Kernel-Bereich geladen werden. AllocPhys-Mem ordnet einen zusammenhängenden physikalischen Speicherbereich zu und gibt die physikalische Adresse zurück. Der Gerätetreiber kann über die Funktion KernelIoControl basierend auf einem benutzerdefinierten IOCTL-Code die physikalische Adresse an die ISR weitergeben.

Die Funktion LoadIntChainHandler wird verwendet, um eine installierbare ISR zu laden und zu registrieren. Die notwendigen Parameter befinden sich in der Struktur (isri), die mit der Funktion „DDKReg_GetIsrInfo(hk- Device, &isri)“ initialisiert wird.

Der erste Parameter (isri.szIsrDll) gibt den Dateinamen der zu ladenden ISR-DLL an. Der zweite Parameter (isri.szIsr- Handler) gibt den Namen der Interrupt- Handler-Funktion an, und der dritte Parameter (isri.dwIrq) definiert den IRQ, für den die installierbare ISR registriert werden soll.