Betriebssysteme Wie entwickle ich einen Gerätetreiber für Windows Embedded CE?

Autor / Redakteur: Rudi Swiontek und Ralf Ebert* / Martina Hafner

Einen Gerätetreiber für ein Betriebssystem zu entwickeln kann eine große Herausforderung für jeden Entwickler sein. Die wichtigsten Aspekte für Microsofts neues Windows Embedded CE 6.0 fast dieser Beitrag zusammen.

Firmen zum Thema

( Archiv: Vogel Business Media )

In der neuesten Version bietet Windows Embedded CE 6.0 völlig neue Features. Windows Mobile und benutzerdefinierte eingebettete Geräte sind durch ein umfangreiches Technologieportfolio verbunden, das einen leistungsfähigen Multithread-Kernel, ein reichhaltiges Angebot an Programmierungs-APIs (Win32, MFC8, ATL, COM, DCOM und .NET Compact Framework) sowie eine funktionsreiche erweiterbare Palette von Entwicklungstools umfasst.

Windows Embedded CE 6.0 ist mit einem neuen Kernel ausgestattet, der die Einschränkungen früherer Kernel überwindet und gleichzeitig den CPUs mehr Leistung entlockt. Die meisten Systemaufrufe können vom Betriebsysten etwa 20% schneller ausgeführt werden weil sich die Systemdienste nun im Kernel befinden.

Die meiste Treiber besitzen ein Stream-Treiber-Interface und werden durch den Gerätemanager vollständig entkoppelt. Dabei spielt es keine Rolle ob es sich um einen User- oder Kernel-Mode-Treiber handelt. Auf beide Treibermodelle wird in diesem Artikel tiefer eingegangen.

Bildergalerie

1. Wie funktioniert das Memory Management?

(Archiv: Vogel Business Media)

Der neue Kernel besitzt eine komplett neue Speicherarchitektur (Bild 1), die die früheren Einschränkungen bezüglich verfügbarer Prozess- und Adressbereichsgrößen aufhebt. CE 6.0 erhöht die maximale Anzahl von Prozessen auf 32.000 und jeder Prozess verfügt über einen 2 GByte großen virtuellen Adressspeicher. In CE 6.0 erhält jeder Prozess seinen eigenen, wirklich privaten Prozess-Adressbereich von 1 GBybte. Die MMU (Memory Management Unit) bildet den virtuellen Adressbereich auf den physikalischen Adressbereich (ROM und RAM) ab.

Die neue Speicherarchitektur erleichtert einiges, besonders für Anwendungen die sehr große Speicherblöcke benötigen; so kann ein Speicherblock mit (VirtualAlloc) zugewiesen werden.

Die Anwender DLLs (User DLLs) liegen im Adressbereich der User-Prozesse und können durch die MMU in jeden anderen Prozess-Adressbereich gelegt (gemapped) werden. Der Programmcode der DLLs ist shared und die Datenbereiche sind prozessglobale Daten.

Für die Interprozess-Kommunikation kann man aus den Adressbereich (Memory Maped Files) gemeinsame Speicherobjekte bis zu 512 MByte allokieren. Die dafür notwendigen Befehle sind CreateFileForMapping, CreateFileMapping, MapViewOfFile und UnmapViewOfFile. Dabei lässt sich das Speicherobjekt als RAM-Objekt oder auch als File-Objekt erzeugen.

Wenn es sich um ein File-Objekt handelt, kann dieses auch auf einem XP/Vista-PC liegen. Dazu muß das Verzeichniss auf einem XP/Vista-PC als shared gekennzeichnet sein. Mit den WinCE Befehlen „net view xp-host-name“ und „net use gemeinsamer_name //xp_host_name/temp“ kann ein temp-Netzwerk-Laufwerk gemappt werden auf das das Mapping-File-Objekt abgespeichert wird. Damit ist es möglich, den zur Verfügung stehende Speicher beliebig zu erweitern. Umfangreiche Testdaten lassen sich somit auf ein externes Verzeichnis abspeichern. Stürtzt die WinCE Test-Applikation ab, so enthält die Datei den letzten Schreibzugriff. Der Zugriff auf ein gemeinsames Objekt lässt sich dabei durch ein Globales-Mutex synchronisieren.

2. Aus welchen Teilen setzt sich die Systemarchitektur zusammen?

Bild 2: Systemarchitektur. Alle Treiber sind Dynamic Link Librarys und werden mit dem Platform Builder entwickelt. (Archiv: Vogel Business Media)

Wie in Bild 2 zu erkennen ist stehen Systemprozesse zur Verfügung um Service-Dienste wie Telnet, Web oder FTP zu nutzen. Es lassen sich auch eigene Dienste entwickeln. Sowohl die Systemdienste, als auch die selbst geschriebenen werden vom ServicesD-Prozess verwalten und administrieren.

Alle Benutzer-Anwendungen (Applikationen) sind User-Mode-Anwendungen und werden mit VS2005 entwickelt. Für die Anwenderprogramme muß ein SDK (Software Development Kit) bereitgestellt werden, das mit dem Platformbuilder erstellt wird. Ferner stellt die Entwicklungsumgebung des VS für alle Anwendungen einen Aplikationdebugger zu Verfügung.

Jeder Treiber ist eine DLL (Dynamic Link Library) und wird mit dem Platform-Builder entwickelt. Zum Testen steht ein Kernelmode-Debugger zur Verfügung mit dem es auch möglich ist in die WinCE Systemaufrufe zu debuggen. Die User-Mode-Treiber benötigen einen User-Prozess (Udevice.exe) in dem sie verwaltet werden können. Die Kernel-Mode Treiber liegen im Adressraum des Kernel-Prozesses (nk.exe) und werden durch den Gerätemanager (Devmgr.dll) verwaltet.

Wenn eine Anwendung (User-Application) einen Buffer erzeugt, der als Aufrufparameter an einen Treiber übergeben wird, so wird der Buffer in den Adress-Bereich des Treibers gemappt. Dieses Mapping erfolgt bei jedem Treiber-Aufruf immer automatisch. Wichtig! Erzeugt der Anwender hingegen eine Struktur und die Struktur enthält als Strukturelement eine Adresse (Embedded-Pointer), so wird diese nicht in den Adressraum des Treibers gemappt. Dies bleibt dann dem Treiber-Entwickler überlassen. Hierfür werden zwei Funktionen bereitgestellt: für einen synchronen Zugriff auf den Embedded-Pointer ist dies „CeOpenCallerBuffer“ und für den asynchronen Zugriff „CeAllocAsynchronousBuffer“.

Wie im Bild zu erkennen ist, kann ein Anwender-Programm einen CAN-Treiber sowohl als User- als auch als Kernel-Mode Treiber aufrufen. An den Schnittstellen ändert sich nichts. Wo und wie der CAN-Treiber implementiert wird, ist Aufgabe des Treiber-Entwicklers. Kernel-Mode- Treiber sind sehr schnell und haben Kernel-Zugriffsrechte. Hingegen sind User-Mode-Treiber sehr robust und haben nur Anwender-Zugriffsrechte.

3. Welche Treiber-Typen existieren unter CE?

WinCE unterstützt verschiedene Gerätetreibermodelle. Dies sind die Native Device Treiber, auch built-in-driver genannt, sowie die Stream Interface Driver, auch installable-driver genannt. Die Treibermodelle unterscheiden sich in erster Linie durch die Softwareschnittstelle, die nach außen angeboten wird. Native Treiber bedienen Geräte, welche fest in das System eingebaut sind, wie z. B. eine Tastatur oder ein LCD-Touchscreen. Diese Geräte haben eine besondere Bedeutung für das Betriebsystem, da sie bereits während des Bootvorgangs ansprechbar sind. Um für diese Treibertypen einen Treiber zu entwickeln, ist jeweils die Treiber-Schnittstellen-Beschreibung von Microsoft notwendig.

Der Stream Interface Driver ist typischerweise für Geräte verantwortlich, die vorübergehend an das System angeschlossen und wieder entfernt werden können. Das heißt, dass sie nach dem Booten von WinCE geladen oder entladen werden. Diese Treiber besitzen alle eine identische Schnittstelle und die 10 APIs haben immer folgenden Aufbau: z.B. xxx_Init() steht für CAN_Init().

4. Welche Aufgaben übernimmt der Gerätemanager?

Der Gerätemanager (GM) selbst wird von Bootloader, wie in der Registry aufgeführt ist, [HKEY_LOCAL_MACHINE\init] … “Launch20”=“device.dll” geladen. Alle Treiber, die durch den GM geladen werden sollen, befinden sich unter dem Registy-Schlüssel [HKEY_LOCAL_MACHINE\Drivers\BuiltIn]. Der Registry-Eintrag „Order“=dword:30“ gibt die Ladereihenfolge-Nummer des Treibers an.

Ein geladener Treiber wird als aktiv gekennzeichnet und unter dem Registy-Eintrag „[HKEY_LOCAL_MACHINE\Drivers\Active“ eingetragen und kann nun vom Betriebssystem Power Management Support bekommen. Für die Power-Management-Funktionen im Treiber (CAN_PowerDown(), CAN_PowerUp()) gibt es keine Anwender-Schnittstelle (Aufrufe). Beim Entladen des Treibers wird der Treiber aus dem Verzeichnis „\Active“ entfernt.

Treiber könnten auch durch ein API mit „ActivateDeviceEx“geladen werden. Diese Treiber (z.B. der CAN-Treiber) müssen unter folgendem Eintrag abgelegt sein „[HKEY_LOCAL_MACHINE\Drivers\CAN_Driver“.

Bei Plug- and Play-Geräten gibt das gerade eingefügte Gerät einen Identifer zurück, den der Device Manager nutzt, um den richtigen Treiber zu finden. Andernfalls wird eine Erkennungsroutine durchlaufen, welche die nötigen Informationen beschafft, um den richtigen Treiber ausfindig zu machen. Die Applikation greift auf ein Gerät über Dateisystemfunktionen des Kernels zu. Der Kernel leitet diese Kommandos an den Stream-Treiber weiter.

5. Wie entwickel und integriert man einen Stream-Treiber?

Bild 3: Stream-Treiber bedienen typischerweise Geräte, die vorübergehend an das System anschlossen werden. Sie werden durch einfache File-IO-Befehle realisiert. (Archiv: Vogel Business Media)

Die Schnittstellen für die Stream-Treiber werden durch einfache File-IO-Befehle realisiert (Bild 3). Der GM nimmt alle Anwender-Aufrufe entgegen und entkoppelt sie. Alle WinCE-File-IO-Aufrufe sind synchrone Aufrufe die das Anwenderprogramm solange blockieren, bis der Treiber sich mit einem return zurück meldet.

Bild 4: Stream-Treiber-Schnittstellen. Schematische Darstellung des Zusammenspiels zwischen Anwendung, Gerätemanager und Treiber. (Archiv: Vogel Business Media)

Die Schnittstellen zwischen einem UM- oder KM-Treiber sind identisch. Ein UM- Treiber besitzt nur zusätzliche Registry-Einträge die vom Geratemanager beötigt werden um gemäß den Angaben den Treiber zu laden. Der hier beschriebene Stream-Driver setzt sich aus folgenden Dateien zusammen: CAN_Driver.cpp, CAN_Driver.h, CAN_Driver.def, CAN_Driver.reg und CAN_Driver.bib. Im Bild sind die einzelnen Aufgaben des GM Nummern von 1-20 gekennzeichnet. Nicht alle Parameter des aufrufenden Programmes werden an die Treiberfunktionen weitergegeben.

Das Zusammenspiel (Schnittstellen) zwischen der „Anwendung <> Geräte Manager <> Treiber“ wird durch die Punkte 1-20 beschrieben:

  • 1. hCAN_Device=ActivateDeviceEx (L“Drivers\\CAN_Driver“, NULL, 0, 0);“
  • 2. GM läd den Treiber CAN_Driver.dll und registriert den Treiber unter [HKLM\Drivers\Active]. Ruft die Funktion DLLMain() im CAN_Driver dll auf und führt CAN_Init() aus.
  • 3. CAN_Init() dient zum inizialisieren der Hardware, erzäugen der Events, Thraeds, bereitstellen von Speicher und Interrupt-Initialisierung. CAN_Init( ) {… } return(1); Eine 0 als Return ist immer ein Error vom Treiber.Der Return Wert des Treibers wird im GM gespeichert und als Handle dem Aufrufer bereitgestellt (hCAN_Device).Der Treiber befindet sich im Initial-Zustand (1).
  • 4. Aufruf: bool_var=DeactivateDevice(hCAN_Device);Der Treiber soll entladen weden!
  • 5. Ruft CAN_Deinit() im CAN_Driver.dll auf. Löscht den Treiber aus [HKLM\Drivers\Active]
  • 6. LED_Deinit() {…} return TRUE; wenn der Treiber erfolgreich entladen werden konnte. Alle erzeugten Objekte müssen zurückgegeben werden. Es muss darauf gewartet werden bis alle Threads signalisiert sind „WaitForSingleObject()“. Es wird DLLMain() ausgeführt.
  • 7. hCAN1_Dev=CreateFile( TEXT(„CAN1:“),,,,); Der Anwender hat ein Geräte-Handle von der CAN-Schnittstelle und kann nun vom Treiber lesen oder Daten auf den Treiber schreiben.
  • 8. CAN_Open() {…} return (2); (hCAN1_Dev). Der Treiber befindet sich im Initial-Zustand (2). Ferner können Objekte für den Open-Zustand erzeugt werden.
  • 9. bool_var= CloseHandle(hCAN1_Dev); Geräte Handle wird zurückgegeben.
  • 10. CAN_Close() {…} return (TRUE); Der Treiber befindet sich im Initial-Zustand (1).
  • 11. Die CAN-HW kann benutzt werden: bool_var= WriteFile (hCAN1_Dev, &Buffer, sizeof(Buffer), &nBytesWritten, NULL); Der Buffer-Inhalt wird den Treiber übergeben. Die Variable nBytesWritten enthält die Anzahl der geschriebenen Bytes. (Synchroner Schreibzugriff)
  • 12. CAN_Write() {…} return (nBytes); Der Treiber gibt die Anzahl der geschriebenen Bytes zurück zum Aufrufer.
  • 13. bool_var= ReadFile (hCAN1_Dev, &Buffer, sizeof(Buffer), &nBytesRead, NULL);
  • 14. CAN_Read() {…} return (nBytes);
  • 15. Diese Funktion ist eine bidirektionale Daten-Austausch-Funktion mit dem Treiber. Die übergebenen Adressen werden durch die MMU in den Adressbereich des Treibers gemapped. In diesem Beispiel wird eine Struktur übergeben die Struktur-ElementE als Embedded-Pointer enthält. Diese Pointer werden nicht umgemapped. Die Strukturdefinition: “struct {PBYTE pEmbeddedSource; PBYTE pEmbeddedDest; int len} pcs“; Der Treiber soll die Daten von pEmbeddedSource in den pEmbeddedDest kopieren.Der Aufruf: DeviceIoControl (hFile, 1, &pcs, sizeof (pcs), NULL,0, &dwBytes, NULL);
  • 16. Der Treiber muß die Embedded-Pointer ummappen!LED_IOControl() { PBYTE pMapped1, pMapped2; CeOpenCallerBuffer((PVOID *)&pMapped1,pcs->pBuff1,pcs->nLength,ARG_IO_PTR,false); CeOpenCallerBuffer((PVOID *)&pMapped2,pcs->pBuff2,pcs->nLength,ARG_IO_PTR,false); memcpy (pMapped2, pMapped1, pcs->nLength); }
  • 17. bool_var= SetFilePointer (hCAN1_Dev,,,); Es kann ein File-Pointer auf eine bestimmte Position gesetzt werden.
  • 18. CAN_Seek() {…} return (TRUE);
  • 19. CAN_PowerDown() wird vom Powermanager gerufen. Es gibt kein API auf der Anwender-Seite.
  • 20. CAN_PowerUp() wird vom Powermanager gerufen. Es gibt kein API auf der Anwender-Seite.

6. Welche Besonderheiten des USER-Mode-Treibers sind zu beachten?

Bild 5: User-Mode-Stream-Treiber-Schnittstellen. Der Gerätemanager erkennt anhand der Flags, ob es sich um einen User-Mode-Treiber handelt. (Archiv: Vogel Business Media)

Die Unterschiede zum Kernel-Mode-Treiber sind nur in dem Registry-File zu finden. Der Geräte Manager erzeugt einen User-Prozeß (Udevice.exe) und läd den CAN_Driver als USER-Treiber.

Hier das Registry-File (CAN_Driver.reg):

[HKEY_LOCAL_MACHINE\Drivers\ProcGroup_0004]

„ProcName“=“udevice.exe“

„ProcVolPrefix“=“$udevice“

[HKEY_LOCAL_MACHINE\Drivers\CAN_Driver]

„Dll“=“CAN_Driver.dll“

„Prefix“=“CAN“

„Order“=dword:30

„Index“=dword:1

„FriendlyName“=“Mein CAN Treiber“

;------------Zusätzliche Definitionen für einen User Mode Driver

„Flags“=dword:10 ;User Mode

„UserProcGroup“=dword:4

Der GM erkennt anhand der Flags, das es sich hierbei um einen USER-Mode-Treiber handelt. Ihre Integration und Verwaltung ist sehr aufwendig. In Bild 5 ist die Komplexität der Schnittstellen zu erkennen.

7. Welche Aspekte für höhere Sicherheit gibt es?

Sicherheit ist eines der Hauptthemen der Windows-Embedded-Produkte. Vor der Auflistung der Verbesserungen, die mit CE 6.0 gemacht wurden, sollten auch die Sicherheitsfunktionen erwähnt werden, die bereits vorhanden sind. Im Mittelpunkt steht die Fähigkeit eines Windows CE-betriebenen Geräts, genau zu steuern, welche Anwendungen und DLLs geladen und ausgeführt werden dürfen. Durch die Funktion „OEMCertifyModule“ kann ein Gerät verhindern, dass nicht autorisierter Code ausgeführt wird.

Eine gängige Methode um autorisierte Module zu identifizieren sind digitale Zertifikaten. Die Gerätesicherheit lässt sich mit verschiedenen Konfigurationen einrichten. Beispielsweise lässt sich für unbekannte Module, also solche ohne gültige Zertifikate, der Systemzugriff verweigern. Alternativ kann dieser Mechanismus deaktiviert werden, sodass alle Module uneingeschränkten Zugriff auf alle Systemdienste erhalten.

Eine weitere wichtige Sicherheitsfunktion ist die kryptografische API, die es ermöglicht, dass Anwendungen Datenblöcke mit verschiedenen Verschlüsselungsalgorithmen verschlüsseln und entschlüsseln. Verschlüsselungsalgorithmen werden durch symbolische Namen mit dem Präfix CALG_ gekennzeichnet, z. B. CALG_DES und CALG_AES. Die zweite besteht in der Unterstützung von SSL (Secure Sockets Layer) für sichere HTTP-Verbindungen. Darüber hinaus werden über das PPTP-Protokoll virtuelle private Netzwerke (VPN) unterstützt. Um sichere Verbindungen mit Serversystemen zu ermöglichen, bietet Windows CE Unterstützung für verschiedene Authentifizierungsmechanismen, darunter das Windows NT LAN Manager-Protokoll und das beständigere Kerberos-Authentifizierungsprotokoll.

Weitere Funktionen aus früheren Windows CE-Versionen umfassen die Verwaltung von Anmeldeinformationen, Unterstützung von Smartcards, DPAPI, Unterstützung von öffentlichen Schlüsselzertifikaten (PKI) sowie das Local Authentication Subsystem (LASS).

Was sind nun die neuen Sicherheitsfunktionen in CE 6.0? Wie bereits oben erwähnt, gibt es eine strikte Trennung zwischen Benutzermodus-Code und Kernelmodus-Code. In CE 6.0 werden die Parameter für die geschützte Serverbibliothek (PSL) und E/A-Steuerung (IOCTL), die vom Benutzermodus in den Kernelmodus übergeben werden, gründlicher überprüft, wodurch die Sicherheit und Stabilität im Kernelmodus verbessert wird.

Die Parametersicherheit eines Systems kann deutlich durch Unterstützung eines sicheren Ladeprogramms verbessert werden. Dieses gewährleistet, dass nur vertrauenswürdiger Code auf einem System ausgeführt wird.

Ein weiterer Bereich, in dem Sicherheitsverbesserungen vorgenommen wurden, ist der Assistent für Windows CE OS-Design (bzw. für neue Plattformen). Wird eine Plattform mit einer Funktion konfiguriert, die die Gerätesicherheit beeinträchtigen könnte, wird eine Sicherheitswarnung ausgegeben. (OBEX-Server).

Eine weitere Funktion, die zur CE-6.0-Sicherheit hinzugefügt wurde, ist die Unterstützung eines sicheren Boot-Ladeprogramms. Damit wird sichergestellt, dass heruntergeladene Betriebssystem-Images (NK.BIN) gültige digitale Signaturen enthalten, bevor sie in einem System installiert und ausgeführt werden dürfen.

Fazit: Die Treiberentwicklung unter Windows CE ist gegenüber XP sehr strukturiert und einfach umzusetzen, da der Gerätemanager die komplette Administration des Treibers übernimmt. Auch die einfache Auswahl-Möglichkeit um was für einen Treiber-Type es sich handelt, einen User-Mode Treiber, der sehr robust ist oder um einen Kernel-Mode Treiber, der die gleichen Zugriffsrechte wie das Betriebssystem besitzt, ist wohl durchdacht.

Ausblick: Die Treiber-Schnittstellen in der neuen Version werden sich nicht ändern. Die gravierende Erweiterung wird die Unterstützung mehrere CPUs sein. Damit lassen sich Threads auf mehrere CPUs verteilen. Dadurch erhält WinCE (7) einen gigantische Leistungsschub.

(ID:264282)