ORbit16™ – Leggere un mouse USB
Ho pubblicato un programma di esempio, completo di sorgente in MPLAB C30 C Compiler, che “legge” un mouse USB collegato sulla porta USB HOST di ORbit16™. Progetto MPLAB completo di sorgenti ed eseguibile precompilato è presente nella pagina apposita sul sito dedicato ad ORbit16™.
L’esempio in oggetto l’ho derivato dall’esempio della MAL (revisione 18/10/2011) chiamato “Device – HID – Mouse”. L’esempio originale della Microchip rileva il movimento lungo gli assi X e Y e i pulsanti destro e sinistro, mostrando i valori letti dal report USB sia su UART che su un display classico da 16×2 caratteri.
Dal momento che leggere un valore da un report USB non fornisce proprio un’idea precisa del fatto che il mouse si sta muovendo, anche perchè i dati letti non sono di interpretazione immediata, ho pensato di fare una cosa più carina: muovendo il mouse si muove una freccia all’interno di un display tipo Nokia 5110/3310 (quelli basati sul controller philips PCD8544).
Oltre alla lettura del movimento e dei 2 pulsanti, caratteristiche presenti “di serie”, ho aggiunto la rilevazione del pulsante centrale e del movimento della rotella. Sul display ho lasciato l’ultima riga “libera” nel senso che il puntatore non può muoversi all’interno di questa riga, utilizzata quindi come una sorta di barra di stato: nella zona sinistra di tale barra viene visualizzata una lettera L quando viene premuto il pulsante sinistro, una lettera R per il pulsante destro e una lettera C per il pulsante centrale, le lettere scompaiono quando il pulsante viene rilasciato (invece l’esempio microchip dava un’idea sbagliata: sembrava che i pulsanti di un mouse USB funzionassero in ON/OFF, che non fosse realmente così si capiva soltanto spulciando il codice, ma in maniera molto attenta).
Ruotando la rotella del mouse viene infine incrementato/decrementato un counter, indicato in basso a destra con la dicitura: SCRL. Il counter si trova in partenza al valore 100, ruotando la rotella verso l’alto il counter aumenta di unità ad ogni scatto fino a 200 e ruotandola verso il basso diminuisce fino a zero. Penso che l’esempio come l’ho concepito fornisce sicuramente un’idea immediata ed è anche utilizzabile da subito per le proprie applicazioni con poche modifiche.
In questo video è illustrato il funzionamento della demo:
Indice dei contenuti
Limitazioni
Questo esempio, come tutti gli esempi della Microchip in cui si fa uso del modulo USB come HOST, non funziona con i dispositivi USB compositi (composite) e con quelli composti (compound). Cosa siano questi due tipi di dispositivi USB lo spiega la stessa microchip nelle sue FAQ. Un dispositivo composto ha un HUB al suo interno, un dispositivo composito utilizza più di un’interfaccia su una singola periferica.
Nonostante ciò, ho provato personalmente oltre 30 mouse USB con questo esempio, e due mouse non hanno funzionato nonostante non siano dispositivi nè compositi nè composti e non arrivino ad assorbire nemmeno 100mA (il massimo è 500mA). Tali mouse non funzionano con questo esempio e non restituiscono nemmeno nessun tipo di errore. I due mouse che non mi funzionano sono entrambi della Logitech (modelli M100 e BZ96C). Attualmente ignoro il motivo, se qualcuno ha già affrontato tale problema o ha suggerimenti, può lasciare informazioni nei commenti.
Tutto questo per dirvi che c’è una certa percentuale di mouse che potrebbe non funzionare con questo esempio.
Funzionamento del firmware
A parte tutto il background relativo al funzionamento dell’USB (per il quale ho messo qui alcuni link utili), un documento utile è senz’altro l’AN1141 della Microchip: USB EMbedded Host Stack Programmer’s Guide, che serve a capire in che modo il framework USB, relativamente alla parte HOST, è stato strutturato e come va modificato in base alle proprie esigenze.
Bisogna innanzitutto lodare la Microchip che con la MAL ha fatto senz’altro una grande opera, semplificando di molto lo sviluppo delle applicazioni, soprattutto quelle relative all’utilizzo dell’USB che, diciamoci la verità: facile non è, è anzi una delle cose più complicate con le quali abbia mai lavorato. Nella MAL abbiamo quindi questo framework costituito da tutta una serie di files sparsi un po’ ovunque nelle varie cartelle, che permette di implementare la funzionalità USB su tutta una serie di MCU: in base al dispositivo target selezionato vari #if e direttive fanno in modo di caricare soltanto le parti di codice previste per il nostro picmicro con tutti gli aggiustamenti necessari ai vari compilatori (MPLAB C18, C30, C32). Per cui tutti gli esempi facenti uso di una determinata categoria di dispositivo USB (es.: USB HOST, non disponibile per i PIC18) fanno tutti uso degli stessi files a meno del file principale e di alcuni files di configurazione. Questo è il motivo per il quale tutti i vari files sono “sparsi” in cartelle ordinate: per evitare di ricopiarli tutti ogni volta per ogni esempio dato che sono sempre gli stessi. Tale scelta fa anche in modo che eventuali aggiornamenti della MAL siano più facilmente implementabili sostituendo unicamente i files sorgente aggiornati.
Per motivi di studio, ad ogni modo è sicuramente più comodo avere tutti i files a portata di mano, per tale motivo oltre a tutte le modifiche su elencate ho anche modificato i files di progetto per poter studiare meglio il funzionamento del modulo USB. Leggere un buon libro è sicuramente un ottimo punto di partenza, ma senza esempi pratici sottomano, da poter modificare per capire meglio, non serve a nulla. ORbit16™, ma soprattutto la serie di esempi già pronti che metto a disposizione, è nata appunto per questo.
Quando un dispositivo USB agisce come HOST sappiamo che questi deve accettare un device USB, riconoscerlo (enumerarlo) e inviargli i giusti comandi e quindi interpretare le risposte in maniera corretta. Su un PC questo processo è abbastanza semplice: abbiamo tutta una serie di drivers e il dispositivo collegato viene subito riconosciuto dato che il device gli invia le sue informazioni (ciao host, io sono un mouse! sono una tastiera! ecc) e gli viene quindi affibbiato il driver corretto che fa in modo di far comunicare il dispositivo con il sistema operativo.
Su una MCU la cosa non è così immediata anche dato il quantitativo di memoria ridotto che non ci consente certo di implementare i driver per tutti i dispositivi USB. Per tale motivo si fa in modo che la MCU sia in grado di interfacciarsi ad un solo tipo dispositivo o comunque ad un numero ristretto e ragionevole (sei un mouse? No! Allora niente, non possiamo comunicare. Si? Ok, andiamo avanti).
Come si fa a capire che il dispositivo collegato è un mouse USB piuttosto che una tastiera o un altro dispositivo? Quando il device si collega all’host vengono trasferiti tutta una serie di dati, tra cui il Device Descriptor.
Il Device Descriptor contiene VID Vendor ID – assegnato dall’ USB Implementation Forum – e PID Product ID. Nel device descriptor sono anche contenuti il numero di revisione hardware e altre informazioni. In questo descriptor abbiamo inoltre le definizioni di Classe, Sottoclasse e Protocollo che aiutano ad identificare il tipo di dispositivo.
La possibilità, per un dispositivo, di appartenere ad una specifica classe/sottoclasse/protocollo gli permette di funzionare senza la necessità di utilizzare drivers specifici dato che utilizza, per la comunicazione, set di istruzioni predefiniti.
Molti dispositivi, tra i quali anche i mouse, specificano invece Classe, Sottoclasse Protocollo nell’Interface Descriptor invece che nel Device Descriptor.
Nella MAL la Microchip fa uso di una tabella chiamata TPL (Targeted Peripheral List) nella quale andiamo a definire i dispositivi con i quali possiamo interfacciarci nella nostra applicazione. I dispositivi vengono specificati in tale tabella o mediante l’accoppiata VID/PID o mediante le definizioni di Classe, Sottoclasse e Protocollo.
Tale tabella è contenuta nel file usb_config.c (uno di quei files “non fissi” del framework, presente, per ogni progetto, nella stessa cartella in cui è presente il programma principale). Relativamente all’esempio del mouse tale tabella è la seguente:
USB_TPL usbTPL[] = { { INIT_CL_SC_P( 3ul, 1ul, 2ul ), 0, 0, {TPL_CLASS_DRV} } // (null) }; |
Il nome della funzione contenuta nella struttura è palese: INIT CLass SubClass Protocol. In questo esempio diciamo al framework di interfacciarsi unicamente con i dispositivi appartenenti alla Classe 3 (Dispositivi HID – Human Interface Device) , Sottoclasse 1 e Protocollo 2 : I mouse in pratica. I numeri sono seguiti da ul che sta per unsigned long.
La sottoclasse 1 (solo per i dispositivi HID) indica che il tipo di dispositivo può anche essere riconosciuto in fase di boot, ovvero dal firmware della bios o, in altre parole, può comunicare con l’host anche se questo non ha caricato i driver: questo è necessario soprattutto per la tastiera che, a differenza del mouse, è un componente che deve essere per forza presente all’avvio. Se la sottoclasse fosse stata zero, voleva dire che il dispositivo non può essere riconosciuto durante il boot, ma nessuna tastiera o mouse ha la sottoclasse zero, questo anche perchè tastiera e mouse devono funzionare sempre anche quando, ad esempio, windows viene avviato in modalità provvisoria – modalità durante la quale non vengono caricati i drivers. Il Protocollo infine 2 identifica nello specifico un mouse (il protocollo 1 identifica una tastiera). Questi 3 parametri sono strettamente associati tra loro: protocollo 2 identifica un mouse solo nel caso in cui Classe=3 e sottoclasse=1.
Il mouse, una volta collegato, tramite il suo interface descriptor dirà al PIC di essere un dispositivo HID (Classe 3), appartenente alla sottoclasse 1, protocollo 2. Il firmware esegue il confronto di tali dati, ricevuti dal device, con quelli contenuti nella TPL e capisce di potersi interfacciare con esso, in caso contrario il device viene “snobbato” e dal firmware viene restituito un errore.
Se ad esempio si prova a collegare un dispositivo diverso da un mouse, viene restituito l’errore “Cannot enumerate the device”
Il mouse, una volta enumerato, ha una modalità di trasferimento dati ad interrupt (che è una delle 4 modalità di trasferimento previste dall’USB): ad ogni cambio di stato (pressione/rilascio pulsante, movimento ecc) l’ HOST riceve i dati aggiornati (tutti: anche se è stato solo premuto un pulsante, riceve comunque anche i dati degli assi e di tutto il resto).
Quello che io sto chiamando stato, che altro non è che una serie di bytes da interpretare, è contenuto in quello che invece si chiama HID Report Descriptor.
L’HID Report Descriptor è ovviamente fornito soltanto dai dispositivi aventi la classe interfaccia=3. Il formato dei dati inviati dai dispositivi HID è contenuto in un documento chiamato HID Usage Tables.
Il report descriptor, nel firmware sul PIC, viene letto dalla funzione USB_HID_DataCollectionHandler, presente nel programma principale (main.c). Tale funzione viene richiamata tramite un alias definito in config.h:
#define APPL_COLLECT_PARSED_DATA USB_HID_DataCollectionHandler |
L’USB genera un’interruzione e tali richieste sono gestite nel programma principale dalla funzione USB_ApplicationEventHandler. Anche questa funzione viene richiamata, in più punti del framework, tramite un alias definito anch’esso in config.h:
#define USB_HOST_APP_EVENT_HANDLER USB_ApplicationEventHandler |
In tale funzione, al momento opportuno (EVENT_HID_RPT_DESC_PARSED : che avvisa che il report descriptor è stato ricevuto), viene richiamata la macro APPL_COLLECT_PARSED_DATA (che non è altro che un richiamo alla funzione USB_HID_DataCollectionHandler).
Tale funzione legge il report e ne estrae i dati “interessanti” in maniera più facile da interpretare. Nel report descriptor sono contenuti vari “oggetti” raggruppati opportunamente. Alcuni di questi oggetti si chiamano Usage Page. Le usage page forniscono una descrizione dei dati contenuti nel report. Un mouse utilizza le usage page 0x01 (Generic Desktop) e 0x09 (Buttons).
Anche tastiere e joystick utilizzano la usage page 0x01. La usage page 0x09 viene utilizzata da sistemi atti ad eseguire una selezione, muovere e manipolare l’ambiente. In questa pagina viene definito un bottone primario, usato per selezionare-trascinare-attivare su doppio click, corrispondente al pulsante sinistro del mouse sui sistemi windows e all’unico pulsante su MacOs, un pulsante secondario per sfogliare le proprietà degli oggetti e quindi un pulsante terziario e così via. Man mano che aumenta il numero di pulsanti, decresce la loro “importanza”. In ogni caso per i Mouse vengono sempre definiti massimo 3 pulsanti. Se un mouse ha più di 3 pulsanti allora il produttore deve fornire i drivers per poter usare i pulsanti aggiuntivi.
I valori numerici delle usage page sfruttate dal mouse sono definiti per comodità come costanti nel programma principale:
#define USAGE_PAGE_BUTTONS (0x09) #define USAGE_PAGE_GEN_DESKTOP (0x01) |
Il DataCollectionHandler riempie quindi due strutture che sono Appl_Mouse_Buttons_Details (che conterrà tutte le informazioni necessarie ad estrarre dal report descriptor i dati definiti dalla usage page 0x09 – buttons) e Appl_XY_Axis_Details (relativa alla usage page 0x01 – generic desktop). Il tipo di tali strutture è definito in definito in usb_host_hid.h:
typedef struct _HID_DATA_DETAILS { WORD reportLength; // reportLength - the expected length of the parent report. WORD reportID; // reportID - report ID - the first byte of the parent report. BYTE bitOffset; // BitOffset - bit offset within the report. BYTE bitLength; // bitlength - length of the data in bits. BYTE count; // count - what's left of the message after this data. BYTE signExtend; // extend - sign extend the data. BYTE interfaceNum; // interfaceNum - informs HID layer about interface number. } HID_DATA_DETAILS; |
Tali informazioni in pratica servono a capire nel report descriptor in quale punto reperire le informazioni di nostro interesse. I dati interessanti vengono infine estratti dal report tramite la funzione USBHostHID_ApiImportData definita sempre in usb_host_hid.c:
BOOL USBHostHID_ApiImportData(BYTE *report, WORD reportLength, HID_USER_DATA_SIZE *buffer,HID_DATA_DETAILS *pDataDetails) |
in cui: *report è il report descriptor grezzo fornito dal device, reportLength è la sua lunghezza, *buffer è l’array in cui “travasare” i dati che ci interessano (il tipo HID_USER_DATA_SIZE è un semplice intero a 8, 16 o 32 bit a seconda del device) e infine *pDataDetails è appunto la struttura di cui parlavamo prima in cui si trovano le informazioni necessarie per poter estrarre i dati relativi alle usage page dei dispositivi dal report.
I dati dei pulsanti vengono quindi trasferiti in Appl_Button_report_buffer[3]; e quelli degli assi in Appl_XY_report_buffer[3] all’interno della funzione App_ProcessInputReport:
USBHostHID_ApiImportData(Appl_raw_report_buffer.ReportData, Appl_raw_report_buffer.ReportSize,Appl_Button_report_buffer, &Appl_Mouse_Buttons_Details); USBHostHID_ApiImportData(Appl_raw_report_buffer.ReportData, Appl_raw_report_buffer.ReportSize,Appl_XYW_report_buffer, &Appl_XYW_Axis_Details); |
L’indice zero di Appl_Button_report_buffer contiene lo stato del pulsante sinistro, l’indice 1 del pulsante destro e l’indice 2 del pulsante centrale (la pressione della rotella). L’indice zero di Appl_XY_report_buffer contiene il movimento lungo l’asse X, l’indice 1 quello lungo l’asse Y e infine l’indice 2 contiene lo spostamento della rotella (scroll wheel).
Formato dei dati
Relativamente agli assi, il dato letto dal report è relativo: il valore restituito illustra una variazione di posizione rispetto alla posizione precedente, quindi non abbiamo valori di X e Y ma piuttosto di ΔX e ΔY, il dato letto dal report è quindi di tipo signed.
Anche questo, nell’esempio originale della Microchip, crea molta confusione: il dato lungo gli assi viene difatti sempre mostrato come unsigned, non facendo capire subito all’utente alle prime armi in quale direzione il mouse si muove. Nell’esempio rielaborato da me, il movimento del mouse è palese!
Il valore riportato è espresso in unità adimensionali chiamate mickeys.
Mickeys? Movimento del mouse espresso in Mickeys? Ha qualcosa a che fare con Mickey Mouse?
Un valore maggiore di zero su X indica che il mouse (il puntatore) si è spostato verso destra, un valore negativo indica spostamento verso sinistra. Il valore zero indica che non c’è stata variazione lungo l’asse X.
Un valore maggiore di zero su Y indica che il puntatore si è spostato verso il basso, di conseguenza un valore negativo indica spostamento verso il basso e valore zero = nessuno spostamento lungo Y.
Relativamente alla rotella di scroll, anche tale valore è del tipo signed: un valore maggiore di zero indica una rotazione della rotella in senso antiorario (rotella ruotata in avanti), un valore minore di zero indica una rotazione in senso orario (rotella ruotata verso se stessi).
Lo stato dei pulsanti è indicato in maniera più semplice: 1 premuto, zero non premuto.
Download
Progetto MPLAB completo di sorgenti ed HEX precompilato è disponibile a coloro che hanno acquistato una versione qualsiasi di ORbit16™ a questa pagina.
Non sai cos’è ORbit16™ ? Vedi qui