Servizi seriali in win32: concetti base

Come per altri servizi di accesso all’HW, i progettisti di NT hanno cercato di astrarre quanto più possibile l’accesso ai dispositivi e anzi di limitarne l’uso.

Ricordiamo in questa sede che alcuni degli obiettivi di NT (e quindi del "figlio" Win32):

  • Indipendenza dall’hardware (Esiste uno strato di astrazione detto HAL, HW Abstraction Layer, che si frappone fra il software e i dispositivi fisici virtualizzandoli).
  • Accesso ai dispositivi permesso solo al kernel per migliorare la stabilità e gestire la concorrenza negli accessi
  • Accesso solo attraverso API standard scritte in "C" per ovvi motivi di portabilità

Aggiungiamo che, in simmetria con la filosofia Unix "tutto è un file", anche sotto NT si è teso a uniformare l’accesso alla seriale all’accesso ad un file.

Ed infatti nelle API le chiamate, come vedremo nel seguito, sono le stesse, sia che si tratti di un file, di una socket, di una mailslot o delle porte seriali.

Citiamo per completezza il fatto che esistono ancora alcune chiamate, in effetti a 16 Bit, che utilizzano funzione dedicate. L’SDK ufficiale le sconsiglia perché obsolete e inutili: in ambiente win32 sono rimappate alle funzione "file oriented".

La logica di funzionamento può essere così riassunta:

  • Apertura connessione
  • Rilevamento / Settaggio parametri di comunicazione (se necessario)
  • Lettura / Scrittura
  • Chiusura

E’ anche possibile verificare lo stato della porta e della linea con opportune chiamate.

All’interno di Windows è stata definita una apposita struttura atta a descrivere i parametri della connessione, il DCB (Device Control Block), blocco di controllo del dispositivo. Lo riportiamo per intero segnalando solo i campi "interessanti".

Rimandiamo alla bibliografia [3] per i dettagli.

typedef struct _DCB {
DWORD DCBlength;
DWORD BaudRate;
DWORD fBinary :1;
DWORD fParity :1;
DWORD fOutxCtsFlow :1;
DWORD fOutxDsrFlow :1;
DWORD fDtrControl :2;
DWORD fDsrSensitivity :1;
DWORD fTXContinueOnXoff :1;
DWORD fOutX :1;
DWORD fInX :1;
DWORD fErrorChar :1;
DWORD fNull :1;
DWORD fRtsControl :2;
DWORD fAbortOnError :1;
DWORD fDummy2 :17;
WORD wReserved;
WORD XonLim;
WORD XoffLim;
BYTE ByteSize;
BYTE Parity;
BYTE StopBits;
char XonChar;
char XoffChar;
char ErrorChar;
char EofChar;
char EvtChar;
WORD wReserved1;
} DCB

Segnaliamo:

BaudRate la frequenza di trasmissione

ByteSize numero di bit: 5,6,7 o 8 bit prima del fronte di salita del bit di stop

Parity parità: pari, dispari, nessuna, mark o space.

StopBits numero di bit di stop

Esistono delle opportune define per tutti i possibili valori dei parametri.

Ne riportiamo solo alcuni.

#define NOPARITY      0
#define ODDPARITY     1
#define EVENPARITY    2
#define MARKPARITY    3
#define SPACEPARITY   4

#define ONESTOPBIT    0
#define ONE5STOPBITS  1
#define TWOSTOPBITS   2

Lo stesso dicasi per le velocità.

Una variabile di tipo DCB può essere settata e passata ad una API per settare i parametri, ma è anche possibile passare un DCB ad una funzione di "stato" che provvederà a riempirla con le impostazioni correnti.

Consigliamo fin d’ora di procedere in questo modo:

  • azzerare una variabile di tipo DCB
  • passarla ad una funzione di "stato" che la riempia coi parametri correnti.
  • modificare SOLO i parametri necessari
  • ripassare il DCB a Windows

in tal modo non siamo obbligati a settare ogni campo del DCB.

 

API SERIALI win32

Analizziamo le principali funzioni nell’ordine sopra descritto.

Apertura

E’ sufficiente utilizzare:

HANDLE CreateFile( LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);

dove:

lpFileName è il nome della porta, pes. "COM1".

DwDesiredAccess è il tipo di accesso: poiché la seriale è bidirezionale, vale: GENERIC_READ | GENERIC_WRITE

dwShareMode è messo a 0. Ha senso solo per i file che possono essere sharati in R/W.

lpSecurityAttributes è messo a NULL, non utilizzeremo attributi di sicurezza

dwCreationDisposition specifica come creare il file. In questo caso: OPEN_EXISTING

dwFlagsAndAttributes per i file può essere hidden, di sistema etc. Qui semplicemente 0.

hTemplateFile NULL. Non usiamo template.

La funzione, come usuale in win32, restitusce un Handle. NON testate se l’Handle ritornato vale NULL: va confrontato con INVALID_HANDLE_VALUE.

 

Settaggio parametri di comunicazione (se necessario)

Come detto, svuotiamo un DCB e prima lo passiamo ad una funzione di "stato":

BOOL GetCommState(
        HANDLE hFile, // handle to communications device
        LPDCB lpDCB // pointer to device-control block structure
      );

dopo aver ottenuto lo stato corrente, nella variabile di tipo DCB passata per indirizzo, la modifichiamo e la passiamo a:

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

(Si veda l’esempio di codice più avanti)

Lettura / Scrittura

E’ sufficiente utilizzare:

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 for
                               // overlapped I/O
      );


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

Essendo le stesse funzioni usate per i file, non ci dilunghiamo a commentare i parametri [4].

Ricordiamo però che sia ReadFile che WriteFile sono bloccanti, ossia non ritornano "su" dalla syscall se non hanno rispettivamente ricevuto o inviato il numero di caratteri specificato.

Ciò significa che il nostro programma sarà in wait se non ci sono caratteri da ricevere, ma anche che non risponderà all’input utente finché non avrà inviato lungo il canale fisico tutti i byte specifcati da nNumberOfBytesToWrite.

Potrebbe essere quindi necessario threadare tali attività per non aver un programma né in polling (spreco di CPU) per ricevere qualche dato, né in blocco su una scrittura lenta, o viceversa.

Inoltre se vogliamo sia leggere che scrivere contemporaneamente, dobbiamo inventarci un meccanismo di alternanza fra gestione dell’input utente, scrittura e lettura. In breve uno specie di scheduler che interrompa le 3 operazioni descritte.

Non lo faremo: nel nostro breve esempio possiamo tollerare che le operazioni siano bloccanti.

Le alternative sono due:

  • thread di R/W
  • usare l’I/O Overlapped. (Non ne dicutiamo qui. Diciamo solo che è un "bagno di sangue" di logica asincrona da gestire con parametri un po’ complessi).

 

Chiusura

Banalmente come per ogni risorsa di Windows:

BOOL CloseHandle(HANDLE hObject);

NB: non usate fclose! Non è un file!

 

Logica dell’esempio

Vogliamo realizzare un breve programma, per semplicità Console, che:

  • Apra la connessione ad un modem seriale
  • Legga i valori correnti
  • Imposti i nuovi parametri della connessione
  • Invii la classica sequenza "ATI3" (richiesta di informazioni)
  • Cicli in attesa della stringa di risposta stampando i caratteri ricevuti

Il comando ATI3 è un "classico" come tutti comandi modem comincia con AT, ossia ATTENTION, e va chiuso con un carattere di invio, come per tutti i comandi.

L’esempio di codice

Cominciamo a scrivere un programma Console stile "Hello Word" , e sia MySerial.

(tralasciamo i passi necessari; li abbiamo già visti in altri articoli)

Cominciamo ad aprire la connessione alla seriale:

#include "stdafx.h" 
#include "windows.h"
#include "conio.h"

int main(int argc, char* argv[])
{
       HANDLE myPortH= CreateFile(
              "COM1",
               GENERIC_READ | GENERIC_WRITE,
               0,
               0,
               OPEN_EXISTING,
               0,
               0);

       if (myPortH == INVALID_HANDLE_VALUE)
       {
             printf("error opening\n");
             // error opening port; abort
             return -1;
       }
       printf("COMM Open\n\n");
       getch();
       CloseHandle(myPortH);
       return 0;
}

Provate ad eseguirlo: a meno di PC senza seriale, dovrebbe partire.

Lo strano #include "windows.h" è necessario perché #include "stdafx.h" non include tutti i tipi delle API.

Leggiamo i parametri correnti:

DCB myDCB;

memset(&myDCB, 0, sizeof(DCB)); BOOL Ok = GetCommState(myPortH,// handle to communications device &myDCB // pointer to device-control block //structure ); printf("BaudRate %d\n", myDCB.BaudRate); printf("ByteSize %d\n", myDCB.ByteSize); printf("Parity %d\n", myDCB.Parity); printf("StopBits %d\n", myDCB.StopBits);

Notiamo che per parità e bit di stop, otteniamo le define di Windows. Per una visualizzazione completa, dovremmo creare degli switch. Facciamolo per i Bit di Stop.

switch(myDCB.StopBits)
{
case ONESTOPBIT:
     printf("ONESTOPBIT\n");
     break;
case ONE5STOPBITS:
     printf("ONE5STOPBITS\n");
     break;
case TWOSTOPBITS:
     printf("TWOSTOPBITS\n");
     break;
}

Settiamo ora la velocità a 9600, con opportuna define:

myDCB.BaudRate = CBR_9600;
      Ok = SetCommState(myPortH, // handle to communications device
            &myDCB // pointer to device-control block structure
            );

Aggiungiamo la scrittura della stringa:

char msg[] = "ATI3";
DWORD NumberOfBytesWritten=0;
DWORD nNumberOfBytesToWrite = strlen(msg);
Ok = WriteFile(  myPortH,// handle to file to write to
                 msg,// pointer to data to write
                 nNumberOfBytesToWrite,// number of bytes to write
                 &NumberOfBytesWritten, // ptr.to n.bytes written
                    NULL// pointer to structure for
);
printf("Written %d bytes \n", NumberOfBytesWritten);

Il modem dovrebbe lampeggiare.

Poiché le stringhe di comando vanno chiuse da un CR (ASCII 13), duplichiamo il codice e modifichiamo:

char CR[] = {13, 0};
NumberOfBytesWritten=0;
nNumberOfBytesToWrite = strlen(CR);
Ok = WriteFile(    myPortH,
                     CR,
                     nNumberOfBytesToWrite,
                     &NumberOfBytesWritten,
                     NULL
              );

Ora mettiamoci in polling e leggiamo. Interromperemo il ciclo con un tasto.

Come tutti i veri programmatori "C" usiamo l’unico ciclo "vero", il for e leggiamo char by char:

printf("\nreading...\n\n\n");
for(;;)
{
      char c;
      DWORD NumberOfBytesRead;
      Ok = ReadFile(myPortH,
                  &c, // pointer to buffer that receives data
                  1, // number of bytes to read
                  &NumberOfBytesRead, // ptr. to n. of bytes read
                  NULL
                  );

      if (NumberOfBytesRead > 0)
            printf("%c", c);

      int keydown = _kbhit();
      if (keydown)
             break;
}

Notiamo che, come detto, la routine è bloccante: riuscite a interrompere con un tasto SOLO se arriva "qualcosa" da modem che fa ritornare al programma la ReadFile.

La cosa era prevista.

 

Dialing on modem

Vediamo ora se si riesce a far comporre i numeri al modem.

La logica è la seguente:

  • inviare "ATX3" (Ignora segnale di linea, il tono dei film "americani")
  • Inviare a capo
  • Inviare "ATDPxxxxx"
  • Inviare a capo

Poiché le funzioni di invio sono simili e va sempre inviato l’a capo, scriviamo una funzione che invii una stringa completa di a capo.

void write(HANDLE myPortH, char * msg)
{
    DWORD NumberOfBytesWritten=0;
    DWORD nNumberOfBytesToWrite = strlen(msg);
    BOOL Ok;
    if(nNumberOfBytesToWrite)
    {
        Ok = WriteFile( myPortH,// handle to file to write to
        msg,// pointer to data to write to file
        nNumberOfBytesToWrite,// n. bytes to write
        &NumberOfBytesWritten,//ptr n.bytes written
        NULL
        );
        printf("Written %s\n%d bytes\n\n", msg, numberOfBytesWritten);
    }

    char CR[] ={13, 10, 0};
    NumberOfBytesWritten=0;
    nNumberOfBytesToWrite = strlen(CR);
    Ok = WriteFile( myPortH,// handle to file to write to
                    &CR,// pointer to data to write to file
                    nNumberOfBytesToWrite,//n.of bytes to write
                    &NumberOfBytesWritten,
                    NULL
    );
    printf("CR %d bytes \n", NumberOfBytesWritten);
}

la relativa chiamata sarà:

write(hComm, "ATDP12345678");

Sarà ora sufficiente inserire la chiamata alla funzione nel main.

Tralasciamo la classica attività di editing: il lettore è un duro programmatore C…

 

IL LISTATO

E diamo una visione d’insieme del listato, soprattutto per permettere un agile Copy&Paste. Dopo alcuni piccoli aggiustamenti soprattutto alla visibilità delle variabili, eccolo:

// My_serial.cpp : Defines the entry point for the console application.
// 
          

#include "stdafx.h" #include "windows.h" #include "conio.h"

BOOL setComm(HANDLE hComm) { DCB dcb; BOOL res; FillMemory(&dcb, sizeof(dcb), 0); res = GetCommState(hComm, &dcb); // get current DCB if (!res) // Error in GetCommState return FALSE; printf("\nBaudRate %d ByteSize %d StopBits %d\n", dcb.BaudRate, dcb.ByteSize, dcb.StopBits); // Update DCB rate. dcb.ByteSize = 8; dcb.StopBits = ONESTOPBIT; dcb.BaudRate = CBR_9600; dcb.Parity =NOPARITY;// none dcb.fAbortOnError = 0; // no abort // Set new state. res = SetCommState(hComm, &dcb); // Error in SetCommState. Possibly a problem with the communications // port handle or a problem with the DCB structure itself. if (!res) return FALSE; DWORD err = GetLastError(); if ( err) printf("\n ERROR % d\n", err); return TRUE; }

void state(HANDLE hComm) { DCB dcb; BOOL res; FillMemory(&dcb, sizeof(dcb), 0); res = GetCommState(hComm, &dcb); // get current DCB DWORD err = GetLastError(); if ( err) printf("\n receive ERROR % d\n", err); SetLastError(0); }

BOOL write(HANDLE hComm, char * buf) { DWORD nNumberOfBytesToWrite = strlen(buf); DWORD NumberOfBytesWritten; LPOVERLAPPED lpOverlapped = NULL; BOOL res = WriteFile( hComm, // handle to file to write to buf, // pointer to data to write to file nNumberOfBytesToWrite, // number of bytes to write &NumberOfBytesWritten, // pointer to number of bytes written lpOverlapped // pointer to structure for overlapped I/O ); if (res) printf("\nwrite OK\n"); else { DWORD err = GetLastError(); printf("\nwrite ERROR %d \n", err); } return res; }

int main(int argc, char* argv[]) { printf("Hello World!\n"); char gszPort[] = "COM1"; HANDLE hComm; hComm = CreateFile( gszPort, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, // NO ! FILE_FLAG_OVERLAPPED, 0); if (hComm == INVALID_HANDLE_VALUE) { printf("error opening\n"); // error opening port; abort } else { char c; char CR[] = {13,10,0}; DWORD nNumberOfBytesToRead = 1; DWORD NumberOfBytesRead=0; LPOVERLAPPED lpOverlapped = NULL; // pointer to structure for data BOOL bResult; bResult = setComm(hComm); //setTimeOut(hComm); write(hComm, "ATX3"); write(hComm, CR); write(hComm, CR); write(hComm, "ATDP12345678"); write(hComm, CR); write(hComm, CR); write(hComm, CR); write(hComm, CR); for(;;) { //state(hComm); nNumberOfBytesToRead = 1; bResult = ReadFile( hComm,// handle of file to read &c,// pointer to buffer that receives data nNumberOfBytesToRead,// number of bytes to read &NumberOfBytesRead,// pointer to number of bytes read lpOverlapped// pointer to structure for data ); //printf("\n%d",NumberOfBytesRead); if (NumberOfBytesRead>0) printf("%c", c); }// end for CloseHandle(hComm); } getch(); return 0; }

Conclusioni

Sebbene la seriale sia sempre meno usata, è ancora fondamentale per connessioni a server, ad apparecchiature di rete (switch e router), ad appareccchiature industriali, a centralini e cosi’ via.

Addirittura su server di Apple è tornata dopo 10 anni….

 

RIFERIMENTI

  1. EIA232E - Interface Between Data Terminal Equipment and Data Circuit-Terminating Equipment Employing Serial Binary Data Interchange, revised from EIA232D, July 1991. Application notes for the EIA232 standard, formerly Industrial Electronics Bulletin #9
  2. http://www.lammertbies.nl/comm/cable/RS-232.html
  3. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wcecoreos5/html/wce50lrfdcb.asp
  4. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/createfile.asp
  5. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnfiles/html/msdn_serial.asp

Prima parte