.NET nanoFramework Qorvo DW1000 – RAK13801 Device SPI

When developing libraries it’s good to have a selection of different platforms for testing as this can significantly improve the quality and robustness of the implementation. A few months ago I noticed that RAK Wireless have a UWB Module Decawave DW1000 Wisbblock so I added one to an order.

My second Qorvo DW1000 setup is a RAK120000 Wisblock Core module, on a RAK19007 WisBlock Base with a RAK13801 WisBlock Wireless module

RAK12000 + RAK19007 + RAK13801 test platform

The Qorvo DW1000 module has a Serial Peripheral Interface (SPI) so the Master In Slave Out(MISO), Master Out Slave In(MOSI), Serial Clock(SCLK) and Chip Slave Select(CSS) pins of the RAK11200 WisBiock Core Module have to be setup using the Configuration.SetPinFunction method of the nanoFramework.Hardware.Esp32 library.

RAK11200 Schematic with SPI pins highlighted.
RAK13801 Schematic with SPI pins highlighted.

I have added a couple of C# processor directives (MAKERFABS_ESP32UWB & RAK11200_RAK1907_RAK13801) so the platform that the Qorvo DW1000 module is running on can be configured.

public class Program
{
#if MAKERFABS_ESP32UWB
    private const int SpiBusId = 1;
    private const int chipSelectLine = Gpio.IO04;
#endif
#if RAK11200_RAK1907_RAK13801
    private const int SpiBusId = 1;
    private const int chipSelectLine = Gpio.IO32;
#endif

    public static void Main()
    {
        Thread.Sleep(5000);

        Debug.WriteLine("devMobile.IoT.Dw1000.ShieldSPI starting");

        try
        {
#if MAKERFABS_ESP32UWB
            Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO23, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO18, DeviceFunction.SPI1_CLOCK);
#endif
#if RAK11200_RAK1907_RAK13801
            Configuration.SetPinFunction(Gpio.IO35, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO25, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO33, DeviceFunction.SPI1_CLOCK);
#endif
            var settings = new SpiConnectionSettings(SpiBusId, chipSelectLine)
            {
                ClockFrequency = 2000000,
                Mode = SpiMode.Mode0,
            };

            using (SpiDevice device = SpiDevice.Create(settings))
            {
                while (true)
                {
                    byte[] writeBuffer = new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0 }; // 0x0 = DEV_ID
                    byte[] readBuffer = new byte[writeBuffer.Length];

                    device.TransferFullDuplex(writeBuffer, readBuffer); // 15, 48, 1, 202, 222

                    uint ridTag = (uint)(readBuffer[4]<< 8 | readBuffer[3]);
                    byte model = readBuffer[2];
                    byte ver = (byte)(readBuffer[1] >> 4);
                    byte rev = (byte)(readBuffer[1] & 0x0f);

                    Debug.WriteLine(String.Format($"RIDTAG 0x{ridTag:X2} MODEL 0x{model:X2} VER 0X{ver:X2} REV 0x{rev:X2}"));

                   Thread.Sleep(10000);
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
}

The alignment of the RAK11200 WisBiock Core Module pins and labels on the circuit diagram tripped me up. My initial configuration caused the device to reboot every time the application started.

Visual Studio 2022 Debug window displaying the decoded value from Register 0x0

At the top of test applications, I usually have a brief delay i.e Thread.Sleep(5000) so I can attach the debugger or erase the flash before the application crashes.

.NET nanoFramework Qorvo DW1000 – Makerfabs Device SPI

The Makerfabs ESP32 UWB(Ultra Wideband) module has a Qorvo DW1000 and Espressif ESP32 module. The Espressif ESP32 module can run the .NET nanoFramework but does not have a Qorvo DW1000 library. (March2023)

Makerfabs ESP32 UWB(Ultra Wide Band) module

Before any coding I used nanoff to “flash” the Espressif ESP32 module with the latest version of .NET nanoFramework

Flashing Makerfabs ESP32 UWB module with nanoff

The Qorvo DW1000 module has a Serial Peripheral Interface (SPI) so the Master In Slave Out(MISO), Master Out Slave In(MOSI), Serial Clock(SCLK) and Chip Slave Select(CSS) pins have to be configured using the Configuration.SetPinFunction method of the nanoFramework.Hadware.Esp32 library

Makerfabs ESP32 UWB module schematic

Even though SPI is an industry standard there are often subtle differences which need to be taken into account when reading from/writing to registers. The DW1000 has a static “Device Identifier” which I used to debug my “proof of concept” code.

DW1000 Datasheet Register Map documentation for Register 0x00

The DeviceSPI program reads register 0x00 and then displays the decoded payload.

public class Program
{
#if MAKERFABS_ESP32UWB
    private const int SpiBusId = 1;
    private const int chipSelectLine = Gpio.IO04;
#endif

    public static void Main()
    {
        Thread.Sleep(5000);

        Debug.WriteLine("devMobile.IoT.Dw1000.ShieldSPI starting");

        try
        {
#if MAKERFABS_ESP32UWB
            Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO23, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO18, DeviceFunction.SPI1_CLOCK);
#endif
            var settings = new SpiConnectionSettings(SpiBusId, chipSelectLine)
            {
                ClockFrequency = 2000000,
                Mode = SpiMode.Mode0,
            };

            using (SpiDevice device = SpiDevice.Create(settings))
            {
                Thread.Sleep(500);

                while (true)
                {
                    /*
                    byte[] writeBuffer = new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0 }; // 0x0 = DEV_ID
                    byte[] readBuffer = new byte[writeBuffer.Length];

                    device.TransferFullDuplex(writeBuffer, readBuffer); // 15, 48, 1, 202, 222
                    */

                    byte[] writeBuffer = new byte[] { 0x0 }; // 0x0 = DEV_ID
                    byte[] readBuffer = new byte[5];

                    device.TransferFullDuplex(writeBuffer, readBuffer); // 15, 48, 1, 202, 222
                       
                    uint ridTag = (uint)(readBuffer[4]<< 8 | readBuffer[3]);
                    byte model = readBuffer[2];
                    byte ver = (byte)(readBuffer[1] >> 4);
                    byte rev = (byte)(readBuffer[1] & 0x0f);

                    Debug.WriteLine(String.Format($"RIDTAG 0x{ridTag:X2} MODEL 0x{model:X2} VER 0X{ver:X2} REV 0x{rev:X2}"));

                    Thread.Sleep(10000);
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
}
Visual Studio 2022 Debug window displaying the decoded value from Register 0x0

The DW1000 User Manual is > 240 pages, with roughly 140 pages of detailed documentation about the DW1000 register set so progress will be slow.

.NET nanoFramework Qorvo DW1000 – flashing & flashing

A few months ago one of my customers purchased five Makerfabs ESP32 UWB(Ultra Wideband) devices to see how the range of Qorvo DW1000 modules was impacted by the steel gates and fences in a stock yard. The .NET nanoFramework runs on Espressif ESP32 devices but there is currently (Feb2023) no Qorvo DW1000 module support so I borrowed the devices.

First step was to use nanoff to flash the device with the latest suitable firmware

The Makerfabs board has 5 LEDs, one illuminated when the device has power, and the other four are connected to the Qorvo DW1000 module.

Makerfabs ESP32 UWB schematic

An important milestone for any hardware project is to get a Light Emitting Diode(LED) to flash but there none connected to the Espressif ESP32 module.

I used a Grove-4 pin Male Jumper to Grove 4 pin Conversion Cable to connect a Seeedstudio LED to the device

public class Program
{
    private static GpioController _gpioController;

    public static void Main()
    {
        _gpioController = new GpioController();

        //GpioPin led = _gpioController.OpenPin(Gpio.IO00, PinMode.Output);
        //GpioPin led = _gpioController.OpenPin(Gpio.IO01, PinMode.Output);
        //GpioPin led = _gpioController.OpenPin(Gpio.IO02, PinMode.Output);
        //GpioPin led = _gpioController.OpenPin(Gpio.IO03, PinMode.Output);
        GpioPin led = _gpioController.OpenPin(Gpio.IO04, PinMode.Output);

        led.Write(PinValue.Low);

        while (true)
        {
           led.Toggle();
           Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(525);
        }
    }
}

The test code was “inspired” by the .NET nanoFramework Blinky sample

.NET Core 5 SX127X library Arduino Duplex

The arduino-LoRa library LoRaDuplex sample is the basis for the last in this series of posts. The LoRaDuplex sample implements a basic protocol for addressed messages. The message payload starts with the destination address(byte), source address(byte), message counter(byte), payload length(byte), and then the payload(array of bytes).

LoRaDuplex

The sample code has configuration settings for the local address and destination (address).

#include <SPI.h>              // include libraries
#include <LoRa.h>

const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

String outgoing;              // outgoing message

byte msgCount = 0;            // count of outgoing messages
byte localAddress = 0xAA;     // address of this device
byte destination = 0x0;      // destination to send to
long lastSendTime = 0;        // last send time
int interval = 2000;          // interval between sends

void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);

  Serial.println("LoRa Duplex");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.enableCrc();

  Serial.println("LoRa init succeeded.");
}

void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "HeLoRa World!";   // send a message
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    interval = random(2000) + 29000;    // 2-3 seconds
  }

  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.write(destination);              // add destination address
  LoRa.write(localAddress);             // add sender address
  LoRa.write(msgCount);                 // add message ID
  LoRa.write(outgoing.length());        // add payload length
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}

void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return

  // read packet header bytes:
  int recipient = LoRa.read();          // recipient address
  byte sender = LoRa.read();            // sender address
  byte incomingMsgId = LoRa.read();     // incoming msg ID
  byte incomingLength = LoRa.read();    // incoming msg length

  String incoming = "";

  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  if (incomingLength != incoming.length()) {   // check length for error
    Serial.println("error: message length does not match length");
    return;                             // skip rest of function
  }

  // if the recipient isn't this device or broadcast,
  if (recipient != localAddress && recipient != 0xFF) {
    Serial.println("This message is not for me.");
    return;                             // skip rest of function
  }

  // if message is for this device, or broadcast, print details:
  Serial.println("Received from: 0x" + String(sender, HEX));
  Serial.println("Sent to: 0x" + String(recipient, HEX));
  Serial.println("Message ID: " + String(incomingMsgId));
  Serial.println("Message length: " + String(incomingLength));
  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}
Arduino Monitor displaying information about the messages sent and received by the Duplex sample

In the Visual Studio output window I could see the SX127XLoRaDeviceClient sending and receiving messages

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
17:26:10-TX to 0xAA from 0x00 count 1 length 28 "Hello LoRa from .NET Core! 1"
17:26:10-TX Done
17:26:20-TX to 0xAA from 0x00 count 2 length 28 "Hello LoRa from .NET Core! 2"
17:26:20-TX Done
17:26:30-TX to 0xAA from 0x00 count 3 length 28 "Hello LoRa from .NET Core! 3"
17:26:30-TX Done
17:26:31-RX to 0x00 from 0xAA count 0 length 13 "HeLoRa World!" snr 9.5 packet rssi -57dBm rssi -100dBm 
17:26:40-TX to 0xAA from 0x00 count 4 length 28 "Hello LoRa from .NET Core! 4"
17:26:40-TX Done
17:26:50-TX to 0xAA from 0x00 count 5 length 28 "Hello LoRa from .NET Core! 5"
17:26:50-TX Done
17:27:00-TX to 0xAA from 0x00 count 6 length 28 "Hello LoRa from .NET Core! 6"
17:27:00-TX Done
17:27:01-RX to 0x00 from 0xBB count 1 length 13 "HeLoRa World!" snr 9.8 packet rssi -50dBm rssi -100dBm 
17:27:10-TX to 0xAA from 0x00 count 7 length 28 "Hello LoRa from .NET Core! 7"
17:27:10-TX Done
17:27:20-TX to 0xAA from 0x00 count 8 length 28 "Hello LoRa from .NET Core! 8"
17:27:20-TX Done
17:27:30-TX to 0xAA from 0x00 count 9 length 28 "Hello LoRa from .NET Core! 9"
17:27:30-TX Done

I modified the SX127X.NetCore SX127XLoRaDeviceClient adding one final conditional compile option(LoRaDuplex) for this sample

static void Main(string[] args)
{
	int messageCount = 1;

	sX127XDevice.Initialise(
			SX127XDevice.RegOpModeMode.ReceiveContinuous,
			915000000.0,
			powerAmplifier: SX127XDevice.PowerAmplifier.PABoost,
#if LORA_SENDER // From the Arduino point of view
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_RECEIVER // From the Arduino point of view, don't actually need this as already inverted
			invertIQTX: true
#endif

#if LORA_SET_SYNCWORD
			syncWord: 0xF3,
			invertIQTX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SET_SPREAD
			spreadingFactor: SX127XDevice.RegModemConfig2SpreadingFactor._256ChipsPerSymbol,
			invertIQTX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SIMPLE_NODE // From the Arduino point of view
			invertIQTX: false,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SIMPLE_GATEWAY // From the Arduino point of view
			invertIQRX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_DUPLEX
			rxPayloadCrcOn: true
#endif
			);

#if DEBUG
	sX127XDevice.RegisterDump();
#endif

#if !LORA_RECEIVER
	sX127XDevice.OnReceive += SX127XDevice_OnReceive;
	sX127XDevice.Receive();
#endif
#if !LORA_SENDER
	sX127XDevice.OnTransmit += SX127XDevice_OnTransmit;
#endif

#if LORA_SENDER
	Thread.Sleep(-1);
#else
	Thread.Sleep(5000);
#endif

	while (true)
	{
		string messageText = "Hello LoRa from .NET Core! " + messageCount.ToString();

#if LORA_DUPLEX
		byte[] messageBytes = new byte[messageText.Length+4];

		messageBytes[0] = 0xaa;
		messageBytes[1] = 0x00;
		messageBytes[2] = (byte)messageCount;
		messageBytes[3] = (byte)messageText.Length;

		Array.Copy(UTF8Encoding.UTF8.GetBytes(messageText), 0, messageBytes, 4, messageBytes[3]);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}-TX to 0x{messageBytes[0]:X2} from 0x{messageBytes[1]:X2} count {messageBytes[2]} length {messageBytes[3]} \"{messageText}\"");
#else
		byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}- Length {messageBytes.Length} \"{messageText}\"");
#endif

		messageCount += 1;

		sX127XDevice.Send(messageBytes);

		Thread.Sleep(10000);
	}
}

private static void SX127XDevice_OnReceive(object sender, SX127XDevice.OnDataReceivedEventArgs e)
{
	string messageText;

#if LORA_DUPLEX
	if ((e.Data[0] != 0x00) && (e.Data[0] != 0xFF))
	{
#if DEBUG
		Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss}-RX to 0x{e.Data[0]:X2} from 0x{e.Data[1]:X2} invalid address");
#endif
		return;
	}

	// check payload not to long/short
	if  ((e.Data[3] + 4) != e.Data.Length)
	{
		Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss}-RX Invalid payload");

		return;
	}

	try
	{
		messageText = UTF8Encoding.UTF8.GetString(e.Data, 4, e.Data[3]);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}-RX to 0x{e.Data[0]:X2} from 0x{e.Data[1]:X2} count {e.Data[2]} length {e.Data[3]} \"{messageText}\" snr {e.PacketSnr:0.0} packet rssi {e.PacketRssi}dBm rssi {e.Rssi}dBm ");
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
#else
	try
	{
		messageText = UTF8Encoding.UTF8.GetString(e.Data);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}-RX length {e.Data.Length} \"{messageText}\" snr {e.PacketSnr:0.0} packet rssi {e.PacketRssi}dBm rssi {e.Rssi}dBm ");
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
#endif
}

The inbound messages have to have a valid Cyclic Redundancy Check(CRC) and I ignore messages with an invalid payload length. The message protocol is insecure (but fine for demos) as the messages are sent as “plain text”, and the message headers/payload can be tampered with.

Summary

While testing the LoRaDuplex sample I found a problem with how my code managed the invertIQRX & invertIQTX flags in RegInvertIQ. I noticed the even though I was setting the InvertIQRX(bit6) and invertIQTX(bit0) flags correctly messages weren’t getting delivered.

Semtech SX127X data sheet RegInvertQ and RegInvertQ2 documetnation

After looking at my code I realised I wasn’t configuring the RegInvertIQ properly because bits 1-5 were getting set to 0x0 (initially I had byte regInvertIQValue = 0) rather than 0x13(regInvertIQValue = RegInvertIdDefault)

...
// RegInvertId
private const byte RegInvertIdDefault = 0b00100110;
private const byte InvertIqRXOn = 0b01000000;
private const byte InvertIqRXOff = 0b00000000;
public const bool InvertIqRXDefault = false;

private const byte InvertIqTXOn =  0b00000001; 
private const byte InvertIqTXOff = 0b00000000;
...

if ((invertIQRX != InvertIqRXDefault) || (invertIQTX != InvertIqTXDefault))
{
	// Initially this was byte regInvertIQValue = 0;
	byte regInvertIQValue = RegInvertIdDefault;

	if (invertIQRX)
	{
		regInvertIQValue |= InvertIqRXOn;
	}

	if (invertIQTX)
	{
		regInvertIQValue |= InvertIqTXOn;
	}

	this.WriteByte((byte)Registers.RegInvertIQ, regInvertIQValue);

	if (invertIQRX || invertIQTX)
	{
		this.WriteByte((byte)Registers.RegInvertIQ2, RegInvertIq2On);
	}
	else
	{
		this.WriteByte((byte)Registers.RegInvertIQ2, RegInvertIq2Off);
	}
}

.NET Core SX127X library Arduino LoRaSimpleNode & LoRaSimpleGateway

The LoRaSimpleNode and LoRaSimpleGateway samples shows how the receive and transmit IQ can be inverted.

LoRaSimpleNode

This sample uses all default settings except for frequency with InvertIQ enabled in receive more and disabled in Transmit mode

void loop() {
  if (runEvery(1000)) { // repeat every 1000 millis

    String message = "HeLoRa World! ";
    message += "I'm a Node! ";
    message += millis();

    LoRa_sendMessage(message); // send a message

    Serial.println("Send Message!");
  }
}

void LoRa_rxMode(){
  LoRa.enableInvertIQ();                // active invert I and Q signals
  LoRa.receive();                       // set receive mode
}

void LoRa_txMode(){
  LoRa.idle();                          // set standby mode
  LoRa.disableInvertIQ();               // normal mode
}

void LoRa_sendMessage(String message) {
  LoRa_txMode();                        // set tx mode
  LoRa.beginPacket();                   // start packet
  LoRa.print(message);                  // add payload
  LoRa.endPacket();                     // finish packet and send it
  LoRa_rxMode();                        // set rx mode
}

void onReceive(int packetSize) {
  String message = "";

  while (LoRa.available()) {
    message += (char)LoRa.read();
  }

  Serial.print("Node Receive: ");
  Serial.println(message);

}
Arduino Monitor displaying the output of the Arduino-LoRa Simple Node sample

In the Visual Studio output window I could see the received messages.

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
17:46:31-RX length 31 "HeLoRa World! I'm a Node! 69000" snr 10.3 packet rssi -57dBm rssi -98dBm 
17:46:32-RX length 31 "HeLoRa World! I'm a Node! 70000" snr 9.8 packet rssi -56dBm rssi -104dBm 
17:46:33-RX length 31 "HeLoRa World! I'm a Node! 71000" snr 10.0 packet rssi -57dBm rssi -104dBm 
17:46:34-RX length 31 "HeLoRa World! I'm a Node! 72000" snr 9.8 packet rssi -56dBm rssi -102dBm 
17:46:35-RX length 31 "HeLoRa World! I'm a Node! 73000" snr 9.8 packet rssi -59dBm rssi -102dBm 
17:46:36- Length 28 "Hello LoRa from .NET Core! 1"
17:46:36-TX Done
17:46:37-RX length 31 "HeLoRa World! I'm a Node! 75000" snr 9.3 packet rssi -58dBm rssi -102dBm 
17:46:38-RX length 31 "HeLoRa World! I'm a Node! 76000" snr 9.0 packet rssi -58dBm rssi -102dBm 
17:46:39-RX length 31 "HeLoRa World! I'm a Node! 77000" snr 9.8 packet rssi -59dBm rssi -104dBm 
17:46:40-RX length 31 "HeLoRa World! I'm a Node! 78000" snr 9.5 packet rssi -57dBm rssi -102dBm 
17:46:41-RX length 31 "HeLoRa World! I'm a Node! 79000" snr 9.5 packet rssi -55dBm rssi -102dBm 
17:46:42-RX length 31 "HeLoRa World! I'm a Node! 80000" snr 9.8 packet rssi -57dBm rssi -104dBm 
17:46:43-RX length 31 "HeLoRa World! I'm a Node! 81000" snr 9.5 packet rssi -58dBm rssi -104dBm 
17:46:44-RX length 31 "HeLoRa World! I'm a Node! 82000" snr 9.5 packet rssi -58dBm rssi -104dBm 
17:46:45-RX length 31 "HeLoRa World! I'm a Node! 83000" snr 9.0 packet rssi -58dBm rssi -94dBm 
17:46:46- Length 28 "Hello LoRa from .NET Core! 2"
17:46:46-TX Done
17:46:47-RX length 31 "HeLoRa World! I'm a Node! 85000" snr 9.0 packet rssi -58dBm rssi -104dBm 
17:46:48-RX length 31 "HeLoRa World! I'm a Node! 86000" snr 9.5 packet rssi -58dBm rssi -104dBm 
17:46:49-RX length 31 "HeLoRa World! I'm a Node! 87000" snr 9.5 packet rssi -58dBm rssi -102dBm 
17:46:50-RX length 30 "HeLoRa World! I'm a Node! 1000" snr 9.5 packet rssi -58dBm rssi -102dBm 
17:46:51-RX length 30 "HeLoRa World! I'm a Node! 2000" snr 9.5 packet rssi -58dBm rssi -104dBm 
17:46:52-RX length 30 "HeLoRa World! I'm a Node! 3000" snr 9.3 packet rssi -58dBm rssi -102dBm 
17:46:53-RX length 30 "HeLoRa World! I'm a Node! 4000" snr 9.5 packet rssi -58dBm rssi -102dBm 
17:46:54-RX length 30 "HeLoRa World! I'm a Node! 5000" snr 10.0 packet rssi -57dBm rssi -102dBm 
17:46:55-RX length 30 "HeLoRa World! I'm a Node! 6000" snr 10.0 packet rssi -57dBm rssi -102dBm 
17:46:56- Length 28 "Hello LoRa from .NET Core! 3"
17:46:56-TX Done
17:46:56-RX length 30 "HeLoRa World! I'm a Node! 7000" snr 9.8 packet rssi -57dBm rssi -104dBm 
17:46:57-RX length 30 "HeLoRa World! I'm a Node! 8000" snr 10.0 packet rssi -57dBm rssi -102dBm 
17:46:58-RX length 30 "HeLoRa World! I'm a Node! 9000" snr 9.8 packet rssi -57dBm rssi -104dBm 
17:46:59-RX length 31 "HeLoRa World! I'm a Node! 10000" snr 9.8 packet rssi -57dBm rssi -100dBm 
17:47:00-RX length 31 "HeLoRa World! I'm a Node! 11000" snr 9.8 packet rssi -57dBm rssi -99dBm 
17:47:01-RX length 31 "HeLoRa World! I'm a Node! 12000" snr 9.3 packet rssi -57dBm rssi -104dBm 
17:47:04-RX length 30 "HeLoRa World! I'm a Node! 1000" snr 9.5 packet rssi -57dBm rssi -100dBm 
17:47:05-RX length 30 "HeLoRa World! I'm a Node! 2000" snr 10.0 packet rssi -57dBm rssi -100dBm 
17:47:06- Length 28 "Hello LoRa from .NET Core! 4"
17:47:06-TX Done

LoRaSimpleGateway

The SimpleGateway uses all the same settings but with InvertIQ enabled in Transmit mode and disabled in Receive mode

#include <SPI.h>              // include libraries
#include <LoRa.h>

const long frequency = 915E6;  // LoRa Frequency

const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;        // LoRa radio reset
const int irqPin = 2;          // change for your board; must be a hardware interrupt pin

void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);

  LoRa.setPins(csPin, resetPin, irqPin);

  if (!LoRa.begin(frequency)) {
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  Serial.println("LoRa init succeeded.");
  Serial.println();
  Serial.println("LoRa Simple Gateway");
  Serial.println("Only receive messages from nodes");
  Serial.println("Tx: invertIQ enable");
  Serial.println("Rx: invertIQ disable");
  Serial.println();

  LoRa.onReceive(onReceive);
  LoRa_rxMode();
}

void loop() {
  if (runEvery(5000)) { // repeat every 5000 millis

    String message = "HeLoRa World! ";
    message += "I'm a Gateway! ";
    message += millis();

    LoRa_sendMessage(message); // send a message

    Serial.println("Send Message!");
  }
}

void LoRa_rxMode(){
  LoRa.disableInvertIQ();               // normal mode
  LoRa.receive();                       // set receive mode
}

void LoRa_txMode(){
  LoRa.idle();                          // set standby mode
  LoRa.enableInvertIQ();                // active invert I and Q signals
}

void LoRa_sendMessage(String message) {
  LoRa_txMode();                        // set tx mode
  LoRa.beginPacket();                   // start packet
  LoRa.print(message);                  // add payload
  LoRa.endPacket();                     // finish packet and send it
  LoRa_rxMode();                        // set rx mode
}

void onReceive(int packetSize) {
  String message = "";

  while (LoRa.available()) {
    message += (char)LoRa.read();
  }

  Serial.print("Gateway Receive: ");
  Serial.println(message);
}

Arduino Monitor displaying the output of the Arduino-LoRa Simple Gateway sample

In the Visual Studio output window I could see messages getting transmitted with sent confirmations.

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
17:51:39-RX length 34 "HeLoRa World! I'm a Gateway! 10000" snr 9.3 packet rssi -59dBm rssi -102dBm 
17:51:39- Length 28 "Hello LoRa from .NET Core! 1"
17:51:39-TX Done
17:51:44-RX length 34 "HeLoRa World! I'm a Gateway! 15000" snr 9.3 packet rssi -58dBm rssi -102dBm 
17:51:49-RX length 34 "HeLoRa World! I'm a Gateway! 20000" snr 9.3 packet rssi -59dBm rssi -100dBm 
17:51:49- Length 28 "Hello LoRa from .NET Core! 2"
17:51:49-TX Done
17:51:54-RX length 34 "HeLoRa World! I'm a Gateway! 25000" snr 9.0 packet rssi -58dBm rssi -102dBm 
17:51:59-RX length 34 "HeLoRa World! I'm a Gateway! 30000" snr 9.3 packet rssi -58dBm rssi -100dBm 
17:51:59- Length 28 "Hello LoRa from .NET Core! 3"
17:51:59-TX Done
17:52:04-RX length 34 "HeLoRa World! I'm a Gateway! 35000" snr 9.3 packet rssi -60dBm rssi -104dBm 
17:52:09-RX length 34 "HeLoRa World! I'm a Gateway! 40000" snr 9.5 packet rssi -59dBm rssi -104dBm 
17:52:09- Length 28 "Hello LoRa from .NET Core! 4"
17:52:09-TX Done
17:52:14-RX length 34 "HeLoRa World! I'm a Gateway! 45000" snr 9.5 packet rssi -59dBm rssi -102dBm 
17:52:19-RX length 34 "HeLoRa World! I'm a Gateway! 50000" snr 9.3 packet rssi -60dBm rssi -104dBm 
17:52:19- Length 28 "Hello LoRa from .NET Core! 5"
17:52:19-TX Done
17:52:24-RX length 34 "HeLoRa World! I'm a Gateway! 55000" snr 9.8 packet rssi -60dBm rssi -102dBm 
17:52:29-RX length 34 "HeLoRa World! I'm a Gateway! 60000" snr 9.3 packet rssi -60dBm rssi -104dBm 
17:52:29- Length 28 "Hello LoRa from .NET Core! 6"
17:52:29-TX Done
17:52:34-RX length 34 "HeLoRa World! I'm a Gateway! 65000" snr 9.0 packet rssi -60dBm rssi -102dBm 
17:52:39-RX length 34 "HeLoRa World! I'm a Gateway! 70000" snr 9.3 packet rssi -60dBm rssi -102dBm 
17:52:39- Length 28 "Hello LoRa from .NET Core! 7"
17:52:39-TX Done
17:52:44-RX length 34 "HeLoRa World! I'm a Gateway! 75000" snr 8.8 packet rssi -58dBm rssi -102dBm 
17:52:49-RX length 34 "HeLoRa World! I'm a Gateway! 80000" snr 9.0 packet rssi -59dBm rssi -102dBm 
17:52:49- Length 28 "Hello LoRa from .NET Core! 8"
17:52:49-TX Done
17:52:54-RX length 34 "HeLoRa World! I'm a Gateway! 85000" snr 9.8 packet rssi -60dBm rssi -102dBm 
17:52:59-RX length 34 "HeLoRa World! I'm a Gateway! 90000" snr 9.0 packet rssi -60dBm rssi -102dBm 
17:52:59- Length 28 "Hello LoRa from .NET Core! 9"
17:52:59-TX Done
17:53:04-RX length 34 "HeLoRa World! I'm a Gateway! 95000" snr 9.3 packet rssi -59dBm rssi -100dBm 
17:53:09-RX length 35 "HeLoRa World! I'm a Gateway! 100000" snr 9.0 packet rssi -59dBm rssi -102dBm 
17:53:09- Length 29 "Hello LoRa from .NET Core! 10"
17:53:09-TX Done
17:53:14-RX length 35 "HeLoRa World! I'm a Gateway! 105000" snr 9.5 packet rssi -56dBm rssi -102dBm 
17:53:19-RX length 35 "HeLoRa World! I'm a Gateway! 110000" snr 9.3 packet rssi -59dBm rssi -102dBm 
17:53:19- Length 29 "Hello LoRa from .NET Core! 11"
17:53:19-TX Done

I then modified the SX127X.NetCore SX127XLoRaDeviceClient adding even more conditional compile options for the LoRaSampleNode and LoRaSampleGateway samples.

int messageCount = 1;

sX127XDevice.Initialise(
		SX127XDevice.RegOpModeMode.ReceiveContinuous,
		915000000.0,
		powerAmplifier: SX127XDevice.PowerAmplifier.PABoost,
		// outputPower: 5, outputPower: 20, outputPower:23,
		//powerAmplifier: SX127XDevice.PowerAmplifier.Rfo,	
		//outputPower:-1, outputPower: 14,
#if LORA_SENDER // From the Arduino point of view
		rxDoneignoreIfCrcMissing: false
#endif
#if LORA_RECEIVER // From the Arduino point of view, don't actually need this as already inverted
		invertIQTX: true
#endif

#if LORA_SET_SYNCWORD
		syncWord: 0xF3,
		invertIQTX: true,
		rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SET_SPREAD
		spreadingFactor: SX127XDevice.RegModemConfig2SpreadingFactor._256ChipsPerSymbol,
		invertIQTX: true,
		rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SIMPLE_NODE // From the Arduino point of view
		invertIQTX: false,
		rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SIMPLE_GATEWAY // From the Arduino point of view
		invertIQRX: true,
		rxDoneignoreIfCrcMissing: false
#endif
		);

#if DEBUG
		sX127XDevice.RegisterDump();
#endif

#if !LORA_RECEIVER
		sX127XDevice.OnReceive += SX127XDevice_OnReceive;
		sX127XDevice.Receive();
#endif
#if !LORA_SENDER
		sX127XDevice.OnTransmit += SX127XDevice_OnTransmit;
#endif

#if LORA_SENDER
		Thread.Sleep(-1);
#else
		Thread.Sleep(5000);
#endif

		while (true)
		{
			string messageText = "Hello LoRa from .NET Core! " + messageCount.ToString();

			byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);

			Console.WriteLine($"{DateTime.Now:HH:mm:ss}- Length {messageBytes.Length} \"{messageText}\"");

			messageCount += 1;

			sX127XDevice.Send(messageBytes);

			Thread.Sleep(10000);
		}
	}

Summary

While testing the LoRaReceiver sample I found a problem with how my code managed the transmit power by accidentally commenting out the “paBoost: true” parameter of the initialise method. When I did this the Seeeduino V4.2 and Dragino Shield stopped receiving messages.

I had assumed a user could configure the the output power using the initialise method but that was difficult/possible. After some digging I found that I needed to use RegPAConfigPADac and PABoost (I need to find a device which uses RFO for testing). So I removed several of the configuration parameters from the Intialise method and replaced them with one called outputPower. I then re-read the SX127X data sheet and had a look at some other libraries.

The Arduino-LoRa code has SetPower

void LoRaClass::setTxPower(int level, int outputPin)
{
  if (PA_OUTPUT_RFO_PIN == outputPin) {
    // RFO
    if (level < 0) {
      level = 0;
    } else if (level > 14) {
      level = 14;
    }

    writeRegister(REG_PA_CONFIG, 0x70 | level);
  } else {
    // PA BOOST
    if (level > 17) {
      if (level > 20) {
        level = 20;
      }

      // subtract 3 from level, so 18 - 20 maps to 15 - 17
      level -= 3;

      // High Power +20 dBm Operation (Semtech SX1276/77/78/79 5.4.3.)
      writeRegister(REG_PA_DAC, 0x87);
      setOCP(140);
    } else {
      if (level < 2) {
        level = 2;
      }
      //Default value PA_HF/LF or +17dBm
      writeRegister(REG_PA_DAC, 0x84);
      setOCP(100);
    }

    writeRegister(REG_PA_CONFIG, PA_BOOST | (level - 2));
  }
}

The AdaFruit version of RadioHead library has SetTxPower which has been “tweaked”

void RH_RF95::setTxPower(int8_t power, bool useRFO)
{
    // Sigh, different behaviours depending on whther the module use PA_BOOST or the RFO pin
    // for the transmitter output
    if (useRFO)
    {
	if (power > 14)
	    power = 14;
	if (power < -1)
	    power = -1;
	spiWrite(RH_RF95_REG_09_PA_CONFIG, RH_RF95_MAX_POWER | (power + 1));
    }
    else
    {
	if (power > 23)
	    power = 23;
	if (power < 5)
	    power = 5;

	// For RH_RF95_PA_DAC_ENABLE, manual says '+20dBm on PA_BOOST when OutputPower=0xf'
	// RH_RF95_PA_DAC_ENABLE actually adds about 3dBm to all power levels. We will us it
	// for 21, 22 and 23dBm
	if (power > 20)
	{
	    spiWrite(RH_RF95_REG_4D_PA_DAC, RH_RF95_PA_DAC_ENABLE);
	    power -= 3;
	}
	else
	{
	    spiWrite(RH_RF95_REG_4D_PA_DAC, RH_RF95_PA_DAC_DISABLE);
	}

	// RFM95/96/97/98 does not have RFO pins connected to anything. Only PA_BOOST
	// pin is connected, so must use PA_BOOST
	// Pout = 2 + OutputPower.
	// The documentation is pretty confusing on this topic: PaSelect says the max power is 20dBm,
	// but OutputPower claims it would be 17dBm.
	// My measurements show 20dBm is correct
	spiWrite(RH_RF95_REG_09_PA_CONFIG, RH_RF95_PA_SELECT | (power-5));
    }
}

The LoRa Shield Arduino library has two methods setPower(char p) and setPowerNum(uint8_t pow)

/*
 Function: Sets the signal power indicated as input to the module.
 Returns: Integer that determines if there has been any error
   state = 2  --> The command has not been executed
   state = 1  --> There has been an error while executing the command
   state = 0  --> The command has been executed with no errors
   state = -1 --> Forbidden command for this protocol
 Parameters:
   pow: power option to set in configuration. The input value range is from 
   0 to 14 dBm.
*/
int8_t SX1278::setPowerNum(uint8_t pow)
{
  byte st0;
  int8_t state = 2;
  byte value = 0x00;

  #if (SX1278_debug_mode > 1)
	  Serial.println();
	  Serial.println(F("Starting 'setPower'"));
  #endif

  st0 = readRegister(REG_OP_MODE);	  // Save the previous status
  if( _modem == LORA )
  { // LoRa Stdby mode to write in registers
	  writeRegister(REG_OP_MODE, LORA_STANDBY_MODE);
  }
  else
  { // FSK Stdby mode to write in registers
	  writeRegister(REG_OP_MODE, FSK_STANDBY_MODE);
  }
  
  if ( (pow >= 2) && (pow <= 20) )
  { // Pout= 17-(15-OutputPower) = OutputPower+2
	  if ( pow <= 17 ) {
		writeRegister(REG_PA_DAC, 0x84);
	  	pow = pow - 2;
	  } else { // Power > 17dbm -> Power = 20dbm
		writeRegister(REG_PA_DAC, 0x87);
		pow = 15;
	  }
	  _power = pow;
  }
  else
  {
	  state = -1;
	  #if (SX1278_debug_mode > 1)
		  Serial.println(F("## Power value is not valid ##"));
		  Serial.println();
	  #endif
  }

  writeRegister(REG_PA_CONFIG, _power);	// Setting output power value
  value = readRegister(REG_PA_CONFIG);

  if( value == _power )
  {
	  state = 0;
	  #if (SX1278_debug_mode > 1)
		  Serial.println(F("## Output power has been successfully set ##"));
		  Serial.println();
	  #endif
  }
  else
  {
	  state = 1;
  }

  writeRegister(REG_OP_MODE, st0);	// Getting back to previous status
  return state;
}

The SEMTECH library(V2.1.0) manages sleeping the device, reading the existing configuration and updating it as required which was a bit more functionality that I wanted.

void SX1276LoRaSetRFPower( int8_t power )
{
    SX1276Read( REG_LR_PACONFIG, &SX1276LR->RegPaConfig );
    SX1276Read( REG_LR_PADAC, &SX1276LR->RegPaDac );
    
    if( ( SX1276LR->RegPaConfig & RFLR_PACONFIG_PASELECT_PABOOST ) == RFLR_PACONFIG_PASELECT_PABOOST )
    {
        if( ( SX1276LR->RegPaDac & 0x87 ) == 0x87 )
        {
            if( power < 5 )
            {
                power = 5;
            }
            if( power > 20 )
            {
                power = 20;
            }
            SX1276LR->RegPaConfig = ( SX1276LR->RegPaConfig & RFLR_PACONFIG_MAX_POWER_MASK ) | 0x70;
            SX1276LR->RegPaConfig = ( SX1276LR->RegPaConfig & RFLR_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power - 5 ) & 0x0F );
        }
        else
        {
            if( power < 2 )
            {
                power = 2;
            }
            if( power > 17 )
            {
                power = 17;
            }
            SX1276LR->RegPaConfig = ( SX1276LR->RegPaConfig & RFLR_PACONFIG_MAX_POWER_MASK ) | 0x70;
            SX1276LR->RegPaConfig = ( SX1276LR->RegPaConfig & RFLR_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power - 2 ) & 0x0F );
        }
    }
    else
    {
        if( power < -1 )
        {
            power = -1;
        }
        if( power > 14 )
        {
            power = 14;
        }
        SX1276LR->RegPaConfig = ( SX1276LR->RegPaConfig & RFLR_PACONFIG_MAX_POWER_MASK ) | 0x70;
        SX1276LR->RegPaConfig = ( SX1276LR->RegPaConfig & RFLR_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power + 1 ) & 0x0F );
    }
    SX1276Write( REG_LR_PACONFIG, SX1276LR->RegPaConfig );
    LoRaSettings.Power = power;
}

All the of the examples I looked at were different and some had manual tweaks, others I have not included were just wrong. I have based my beta version on a hybrid of the Arduino-LoRa, RadioHead and Semtech libraries. I need to test my code and confirm that I have the limits and offsets correct for the PABoost and RFO modes.

// RegPaDac more power
[Flags]
public enum RegPaDac
{
	Normal = 0b01010100,
	Boost = 0b01010111,
}
private const byte RegPaDacPABoostThreshold = 20;

// Validate the OutputPower
if (powerAmplifier == PowerAmplifier.Rfo)
{
	if ((outputPower < OutputPowerRfoMin) || (outputPower > OutputPowerRfoMax))
	{
		throw new ArgumentException($"outputPower must be between {OutputPowerRfoMin} and {OutputPowerRfoMax}", nameof(outputPower));
	}
}
if (powerAmplifier == PowerAmplifier.PABoost)
{
	if ((outputPower < OutputPowerPABoostMin) || (outputPower > OutputPowerPABoostMax))
	{
		throw new ArgumentException($"outputPower must be between {OutputPowerPABoostMin} and {OutputPowerPABoostMax}", nameof(outputPower));	
	}
}

if (( powerAmplifier != PowerAmplifierDefault) || (outputPower != OutputPowerDefault))
{
	byte regPAConfigValue = RegPAConfigMaxPowerMax;

	if (powerAmplifier == PowerAmplifier.Rfo)
	{
		regPAConfigValue |= RegPAConfigPASelectRfo;

		regPAConfigValue |= (byte)(outputPower + 1);

		this.WriteByte((byte)Registers.RegPAConfig, regPAConfigValue);
	}

	if (powerAmplifier == PowerAmplifier.PABoost)
	{
		regPAConfigValue |= RegPAConfigPASelectPABoost;

		if (outputPower > RegPaDacPABoostThreshold)
		{
			this.WriteByte((byte)Registers.RegPaDac, (byte)RegPaDac.Boost);

			regPAConfigValue |= (byte)(outputPower - 8);

			this.WriteByte((byte)Registers.RegPAConfig, regPAConfigValue);
		}
		else
		{
			this.WriteByte((byte)Registers.RegPaDac, (byte)RegPaDac.Normal);

			regPAConfigValue |= (byte)(outputPower - 5);

			this.WriteByte((byte)Registers.RegPAConfig, regPAConfigValue);
		}
	}
}

.NET Core SX127X library Arduino LoRaSender & LoRaReceiver

The arduino-LoRa library comes with a number of samples showing how to use its functionality. The LoRaSender and LoRaReceiver samples show the bare minimum of code required to send and receive messages.

LoRaSender

This sample uses all default settings except for frequency

#include <SPI.h>
#include <LoRa.h>

int counter = 0;

void setup() {
  Serial.begin(9600);
  while (!Serial);

  Serial.println("LoRa Sender");

  if (!LoRa.begin(915E6)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
  
  delay(5000);  
}

void loop() {
  Serial.print("Sending packet: ");
  Serial.println(counter);

  // send packet
  LoRa.beginPacket();
  LoRa.print("hello ");
  LoRa.print(counter);
  LoRa.endPacket();

  counter++;

  delay(5000);
}

Arduino-LoRa library LoRaSender monitor output

In the Visual Studio output window I could see the received messages including a “corrupted” one which was displayed because the SX127XLoRaDeviceClient couldn’t force Cyclic Redundancy Check(CRC)s.

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
17:08:14-RX length 108 "hello 7" snr 9.3 packet rssi -63dBm rssi -102dBm 
17:08:24-RX length 108 "hello 0" snr 9.5 packet rssi -64dBm rssi -104dBm 
17:08:29-RX length 108 "hello 1" snr 9.3 packet rssi -64dBm rssi -102dBm 
17:08:34-RX length 108 "hello 2" snr 9.5 packet rssi -64dBm rssi -102dBm 
17:08:39-RX length 108 "hello 3" snr 8.5 packet rssi -61dBm rssi -104dBm 
17:08:44-RX length 108 "hello 4" snr 8.5 packet rssi -62dBm rssi -104dBm 
17:08:49-RX length 108 "hello 5" snr 9.3 packet rssi -64dBm rssi -104dBm 
17:08:54-RX length 108 "hello 6" snr 9.3 packet rssi -64dBm rssi -102dBm 
17:08:59-RX length 108 "hello 7" snr 9.3 packet rssi -64dBm rssi -102dBm 
17:09:04-RX length 108 "hello 8" snr 9.3 packet rssi -64dBm rssi -100dBm 
17:09:09-RX length 108 "hello 9" snr 9.3 packet rssi -64dBm rssi -102dBm 
17:09:14-RX length 108 "hello 10" snr 8.8 packet rssi -58dBm rssi -102dBm 
17:09:19-RX length 108 "hello 11" snr 9.3 packet rssi -60dBm rssi -104dBm 
17:09:24-RX length 108 "hello 12" snr 9.5 packet rssi -59dBm rssi -104dBm 
17:09:29-RX length 108 "hello 13" snr 9.0 packet rssi -60dBm rssi -102dBm 
17:09:34-RX length 108 "hello 14" snr 9.5 packet rssi -59dBm rssi -105dBm 
17:09:39-RX length 108 "hello 15" snr 9.0 packet rssi -57dBm rssi -102dBm 
17:09:44-RX length 108 "hello 16" snr 9.3 packet rssi -61dBm rssi -104dBm 
17:09:49-RX length 108 "hello 17" snr 9.5 packet rssi -61dBm rssi -104dBm 
17:09:54-RX length 108 "hello 18" snr 9.0 packet rssi -59dBm rssi -104dBm 
17:09:59-RX length 108 "hello 19" snr 9.3 packet rssi -61dBm rssi -102dBm 
17:10:04-RX length 108 "hello 20" snr 9.0 packet rssi -59dBm rssi -104dBm 
17:10:09-RX length 108 "hello 21" snr 9.3 packet rssi -61dBm rssi -102dBm 
17:10:14-RX length 108 "hello 22" snr 9.5 packet rssi -60dBm rssi -102dBm 
17:10:19-RX length 108 "hello 23" snr 9.3 packet rssi -60dBm rssi -104dBm 
17:10:24-RX length 108 "hello 24" snr 9.3 packet rssi -60dBm rssi -103dBm 
17:10:26-RX length 212 "�Q�Ԕv&G=����[9Y���
2S��ᒵ��O*�Ϥ��X��p쏊��" snr 50.8 packet rssi -104dBm rssi -102dBm 
17:10:30-RX length 108 "hello 25" snr 9.5 packet rssi -60dBm rssi -102dBm 
17:10:35-RX length 108 "hello 26" snr 9.3 packet rssi -60dBm rssi -104dBm 

LoRaReceiver

This sample also uses all default settings except of the frequency

#include <SPI.h>
#include <LoRa.h>

void setup() {
  Serial.begin(9600);
  while (!Serial);

  Serial.println("LoRa Receiver");

  if (!LoRa.begin(915E6)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
}

void loop() {
  // try to parse packet
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    // received a packet
    Serial.print("Received packet '");

    // read packet
    while (LoRa.available()) {
      Serial.print((char)LoRa.read());
    }

    // print RSSI of packet
    Serial.print("' with RSSI ");
    Serial.println(LoRa.packetRssi());
  }
}
Arduino-LoRa library LoRaReceiver monitor output

In the Visual Studio output window I could see messages getting transmitted with sent confirmations.

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
17:21:19- Length 28 "Hello LoRa from .NET Core! 1"
17:21:19-TX Done
17:21:29- Length 28 "Hello LoRa from .NET Core! 2"
17:21:29-TX Done
17:21:39- Length 28 "Hello LoRa from .NET Core! 3"
17:21:39-TX Done
17:21:49- Length 28 "Hello LoRa from .NET Core! 4"
17:21:49-TX Done
17:21:59- Length 28 "Hello LoRa from .NET Core! 5"
17:21:59-TX Done
17:22:09- Length 28 "Hello LoRa from .NET Core! 6"
17:22:09-TX Done
17:22:19- Length 28 "Hello LoRa from .NET Core! 7"
17:22:19-TX Done
17:22:29- Length 28 "Hello LoRa from .NET Core! 8"
17:22:29-TX Done
17:22:39- Length 28 "Hello LoRa from .NET Core! 9"
17:22:39-TX Done
17:22:49- Length 29 "Hello LoRa from .NET Core! 10"
17:22:49-TX Done
17:22:59- Length 29 "Hello LoRa from .NET Core! 11"
17:22:59-TX Done
17:23:09- Length 29 "Hello LoRa from .NET Core! 12"
17:23:09-TX Done
17:23:19- Length 29 "Hello LoRa from .NET Core! 13"
17:23:19-TX Done
17:23:29- Length 29 "Hello LoRa from .NET Core! 14"
17:23:29-TX Done
17:23:39- Length 29 "Hello LoRa from .NET Core! 15"
17:23:39-TX Done
17:23:49- Length 29 "Hello LoRa from .NET Core! 16"
17:23:49-TX Done
17:23:59- Length 29 "Hello LoRa from .NET Core! 17"
17:23:59-TX Done
17:24:09- Length 29 "Hello LoRa from .NET Core! 18"
17:24:09-TX Done

I modified the SX127X.NetCore SX127XLoRaDeviceClient adding a conditional compile options for each sample

static void Main(string[] args)	
{
	int messageCount = 1;

	sX127XDevice.Initialise(
			SX127XDevice.RegOpModeMode.ReceiveContinuous,
			915000000.0,
			powerAmplifier: SX127XDevice.PowerAmplifier.PABoost,
#if LORA_SENDER // From the Arduino point of view
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_RECEIVER // From the Arduino point of view, don't actually need this as already inverted
			invertIQTX: true
#endif

#if LORA_SET_SYNCWORD
			syncWord: 0xF3,
			invertIQTX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SET_SPREAD
			spreadingFactor: SX127XDevice.RegModemConfig2SpreadingFactor._256ChipsPerSymbol,
			invertIQTX: true,
			rxDoneignoreIfCrcMissing: false
#endif
	);

#if DEBUG
	sX127XDevice.RegisterDump();
#endif

#if LORA_SENDER
	sX127XDevice.OnReceive += SX127XDevice_OnReceive;
	sX127XDevice.Receive();
#endif
#if LORA_RECEIVER
	sX127XDevice.OnTransmit += SX127XDevice_OnTransmit;
#endif

#if LORA_SENDER
	Thread.Sleep(-1);
#else
	Thread.Sleep(5000);
#endif

	while (true)
	{
		string messageText = "Hello LoRa from .NET Core! " + messageCount.ToString();

		byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}- Length {messageBytes.Length} \"{messageText}\""); 

		sX127XDevice.Send(messageBytes);

		messageCount += 1;

		Thread.Sleep(10000);
	}
}

Summary

While testing the LoRaReceiver sample I found a problem with how my code managed the RegOpMode register LoRa status value. In previous versions of the code I used RegOpModeModeDefault to manage status when the ProcessTxDone(byte IrqFlags) method completed and Receive() was called.

I had assumed that that the device would always be set with SetMode(RegOpModeModeDefault) but RegOpModeModeDefault was always RegOpModeMode.Sleep.

.NET Core SX127X library Arduino LoRaSetSpread

“Playing nice” with Arduino-LoRa Samples

The arduino-LoRa library comes with a number of samples showing how to configure a SX127X device. The LoRaSetSpread sample sets the RegModemtConfig2 (masking out previous CodingRate and ImplicitHeaderModeOn with 0xFF) to configure spreading Factor (bits 4-7) to 8 which is 256 chips/symbol.

..
void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);

  Serial.println("LoRa Duplex - Set spreading factor");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin); // set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.setSpreadingFactor(8);           // ranges from 6-12,default 7 see API docs
  Serial.println("LoRa init succeeded.");
}

In my library I use an enumeration to represent the different spreading factors to make configuration easier for humans. (the _ prefix is due to the C# language syntax)

// RegModemConfig2
public enum RegModemConfig2SpreadingFactor : byte
{
	_64ChipsPerSymbol = 0b01100000,
	_128ChipsPerSymbol = 0b01110000,
	_256ChipsPerSymbol = 0b10000000,
	_512ChipsPerSymbol = 0b10010000,
	_1024ChipsPerSymbol = 0b10100000,
	_2048ChipsPerSymbol = 0b10110000,
	_4096ChipsPerSymbol = 0b11000000,
}
private const RegModemConfig2SpreadingFactor RegModemConfig2SpreadingFactorDefault = RegModemConfig2SpreadingFactor._128ChipsPerSymbol;

The SX127X.NetCore only sets the spreadingFactor, symbolTimeout, txContinuousMode, or rxPayloadCrcOn registers if any of them is different from their defaults.

// Set regModemConfig2 if any of the settings not defaults
if ((spreadingFactor != RegModemConfig2SpreadingFactorDefault) || (txContinuousMode != false) | (rxPayloadCrcOn != false) || (symbolTimeout != SymbolTimeoutDefault))
{
	byte RegModemConfig2Value = (byte)spreadingFactor;
	if (txContinuousMode)
	{
		RegModemConfig2Value |= RegModemConfig2TxContinuousModeOn;
	}
	if (rxPayloadCrcOn)
	{
		RegModemConfig2Value |= RegModemConfig2RxPayloadCrcOn;
	}
	// Get the MSB of SymbolTimeout
	byte[] symbolTimeoutBytes = BitConverter.GetBytes(symbolTimeout);

	// Only the zeroth & second bit of byte matter
	symbolTimeoutBytes[1] &= SymbolTimeoutMsbMask;
	RegModemConfig2Value |= symbolTimeoutBytes[1];
	this.WriteByte((byte)Registers.RegModemConfig2, RegModemConfig2Value);
}

I modified the SX127X.NetCore SX127XLoRaDeviceClient to change the SpreadingFactor to _256ChipsPerSymbol (0b10000000) to match the Arduino client.

...
#if UPUTRONICS_RPIPLUS_CS0 && !UPUTRONICS_RPIPLUS_CS1
	SX127XDevice sX127XDevice = new SX127XDevice(SX127XDevice.ChipSelectLine.CS0, 25);
#endif
#if UPUTRONICS_RPIPLUS_CS1 && UPUTRONICS_RPIPLUS_CS0
	SX127XDevice sX127XDevice = new SX127XDevice(SX127XDevice.ChipSelectLine.CS1, 16);
#endif

	sX127XDevice.Initialise(
		SX127XDevice.RegOpModeMode.ReceiveContinuous,
		Frequency,
		paBoost: true,
#if LORA_SET_SYNCWORD
		syncWord: 0xF3,
		invertIQTX: true,
		rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SET_SPREAD
		spreadingFactor: SX127XDevice.RegModemConfig2SpreadingFactor._256ChipsPerSymbol,
		invertIQTX: true,
		rxDoneignoreIfCrcMissing: false
   );

#if DEBUG
	sX127XDevice.RegisterDump();
#endif
...

In the Visual Studio output window I could see that RegModemConfig2(0x1E) was set to 0x80.

Register dump
Register 0x01 - Value 0X85 - Bits 10000101
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0Xe4 - Bits 11100100
Register 0x07 - Value 0Xc0 - Bits 11000000
Register 0x08 - Value 0X00 - Bits 00000000
Register 0x09 - Value 0Xcf - Bits 11001111
Register 0x0a - Value 0X09 - Bits 00001001
Register 0x0b - Value 0X2b - Bits 00101011
Register 0x0c - Value 0X20 - Bits 00100000
Register 0x0d - Value 0X00 - Bits 00000000
Register 0x0e - Value 0X80 - Bits 10000000
Register 0x0f - Value 0X00 - Bits 00000000
Register 0x10 - Value 0X00 - Bits 00000000
Register 0x11 - Value 0X00 - Bits 00000000
Register 0x12 - Value 0X00 - Bits 00000000
Register 0x13 - Value 0X00 - Bits 00000000
Register 0x14 - Value 0X00 - Bits 00000000
Register 0x15 - Value 0X00 - Bits 00000000
Register 0x16 - Value 0X00 - Bits 00000000
Register 0x17 - Value 0X00 - Bits 00000000
Register 0x18 - Value 0X04 - Bits 00000100
Register 0x19 - Value 0X00 - Bits 00000000
Register 0x1a - Value 0X00 - Bits 00000000
Register 0x1b - Value 0X3d - Bits 00111101
Register 0x1c - Value 0X00 - Bits 00000000
Register 0x1d - Value 0X72 - Bits 01110010
Register 0x1e - Value 0X80 - Bits 10000000
Register 0x1f - Value 0X64 - Bits 01100100
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X08 - Bits 00001000
Register 0x22 - Value 0X01 - Bits 00000001
Register 0x23 - Value 0Xff - Bits 11111111
Register 0x24 - Value 0X00 - Bits 00000000
Register 0x25 - Value 0X00 - Bits 00000000
Register 0x26 - Value 0X04 - Bits 00000100
Register 0x27 - Value 0X00 - Bits 00000000
Register 0x28 - Value 0X00 - Bits 00000000
Register 0x29 - Value 0X00 - Bits 00000000
Register 0x2a - Value 0X00 - Bits 00000000
Register 0x2b - Value 0X00 - Bits 00000000
Register 0x2c - Value 0X0d - Bits 00001101
Register 0x2d - Value 0X50 - Bits 01010000
Register 0x2e - Value 0X14 - Bits 00010100
Register 0x2f - Value 0X45 - Bits 01000101
Register 0x30 - Value 0X55 - Bits 01010101
Register 0x31 - Value 0Xc3 - Bits 11000011
Register 0x32 - Value 0X05 - Bits 00000101
Register 0x33 - Value 0X37 - Bits 00110111
Register 0x34 - Value 0X1c - Bits 00011100
Register 0x35 - Value 0X0a - Bits 00001010
Register 0x36 - Value 0X03 - Bits 00000011
Register 0x37 - Value 0X0a - Bits 00001010
Register 0x38 - Value 0X42 - Bits 01000010
Register 0x39 - Value 0X12 - Bits 00010010
Register 0x3a - Value 0X49 - Bits 01001001
Register 0x3b - Value 0X19 - Bits 00011001
Register 0x3c - Value 0X00 - Bits 00000000
Register 0x3d - Value 0Xaf - Bits 10101111
Register 0x3e - Value 0X00 - Bits 00000000
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Text.Encoding.Extensions.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
10:04:22-RX PacketSnr 11.8 Packet RSSI -45dBm RSSI -109dBm 15 byte message "HeLoRa World! 2"
10:04:31-RX PacketSnr 11.5 Packet RSSI -45dBm RSSI -104dBm 15 byte message "HeLoRa World! 4"
10:04:32-TX To 0x48 From 0x65 Count 108 28 bytes message Hello LoRa from .NET Core! 1
10:04:32-TX Done
10:04:41-RX PacketSnr 12.0 Packet RSSI -45dBm RSSI -104dBm 15 byte message "HeLoRa World! 6"
10:04:42-TX To 0x48 From 0x65 Count 108 28 bytes message Hello LoRa from .NET Core! 2
10:04:42-TX Done

This matched the Arduino serial monitor output.

Summary

The LoRaSetSpread sample illustrates how the SX127X.NetCore library modifies register(s) only if a value specified in the Initialise method parameter list is different from the default.

.NET Core SX127X library Arduino LoRaSetSyncWord

“Playing nice” with Arduino-LoRa Samples

The arduino-LoRa library comes with a number of samples showing how to configure a SX127X device. The LoRaSetSyncWord sample sets the RegSyncWord register to 0x53

SX127X registers including RegSyncWord
...
void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);
  
  Serial.println("LoRa Duplex - Set sync word");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.setSyncWord(0xF3);           // ranges from 0-0xFF, default 0x34, see API docs
  Serial.println("LoRa init succeeded.");
}
...

I modified the SX127X.NetCore SX127XLoRaDeviceClient to change the SyncWord to 0x53 to match the Arduino client.

...
#if UPUTRONICS_RPIPLUS_CS0 && !UPUTRONICS_RPIPLUS_CS1
		SX127XDevice sX127XDevice = new SX127XDevice(SX127XDevice.ChipSelectLine.CS0, 25);
#endif
#if UPUTRONICS_RPIPLUS_CS1 && UPUTRONICS_RPIPLUS_CS0
		SX127XDevice sX127XDevice = new SX127XDevice(SX127XDevice.ChipSelectLine.CS1, 16);
#endif

		sX127XDevice.Initialise(
			SX127XDevice.RegOpModeMode.ReceiveContinuous,
			Frequency,
			paBoost: true,
#if LORA_SET_SYNCWORD
			syncWord:0x53,
			invertIQTX: true,
#endif			
			rxPayloadCrcOn:true, rxDoneignoreIfCrcMissing:false
			);

#if DEBUG
		sX127XDevice.RegisterDump();
#endif
...

The SX127X.NetCore only sets the RegSyncWord register if it is different from the 0x12 default

...
// RegSyncWordDefault 
if (syncWord != RegSyncWordDefault)
{
	this.WriteByte((byte)Registers.RegSyncWord, syncWord);
}
...

In the Visual Studio output window I could see that RegSyncWord(0x39) was set to 0x53.

...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Memory.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register dump
Register 0x01 - Value 0X85 - Bits 10000101
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0Xe4 - Bits 11100100
Register 0x07 - Value 0Xc0 - Bits 11000000
Register 0x08 - Value 0X00 - Bits 00000000
Register 0x09 - Value 0Xcf - Bits 11001111
Register 0x0a - Value 0X09 - Bits 00001001
Register 0x0b - Value 0X2b - Bits 00101011
Register 0x0c - Value 0X20 - Bits 00100000
Register 0x0d - Value 0X00 - Bits 00000000
Register 0x0e - Value 0X80 - Bits 10000000
Register 0x0f - Value 0X00 - Bits 00000000
Register 0x10 - Value 0X00 - Bits 00000000
Register 0x11 - Value 0X00 - Bits 00000000
Register 0x12 - Value 0X00 - Bits 00000000
Register 0x13 - Value 0X00 - Bits 00000000
Register 0x14 - Value 0X00 - Bits 00000000
Register 0x15 - Value 0X00 - Bits 00000000
Register 0x16 - Value 0X00 - Bits 00000000
Register 0x17 - Value 0X00 - Bits 00000000
Register 0x18 - Value 0X04 - Bits 00000100
Register 0x19 - Value 0X00 - Bits 00000000
Register 0x1a - Value 0X00 - Bits 00000000
Register 0x1b - Value 0X39 - Bits 00111001
Register 0x1c - Value 0X00 - Bits 00000000
Register 0x1d - Value 0X72 - Bits 01110010
Register 0x1e - Value 0X74 - Bits 01110100
Register 0x1f - Value 0X64 - Bits 01100100
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X08 - Bits 00001000
Register 0x22 - Value 0X01 - Bits 00000001
Register 0x23 - Value 0Xff - Bits 11111111
Register 0x24 - Value 0X00 - Bits 00000000
Register 0x25 - Value 0X00 - Bits 00000000
Register 0x26 - Value 0X04 - Bits 00000100
Register 0x27 - Value 0X00 - Bits 00000000
Register 0x28 - Value 0X00 - Bits 00000000
Register 0x29 - Value 0X00 - Bits 00000000
Register 0x2a - Value 0X00 - Bits 00000000
Register 0x2b - Value 0X00 - Bits 00000000
Register 0x2c - Value 0X1a - Bits 00011010
Register 0x2d - Value 0X50 - Bits 01010000
Register 0x2e - Value 0X14 - Bits 00010100
Register 0x2f - Value 0X45 - Bits 01000101
Register 0x30 - Value 0X55 - Bits 01010101
Register 0x31 - Value 0Xc3 - Bits 11000011
Register 0x32 - Value 0X05 - Bits 00000101
Register 0x33 - Value 0X37 - Bits 00110111
Register 0x34 - Value 0X1c - Bits 00011100
Register 0x35 - Value 0X0a - Bits 00001010
Register 0x36 - Value 0X03 - Bits 00000011
Register 0x37 - Value 0X0a - Bits 00001010
Register 0x38 - Value 0X42 - Bits 01000010
Register 0x39 - Value 0X53 - Bits 01010011
Register 0x3a - Value 0X49 - Bits 01001001
Register 0x3b - Value 0X19 - Bits 00011001
Register 0x3c - Value 0X00 - Bits 00000000
Register 0x3d - Value 0Xaf - Bits 10101111
Register 0x3e - Value 0X00 - Bits 00000000
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Text.Encoding.Extensions.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
18:22:42-RX PacketSnr 10.0 Packet RSSI -48dBm RSSI -104dBm 15 byte message "HeLoRa World! 0"
Sending 28 bytes message Hello LoRa from .NET Core! 1
18:22:42-TX Done
Sending 28 bytes message Hello LoRa from .NET Core! 2
18:22:52-TX Done
18:22:53-RX PacketSnr 9.8 Packet RSSI -46dBm RSSI -103dBm 15 byte message "HeLoRa World! 2"
Sending 28 bytes message Hello LoRa from .NET Core! 3
18:23:02-TX Done
18:23:04-RX PacketSnr 10.0 Packet RSSI -46dBm RSSI -104dBm 15 byte message "HeLoRa World! 4"
Sending 28 bytes message Hello LoRa from .NET Core! 4
18:23:12-TX Done
18:23:14-RX PacketSnr 9.5 Packet RSSI -46dBm RSSI -104dBm 15 byte message "HeLoRa World! 6"
Sending 28 bytes message Hello LoRa from .NET Core! 5
18:23:22-TX Done
18:23:26-RX PacketSnr 10.0 Packet RSSI -47dBm RSSI -105dBm 15 byte message "HeLoRa World! 8"
Sending 28 bytes message Hello LoRa from .NET Core! 6
18:23:32-TX Done
18:23:37-RX PacketSnr 10.0 Packet RSSI -48dBm RSSI -104dBm 16 byte message "HeLoRa World! 10"
Sending 28 bytes message Hello LoRa from .NET Core! 7
18:23:42-TX Done
18:23:48-RX PacketSnr 9.8 Packet RSSI -48dBm RSSI -104dBm 16 byte message "HeLoRa World! 12"
Sending 28 bytes message Hello LoRa from .NET Core! 8
18:23:52-TX Done
18:24:00-RX PacketSnr 12.5 Packet RSSI -48dBm RSSI -104dBm 16 byte message "HeLoRa World! 14"
Sending 28 bytes message Hello LoRa from .NET Core! 9
18:24:02-TX Done
18:24:11-RX PacketSnr 10.0 Packet RSSI -47dBm RSSI -104dBm 16 byte message "HeLoRa World! 16"
Sending 29 bytes message Hello LoRa from .NET Core! 10
18:24:12-TX Done
Sending 29 bytes message Hello LoRa from .NET Core! 11
18:24:22-TX Done
18:24:23-RX PacketSnr 9.3 Packet RSSI -49dBm RSSI -104dBm 16 byte message "HeLoRa World! 18"
Sending 29 bytes message Hello LoRa from .NET Core! 12
18:24:32-TX Done
18:24:34-RX PacketSnr 9.5 Packet RSSI -49dBm RSSI -104dBm 16 byte message "HeLoRa World! 20"
Sending 29 bytes message Hello LoRa from .NET Core! 13
18:24:42-TX Done
18:24:45-RX PacketSnr 9.3 Packet RSSI -49dBm RSSI -104dBm 16 byte message "HeLoRa World! 22"
Sending 29 bytes message Hello LoRa from .NET Core! 14
18:24:52-TX Done
The thread 0x74ab has exited with code 0 (0x0).
18:24:55-RX PacketSnr 9.5 Packet RSSI -49dBm RSSI -103dBm 16 byte message "HeLoRa World! 24"
The program 'dotnet' has exited with code 0 (0x0).

This matched the Arduino serial monitor output.

Arduino-Lora LoRaSetSyncWord serial monitor

Summary

Usually the SX127X.NetCore library modifies register(s) only if a value specified in the Initialise method parameter list is different from the default.

Some values span multiple registers e.g. frequency uses RegFrMsb, RegFrMid & RegFrLsb, and multiple options can be specified in a single register e.g. RegOpMode which complicates the code.

Note the SyncWord only a byte….

.NET Core SX127X library Part6

Enumerations, Constants and Masks

This was a huge (and tedious) task. I went thru the SX127X datasheet and created enumerations, masks, flags, defaults, and constants for most of the LoRa mode configuration options. The list of LoRa registers with a brief overview of each one starts on page 108 and finishes on page 115 of the datasheet.

SX127X data sheet page 108 Register 0x0 to 0x07

The registers enumeration is approximately fifty lines long, some of which I have ignored because I don’t need them or I can’t figure out what they do.

		// Registers from SemTech SX127X Datasheet
		enum Registers : byte
		{
			MinValue = RegOpMode,

			RegFifo = 0x0,
			RegOpMode = 0x01,
			//Reserved 0x02-0x06 
			RegFrMsb = 0x06,
			RegFrMid = 0x7,
			RegFrLsb = 0x08,
			RegPAConfig = 0x09,
			//RegPARamp = 0x0A, // not included as FSK/OOK functionality
			RegOcp = 0x0B,
			RegLna = 0x0C,
			RegFifoAddrPtr = 0x0D,
			RegFifoTxBaseAddr = 0x0E,
			RegFifoRxCurrent = 0x10,
			RegIrqFlagsMask = 0x11,
			RegIrqFlags = 0x12,
			RegRxNbBytes = 0x13,
			// RegRxHeaderCntValueMsb=0x14
			// RegRxHeaderCntValueLsb=0x15
			// RegRxPacketCntValueMsb=0x16
			// RegRxPacketCntValueMsb=0x17
			// RegModemStat=0x18
			RegPktSnrValue=0x19,
			RegPktRssiValue=0x1A,
			RegRssiValue=0x1B,
			RegHopChannel=0x1C,
			RegModemConfig1 = 0x1D,
			RegModemConfig2 = 0x1E,
			RegSymbTimeout = 0x1F,
			RegPreambleMsb = 0x20,
			RegPreambleLsb = 0x21,
			RegPayloadLength = 0x22,
			RegMaxPayloadLength = 0x23,
			RegHopPeriod = 0x24,
			// RegFifiRxByteAddr = 0x25
			RegModemConfig3 = 0x26,
			RegPpmCorrection = 0x27,
			// RegFeiMsb = 0x28
			// RegFeiMid = 0x29
			// RegFeiLsb = 0x2A
			// Reserved 0x2B
			// RegRssiWideband = 0x2C
			// Reserved 0x2D-0x30
			RegDetectOptimize = 0x31,
			// Reserved 0x32
			RegInvertIQ = 0x33,
			// Reserved 0x34-0x36
			RegDetectionThreshold = 0x37,
			// Reserved 0x38
			RegSyncWord = 0x39,
			RegInvertIQ2 = 0x3B,
			RegDioMapping1 = 0x40,
			RegVersion = 0x42,

			MaxValue = RegVersion,
		}

For each register I worked out from the documentation what it was used for, did I need to implement it and if so how. For example RegOpMode controls the operating mode of the module and has a state machine as well

		// RegOpMode mode flags
		private const byte RegOpModeLongRangeModeLoRa = 0b10000000;
		private const byte RegOpModeLongRangeModeFskOok = 0b00000000;
		private const byte RegOpModeLongRangeModeDefault = RegOpModeLongRangeModeFskOok;

		private const byte RegOpModeAcessSharedRegLoRa = 0b00000000;
		private const byte RegOpModeAcessSharedRegFsk = 0b01000000;
		private const byte RegOpModeAcessSharedRegDefault = RegOpModeAcessSharedRegLoRa;

		private const byte RegOpModeLowFrequencyModeOnHighFrequency = 0b00000000;
		private const byte RegOpModeLowFrequencyModeOnLowFrequency = 0b00001000;
		private const byte RegOpModeLowFrequencyModeOnDefault = RegOpModeLowFrequencyModeOnLowFrequency;

		[Flags]
		public enum RegOpModeMode : byte
		{
			Sleep = 0b00000000,
			StandBy = 0b00000001,
			FrequencySynthesisTX = 0b00000010,
			Transmit = 0b00000011,
			FrequencySynthesisRX = 0b00000100,
			ReceiveContinuous = 0b00000101,
			ReceiveSingle = 0b00000110,
			ChannelActivityDetection = 0b00000111,
		};

		// Frequency configuration magic numbers from Semtech SX127X specs
		private const double SX127X_FXOSC = 32000000.0;
		private const double SX127X_FSTEP = SX127X_FXOSC / 524288.0;
		private const double SX127XMidBandThreshold = 525000000.0; // Search for RF_MID_BAND_THRESH GitHub LoRaNet LoRaMac-node/src/boards/sx1276-board.h
		private const int RssiAdjustmentHF = -157;
		private const int RssiAdjustmentLF = -164;

		// RegFrMsb, RegFrMid, RegFrLsb
		private const double FrequencyDefault = 434000000.0;

Some of the documentation is incredibly detailed but has little impact on my use case. (If someone needs this sort of functionality I will add it)

Some operations have state machines which add even more implementation complexity

State transition diagram for transmitting messages in LoRa mode

In my library I reset the SX127X then configure any “non-default” settings as the application starts. My applications ten to change only a limited number of registers once they are running. For any register(s) that can be changed while the application is running I have a “shadow” variable for each of them so I don’t have to read the register before writing it.

public void Initialise(RegOpModeMode modeAfterInitialise, // RegOpMode
	double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
	bool rxDoneignoreIfCrcMissing = true, bool rxDoneignoreIfCrcInvalid = true,
	bool paBoost = false, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
	bool ocpOn = RegOcpDefault, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
	RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoost = LnaBoostDefault, // RegLna
	RegModemConfigBandwidth bandwidth = RegModemConfigBandwidthDefault, RegModemConfigCodingRate codingRate = RegModemConfigCodingRateDefault, RegModemConfigImplicitHeaderModeOn implicitHeaderModeOn = RegModemConfigImplicitHeaderModeOnDefault, //RegModemConfig1
	RegModemConfig2SpreadingFactor spreadingFactor = RegModemConfig2SpreadingFactorDefault, bool txContinuousMode = false, bool rxPayloadCrcOn = false,
	ushort symbolTimeout = SymbolTimeoutDefault,
	ushort preambleLength = PreambleLengthDefault,
	byte payloadLength = PayloadLengthDefault,
	byte payloadMaxLength = PayloadMaxLengthDefault,
	byte freqHoppingPeriod = FreqHoppingPeriodDefault,
	bool lowDataRateOptimize = LowDataRateOptimizeDefault, bool agcAutoOn = AgcAutoOnDefault,
	byte ppmCorrection = ppmCorrectionDefault,
	RegDetectOptimizeDectionOptimize detectionOptimize = RegDetectOptimizeDectionOptimizeDefault,
	bool invertIQRX = InvertIqRXDefault, bool invertIQTX = InvertIqTXDefault,
	RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
	byte syncWord = RegSyncWordDefault)
{
	RegOpModeModeCurrent = modeAfterInitialise; // TODO 

	Frequency = frequency; // Store this away for RSSI adjustments
	RxDoneIgnoreIfCrcMissing = rxDoneignoreIfCrcMissing;
	RxDoneIgnoreIfCrcInvalid = rxDoneignoreIfCrcInvalid;
	InvertIQRX = invertIQRX;
	InvertIQTX = invertIQTX;

	// Strobe Reset pin briefly to factory reset SX127X chip
	if (ResetLogicalPinNumber != 0)
	{
		gpioController.Write(ResetLogicalPinNumber, PinValue.Low);
		Thread.Sleep(20);
		gpioController.Write(ResetLogicalPinNumber, PinValue.High);
		Thread.Sleep(20);
	}

	// Put the device into sleep mode so registers can be changed
	SetMode(RegOpModeMode.Sleep);

	// Configure RF Carrier frequency 
	if (frequency != FrequencyDefault)
	{
		byte[] bytes = BitConverter.GetBytes((long)(frequency / SX127X_FSTEP));
		this.WriteByte((byte)Registers.RegFrMsb, bytes[2]);
		this.WriteByte((byte)Registers.RegFrMid, bytes[1]);
		this.WriteByte((byte)Registers.RegFrLsb, bytes[0]);
	}
....
}

Summary

There are a huge number of configuration options for an SX127X device so my library exposes the ones required for common use cases. If a scenario is not supported the ReadByte, ReadBytes, ReadWordMsbLsb, WriteByte, WriteBytes, WriteWordMsbLsb and RegisterDump methods are available. But, beware the SX127X is complex to configure and operate.