Arduino e comunicação sem fio: módulo NRF24L01+

Como configurar e programar a conexão sem fio direta entre Arduinos usando o módulo NRF24L01+ para transmitir comandos e receber respostas.

O módulo NRF24L01+ é bastante popular e bem documentado, e algumas das bibliotecas que o suportam vem acompanhadas de bons programas de exemplo.

Neste artigo, veremos os elementos básicos de uma aplicação prática: um programa que pode ser instalado tanto no Arduino que estiver atuando como mestre (que comanda as transmissões e recebe os dados), quanto no que estiver configurado como subalterno (que aguarda os comandos e transmite os dados).

Ambos transmitirão e receberão dados: o mestre envia comandos e recebe as respostas do subalterno, e o subalterno aguarda a recepção dos comandos e envia a resposta de acordo com cada comando recebido.

Conhecendo o NRF24L01+

O chip nRF24L01+, da Nordic, é uma dessas maravilhas da tecnologia moderna: reúne transmissão e recepção em um mesmo componente, consome pouca energia (pode funcionar meses ou até anos com uma pilha AA), transmite/recebe a até 2Mbps e encapsula boa parte da complexidade da comunicação digital via rádio, deixando os microcontroladores livres para se preocupar com as demandas da aplicação.

Ele é usado em periféricos sem fio para computadores, em acessórios esportivos e de jogos eletrônicos, em brinquedos, controles remotos avançados, headsets, joysticks, e em eletrônicos de consumo em geral.

A comunicação do NRF24L01+ é direta, sem roteadores ou outra infra-estrutura. – não é um chip WiFi.

A comunicação usando esse tipo de chip é diretamente entre dispositivos, sem intermediação por roteadores ou similares – não é um adaptador WiFi, nem usa protocolos da Internet, como o TCP e o IP. Normalmente eles atuam em pares (como em um teclado sem fio conectado a um adaptador USB), ou um mestre controla vários subalternos (como em uma rede de sensoreamento na qual um Arduino com esse chip obtém dados de vários outros Arduinos com esse chip, e os registra ou envia para um PC via serial). Outras topologias também são possíveis, inclusive redes estrela com até 6 nós.

No mundo Arduino, é frequente ele ser visto na forma de um módulo que inclui não apenas o chip, mas também todos os (poucos) elementos adicionais necessários ao seu funcionamento: um cristal para sincronização precisa, os capacitores e resistores associados a este cristal, e uma antena, frequentemente impressa na própria placa.

Esses módulos são fáceis de encontrar em lojas de eletrônica nacionais, e saem bastante baratos para comprar em quantidade nos grandes sites de comércio eletrônico chineses.

Este artigo foi escrito com um par de módulos iguais a este. Ao escolher um modelo para prototipação, sugiro optar por um que já tenha pinos DIP soldados ou com espaço disponível para soldá-los. Fora isso, vale a pena verificar itens como a antena (ou mesmo a presença de um conector para antena externa), que podem influenciar bastante no alcance.

De modo geral, os demais parâmetros de operação variam pouco entre os modelos, pois dependem basicamente do chip. Os principais são:

  • Opera nas frequências da faixa de 2,4GHz (que é livre para uso sem necessidade de registro na Anatel ou similares)
  • 125 canais
  • Baixíssimo consumo de energia
  • Velocidades selecionáveis entre 250Kbps e 2Mbps
  • Modulação FSK/GSK
  • Suporte a comunicação multipontos
  • Capacidade de evitar interferência e de saltar entre frequências

A comunicação entre o módulo nRF24L01+ e o Arduino ocorre usando o protocolo SPI e, embora o módulo opere a 3,3V (e assim precise ser alimentado por meio do pino 3V3 do Arduino), ele aceita e envia sinais compatíveis com o nível lógico de 5V do Arduino Uno e seus similares, sem precisar de conversão.

Arduino e nRF24L01+: como conectar

A pinagem dos meus módulos nRF24L01+ é de 8 pinos, e foi conectada ao Arduino Uno como segue:

Os números de pinos do Arduino mencionados acima referem-se ao uso da popular biblioteca RF24 em um Arduino Uno, na forma como configurei para este artigo – outras configurações podem assumir outros valores.

Os pinos CE e CSN podem ser conectados a outros pinos do Arduino que não o 9 e o 10, se necessário – mas isso exigirá alteração no nosso programa de exemplo, no trecho em que faz referência a esses pinos.

Ainda, existem módulos com pinagens diferentes da exibida: verifique a documentação do seu, e adeque. E se o seu Arduino tiver os pinos da comunicação SPI em outras posições (como ocorre com o Leonardo e o Mega), adeque também.

Não plugue os pinos diretamente a uma breadboard: cada par ficará em curto entre si. Uma alternativa simples é usar jumpers macho-fêmea para ligar.

Arduino e nRF24L01+: como programar

O exemplo a seguir considera 2 Arduinos Uno, cada um conectado a um módulo NRF24L01+, e ambos próximos entre si1. Lembre-se de instalar previamente a biblioteca RF24.

Nosso programa implementa os 2 modos de operação: mestre e subalterno. A instalação é a mesma em ambos os Arduinos.

O mesmo código deve ser instalado em ambos os Arduinos, sem alteração. Durante a execução, um deles será o mestre, e o outro será o subalterno. Para configurar um deles como o mestre, passe um resistor entre seu pino 2 e o pino GND – o programa irá procurar por essa conexão para saber qual papel deve assumir. Não há problema em trocar, durante a execução, qual deles será o mestre – mas se/enquanto ambos estiverem simultaneamente como mestre ou como subalterno, a comunicação não será bem-sucedida.

O programa a seguir se baseia na transmissão de pacotes de dados, com tamanho fixo (sempre uma matriz de 0 a 5 inteiros), entre os Arduinos. O primeiro inteiro da matriz será sempre o identificador do tipo de pacote (um questionamento, uma resposta, etc.), e nos demais o programa pode colocar livremente os dados que deseja comunicar: o estado de uma porta, um sensor, o tempo decorrido desde algum evento, etc.

O quadro a seguir indica uma saída possível do programa rodando no mestre:

Enviei comando 11
Recebi resposta de interrogacao: 8325
 
Enviei comando 21
Recebi resposta de um ping enviado ha 985ms.

Enviei comando 11
Não houve resposta
 
Enviei comando 11
Recebi resposta de interrogacao: 8436

O programa define 2 tipos de questionamento e suas respectivas respostas, e controla o envio e recebimento de todos eles:

#include <nRF24L01.h>
#include <RF24.h>
#include <SPI.h>

RF24 radio(9,10);

const uint64_t PIPE_COMANDO = 0xE8E8F0F0E1LL;
const uint64_t PIPE_RESPOSTA = 0xE8E8F0F0E2LL;


const int CMD_INTERROGA=11;
const int CMD_RESPONDEINTERROG=12;
const int CMD_PING=21;
const int CMD_RESPONDEPING=22;

int msg[5];

const byte pinoChave=2;
boolean temMensagem;

void setup() {
  Serial.begin(9600);
  pinMode(pinoChave,INPUT_PULLUP);
  radio.begin();
  radio.openWritingPipe(PIPE_COMANDO);
  radio.openReadingPipe(1,PIPE_RESPOSTA);
  radio.startListening();
}

void enviaMsg() {
  radio.stopListening();
  radio.write(msg, sizeof(msg));
  radio.startListening();
}
 
void interroga(int comando) {
  msg[0]=comando; 
  msg[1]=millis() % 32768;
  enviaMsg();
  Serial.print("Enviei comando ");
  Serial.println(msg[0]);
  for (int i=0; i<500; i++) {
    temMensagem=radio.available();
    if (temMensagem) {
      break;
    }  
    delay(4);
  }  
  if (temMensagem) {
    boolean concluido=false;
    while (!concluido) {
      concluido=radio.read(msg, sizeof(msg));
    }  
    switch (msg[0]) {
      case CMD_RESPONDEINTERROG:
        Serial.print("Recebi resposta de interrogacao: ");
        Serial.println(msg[1]);
        break;
      case CMD_RESPONDEPING:
        Serial.print("Recebi resposta de um ping enviado ha ");
        Serial.print((millis() % 32768)-msg[1]);
        Serial.println("ms.");
        break;
      default:
        Serial.print("Recebi resposta desconhecida ou impropria:");
        Serial.print(msg[0]);
        Serial.print(" ");
        Serial.println(msg[1]);
    }  
  } else {
    Serial.println("Nao houve resposta");
  }    
  Serial.println(" ");
} 

void responde() {
  int resposta=0;
  if (radio.available()) {
    boolean concluido=false;
    while (!concluido) {
      concluido=radio.read(msg, sizeof(msg));
    }  
    switch (msg[0]) {
      case CMD_INTERROGA:
          resposta=random(8000,9000);
          Serial.print("Recebi comando de interrogacao e vou responder: ");
          Serial.println(resposta);
          msg[0]=CMD_RESPONDEINTERROG;
          msg[1]=resposta;
          enviaMsg();
          break;
      case CMD_PING:
          msg[0]=CMD_RESPONDEPING;
          enviaMsg();
          Serial.print("Recebi comando ping e ja respondi: ");
          Serial.println(msg[1]);
          break;
        default:
          Serial.print("Recebi comando desconhecido ou improprio:");
          Serial.println(msg[0]);
      }  
  } else {
    Serial.println("Nao ha mensagens disponiveis");
  }  
} 
 
 
void loop() {
  if (digitalRead(pinoChave)==LOW) {
    if (random(0,5)==3) interroga(CMD_PING);
    else interroga(CMD_INTERROGA);
  } else {
    responde();
  }  
  delay(1000);
}

As linhas iniciais, em vermelho, executam uma sequência já bem conhecida de referenciar bibliotecas e instanciar um objeto para acesso aos métodos dela.

As 2 últimas linhas do bloco em vermelho oferecem algo diferente, entretanto: são elas que definem as pipes (“dutos”) por onde serão transmitidos os dados nas duas direções: o envio de comandos e a recepção de respostas2

A seguir, em laranja, temos definições essenciais para o funcionamento do programa: os tipos de mensagens que serão enviadas e/ou recebidas. Veremos mais detalhes a seguir, mas note que são 2 pares de constantes: CMD_INTERROGA com CMD_RESPONDEINTERROG e CMD_PING com CMD_RESPONDEPING. Também em laranja temos a definição da matriz msg, composta de 6 (0 a 5) números inteiros. Essa mesma matriz será usada para enviar e receber as mensagens.

A inicialização do programa está em verde, e começa ativando a Serial e definindo o modo do pino 2 (definido em pinoChave), que será usado mais adiante para verificar se um Arduino deve agir como mestre (se houver um resistor entre o pino 2 e o GND) ou como subalterno (se não houver).

A seguir, ainda em verde, vem a inicialização e configuração inicial do rádio. Note que definimos as 2 pipes (radio.openWritingPipe e radio.openReadingPipe) e colocamos o rádio em modo de recepção de pacotes (radio.startListening), que será nosso modo básico de operação (exceto quando for a hora de transmitir algo, como veremos a seguir).

Assim na vida como no NRF24L01: para falar bem, acaba sendo necessário parar de ouvir, nem que seja por um instante.

Em cor salmão temos a enviaMsg(), uma rotina de uso geral usada pelas funções que vêm a seguir. O que ela faz é muito simples: desativa o modo de recepção de pacotes (radio.stopListening), aí transmite o conteúdo que estiver armazenado na matriz msg, e volta a ativar o modo de recepção de pacotes.

Agora, antes de ver as linhas imediatamente a seguir, vamos dar um grande salto, diretamente para o final do programa, no trecho marcado em marrom. É o loop, a função que o Arduino executa continuamente. Veja como ele é simples – a cada execução:

  1. Se o estado do pino 2 for LOW (ou seja, se ele estiver conectado ao Terra, indicando que este Arduino deve agir como o mestre):
    • Se um número sorteado entre 0 e 4 for 3, chama a função interroga() passando como parâmetro CMD_PING – e, nos demais casos do sorteio, chama a função interroga passando como parâmetro CMD_INTERROGA.
  2. Se o estado do pino 2 não for LOW, conclui-se que este Arduino é o subordinado, e assim é chamada a função responde().

Note que a complexidade do programa foi toda removida do loop e transportada para as 2 funções mencionadas: interroga() e responde(). O loop se limita a chamá-las, dependendo de se identificar como mestre ou subalterno. No caso do mestre, o loop ainda escolhe entre interrogar passando como parâmetro o comando PING ou o comando INTERROGA.

A função interroga() só é executada no mestre, e se encarrega de enviar um comando, aguardar a resposta do subordinado e processá-la.

Retorne agora para o trecho em roxo, que é o início da função interroga(), executada apenas no Arduino que estiver como mestre. Note que a sua primeira linha move para o primeiro elemento da matriz cmd o valor recebido como parâmetro pela função, que – já sabemos – será CMD_PING ou CMD_INTERROGA.

A segunda linha move para o segundo elemento da matriz os 2 últimos bytes da função millis(), que conta o número de milissegundos desde a inicialização. Isso é relevante apenas para o comando CMD_PING, e veremos a razão a seguir. A seguir a nossa já conhecida função enviaMsg() é chamada, e uma descrição do que acaba de acontecer é enviada para o monitor serial.

Neste momento a transmissão já está completa, mas nós queremos receber uma resposta do Arduino subordinado – e aguardar por essa resposta é o que fazem os 2 trechos em cor dourada da mesma função interroga(). Note que é um loop simples, que repetirá 500 vezes o delay de 4 milissegundos (o tempo total será algo pouco acima de 2 segundos), a não ser que a função radio.available() (que informa que um pacote foi recebido pelo rádio) retorne um valor positivo antes disso. Se tiver mensagem, o trecho em cinza será executado, senão a mensagem em dourado no final da função, informando que não foi recebida nenhuma resposta, será exibida.

O trecho em cinza na função interroga() só tem um aspecto realmente diferente: ele usa radio.read para mover para a matriz msg o conteúdo do pacote recebido do rádio (se vários tiverem sido recebidos, ele continua lendo até chegar ao último). A seguir, dependendo do que estiver no primeiro elemento da matriz (CMD_RESPONDEINTERROG ou CMD_RESPONDEPING), ele exibe a resposta da interrogação, ou calcula quanto tempo faz desde que o ping foi enviado (lembra que havíamos colocado o valor de millis() no pacote antes de enviá-lo? Ele terá sido transmitido de volta para nós, como veremos a seguir) e exibe o resultado.

A função responde() só é executada no subalterno, e aguarda por comandos, respondendo-os conforme chegam.

Finalizando, em cor fúcsia, temos a função responde() que, como sabemos, só executada no Arduino que não estiver como mestre. Note que ela inicia com um loop similar ao do trecho em cinza da função anterior, para receber na matriz msg o pacote que tiver sido recebido do rádio – mas, ao contrário da função anterior, ela não tem limite de tempo de espera para isso, porque o subalterno não terá nada para fazer enquanto o mestre não lhe enviar um comando. Após receber um pacote, ele seleciona (via switch) o que fazer, dependendo do comando que tiver sido expresso no primeiro elemento da matriz cmd:

  • se tiver sido um CMD_INTERROGA, envia um CMD_RESPONDEINTERROG, colocando no segundo elemento da matriz um número aleatório entre 8000 e 9000 (aqui você colocaria o valor de um sensor, ou o que estivesse monitorando, em uma aplicação prática).
  • se tiver sido um CMD_PING, envia um CMD_RESPONDEPING, sem alterar o conteúdo do segundo elemento da matriz recebida, que conterá o valor de millis() que foi enviado pelo mestre.

Agora releia o programa para observar que em nenhum momento fizemos referência à seleção de canal, velocidade de transmissão, número e intervalo de tentativas de retransmissão, e outros elementos que o NRF24L01+ oferece. A configuração default é adequada para boa parte dos usos mas, quando você precisar modificar, os métodos da biblioteca RF24 estarão à sua disposição

Note ainda que as funções essenciais de tratamento de erro na camada de aplicação – comando impróprio, resposta imprópria, resposta não recebida a tempo – estão presentes e, como o restante do código, podem ser adaptados à sua aplicação e topologia. Boas transmissões!

 
  1.  A distância, a presença de ruído eletromagnético (seu telefone sem fio, o roteador, o forno de microondas e vários aparelhos com controle remoto podem interferir) e outros elementos do ambiente podem interferir no resultado.

  2.  Os identificadores de pipes constam no programa como números de 5 bytes expressos em hexadecimal e com o identificador LL (que indica o tipo "long long") ao final. Eles podem ser escolhidos arbitrariamente mas, caso seu programa vá ter mais de um canal de recepção simultâneos, os 4 primeiros bytes deles devem ser idênticos entre si.

Comentar

Dos leds ao Arduino, ESP8266 e mais

Aprenda eletrônica com as experiências de um geek veterano dos bits e bytes que nunca tinha soldado um led na vida, e resolveu narrar para você o que descobre enquanto explora esse universo – a partir da eletrônica básica, até chegar aos circuitos modernos.

Por Augusto Campos, autor do BR-Linux e Efetividade.net.

Recomendados

Livro recomendado


Artigos já disponíveis

Comunidade Arduino

O BR-Arduino é integrante da comunidade internacional de entusiastas do Arduino, mas não tem relação com os criadores e distribuidores do produto, nem com os detentores das marcas registradas.

Livros recomendados