Genève, 07-18 lug. USB Linux driver per Cypress FX2

Pagina creata da Sara Bernardi
 
CONTINUA A LEGGERE
Genève, 07-18 lug.

USB
                                        Istituto Nazionale
                                        di Fisica Nucleare

                               Linux driver per Cypress FX2
                                                      Di: Francesco Perri - Università degli studi di Perugia, Informatica.
                                                                                    Mailto: francesco.perri.gfs@libero.it
Panoramica sul mondo USB
Il modo di interconnettere periferiche di ogni tipo ad un Personal Computer negli ultimi anni è
radicalmente cambiato grazie all'invezerione e alla diffusione del bus dati ad alta velocità USB
(Universal Serial Bus) che sta sostituendo rapidamente i vecchi sistemi di interfacciamento, come le
celebri porte PS/2, RS-232, Centronix, ma soprattutto tutti i più svariati tipi di connettori delle
particolari interfacce dedicate a specifici compiti.
Il successo enorme ottenuto dal nuovo sistema è dovuto alle sue fondamentali caratteristiche sia
hardware che software:
        ➢ Il bus, come detto, è seriale ed usa perciò sia cavi che connettori semplici ed economici.
        ➢ Il segnale elettrico all'interno del cavo è di tipo differenziale, permettendo una maggiore
            resitstenza al rumore e la possibilità quindi di usare velocità più elevate o cavi più
            lunghi.
        ➢ Il bus USB fornisce direttamente l'alimentazione di potenza per dispositivi periferici di
            basso e medio assorbimento, risparmiando l'impiego di alimentatori ed ulteriori cavi.
        ➢ I disposivi possono essere connessi e sconnessi a caldo, senza quindi dover riavviare il
            sistema.
        ➢ Si posssono connettere contemporaneamente molteplici periferiche anche di natura
            completamente diversa.
        ➢ Il dettaglio elettrico, la serializzazione dei dati e tutta la gestione della comunicazione
            USB sono gestiti centralmente da un driver facente parte del sistema operativo e comune
            a tutti i dispositivi. Ne segue che per sviluppare o modificare il software di nuove
            periferiche si deve solo trattare la comunicazione USB ad alto livello astraendo da tutti i
            dettagli della comunicazione, rendendo i programmi più semplici da sviluppare e molto
            più efficienti nel funzionamento.
        ➢ Esistono diverse famiglie di chip molto economici ed efficienti da impiegare nei
            dispositivi che sono in grado di gestire automaticamente tutta la comunicazione USB dal
            lato computer-dispositivo. Dal lato dispositivo-utente, invece, si occupano della
            parallelizzazione dei dati, ove richiesto, e spesso della loro traduzione nei principali
            protocolli comunicativi standard come USART RS-232, Parallel SPP-EPP-ECP, Serial
            I²C compatibile, Ethernet, ecc.
        ➢ La maggior parte dei dispositivi è in grado di funzionare su qualsiasi sistema operativo
            che gestisca il bus USB, favorendo la diffusione del dispositivo stesso o la sua
            portabilità.

Il chip CY7C68013 e la Test Board EZ-USB-FX2
La scheda elettronica, prodotta da Cypress semiconductor's, EZ-USB-FX2 è particolarmente utile
negli ambienti di sviluppo dei dispositivi per effettuare studi approfonditi e test sul funzionamento
del sistema USB. L'EZ-USB-FX2 può essere vista come un banco di prova, che offre molteplici
connettori e strumenti per studiare e monitorare le comunicazioni tra il computer e il dispositivo
tramite bus USB. Data la sua semplicità d'uso e la sua potenza questa scheda può anche essere
utilizzata direttamente nello sviluppo di dispositivi di controllo. Il suo componente principale, il
processore CY7C68013, infatti oltre che a gestire interamente la comunicazione USB tramite due
FIFO molto capienti, ha integrato anche un micro-controller avanzato della famiglia Intel 8051.
Qust'ultimo può essere programmato direttamente tramite il bus USB ed eseguire controlli di
monitoraggio e sviluppare una politica gestionale sulla comunicazione. In particolare, questi servizi
sono solo ad un livello di astrazione tale da poter ignorare ogni dettaglio elettronico della
comunicazione, data la lentezza dell'8051, rispetto alle tipiche frequenze di trasmissione del bus
USB. In oltre il micro-controller può eseguire i normali programmi usati sui micro-controller, ma la
quantità delle sue risorse è del tutto eccezionale. Vi sono infatti due porte seriali USART RS-232, tre
porte general purpose, una porta I²C compatibile, tre timer, vaste memorie RAM ed EEPROM e via
dicendo. E' proprio il numero di risorse disponibili a costi non troppo elevati e quindi la flessibilità
di applicazione che fanno di questo processore uno dei principali componenti per lo sviluppo di
sistemi USB avanzati.
La Cypress semiconductor's fornisce per questo processore il driver, varie suite di compilatori ed
una vasta gamma di software di prova per piattaforme MS-Windows, ma rilascia solo pochi abbozzi
di driver per piattaforme Unix/Linux & Machintosh, pur dichiarandone la perfetta compatibilità.
Essendo Linux la piattaforma principalmente utilizzata negli ambienti scientifici, nasce la necessità
di scrivere e testare un driver di comunicazione del processore CY7C68013 per queste piattaforme.

Architettura di un driver
Prima di procedere nello sviluppo del driver USB, è necessario definire meglio l'architettura di un
driver generico.
Innanzi tutto diciamo che un driver è un particolare programma che fa da appendice al sistema
operativo rendendolo capace di usare e gestire efficientemente un particolare dispositivo periferico.
Il concetto di driver è relativamente recente. Nei primi sistemi informatici, inclusi i primi personal
computer, infatti erano le applicazioni stesse a gestire i dispositivi che esse utilizzavano. Ma questa
filosofia era svantaggiosa per due motivi: in primo luogo scrivere le applicazioni era molto più
complesso e richiedeva programmatori con conoscenze approfondite circa gli standard dei
dispositivi e dei meccanismi dei sistemi operativi, in secondo luogo, nel momento in cui si doveva
sostituire il dispositivo, un esempio classico ne è la stampante, era molto difficile ed oneroso dover
cambiare per ogni applicazione tutte le impostazioni riguardanti l'uso del dispositivo.
La filosofia odierna prevede, invece, che le applicazioni ignorino quali siano i dispositivi
effettivamente utilizzati, e che parlino tutte un linguaggio standard, intendendo i loro dispositivi
ideali. E' compito invece del sistema operativo tradurre i comandi inviati al dispositivo virtuale nei
giusti comandi comprensibili al particolare dispositivo reale usato. Questo è il compito principale
del driver. Così questo costituisce l'unica parte di software che è a conoscenza sia del sistema
operativo adottato che delle caratteristiche tecniche del dispositivo reale. In questo modo è molto
più semplice ed economico sviluppare applicazioni in quanto non sono più necessari programmatori
specializzati, se non per lo sviluppo dei driver, ed è possibile far usare ad applicazioni diverse i
medesimi dispositivi. In caso di sostituzione o dei dispositivi o delle applicazioni, è sufficiente fare
solamente un'unica operazione di aggiornamento, senza dover fare onerose modifiche in cascata.
L'architettura di un driver è scomponibile livelli gerarchici fondamentali, ciascuno dei quali
implementa e offre uno o più servizi che verranno usati dai livelli superiori, sfruttando quelli offerti
dai livelli inferiori. Rifacendosi un po' alla nomenclatura della pila ISO-OSI usata per le
connessioni delle reti di computer, possiamo quindi ripartire i compiti del driver in secondo quanto
lo schema seguente illustra:

                             Applicativo   Fornisce primitive ad alto livello per le
                        4                  applicazioni che useranno il dispositivo
                              Protocollo   Stabilisce le regole del protocollo di
                         3                 trasmissione.
                              Data-Link    Controlla la connessione fisica e logica
                        2                  del dispositivo e la trasmissione dei dati
                                Fisico     Gestisce la comunicazione elettrica tra il
                        1                  PC e il dispositivo
Il primo livello, quello fisico, fornisce tutta una serie di primitive che servono per variare lo
stato dei singoli bit dei registri delle porte di comunicazione con il dispositivo. In pratica, se
consideriamo un generico dispositivo elettrico, utilizzando le primitive del livello fisico siamo in
grado di controllare la tensione delle linee elettriche delle porte di comunicazione, o l'accensione
del led se stiamo consideriamo invece un dispositivo ad infrarossi o connesso tramite fibra ottica.
         Il secondo livello è generalmente detto livello di data-link. Sfruttando le primitive offerte da
livello fisico, il data-link stabilisce e gestisce la connessione logica del dispositivo ed organizza
intere sequenze di variazioni di stato dei registri del livello fisico, affinché un byte, o un blocco di
byte, a seconda del tipo di dispositivo, sia trasmesso o letto correttamente. Dopo ogni operazione di
lettura o scrittura, il data-link prevede che si ricevano o si inviino i segnali di ack per garantire
l'efficienza della trasmissione. Quest'ultima parte tecnicamente viene detta l'handshake della
comunicazione.
Si noti che in un dispositivo con connessione parallela l'handshake è una funzione abbastanza
semplice in quanto tutti i bit vengono trasmessi contemporaneamente su linee separate. In un
dispositivo con connessione seriale, invece, l'handshake è molto più complesso in quanto bisogna
studiare un meccanismo tramite il quale si sia sempre in grado di distinguere un bit dall'altro dal
momento che questi corrono tutti nella medesima linea. Normalmente, si inseriscono durante la
trasmissione, speciali condizioni dei segnali elettrici che costituiscono i codici di controllo più o
meno complessi in grado di identificare univocamente ogni singolo bit di dati di una trasmissione
seriale. Tutto questo meccanismo va comunque a scapito della velocità di trasmissione. Nel livello
di data-link sono implementati i meccanismi di handshake.
         Il trezo livello normalmente descrive le rgole del protocollo di trasmissione. A differenza
dei precedenti livelli dove le velocità operative sono elevatissime, in questo livello la velocità non
rappresenta una scadenza da dover rispettare. Si devono infatti solamente verificare che interi
blocchi di dati in movimento, siano conformi alle specifiche del protocollo, per essere certi della
loro correttezza e per essere in grado di memorizzarli nelle giuste locazioni e, infine, di eseguire su
di essi le giuste operazioni. Nel sitema USB questa parte è invisibile agli utenti e controllata
interamente dal gestore USB. Per dispositivi dedicati ad applicazioni più complesse è tuttavia
permesso ai programmatori di vederne solamente quella parte che il micro-controller può gestire.
Variando quindi il firmware dell'8051 si possono scrivere varianti del protocollo di connessione con
il dispositivo più efficienti per compiti specializzati.
         Il quarto livello, l'"Applicativo", è quasi tutto ciò che il programmatore riesce a vedere di
un driver. Questo livello fornisce le vere primitive che verranno invocate dal sistema operativo,
ogni volta che un'applicazione vuole utilizzare il dispositivo.
Il punto di forza, e quindi il successo dell'uso dei driver risiede nello sfruttare appieno il fatto che
tutte le operazioni eseguibili in un dispositivo, sono in fondo sempre operazioni di lettura o di
scrittura di registri, porte o dati. Quindi tutte le varie funzioni sono sempre riconducibili alle stesso
formato delle operazioni di I/O usate per i file. Le primitive offerte dal livello "applicativo" possono
essere perciò standardizzate e rese indipendenti dal dispositivo, immaginando quest'ultimo come se
fosse un file.
Un generico driver sviluppa tutti e quattro i livelli descritti. E' infatti un programma che richiede la
piena coscienza di tutte e tre le componenti in gioco: il dispositivo, il sistema di comunicazione e il
sistema operativo utilizzato. Per questa ragione sviluppare un driver è sempre una cosa difficile e
costosa.

Driver tradizionali e driver USB
Data la grande innovazione fisica e la grande rivoluzione strutturale della logica del sistema USB,
possiamo dividere i dirver dei dispositivi in due grandi classi: quelli tradizionali in una prima e
quelli USB o "simil - USB" nella seconda.
Lo schema seguente mostra una sintesi sul concetto di driver tradizionale.
Applicazione/i

                                               Sistema Operativo

                                      Driver                     Dispositivo

L'applicazione chiede al sistema operativo di poter accedere al dispositivo virtuale come se fosse un
file. Il sistema operativo utilizza il driver per tradurre le operazioni standard di lettura e scrittura su
file nelle giuste sequenze di comandi accettati ed eseguiti dal dispositivo reale. Le funzioni di I/O
sulle porte sono effettuate comunque dal drive con codice assembly e solo in rari casi dal sistema
operativo, tramite l'uso di apposite primitive.
Dal momento che le porte di comunicazione sulle quali è connesso il dispositivo non sono quasi
mai condivise da altre periferiche, salvo rari casi, come la porta seriale RS-485 usata in ambienti
industriali, il driver sviluppa anche i livelli 3, 2, ed 1 della tabella mostrata precedentemente.
Quindi il driver del dispositivo coincide il più delle volte con il driver del sistema di
comunicazione.
Una delle maggiori caratteristiche del sistema USB è, invece, costituita dal fatto che si possono
connettere contemporaneamente, fino a 127 dispositivi diversi o uguali. Per questo motivo
abbiamo perciò un'architettura dei driver notevolmente diversa e, contrariamente a quanto si possa
pensare, assai più semplice. Lo schema successivio ne riassume la struttura.

                                                Applicazione/i

                                                    Driver

                                               Sistema Operativo
                                                 Gestore USB
                                                  Dispositivo

Come si può notare il sistema operativo include un gestore del sistema USB. Questa parte sviluppa i
livelli 1, 2 e parte del 3 per il bus USB, perché sono comuni a tutti i dispositivi e sarebbe poco
sensato caricarla più volte e per altro sarebbe soltanto una fonte di conflitti nell'accesso al bus. Il
driver di un dispositivo USB qualunque, sviluppa quindi solo il quarto livello, quello "applicativo".
In parole semplici un driver USB stabilisce l'inizializzazione iniziale e lo stato finale del
dispositivo, decide dove devono essere memorizzati i dati e da dove devono essere prelevati tra le
memorie del dispositivo, stabilisce anche quali sono i comandi che il dispositivo può accettare e via
dicendo. Ma questa classe di driver non implementa mai il "come" devono essere trasmessi i dati
perché se ne occupa il gestore USB. Infatti non sono mai necessare funzioni di I/O diretto scritte in
assebly o chiamate alle primitive di I/O del sistema operativo.
Se il concetto di driver aveva già dimezzato i costi di sviluppo delle applicazioni, e dei dispositivi
stessi, il sistema USB li ha fatti scendere ulteriormente. Non è necessario infatti specializzare i
programmatori sulle specifiche tecniche dei segnali, dei tempi di trasmissione, ecc. Tutto vine
gestito una volta per tutte dal sistema operativo in maniera del tutto naturale ed efficiente. In più il
sistema USB non usa le normali risorse del sisema operativo riservate alle periferiche, quali gli
interrupt, alcuni registri e particolari regioni di memoria, che sono in quantità molto limitata e
sicuramente non sufficiente a soddisfare tutte le possibili 127 periferiche. Il sistema USB usa una
sola di queste risorse (l'idirizzo del gestore USB e una sola linea di interrupt) e poi provvede a
sviluppare internamente gli indirizzi e un sistema di interruzione per tutti gli altri dispositivi. Il
gestore USB conosce queste architetture interne e le rende trasparenti al programmatore,
fornendogli una serie di funzioni molto flessibili, ad alto livello, per scrivere con semplicità i vari
driver come se avessero sempre disponibili una linea di interrupt, registri e regioni di memoria
riservate.

I driver degli ambienti Unix/Linux
Negli ambienti Unix e Linux sviluppare nuovi driver è una cosa relativamenete semplice. Il kernel,
il nocciolo del sistema operativo, è, infatti, scritto in linguaggio C, lo stesso necessario per la
scrittura dei driver, e sono sempre disponibili i sorgenti e gli header file di tutti i moduli che lo
compongono, cosa che non avviene negli ambienti Microsoft, Machintosh, ecc. Questi file sorgenti
sono necessari per studiare i prototipi delle funzioni e delle primitive del sistema operativo in modo
da poterle sfruttare al meglio. Tuttavia la programmazione di un driver presenta delle caratteristiche
di programmazione del tutto particolari, diverse e spesso sconsigliate nello sviluppo di normali
applicazioni.
Per definizione un driver è un'appendice, un'estensione del sistema operativo, che lavora quindi nel
così detto system space, ovvero l'insieme composto dalle aree di memoria e dalle istruzioni riservate
esclusivamente al sistema operativo. Per ciò tutti i suoi componenti devono essere inseriti
all'interno del kernel del sistema operativo. Risulta del tutto naturale quindi il fatto che non sia
possibile utilizzare le normali librerie previste nel linguaggio C, perché sono progettate per lavorare
nel user-space e quindi si creerebbero una serie di incoerenze e conflitti nell'accesso alle risorse ed
alla memoria. Anche se di solito per un driver non servono le normali librerie C, per risolvere
questa limitazione gli ambienti Unix & Linux offrono una vasta serie di librerie adtatte ad essere
usate in questi moduli; ad esempio la funzione printk() sostituisce, con identico funzionamento,
la normale printf(), con la sola differenza che l'output generalmente non viene visualizzato sul
video, ma viene registrato nel log file /var/log/messages.
Un altro tartto caratteristico della programmazione di un driver è che non si generano mai file
eseguibili durante la compilazione, ma solamente i file oggetto. Questi devono essere inseriti nel
kernel prima di poter utilizzare il dispositivo, tramite il comando insmod nome_modulo.o.
Per rimuovere un modulo dal kernel invece si utilizza il comando rmmod nome_modulo, entrabi
i comandi devono essere lanciati con i diritti del super utente.
Un'altra differenza ancora tra la porgrammazione di un driver e quella di una applicazione è la
assenza di una funzione main() e una sua sostituzione con due particolari funzioni che devono
essere sempre presenti: init_module() e cleanup_module(). Queste due funzioni vengono
chiamate ogni volta che un modulo kernel viene inserito o rimosso, rispettivamente, e attuano una
corretta inizializzazione o cancellazione sia delle strutture dati interne al driver, che dei valori dei
registri e delle porte del dispositivo fisico.
Un altro stile particolare nella programmazione dei driver è dato dall'uso frequente del comando:
goto etichetta. Sebbene tale comando è sconsigliato nel normale contesto applicativo, perché
fonte di errori logigi all'interno del programma, difficilmente rilevabili, nella programmazione dei
driver o dei moduli kernel in generale è molto usato e forse è addirittura l'unico caso in cui è
fortemente consigliato. Un errore di comunicazione o un valore errato nei registri durante
l'esecuzione delle funzioni di un driver, possono infatti facilmente mandare in blocco l'intero
sistema, con conseguenze spesso spiacevoli. E' necessario quindi controllare per ogni operazione di
comunicazione con il dispositivo il corretto esito e per ogni manipolazione delle strutture dati del
kernel assicurarsi della loro consistenza. In caso di errori e per ogni classe di errore, è necessario
avere una particolare funzione che sia in grado di ripristinare correttamente lo stato del sistema. Il
numero delle funzioni di ripristino sarebbe quindi notevolmente elevato. E', perciò, molto più
semplice ed immediato utilizzare la funzione goto etichetta, che in aggiunta non prevede
ritorni, i quali sarebbero in questo contesto possibili fonti di errori logici.
I dettagli definiti in questo paragrafo sono solo una minima parte di quello che è necessario sapere
per progettare e sviluppare dirver efficienti, ma spiegazioni più approfondite esulerebbero dagli
obbiettivi di questo articolo. Ci concentriamo quindi direttamente sullo sviluppo del driver per la
Test Board EZ-USB-FX2.
Il codice del driver USB
Il codice sorgente del driver USB per la scheda EZ-USB-FX2 è suddiviso molto semplicemente in
soli tre file distinti, come mostrato dallo schema seguente.

                                             IOCTLCommand.h

                          USB_DRV.h            USB_DRV.c                Firmware.h

Il file header USB_DRV.h
Questo file contiene le definizioni che costituiscono l'intera struttura dati del driver e i prototipi
delle sue funzioni. Le sue prime definizioni, però, più che far parte del driver, sono proprie della
filosofia di programmzione dei moduli kernel in generale.
Ogni modulo che deve essere inserito nel sistema operativo deve sempre avere innanzi tutto una
precisa definizione della versione del kernel per il quale esso è stato scritto e complilato.
Diversamente, non sarebbe possibile stabilire se un modulo è trasportabile o utilizzabile con una
nuova versione del kernel. Un errore di versione potrebbe portare il sistema in uno stato instabile e,
molto più frequentemente, in stallo. Per semplificare tale dichiarazione è possibile indicare uno
spazio dove il compilatore salva automaticamente la versione del kernel utilizzata.
Devono, inoltre, essere sempre presenti, per motivi di sicurezza, anche delle definizioni che
indichino gli estremi dell'autore del modulo e una sua piccola descrizione. Un driver senza queste
definizioni sarebbe sicuramente improbabile come una bottiglia di vino senza il marchio DOC, e
potrebbe esporre il sistema ad attacchi maliziosi veramente devastanti, in quanto il modulo diventa
parte integrante del kernel con la possibilità di accedere senza limitazione alcuna a qualsiasi risorsa
del sisema. Infine, vi deve essere una definizione che dichiari il tipo di licenza d'uso del modulo.
.
.
.
#define USBDRV_VERSION "1.0"
#define USBDRV_AUTHOR "Francesco Perri"
#define USBDRV_DESC    "Cypress EZ-USB-FX2 (CY3681) Development Board"
.
.
.
Completate le definizioni generali, nel file USB_DRV.h, vi sono una serie di definizioni proprie dei
meccanismi del bus di comunicazione. Per un driver USB sono necessarie due definizioni di codici:
Un primo codice detto vendor_id identifica la ditta che produce il dispositivo e un secondo, il
product_id che identifica il dispositivo stesso.
.
.
.
#define CYPRESS_VENDOR_ID 0x04B4
#define CYPRESS_PRODUCT_ID 0x0081
.
.
.
Ogni volta che il dispositivo viene connesso, il gestore USB legge i codici vendor_id e product_id
da una memoria EEPROM del dispositivo stesso e li confronta con quelli contenuti in ogni driver
USB. Se il gestore trova in un driver una coppia di codici identici a quelli letti nel dispositivo,
allora crea il "binomio" driver-dispositivo: da ora in avanti, infatti, tutte le operazioni richieste dal
dispositivo verrando indirizzate al driver trovato e viceversa. Se invece, il gestore USB non trova
nessun driver che abbia la stessa coppia di codici a quelli contenuti nel dispositivo, lo notifica con
un messaggio nel log file di sistema (/var/log/messages) e porta il dispositivo in uno stato di
attesa. Con questo meccanismo, il sistema operativo è in grado di associare ad ogni dispositivo il
giusto driver, come mostrato nello schema che segue.

                                             vendor_id
                       EZ-USB-FX2
                        DRIVER                   &
                                             product_id

                       Mouse USB
                                                      Gestore USB
                        DRIVER                              &
                                                       OS - Kernel

                          ...                                            vendor_id
                          ...                                                &
                        DRIVER                                           product_id
                                                          Mouse
                                                          USB

                                              ...                      EZ-USB-
                                             USB                         FX2

Ora è necessario associare le operazioni richieste dalle applicazioni al giusto driver. Come detto le
richieste sono sempre delle operazioni sui file. Una applicazione quindi per usare un dispositivo,
chiede la scrittura o la lettura nel file ad esso associato. Per fare questa associazione i sistemi Unix
& Linux prevedono innanzitutto la creazione di un file vero e proprio, di solito collocato nella
directory /dev/usb/, che rappresenta il dispositivo virtuale avente un nome che
mnemonicamente identifichi il dispositivo stesso.
Quando si vuole utilizzare un nuovo dispositivo, si deve prima creare la sua astrazione virtuale, cioè
il suo file associato. Generalmente questo viene eseguito dai programmi di installazione, ma in sede
di sviluppo di dispositivi propri si usa il comando: mknod /dev/usb/nome_disp_virtuale 
 .
Il parametro più importante è l'inode. Esso deve essere diverso per ogni file associato ad un
dispositivo. Le applicazioni scritte dall'utente, infatti, faranno richesta di operazioni sul file
indicandone il nome mnemonico, mentre il sistema operativo le reindirizza a quel driver che ha
definito al suo interno come minor_number lo stesso valore di inode del file. Nel file USB_DRV.h
è presente quindi anche la seguente definizione:
.
.
.
#define USBDRV_MINOR 24
.
.
.
Lo schema seguente completa la sintesi precedente del meccanismo di identificazione del driver di
un dispositivo USB.

                     Inode (minor-number)
   EZ-USB-FX2
    DRIVER

                                                               /dev/usb/mouse
                vendor_id
                    &                             Minor-number
                product_id

                                  Gestore USB                                      File Name
   Mouse USB                                                                                   Test.cpp
    DRIVER                              &                          /dev/ubs/FX2
                                   OS - Kernel

                                                                    /dev/usb/...

      ...
      ...                                             vendor_id
    DRIVER
                                                          &
                                     Mouse            product_id
                                     USB

                          ...                      EZ-USB-
                         USB                         FX2

Il modulo Test.cpp mostrato nello schema, rappresenta una generica applicazione che richiede
l'uso del dispositivo inviando comandi di lettura/scrittura al file filename.
Ora il sistema è in grado di gestire le richeste delle applicazioni e di inviarle al dispositivo con i
seguenti passi:
          ➢ Il programmatore dell'applicazione richiede operazioni di lettura e scrittura al file
             chiamandolo per nome.
          ➢ Il sistema operativo sa che a questo file in realtà è associato ad un dispositivo, controlla
             il suo inode e cerca il driver che abbia il minor-number dello stesso valore.
          ➢ Il driver a sua volta cerca il dispositivo che abbia gli stessi codici di vendor_id e
             product_id e, una volta trovatolo, gli invia nel giusto formato le operazioni richieste.
Tutti i driver di dispositivi USB, oltre ad avere le precedenti definizioni per far funzionare i
meccanismi di identificazione di caratte generale appena spiegati, rappresentano il dispositivo e le
sue principali caratteristiche mediante una struttura dati. Questa struttura dati deve salvare tutte le
informazioni necessare per la comunicazione USB e per lo scambio di dati richiesti da un
dispositivo. Generalmente viene indicata come il descrittore del driver.
.
.
struct device_driver_data
  {
    struct usb_device *usbdev;
    struct usb_interface *interface;
    struct usb_interface_descriptor *interface_desc;

     unsigned char *inbuf;
     int insize;
     __u8 inadd;

     unsigned char *outbuf;
     int outsize;
     __u8 outadd;

     struct urb *outurb;
     int urb_opened;
     int device_opened;

      struct semaphore sem;
    };
.
.
               Il puntatore *usbdev contiene l'indirizzo ad una particolare struttura dati che
descrive lo stato e le funzioni del sistema USB. Ogni driver per comunicare con il dispositivo ad
esso associato deve utilizzare tale struttura, in quanto essa rappresenta l'interfaccia tra il driver e il
gestore USB del sistema operativo.
               I puntatori *interface e *interface_desc descrivono invece un'interfaccia
logica di livello di astrazione superiore alla precedente, che ignora quindi le specifiche della
comunicazione, attraverso la quale è possibile inviare e ricevere blocchi interi di dati al o dal
dispositivo. Un dispositivo può avere deverse configurazioni della sua intefraccia logica. Detta
interfaccia è costituita da una serie di punti di accesso detti end-point che sono dei buffer di IN e di
OUT con le loro variabili di controllo, di dimensione variabile (più semplicemente sono locazioni
di memoria attraverso le quali avviene lo scambio di dtati di dimensioni, di direzione e a velocità
prestabilite). Solo attraverso l'uso di questi buffer è possible dialogare con il dispositivo.
Normalmente un dispositivo può essere configurato in diversi modi, ciascuno dei quali pervede
l'uso di end-point di dimesione e velocità diverse a seconda dell'impiego finale del dispositivo. In
ogni caso però, qualsiasi dispositivo USB deve sempre avere la configuarzione di end-point zero,
nella quale le dimensioni, la direzione IN-OUT dei buffer e la velocità di trasmissione sono
standard. Questa configurazione è infatti l'unico modo sicuro tramite il quale un dispositivo può
sempre essere riconosciuto.
Il driver, una volta associato al suo dispositivo e controllato il suo corretto status tramite l'end-point
zero, può cambiare l'interfaccia, cioè la configurazione del numero, della velocità, del verso e delle
dimensioni degli end-point, manipolando semplicemente questi puntatori. I buffer degli end-point
sono direttamente interessati alla comunicazione via USB, non possono quindi essere utilizzati per
scriverci o leggerli direttamente, ma bisogna utilizzare altri buffer ad hoc.
               I puntatori e i delimitatori di dimensione dei buffer di INPUT ed OUTPUT sono
ordinarie strutture per lo scambio effettivo di dati. Ci limitiamo solo a dire che ad esse sono
assegnate regioni di memoria appartenenti al kernel, quindi non è possibile importarvi o esportarvi
dati con i normali operatori di assegnamento. Si devono sempre usare le primitive del sistema
operativo per garantire la sicurezza dei dati e possibilmante si dovrebbero sempre usare meccanismi
di concorrena, per garantirne, invece, la consistenza.
                Il puntatore *outurb identifica uno spazio nel gestore USB tramite il quale è
possibile far uscire i dati, senza passare per gli end-point e le altre strutture USB, direttamente verso
il dispositivo. In genere questa tecnica è usata solamente per effettuare test di controllo o operazioni
ad altissima velocità, ma è necessario avere una conoscienza apporfondita del gestore USB.
                Infine il contatore interno device_opened viene incrementato ogni volta che il
driver deve aprire il file "virtuale" associato, ossia ad ogni connessione del dispositivo. Con questo
contatore il sitstema tiene quindi traccia di quei dispositivi che vengono gestiti da uno stesso driver.
Ad ogni chiusura del file associato il contatore deve essere decerementato. Normalmente, un driver
USB deve essere in grado di pilotare contemporaneamente le operazioni richieste da più di un
dispositivo identico ad altri. Questa è una delle caratteristiche fondamentali del bus USB. Perciò
l'intera struttura dati è inserita in un vettore, o, il più delle volte, è creata dinamicamente alla
connessione del dispositivo. Opportune politiche e meccanismi di muta esclusione devono evitare le
collisioni tra le richieste di un dispositivo ed un altro.

Il codice del file USB_DRV.c
Questo file è la componenete principale del driver preso in considerazione. Qui vi sono gli sviluppi
di tutte le funzioni che il driver deve contenere. Una sintesi dei principali compiti svolti dalle
funzioni è la seguente:
      ➢ Inizializzare il driver stesso all'interno del kernel.
      ➢ Liberare la memoria del kernel e portare il, o i dispositivi in stato di attesa all'eventuale
         rimozione del driver.
      ➢ Gestire la connessione del dispositivo.
      ➢ Gestire la sconnessione del dispositivo.
      ➢ Aprire e chiudere il file associato al driver.
      ➢ Tradurre le operazioni di lettura e scrittura dal formato "file" nelle sequenze di I/O e di
         comandi comprensibili dal dispositivo.
      ➢ Fornire le funzioni IOCTL.
      ➢ Sviluppare una funzione in grado di restituire un ACK dopo operazioni di I/O.

Inserimento e rimozione del driver
Le operazioni di inizializzazione del driver all'interno del kernel sono molto semplici, in particolar
modo       per     i     dispositivi      USB.     E'      sufficiente   chiamare      la     funzione
usb_register(&usb_device) propria del gestore USB e controllarne il valore di ritorno. Se
tale valore è negativo allora vuol dire che la registrazione del driver non è stata eseguita
correttamente. Ciò avviane o a causa di errori logici grossolani durante la progettazione del driver,
o raramente per mancanza di risorse nel kernel. Se l'operazione, invece, restituisce un valore non
negativo, ossia se va a buon fine, allora il parametro di ingresso conterrà l'indirizzo della struttura
dati che descrive le interazioni tra il gestore USB e il driver stesso.
La funzione complementare, quella che viene chiamata in caso di remozione del driver, deve
liberare le risorse del kernel occupate ed eventualmente portare il dispositivo, come detto
precedentemente, in stato di attesa, se questo risulta essere ancora connesso.
Le due funzioni devono avere un nome fisso, uguale ad ogni modulo kernel: int ins_mod() e
void cleanup_mod(). Tuttavia è possibile superare questa limitazione (utile nella stesura di
diversi driver simili tra di loro per il medesimo dispositivo, con la necessità di usare quindi nomi
mnemonici diversi) usando le funzioni macro module_init(*funzione) e
module_exit(*funzione), indicando come parametri i nomi delle corrispondenti funzioni di
inizializzazione e rimozione.
La funzione handle_probe
Il compito di gestione della connessione del dispositivo è sicuramente la parte più complessa del
driver, dato l'elevato numero di variabili che devono essere controllate ed impostate.
Si noti che in sede di sviluppo del driver è sempre consigliato scrivere un breve messaggio, nel log
di sistema, prima e dopo ogni operazione, descrivendone il suo esito e lo stato delle strutture dati
modificate, onde evitare di avere mal funzionamenti molto difficili d'aggiustare, data la scarsa
quantità degli strumenti di debug a disposizione. In oltre, al fine di individuare errori di concetto
sulla progettazione del driver e o malfunzionamenti dell'hardware del dispositivo, è bene avere nel
log file una descrizione molto dettagliata dello stato iniziale sia di tutte le strutture dati del driver
che del dispositivo. Sono queste le ragioni per cui la funzione che gestisce la connessione del
dispositivo è molto articolata nel suo sviluppo e ricca di messaggi di sistema.
Questa funzione, generalmente chiamata handle_probe o con nomi simili, è invocata dal gestore
USB al momento della connessione di un nuovo dispositivo, o all'avvio del sistema USB. In ogni
driver USB la handle_probe è sempre presente e deve sempre prevedere una parte di codice che
esegua il confronto tra la coppia di codici vendor_id e di product_id con quella contenuta nel
dispositivo appena connesso. Se le coppie di codici non corrispondono la funzione deve ritornare il
valore NULL che è riconosciuto dal getore USB come il segnale per continuare ad interpellare altri
driver chiamando la corrispondente funzione di gestione della connessione.
.
.
if ((usbdev->descriptor.idVendor != CYPRESS_VENDOR_ID) ||
    (usbdev->descriptor.idProduct != CYPRESS_PRODUCT_ID))
  return NULL;
.
.
In caso contrario, vengono inizializzati tutti i puntatori e le strutture dati necessarie alla creazione
del "binomio" dispositivo-driver.
Un altro compito, quindi, della funzione handle_probe è quello di assegnare lo spazio opportuno
per creazione di tali strutture. Se il kernel non dispone di un sufficente spazio allora il dispositivo
non può essere utilizzabile ed è come se per esso non esistesse un driver (da qui la necessità di
scrivere un messaggio nel log file che notifichi sempre i motivi per cui il dispositivo è stato rifiutato
dal gestore USB).
Oltre alle risorse del kernel devono essere disponibili una sufficiente quantità di risorse del sistema
USB, come la banda di trasmissione e una certa velocità, ecc. Il gestore USB deve garantire, infatti,
che tutti i dispositivi accettati possano essere serviti nel giusto modo. Nelle specifiche del bus USB
non è ammesso che un dispositivo venga servito con ritardi superiori al valore massimo specificato
dal dispositivo stesso.
Nel caso in cui ci siano risorse sufficienti, le strutture che vengono inizializzate sono:
       ➢ struct device_driver_data *dev;
       ➢ struct usb_endpoint_descripor *ep;
Il gestore USB inizializzerà i valori delle variabili del descrittore del driver e salverà il suo indirizzo
nel puntatore *dev.
La struttura indicata dal puntatore *ep serve per individuare e memorizzare tutte le possibili
configurazioni degli end-point che il dispositivo può assumere. Se si vogliono utilizzare
configurazioni diverse dall'end-point zero, è necessario assegnare la configurazione contenuta nella
struttura di *ep che meglio si adatta alle esigenze di funzionamento del dispositivo, alla struttura di
*interface del descrittore del driver, come visto in precedenza.
La funzione handle_probe generalmente ospita il codice, o meglio la chiamata ad una funzione
separata, di inizializzazione delle risorse del dispositivo. Si tratta dell'inizializzazione di un livello
di astrazione superiore, rispetto alle precedenti, che definisce le configurazioni del funzionamento
del dispositivo e non delle sue porte di comunicazione.
Nel caso della scheda Cypress EZ-USB-FX2, ad esempio, c'è bisogno di scaricare il firmware del
micro-controller 8051 ad ogni connessione del dispositivo, il quale definirà il comportamento
operativo della scheda (si noti che il reset del dispositivo corrisponde ad una sequenza di
sconnessione e connessione).

Le funzioni file_open(...) e file_close(...)
Sono invocate dal sistema operativo ogni volta che un'applicazione chiede l'apertura del file
rappresentante il dispositivo virtuale. Il sistema operativo, invece di aprire o chiudere il file, inoltra
la richiesta al driver. Quest'ultimo incrementa il contatore device_opened visto prima. Un
driver può servire più applicazioni contemporaneamente con l'uso di meccanismi di concorrenza,
oppure permettere solo un accesso seriale al driver, quindi al dispositivo. In sede di sviluppo e di
prova del dispositivo è più semplice impedire accessi multipli, utilizzando il contatore
device_opened come se fosse un falg booleano

Le funzioni di Read & Write
Le funzioni di lettura e scrittura contenute nel driver sostituiscono, tecnicamente si dice che fanno
un over load, le normali funzioni di lettura e scrittura fornite dal sistema operativo per i file.
Il compito di tali funzioni è di leggere o scrivere un blocco di dati dalle code FIFO del bus USB
utilizzando le primitive fornite dal suo gestore. Il gestore USB mette a disposizione svariate
primitive che offrono la possibilità di prelevare o inviare un byte, un intero buffer di dati, oppure
inviare messaggi di controllo in particolari indirizzi del processore USB, e via dicendo.
Nel driver preso in considerazione è utilizzata solo la primitiva usb_bulk_msg(...) che legge
o scrive un blocco di dati sotto forma di messaggio. L'operazione di lettura o di scrittura sono
selezionate a seconda della pipe indicata nei suoi parametri di ingresso rcv... per la lettura e
snd... per la scrittura. Il messaggio da scrivere è letto dal buffer di OUTPT del descrittore del
driver e inviato alle FIFO del sistema USB. Il messaggio che, invece, deve essere letto è copiato
dalle FIFO nel buffer di lettura del descrittore.
.
.
rtrn_val = usb_bulk_msg(dev_data->usbdev,
                        usb_rcvbulkpipe(dev_data->usbdev, dev_data->inadd),
                        (unsigned char *)dev_data->inbuf, dev_data->insize,
                        &n, HZ*3);
.
.
Il parametro HZ*3 stabilisce in secondi il tempo che il gestore USB deve restare in attesa, prima di
stabilire e notificare un errore di timeout.
Per poter inviare i dati alle applicazioni è necessario farle uscire dallo spazio kernel e portarle nel
cosiddetto user-space, quello delle applicazioni. Le primitive di sistema operativo assolvono questo
compito, garantendo, come detto prima, la sicurezza degli accessi al kernel.
.
.
if(!rtrn_val)
  copy_to_user(buffa, dev_data -> inbuf, n);
.
.
La funzione IOCTL
Spesso un dispositivo ammette, come già detto, modalità di funzionamento diverse, e comunque le
sole operazioni di read & write sarebbero limitative.
Spesso è necessario affidare alcuni compiti anziché al driver, direttamente alle applicazioni. Nello
sviluppo di un driver negli ambienti Unix & Linux è semplice programmare una funzione che
controlli le operazioni di I/O del dispositivo.
Il meccanismo di IOCTL lavora tramite un'apposita funzione contenuta in ogni driver che prende
come parametro di ingresso almeno un particolare valore: un numero, o una stringa, ecc. Questo
valore rappresenta il codice di un comando. Il programmatore di un driver deve definire quanti e
quali comandi la funzione può riconoscere e le loro operazioni.
Dal punto di vista del sistema USB, la complessità di questa funzione risiede solamente nella
progettazione dei comandi che essa deve riconoscere affinché il driver risulti allo stesso tempo il
più flessibile e sicuro possibile.
Nel caso, ad esempio, della scheda EZ-USB-FX2 il driver dovrebbe avere tra i comandi di IOCTL
una funzione capace di cambiare il firmware del 8051. Le applicazioni sarebbero quindi ingrado di
scegliere ciascuna il proprio firmware da caricare.
Se però la stessa scheda dovesse essere impegnata nel controllo di macchine delicate, ad esempio
strumenti medici, o industriali da cui può dipendere anche la salute degli utenti, si deve inserire un
meccanismo di autenticazione delle richieste di comandi IOCTL oppure procedere con la rimozione
di comandi più delicati.
Puoi anche leggere