Creating a simple LoRa Network

Practical tips for the board NZ32-SC151 of Modtronix

----------------------------------------------
Creating a LoRa network using InAir9B using STM32

Hardware:
We use the board InAir9B that includes the chip SX1276 (modtronix.com)
The board NZ32-SC151, with STM32L151RC that is pin compatible with AnAir9B (modtronix.com)

To program the microproc, you need just a usb cable that can be connected to the NZ32, however there is no way to debug/emulate. You just change the firmware usng USB  ootloader each time you need to test a program.

Sofware tool:
You need to go to download the "System Workbench for STM32" from http://www.openstm32.org.
More details can be found here: https://github.com/modtronix-com/devkit_sx1276

--------------------------------
Libraries

Add the required library in order to send/receive packets
Go on Github
https://github.com/modtronix-com/devkit_sx1276

click on the "clone or download"
Save the file .zip
extract into a known directory (for exemple projetLoRa)
The folder's name is "devkit_sx1276-master"

Run System Workbench, but select the folder of the workspace : "devkit_sx1276-master/SW4STM32"
goto  File -> import then general-> existing projects in workspace, then select the folder SW4STM32, then finish.
At the next opening of project, use the folder of the work space SW4STM32
--------------------------------------------------------------------------------------------------------

A complete working project used at ENSIL can be dowloaded from : project

--------------------------------------------------------------------------------------------------------

How to create and download the bootloader

from modtronics site;
http://wiki.modtronix.com/doku.php?id=products:nz-stm32:nz32-sc151#programming_and_debugging
dans la partie "Required software", télécharger STSW-STM32080. Dézipper et installet DFuSe demo.exe

C'est pour envoyer le fichier .dfu dans la memoire du microC.
Mais il faut déjà générer ce fichier .dfu. Pour cela, il faut que l'IDE (eclipse) génère un fichier .hex, que l'on transformera en .dfu

Mais l'IDE ne génère qu'un fichier .bin. Pour le faire génerer le .hex, aller, sous eclipse, dans le menu
projet -> properties -> c/c++ build -> Setting -> build steps -> post-build step
et ajouter à la fin de la ligne de commande:
 &&  arm-none-eabi-objcopy -O ihex "${BuildArtifactFileBaseName}.elf" "${BuildArtifactFileBaseName}.hex"

Rebuilder le projet et le fichier hex se trouve dans le répertoir debug.

Pour le transformet en .dfu, lancre l'appli
ST Microelectronics et puis dfu file manager

Remarque:
Quand vous connecter la carte au PC, il n'est pas visible, appuyer sur Boot, et puis reseter. La carte est visible (à verifier sur device manager). L'appli DFuSeDemo peut fonctionner.

Il est possible de faire une copie de la mémoire sur PC au cas où (cadre "upload Action").

Utiliser maintenat STMicroelectyronics -> DFuSeDemo pour envoyer au Micro.

Tres important:
Quand vous utilisez DFuSe Demo, il donne trois valeurs importante: Vendor ID, Product ID, version. Ces 3 valeurs devoient être utilisées pour générer le fichier .hex avec le DFU File Manager.

Un reset executera le programme sur la carte !

--------------------------------------------------------------------------------------------
How to send/receive a packet
1- Add the .h
#include "mbed.h"
#include "../SX1276Lib/registers/sx1276Regs-LoRa.h"
#include "../SX1276Lib/sx1276/sx1276-inAir.h"

--------------------------------------------
2-define configration constants:
#define RF_FREQUENCY                    868700000           // 868MHz
#define TX_OUTPUT_POWER                 14                  // 14 dBm for inAir9
#define LORA_BANDWIDTH                  8                   // 0: 7.8 kHz,  1: 10.4 kHz, 2: 15.6kHz, 3: 20.8kHz,
                                                            // 4: 31.25kHz, 5: 41.7 kHz, 6: 62.5 kHz,
                                                            // 7: 125 kHz,  8: 250 kHz,  9: 500 kHz
#define LORA_SPREADING_FACTOR           12                  // SF7..SF12
#define LORA_CODINGRATE                 1                   // 1=4/5, 2=4/6, 3=4/7, 4=4/8
#define LORA_PREAMBLE_LENGTH            8                   // Same for Tx and Rx
#define LORA_SYMBOL_TIMEOUT             5                   // Symbols
#define LORA_FIX_LENGTH_PAYLOAD_ON      false
#define LORA_FHSS_ENABLED               false
#define LORA_NB_SYMB_HOP                4
#define LORA_IQ_INVERSION_ON            false
#define LORA_CRC_ENABLED                true

#define TX_TIMEOUT_VALUE                2000000     // in us
#define RX_TIMEOUT_VALUE                3500000     // in us

--------------------------------------------

3- define of the buffer that you want to send
#define BUFFER_SIZE                     32          // Define the payload size here
uint8_t Buffer[BUFFER_SIZE];
--------------------------------------------
4- define the prototype of the functions correponsing to the events
void OnTxDone(void);
void OnTxTimeout(void);
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr);
void OnRxTimeout(void);
void OnRxError(void);

--------------------------------------------
5- If you want to send/receive text to/from PC, then add:
Serial      pc(USBTX, USBRX);
--------------------------------------------
6- Create an instance of SX1276inAir:
SX1276inAir  radio(OnTxDone, OnTxTimeout, OnRxDone, OnRxTimeout, OnRxError, NULL, NULL);
The constructor is simplified with just 7 functions. You can take a look at sx1276lib->sx1276->sx1276-inAir.h to see the othet constructor with more functions.

you may now define all the functions, like

void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr)
{
    radio.Sleep();
    BufferSize = size;
    memcpy(Buffer, payload, BufferSize);
    LoRaRssi = rssi;
    LoRaSNR = snr;
    State = RX_DONE;
    radio.Rx(RX_TIMEOUT_VALUE); // I don't know why but you should put this line !! Or you can juste put O as argument to push in continuous mode
}

The class SX1276inAir inherts from the class SX1276 and this latter inherits from the class "radio".
So for the configuration you can use the following at the main.cpp:
radio.SetBoardType(BOARD_INAIR9);
radio.SetChannel(RF_FREQUENCY);
--------------------------------------------
7- In the main program, you can configure the UART, and the modem:
int main() {
    wait_ms(500); // start delay
    // configure uart port
    pc.baud(19200);
    pc.format(8, SerialBase::None, 1);
   // setup the modern
    radio.SetTxConfig(
            MODEM_LORA,
            TX_OUTPUT_POWER,
            0,
            LORA_BANDWIDTH,
            LORA_SPREADING_FACTOR,
            LORA_CODINGRATE,
            LORA_PREAMBLE_LENGTH,
            LORA_FIX_LENGTH_PAYLOAD_ON,
            LORA_CRC_ENABLED,
            LORA_FHSS_ENABLED,
            LORA_NB_SYMB_HOP,
            LORA_IQ_INVERSION_ON,
            TX_TIMEOUT_VALUE
    );
    radio.SetRxConfig(
            MODEM_LORA,
            LORA_BANDWIDTH,
            LORA_SPREADING_FACTOR,
            LORA_CODINGRATE,
            0,
            LORA_PREAMBLE_LENGTH,
            LORA_SYMBOL_TIMEOUT,
            LORA_FIX_LENGTH_PAYLOAD_ON,
            0,
            LORA_CRC_ENABLED,
            LORA_FHSS_ENABLED,
            LORA_NB_SYMB_HOP,
            LORA_IQ_INVERSION_ON,
            true
    );
--------------------------------------------

8- to be sure that you are connected to the board:

    while (radio.Read(REG_VERSION) == 0x00)
    {
        pc.printf("Trying to connect to radio device\r\n");
        wait_ms(200);
    }
--------------------------------------------

9- To turn on/off a board LED:
define first as global
DigitalOut led(LED1);
then use in main led = 1 ; or led = 0; or led = !led;

--------------------------------------------

10- sending a packet:
first define a buffer and fill it:
uint8_t  Buffer[BUFFER_SIZE]; to be defined in global
radio.Send(Buffer, BUFFER_SIZE);
you may change the State that is defined as
volatile RadioState State = LOWPOWER;
to LOWPOWER, and then waiting for the OnTxDone event.
In the function related to the event OnTxDone:
void OnTxDone(void)
{
    radio.Sleep();
    State = TX_DONE;
}
--------------------------------------------

11- Receiving a packet
When a packet is received by sx1276, the event onRxDone is called (according to what we define above). We may handle it by the following function:
void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr)
{
    radio.Sleep();
    BufferSize = size;
    memcpy(Buffer, payload, BufferSize);
    LoRaRssi = rssi;
    LoRaSNR = snr;
    State = RX_DONE;
}
--------------------------------------------

12- the main prgram is a state machine like:
       switch(State)
        {
            case TX_DONE:
                State = LOWPOWER;
                break;

            case RX_DONE:
                led = !led;
                pc.printf("N%dM%dT%d%dH%dP%dR%d\n",Buffer[0], Buffer[1], Buffer[2], Buffer[3], Buffer[4], Buffer[5], LoRaRssi);
                State = LOWPOWER;
                break;

            case LOWPOWER:
                break;

            default:
                State = LOWPOWER;
        } // end switch

--------------------------------------------
13- Using a timer to create periodc events
in the global part
Ticker   myTicker;

Then create the timer object in the main with the name of the function that should be used:
myTicker.attach(ticker_callback, 5.0);

then defin the call back function:
void ticker_callback() {
  led = !led;
}
 
int main() {
myTicker.attach(ticker_callback, 0.5f);
while(1) {}
}

------------------------------------------------------------------------------------------------------
Making the micrproc sleep and wake up.
Two modes: sleep and deep sleep. Sleep mode the µP sleeps but not all the other peripherals, the consumption is reduced. But in deep sleep mode everything that need clock is in sleep and only the asynchronous circuits can work. You need a wake-up mechanism to get out of deep sleep mode. In the latter case, the consumption is almost zero.

You need to include the file
#include "WakeUp.h"

Before you use the function deepsleep( ), you need to fix the duration of sleep. You must call therefore the wakeup::set(duration in second) function:
WakeUp::set(240); // for a sleep of 4 minutes
deepsleep( );



------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------
Some details about SX1276
------------------------------------------------------------------------------------------------------
Packet size in mode fixed packet length.
The length must be the same for all the network nodes RX nodes, TX nodes, RX/TX nodes
- One optional nodeID byte
- The payload has the bytes whose length can be from 0 to  2047
- Two byte optional CRC

The real packet will have a preamble (0 to 65536 bytes) of 010101 ... , and a syncWord (network ID), before the above payload.



IMPORTANT
At each OnTxDone (in the onTxDone routine) you must manually call radio.Standby() or radio.Sleep(). If not, the send command does not work (it is what I tested practically but in the data sheet of sx1276, it is written that the radio goes automatically to standby mode) so I don't know why should we do that manually.
Note: in the standby mode, the µP does not resume your program after waking up. I think it goes to a reset because in this mode, even the memory is not powered.
------------------------------------------------------------------------------
Sleep mode before writing into the configuration registers
Registers are readable in all device mode including Sleep. However, they should be written only in Sleep and Standby modes.
------------------------------------------------------------------------------
Receiver mode
In a typical case when at the gateway a packet can arrive in any time, continous mod (Rx continuous) is selected. Butif we are in the node and after transmitting we just allow a temporal window for receiving possible incoming packet, we should use the Rx single mode.
In Rx Single, if a preamble is received before end of the window, RxDone interrupt is generated and radio goes to standby state. If not, RxTimeOut interrupt is generated and radio goes to standby.

Rx Single and Rx Continuous Use Cases (from the data sheet)
The LoRa single reception mode is used mainly in battery operated systems or in systems where the companion microcontroller has a limited availability of timers. In such systems, the use of the timeout present in Rx Single reception mode allows the end user to limit the amount of time spent in reception (and thus limiting the power consumption) while not using any of the companion MCU timers (the MCU can then be in sleep mode while the radio is in the reception mode). The RxTimeout interrupt generated at the end of the reception period is then used to wake-up the companion MCU. One of the advantages of the RxSingle mode is that the interrupt RxTimeout will not be triggered if the device is currently receiving data, thus giving the priority to the reception of the data over the timeout. However, if during the reception, the device loses track of the data due to external perturbation, the device will drop the reception, flag the interrupt RxTimeout and go in StandBy mode to decrease the power consumption of the system. On the other hand, The LoRa continuous reception mode is used in systems which do not have power restrictions or on system where the use of a companion MCU timer is preferred over the radio embedded timeout system. In RxContinuous mode, the radio will track any LoRa signal present in the air and carry on the reception of packets until the companion MCU sets the radio into another mode of operation. Upon reception the interrupt RxDone will be trigged but the device will stay in Rx Mode, ready for the reception of the next packet.

Payload Data Extraction from FIFO (from data sheet)
In order to retrieve received data from FIFO the user must ensure that ValidHeader, PayloadCrcError, RxDone and RxTimeout interrupts in the status register RegIrqFlags are not asserted to ensure that packet reception has terminated successfully (i.e. no flags should be set). In case of errors the steps below should be skipped and the packet discarded. In order to retrieve valid received data from the FIFO the user must:  RegRxNbBytes Indicates the number of bytes that have been received thus far.  RegFifoAddrPtr is a dynamic pointer that indicates precisely where the Lora modem received data has been written up to.  Set RegFifoAddrPtr to RegFifoRxCurrentAddr. This sets the FIFO pointer to the location of the last packet received in the FIFO. The payload can then be extracted by reading the register RegFifo, RegRxNbBytes times.  Alternatively, it is possible to manually point to the location of the last packet received, from the start of the current packet, by setting RegFifoAddrPtr to RegFifoRxByteAddr minus RegRxNbBytes. The payload bytes can then be read from the FIFO by reading the RegFifo address RegRxNbBytes times.

------------------------------------------------------------------------------
Packet Filtering
The SX1276/77/78/79 packet handler offers several mechanisms for packet filtering, ensuring that only useful packets are made available to the uC, reducing significantly system power consumption and software complexity.

Sync Word Based
Sync word filtering/recognition is used for identifying the start of the payload and also for network identification. As previously described, the Sync word recognition block is configured (size, value) in RegSyncConfig and RegSyncValue(i) registers. This information is used, both for appending Sync word in Tx, and filtering packets in Rx. Every received packet which does not start with this locally configured Sync word is automatically discarded and no interrupt is generated. When the Sync word is detected, payload reception automatically starts and SyncAddressMatch is asserted.

Address Based
Address filtering can be enabled via the AddressFiltering bits. It adds another level of filtering, above Sync word (i.e. Sync must match first), typically useful in a multi-node networks where a network ID is shared between all nodes (Sync word) and each node has its own ID (address). Two address based filtering options are available:  AddressFiltering = 01: Received address field is compared with internal register NodeAddress. If they match then the packet is accepted and processed, otherwise it is discarded.  AddressFiltering = 10: Received address field is compared with internal registers NodeAddress and BroadcastAddress. If either is a match, the received packet is accepted and processed, otherwise it is discarded. This additional check with a constant is useful for implementing broadcast in a multi-node networks Please note that the received address byte, as part of the payload, is not stripped off the packet and is made available in the FIFO. In addition, NodeAddress and AddressFiltering only apply to Rx. On Tx side, if address filtering is expected, the address byte should simply be put into the FIFO like any other byte of the payload. As address filtering requires a Sync word match, both features share the same interrupt flag SyncAddressMatch.