Quando si affronta la progettazione di un sistema embedded, uno dei limiti è quello di non riuscire a prevedere in maniera precisa il numero di pin del micro che saranno necessari. Ecco come risolvere questo problema ricorrendo all’uso dei port expander.
Nella progettazione di sistemi embedded un vincolo importante da considerare è il numero di pin disponibili del sistema a microcontrollore scelto. Spesso il fallimento di un progetto può dipendere proprio dalla scelta errata del modello di micro, dotato di un numero di pin inferiore rispetto a quello necessario a pilotare tutte le periferiche. D’altro canto anche una pianificazione attenta e l’adozione di margini di sicurezza non sempre permettono di ottenere il risultato migliore. La soluzione più corretta in questi casi è quindi quella di selezionare il micro sulla base delle altre specifiche (come consumo di potenza, interfacce, capacità di elaborazione) e poi aumentare il numero di linee mediante dispositivi di espansione, detti: port expander (PE). I PE consentono di disporre di un numero di pin molto elevato a fronte di un occupazione di soli 2 o 3 pin del micro. Si tratta di una particolare classe di dispositivi, generalmente di tipo slave, collegati su un unico bus di comunicazione seriale. Il micro, tramite 2 interfaccia I2C o SPI, seleziona la linea desiderata ed effettua l’operazione di lettura o scrittura. La Figura 1 mostra una tipica configurazione del bus con i port expander.
L’indirizzo di ciascuno slave è 7 determinato da 7 bit, ossia 128 dispositivi (= 27); in realtà nella maggior parte dei casi il programmatore potrà selezionare solo un sotto gruppo di tali 7 bit. Ad esempio, nel caso del port expander MCP23008 della Microchip (Figura 2) solo tre bit saranno selezionabili tramite i pin A2, A1 e A0; questo consente di collegare sul bus fino a 8 moduli ( = 23).
Considerato che il chip fornisce 8 linee di ingresso/uscita, si avranno ben 64 linee complessivamente che potranno essere aggiunge al proprio progetto. Tutto questo impegnando solo 2 le due linee I2C del micro! Oltre al numero di linee del PE, che rappresenta la caratteristica fondamentale, esistono delle varianti che consentono di gestire uscite con PWM. Queste possono essere utili per modificare la luminosità dei LED o per il controllo di motori brushless DC. Un altro aspetto da valutare nella scelta di un port expander è la sua modalità di configurazione. I modelli più avanzati dispongono di una memoria non volatile che consente di salvare la configurazione dei pin (direzione, stato dell’uscita). Nella maggior parte dei casi, comunque, tale setup deve essere effettuato ogni volta all’accensione del dispositivo. Quando si programma un micro, una delle caratteristiche molto sfruttate sono i pin di interrupt, che consento di gestire eventi esterni asincroni; moti PE forniscono delle linee con tale funzione, permettendo di moltiplicare quelle già presenti di default nel micro.
LA PROGETTAZIONE CON UN I/O EXPANDER
Come utilizzarli?
Dopo aver descritto le caratteristiche dei port expander ed evidenziato i vantaggi offerti nella progettazione, si esamineranno due casi pratici. Si vedrà in particolare come interfacciarli con un microcontrollore (nel caso specifico un PIC) e come scrivere il relativo codice (sarà presentato un esempio in CCS e in MikroC).
MCP23008 con I2C
L’MCP23008 è uno dei port expander proposti da 2 Microchip, dotato di interfaccia I C. Il relativo pinout è mostrato in Figura 3.
Come si nota i pin A2, A1 e A0 consento di selezionare l’indirizzo del chip sul bus, portandoli ad “1” o a “0”. La presenza del pin INT consente di programmare un interrupt su un determinato evento di ciascuna linea. Tali eventi possono essere:
- Variazione di una qualunque linea, rispetto al valore precedente;
- Variazione di una qualunque linea, rispetto al valore di default (impostato nel registro DEFVAL).
Infine, con GPx sono indicate le porte di espansione, configurabili sia come ingressi che come uscite. In Figura 4 è mostrato lo schema a blocchi del dispositivo, in cui sono riportati anche i registri di configurazione.
Essi consento di impostare il comportamento del port expander. La lista completa dei relativi indirizzi è riportata in Tabella 1.
Per impostare la direzione delle linee di uscita si utilizza il registro IODIR, mentre per leggere o scrivere la linea è utilizzato il registro GPIO. Se si vuole, ad esempio, che tutte le porte siano configurate come output e che le prime quattro linee (GP0…GP3) siano a “0” e restanti ad “1” (GP4…GP7), allora si scriverà quanto segue:
- IODIR = 00h
- GPIO = F0h
Per il controllo del dispositivo è necessario seguire il protocollo riportato nel datasheet. Per semplicità nel Listato 1 sono riportate le funzioni a basso livello per il controllo di tale dispositivo.
1. // Byte di controllo 2. #define CTRL_BYTE_W 0b01000000 3. #define CTRL_BYTE_R 0b01000001 4. // Linee di uscita 5. #define GP7 0x07 6. #define GP6 0x06 7. #define GP5 0x05 8. #define GP4 0x04 9. #define GP3 0x03 10. #define GP2 0x02 11. #define GP1 0x01 12. #define GP0 0x00 // —————————————————————————————- // La funzione effettua la lettura di // un registro del MCP23008 // input: ind_reg -> registro da // leggere // output: valore letto // —————————————————————————————- 18. int i2c_lettura(int ind_reg){ 19. unsigned int temp_dato; 20. i2c_start(); 21. i2c_write(CTRL_BYTE_W); 22. i2c_write(ind_reg); 23. i2c_start(); 24. i2c_write(CTRL_BYTE_R); 25. temp_dato = i2c_read(0); 26. i2c_stop(); 27. return temp_dato; 28. } // —————————————————————————————- // La funzione effettua la scrittura // di un registro del MCP23008 // input: ind_reg -> registro da // scrivere // input: dato -> valore da // scrivere // —————————————————————————————- 34. void i2c_scrittura(int ind_reg, int dato){ 35. i2c_start(); 36. i2c_write(CTRL_BYTE_W); 37. i2c_write(ind_reg); 38. i2c_write(dato); 39. i2c_stop(); 40. delay_ms(50); 41. } // —————————————————————————————- // La procedura restituisce il valore // di tutti i registri // —————————————————————————————- 45. void i2c_mostra_reg(void){ 46. int i,dato; 47. for(i=0;i<0x0B;i++){ 48. dato = i2c_lettura(i); 49. printf(“\n\rRegistro %i: %u”,i,dato); 50. delay_ms(100); 51. } 52. }
Listato 1 |
È stato utilizzato il compilatore CCS versione 3.249 e il microcontrollore PIC (qualsiasi modello dotato di interfaccia I2C è perfetto). Queste funzioni sono:
- i2c_lettura(). Passando come parametro di input l’indirizzo di uno dei registri riportati in Tabella 1, restituisce il valore corrispondente.
- i2c_scrittura(). Effettua la scrittura di un registro passando come parametri di input, rispettivamente, l’indirizzo del registro ed il valore da assegnarli.
- i2c_mostra_reg(). Può essere utilizzata in fase di debug per visualizzare lo stato di tutti i registri tramite interfaccia seriale, configurata a 9600 baud.
Le righe 2 e 3 del Listato 1 riportano le define del codice che serve per eseguire l’operazione di lettura e scrittura nel caso di un solo port expander. L’indirizzo utilizzato è infatti A2=0, A1=0 e A0=0 (secondo quanto riportato nella Figura 2); qualora si volesse collegare un secondo dispositivo sul bus è necessario utilizzare un indirizzo diverso dal precedente, per esempio A2=0, A1=0 e A0=1. Sulla base delle suddette funzioni se ne possono facilmente costruire altre di alto livello, come quelle riportate nel Listato 2:
- MCP23008_output_high(). È la funzione equivalente ad output_high() del compilatore CCS. Passando come parametro di input una delle linee dell’MCP23008, la linea sarà configurata come uscita al valore logico alto (ad esempio, MCP23008_output_high(0) porta la linea GP0 al valore “1”).
- MCP23008_output_low(). È la funzione equivalente ad output_low() del compilatore CCS. Passando come parametro di input una delle linee dell’MCP23008, la linea sarà configurata come uscita al valore logico basso (ad esempio, MCP23008_output_high(6) porta la linea GP6 al valore “0”).
- MCP23008_input_state(). È la funzione corrispondente a input_state() del CCS. Passando come paramentro di ingresso uno dei pin dell’MCP23008, sarà restituito lo stato attuale del pin stesso.
// —————————————————————————————- // La funzione porta al valore alto // una delle linee di uscita // input: pin -> pin da asserire // (PIN_GPx con x = 0...7) // —————————————————————————————- 5. void MCP23008_output_high(int pin){ 6. int _iodir, _gpio; 7. int mask = 0; 8. _iodir = i2c_lettura(IODIR); 9. _gpio = i2c_lettura(GPIO); 10. // Configura il pin come uscita 11. mask = ~(1 << pin); 12. _iodir = _iodir & mask; 13. i2c_scrittura(IODIR,_iodir); 14. // Configura lo stato 1 15. mask = 1 << pin; 16. _gpio = _gpio | mask; 17. i2c_scrittura(GPIO,_gpio); 18. } // —————————————————————————————- // La funzione porta al valore basso // una delle linee di uscita // input: pin -> pin da asserire // (PIN_GPx con x = 0...7) // —————————————————————————————- 23. void MCP23008_output_low(int pin){ 24. int _iodir, _gpio; 25. int mask = 0; 26. _iodir = i2c_lettura(IODIR); 27. _gpio = i2c_lettura(GPIO); 28. // Configura il pin come uscita 29. mask = 1 << pin; 30. _iodir = _iodir | mask; 31. i2c_scrittura(IODIR,_iodir); 32. // Configura lo stato 0 33. mask = ~(1 << pin); 34. _gpio = _gpio & mask; 35. i2c_scrittura(GPIO,_gpio); 36. } // —————————————————————————————- // La funzione legge una delle linee // di ingresso // input: pin -> pin da asserire // (PIN_GPx con x = 0...7) // output: stato della linea // —————————————————————————————- 42. int MCP23008_input_state(int pin){ 43. int _iodir, _gpio; 44. int mask = 0; 45. _iodir = i2c_lettura(IODIR); 46. // Configura il pin come ingresso 47. mask = 1 << pin; 48. _iodir = _iodir | mask; 49. i2c_scrittura(IODIR,_iodir); 50. // Leggo lo stato 51. _gpio = i2c_lettura(GPIO); 52. _gpio = _gpio & mask; 53. return (_gpio >> pin); 54. }
Listato 2 |
Inserendo queste funzioni all’interno del proprio codice è possibile utilizzare le linee del PE come fossero quelle del microcontrollore. Anche se le funzioni proposte sono state scritte per un particolare tipo di micro e con uno specifico compilatore, la trattazione non perde di generalità. Il porting del codice su un altro modello e con un altro compilatore non risulta particolarmente complesso. L’importante è che si disponga del 2 l’interfaccia I2C. È anche possibile utilizzare la scheda di test fornita da Microchip per testare le funzionalità di questo chip.
MCP23S17 CON SPI
L’interfaccia I2C non è l’unica comunicazione seriale che è possibile utilizzare con i port expander. Esistono anche modelli interfacciabili con la periferica SPI. Un esempio è rappresentato dal dispositivo MCP23S17, il quale è dotato di ben 2 porte di comunicazione (PORTA e PORTB), per un totale di 16 linee aggiuntive (vedere Figura 5).
Le linee di indirizzamento sono sempre pari a 3. Questo significa che a fronte di sole 3 linee utilizzate dal micro per controllarlo, si guadagnano ben:
23 (dispositivi indirizzabili)×16 (linee/dispositivo) = 128
Una possibilità per controllare questo chip, oltre che a sviluppare dei driver analoghi a quelli visti in precedenza, è rappresentata dai compilatori MikroC, MikroBasic o MikroPascal. Essi mettono a disposizione una libreria già pronta di funzioni per pilotare l’MCP23S17. Tra le altre si ricordano:
- Expander_Init, deve essere richiamata prima di tutte le altre funzioni e serve per inizializzare il dispositivo.
- Expander_Read_PortA, legge le linee della porta A.
- Expander_Read_PortB, legge le linee della porta B.
- Expander_Write_PortA, utilizza le linee della porta A come uscita.
- Expander_Write_PortB, utilizza le linee della porta B come uscita.
- Expander_Set_DirectionPortA, imposta la direzione delle linee della porta A (ingresso o uscita).
- Expander_Set_DirectionPortB, imposta la direzione delle linee della porta B (ingresso o uscita).
Il Listato 3 presenta un semplice esempio di codice C in cui sono utilizzate le funzioni di libreria dell’MCP23S17, usando il compilatore MikroC v6 ed il PIC16F877A. La scheda può essere testata tramite l’add-on fornito da Mikroelektronika e la EasyPIC.
1. // Progetto Port Expander MCP23S17 2. // Autore: S.Giusto 3. // Compilatore: MikroC v6 4. // PIC16F877A 5. void main(){ 6. TRISB = 0; 7. TRISD = 0; 8. Spi_Init(); // inizializzazione interfaccia SPI 9. Expander_Init(0, &PORTC, 0, &PORTC, 1); // inizializzazione del port expander 10. Expander_Set_DirectionPortA(0, 0xFF); // configurazione portA come ingresso 11. Expander_Set_PullUpsPortA(0, 0xFF); // configurazione pull-up di portA 12. Expander_Set_DirectionPortB(0, 0xFF); // configurazione portB come ingresso 13. Expander_Set_PullUpsPortB(0, 0xFF); // configurazione pull-up di portB 14. while(1) { 15. PORTB = Expander_Read_PortA(0); // lettura portA e visualizzazione su portB 16. PORTD = Expander_Read_PortB(0); // lettura portB e visualizzazione su portD 17. Delay_100ms(); 18. } 19. }
Listato 3 |
CONCLUSIONI
L’utilizzo di un port expander può rivelarsi utile in varie situazioni, soprattutto in fase di progettazione quando il numero di linee non è ancora ben precisato. Esistono tre possibili modi di utilizzare un port expander, per sfruttare al massimo le sue capacità:
- Aggiungere un PE ad un progetto per poter eseguire la prototipazione in tempi rapidi, come supporto nelle fasi di test e debug con l’obiettivo di escluderlo poi dal progetto.
- Aggiungere un PE ad un progetto senza una ben precisa funzione, per avere sempre un margine di sicurezza in futuro.
- Ridurre le esigenze in termini di pin del processore utilizzando i PE laddove possibile.
Quelli presentati in quest’articolo sono solo alcuni dei modelli disponibili; ne esistono di varie tipologie, per gli usi più disparati. Il loro funzionamento, tuttavia, si discosta di poco rispetto a quello descritto in questo articolo.