ultimo aggiornamento: 25 Gennaio 2010
English version

USB e PIC: breve guida al firmware USB HID

Guida su come usare un framework USB open source per microcontrollori PIC



Introduzione
Classe HID
Usare il firmware
Compilare
Simulare
Comunicare col PC
Linux
Windows
Scarica
Risorse in rete
Contatti

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):
circuito
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  email.png

Torna su

Pagina principale