In battery LPWAN applications reducing the power consumption has a high priority. Reduction of power consumption is done by keeping the MCU and where possible, as well the peripherals in sleep mode. They should ony wakeup on an external event or after a specified time to transmit data.
The Arduino sketch is in the folder RAK4631-DeepSleep-LoRaWan.
When using the Arduino framework for the nRF52 modules it is not obvious how to send the MCU into deep-sleep. But there are methods made available from the underlaying FreeRTOS OS that can be used to send a task into sleep mode. And when all tasks are in sleep mode, the MCU is going into low power sleep mode.
It is not as perfect implemented as for example on the ESP32 MCU's where specific sleep functions can be called.
But there are several methods, and here we will have a look into two of them. One is using the delay
function, the other one is to use semaphores
in your application.
This command will send the task into sleep for x milliseconds. This sounds easy to use, however, is not very practical. Because while in the delay()
function, the task cannot receive any information about external events, like an interrupt from a sensor or from a 9DOF sensor. So for most scenarios the delay
is not a good solution.
FreeRTOS provides semaphores
to control task switches and let tasks "sleep" while waiting for an event. Looking into the FreeRTOS documentation, you can see there are several types of semaphores, named binary
, counting
, mutex
and recursive mutex
. To keep things simple here, I will use the binary
semaphore in this example.
I am here citing the FreeRTOS documentation:
Think of a binary semaphore as a queue that can only hold one item. The queue can therefore only be empty or full (hence binary). Tasks and interrupts using the queue don’t care what the queue holds – they only want to know if the queue is empty or full. This mechanism can be exploited to synchronise (for example) a task with an interrupt.
Consider the case where a task is used to service a peripheral. Polling the peripheral would be wasteful of CPU resources, and prevent other tasks from executing. It is therefore preferable that the task spends most of its time in the Blocked state (allowing other tasks to execute) and only execute itself when there is actually something for it to do. This is achieved using a binary semaphore by having the task Block while attempting to ‘take’ the semaphore. An interrupt routine is then written for the peripheral that just ‘gives’ the semaphore when the peripheral requires servicing. The task always ‘takes’ the semaphore (reads from the queue to make the queue empty), but never ‘gives’ it. The interrupt always ‘gives’ the semaphore (writes to the queue to make it full) but never takes it.
This example will use separate tasks. One is the Arduino loop()
function (referred to in the following parts as loopTask
), which is actually a task on FreeRTOS.
The SX126x-Arduino library creates it's own background task, that is sleeping most of the time and only waking up when the SX1262 LoRa transceiver raises an interrupt and then handles the LoRa events. We call this second task loraTask
.
For the loopTask
, we create a semaphore called taskEvent
, that is given by two different events:
- a timer event, which wakes up the
loopTask
every 2 minutes to send a status message to the LoRaWan server - a LoRaWAN downlink event, that is triggered if a downlink package from the LoRaWAN server has arrived
And for the loraTask
, the library creates a second semaphore, called loraEvent
, that is given by only one event. This event is the interrupt signal of the SX1262 transceiver. Only if the SX1262 transceiver is setting the interrupt signal the loraTask
has to wakeup and handle the event.
In the example code the taskEvent
semaphore is taken by the setup functions before the loopTask
is started. Once the two tasks are started, they will call xSemaphoreTake(semaphore, portMAX_DELAY)
. The first parameter is the semaphore the task wants to take, the second parameter is the time the task will wait for the semaphore to be available. portMAX_DELAY
means that the function will not return until the semaphore is given ==> the task goes to sleep!
If you look into the source code, you can see that all "Serial.xxxx" calls are surrounded by
#ifndef MAX_SAVE
<some code here>
#endif
As we want to achieve maximum power savings, the Serial port MUST NOT be initialized. Because not only does the hardware of the Serial port consume energy, FreeRTOS is as well starting a task running in the background (and never sleeps), that prevents the MCU from sleeping.
However, while testing this application, you might want to get some debug output. To enable the debug output, comment out the line
#define MAX_SAVE
in the file main.h
.
The first thing in setup()
is to create the taskEvent
semaphore that will later hold the loopTask
in sleep mode.
// Create the LoRaWan event semaphore
taskEvent = xSemaphoreCreateBinary();
// Initialize semaphore
xSemaphoreGive(taskEvent);
Then after some GPIO setup, we call the function that initializes the SX1262 transceiver. More about that in the next paragraph. At the end of the setup() function you can find the code lines, that take the taskEvent
semaphore and therefor blocks the main loop from executing
// Take the semaphore so the loop will go to sleep until an event happens
xSemaphoreTake(taskEvent, 10);
In the initLoRaWan()
function the usual setup calls to initiate the LoRaWan library are done. Details about this can be found in the other examples.
And then we can start the LoRaWAN join process.
// Start Join procedure
#ifndef MAX_SAVE
Serial.println("Start network join request");
#endif
lmh_join();
If the SX1262 sets its interrupt to inform the MCU about an event (can be RX, TX or error events), it is handled inside the library by it's own loraTask
. Depending on the event, the callbacks we defined with
static lmh_callback_t lora_callbacks
are called.
Handling the LoRa events is done in the functions lorawan_has_joined_handler()
, lorawan_rx_handler()
, lorawan_confirm_class_handler()
, ... that are similar to the same functions in the "normal" LoRaWAN example.
These callbacks are called by the loraTask
inside the library.
There are only a few differences to other LoRaWAN examples:
lorawan_has_joined_handler()
After successfully joining the LoRaWAN network, we start the timer that will wake up the loopTask
every 2 minutes to send a status messages.
// Now we are connected, start the timer that will wakeup the loop frequently
taskWakeupTimer.begin(SLEEP_TIME, periodicWakeup);
taskWakeupTimer.start();
lorawan_rx_handler()
If we received a downlink package from the LoRaWAN server, we wake up the taskLoop
that will handle the data package.
eventType = 0;
// Notify task about the event
if (taskEvent != NULL)
{
#ifndef MAX_SAVE
Serial.println("Waking up loop task");
#endif
xSemaphoreGive(taskEvent);
}
Beside of giving the taskEvent
semaphore, we set as well the flag eventType = 0;
which tells the taskLoop
that it woke up because a data package has arrived.
As already mentioned, the loopTask
sleeps until either a downlink package is received, or wakes up every 2 minutes to send a status package.
After the node has joined the LoRaWAN network, a timer is initialized (see above), that calls every 2 minutes the function periodicWakeup()
. In this function, the taskEvent
semaphore is given, which will wake up the loopTask
.
void periodicWakeup(TimerHandle_t unused)
{
// Switch on blue LED to show we are awake
digitalWrite(LED_CONN, HIGH);
eventType = 1;
// Give the semaphore, so the loop task will wake up
xSemaphoreGiveFromISR(taskEvent, pdFALSE);
}
Here in addition to give the semaphore, we set in addition a flag eventType = 1;
to inform the taskLoop
that it was woken up because of the periodic alarm.
The loop task is quite simple. First we switch of the indicator LED to show that the nRF52 is going to sleep
void loop(void)
{
// Switch off blue LED to show we go to sleep
digitalWrite(LED_BUILTIN, LOW);
Next we call xSemaphoreTake()
with portMAX_DELAY
which puts the task into sleep until the semaphore is available
// Sleep until we are woken up by an event
if (xSemaphoreTake(taskEvent, portMAX_DELAY) == pdTRUE)
{
If the semaphore is given either by the periodic alarm or the SX1262 has received a data package from the LoRaWAN server, the indicator LED is switched on
// Switch on blue LED to show we are awake
digitalWrite(LED_BUILTIN, HIGH);
delay(500); // Only so we can see the blue LED
Then the eventType
is checked to take actions according to the event. In case a downlink package was received, the package should be handled. For simplicity I skipped that part in this example.
// Check the wake up reason
switch (eventType)
{
case 0: // Wakeup reason is package downlink arrived
#ifndef MAX_SAVE
Serial.println("Received package over LoRaWan");
#endif
if (rcvdLoRaData[0] > 0x1F)
{
#ifndef MAX_SAVE
Serial.printf("%s\n", (char *)rcvdLoRaData);
#endif
}
else
{
#ifndef MAX_SAVE
for (int idx = 0; idx < rcvdDataLen; idx++)
{
Serial.printf("%X ", rcvdLoRaData[idx]);
}
Serial.println("");
#endif
}
break;
If the wake event was triggered by the periodic timer, a LoRaWan package is sent to the LoRaWAN server
case 1: // Wakeup reason is timer
#ifndef MAX_SAVE
Serial.println("Timer wakeup");
#endif
/// \todo read sensor or whatever you need to do frequently
// Send the data package
if (sendLoRaFrame())
{
#ifndef MAX_SAVE
Serial.println("LoRaWan package sent successfully");
#endif
}
else
{
#ifndef MAX_SAVE
Serial.println("LoRaWan package send failed");
/// \todo maybe you need to retry here?
#endif
}
break;
After the event is handled, the taskLoop
goes back to sleep.
// Go back to sleep
xSemaphoreTake(taskEvent, 10);
}
}
This code is written as an example and is not perfect in all parts. The goal was to show how to minimize the power consumption of a system that is based on a nRF52 and a SX1262.