Manual
do
Maker
.
com
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.
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.
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.
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:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 |
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.
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:
Tirei print só do canto do terminal, já que é a parte que importava.
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:
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:
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!
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.
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:
Fácil ou não?
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.
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.