Cuando un proyecto crece en complejidad no es raro encontrarse con que hacen falta más puertos de entrada-salida de los que hay disponibles en el microcontrolador seleccionado inicialmente. La solución puede ser optar por otro modelo de MCU de la misma familia pero que disponga de más patillas o añadir al proyecto un integrado, como el PCF8574, que aporte los GPIO que falten y se comunique con el µC con un bus I2C (que el bus que utiliza el PCF8574) o un bus SPI, con los que se puede conectar al microcontrolador varios dispositivos de manera simultánea.
El PCF8574 es muy popular porque, además de ser
barato, es sencillo tanto incluirlo en un circuito como usarlo desde un programa. Se presenta en varios encapsulados, incluyendo DIP, por lo que es ideal para hacer prototipos y pruebas antes de implementar el integrado en el proyecto definitivo.
La conexión del PCF8574 es muy sencilla. Por el lado del microcontrolador se conecta al bus I2C y a un pin que soporte una interrupción. Las tres líneas, las dos del bus y la de la interrupción, deben contar con las correspondientes resistencias pull-up.
La parte fija de la dirección IC del PCF8574 es 0b0100AAA (0x32 | AAA) y la del PCF8574A es 0b0100AAA0 (0x64 | AAA). Los tres dígitos representados con AAA se configuran con las patillas A0 a A2.
. Conectándolas a masa (a nivel bajo), como en el esquema de conexión del ejemplo de abajo, se consigue la dirección base 0b0100000 (0x32), o 0b0100AAA0 (0x64) en el caso del PCF8574A.
El PCF8574 dispone de resistencias pull-up internas en los pin de entrada y salida P0 a P7 por lo que al inicio de su funcionamiento (al alimentarlo) están a nivel alto.
La alimentación del PCF8574 está en el intervalo de 2,5 V a 6 V, así que puede usarse con un rango muy amplio de µC. Como no ofrece a la salida una corriente muy alta, es conveniente conectar las salidas del integrado con un driver (un simple transistor en corte o saturación puede servir) o, por ejemplo para usar LED, activarlos a nivel bajo y no alimentarlos con la salida del PCF8574.
Aunque es muy sencillo incorporar la versión DIP a un prototipo o usarla para pruebas, también existen módulos, como el de la imagen de abajo, que incluyen el PCF8574A, las resistencias del bus I2C (aunque normalmente no la de la interrupción) y recursos para configurar la dirección del dispositivo y encadenar módulos al estilo daisy chain.
El PCF8574 puede manejarse desde una placa Arduino usando la librería Wire en el programa con el que se explote. Para usarlo en modo escritura basta con enviar el valor del estado de los 8 bits que representan el puerto. La lectura puede hacerse de dos modos: que el programa tome la iniciativa para realizar el muestreo (por ejemplo cada cierto intervalo de tiempo) o que se realice la lectura del estado sólo cuando se produzca un cambio, del que el integrado avisa al microcontrolador con la interrupción.
En el siguiente código se muestra un ejemplo de cómo usar la salida del PCF8574. El funcionamiento consiste, básicamente, en enviar el código que representa el nivel de las patillas a la dirección I2C del integrado. Para hacer esta operación con la librería Wire de Arduino hay que activar las comunicaciones con Wire.begin() (que no será necesario invocar más que una vez), acceder a la dirección del PCF8574 o del PCF8574A con Wire.beginTransmission(), enviar el valor con Wire.write() y liberar el bus I2C con Wire.endTransmission().
#define ESPERA_ENTRE_CAMBIOS 1000
#define DIRECCION_PCF8574 32 // 0B0100000
#define DIRECCION_PCF8574A 64 // 0B01000000
#include <Wire.h>
long cronometro_cambio=0;
long tiempo_transcurrido;
byte codigo=0;
void setup()
{
Wire.begin();
}
void loop()
{
tiempo_transcurrido=millis()-cronometro_cambio;
if(tiempo_transcurrido>ESPERA_ENTRE_CAMBIOS)
{
cronometro_cambio=millis();
Wire.beginTransmission(DIRECCION_PCF8574);
Wire.write(codigo++);
Wire.endTransmission();
}
}
El ejemplo de abajo es el caso más sencillo de lectura del PCF8574, simplemente se consulta el estado cada cierto intervalo de tiempo y se envía a la consola serie (como ejemplo de uso). Las principales diferencias con el código del ejemplo anterior consisten en que se solicitan los datos con Wire.requestFrom() y se cargan del bus con Wire.read().
#define ESPERA_ENTRE_CAMBIOS 1000
#define DIRECCION_PCF8574 32 // 0B0100000
#define DIRECCION_PCF8574A 64 // 0B01000000
#include <Wire.h>
long cronometro_cambio=0;
long tiempo_transcurrido;
byte lectura=0;
void setup()
{
Serial.begin(9600);
Wire.begin();
}
void loop()
{
tiempo_transcurrido=millis()-cronometro_cambio;
if(tiempo_transcurrido>ESPERA_ENTRE_CAMBIOS)
{
cronometro_cambio=millis();
Wire.requestFrom(DIRECCION_PCF8574,1); // Pedir a la dirección DIRECCION_PCF8574 1 byte
lectura=Wire.read();
Serial.println(lectura);
}
}
La parte menos ortodoxa del código anterior consiste en leer el bus I2C sin saber si se habrán recibido ya los datos o no. Para hacerlo de forma más correcta se puede usar Wire.available() para saber cuántos bytes de datos se han recibido en la última consulta.
El formato más básico, que suele ser suficiente, consiste en consultar los datos recibidos y operar con ellos desde una estructura if() para evitar leerlos si no están disponibles.
#define ESPERA_ENTRE_CAMBIOS 1000
#define DIRECCION_PCF8574 32 // 0B0100000
#define DIRECCION_PCF8574A 64 // 0B01000000
#include <Wire.h>
long cronometro_cambio=0;
long tiempo_transcurrido;
byte lectura=0;
void setup()
{
Serial.begin(9600);
Wire.begin();
}
void loop()
{
tiempo_transcurrido=millis()-cronometro_cambio;
if(tiempo_transcurrido>ESPERA_ENTRE_CAMBIOS)
{
cronometro_cambio=millis();
Wire.requestFrom(DIRECCION_PCF8574,1);
if(Wire.available())
{
lectura=Wire.read();
Serial.println(lectura);
}
}
}
Aunque, en principio, el método anterior funciona, ya que el tiempo que tarda en ejecutarse la consulta es suficiente para que los datos hayan podido transmitirse, lo más correcto sería esperar hasta que lleguen los datos solicitados o hasta que pase un intervalo de tiempo prefijado (timeout) después del cual se podría suponer que se ha producido algún tipo de error y no llegarán esos datos.
#define ESPERA_ENTRE_CAMBIOS 1000
#define DIRECCION_PCF8574 32 // 0B0100000
#define DIRECCION_PCF8574A 64 // 0B01000000
#define TIMEOUT_I2C 10 // 10 milisegundos de espera antes de renunciar a leer el bus I2C
#include <Wire.h>
long cronometro_timeout_i2c;
long cronometro_cambio=0;
long tiempo_transcurrido;
byte lectura=0;
void setup()
{
Serial.begin(9600);
Wire.begin();
}
void loop()
{
tiempo_transcurrido=millis()-cronometro_cambio;
if(tiempo_transcurrido>ESPERA_ENTRE_CAMBIOS)
{
cronometro_cambio=millis();
Wire.requestFrom(DIRECCION_PCF8574,1);
cronometro_timeout_i2c=millis();
do
{
tiempo_transcurrido=millis()-cronometro_timeout_i2c;
}
while(!Wire.available()&&tiempo_transcurrido<TIMEOUT_I2C);
if(Wire.available())
{
lectura=Wire.read();
Serial.println(lectura);
}
}
}
Muestrear el estado del puerto del PCF8574 cada cierto periodo de tiempo o cuando lo determinen ciertas condiciones del estado del sistema (por ejemplo, la lectura de otras entradas) es perfectamente funcional, pero en algunas situaciones hay que responder a determinada lectura, cuando lleguen nuevos datos, lo antes posible, no cuando el intervalo de muestreo lo determine. Para esos casos lo más eficaz es utilizar una interrupción hardware, que en el PCF8574 se activa con cada cambio en el estado del puerto, es decir, con cada nueva lectura.
Las diferentes placas Arduino tienen asignaciones de patillas asociadas a interrupciones por hardware un poco diferentes.
• Los basados en el ATmega 328, como Arduino Uno, Arduino Nano o Arduino Mini usan las patillas 2 y 3.
• Arduino Mega, Arduino Mega 2560 y Arduino Mega ADK es capaz de detectar interrupciones hardware en las patillas 2, 3, 18, 19, 20 y 21.
• Los basados en el ATmega 32U4 como Arduino Micro o Arduino Leonardo detectan interrupciones en las patillas 0, 1, 2, 3, 7.
• Arduino Due puede usar todas las patillas para las interrupciones hardware.
• Arduino Zero (Genuino Zero fuera de USA) puede atender interrupciones hardware en todas las patillas excepto en la número 4.
• Arduino MKR1000 (Genuino MKR1000 fuera de USA) utiliza para las interrupciones hardware las patillas 0, 1, 4, 5, 6, 7, 8, 9, A1 y A2.
Para asignar las interrupciones se usa attachInterrupt() y para desactivarlas detachInterrupt(). La asignación de la interrupción necesita que se indique:
• el número de interrupción, que se puede obtener con digitalPinToInterrupt() indicándole el número del pin (en lugar del número de interrupción, que podría dar lugar a confusión por el modelo de placa),
• la función que se invoca cuando se genera la interrupción y
• el estado que se detecta, que puede ser LOW, CHANGE, RISING o FALLING según se considere el nivel bajo, un cambio en el pin de la interrupción, el flanco de ascenso o el flanco de descenso, respectivamente.
Conforme a lo anterior, el código de lectura del PCF8574 que lee su estado solamente cuando se genera una interrupción podría quedar como el del siguiente ejemplo:
#define PIN_INTERRUPCION 7
#define DIRECCION_PCF8574 32 // 0B0100000
#define DIRECCION_PCF8574A 64 // 0B01000000
#define TIMEOUT_I2C 10 // 10 milisegundos de espera antes de renunciar a leer el bus I2C
#include <Wire.h>
byte lectura=0;
boolean lectura_pendiente=true;
long cronometro_timeout_i2c;
long tiempo_transcurrido;
void setup()
{
Serial.begin(9600);
Wire.begin();
pinMode(PIN_INTERRUPCION,INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPCION),recibir_PCF8574,FALLING);
}
void loop()
{
if(lectura_pendiente)
{
Wire.requestFrom(DIRECCION_PCF8574,1);
cronometro_timeout_i2c=millis();
do
{
tiempo_transcurrido=millis()-cronometro_timeout_i2c;
}
while(!Wire.available()&&tiempo_transcurrido<TIMEOUT_I2C);
if(Wire.available())
{
lectura=Wire.read();
Serial.println(lectura);
}
lectura_pendiente=false;
}
}
void recibir_PCF8574()
{
detachInterrupt(digitalPinToInterrupt(PIN_INTERRUPCION));
lectura_pendiente=true;
attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPCION),recibir_PCF8574,FALLING);
}
En el ejemplo anterior se usa la variable global lectura_pendiente como un indicador de que se ha producido un cambio en el puerto. La función que gestiona la interrupción se encarga del mínimo proceso necesario y el código de lectura queda dentro del bucle de forma análoga a los anteriores ejemplos.
Al utilizar interrupciones, en lugar de muestrear «manualmente» el estado del puerto, será muy importante atender a las condiciones en las que se lanza la interrupción, para evitar efectos como los rebotes en contactos. La solución puede ser hardware (con un condensador o con una puerta lógica) o software, modificando el código para no actuar si el periodo entre cambios de estado del puerto no se produce en determinado intervalo.