Los expansores de puertos GPIO se usan de forma muy habitual en los circuitos microcontrolados, tanto con la finalidad de aumentar las entradas-salidas, que suele ser un recurso escaso en un µC, como para poder separar (1) a nivel físico los dispositivos controlados del circuito que los controla y (2) a nivel lógico su gestión en el programa.
El MCP23S17 permite controlar un puerto de 16 bits o 2 puertos de 8 bits de entrada-salida genéricos (GPIO) gestionándolos desde un microcontrolador por medio del bus SPI. Existe un integrado similar, el MCP23017, que usa comunicaciones I2C, cuyo funcionamiento es equiparable excepto por el tipo de bus que utiliza y la velocidad de transmisión de datos que implica: mientras que el MCP23017 (I2C) puede funcionar a 100 KHz, 400 KHz o 1,7 MHz, el MCP23S17 (SPI) del que trata este texto funciona hasta a 10 MHz.
Para avisar al microcontrolador de un cambio de estado en los puertos, el MCP23S17 dispone de dos interrupciones INTA para el puerto de 8 bits A e INTB para el puerto de 8 bits B. Estas interrupciones son configurables en señal (activa en nivel alto o bajo), en activación asociada (como una operación OR sobre ambas interrupciones) o independiente y en modo: activación por cualquier cambio o cuando sea diferente de un valor preconfigurado (el del registro DEFVAL).
El MCP23S17 puede funcionar según dos tipos de acceso. El modo byte, que no incrementa la dirección al terminar la operación, por lo que sirve para realizar accesos sucesivos a la misma dirección, y el modo secuencial, que incrementa la dirección automáticamente, por lo que sirve para alternar las direcciones automáticamente después de cada acceso.
La implementación hardware del MCP23S17 es muy sencilla, los únicos componentes pasivos que necesita para su funcionamiento son las resistencias para establecer los niveles alto o bajo por defecto. Como además existe versión DIP, es muy cómodo para aplicarlo en montajes de prueba como componente auxiliar.
En el siguiente esquema se muestran los bloques de conexión del MCP23S17. Las patillas A0, A1 y A2 establecen los tres penúltimos bits de la dirección en el bus SPI. Como la señal de reset se establece a nivel bajo, es necesario conectarla a una resistencia pull-up. La alimentación del integrado puede estar comprendida entre 1V8V y 5V5.
Tanto la configuración como las operaciones de lectura y escritura sobre el MCP23S17 se realizan accediendo a los diferentes registros organizados en pares para facilitar la configuración del puerto A y B. Excepto en el caso de IOCON, que es el registro de configuración de entrada salida principal, las direcciones de los registros dependen del bit BANK del registro IOCON. Cuando se establece el bit IOCON.BANK a nivel bajo las direcciones de los registros para el puerto A y B se alternan: primero un registro que configura el puerto A y luego el de configuración equivalente para el puerto B. Si el bit IOCON.BANK vale 1, todos los registros del puerto A se sitúan al principio y luego se encuentran todos los del puerto B. Para evitar errores que generen direcciones incorrectas, no debe cambiarse IOCON.BANK cuando el modo de direccionamiento es secuencial porque genera automáticamente una nueva dirección, que en este caso podría no ser válida, después de cada operación.
dirección | registro | funcionamiento | |
BANK=0 | BANK=1 | ||
0x00 | 0x00 | IODIRA | Configura el modo como entrada o como salida. Los bits a nivel alto indican que el GPIO del puerto correspondiente (A o B) es de entrada y los bits que estén a nivel bajo en el registro indican que el GPIO es de salida. |
0x01 | 0x10 | IODIRB | |
0x02 | 0x01 | IPOLA | Establece la polaridad del bit del puerto. Cuando el bit está a nivel alto, el pin correspondiente GPIO invierte su valor; cuando el bit está a nivel bajo la polaridad es la normal y un valor alto en un pin corresponde con una señal a nivel alto y uno bajo a un nivel bajo. |
0x03 | 0x11 | IPOLB | |
0x04 | 0x02 | GPINTENA | Sirve para establecer los bits que producen la activación de la interrupción. Los bits de GPINTENA-GPINTENB a nivel alto influyen en el disparo de la interrupción y los que estén a nivel bajo no afectan a la generación de las interrupciones. |
0x05 | 0x12 | GPINTENB | |
0x06 | 0x03 | DEFVALA | Almacena el valor del puerto para no lanzar la interrupción si INTCON está activo. Si al comparar el estado actual del puerto el valor es diferente del configurado en DEFVALA-DEFVALB y los bits de INTCONA-INTCONB indican que su valor debe tenerse en cuenta, se dispara la interrupción. |
0x07 | 0x13 | DEFVALB | |
0x08 | 0x04 | INTCONA | Establece los bits del puerto que se comparan con DEFVAL para activar la interrupción cuando sean diferentes (los que estén a nivel alto). Los bits de INTCONA-INTCONB cuyo valor sea cero no se consideran al comparar el estado actual del puerto con el valor almacenado en DEFVALA-DEFVALB para disparar la interrupción. |
0x09 | 0x14 | INTCONB | |
0x0a | 0x05 | IOCON |
Ambos registros contienen la misma información y si se cambia el valor de uno también cambia el valor de otro. Para evitar confusiones no se les nombra como IOCONA-IOCONB sino que se hace referencia a ambos como IOCON. Bits que determinan la configuración de entrada salida:
|
0x0b | 0x15 | IOCON | |
0x0c | 0x06 | GPPUA | Sirve para controlar las resistencias pull-up internas de los puertos. Cuando se establece a nivel alto un bit de entrada de un puerto (un pin) se activa la resistencia interna de 100 kΩ. |
0x0d | 0x16 | GPPUB | |
0x0e | 0x07 | INTFA | Es un indicador de los bits que han producido una interrupción. El registro establece a nivel alto los bits por los cuales se genera la interrupción. Es de solo-lectura, no de escritura. |
0x0f | 0x17 | INTFB | |
0x10 | 0x08 | INTCAPA | Contiene el estado de cada puerto en el momento de producirse una interrupción (la «captura» del estado del puerto) y sirve para poder analizarlo permitiendo que la entrada cambie a otro estado mientras tanto. |
0x11 | 0x18 | INTCAPB | |
0x12 | 0x09 | GPIOA | Cuando se accede para leer devuelve el valor del puerto de entrada pero si se accede para escribir tiene el mismo efecto que OLAT: escribir en el latch de salida, es decir, no accede directamente al puerto de salida sino a través del latch de salida. |
0x13 | 0x19 | GPIOB | |
0x14 | 0x0a | OLATA | Da acceso al latch de salida del puerto correspondiente. Leer OLAT no accede directamente al puerto. Escribir en GPIO o en OLAT modifica el latch de salida que posteriormente modificará el puerto (la patilla) de salida. |
0x15 | 0x1a | OLATB |
El proceso para utilizar el MCP23S17 desde un microcontrolador es muy simple, consiste en establecer las comunicaciones SPI, preparar la configuración base e ir intercambiando información (recibiendo el estado o solicitando un estado) ya sea por las condiciones que impone el programa, incluyendo otras lecturas externas o control del tiempo, o por las interrupciones que genere el MCP23S17.
A nivel de hardware, para la implementación más simple no es necesario ningún componente pasivo, basta con conectar la alimentación y la patilla de reset a nivel alto. Si hubiera varios MCP23S17 en la misma línea de comunicaciones SPI habría que establecer diferentes direcciones para cada uno de ellos con las patillas A0, A1 y A2 a nivel alto o bajo.
Para usarlo desde Arduino se puede trabajar con la librería SPI. El proceso de explotación es el habitual:
- preparar la librería con
SPI.begin()
- establecer el pin CS (SS) como salida con
pinMode()
- ponerlo a nivel alto con
digitalWrite()
para desactivar el bus SPI al inicio - para cada escritura ponerlo a nivel bajo con
digitalWrite()
- enviar la dirección, el registro y el valor con
SPI.transfer()
- liberar las comunicaciones volviendo a poner CS (SS) a nivel alto con
digitalWrite()
Como el acceso implica varios pasos se puede empaquetar en una función. En el siguiente ejemplo, que muestra cómo escribir en un puerto, la única razón para crear la función es hacer el código más legible pero, en general, es la forma correcta ya que simplificará el código al acceder al MCP23S17 desde distintos puntos del programa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Activa secuencialmente un LED de una serie #define IODIRA 0x00 // Registro que establece el modo de las patillas del puerto A (salida=0, entrada=1) Por defecto se consideran de entrada #define GPIOA 0x12 // Como BANK está inicialmente establecido a 0, la dirección de GPIOA es 0x12, con BANK=1 sería 0x09 #define DIRECCION_ESCRITURA 0B01000010 // Las patillas A2 y A1 a nivel bajo y A0 a nivel alto (y HAEN a uno). El último bit a cero es el que diferencia la dirección de escritura de la de lectura, en la que el último bit es uno #define PIN_MCP23S17 10 // Pin en el que se conecta CS para activar por SPI (Suele ser necesario establecer a nivel alto también el pin 10 aunque se utilice otro para CS/SS) #include <SPI.h> byte contador_bit=1; // Contador del bit que se activa (del LED que se enciende) void setup() { SPI.begin(); // Inicializar la librería de comunicaciones SPI de Arduino pinMode(PIN_MCP23S17,OUTPUT); // Establecer el pin CS/SS del MCP23S17 como de salida (si se usa otro que no sea el 10, puede que igualmente sea necesario poner el 10 a nivel bajo para iniciar las comunicaciones) digitalWrite(PIN_MCP23S17,HIGH); // Empezar con las comunicaciones SPI desactivadas enviar_MCP23S17(IODIRA,0B00000000); // Configurar todos los bits del puerto A como salida (En binario para visualizar mejor cómo estará cada bit) } void loop() { enviar_MCP23S17(GPIOA,contador_bit); // Poner el bit que toque a nivel alto delay(40); // Esperar un poco (40 milisegundos) con el LED encendido enviar_MCP23S17(GPIOA,0); // Todos los bits del puerto a nivel bajo (apagar todos los LED) contador_bit=contador_bit?contador_bit<<1:1; // Pasar al siguiente bit (siguiente LED de la serie) } void enviar_MCP23S17(byte registro, byte valor) // Función que envía un valor al puerto A del MCP23S17) { digitalWrite(PIN_MCP23S17,LOW); // Activar las comunicaciones SPI poniendo CS/SS a nivel bajo SPI.transfer(DIRECCION_ESCRITURA); // Preparar para escribir (usar la dirección de escritura) SPI.transfer(registro); // Enviar el registro que se quiere modificar SPI.transfer(valor); // Enviar el valor del registro digitalWrite(PIN_MCP23S17,HIGH); // Desactivar las comunicaciones SPI. Dejar libre el bus para poder comunicar con otros dispositivos } |
En el siguiente ejemplo se muestra cómo leer un puerto. Para ver el resultado de la operación de lectura se escribe en el puerto de salida. El montaje podría estar formado por una batería de pulsadores o conmutadores conectados al puerto B la misma batería de LED del ejemplo anterior en el puerto A.
Al contrario de lo que ocurría en el montaje del anterior ejemplo, en este caso sí son necesarios algunos componentes pasivos (resistencias) para establecer el valor por defecto (salvo que se obtenga en la entrada, por ejemplo, conmutando entre VDD y VSS). En mi caso, una resistencia conecta cada pin del puerto B a masa para que el valor sea cero salvo que se actúe sobre el pulsador, conectado a la alimentación positiva, en cuyo caso tomaría el valor uno.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#define IODIRA 0x00 // Registro que establece el modo de las patillas del puerto A (salida=0, entrada=1) Por defecto se consideran de entrada #define IODIRB 0x01 // Registro que establece el modo de las patillas del puerto B (salida=0, entrada=1) Como BANK está inicialmente establecido a 0, la dirección de IODIRB es 0x01, con BANK=1 sería 0x10 #define GPIOA 0x12 // Como BANK está inicialmente establecido a 0, la dirección de GPIOA es 0x12, con BANK=1 sería 0x09 #define GPIOB 0x13 // Como BANK está inicialmente establecido a 0, la dirección de GPIOB es 0x13, con BANK=1 sería 0x19 #define DIRECCION_ESCRITURA 0B01000010 // Las patillas A2 y A1 a nivel bajo y A0 a nivel alto (y HAEN a uno). El último bit a cero es el que diferencia la dirección de escritura de la de lectura, en la que el último bit es uno #define DIRECCION_LECTURA 0B01000011 // Las patillas A2 y A1 a nivel bajo y A0 a nivel alto (y HAEN a uno). El último bit a uno es el que diferencia la dirección de lectura de la de escritura, en la que el último bit es cero #define PIN_MCP23S17 10 // Pin en el que se conecta CS para activar por SPI (Suele ser necesario establecer a nivel alto también el pin 10 aunque se utilice otro para CS/SS) #include <SPI.h> byte estado; // Estado del puerto en el momento de leerlo void setup() { SPI.begin(); // Inicializar la librería de comunicaciones SPI de Arduino pinMode(PIN_MCP23S17,OUTPUT); // Establecer el pin CS/SS del MCP23S17 como de salida (si se usa otro que no sea el 10, puede que igualmente sea necesario poner el 10 a nivel bajo para iniciar las comunicaciones) digitalWrite(PIN_MCP23S17,HIGH); // Empezar con las comunicaciones SPI desactivadas enviar_MCP23S17(IODIRA,0B00000000); // Configurar todos los bits del puerto A como salida (En binario para visualizar mejor cómo estará cada bit) enviar_MCP23S17(IODIRB,0B11111111); // Configurar todos los bits del puerto B como entrada (En binario para visualizar mejor cómo estará cada bit) } void loop() { estado=recibir_MCP23S17(GPIOB); // Leer el estado de B enviar_MCP23S17(GPIOA,estado); // Establecer la salida en A como la entrada de B delay(100); // Esperar un poco (100 milisegundos) con los LED encendidos } void enviar_MCP23S17(byte registro, byte valor) // Función que envía un valor al puerto A del MCP23S17) { digitalWrite(PIN_MCP23S17,LOW); // Activar las comunicaciones SPI poniendo CS/SS a nivel bajo SPI.transfer(DIRECCION_ESCRITURA); // Preparar para escribir (usar la dirección de escritura) SPI.transfer(registro); // Enviar el registro que se quiere modificar SPI.transfer(valor); // Enviar el valor del registro digitalWrite(PIN_MCP23S17,HIGH); // Desactivar las comunicaciones SPI. Dejar libre el bus para poder comunicar con otros dispositivos } byte recibir_MCP23S17(byte registro) // Función que envía un valor al puerto A del MCP23S17) { byte resultado; digitalWrite(PIN_MCP23S17,LOW); // Activar las comunicaciones SPI poniendo CS/SS a nivel bajo SPI.transfer(DIRECCION_LECTURA); // Preparar para leer (usar la dirección de lectura) SPI.transfer(registro); // Enviar el registro que se quiere modificar resultado=SPI.transfer(0); // Enviar cualquier valor para leer el resultado desde el MCP23S17 digitalWrite(PIN_MCP23S17,HIGH); // Desactivar las comunicaciones SPI. Dejar libre el bus para poder comunicar con otros dispositivos return resultado; } |
Para no tener al microcontrolador esperando cada cierto tiempo para leer el estado del puerto se pueden utilizar las interrupciones. En el siguiente ejemplo se muestra la configuración de las interrupciones, activándolas con el registro GPINTENB, y cómo sería el código de Arduino que lee el estado del puerto B y lo refleja a la salida en el puerto A. Para verificar el funcionamiento correcto de la configuración de las interrupciones no se han utilizado todos los bits para generarla de forma que solamente los cuatro menos significativos la activan aunque al lanzarla se lea el estado de todo el puerto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#define IODIRA 0x00 // Registro que establece el modo de las patillas del puerto A (salida=0, entrada=1) Por defecto se consideran de entrada #define IODIRB 0x01 // Registro que establece el modo de las patillas del puerto B (salida=0, entrada=1) Como BANK está inicialmente establecido a 0, la dirección de IODIRB es 0x01, con BANK=1 sería 0x10 #define GPINTENB 0x05 // Con BANK=0 GPINTENB=0x05 con BANK=1 GPINTENB=0x12 #define GPIOA 0x12 // Con BANK=0 GPIOA=0x12 con BANK=1 GPIOA=0x09 #define GPIOB 0x13 // Con BANK=0 GPIOB=0x13 con BANK=1 GPIOB=0x19 #define DIRECCION_ESCRITURA 0B01000010 // Las patillas A2 y A1 a nivel bajo y A0 a nivel alto (y HAEN a uno). El último bit a cero es el que diferencia la dirección de escritura de la de lectura, en la que el último bit es uno #define DIRECCION_LECTURA 0B01000011 // Las patillas A2 y A1 a nivel bajo y A0 a nivel alto (y HAEN a uno). El último bit a uno es el que diferencia la dirección de lectura de la de escritura, en la que el último bit es cero #define PIN_MCP23S17 10 // Pin en el que se conecta CS para activar por SPI (Suele ser necesario establecer a nivel alto también el pin 10 aunque se utilice otro para CS/SS) #define PIN_INTERRUPCION 2 // Pin en el que se conecta INTB #include <SPI.h> byte interrupcion=digitalPinToInterrupt(PIN_INTERRUPCION); // Calcular el número de interrupción asociada al pin que conecta INTB (con digitalPinToInterrupt para que no dependa de la placa) void setup() { SPI.begin(); // Inicializar la librería de comunicaciones SPI de Arduino pinMode(PIN_MCP23S17,OUTPUT); // Establecer el pin CS/SS del MCP23S17 como de salida (si se usa otro que no sea el 10, puede que igualmente sea necesario poner el 10 a nivel bajo para iniciar las comunicaciones) digitalWrite(PIN_MCP23S17,HIGH); // Empezar con las comunicaciones SPI desactivadas enviar_MCP23S17(IODIRA,0B00000000); // Configurar todos los bits del puerto A como salida (En binario para visualizar mejor cómo estará cada bit) enviar_MCP23S17(IODIRB,0B11111111); // Configurar todos los bits del puerto B como entrada (En binario para visualizar mejor cómo estará cada bit) enviar_MCP23S17(GPINTENB,0B00001111); // Solamente los 4 bits menos significativos son susceptibles de generar una interrupcióntienen efecto) attachInterrupt(interrupcion,mostrar_estado,CHANGE); } void loop() { } void enviar_MCP23S17(byte registro, byte valor) // Función que envía un valor al puerto A del MCP23S17) { digitalWrite(PIN_MCP23S17,LOW); // Activar las comunicaciones SPI poniendo CS/SS a nivel bajo SPI.transfer(DIRECCION_ESCRITURA); // Preparar para escribir (usar la dirección de escritura) SPI.transfer(registro); // Enviar el registro que se quiere modificar SPI.transfer(valor); // Enviar el valor del registro digitalWrite(PIN_MCP23S17,HIGH); // Desactivar las comunicaciones SPI. Dejar libre el bus para poder comunicar con otros dispositivos } byte recibir_MCP23S17(byte registro) // Función que envía un valor al puerto A del MCP23S17) { byte resultado; digitalWrite(PIN_MCP23S17,LOW); // Activar las comunicaciones SPI poniendo CS/SS a nivel bajo SPI.transfer(DIRECCION_LECTURA); // Preparar para leer (usar la dirección de lectura) SPI.transfer(registro); // Enviar el registro que se quiere modificar resultado=SPI.transfer(0); // Enviar cualquier valor para leer el resultado desde el MCP23S17 digitalWrite(PIN_MCP23S17,HIGH); // Desactivar las comunicaciones SPI. Dejar libre el bus para poder comunicar con otros dispositivos return resultado; } void mostrar_estado() { byte estado; // Estado del puerto en el momento de leerlo detachInterrupt(interrupcion); estado=recibir_MCP23S17(GPIOB); // Leer el estado de B enviar_MCP23S17(GPIOA,estado); // Establecer la salida en A como la entrada de B attachInterrupt(interrupcion,mostrar_estado,CHANGE); } |
Adrian
Muy buenas,
Estoy intentando comunicarme con una placa que integra un MCP23S17 y un LCD (LCD mini click de MikroE), y la explicacion sobre el funcionamiento del integrado me parece muy completa y de mucha ayuda para entender mejor el datacheet; aun con todo esto tengo un pequeño problema:
Estoy probando el primer programa de ejemplo que propones con un Arduino mega2560. Debido a que mi chip ya esta integrado en una placa (LCD click de MikroE), tengo que realizar algunos cambios en el programa (cambios detallados al final).
El principal problema es que intento activar las señales GPA 7-4, pero al medir con el multimetro las patas del integrado no me aparece ningun valor de tension (de hecho, el valor se queda oscilando y no fijo a 0). Aunque no tengo resolucion para verlo al detalle, puedo detectar la bajada de la linea de CS, y que la linea MOSI parece enviar, por lo que intuyo que no es un problema fisico, si no mas bien del comando que estoy enviando.
Los cambios de mi codigo son los siguientes:
**Debido a que mis señales A2-0 estan internamente conectadas a GND en mi placa. Mi comando para escritura queda
#define DIRECCION_ESCRITURA 0B01000000
**Utilizo una linea de CS distinta en el pin 37
#define PIN_MCP23S17 37
**Como no quiero que el chip se resetee, he conectado la linea de RESET a Vcc
**Como (de momento) no voy a leer, las dos lineas de interrupcion estan al aire
**Envio un «encender todos los leds» y tras un tiempo los apago
void loop()
{
Serial.print(«\nEnviar activar»);
enviar_MCP23S17(GPIOA,0xFF);
delay(4000);
Serial.print(«\nEnviar des-activar»);
enviar_MCP23S17(GPIOA,0x00); // Todos los bits del puerto a nivel bajo (apagar todos los LED)
delay(4000);
}
Muchas gracias por tu ayuda. Un saludito 😉
Salomo
Un articulo muy interesante.
Pero estoy haciendo un proyecto con Arduino en donde necesito poder intalar unos 10 chips MCP23017.
Como solo dispone 3 bits de direcciones como muchopodre direccionar 8 chips.
¿Existe alguna solucion al respecto?