Manual

do

Maker

.

com

ESP32 com PCF8575

ESP32 com PCF8575

Antes de escrever a biblioteca, achei que seria interessante escrever um artigo usando ESP32 com PCF8575, por quatro razões: A conversão de inteiro para byte, a forma de apresentar na serial, o uso de bitwise para ler byte a byte e o modo de escrever 2 bytes de uma vez para esse expansor de IO de 2 bytes.

A placa utilizada é o "downloader" da Saravati, que tem também um gravador para ESP8266 e o tradicional para ESP-01. Os detalhes estão no artigo sobre gravadores.

Expansor de IO

Escrevi diversos artigos sobre o PCF8574, expansor de IO de 8 bits. Recentemente iniciei a escrita sobre o PCF8575, que é um expansor de IO de 16 bits. Vou deixar todos os conceito claros, então mostrar alguma coisa sobre esse novo expansor.

PCF8574

Um dos artigos que sempre recomendo é o "Dominando PCF8574", que é um artigo bem elaborado sobre o tema.

O PCF8574 é um expansor de IO de 8 bits. Suponhamos que você esteja usando um ESP-01 em seu projeto. Ele oferece apenas 2 pinos de GPIO, ou seja, não daria para controlar um módulo de 4 relés, por exemplo. Porém, utilizando um PCF8574, os 2 GPIOs disponíveis seriam usados para se comunicar com o expansor de IO, então invés de 2 GPIOs, você teria 8! Agora vamos ao conceito.

Base binária

O expansor de IO PCF8574 ou PCF8575 são controlados pelos bits. Tendo o PCF8574 8 bits, significa que ele tem 1 byte. Com um byte podemos ter valores de 0 à 255, totalizando 256 valores. Para fazer um controle adequado "sem" o uso de bibliotecas e com menor custo de processamento, o ideal é conhecer a base binária. Sabendo que a base binária é lida da direita para a esquerda e sendo o PCF8574 um expansor de IO de 8 bits, temos:

76543210
1286432168421
2726252423222120

Na primeira linha, temos o valor posicional dos bits; da direita para a esquerda.

Na segunda linha, temos os valores de cada bit posicional.

Na terceira linha, temos a razão desses valores.

Se todos os bits estiverem em LOW e quiséssemos colocar os bits 0 e 7 em HIGH, deveríamos passar o valor de ambos os bits somados: 129. Isso porque quando escrevemos para o PCF8674, escrevemos 1 byte, então o valor é distribuído nos bits correspondentes. Sempre escrevemos 1 byte. O que não corresponder a 1 será 0. Mais um exemplo seria escrever o valor 254. Esse valor corresponde à soma de 128 + 64 + 32 + 16 + 8 + 4 + 2. Assim sendo, teríamos os bits 2, 3, 4, 5, 6 e 7 em HIGH. Se desejar escrever algo mais visual, invés de escrever o valor 254, poderia passar o valor binário para a função correspondente, sendo 0b11111110. Mas não é prático manipular os valores assim e mostro a razão mais adiante.

Bitwise

Agora suponhamos que já levantamos o bit 7, como mostrado na tabela anterior; isso significa que escrevemos 128 no expansor de IO. Agora precisamos levantar o bit 0; se escrevermos 1, o bit cujo valor correspondente é 128 se tornará 0. O PCF8574 não preservará o estado anterior. Nesse caso, precisaríamos escrever 129. Mas invés de ficarmos validando quais bits estão em HIGH, podemos utilizar bitwise para reduzir o processamento e facilitar as coisas, sem precisar saber os valores e sem precisar compor um array de bits. Vamos levantar o bit 7:

uint8_t valor_para_o_pcf = 1<<7;

Agora queremos levantar o bit na posição 0. Não precisamos sequer saber o estado atual dos bits:

valor_para_o_pcf = valor_para_o_pcf|(1<<0);

Indo por partes: Usamos o operador unário | para somar. Então, deslocamos 1 bit para a posição 0. Essa é a maneira mais clara que consigo explicar, mas para ser mais prático, poderíamos fazer a mesma operação assim:

valor_para_o_pcf |= (1<<0);

Claro, aqui estamos vendo como atribuir os valores à variável que será escrita para o expansor de IO. O código de exemplo para o PCF8574 pode ser visto no artigo "Dominando PCF8574". Nesse artigo o foco é mostrar como escrever e ler os 2 bytes do PCF8575, mas espero que qualquer um possa entender o conceito. No vídeo mostrarei a operação diretamente no PCF8575, mas o que acabei de mostrar acima é isso que mostro no bpython:

bpython-bitwise-buf.jpg

Tirei print só do canto do terminal, já que é a parte que importava.

ESP32 com PCF8574 - Leitura e escrita

Usando a API do Arduino - seja através da IDE do Arduino ou PlatformIO, em qualquer IDE - utilizamos a biblioteca Wire.h para interagir diretamente com o dispositivo. A segunda coisa a considerar é que os 16 bits estão separados em duas regiões, portanto ainda são 2 bytes separados:

i2c-pcf8575-ports.jpg

A terceira coisa é que agora teremos que escrever os 2 bytes de uma vez, se pretendemos manipular bits de ambos os "lados". Existem diversas maneiras de fazer essa interação, sendo que para escrever, enviamos um array de bytes. Vou mostrar o código usado para a prova de conceito no vídeo e o disporei mais abaixo, mas preciso mostrar alguns trechos antes. Comecemos pelo buffer.

uint8_t to_write[2] = {255,255};

O uint8_t -ou , unsigned char - é o tipo que comporta 256 valores (0 à 255). Acima temos um array de 2 bytes para comportar os valores a escrever à esquerda e à direita. O PCF8575 inicia com ambos os bytes em HIGH, portanto a atribuição dos valores nesse momento é apenas figurativa.

Para escrever, simplesmente utilizamos a sequência:

  • iniciar transmissão para dado endereço
  • escrever valores
  • finalizar transmissão

Porém, como é chato repetir isso em todo o código, vale criar uma função. No caso:

void writeByte(uint8_t *values, uint8_t addr){
  Wire.beginTransmission(addr);
  Wire.write(values,2);
  Wire.endTransmission();
}

No construtor da função passamos um ponteiro, já que não se trata de um único uint8_t, assim como passamos o endereço, já que podemos ter diversos PCF8574 interconectados. Em Wire.write passamos esse ponteiro e o número de bytes a escrever.

Fiz a leitura em outro buffer (buf), assim comprovamos que os valores lidos estão vindo do PCF8575. Para ler, coloquei no início do loop:

Wire.requestFrom(ADDR,2);
  if (Wire.available()){
    Wire.readBytes(buf,2);
  }

E agora vamos para uma das partes mais legais desse programa!

int para bytes

Temos 16 bits para escrever e em casos reais, não quereremos ter que escrever byte a byte, já que passamos ambos os bytes de uma vez para a escrita. Mas unsigned char é 1 byte, enquanto int é 2 bytes. Nesse caso, porque não armazenar tudo em um tipo int e separar depois?

Para fazer o teste de escrita em todos os endereços possíveis, criei primeiramente uma variável:

int incremental = 0;

Então no loop fiz uma condicional ternária:

incremental = incremental > 65534 ? 0 : incremental+1;

Desse modo, quando chegar no limite do int, voltamos ao 0.

Agora precisamos separar 8 bits para cada unsigned char do array to_write.

 to_write[1] = incremental &~ (255 <<8);
 to_write[0] = incremental >> 8;

Pegamos os bits menos significativos (LSB - os bits da direita) e colocamos no índice 1. Pegamos os bits mais significativos (MSB - os da esquerda) e colocamos no índice 0.

Descobrir o endereço i2c automaticamente

Essa é a cereja do bolo desse artigo do ESP32 com PCF8575 e servirá para qualquer outro dispositivo que esteja na mesma condição!

Se estivermos utilizando apenas 1 dispositivo i2c, podemos fazer a descoberta do endereço automaticamente. Fiz um header scanner.h com o seguinte código:

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

bool i2c_exists = false;
uint8_t scanner(){
  byte error, address;
 
  Serial.println("Scanning...");

  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
 
    if (error == 0){
      Serial.print("I2C device found");
      i2c_exists = true;
      return address;
    }
    else if (error==4){
      Serial.print("Unknown error");
      return 0;
    }    
  }
  if (i2c_exists == false){
    Serial.println("No I2C devices found");
    return 0;
  }
}

E o código completo de teste no sketch ficou assim:

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

uint8_t ADDR = 0x20; //será sobescrito, o valor aqui é só enfeite

uint8_t buf[2];
uint8_t to_write[2] = {255,255};

int incremental = 0;


void writeByte(uint8_t *values, uint8_t addr){
  Wire.beginTransmission(addr);
  Wire.write(values,2);
  Wire.endTransmission();
}

void setup(){
  Wire.begin(21,22);
  delay(500);
  Serial.begin(9600);

  delay(2000);
  ADDR = scanner(); //descobre o endereço do dispositivo
  if (ADDR == 0){
    Serial.println("ouch....");
    while (true);
  }
}
 
void loop(){
  Wire.requestFrom(ADDR,2);
  if (Wire.available()){
    Wire.readBytes(buf,2);
  }

  Serial.printf("left byte: %d - right byte: %d\n",buf[0],buf[1]);

  to_write[1] = incremental &~ (255 <<8);
  to_write[0] = incremental >> 8;
 
  vTaskDelay(pdMS_TO_TICKS(10));
  writeByte(to_write,ADDR);
  vTaskDelay(pdMS_TO_TICKS(10));
  incremental = incremental > 65534 ? 0 : incremental+1;
}

Se for usar esse código na IDE do Arduino, remova o header Arduino.h de ambos os arquivos; do sketch e do scanner.h. Utilizando PlatformIO no VS Code, o resultado na serial ficou assim:

pcf8575-read-write-702x1024.jpg

Fácil ou não?

Vídeo do ESP32 com PCF8575

O vídeo estará disponível em nosso canal DobitaobyteBrasil no Youtube. Demonstrarei o código funcionando e também o estado dos pinos, utilizando o RPi Pico como analisador lógico. Se quiser fazer um analisador lógico como esse que fiz, leia o artigo "Analisador lógico com a RP Pico", da série "Laboratório Maker".

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.