In Breve
Firmware open source GPL2
USB 2.0 full speed
Classe HID
Funziona sui 18F2550, 2450, 2455, 2553, 4450, 4455, 4550, 4553
Codice in C, compilabile con MCC18 (anche versione gratuita)
Non sono necessari driver
Esempio di comunicazione per Windows e Linux
Per compilare il software non servono librerie proprietarie nè DDK
La versione Linux usa hiddev, il gestore nativo per HID
Introduzione
Questa guida non ha alcuna pretesa di completezza. L'ho scritta con il proposito di fornire e documentare un
esempio concreto e veloce di implementazione di una periferica USB usando un microcontrollore PIC18,
sia dal lato firmware che software.
Per informazioni più dettagliate rimando alla sezione
risorse.
Benché l'interfaccia USB esista da parecchi anni, solo di recente, vista la sparizione delle seriali e parallele,
sta diventando più comune nel campo dei microcontrollori a basso costo come i PIC18.
Dal lato hardware è molto semplice realizzare una periferica USB, come si vede dallo schema seguente
(i LED servono solo per segnalare lo stato del collegamento):
Contrariamente a quelle che l'hanno preceduta, l'interfaccia USB necessita di un complicato processo di enumerazione
anche solo per far riconoscere la periferica da parte del sistema.
Per facilitarne l'uso esistono i cosiddetti framework USB, librerie di codice che nascondono la complicazione di
questo processo a chi vuole semplicemente e velocemente far comunicare PC e microcontrollore.
Uno dei più famosi e utilizzati è quello della Microchip, che però ha l'inconveniente di essere rilasciato con
una licenza chiusa, oltre ad avere una certa (e credo immotivata) complessità.
Oltre a questo più o meno tutti i produttori di compilatori ne forniscono uno con i loro prodotti, chiaramente a
pagamento.
Sul fronte open source esistono esempi di USB generico, ma qui voglio documentare un framework finora pressoché
sconosciuto e difficile da trovare, che ha anche il pregio di implementare la classe HID.
L'autore è Alexander Enzmann; nel lontano 2005 ha pubblicato il tutto sulla rivista Nuts&Volts senza però documentare in
rete nè promuovere il suo splendido lavoro.
Io ho scoperto di recente questo firmware e ho convertito tutti i miei progetti (vedi
programmatore USB).
Ho dovuto fare qualche modifica al codice per adattarlo al compilatore MCC18, visto che in origine
era stato scritto per SDCC.
La classe HID
Il protocollo USB divide le periferiche in classi, ognuna delle quali ha delle limitazioni sulla velocità e
il tipo di trasferimento; è anche possibile inventare nuove classi e sfruttare a piacimento tutte le
risorse previste dal protocollo.
Il vantaggio di usare una classe già definita è che non sono necessari driver appositi, in quanto tutti i
sistemi operativi moderni le supportano già.
In particolare la classe HID (Human Interface Device), di cui fanno parte anche tastiere e mouse,
è perfetta per le esigenze di interfacciamento di piccoli microcontrollori: il trasferimento dati è generalmente
di tipo
interrupt con pacchetti di massimo 64 byte e velocità massima di 64KB/s.
Durante l'enumerazione dei dispositivi HID il sistema legge una struttura dati chiamata
HID report descriptor, che
descrive quanti dati trasferire come devono essere interpretati.
Questo permette di avere una grande flessibilità sul tipo di periferica e sui dati trasferiti:
si possono definire nuovi tipi di dispositivo, come per esempio tastiere con tasti non standard o con
puntatore integrato, e in generale combinazioni non previste espressamente dal sistema operativo;
per maggiori informazioni consiglio di leggere la
specifica della classe HID.
Nei progetti elettronici in cui interessa il puro e semplice trasferimento dati non ha senso descrivere a cosa
servono i singoli bit, quindi nel
report descriptor viene specificata solo la dimensione del pacchetto dati.
Il trasferimento dati avviene tramite i cosiddetti
output report e
input report, in direzione rispettivamente
da e verso il PC; opzionalmente si può anche usare il
feature report nelle due direzioni.
Configurare e usare il firmware
Per prima cosa è necessario installare MPLAB e MCC18 (va bene la versione di prova), scaricabili entrambi
dal
sito della Microchip
Qui è possibile scaricare un esempio applicativo completo di sorgenti C e progetto MPLAB.
La periferica attende un trasferimento USB in ingresso e scrive sulla porta B (PORTB<7:2>) il primo byte del pacchetto ricevuto;
RB1 e RB0 indicano il tipo di trasferimento usato:
01 =
output report via
interrupt,
10 =
feature report,
11 =
output report via
control endpoint.
Viene quindi generata una risposta che è composta da:
stato della porta A (1 byte);
tipo di trasferimento, 0xF0 se
input via interrupt, 0xF1 se
feature, 0xF2 se
input via control EP;
un contatore a 2 byte incrementato ogni 10.93 ms;
i rimanenti byte ricevuti l'ultima volta.
Il file usb.c contiene tra le altre cose le strutture dati che identificano la periferica, di cui la prima ad essere letta è
il
device descriptor:
rom byte deviceDescriptor[] =
{
0x12, 0x01, // bLength, bDescriptorType
0x00, 0x02, // bcdUSB (low byte), bcdUSB (high byte)
0x00, 0x00, // bDeviceClass, bDeviceSubClass
0x00, E0SZ, // bDeviceProtocl, bMaxPacketSize
0xD8, 0x04, // idVendor (low byte), idVendor (high byte)
0xFF, 0x01, // idProduct (low byte), idProduct (high byte)
0x01, 0x00, // bcdDevice (low byte), bcdDevice (high byte)
0x01, 0x02, // iManufacturer, iProduct
0x00, 0x01 // iSerialNumber (none), bNumConfigurations
};
Vid e
Pid devono di norma essere assegnati dal consorzio USB dietro pagamento, ma ovviamente per applicazioni non commerciali
si può scegliere un numero a caso, ad es. 4D8 1FF; 0x4D8 è l'identificativo Microchip.
Il
report descriptor specifica in questo caso pacchetti di ingresso e uscita di 64 byte con applicazione riservata,
vale a dire che il sistema operativo non si occuperà di decodificarli, come invece avviene per i dispositivi di
ingresso quali tastiere e mouse; il
feature report è di 64 byte:
rom byte HIDReport[HID_REPORT_SIZE] = {
0x06, 0xa0, 0xff, // USAGE_PAGE (Vendor Defined Page 1)
0x09, 0x01, // USAGE (Vendor Usage 1)
0xa1, 0x01, // COLLECTION (Application)
// The Input report
0x09, 0x03, // Usage ID - vendor defined
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8 bits)
0x95, 0x40, // Report Count (64 fields)
0x81, 0x02, // Input (Data, Variable, Absolute)
// The Output report
0x09, 0x04, // Usage ID - vendor defined
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8 bits)
0x95, 0x40, // Report Count (64 fields)
0x91, 0x02, // Output (Data, Variable, Absolute)
// The Feature report
0x09, 0x01, // Usage ID - vendor defined
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8 bits)
0x95, 0x40, // Report Count (64 fields)
0xB1, 0x02, // Feature (Data, Variable, Absolute)
0xc0 // END_COLLECTION
};
La differenza fra
input/output report e
feature report è che i primi transitano generalmente dall'endpoint 1 in modalità
interrupt (a 64KB/s max), il secondo obbligatoriamente dall'endpoint 0 in modalità
control, e deve condividere la
banda passante con i messaggi standard USB; in questo caso, diversamente dai trasferimenti
interrupt, il sistema
operativo non garantisce una precisa latenza, ma ottimizza il flusso dei dati secondo il carico sul bus; il
trasferimento dati avviene a pezzi secondo la dimensione bMaxPacketSize0, specificata nel
device descriptor;
in usb.h questa è definita come E0SZ.
Bisogna ricordare che il
feature report è stato pensato per comunicare sporadicamente opzioni di configurazione o
attributi che modificano la funzionalità dei dispositivi (ad esempio lo stato dei led di una tastiera), lasciando agli
input/output report il compito di trasportare i dati veri e propri; però nulla vieta di farci transitare altri dati,
se la mancanza di una latenza garantita non è rilevante ai fini dell'applicazione.
Nel caso non si volesse usare alcun
feature report si può eliminare la sezione corrispondente dal
report descriptor;
a questo punto non sono più necessari nè
HIDFeatureBuffer[HID_FEATURE_REPORT_BYTES] nè le funzioni
SetupFeatureReport(byte reportID),
SetFeatureReport(byte reportID), e
GetFeatureReport(byte reportID);
fatto ciò bisogna eliminare anche le chiamate a queste funzioni,
presenti in
ProcessControlTransfer(void) e
ProcessHIDRequest(void).
Un secondo modo di accedere agli
input/output report è tramite le richieste HID GET_REPORT e SET_REPORT dirette
all'endpoint 0 (modalità
control); il trasferimento è simile a quello del
feature report, e anche qui valgono le stesse considerazioni
sulla banda passante fatte prima.
Nella documentazione dei sistemi operativi è sempre consigliato di servirsi del trasferimento
interrupt
per comunicazioni continuative.
Se non si ha intenzione di usare GET_REPORT e SET_REPORT si possono cancellare le funzioni
SetupOutputReport(byte reportID),
SetOutputReport(byte reportID), e
GetInputReport(byte reportID),
ed eliminare anche le chiamate in
ProcessControlTransfer(void) e
ProcessHIDRequest(void).
Ritornando alla configurazione del firmware, in usb.c sono dichiarati anche i descrittori stringa: vengono letti dal sistema
e danno una descrizione testuale della periferica e del produttore; la codifica è unicode a 2 byte, ad esempio:
rom byte stringDescriptor1[] =
{
18, STRING_DESCRIPTOR, // bLength, bDscType
'S',0,'t',0,'r',0,'i',0,'n',0,'g',0,' ',0,'1',0,
};
In usb.h sono specificate nuovamente le dimensioni dei pacchetti dati (input, output, e feature report); queste devono essere
concordi con quanto scritto nel
report descriptor:
#define HID_INPUT_REPORT_BYTES 64
#define HID_OUTPUT_REPORT_BYTES 64
#define HID_FEATURE_REPORT_BYTES 64
La gestione della comunicazione USB è implementata senza utilizzare alcun interrupt; questo è possibile perchè
le specifiche sul tempo di risposta sono molto rilassate (una volta finita l'enumerazione interessa solo il limite
di 5 secondi per i dati) e la periferica interna si occupa automaticamente di generare dei pacchetti NAck
in assenza di dati validi.
Il vantaggio è che le funzioni utente possono prendere pieno controllo del microcontrollore.
Il ciclo principale richiama in sequenza la routine di gestione USB e la funzione utente
ProcessIO():
while(1){
EnableUSBModule();
if(UCFGbits.UTEYE != 1) ProcessUSBTransactions();
ProcessIO();
}
Evidentemente
ProcessUSBTransactions() si occupa della gestione del protocollo, ossia di quello che arriva all'endpoint 0.
La cosa che interessa veramente è come scambiare i dati; nel caso di trasferimento
interrupt i pacchetti
specificati nel
report descriptor vengono ricevuti sull'endpoint 1 out
(la direzione viene sempre presa in riferimento al pc), e trasmessi sull'ep1in.
Per ricevere bisogna chiamare la funzione:
number_of_bytes_read = HIDRxReport(receive_buffer, HID_OUTPUT_REPORT_BYTES);
Questa copia i dati dal buffer ep1out (
HIDRxBuffer, che risiede nella RAM USB) a
receive_buffer e
indica quanti byte sono stati ricevuti; la lettura abilita la periferica USB a ricevere un nuovo pacchetto.
In maniera simile opera la funzione di trasmissione:
number_of_bytes_written = HIDTxReport(transmit_buffer, HID_INPUT_REPORT_BYTES);
Nel caso di trasferimento via
control EP (richieste GET_REPORT e SET_REPORT) bisogna inserire il
codice utente nelle funzioni
SetOutputReport(byte reportID) e
GetInputReport(byte reportID).
Quindi col trasferimento
interrupt è il programma utente a dover controllare se è stato ricevuto qualcosa;
invece col trasferimento
control la routine di gestione protocollo chiama le funzioni utente non appena riceve
una richiesta di scrittura o lettura.
Per quanto riguarda il
feature report la modalità è simile a quella
control, cambiano solo
le funzioni in gioco:
SetFeatureReport(byte reportID) e
GetFeatureReport(byte reportID).
Compilare
Nel progetto è selezionato il compilatore MCC18, ma molto probabilmente il percorso degli eseguibili non corrisponde;
alla prima compilazione MPLAB chiede se si vuole usare il percorso salvato o quello reale; scegliere il secondo.
E` necessario anche correggere il percorso delle librerie:
dalle opzioni di progetto, sezione
directories premere
suite defaults o impostare manualmente i precorsi.
Se si vuole cambiare dispositivo (quello selezionato è il 18F2550), si può fare da Configure->Select Device.
Bisognerà anche cambiare i bit di configurazione in accordo con quelli esistenti e cambiare lo script del linker;
i 18F2450 e 4450 hanno meno memoria quindi lo stack deve essere ridotto (40 byte vanno comunque benissimo).
Il quarzo deve avere una frequenza multipla di 4 MHz, e bisogna impostare il divisore di ingresso di conseguenza;
attualmente è previsto un quarzo a 12 MHz.
Simulare
Il simulatore MPSIM non supporta per ora la periferica USB, e in ogni caso sarebbe inutile simulare ogni volta
tutta l'enumerazione; quello che è importante è controllare il codice che utilizza i dati.
Si può fare come segue:
aggiungere un breakpoint sulla linea
if((deviceState < CONFIGURED)||(UCONbits.SUSPND==1)) return;
mandare in esecuzione (Run, F9, o pulsante sulla barra);
dalla finestra
File registers o
Watch cambiare il valore della variabile
deviceState scrivendo 5
(che corrisponde allo stato CONFIGURED).
In questo modo abbiamo saltato l'enumerazione; ora bisogna fermarsi dopo la funzione
HIDRxReport
e cambiare il numero di byte ricevuti (variabile
number_of_bytes_read).
I dati veri e propri vanno scritti sul buffer
rxBuffer.
Da ora in poi procedere come per un normale programma.
Per simulare la trasmissione basta semplicemente cambiare la variabile
number_of_bytes_written,
se utilizzata.
Comunicare col PC
Il codice per comunicare varia in base al sistema operativo; in generale è possibile servirsi di apposite
librerie che si occupano di tutto, dalla rilevazione della periferica alla comunicazione;
io preferisco eliminare totalmente la dipendenza da altri
pacchetti e accedere usando il solo sistema operativo.
Windows
L'esempio in C (compilato con
DevC++) produce un'applicazione
da linea di comando che spedisce e riceve un pacchetto dati di 64 byte (configurabile) usando vari metodi.
Per fare a meno del DDK (driver development kit) si usa la tecnica del link esplicito alle librerie di sistema
hid.dll e setupapi.dll, caricando poi a mano le funzioni utilizzate.
Il programma fa una scansione dei dispositivi HID fino a trovarne uno con vid&pid giusto.
Ci sono 3 metodi per scambiare dati: tramite
input/output report con modalità
interrupt,
control, o tramite
feature report.
Il firmware della periferica deve ovviamente supportare la modalità scelta.
Il metodo consigliato dalla Microsoft per trasferimenti continui è quello
interrupt, che poi
usa le stesse funzioni usate per i file:
Result=WriteFile(WriteHandle,bufferU,DIMBUF,&BytesWritten,NULL);
per scrivere, e
Result = ReadFile(ReadHandle,bufferI,DIMBUF,&NumberOfBytesRead,(LPOVERLAPPED) &HIDOverlapped);
Result = WaitForSingleObject(hEventObject,10);
ResetEvent(hEventObject);
per leggere.
La lettura è asincrona, vale a dire che ReadFile ritorna subito; bisogna quindi usare
WaitForSingleObject per
attendere l'esecuzione dell'operazione per un certo lasso di tempo.
La comunicazione con modalità
control (ossia tramite SET_REPORT e GET_REPORT) avviene usando le funzioni:
HidD_SetOutputReport(DeviceHandle,bufferOut,n)
per scrivere, e
HidD_GetOutputReport(DeviceHandle,bufferIn,n)
per leggere.
In maniera simile si accede al
feature report:
HidD_SetFeature(DeviceHandle,bufferOut,n)
per scrivere, e
HidD_GetFeature(DeviceHandle,bufferIn,n)
per leggere.
In tutti i casi il primo byte trasferito indica il
report ID (di solito 0); seguono i dati da trasferire,
quindi la dimensione totale aumenta di uno (qui 65 byte).
Richiedere meno dati di quanto scritto nel
report descriptor provoca un errore di trasferimento, invece
se si specifica un numero maggiore il resto sarà riempito con zeri.
Per i dettagli consiglio di esaminare il codice.
Opzioni possibili per HidCom:
-h, --help guida
-c, --control usa control endpoint
-d, --delay ritardo tra scrittura e lettura (ms)
-f, --feature usa feature report
-i, --info visualizza informazioni
-I, --increment incrementa byte 6 a ogni trasferimento
-p, --pid Product ID [0x1FF]
-q, --quiet mostra solo risposte
-r, --repeat ripeti N volte
-s, --size dimensione report
-v, --vid Vendor ID [0x4D8]
Un esempio di utilizzo:
HidCom -i -r 5 -d 16 1 2 3 4 5 6 7 8 9 a b c d e f 0
Linux
Tra i tanti possibili modi di comunicare io uso quello forse più a basso livello: vado infatti a interagire con hiddev,
che sarebbe il driver nativo per dispositivi HID; questo evita l'utilizzo di librerie esterne.
Ogni volta che si attacca una periferica HID il sistema crea un device /dev/usb/hiddevX dove X è un numero progressivo.
Per poter avviare la comunicazione servono i diritti di lettura su questo device, per cui bisogna scrivere:
>sudo chmod a+r /dev/usb/hiddev0
Per abilitare permanentemente un utente alla lettura si può procedere come segue
(su Ubuntu e altre distribuzioni basate su Debian, verificare per le altre):
da root creare un file /etc/udev/rules.d/99-hiddev.rules
nel caso si voglia abilitare un gruppo di utenti scrivervi:
KERNEL=="hiddev[0-9]", SUBSYSTEM=="usb", SYSFS{idProduct}=="01ff", SYSFS{idVendor}=="04d8", GROUP="<gruppo>"
in cui <gruppo> è uno dei gruppi a cui appartiene l'utente (per un elenco digitare "groups");
utilizzare un gruppo adeguato e se necessario aggiungere l'utente al gruppo scelto ("addgroup <utente> <gruppo>").
Oppure, nel caso si vogliano abilitare tutti gli utenti, cambiare solo i permessi di lettura:
KERNEL=="hiddev[0-9]", SUBSYSTEM=="usb", SYSFS{idProduct}=="01ff", SYSFS{idVendor}=="04d8", MODE="0664"
riavviare udev per applicare le modifiche:
>/etc/init.d/udev reload
I valori di pid e vid sono quelli dell'esempio di prima, ovviamente vanno cambiati nel caso siano differenti.
Il
programma comunica tramite chiamate
ioctl,
passando delle srutture dati che specificano il tipo di trasferimento.
struct hiddev_report_info rep_info_i,rep_info_u;
struct hiddev_usage_ref_multi ref_multi_i,ref_multi_u;
Hiddev è stato pensato per i dispositivi di input/output classici, e quindi ha molte funzioni per controllare i singoli
usage specificati nel
report descriptor; qui però serve un modo per trasferire tutto in un'unica funzione;
questo è possibile tramite HIDIOCSUSAGES e HIDIOCGUSAGES; i dati stessi stanno in
ref_multi_u.values e
ref_multi_i.values.
Trasferimento
interrupt:
ioctl(fd,HIDIOCSUSAGES, &ref_multi_u); //Scrive ref_multi_u.values
ioctl(fd,HIDIOCSREPORT, &rep_info_u);
...
ioctl(fd,HIDIOCGUSAGES, &ref_multi_i); //Legge in ref_multi_i.values
ioctl(fd,HIDIOCGREPORT, &rep_info_i);
Trasferimento
control:
ioctl(fd,HIDIOCSUSAGES, &ref_multi_u); //Scrive ref_multi_u.values
ioctl(fd,HIDIOCSREPORT, &rep_info_u);
...
ioctl(fd,HIDIOCGREPORT, &rep_info_i);
ioctl(fd,HIDIOCGUSAGES, &ref_multi_i); //Legge in ref_multi_i.values
In realtà solo la lettura avviene in modalità
control; fino ad ora non sono riuscito a trovare un modo per
forzare una scrittura
control.
D'altronde non c'è motivo di usare questa modalità quando esiste quella
interrupt, e un dispositivo senza
interrupt endpoint non viene neanche assegnato a
hiddev.
Trasferimento tramite
feature report (dopo aver specificato HID_REPORT_TYPE_FEATURE nelle strutture dati):
ioctl(fd,HIDIOCSUSAGES, &ref_multi_u); //Scrive ref_multi_u.values
ioctl(fd,HIDIOCSREPORT, &rep_info_u);
...
ioctl(fd,HIDIOCGREPORT, &rep_info_i);
ioctl(fd,HIDIOCGUSAGES, &ref_multi_i); //Legge in ref_multi_i.values
per i dettagli consiglio di esaminare il codice.
Opzioni possibili per HidCom:
-h, --help guida
-c, --control usa control endpoint
-d, --delay ritardo tra scrittura e lettura (ms)
-f, --feature usa feature report
-i, --info visualizza informazioni
-I, --increment incrementa byte 6 a ogni trasferimento
--path device path [/dev/usb/hiddev0]
-p, --pid Product ID [0x1FF]
-q, --quiet mostra solo risposte
-r, --repeat ripeti N volte
-s, --size dimensione report
-v, --vid Vendor ID [0x4D8]
Un esempio di utilizzo:
HidCom -i -r 5 -I 16 1 2 3 4 5 6 7 8 9 a b c d e f 0
Scarica
Firmware:
progetto MPLAB completo
o
firmware compilato per 18F2550(.hex)
HidCom per Linux
HidCom per Windows
Risorse in rete
DevC++
Standard USB 2.0
HID page on USB.org
USB Central
USB & PIC
Microchip
Documentazione su hiddev
licenze GNU/GPL
Contatti
Per informazioni o commenti:
Alberto Maccioni