terça-feira, setembro 15, 2015

Rádio nRF24L01+: Primeiros Experimentos com o Arduino

Existem várias bibliotecas disponíveis para o nRF24L01+ e inúmeros posts e tutoriais na internet que mostram um exemplo curto de uso delas. Como eu gosto de entender os detalhes, vou começar com um experimento simples que acessa diretamente o nRF24L01+, escrito a partir do datasheet e do código da biblioteca RF24.




Hardware

Neste meu experimento vou fazer a comunicação entre dois Arduinos Nano, cada um com uma tecla e um LED. Quando a tecla for apertada ou solta em um Arduino, ele enviará uma mensagem para o outro acender ou apagar o LED.


Nada de muito especial na montagem. O botão é ligado entre o pino digital 7 e terra. O LED é ligado ao terra e, via um resistor de 220R, ao pino digital 6. Para selecionar endereços diferentes, um dos Arduinos terá o pino digital 8 ligado a terra e o outro o pino digital 8 desconectado.

A placa com o nRF24L01+ é ligada da seguinte forma (neste teste simples não vou usar o sinal IRQ):
  • GND  (1)ao GND ou ISP 6
  • Vcc (2) ao 3V
  • CE (3) ao pino digital 9
  • CSN (4) ao pino digital 10
  • SCK (5) ao pino digital 13 ou ISP 3
  • MOSI (6) ao pino digital 11 ou ISP 4
  • MISO (7) ao pino digital 12 ou ISP 1
A montagem foi feita em uma mini protoboard, a placa de rádio ficou pendurada pois o conector não é apropriado para espetar diretamente.



Software

O software completo pode ser baixado dos arquivos do blog (TestenRF24L01.zip) ou do github.

A comunicação com o nRF24L01+ é feita via SPI, uma transação é iniciada baixando o sinal CSN e finalizada retornando-o ao nível alto. O primeiro byte enviado é um comando, dependendo do comando bytes adicionais podem ser escritos ou lidos. É importante lembrar que a comunicação SPI ocorre sempre simultaneamente nos dois sentidos: a cada byte transmitido é recebido.

Vamos começar vendo duas rotinas simples, para enviar um comando e escrever e ler em um registrador. Existe um comando para leitura e outro para escrita, o número do registrador é colocado no próprio comando:
// Envia comando ao nRF24L01+
uint8_t send_command(uint8_t cmd)
{
  uint8_t status;

  digitalWrite(pinCSN, LOW);
  status = SPI.transfer(cmd);
  digitalWrite(pinCSN, HIGH);
  return status;
}

// Escreve um valor em um registrador do nRF24L01+
uint8_t write_register(uint8_t reg, uint8_t value)
{
  uint8_t status;

  digitalWrite(pinCSN, LOW);
  status = SPI.transfer( W_REGISTER | ( REGISTER_MASK & reg ) );
  SPI.transfer(value);
  digitalWrite(pinCSN, HIGH);
  return status;
}

// Le um registrador do nRF24L01+
uint8_t read_register(uint8_t reg)
{
  uint8_t result;

  digitalWrite(pinCSN, LOW);
  SPI.transfer( R_REGISTER | ( REGISTER_MASK & reg ) );
  result = SPI.transfer(0xff);
  digitalWrite(pinCSN, HIGH);
  #ifdef TRACE
  Serial.print ("Reg ");
  Serial.print (reg, HEX);
  Serial.print (" = ");
  Serial.println (result, HEX);
  #endif
  return result;  
}
Os registradores de endereço, que veremos adiante, são um pouco diferentes. Após o comando de leitura ou escrita são lidos ou escritos vários bytes. A rotina abaixo será usada para escrever um endereço:
// Escreve varios valores em um registrador do nRF24L01+
uint8_t writeN_register(uint8_t reg, uint8_t *pValue, uint8_t n)
{
  uint8_t status;

  digitalWrite(pinCSN, LOW);
  status = SPI.transfer( W_REGISTER | ( REGISTER_MASK & reg ) );
  while (n--)
    SPI.transfer(*pValue++);
  digitalWrite(pinCSN, HIGH);
  return status;
}
Os endereços nos pacotes podem ter de 3 a 5 bytes, vou trabalhar com o mínimo. Para a transmissão existe um registrador, TX_ADDR, onde é deve ser escrito o endereço do destino. A recepção é mais complicada, pois existem seis "pipes" (quase) independentes, numerados de 0 a 5. A ideia é que você pode colocar endereços diferentes nestes seis pipes e receber mensagens com até seis endereços diferentes. Mais precisamente, apenas os pipes 0 e 1 possuem endereços completos (3 a 5 bytes), os pipes 2 a 5 possuem apenas o byte menos significativo do endereço, os bytes mais significativos são os do pipe 1.

Para confundir um pouco mais, o pipe 0 é automaticamente usado pelo recurso de ACK automático do nRF24L01+ . Isto significa que o seu endereço será automaticamente alterado após uma transmissão.

No meu caso vou ficar no mais simples possível, recebendo apenas no pipe 1 e deixando o pipe 0 para a recepção do ACK automático.

Com estas explicações, podemos ver a rotina de iniciação do rádio. Conforme indicado nos cometários, adotei uma configuração simples e fixa para os parâmetros.
// Iniciacao do Radio
void radioInit()
{
  uint8_t addr[3];
  
  Serial.println ("Inciando radio...");
  
  // Incia o SPI
  SPI.begin();
  
  // Inicia os sinais de controle
  pinMode (pinCE, OUTPUT);
  digitalWrite(pinCE, LOW);
  pinMode (pinCSN, OUTPUT);
  digitalWrite(pinCSN, HIGH);

  // Configura o radio
  delay(5);  // para o caso do radio acabar de ser ligado
  write_register(CONFIG, 0b00001100);  // CRC de 16 bits
  write_register(SETUP_RETR, 0x5F);// ate 15 retries, timeout = 1,5ms
  write_register(RF_SETUP, 0x06);  // 1Mbps, Potencia maxima
  write_register(FEATURE,0 );      // trabalhar com pacotes de tamanho fixo
  write_register(DYNPD,0);         // trabalhar com pacotes de tamanho fixo
  write_register(NRF_STATUS, _BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT));
  write_register(RF_CH, 76);       // usar canal 76
  send_command(FLUSH_RX);          // limpa a recepcao
  send_command(FLUSH_TX);          // limpa a transmissao
  write_register(CONFIG, read_register(CONFIG) | _BV(PRIM_RX));  // modo Rx
  powerUp();                       // liga o radio

  // Configura os enderecos  
  addr[0] = addrTx;
  addr[1] = 0;
  addr[2] = 0;
  write_register(SETUP_AW, 1);          // enderecos de 3 bytes
  writeN_register(TX_ADDR, addr, 3);    // endereco de transmissao
  writeN_register(RX_ADDR_P0, addr, 3); // auto ACK
  addr[0] = addrRx;
  writeN_register(RX_ADDR_P1, addr,3);  // endereco de recepcao
  write_register(EN_RXADDR,2);          // recepcao habilitada no pipe 1

  //  Pacotes com 1 byte de dado
  write_register(RX_PW_P1, 1);
  
  Serial.println ("Radio iniciado.");
}

// Liga o radio
void powerUp(void)
{
   uint8_t cfg = read_register(CONFIG);

   // Se estava desligado, liga e espera iniciar
   if (!(cfg & _BV(PWR_UP)))
   {
      write_register(CONFIG, cfg | _BV(PWR_UP));
      delay(5);
   }
}
A minha aplicação vai deixar o rádio com a recepção ligada a maior parte do tempo. Para ligar a recepção basta selecionar recepção no registrador CONFIG e levantar o sinal CE, o resto dos registradores já foram programados na iniciação. Fora isto, os indicadores no registrador de status são limpos:
// Inicia a recepcao
void startRx(void)
{
  write_register(CONFIG, read_register(CONFIG) | _BV(PRIM_RX));
  write_register(NRF_STATUS, _BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT) );
  digitalWrite(pinCE, HIGH);
}
Para verificar se ocorreu uma recepção basta testar o bit correspondente no registrador de status:
// Testa se tem recepcao
uint8_t temRx(void)
{
  return read_register(NRF_STATUS) & _BV(RX_DR);
}
Detectada uma recepção, os bytes de dados (payload) podem ser lidos através do comando R_RX_PAYLOAD. No meu caso será um único byte:
// Le o dado recebido e limpa flag de recebido
uint8_t rxDado(void)
{
  uint8_t dado;
  
  digitalWrite(pinCSN, LOW);
  SPI.transfer(R_RX_PAYLOAD);
  dado = SPI.transfer(0xFF);
  digitalWrite(pinCSN, HIGH);
  write_register(NRF_STATUS, _BV(RX_DR));
  return dado;  
}
Para desligar a recepção, é preciso baixar o sinal CE, aguardar uma eventual recepção em curso ser finalizada (inclusive o envio do ACK) e depois chavear para transmissão no registrador CONFIG.
// Para a recepcao
void stopRx(void)
{
  // Desliga RX
  digitalWrite(pinCE, LOW);
  delayMicroseconds(65);   // Tempo de recepcao
  delayMicroseconds(65);   // Tempo de envio de ACK
  send_command(FLUSH_TX);  // Limpa eventual ACK
  write_register(CONFIG, (read_register(CONFIG) ) & ~_BV(PRIM_RX));
  
  // Habilita recepcao no pipe 0 (para ACK)
  write_register(EN_RXADDR,read_register(EN_RXADDR) | 1);
}
Uma vez desligada a recepção, podemos disparar uma transmissão. Os dados devem ser enviados em uma única transação SPI, usando o comando W_TX_PAYLOAD. Escritos os bytes, basta levantar o sinal CE para automagicamente o pacote ser transmitido e retransmitido até receber um ACK ou estourar o limite de retransmissões. Ambas ocorrências (sucesso ou erro) são sinalizadas no registrador de status. Ao final da transmissão o sinal CE é devolvido ao nível baixo.
// Envia um byte
uint8_t txDado (uint8_t dado)
{
  uint8_t status;
  
  // Coloca na fila o byte a transmitir
  digitalWrite(pinCSN, LOW);
  SPI.transfer(W_TX_PAYLOAD);
  SPI.transfer(dado);
  digitalWrite(pinCSN, HIGH);
  
  // Dispara a transmissao
  digitalWrite(pinCE, HIGH);
  
  // Espera concluir
  while ((read_register(NRF_STATUS) & (_BV(TX_DS) | _BV(MAX_RT))) == 0)
    ;
    
  // Desligar o transmissor
  digitalWrite(pinCE, LOW);

  // desliga recepcao no pipe 0
  write_register(EN_RXADDR,2);  
  
  // Verifica o resultado
  status = write_register(NRF_STATUS,_BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT));
  if( status & _BV(MAX_RT))
  {
    send_command(FLUSH_TX);
    return 0;
  }
  else
  {
    return 1;
  }
}
Para completar o programa, temos a iniciação geral e a aplicação principal:
void setup() 
{
  // Serial para debug
  Serial.begin(9600);

  // Iniciacao dos pinos de LED, Botao e Selecao do endereco
  pinMode (pinLED, OUTPUT);
  pinMode (pinBotao, INPUT);
  digitalWrite (pinBotao, HIGH);
  pinMode (pinSelAddr, INPUT);
  digitalWrite (pinSelAddr, HIGH);

  // Seleciona os enderecos
  if (digitalRead(pinSelAddr) == LOW)
  {
    addrRx = 1;
    addrTx = 2;
  }
  else
  {
    addrRx = 2;
    addrTx = 1;
  }
  Serial.print("Recebendo no endereco ");
  Serial.println(addrRx);

  // Iniciacao do Radio
  radioInit();
  startRx();
}

void loop() 
{
  uint8_t botao;
  
  botao = digitalRead(pinBotao);
  if (botao != botaoAnt)
  {
    botaoAnt = botao;
    Serial.print ("Transmitindo: ");
    stopRx();
    if (txDado (botao))
       Serial.println ("Ok");
    else    
       Serial.println ("Erro");
    startRx();
  }
  else if (temRx())
  {
    uint8_t dado = rxDado();
    Serial.print("Recebido ");
    Serial.println(dado);
    if (dado == LOW)
      digitalWrite(pinLED, HIGH);
    else
      digitalWrite(pinLED, LOW);
  }
}

O que sobra (e pode ser visto no programa completo) são as definições das constantes.

No próximo post vamos ver esta mesma aplicação usando a biblioteca RF24.

05/04/16: Deixado mais claro que o pino de seleção de endereço é o digital 8 do Arduino.

7 comentários:

Tom Ferreira - Webmaster disse...

Boa noite, Daniel,

Sou novato em transmissão de dados. Usei um par do módulo RF 433Mhz para enviar e receber dados no mesmo arduino e ficou bom, porém a conexão depende de uma antena bem grande.

Agora estou tentando usar o NRF, mas é bem complicado para quem inicia. Gostaria de montar um circuito onde o mesmo Arduino pudesse transmitir e receber.

Além disso, gostaria de transmitir para outros Arduinos ao redor, como se eu montasse uma rede, por exemplo: aciono um botão no Arduino A e no Arduino B, C, D... liga um LED... não sei até quantas conexões dessa posso conseguir.

Poderia, por favor, me sinalizar de uma forma simples como fazer e se tem como?

Desde já muito obrigado e parabéns pelo seu trabalho e explicação. Vou tentar usar um pouco do que li aqui.

Abraços. Tom

Daniel Quadros disse...

Tom,

O alcance deste módulo que estou usando não é muito grande, para ter um alcance razoável é preciso um amplificador e uma antena externa, como este módulo aqui.

A montagem do hardware é simples, o que é complicado com o nRF24L01+ é o software. O jeito mais fácil de fazer o que você quer é usando a biblioteca RF24, veja os meus posts sobre ela.

Unknown disse...

boas;

estou tentado o seu exemplos ,mas sempre que pressiono o botão de pressão diz tramiter erro tanto num como no outro .
estou usando dois nano com chip ch304 e não estou usado os isp deles .
quanto ao pin 8 ligado o terra tem de se programar o modulo com pino ligado ao terra ou so se liga depois de programado ?
se poder me dar ai uma mãozinha agradecia

Daniel Quadros disse...

Luís, você deve montar um dos Arduinos com o pino 8 ligado ao terra e o outro com o pino 8 sem conexão. Desta forma eles ficarão com endereço diferente. O pino 8 é testado na iniciação (rotina setup()).

Unknown disse...

sim eu sei isso mas faço isso e carrego o programa com o pino ligado ou na transferência de dados não importa só quando se liga os 2 e que se liga o pino 8 (irq) ao gnd ?

Daniel Quadros disse...

Luís, é o pino 8 (Digital 8) do Arduino, não do rádio. O pino 8 precisa estar na posição correta no início da execução do programa, não faz diferença na hora da carga. Se você mudar a ligação do pino 8 depois de carregar o programa, dê um reset para o pino ser lido novamente.

Unknown disse...

já funciona xd.
realmente foi falta de atenção minha , não ter realmente reparado que era o pino 8 do arduino não do radio .

a única coisa fora de anormal e que um dos radio ao transmitir diz transmitir erro mas faz a transmissão .
O outro já não, transmiti e diz transmissão ok .