Communication RS232 en C++ Builder  
 

 

Première partie : Gestion des entrées/Sorties de périphériques

L’une des grandes force Windows NT et de Windows 95 réside dans le nombre de périphériques que ces environnements savent gérer. D’un façon générale, nous pouvons définir un périphérique comme quelque chose permettant une communication. Voici une liste décrivant leur usage habituel :

Périphériques Usage
Fichier Stockage de données
Répertoire Attributs et compression de fichiers
Disque logique Formatage
Disque physique Table de partitionnement
Port série Transmission de données en série
Port parallèle Envoi de données en parallèle
Console Une fenêtre texte

Win32 tente de masquer les différences entre périphériques afin de faciliter la tâche du développeur d’application. Ainsi, lorsque vous avez ouvert un périphérique, les fonctions Win32 qui permettent de lire ou d’écrire des données, sont les même quel que soit le mode de communication employé.

Cette partie présente les différents mécanismes disponibles pour lire et écrire des données sur un périphérique. Nous nous intéresserons plus particulièrement à la manipulation de ces fonctions pour des périphérique de type port série.

Il faut noter que bien que les fonctions utilisées soient communes à tout type de périphérique leur utilisation et paramétrage diffère d’un périphérique à l’autre. Par exemple nous pouvons régler la vitesse de transmission sur une ligne série, mais ce paramètre n’a aucun sens pour un fichier.

Ouvrir et fermer un périphérique

Avant de pouvoir effectuer une quelconque opération d’entrée/sortie, vous devez ouvrir le périphérique et obtenir un handle. D’une façon générale la plupart des périphériques sont ouvert avec la fonction CreateFile, dont la description est donnée ci après :

HANDLE CreateFile(
    LPCTSTR lpFileName,
    DWORD dwDesiredAccess,
    DWORD dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDistribution,
    DWORD dwFlagsAndAttributes,
    HANDLE hTemplateFile
   );

Le paramètre lpFileName identifie le type de périphérique ainsi que l’instance spécifique de ce périphérique.  LpFileName aura pour valeur :

  • « COMx » avec n° du port série dans le cas de l’ouverture du port série
  • « LPTx » avec x n° du port parallèle dans le cas de l’ouverture du port parallèle
  • Un nom de fichier dans le cas de l’ouverture dans fichier

Le paramètre dwDesiredAccess précise comment vous souhaitez transférer les données avec le périphérique. Quatre valeurs sont possibles :

Valeur Signification
0 Vous ne souhaitez ni Lire ni écrire sur le périphérique.
Vous souhaitez juste changer sa configuration et ses réglages
GENERIC_READ Permet des accès en lecture seule sur le périphérique
GENERIC_WRITE Permet des accès en écriture seule sur le périphérique.
GENERIC_READ| GENERIC_WRITE Permet des accès en lecture et en écriture sur le périphérique

Le paramètre dwShareMode détermine les privilèges de partage. Sous Windows 95 et Windows NT, un périphérique peut être employé par plusieurs ordinateurs à la fois ou par plusieurs processus en même temps. Il devient alors important de pouvoir réduire les droits d’accès des autres systèmes et processus sur le périphérique que vous utilisez. Les différentes valeurs possibles pour dwShareMode sont :

Valeur Signification
Vous souhaitez un accès exclusif à ce périphérique. S’il est déjà ouvert, CreateFile échoue.
Si vous l’ouvrez et qu’un autre appel CreateFile est effectué ensuite, cet appel échouera.
FILE_SHARE_READ Vous souhaitez utiliser le périphérique en lecture seul. S’il est déjà ouvert en écriture, CreateFile échouera.
Si vous l’ouvrez et qu’un autre appel CreateFile est effectué en écriture, cet appel échouera.
FILE_SHARE_WRITE Vous souhaitez uniquement écrire dans le périphérique. S’il est déjà ouvert en lecture, CreateFile échouera.
Si vous l’ouvrez et qu’un autre appel CreateFile est effectué en lecture, cet appel échouera.
FILE_SHARE_READ| FILE_SHARE_WRITE Vous souhaitez utiliser le périphérique en lecture et en écriture.
Si le périphérique est déjà ouvert pour un accès exclusif, CreateFile échouera.
Si vous l’ouvrez et qu’un autre appel  CreateFile est effectué en lecture ou en écriture, cet appel échouera.

Le paramètre lpSecurityAttributes, pointe sur une structure SECURITY_ATTRIBUTES qui vous permet de préciser les informations de sécurité de l’objet du noyau associé au périphérique. Vous définissez également si le handle obtenu en retour sera héritable ou non ( permettant ainsi à des processus enfant d’accéder au périphérique). Le paramètre est  NULL si vous souhaiter mettre en œuvre la sécurité par défaut et un handle non héritable.

Le paramètre dwCreationDistribution est utile lorsque CreateFile ouvre un fichier. Les différentes valeurs dwCreationDistribution sont :

Valeur Signification
CREATE_NEW  Demande à CreateFile de créer un nouveau fichier. Echec si un fichier de même nom existe.
CREATE_ALWAYS  Demande à CreateFile de créer un fichier sans vérification de l’existance ou non de l’existance d’un fichier de même nom.
OPEN_EXISTING  Demande à CreateFile d’ouvrir un fichier existant. Erreur si le fichier n’existe pas.
OPEN_ALWAYS  Demande à CreateFile d’ouvrir un fichier s’il existe ou de le créer s’il n’existe pas encore.
TRUNCATE_EXISTING  Demande à CreateFile d’ouvrir un fichier existant et de ramener sa taille à 0 octet.
Echec si le fichier n’existe pas déjà. Le drapeau GENERIC_WRITE doit être également utilisé.

Lorsque vous appelez CreateFile pour ouvrir un périphérique qui n’est pas un fichier, vous devez utiliser OPEN_EXISTING pour le paramètre dwCreationDistribution.

Le paramètre dwFlagsAndAttributes vous permet de définir des drapeaux affinant la communication avec le périphérique et si le périphérique est un fichier vous obtenez ses attributs. Dans le cas de la communication qui nous intéresse nous n’utiliserons pas ce paramètre : valeur = 0. Il faut noter que si vous souhaitez utiliser le périphérique de manière asynchrone, il faut utiliser le drapeau FILE_FLAG_OVERLAPPED. Ce drapeau indique au système que vous voulez accéder au fichier de manière asynchrone ( par défaut l’ouverture est synchrone).
Si vous communiquer en mode synchrone, le programme est suspendu dans l’attente des informations devant être lues. Une fois la lecture terminée, le programme reprend le contrôle et continue sont exécution.
Théoriquement les entrées/sorties sur les périphériques étant très lentes par rapport à la plupart des opérations du processeur, il est préférable de communiquer avec un périphérique de façon asynchrone. Dans le principe, vous appelez une fonction demandant au système d’exploitation de lire ou d’écrire des données mais cette fonction rend la main immédiatement, sans attendre la fin de l’opération. Le système d’exploitation traite votre demande de façon autonome et vous renvoie un message lorsque le traitement est terminé. Pendant ce temps votre application a continué à s’exécuter. Nous ne traiterons pas ce cas dans notre exemple.

Le paramètre hTemplateFile permet de spécifier les attributs du fichier. Il aura pour valeur NULL dans notre cas

Nous allons maintenant aborder la lecture et l’écriture de donnée synchrone sur un périphérique

Lecture et écriture synchrone sur un périphérique :

Cette section aborde les fonctions de Win32 qui permettent des lectures et des écritures synchrones sur des périphériques ( nous ne traiterons pas les lecture asynchrones ). Rappel un périphérique peut  être un fichier, un canal, un port …

La méthode la plus simple et la plus facile à mettre en œuvre pour lire et écrire des fichiers passe par les deux fonctions :

BOOL ReadFile(
    HANDLE hFile, // handle of file to read
    LPVOID lpBuffer, // address of buffer that receives data
    DWORD nNumberOfBytesToRead, // number of bytes to read
    LPDWORD lpNumberOfBytesRead, // address of number of bytes read
    LPOVERLAPPED lpOverlapped  // address of structure for data
   );

BOOL WriteFile(
    HANDLE hFile, // handle to file to write to
    LPCVOID lpBuffer, // pointer to data to write to file
    DWORD nNumberOfBytesToWrite, // number of bytes to write
    LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
    LPOVERLAPPED lpOverlapped  // pointer to structure needed for overlapped I/O
   );

Bien que chacune de ces fonctions possèdent le mot File dans son nom, elles peuvent être utilisées avec n’importe quel type de périphérique.

Le paramètre hFile identifie le handle du périphérique auquel vous souhaitez accéder.

Le paramètre lpBuffer pointe sur une mémoire tampon qui servira lors des échanges d’écriture/lecture entre le périphérique et l’application.

Le paramètre nNumberOfBytesToWrite et nNumberOfBytesToRead indique à ReadFile et WriteFile combien d’octets doivent être lus ou écrits sur le périphérique.

Le paramètre  lpNumberOfBytesRead et lpNumberOfBytesWritten indique l’adresse d’un DWORD contenant le nombre d’octets réellement transmis.

Le paramètre lpOverlapped doit être NULL lors d’opération synchrone.

Les fonctions ReadFile et WriteFile renvoient TRUE en cas de succès. ReadFile ne peut être utilisée qu’avec des périphériques ouvert avec le drapeau GENERIC_READ. De même WriteFile ne peut être utilisée que si le périphérique est ouvert avec le drapeau GENERIC_WRITE.

Configuration du périphérique de communication

Le fichier étant ouvert il ne nous reste plus qu’à configurer le port de communication. Pour cela nous utilisons la fonction SetCommSate :

BOOL SetCommState(
    HANDLE hFile, // handle of communications device
    LPDCB lpDCB // address of device-control block structure
   );

Le paramètre hFile identifie le handle de communication retourné par la fonction CreateFile

Le paramètre lpDCB indique l’adresse de la structure DCB contenant les informations de configuration spécifique au port de communication.

Description de la structure DCB

La structure DCB permet de configurer le port serie. Cette structure est définie comme suit :

    typedef struct _DCB { // dcb
        DWORD DCBlength;           // sizeof(DCB)
        DWORD BaudRate;            // current baud rate
        DWORD fBinary: 1;          // binary mode, no EOF check
        DWORD fParity: 1;          // enable parity checking

        DWORD fOutxCtsFlow:1;      // CTS output flow control
        DWORD fOutxDsrFlow:1;      // DSR output flow control
        DWORD fDtrControl:2;       // DTR flow control type
        DWORD fDsrSensitivity:1;   // DSR sensitivity

        DWORD fTXContinueOnXoff:1; // XOFF continues Tx
        DWORD fOutX: 1;            // XON/XOFF out flow control
        DWORD fInX: 1;             // XON/XOFF in flow control
        DWORD fErrorChar: 1;       // enable error replacement
        DWORD fNull: 1;            // enable null stripping
        DWORD fRtsControl:2;       // RTS flow control
        DWORD fAbortOnError:1;     // abort reads/writes on error
        DWORD fDummy2:17;          // reserved
        WORD wReserved;            // not currently used

        WORD XonLim;               // transmit XON threshold
        WORD XoffLim;              // transmit XOFF threshold
        BYTE ByteSize;             // number of bits/byte, 4-8
        BYTE Parity;               // 0-4=no,odd,even,mark,space
        BYTE StopBits;             // 0,1,2 = 1, 1.5, 2
        char XonChar;              // Tx and Rx XON character
        char XoffChar;             // Tx and Rx XOFF character
        char ErrorChar;            // error replacement character

        char EofChar;              // end of input character
        char EvtChar;              // received event character
        WORD wReserved1;           // reserved; do not use
    } DCB;

Le paramètre DCBlength donne la taille en octets de la structure.

Le paramètre BaudRate spécifie la vitesse de transmission sur le port série. La valeur peut être n’importe quelle valeur ou une des valeurs standards définies ci après :

CBR_110
CBR_300
CBR_600
CBR_1200
CBR_2400
CBR_4800
CBR_9600
CBR_14400

CBR_19200
CBR_38400
CBR_56000
CBR_57600
CBR_115200
CBR_128000
CBR_256000

FBinary  doit être à TRUE pour des application Windows.

Le paramètre fParity spécifie la vérification ou non de la parité. TRUE la parité est vérifiée et une erreur est générée si nécessaire.

Les paramètres suivants ne sont pas utilisés en communication 3 fils. Il sont uniquement à configurer dans le cas de protocole XON/XOFF et permettent de contrôler les signaux DTR ( terminal prêt), CTS(Voie Libre), DSR ( jeu de donnée prêt), RTS (demande pour émettre) . Nous ne rentrerons donc pas dans le détail de ces paramètres. De façon générale, et dans le cadre de cette présentation, nous leur affecterons la valeur 0.

FOutxCtsFlow
Specifies whether the CTS (clear-to-send) signal is monitored for output flow control. If this member is TRUE and CTS is turned off, output is suspended until CTS is sent again.

fOutxDsrFlow
Specifies whether the DSR (data-set-ready) signal is monitored for output flow control. If this member is TRUE and DSR is turned off, output is suspended until DSR is sent again.

fDtrControl
Specifies the DTR (data-terminal-ready) flow control. This member can be one of the following values:

Value Meaning
DTR_CONTROL_DISABLE Disables the DTR line when the device is opened and leaves it disabled.
DTR_CONTROL_ENABLE Enables the DTR line when the device is opened and leaves it on..
DTR_CONTROL_HANDSHAKE Enables DTR handshaking. If handshaking is enabled, it is an error for
the application to adjust the line by using the EscapeCommFunction function.

fDsrSensitivity
Specifies whether the communications driver is sensitive to the state of the DSR signal. If this member is TRUE, the driver ignores any bytes received, unless the DSR modem input line is high.

fTXContinueOnXoff
Specifies whether transmission stops when the input buffer is full and the driver has transmitted the XoffChar character. If this member is TRUE, transmission continues after the input buffer has come within XoffLim bytes of being full and the driver has transmitted the XoffChar character to stop receiving bytes. If this member is FALSE, transmission does not continue until the input buffer is within XonLim bytes of being empty and the driver has transmitted the XonChar character to resume reception.

fOutX
Specifies whether XON/XOFF flow control is used during transmission. If this member is TRUE, transmission stops when the XoffChar character is received and starts again when the XonChar character is received.

fInX
Specifies whether XON/XOFF flow control is used during reception. If this member is TRUE, the XoffChar character is sent when the input buffer comes within XoffLim bytes of being full, and the XonChar character is sent when the input buffer comes within XonLim bytes of being empty.

fErrorChar
Specifies whether bytes received with parity errors are replaced with the character specified by the ErrorChar member. If this member is TRUE and the fParity member is TRUE, replacement occurs.

fNull
Specifies whether null bytes are discarded. If this member is TRUE, null bytes are discarded when received.

fRtsControl
Specifies the RTS (request-to-send) flow control. If this value is zero, the default is RTS_CONTROL_HANDSHAKE. This member can be one of the following values:

Value Meaning
RTS_CONTROL_DISABLE  Disables the RTS line when the device is opened and leaves it disabled.
RTS_CONTROL_ENABLE Enables the RTS line when the device is opened and leaves it on.
RTS_CONTROL_HANDSHAKE

Enables RTS handshaking. The driver raises the RTS line when the "type-ahead" (input)
buffer is less than one-half full and lowers the RTS line when the buffer is more than
three-quarters full. If handshaking is enabled, it is an error for the application to adjust
the line by using the EscapeCommFunction function.

RTS_CONTROL_TOGGLE  Specifies that the RTS line will be high if bytes are available for transmission.
After all buffered bytes have been sent, the RTS line will be low.

fAbortOnError
Specifies whether read and write operations are terminated if an error occurs. If this member is TRUE, the driver terminates all read and write operations with an error status if an error occurs. The driver will not accept any further communications operations until the application has acknowledged the error by calling the ClearCommError function.

fDummy2
Reserved; do not use.

wReserved
Not used; must be set to zero.

XonLim
Specifies the minimum number of bytes allowed in the input buffer before the XON character is sent.

XoffLim
Specifies the maximum number of bytes allowed in the input buffer before the XOFF character is sent. The maximum number of bytes allowed is calculated by subtracting this value from the size, in bytes, of the input buffer.

XonChar
Specifies the value of the XON character for both transmission and reception.

XoffChar
Specifies the value of the XOFF character for both transmission and reception.

ErrorChar
Specifies the value of the character used to replace bytes received with a parity error.

EofChar
Specifies the value of the character used to signal the end of data.

EvtChar
Specifies the value of the character used to signal an event.

wReserved1
Reserved; do not use.

Le paramètre ByteSize définit le nombre de bits de l’octet

Le paramètre Parity définit le type de parité utilisé dans le protocole. Les valeurs possibles sont :

Valeur Description
EVENPARITY Parité paire
NOPARITY Pas de parité
ODDPARITY Parité impaire

Le paramètre StopBits définit le nombre de stop bit. Les valeurs possibles sont :

Valeur Description
ONESTOPBIT 1 stop bit
ONE5STOPBITS 1.5 stop bits
TWOSTOPBITS 2 stop bits

Définition des TimeOut de communication

La définition des timeouts de communication est utilisée pour arrêter le process de communication au bout de x (ms). Dans notre cas un timeout sera utilisé pour sortir de la boucle de réception étant donnée que l’on ne veut pas gérer la taille des trames reçue. Nous fixerons donc une taille de buffer de réception fixe que nous traiterons en fin de time out.

La gestion des timeouts se fait à l’aide des commandes SetCommTimeouts pour la configuration, GetCommTimeouts pour la lecture de la configuration et de la structure COMMTIMEOUTS contenant la description des timeouts :

Fonction SetCommTimeouts : utilisée pour configurer les timeOuts

BOOL SetCommTimeouts(
    HANDLE hFile,                                          // handle of communications device
    LPCOMMTIMEOUTS lpCommTimeouts // address of communications time-out structure
   );

Le paramètre hFile identifie le port de communication retourné par la fonction CreateFile

Le paramètre lpCommTimeouts pointe sur une structure COMMTIMEOUTS qui contient les nouvelles valeurs de time out

La valeur de retour est différente de 0 en cas de succès.

Fonction GetCommTimeouts : utilisée pour lire les timeouts

BOOL GetCommTimeouts(
    HANDLE hFile,                                           // handle of communications device
    LPCOMMTIMEOUTS lpCommTimeouts  // address of comm. time-outs structure
   );

Le paramètre hFile identifie le port de communication retourné par la fonction CreateFile

Le paramètre lpCommTimeouts pointe une structure contenant la configuration actuelle des timeouts.

La valeur de retour est différente de 0 en cas de succès.

La structure COMMTIMEOUTS : utilisée pour définir les timeouts

typedef struct _COMMTIMEOUTS {   // ctmo
    DWORD ReadIntervalTimeout;
    DWORD ReadTotalTimeoutMultiplier;
    DWORD ReadTotalTimeoutConstant;
    DWORD WriteTotalTimeoutMultiplier;
    DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

ReadIntervalTimeout
Specifies the maximum time, in milliseconds, allowed to elapse between the arrival of two characters on the communications line. During a ReadFile operation, the time period begins when the first character is received. If the interval between the arrival of any two characters exceeds this amount, the ReadFile operation is completed and any buffered data is returned. A value of zero indicates that interval time-outs are not used.
A value of MAXDWORD, combined with zero values for both the ReadTotalTimeoutConstant and ReadTotalTimeoutMultiplier members, specifies that the read operation is to return immediately with the characters that have already been received, even if no characters have been received.

ReadTotalTimeoutMultiplier
Specifies the multiplier, in milliseconds, used to calculate the total time-out period for read operations. For each read operation, this value is multiplied by the requested number of bytes to be read.

ReadTotalTimeoutConstant
Specifies the constant, in milliseconds, used to calculate the total time-out period for read operations. For each read operation, this value is added to the product of the ReadTotalTimeoutMultiplier member and the requested number of bytes.
A value of zero for both the ReadTotalTimeoutMultiplier and ReadTotalTimeoutConstant members indicates that total time-outs are not used for read operations.

WriteTotalTimeoutMultiplier
Specifies the multiplier, in milliseconds, used to calculate the total time-out period for write operations. For each write operation, this value is multiplied by the number of bytes to be written.

WriteTotalTimeoutConstant
Specifies the constant, in milliseconds, used to calculate the total time-out period for write operations. For each write operation, this value is added to the product of the WriteTotalTimeoutMultiplier member and the number of bytes to be written.
A value of zero for both the WriteTotalTimeoutMultiplier and WriteTotalTimeoutConstant members indicates that total time-outs are not used for write operations.

Remarks

If an application sets ReadIntervalTimeout and ReadTotalTimeoutMultiplier to MAXDWORD and sets ReadTotalTimeoutConstant to a value greater than zero and less than MAXDWORD, one of the following occurs when the ReadFile function is called:
- If there are any characters in the input buffer, ReadFile returns immediately with the characters in the buffer.
- If there are no characters in the input buffer, ReadFile waits until a character arrives and then returns immediately.
- If no character arrives within the time specified by ReadTotalTimeoutConstant, ReadFile times out.

Deuxième partie : Application des différente fonctions de gestion des Entrées / Sorties

Nous avons présenté dans le paragraphe précédent une liste de fonctions et de structures permettant la gestion des périphériques. Dans cette partie nous utiliserons ces fonctions pour configurer un port série et faire une lecture/écriture sur le port série.

Configuration du port série : exemple de fonction

/*------------------------------------------------------------------------*/
/*                 Fonction d’initialisation d’un port RS232              */
/*                                                                                             */
/* Paramétres                                                                         */
/*  char *Port    : "COM1" pour Port1, "COM2" pour port2         */
/*  char *Parite  : "Paire", "Impaire", "Aucune"                          */
/*  char *Vitesse : la vitesse en bauds                                   */
/*  char *Data    : le nombre de bit de donnée                            */
/*  char *StopBit : Le nombre de stop Bit                                 */
/*                                                                                             */
/* Retour : un entier egal à 0 si erreur.                                 */
/*------------------------------------------------------------------------*/

int InitCOM(char *Port,char *Parite,char *Vitesse,char *Data,char *StopBit)

 {
 DCB dcb;              // déclarer une variable contenant la configuration du port
 HANDLE hCom;  // déclarer un handle
 DWORD dwError; // n° de l’erreur
 BOOL fSuccess;    // tout c’est bien passé

  /*--------------------------------------------------------*/
 /*                 Ouverture du port de Com                */
 /*--------------------------------------------------------*/
 hCom = CreateFile(
    Port,                         // Choix du port « COMx »
    GENERIC_READ | GENERIC_WRITE, // accès pour lire et écrire sur le port
    0,                            // accès exclusif au port de COM
    NULL,                         // sécurité par défaut
    OPEN_EXISTING,                //Doit être à cette valeur car se n’est pas un fichier
    0,
    NULL                          // mode synchrone
 
/*-----------------------------------------------------------*/
/*         Vérifier si handle ouvert correctement            */
/*-----------------------------------------------------------*/
 if (hCom == INVALID_HANDLE_VALUE)
  {
    dwError = GetLastError();
    /* Fichier non créer Gérer l'erreur */
  }

/*-----------------------------------------------------------*/
/* Lecture Configuration initiale                            */
/*-----------------------------------------------------------*/

 fSuccess = GetCommState(hCom, &dcb);

 if (!fSuccess)
  {
    /* Gérer l'erreur*/
  }

 /*-------------------------------------------------------------------*/
 /* Configuration du port                                             */
 /*-------------------------------------------------------------------*/

 /* Gestion de la vitesse */
 dcb.BaudRate = StrToInt(Vitesse);
 /* Gestion du nombre de bits */
 dcb.ByteSize = StrToInt(Data);
 /* Gestion de la parité */
 if (strcmp(Parite,"Aucune")==0)

  dcb.Parity = NOPARITY;

if (strcmp(Parite,"Paire")==0)

  dcb.Parity = EVENPARITY;

 if (strcmp(Parite,"Impaire")==0)

  dcb.Parity = ODDPARITY;

 /* Gestion du Stop Bit */

 if (strcmp(StopBit,"1")==0)
  dcb.StopBits = ONESTOPBIT;
 if (strcmp(StopBit,"1.5")==0)
  dcb.StopBits = ONE5STOPBITS;
 if (strcmp(StopBit,"2")==0)
  dcb.StopBits = TWOSTOPBITS;
  dcb.DCBlength;
  dcb.BaudRate;
  dcb.fBinary=1;
  dcb.fParity=0;
  dcb.fOutxCtsFlow=0;
  dcb.fOutxDsrFlow=0;
  dcb.fDtrControl=0;
  dcb.fDsrSensitivity=0;
  dcb.fTXContinueOnXoff=0;
  dcb.fRtsControl=0;

/*-----------------------------------------------*/
/* Configurer le port                            */
/*-----------------------------------------------*/
 fSuccess = SetCommState(hCom, &dcb);
 if (!fSuccess)
  {
   /* Gérer l'erreur*/
  }

 /*------------------------------------------*/
 /* fermer le port de com                    */
 /*------------------------------------------*/
 CloseHandle(hCom);
 return(fSuccess);
 }

Ecriture sur un port série : Exemple de fonction d’envoie de donnée

/*-----------------------------------------------------------------------------*/
/*                    Envoyer une chaine de caractére sur la RS232             */
/*                                                                             */
/* Paramètres :                                                                */
/*             char *Chaine La chaine à Envoyer                                */
/*             char *Port, le port de COM : "COM1" ou "COM2"                   */
/*             char *EolChar, le caractère fin de ligne                        */
/*                                                                             */
/* Retour : 0 si erreur                                                        */
/*-----------------------------------------------------------------------------*/

int EnvoiChaineRS232(char *Chaine,char *Port,char *EolChar)
 {
  HANDLE hCom;
  DWORD dwError;
  BOOL fSuccess;
  DWORD dwEvtMask;
  int i;
  int NbOctet;
  char *Message;
  unsigned long nBytesWrite;
 
  Message = new char[200];

 /*-----------------------------------------------*/
 /* Ouverture du port de communiucation           */
 /*-----------------------------------------------*/

 hCom = CreateFile(Port,
    GENERIC_READ | GENERIC_WRITE,
    0,
    NULL,
    OPEN_EXISTING,
    0,
    NULL
    );
 /*--------------------------------------------------*/
 /*         Envoi de la chaine + Eol caractére       */
 /*--------------------------------------------------*/

 if(strcmp(EolChar,"CR")==0)
  sprintf(Message,"%s%c",Chaine,0x0D);
 if(strcmp(EolChar,"LF")==0)
  sprintf(Message,"%s%c",Chaine,0x0A);

 if(strcmp(EolChar,"LF/CR")==0)
  sprintf(Message,"%s%c%c",Chaine,0x0A,0x0D);

 if(strcmp(EolChar,"CR/LF")==0)
  sprintf(Message,"%s%c%c",Chaine,0x0D,0x0A);

// compter le nombre d’octet à envoyer
 NbOctet = StrLen(Message);

// écrire dans le fichier
WriteFile(hCom,Message,NbOctet,&nBytesWrite,NULL);

// Fermer le handle de com
 CloseHandle(hCom);

// Libération mémoire
delete[] Message;

return(fSuccess);
}

Lecture sur un port série : Exemple de fonction de réception de données


/*------------------------------------------------------------------------------*/
/*                    Recevoir une chaine de caractére sur la RS232             */
/*                                                                              */
/* Paramètres :                                                                 */
/*             char *ChaineRecue  ,   buffer de réception                       */
/*             char *Port, le port de COM : "COM1" ou "COM2"                    */
/*------------------------------------------------------------------------------*/

int RecevoirRS232(char *ChaineRecue,char *Port)
 {
  DCB dcb;
  HANDLE hCom;
  DWORD dwError;
  BOOL fSuccess;
  BOOL bResult;
  DWORD dwEvtMask;
  COMMTIMEOUTS tTimeout;
  unsigned long nBytesRead;
  char *inBuffer;
  int TimeoutRead;
  int i;
  int NbOctet;
  char c;

 /*-------------------------------------*/
 /* Initialisation des variables        */
 /*-------------------------------------*/

 inBuffer  = new char[200]; // réservation mémoire pour le buffer de récéption
 sprintf(inBuffer,"%s","");
 nBytesRead=0;

 /*-----------------------------------------------*/
 /* Ouverture du port de communication            */
 /*-----------------------------------------------*/

 hCom = CreateFile(Port,
    GENERIC_READ | GENERIC_WRITE,
    0,
    NULL,
    OPEN_EXISTING,
    0,
    NULL
    );
 /*----------------------------------*/
 /*    Définition des timeouts       */
 /*----------------------------------*/

 TimeoutRead = 500; // timeout de 500ms
 tTimeout.ReadIntervalTimeout = MAXWORD;
 tTimeout.ReadTotalTimeoutMultiplier = 0;
 tTimeout.ReadTotalTimeoutConstant = TimeoutRead; // pas de time out = 0
 tTimeout.WriteTotalTimeoutMultiplier = 0;
 tTimeout.WriteTotalTimeoutConstant = 0;

// configurer le timeout
 SetCommTimeouts(hCom,&tTimeout);

 /*-------------------------------------------------------------*/
 /* boucle d'attente de lecture des octets                      */
 /* Sortie de la boucle par timeout par exemple, si             */
 /* l’on suppose le format de la trame reçu n’est               */
 /* variable.                                                   */
 /*-------------------------------------------------------------*/

  bResult = ReadFile(hCom,inBuffer,199,&nBytesRead,NULL);

  if (nBytesRead!=0) // Il existe des octets lus
   {
    // Mettre en forme la trame : recherche de CR
   }
 else
  sprintf(inBuffer,, »%s », “Pas de donnée reçue”) ;
  sprintf(ChaineRecue,"%s",inBuffer); // Retourner la chaine recue

// fermer le port
 CloseHandle(hCom);

// Libérer la mémoire
 delete[] inBuffer;

 return(fSuccess);
}

 Exemple d’appel de ses fonctions

Soit à configurer le port série 1, à 9600 bauds, sans de parité,  1 stop bit, 8 bits de data et envoyer la trame « ID ? » avec retour chariot à un périphérique qui nous retournera son identification. Un exemple de mise en œuvre est proposé ci après :

void main(void)
{
 int Erreur ;
char *TrameRecue ;
TrameRecue = new char[200] ;
Erreur=InitCOM(« COM1 »,  « Aucune », « 9600 », « 8 »,  « 1 ») ;
if (Erreur !=0) // périphérique initialisé correctement
 {
   // envoi de la commande d’identification
   EnvoiChaineRS232(« ID ? », « COM1 », « CR ») ;
   // recevoir la réponse
   RecevoirRS232(TrameRecue,  « COM1 ») ;
   // Traiter la trame recue
  }
else
 {
  // afficher une erreur
 }

// Libération mémoire
delete[] TrameRecue ;
}

Troisième partie : Utilisation d’un THREAD et gestion des exceptions

Lancement d’une tâche de « fond » : utilisation d’un THREAD

Dans cette partie nous aborderons (très simplement) la notion de thread et comment la mettre en œuvre.

Un thread est « équivalent à une tache de fond » qui tourne en permanence au sein du thread principal qui est votre application.

A titre d’exemple, un tableur doit recalculer la feuille de calcul à chaque nouvelle saisie.  Le thread principal est la saisi, auquel on associe un thread secondaire de priorité inférieure le recalcul de la feuille qui sera donc exécuter automatiquement et uniquement quand l’utilisateur ne fait aucune saisie dans la feuille de calcul.

Pour Créer un thread en C++ Builder, sélectionner dans l’option Fichier/Nouveau l’icône objet Thread. Donner un nom à la classe, par exemple ThreadNomDuProcess. Un fichier est créé par C++ Builder du Type :

#include <vcl.h>
#pragma hdrstop
#include "UnitThreadNomDuProcess.h"
#pragma package(smart_init)
__fastcall ThreadCompteur:: ThreadNomDuProcess (bool CreateSuspended)
        : TThread(CreateSuspended)
{
}
//---------------------------------------------------------------------------
void __fastcall ThreadNomDuProcess::Execute()
{
        //---- Placez le code du thread ici ----
}
//---------------------------------------------------------------------------

Execute doit tester la valeur de la propriété Terminated afin de déterminer s'il faut sortir du thread. Typiquement le code de la méthode Execute doit être du type :

void __fastcall ThreadNomDuProcess::Execute()
{
        //---- Placez le code du thread ici ----
       while ( !Terminated)
        // actions répétitives à faire
}
//---------------------------------------------------------------------------

Un thread commence lorsque Create est appelée avec le paramètre CreateSuspended initialisé à false, ou si la méthode Resume est appelée après un appel de Create dans lequel CreateSuspended est initialisé à true.

Pour exécuter un thread il suffit de lancer la méthode Resume() : Reprend l'exécution d'un thread interrompu.

Pour Arrêter un thread il suffit de lancer la méthode Terminate() : Signale au thread de s'arrêter en affectant la valeur true à la propriété Terminated.

Terminate initialise la propriété Terminated du thread à true, en signalant que le thread doit se terminer dès que possible. A l'inverse de l'API  Windows TerminateThread, qui force le thread à se terminer immédiatement, la méthode Terminate demande simplement que le thread se termine. Ceci permet au thread d'exécuter tout nettoyage avant de se fermer.

Pour que Terminate fonctionne, la méthode Execute du thread et toute méthode appelée par Execute doit tester périodiquement Terminated et quitter lorsqu'elle vaut true.

Prenons un exemple : Nous voulons réaliser un programme dans lequel

  • L’utilisateur saisi une chaîne de cararatère
  • La chaîne de caractère est recopiée et affichée dans une autre zone de l’écran automatiquement, toutes les secondes
  • Un indicateur du temps écoulé depuis le lancement du programme s’affiche à l’écran

Dans cet exemple nous pouvons considérer que l’affichage du temps écoulé est la mise à jour de la recopie de la chaîne de caractère est une tâche de fond : d’où l’utilisation d’un thread.

Créer l’interface utilisateur suivante :

Choisir fichier/Nouveau projet et enregistrer le avec les noms par défauts sur le répertoire voulu.

Ajouter un objet Thread au projet : Fichier/Nouveau choisir objet thread. Donner lui comme nom de classe ThreadCompteur. Enregistrer le fichier sous le répertoire voulu avec comme nom UnitThreadCompteur.cpp

Ecrivez le code suivant : dans Unit1.cpp

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#include "UnitThreadCompteur.h"
//---------------------------------------------------------------------------

#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
ThreadCompteur *ThCompteur; // définition d'un pointeur sur le thread

/*----------------------------------------------------------------------*/
/* Création du process de comptage                                        */
/*----------------------------------------------------------------------*/

void ProcessCompteur_Create(void)
 {
  ThCompteur=new ThreadCompteur(true);  // resevation de la mémoire
  ThCompteur->Priority = tpLower;       // priorité inférieure
  ThCompteur->Resume();                 // Lancement du traite
  }

/*-----------------------------------------------------------------------*/
/* désactivation du process de Comptage                                */
/*-----------------------------------------------------------------------*/

void ProcessCompteur_Kill(void)
 {
  ThCompteur->Terminate(); // arrêt du thread
  delete ThCompteur;       // libération mémoire
 }

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
 ProcessCompteur_Create(); // lancement de la tâche d'acquisition au
                           // démarage du programme
}
//---------------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
 Label1->Caption = Edit1->Text; // recopie du test si action sur le Bouton
}
//---------------------------------------------------------------------------

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
 ProcessCompteur_Kill(); // arrêter le thread en fin de programme
}
//---------------------------------------------------------------------------

Ecrivez le code suivant : dans UnitThreadCompteur.cpp

#include <vcl.h>
#pragma hdrstop
#include "UnitThreadCompteur.h"
#include "Unit1.h"
#pragma package(smart_init)

__fastcall ThreadCompteur::ThreadCompteur(bool CreateSuspended)
        : TThread(CreateSuspended)
{
}
//---------------------------------------------------------------------------
// code du thread : action à réaliser
//---------------------------------------------------------------------------

void __fastcall ThreadCompteur::Execute()
{
 int Compteur;
 Compteur = 0; // inialisation du compteur
 while (!Terminated)  // test si demande d'arrêt du thread
  {
   Compteur++;
   Compteur=Compteur%60;
   Sleep(1000);
   Form1->LabelCompteur->Caption = StrToInt(Compteur);
   Form1->Label1->Caption = Form1->Edit1->Text;
  }
}

En exécutant le programme le compteur s’incrémentera toutes les secondes et son contenu sera affiché à l’écran .De même la donnée saisie par l’utilisateur sera recopiée toutes les secondes.

Gestion des exceptions

Une exception est assimilable à une erreur. L’exemple la plus simple est la division par 0. Un programme correctement écrit doit donc gérer tous les types d’erreurs susceptibles d’engendrer des disfonctionnements et de « planter » le programme.
Dans cette partie nous décrirons une méthode permettant de gérer des exceptions.

Les exceptions sont déclenchées quand une erreur d'exécution se produit dans une application, par exemple une tentative de division par zéro. Généralement, quand une exception est déclenchée, une instance d'exception affiche une boîte de dialogue décrivant la condition d'erreur. Si une application ne traite pas la condition d'exception, le gestionnaire d'exception par défaut est appelé. Ce gestionnaire affiche également une boîte de dialogue avec un bouton OK qui permet normalement à une application de poursuivre les traitements quand l'utilisateur clique sur OK.

L'objet Exception offre une interface homogène aux conditions d'erreur et permet aux applications de gérer les conditions d'erreur d'une manière élégante. Les applications peuvent , intercepter et gérer des exceptions spécifiques dans des blocs try..catch.

La gestion d’une exceptions  à l’aide des blocs try et catch et de la forme gènerale :

try
 {
  // Ecrire le code où une exception est susceptible de se produire
 }
catch(…)
 {
  // Gérer l’exception
 }

L’exemple ci dessous illustre la gestion d’une exception, lors d’un calcul. L’utilisateur saisi 2 nombres A et B et une action sur le bouton A/B calcul et affiche le résultat de la division de A par B.
Créer un nouveau projet et donner les noms UnitDivision .cpp pour le code et ProjectDivision pour le nom de projet.

Créer l’écran utilisateur suivant :

La division impose que :
- A et B soit des nombres
- B soit différent de 0

Si ces 2 conditions ne sont pas respectées le programme générera une erreur. L’objectif est d’intercepter ces erreurs.

Nous devons donc contrôler que A et B sont des nombres pour cela nous décrirons 2 méthodes pour gérer ces exceptions :
- Pour A nous utiliserons les message d’erreur du système
- Pour B nous définirons notre message d’erreur

Nous devons contrôler lors de la division que B est différent de 0. Dans le cas ou B est différent de 0 nous n’afficherons pas de message d’erreur mais nous donnerons comme résultat DIV/0.

Ecrivez le code ci après dans le fichier UnitDivision.cpp :

#include <vcl.h>
#pragma hdrstop
#include "UnitDivision.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
 // effacement des données
 EditA->Text = "";
 EditB->Text = "";
 Edit1->Text = "";
}
//---------------------------------------------------------------------------
// Gestion de la saisi sur A
//---------------------------------------------------------------------------
void __fastcall TForm1::EditAChange(TObject *Sender)
{
 float Nb;
 // Nous ne voulons saisir que des nombres
 // Une execption peut se produire => prévoir sa gestion
 try
  {
   // Nous vérifions que la chaine est différente de chaine vide
   if (StrLen(EditA->Text.c_str())!=0)
    Nb = StrToFloat(EditA->Text.c_str()); // Déclenchement de l'exception
  }                                       // si cconvertion échoue

 catch(Exception &UneException) // il y à une exception afficher un message
  {
   // Afficher le message d’erreur
   Application->ShowException(&UneException);

   // Traitement si nécessaire, dans notre cas remise à 0
   EditA->Text = "0";
  }
}
//---------------------------------------------------------------------------
// Gestion de la saisi sur B
//---------------------------------------------------------------------------

void __fastcall TForm1::EditBChange(TObject *Sender)
{
 float Nb;
 char *Chaine;
 Chaine = new char[100];

  // Nous ne voulons saisir que des nombres
 // Une exception peut se produire => prévoir sa gestion
 try
  {
   // Nous vérifions que la chaine est différente de chaine vide
   if (StrLen(EditB->Text.c_str())!=0)

    Nb = StrToFloat(EditB->Text.c_str()); // Déclenchement de l'exception
  }                                                              // si convertion échoue

 catch(Exception &UneException) // il y à une exception
  {
   // Afficher notre message et le type d'erreur systeme
   UneException.Message = " : Erreur de saisi pour nombre B";
   ShowMessage( AnsiString(UneException.ClassName())+UneException.Message);
   // Traitement si nécessaire, dans notre cas suppression
   // du dernier caractère saisi
   AnsiString(Chaine) = EditB->Text;
   Nb = StrLen(Chaine.c_str());

   Chaine[Nb]='\0'; // fin de chaine
   EditB->Text = Chaine;
  }
  // Libération mémoire
  delete[] Chaine;
}
//---------------------------------------------------------------------------
// Gestion du calcul de A / B
//---------------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  int BestNul;
  float r;
 // Nous sommes sûr que A et B sont des nombres ou une chaîne vide
 // il nous reste à vérifier que A et B sont différents de chaîne vide
 // et que B n'est pas égale à 0
 // Si A et B différent de chaine vide on peut faire par exemple
 if ( (StrLen(EditA->Text.c_str())!=0)&&(StrLen(EditB->Text.c_str())!=0))

  {
   // Il nous faut vérifier que B n'est pas nul. Nous le gérerons pour la forme
   // par une exception. Vous pouvez le gérer autrement bien sûr

   try
    {
     1/StrToFloat(EditB->Text.c_str()); // exception ?
     BestNul = 0; // B n'est pas nul
    }
   catch(...)
    {
     // Pas d'affichage de message, Mise à jour d'un flag d'erreur
     BestNul = 1; // B est nul
    }

    // Faire le calcul si possible
    if (BestNul == 0)
     {

      r=EditA->Text.ToDouble()/EditB->Text.ToDouble();

      Edit1->Text = FloatToStr(r);
     }

    else
     Edit1->Text = "DIV/0";
  } // fin si chaîne vide

 else // A ou/et B sont vide
  {
   // Faire ce que vous voulez
  }
}
//---------------------------------------------------------------------------

Conclusion

La programmation sous Windows est extraordinairement puissante, et nécessite un foule de connaissances à acquérir, mais elle tend à devenir un standard du marché.

Les différents point abordés dans cette présentation ne sont que des bases, vous permettant de créer rapidement quelques applications. Une utilisation régulière et approfondie vous permettra certainement d’utiliser ces bases autrement et certainement plus simplement.

Xavier Brun - 30/09/00
 

-= From guill.net =-