GPIO aggiuntivi MCP23x17
E’ possibile collegare dei GPIO aggiuntivi al Raspberry Pi in modo da aumentare il numero di pin comandabili.
Ad esempio si possono collegare i chip MCP23x17:
-MCP23017 che usa il protocollo I2C
-MCP23S17 con protocollo SPI.
Questi integrati hanno due PORT con 8 pin ciascuno, una porta di comunicazione I2C o SPI secondo il modello, alimentazione, reset e 2 pin per la gestione degli interrupt.
Possono essere collegati sul bus assieme ad altri chip dello stesso tipo indirizzandoli con tre bit, quindi se ne possono collegare fino ad 8, questo permette di arrivare a comandadare fino 128 pin di input/output.
I pin non hanno funzioni particolari, possono essere solo ingressi o uscite ed è possibile settarne il pullup, ma non ci sono ad esempio funzioni di pwn o altro.
Per la configurazione bisogna settare alcuni registri sul chip per indicare la configurazione delle porte, ingressi, uscite, pullup, interrupt come riportato in questa pagina oltre che sul datasheet riportato sopra.
I due bus a cui possono essere collegati questi integrati si basano su una configurazione di tipo master-slave, perciò normalmente vengono usati in polling, questo non comporta molto dispendio di CPU però se le variazioni sui pin sono veloci si rischia di perderne.
In questo documento viene illustrato come interagire dialogando sul bus SPI ed utilizzando un programmino in c.
Non vengono utilizzate però le possibilità di utilizzo degli interrupt.
Volendo ottimizzare l’uso di risorse della CPU ci sembrava utile riuscire a sfruttare questa funzione.
In pratica si tratta di collegare uno o due pin dell’integrato ad un pin del GPIO di Raspberry Pi per scatenare la lettura del porta in input quando si verifica un evento. Questo può essere fatto praticamente settando tre registri del MCP23X17.
Le possibilità sono diverse:
- Sulle porte settate come ingressi, ogni singolo pin può generare interrupt, si può settare uno stato predefinito a cui si deve trovare il pin (questo però spesso non é l’ideale, vedi sotto).
- In alternativa é possibile settare i registri in modo che venga emesso un interrupt ad ogni variazione sui pin dichiarati.
La gestione degli interrupt é leggermenre diversa da quanto avviene in altri circuiti, infatti il registro degli interrupt viene resettato appena viene letto il registro degli interrupt oppure il port.
Questo significa che un settaggio come indicato al primo punto genera interrupt a raffica se sul pin arrivano segnali stabili nel tempo e li leggiamo subito.
Per questi test abbiamo utilizzato una scheda Pi-Face nella versione già assemblata venduta da Farnell in modo da velocizzare i test.
Qui sotto il codice creato, probabilmente non ancora definitivo: per utilizzarlo salvarlo su un file di testo (es piface.c) e compilare ad esempio con:
gcc piface.c -o piface
poi lanciarlo con
./piface /dev/spi0.0
/**************************************** * basic SPI demo for mcp23s17 * *****************************************/ #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/spi/spidev.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdint.h> #define CMD_WRITE 0x40 #define CMD_READ 0x41 static char *spiDevice = "/dev/spidev0.0" ; static uint8_t spiMode = 0 ; static uint8_t spiBPW = 8 ; static uint32_t spiSpeed = 5000000 ; static uint16_t spiDelay = 0; // MCP23S17 Registers #define IOCON 0x0A #define IODIRA 0x00 #define IPOLA 0x02 #define GPINTENA 0x04 #define DEFVALA 0x06 #define INTCONA 0x08 #define GPPUA 0x0C #define INTFA 0x0E #define INTCAPA 0x10 #define GPIOA 0x12 #define OLATA 0x14 #define IODIRB 0x01 #define IPOLB 0x03 #define GPINTENB 0x05 #define DEFVALB 0x07 #define INTCONB 0x09 #define GPPUB 0x0D #define INTFB 0x0F #define INTCAPB 0x11 #define GPIOB 0x13 #define OLATB 0x15 int spi_fd; int readconf() { } int pinsetup () { int fd = 0; fd = open("/sys/class/gpio/export",O_WRONLY); write(fd,"25\n",3); close(fd); fd = open("/sys/class/gpio/gpio25/direction",O_WRONLY); write(fd,"in\n",3); close(fd); fd = open("/sys/class/gpio/gpio25/edge",O_RDWR); write(fd,"falling\n",8); // i valori possibili sono rising/falling/both/none consigliato falling close(fd); fd = open("/sys/class/gpio/gpio25/value",O_RDWR); return fd; } static uint8_t readByte (uint8_t reg) { uint8_t tx [4] ; uint8_t rx [4] ; struct spi_ioc_transfer spi ; tx [0] = CMD_READ ; tx [1] = reg ; tx [2] = 0 ; spi.tx_buf =(unsigned long)tx ; spi.rx_buf =(unsigned long)rx ; spi.len =3; spi.delay_usecs = spiDelay ; spi.speed_hz = spiSpeed ; spi.bits_per_word = spiBPW ; ioctl (spi_fd, SPI_IOC_MESSAGE(1), &spi) ; return rx [2] ; } static void writeByte (uint8_t reg, uint8_t data) { uint8_t spiBufTx [3] ; uint8_t spiBufRx [3] ; struct spi_ioc_transfer spi ; spiBufTx [0] = CMD_WRITE ; spiBufTx [1] = reg ; spiBufTx [2] = data ; spi.tx_buf = (unsigned long)spiBufTx ; spi.rx_buf = (unsigned long)spiBufRx ; spi.len =3; spi.delay_usecs = spiDelay ; spi.speed_hz = spiSpeed ; spi.bits_per_word = spiBPW ; ioctl (spi_fd, SPI_IOC_MESSAGE(1), &spi) ; } static void ruota_led() { uint8_t leds; leds = readByte (GPIOA) ; if (leds==4 || leds==0) { writeByte(GPIOA,128); } else { writeByte(GPIOA,leds/2); } } /*spi_open * - Open the given SPI channel and configures it. * - there are normally two SPI devices on your PI: * /dev/spidev0.0: activates the CS0 pin during transfer * /dev/spidev0.1: activates the CS1 pin during transfer * */ int spi_open(char* dev) { if((spi_fd = open(dev, O_RDWR)) < 0){ printf("error opening %s\n",dev); return -1; } return 0; } int main(int argc, char* argv[]) { unsigned char data = 0xAF; struct timeval tv; int pin25; fd_set set, setbackup; char pin25buf[1024]; pin25 = pinsetup(); FD_ZERO(&set); FD_SET(pin25,&set); setbackup = set; int val; if(argc <= 1){ printf("too few args, try %s /dev/spidev0.0\n",argv[0]); return -1; } // open and configure SPI channel. (/dev/spidev0.0 for example) if(spi_open(argv[1]) < 0){ printf("spi_open failed\n"); return -1; } writeByte (IODIRA, 0x00) ; // Port A -> Outputs writeByte (GPIOA, 0x00) ; writeByte(GPPUA, 0xFF); // set port A pullups on writeByte(GPPUB, 0xFF); // set port B pullups on writeByte (IODIRB, 0xFF) ; writeByte (GPINTENB,0xFF); //Abilita l'interrupt sul portB writeByte (DEFVALB,0xFF); //Setta il valore di default dei pin del port B se un un pin non è a uno emette interrupt writeByte (INTCONB,0x00); //Setta il valore di per il confronto se a 1 compara con il valore di DEFVAL altrimenti controlla le variazioni di stato while (1) { tv.tv_sec = 30; tv.tv_usec = 0; val = select(pin25+1, NULL, NULL, &set, &tv); set = setbackup; if(val > 0) { if(FD_ISSET(pin25,&set)) { write(1,"evento su pin 25\n",17); /* antirimbalzo */ tv.tv_sec = 0; tv.tv_usec = 50000; val = select(0, NULL, NULL, NULL, &tv); /* fine antirimbalzo */ lseek(pin25,0,SEEK_SET); read(pin25,pin25buf,1024); } else write(1,"???\n",4); } else if(val < 0) write(1,"SIGNAL NON BLOCCATO\n",20); else write(1,"TIMEOUT\n",8); // Port B -> Inputs data = readByte (GPIOB) ; printf("RECEIVED: %.2X\n",data); //ruota_led(); } close(spi_fd); return 0; }
Ulteriori esempi qui: su github
La seguente è una versione di codice funzionante per MCP23017, basata sul codice soprastante, essenzialmente sono stati reimplementati solo i metodi per la scrittura/lettura sul bus i2c.
Può essere utilizzato come nel seguente esempio, per usare l'MCP23017, pilotando degli input provenienti dal PORTA ed indirizzandoli al PORTB:
ingresso attivo | uscita attiva GPIOA0 ---> GPIOB0 GPIOA1&GPIOA3 -> GPIOB1&GPIOB3
Il funzionamento è abbastanza semplice, vengono settati i registri (si veda il codice per i relativi valori):
- IODIRA: saranno tutti pin di input
- IODIRB: saranno tutti pin di output
- GPPUA: viene settata la resistenza di pullup per tutti gli input
- DEFVAL: è il registro con cui viene fatto il confronto con GPIOA per scatenare gli interrupts
- GPINTENA: abilita gli interrupt sul port A
- INTCONA: sceglie con quale modalità far scattare gli interrupts, se il bit è a 1 confrontando GPIOA con DEFVAL se a 0 solo nei cambi di stato (nel nostro caso erano tutti a 0 quindi l'interrupt scattava una volta quando veniva premuto uno dei tasti e una volta quando veniva rilasciato);
La configurazione del circuito è semplice ed è la seguente:
I pulsanti sono collegati sul primo pin a massa, mentre l'altro agli ingressi del PORTA. In output (PORTB) sono collegati i LED con le relative resistenze, che finiscono anch'esse a massa;
Sugli altri pin il collegamento è il seguente:
- A0,A1,A2,Vss: GND
- RESET,Vdd: VCC(3,3V)
- SDA,SCL: GPIO3 e GPIO5 (per la comunicazione con il rasp sul bus i2c)
- INTA: collegato alla GPIO25 del rasp (per l'invio sei segnali di interrupt)
Nota: Viene usata una bitmask di uno in xor sul PORTB a causa del fatto che con la resistenza di pullup a on sugli ingressi i bit saranno tutti a 1, quindi il cambio di stato si avrà portando il bit a 0.
/**************************************** * basic i2c demo for mcp23017 *****************************************/ #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/i2c-dev.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdint.h> // MCP23x17 Registers #define IOCON 0x0A #define IODIRA 0x00 #define IPOLA 0x02 #define GPINTENA 0x04 #define DEFVALA 0x06 #define INTCONA 0x08 #define GPPUA 0x0C #define INTFA 0x0E #define INTCAPA 0x10 #define GPIOA 0x12 #define OLATA 0x14 #define IODIRB 0x01 #define IPOLB 0x03 #define GPINTENB 0x05 #define DEFVALB 0x07 #define INTCONB 0x09 #define GPPUB 0x0D #define INTFB 0x0F #define INTCAPB 0x11 #define GPIOB 0x13 #define OLATB 0x15 //MPC23017 address #define DEVICE_ADDRESS 0x20 int i2c_fd; int pinsetup () { int fd = 0; fd = open("/sys/class/gpio/export",O_WRONLY); write(fd,"25\n",3); close(fd); fd = open("/sys/class/gpio/gpio25/direction",O_WRONLY); write(fd,"in\n",3); close(fd); fd = open("/sys/class/gpio/gpio25/edge",O_RDWR); write(fd,"falling\n",8);// i valori possibili sono rising/falling/both/none consigliato falling close(fd); fd = open("/sys/class/gpio/gpio25/value",O_RDWR); return fd; } int readByte(uint8_t regist){ uint8_t pkt_d[2]; pkt_d[0] = regist; pkt_d[1] = 0; if (write(i2c_fd, &pkt_d, 1) != 1) { perror("write error in readByte"); return -1; } if (read(i2c_fd, &pkt_d, 2) != 2) { perror("read error in readByt"); return -1; } return (int) pkt_d[0]; } int writeByte( uint8_t regist, uint8_t value){ uint8_t pkt_d[2]; pkt_d[0] = regist; pkt_d[1] = value; if (write(i2c_fd, &pkt_d, 2) != 2) { perror("write error in writeByte"); return -1; } return 1; } int i2c_open(char* dev) { if((i2c_fd = open(dev, O_RDWR)) < 0){ printf("error opening %s\n",dev); return -1; } if (ioctl(i2c_fd, I2C_SLAVE, DEVICE_ADDRESS) < 0) { perror("error setting I2C_SLAVE"); return -1; } return 0; } int main(int argc, char* argv[]) { unsigned char data = 0xAF; struct timeval tv; int pin25; fd_set set, setbackup; char pin25buf[1024]; pin25 = pinsetup(); FD_ZERO(&set); FD_SET(pin25,&set); int val; if(argc <= 1){ printf("too few args, try %s /dev/i2c-0\n",argv[0]); return -1; } // open and configure i2c channel. (/dev/i2c-0 for example) if(i2c_open(argv[1]) < 0){ printf("i2c_open failed\n"); return -1; } writeByte(GPPUA, 0xFF); // setto pullups sul PORTA writeByte(IODIRA, 0xFF); // setto PORTA come ingressi writeByte(GPINTENA,0xFF); //Abilita gli interrupts sul PORTA //writeByte(DEFVALA,0xFF); //Setta il valore di default del PORTA (usato per la configurazione alternativa) writeByte(INTCONA,0x00); //Setta il registro per far scattare interrupt alle variazioni di stato //writeByte(GPIOA,0x00); //setto a zero tutti i bit del port A, probabilmente non è mportante essendo il pullup a on writeByte(IODIRB,0x00); // setto PORTB come uscite while (1) { tv.tv_sec = 30; tv.tv_usec = 0; val = select(pin25+1, NULL, NULL, &set, &tv); if(val > 0) { if(FD_ISSET(pin25,&set)) { write(1,"evento su pin 25\n",17); /* antirimbalzo */ tv.tv_sec = 0; tv.tv_usec = 50000; val = select(0, NULL, NULL, NULL, &tv); /* fine antirimbalzo */ read(pin25,NULL,0); } else write(1,"???\n",4); } else { FD_ZERO(&set); FD_SET(pin25,&set); write(1,"TIMEOUT\n",8); } // Port A -> Inputs data = readByte (GPIOA) ; data = (data^0xff); // metto in xor con una maschera di 1 per avere il giusto valore in uscita printf("RECEIVED: %.2X\n",data); // Port B -> Output writeByte(GPIOB,data); } close(i2c_fd); return 0; }
Può essere compilato con:
gcc test_i2c.c -o test_i2c
Per lanciarlo:
./test_i2c /dev/i2c-1