Introduction
Driver
Généralités
Chargement
du driver
Développement
du driver
Utilisation
du driver
Driver
Liaison Série
Modification
de la base de registre
Création
de la dll
MSD_Init
MSD_Open
MSD_IOCTL
Application
Généralités
HyperTerminal
personnalisé
Conclusion
Introduction
Dans
un autre
article,
nous avons vu comment utiliser Platform Builder pour réaliser sa propre
implémentation de Windows CE. Nous avons également vu comment tester l’OS sur
un PC cible (CEPC). Nous allons maintenant détailler la création de drivers
pour un matériel spécifique, détails suivis d’un exemple pour le port série.
Nous développerons ensuite une application type HyperTerminal dans laquelle
nous utiliserons ce driver. Le driver en question sera codé en C sous Platform
Builder et l’application en C# sous Visual Studio 2003.
Nous
n’allons pas détailler ici les opérations de compilation et de déploiement du
noyau. Afin de faire fonctionner le driver et l’application présentés dans cet
article, il faudra avoir préalablement compilé un noyau Windows CE pour CEPC et
l’avoir déployé sur la machine cible. Ces étapes sont décrites dans
l’article
mentionné plus haut. Il ne faudra pas oublier d’inclure le
« Compact Framework » dans le noyau.
Passons
maintenant à la présentation des drivers sous Windows CE.
Driver
Dans ce chapitre, nous allons vous
présenter dans un premier temps les concepts généraux liés aux drivers sous
Windows CE, à savoir leur chargement, leur implémentation et leur utilisation à
partir d’une application. Ensuite, nous illustrerons ces concepts au moyen d’un
exemple : la réalisation d’un driver de gestion du port série.
Généralités
Un
driver est un programme qui fait le lien entre un composant matériel et les
logiciels qui veulent utiliser ce composant. Généralement, le matériel en
question possède des registres adressables pouvant être configurés et
lus : le rôle du driver est d’accéder à ces registres et de fournir au
programme appelant une interface facilement exploitable.
Sous
Windows CE, les drivers prennent obligatoirement la forme de dlls. Ils peuvent
être de deux types que l’on appelle stream
interface drivers et native drivers.
Les premiers correspondent aux drivers de périphériques consommateurs et/ou
producteurs de flux de données. Typiquement, cela correspond par exemple à un
driver d’imprimante ou de port série. Tous ces stream drivers exposent une même interface logicielle. Les applications les
utilisent comme s’il s’agissait de fichiers.
Les native drivers,
quant à eux, regroupent tous les drivers qui exposent une interface spécifique,
le but étant d’améliorer les performances. On trouve dans cette catégorie les
drivers d’affichage, de batteries... On y trouve également les drivers des
périphériques de base comme la souris ou le clavier. On peut noter qu’un driver
peut implémenter à la fois l’interface ‘native’ et l’interface ‘stream’. Pour
de plus amples détails sur les différents types de drivers sous Windows CE,
référez vous à cet
article.
Dans
la suite de cet article, nous ne traiterons que des stream drivers ; ils sont en effet les plus génériques et les plus simples à
implémenter et utiliser.
Un
stream driver possède un nom composé
de 3 lettres (le préfixe), d’un chiffre unique entre 0 et 9 (l’index) et d’un
caractère deux points : « COM1:
», « COM2: » ou « LPT1: » ...
Ce nom sert de lien entre le driver et une application qui souhaite l’utiliser.
Avant cela, le driver doit être initialisé par le système. Cette initialisation
rend le driver accessible aux applications clientes.
Chargement du driver
Une
première méthode pour activer un driver consiste à charger la dll
correspondante directement depuis
l’application cliente. Pour cela, il existe deux fonctions de l’API se trouvant
dans « coredll.dll » :
-
LoadDriver qui rend le driver
accessible à l’application seule.
-
RegisterDevice qui rend le
driver accessible à tout le monde.
LoadDriver n’a
besoin que du nom de la dll en paramètres et renvoie un handle. RegisterDevice nécessite, quant à
lui, le préfixe, l’index, le nom de la dll et les informations optionnelles
associées au driver. Voici d’ailleurs la déclaration de
RegisterDevice en C# .NET :
// Déclaration de RegisterDevice
[DllImport("coredll.dll", CharSet=CharSet.Unicode)]
public static extern
IntPtr RegisterDevice (string
lpszType,
int
dwIndex,
string
lpszLib,
int
dwInfo);
Et voici un exemple d’utilisation :
// Enregistrement du driver dans la
registry
// Le nom est 'MSD1:'
hDevice = RegisterDevice("MSD", 1, "my_serial_driver.dll", 0);
Une
deuxième méthode consiste à enregistrer le driver dans la base de registre.
Enregistrer un driver revient à créer une sous-clé dans
[HKEY_LOCAL_MACHINE\Drivers\Active\] contenant une valeur (Key)
qui pointe vers la clé du driver :
Cet enregistrement peut être réalisé soit manuellement, soit automatiquement.
∙
L’enregistrement manuel se fait au moyen de la fonction
ActivateDevice qui inscrit dans la clé
Active la clé du driver (elle-même déjà présente dans la base de
registre à un endroit quelconque). Cette fonction est à préférer à la fonction RegisterDevice. L’enregistrement manuel peut être utilisé par exemple lorsqu’un
périphérique est branché à chaud sur le port USB, ou lorsqu’une application
charge un driver privé et non partagé.
∙ L’enregistrement automatique consiste, lui, à inscrire la clé
du driver nouvellement implémenté dans [HKEY_LOCAL_MACHINE\Drivers\BuiltIn\AnyDriver] (grâce
au fichier « platform.reg » dans Patform Builder) :
Au lancement du système, tous les drivers qui figurent à cet emplacement
seront enregistrés dans Active (et
donc accessibles par toutes les applications).
Cette méthode ‘automatique’ rend accessibles les drivers à
toutes les applications clientes dès le démarrage de Windows. Elle est la plus
élégante dans notre cas. C’est donc celle que nous utiliserons pour notre
exemple.
Développement du driver
Concernant
le développement d’un stream driver à
proprement parler, ce dernier doit implémenter un certain nombre de fonctions
exposées pour les applications clientes. Ces fonctions doivent se nommer XXX_Nom_de_la_fonction,
où XXX reprend le préfixe du driver. Parmi elles, certaines sont, de fait,
indispensables :
-
XXX_Init : initialisation du driver. C’est ici que l’on
place par exemple les bonnes valeurs dans les registres spécifiques du
composant (au niveau matériel) afin d’en assurer la bonne utilisation par la
suite. Cette fonction est appelée lors du chargement du driver. Elle prend en
paramètre un dwContext.
dwContext permet de différencier des appels à
XXX_Init, autorisant par exemple l’utilisation du même driver pour XXX1
et XXX2. Lors d’un appel automatique à XXX_Init
(cas d’un driver enregistré dans BuiltIn),
dwContext est à la valeur de la
donnée contenue dans [HKEY_LOCAL_MACHINE\Drivers\BuiltIn\MyDriver\Contex].
-
XXX_Open : ouverture d’un flux vers le driver. C’est ici
que l’on effectue les traitements à effectuer lors de l’ouverture du driver par
une application. Cette fonction est appelée quand l’application exécute
CreateFile.
-
XXX_Close : fermeture du driver par une application. Cette
fonction est appelée quand l’application exécute
CloseHandle.
-
XXX_Deinit : désinitialisation du driver. Cette fonction
est appelée lors de la libération du driver.
D’autres sont plus facultatives :
-
XXX_IOControl : traitements spécifiques du driver. Cette
fonction expose les différentes opérations que le driver est capable de
réaliser. Cette fonction est appelée quand l’application exécute
DeviceIOControl. Elle prend notamment en paramètre un code lui
permettant de savoir quelle opération effectuer.
-
XXX_Read : lecture à partir du driver. Cette fonction est
appelée quand l’application exécute ReadFile.
-
XXX_Write : écriture vers le driver. Cette fonction est
appelée quand l’application exécute WriteFile.
-
XXX_Seek : déplacement du curseur de lecture/écriture du
flux ouvert vers le driver. Cette fonction est appelée quand l’application
exécute SetFilePointer.
-
XXX_Power_Up : traitements à effectuer quand le système se
réveille.
-
XXX_Power_Down : traitements à effectuer quand le système
est sur le point de s’interrompre.
Dans le cas d’un projet dans Platform Builder, toutes ces
fonctions sont à déclarer dans un fichier .def et à implémenter dans le fichier
.cpp principal.
Utilisation du driver
Une
fois le driver implémenté, il ne reste plus qu’à l’appeler depuis une
application. Puisque le driver est déjà enregistré, il suffit dans un premier
temps d’ouvrir un flux vers ce driver. Ceci est rendu possible par
l’utilisation de la fonction CreateFile
présente dans la dll « coredll.dll ». En C#, cette fonction se
déclare comme suit :
// Déclaration de CreateFile
[DllImport("coredll.dll")]
public
static
extern
IntPtr CreateFile(
string
lpFileName,
uint
dwDesiredAccess,
uint
dwShareMode,
IntPtr
lpSecurityAttributes,
uint
dwCreationDisposition,
int
dwFlagsAndAttributes, IntPtr hTemplateFile );
Son appel permet de récupérer un handle vers le driver :
// Ouverture d'un flux vers le driver
hMSD = CreateFile("MSD1:", GENERIC_READ | GENERIC_WRITE, 0,
IntPtr.Zero,
OPEN_EXISTING, 0,
IntPtr.Zero);
Pour
utiliser par la suite les fonctionnalités du driver (implémentées dans la
fonction XXX_IOControl) vous devez utiliser la fonction DeviceIOControl présente également dans « coredll.dll ». Cette fonction
se déclare comme suit :
// Déclaration de DeviceIoControl
[DllImport("coredll.dll")]
public
static
extern
bool
DeviceIoControl(
IntPtr
hDevice,
//Pointeur vers le driver retourné par CreateFile
uint
dwIoControlCode,
//
Code identifiant l’opération à effectuer
IntPtr
lpInBuffer,
// Buffer pour des données en entrée
int
nInBufferSize,
//
Taille du buffer d’entrée
IntPtr
lpOutBuffer,
// Buffer pour données
en
sortie
int
nOutBufferSize, //
Taille du buffer de sortie
ref
int
lpBytesReturned,
// Nombre d’octets en sortie
IntPtr
lpOverlapped
// Pas utilisé
);
Le
code identifiant l’opération à effectuer a été attribué lors du développement
du driver dans la fonction XXX_IOControl. Le driver et l’application le
calculent à partir du même nombre choisi par le concepteur du driver. On
utilise pour cela la fonction CTL_CODE déclarée dans le fichier
« windev.h ». En C#, nous devons la redéfinir :
// Déclaration et définition de
CTl_CODE
public
static
uint
CTL_CODE(uint
DeviceType,
uint
Function,
uint
Method,
uint
Access)
{
return((DeviceType << 16) | (Access << 14) | (Function
<< 2) | Method);
}
Voici
maintenant un appel possible en C# de la fonction DeviceIOControl pour
l’envoi d’un caractère (code d’opération 2048) :
bool
ok;
//
Pour le retour des fonctions
IntPtr buffPtr;
// Buffer d'entrée
int
byret = 0;
// Retour du nombre d'octets utilisés
char
txChar = c;
// Caractère à envoyer
unsafe
{
buffPtr
= (IntPtr)(&txChar);
ok
= DeviceIoControl(
hMSD,
CTL_CODE(FILE_DEVICE_UNKNOWN,
2048,
METHOD_BUFFERED,
FILE_ANY_ACCESS),
buffPtr,
1,
IntPtr.Zero,
0,
ref
byret,
IntPtr.Zero);
}
Notez que l’on doit passer en code unsafe afin de récupérer l’adresse de txChar (autorisez les compilations unsafe dans les propriétés du projet).
Enfin,
nous devons fermer le flux utilisé vers le driver au moyen de la fonction CloseHandle :
// Déclaration de CloseHandle
[DllImport("coredll.dll")]
public
static
extern
bool
CloseHandle(IntPtr hObject);
CloseHandle(hMSD);
Notez que toutes ces fonctions sont définies dans
« coredll.dll » pour Windows CE alors qu’elles sont présentes dans
« kernel32.dll » pour les versions traditionnelles de Windows.
Finalement,
afin d’illustrer tous ces points, nous allons développer un driver utilisant le
port série. Ce driver servira par la suite à réaliser une application basique
de type HyperTerminal.
Driver Liaison Série
Nous
allons maintenant réaliser un driver capable d’envoyer et de recevoir des
caractères (un par un) sur le port série à un débit de 9600 bauds. Ce driver
sera réalisé en C sous Platform Builder.
Nous
allons donc créer une dll qui implémente les fonctions que nous venons de voir.
Nous allons également modifier la base de registre du noyau de Windows CE afin
qu’il charge notre dll au démarrage, rendant notre driver disponible pour les
applications.
Modification de la base de registre
Comme
nous voulons utiliser les deux ports série de notre machine cible, nous devons
créer deux entrées dans la base de registre, une pour chaque port. Cependant,
chacune de ces entrées pointe vers la même dll. Pour cela, il suffit d’ajouter
ces lignes au fichier « platform.reg » (il est accessible dans
l’onglet ParameterView) :
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\MySerial1]
"Index"=dword:1
"Prefix"="MSD"
"Dll"="my_serial_driver.dll"
"Order"=dword:0
"FriendlyName"="Driver serie perso pour le port serie 1"
"Context"=dword:1
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\MySerial2]
"Index"=dword:2
"Prefix"="MSD"
"Dll"="my_serial_driver.dll"
"Order"=dword:0
"FriendlyName"="Driver
serie perso pour le port serie 2"
"Context"=dword:2
Vous pourrez par la suite vérifier ces entrées dans la base de
registre de la machine cible. Pour cela, il faudra compiler le noyau avec
l’option « Enable CE Target Control Support », le déployer sur le
CEPC et lancer le « Remote Registry Editor » (dans le menu
« Tools »). Il vous est proposé une liste de connexions ; si la
connexion au CEPC est déjà présente, sélectionnez la. Sinon, annulez la boîte
de dialogue et cliquez sur « Connection
à
Configure Windows CE Platform Manager... ». Cliquez sur « Add
Device ». La connexion est ajoutée. Entrez ces propriétés en cliquant sur
« Properties » :
Nous voilà donc avec deux drivers : MSD1 et MSD2. Ces deux entrées
ont en commun le préfixe de notre driver (MSD), et la dll qui contient l’implémentation
du driver. Elles sont distinguées par l’index du driver et par le contexte.
Nous verrons plus loin comment s’utilise le contexte.
Création de la dll
Nous
allons maintenant créer notre dll, à savoir un projet « WCE Dynamic-Link
Library » :
Platform Builder crée automatiquement un fichier .cpp. Ajoutez
un fichier .def à votre projet.
Placez-y les lignes suivantes servant de déclaration des fonctions qui seront
implémentées et exportées dans le fichier .cpp :
;LIBRARY
STRINGS
EXPORTS
MSD_Init
MSD_Deinit
MSD_Open
MSD_Close
MSD_Read
MSD_Write
MSD_IOControl
MSD_PowerDown
MSD_PowerUp
MSD_Seek
Dans
le fichier « my_serial_driver.cpp », nous allons implémenter le code
des fonctions qui nous intéressent ici :
Init, Open, IOControl. Les autres fonctions existent mais sont laissées vides (elles se
contentent d’envoyer un message de déboguage).
MSD_Init
Nous
devons avant tout définir plusieurs constantes ; elles correspondent aux
adresses mémoire des registres du port série (déplacements par rapport à
l’adresse de base) et permettent la bonne configuration du port (vitesse,
parité, bit de stop...). Toutes ces constantes peuvent être obtenues dans la
documentation du port série.
Les
registres sont configurés dans la fonction MSD_Init
comme suit :
//
initialisation du statut du driver
WRITE_PORT_UCHAR(IoPortBase
+ comLineControl, 0x80);
WRITE_PORT_UCHAR(IoPortBase
+ comDivisorLow, 0x0C);
WRITE_PORT_UCHAR(IoPortBase
+ comDivisorHigh, 0x00);
WRITE_PORT_UCHAR(IoPortBase
+ comFIFOControl, 0x00);
WRITE_PORT_UCHAR(IoPortBase
+ comLineControl, 0x03);
WRITE_PORT_UCHAR(IoPortBase
+ comIntEnable, 0x00);
WRITE_PORT_UCHAR(IoPortBase
+ comModemControl, 0x03);
WRITE_PORT_UCHAR est
une macro qui permet d’écrire à l’adresse indiquée la valeur souhaitée de
l’octet. L’adresse des registres que l’on doit configurer est calculée par
rapport à une adresse de base IoPortBase.
Elle vaut 0x3F8 pour le port série 1, et 0x2F8 pour le port série 2. Les
déplacements par rapport à l’adresse de base correspondent aux adresses des
registres configurant les fonctionnalités suivantes :
-
comLineControl : le
registre situé à cette adresse sert à spécifier si l’on souhaite placer un bit
de stop à la fin de chaque paquet, si l’on souhaite un contrôle de parité (et
si oui, quel type de parité)...
-
comDivisorLow et
comDivisorHigh : ces deux registres servent à spécifier le débit
de la connexion à établir. Leur valeur correspond au diviseur à appliquer à
l’horloge du port série afin d’obtenir le débit de transmission. Une valeur de
12 (0x0C) comme ici donne un débit de 9600 bauds.
-
comFIFOControl : ce
registre permet de valider ou invalider la file du port série, ainsi que de la
vider.
-
comIntEnable : ce
registre permet de masquer ou non les interruptions. En le mettant ici à zéro,
on masque toutes les interruptions.
-
comModemControl : ce
registre sert à configurer l’émetteur et le récepteur du port série. Il permet
par exemple de placer l’émetteur en boucle.
Comme
nous voulons développer un driver utilisable pour les deux ports,
IoPortBase doit être paramétrable. Nous allons à cet effet utiliser le
paramètre dwContext de
MSD_Init. Lorsqu’il appelle MSD_Init
au démarrage, Windows donne à ce paramètre la valeur qu’il trouve dans la base
de registre. Comme notre dll est enregistrée deux fois (MSD1 et MSD2),
MSD_Init sera appelé deux fois : une première fois avec dwContext=1 et la deuxième fois avec dwContext=2. Notez bien que même si une dll est utilisée plus d’une fois (ici pour
MSD1 et MSD2), elle n’est chargée qu’une seule fois par Windows. Le code de
notre fonction MSD_Init tient donc
compte de dwContext pour
l’initialisation du driver :
// fonction d'initialisation du driver
DWORD MSD_Init(DWORD dwContext)
{
// adresse du port série
PUCHAR
IoPortBase;
// valeur de retour
DWORD
dwRet;
switch((int)dwContext)
{
case
1:
// MSD1:
IoPortBase
= ((PUCHAR)0x03F8);
dwRet
= 1;
break;
case
2: // MSD2:
IoPortBase
= ((PUCHAR)0x02F8);
dwRet
= 2;
break;
default:
// Au cas où...
IoPortBase
= ((PUCHAR)0x03F8);
dwRet
= 1;
break;
}
//
initialisation du statut du driver
WRITE_PORT_UCHAR(IoPortBase+comLineControl,
0x80);
WRITE_PORT_UCHAR(IoPortBase+comDivisorLow,0x0C);
WRITE_PORT_UCHAR(IoPortBase+comDivisorHigh,
0x00);
WRITE_PORT_UCHAR(IoPortBase+comFIFOControl,
0x00);
WRITE_PORT_UCHAR(IoPortBase+comLineControl,
0x03);
WRITE_PORT_UCHAR(IoPortBase+comIntEnable, 0x00);
WRITE_PORT_UCHAR(IoPortBase+comModemControl,0x03);
return
dwRet;
}
La valeur de retour de MSD_Init
est enregistrée par Windows, associée au nom du driver : 1 pour MSD1 et 2
pour MSD2. Il s’agit d’un « handle
de DeviceContext ». Cela va nous
permettre d’identifier le bon driver lors de l’appel à MSD_Open.
MSD_Open
MSD_Open
est appelé par le système lorsqu’une application cliente ouvre le driver avec
CreateFile. L’appel à CreateFile
précise le nom du driver (MSD1: ou MSD2: par exemple). Windows va donc appeler MSD_Open en lui passant en paramètre le handle de DeviceContext adéquat. C’est
donc en fonction de ce handle que
l’on va ouvrir le port 1 ou le port 2. Dans notre cas, l’ouverture du port
série consiste simplement à vider le buffer de réception :
// fonction d'ouverture de flux vers le
driver
DWORD MSD_Open(DWORD hDeviceContext, DWORD AccessCode, DWORD
ShareMode)
{
//
adresse du port série
PUCHAR
IoPortBase;
//
valeur de retour
DWORD dwRet;
switch((int)hDeviceContext)
{
case
1:
// MSD1
IoPortBase
= ((PUCHAR)0x03F8);
dwRet
= 100;
break;
case
2:
// MSD2
IoPortBase
= ((PUCHAR)0x02F8);
dwRet
= 200;
break;
default:
// Au cas où...
IoPortBase
= ((PUCHAR)0x03F8);
dwRet
= 100;
break;
}
//
vidage du buffer de reception si necessaire
while
((READ_PORT_UCHAR(IoPortBase + comLineStatus) &
LS_RX_DATA_READY)
== 1)
{
//
tant qu'il y a un caractère, on le lit
READ_PORT_UCHAR(IoPortBase
+ comRxBuffer);
}
return
dwRet;
}
De
la même manière que MSD_Init renvoie
un handle vers le driver (MSD1 ou
MSD2), MSD_Open renvoie un handle ou OpenContext.
OpenContext permet à une application d’identifier de manière unique son
flux. Ainsi, plusieurs applications peuvent ouvrir des flux vers un driver via
CreateFile, sans qu’il y ait de confusion.
OpenContext est retourné par CreateFile
et doit être conservé par l’application pour ses appels à DeviceIOControl, ReadFile,
WriteFile et SetFilePointer.
Normalement, chaque appel à CreateFile (et donc à MSD_Open) devrait
renvoyer un OpenContext différent et
unique. Nous ne l’avons pas fait ici, pour des raisons de clarté.
En outre, OpenContext
doit garder la trace du DeviceContext,
afin de permettre aux fonctions telles que MSD_IOCTL
de le retrouver.
MSD_IOCTL
Cette
fonction permet d’effectuer des opérations sur le driver. Elle est appelée
lorsque l’application exécute DeviceIOControl.
Tous les paramètres de DeviceIOControl
sont passés à MSD_IOCTL (ils
comprennent le OpenContext, le code
du traitement à réaliser, les buffers d’entrée et de sortie ainsi que leurs
tailles respectives...). Voici le code de la fonction :
// fonction d'appel aux contrôles
fournis par le driver
BOOL MSD_IOControl(DWORD hOpenContext,
DWORD
dwCode,
PBYTE
pBufIn,
DWORD
dwLenIn,
PBYTE
pBufOut,
DWORD
dwLenOut,
PDWORD
pdwActualOut)
{
// adresse du port série
PUCHAR
IoPortBase;
switch((int)hOpenContext)
{
case
100:
IoPortBase
= ((PUCHAR)0x03F8);
break;
case
200:
IoPortBase
= ((PUCHAR)0x02F8);
break;
default:
IoPortBase
= ((PUCHAR)0x03F8);
break;
}
//
en fonction de ce que l'on demande au driver
switch(dwCode)
{
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//!!!!!!!!!!! Traitements en fonction du code dwCode
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
return
TRUE;
}
Dans
notre cas, les traitements à réaliser seront les suivants :
-
IOCTL_PUTC : envoi d’un caractère :
// si l'on veut écrire un caractère
case
IOCTL_PUTC:
// on attend tant qu'il n'y a rien à lire
while(!(READ_PORT_UCHAR(IoPortBase
+ comLineStatus)& LS_THR_EMPTY));
// on ecrit à l'adresse pBufIn ce qui vient d'être lu
WRITE_PORT_UCHAR(IoPortBase
+ comTxBuffer, *pBufIn);
break;
-
IOCTL_GETC : récupération d’un caractère :
// si l'on veut lire un caractère
case
IOCTL_GETC:
// ecriture du caractère à envoyer à l'adresse pBufOut
*pBufOut
= READ_PORT_UCHAR(IoPortBase + comRxBuffer);
*pdwActualOut
= 1;
break;
-
IOCTL_GET_RX_STATUS : récupération du statut du port (caractère
reçu...) :
case
IOCTL_GET_RX_STATUS:
// écriture du statut du driver à l'adresse pBufOut
*pBufOut=(READ_PORT_UCHAR(IoPortBase+comLineStatus)&LS_RX_DATA_READY);
*pdwActualOut
= 1;
break;
Voilà
qui conclut le développement de notre driver de gestion du port série. Le code
complet de ce driver est disponible ici.
Voici également un schéma qui résume les étapes de l’utilisation d’un driver
par une application cliente :
Application
Ce
deuxième chapitre a pour but
de détailler l’utilisation des drivers à partir d’applications. Pour cela, nous
commencerons par exposer les concepts généraux, puis nous créerons un programme
de type HyperTerminal permettant d’envoyer et de recevoir des caractères via le
port série. Ce programme sera développé en C# .NET.
Généralités
Lorsque
vous devez développer une application pour Windows CE, plusieurs moyens sont à
votre disposition :
-
Platform Builder
-
eMbedded Visual C++
-
Visual Studio .NET 2003
Platform Builder vous permet de développer une application en
C/C++ et de l’envoyer vers votre CEPC ou un émulateur.
eMbedded Visual C++, lui, sert à développer des applications
en C++ pour vos PocketPC, SmartPhone et autres... Il inclut un émulateur
PocketPC pour tester vos applications. Ce logiciel, gratuit, est téléchargeable
sur le
site de Microsoft.
Enfin, Visual Studio 2003 offre la possibilité de créer un
projet « Application Smart Device » afin de développer ses propres
programmes en .NET.
Nous
allons écrire notre programme d’HyperTerminal en C# au moyen de Visual Studio
2003. Après avoir sélectionné « Application Smart Device », nous
choisissons de développer une application Windows :
Nous devons configurer la connexion que nous désirons utiliser
pour le déploiement et le déboguage de l’application. Dans notre cas, nous
devons déployer l’application sur le CEPC, ce qui est n’est pas possible avec
Visual Studio. Il nous reste donc deux possibilités :
-
amener l’exécutable ou le CAB de l’exécutable sur la machine cible via
le réseau, disquette... si les drivers adéquats sont installés.
-
inclure l’exécutable dans le noyau avec Platform Builder. Pour cela,
cliquez sur « Platform
à
Insert
à
User Feature... » et entrez le chemin de l’exécutable à inclure. Après
cette opération (ou après une modification de l’exe), il n’est pas nécessaire
de recompiler le noyau ; il suffit de refaire une image (« Build
à
Make Image »).
Dans la suite, nous utiliserons cette deuxième méthode.
Nous emploierons les fonctions marshallées de l’API Windows
présentées dans la première partie.
HyperTerminal personnalisé
Afin
de tester notre driver, nous allons développer une application fortement
inspirée de l’HyperTerminal de Windows. Le but est de pouvoir communiquer de
notre machine cible vers un autre PC relié par un câble série croisé. Sur ce
PC, il faudra ouvrir un HyperTerminal classique en configurant la vitesse à
9600 bauds. L’application permettra de choisir le port sur lequel dialoguer. Si
vous choisissez le port 1, vous verrez, en plus des caractères envoyés par le
CEPC, apparaître des messages de déboguage indiquant quelles fonctions du
driver sont appelées. L’apparition de ces messages dépend des options de
compilation.
Voici une capture d’écran de l’application :

Lorsque
l’on clique sur « Initialisation », l’application appelle la fonction
CreateFile et ouvre ainsi un flux vers le driver spécifié par les
boutons radio :
///<summary>
/// Lorsqu'on appuie sur le bouton
'Initialisation'
///</summary>
///<param name="sender">Objet à l'origine de l'appel</param>
///<param name="e">Paramètre du clic</param>
private
void
initButton_Click(object
sender, System.EventArgs e)
{
string
port = "MSD1:";
if(MSD2.Checked)
port
= "MSD2:";
MSD1.Enabled
=
false;
MSD2.Enabled
=
false;
// Ouverture d'un flux vers le driver
hMSD
= DriverManagement.CreateFile(
port,
DriverManagement.GENERIC_READ
| DriverManagement.GENERIC_WRITE,
0,
IntPtr.Zero,
DriverManagement.OPEN_EXISTING,
0,
IntPtr.Zero);
// Si erreur
if((int)hMSD
== DriverManagement.INVALID_HANDLE_VALUE)
{
MessageBox.Show("Echec
de CreateFile");
// Message
statusLabel.ForeColor
= Color.Red;
statusLabel.Text
= "driver non ouvert";
return;
}
statusLabel.ForeColor
= Color.Green;
statusLabel.Text
= "connecté";
// Initialisation du timer
rxTimer
=
new
Timer();
rxTimer.Interval
= 100;
rxTimer.Tick
+=
new
EventHandler(rxTimer_Tick);
rxTimer.Enabled
=
true;
}
Notez ici l’utilisation de méthodes et d’attributs de la
classe personnelle DriverManagement.
Nous avons créé cette classe dans notre application afin de faciliter le
maniement des fonctions d’accès aux drivers. Elle contient les déclarations des
fonctions de l’API Windows utiles à la gestion du driver, ainsi que les
constantes nécessaires à ces fonctions.
Une
fois la connexion établie, on envoie tous les caractères tapés dans la zone de
« Texte envoyé ». Ceci se fait au moyen de la fonction
DeviceIOControl avec le code du traitement MSD_IOCTL_PUTC :
///<summary>
/// Envoi d'un caractère
///</summary>
///<param name="c">Caractère à envoyer</param>
private
void
SendChar(char
c)
{
bool
ok;
// Pour le retour
des fonctions
IntPtr
buffPtr;
// Buffer d'entrée
int
byret = 0;
// Retour du
nombre d'octets utilisés
char
txChar = c;
//
Caractère à envoyer
txTextBox.Text
+= txChar;
unsafe
{
buffPtr
= (IntPtr)(&txChar);
ok =
DriverManagement.DeviceIoControl(
hMSD,
DriverManagement.CTL_CODE(
DriverManagement.FILE_DEVICE_UNKNOWN,
2048,
DriverManagement.METHOD_BUFFERED,
DriverManagement.FILE_ANY_ACCESS),
buffPtr,
1,
IntPtr.Zero,
0,
ref
byret,
IntPtr.Zero);
}
// Si erreur
if(!ok)
{
statusLabel.ForeColor
= Color.Red;
statusLabel.Text
= "échec lors de l'envoi";
return;
}
statusLabel.ForeColor
= Color.Green;
statusLabel.Text
= "connecté";
}
La
réception, quant à elle, se fait grâce à un timer, initialisé lors du clic sur
le bouton « Initialisation » (voir plus haut).
A chaque top du timer, on vérifie le statut de réception et s’il y a le lieu,
on récupère le caractère lu :
///<summary>
/// Lorsque le timer se déclenche
///</summary>
///<param name="state">Paramètre passé le cas
échéant</param>
private
void
rxTimer_Tick(object
sender, EventArgs e)
{
bool
ok;
// Pour le retour
des fonctions
IntPtr
buffPtr;
// Buffer de sortie
int
byret = 0;
// Retour du
nombre d'octets utilisés
byte
status;
// Retour du
statut du driver
char
rxChar;
// Caractère
reçu
unsafe
{
//
Initialisation du pointeur vers l'octet de statut
buffPtr
= (IntPtr)(&status);
// On demande son statut au driver
ok =
DriverManagement.DeviceIoControl(
hMSD,
DriverManagement.CTL_CODE(
DriverManagement.FILE_DEVICE_UNKNOWN,
2050,
DriverManagement.METHOD_BUFFERED,
DriverManagement.FILE_ANY_ACCESS),
IntPtr.Zero,
0,
buffPtr,
1,
ref
byret,
IntPtr.Zero);
}
// Si erreur
if(!ok)
{
statusLabel.ForeColor
= Color.Red;
statusLabel.Text
= "statut du driver inaccessible";
return;
}
// Si réception de caractère
if(status
== 1)
{
unsafe
{
// Initialisation du pointeur vers le caractère à lire
buffPtr
= (IntPtr)(&rxChar);
// On demande au driver le caractère lu
ok
= DriverManagement.DeviceIoControl(
hMSD,
DriverManagement.CTL_CODE(
DriverManagement.FILE_DEVICE_UNKNOWN,
2049,
DriverManagement.METHOD_BUFFERED,
DriverManagement.FILE_ANY_ACCESS),
IntPtr.Zero,
0,
buffPtr,
1,
ref
byret,
IntPtr.Zero);
}
// Si erreur
if(!ok)
{
statusLabel.ForeColor
= Color.Red;
statusLabel.Text
= "échec lors de la réception";
return;
}
// On affiche le caractère reçu
rxTextBox.Text
+= rxChar;
}
statusLabel.ForeColor
= Color.Green;
statusLabel.Text
= "connecté";
}
Finalement, la fonction CloseHandle
est appelée lors de l’événement Closing
de la fenêtre.
Le code complet de l’application est téléchargeable
ici. Cette application est, somme toute, assez modeste. Elle illustre
cependant très simplement l’utilisation du driver. On peut ensuite y ajouter
tout le développement .NET désiré.
Conclusion
Nous avons développé un stream
driver pour la gestion du port série, ainsi qu’une application .NET l’utilisant.
La réalisation du driver nécessite l’implémentation dans une dll de certaines
fonctions, ainsi que l’inscription du driver dans la base de registre. Du
côté de l’application, il suffit d’utiliser les foncions de l’API Windows
qui initialisent le driver et utilisent ses fonctionnalités. L’écriture d’un
stream driver plus complexe reposerait
sur les mêmes principes mais nécessiterait une con
naissance parfaite du matériel visé et de sa configuration. Il pourrait
s’agir d’un driver de communication cryptée. Il pourrait également être question
d’une carte électronique artisanale connectée à un port du PC. Dans ces deux
cas, on pourrait soit s’appuyer sur un driver existant, soit réimplémentant
la gestion du port utilisé (comme il est fait dans cet article). En effet,
il est possible de créer un driver qui emploie les fonctionnalités d’un driver
de plus bas niveau. On peut imaginer de la même manière un driver s’appuyant
sur le driver du bus PCI ; dans ce cas cependant, il s’agirait plus probablement
d’un native driver exportant une
interface spécifique à la carte.