Controle de até 8 relés utilizando GPIO e I2C

Controle de botoeira

Essa solução não é o que se pode chamar de “brilhante”, mas é uma maneira de economizar uma grana quando você precisa controlar a condição de vários relés baseados no estado de uma botoeira.

Pra quem não sabe, uma “botoeira” é um painel de botões para definir quando algo deve estar ligado ou desligado. Esse quadro de comando é um exemplo:

botoeira com pcf8574
botoeira com pcf8574

Essas chaves na base do quadro de comando não são botões de pulso, eles preservam um estado. Nesse caso, suponhamos que você precise monitorar 8 estados de botões para, conforme o estado, acionar até 8 relés. Nesse caso, você já teria que considerar de imediato um Arduino Mega, porque seriam necessários 16 GPIOs para fazer esse controle. Isso, se não tiver mais absolutamente nada a controlar, mas se você está optando por um controle através de uma MCU, certamente tem mais coisa a controlar (como por exemplo, LEDs de status, como os LEDs dispostos na caixa). Bem, tem uma solução simples para aproveitar os recursos do seu Arduino somado a um expansor de IO PCF8574.

Exemplo de uso com 1 PCF8574

Supondo que esteja utilizando um Arduino UNO, Leonardo ou Nano, serão necessários apenas 8 pinos de GPIO para acionar o relé e 2 pinos do I²C para controlar o PCF8574. Conforme o estado de cada pino do PCF8574, os relés correspondentes serão acionados ou desligados. Para isso, utilizaremos deslocamento de bits no PCF8574 e para relacionar os valores do PCF8574 aos respectivos pinos dos relés, podemos fazer um array contendo os pinos desses relés, depois varremos em um loop o estado de cada pino. Se você precisa de informações sobre a manipulação do PCF8574, recomendo esse artigo. Se estiver querendo descobrir o endereçamento do seu PCF8574, a forma mais simples é utilizando o scanner, descrito mais abaixo. Agora, vamos ao código, devidamente comentado.

#include <Wire.h>

#define ADDRESS      0x22 //endereço do "meu" PCF8574

#define TURN_OFF     1 //meu modulo desliga em HIGH
#define TURN_ON      0 //e liga em LOW

byte buf = 0; //buffer para armazenar o valor do PCF8574

byte relays[8] = {4,5,8,9,10,11,12,13}; //pinos para cada um dos reles

//função para ligar ou desligar os relés conforme os valores do PCF8574
void changeRelayState(){
  byte state = buf; //equipara o buffer com o comparador e faz um loop nos pinos
  for (int i=0;i<8;i++){
    state = buf|(1<<i); //incrementa o valor do buffer com a respectiva posição
    //se for possível incrementar o valor do buffer, significa que o 
    //pino do PCF8574 está em LOW
    if (state != buf){
      digitalWrite(relays[i],TURN_OFF); //nesse caso, desliga o relay da posição 'i'
      Serial.print("OFF: ");
      Serial.println(relays[i]);
      state = buf;
    }
    /*Senão, liga o relay da posição 'i'*/
    else{
      digitalWrite(relays[i],TURN_ON);
      Serial.print("ON: ");
      Serial.println(relays[i]);
    }
  }
}

void setup()
{
  Wire.begin();
  Serial.begin(115200);

  /*
   * Como estou utilizando apenas um módulo duplo, fiz o pinMode diretamente, mas
   * se tiver vários relays, basta rodar esse loop no mesmo tamanho do array relays[],
   * com limite de 8, já que o máximo de pinos no PCF8574 é 8.
  for (int i=0;i<8;i++){
    pinMode(relays[i],OUTPUT);
  }
  */
  pinMode(RELAY_PORT_1,OUTPUT);
  pinMode(RELAY_PORT_2,OUTPUT);

  digitalWrite(relays[0],TURN_ON); //estado inicial: ligado
  digitalWrite(relays[1],TURN_ON);
   
  delay(2000);
  Serial.println("Starting...");
}


void loop()
{  
  Wire.beginTransmission(ADDRESS); //inicia transmissao no barramento i2c
  Wire.write(0xFF); //coloca tudo em HIGH. O que estiver em GND vai para LOW sozinho.
  Wire.endTransmission(); //fim do envio do comando.
  delay(1000);

  Wire.requestFrom(ADDRESS,1); //solicita 1 byte (8 bits)
  if (Wire.available()){
    buf = Wire.read(); //armazena no buffer a leitura desse byte
  }
  Serial.println(buf);
  changeRelayState();//manipula os relays conforme os bits do PCF8574
}

Você pode questionar: “Mas, se o relay já está ligado, vai fazer digitalWrite novamente”. Sim, vai fazer mesmo, mas não muda nada. Assim haverá também uma ação imediata sem a necessidade de mais uma instrução do comparador condicional ‘if’.

Outra coisa que talvez lhe gere dúvida é sobre o deslocamento de bits. No video estou explicando como fiz a relação entre os bits do PCF8574 e os pinos dos relés, inclusive mostrando in flow no bPython para que fique fácil compreender.

Scanner I2C

Eu não decoro os endereçamentos dos PCF8574 e tenho uma baita preguiça de procurar até mesmo nos meus artigos, então, gosto de usar esse scanner I²C que encontrei há algum tempo no http://forum.arduino.cc/index.php?topic=197360 e que tem evoluido com o passar do tempo. Tudo o que você precisa fazer é colar esse código em um sketch, subir para o Arduino com o dispositivo I²C devidamente conectado e se guiar pelo menu que aparece no monitor serial. Dá pra fazer um single scan com ‘s’ ou um continuous scan com ‘c’, entre outros parâmetros configuráveis, mas não necessários para a maioria dos dispositivos que normalmente utilizamos.

//
//    FILE: MultiSpeedI2CScanner.ino
//  AUTHOR: Rob Tillaart
// VERSION: 0.1.7
// PURPOSE: I2C scanner at different speeds
//    DATE: 2013-11-05
//     URL: http://forum.arduino.cc/index.php?topic=197360
//
// Released to the public domain
//

#include <Wire.h>
#include <Arduino.h>

TwoWire *wi;

const char version[] = "0.1.7";


// INTERFACE COUNT (TESTED TEENSY 3.5 AND ARDUINO DUE ONLY)
int wirePortCount = 1;
int selectedWirePort = 0;


// scans devices from 50 to 800KHz I2C speeds.
// lower than 50 is not possible
// DS3231 RTC works on 800 KHz. TWBR = 2; (?)
const long allSpeed[] = {
  50, 100, 200, 300, 400, 500, 600, 700, 800
};
long speed[sizeof(allSpeed) / sizeof(allSpeed[0])];
int speeds;

int addressStart = 0;
int addressEnd = 127;


// DELAY BETWEEN TESTS
#define RESTORE_LATENCY  5    // for delay between tests of found devices.
bool delayFlag = false;


// MINIMIZE OUTPUT
bool printAll = true;
bool header = true;


// STATE MACHINE
enum states {
  STOP, ONCE, CONT, HELP
};
states state = STOP;


// TIMING
uint32_t startScan;
uint32_t stopScan;


void setup()
{
  Serial.begin(115200);
  delay(2000);
  Serial.println("Starting...");
  Wire.begin();

#if defined WIRE_IMPLEMENT_WIRE1 || WIRE_INTERFACES_COUNT > 1
  Wire1.begin();
  wirePortCount++;
#endif
#if defined WIRE_IMPLEMENT_WIRE2 || WIRE_INTERFACES_COUNT > 2
  Wire2.begin();
  wirePortCount++;
#endif
#if defined WIRE_IMPLEMENT_WIRE3 || WIRE_INTERFACES_COUNT > 3
  Wire3.begin();
  wirePortCount++;
#endif

  wi = &Wire;

  setSpeed('0');
  displayHelp();
}


void loop()
{
  char command = getCommand();
  switch (command)
  {
    case '@':
      selectedWirePort = (selectedWirePort + 1) % wirePortCount;
      Serial.print(F("I2C PORT=Wire"));
      Serial.println(selectedWirePort);
      switch (selectedWirePort)
      {
        case 0:
          wi = &Wire;
          break;
        case 1:
#if defined WIRE_IMPLEMENT_WIRE1 || WIRE_INTERFACES_COUNT > 1
          wi = &Wire1;
#endif
          break;
        case 2:
#if defined WIRE_IMPLEMENT_WIRE2 || WIRE_INTERFACES_COUNT > 2
          wi = &Wire2;
#endif
          break;
        case 3:
#if defined WIRE_IMPLEMENT_WIRE3 || WIRE_INTERFACES_COUNT > 3
          wi = &Wire3;
#endif
          break;
      }
      break;

    case 's':
      state = ONCE;
      break;
    case 'c':
      state = CONT;
      break;
    case 'd':
      delayFlag = !delayFlag;
      Serial.print(F("<delay="));
      Serial.println(delayFlag ? F("5>") : F("0>"));
      break;

    case 'e':
      // eeprom test TODO
      break;

    case 'h':
      header = !header;
      Serial.print(F("<header="));
      Serial.println(header ? F("yes>") : F("no>"));
      break;
    case 'p':
      printAll = !printAll;
      Serial.print(F("<print="));
      Serial.println(printAll ? F("all>") : F("found>"));
      break;

    case '0':
    case '1':
    case '2':
    case '4':
    case '8':
      setSpeed(command);
      break;

    case 'a':
      setAddress();
      break;

    case 'q':
    case '?':
      state = HELP;
      break;
    default:
      break;
  }

  switch (state)
  {
    case ONCE:
      I2Cscan();
      state = HELP;
      break;
    case CONT:
      I2Cscan();
      delay(1000);
      break;
    case HELP:
      displayHelp();
      state = STOP;
      break;
    case STOP:
      break;
    default: // ignore all non commands
      break;
  }
}


void setAddress()
{
  if (addressStart == 0)
  {
    addressStart = 8;
    addressEnd = 120;
  }
  else
  {
    addressStart = 0;
    addressEnd = 127;
  }
  Serial.print(F("<address Range = "));
  Serial.print(addressStart);
  Serial.print(F(".."));
  Serial.print(addressEnd);
  Serial.println(F(">"));

}

void setSpeed(char sp)
{
  switch (sp)
  {
    case '1':
      speed[0] = 100;
      speeds = 1;
      break;
    case '2':
      speed[0] = 200;
      speeds = 1;
      break;
    case '4':
      speed[0] = 400;
      speeds = 1;
      break;
    case '8':
      speed[0] = 800;
      speeds = 1;
      break;
    case '0':  // reset
      speeds = sizeof(allSpeed) / sizeof(allSpeed[0]);
      for (int i = 0; i < speeds; i++)
      {
        speed[i] = allSpeed[i];
      }
      break;
  }
}

char getCommand()
{
  char c = '\0';
  if (Serial.available())
  {
    c = Serial.read();
  }
  return c;
}

void displayHelp()
{
  Serial.print(F("\nArduino MultiSpeed I2C Scanner - "));
  Serial.println(version);
  Serial.println();
  Serial.print(F("I2C ports: "));
  Serial.println(wirePortCount);
  Serial.println(F("\t@ = toggle Wire - Wire1 - Wire2 [TEENSY 3.5 or Arduino Due]"));
  Serial.println(F("Scanmode:"));
  Serial.println(F("\ts = single scan"));
  Serial.println(F("\tc = continuous scan - 1 second delay"));
  Serial.println(F("\tq = quit continuous scan"));
  Serial.println(F("\td = toggle latency delay between successful tests. 0 - 5 ms"));
  Serial.println(F("Output:"));
  Serial.println(F("\tp = toggle printAll - printFound."));
  Serial.println(F("\th = toggle header - noHeader."));
  Serial.println(F("\ta = toggle address range, 0..127 - 8..120"));
  Serial.println(F("Speeds:"));
  Serial.println(F("\t0 = 50 - 800 Khz"));
  Serial.println(F("\t1 = 100 KHz only"));
  Serial.println(F("\t2 = 200 KHz only"));
  Serial.println(F("\t4 = 400 KHz only"));
  Serial.println(F("\t8 = 800 KHz only"));
  Serial.println(F("\n\t? = help - this page"));
  Serial.println();
}


void I2Cscan()
{
  startScan = millis();
  uint8_t count = 0;

  if (header)
  {
    Serial.print(F("TIME\tDEC\tHEX\t"));
    for (uint8_t s = 0; s < speeds; s++)
    {
      Serial.print(F("\t"));
      Serial.print(speed[s]);
    }
    Serial.println(F("\t[KHz]"));
    for (uint8_t s = 0; s < speeds + 5; s++)
    {
      Serial.print(F("--------"));
    }
    Serial.println();
  }

  // TEST
  // 0.1.04: tests only address range 8..120
  // --------------------------------------------
  // Address  R/W Bit Description
  // 0000 000   0 General call address
  // 0000 000   1 START byte
  // 0000 001   X CBUS address
  // 0000 010   X reserved - different bus format
  // 0000 011   X reserved - future purposes
  // 0000 1XX   X High Speed master code
  // 1111 1XX   X reserved - future purposes
  // 1111 0XX   X 10-bit slave addressing
  for (uint8_t address = addressStart; address <= addressEnd; address++)
  {
    bool printLine = printAll;
    bool found[speeds];
    bool fnd = false;

    for (uint8_t s = 0; s < speeds ; s++)
    {
#if ARDUINO >= 158
      wi->setClock(speed[s] * 1000);
#else
      TWBR = (F_CPU / (speed[s] * 1000) - 16) / 2;
#endif
      wi->beginTransmission (address);
      found[s] = (wi->endTransmission () == 0);
      fnd |= found[s];
      // give device 5 millis
      if (fnd && delayFlag) delay(RESTORE_LATENCY);
    }

    if (fnd) count++;
    printLine |= fnd;

    if (printLine)
    {
      Serial.print(millis());
      Serial.print(F("\t"));
      Serial.print(address, DEC);
      Serial.print(F("\t0x"));
      if (address < 0x10) Serial.print(0, HEX);
      Serial.print(address, HEX);
      Serial.print(F("\t"));

      for (uint8_t s = 0; s < speeds ; s++)
      {
        Serial.print(F("\t"));
        Serial.print(found[s] ? F("V") : F("."));
      }
      Serial.println();
    }
  }

  stopScan = millis();
  if (header)
  {
    Serial.println();
    Serial.print(count);
    Serial.print(F(" devices found in "));
    Serial.print(stopScan - startScan);
    Serial.println(F(" milliseconds."));
  }
}

E se eu quiser controlar por pulso?

Se for controlar por pulso, pra fazer com PCF8574 fica um pouco complicado. Eu fiz um pequeno código de exemplo para controle por pulso sem a utilização do módulo I²C. Basicamente, coloco um botão com “destino” ao GND e coloco um pino em pullup. Quando ele for para GND, ele muda o estado em uma variável de memória, chamada ports[], que é um array de estados.

bool ports[2] = {1};
void setup() {
  Serial.begin(115200);
  //coloca em pullup e quando for pra gnd, gera o evento
  pinMode(2,INPUT_PULLUP);
  pinMode(4,OUTPUT);
  pinMode(5,OUTPUT);
  digitalWrite(4,HIGH);
  digitalWrite(5,HIGH);
}

void loop() {
  if (digitalRead(2) == LOW){
    ports[0] = !ports[0];
    digitalWrite(4,ports[0]);
    Serial.println(ports[0]);
    while(digitalRead(2) == LOW);
  }
  delay(200); //so pra nao retornar imediatamente ao evento
  // put your main code here, to run repeatedly:

}

Onde comprar o PCF8574

Sempre recomendo um dos parceiros do site, mas meus caros, “nenhum” deles tem o módulo, ou seja, essa vou ficar devendo. O dispositivo é realmente barato e vale a pena a aquisição. Inclusive, no próximo artigo vou mostrar como espelhar a configuração de um PCF8574 em outro PCF8574, o que simplifica mais ainda o controle dos relés. Também vou mostrar como fazer espelhamento remoto dos relés utilizando o rádio NRF24L01, é só acompanhar!

Inscreva-se no nosso newsletter, alí em cima à direita e receba novos posts por email.

Siga-nos no Do bit Ao Byte no Facebook.

Prefere twitter? @DobitAoByte.

Inscreva-se no nosso canal Do bit Ao Byte Brasil no YouTube.

Nossos grupos:

Arduino BR – https://www.facebook.com/groups/microcontroladorarduinobr/
Raspberry Pi BR – https://www.facebook.com/groups/raspberrybr/
Orange Pi BR – https://www.facebook.com/groups/OrangePiBR/
Odroid BR – https://www.facebook.com/groups/odroidBR/
Sistemas Embarcados BR – https://www.facebook.com/groups/SistemasEmbarcadosBR/
MIPS BR – https://www.facebook.com/groups/MIPSBR/
Do Bit ao Byte – https://www.facebook.com/groups/dobitaobyte/

Próximo post a caminho!

Djames Suhanko

Djames Suhanko é Perito Forense Digital. Já atuou com deployer em sistemas de missão critica em diversos países pelo mundão. Programador Shell, Python, C, C++ e Qt, tendo contato com embarcados ( ora profissionalmente, ora por lazer ) desde 2009.

Um comentário em “Controle de até 8 relés utilizando GPIO e I2C

Deixe uma resposta