Manual
do
Maker
.
com
Nesse outro artigo mostrei como fazermos a leitura do sensor de luminosidade GY-30 através da biblioteca BH1750, que implementa todas as instruções do sensor e devolve o resultado em lux. Dessa vez, vamos ver algo mais básico ainda, mas escrevendo nossa própria comunicação baseado no datasheet.
Esse sensor tem bastante precisão e velocidade, mas nesse artigo não vamos converter a leitura para lux, apenas pegar o valor raw e usar como gatilho. Usando esse pretexto, não precisarei implementar todos os recursos e consigo mostrar e discorrer sobre a comunicação I2C com um pouco mais de detalhamento. Vamos lá?
Tudo tem datasheet. Quando precisamos de dados técnicos, é o melhor lugar a recorrer, sem chance de pegar uma informação errada ou mal interpretada. O problema é que não se trata de uma informação trivial, diversos conceitos podem ser necessários, conforme a complexidade do elemento em questão. Por exemplo, o primeiro driver I2C que escrevi foi para o BMP180 no Raspberry. Na época eu não sei se já tinha biblioteca pronta, mas não fiz questão também porque pude aprender com meu ex-chefe como usar o datasheet. Para o Raspberry, escrevi em Python.
O datasheet desse sensor de luminosidade GY-30 pode ser pego diretamente nesse link, tratando-se do CI BH1750.
Por se tratar de um módulo pronto para uso, não li diversas partes do datasheet. Fui direto à parte Instructions Set Architecture.
Achei interessante que nesse datasheet os registradores foram colocados em binário invés de hexa. O set de instruções está na página 5.
Nessa mesma página, estão as recomendações do modo de operação (apesar de existirem modos alternativos) e comando reset (necessário).
Depois de implementar os endereços, no código através de um enumerador, pulei para a página 7, onde tem um exemplo de comunicação com o dispositivo. Também tem a fórmula para fazer a conversão do dado bruto para lux, mas me interessava apenas ler o dado bruto.
A leitura deve ser de 2 Bytes, sendo o Byte alto [15:8] e o Byte baixo [7:0]. No código vai ficar claro, está inclusive comentado.
A comunicação I2C é feita através da biblioteca Wire, que implementa algumas características que nos deixam livres de mais implementações ainda. Mas o que quero deixar claro aqui é a forma que fazemos essa comunicação com um periférico, assimq uando você ver o código de alguém, já conseguirá separar automaticamente o que pertence à comunicação e o que pertence à lógica do programa.
Sempre que formos utilizar I2C, o primeiro passo é incluir a biblioteca:
#include <Wire.h>
Depois em setup(), inicializar o barramento:
Wire.begin();
A partir daí temos 2 funções principais, que é leitura e escrita. Devemos ter em mente que o barramento I2C suporta até 127 dispositivos endereçáveis. Logo, quando queremos fazer uma comunicação com qualquer dispositivo I2C, o primeiro passo é saber seu endereço.
Se você tem um dispositivo e não sabe qual endereço ele está usando, pode descobrir através de um scanner I2C. Aqui tem o código de um.
Para escrever dados para um dispositivo, precisamos da sequência:
Alguns dispositivos podem requerer um intervalo de tempo para leitura, devido a processamento interno. O datasheet deixará isso claro, quando for necessário.
Considerando o formato da comunicação de escrita, o código resultante deve ser algo como:
Wire.beginTransmission(0x23); // inicia a comunicação com o dispositivo 0x23
Wire.write(0x40); //escreve o hexadecimal 0x40 (esse valor é um exemplo)
Wire.endTransmission(); //finaliza a transmissão
O valor 0x40 é hipotético, se não escrever informações conforme o protocolo, não vai obter resultados.
Outra coisa importante de citar é que os dados de comunicação são Bytes, não strings literais. A informação é sucinta no barramento, pois no final das contas devemos considerar velocidade não só de processamento, mas de tempo de ocupação do barramento. Minha recomendação é que acostume-se a pensar nesse formato. Inclusive, por essa razão escrevi um exemplo de comunicação com LoRa, criando um protocolo básico e sem CRC.
A leitura é um pouco mais elaborada, mas vai ficar muito claro nas próximas linhas, não se preocupe.
Existe processamento de modo continuo e "single shot". Isto é, alguns sensores oferecem o recurso de processamento contínuo, bastando pegar o resultado ao fazer uma leitura. Em alguns casos isso é bom, principalmente quando se trata de economizar energia. Mas tudo tem contrapartida; em alguns casos o barramento ficará integralmente ocupado, não sendo possível interagir com outros dispositivos I2C. Então nessas condições é melhor fazer uma requisição de processamento e uma de leitura. O tempo de resposta e ocupação da MCU variará um pouco, mas isso é significativo em dispositivos de missão crítica. Por exemplo, em uma estação meteorológica não haveria problema de esperar 200ms para ter uma resposta.
Seguindo a linha de raciocínio "single shot", teriamos então a seguinte sequência:
A forma de escrita já deve estar clara. A requisição de dados tem alguns detalhes importantes a citar.
Quando requisitamos os dados, passamos o endereço e o número de Bytes desejados. Esse número variável de dados depende da resposta do dispositivo em questão. Por exemplo, o datasheet do sensor de luminosidade GY-30 mostra que a resposta tem 2 Bytes de tamanho e esses Bytes devem ser tratados.
Após solicitarmos os Bytes de resposta, devemos aguardar a disponibilidade desses dados. Não é uma boa ideia colocar o processamento em loop, pois a MCU pode ter outras tarefas. Dispositivos que não oferecem interrupção poderão ser lidos em polling, por exemplo. Se for no ESP32 (como é o caso desse artigo), pode-se criar uma tarefa exclusiva pra cada sensor, pode-se criar timers de leitura e mais um monte de possibilidades diferentes. Nesse artigo estou escrevendo uma comunicação pífia, sem nenhum propósito que não exemplificar o uso do I2C com dispositivos que tenham processamento independente.
Após fazer a leitura, o resto é lógica do programa principal. Vejamos agora um código para esse sensor.
O código está todo comentado. Subindo esse sketch, você verá no monitor serial o valor de leitura, daí pode testar o gatilho que seja interessante para seu projeto conforme a luminosidade.
#include <Arduino.h>
#include <Wire.h>
#define SENSOR_ADDR 0x23
//Pag. 5 datasheet
//Recomendado: MODE_HR1 (120ms)
//reset necessário ao ligar (enviar POWER_ON seguido de RESET). Aguardar (2ms)
enum instruction_set {
POWER_DOWN = 0x00,
POWER_ON = 0x01,
RESET = 0x07,
MODE_HR1 = 0x10,
MODE_HR2 = 0x11,
MODE_LR = 0x13,
MODE_1THR1 = 0x20,
MODE_1THR2 = 0x21,
MODE_1TLR = 0x23
};
uint16_t read_sensor(uint8_t cmd, uint8_t waits); //protótipo da função de leitura
void send_command(uint8_t cmd, uint8_t waits); //protótipo da função de envio de comandos
void sensor_start(); //protótipo da função de inicialização do dispositivo (requerido no datasheet)
void sensor_mode(uint8_t mode); //protótipo da função de modo
uint16_t read_sensor(uint8_t cmd, uint8_t waits){
uint16_t data = 0; //variável para guardar a leitura
//Envia o comando e faz o delay
Wire.beginTransmission(SENSOR_ADDR);
Wire.write(cmd);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(waits));
//Agora já deve ter sido processada a requisição, então pega-se a resposta
Wire.requestFrom(SENSOR_ADDR,2); //solicitado 2 Bytes de resposta ao dispositivo 0x23 (definido na macro SENSOR_ADDR)
//Se o dispositivo estiver acessível, faz a leitura
if (Wire.available()){
//Lê
data = Wire.read();
//desloca 8 bits para a esquerda
data <<= 8;
//faz a máscara com os bits da direita
data|= Wire.read();
//Só para debug
Serial.println("ok");
}
//Se não tivesse leitura a fazer, a função deveria retornar um valor ainda assim.
return data;
}
//Como exemplificado no artigo...
void send_command(uint8_t cmd, uint8_t waits){
Wire.beginTransmission(SENSOR_ADDR);
Wire.write(cmd);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(waits));
}
//Como especificado no datasheet, garantir que o dispositivo esteja up, então fazer um reset na inicialização.
void sensor_start(){
send_command(POWER_ON,2);
send_command(RESET,2);
}
//Não passa de uma escrita, não precisaria de mais uma função para fazer o mesmo, mas visualmente fica fácil de saber seu propósito.
void sensor_mode(uint8_t mode){
//Pag. 7 datasheet: Aguardar completar 1st (max. 180ms)
Wire.beginTransmission(SENSOR_ADDR);
Wire.write(mode);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(182));
}
void setup(){
Serial.begin(9600);
Wire.begin(); //inicializa o barramento i2c
vTaskDelay(pdMS_TO_TICKS(200)); //faz um delay
sensor_start(); //inicializa o dispositivo
sensor_mode(MODE_HR1); //modo recomendado no datasheet
}
void loop() {
uint8_t reading = read_sensor(MODE_1TLR,190); //modo "single shot"
Serial.println(reading);
vTaskDelay(pdMS_TO_TICKS(1000));
}
Esse não vai ter vídeo, mas estou com 2 vídeos atrasados porque preciso reinstalar meu sistema no Notebook. Em breve subo os vídeos relacionados aos artigos anteriores, não deixe de se inscrever em nosso canal DobitaobyteBrasil no Youtube!
Esse sensor está disponível na Curto Circuito, que tem uma variedade bem significativa de sensores de diversos barramentos, dê uma conferida!
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.