Build a Linux Driver for PineDio LoRa SX1262 USB Adapter

📝 28 Oct 2021

UPDATE: This PineDio USB driver is incomplete. Please use JF002/loramac-node instead

What if our Laptop Computer could talk to other devices…

Over a Long Range, Low Bandwidth wireless network like LoRa?

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

Yep that’s possible today… With Pinebook Pro and the PineDio LoRa SX1262 USB Adapter! (Pic below)

This article explains how we built the LoRa SX1262 Driver for PineDio USB Adapter and tested it on Pinebook Pro (Manjaro Linux Arm64)…

Our LoRa SX1262 Driver is still incomplete (it’s not a Kernel Driver yet), but the driver talks OK to other LoRa Devices. (With some limitations)

Read on to learn more…

PineDio LoRa SX1262 USB Adapter

§1 PineDio LoRa USB Adapter

PineDio LoRa USB Adapter looks like a simple dongle…

  1. Take a CH341 USB-to-Serial Interface Module

    (Top half of pic below)

  2. Connect it to a Semtech SX1262 LoRa Module over SPI

    (Bottom half of pic below)

And we get the PineDio LoRa USB Adapter!

Schematic for PineDio LoRa SX1262 USB Adapter

(Source)

So CH341 exposes the SPI Interface for SX1262 over USB?

Yep Pinebook Pro shall control SX1262 over SPI, bridged by CH341.

Which means that we need to install a CH341 SPI Driver on Pinebook Pro.

(More about this in a while)

What about other pins on SX1262: DIO1, BUSY and NRESET?

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

BUSY is read by our computer to check if SX1262 is busy.

NRESET is toggled by our computer to reset the SX1262 module.

Pinebook Pro shall control these pins via the GPIO Interface on CH341, as we’ll see in a while.

(More about PineDio USB)

(CH341 Datasheet)

PineDio Stack BL604 RISC-V Board (foreground) talking to The Things Network via RAKWireless RAK7248 LoRaWAN Gateway (background)

§2 LoRa SX1262 Driver for PineDio USB

Where did the PineDio USB LoRa Driver come from?

Believe it or not… The PineDio USB LoRa Driver is the exact same driver running on PineCone BL602 and PineDio Stack BL604! (Pic above)

But modified to talk to CH341 SPI for PineDio USB.

(And compiled for Arm64 instead of RISC-V 32-bit)

The BL602 / BL604 LoRa Driver was ported from Semtech’s Reference Implementation of SX1262 Driver…

The Things Network in Singapore

(Source)

§2.1 LoRaWAN Support

There are many LoRa Drivers out there, why did we port Semtech’s Reference Driver?

That’s because Semtech’s Reference Driver supports LoRaWAN, which adds security features to low-level LoRa.

(Like for authentication and encryption)

How useful is LoRaWAN? Will we be using it?

Someday we might connect PineDio USB to a LoRaWAN Network like…

Thus it’s good to build a LoRa Driver for PineDio USB that will support LoRaWAN in future.

(I tried porting this new driver by Semtech… But gave up when I discovered it doesn’t support LoRaWAN)

(Seeking security on LoRa without LoRaWAN? Check out the LoRaWAN alternatives)

§2.2 NimBLE Porting Layer

Do we call any open source libraries in our PineDio USB Driver?

Yes we call NimBLE Porting Layer, the open source library for Multithreading Functions…

To transmit and receive LoRa Messages we need Timers and Background Threads. Which are provided by NimBLE Porting Layer.

Have we used NimBLE Porting Layer before?

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

So we’re really fortunate that NimBLE Porting Layer complies on Arm64 Linux as well.

(It’s part of PineTime InfiniTime too!)

Pinebook Pro with PineDio USB Adapter

§3 Read SX1262 Registers

What’s the simplest way to test our USB PineDio Driver?

To test whether our USB PineDio Driver is working with CH341 SPI, we can read the LoRa SX1262 Registers.

Here’s how: main.c

/// Main Function
int main(void) {
  //  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\r\n", addr, val);
  }
}

(SX126xIoInit is defined here)

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

In our PineDio USB Driver, SX126xReadRegister calls SX126xReadRegisters and sx126x_read_register to read each register: sx126x-linux.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-linux.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
  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;
}

And the values of the registers are returned by SX1262 over SPI.

(More about sx126x_hal_read later)

§3.1 Run the Driver

Follow the instructions to install the CH341 SPI Driver

Follow the instructions to download, build and run the PineDio USB Driver…

Remember to edit src/main.c and uncomment…

#define READ_REGISTERS

Build and run the PineDio USB Driver…

## Build PineDio USB Driver
make

## Run PineDio USB Driver
sudo ./lora-sx1262

And watch for these SX1262 Register Values

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

(See the Output Log)

(See the dmesg Log)

If we see these values… Our PineDio USB Driver is talking correctly to CH341 SPI and SX1262!

Note that the values above will change when we transmit and receive LoRa Messages.

Let’s do that next.

Reading SX1262 Registers on PineDio USB

§3.2 Source Files for Linux

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

Yep we have layers of Source Files in our SX1262 Driver…

  1. Source Files specific to Linux

    (For PineDio USB and Pinebook Pro)

  2. Source Files specific to BL602 and BL604

    (For PineCone BL602 and PineDio Stack BL604)

  3. Source Files common to all platforms

    (For Linux, BL602 and BL604)

The Source Files specific to Linux are…

All other Source Files are shared by Linux, BL602 and BL604.

(Except sx126x-board.c which is the BL602 / BL604 Interface for SX1262)

§4 LoRa Parameters

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

To find out which LoRa Frequency we should use for our region…

We set the LoRa Frequency like so: 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: 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.

These are LoRa Transmitter and Receiver programs based on RAKwireless WisBlock (pic below) that I used for testing PineDio USB…

Thus the LoRa Parameters for PineDio USB should match the above.

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!)

When we connect PineDio USB to The Things Network, we need to comply with their Fair Use Policy…

RAKwireless WisBlock LPWAN Module mounted on WisBlock Base Board

§4.1 Initialise LoRa SX1262

Our init_driver function takes the above LoRa Parameters and initialises LoRa SX1262 like so: 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 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);

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 (Linux and BL602), defined in radio.c

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

Transmitting a LoRa Message

§5 Transmit LoRa Message

Now we’re ready to transmit a LoRa Message! Here’s how: 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 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: 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”: demo.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 transmit the LoRa Message

  //  We compute the message length, up to max 29 bytes.
  //  CAUTION: Anything more will cause message corruption!
  #define MAX_MESSAGE_SIZE 29
  uint8_t size = sizeof loraping_buffer > MAX_MESSAGE_SIZE
    ? MAX_MESSAGE_SIZE 
    : sizeof loraping_buffer;

  //  We send the transmit buffer, limited to 29 bytes.
  //  CAUTION: Anything more will cause message corruption!
  Radio.Send(loraping_buffer, size);

  //  TODO: Previously we send 64 bytes, which gets garbled consistently.
  //  Does CH341 limit SPI transfers to 31 bytes?
  //  (Including 2 bytes for SX1262 SPI command header)
  //  Radio.Send(loraping_buffer, sizeof loraping_buffer);
}

(RadioSend is explained here)

Our PineDio USB Driver has an issue with CH341 SPI Transfers

Transmitting a LoRa Message on PineDio USB longer than 29 bytes will cause message corruption!

Thus we limit the Transmit LoRa Message Size to 29 bytes.

(There’s a way to fix this… More about CH341 later)

When the LoRa Message has been transmitted, the LoRa Driver calls our Callback Function on_tx_done defined in 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: 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 Driver

Follow the instructions to install the CH341 SPI Driver

Follow the instructions to download, build and run the PineDio USB Driver…

Remember to edit src/main.c and uncomment…

#define SEND_MESSAGE

Also edit src/main.c and set the LoRa Parameters. (As explained earlier)

Build and run the PineDio USB Driver…

## Build PineDio USB Driver
make

## Run PineDio USB Driver
sudo ./lora-sx1262

We should see PineDio USB transmitting our 29-byte LoRa Message…

send_message
RadioSend: size=29
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 

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

(See the Output Log)

(See the dmesg Log)

On RAKwireless WisBlock we should see the same 29-byte LoRa Message received…

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 

(See the WisBlock Log)

PineDio USB has successfully transmitted a 29-byte LoRa Message to RAKwireless WisBlock!

RAKwireless WisBlock receives 29-byte LoRa Message from RAKwireless WisBlock

§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)

LoRa Message captured with Software Defined Radio

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

More about LoRa Chirps and Software Defined Radio…

Receiving a LoRa Message with PineDio USB

§6 Receive LoRa Message

Let’s receive a LoRa Message on PineDio USB!

This is how we do it: 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.

(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;
}

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

Then we call RadioOnDioIrq to handle the Message Received Event. (If any)

(RadioOnDioIrq is explained here)

receive_message is defined like so: 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);
}

(RadioRx is explained here)

When the LoRa Driver receives a LoRa Message, it calls our Callback Function on_rx_done defined in 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]);
  }
  printf("\r\n");
}

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

The LoRa Driver calls our Callback Function on_rx_timeout defined in 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: 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 Driver

Follow the instructions to install the CH341 SPI Driver

Follow the instructions to download, build and run the PineDio USB Driver…

Remember to edit src/main.c and uncomment…

#define RECEIVE_MESSAGE

Also edit src/main.c and set the LoRa Parameters. (As explained earlier)

Build and run the PineDio USB Driver…

## Build PineDio USB Driver
make

## Run PineDio USB Driver
sudo ./lora-sx1262

Switch over to RAKwireless WisBlock and transmit a 28-byte LoRa Message…

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 
OnTxDone

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

(See the WisBlock Log)

On PineDio USB we should see the same 28-byte LoRa Message…

IRQ_RX_DONE
03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 48 65 6c 6c 6f 00 01 02 
IRQ_PREAMBLE_DETECTED
IRQ_HEADER_VALID
receive_message

(See the Output Log)

(See the dmesg Log)

PineDio USB has successfully received a 28-byte LoRa Message from RAKwireless WisBlock!

PineDio USB receives a 28-byte LoRa Message from RAKwireless WisBlock

Why 28 bytes?

Our PineDio USB Driver has an issue with CH341 SPI Transfers

Receiving a LoRa Message on PineDio USB longer than 28 bytes will cause message corruption!

Thus we limit the Receive LoRa Message Size to 28 bytes.

There’s a way to fix this… Coming up next!

Schematic for PineDio LoRa SX1262 USB Adapter

(Source)

§7 CH341 SPI Interface

Remember that PineDio USB Dongle contains a CH341 USB-to-Serial Interface Module that talks to LoRa SX1262 (over SPI)…

Pinebook Pro (Manjaro Linux Arm64) has a built-in driver for CH341… But it doesn’t support SPI.

Thus for our PineDio USB Driver we’re calling this CH341 SPI Driver

We install the CH341 SPI Driver with these steps…

Now let’s call the CH341 SPI Driver from our PineDio USB Driver.

(Note: PineDio Wiki recommends dimich-dmb/spi-ch341-usb, but it didn’t transmit LoRa packets during my testing)

§7.1 Initialise SPI

Here’s how our PineDio USB Driver calls CH341 SPI Driver to initialise the SPI Bus: sx126x-linux.c

/// SPI Bus
static int spi = 0;

/// Init the SPI Bus. Return 0 on success.
static int init_spi(void) {
  //  Open the SPI Bus
  spi = open("/dev/spidev1.0", O_RDWR);
  assert(spi > 0);

  //  Set to SPI Mode 0
  uint8_t mmode = SPI_MODE_0;
  int rc = ioctl(spi, SPI_IOC_WR_MODE, &mmode);
  assert(rc == 0);

  //  Set LSB/MSB Mode
  uint8_t lsb = 0;
  rc = ioctl(spi, SPI_IOC_WR_LSB_FIRST, &lsb);
  assert(rc == 0);
  return 0;
}

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

(We’ve seen init_driver earlier)

(RadioInit is explained here)

§7.2 Transfer SPI

To transfer SPI Data between PineDio USB and CH341 / SX1262, we do this: sx126x-linux.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(len > 0);
  assert(len <= 31);  //  CAUTION: CH341 SPI doesn't seem to support 32-byte SPI transfers 

  //  Prepare SPI Transfer
  struct spi_ioc_transfer spi_trans;
  memset(&spi_trans, 0, sizeof(spi_trans));
  spi_trans.tx_buf = (unsigned long) tx_buf;  //  Transmit Buffer
  spi_trans.rx_buf = (unsigned long) rx_buf;  //  Receive Buffer
  spi_trans.cs_change = true;   //  Set SPI Chip Select to Low
  spi_trans.len       = len;    //  How many bytes
  printf("spi tx: "); for (int i = 0; i < len; i++) { printf("%02x ", tx_buf[i]); } printf("\n");

  //  Transfer and receive the SPI buffers
  int rc = ioctl(spi, SPI_IOC_MESSAGE(1), &spi_trans);
  assert(rc >= 0);
  assert(rc == len);

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

(transfer_spi will be called by our PineDio USB Driver, as we’ll see later)

transfer_spi has a strange assertion that stops large SPI transfers…

//  CAUTION: CH341 SPI doesn't seem to 
//  support 32-byte SPI transfers 
assert(len <= 31);

We’ll learn why in a while.

PineDio USB transmits a garbled 64-byte LoRa Message to RAKwireless WisBlock

§7.3 Long Messages are Garbled

What happens when we transmit a LoRa Message longer than 29 bytes?

The pic above shows what happens when we transmit a long message (64 bytes) from PineDio USB to RAKwireless WisBlock…

  1. Our 64-byte message is garbled when received

    (By RAKwireless WisBlock)

  2. But the message is consistently garbled

    (RAKwireless WisBlock receives the same garbled message twice, not any random message)

  3. Which means it’s not due to Radio Interference

    (Radio Interference would garble the messages randomly)

By tweaking our PineDio USB Driver, we discover two shocking truths…

  1. Transmitting a LoRa Message on PineDio USB longer than 29 bytes will cause message corruption!

  2. Receiving a LoRa Message on PineDio USB longer than 28 bytes will cause message corruption!

Let’s trace the code and solve this mystery.

§7.4 Transmit Long Message

Our PineDio USB Driver calls this function to transmit a LoRa Message: sx126x-linux.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
  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
  );
}

In this code we prepare a SX1262 Write Buffer Command (2 bytes) and pass the Command Buffer (plus Data Buffer) to sx126x_hal_write.

(Data Buffer contains the 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 transfers the Command Buffer and Data Buffer over SPI: sx126x-linux.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 use an internal 1024-byte buffer for SPI Transfers, so we’re hunky dory here: sx126x-linux.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];

sx126x_hal_write calls transfer_spi to transfer the SPI Data.

(We’ve seen transfer_spi earlier)

Thus transfer_spi looks highly sus for transmitting Long LoRa Messages.

What about receiving Long LoRa Messages?

§7.5 Receive Long Message

Our PineDio USB Driver calls this function to receive a LoRa Message: sx126x-linux.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
  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;
}

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

(Data Buffer will contain the received LoRa Message)

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

(SX126xGetPayload and SX126xReadBuffer are explained here)

sx126x_hal_read transfers the Command Buffer over SPI: sx126x-linux.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 that has been read over SPI.

sx126x_hal_read also calls transfer_spi to transfer the SPI Data.

Now transfer_spi is doubly sus… The same function is called to transmit AND receive Long LoRa Messages!

CH341 SPI Driver fails when transferring 32 bytes

(Source)

§7.6 SPI Transfer Fails with 32 Bytes

Does transfer_spi impose a limit on the size of SPI Transfers?

With some tweaking, we discover that transfer_spi garbles the data when transferring 32 bytes or more!

This seems to be a limitation of the CH341 SPI Driver.

(Due to CH341_USB_MAX_BULK_SIZE maybe?)

Hence we limit all SPI Transfers to 31 bytes: sx126x-linux.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) {
  //  CAUTION: CH341 SPI doesn't seem to 
  //  support 32-byte SPI transfers 
  assert(len <= 31);

Why 29 bytes for the max transmit size? And 28 bytes for the max receive size?

That’s because…

But wait! We might have a fix for Long LoRa Messages…

SX1262 Commands for WriteBuffer and ReadBuffer

(From Semtech SX1262 Datasheet)

§7.7 Fix Long Messages

Is there a way to fix Long LoRa Messages on PineDio USB?

Let’s look back at our code in sx126x_write_buffer.

To transmit a LoRa Message, we send the WriteBuffer Command to SX1262 over SPI…

  1. WriteBuffer Command: 0x0E

  2. WriteBuffer Offset: 0x00

  3. WriteBuffer Data: Transfer 29 bytes (max)

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

If we try to transmit a LoRa Message that’s longer than 29 bytes, the SPI Transfer fails.

This appears in the Output Log as…

sx126x_hal_write: 
  command_length=2, 
  data_length=29
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 
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 

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

Can we transfer in smaller chunks instead?

Yes! According to the SX1262 Datasheet (pic above), we can copy the LoRa Message in smaller chunks (29 bytes), by changing the WriteBuffer Offset

  1. WriteBuffer Command: 0x0E

  2. WriteBuffer Offset: 0x00

  3. WriteBuffer Data: Transfer first 29 bytes

  4. WriteBuffer Command: 0x0E

  5. WriteBuffer Offset: 0x1D (29 decimal)

  6. WriteBuffer Data: Transfer next 29 bytes

We need to mod the code in sx126x_write_buffer to copy the LoRa Message in 29-byte chunks.

Awesome! Will this work for receiving Long LoRa Messages?

Yep! 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 28 bytes (max)

Which appears in the Output Log as…

sx126x_hal_read: 
  command_length=3, 
  data_length=28
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 
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 

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

Instead of reading the entire LoRa Message (from SX1262 Receive Buffer) in a single chunk, we should read it in 28-byte chunks

  1. ReadBuffer Command: 0x1E

  2. ReadBuffer Offset: 0x00

  3. ReadBuffer NOP: 0x00

  4. ReadBuffer Data: Transfer first 28 bytes

  5. ReadBuffer Command: 0x1E

  6. ReadBuffer Offset: 0x1C (28 decimal)

  7. ReadBuffer NOP: 0x00

  8. ReadBuffer Data: Transfer next 28 bytes

We need to fix sx126x_read_buffer to read the LoRa Message in 28-byte chunks.

Is this fix for Long LoRa Messages really necessary?

Maybe not!

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

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

When we connect PineDio USB to The Things Network, we need to comply with their Fair Use Policy…

With CBOR Encoding, we can compress simple LoRa Messages (Sensor Data) into 12 bytes roughly. (See this)

Thus 28 bytes might be sufficient for many LoRa Applications.

(Long LoRa Messages are more prone to Radio Interference and Collisions as well)

(But lemme know if you would like me to fix this!)

§8 CH341 GPIO Interface

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

PineDio USB needs a GPIO Interface to control these SX1262 Pins

We may call the GPIO Interface that’s provided by the CH341 SPI Driver

But the current PineDio USB Driver doesn’t use GPIO yet.

Controlling SX1262 without GPIO

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 CH341 GPIO Interface

  1. Initialise the GPIO Pins: SX126xIoInit

    (Explained here)

  2. Register GPIO Interrupt Handler for DIO1: SX126xIoIrqInit

    (Explained here)

  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.

Here’s how we’ll do multithreading…

Multithreading with NimBLE Porting Layer

§9 Multithreading with NimBLE Porting Layer

How will we receive LoRa Messages with GPIO Interrupts?

After we have implemented GPIO Interrupts in our PineDio USB Driver, this is how we’ll receive LoRa Messages (see pic above)…

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

  2. CH341 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

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 on PineDio USB, 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?

We’ll call NimBLE Porting Layer, the open source library for Multithreading Functions…

Which has been compiled into our PineDio USB Driver…

The code below shall be updated to start the Background Thread: 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 Linux.

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

PineDio LoRa Family: PineDio Gateway, PinePhone Backplate and USB Adapter

PineDio LoRa Family: PineDio Gateway, PinePhone Backplate and USB Adapter

§10 What’s Next

Now that we have a Basic LoRa Driver for PineDio USB, we can explore all kinds of fun possibilities…

  1. Merge the PineDio USB Driver back into BL602 IoT SDK

    (So we can maintain a single LoRa Driver for for PineDio USB and PineDio Stack BL604)

    UPDATE: We have merged PineDio USB Driver into BL602 IoT SDK!

  2. Implement LoRaWAN on PineDio USB

    (So we can connect Pinebook Pro to The Things Network and Helium)

    (We’ll port the BL602 LoRaWAN Driver to Linux)

  3. Explore LoRa Mesh Networks for PineDio USB…

    Meshtastic (Data Mesh), QMesh (Voice Mesh), Mycelium Mesh (Text Mesh)

  4. Test PineDio USB with PineDio LoRa Gateway

    “PineDio LoRa Gateway: Testing The Prototype”

  5. PinePhone LoRa Backplate (pic above) is an intriguing accessory…

    The Backplate connects PinePhone to LoRa SX1262 via ATtiny84… Which runs an Arduino I2C-To-SPI Bridge!

    (Our PineDio USB Driver might run on PinePhone if we talk to I2C instead of SPI)

    (Check the updates here)

Lemme know what you would like to see!

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/usb.md

§11 Notes

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

§12 Appendix: Install CH341 SPI Driver

To install CH341 SPI Driver on Pinebook Pro Manjaro Arm64

## Install DKMS so that we may load Kernel Drivers dynamically
sudo pacman -Syu dkms base-devel --needed

## Install Kernel Headers for Manjaro: https://linuxconfig.org/manjaro-linux-kernel-headers-installation
uname -r 
## Should show "5.14.12-1-MANJARO-ARM" or similar
sudo pacman -S linux-headers
pacman -Q | grep headers
## Should show "linux-headers 5.14.12-1" or similar

## Reboot to be safe
sudo reboot

## Install CH341 SPI Driver
git clone https://codeberg.org/JF002/spi-ch341-usb
pushd spi-ch341-usb
## TODO: Edit Makefile and change...
##   KERNEL_DIR  = /usr/src/linux-headers-$(KVERSION)/
## To...
##   KERNEL_DIR  = /lib/modules/$(KVERSION)/build
make
sudo make install
popd

## Unload the CH341 Non-SPI Driver if it has been automatically loaded
lsmod | grep ch341
sudo rmmod ch341

## Load the CH341 SPI Driver
sudo modprobe spi-ch341-usb

Then do this…

  1. Plug in PineDio USB

  2. Check that the CH341 SPI Driver has been correctly loaded…

    dmesg
  3. We should see…

    ch341_gpio_probe: 
    registered GPIOs from 496 to 511

    The CH341 SPI Driver has been loaded successfully.

    (See the complete log)

  4. If we see…

    spi_ch341_usb: 
    loading out-of-tree module taints kernel

    It means the older CH341 Non-SPI Driver has been loaded.

    (See the complete log)

To remove the older CH341 Non-SPI Driver

  1. Unplug PineDio USB

  2. Enter…

    ## Unload the CH341 Non-SPI Driver
    sudo rmmod ch341
  3. Plug in PineDio USB

  4. Enter…

    dmesg

    And recheck the messages.

(More about CH341 SPI Driver)

§12.1 When Rebooting

Whenever we reboot our computer…

  1. Plug in PineDio USB

  2. Check that the CH341 SPI Driver has been correctly loaded…

    dmesg
  3. We should see…

    ch341_gpio_probe: 
    registered GPIOs from 496 to 511

    The CH341 SPI Driver has been loaded successfully.

    (See the complete log)

  4. If we see…

    spi_ch341_usb: 
    loading out-of-tree module taints kernel

    It means the older CH341 Non-SPI Driver has been loaded.

    (See the complete log)

To remove the older CH341 Non-SPI Driver

  1. Unplug PineDio USB

  2. Enter…

    ## Unload the CH341 Non-SPI Driver
    sudo rmmod ch341
  3. Plug in PineDio USB

  4. Enter…

    dmesg

    And recheck the messages.

§13 Appendix: Build PineDio USB Driver

To build PineDio USB Driver on Pinebook Pro Manjaro Arm64

  1. Follow the instructions in the previous section to install CH341 SPI Driver.

  2. Check that the CH341 SPI Driver has been loaded. (dmesg)

  3. Enter at the command prompt…

## Download PineDio USB Driver
git clone --recursive https://github.com/lupyuen/lora-sx1262
cd lora-sx1262

## TODO: Edit src/main.c and uncomment 
## READ_REGISTERS, SEND_MESSAGE or RECEIVE_MESSAGE.
## See "PineDio USB Operations" below.

## TODO: Edit src/main.c and update the LoRa Parameters.
## See "LoRa Parameters" above.

## Build PineDio USB Driver
make

## Run PineDio USB Driver Demo
sudo ./lora-sx1262

(See the Output Log)

(More about PineDio USB)

§13.1 PineDio USB Operations

The PineDio USB Demo supports 3 operations…

  1. Read SX1262 Registers:

    Edit src/main.c and uncomment…

    #define READ_REGISTERS

    (See the Read Register Log)

  2. Send LoRa Message:

    Edit src/main.c and uncomment…

    #define SEND_MESSAGE

    (See the Send Message Log)

  3. Receive LoRa Message:

    Edit src/main.c and uncomment…

    #define RECEIVE_MESSAGE

    (See the Receive Message Log)

§14 Appendix: Radio Functions

In this section we explain the Platform-Independent (Linux and BL602) Radio Functions, which are defined in radio.c

§14.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)

§14.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)

§14.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-linux.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).

§14.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;
  }
}

§14.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
  TimerStart( &TxTimeoutTimer, TxTimeout );
}

(TimerStart 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)

§14.6 RadioRx: Receive Message

RadioRx receives 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 ) {
    TimerStart( &RxTimeoutTimer, timeout );
  }

(TimerStart 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…

§14.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…

§14.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.

§14.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-linux.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)

§14.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 
        ) );
      }
    }

§14.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.

§14.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)

§14.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)

§14.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)

§14.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.

§14.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();
}

§14.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)

Pinebook Pro with PineDio USB Adapter