AngoLinux

Comunicazione tramite seriale - miniterminale

- A cura del Prof. Stefano Salvi -


Cominciamo a fare programmi che utilizzano la comunicazione seriale.

Il testo dell'esercizio è il seguente:

Scrivere un programma in C che implementio una semplice comunicazione seriale. Il programma dividerà lo schermo in tre finestre:
  • Una finestra di circa mezzo schermo in alto nella quale verranno visualizzati i caratteri ricevuti dall'interfaccia seriale
  • Una finestra un poco più piccola che visualizzerà i caratteri digitati da tastiera, che verranno trasmessi tramite la stessa interfaccia
  • Una finestra di una sola riga in fondo allo schermo, che viualizzerà data ed ora, aggiornati almeno una volta al secondo.
Il programma imposterà la prima interfaccia seriale (/dev/tyS1) in modo noncanonical a 9600 Baud. Per terminare il programma si dovrà premere il tasto Esc.
Non si useranno né processi né thread.

NOTE:

  • Per poter utilizzare un'interfaccia seriale come utente normale occorre che l'utente in questione sia associato al gruppo dialout. Per associare l'utente pippo al gruppo dialout occorre digitare il seguente comando, con privilegio di root (notare il prompt #):
    # gpasswd -a pippo dialout
  • Per la visualizzazione si useranno le funzioni di ncurses, come spiegate nel relativo tutorial.
  • È opportuno scrivere una funzione che crei una finestra con cornice. Questa funzione creerà prima una finestra leggermente più grande, alla quale disegnerà la cornice con la funzione box, quindi distruggerà questa finestra e ne creerà una della dimensione scelta, nella sola area utile della cornice. La funzione ritornerà il puntatore alla finestra utile creata.
  • Le interfacce seriali corrispondono ai dispositivi /dev/ttyS0 (COM1 sotto DOS) e /dev/ttys1 (COM2 sotto DOS). Possono venire aperti come normali file, ma il sistema operativo esegue una serie di filtraggi sui dati inviati e ricevuti (modo canoncal). Occorre quindi indicare al sistema di sospendere questi filtraggi (ponendo l'interfaccia nel cosidetto modo noncanonical). Occorre inoltre impostare correttamente le caratteristiche della comuinicazione. Riporto, dalla documentazione info su libc ("Low-Level Terminal Interface"->"Noncanonical Mode Example"):

    Noncanonical Mode Example

    Here is an example program that shows how you can set up a terminal device to read single characters in noncanonical input mode, without echo.
         #include <unistd.h>
         #include <stdio.h>
         #include <stdlib.h>
         #include <termios.h>
         
         /* Use this variable to remember original terminal attributes. */
         
         struct termios saved_attributes;
         
         void
         reset_input_mode (void)
         {
           tcsetattr (STDIN_FILENO, TCSANOW, &saved_attributes);
         }
         
         void
         set_input_mode (void)
         {
           struct termios tattr;
           char *name;
         
           /* Make sure stdin is a terminal. */
           if (!isatty (STDIN_FILENO))
             {
               fprintf (stderr, "Not a terminal.\n");
               exit (EXIT_FAILURE);
             }
         
           /* Save the terminal attributes so we can restore them later. */
           tcgetattr (STDIN_FILENO, &saved_attributes);
           atexit (reset_input_mode);
         
           /* Set the funny terminal modes. */
           tcgetattr (STDIN_FILENO, &tattr);
           tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
           tattr.c_cc[VMIN] = 1;
          
           /* Set the funny terminal modes. */
           tcgetattr (STDIN_FILENO, &tattr);
           tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
           tattr.c_cc[VMIN] = 1;
           tattr.c_cc[VTIME] = 0;
           tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);
         }
         
         int
         main (void)
         {
           char c;
         
           set_input_mode ();
         
           while (1)
             {
               read (STDIN_FILENO, &c, 1);
               if (c == '\004')          /* `C-d' */
                 break;
               else
                 putchar (c);
             }
         
           return EXIT_SUCCESS;
         }

    This program is careful to restore the original terminal modes before exiting or terminating with a signal. It uses the `atexit' function (*note Cleanups on Exit) to make sure this is done by `exit'.

    The shell is supposed to take care of resetting the terminal modes when a process is stopped or continued; see *Note Job Control. But some existing shells do not actually do this, so you may wish to establish handlers for job control signals that reset terminal modes. The above example does so.

  • È opportuno porre l'inizializzazione della seriale in una funzione indipendente, alla quale passeremo il nome del dispositivo e ci ritornerà il descrittore del file aperto.
  • Il nostro programma dovrà verificare la presenza di caratteri su due dispositivi, rappresentati dai realtivi file:
    1. La tastiera (file stdin, descrittore fileno(stdin))
    2. La seriale (il descrittore ritornato dalla nostra funzione di inizializzazione)
    Per evitare di porre i due file in modalità non bloccante e sovraccaricare il sistema continuando a verificarli a turno (polling), possiamo utilizzare la funzione select. Questa funzione richiede tre gruppi di descritori, uno da controllare per dati in ingresso, uno per spazio nella coda di trasmissione ed un terzo per errori.
    Quando richiamiamo la select, essa attenderà che avvenga un evneto in uno dei tre gruppi di file, prima di ritornare. Essa consente anche, con un quarto parametro, di indicare un timeout, trascorso il quale la funzione ritornerà anche se non sono accaduti eventi. Per una trattazione più competa della funzione select, consultare la sezione La funzione select nell'esercizio "L'esperienza di Quinta".
  • Per ottenere data ed ora corrente, si userà la funzione time (NULL);. questa funzione ritorna il numero di secondi trascorsi dal 1^ Gennaio 1970 (data chiamata the Epoch, con un tipo time_t.
  • Per convertire in stringa, in formato leggibile, il numero ritornato dalla funzione time (NULL);, si può passare l'indirizzo di una variabile contenete il numero di secondi ritornato dalla funzione alla funzione char *ctime(const time_t *timep); che convertirà il dato, lo formatterà come data ed ora e lascerà il risultato in un buffer statico del quale ritornerà l'indirizzo. Potremo passare l'indirizzo ritornato direttamente alla funzione di stampa.

Una possibile soluzione è la seguente:
/* seriale.c
 * Stefano Salvi - 26/10/2002
 * Programma che realizza una semplice 'chat' seriale tra due cumputer.
 * Utilizzando curses divide il video in due parti.
 * Nella parte superiore verranno visualizzati i caratteri ricevuti dalla seriale.
 * Nella parte inferiore verranno visualizzati i caratteri premuti sulla tastiera,
 * che verranno contemporaneamente spediti.
 * Il programma termina quando si preme il tasto di escape
 * La seriale verra' programmata a 9600 baud.
 */
#include <stdio.h>
#include <termios.h>
#include <ncurses.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/stat.h>

#define        ESC        27                /* Carattere per terminare */

int init(char *comname);        // Funzione che apre una seriale e la inizializza
// Funzione che crea una finestra e le disegna una cornice intorno
WINDOW *framedWin (int h, int w, int y, int x);

int main()
{
WINDOW *fi;                        // Finestra di NCurses - dati ricevuti
WINDOW *fo;                        // Finestra di NCurses - dati trasmessi
WINDOW *ft;                        // Finestra di NCurses - orologo (ultima riga)
int com;                        // handle del file della seriale
int uscita;
fd_set set;                        // set di descrittori per 'select'
struct timeval timeout;                // Timeout per 'select'
char input,output;                // Caratteri ricevuti e da trasmettere
time_t t;                        // Data ed ora
        
  com = init("/dev/ttyS0");        // Apre il canale seriale

  initscr();                        // Inizializza la finestra con curses
  start_color();                // Inizializza anche i colori
  noecho();                        // Imposta la tastiera senza echo
  cbreak();                        // Restituisce ogni singolo carattere appena premuto
  clear();                        // Pulisce lo schermo

  fi=framedWin(10,78,1,1);        // Finestra superiore - caratteri ricevuti
  fo=framedWin(9,78,13,1);        // Finestra inferiore - caratteri trasmessi
  ft=newwin(1,80,23,0);                // Ultima riga - Orologio
  wclear(ft);                        // Pulisce lo schermo

  do {
    FD_ZERO(&set);                // Svuota il set
    FD_SET(com,&set);                // Ci aggiunge la seriale
    FD_SET(fileno(stdin),&set);        // Ci aggiunge anche la tastiera (standard input)
    timeout.tv_sec=1;                // Imposta un timeout (1 secondi)
    timeout.tv_usec=0;                // 0 microsecondi

    uscita = select(FD_SETSIZE, &set, NULL, NULL, &timeout);

    if(FD_ISSET(com,&set))        // La seriale e' nel set (ha un evento)
    {
      read(com,&input,1);        // Leggo il carattere dalla seriale
      waddch(fi,input);                // Lo stampo sulla finestra
      wrefresh(fi);                // Aggiorno la finestra
    }

    if(FD_ISSET(fileno(stdin),&set)) // Se c'e' un carattere nello standard input
    {
      output=wgetch(fo);        // Lo leggo dalla tastiera (nella finestra)
      if (output==ESC)                // Se il carattere e' 'Escape'
      {
        uscita=-1;                // Segnalo fine ciclo
      } else {                        // Altrimenti
        write(com,&output,1);        // Lo spedisco sulla seriale
        waddch(fo,output);        // Lo aggiungo alla finestra
        // wrefresh(fo);        // Non rinfresco la finestra - lo faccio dopo aver scritto l'ora
      }
    }

    /* Aggiorna l'orologio */
    t = time (NULL);
    mvwaddstr (ft,0,0,ctime (&t));// Legge l'ora (in secondi)
    wrefresh (ft);
    wrefresh(fo);                // Riposiziona il cursore nella finestra in input

  } while (uscita!=-1);                // Finche' il flag non disce di uscire

  close(com);                        // Chiudo il canale seriale

  delwin(fi);                        // Cancello la finestra superiore
  delwin(fo);                        // Cancello la finestra inferiore

  endwin ();                        // Rimette tutto a posto

  return 0;
}

/* Funzione che crea una finestra e le disegna una cornice intorno
 * La finestra ha altezza 'h', larghezza 'w', e' posizionata ad 'x', 'y'.
 * La cornice e' esterna alla finestra (crea una finestra temporanea per disegnarla)
 * La cornice e' gialla su sfondo blu.
 * La finesra e' blu, con testo bianco.
 */
WINDOW *framedWin (int h, int w, int y, int x)
{
WINDOW *fi;                        // Rinestra di NCurses

  init_pair(1,COLOR_YELLOW,COLOR_BLUE); 
  init_pair(2,COLOR_WHITE,COLOR_BLUE);
 
  fi=newwin(h+2,w+2,y-1,x-1);        // Finestra esterna (per cornice)
  wbkgd(fi,COLOR_PAIR(1));        // Cornice gialla su fondo blu
  box(fi,ACS_VLINE,ACS_HLINE);        // Disegna la cornice intorno alla finestra
  wrefresh(fi);                        // Aggiorna la finestra (disegna la cornice)
  delwin(fi);                        // Cancella la finestra della cornice

  fi=newwin(h,w,y,x);                // Finestra della dimesione data
  wbkgd(fi,COLOR_PAIR(2));        // Testo bianco su sfondo blu
  wclear(fi);                        // Pulisce lo schermo
  scrollok(fi,1);                // Abilita lo scroll
  wrefresh(fi);                        // Aggiorna la finestra (la cancella)
  return fi;
}

int init(char *comname)
{
int com;                        // Il file per la seriale
struct termios tattr;                // Struttura per impostare le caratteristiche della seriale

  com = open (comname,O_RDWR | O_SYNC);        // Apre il dispositivo (Lettura/scrittura, sincrono)

  if (com == -1)                        // Open ritorna errore
  {
    perror ("Non posso aprire la seriale"); // Stampa il messaggio di errore
    exit (2);                                // E termina drasticamente con errore
  }

  /* Impostazione della seriale
   * fonte info libc->"Low-Level Terminal Interface"->"Noncanonical Mode Example" */
  tcgetattr (com, &tattr);                // Recupera gli attributi correnti della seriale

  // Modi di ingresso - spegne i flag indicati
  tattr.c_iflag &= ~(INLCR|IGNCR|ICRNL|IXON);
  // Modi di uscita - spegne i flag indicati
  tattr.c_oflag &= ~(OPOST|ONLCR|OCRNL|ONLRET); 
  /* Modi di controllo - imposta 8 bit (CS8), abilita ricezione (CREAD),
   *ignora segnali di controllo (CLOCAL) */
  tattr.c_cflag = CS8 | CREAD | CLOCAL; 
  tattr.c_lflag &= ~(ICANON|ECHO);        // elimina traduzioni di carateri (ICANON) ed ECHO.
  tattr.c_cc[VMIN] = 1;                        // restituisce dati dopo aver ricevuto 1 carattere
  tattr.c_cc[VTIME] = 0;                // nessun timeout

  cfsetispeed (&tattr, B9600);                // Imposta la velocita' di ricezione
  cfsetospeed (&tattr, B9600);                // Imposta la velocita' di trasmissione

  tcsetattr (com, TCSAFLUSH, &tattr);        // Imposta i nuovo attributi

  return com;                                // Ritrona l'handle del file aperto
}

Per provare il programma, scaricare il sorgente, compilarlo con il comando cc seriale.c -o seriale -lncurses ed eseguirlo con il comando ./seriale.


[Home Page dell'ITIS "Fermi"] [Indice Quinta] [Precedente] [Successivo]

© Ing. Stefano Salvi - Released under GPL licence