LoRa SX1262 on Apache NuttX OS

📝 22 Dec 2021

PineDio Stack BL604 RISC-V Board with onboard Semtech SX1262 LoRa Transceiver (left)… Sniffed wirelessly with Airspy R2 Software Defined Radio (right)

PineDio Stack BL604 RISC-V Board with onboard Semtech SX1262 LoRa Transceiver (left)… Sniffed wirelessly with Airspy R2 Software Defined Radio (right)

LoRa is an awesome wireless technology for IoT that will transmit small packets over super long distances

(Up to 5 km or 3 miles in urban areas… 15 km or 10 miles in rural areas!)

Let’s port LoRa to Apache NuttX OS!

(More about LoRa)

Doesn’t NuttX support LoRa already?

Yep NuttX has a standalone LoRa Driver for Semtech SX1276 Transceiver (Radio Transmitter + Receiver)…

(That doesn’t work with LoRaWAN yet)

Today we build a NuttX Driver for the (newer) Semtech SX1262 Transceiver

Our LoRa SX1262 Driver shall be tested on Bouffalo Lab’s BL602 and BL604 RISC-V SoCs.

(It will probably run on ESP32, since we’re calling standard NuttX Interfaces)

Eventually our LoRa SX1262 Driver will support the LoRaWAN Wireless Protocol.

How useful is LoRaWAN? Will we be using it?

Our LoRa SX1262 Driver will work perfectly fine for unsecured Point-to-Point Wireless Communication.

But if we need to relay data packets securely to a Local Area Network or to the internet, we need LoRaWAN.

(More about LoRaWAN)

1 Small Steps

So today we’ll build the NuttX Drivers for LoRa SX1262 and LoRaWAN?

Not quite. Implementing LoRa AND LoRaWAN is a complex endeavour.

Thus we break the implementation into small steps…

Porting LoRaWAN to NuttX OS

1.1 LoRaWAN Support

Why is LoRaWAN so complex?

LoRaWAN works slightly differently across the world regions, to comply with Local Wireless Regulations: Radio Frequency, Maximum Airtime (Duty Cycle), Listen Before Talk, …

Thus we should port Semtech’s LoRaWAN Stack to NuttX with minimal changes, in case of future updates. (Like for new regions)

This also means that we should port Semtech’s SX1262 Driver to NuttX as-is, because of the dependencies between the LoRaWAN Stack and the SX1262 Driver.

1.2 LoRa SX1262 Library

Where did the LoRa SX1262 code come from?

Our LoRa SX1262 Library originated from Semtech’s Reference Implementation of SX1262 Driver (29 Mar 2021)…

Which we ported to Linux and BL602 IoT SDK

And we’re porting now to NuttX.

(Because porting Linux code to NuttX is straightforward)

How did we create the LoRa SX1262 Library?

We followed the steps below to create “nuttx/libs/libsx1262” by cloning a NuttX Library…

Then we replaced the “libsx1262” folder by a Git Submodule that contains our LoRa SX1262 code…

cd nuttx/nuttx/libs
rm -r libsx1262
git rm -r libsx1262
git submodule add --branch nuttx https://github.com/lupyuen/lora-sx1262 libsx1262

Note that we’re using the older “nuttx” branch of the “lora_sx1262” repo, which doesn’t use GPIO Interface and NimBLE Porting Layer. (And doesn’t support LoRaWAN)

1.3 Library vs Driver

NuttX Libraries vs Drivers… What’s the difference?

Our LoRa SX1262 code is initially packaged as a NuttX Library (instead of NuttX Driver) because…

Eventually our LoRa SX1262 code shall be packaged as a NuttX Driver

Check out the ioctl() interface for the existing SX1276 Driver in NuttX: sx127x.c

SPI Test Driver

But how will our library access the NuttX SPI Interface?

The NuttX SPI Interface is accessible by NuttX Drivers, but not NuttX Apps.

Thankfully in the previous article we have created an SPI Test Driver “/dev/spitest0” that exposes the SPI Interface to NuttX Apps (pic above)…

For now we’ll call this SPI Test Driver in our LoRa SX1262 Library.

Inside PineDio Stack BL604

2 Connect SX1262 Transceiver

Our code has been configured for PineDio Stack BL604 and its onboard SX1262 Transceiver. (Pic above)

Based on this schematic for PineDio Stack BL604 (version 2)…

SX1262 Interface on PineDio Stack

We have configured the following BL604 Pin Definitions in board.h

SX1262BL604 PinNuttX Pin
MOSIGPIO 13BOARD_SPI_MOSI
MISOGPIO 0BOARD_SPI_MISO
SCKGPIO 11BOARD_SPI_CLK
CSGPIO 15BOARD_GPIO_OUT1
BUSYGPIO 10BOARD_GPIO_IN1
DIO1GPIO 19BOARD_GPIO_INT1
NRESETGPIO 18Not assigned yet
/* Busy Pin for PineDio SX1262 */

#define BOARD_GPIO_IN1    (GPIO_INPUT | GPIO_FLOAT | \
                            GPIO_FUNC_SWGPIO | GPIO_PIN10)

/* SPI Chip Select for PineDio SX1262 */

#define BOARD_GPIO_OUT1   (GPIO_OUTPUT | GPIO_PULLUP | \
                            GPIO_FUNC_SWGPIO | GPIO_PIN15)

/* GPIO Interrupt (DIO1) for PineDio SX1262 */

#define BOARD_GPIO_INT1   (GPIO_INPUT | GPIO_PULLUP | \
                            GPIO_FUNC_SWGPIO | GPIO_PIN19)

/* SPI Configuration: Chip Select is unused because we control via GPIO instead */

#define BOARD_SPI_CS   (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_SPI | GPIO_PIN8)  /* Unused */
#define BOARD_SPI_MOSI (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_SPI | GPIO_PIN13)
#define BOARD_SPI_MISO (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_SPI | GPIO_PIN0)
#define BOARD_SPI_CLK  (GPIO_INPUT | GPIO_PULLUP | GPIO_FUNC_SPI | GPIO_PIN11)

(Which pins can be used? See this)

GPIO Output BOARD_GPIO_OUT1 becomes our SPI Chip Select. (See this)

BOARD_GPIO_IN1 (Busy Pin) and BOARD_GPIO_INT1 (DIO1) will be used for LoRaWAN in the next article.

For BL602: Connect SX1262 to these pins. Copy the BL602 Pin Definitions from board.h to…

boards/risc-v/bl602/bl602evb/include/board.h

For ESP32: Connect SX1262 to these pins

Before testing, remember to connect the LoRa Antenna

(So we don’t fry the SX1262 Transceiver as we charge up the Power Amplifier)

PineDio Stack BL604 with Antenna

What are these SX1262 pins: DIO1, BUSY and NRESET?

DIO1 is used by SX1262 to signal that a LoRa Packet has been received.

BUSY is tells us if SX1262 is busy.

NRESET is toggled to reset the SX1262 module.

Although our SX1262 Library doesn’t use these pins, it works somewhat OK for sending and receiving LoRa Messages.

(We’ll learn why in a while)

Reading SX1262 Registers

(Source)

3 Read SX1262 Registers

What’s the simplest way to test our SX1262 Library?

To test whether our SX1262 Library is sending SPI Commands correctly to the SX1262 Transceiver, we can read the SX1262 Registers.

Here’s how: sx1262_test_main.c

/// Main Function
int main(int argc, FAR char *argv[]) {
  //  Read SX1262 registers 0x00 to 0x0F
  read_registers();
  return 0;
}

/// Read SX1262 registers
static void read_registers(void) {
  //  Init the SPI port
  SX126xIoInit();

  //  Read and print the first 16 registers: 0 to 15
  for (uint16_t addr = 0; addr < 0x10; addr++) {
    //  Read the register
    uint8_t val = SX126xReadRegister(addr);

    //  Print the register value
    printf("Register 0x%02x = 0x%02x\n", addr, val);
  }
}

(SX126xIoInit is defined here)

In our Test App we call read_registers and SX126xReadRegister to read a bunch of SX1262 Registers. (0x00 to 0x0F)

In our SX1262 Library, SX126xReadRegister calls SX126xReadRegisters and sx126x_read_register to read each register: sx126x-nuttx.c

/// Read an SX1262 Register at the specified address
uint8_t SX126xReadRegister(uint16_t address) {
  //  Read one register and return the value
  uint8_t data;
  SX126xReadRegisters(address, &data, 1);
  return data;
}

/// Read one or more SX1262 Registers at the specified address.
/// `size` is the number of registers to read.
void SX126xReadRegisters(uint16_t address, uint8_t *buffer, uint16_t size) {
  //  Wake up SX1262 if sleeping
  SX126xCheckDeviceReady();

  //  Read the SX1262 registers
  int rc = sx126x_read_register(NULL, address, buffer, size);
  assert(rc == 0);

  //  Wait for SX1262 to be ready
  SX126xWaitOnBusy();
}

(We’ll see SX126xCheckDeviceReady and SX126xWaitOnBusy in a while)

sx126x_read_register reads a register by sending the Read Register Command to SX1262 over SPI: sx126x-nuttx.c

/// Send a Read Register Command to SX1262 over SPI
/// and return the results in `buffer`. `size` is the
/// number of registers to read.
static int sx126x_read_register(const void* context, const uint16_t address, uint8_t* buffer, const uint8_t size) {
  //  Reserve 4 bytes for our SX1262 Command Buffer
  uint8_t buf[SX126X_SIZE_READ_REGISTER] = { 0 };

  //  Init the SX1262 Command Buffer
  buf[0] = RADIO_READ_REGISTER;       //  Command ID (0x1D)
  buf[1] = (uint8_t) (address >> 8);  //  MSB of Register ID
  buf[2] = (uint8_t) (address >> 0);  //  LSB of Register ID
  buf[3] = 0;                         //  Unused

  //  Transmit the Command Buffer over SPI 
  //  and receive the Result Buffer
  int status = sx126x_hal_read( 
    context,  //  Context (unsued)
    buf,      //  Command Buffer
    SX126X_SIZE_READ_REGISTER,  //  Command Buffer Size: 4 bytes
    buffer,   //  Result Buffer
    size,     //  Result Buffer Size
    NULL      //  Status not required
  );
  return status;
}

(More about sx126x_hal_read later)

This transmits the following Read Register Command to SX1262…

1d 00 08 00 00 

(0x1D is the Command ID, 0x08 is the Register ID)

SX1262 responds with…

a2 a2 a2 a2 80 

The last byte is the Register Value: 0x80

3.1 Build the Firmware

Let’s build the modified NuttX Firmware that contains our LoRa SX1262 Library and Test App

  1. Install the build prerequisites…

    “Install Prerequisites”

  2. Download the modified source code…

    mkdir nuttx
    cd nuttx
    git clone --recursive --branch sx1262 https://github.com/lupyuen/nuttx nuttx
    git clone --recursive --branch sx1262 https://github.com/lupyuen/nuttx-apps apps

    Note that we’re using the older “sx1262” branches of the NuttX OS and NuttX Apps repos, which don’t use GPIO Interface and NimBLE Porting Layer. (And don’t support LoRaWAN)

    (For PineDio Stack BL604: The SX1262 Library and Test App are already preinstalled)

  3. Edit apps/examples/sx1262_test_main.c and uncomment…

    #define READ_REGISTERS
  4. Configure the build…

    cd nuttx
    
    # For BL602: Configure the build for BL602
    ./tools/configure.sh bl602evb:nsh
    
    # For PineDio Stack BL604: Configure the build for BL604
    ./tools/configure.sh bl602evb:pinedio
    
    # For ESP32: Configure the build for ESP32.
    # TODO: Change "esp32-devkitc" to our ESP32 board.
    ./tools/configure.sh esp32-devkitc:nsh
    
    # Edit the Build Config
    make menuconfig 
  5. Enable the GPIO Driver in menuconfig…

    “Enable GPIO Driver”

    Enable the GPIO Driver

  6. Enable the SPI Peripheral and SPI Character Driver

    “Enable SPI”

    Enable SPI

  7. Enable our SPI Test Driver “/dev/spitest0”…

    “Enable SPI”

    Select SPI Test Driver

  8. Enable GPIO and SPI Logging for easier troubleshooting…

    (Might be good to uncheck “GPIO Informational Output” and “SPI Informational Output”)

    “Enable Logging”

    Enable Logging

  9. Enable our SX1262 Library

    “Enable Library”

    Enable Library

  10. Enable our SX1262 Test App

    Check the box for “Application Configuration”“Examples”“SX1262 Test”

    Enable SX1262 Test App

  11. Save the configuration and exit menuconfig

    (Here’s the .config for BL602)

  12. For ESP32: Edit esp32_bringup.c to register our SPI Test Driver (See this)

  13. Build, flash and run the NuttX Firmware on BL602 or ESP32…

    “Build, Flash and Run NuttX”

3.2 Run the Firmware

Finally we run the NuttX Firmware and test our LoRa SX1262 Library

  1. In the NuttX Shell, enter…

    ls /dev

    Our SPI Test Driver should appear as “/dev/spitest0”

    Our SPI Test Driver appears as “/dev/spitest0”

  2. In the NuttX Shell, enter…

    sx1262_test
  3. We should see these SX1262 Register Values (pic below)…

    Register 0x00 = 0x00
    ...
    Register 0x08 = 0x80
    Register 0x09 = 0x00
    Register 0x0a = 0x01

    (See the Output Log)

    Our LoRa SX1262 Library talks OK to the SX1262 Transceiver!

    Note that the values above will change when we transmit and receive LoRa Messages. Let’s do that now.

Reading SX1262 Registers

3.3 Source Files

We’re seeing layers of code, like an onion? (Or Shrek)

Yep our SX1262 Library is structured as layers of Source Files because we hope to support three platforms…

  1. NuttX (Just Completed)

  2. Linux (Completed: PineDio USB)

  3. BL602 IoT SDK (Completed)

The Platform-Independent Source Files shared by all platforms are…

(We should minimise changes to the above files, because they will be called by Semtech’s LoRaWAN Driver)

The Platform-Specific Source Files for NuttX are…

The Source Files are called like this…

Test Appradio.csx126x.csx126x-nuttx.c

4 LoRa Parameters

Before we transmit and receive LoRa Messages, let’s talk about the LoRa Parameters.

Check this doc to find out which LoRa Frequency we should use for our region…

We set the LoRa Frequency in our SX1262 Test App like so: sx1262_test_main.c

/// TODO: We are using LoRa Frequency 923 MHz 
/// for Singapore. Change this for your region.
#define USE_BAND_923

Change USE_BAND_923 to USE_BAND_433, 780, 868 or 915.

(See the complete list)

Below are the other LoRa Parameters: sx1262_test_main.c

/// LoRa Parameters
#define LORAPING_TX_OUTPUT_POWER            14        /* dBm */

#define LORAPING_BANDWIDTH                  0         /* [0: 125 kHz, */
                                                      /*  1: 250 kHz, */
                                                      /*  2: 500 kHz, */
                                                      /*  3: Reserved] */
#define LORAPING_SPREADING_FACTOR           7         /* [SF7..SF12] */
#define LORAPING_CODINGRATE                 1         /* [1: 4/5, */
                                                      /*  2: 4/6, */
                                                      /*  3: 4/7, */
                                                      /*  4: 4/8] */
#define LORAPING_PREAMBLE_LENGTH            8         /* Same for Tx and Rx */
#define LORAPING_SYMBOL_TIMEOUT             5         /* Symbols */
#define LORAPING_FIX_LENGTH_PAYLOAD_ON      false
#define LORAPING_IQ_INVERSION_ON            false

#define LORAPING_TX_TIMEOUT_MS              3000    /* ms */
#define LORAPING_RX_TIMEOUT_MS              10000    /* ms */
#define LORAPING_BUFFER_SIZE                64      /* LoRa message size */

(More about LoRa Parameters)

During testing, these should match the LoRa Parameters used by the LoRa Transmitter / Receiver.

In a while we’ll use RAKwireless WisBlock (pic below) to test our SX1262 Library.

Below are the LoRa Transmitter and Receiver programs (Arduino) that we’ll run on WisBlock…

The LoRa Parameters above should match the ones in our SX1262 Test App for NuttX.

Are there practical limits on the LoRa Parameters?

Yes we need to comply with the Local Regulations on the usage of ISM Radio Bands: FCC, ETSI, …

(Blasting LoRa Messages non-stop is no-no!)

RAKwireless WisBlock LPWAN Module mounted on WisBlock Base Board

4.1 Initialise LoRa SX1262

Let’s watch how the LoRa Parameters are used to initialise the SX1262 Transceiver.

The init_driver function in our Test App takes the LoRa Parameters and initialises LoRa SX1262 like so: sx1262_test_main.c

/// Command to initialise the LoRa Driver.
/// Assume that create_task has been called to init the Event Queue.
static void init_driver(char *buf, int len, int argc, char **argv) {
  //  Set the LoRa Callback Functions
  RadioEvents_t radio_events;
  memset(&radio_events, 0, sizeof(radio_events));  //  Must init radio_events to null, because radio_events lives on stack!
  radio_events.TxDone    = on_tx_done;     //  Packet has been transmitted
  radio_events.RxDone    = on_rx_done;     //  Packet has been received
  radio_events.TxTimeout = on_tx_timeout;  //  Transmit Timeout
  radio_events.RxTimeout = on_rx_timeout;  //  Receive Timeout
  radio_events.RxError   = on_rx_error;    //  Receive Error

Here we set the Callback Functions that will be called when a LoRa Message has been transmitted or received, also when we encounter a transmit / receive timeout or error.

(We’ll see the Callback Functions in a while)

Next we call our SX1262 Library to initialise the LoRa Transceiver and set the LoRa Frequency

  //  Init the SPI Port and the LoRa Transceiver
  Radio.Init(&radio_events);

  //  Set the LoRa Frequency
  Radio.SetChannel(RF_FREQUENCY);

Then we set the LoRa Transmit Parameters

  //  Configure the LoRa Transceiver for transmitting messages
  Radio.SetTxConfig(
    MODEM_LORA,
    LORAPING_TX_OUTPUT_POWER,
    0,        //  Frequency deviation: Unused with LoRa
    LORAPING_BANDWIDTH,
    LORAPING_SPREADING_FACTOR,
    LORAPING_CODINGRATE,
    LORAPING_PREAMBLE_LENGTH,
    LORAPING_FIX_LENGTH_PAYLOAD_ON,
    true,     //  CRC enabled
    0,        //  Frequency hopping disabled
    0,        //  Hop period: N/A
    LORAPING_IQ_INVERSION_ON,
    LORAPING_TX_TIMEOUT_MS
  );

Finally we set the LoRa Receive Parameters

  //  Configure the LoRa Transceiver for receiving messages
  Radio.SetRxConfig(
    MODEM_LORA,
    LORAPING_BANDWIDTH,
    LORAPING_SPREADING_FACTOR,
    LORAPING_CODINGRATE,
    0,        //  AFC bandwidth: Unused with LoRa
    LORAPING_PREAMBLE_LENGTH,
    LORAPING_SYMBOL_TIMEOUT,
    LORAPING_FIX_LENGTH_PAYLOAD_ON,
    0,        //  Fixed payload length: N/A
    true,     //  CRC enabled
    0,        //  Frequency hopping disabled
    0,        //  Hop period: N/A
    LORAPING_IQ_INVERSION_ON,
    true      //  Continuous receive mode
  );    
}

The Radio functions are Platform-Independent, defined in our SX1262 Library: radio.c

(The Radio functions will also be called when we implement LoRaWAN)

Transmitting a LoRa Message

(Source)

5 Transmit LoRa Message

Now we’re ready to transmit a LoRa Message in our SX1262 Test App! Here’s how: sx1262_test_main.c

/// Main Function
int main(void) {
  //  Init SX1262 driver
  init_driver();

  //  TODO: Do we need to wait?
  sleep(1);

  //  Send a LoRa message
  send_message();
  return 0;
}

We begin by calling init_driver in our Test App to set the LoRa Parameters and the Callback Functions.

(We’ve seen init_driver in the previous section)

To transmit a LoRa Message, send_message calls send_once: sx1262_test_main.c

/// Send a LoRa message. Assume that SX1262 driver has been initialised.
static void send_message(void) {
  //  Send the "PING" message
  send_once(1);
}

send_once prepares a 64-byte LoRa Message containing the string “PING”: sx1262_test_main.c

/// We send a "PING" message and expect a "PONG" response
const uint8_t loraping_ping_msg[] = "PING";
const uint8_t loraping_pong_msg[] = "PONG";

/// 64-byte buffer for our LoRa message
static uint8_t loraping_buffer[LORAPING_BUFFER_SIZE];

/// Send a LoRa message. If is_ping is 0, send "PONG". Otherwise send "PING".
static void send_once(int is_ping) {
  //  Copy the "PING" or "PONG" message 
  //  to the transmit buffer
  if (is_ping) {
    memcpy(loraping_buffer, loraping_ping_msg, 4);
  } else {
    memcpy(loraping_buffer, loraping_pong_msg, 4);
  }

Then we pad the 64-byte message with values 0, 1, 2, …

  //  Fill up the remaining space in the 
  //  transmit buffer (64 bytes) with values 
  //  0, 1, 2, ...
  for (int i = 4; i < sizeof loraping_buffer; i++) {
    loraping_buffer[i] = i - 4;
  }

And we call our SX1262 Library to transmit the LoRa Message

  //  We send the transmit buffer (64 bytes)
  Radio.Send(loraping_buffer, sizeof loraping_buffer);
}

(RadioSend is explained here)

When the LoRa Message has been transmitted, the SX1262 Library calls our Callback Function on_tx_done defined in sx1262_test_main.c

/// Callback Function that is called when our LoRa message has been transmitted
static void on_tx_done(void) {
  //  Log the success status
  loraping_stats.tx_success++;

  //  Switch the LoRa Transceiver to 
  //  low power, sleep mode
  Radio.Sleep();
}

(RadioSleep is explained here)

Here we log the number of packets transmitted, and put LoRa SX1262 into low power, sleep mode.

Note: on_tx_done won’t actually be called in our current driver, because we haven’t implemented Multithreading. (More about this later)

To handle Transmit Timeout Errors, we define the Callback Function on_tx_timeout: sx1262_test_main.c

/// Callback Function that is called when our LoRa message couldn't be transmitted due to timeout
static void on_tx_timeout(void) {
  //  Switch the LoRa Transceiver to 
  //  low power, sleep mode
  Radio.Sleep();

  //  Log the timeout
  loraping_stats.tx_timeout++;
}

5.1 Run the Firmware

Let’s test our SX1262 Library and transmit a LoRa Message

  1. Assume that we have downloaded and configured our NuttX code…

    “Build the Firmware”

  2. Edit apps/examples/sx1262_test_main.c and uncomment…

    #define SEND_MESSAGE
  3. Also edit sx1262_test_main.c and set the LoRa Parameters. (As explained earlier)

  4. Build, flash and run the NuttX Firmware on BL602 or ESP32…

    “Build, Flash and Run NuttX”

  5. Switch over to RAKwireless WisBlock and run our LoRa Receiver

    wisblock-lora-receiver

    Check that the LoRa Parameters are correct…

    LoRa Parameters for WisBlock Receiver

  6. In the NuttX Shell, enter…

    sx1262_test
  7. We should see our SX1262 Library transmitting a 64-byte LoRa Message

    send_message
    RadioSend: size=64
    50 49 4e 47 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b

    (“PING” followed by 0, 1, 2, …)

    (See the Output Log)

  8. On WisBlock we should see the same 64-byte LoRa Message received by WisBlock…

    LoRaP2P Rx Test
    Starting Radio.Rx
    OnRxDone: Timestamp=18, RssiValue=-28 dBm, SnrValue=13, 
    Data=50 49 4e 47 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b

    Our SX1262 Library has successfully transmitted a 64-byte LoRa Message to RAKwireless WisBlock!

Our SX1262 Library transmits a LoRa Message to RAKwireless WisBlock

In case of problems, try troubleshooting with a Software Defined Radio like Airspy R2…

PineDio Stack BL604 RISC-V Board with onboard Semtech SX1262 LoRa Transceiver (left)… Sniffed wirelessly with Airspy R2 Software Defined Radio (right)

5.2 Spectrum Analysis with SDR

What if nothing appears in our LoRa Receiver?

Use a Spectrum Analyser (like a Software Defined Radio) to sniff the airwaves and check whether our LoRa Message is transmitted…

  1. At the right Radio Frequency

    (923 MHz below)

  2. With sufficient power

    (Red stripe below)

Spectrum Analysis of LoRa Message with SDR

LoRa Messages have a characteristic criss-cross shape: LoRa Chirp. (Like above)

More about LoRa Chirps and Software Defined Radio…

Receiving a LoRa Message

(Source)

6 Receive LoRa Message

Watch the (turn)tables turn as we receive a LoRa Message with our SX1262 Library! This is how we do it: sx1262_test_main.c

/// Main Function
int main(void) {
  //  TODO: Create a Background Thread 
  //  to handle LoRa Events
  create_task();

We start by creating a Background Thread to handle LoRa Events in our Test App.

(create_task doesn’t do anything because we haven’t implemented Multithreading. More about this later)

Next we set the LoRa Parameters and the Callback Functions

  //  Init SX1262 driver
  init_driver();

  //  TODO: Do we need to wait?
  sleep(1);

(Yep the same init_driver we’ve seen earlier)

For the next 10 seconds we poll and handle LoRa Events (like Message Received)…

  //  Handle LoRa events for the next 10 seconds
  for (int i = 0; i < 10; i++) {
    //  Prepare to receive a LoRa message
    receive_message();

    //  Process the received LoRa message, if any
    RadioOnDioIrq(NULL);
    
    //  Sleep for 1 second
    usleep(1000 * 1000);
  }
  return 0;
}

(Polling isn’t efficient, we’ll discuss the enhancements later)

We call receive_message to get SX1262 ready to receive a single LoRa Message.

Then we call RadioOnDioIrq (from our SX1262 Library) to handle the Message Received Event. (If any)

(RadioOnDioIrq is explained here)

receive_message is defined in our Test App: sx1262_test_main.c

/// Receive a LoRa message. Assume that SX1262 driver has been initialised.
/// Assume that create_task has been called to init the Event Queue.
static void receive_message(void) {
  //  Receive a LoRa message within the timeout period
  Radio.Rx(LORAPING_RX_TIMEOUT_MS);
}

This code calls RadioRx (from our SX1262 Library) to prep SX1262 to receive a single LoRa Message.

(RadioRx is explained here)

When our SX1262 Library receives a LoRa Message, it calls our Callback Function on_rx_done defined in our Test App: sx1262_test_main.c

/// Callback Function that is called when a LoRa message has been received
static void on_rx_done(
  uint8_t *payload,  //  Buffer containing received LoRa message
  uint16_t size,     //  Size of the LoRa message
  int16_t rssi,      //  Signal strength
  int8_t snr) {      //  Signal To Noise ratio

  //  Switch the LoRa Transceiver to low power, sleep mode
  Radio.Sleep();

  //  Log the signal strength, signal to noise ratio
  loraping_rxinfo_rxed(rssi, snr);

on_rx_done switches the LoRa Transceiver to low power, sleep mode and logs the received packet.

Next we copy the received packet into a buffer…

  //  Copy the received packet
  if (size > sizeof loraping_buffer) {
    size = sizeof loraping_buffer;
  }
  loraping_rx_size = size;
  memcpy(loraping_buffer, payload, size);

Finally we dump the buffer containing the received packet…

  //  Dump the contents of the received packet
  for (int i = 0; i < loraping_rx_size; i++) {
    printf("%02x ", loraping_buffer[i]);
  }
  puts("");
}

What happens when we don’t receive a packet in 10 seconds? (LORAPING_RX_TIMEOUT_MS)

Our SX1262 Library calls our Callback Function on_rx_timeout defined in our Test App: sx1262_test_main.c

/// Callback Function that is called when no LoRa messages could be received due to timeout
static void on_rx_timeout(void) {
  //  Switch the LoRa Transceiver to low power, sleep mode
  Radio.Sleep();

  //  Log the timeout
  loraping_stats.rx_timeout++;
}

We switch the LoRa Transceiver into sleep mode and log the timeout.

Note: on_rx_timeout won’t actually be called in our current driver, because we haven’t implemented Multithreading. (More about this later)

To handle Receive Errors, we define the Callback Function on_rx_error in our Test App: sx1262_test_main.c

/// Callback Function that is called when we couldn't receive a LoRa message due to error
static void on_rx_error(void) {
  //  Log the error
  loraping_stats.rx_error++;

  //  Switch the LoRa Transceiver to low power, sleep mode
  Radio.Sleep();
}

6.1 Run the Firmware

Let’s test our SX1262 Library and receive a LoRa Message

  1. Assume that we have downloaded and configured our NuttX code…

    “Build the Firmware”

  2. Edit apps/examples/sx1262_test_main.c and uncomment…

    #define RECEIVE_MESSAGE
  3. Also edit sx1262_test_main.c and set the LoRa Parameters. (As explained earlier)

  4. Build, flash and run the NuttX Firmware on BL602 or ESP32…

    “Build, Flash and Run NuttX”

  5. Switch over to RAKwireless WisBlock and run our LoRa Transmitter

    wisblock-lora-transmitter

    Check that the LoRa Parameters are correct…

    LoRa Parameters for WisBlock Transmitter

  6. WisBlock transmits a 64-byte LoRa Message every 5 seconds…

    LoRap2p Tx Test
    send: 48 65 6c 6c 6f 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a
    OnTxDone

    (“Hello” followed by 0, 1, 2, …)

  7. In the NuttX Shell, enter…

    sx1262_test
  8. On NuttX we should see the same 64-byte LoRa Message

    IRQ_RX_DONE
    48 65 6c 6c 6f 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a
    IRQ_PREAMBLE_DETECTED
    IRQ_HEADER_VALID
    receive_message

    (See the Output Log)

    Congratulations our SX1262 Library has successfully received the 64-byte LoRa Message from RAKwireless WisBlock!

Our SX1262 Library receives a LoRa Message from RAKwireless WisBlock

7 SPI Interface

Porting the SX1262 Library from Linux to NuttX… Was it difficult?

Not at all! NuttX works much like Linux because of its POSIX Compliance.

Most of the porting effort (14 minutes!) was spent on…

  1. SPI Interface

  2. GPIO Interface

Because the interfaces work differently on NuttX vs Linux.

Let’s dive into the SPI Interface: How we initialise the interface and transfer data over SPI.

SPI Test Driver

7.1 Initialise SPI

In the previous article we have created an SPI Test Driver “/dev/spitest0” that exposes the SPI Interface to NuttX Apps (pic above)…

Our SX1262 Library opens the SPI Test Driver to initialise the SPI Bus like so: sx126x-nuttx.c

/// SPI Bus
static int spi = 0;

/// Chip Select Pin (GPIO Output)
static int cs = 0;

/// Init the SPI Bus and Chip Select Pin. Return 0 on success.
static int init_spi(void) {
  //  Open the SPI Bus (SPI Test Driver).
  //  Defaults to "/dev/spitest0"
  spi = open(SPI_DEVPATH, O_RDWR);
  assert(spi > 0);

  //  Open GPIO Output for SPI Chip Select.
  //  Defaults to "/dev/gpio1"
  cs = open(CS_DEVPATH, O_RDWR);
  assert(cs > 0);

  //  Get SPI Chip Select Pin Type
  enum gpio_pintype_e pintype;
  int ret = ioctl(cs, GPIOC_PINTYPE, (unsigned long)((uintptr_t)&pintype));
  assert(ret >= 0);

  //  Verify that SPI Chip Select is GPIO Output (not GPIO Input or GPIO Interrupt)
  assert(pintype == GPIO_OUTPUT_PIN);

  //  TODO: Set SPI Chip Select to High for all SPI Devices
  return 0;
}

(SPI_DEVPATH and CS_DEVPATH are explained in the Appendix)

init_spi is called by SX126xIoInit, which is called by RadioInit and init_driver

(We’ve seen init_driver earlier in our Test App)

(RadioInit is explained here)

Where are SPI Mode and SPI Frequency defined?

SPI Mode and SPI Frequency are defined in the SPI Test Driver…

Why did we use GPIO Output?

We’re controlling the SPI Chip Select Pin (/dev/gpio1) ourselves via GPIO Output, as explained below…

More about GPIO Output in the next section.

Initialise SPI

(Source)

7.2 Transfer SPI

To transfer SPI Data to SX1262 via our SPI Test Driver, we do this: sx126x-nuttx.c

/// Blocking call to transmit and receive buffers on SPI. Return 0 on success.
static int transfer_spi(const uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) {
  assert(spi > 0);
  assert(cs  > 0);
  assert(len > 0);
  assert(len <= SPI_BUFFER_SIZE);
  _info("spi tx: "); for (int i = 0; i < len; i++) { _info("%02x ", tx_buf[i]); } _info("\n");

  //  Set SPI Chip Select to Low
  int ret = ioctl(cs, GPIOC_WRITE, 0);
  assert(ret >= 0);

  //  Transmit data over SPI
  int bytes_written = write(spi, tx_buf, len);
  assert(bytes_written == len);

  //  Receive SPI response
  int bytes_read = read(spi, rx_buf, len);
  assert(bytes_read == len);

  //  Set SPI Chip Select to High
  ret = ioctl(cs, GPIOC_WRITE, 1);
  assert(ret >= 0);

  _info("spi rx: "); for (int i = 0; i < len; i++) { _info("%02x ", rx_buf[i]); } _info("\n");
  return 0;
}

Note that we control SPI Chip Select ourselves with GPIO Output. The code above is explained in…

Let’s watch how transfer_spi is called by our SX1262 Library to transmit and receive LoRa Messages.

Transfer SPI

(Source)

7.3 Transmit Message

Later as we walk through the sending of a LoRa Message (RadioSend), we’ll learn that our SX1262 Driver calls this function to transfer the LoRa Message to SX1262: sx126x-nuttx.c

static int sx126x_write_buffer(const void* context, const uint8_t offset, const uint8_t* buffer, const uint8_t size) {
  //  Prepare the Write Buffer Command (2 bytes)
  uint8_t buf[SX126X_SIZE_WRITE_BUFFER] = { 0 };
  buf[0] = RADIO_WRITE_BUFFER;  //  Write Buffer Command: 0x0E
  buf[1] = offset;              //  Write Buffer Offset

  //  Transfer the Write Buffer Command to SX1262 over SPI
  return sx126x_hal_write(
    context,  //  Context
    buf,      //  Command Buffer
    SX126X_SIZE_WRITE_BUFFER,  //  Command Buffer Size (2 bytes)
    buffer,   //  Write Data Buffer
    size      //  Write Data Buffer Size
  );
}

(sx126x_write_buffer is called by RadioSend)

In code above we prepare a SX1262 Write Buffer Command (0x0E 0x00) and pass the Command Buffer (plus Data Buffer) to sx126x_hal_write.

(Data Buffer contains the 64-byte LoRa Message to be transmitted)

Note that Write Buffer Offset is always 0, because of SX126xSetPayload and SX126xWriteBuffer.

(SX126xSetPayload and SX126xWriteBuffer are explained here)

sx126x_hal_write calls transfer_spi to transfer the Command Buffer and Data Buffer over SPI: sx126x-nuttx.c

/**
 * Radio data transfer - write
 *
 * @remark Shall be implemented by the user
 *
 * @param [in] context          Radio implementation parameters
 * @param [in] command          Pointer to the buffer to be transmitted
 * @param [in] command_length   Buffer size to be transmitted
 * @param [in] data             Pointer to the buffer to be transmitted
 * @param [in] data_length      Buffer size to be transmitted
 *
 * @returns Operation status
 */
static int sx126x_hal_write( 
  const void* context, const uint8_t* command, const uint16_t command_length,
  const uint8_t* data, const uint16_t data_length ) {
  printf("sx126x_hal_write: command_length=%d, data_length=%d\n", command_length, data_length);

  //  Total length is command + data length
  uint16_t len = command_length + data_length;
  assert(len > 0);
  assert(len <= SPI_BUFFER_SIZE);

  //  Clear the SPI Transmit and Receive buffers
  memset(&spi_tx_buf, 0, len);
  memset(&spi_rx_buf, 0, len);

  //  Copy command bytes to SPI Transmit Buffer
  memcpy(&spi_tx_buf, command, command_length);

  //  Copy data bytes to SPI Transmit Buffer
  memcpy(&spi_tx_buf[command_length], data, data_length);

  //  Transmit and receive the SPI buffers
  int rc = transfer_spi(spi_tx_buf, spi_rx_buf, len);
  assert(rc == 0);
  return 0;
}

(We’ve seen transfer_spi in the previous section)

What are spi_tx_buf and spi_rx_buf?

They are the internal 1024-byte buffers for SPI Transfers: sx126x-nuttx.c

/// Max size of SPI transfers
#define SPI_BUFFER_SIZE 1024

/// SPI Transmit Buffer
static uint8_t spi_tx_buf[SPI_BUFFER_SIZE];

/// SPI Receive Buffer
static uint8_t spi_rx_buf[SPI_BUFFER_SIZE];

7.4 Receive Message

Later as we inspect the code that receives LoRa Messages, we’ll see that our SX1262 Library calls this function when a Receive Done Event is triggered: sx126x-nuttx.c

static int sx126x_read_buffer(const void* context, const uint8_t offset, uint8_t* buffer, const uint8_t size) {
  //  Prepare the Read Buffer Command (3 bytes)
  uint8_t buf[SX126X_SIZE_READ_BUFFER] = { 0 };
  buf[0] = RADIO_READ_BUFFER;  //  Read Buffer Command: 0x1E
  buf[1] = offset;             //  Read Buffer Offset
  buf[2] = 0;                  //  NOP

  //  Transfer the Read Buffer Command to SX1262 over SPI
  int status = sx126x_hal_read( 
    context,  //  Context
    buf,      //  Command Buffer
    SX126X_SIZE_READ_BUFFER,  //  Command Buffer Size (3 bytes)
    buffer,   //  Read Data Buffer
    size,     //  Read Data Buffer Size
    NULL      //  Ignore the status
  );
  return status;
}

(sx126x_read_buffer is called by the Receive Done Event)

In this code we prepare a SX1262 Read Buffer Command (0x1E 0x00 0x00) and pass the Command Buffer (plus Data Buffer) to sx126x_hal_read.

(Data Buffer will contain the received 64-byte LoRa Message)

Note that Read Buffer Offset is always 0, because of SX126xGetPayload and SX126xReadBuffer.

(SX126xGetPayload and SX126xReadBuffer are explained here)

sx126x_hal_read calls transfer_spi to transfer the Command Buffer over SPI: sx126x-nuttx.c

/**
 * Radio data transfer - read
 *
 * @remark Shall be implemented by the user
 *
 * @param [in] context          Radio implementation parameters
 * @param [in] command          Pointer to the buffer to be transmitted
 * @param [in] command_length   Buffer size to be transmitted
 * @param [in] data             Pointer to the buffer to be received
 * @param [in] data_length      Buffer size to be received
 * @param [out] status          If not null, return the second SPI byte received as status
 *
 * @returns Operation status
 */
static int sx126x_hal_read( 
  const void* context, const uint8_t* command, const uint16_t command_length,
  uint8_t* data, const uint16_t data_length, uint8_t *status ) {
  printf("sx126x_hal_read: command_length=%d, data_length=%d\n", command_length, data_length);

  //  Total length is command + data length
  uint16_t len = command_length + data_length;
  assert(len > 0);
  assert(len <= SPI_BUFFER_SIZE);

  //  Clear the SPI Transmit and Receive buffers
  memset(&spi_tx_buf, 0, len);
  memset(&spi_rx_buf, 0, len);

  //  Copy command bytes to SPI Transmit Buffer
  memcpy(&spi_tx_buf, command, command_length);

  //  Transmit and receive the SPI buffers
  int rc = transfer_spi(spi_tx_buf, spi_rx_buf, len);
  assert(rc == 0);

  //  Copy SPI Receive buffer to data buffer
  memcpy(data, &spi_rx_buf[command_length], data_length);

  //  Return the second SPI byte received as status
  if (status != NULL) {
    assert(len >= 2);
    *status = spi_rx_buf[1];
  }
  return 0;
}

And returns the Data Buffer received from SX1262 over SPI.

7.5 SPI In Action

We’ve talked about sx126x_write_buffer and sx126x_read_buffer, let’s watch them in action as we transmit and receive 64-byte LoRa Messages.

To transmit a LoRa Message, sx126x_write_buffer sends the WriteBuffer Command to SX1262 over SPI…

  1. WriteBuffer Command: 0x0E

  2. WriteBuffer Offset: 0x00

  3. WriteBuffer Data: Transfer 64 bytes

This copies the entire 64-byte LoRa Message into the SX1262 Transmit Buffer as a single (huge) chunk.

This appears in the Transmit Log as…

sx126x_hal_write: 
  command_length=2, 
  data_length=64
spi tx: 
  0e 00 
  50 49 4e 47 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b
spi rx: 
  a2 a2 
  a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2 a2

The 64-byte LoRa Message transmitted appears in the SPI Transmit Log above: 50 49 4e 47...

(“50 49 4e 47...” is “PING” followed by 0, 1, 2, …)

To receive a LoRa Message, sx126x_read_buffer sends this ReadBuffer Command to SX1262 over SPI…

  1. ReadBuffer Command: 0x1E

  2. ReadBuffer Offset: 0x00

  3. ReadBuffer NOP: 0x00

  4. ReadBuffer Data: Transfer 64 bytes

Which appears in the Receive Log as…

sx126x_hal_read: 
  command_length=3, 
  data_length=64
spi tx: 
  1e 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
spi rx: 
  d2 d2 d2 
  48 65 6c 6c 6f 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 

The 64-byte LoRa Message received appears in the SPI Receive Log above: 48 65 6c 6c 6f...

(“48 65 6c 6c 6f...” is “Hello” followed by 0, 1, 2, …)

8 GPIO Interface

Besides SPI, what Interfaces do we need to control the SX1262 Transceiver?

Our SX1262 Library needs a GPIO Interface to control these SX1262 Pins

(PineDio Stack BL604 uses different GPIO Names, see the Appendix)

Let’s look at the implementation of the GPIO Interface for the new SX1262 Library that will support LoRaWAN.

(See this for the old SX1262 Library)

8.1 Initialise GPIO

This function is called to initialise the GPIO Pins when our app starts: SX126xIoInit

/// Initialise GPIO Pins and SPI Port. Called by SX126xIoIrqInit.
/// Note: This is different from the Reference Implementation,
/// which initialises the GPIO Pins and SPI Port at startup.
void SX126xIoInit( void ) {
  //  Init the Event Queue if not initialised. 
  //  TimerInit is called before SX126xIoInit, 
  //  so the Event Queue should already be initialised.
  init_event_queue();

  //  Init GPIO Pins. Event Queue must be initialised before this.
  int rc = init_gpio();
  assert(rc == 0);

  //  Init SPI Bus
  rc = init_spi();
  assert(rc == 0);
}

(We’ll see init_event_queue in the next chapter)

(We’ve seen init_spi earlier)

init_gpio initialises the GPIO Pins like so: sx126x-nuttx.c

/// SX1262 Busy Pin (GPIO Input)
static int busy = 0;

/// SX1262 DIO1 Pin (GPIO Interrupt)
static int dio1 = 0;

/// Init the GPIO Pins. Return 0 on success.
static int init_gpio(void) {
  //  Open GPIO Input for SX1262 Busy Pin.
  //  Defaults to "/dev/gpio0"
  busy = open(BUSY_DEVPATH, O_RDWR);
  assert(busy > 0);

(BUSY_DEVPATH is explained in the Appendix)

We begin by opening the GPIO Input for the Busy Pin “/dev/gpio0”.

We fetch the GPIO Pin Type and verify that it’s GPIO Input…

  //  Get SX1262 Busy Pin Type
  enum gpio_pintype_e pintype;
  int ret = ioctl(  //  Execute a GPIO Command...
    busy,           //  GPIO Descriptor
    GPIOC_PINTYPE,  //  Get GPIO Pin Type
    (unsigned long)((uintptr_t) &pintype)  //  Returned Pin Type
  );
  assert(ret >= 0);

  //  Verify that SX1262 Busy Pin is GPIO Input (not GPIO Output or GPIO Interrupt)
  assert(pintype == GPIO_INPUT_PIN);  //  No pullup / pulldown

Next we open the GPIO Interrupt for the DIO1 Pin “/dev/gpio2”.

  //  Open GPIO Interrupt for SX1262 DIO1 Pin
  //  Defaults to "/dev/gpio2"
  dio1 = open(DIO1_DEVPATH, O_RDWR);
  assert(dio1 > 0);

(DIO1_DEVPATH is explained in the Appendix)

We fetch the GPIO Pin Type and verify that it’s GPIO Interrupt…

  //  Get SX1262 DIO1 Pin Type
  ret = ioctl(      //  Execute a GPIO Command...
    dio1,           //  GPIO Descriptor
    GPIOC_PINTYPE,  //  Get GPIO Pin Type
    (unsigned long)((uintptr_t) &pintype)  //  Returned Pin Type
  );
  assert(ret >= 0);
  printf("DIO1 pintype before=%d\n", pintype);

  //  Verify that SX1262 DIO1 Pin is GPIO Interrupt (not GPIO Input or GPIO Output)
  assert(pintype == GPIO_INTERRUPT_PIN);

Remember that DIO1 shifts from Low to High when a LoRa Packet has been transmitted or received.

Thus we configure NuttX to trigger a GPIO Interrupt on Rising Edge

  //  Change DIO1 Pin to Trigger GPIO Interrupt on Rising Edge
  //  TODO: Crashes at ioexpander/gpio.c (line 544) because change failed apparently
  puts("init_gpio: change DIO1 to Trigger GPIO Interrupt on Rising Edge");
  ret = ioctl(         //  Execute a GPIO Command...
    dio1,              //  GPIO Descriptor
    GPIOC_SETPINTYPE,  //  Set GPIO Pin Type
    (unsigned long) GPIO_INTERRUPT_RISING_PIN  //  Requested Pin Type
  );
  assert(ret >= 0);

(This crashes NuttX with an Assertion Failure, so we have disabled the assertion)

We fetch the GPIO Pin Type and verify that it’s GPIO Interrupt Triggered on Rising Edge…

  //  Get SX1262 DIO1 Pin Type again
  ret = ioctl(      //  Execute a GPIO Command...
    dio1,           //  GPIO Descriptor
    GPIOC_PINTYPE,  //  Get GPIO Pin Type
    (unsigned long)((uintptr_t) &pintype)  //  Returned Pin Type
  );
  assert(ret >= 0);
  printf("DIO1 pintype after=%d\n", pintype);

  //  Verify that SX1262 DIO1 Pin is GPIO Interrupt on Rising Edge
  //  TODO: This fails because the Pin Type remains as GPIO_INTERRUPT_PIN
  //  assert(pintype == GPIO_INTERRUPT_RISING_PIN);  //  Trigger interrupt on rising edge

  //  Omitted: Start the Background Thread to process DIO1 interrupts
  ...

(But there’s a quirk in NuttX so we have disabled the assertion)

In the next section we’ll create a Background Thread to handle the GPIO Interrupt.

Why do we verify the GPIO Pin Types? (Input / Output / Interrupt)

The GPIO Pin Names look awfully similar: /dev/gpio0, gpio1, gpio2, …

It’s easy to mix up the GPIO Pins. Hence we verify the GPIO Pin Types to be sure that we got the right pin.

Initialise GPIO

8.2 Start DIO1 Thread

In the rest of init_gpio we create a Background Thread to handle GPIO Interrupts from DIO1: sx126x-nuttx.c

/// Init the GPIO Pins. Return 0 on success.
static int init_gpio(void) {
  //  Omitted: Open GPIO Input for SX1262 Busy Pin
  //  Omitted: Open GPIO Interrupt for SX1262 DIO1 Pin
  //  Omitted: Change DIO1 Pin to Trigger GPIO Interrupt on Rising Edge
  ...
  //  Init the Background Thread Attributes
  static pthread_attr_t attr;
  ret = pthread_attr_init(&attr);
  assert(ret == 0);

After we have initialised the Thread Attributes, we create the Background Thread

  //  Start the Background Thread to process DIO1 interrupts
  static pthread_t thread;
  ret = pthread_create(  //  Create a Background Thread
    &thread,             //  Returned Thread
    &attr,               //  Thread Attributes
    process_dio1,        //  Function that will be executed by the thread
    0                    //  Argument to pass to the thread
  );
  assert(ret == 0);

Let’s look inside process_dio1, the function that will be executed by the thread.

Handle DIO1 Interrupt

8.3 Handle DIO1 Interrupt

We have created a Background Thread that will handle GPIO Interrupts from DIO1. (Whenever a LoRa Packet is transmitted or received)

The thread shall do this…

  1. Define a NuttX Signal that will be signalled on GPIO Interrupt

  2. Wait for the GPIO Interrupt (Signal) to be triggered by DIO1

  3. Add an Event to the Event Queue

  4. Repeat forever

(Event Queue comes from NimBLE Porting Layer, explained in the next chapter)

Below is the code for our Background Thread: sx126x-nuttx.c

/// Handle DIO1 Interrupt by adding to Event Queue
void *process_dio1(void *arg) {
  assert(dio1 > 0);

  //  Define the DIO1 Interrupt Event
  static struct ble_npl_event ev;
  ble_npl_event_init(  //  Init the Event for...
    &ev,               //  Event
    RadioOnDioIrq,     //  Event Handler Function
    NULL               //  Argument to be passed to Event Handler
  );

The thread begins by defining the DIO1 Interrupt Event that will be added to the Event Queue.

When the Event Loop receives this Event, it will call RadioOnDioIrq to process the received packet.

Next we define the NuttX Signal that will be signalled on GPIO Interrupt…

  //  Define the signal
  #define SIG_DIO1 1
  struct sigevent notify;
  notify.sigev_notify = SIGEV_SIGNAL;
  notify.sigev_signo  = SIG_DIO1;

We register the Signal that will be triggered on GPIO Interrupt…

  //  Set up to receive signal from GPIO Interrupt (DIO1 rising edge)
  int ret = ioctl(   //  Execute a GPIO Command...
    dio1,            //  GPIO Descriptor
    GPIOC_REGISTER,  //  Register GPIO Interrupt
    (unsigned long) &notify  //  Signal to be notified on GPIO Interrupt
  );
  assert(ret >= 0);

Then we add the Signal to a Signal Set (which we shall await later)…

  //  Create an empty Signal Set
  sigset_t set;
  sigemptyset(&set);

  //  Add the signal to the Signal Set
  sigaddset(
    &set,     //  Signal Set
    SIG_DIO1  //  Signal to be added
  );

We loop forever. Inside the loop we await the Signal Set for up to 60 seconds…

  //  Loop forever waiting for the signal (DIO1 rising edge)
  for (;;) {

    //  Wait up to 60 seconds for the Signal Set
    struct timespec ts;
    ts.tv_sec  = 60;
    ts.tv_nsec = 0;
    ret = sigtimedwait(&set, NULL, &ts);

(We should probably wait forever)

If we were signalled (due to GPIO Interrupt), we add our Event to the Event Queue…

    //  Were we signalled?
    if (ret >= 0) {
      //  We were signalled. Add the DIO1 Interrupt Event to the Event Queue.
      puts("DIO1 add event");
      ble_npl_eventq_put(&event_queue, &ev);

(Which gets handled by the Event Loop in the next chapter)

If we weren’t signalled in 60 seconds, everything is hunky dory, just try again…

    } else {
      //  We were not signalled
      int errcode = errno;
      if (errcode == EAGAIN) { puts("DIO1 timeout"); }
      else { fprintf(stderr, "ERROR: Failed to wait signal %d: %d\n", SIG_DIO1, errcode); return NULL; }
    }
  }

Finally this loops back perpetually, awaiting the next GPIO Interrupt (Signal) or timeout.

That’s it for the Background Thread! We don’t do much here, we do all the work in the Event Loop.

(Which is probably safer for Multithreading)

Handle DIO1 Interrupt

8.4 Read DIO1 State

Our SX1262 Library calls this function to read the DIO1 Pin State: sx126x-nuttx.c

uint32_t SX126xGetDio1PinState( void ) {
  //  Return the value of DIO1 Pin
  assert(dio1 > 0);

  //  Read the GPIO Input
  bool invalue;
  int ret = ioctl(  //  Execute a GPIO Command...
    dio1,           //  GPIO Descriptor
    GPIOC_READ,     //  Read GPIO Input
    (unsigned long)((uintptr_t) &invalue)  //  Returned Value
  );
  assert(ret >= 0);

  //  Return the value: 1 or 0
  return invalue ? 1 : 0;
}

Check Busy State

8.5 Check Busy State

The Busy Pin goes High when SX1262 is busy. We wait for SX1262 by reading the GPIO Input for the Busy Pin: sx126x-nuttx.c

void SX126xWaitOnBusy( void ) {
  assert(busy > 0);

  //  Loop until Busy Pin is Low
  for (;;) {
    //  Read Busy Pin
    bool invalue;
    int ret = ioctl(  //  Execute a GPIO Command...
      busy,           //  GPIO Descriptor
      GPIOC_READ,     //  Read GPIO Pin
      (unsigned long)((uintptr_t) &invalue)  //  Returned value
    );
    assert(ret >= 0);

    //  Exit if Busy Pin is Low
    if (invalue == 0) { break; }
  }
}

(SX126xWaitOnBusy is called by SX126xCheckDeviceReady, which wakes up SX1262 before checking if SX1262 is busy)

Earlier we talked about the Event Queue, let’s dive into the NimBLE Porting Layer.

Multithreading with NimBLE Porting Layer

9 Multithreading with NimBLE Porting Layer

Our SX1262 Library was designed for Multithreading by calling the open-source NimBLE Porting Layer

To transmit and receive LoRa Messages without polling we need Timers and Event Queues. Which are provided by NimBLE Porting Layer.

Have we used NimBLE Porting Layer on other platforms?

Yep we used NimBLE Porting Layer in the LoRa SX1262 and SX1276 Drivers for BL602 IoT SDK…

NimBLE Porting Layer compiles on NuttX as well…

Note that NimBLE Porting Layer needs POSIX Timers and Message Queues (plus more) to work on NuttX…

How will we receive LoRa Messages with GPIO Interrupts?

According to the pic above…

  1. When SX1262 receives a LoRa Message, it triggers a GPIO Interrupt on Pin DIO1

  2. In the previous chapter we started a Background Thread that waits for GPIO Interrupts and adds a DIO1 Interrupt Event to our Event Queue

  3. In our NuttX App, the Foreground Thread runs an Event Loop that handles every Event in our Event Queue

  4. When the Event Loop receives the DIO1 Interrupt Event, it calls RadioOnDioIrq to process the received LoRa Message

Let’s look at the implementation of NimBLE Porting Layer for the new SX1262 Library that will support LoRaWAN.

(See this for the old SX1262 Library)

9.1 Event Queue

What Events will be added to our Event Queue?

Our Event Queue will have two types of Events

  1. GPIO Interrupt: Triggered via DIO1 when SX1262 has transmitted or received a LoRa Message

  2. Timer Events: All Timers for LoRa and LoRaWAN will insert Events into our Event Queue upon timeout

All Events are handled First In First Out by our Event Loop.

How do we create the Event Queue?

Our SX1262 Library creates the Event Queue by calling NimBLE Porting Layer: sx126x-nuttx.c

/// Event Queue containing Events to be processed. Exposed to NuttX App for Event Loop.
struct ble_npl_eventq event_queue;

/// True if Event Queue has been initialised
static bool is_event_queue_initialised = false;

/// Init the Event Queue
static void init_event_queue(void) {

  //  Init only once
  if (is_event_queue_initialised) { return; }
  is_event_queue_initialised = true;

  //  Init the Event Queue by calling NimBLE Porting Layer
  ble_npl_eventq_init(&event_queue);
}

Handling LoRaWAN Events with NimBLE Porting Layer

9.2 Event Loop

Let’s look at the Event Loop that handles the LoRa and LoRaWAN Events in our Event Queue: lorawan_test_main.c

/// Event Loop that dequeues Events from the Event Queue and processes the Events
static void handle_event_queue(void *arg) {

  //  Loop forever handling Events from the Event Queue
  for (;;) {

    //  Get the next Event from the Event Queue
    struct ble_npl_event *ev = ble_npl_eventq_get(
      &event_queue,         //  Event Queue
      BLE_NPL_TIME_FOREVER  //  No Timeout (Wait forever for event)
    );

This code runs in the Foreground Thread of our NuttX App.

Here we loop forever, waiting for Events from the Event Queue.

When we receive an Event, we remove the Event from the Event Queue…

    //  If no Event due to timeout, wait for next Event.
    //  Should never happen since we wait forever for an Event.
    if (ev == NULL) { printf("."); continue; }

    //  Remove the Event from the Event Queue
    ble_npl_eventq_remove(&event_queue, ev);

We call the Event Handler Function that was registered with the Event…

    //  Trigger the Event Handler Function
    ble_npl_event_run(ev);

The rest of the Event Loop handles LoRaWAN Events. We’ll cover this in the next article…

    //  For LoRaWAN: Processes the LoRaMac events
    LmHandlerProcess( );

    //  For LoRaWAN: If we have joined the network, do the uplink
    if (!LmHandlerIsBusy( )) {
      UplinkProcess( );
    }

    //  For LoRaWAN: Handle Low Power Mode
    CRITICAL_SECTION_BEGIN( );
    if( IsMacProcessPending == 1 ) {
      //  Clear flag and prevent MCU to go into low power modes.
      IsMacProcessPending = 0;
    } else {
      //  The MCU wakes up through events
      //  TODO: BoardLowPowerHandler( );
    }
    CRITICAL_SECTION_END( );
  }
}

And we loop back perpetually, waiting for Events and handling them.

That’s how we handle LoRa and LoRaWAN Events with NimBLE Porting Layer!

Porting LoRaWAN to NuttX OS

10 What’s Next

In our next article we’ll move on to LoRaWAN! We’ll port Semtech’s Reference LoRaWAN Stack to NuttX…

We’re porting plenty of code to NuttX: LoRa, LoRaWAN and NimBLE Porting Layer. Do we expect any problems?

Yep we might have issues keeping our LoRaWAN Stack in sync with Semtech’s version. (But we shall minimise the changes)

We have started porting the Rust Embedded HAL to NuttX. Here’s what we’ve done…

Now LoRa works on Rust too…

Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/sx1262.md

11 Notes

  1. This article is the expanded version of this Twitter Thread

  2. We’re porting plenty of code to NuttX: LoRa, LoRaWAN and NimBLE Porting Layer. Do we expect any problems?

    Conundrum. Lemme know your thoughts!

  3. How do other Embedded Operating Systems implement LoRaWAN?

    We’re adopting the Zephyr approach to keep our LoRaWAN Stack in sync with Semtech’s.

  4. We have already ported LoRaWAN to BL602 IoT SDK (see this), why are we porting again to NuttX?

    Regrettably BL602 IoT SDK has been revamped (without warning) to the new “hosal” HAL (see this), and the LoRaWAN Stack will no longer work on the revamped BL602 IoT SDK.

    For easier maintenance, we shall code our BL602 and BL604 projects with Apache NuttX OS instead.

    (Which won’t get revamped overnight!)

  5. Will NuttX become the official OS for PineDio Stack BL604 when it goes on sale?

    It might! But first let’s get LoRaWAN (and ST7789) running on PineDio Stack.

12 Appendix: SPI and GPIO Device Paths

PineDio Stack BL604 uses the GPIO Expander, which assigns meaningful names to GPIO Pins…

For PineDio Stack we changed the definition of DIO1_DEVPATH to “/dev/gpio19” in Kconfig / menuconfig…

CONFIG_LIBSX1262_SPI_DEVPATH="/dev/spitest0"
CONFIG_LIBSX1262_CS_DEVPATH="/dev/gpio15"
CONFIG_LIBSX1262_BUSY_DEVPATH="/dev/gpio10"
CONFIG_LIBSX1262_DIO1_DEVPATH="/dev/gpio19"

(Source)

(Note also the changes to SPI_DEVPATH, CS_DEVPATH and BUSY_DEVPATH)

For backward compatibility with BL602 (which doesn’t use GPIO Expander), we default DIO1_DEVPATH to “/dev/gpio2” if DIO1_DEVPATH isn’t configured…

//  Define the SPI Test Driver for SX1262. (Not the regular SPI Driver)

#ifdef CONFIG_LIBSX1262_SPI_DEVPATH
#define SPI_DEVPATH CONFIG_LIBSX1262_SPI_DEVPATH
#else
#define SPI_DEVPATH "/dev/spitest0"
#endif  //  CONFIG_LIBSX1262_SPI_DEVPATH

//  Define the GPIOs for SX1262 Chip Select (Output), Busy (Input) and DIO1 (Interrupt)

#ifdef CONFIG_LIBSX1262_CS_DEVPATH
#define CS_DEVPATH CONFIG_LIBSX1262_CS_DEVPATH
#else
#define CS_DEVPATH "/dev/gpio1"
#endif  //  CONFIG_LIBSX1262_CS_DEVPATH

#ifdef CONFIG_LIBSX1262_BUSY_DEVPATH
#define BUSY_DEVPATH CONFIG_LIBSX1262_BUSY_DEVPATH
#else
#define BUSY_DEVPATH "/dev/gpio0"
#endif  //  CONFIG_LIBSX1262_BUSY_DEVPATH

#ifdef CONFIG_LIBSX1262_DIO1_DEVPATH
#define DIO1_DEVPATH CONFIG_LIBSX1262_DIO1_DEVPATH
#else
#define DIO1_DEVPATH "/dev/gpio2"
#endif  //  CONFIG_LIBSX1262_DIO1_DEVPATH

(Source)

(Note also the defaults for SPI_DEVPATH, CS_DEVPATH and BUSY_DEVPATH)

13 Appendix: Create a NuttX Library

(For BL602 and ESP32)

This section explains the steps to create a NuttX Library named “libsx1262”.

(Change “libsx1262” to the desired name of our library)

  1. Browse to the “nuttx/libs” folder

  2. Copy the “libdsp” subfolder and paste it as “libsx1262”

    Copy the “libdsp” subfolder and paste it as “libsx1262”

    (Source)

  3. Inside the “libsx1262” folder, delete all source files except “lib_misc.c”

  4. Edit “Makefile”. Remove all “CSRCS” lines except…

    CSRCS += lib_misc.c

    (See Makefile)

  5. Inside the “libsx1262” folder, search and replace all “libdsp” by “libsx1262”

    Be sure to Preserve Case!

    Change all “libdsp” to “libsx1262”

    (See changes)

    (See libsx1262 folder)

  6. Edit the file “Kconfig”

    Update the section “menuconfig LIBSX1262” as follows…

    menuconfig LIBSX1262
        bool "Semtech SX1262 Library"
        default n
        ---help---
            Enable build for Semtech SX1262 functions    

    Update Kconfig

    (Source)

  7. Edit the file “lib_misc.c”. Remove all the code and add…

    #include <stdio.h>  /* TODO: Fix this for kernel mode */
    #include <sx1262.h>
    
    void test_libsx1262(void)
    {
      puts("libsx1262 OK!");
    }

    We’ll call this function in a while.

    (See lib_misc.c)

  8. Browse to the “nuttx/include” folder

  9. Copy the file “dsp.h” and paste it as “sx1262.h”

  10. Inside the file “sx1262.h”, search and replace all “dsp” by “sx1262”

    Remember to Preserve Case!

    (See sx1262.h)

  11. Edit the file “sx1262.h”, remove all Public Functions Prototypes and add…

    void test_libsx1262(void);

    We’ll test this function in a while.

    (See sx1262.h)

13.1 Update Makefiles and Kconfig

Next we update the Makefiles and Kconfig so that NuttX will build our library…

  1. Browse to the “nuttx/tools” folder

  2. Edit the file “Directories.mk”

    After “libdsp”, insert this section…

    ifeq ($(CONFIG_LIBSX1262),y)
    KERNDEPDIRS += libs$(DELIM)libsx1262
    else
    CLEANDIRS += libs$(DELIM)libsx1262
    endif

    As shown below…

    Update “Directories.mk”

    (Source)

  3. Edit the files “FlatLibs.mk”, “KernelLibs.mk” and “ProtectedLibs.mk”

    After “libopenamp”, insert this section…

    ifeq ($(CONFIG_LIBSX1262),y)
    NUTTXLIBS += staging$(DELIM)libsx1262$(LIBEXT)
    endif

    As shown below…

    Update Makefiles

    (Source)

  4. Edit the file “LibTargets.mk”

    After “libdsp”, insert this section…

    libs$(DELIM)libsx1262$(DELIM)libsx1262$(LIBEXT): pass2dep
        $(Q) $(MAKE) -C libs$(DELIM)libsx1262 libsx1262$(LIBEXT) EXTRAFLAGS="$(EXTRAFLAGS)"
    
    staging$(DELIM)libsx1262$(LIBEXT): libs$(DELIM)libsx1262$(DELIM)libsx1262$(LIBEXT)
        $(Q) $(call INSTALL_LIB,$<,$@)

    As shown in the pic above.

  5. Browse to the “nuttx” folder

  6. Edit the file “Kconfig”

    Inside the section menu “Library Routines”, add this line…

    source "libs/libsx1262/Kconfig"

    Update Root Kconfig

    (Source)

  7. Run the following…

    # TODO: Change this to the path of our "nuttx" folder
    cd nuttx/nuttx
    
    # Preserve the Build Config
    cp .config ../config
    
    # Erase the Build Config
    make distclean
    
    # For BL602: Configure the build for BL602
    ./tools/configure.sh bl602evb:nsh
    
    # For PineDio Stack BL604: Configure the build for BL604
    ./tools/configure.sh bl602evb:pinedio
    
    # For ESP32: Configure the build for ESP32.
    # TODO: Change "esp32-devkitc" to our ESP32 board.
    ./tools/configure.sh esp32-devkitc:nsh
    
    # Restore the Build Config
    cp ../config .config
    
    # Edit the Build Config
    make menuconfig 

13.2 Enable Library

We enable our library as follows…

  1. In menuconfig, select “Library Routines”

    Check the box for “Semtech SX1262 Library”

    Enable Library

  2. Hit “Save” then “OK” to save the NuttX Configuration to “.config”

  3. Hit “Exit” until menuconfig quits

13.3 Verify Library

To verify our library…

  1. We create a simple NuttX App: apps/examples/sx1262_test

    #include <nuttx/config.h>
    #include <stdio.h>
    #include <assert.h>
    #include <fcntl.h>
    #include <sx1262.h>
    
    int main(int argc, FAR char *argv[])
    {
      printf("Sx1262_test, World!!\n");
    
      /* Call SX1262 Library */
    
      test_libsx1262();
    
      return 0;
    }

    (Source)

  2. Build (“make”), flash and run the NuttX Firmware on BL602 or ESP32.

  3. In the NuttX Shell, enter…

    sx1262_test
  4. We should see the message…

    libsx1262 OK!

    Congratulations our library is now running on NuttX!

    Our library runs OK

14 Appendix: Build, Flash and Run NuttX

(For BL602 and ESP32)

Below are the steps to build, flash and run NuttX on BL602 and ESP32.

The instructions below will work on Linux (Ubuntu), WSL (Ubuntu) and macOS.

(Instructions for other platforms)

(See this for Arch Linux)

14.1 Build NuttX

Follow these steps to build NuttX for BL602 or ESP32…

  1. Install the build prerequisites…

    “Install Prerequisites”

  2. Assume that we have downloaded and configured our NuttX code…

    “Build the Firmware”

  3. To build NuttX, enter this command…

    make
  4. We should see…

    LD: nuttx
    CP: nuttx.hex
    CP: nuttx.bin

    (See the complete log for BL602)

  5. For WSL: Copy the NuttX Firmware to the c:\blflash directory in the Windows File System…

    #  /mnt/c/blflash refers to c:\blflash in Windows
    mkdir /mnt/c/blflash
    cp nuttx.bin /mnt/c/blflash

    For WSL we need to run blflash under plain old Windows CMD (not WSL) because it needs to access the COM port.

  6. In case of problems, refer to the NuttX Docs

    “BL602 NuttX”

    “ESP32 NuttX”

    “Installing NuttX”

Building NuttX

14.2 Flash NuttX

For ESP32: See instructions here (Also check out this article)

For BL602: Follow these steps to install blflash

  1. “Install rustup”

  2. “Download and build blflash”

We assume that our Firmware Binary File nuttx.bin has been copied to the blflash folder.

Set BL602 / BL604 to Flashing Mode and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to High (Like this)

  2. Disconnect the USB cable and reconnect

    Or use the Improvised Reset Button (Here’s how)

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the H Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Connect BL10 to the USB port

  2. Press and hold the D8 Button (GPIO 8)

  3. Press and release the EN Button (Reset)

  4. Release the D8 Button

For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to 3.3V

  3. Reconnect the board to the USB port

Enter these commands to flash nuttx.bin to BL602 / BL604 over UART…

# For Linux: Change "/dev/ttyUSB0" to the BL602 / BL604 Serial Port
blflash flash nuttx.bin \
  --port /dev/ttyUSB0 

# For macOS: Change "/dev/tty.usbserial-1410" to the BL602 / BL604 Serial Port
blflash flash nuttx.bin \
  --port /dev/tty.usbserial-1410 \
  --initial-baud-rate 230400 \
  --baud-rate 230400

# For Windows: Change "COM5" to the BL602 / BL604 Serial Port
blflash flash c:\blflash\nuttx.bin --port COM5

(See the Output Log)

For WSL: Do this under plain old Windows CMD (not WSL) because blflash needs to access the COM port.

(Flashing WiFi apps to BL602 / BL604? Remember to use bl_rfbin)

(More details on flashing firmware)

Flashing NuttX

14.3 Run NuttX

For ESP32: Use Picocom to connect to ESP32 over UART…

picocom -b 115200 /dev/ttyUSB0

(More about this)

For BL602: Set BL602 / BL604 to Normal Mode (Non-Flashing) and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to Low (Like this)

  2. Disconnect the USB cable and reconnect

    Or use the Improvised Reset Button (Here’s how)

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the L Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Press and release the EN Button (Reset)

For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to GND

  3. Reconnect the board to the USB port

After restarting, connect to BL602 / BL604’s UART Port at 2 Mbps like so…

For Linux:

screen /dev/ttyUSB0 2000000

For macOS: Use CoolTerm (See this)

For Windows: Use putty (See this)

Alternatively: Use the Web Serial Terminal (See this)

Press Enter to reveal the NuttX Shell

NuttShell (NSH) NuttX-10.2.0-RC0
nsh>

Congratulations NuttX is now running on BL602 / BL604!

(More details on connecting to BL602 / BL604)

Running NuttX

macOS Tip: Here’s the script I use to build, flash and run NuttX on macOS, all in a single step: run.sh

Script to build, flash and run NuttX on macOS

(Source)

15 Appendix: Radio Functions

In this section we explain the Platform-Independent Radio Functions for Semtech SX1262 Transceiver: radio.c

The code is nearly identical to Semtech’s Reference Implementation of SX1262 Driver (29 Mar 2021).

(So it should work perfectly fine with Semtech’s LoRaWAN Stack, as explained in the next article)

The Radio Functions will trigger the following Callback Functions to handle Radio Events: radio.h

The Callback Functions are defined in our LoRa Test App.

(Also in the LoRaWAN Library, as explained in the next article)

15.1 RadioInit: Initialise LoRa Module

RadioInit initialises the LoRa SX1262 Module: radio.c

void RadioInit( RadioEvents_t *events ) {
  //  We copy the Event Callbacks from "events", because
  //  "events" may be stored on the stack
  assert(events != NULL);
  memcpy(&RadioEvents, events, sizeof(RadioEvents));
  //  Previously: RadioEvents = events;

The function begins by copying the list of Radio Event Callbacks

This differs from the Semtech Reference Implementation, which copies the pointer to RadioEvents_t instead of the entire RadioEvents_t.

(Which causes problems when RadioEvents_t lives on the stack)

Next we init the SPI and GPIO Ports, wake up the LoRa Module, and init the TCXO Control and RF Switch Control.

  //  Init SPI and GPIO Ports, wake up the LoRa Module,
  //  init TCXO Control and RF Switch Control.
  SX126xInit( RadioOnDioIrq );

(SX126xInit is defined here)

(RadioOnDioIrq is explained here)

We set the LoRa Module to Standby Mode

  //  Set LoRa Module to standby mode
  SX126xSetStandby( STDBY_RC );

We set the Power Regulation: LDO or DC-DC

  //  TODO: Declare the power regulation used to power the device
  //  This command allows the user to specify if DC-DC or LDO is used for power regulation.
  //  Using only LDO implies that the Rx or Tx current is doubled

  //  #warning SX126x is set to LDO power regulator mode (instead of DC-DC)
  //  SX126xSetRegulatorMode( USE_LDO );   //  Use LDO

  //  #warning SX126x is set to DC-DC power regulator mode (instead of LDO)
  SX126xSetRegulatorMode( USE_DCDC );  //  Use DC-DC

(SX126xSetRegulatorMode is defined here)

This depends on how our LoRa Module is wired for power.

For now we’re using DC-DC Power Regulation. (To be verified)

(More about LDO vs DC-DC Power Regulation)

We set the Base Addresses of the Read and Write Buffers to 0…

  //  Set the base addresses of the Read and Write Buffers to 0
  SX126xSetBufferBaseAddress( 0x00, 0x00 );

(SX126xSetBufferBaseAddress is defined here)

The Read and Write Buffers are accessed by sx126x_read_buffer and sx126x_write_buffer.

We set the Transmit Power and the Ramp Up Time

  //  TODO: Set the correct transmit power and ramp up time
  SX126xSetTxParams( 22, RADIO_RAMP_3400_US );
  //  TODO: Previously: SX126xSetTxParams( 0, RADIO_RAMP_200_US );

(SX126xSetTxParams is defined here)

Ramp Up Time is the duration (in microseconds) we need to wait for SX1262’s Power Amplifier to ramp up (charge up) to the configured Transmit Power.

For easier testing we have set the Transmit Power to 22 dBm (highest power) and Ramp Up Time to 3400 microseconds (longest duration).

(To give sufficient time for the Power Amplifier to ramp up to the highest Transmit Power)

After testing we should revert to the Default Transmit Power (0) and Ramp Up Time (200 microseconds).

(More about the Transmit Power)

(Over Current Protection in SX126xSetTxParams)

We configure which LoRa Events will trigger interrupts on each DIO Pin…

  //  Set the DIO Interrupt Events:
  //  All LoRa Events will trigger interrupts on DIO1
  SX126xSetDioIrqParams(
    IRQ_RADIO_ALL,   //  Interrupt Mask
    IRQ_RADIO_ALL,   //  Interrupt Events for DIO1
    IRQ_RADIO_NONE,  //  Interrupt Events for DIO2
    IRQ_RADIO_NONE   //  Interrupt Events for DIO3
  );

(SX126xSetDioIrqParams is defined here)

(All LoRa Events will trigger interrupts on DIO1)

We define the SX1262 Registers that will be restored from Retention Memory when waking up from Warm Start Mode…

  //  Add registers to the retention list (4 is the maximum possible number)
  RadioAddRegisterToRetentionList( REG_RX_GAIN );
  RadioAddRegisterToRetentionList( REG_TX_MODULATION );

(RadioAddRegisterToRetentionList is defined here)

Finally we init the Timeout Timers (from NimBLE Porting Layer) for Transmit Timeout and Receive Timeout

  //  Initialize driver timeout timers
  TimerInit( &TxTimeoutTimer, RadioOnTxTimeoutIrq );
  TimerInit( &RxTimeoutTimer, RadioOnRxTimeoutIrq );

  //  Interrupt not fired yet
  IrqFired = false;
}

(TimerInit is defined here)

15.2 RadioSetChannel: Set LoRa Frequency

RadioSetChannel sets the LoRa Frequency: radio.c

void RadioSetChannel( uint32_t freq ) {
  SX126xSetRfFrequency( freq );
}

RadioSetChannel passes the LoRa Frequency (like 923000000 for 923 MHz) to SX126xSetRfFrequency.

SX126xSetRfFrequency is defined as follows: sx126x.c

void SX126xSetRfFrequency( uint32_t frequency ) {
  uint8_t buf[4];
  if( ImageCalibrated == false ) {
    SX126xCalibrateImage( frequency );
    ImageCalibrated = true;
  }
  uint32_t freqInPllSteps = SX126xConvertFreqInHzToPllStep( frequency );
  buf[0] = ( uint8_t )( ( freqInPllSteps >> 24 ) & 0xFF );
  buf[1] = ( uint8_t )( ( freqInPllSteps >> 16 ) & 0xFF );
  buf[2] = ( uint8_t )( ( freqInPllSteps >> 8 ) & 0xFF );
  buf[3] = ( uint8_t )( freqInPllSteps & 0xFF );
  SX126xWriteCommand( RADIO_SET_RFFREQUENCY, buf, 4 );
}

(SX126xCalibrateImage is defined here)

(SX126xConvertFreqInHzToPllStep is defined here)

(SX126xWriteCommand is defined here)

15.3 RadioSetTxConfig: Set Transmit Configuration

RadioSetTxConfig sets the LoRa Transmit Configuration: radio.c

void RadioSetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev,
  uint32_t bandwidth, uint32_t datarate,
  uint8_t coderate, uint16_t preambleLen,
  bool fixLen, bool crcOn, bool freqHopOn,
  uint8_t hopPeriod, bool iqInverted, uint32_t timeout ) {

  //  LoRa Modulation or FSK Modulation?
  switch( modem ) {
    case MODEM_FSK:
      //  Omitted: FSK Modulation
      ...

Since we’re using LoRa Modulation instead of FSK Modulation, we skip the section on FSK Modulation.

We begin by populating the Modulation Parameters: Spreading Factor, Bandwidth and Coding Rate…

    case MODEM_LORA:
      //  LoRa Modulation
      SX126x.ModulationParams.PacketType = 
        PACKET_TYPE_LORA;
      SX126x.ModulationParams.Params.LoRa.SpreadingFactor = 
        ( RadioLoRaSpreadingFactors_t ) datarate;
      SX126x.ModulationParams.Params.LoRa.Bandwidth =  
        Bandwidths[bandwidth];
      SX126x.ModulationParams.Params.LoRa.CodingRate = 
        ( RadioLoRaCodingRates_t )coderate;

Depending on the LoRa Parameters, we optimise for Low Data Rate

      //  Optimise for Low Data Rate
      if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
      ( ( bandwidth == 1 ) && ( datarate == 12 ) ) ) {
        SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01;
      } else {
        SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x00;
      }

Next we populate the Packet Parameters: Preamble Length, Header Type, Payload Length, CRC Mode and Invert IQ…

      //  Populate Packet Type
      SX126x.PacketParams.PacketType = PACKET_TYPE_LORA;

      //  Populate Preamble Length
      if( ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF5 ) ||
        ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF6 ) ) {
        if( preambleLen < 12 ) {
          SX126x.PacketParams.Params.LoRa.PreambleLength = 12;
        } else {
          SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen;
        }
      } else {
        SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen;
      }

      //  Populate Header Type, Payload Length, CRC Mode and Invert IQ
      SX126x.PacketParams.Params.LoRa.HeaderType = 
        ( RadioLoRaPacketLengthsMode_t )fixLen;
      SX126x.PacketParams.Params.LoRa.PayloadLength = 
        MaxPayloadLength;
      SX126x.PacketParams.Params.LoRa.CrcMode = 
        ( RadioLoRaCrcModes_t )crcOn;
      SX126x.PacketParams.Params.LoRa.InvertIQ = 
        ( RadioLoRaIQModes_t )iqInverted;

We set the LoRa Module to Standby Mode and configure it for LoRa Modulation (or FSK Modulation)…

      //  Set LoRa Module to Standby Mode
      RadioStandby( );

      //  Configure LoRa Module for LoRa Modulation (or FSK Modulation)
      RadioSetModem( 
        ( SX126x.ModulationParams.PacketType == PACKET_TYPE_GFSK ) 
        ? MODEM_FSK 
        : MODEM_LORA
      );

(RadioStandby is defined here)

(RadioSetModem is defined here)

We configure the LoRa Module with the Modulation Parameters and Packet Parameters

      //  Configure Modulation Parameters
      SX126xSetModulationParams( &SX126x.ModulationParams );

      //  Configure Packet Parameters
      SX126xSetPacketParams( &SX126x.PacketParams );
      break;
  }

(SX126xSetModulationParams is defined here)

(SX126xSetPacketParams is defined here)

This is a Workaround for Modulation Quality with 500 kHz Bandwidth

  // WORKAROUND - Modulation Quality with 500 kHz LoRa Bandwidth, see DS_SX1261-2_V1.2 datasheet chapter 15.1
  if( ( modem == MODEM_LORA ) && ( SX126x.ModulationParams.Params.LoRa.Bandwidth == LORA_BW_500 ) ) {
    SX126xWriteRegister( 
      REG_TX_MODULATION, 
      SX126xReadRegister( REG_TX_MODULATION ) & ~( 1 << 2 ) 
    );
  } else {
    SX126xWriteRegister( 
      REG_TX_MODULATION, 
      SX126xReadRegister( REG_TX_MODULATION ) | ( 1 << 2 ) 
    );
  }
  // WORKAROUND END

(SX126xWriteRegister is defined here)

(SX126xReadRegister is defined here)

We finish by setting the Transmit Power and Transmit Timeout

  //  Set Transmit Power
  SX126xSetRfTxPower( power );

  //  Set Transmit Timeout
  TxTimeout = timeout;
}

SX126xSetRfTxPower is defined in sx126x-nuttx.c

void SX126xSetRfTxPower( int8_t power ) {
  //  TODO: Previously: SX126xSetTxParams( power, RADIO_RAMP_40_US );
  SX126xSetTxParams( power, RADIO_RAMP_3400_US );  //  TODO
}

For easier testing we have set the Ramp Up Time to 3400 microseconds (longest duration).

After testing we should revert to the Default Ramp Up Time (40 microseconds).

15.4 RadioSetRxConfig: Set Receive Configuration

RadioSetRxConfig sets the LoRa Receive Configuration: radio.c

void RadioSetRxConfig( RadioModems_t modem, uint32_t bandwidth,
  uint32_t datarate, uint8_t coderate,
  uint32_t bandwidthAfc, uint16_t preambleLen,
  uint16_t symbTimeout, bool fixLen,
  uint8_t payloadLen,
  bool crcOn, bool freqHopOn, uint8_t hopPeriod,
  bool iqInverted, bool rxContinuous ) {

  //  Set Symbol Timeout
  RxContinuous = rxContinuous;
  if( rxContinuous == true ) {
    symbTimeout = 0;
  }

  //  Set Max Payload Length
  if( fixLen == true ) {
    MaxPayloadLength = payloadLen;
  }
  else {
    MaxPayloadLength = 0xFF;
  }

We begin by setting the Symbol Timeout and Max Payload Length.

Since we’re using LoRa Modulation instead of FSK Modulation, we skip the section on FSK Modulation…

  //  LoRa Modulation or FSK Modulation?
  switch( modem )
  {
    case MODEM_FSK:
      //  Omitted: FSK Modulation
      ...

We populate the Modulation Parameters: Spreading Factor, Bandwidth and Coding Rate…

    case MODEM_LORA:
      //  LoRa Modulation
      SX126xSetStopRxTimerOnPreambleDetect( false );
      SX126x.ModulationParams.PacketType = 
        PACKET_TYPE_LORA;
      SX126x.ModulationParams.Params.LoRa.SpreadingFactor = 
        ( RadioLoRaSpreadingFactors_t )datarate;
      SX126x.ModulationParams.Params.LoRa.Bandwidth = 
        Bandwidths[bandwidth];
      SX126x.ModulationParams.Params.LoRa.CodingRate = 
        ( RadioLoRaCodingRates_t )coderate;

Depending on the LoRa Parameters, we optimise for Low Data Rate

      //  Optimise for Low Data Rate
      if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
      ( ( bandwidth == 1 ) && ( datarate == 12 ) ) ) {
        SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01;
      } else {
        SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x00;
      }

We populate the Packet Parameters: Preamble Length, Header Type, Payload Length, CRC Mode and Invert IQ…

      //  Populate Packet Type
      SX126x.PacketParams.PacketType = PACKET_TYPE_LORA;

      //  Populate Preamble Length
      if( ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF5 ) ||
          ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF6 ) ){
        if( preambleLen < 12 ) {
          SX126x.PacketParams.Params.LoRa.PreambleLength = 12;
        } else {
          SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen;
        }
      } else {
        SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen;
      }

      //  Populate Header Type, Payload Length, CRC Mode and Invert IQ
      SX126x.PacketParams.Params.LoRa.HeaderType = 
        ( RadioLoRaPacketLengthsMode_t )fixLen;
      SX126x.PacketParams.Params.LoRa.PayloadLength = 
        MaxPayloadLength;
      SX126x.PacketParams.Params.LoRa.CrcMode = 
        ( RadioLoRaCrcModes_t )crcOn;
      SX126x.PacketParams.Params.LoRa.InvertIQ = 
        ( RadioLoRaIQModes_t )iqInverted;

We set the LoRa Module to Standby Mode and configure it for LoRa Modulation (or FSK Modulation)…

      //  Set LoRa Module to Standby Mode
      RadioStandby( );

      //  Configure LoRa Module for LoRa Modulation (or FSK Modulation)
      RadioSetModem( 
          ( SX126x.ModulationParams.PacketType == PACKET_TYPE_GFSK ) 
          ? MODEM_FSK 
          : MODEM_LORA 
      );

(RadioStandby is defined here)

(RadioSetModem is defined here)

We configure the LoRa Module with the Modulation Parameters, Packet Parameters and Symbol Timeout…

      //  Configure Modulation Parameters
      SX126xSetModulationParams( &SX126x.ModulationParams );

      //  Configure Packet Parameters
      SX126xSetPacketParams( &SX126x.PacketParams );

      //  Configure Symbol Timeout
      SX126xSetLoRaSymbNumTimeout( symbTimeout );

(SX126xSetModulationParams is defined here)

(SX126xSetPacketParams is defined here)

(SX126xSetLoRaSymbNumTimeout is defined here)

This is a Workaround that optimises the Inverted IQ Operation

      // WORKAROUND - Optimizing the Inverted IQ Operation, see DS_SX1261-2_V1.2 datasheet chapter 15.4
      if( SX126x.PacketParams.Params.LoRa.InvertIQ == LORA_IQ_INVERTED ) {
        SX126xWriteRegister( 
          REG_IQ_POLARITY, 
          SX126xReadRegister( REG_IQ_POLARITY ) & ~( 1 << 2 ) 
        );
      } else {
        SX126xWriteRegister( 
          REG_IQ_POLARITY, 
          SX126xReadRegister( REG_IQ_POLARITY ) | ( 1 << 2 ) 
        );
      }
      // WORKAROUND END

(SX126xWriteRegister is defined here)

(SX126xReadRegister is defined here)

We finish by setting the Receive Timeout to No Timeout (always receiving)…

      // Timeout Max, Timeout handled directly in SetRx function
      RxTimeout = 0xFFFF;
      break;
  }
}

15.5 RadioSend: Transmit Message

RadioSend transmits a LoRa Message: radio.c

void RadioSend( uint8_t *buffer, uint8_t size ) {

  //  Set the DIO Interrupt Events:
  //  Transmit Done and Transmit Timeout
  //  will trigger interrupts on DIO1
  SX126xSetDioIrqParams( 
    IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,  //  Interrupt Mask
    IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,  //  Interrupt Events for DIO1
    IRQ_RADIO_NONE,  //  Interrupt Events for DIO2
    IRQ_RADIO_NONE   //  Interrupt Events for DIO3
  );

(SX126xSetDioIrqParams is defined here)

We begin by configuring which LoRa Events will trigger interrupts on each DIO Pin.

(Transmit Done and Transmit Timeout will trigger interrupts on DIO1)

Next we configure the Packet Parameters

  //  Populate the payload length
  if( SX126xGetPacketType( ) == PACKET_TYPE_LORA ) {
    SX126x.PacketParams.Params.LoRa.PayloadLength = size;
  } else {
    SX126x.PacketParams.Params.Gfsk.PayloadLength = size;
  }
  //  Configure the packet parameters
  SX126xSetPacketParams( &SX126x.PacketParams );

(SX126xGetPacketType is defined here)

(SX126xSetPacketParams is defined here)

We finish by sending the Message Payload and starting the Transmit Timer

  //  Send message payload
  SX126xSendPayload( buffer, size, 0 );

  //  Start Transmit Timer
  TimerStart2( &TxTimeoutTimer, TxTimeout );
}

(TimerStart2 is defined here)

SX126xSendPayload is defined below: sx126x.c

///  Send message payload
void SX126xSendPayload( uint8_t *payload, uint8_t size, uint32_t timeout ) {
  //  Copy message payload to Transmit Buffer
  SX126xSetPayload( payload, size );

  //  Transmit the buffer
  SX126xSetTx( timeout );
}

(SX126xSetTx is defined here)

This code copies the Message Payload to the SX1262 Transmit Buffer and transmits the message.

SX126xSetPayload copies to the Transmit Buffer by calling SX126xWriteBuffer: sx126x.c

/// Copy message payload to Transmit Buffer
void SX126xSetPayload( uint8_t *payload, uint8_t size ) {
  //  Copy message payload to Transmit Buffer
  SX126xWriteBuffer( 0x00, payload, size );
}

SX126xWriteBuffer wakes up the LoRa Module, writes to the Transmit Buffer and waits for the operation to be completed: sx126x.c

/// Copy message payload to Transmit Buffer
void SX126xWriteBuffer( uint8_t offset, uint8_t *buffer, uint8_t size ) {
  //  Wake up SX1262 if sleeping
  SX126xCheckDeviceReady( );

  //  Copy message payload to Transmit Buffer
  int rc = sx126x_write_buffer(NULL, offset, buffer, size);
  assert(rc == 0);

  //  Wait for SX1262 to be ready
  SX126xWaitOnBusy( );
}

(SX126xCheckDeviceReady is defined here)

(sx126x_write_buffer is explained here)

(SX126xWaitOnBusy is defined here)

When the LoRa Message is transmitted (successfully or unsuccessfully), the LoRa Module triggers a DIO1 Interrupt.

Our driver calls RadioIrqProcess to process the interrupt. (See this)

15.6 RadioRx: Receive Message

RadioRx preps SX1262 to receive a single LoRa Message: radio.c

void RadioRx( uint32_t timeout ) {

  //  Set the DIO Interrupt Events:
  //  All LoRa Events will trigger interrupts on DIO1
  SX126xSetDioIrqParams(
    IRQ_RADIO_ALL,   //  Interrupt Mask
    IRQ_RADIO_ALL,   //  Interrupt Events for DIO1
    IRQ_RADIO_NONE,  //  Interrupt Events for DIO2
    IRQ_RADIO_NONE   //  Interrupt Events for DIO3
  );

(SX126xSetDioIrqParams is defined here)

We begin by configuring which LoRa Events will trigger interrupts on each DIO Pin.

(All LoRa Events will trigger interrupts on DIO1)

We start the Receive Timer to catch Receive Timeouts…

  //  Start the Receive Timer
  if( timeout != 0 ) {
    TimerStart2( &RxTimeoutTimer, timeout );
  }

(TimerStart2 is defined here)

Now we begin to receive a LoRa Message continuously, or until a timeout occurs…

  if( RxContinuous == true ) {
    //  Receive continuously
    SX126xSetRx( 0xFFFFFF ); // Rx Continuous
  } else {
    //  Receive with timeout
    SX126xSetRx( RxTimeout << 6 );
  }
}

SX126xSetRx enters Receive Mode like so: sx126x.c

void SX126xSetRx( uint32_t timeout ) {
  uint8_t buf[3];

  //  Remember we're in Receive Mode
  SX126xSetOperatingMode( MODE_RX );

  //  Configure Receive Gain
  SX126xWriteRegister( REG_RX_GAIN, 0x94 ); // default gain

  //  Enter Receive Mode
  buf[0] = ( uint8_t )( ( timeout >> 16 ) & 0xFF );
  buf[1] = ( uint8_t )( ( timeout >> 8 ) & 0xFF );
  buf[2] = ( uint8_t )( timeout & 0xFF );
  SX126xWriteCommand( RADIO_SET_RX, buf, 3 );
}

(SX126xSetOperatingMode is defined here)

(SX126xWriteRegister is defined here)

(SX126xWriteCommand is defined here)

When a LoRa Message is received (successfully or unsuccessfully), the LoRa Module triggers a DIO1 Interrupt.

Our driver calls RadioIrqProcess to process the interrupt, which is explained next…

15.7 RadioIrqProcess: Process Transmit and Receive Interrupts

RadioIrqProcess processes the interrupts that are triggered when a LoRa Message is transmitted and received: radio.c

/// Process Transmit and Receive Interrupts.
/// For BL602: Must be run in the Application
/// Task Context, not Interrupt Context because 
/// we will call printf and SPI Functions here.
void RadioIrqProcess( void ) {

  //  Remember and clear Interrupt Flag
  CRITICAL_SECTION_BEGIN( );
  const bool isIrqFired = IrqFired;
  IrqFired = false;
  CRITICAL_SECTION_END( );

(Note: Critical Sections are not yet implemented)

The function begins by copying the Interrupt Flag and clearing the flag.

(The Interrupt Flag is set by RadioOnDioIrq)

The rest of the function will run only if the Interrupt Flag was originally set

  //  IrqFired must be true to process interrupts
  if( isIrqFired == true ) {
    //  Get the Interrupt Status
    uint16_t irqRegs = SX126xGetIrqStatus( );

    //  Clear the Interrupt Status
    SX126xClearIrqStatus( irqRegs );

(SX126xGetIrqStatus is defined here)

(SX126xClearIrqStatus is defined here)

This code fetches the Interrupt Status from the LoRa Module and clears the Interrupt Status.

If DIO1 is still High, we set the Interrupt Flag for future processing…

    //  Check if DIO1 pin is High. If it is the case revert IrqFired to true
    CRITICAL_SECTION_BEGIN( );
    if( SX126xGetDio1PinState( ) == 1 ) {
      IrqFired = true;
    }
    CRITICAL_SECTION_END( );

Interrupt Status tells us which LoRa Events have just occurred. We handle the LoRa Events accordingly…

15.7.1 Transmit Done

When the LoRa Module has transmitted a LoRa Message successfully, we stop the Transmit Timer and call the Callback Function for Transmit Done: radio.c

    //  If a LoRa Message was transmitted successfully...
    if( ( irqRegs & IRQ_TX_DONE ) == IRQ_TX_DONE ) {

      //  Stop the Transmit Timer
      TimerStop( &TxTimeoutTimer );

      //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
      SX126xSetOperatingMode( MODE_STDBY_RC );

      //  Call the Callback Function for Transmit Done
      if( ( RadioEvents.TxDone != NULL ) ) {
        RadioEvents.TxDone( );
      }
    }

(TimerStop is defined here)

(SX126xSetOperatingMode is defined here)

TxDone points to the on_tx_done Callback Function that we’ve seen earlier.

15.7.2 Receive Done

When the LoRa Module receives a LoRa Message, we stop the Receive Timer: radio.c

    //  If a LoRa Message was received...
    if( ( irqRegs & IRQ_RX_DONE ) == IRQ_RX_DONE ) {

      //  Stop the Receive Timer
      TimerStop( &RxTimeoutTimer );

In case of CRC Error, we call the Callback Function for Receive Error

      if( ( irqRegs & IRQ_CRC_ERROR ) == IRQ_CRC_ERROR ) {

        //  If the received message has CRC Error...
        if( RxContinuous == false ) {
          //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
          SX126xSetOperatingMode( MODE_STDBY_RC );
        }

        //  Call the Callback Function for Receive Error
        if( ( RadioEvents.RxError ) ) {
          RadioEvents.RxError( );
        }

RxError points to the on_rx_error Callback Function that we’ve seen earlier.

If the received message has no CRC Error, we do this Workaround for Implicit Header Mode Timeout Behavior

      } else {
        //  If the received message has no CRC Error...
        uint8_t size;

        //  If we are receiving continously...
        if( RxContinuous == false ) {
          //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
          SX126xSetOperatingMode( MODE_STDBY_RC );

          // WORKAROUND - Implicit Header Mode Timeout Behavior, see DS_SX1261-2_V1.2 datasheet chapter 15.3
          SX126xWriteRegister( REG_RTC_CTRL, 0x00 );
          SX126xWriteRegister( 
            REG_EVT_CLR, 
            SX126xReadRegister( REG_EVT_CLR ) | ( 1 << 1 ) 
          );
          // WORKAROUND END
        }

(SX126xWriteRegister is defined here)

(SX126xReadRegister is defined here)

Then we copy the Received Message Payload and get the Packet Status

        //  Copy the Received Message Payload (max 255 bytes)
        SX126xGetPayload( RadioRxPayload, &size , 255 );
        
        //  Get the Packet Status:
        //  Packet Signal Strength (RSSI), Signal-to-Noise Ratio (SNR),
        //  Signal RSSI, Frequency Error
        SX126xGetPacketStatus( &RadioPktStatus );

(SX126xGetPacketStatus is defined here)

And we call the Callback Function for Receive Done

        //  Call the Callback Function for Receive Done
        if( ( RadioEvents.RxDone != NULL ) ) {
          RadioEvents.RxDone( 
            RadioRxPayload, 
            size, 
            RadioPktStatus.Params.LoRa.RssiPkt, 
            RadioPktStatus.Params.LoRa.SnrPkt 
          );
        }
      }
    }

RxDone points to the on_rx_done Callback Function that we’ve seen earlier.

SX126xGetPayload copies the received message payload from the SX1262 Receive Buffer: sx126x.c

/// Copy message payload from Receive Buffer
uint8_t SX126xGetPayload( uint8_t *buffer, uint8_t *size,  uint8_t maxSize ) {
  uint8_t offset = 0;

  //  Get the size and offset of the received message
  //  in the Receive Buffer
  SX126xGetRxBufferStatus( size, &offset );
  if( *size > maxSize ) {
    return 1;
  }

  //  Copy message payload from Receive Buffer
  SX126xReadBuffer( offset, buffer, *size );
  return 0;
}

(SX126xGetRxBufferStatus is defined here)

SX126xReadBuffer wakes up the LoRa Module, reads from the Receive Buffer and waits for the operation to be completed: sx126x-nuttx.c

/// Copy message payload from Receive Buffer
void SX126xReadBuffer( uint8_t offset, uint8_t *buffer, uint8_t size ) {
  //  Wake up SX1262 if sleeping
  SX126xCheckDeviceReady( );

  //  Copy message payload from Receive Buffer
  int rc = sx126x_read_buffer(NULL, offset, buffer, size);
  assert(rc == 0);

  //  Wait for SX1262 to be ready
  SX126xWaitOnBusy( );
}

(SX126xCheckDeviceReady is defined here)

(sx126x_read_buffer is explained here)

(SX126xWaitOnBusy is defined here)

15.7.3 CAD Done

Channel Activity Detection lets us detect whether there’s any ongoing transmission in a LoRa Radio Channel, in a power-efficient way.

We won’t be doing Channel Activity Detection in our driver: radio.c

    //  If Channel Activity Detection is complete...
    if( ( irqRegs & IRQ_CAD_DONE ) == IRQ_CAD_DONE ) {

      //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
      SX126xSetOperatingMode( MODE_STDBY_RC );

      //  Call Callback Function for CAD Done
      if( ( RadioEvents.CadDone != NULL ) ) {
        RadioEvents.CadDone( ( 
            ( irqRegs & IRQ_CAD_ACTIVITY_DETECTED ) 
            == IRQ_CAD_ACTIVITY_DETECTED 
        ) );
      }
    }

15.7.4 Transmit / Receive Timeout

When the LoRa Module fails to transmit a LoRa Message due to Timeout, we stop the Transmit Timer and call the Callback Function for Transmit Timeout: radio.c

    //  If a LoRa Message failed to Transmit or Receive due to Timeout...
    if( ( irqRegs & IRQ_RX_TX_TIMEOUT ) == IRQ_RX_TX_TIMEOUT ) {

      //  If the message failed to Transmit due to Timeout...
      if( SX126xGetOperatingMode( ) == MODE_TX ) {

        //  Stop the Transmit Timer
        TimerStop( &TxTimeoutTimer );

        //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
        SX126xSetOperatingMode( MODE_STDBY_RC );

        //  Call the Callback Function for Transmit Timeout
        if( ( RadioEvents.TxTimeout != NULL ) ) {
          RadioEvents.TxTimeout( );
        }
      }

(SX126xGetOperatingMode is defined here)

TxTimeout points to the on_tx_timeout Callback Function that we’ve seen earlier.

When the LoRa Module fails to receive a LoRa Message due to Timeout, we stop the Receive Timer and call the Callback Function for Receive Timeout

      //  If the message failed to Receive due to Timeout...
      else if( SX126xGetOperatingMode( ) == MODE_RX ) {

        //  Stop the Receive Timer
        TimerStop( &RxTimeoutTimer );

        //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
        SX126xSetOperatingMode( MODE_STDBY_RC );

        //  Call the Callback Function for Receive Timeout
        if( ( RadioEvents.RxTimeout != NULL ) ) {
          RadioEvents.RxTimeout( );
        }
      }
    }

RxTimeout points to the on_rx_timeout Callback Function that we’ve seen earlier.

15.7.5 Preamble Detected

Preamble is the Radio Signal that precedes the LoRa Message. When the LoRa Module detects the Preamble Signal, it knows that it’s about to receive a LoRa Message.

We don’t need to handle the Preamble Signal, the LoRa Module does it for us: radio.c

    //  If LoRa Preamble was detected...
    if( ( irqRegs & IRQ_PREAMBLE_DETECTED ) == IRQ_PREAMBLE_DETECTED ) {
      //__NOP( );
    }

Our Receive Message Log shows that the Preamble Signal (IRQ_PREAMBLE_DETECTED) is always detected before receiving a LoRa Message.

(IRQ_PREAMBLE_DETECTED appears just before the LoRa Header: IRQ_HEADER_VALID)

(More about LoRa Preamble)

15.7.6 Sync Word Valid

Sync Words are 16-bit values that differentiate the types of LoRa Networks.

The LoRa Module detects the Sync Words when it receive a LoRa Message: radio.c

    //  If a valid Sync Word was detected...
    if( ( irqRegs & IRQ_SYNCWORD_VALID ) == IRQ_SYNCWORD_VALID ) {
      //__NOP( );
    }

Note that the Sync Word differs for LoRaWAN vs Private LoRa Networks…

//  Syncword for Private LoRa networks
#define LORA_MAC_PRIVATE_SYNCWORD                   0x1424

//  Syncword for Public LoRa networks (LoRaWAN)
#define LORA_MAC_PUBLIC_SYNCWORD                    0x3444

(More about Sync Words)

15.7.7 Header Valid

The LoRa Module checks for a valid LoRa Header when receiving a LoRa Message: radio.c

    //  If a valid Header was received...
    if( ( irqRegs & IRQ_HEADER_VALID ) == IRQ_HEADER_VALID ) {
      //__NOP( );
    }

Our Receive Message Log shows that the LoRa Header (IRQ_HEADER_VALID) is always detected before receiving a LoRa Message.

(IRQ_HEADER_VALID appears right after the Preamble Signal: IRQ_PREAMBLE_DETECTED)

15.7.8 Header Error

When the LoRa Module detects a LoRa Header with CRC Error, we stop the Receive Timer and call the Callback Function for Receive Timeout: radio.c

    //  If a Header with CRC Error was received...
    if( ( irqRegs & IRQ_HEADER_ERROR ) == IRQ_HEADER_ERROR ) {

      //  Stop the Receive Timer
      TimerStop( &RxTimeoutTimer );

      if( RxContinuous == false ) {
        //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC
        SX126xSetOperatingMode( MODE_STDBY_RC );
      }

      //  Call the Callback Function for Receive Timeout
      if( ( RadioEvents.RxTimeout != NULL ) ) {
        RadioEvents.RxTimeout( );
      }
    }
  }
}

RxTimeout points to the on_rx_timeout Callback Function that we’ve seen earlier.

15.7.9 RadioOnDioIrq

RadioIrqProcess (as defined above) is called by RadioOnDioIrq to handle LoRa Transmit and Receive Events: radio.c

/// Callback Function for Transmit and Receive Interrupts.
/// For BL602: This function runs in the context of the 
/// Background Application Task. So we are safe to call 
/// printf and SPI Functions now.
void RadioOnDioIrq( struct ble_npl_event *ev ) {
  //  Set the Interrupt Flag
  IrqFired = true;

  //  BL602 Note: It's OK to process the interrupt here because we are in
  //  Application Task Context, not Interrupt Context.
  //  The Reference Implementation processes the interrupt in the main loop.
  RadioIrqProcess();
}

15.8 RadioSleep: Switch to Sleep Mode

RadioSleep switches SX1262 to low-power sleep mode: radio.c

/// Switch to Sleep Mode
void RadioSleep( void ) {
  SleepParams_t params = { 0 };
  params.Fields.WarmStart = 1;

  //  Switch to Sleep Mode and wait 2 milliseconds
  SX126xSetSleep( params );
  DelayMs( 2 );
}

SX126xSetSleep executes the Sleep Command on the LoRa Module: sx126x.c

/// Switch to Sleep Mode
void SX126xSetSleep( SleepParams_t sleepConfig ) {
  //  Switch off antenna (not used)
  SX126xAntSwOff( );

  //  Compute Sleep Parameter
  uint8_t value = ( 
      ( ( uint8_t )sleepConfig.Fields.WarmStart << 2 ) |
      ( ( uint8_t )sleepConfig.Fields.Reset << 1 ) |
      ( ( uint8_t )sleepConfig.Fields.WakeUpRTC ) 
  );

  if( sleepConfig.Fields.WarmStart == 0 ) {
    // Force image calibration
    ImageCalibrated = false;
  }

  //  Run Sleep Command
  SX126xWriteCommand( RADIO_SET_SLEEP, &value, 1 );
  SX126xSetOperatingMode( MODE_SLEEP );
}

(SX126xAntSwOff is defined here)

(SX126xWriteCommand is defined here)

(SX126xSetOperatingMode is defined here)

16 Appendix: GPIO Pin Type Issue

When we switch a GPIO Interrupt Pin Type to Trigger On Rising Edge, it crashes with an Assertion Failure…

nsh> gpio -t 8 -w 1 /dev/gpio2

Driver: /dev/gpio2
up_assert: Assertion failed at file:ioexpander/gpio.c line: 544 task: gpio

(For PineDio Stack BL604: Use “/dev/gpio19”)

I’ll submit a NuttX Issue, meanwhile I have disabled the assertion…

GPIO Pin Type Issue

17 Appendix: NimBLE Callout Issue

NimBLE Porting Layer doesn’t work for multiple Callout Timers unless we loop the thread…

NimBLE Callout Issue

(Source)

I will submit a Pull Request to Apache NimBLE.

UPDATE: Unfortunately the thread never terminates, so any NuttX App that calls NimBLE Callouts won’t terminate either. (Even when we call exit()). We need to terminate the thread in our code.

18 Appendix: Previous SX1262 Library

This section describes the previous (obsolete) version of the SX1262 Library…

Which has been superseded by the new version of the SX1262 Library…

The previous version does NOT support LoRaWAN, GPIO Interface and NimBLE Porting Layer.

Huh? SX1262 works without GPIO control?

We found some sneaky workarounds to control LoRa SX1262 without GPIO

These sneaky hacks will need to be fixed by calling the GPIO Interface.

What needs to be fixed for GPIO?

We need to mod these functions to call the NuttX GPIO Interface

  1. Initialise the GPIO Pins: SX126xIoInit

    (Similar to BL602 IoT SDK)

  2. Register GPIO Interrupt Handler for DIO1: SX126xIoIrqInit

    (Similar to BL602 IoT SDK)

  3. Reset SX1262 via GPIO: SX126xReset

    void SX126xReset(void) {
        //  TODO: Set Reset pin to Low
        //  rc = bl_gpio_output_set(SX126X_NRESET, 1);
        //  assert(rc == 0);
    
        //  Wait 1 ms
        DelayMs(1);
    
        //  TODO: Configure Reset pin as a GPIO Input Pin, no pullup, no pulldown
        //  rc = bl_gpio_enable_input(SX126X_NRESET, 0, 0);
        //  assert(rc == 0);
    
        //  Wait 6 ms
        DelayMs(6);
    }
  4. Check SX1262 Busy State via GPIO: SX126xWaitOnBusy

    (SX126xWaitOnBusy is called by SX126xCheckDeviceReady, which wakes up SX1262 before checking if SX1262 is busy)

    void SX126xWaitOnBusy(void) {
      //  TODO: Fix the GPIO check for busy state.
      //  while( bl_gpio_input_get_value( SX126X_BUSY_PIN ) == 1 );
    
      //  Meanwhile we sleep 10 milliseconds
      usleep(10 * 1000);
    }
  5. Get DIO1 Pin State: SX126xGetDio1PinState

    uint32_t SX126xGetDio1PinState(void) {    
      //  TODO: Read and return DIO1 Pin State
      //  return bl_gpio_input_get_value( SX126X_DIO1 );
    
      //  Meanwhile we always return 0
      return 0;
    }

When we have implemented GPIO Interrupts in our driver, we can remove the Event Polling. And we run a Background Thread to handle LoRa Events.

How will we receive LoRa Messages with GPIO Interrupts?

After we have implemented GPIO Interrupts in our SX1262 Library, this is how we’ll receive LoRa Messages without polling (see pic above)…

  1. When SX1262 receives a LoRa Message, it triggers a GPIO Interrupt on Pin DIO1

  2. GPIO Driver forwards the GPIO Interrupt to our Interrupt Handler Function handle_gpio_interrupt

  3. handle_gpio_interrupt enqueues an Event into our Event Queue

  4. Our Background Thread removes the Event from the Event Queue and calls RadioOnDioIrq to process the received LoRa Message

We handle GPIO Interrupts the same way in our LoRa SX1262 Driver for BL602 IoT SDK

Why do we need a Background Thread?

This will allow our LoRa Application to run without blocking (waiting) on incoming LoRa Messages.

This is especially useful when we implement LoRaWAN with our SX1262 Library, because LoRaWAN needs to handle asynchronous messages in the background.

(Like when we join a LoRaWAN Network)

How will we implement the Background Thread and Event Queue?

The code below shall be updated to start the Background Thread by calling NimBLE Porting Layer: sx1262_test_main.c

/// TODO: Create a Background Thread to handle LoRa Events
static void create_task(void) {
  //  Init the Event Queue
  ble_npl_eventq_init(&event_queue);

  //  Init the Event
  ble_npl_event_init(
    &event,        //  Event
    handle_event,  //  Event Handler Function
    NULL           //  Argument to be passed to Event Handler
  );

  //  TODO: Create a Background Thread to process the Event Queue
  //  nimble_port_freertos_init(task_callback);
}

And we shall implement the GPIO Interrupt Handler Function handle_gpio_interrupt for NuttX.

(We don’t need to code the Event Queue, it has been done here)

When will we begin the implementation?

Very soon! We shall implement the Background Thread and Event Queue as we port the LoRaWAN Stack to NuttX.

(Because LoRaWAN needs multithreading to work)