Protocolo de comunicação I2C com Arduino e NodeMCU

Eletrônica Arduino 16 de Novembro de 2019 às 23:17

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.
Protocolo I2C: Condições de START e STOP.

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.

Protocolo I2C: Mestre enviando dados ao Escravo.

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.

Protocolo I2C: Mestre lendo dados do Escravo.

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.

Protocolo de comunicação I2C no Arduino Atmega328p e NodeMCU LOLin ESP12E.

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.

Módulo de conversor de nível lógico bidirecional 8 canais.

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);       
}
Escravo recebendo dados do mestre via função receiveEvent(int numByte).

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 :) ");
}
Lendo e escrevendo dados no Mestre e Escravo com Monitor Serial.

Referências

  1. McROBERTS, M; Arduino básico. 1 ed. Novatec, 2011.
  2. VALDEZ, J; BECKER, J; Application Report: Understanding the I2C Bus, Texas Instruments, 2015.