LoRa com Raspberry e Arduino

Estou trabalhando em um projeto cujo hardware não pode ser divulgado. A imagem de destaque é o vazamento desse produto, do qual não posso sequer citar as especificações, mas enfim, é um hat para Raspberry e tem LoRa e fiz o teste inicial do LoRa com Raspberry e Arduino, com um bocado de esforço. Detalho.

LoRa com Raspberry e Arduino

Rádio é rádio, certo? O Driver é o mesmo para ambos, estou utilizando RadioHead, instalável pelo gerenciador de biblioteca do Arduino ou você pode pegá-lo nesse link.

Se quiser placas LoRa industriais, visite a AFE e faça sua escolha. Não é custo de hardware chinês, já adianto. É hardware profissional, mas nada impede que hobistas abusados (como eu) as tenham.

Não foi uma configuração transparente por diversos motivos e é importante salientar os pontos aqui.

Configurar LoRA rf95 no Raspberry (bcm2835)

No Raspberry não basta a biblioteca RadioHead. É necessário instalar previamente a biblioteca BCM2835, que dá acesso aos GPIO e outras funções do chip usado no Raspberry. Pegue-a nesse link. Se seu Raspberry 3 não estiver preparado para desenvolvimento, as dependências surgirão durante o processo a seguir:

tar zxvf bcm2835-1.xx.tar.gz
cd bcm2835-1.xx
./configure
make
sudo make check
sudo make install

Se estiver utilizando Raspberry Pi 2, execute raspi-config e em Advanced Options habilite a opção Enable Device Tree, então salve e reinicie-o antes de proceder com a configuração da biblioteca supracitada.

Pinos padrão SPI do Raspberry

Os pinos padrão são:

  • P1-19 (MOSI)
  • P1-21 (MISO)
  • P1-23 (CLK)
  • P1-24 (CE0)
  • P1-26 (CE1)

Para multi-rádio, uma implementação extra é necessária, mas isso só poderei mostrar quando a AF Eletrônica tiver um hardware para tal.

SPI auxiliar do Raspberry

Os pinos auxiliares são:

  • P1-38 (MOSI)
  • P1-35 (MISO)
  • P1-40 (CLK)
  • P1-36 (CE2)

Esse é o segundo SPI do Raspberry (wow).

Mais informações sobre a BMC2835 podem ser encontradas aqui.

Desabilite o SPI se estiver habilitado (!)

Estranho, não? Nem tanto, na verdade.

Através do configurador raspi-config podemos habilitar a interface SPI, entre outras. Porém, se estiver utilizando a biblioteca BCM2835, é necessário desabilitar o módulo do kernel Linux porque a biblioteca BMC2835 interage em baixo nível com o hardware, independente do módulo carregado pelo kernel.

Entre no configurador digitando raspi-config, então vá em Interface Options > SPI e desabilite-a.

Baixe a biblioteca RadioHead

Nesse ponto você já deve ter reiniciado seu Raspberry, caso a interface SPI estivesse habilitada. Tendo compilado e instalado a biblioteca BCM2835, agora baixe a biblioteca RadioHead. Por exemplo:

wget -c http://www.airspayce.com/mikem/arduino/RadioHead/RadioHead-1.89.zip
unzip RadioHead-1.89.zip
cd RadioHead/examples/raspi/rf95

Nesse diretório que você deverá entrar após a extração do pacote baixado, estarão contidos os arquivos Makefile, rf95_client.cpprf95_server.cpp.

Detalhes importantes (fundamentais?)

O exemplo de client e server é para comunicação entre dois Raspberry Pi. No primeiro momento, eu precisava provar o conceito, sem endereçar os destinos dos pacotes (e tem muitas razões para isso). Desse modo, o rádio LoRa da placa da AF Eletrônica joga a informação no ar para quem quiser pegar. Para que a recepção ocorresse, foi necessário escrever algumas modificações no exemplo do server (que é o Raspberry).

Antes de expôr o código de exemplo, devemos considerar alguns pontos.

Não use interrupção

no site do RadioHead foi relatado que existe um bug conhecido e não solucionado, que não permite a utilização de interrupção do rádio. Antes de tomar conhecimento, eu já havia feito as configurações e testado. Com poucos minutos o Raspberry congelava, portanto, não tente usar interrupção até que o bug seja solucionado.

Declare os pinos da placa no arquivo ../RaspBoards.h

Esse arquivo contém a definição dos pinos para cada tipo de placa. Como a que estou utilizando para desenvolvimento ainda não está no mercado, tive que fazer as declarações. Vou dar como exemplo um dos modelos conhecidos:

// Dragino Raspberry PI hat (no obboard led)
// =========================================
// see https://github.com/dragino/Lora
#elif defined (BOARD_DRAGINO_PIHAT)
#define RF_CS_PIN  RPI_V2_GPIO_P1_22 // Slave Select on GPIO25 so P1 connector pin #22
#define RF_IRQ_PIN RPI_V2_GPIO_P1_07 // IRQ on GPIO4 so P1 connector pin #7
#define RF_RST_PIN RPI_V2_GPIO_P1_11 // Reset on GPIO17 so P1 connector pin #11
#define RF_LED_PIN NOT_A_PIN                // No onboard led to drive

No Raspberry não devemos utiizar a interrupção, portanto defina o pino como NOT_A_PIN.

Ajuste a frequência

O padrão é 434MHz à 13dBm, mas no EUA a regulamentação convenciona o uso de +14dBm, por isso é necessário modificar ou comentar a linha rf95.setTxPower.

Modifique também a frequência no define correspondente para 915.00.

Código do rf95_server.cpp modificado

Baseado no código de exemplo, as modificações resultaram em:

#include <bcm2835.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

#include <RH_RF69.h>
#include <RH_RF95.h>

// AFEletronica MR
// see <link unavailable yet>
#define BOARD_AF_ELETRONICA

// Now we include RasPi_Boards.h so this will expose defined
// constants with CS/IRQ/RESET/on board LED pins definition
#include "../RasPiBoards.h"

// Our RFM95 Configuration
#define RF_FREQUENCY  915.00

// Create an instance of a driver
RH_RF95 rf95(RF_CS_PIN, RF_IRQ_PIN);
//RH_RF95 rf95(RF_CS_PIN);

//Flag for Ctrl-C
volatile sig_atomic_t force_exit = false;

void sendAnswer(){
  char radiopacket[20] = "Good job!   #      ";
  //itoa(packetnum++, radiopacket+13, 10);
  printf("\nAnswering now...\n");
  radiopacket[19] = 0;
  rf95.setModeTx();
  rf95.send((uint8_t *)radiopacket, 20);
  rf95.waitPacketSent();
  rf95.setModeRx();

}

void sig_handler(int sig)
{
  printf("\n%s Break received, exiting!\n", __BASEFILE__);
  force_exit=true;
}

//Main Function
int main (int argc, const char* argv[] )
{
  unsigned long led_blink = 0;

  signal(SIGINT, sig_handler);
  //printf( "%s\n", __BASEFILE__);

  if (!bcm2835_init()) {
    fprintf( stderr, "%s bcm2835_init() Failed\n\n", __BASEFILE__ );
    return 1;
  }

#ifdef RF_RST_PIN
  //printf( ", RST=GPIO%d", RF_RST_PIN );
  // Pulse a reset on module
  pinMode(RF_RST_PIN, OUTPUT);
  digitalWrite(RF_RST_PIN, LOW );
  bcm2835_delay(150);
  digitalWrite(RF_RST_PIN, HIGH );
  bcm2835_delay(100);
#endif

  if (!rf95.init()) {
    fprintf( stderr, "\nRF95 module init failed, Please verify wiring/module\n" );
  } else {
    // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on

    // The default transmitter power is 13dBm, using PA_BOOST.
    // If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then
    // you can set transmitter powers from 5 to 23 dBm:
    //  driver.setTxPower(23, false);
    // If you are using Modtronix inAir4 or inAir9,or any other module which uses the
    // transmitter RFO pins and not the PA_BOOST pins
    // then you can configure the power transmitter power for -1 to 14 dBm and with useRFO true.
    // Failure to do that will result in extremely low transmit powers.
    // rf95.setTxPower(14, true);


    // RF95 Modules don't have RFO pin connected, so just use PA_BOOST
    // check your country max power useable, in EU it's +14dB
    rf95.setTxPower(13, false);

    // You can optionally require this module to wait until Channel Activity
    // Detection shows no activity on the channel before transmitting by setting
    // the CAD timeout to non-zero:
    //rf95.setCADTimeout(10000);

    // Adjust Frequency
    rf95.setFrequency(RF_FREQUENCY);

    // Be sure to grab all node packet
    // we're sniffing to display, it's a demo
    rf95.setPromiscuous(true);

    // We're ready to listen for incoming message
    rf95.setModeRx();

    //Begin the main body of code
    while (!force_exit) {

        if (rf95.available()) {
          // Should be a message for us now
          uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
          uint8_t len  = sizeof(buf);
          uint8_t from = rf95.headerFrom();
          uint8_t to   = rf95.headerTo();
          uint8_t id   = rf95.headerId();
          uint8_t flags= rf95.headerFlags();;
          int8_t rssi  = rf95.lastRssi();

          if (rf95.recv(buf, &len)) {
            printf("Ok!\n");
            printbuffer(buf, len);
        printf("\n%s\n",buf);
            sendAnswer();

          } else {
            Serial.print("receive failed");
          }
          printf("\n");
        }

      // Let OS doing other tasks
      // For timed critical appliation you can reduce or delete
      // this delay, but this will charge CPU usage, take care and monitor
      bcm2835_delay(5);
    }
  }
  printf( "\n%s Ending\n", __BASEFILE__ );
  bcm2835_close();
  return 0;
}

Deixei o programa original tanto quanto foi possível. Tendo feito essas modificações, agora basta compilar e executar:

make
sudo ./rf95_server

O programa ficará em execução aguardando por qualquer mensagem.

Configurar LoRa rf95 no Arduino

Estou utilizando também uma placa da AF Eletrônica. No caso, essa (link para compra – aproveite porque está absurdamente barata), da imagem abaixo (descrita nesse artigo).

Essa placa utiliza um Arduino Pro-mini 3v3 (pode ser 5v) e tem slot para RF4463Pro e LoRa1276, à sua escolha. Estou utilizando LoRa1276 nela.

Para instalar a biblioteca, vá ao gerenciador de bibliotecas da IDE do Arduino e digite RadioHead. Instale-a. Após, use esse código no sketch:

#include <SPI.h>
#include <RH_RF95.h>

// Change to 434.0 or other frequency, must match RX's freq!
#define RF95_FREQ 915.0

// Singleton instance of the radio driver
RH_RF95 rf95;

void setup()
{
  while (!Serial);
  Serial.begin(115200);
  delay(100);

  while (!rf95.init()) {
    Serial.println("LoRa radio init failed");
    while (1);
  }
  Serial.println("LoRa radio init OK!");

  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  if (!rf95.setFrequency(RF95_FREQ)) {
    Serial.println("setFrequency failed");
    while (1);
  }
  Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);

  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on

  // The default transmitter power is 13dBm, using PA_BOOST.
  // If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then
  // you can set transmitter powers from 5 to 23 dBm:
  rf95.setTxPower(13, false);
}

int16_t packetnum = 0;  // packet counter, we increment per xmission

void loop()
{
  Serial.println("Sending to rf95_server");
  // Send a message to rf95_server

  char radiopacket[20] = "Hello World #      ";
  itoa(packetnum++, radiopacket+13, 10);
  Serial.print("Sending "); Serial.println(radiopacket);
  radiopacket[19] = 0;

  Serial.println("Sending..."); delay(10);
  rf95.send((uint8_t *)radiopacket, 20);

  Serial.println("Waiting for packet to complete..."); delay(10);
  rf95.waitPacketSent();
  // Now wait for a reply
  uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
  uint8_t len = sizeof(buf);

  Serial.println("Waiting for reply..."); delay(10);
  if (rf95.waitAvailableTimeout(1000))
  {
    // Should be a reply message for us now
    if (rf95.recv(buf, &len))
   {
      Serial.print("Got reply: ");
      Serial.println((char*)buf);
      Serial.print("RSSI: ");
      Serial.println(rf95.lastRssi(), DEC);
    }
    else
    {
      Serial.println("Receive failed");
    }
  }
  else
  {
    Serial.println("No reply, is there a listener around?");
  }
  delay(1000);
}

Bem mais simples que o Raspberry, hum? Mas convenhamos que agora trata-se de uma microcontroladora e um driver direto, com tudo já definido. Claro que o mínimo foi necessário; ajustar txPower para 915MHz, 13dBm. Subindo o sketch, a comunicação já deve começar em um ou dois segundos e o resultado deve ser algo como:

Espero que esse artigo lhe seja útil, porque foi muito duro chegar nesse ponto.

Meu ambiente consiste de um Raspberry Pi 3 na sala (piso inferior da casa) e um AFSmartRadio no meu quarto (piso superior). A imagem do terminal do Raspberry mostrada acima é de um acesso por SSH, enquanto a IDE Atom (ao fundo) é local, no notebook.

Até a próxima!