Protocolo I2C – Setup Multimaster com 2 Arduinos

Ola pessoal. I2C novamente.

Vamos “finalizar” o assunto I2C falando sobre setups multimaster utilizando dois Arduinos, onde ora um será o master, ora o outro.

Protocolo I2C e multimaster

Pesquisando pela net podemos encontrar algumas referências sobre setups multimaster, mas a maioria fica na teoria sobre o protocolo estar preparado para multimaster, sem exemplos reais de implementação. Alguns materiais até apresentam sketches de exemplo, mas sem nenhuma explicação teórica do funcionamento, o que dá a impressão que não ocorre colisão por pura sorte. Ainda, encontramos alguns componentes que implementam um I2C “estilo” proprietário, onde mecanismos de detecção de colisões tornam possível o uso nativo de múltiplos master.

Colisão

Em networking o termo colisão é bem conhecido, e refere-se a uma característica das redes que utilizavam cabeamento coaxial e UTP em half-duplex. Nestas redes poderia acontecer a situação onde duas máquinas tentavam transmitir ao mesmo tempo, e com isso os sinas (HIGH e LOW) se misturavam, tornando inútil a transmissão das duas máquinas. No protocolo I2C “raiz” não temos colisão, pois há apenas um master coordenando a comunicação, mas quando queremos implementar um setup I2C mais rebuscado, ai podemos ter problemas, pois se dois master tentarem iniciar uma comunicação ao mesmo tempo, as chances são grandes de que os sinais transmitidos por cada um se misturem, inutilizando o barramento.

Solução prática I2C multimaster

Deixando de lado então a possibilidade teórica do protocolo I2C controlar o barramento, vamos implementar em nosso sketch esse controle. Para isso vamos utilizar dois Arduinos, sendo que em um deles teremos um sensor de temperatura DS18B20, e o outro apenas solicitará a temperatura de tempos em tempos. Em outros posts sobre I2C tínhamos o master consultando informações do slave, mas este passava a informação solicitada em resposta a um evento onRequest. Aqui nós também vamos passar informações via I2C, mas desta vez o dispositivo “slave” vai passar a informação tornando-se um master.

Para que não ocorra colisões temos que nos certificar que a todo momento somente um dispositivo estará atuando como master. Vamos ver o diagrama de conexões que utilizaremos. Para facilitar, vou chamar o Arduino com o sensor DS18B20 apenas de sensor, e o outro Arduino será chamado simplesmente de controlador.

I2C multimaster

Esquema I2C multimaster utilizando dois Arduinos

O esquema de ligações é simples, vamos ver os códigos utilizados nos Arduinos controlador e sensor.

Código Controlador

#include <Wire.h>
#define i2c_address 0x1
#define slave_B 0x2
byte comando = 0;
boolean status_sensor = 0;
boolean status_controlador=1;
typedef union
{
 float valor;
 uint8_t bytes[4];
} FLOATUNION_t;
FLOATUNION_t temperatura;

void setup() {
  Serial.begin(9600);
  Wire.begin(i2c_address);
  Wire.onReceive(receivEvent);

    while (!status_sensor){
    status_sensor=get_status(slave_B);
    if (status_sensor){
    Serial.println("Conexao com sensor:   OK");
    Serial.println("Iniciando sketch...");
    delay(1000);
    } 
  }
  Serial.println("Inicializacao concluida");
  Serial.println("");
  delay(5000);
}

void loop() {
  if(status_controlador){
    Serial.print("Solicitando temperatura...  ");
    envia_comando(slave_B,1);
    status_controlador=0;
  }
  delay(5000);
}

void envia_comando(byte y, int x) {
  Wire.beginTransmission(y);
  Wire.write(x);
  Wire.endTransmission();
  comando = 0;
}

boolean get_status(byte y) {
  Wire.requestFrom(slave_B, 1);
  return Wire.read();
}

void receivEvent(int x){
 for (int i=0; i<4; i++){
  temperatura.bytes[i]=Wire.read();
 }
  Serial.print(temperatura.valor); 
  Serial.println(" graus Celsius");
  Serial.println("");
  status_controlador=1;
}

Iniciamos declarando as variáveis que utilizaremos. Notem que tenho as variáveis status_sensor e status_controlador para fazer o controle necessário sobre quem pode atuar como master. Além das variáveis “normais”, defini um tipo FLOATUNION_t, e instanciei a variável temperatura. Inicialmente status_sensor tem o valor 0, pois estamos considerando que o Arduino sensor não esta pronto. Já status_controlador tem o valor 1, pois ele é quem vai ativar o Arduino sensor.

setup()

Em setup() temos uma situação interessante. Aqui normalmente apenas inicializamos varáveis e objetos como a serial e o barramento I2C, mas desta vez vamos ter código executável até que determinada situação ocorra e prossigamos finalmente para o loop(). No caso nós verificamos a variável status_sensor indefinidamente dentro de um loop while, até que o teste retorne FALSE, indicando que o Arduino sensor esta operante. Essa indicação é obtida efetuado uma requisição I2C ao Arduino sensor.

Uma vez que o sketch alcance o loop(), a lógica é simples. Verificamos se o Arduino controlador é o master I2C através da variável status_controlador. Caso ele seja o master, nós enviamos um comando ao Arduino sensor e atribuímos FALSE à variável status_controlador. Com isso indicamos que o controlador não pode mais atuar como master.

Código Sensor

#include <Wire.h>
#include <OneWire.h>
#define i2c_address 0x2
#define slave_A 0x1
OneWire ds(10);
boolean sensor_status = 0;
byte comando = 0;
typedef union
{
 float valor;
 uint8_t bytes[4];
} FLOATUNION_t;
FLOATUNION_t temperatura;

void setup() {
  Serial.begin(9600);
  Wire.begin(i2c_address);
  Wire.onReceive(receivEvent);
  Wire.onRequest(requestEvent);
  Serial.println("Aguardando controlador");
  while(!sensor_status){
    Serial.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.println("Conectado ao controlador!");
  
}

void loop() {

  le_temperatura();
  if( comando == 1 ){
    Serial.println("Controlador solicitando temperatura");
    informa_temperatura();
    comando=0;
  }
    
}

void receivEvent(int x) {
  comando = Wire.read();
  }

void requestEvent(){
  sensor_status=1;
  Wire.write(sensor_status);
}

void informa_temperatura(){
  Wire.beginTransmission(slave_A);
  for (int i=0; i<4; i++){
    Wire.write(temperatura.bytes[i]);
  }
  Wire.endTransmission();
}
void le_temperatura() {
  byte i;
  byte data[12];
  byte addr[8];
  float celsius;
  ds.reset_search();
  while (ds.search(addr)) {

    Serial.println();

    ds.reset();
    ds.select(addr);

    float inicio = millis();
    ds.write(0x44);        // start conversion, use ds.write(0x44,1) with parasite power on at the end
    while (ds.read() == 0) {

    }
    float fim = millis();

    delay(1000);     // maybe 750ms is enough, maybe not

    ds.reset();
    ds.select(addr);
    ds.write(0xBE);         // Read Scratchpad

    for ( i = 0; i < 9; i++) {           // we need 9 bytes
      data[i] = ds.read();
    }

    int16_t raw = (data[1] << 8) | data[0];

    byte cfg = (data[4] & 0x60);
    // at lower res, the low bits are undefined, so let's zero them
    if (cfg == 0x00) raw = raw & ~7;  // 9 bit resolution, 93.75 ms
    else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
    else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
    //// default is 12 bit resolution, 750 ms conversion time

    celsius = (float)raw / 16.0;
    Serial.print("  Temperature: ");
    Serial.print(celsius);
    Serial.println(" Celsius, ");
        Serial.print("  Tempo Gasto: ");
    Serial.print(fim - inicio);
    Serial.println(" milesegundos");
    Serial.println("");
    temperatura.valor=celsius;
  }
}

Iniciamos o código do Arduino sensor declarando as variáveis que serão utilizadas. De particular interesse temos a variável sensor_status e o tipo FLOATUNION_t. A variável sensor_status serve para ativar o sketch, ou seja, nada será executado até que o valor de sensor_status seja alterado.

setup()

Em setup() inicializamos os objetos necessários, declaramos os manipuladores de evento desejados, e então entramos em um loop while até que o valor de sensor_status seja alterado. Notem que não temos em setup() nenhum código que pode alterar o valor de sensor_status. A única situação onde esse valor pode ser alterado é na função requestEvent(), sendo esta o manipulador do evento I2C onRequest declarado logo acima. Em resumo, ficaremos neste loop até que o controlador envie uma requisição e a função requestEvent possa alterar o valor de sensor_status. Para finalizar, observem que a função retorna o valor de sensor_status para o controlador.

Uma vez que saímos do setup(), entramos no loop. Aqui temos a função le_temperatura(), já conhecida de vocês. Após lermos a temperatura do DS18B20, nós verificamos se o valor da variável comando é 1. Caso seja, o sketch chama a função informa_temperatura().

Assumindo como master

É exatamente a função informa_temperatura() que transforma o sensor em um I2C master, assumindo o barramento e iniciando a escrita no controlador. Wire.beginTransmission() inicia a transmissão, Wire.write() efetivamente transfere os bytes desejados e então finalizamos com Wire.endTransmission().

Após a execução da função informa_temperatura(), atribuímos novamente o valor 0 à variável comando. Observem que com isto o sensor somente executará comandos como master I2C quando for instruído pelo controlador.

Tipo FLOATUNION_t – Enviado um float via I2C

Claro que o objetivo do post foi mostrar um setup I2C multimaster, mas temos uma questão interessante aqui que é a transmissão de um tipo float via I2C, onde somente podemos enviar um byte por vez(ou um array de bytes). Lembrando que o tipo float é composto por 4 bytes. O código do controlador e do sensor segue abaixo:

// codigo comun
typedef union
{
 float valor;
 uint8_t bytes[4];
} FLOATUNION_t;
FLOATUNION_t temperatura;

//codigo do sensor
temperatura.valor=celsius;
  for (int i=0; i<4; i++){
    Wire.write(temperatura.bytes[i]);
  }

//codigo do controlador
 for (int i=0; i<4; i++){
  temperatura.bytes[i]=Wire.read();
 }
 Serial.print(temperatura.valor);

Quando criamos uma definição de dados do tipo union, as variáveis criadas dentro da estrutura compartilham o mesmo espaço de memória.  Veja que temos a variável valor do tipo float(4 bytes) e temos um array chamados bytes, do tipo uint8_t(mesma coisa que o tipo byte ou char) também ocupando 4 bytes. Pois bem, estes 4 bytes de memória são compartilhados entre os dois tipos, então vejam que ao atribuirmos um float à variável valor automaticamente estaremos preenchendo o array bytes.

No sensor nós atribuímos o valor Celsius à variável temperatura.valor, mas quando vamos transmitir via I2C nós chamamos os valores sequencias do array temperatura.bytes[].

Já no controlador, nós primeiramente preenchemos temperatura.bytes[] lendo a partir do barramento I2C com Wire.read(), mas imprimimos o valor na console serial com temperatura.valor.

Resumo

Bem, talvez este post esteja um pouco confuso, talvez não… 😀 O fato é que fazer o controle  do barramento diretamente no sketch envolve um lógica extra. Além de pensar no que você quer fazer efetivamente, tem que pensar também no controle necessário para que dois dispositivos possam atuar como master no barramento.

Bem, fiquem a vontade com as dúvidas que surgirem. E caso tenham encontrado algum material com uma abordagem diferente para I2C multimaster, por favor informem, ficarei muito agradecido.

Abs a todos.

 

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

7 + vinte =