Genève, 07-18 lug. USB Linux driver per Cypress FX2
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
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