Appunti sullo sviluppo di un driver per Linux per il controllo di una automobilina radiocomandata

di Marco Faella e Marco Trucillo

 

2.1    Major e minor

Ogni file speciale è collegato al "suo" driver tramite due numeri, detti major e minor, di cui il primo identifica il driver vero e proprio, mentre il secondo serve a discriminare tra diverse periferiche nel caso che uno stesso driver ne gestisca più d'una. 
E' possibile visualizzare i numeri associati ai file speciali presenti con il semplice comando ls -l. Il major e il minor saranno, nell'ordine, i due numeri che precedono la data di creazione del file. Ecco alcuni esempi: 
 
file speciale
major
minor
/dev/zero
38
0
/dev/mem
2
0
/dev/null
2
2

Dalla tabella si evince che i dispositivi (virtuali) mem e null sono gestiti dallo stesso driver (perché hanno major uguale). 
Fisseremo per il nostro driver il major 50, presumibilmente disponibile su di un sistema tipico. Il file speciale, che chiameremo car0, avrà minor pari a zero, per suggerire che è la prima (e nella maggior parte dei casi anche l'unica) macchinina controllata dal driver. 
 
 

 

2.2    I comandi

 A questo punto non ci resta che decidere come far corrispondere ad ogni carattere inviato al driver un comando per il telecomando. Una possibilità consiste nel prendere i 4 bit meno significativi di ciascun carattere ed interpretarli direttamente come i 4 comandi disponibili, dopo aver filtrato quelli contraddittori. 
In altre parole, se il carattere inviato ha rappresentazione binaria <c7 c6 c5 c4 | c3 c2 c1 c0>, i suoi bit saranno così interpretati: 
c0=1 --> sterzo a destra
c1=1 --> sterzo a sinistra
c2=1 --> motore indietro
c3=1 --> motore avanti.
Rimangono inutilizzati i 4 bit più significativi. Possiamo farne qualcosa di utile: specificheranno la durata in secondi del comando. In pratica, useremo la macro: 
#define CAR_TIME(data) ((data & 240) >> 4)
per ottenere un intero da 0 a 15 che rappresenterà il numero di secondi in cui il comando dovrà essere attivo. 
Ad esempio, il carattere "Y", in decimale 89 ed in binario <0101 | 1001>, indicherà di andare avanti a destra per 5 secondi, mentre il carattere "B", in decimale 66 ed in binario <0100 | 0010>, richiede di tenere le ruote sterzate a sinistra per 4 secondi (un comando alquanto inutile, in effetti). 
 

2.3    Car.h

Presentiamo l'header che conterrà le costanti e le macro usate nel corpo del driver. 
Mentre lo scopo di alcune di esse è evidente, l'utilità di altre sarà chiarita solo più avanti. 

struct car_struct {  
   int base; // base address io  
   int flag; // stato del dispositivo  
};  

/* Costanti  varie */  

#define CAR_MAJOR 50  

#define STOP       0  
#define AVANTI     1  
#define DIETRO     2  
#define DESTRA     4  
#define SINISTRA   8  

#define CAR_EXIST 0x0001  
#define CAR_BUSY  0x0002  
#define CAR_RESET 1  

/* Macro varie */ 
#define CAR_B(minor)    car_table[minor].base     /* indirizzo di base della porta (i/o)  */  
#define CAR_S(minor) inb_p(CAR_B((minor)) + 1)    /* stato della porta    */  
#define CAR_F(minor)    car_table[minor].flag     /* restituisce lo stato */  
#define CAR_TIME(data)  ( ( data & 240 ) >> 4 )   /* restituisce il tempo di esecuzione del comando */  
#define CAR_DATA(data)  ( data & 15 )             /* restituisce il comando da effettuare */  
 

 

2.4    Open e Close

Entriamo finalmente nel merito del driver, cominciandocon le funzioni che saranno chiamate quando un applicativo invocherà una open o una close sul file speciale car
L'unico compito che queste due funzioni dovranno assolvere è far si che un solo programma per volta possa aprire con successo il file. 
Ad ogni macchina (contempleremo anche la possibilità che ce ne siano due) assoceremo un carattere che ne rappresenti lo stato; ne useremo però soltanto due bit: uno per specificare se la macchina è presente e l'altro per indicare se la macchina è attualmente in uso (cioè se il suo file speciale è stato aperto). 
Ecco il codice delle funzioni di apertura e chiusura: 

int car_open(struct inode * inode, struct file * file) 
{ 
 unsigned int minor = MINOR(inode->i_rdev); 
  
 if (minor >= CAR_NO) return -ENXIO;// auto non esistente 
  
 if (! (CAR_F(minor) & CAR_EXIST) ) 
    return -ENODEV;// auto non esistente 
  
 if (CAR_F(minor) & CAR_BUSY) 
    return -EBUSY; // auto gia in uso 
  
 CAR_F(minor) |= CAR_BUSY; // occupo l'auto 
 return 0; 
} 
 

void car_release(struct inode * inode, struct file * file) 
{ 
 unsigned int minor = MINOR(inode->i_rdev); 
 car_reset(minor); 
 CAR_F(minor) &= ~CAR_BUSY; // libero l'auto 
} 
 

2.5     Ioctl

Forniremo anche una funzione di i/o control, che però eseguirà un solo comando (CAR_RESET), incaricato di far fermare la macchinina. Ecco le due funzioni interessate. 

int car_reset(int minor) 
{ 
 outb_p(0x0, CAR_B(minor)); 
 return CAR_S(minor); 
} 

int car_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) 
{ 
 unsigned int minor = MINOR(inode->i_rdev); 
 int retval = 0; 

 if (minor >= CAR_NO) /* minor troppo alto */ 
    return -ENODEV; 
 if ((CAR_F(minor) & CAR_EXIST) == 0) /* questa macchinina non esiste */ 
    return -ENODEV; 

 switch ( cmd ) { 
   case CAR_RESET: 
   car_reset(minor); 
   break; 
   
  default: 
   retval = -EINVAL; 
 } 
 return retval; 
}