Virtualisation avec des Experts
Bienvenue sur le Laboratoire SUPINFO des technologies Microsoft !
Nous sommes étudiants et travaillons sur des centaines de projets sur les technologies Microsoft.
Nous préparons la migration de ce portail vers les technologies SharePoint 2010.
L'équipe du Laboratoire Microsoft




Tous les Articles du Laboratoire Microsoft

Programmation sur Windows CE : Création d'un pilote
Accueil > Articles > Matériels
Auteurs 
Julien BAKMEZDJIAN



 Tous les articles de cet auteur

3,5/5

Bien


53805
51/183

                                   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.




En Savoir Plus 
Evaluez cet article 


Pour afficher ou poster un commentaire, cliquez sur ce lien : Forum-Microsoft



Retrouvez ci-dessous les autres sections du Laboratoire Microsoft