Introdução ao protocolo I2C
No tutorial anterior foi demonstrado como expandir portas digitais OUTPUT e manipular dados utilizando um Shift Register com Arduino ou NodeMCU. A ideia deste tutorial é realizar uma troca de informações com essas duas plataformas interligadas, para isso ocorrer é necessário implementar o protocolo de comunicação I2C (Inter-Integrated Circuit) ou também conhecido como TWI (Two Wire Interface).
Este protocolo foi desenvolvido pela Philips Semiconductors com o intuito de construir um barramento bidirecional usando apenas duas linhas de comunicação, aplicando um ou vários dispositivos mestres (Masters) que se comunicam com um ou vários dispositivos escravos (Slaves). É comum encontrar este tipo de padrão em EEPROMs como a AT24C02 , portas analógicas e digitais (ADCs/DACs) disponíveis no Arduino, NodeMCU e módulos, e em controle de displays OLEDs e LCDs.
A estrutura física é composta pela linha serial de dados (SDA) e a linha serial de clock (SCL), e normalmente são conectadas ao VCC com dois resistores de pull-up, onde seus valores são proporcionais à capacitância dos barramentos. A operação básica da comunicação se consiste na transferência dos dados entre os dispositivos a partir de comandos como START, STOP, R/!W, ACK e NACK. O procedimento da forma mais simples possível é descrita em alguns passos.
- Um mestre precisa transmitir informações a um escravo, então o comando START é realizado para iniciar a linha de SDA.
- O endereço de reconhecimento do escravo é implementado, sendo possível enviar o byte desejado ao registrador de dados, onde são transmitidos na ordem MSBFIRST, ou o bit mais significativo primeiro.
- A linha de SCL oscila conforme os bits são transferidos, onde nível alto representa estabilidade nos bits da linha SDA.
- Terminando a transmissão o mestre envia um comando de STOP, deixando as linhas de SDA e SCL em modo ocioso ou nível alto, pronto para a próxima comunicação.
Aprofundando-se mais no protocolo, existem outros comandos para controle da transmissão, como a etapa de validação dos dados. Expandindo os passos anteriores, a etapa de validação ACK é realizada com o mestre liberando a linha de SDA, o escravo seta a linha em nível baixo para enviar um bit de ACK em nível alto, este bit significa que o byte de endereço e dados foi recebido com sucesso e que outros dados podem ser enviados ao registrador.
Além do comando ACK, temos o comando NACK destinado a situações onde o byte não foi recebido corretamente, o escravo estando em modo busy, não ter a capacidade de receber mais bytes no registrador ou em operações de escrita. Neste caso a linha de SDA permanece em nível alto durante a execução do ACK, se transformando em um comando NACK.
O dados que são transmitidos a partir do mestre são alocados em registradores do escravo, como o endereço de reconhecimento do dispositivo de A0 até A6, o endereço do registrador de B0 até B7, e o registrador de dados de D0 até D7.
O processo de escrita do protocolo I2C se inicia com o mestre enviando o comando START e o endereço de reconhecimento do escravo com um bit de R/!W setado em 0, significando uma operação de escrita. Após o escravo enviar um bit de ACK, o mestre enviará a informação de qual endereço de registrador que ele deseja realizar a operação de escrita. Após outro comando ACK, o mestre inicia o envio do byte para o registrador de dados que ele escolheu, com todos os dados enviados corretamente passando pela validação do escravo, a transmissão é encerrada com um comando STOP executada pelo mestre.
Em uma operação de escrita mais simples, onde só temos valores de HIGH ou LOW em 8 bits para controlar LEDs, o mestre já reconhece o escravo enviando diretamente as informações para o registrador de dados, ignorando o registrador de endereços.
O processo de leitura do protocolo I2C é semelhante ao processo de escrita, porém com algumas diferenças importantes. Depois do mestre enviar a identificação do escravo, ele escolhe qual registrador de endereço para gravação das informações, e após a validação dos dados repetimos o comando START com o registrador de endereço do escravo com o bit de R/!W setado como 1, significando que a operação agora é leitura, é a partir deste ponto que o escravo se tornará um transmissor e o mestre em um receptor, invertendo as suas funções. Quando o mestre libera a linha de SDA, o escravo transmitirá as informações para o registrador de dados do mestre. Recebendo todos os dados requisitados do escravo para leitura, o mestre envia um comando de NACK, informando ao escravo parar todas as operações e liberar a linha de SDA, depois o mestre encerra a comunicação com STOP.
I2C com Arduino e NodeMCU
Com a parte teórica do protocolo de comunicação I2C explanada, podemos iniciar a implementação deste padrão em nosso benefício. Estas duas plataformas de desenvolvimento embarcado possuem pinos relacionados ao protocolo I2C. No caso do Arduino que possuí o microcontrolador Atmega328p, o pino relacionado ao SDA é a porta analógica A4 e o SCL é a porta A5, onde os resistores de pull-up estão setados internamente. As referencias para o NodeMCU LOLin ESP12E é SDA na porta D2 e SCL na porta D1. O segundo passo é escolher qual dispositivo será o mestre ou escravo, neste contexto o Arduino será o escravo e o NodeMCU o mestre, onde cada um receberá um código diferente, sendo o receptor e o transmissor.
Como estamos utilizando plataformas que trabalham em tensões diferentes, no caso do Arduino em 5V e o NodeMCU em 3.3.V, não podemos ligar as linhas de SDA e SCL diretamente nos dispositivos, isso resultaria na danificação das portas digitais ou em consequências mais graves. Para resolver este problema, utilizamos um conversor de nível lógico bidirecional de 8 canais.
A ligação dos fios neste módulo são bem simples, aplicamos como referência a tensão no pino VCCA 5V e no pino VCCB 3.3V, não podendo realizar estas ligações invertidas. Para ligar os fios de SDA e SCL vindos do Arduino utilizamos os pinos do módulo A0 e A1, e do outro lado utilizamos B0 e B1 para os fios de SDA e SCL do NodeMCU. Qualquer dúvida na montagem ou para testar o módulo, utilize um multímetro para medir as tensões.
Com estas informações em mãos, podemos realizar a montagem no protoboard, que deve ficar conforme as imagens abaixo.
Programando o Mestre para escrever dados
Como já mencionado neste tutorial, o dispositivo mestre escolhido é o NodeMCU. Para escrever algum dado nos registradores do escravo é necessário utilizar a biblioteca Wire e suas funções oficiais da IDE Arduino.
Para iniciar o barramento I2C é necessário adicionar no setup a função Wire.begin(); como sendo mestre, não é necessário colocar o parâmetro de endereço. A primeira função a ser implementada dentro do loop é a Wire.beginTransmission(address_slave); esta função inicia a comunicação com START e referencia o escravo por seu endereço de dispositivo 0x0A, agora podemos transmitir dados ao escravo utilizando a função Wire.write(value); onde podemos enviar strings, valores, vetores e o número de bytes, neste exemplo vamos implementar uma variável global do tipo byte chamada valor, e envia-la utilizando a função para escrever/transmitir, também vamos enviar uma string "tecdicas " de 9 bytes. Terminando a transmissão dos dados, encerramos a comunicação com STOP utilizando a função Wire.endTransmission();. Após compilar e enviar este código ao NodeMCU, é necessário programar o escravo para receber estes dados.
/**
* Código para o dispositivo mestre escrever dados no escravo.
*
* MESTRE: NODEMCU ESP12E
*
* Autor: Nicholas Zambetti
* Adaptações: tecdicas
*
* 30/01/2019
*
*/
#include <Wire.h>
byte valor = 0xFF;
void setup()
{
Wire.begin();
}
void loop()
{
Wire.beginTransmission(0x0A);
Wire.write("tecdicas ");
Wire.write(valor);
Wire.endTransmission();
delay(1000);
}
Programando o Escravo para ler dados
O dispositivo escravo é o Arduino com microcontrolador Atmega328p, para este tutorial foi usado um Arduino Nano pela sua praticidade. Com o mestre funcionando perfeitamente, o escravo precisa ser capacitado para receber os dados em seus registradores e imprimir os valores no Monitor Serial.
Como no mestre, deve ser utilizada a função Wire.begin(address_slave), mas com o endereço do escravo 0x0A. Para o escravo receber as informações transmitidas pelo mestre é necessário adicionar a função Wire.onReceive(receiveEvent); registrando a função/parâmetro receiveEvent ou handler para manipular os dados recebidos. O loop será em branco. Neste ponto implementamos a função do evento recebido, sendo receiveEvent(int numBytes), onde a leitura dos dados em string será iniciada com a função Wire.available(); em um loop para verificar se existem informações a serem lidas, se existir, utilizamos a função Wire.read(); para ler os 9 bytes "tecdicas " vindos do mestre, esses valores são alocados em uma variável do tipo char para serem visualizadas no Monitor Serial. Para receber o valor de 0xFF utilizamos a Wire.read() alocando o valor em uma variável do tipo inteiro para ser visualizada na serial em hexadecimal, binário, octal e decimal. Compilando e enviando o código para o Arduino Nano, podemos abrir o monitor serial e visualizar os dados vindos do mestre.
/**
* Código para o dispositivo escravo receber dados do mestre.
*
* SLAVE: ARDUINO NANO ATMEGA328P
*
* Autor: Nicholas Zambetti
* Adaptações: tecdicas
*
* 30/01/2019
*
*/
#include <Wire.h>
void setup()
{
Wire.begin(0x0A);
Wire.onReceive(receiveEvent);
Serial.begin(9600);
}
void loop() {}
void receiveEvent(int numBytes)
{
while (1 < Wire.available())
{
char td = Wire.read();
Serial.print(td);
}
int valor = Wire.read();
Serial.println("");
Serial.println(valor, HEX);
Serial.println(valor, BIN);
Serial.println(valor, OCT);
Serial.println(valor);
}
Piscando um LED e enviando uma mensagem ao mestre
Código do mestre
Neste exemplo o mestre irá enviar os valores de 0 ou 1 para piscar um LED conforme a variável estadoLed, onde seu estado é invertido a cada loop. Para receber a mensagem do escravo utilizamos a função Wire.requestFrom(address_slave, tamanhoByte); onde inserimos o endereço do escravo e o tamanho da mensagem, que no caso são 16 bytes, se você quiser escrever uma mensagem de sua preferencia utilize o site UTF-8 string length & byte counter para saber o tamanho correto. O processo para leitura dos dados é o mesmo do escravo receptor desenvolvido anteriormente. Compilando e enviando o código ao NodeMCU, agora precisamos programar o código do escravo, para receber estes comandos do LED e enviar a solicitação da mensagem.
/**
* Código para o dispositivo mestre escrever e receber dados do escravo.
*
* MESTRE: NODEMCU ESP12E
*
* Autor: tecdicas
*
* 30/01/2019
*
*/
#include <Wire.h>
#define SLAVE 0x0A
int estadoLed = 0;
void setup()
{
Wire.begin();
Serial.begin(9600);
}
void loop()
{
// Inicia a transmissão START e envia os comandos ao escravo.
Wire.beginTransmission(SLAVE);
if (estadoLed == 1)
{
Wire.write(1);
}
else if (estadoLed == 0)
{
Wire.write(0);
}
else
{
Serial.println("Desconhecido");
}
// Fecha a transmissão STOP
Wire.endTransmission();
// Recebe do escravo de endereço 0x0A a mensagem de 16 bytes.
Wire.requestFrom(SLAVE, 16);
while (Wire.available())
{
char hello = Wire.read();
Serial.print(hello);
}
estadoLed = !estadoLed;
delay(1000);
}
Código do escravo
O escravo recebe as instruções via função receiveEvent(int numBytes) para piscar um LED na porta digital 4 do Arduino Nano, e para enviar a mensagem ao mestre utiliza-se a função Wire.onRequest(requestEvent); registrando a função/parâmetro requestEvent ou handler, nele será enviado a requisição feita pelo mestre, utilizando a função Wire.write(value) com a mensagem "Hello master :) ". Compilando e enviando o código ao Arduino Nano, podemos abrir os monitores seriais do mestre e escravo ao mesmo tempo para visualizar as informações. Certifique-se das configurações da porta COM e placa em cada IDE Arduino.
/**
* Código para o dispositivo escravo escrever e receber dados do mestre.
*
* SLAVE: ARDUINO NANO ATMEGA328P
*
* Autor: tecdicas
*
* 30/01/2019
*
*/
#include <Wire.h>
#define LED 4
#define SLAVE 0x0A
void setup()
{
Wire.begin(SLAVE);
Serial.begin(9600);
Wire.onReceive(receiveEvent);
Wire.onRequest(requestEvent);
pinMode(LED, OUTPUT);
}
void loop(){}
// Recebe os comandos do mestre.
void receiveEvent(int numBytes)
{
if (Wire.available())
{
char valor = Wire.read();
Serial.println("DADO RECEBIDO");
if (valor == 0)
{
digitalWrite(LED, LOW);
Serial.println("DESLIGADO");
}
else if (valor == 1)
{
digitalWrite(LED, HIGH);
Serial.println("LIGADO");
}
else
{
Serial.println("Desconhecido");
}
}
}
// Envia uma mensagem ao mestre.
void requestEvent()
{
Wire.write("Hello master :) ");
}
Referências
- McROBERTS, M; Arduino básico. 1 ed. Novatec, 2011.
- VALDEZ, J; BECKER, J; Application Report: Understanding the I2C Bus, Texas Instruments, 2015.