📝 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)
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!
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.
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…
Today we do the SX1262 Library (top right)
And we test with our LoRa App (top left)
In the next article we’ll do the LoRaWAN Library and test with our LoRaWAN App
Eventually we shall wrap the SX1262 and LoRaWAN Libraries as NuttX Drivers
(Because that’s the proper design for NuttX)
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.
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)
NuttX Libraries vs Drivers… What’s the difference?
Our LoRa SX1262 code is initially packaged as a NuttX Library (instead of NuttX Driver) because…
NuttX Libraries are easier to code and troubleshoot
NuttX Libraries may be called by NuttX Apps AND NuttX Drivers
(So we can test our library with a NuttX App)
Eventually our LoRa SX1262 code shall be packaged as a NuttX Driver…
Our code shall run inside NuttX OS, which means…
Our driver needs to expose an ioctl() interface to NuttX Apps
(Which will be cumbersome to code)
Check out the ioctl() interface for the existing SX1276 Driver in NuttX: sx127x.c
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.
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)…
We have configured the following BL604 Pin Definitions in board.h
SX1262 | BL604 Pin | NuttX Pin |
---|---|---|
MOSI | GPIO 13 | BOARD_SPI_MOSI |
MISO | GPIO 0 | BOARD_SPI_MISO |
SCK | GPIO 11 | BOARD_SPI_CLK |
CS | GPIO 15 | BOARD_GPIO_OUT1 |
BUSY | GPIO 10 | BOARD_GPIO_IN1 |
DIO1 | GPIO 19 | BOARD_GPIO_INT1 |
NRESET | GPIO 18 | Not 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)
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)
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
Let’s build the modified NuttX Firmware that contains our LoRa SX1262 Library and Test App…
Install the build prerequisites…
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)
Edit apps/examples/sx1262_test_main.c and uncomment…
#define READ_REGISTERS
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
Enable the GPIO Driver in menuconfig…
Enable the SPI Peripheral and SPI Character Driver…
Enable our SPI Test Driver “/dev/spitest0”…
Enable GPIO and SPI Logging for easier troubleshooting…
(Might be good to uncheck “GPIO Informational Output” and “SPI Informational Output”)
Enable our SX1262 Library…
Enable our SX1262 Test App…
Check the box for “Application Configuration” → “Examples” → “SX1262 Test”
Save the configuration and exit menuconfig
For ESP32: Edit esp32_bringup.c to register our SPI Test Driver (See this)
Build, flash and run the NuttX Firmware on BL602 or ESP32…
Finally we run the NuttX Firmware and test our LoRa SX1262 Library…
In the NuttX Shell, enter…
ls /dev
Our SPI Test Driver should appear as “/dev/spitest0”
In the NuttX Shell, enter…
sx1262_test
We should see these SX1262 Register Values (pic below)…
Register 0x00 = 0x00
...
Register 0x08 = 0x80
Register 0x09 = 0x00
Register 0x0a = 0x01
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.
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…
NuttX (Just Completed)
Linux (Completed: PineDio USB)
BL602 IoT SDK (Completed)
The Platform-Independent Source Files shared by all platforms are…
(Semtech’s Driver for SX1262 Transceiver)
(Semtech’s Radio Interface for LoRa Transceivers)
(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…
(NuttX Interface for SX1262)
The Source Files are called like this…
Test App → radio.c → sx126x.c → sx126x-nuttx.c
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.
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 */
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!)
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
RadioInit: Init LoRa SX1262
RadioSetChannel: Set LoRa Frequency
RadioSetTxConfig: Set LoRa Transmit Configuration
RadioSetRxConfig: Set LoRa Receive Configuration
(The Radio functions will also be called when we implement LoRaWAN)
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);
}
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++;
}
Let’s test our SX1262 Library and transmit a LoRa Message…
Assume that we have downloaded and configured our NuttX code…
Edit apps/examples/sx1262_test_main.c and uncomment…
#define SEND_MESSAGE
Also edit sx1262_test_main.c and set the LoRa Parameters. (As explained earlier)
Build, flash and run the NuttX Firmware on BL602 or ESP32…
Switch over to RAKwireless WisBlock and run our LoRa Receiver…
Check that the LoRa Parameters are correct…
In the NuttX Shell, enter…
sx1262_test
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
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!
In case of problems, try troubleshooting with a Software Defined Radio like Airspy R2…
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…
At the right Radio Frequency
(923 MHz below)
With sufficient power
(Red stripe below)
LoRa Messages have a characteristic criss-cross shape: LoRa Chirp. (Like above)
More about LoRa Chirps and Software Defined Radio…
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.
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();
}
Let’s test our SX1262 Library and receive a LoRa Message…
Assume that we have downloaded and configured our NuttX code…
Edit apps/examples/sx1262_test_main.c and uncomment…
#define RECEIVE_MESSAGE
Also edit sx1262_test_main.c and set the LoRa Parameters. (As explained earlier)
Build, flash and run the NuttX Firmware on BL602 or ESP32…
Switch over to RAKwireless WisBlock and run our LoRa Transmitter…
Check that the LoRa Parameters are correct…
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
In the NuttX Shell, enter…
sx1262_test
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
Congratulations our SX1262 Library has successfully received the 64-byte LoRa Message from RAKwireless WisBlock!
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…
SPI Interface
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.
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)
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.
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.
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];
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.
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…
WriteBuffer Command: 0x0E
WriteBuffer Offset: 0x00
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…
ReadBuffer Command: 0x1E
ReadBuffer Offset: 0x00
ReadBuffer NOP: 0x00
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, …)
Besides SPI, what Interfaces do we need to control the SX1262 Transceiver?
Our SX1262 Library needs a GPIO Interface to control these SX1262 Pins…
BUSY at /dev/gpio0: GPIO Input that tells us whether SX1262 is busy
(BUSY is High when SX1262 is busy)
Chip Select at /dev/gpio1: Select or deselect SX1262 on the SPI Bus
(Chip Select is Low when SX1262 is selected)
DIO1 at /dev/gpio2: GPIO Interrupt used by SX1262 to signal that a LoRa Packet has been transmitted or received
(DIO1 shifts from Low to High when that happens)
NRESET: GPIO Output that is toggled to reset the SX1262 module
(NRESET is not implemented yet. To reset SX1262, we power cycle the board. See this)
(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)
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.
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.
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…
Define a NuttX Signal that will be signalled on GPIO Interrupt
Wait for the GPIO Interrupt (Signal) to be triggered by DIO1
Add an Event to the Event Queue
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) ¬ify // 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)
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;
}
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.
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…
When SX1262 receives a LoRa Message, it triggers a GPIO Interrupt on Pin DIO1
In the previous chapter we started a Background Thread that waits for GPIO Interrupts and adds a DIO1 Interrupt Event to our Event Queue
In our NuttX App, the Foreground Thread runs an Event Loop that handles every Event in our Event Queue
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)
What Events will be added to our Event Queue?
Our Event Queue will have two types of Events…
GPIO Interrupt: Triggered via DIO1 when SX1262 has transmitted or received a LoRa Message
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);
}
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);
For DIO1 Interrupts: We call RadioOnDioIrq to handle the packet transmitted / received notification
For Timer Events: We call the Timeout Function defined in the Timer
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!
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
This article is the expanded version of this Twitter Thread
We’re porting plenty of code to NuttX: LoRa, LoRaWAN and NimBLE Porting Layer. Do we expect any problems?
If we implement LoRa and LoRaWAN as NuttX Drivers, we’ll have to scrub the code to comply with the NuttX Coding Conventions.
This makes it harder to update the LoRaWAN Driver when there are changes in the LoRaWAN Spec. (Like for a new LoRaWAN Region)
Alternatively we may implement LoRa and LoRaWAN as External Libraries, similar to NimBLE for NuttX.
(The Makefile downloads the External Library during build)
But then we won’t get a proper NuttX Driver that exposes the ioctl() interface to NuttX Apps.
Conundrum. Lemme know your thoughts!
How do other Embedded Operating Systems implement LoRaWAN?
Mynewt embeds a Partial Copy of Semtech’s LoRaWAN Stack into its source tree.
Zephyr maintains a Complete Fork of the entire LoRaWAN Repo by Semtech. Which gets embedded during the Zephyr build.
We’re adopting the Zephyr approach to keep our LoRaWAN Stack in sync with Semtech’s.
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!)
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.
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"
(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
(Note also the defaults for SPI_DEVPATH, CS_DEVPATH and BUSY_DEVPATH)
(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)
Browse to the “nuttx/libs” folder
Copy the “libdsp” subfolder and paste it as “libsx1262”
Inside the “libsx1262” folder, delete all source files except “lib_misc.c”
Edit “Makefile”. Remove all “CSRCS” lines except…
CSRCS += lib_misc.c
Inside the “libsx1262” folder, search and replace all “libdsp” by “libsx1262”
Be sure to Preserve Case!
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
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.
Browse to the “nuttx/include” folder
Copy the file “dsp.h” and paste it as “sx1262.h”
Inside the file “sx1262.h”, search and replace all “dsp” by “sx1262”
Remember to Preserve Case!
Edit the file “sx1262.h”, remove all Public Functions Prototypes and add…
void test_libsx1262(void);
We’ll test this function in a while.
Next we update the Makefiles and Kconfig so that NuttX will build our library…
Browse to the “nuttx/tools” folder
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…
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…
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.
Browse to the “nuttx” folder
Edit the file “Kconfig”
Inside the section menu “Library Routines”, add this line…
source "libs/libsx1262/Kconfig"
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
We enable our library as follows…
In menuconfig, select “Library Routines”
Check the box for “Semtech SX1262 Library”
Hit “Save” then “OK” to save the NuttX Configuration to “.config”
Hit “Exit” until menuconfig quits
To verify our library…
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;
}
Build (“make”), flash and run the NuttX Firmware on BL602 or ESP32.
In the NuttX Shell, enter…
sx1262_test
We should see the message…
libsx1262 OK!
Congratulations our library is now running on 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)
Follow these steps to build NuttX for BL602 or ESP32…
Install the build prerequisites…
Assume that we have downloaded and configured our NuttX code…
To build NuttX, enter this command…
make
We should see…
LD: nuttx
CP: nuttx.hex
CP: nuttx.bin
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.
In case of problems, refer to the NuttX Docs…
For ESP32: See instructions here (Also check out this article)
For BL602: Follow these steps to install 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:
Set the GPIO 8 Jumper to High (Like this)
Disconnect the USB cable and reconnect
Or use the Improvised Reset Button (Here’s how)
For PineCone BL602:
Set the PineCone Jumper (IO 8) to the H
Position (Like this)
Press the Reset Button
For BL10:
Connect BL10 to the USB port
Press and hold the D8 Button (GPIO 8)
Press and release the EN Button (Reset)
Release the D8 Button
For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:
Disconnect the board from the USB Port
Connect GPIO 8 to 3.3V
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
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)
For ESP32: Use Picocom to connect to ESP32 over UART…
picocom -b 115200 /dev/ttyUSB0
For BL602: Set BL602 / BL604 to Normal Mode (Non-Flashing) and restart the board…
For PineDio Stack BL604:
Set the GPIO 8 Jumper to Low (Like this)
Disconnect the USB cable and reconnect
Or use the Improvised Reset Button (Here’s how)
For PineCone BL602:
Set the PineCone Jumper (IO 8) to the L
Position (Like this)
Press the Reset Button
For BL10:
For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:
Disconnect the board from the USB Port
Connect GPIO 8 to GND
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)
macOS Tip: Here’s the script I use to build, flash and run NuttX on macOS, all in a single step: run.sh
In this section we explain the Platform-Independent Radio Functions for Semtech SX1262 Transceiver: radio.c
RadioInit: Initialise LoRa SX1262
RadioSetChannel: Set LoRa Frequency
RadioSetTxConfig: Set LoRa Transmit Configuration
RadioSetRxConfig: Set LoRa Receive Configuration
RadioSend: Transmit a LoRa Message
RadioRx: Receive one LoRa Message
RadioIrqProcess: Process Transmit and Receive Interrupts
RadioSleep: Switch SX1262 to low-power sleep mode
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
TxDone: Transmit Done
RxDone: Receive Done
RxError: Receive Error
TxTimeout: Transmit Timeout
RxTimeout: Receive Timeout or Header Error
CadDone: Channel Activity Detection Done
The Callback Functions are defined in our LoRa Test App.
(Also in the LoRaWAN Library, as explained in the next article)
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…
TxDone: Called when a LoRa Message has been transmitted
RxDone: Called when a LoRa Message has been received
TxTimeout: Called upon timeout when transmitting a LoRa Message
RxTimeout: Called upon timeout when receiving a LoRa Message
RxError: Called when a LoRa Message has been received with CRC Error
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 );
(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;
}
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)
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).
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;
}
}
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 );
}
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 );
}
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)
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 );
}
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…
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…
Transmit Done
Receive Done
CAD Done
Transmit / Receive Timeout
Preamble Detected
Sync Word Valid
Header Valid
Header Error
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( );
}
}
(SX126xSetOperatingMode is defined here)
TxDone points to the on_tx_done Callback Function that we’ve seen earlier.
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)
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
) );
}
}
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.
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)
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
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)
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.
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();
}
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)
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…
NimBLE Porting Layer doesn’t work for multiple Callout Timers unless we loop the thread…
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.
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…
DIO1: Because we don’t support GPIO Interrupts (yet), we poll the SX1262 Status every second to check if a LoRa Packet has been received.
BUSY: Instead of reading this pin to check if SX1262 is busy, we sleep 10 milliseconds.
NRESET: To reset the SX1262 module, we manually restart our NuttX gadget.
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…
Initialise the GPIO Pins: SX126xIoInit
Register GPIO Interrupt Handler for DIO1: SX126xIoIrqInit
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);
}
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);
}
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)…
When SX1262 receives a LoRa Message, it triggers a GPIO Interrupt on Pin DIO1
GPIO Driver forwards the GPIO Interrupt to our Interrupt Handler Function handle_gpio_interrupt
handle_gpio_interrupt enqueues an Event into our Event Queue
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)