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:
  1. // Envia comando ao nRF24L01+  
  2. uint8_t send_command(uint8_t cmd)  
  3. {  
  4.   uint8_t status;  
  5.   
  6.   digitalWrite(pinCSN, LOW);  
  7.   status = SPI.transfer(cmd);  
  8.   digitalWrite(pinCSN, HIGH);  
  9.   return status;  
  10. }  
  11.   
  12. // Escreve um valor em um registrador do nRF24L01+  
  13. uint8_t write_register(uint8_t reg, uint8_t value)  
  14. {  
  15.   uint8_t status;  
  16.   
  17.   digitalWrite(pinCSN, LOW);  
  18.   status = SPI.transfer( W_REGISTER | ( REGISTER_MASK & reg ) );  
  19.   SPI.transfer(value);  
  20.   digitalWrite(pinCSN, HIGH);  
  21.   return status;  
  22. }  
  23.   
  24. // Le um registrador do nRF24L01+  
  25. uint8_t read_register(uint8_t reg)  
  26. {  
  27.   uint8_t result;  
  28.   
  29.   digitalWrite(pinCSN, LOW);  
  30.   SPI.transfer( R_REGISTER | ( REGISTER_MASK & reg ) );  
  31.   result = SPI.transfer(0xff);  
  32.   digitalWrite(pinCSN, HIGH);  
  33.   #ifdef TRACE  
  34.   Serial.print ("Reg ");  
  35.   Serial.print (reg, HEX);  
  36.   Serial.print (" = ");  
  37.   Serial.println (result, HEX);  
  38.   #endif  
  39.   return result;    
  40. }  
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:
  1. // Escreve varios valores em um registrador do nRF24L01+  
  2. uint8_t writeN_register(uint8_t reg, uint8_t *pValue, uint8_t n)  
  3. {  
  4.   uint8_t status;  
  5.   
  6.   digitalWrite(pinCSN, LOW);  
  7.   status = SPI.transfer( W_REGISTER | ( REGISTER_MASK & reg ) );  
  8.   while (n--)  
  9.     SPI.transfer(*pValue++);  
  10.   digitalWrite(pinCSN, HIGH);  
  11.   return status;  
  12. }  
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.
  1. // Iniciacao do Radio  
  2. void radioInit()  
  3. {  
  4.   uint8_t addr[3];  
  5.     
  6.   Serial.println ("Inciando radio...");  
  7.     
  8.   // Incia o SPI  
  9.   SPI.begin();  
  10.     
  11.   // Inicia os sinais de controle  
  12.   pinMode (pinCE, OUTPUT);  
  13.   digitalWrite(pinCE, LOW);  
  14.   pinMode (pinCSN, OUTPUT);  
  15.   digitalWrite(pinCSN, HIGH);  
  16.   
  17.   // Configura o radio  
  18.   delay(5);  // para o caso do radio acabar de ser ligado  
  19.   write_register(CONFIG, 0b00001100);  // CRC de 16 bits  
  20.   write_register(SETUP_RETR, 0x5F);// ate 15 retries, timeout = 1,5ms  
  21.   write_register(RF_SETUP, 0x06);  // 1Mbps, Potencia maxima  
  22.   write_register(FEATURE,0 );      // trabalhar com pacotes de tamanho fixo  
  23.   write_register(DYNPD,0);         // trabalhar com pacotes de tamanho fixo  
  24.   write_register(NRF_STATUS, _BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT));  
  25.   write_register(RF_CH, 76);       // usar canal 76  
  26.   send_command(FLUSH_RX);          // limpa a recepcao  
  27.   send_command(FLUSH_TX);          // limpa a transmissao  
  28.   write_register(CONFIG, read_register(CONFIG) | _BV(PRIM_RX));  // modo Rx  
  29.   powerUp();                       // liga o radio  
  30.   
  31.   // Configura os enderecos    
  32.   addr[0] = addrTx;  
  33.   addr[1] = 0;  
  34.   addr[2] = 0;  
  35.   write_register(SETUP_AW, 1);          // enderecos de 3 bytes  
  36.   writeN_register(TX_ADDR, addr, 3);    // endereco de transmissao  
  37.   writeN_register(RX_ADDR_P0, addr, 3); // auto ACK  
  38.   addr[0] = addrRx;  
  39.   writeN_register(RX_ADDR_P1, addr,3);  // endereco de recepcao  
  40.   write_register(EN_RXADDR,2);          // recepcao habilitada no pipe 1  
  41.   
  42.   //  Pacotes com 1 byte de dado  
  43.   write_register(RX_PW_P1, 1);  
  44.     
  45.   Serial.println ("Radio iniciado.");  
  46. }  
  47.   
  48. // Liga o radio  
  49. void powerUp(void)  
  50. {  
  51.    uint8_t cfg = read_register(CONFIG);  
  52.   
  53.    // Se estava desligado, liga e espera iniciar  
  54.    if (!(cfg & _BV(PWR_UP)))  
  55.    {  
  56.       write_register(CONFIG, cfg | _BV(PWR_UP));  
  57.       delay(5);  
  58.    }  
  59. }  
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:
  1. // Inicia a recepcao  
  2. void startRx(void)  
  3. {  
  4.   write_register(CONFIG, read_register(CONFIG) | _BV(PRIM_RX));  
  5.   write_register(NRF_STATUS, _BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT) );  
  6.   digitalWrite(pinCE, HIGH);  
  7. }  
Para verificar se ocorreu uma recepção basta testar o bit correspondente no registrador de status:
  1. // Testa se tem recepcao  
  2. uint8_t temRx(void)  
  3. {  
  4.   return read_register(NRF_STATUS) & _BV(RX_DR);  
  5. }  
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:
  1. // Le o dado recebido e limpa flag de recebido  
  2. uint8_t rxDado(void)  
  3. {  
  4.   uint8_t dado;  
  5.     
  6.   digitalWrite(pinCSN, LOW);  
  7.   SPI.transfer(R_RX_PAYLOAD);  
  8.   dado = SPI.transfer(0xFF);  
  9.   digitalWrite(pinCSN, HIGH);  
  10.   write_register(NRF_STATUS, _BV(RX_DR));  
  11.   return dado;    
  12. }  
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.
  1. // Para a recepcao  
  2. void stopRx(void)  
  3. {  
  4.   // Desliga RX  
  5.   digitalWrite(pinCE, LOW);  
  6.   delayMicroseconds(65);   // Tempo de recepcao  
  7.   delayMicroseconds(65);   // Tempo de envio de ACK  
  8.   send_command(FLUSH_TX);  // Limpa eventual ACK  
  9.   write_register(CONFIG, (read_register(CONFIG) ) & ~_BV(PRIM_RX));  
  10.     
  11.   // Habilita recepcao no pipe 0 (para ACK)  
  12.   write_register(EN_RXADDR,read_register(EN_RXADDR) | 1);  
  13. }  
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.
  1. // Envia um byte  
  2. uint8_t txDado (uint8_t dado)  
  3. {  
  4.   uint8_t status;  
  5.     
  6.   // Coloca na fila o byte a transmitir  
  7.   digitalWrite(pinCSN, LOW);  
  8.   SPI.transfer(W_TX_PAYLOAD);  
  9.   SPI.transfer(dado);  
  10.   digitalWrite(pinCSN, HIGH);  
  11.     
  12.   // Dispara a transmissao  
  13.   digitalWrite(pinCE, HIGH);  
  14.     
  15.   // Espera concluir  
  16.   while ((read_register(NRF_STATUS) & (_BV(TX_DS) | _BV(MAX_RT))) == 0)  
  17.     ;  
  18.       
  19.   // Desligar o transmissor  
  20.   digitalWrite(pinCE, LOW);  
  21.   
  22.   // desliga recepcao no pipe 0  
  23.   write_register(EN_RXADDR,2);    
  24.     
  25.   // Verifica o resultado  
  26.   status = write_register(NRF_STATUS,_BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT));  
  27.   if( status & _BV(MAX_RT))  
  28.   {  
  29.     send_command(FLUSH_TX);  
  30.     return 0;  
  31.   }  
  32.   else  
  33.   {  
  34.     return 1;  
  35.   }  
  36. }  
Para completar o programa, temos a iniciação geral e a aplicação principal:
  1. void setup()   
  2. {  
  3.   // Serial para debug  
  4.   Serial.begin(9600);  
  5.   
  6.   // Iniciacao dos pinos de LED, Botao e Selecao do endereco  
  7.   pinMode (pinLED, OUTPUT);  
  8.   pinMode (pinBotao, INPUT);  
  9.   digitalWrite (pinBotao, HIGH);  
  10.   pinMode (pinSelAddr, INPUT);  
  11.   digitalWrite (pinSelAddr, HIGH);  
  12.   
  13.   // Seleciona os enderecos  
  14.   if (digitalRead(pinSelAddr) == LOW)  
  15.   {  
  16.     addrRx = 1;  
  17.     addrTx = 2;  
  18.   }  
  19.   else  
  20.   {  
  21.     addrRx = 2;  
  22.     addrTx = 1;  
  23.   }  
  24.   Serial.print("Recebendo no endereco ");  
  25.   Serial.println(addrRx);  
  26.   
  27.   // Iniciacao do Radio  
  28.   radioInit();  
  29.   startRx();  
  30. }  
  31.   
  32. void loop()   
  33. {  
  34.   uint8_t botao;  
  35.     
  36.   botao = digitalRead(pinBotao);  
  37.   if (botao != botaoAnt)  
  38.   {  
  39.     botaoAnt = botao;  
  40.     Serial.print ("Transmitindo: ");  
  41.     stopRx();  
  42.     if (txDado (botao))  
  43.        Serial.println ("Ok");  
  44.     else      
  45.        Serial.println ("Erro");  
  46.     startRx();  
  47.   }  
  48.   else if (temRx())  
  49.   {  
  50.     uint8_t dado = rxDado();  
  51.     Serial.print("Recebido ");  
  52.     Serial.println(dado);  
  53.     if (dado == LOW)  
  54.       digitalWrite(pinLED, HIGH);  
  55.     else  
  56.       digitalWrite(pinLED, LOW);  
  57.   }  
  58. }  

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 .