S Lazy-H
  • Home
  • About
  • Posts
  • Contact
  • Slide Rules
  • A Biker’s Tale

LoRa Project Part 3

pico
Raspberry Pi
software
Author

Sam Hutchins

Published

July 17, 2025

This is a continuation of the LoRa Project started in Part 1 and Part 2 where we are developing a system to pass messages from a Client to a Server in a peer-to-peer (P2P) configuration. The first introduced some of the groundwork, the second covered some of the issues and functions for the Client (transmitter) side. This post will cover more of the Server (receiver) functions and methods.

However, before we get into that, I wish to entertain the “rest of the story” for the HC-SR501 PIR sensor1 functions. It is really rather simple to implement as there are only three wires for the PIR module, power (5V), Ground, and Output (3.3V). The PIR module will not work reliably without a full 5V DC input, and will just pulse endlessly on 3.3V power input.

Two small functions are used to handle the PIR, one for calibration and one to check the PIR pin status. The calibration is nothing more that a waiting period on power up to allow the module time to set itself for operation. This is the simple delay routine:

Code
static void calibratePIR() {
  cout << "Calibrating PIR." << endl;
  for (uint counter = 0; counter < 60; counter++){
    blinkLED();
  }    
  cout << "\nCalibration completed" << endl;
}

The blinkLED() function introduces a delay of about 1/2 second. The documentation indicates about a minute would be required, but it seems to work fine on 30 seconds. If the range or operation seems off, I may change it to do a minute delay. The function to check the PIR’s status on the output pin is as follows:

Code
static int checkPIR() {
  if (gpio_get(PIR_PIN)) { // get pin status
    cout << "===> MOTION DETECTED! <===" << endl;
    return 1; // true
  } else { return 0; // false
  }
}

One final thought on the Client side was where I attempted to place the BME280 polling routine in a function, but ran into issues as the sensor functions are in a class instance, and I spent too much time trying to get it to find the definitions, so decided to leave those lines of code in the main body as noted in Part 2.

One common function involves the Pico interface, both for USB output (minicom) and control of the RFM95x module, and is essentially the first task before use. In the below function, I am using SPI0, but SPI1 could be used.

Code
static void pico_rfm_interface() {
  /* setup SPI interface */
  spi_init(spi0, 5000 * 1000); // 5 MHz ???
  gpio_set_function(RX_MISO, GPIO_FUNC_SPI); // SPI0 RX/MISO
  gpio_set_function(SCK, GPIO_FUNC_SPI); // SPI0 SCK
  gpio_set_function(TX_MOSI, GPIO_FUNC_SPI); // SPI0 TX/MOSI
  gpio_set_function(CS, GPIO_FUNC_SPI); // SPI0 CS
  // Chip select is active-low
  gpio_init(CS); // SPI0 CS (NSS)
  gpio_set_dir(CS, GPIO_OUT);
  /* Setup manual reset pin*/
  gpio_init(RESET_PIN);
  gpio_set_dir(RESET_PIN, GPIO_OUT);
  gpio_pull_up(RESET_PIN);
  /* Setup GPIO for RFM95 DIO0. See header file for actual Pico pin */
  gpio_init(DIO0);
  gpio_set_dir(DIO0, GPIO_IN);
  /* configure FIFO for RX and TX. */
  write_register(REG_0E_FIFO_TX_BASE_ADDR, TX_BASE);
  write_register(REG_0F_FIFO_RX_BASE_ADDR, RX_BASE);
}

The RFM95x has six DIOx signals brought out on the breakout board, but only DIO0 is needed if only RXdone, TXdone and CADdone are needed. As noted in the comment above, the actual pins are defined in a header file. I use three files in addition to the two Client/Server files. Those I have named RFM95fun.cpp, RFM95.h, and RFM95reg.h. The old style where everything is in one file, with the functions defined, then the main part, followed by the actual functions, make for a cumbersome program to navigate and update, and isn’t object-oriented.2

Now, on to the receiver side… As on the Client/transmitter, the Server/receiver frequency is straightforward to set, as shown in the below function. Both modules must be on the same frequency.

Code
static uint8_t set_frequency(double FREQ) { // set operating frequency
  int frf = (FREQ * 1000000) / FSTEP;
  write_register(REG_06_FRF_MSB, (frf >> 16) & 0xff);
  write_register(REG_07_FRF_MID, (frf >> 8) & 0xff);
  write_register(REG_08_FRF_LSB, frf & 0xff);
  return 0;
}

Then, to verify the frequency, I use this simple function. I want to know!

Code
static void frequency() {
  unsigned char id;
  int msb,mid,lsb;
  read_register(static_cast<int>(REG_06_FRF_MSB), &id, 2);
  msb = id;
  read_register(static_cast<int>(REG_07_FRF_MID), &id, 2);
  mid = id;
  read_register(static_cast<int>(REG_08_FRF_LSB), &id, 2);
  lsb = id;
  uint32_t final = (FSTEP * ((msb << 16) | (mid << 8) | lsb)) /1000000;
  cout << "\nCarrier frequency: " << dec << static_cast<int>(final) << " MHz.\n" << endl;
}

The next step is setting up the MODEM registers for transmission and reception parameters, as below. These would be set as the user desired. Of course, we can’t forget whom we wish to send to (Server), and who we are (Client).

Code
  // Define transceiver parameters.
  /* MODEM1: Sig. BW = 125 kHz, Error Coding Rate = 4/5, default Explicit header mode */
  uint8_t Modem1 = BW_125KHZ | CODING_RATE_4_5; // 0x72; 
  /* MODEM2: Spreading Factor = 128 chips/symbol, CRC enabled */
  uint8_t Modem2 = SPREADING_FACTOR_128CPS | PAYLOAD_CRC_ON; // 0x74;
  /* MODEM3: LNA gain set by internal AGC */
  uint8_t Modem3 = AGC_AUTO_ON; // 0x04;
  set_modem(Modem1, Modem2, Modem3); // Set transceive parameters
  
  // Setup addresses
  uint8_t CLIENT = 0x01; // this node, sender
  uint8_t SERVER = 0x02; // recipient of packet
  uint8_t identifier = 0x00; // sequence count for reliable datagram mode
  uint8_t flags = 0x00; //

Lastly, in the main() section, the while() loop is where everything happens. And a bit of eye candy to indicate when we receive a message.

Code
  while (1) {
      // set receive function
      receive(SERVER, CLIENT); // goto receive continuous mode and wait for RXdone interrupt
      blinkLED();
  }

Now, on to the functions where these things happen, disguised as receive(). On the Client side, we sent the recipient and the sender as the first two bytes of the message header. So, we now retrieve those values to determine if the message is intended for this server.

Code
static int receive(uint8_t SERVER, uint8_t CLIENT) {
  uint8_t val = 0;
  write_register(REG_01_OP_MODE, RX_CONT_MODE); // LoRa mode changed only in Sleep mode. Set receive continuous mode
  write_register(REG_40_DIO_MAPPING1, DIO0_RXdone); // RXDone flag
  //write_register(REG_0D_FIFO_ADDR_PTR, RX_BASE); // set start point
  cout << "Receive mode. Waiting for message..." << endl;
  while(!val) { // wait for RX_DONE flag
    val = payload_ready();
  }
  write_register(REG_12_IRQ_FLAGS, 0xff); // clear IRQ flags
  vector<uint8_t> payload = check_rxbuf();
  uint8_t id; //,rssi,pktRSSI;

  unsigned char id1 = payload.size();
    if (id1 >= 4) { // check for good header
    uint8_t h_to = payload[0];
    uint8_t h_from = payload[1];
    uint8_t h_id = payload[2];
    uint8_t h_flags = payload[3];
    if (h_to == SERVER) { // Is this message for this server? If so, process.
      for(int i=4;i<id1-1;i++) { cout << (char) static_cast<int>(payload[i]); } // show ASCII characters
      cout << endl;
      
      // get SNR (twos complement), and RSSI values
      read_register(REG_19_PKT_SNR_VALUE, &id, 2); // get SNR value, 2s complement value ???
      int8_t snr = (id/4.0);
      read_register(REG_1B_RSSI_VALUE, &id, 2); // get RSSI value, p. 83
      int8_t rssi = static_cast<int8_t>(id);
      read_register(REG_1A_PKT_RSSI_VALUE, &id, 2); // get RSSI value, p. 83
      int8_t pktRSSI = static_cast<int8_t>(id);
      
      if( snr < 0) { 
        rssi = (-157) + rssi + snr; // use snr in calculation
        printf("RSSI: %d dBm\n", rssi); // cout WILL NOT display correctly
      } else { rssi = (-157) + (16/15) * rssi;
        printf("RSSI: %d dBm\tSNR: %d dB\n", rssi,snr); // cout WILL NOT display correctly
      }
    }
  }
  write_register(REG_01_OP_MODE, STBY_MODE); // return to standby/idle
  return 0;
}

Notice the payload = check_rxbuf() line. This is where the message is retrieved from the FIFO buffer and then processed further in the above code. The check_rxbuff() function looks like this.

Code
static std::vector<uint8_t> check_rxbuf() {
  unsigned char id, id1;
  read_register(REG_13_RX_NB_BYTES, &id, 2); // latest packet length
  id1 = id;
  read_register(REG_10_FIFO_RX_CURRENT_ADDR, &id, 2); // get current address
  write_register(REG_0D_FIFO_ADDR_PTR, id); // set addr pointer to current address
  vector<uint8_t> payload;
  for(int i=0;i<=id1;i++) { // read Fifo register
    read_register(REG_00_FIFO, &id, 1);
    payload.push_back(id); // assemble payload from FIFO
  }
  //write_register(REG_12_IRQ_FLAGS, 0xff); // clear IRQ flags
  return payload;
}

Some things I have not implemented are CADdone, plus a few other checks as part of the current effort. For a real system, these need to be in place, especially where there may be more than one Client sending to the same Server. The CAD3 functionality is necessary for frequency hopping spread spectrum (FHSS), as the signal could be below the noise floor of the receiver.

Things yet to do are some range tests. For this, I have to build at least one client on a circuit board of some type, and start placing it progressively further away to determine how far it will work. Currently, I have only used the default transmit power level. More may be required, but I have still to create a function to easily adjust the power levels.

So, that’s it for now. Someone may find this useful. It seems all current development done on this RFM95x module have been either with microPython or Arduino, not with C++, although the RadioHead libraries are mostly C++, but with Arduino syntax. When I was converting similar libraries (LowPowerLabs) for the RFM69HCW, I attempted to use the Arduino libraries and subroutines, but i quickly tired of running down just one more prerequisite to make another library work!

Anyway, this wraps up what I have done so far. There may be more in the future, but this covers the major aspects of the RFM95x, so I doubt there will be further posts on this subject. Have a great day in the Lord Jesus Christ, and may God Bless you and yours!

Footnotes

  1. PIR is a passive infrared sensor that detects IR light, and detects changes in radiation from warm objects.↩︎

  2. From Wikipedia, Object-oriented programming (OOP) is a programming paradigm based on the concept of objects. Objects can contain data (called fields, attributes or properties) and have actions they can perform (called procedures or methods and implemented in code).↩︎

  3. Channel activity detection will check a given channel to detect LoRa preamble signals to avoid collisions.↩︎

© S Lazy-H 2019 -