.NET nanoFramework SX127X LoRa library RegLna LnaGain

Every so often I print my code out (landscape for notes in margin, double sided to save paper, and colour so it looks like Visual Studio 2022) and within 100 lines noticed the first of no doubt many issues. The SX127X RegLNA enumeration was wrong.

// RegLna
[Flags]
public enum RegLnaLnaGain : byte
{
	G1 = 0b00000001,
	G2 = 0b00000010,
	G3 = 0b00000011,
	G4 = 0b00000100,
	G5 = 0b00000101,
	G6 = 0b00000110
}
SX127X RegLna options

The LnaGain value is bits 5-7 rather than rather than bits 0-2 which could be a problem if the specified lnaGain and lnaBoost values are not the default values.

// Set RegLna if any of the settings not defaults
if ((lnaGain != Configuration.LnaGainDefault) || (lnaBoost != Configuration.LnaBoostDefault))
{
	byte regLnaValue = (byte)lnaGain;

	regLnaValue |= Configuration.RegLnaLnaBoostLfDefault;
	regLnaValue |= Configuration.RegLnaLnaBoostHfDefault;

	if (lnaBoost)
	{
		if (_frequency > Configuration.SX127XMidBandThreshold)
		{
			regLnaValue |= Configuration.RegLnaLnaBoostHfOn;
		}
		else
		{
			regLnaValue |= Configuration.RegLnaLnaBoostLfOn;
		}
	}
	_registerManager.WriteByte((byte)Configuration.Registers.RegLna, regLnaValue);
}

The default lnaGain is G1 and the default lnaBoost is false so if the gain was set to G3(011) then LnaBoostHf current would be 150% and LnaGain would be 000 which is a reserved value.

// RegLna
[Flags]
public enum RegLnaLnaGain : byte
{
	G1 = 0b00100000,
	G2 = 0b01000000,
	G3 = 0b01100000,
	G4 = 0b10000000,
	G5 = 0b10100000,
	G6 = 0b11000000
}

I need to check my usage of Configuration.SX127XMidBandThreshold for LnaBoostLf vs. LnaBoostHf is correct.(arduino-LoRa)

.NET nanoFramework SX127X LoRa library “it’s all about timing”

Every so often my nanoFramework SX127X library RangeTester application wouldn’t start. When I poked around with the Visual Studio 2022 debugger the issue went away(a “Heisenbug” in the wild) which made figuring out what was going on impossible.

One afternoon the issue occurred several times in a row, the application wouldn’t startup because the SX127X device detection failed and message transmission was also not being confirmed.(TX Done).

Visual Studio output windows with SX127X detection failure
Visual Studio output windows with no Transmit confirmations
public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin, int resetPin)
{
	_gpioController = gpioController;

	// Factory reset pin configuration
	_resetPin = resetPin;
	_gpioController.OpenPin(resetPin, PinMode.Output);

	_gpioController.Write(resetPin, PinValue.Low);
	Thread.Sleep(20);
	_gpioController.Write(resetPin, PinValue.High);
	Thread.Sleep(100);

	_registerManager = new RegisterManager(spiDevice, RegisterAddressReadMask, RegisterAddressWriteMask);

	// Once the pins setup check that SX127X chip is present
	Byte regVersionValue = _registerManager.ReadByte((byte)Configuration.Registers.RegVersion);
	if (regVersionValue != Configuration.RegVersionValueExpected)
	{
		throw new ApplicationException("Semtech SX127X not found");
	}

	// Interrupt pin for RX message & TX done notification 
	_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

	_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

I could single step through the code and inspect variables with the debugger and it looks like a timing issue with order of the strobing of the reset pin and the initialisation of the RegisterManager. I’ll spend and hour starting and stopping the application, then smoke test the code for 24 hours with a couple of other devices generating traffic just to check.

.NET nanoFramework SX127X LoRa library playing nice with others

So nanoFramework applications using my SX127X library.NetNF can access other General Purpose Input Output(GPIO) ports and Serial Peripheral Interface(SPI) devices I have added SpiDevice and GpioController parameters to the two constructors.

// Hardware configuration support
private readonly int ResetPin;
private readonly GpioController _gpioController = null;
private readonly SpiDevice _sx127xTransceiver = null;
private readonly Object SX127XRegFifoLock = new object();
private double Frequency = FrequencyDefault;
private bool RxDoneIgnoreIfCrcMissing = true;
private bool RxDoneIgnoreIfCrcInvalid = true;

public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin, int resetPin)
{
	_sx127xTransceiver = spiDevice;

	_gpioController = gpioController;

	// As soon as ChipSelectLine/ChipSelectLogicalPinNumber check that SX127X chip is present
	Byte regVersionValue = this.ReadByte((byte)Registers.RegVersion);
	if (regVersionValue != RegVersionValueExpected)
	{
		throw new ApplicationException("Semtech SX127X not found");
	}

	// Factory reset pin configuration
	ResetPin = resetPin;
	_gpioController.OpenPin(resetPin, PinMode.Output);

	_gpioController.Write(resetPin, PinValue.Low);
	Thread.Sleep(20);
	_gpioController.Write(resetPin, PinValue.High);
	Thread.Sleep(20);

	// Interrupt pin for RX message & TX done notification 
	_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

	_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin)
{
	_sx127xTransceiver = spiDevice;

	_gpioController = gpioController;

	// As soon as ChipSelectLine/ChipSelectLogicalPinNumber check that SX127X chip is present
	Byte regVersionValue = this.ReadByte((byte)Registers.RegVersion);
	if (regVersionValue != RegVersionValueExpected)
	{
		throw new ApplicationException("Semtech SX127X not found");
	}

	// Interrupt pin for RX message & TX done notification 
	_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

	_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

I then “over refactored”(broke) the constructor without the resetPin by removing the GpioController parameter which is necessary for the RegisterCallbackForPinValueChangedEvent.

.NET nanoFramework SX127X LoRa library on Github

The source code of my nanoFramework SX127X library is now available on GitHub. I have tested the library and sample applications on Netduino 3Wifi, Sparkfun LoRa Gateway 1 Channel ESP32 for LoRaWAN and ST Micro STM32F7691 Discovery devices.(I can add more platform configurations if there is interest).

STM32F769I Discovery, Netduino 3 Wifi and Sparkfun testrig

I started with a proof of concept update of my RFM9X for nanoFramework library to the new nanoFramework System.Device model (“inspired” by .Net Core System.Device) which was slow going. I then tried “back porting” my SX127X for .Net Core library to the .NET nanoFramework which was much quicker.

namespace devMobile.IoT.SX127xLoRaDevice
{
	using System;
	using System.Text;
	using System.Threading;

	class Program
	{
		private const double Frequency = 915000000.0;
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const int SpiBusId = 1;
#endif
#if NETDUINO3_WIFI
		private const int SpiBusId = 2;
#endif
#if ST_STM32F769I_DISCOVERY
		private const int SpiBusId = 2;
#endif
		private static SX127XDevice sx127XDevice;

		static void Main(string[] args)
		{
			int SendCount = 0;
#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
			int chipSelectLine = Gpio.IO16;
			int interruptPinNumber = Gpio.IO26;
#endif
#if NETDUINO3_WIFI
			// Arduino D10->PB10
			int chipSelectLine = PinNumber('B', 10);
			// Arduino D9->PE5
			int resetPinNumber = PinNumber('E', 5);
			// Arduino D2 -PA3
			int interruptPinNumber = PinNumber('A', 3);
#endif
#if ST_STM32F769I_DISCOVERY
			// Arduino D10->PA11
			int chipSelectLine = PinNumber('A', 11);
			// Arduino D9->PH6
			int resetPinNumber = PinNumber('H', 6);
			// Arduino D2->PA4
			int interruptPinNumber = PinNumber('J', 1);
#endif
			Console.WriteLine("devMobile.IoT.SX127xLoRaDevice Client starting");

			try
			{
#if ESP32_WROOM_32_LORA_1_CHANNEL
				Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
				Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
				Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);

				sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber);
#endif
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
				sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber, resetPinNumber);
#endif

				sx127XDevice.Initialise(SX127XDevice.RegOpModeMode.ReceiveContinuous,
							Frequency,
							lnaGain: SX127XDevice.RegLnaLnaGain.G3,
							lnaBoost:true, 
							powerAmplifier: SX127XDevice.PowerAmplifier.PABoost,
							rxPayloadCrcOn: true,
							rxDoneignoreIfCrcMissing: false
							);

#if DEBUG
				sx127XDevice.RegisterDump();
#endif

				sx127XDevice.OnReceive += SX127XDevice_OnReceive;
				sx127XDevice.Receive();
				sx127XDevice.OnTransmit += SX127XDevice_OnTransmit;

				Thread.Sleep(500);

				while (true)
				{
					string messageText = $"Hello LoRa from .NET nanoFramework {SendCount += 1}!";

					byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
					//Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX {messageBytes.Length} byte message {messageText}");
					//sx127XDevice.Send(messageBytes);

					Thread.Sleep(50000);
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		private static void SX127XDevice_OnReceive(object sender, SX127XDevice.OnDataReceivedEventArgs e)
		{
			try
			{
				// Remove unprintable characters from messages
				for (int index = 0; index < e.Data.Length; index++)
				{
					if ((e.Data[index] < 0x20) || (e.Data[index] > 0x7E))
					{
						e.Data[index] = 0x7C;
					}
				}

				string messageText = UTF8Encoding.UTF8.GetString(e.Data, 0, e.Data.Length);

				Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX PacketSnr {e.PacketSnr:0.0} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message {messageText}");
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		private static void SX127XDevice_OnTransmit(object sender, SX127XDevice.OnDataTransmitedEventArgs e)
		{
			sx127XDevice.SetMode(SX127XDevice.RegOpModeMode.ReceiveContinuous);

			Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX Done");
		}

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
		static int PinNumber(char port, byte pin)
		{
			if (port < 'A' || port > 'J')
				throw new ArgumentException();

			return ((port - 'A') * 16) + pin;
		}
#endif
	}
}

The sample application shows how to configure the library for different devices (SPI port, interrupt pin and optional reset pin) then send/receive payloads. The library is intended to be initialised then run for long periods of time (I’m looking at a month long soak test next) rather than changing configuration while running. The initialise method has many parameters which have “reasonable” default values. (Posts coming about optimising power consumption and range).

I’m looking at extending the library with optional functionality like tamper detection via signing and privacy via payload encryption, and mesh network support.

.NET nanoFramework SX127X LoRa library with Interrupts

To test the nanoFramework transmit and receive with interrupts implementation I used three Dragino LoRa Shields, a Seeeduino V4.2 and a pair of Netduino 3 Wifi devices.

Seeeduino and nanoFramework

I started with transmit as I was confident my Netduino 3 Wifi & Seeeduino + Dragino LoRa Shields could receive messages.

Interrupt pin configuration
SX127X ReqIrqFlags options

The TransmitInterrupt application loads the message to be sent into the First In First Out(FIFO) buffer, RegDioMapping1 is set to interrupt onTxDone(PacketSent-00), then RegRegOpMode-Mode is set to Transmit. When the message has been sent InterruptGpioPin_ValueChanged is called, and the TxDone(0b00001000) flag is set in the RegIrqFlags register.

The ReceiveInterrupt application sets the RegDioMapping1 to interrupt on RxDone(PacketReady-00), then the RegRegOpMode-Mode is set to Receive(TX-101). When a message is received InterruptGpioPin_ValueChanged is called, with the RxDone(0b00001000) flag set in the RegIrqFlags register, and then the message is read from First In First Out(FIFO) buffer.

namespace devMobile.IoT.SX127x.ReceiveTransmitInterrupt
{
...
   public sealed class SX127XDevice
   {
...
      public SX127XDevice(int busId, int chipSelectLine, int interruptPin, int resetPin)
      {
         var settings = new SpiConnectionSettings(busId, chipSelectLine)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared
         };

         SX127XTransceiver = new SpiDevice(settings);

         GpioController gpioController = new GpioController();


         // Factory reset pin configuration
         gpioController.OpenPin(resetPin, PinMode.Output);

         gpioController.Write(resetPin, PinValue.Low);
         Thread.Sleep(20);
         gpioController.Write(resetPin, PinValue.High);
         Thread.Sleep(20);

         // Interrupt pin for RX message & TX done notification 
         gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

         gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
      }
...
   }

      private void InterruptGpioPin_ValueChanged(object sender, PinValueChangedEventArgs e)
      {
         byte irqFlags = this.ReadByte(0x12); // RegIrqFlags
         Debug.WriteLine($"RegIrqFlags 0X{irqFlags:x2}");

         if ((irqFlags & 0b01000000) == 0b01000000)  // RxDone 
         {
            Debug.WriteLine("Receive-Message");
            byte currentFifoAddress = this.ReadByte(0x10); // RegFifiRxCurrent
            this.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

            byte numberOfBytes = this.ReadByte(0x13); // RegRxNbBytes

            // Allocate buffer for message
            byte[] messageBytes = this.ReadBytes(0X0, numberOfBytes);

            // Remove unprintable characters from messages
            for (int index = 0; index < messageBytes.Length; index++)
            {
               if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
               {
                  messageBytes[index] = 0x20;
               }
            }

            string messageText = UTF8Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);
            Debug.WriteLine($"Received {messageBytes.Length} byte message {messageText}");
         }

         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            this.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
            Debug.WriteLine("Transmit-Done");
         }

         this.WriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
         this.WriteByte(0x12, 0xff);// RegIrqFlags
      }

   public class Program
   {
...
   #if NETDUINO3_WIFI
      private const int SpiBusId = 2;
#endif
...

      public static void Main()
      {
         int SendCount = 0;
...
#if NETDUINO3_WIFI
         // Arduino D10->PB10
         int chipSelectLine = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
         // Arduino D2 -PA3
         int interruptPinNumber = PinNumber('A', 3);
#endif
...
  
       Debug.WriteLine("devMobile.IoT.SX127x.ReceiveTransmitInterrupt starting");

         try
         {
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
            SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber, resetPinNumber);
#endif
            Thread.Sleep(500);

            // Put device into LoRa + Sleep mode
            sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

            // Set the frequency to 915MHz
            byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
            sx127XDevice.WriteBytes(0x06, frequencyWriteBytes);

            // More power PA Boost
            sx127XDevice.WriteByte(0x09, 0b10000000); // RegPaConfig

            sx127XDevice.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

            while (true)
            {
               // Set the Register Fifo address pointer
               sx127XDevice.WriteByte(0x0E, 0x00); // RegFifoTxBaseAddress 

               // Set the Register Fifo address pointer
               sx127XDevice.WriteByte(0x0D, 0x0); // RegFifoAddrPtr 

               string messageText = $"Hello LoRa {SendCount += 1}!";

               // load the message into the fifo
               byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
               sx127XDevice.WriteBytes(0x0, messageBytes); // RegFifo 

               // Set the length of the message in the fifo
               sx127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
               sx127XDevice.WriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
               sx127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode 

               Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");

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

The ReceiveTransmitInterrupt application combines the functionality TransmitInterrupt and ReceiveInterrupt programs. The key differences are the RegDioMapping1 setup and in InterruptGpioPin_ValueChanged where the TxDone & RxDone flags in the RegIrqFlags register specify how the interrupt is handled.

.NET nanoFramework SX127X LoRa library Basic Receive & Transmit

For testing nanoFramework device transmit and receive functionality I used an Arduino/Seeeduino with a Dragino LoRa Shield (running one of the Arduino-LoRa samples) as a client device. This was so I could “bootstrap” connectivity and test interoperability with other libraries/platforms.

Arduino/Netduino devices for .NET nanoFramework interoperability test-rig

I started with transmit as I was confident my Seeeduino + Dragino LoRa Shield could receive messages. The TransmitBasic application puts the device into LoRa + Sleep mode as after reset/powering up the device is in FSK/OOK, Low Frequency + Standby mode).

SX127X RegOpMode options

After loading the message to be sent into the First In First Out(FIFO) buffer, the RegOpMode-Mode is set to Transmit(TX-011), and then the RegIrqFlags register is polled until the TxDone flag is set.

SX127X ReqIrqFlags options
public static void Main()
{
  int SendCount = 0;
...
  Debug.WriteLine("devMobile.IoT.SX127x.TransmitBasic starting");

   try
   {
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
     Thread.Sleep(500);

     // Put device into LoRa + Standby mode
     sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

      // Set the frequency to 915MHz
      byte[] frequencyBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
      sx127XDevice.WriteBytes(0x06, frequencyBytes);

      // More power PA Boost
      sx127XDevice.WriteByte(0x09, 0b10000000); // RegPaConfig

      sx127XDevice.RegisterDump();

      while (true)
      {
         sx127XDevice.WriteByte(0x0E, 0x0); // RegFifoTxBaseAddress 

         // Set the Register Fifo address pointer
         sx127XDevice.WriteByte(0x0D, 0x0); // RegFifoAddrPtr 

         string messageText = $"Hello LoRa from .NET nanoFramework {SendCount += 1}!";

         // load the message into the fifo
         byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
         sx127XDevice.WriteBytes(0x0, messageBytes); // RegFifo

         // Set the length of the message in the fifo
         sx127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength

         Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");
         // Set the mode to LoRa + Transmit
         sx127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode 

         // Wait until send done, no timeouts in PoC
         Debug.WriteLine("Send-wait");
         byte irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
         while ((irqFlags & 0b00001000) == 0)  // wait until TxDone cleared
         {
            Thread.Sleep(10);
            irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
            Debug.Write(".");
         }
         Debug.WriteLine("");
         sx127XDevice.WriteByte(0x12, 0b00001000); // clear TxDone bit
         Debug.WriteLine("Send-Done");

         Thread.Sleep(30000);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}
Transmit Basic application output

Once the TransmitBasic application was sending messages reliably I started working on the ReceiveBasic application. As the ReceiveBasic application starts up the SX127X RegOpMode has to be set to sleep/standby so the device can be configured. TOnce that is completed RegOpMode-Mode is set to RxContinuous(101), and the RegIrqFlags register is polled until the RxDone flag is set.

public static void Main()
{
...
   Debug.WriteLine("devMobile.IoT.SX127x.ReceiveBasic starting");

   try
   {
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
      Thread.Sleep(500);

      // Put device into LoRa + Sleep mode
      sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

      // Set the frequency to 915MHz
      byte[] frequencyBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
      sx127XDevice.WriteBytes(0x06, frequencyBytes);

      sx127XDevice.WriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

      sx127XDevice.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

       while (true)
       {
          // Wait until a packet is received, no timeouts in PoC
         Debug.WriteLine("Receive-Wait");
         byte irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
         while ((irqFlags & 0b01000000) == 0)  // wait until RxDone cleared
         {
            Thread.Sleep(100);
            irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
            Debug.Write(".");
         }
         Debug.WriteLine("");
         Debug.WriteLine($"RegIrqFlags 0X{irqFlags:X2}");
         Debug.WriteLine("Receive-Message");
         byte currentFifoAddress = sx127XDevice.ReadByte(0x10); // RegFifiRxCurrent
         sx127XDevice.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

         byte numberOfBytes = sx127XDevice.ReadByte(0x13); // RegRxNbBytes

         // Read the message from the FIFO
         byte[] messageBytes = sx127XDevice.ReadBytes(0x00, numberOfBytes);

         sx127XDevice.WriteByte(0x0d, 0);
         sx127XDevice.WriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits

         // Remove unprintable characters from messages
         for (int index = 0; index < messageBytes.Length; index++)
         {
            if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
            {
               messageBytes[index] = 0x20;
            }
         }

         string messageText = UTF8Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);
         Debug.WriteLine($"Received {messageBytes.Length} byte message {messageText}");

         Debug.WriteLine("Receive-Done");
      }
    }
   catch (Exception ex)
   {
      Debug.WriteLine(ex.Message);
   }
}
Receive Basic application output

Every so often the ReceiveBasic application would display a message sent on the same frequency by a device somewhere nearby.

ReceiveBasic application messages from unknown source

I need to do some more investigation into whether writing 0b00001000 (Transmit) vs. 0b11111111(Receive) to RegIrqFlags is important.

.NET nanoFramework SX127X LoRa library Read & Write

Now that I could reliably dump all the Dragino shield registers I wanted to be able to configure the Semtech 127X device and reset it back to factory settings. A factory reset is done by strobing the SX127X reset pin.

SX127X Reset timing diagram

SX127X Reset process

To support this I added a constructor with an additional parameter for the reset General Purpose Input Output(GPIO) pin number to the SX127XDevice class. The original constructor was retained as the SX127X reset pin is not connected on the SparkFun LoRa Gateway-1-Channel (ESP32) and a limited number of other devices.

namespace devMobile.IoT.SX127x.RegisterReadAndWrite
{
   using System;
   using System.Diagnostics;
   using System.Threading;

   using System.Device.Gpio;
   using System.Device.Spi;

#if ESP32_WROOM_32_LORA_1_CHANNEL
   using nanoFramework.Hardware.Esp32;
#endif

   public sealed class SX127XDevice
   {
      private const byte RegisterAddressMinimum = 0X0;
      private const byte RegisterAddressMaximum = 0x42;
      private const byte RegisterAddressReadMask = 0X7f;
      private const byte RegisterAddressWriteMask = 0x80;

      private readonly SpiDevice SX127XTransceiver;

      public SX127XDevice(int busId, int chipSelectLine, int resetPin)
      {
         var settings = new SpiConnectionSettings(busId, chipSelectLine)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared
         };

         SX127XTransceiver = new SpiDevice(settings);

         // Factory reset pin configuration
         GpioController gpioController = new GpioController();
         gpioController.OpenPin(resetPin, PinMode.Output);

         gpioController.Write(resetPin, PinValue.Low);
         Thread.Sleep(20);
         gpioController.Write(resetPin, PinValue.High);
         Thread.Sleep(20);
      }

      public SX127XDevice(int busId, int chipSelectLine)
      {
         var settings = new SpiConnectionSettings(busId, chipSelectLine)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared,
         };

         SX127XTransceiver = new SpiDevice(settings);
      }

      public Byte ReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress &= RegisterAddressReadMask, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

         return readBuffer[1];
      }

      public ushort ReadWord(byte address)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

         return (ushort)(readBuffer[2] + (readBuffer[1] << 8));
      }

      public ushort ReadWordMsbLsb(byte address)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

         return (ushort)((readBuffer[1] << 8) + readBuffer[2]);
      }

      public byte[] ReadBytes(byte address, byte length)
      {
         byte[] writeBuffer = new byte[length + 1];
         byte[] readBuffer = new byte[writeBuffer.Length];
         byte[] replyBuffer = new byte[length];

         writeBuffer[0] = address &= RegisterAddressReadMask;

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

         Array.Copy(readBuffer, 1, replyBuffer, 0, length);

         return replyBuffer;
      }

      public void WriteByte(byte address, byte value)
      {
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void WriteWord(byte address, ushort value)
      {
         byte[] valueBytes = BitConverter.GetBytes(value);
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void WriteWordMsbLsb(byte address, ushort value)
      {
         byte[] valueBytes = BitConverter.GetBytes(value);
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[1], valueBytes[0] };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void WriteBytes(byte address, byte[] bytes)
      {
         byte[] writeBuffer = new byte[1 + bytes.Length];
         byte[] readBuffer = new byte[writeBuffer.Length];

         Array.Copy(bytes, 0, writeBuffer, 1, bytes.Length);
         writeBuffer[0] = address |= RegisterAddressWriteMask;

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void RegisterDump()
      {
         Debug.WriteLine("Register dump");
         for (byte registerIndex = RegisterAddressMinimum; registerIndex <= RegisterAddressMaximum; registerIndex++)
         {
            byte registerValue = this.ReadByte(registerIndex);

            Debug.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
         }

         Debug.WriteLine("");
      }
   }

   public class Program
   {
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const int SpiBusId = 1;
#endif
#if NETDUINO3_WIFI
      private const int SpiBusId = 2;
#endif
#if ST_STM32F769I_DISCOVERY
      private const int SpiBusId = 2;
#endif


      public static void Main()
      {
         byte[] frequencyBytes;
#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
         int chipSelectLine = Gpio.IO16;
#endif
#if NETDUINO3_WIFI
         // Arduino D10->PB10
         int chipSelectLine = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
#endif
#if ST_STM32F769I_DISCOVERY
         // Arduino D10->PA11
         int chipSelectLine = PinNumber('A', 11);
         // Arduino D9->PH6
         int resetPinNumber = PinNumber('H', 6);
#endif

         Debug.WriteLine("devMobile.IoT.SX127x.RegisterReadAndWrite starting");

         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);

            SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine);
#endif
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
            SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
            Thread.Sleep(500);

            sx127XDevice.RegisterDump();

            while (true)
            {
               Debug.WriteLine("Read RegOpMode (read byte)");
               Byte regOpMode1 = sx127XDevice.ReadByte(0x1);
               Debug.WriteLine($"RegOpMode 0x{regOpMode1:x2}");

               Debug.WriteLine("Set LoRa mode and sleep mode (write byte)");
               sx127XDevice.WriteByte(0x01, 0b10000000);

               Debug.WriteLine("Read RegOpMode (read byte)");
               Byte regOpMode2 = sx127XDevice.ReadByte(0x1);
               Debug.WriteLine($"RegOpMode 0x{regOpMode2:x2}");

               Debug.WriteLine("Read the preamble (read word)");
               ushort preamble = sx127XDevice.ReadWord(0x20);
               Debug.WriteLine($"Preamble 0x{preamble:x2}");

	            Console.WriteLine("Read the preamble (read word)"); // Should be 0x08
			      preamble = sx127XDevice.ReadWordMsbLsb(0x20);
               Debug.WriteLine($"Preamble 0x{preamble:x2}");

               Debug.WriteLine("Read the centre frequency (read byte array)");
               frequencyBytes = sx127XDevice.ReadBytes(0x06, 3);
               Debug.WriteLine($"Frequency Msb 0x{frequencyBytes[0]:x2} Mid 0x{frequencyBytes[1]:x2} Lsb 0x{frequencyBytes[2]:x2}");

               Debug.WriteLine("Set the centre frequency to 915MHz (write byte array)");
               byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 };
               sx127XDevice.WriteBytes(0x06, frequencyWriteBytes);

               Debug.WriteLine("Read the centre frequency (read byte array)");
               frequencyBytes = sx127XDevice.ReadBytes(0x06, 3);
               Debug.WriteLine($"Frequency Msb 0x{frequencyBytes[0]:x2} Mid 0x{frequencyBytes[1]:x2} Lsb 0x{frequencyBytes[2]:x2}");

               sx127XDevice.RegisterDump();

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

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}

The PinNumber helper is more user friendly that the raw numbers and is “inspired” by sample .NET nanoFramework General Purpose Input Output(GPIO) sample code.

Each method was tested by read/writing suitable register(s) in the device configuration (Needed to set it into LoRa mode first).

The next step is to extract the Serial Peripheral Interface(SPI) register access functionality into a module and configure the bare minimum of settings required to get the SX127X to receive and transmit messages.

.NET nanoFramework SX127X LoRa library Registers

Over the years I have ported my HopeRF RFM9X(Now a generic Semtech SX127X ) Windows 10 IoT Core (May 2018) library to .NET microFramework(May 2018), Wilderness Labs Meadow(Jan 2020), GHI Electronics TinyCLR-OS(July 2020), .NET nanoFramework V1(May 2020) and .NET Core(Aug 2021).

All this madness started because I wasn’t confident the frequency calculation of the Emmellsoft Dragino.Lora code was correct. Over the last couple of years I have also found bugs in my Transmit Power, InvertIQ RX/TX with many others yet to be discovered.

For my updated .NET nanoFramework port I have mainly used a half a dozen Dragino LoRa shields for Arduino and Netduino 3 Wifi devices I had lying around. I have also tested the code with SparkFun LoRa Gateway-1-Channel (ESP32) and ST 32F769IDiscovery devices.

STM32F769IDiscovery, Netduino 3 Wifi, and SparkFun LoRa Gateway-1-Channel (ESP32) devices

The Dragino shield uses D10 for chip select, D2 for RFM9X DI0 interrupt and D9 for Reset.

Dragino Arduino LoRa Shield Schematic

Netduino 3 Wifi pin mapping

  • D10->CS->PB10
  • D9->RST->E5

ST 32F769IDiscovery pin mapping

D10->CS->PA11
D9->RST->PH6

Sparkfun ESP32 1 Channel Gateway Schematic

SparkFun LoRa Gateway-1-Channel (ESP32) pin mapping(SX127X reset is not connected)

  • CS->PB10

The first step was to confirm I could read a single(ShieldSPI) then scan all the Semtech SX1276 registers with the new nanoFramework System.Device.SPI Nuget (which was”inspired by” .Net Core System.Device.SPI)

namespace devMobile.IoT.SX127x.ShieldSPI
{
   using System;
   using System.Diagnostics;
   using System.Threading;

   using System.Device.Gpio;
   using System.Device.Spi;

#if ESP32_WROOM_32_LORA_1_CHANNEL
   using nanoFramework.Hardware.Esp32;
#endif

   public class Program
   {
      private const byte RegVersion = 0x42;
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const int SpiBusId = 1;
#endif
#if NETDUINO3_WIFI
      private const int SpiBusId = 2;
#endif
#if ST_STM32F769I_DISCOVERY
      private const int SpiBusId = 2;
#endif

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

#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
         int ledPinNumber = Gpio.IO17;
         int chipSelectLine = Gpio.IO16;
#endif
#if NETDUINO3_WIFI
         int ledPinNumber = PinNumber('A', 10);
         // Arduino D10->PB10
         int chipSelectLine = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
#endif
#if ST_STM32F769I_DISCOVERY
         int ledPinNumber  = PinNumber('J', 5);
         // Arduino D10->PA11
         int chipSelectLine = PinNumber('A', 11);
         // Arduino D9->PH6
         int resetPinNumber = PinNumber('H', 6);
#endif
         Debug.WriteLine("devMobile.IoT.SX127x.ShieldSPI starting");

         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL || NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
            // Setup the onboard LED
            gpioController.OpenPin(ledPinNumber, PinMode.Output);
#endif

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
            // Setup the reset pin
            gpioController.OpenPin(resetPinNumber, PinMode.Output);
            gpioController.Write(resetPinNumber, PinValue.High);
#endif

#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
#endif

            var settings = new SpiConnectionSettings(SpiBusId, chipSelectLine)
            {
               ClockFrequency = 1000000,
               Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
               SharingMode = SpiSharingMode.Shared,
            };

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

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

                  device.TransferFullDuplex(writeBuffer, readBuffer);

                  Debug.WriteLine(String.Format("Register 0x{0:x2} - Value 0X{1:x2}", RegVersion, readBuffer[1]));

#if ESP32_WROOM_32_LORA_1_CHANNEL || NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
                  if ( gpioController.Read(ledPinNumber) == PinValue.High)
						{
                     gpioController.Write(ledPinNumber, PinValue.Low);
                  }
                  else
						{
                     gpioController.Write(ledPinNumber, PinValue.High);
                  }
#endif
                  Thread.Sleep(10000);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}
Shield SPI Debug output
namespace devMobile.IoT.SX127x.RegisterScan
{
   using System;
   using System.Diagnostics;
   using System.Threading;

   using System.Device.Gpio;
   using System.Device.Spi;

#if ESP32_WROOM_32_LORA_1_CHANNEL
   using nanoFramework.Hardware.Esp32;
#endif

   public sealed class SX127XDevice
   {
      private readonly SpiDevice SX127XTransceiver;

      public SX127XDevice(int busId, int chipSelectLine)
      {

         var settings = new SpiConnectionSettings(busId, chipSelectLine)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared
         };

         SX127XTransceiver = new SpiDevice(settings);
      }

      public SX127XDevice(int busId, int chipSelectLine, int resetPin)
      {
         var settings = new SpiConnectionSettings(busId, chipSelectLine)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared
         };

         SX127XTransceiver = new SpiDevice(settings);

         // Factory reset pin configuration
         GpioController gpioController = new GpioController();
         gpioController.OpenPin(resetPin, PinMode.Output);

         gpioController.Write(resetPin, PinValue.Low);
         Thread.Sleep(20);
         gpioController.Write(resetPin, PinValue.High);
         Thread.Sleep(20);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

         return readBuffer[1];
      }
   }

   public class Program
   {
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const int SpiBusId = 1;
#endif
#if NETDUINO3_WIFI
      private const int SpiBusId = 2;
#endif
#if ST_STM32F769I_DISCOVERY
      private const int SpiBusId = 2;
#endif

      public static void Main()
      {
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectLine = Gpio.IO16;
#endif
#if NETDUINO3_WIFI
         // Arduino D10->PB10
         int chipSelectLine = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
#endif
#if ST_STM32F769I_DISCOVERY
         // Arduino D10->PA11
         int chipSelectLine = PinNumber('A', 11);
         // Arduino D9->PH6
         int resetPinNumber = PinNumber('H', 6);
#endif

         Debug.WriteLine("devMobile.IoT.SX127x.RegisterScan starting");

         try
         {
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
            SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif

#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);

            SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine);
#endif

            Thread.Sleep(500);

            while (true)
            {
               for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
               {
                  byte registerValue = sx127XDevice.RegisterReadByte(registerIndex);

                  Debug.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
               }
               Debug.WriteLine("");

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

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}
RegisterScan Debug Output

There is some SparkFun LoRa Gateway-1-Channel (ESP32) specific configuration to map the Serial Peripheral Interface(SPI) pins and an additional NuGet for ESP32 has to be added. For the initial versions I have not used more advanced .NET nanoFramework functionality like SpanByte.

.NET nanoFramework ValueChanged

If you have landed at this post you were most probably searching for issues updating .NET nanoFramework code that used ValueChanged to handle interrupts. Back in mid 2020 the initial version of my Semtech SX127X(HopeRF RFM9X) library used the Windows.Devices.Gpio Nuget package.

public Rfm9XDevice(string spiPort, int chipSelectPin, int resetPin, int interruptPin)
{
    //...
   
   // Interrupt pin for RX message & TX done notification 
   InterruptGpioPin = gpioController.OpenPin(interruptPin);
   InterruptGpioPin.SetDriveMode(GpioPinDriveMode.Input);

   InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;
}

private void InterruptGpioPin_ValueChanged(object sender, GpioPinValueChangedEventArgs e)
{
   if (e.Edge != GpioPinEdge.RisingEdge)
   {
      return;
   }

   byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
   //...
}

Then in March 2022 I updated the CoreLibrary, Runtime.Events, System.Devices.GPIO, System.Devices.SPI NuGets.

I then fixed all the breaking changes (For the initial versions I have not updated the code to use SpanByte etc.).

public Rfm9XDevice(int spiBusId, int chipSelectPin, int resetPin, int interruptPin)
{
   //...

   // Interrupt pin for RX message & TX done notification 
   InterruptGpioPin = gpioController.OpenPin(interruptPin);
   InterruptGpioPin.SetPinMode(PinMode.Input);

   InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;
}

private void InterruptGpioPin_ValueChanged(object sender, PinValueChangedEventArgs e)
{
   if (e.ChangeType != PinEventTypes.Rising)
   {
      return;
   }

   byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
   //...
}

While “soak testing” the ReceiveInterrupt application I noticed that sometimes when I started the application interrupts were not processed or processing stopped after a while.

Visual Studio Debugger output showing intermittent calling of InterruptGpioPin_ValueChanged

I then found the RangeTester application wouldn’t start or run reliably. My original code was based on the Widnows.Devices.GPIO sample so I updated it based on the System.Device.GPIO sample.

public Rfm9XDevice(int spiBusId, int chipSelectPin, int resetPin, int interruptPin)
{
   //...

   // Interrupt pin for RX message & TX done notification 
   gpioController.OpenPin(interruptPin,PinMode.InputPullDown);

   gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

private void InterruptGpioPin_ValueChanged(object sender, PinValueChangedEventArgs e)
{
   byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
   //...
}
Visual Studio Debugger output showing reliable calling of InterruptGpioPin_ValueChanged

If your Windows.Devices.GPIO based project is not reliably handling interrupts after upgrading to System.Device.GPIO and fixing any “breaking changes” the implementation most probably need to be updated to use RegisterCallbackForPinValueChangedEvent as well.

nanoFramework Seeed LoRa-E5 on Github

The source code of my nanoFramework C# Seeed LoRa-E5 library is live on GitHub. My initial test rig was based on an STM32F691DISCOVERY board which has an Arduino Uno R3 format socket for a Grove Base Shield V2.0. I then connected it to my LoRa-E5 Development Kit with a Grove – Universal 4 Pin 20cm Unbuckled Cable(TX/RX reversed)

STM32F769I test rig with Seeedstudio Grove Base shield V2 and LoRa-E5 Development Kit

So far the demo application has been running for a couple of weeks

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.SeeedE5LoRaWANDeviceClient starting
12:00:01 Join start Timeout:25 Seconds
12:00:07 Join finish
12:00:07 Send Timeout:10 Seconds payload BCD:010203040506070809
12:00:13 Sleep
12:05:13 Wakeup
12:05:13 Send Timeout:10 Seconds payload BCD:010203040506070809
12:05:20 Sleep
12:10:20 Wakeup
12:10:20 Send Timeout:10 Seconds payload BCD:010203040506070809
12:10:27 Sleep
12:15:27 Wakeup
12:15:27 Send Timeout:10 Seconds payload BCD:010203040506070809
12:15:34 Sleep
...
11:52:40 Wakeup
11:52:40 Send Timeout:10 Seconds payload BCD:010203040506070809
11:52:45 Sleep
11:57:45 Wakeup
11:57:45 Send Timeout:10 Seconds payload BCD:010203040506070809
11:57:52 Sleep
12:02:52 Wakeup
12:02:52 Send Timeout:10 Seconds payload BCD:010203040506070809
12:02:59 Sleep
12:07:59 Wakeup
12:07:59 Send Timeout:10 Seconds payload BCD:010203040506070809
12:08:07 Sleep
12:13:07 Wakeup
12:13:07 Send Timeout:10 Seconds payload BCD:010203040506070809
12:13:14 Sleep

I have tested the Over The Air Activation(OTAA) code and will work on testing the other functionality over the coming week,

public static void Main()
{
   Result result;

   Debug.WriteLine("devMobile.IoT.SeeedE5LoRaWANDeviceClient starting");

   try
   {
      using (SeeedE5LoRaWANDevice device = new SeeedE5LoRaWANDevice())
      {
         result = device.Initialise(SerialPortId, 9600, UartParity.None, 8, UartStopBitCount.One);
         if (result != Result.Success)
         {
            Debug.WriteLine($"Initialise failed {result}");
            return;
         }

#if CONFIRMED
         device.OnMessageConfirmation += OnMessageConfirmationHandler;
#endif
         device.OnReceiveMessage += OnReceiveMessageHandler;

#if RESET
         Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Reset");
         result = device.Reset();
         if (result != Result.Success)
         {
            Debug.WriteLine($"Reset failed {result}");
            return;
          }
#endif

         Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Region {Region}");
         result = device.Region(Region);
         if (result != Result.Success)
         {
            Debug.WriteLine($"Region failed {result}");
            return;
         }

         Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
         result = device.AdrOn();
         if (result != Result.Success)
         {
            Debug.WriteLine($"ADR on failed {result}");
            return;
         }

               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Port");
               result = device.Port(MessagePort);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"Port on failed {result}");
                  return;
               }

#if OTAA
               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} OTAA");
               result = device.OtaaInitialise(Config.AppEui, Config.AppKey);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"OTAA Initialise failed {result}");
                  return;
               }
#endif

#if ABP
               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ABP");
               result = device.AbpInitialise(DevAddress, NwksKey, AppsKey);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"ABP Initialise failed {result}");
                  return;
               }
#endif

               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start Timeout:{JoinTimeOut.TotalSeconds} Seconds");
               result = device.Join(true, JoinTimeOut);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"Join failed {result}");
                  return;
               }
               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join finish");

               while (true)
               {
#if PAYLOAD_BCD
                  Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout.TotalSeconds} Seconds payload BCD:{PayloadBcd}");
#if CONFIRMED
                  result = device.Send(PayloadBcd, true, SendTimeout);
#else
                  result = device.Send(PayloadBcd, false, SendTimeout);
#endif
#endif

#if PAYLOAD_BYTES
                  Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout.TotalSeconds} Seconds payload Bytes:{BitConverter.ToString(PayloadBytes)}");
#if CONFIRMED
                  result = device.Send(PayloadBytes, true, SendTimeout);
#else
                  result = device.Send(PayloadBytes, false, SendTimeout);
#endif
#endif
                  if (result != Result.Success)
                  {
                     Debug.WriteLine($"Send failed {result}");
                  }

                  Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Sleep");
                  result = device.Sleep();
                  if (result != Result.Success)
                  {
                     Debug.WriteLine($"Sleep failed {result}");
                     return;
                  }

                  Thread.Sleep(300000);

                  Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Wakeup");
                  result = device.Wakeup();
                  if (result != Result.Success)
                  {
                     Debug.WriteLine($"Wakeup failed {result}");
                     return;
                  }
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

The Region, ADR and OtaaInitialise methods only need to be called when the device is first powered up and after a reset.

The library works but should be treated as late beta.