Random wanderings through Microsoft Azure esp. PaaS plumbing, the IoT bits, AI on Micro controllers, AI on Edge Devices, .NET nanoFramework, .NET Core on *nix and ML.NET+ONNX
I had been planning this for a while, then the code broke when I tried to build a version for my SparkFun LoRa Gateway-1-Channel (ESP32). There was a namespace (static configuration class in configuration.cs) collision and the length of SX127XDevice.cs file was getting silly.
This refactor took a couple of days and really changed the structure of the library.
VS2022 Solution structure after refactoring
I went through the SX127XDevice.cs extracting the enumerations, masks and defaults associated with the registers the library supports.
The library is designed to be a approximate .NET nanoFramework equivalent of Arduino-LoRa so it doesn’t support/implement all of the functionality of the SemtechSX127X. Still got a bit of refactoring to go but the structure is slowly improving.
I use Fork to manage my Github repositories, it’s an excellent product especially as it does a pretty good job of keeping me from screwing up.
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.
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.