Random wanderings through Microsoft Azure esp. the IoT bits, AI on Micro controllers, .NET nanoFramework, .NET Core on *nix, and GHI Electronics TinyCLR
While “smoke testing” the application I noticed that if I erased the flash, power cycled the device, then ran the application the first execution would fail because the SemtechSX127X could not be detected.
After printing the code out and reviewing it I noticed that the Configuration.SetPinFunction for the Serial Peripheral Interface(SPI) Master Out Slave In(MOSI), MOSI(Master In Slave Out) and Clock pins was after the opening of the SPI port.
static void Main(string[] args)
{
byte 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 dio0PinNumber = 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 dio0PinNumber = 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 dio0PinNumber = PinNumber('J', 1);
#endif
Console.WriteLine("devMobile.IoT.SX127xLoRaDevice Range Tester starting");
try
{
#f 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 (_gpioController = new GpioController())
using (SpiDevice spiDevice = new SpiDevice(settings))
{
#if ESP32_WROOM_32_LORA_1_CHANNEL
_sx127XDevice = new SX127XDevice(spiDevice, _gpioController, dio0Pin: dio0PinNumber);
#endif
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
_sx127XDevice = new SX127XDevice(spiDevice, _gpioController, dio0Pin: dio0PinNumber, resetPin:resetPinNumber);
#endif
...
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
I assume that the first execution after erasing the flash and power cycling the device, the SPI port pin assignments were not configured when the port was opened, then on the next execution the port was pre-configured.
The RangeTester application flashes on onboard Light Emitting Diode(LED) every time a valid message is received. But, on the ESP32 it turned on when the first message arrived and didn’t turn off. After discussion on the nanoFramework Discord this has been identified as an issue(May 2022).
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.
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.
While trying different myNET nanoFrameworkSemtech SX127X library configurations so I could explore the interactions of RegOcp(Over current protection) + RegOcpTrim I noticed something odd about the power consumption so I revisited how the output power is calculated.
The RegPaConfig register has three settings PaSelect(RFO & PA_BOOST), MaxPower(0..7), and OutputPower(0..15). When in RFO mode the pOut has a range of -4 to 15 and PA_BOOST mode has a range of 2 to 20.
The SX127X also has a power amplifier attached to the PA_BOOST pin and a higher power amplifier which is controlled by the RegPaDac register.
// Set RegPAConfig & RegPaDac if powerAmplifier/OutputPower settings not defaults
if ((powerAmplifier != Configuration.RegPAConfigPASelect.Default) || (outputPower != Configuration.OutputPowerDefault))
{
if (powerAmplifier == Configuration.RegPAConfigPASelect.PABoost)
{
byte regPAConfigValue = (byte)Configuration.RegPAConfigPASelect.PABoost;
// Validate the minimum and maximum PABoost outputpower
if ((outputPower < Configuration.OutputPowerPABoostMin) || (outputPower > Configuration.OutputPowerPABoostMax))
{
throw new ApplicationException($"PABoost {outputPower}dBm Min power {Configuration.OutputPowerPABoostMin} to Max power {Configuration.OutputPowerPABoostMax}");
}
if (outputPower <= Configuration.OutputPowerPABoostPaDacThreshhold)
{
// outputPower 0..15 so pOut is 2=17-(15-0)...17=17-(15-15)
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Default;
regPAConfigValue |= (byte)(outputPower - 2);
_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Normal);
}
else
{
// outputPower 0..15 so pOut is 5=20-(15-0)...20=20-(15-15) // See https://github.com/adafruit/RadioHead/blob/master/RH_RF95.cpp around line 411 could be 23dBm
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Default;
regPAConfigValue |= (byte)(outputPower - 5);
_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Boost);
}
}
else
{
byte regPAConfigValue = (byte)Configuration.RegPAConfigPASelect.Rfo;
// Validate the minimum and maximum RFO outputPower
if ((outputPower < Configuration.OutputPowerRfoMin) || (outputPower > Configuration.OutputPowerRfoMax))
{
throw new ApplicationException($"RFO {outputPower}dBm Min power {Configuration.OutputPowerRfoMin} to Max power {Configuration.OutputPowerRfoMax}");
}
// Set MaxPower and Power calculate pOut = PMax-(15-outputPower), pMax=10.8 + 0.6*MaxPower
if (outputPower > Configuration.OutputPowerRfoThreshhold)
{
// pMax 15=10.8+0.6*7 with outputPower 0...15 so pOut is 15=pMax-(15-0)...0=pMax-(15-15)
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Max;
regPAConfigValue |= (byte)(outputPower + 0);
}
else
{
// pMax 10.8=10.8+0.6*0 with output power 0..15 so pOut is -4=10-(15-0)...10.8=10.8-(15-15)
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Min;
regPAConfigValue |= (byte)(outputPower + 4);
}
_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Normal);
}
}
// Set RegOcp if any of the settings not defaults
if ((ocpOn != Configuration.RegOcp.Default) || (ocpTrim != Configuration.RegOcpTrim.Default))
{
byte regOcpValue = (byte)ocpTrim;
regOcpValue |= (byte)ocpOn;
_registerManager.WriteByte((byte)Configuration.Registers.RegOcp, regOcpValue);
}
After reviewing the code I realised that the the RegPaDac test around line 14 should <= rather than <
All the previous versions of my.NET nanoFrameworkSemtech SX127X (LoRa® Mode) library only supported a Dio0 (RegDioMapping1 bits 6&7) EventHandler. This version supports mapping Dio0, Dio1, Dio2, Dio3, Dio4 and Dio5.
The SX127XLoRaDeviceClient main now has OnRxTimeout, OnReceive, OnPayloadCrcError, OnValidHeader, OnTransmit, OnChannelActivityDetectionDone, OnFhssChangeChannel, and OnChannelActivityDetected event handlers (Based on RegIrqFlags bit ordering)
The Dio0 pin number is the only required pin number parameter, the resetPin, and Dio1 thru Dio5 pin numbers are optional. All the RegDioMapping1 and RegDioMapping2 mappings are disabled on intialisation so there should be no events while the SX127X is being configured.
public SX127XDevice(SpiDevice spiDevice, GpioController gpioController,
int dio0Pin,
int resetPin = 0, // Odd order so as not to break exisiting code
int dio1Pin = 0,
int dio2Pin = 0,
int dio3Pin = 0,
int dio4Pin = 0,
int dio5Pin = 0
)
{
_gpioController = gpioController;
// Factory reset pin configuration
if (resetPin != 0)
{
_resetPin = resetPin;
_gpioController.OpenPin(resetPin, PinMode.Output);
_gpioController.Write(resetPin, PinValue.Low);
Thread.Sleep(20);
_gpioController.Write(resetPin, PinValue.High);
Thread.Sleep(50);
}
_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");
}
// See Table 18 DIO Mapping LoRa® Mode
Configuration.RegDioMapping1 regDioMapping1Value = Configuration.RegDioMapping1.Dio0None;
regDioMapping1Value |= Configuration.RegDioMapping1.Dio1None;
regDioMapping1Value |= Configuration.RegDioMapping1.Dio2None;
regDioMapping1Value |= Configuration.RegDioMapping1.Dio3None;
_registerManager.WriteByte((byte)Configuration.Registers.RegDioMapping1, (byte)regDioMapping1Value);
// Currently no easy way to test this with available hardware
//Configuration.RegDioMapping2 regDioMapping2Value = Configuration.RegDioMapping2.Dio4None;
//regDioMapping2Value = Configuration.RegDioMapping2.Dio5None;
//_registerManager.WriteByte((byte)Configuration.Registers.RegDioMapping2, (byte)regDioMapping2Value);
// Interrupt pin for RXDone, TXDone, and CadDone notification
_gpioController.OpenPin(dio0Pin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(dio0Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
// RxTimeout, FhssChangeChannel, and CadDetected
if (dio1Pin != 0)
{
_gpioController.OpenPin(dio1Pin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(dio1Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
// FhssChangeChannel, FhssChangeChannel, and FhssChangeChannel
if (dio2Pin != 0)
{
_gpioController.OpenPin(dio2Pin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(dio2Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
// CadDone, ValidHeader, and PayloadCrcError
if (dio3Pin != 0)
{
_gpioController.OpenPin(dio3Pin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(dio3Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
// CadDetected, PllLock and PllLock
if (dio4Pin != 0)
{
_gpioController.OpenPin(dio4Pin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(dio4Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
// ModeReady, ClkOut and ClkOut
if (dio5Pin != 0)
{
_gpioController.OpenPin(dio5Pin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(dio5Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
}
The same event handler (InterruptGpioPin_ValueChanged) is used for Dio0 thru Dio5. Each event has a “process” method and the RegIrqFlags register controls which one(s) are called.
The RegIrqFlags bits are cleared individually (with regIrqFlagsToClear) at the end of the event handler. Initially I cleared all the flags by writing 0xFF to RegIrqFlags but this caused issues when there were multiple bits set e.g. CadDone along with CadDetected.
It took some experimentation with the SX127xLoRaDeviceClient application to “reliably” trigger events for testing. To generate CAD Detected event, I had to modify one of the Arduino-LoRa sample applications to send messages without a delay, then have it running as the SX127xLoRaDeviceClient application was starting.
While updating my.NET nanoFrameworkSemtech SX127X library I revisited (because I thought it might still be wrong) how the output power is calculated. I started with the overview of the transmitter architecture in in the datasheet…
The RegPaConfig register has three settings PaSelect(RFO & PA_BOOST), MaxPower(0..7), and OutputPower(0..15). When in RFO mode the pOut has a range of -4 to 15 and PA_BOOST mode has a range of 2 to 20. (The AdaFruitversion of the RadioHead library has differences to the Semtech Lora-net/LoRaMac-Node libraries)
The SX127X also has a power amplifier attached to the PA_BOOST pin and a higher power amplifier which is controlled by the RegPaDac register.
The RegOcp (over current protection) has to be relaxed for the higher power modes
I started with the Semtech Lora-net/LoRaMac-Node library which reads the RegPaConfig, RegPaSelect and RegPaDac registers then does any updates required.
I also reviewed the Arduino-LoRaSemtech library which only writes to the RegPaConfig, RegPaSelect and RegPaDac registers.
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));
}
}
I updated the output power configuration code in the Initialise method of the SX127X library. After reviewing the SX127X datasheet I extended the way the pOut is calculated in RFO mode. The code uses two values for MaxPower(RegPAConfigMaxPower.Min & RegPAConfigMaxPower.Max) so that the full RTO output power range was available.
// Set RegPAConfig & RegPaDac if powerAmplifier/OutputPower settings not defaults
if ((powerAmplifier != Configuration.RegPAConfigPASelect.Default) || (outputPower != Configuration.OutputPowerDefault))
{
if (powerAmplifier == Configuration.RegPAConfigPASelect.PABoost)
{
byte regPAConfigValue = (byte)Configuration.RegPAConfigPASelect.PABoost;
// Validate the minimum and maximum PABoost outputpower
if ((outputPower < Configuration.OutputPowerPABoostMin) || (outputPower > Configuration.OutputPowerPABoostMax))
{
throw new ApplicationException($"PABoost {outputPower}dBm Min power {Configuration.OutputPowerPABoostMin} to Max power {Configuration.OutputPowerPABoostMax}");
}
if (outputPower < Configuration.OutputPowerPABoostPaDacThreshhold)
{
// outputPower 0..15 so pOut is 2=17-(15-0)...17=17-(15-15)
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Default;
regPAConfigValue |= (byte)(outputPower - 2);
_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Normal);
}
else
{
// outputPower 0..15 so pOut is 5=20-(15-0)...20=20-(15-15) // See https://github.com/adafruit/RadioHead/blob/master/RH_RF95.cpp around line 411 could be 23dBm
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Default;
regPAConfigValue |= (byte)(outputPower - 5);
_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Boost);
}
}
else
{
byte regPAConfigValue = (byte)Configuration.RegPAConfigPASelect.Rfo;
// Validate the minimum and maximum RFO outputPower
if ((outputPower < Configuration.OutputPowerRfoMin) || (outputPower > Configuration.OutputPowerRfoMax))
{
throw new ApplicationException($"RFO {outputPower}dBm Min power {Configuration.OutputPowerRfoMin} to Max power {Configuration.OutputPowerRfoMax}");
}
// Set MaxPower and Power calculate pOut = PMax-(15-outputPower), pMax=10.8 + 0.6*MaxPower
if (outputPower > Configuration.OutputPowerRfoThreshhold)
{
// pMax 15=10.8+0.6*7 with outputPower 0...15 so pOut is 15=pMax-(15-0)...0=pMax-(15-15)
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Max;
regPAConfigValue |= (byte)(outputPower + 0);
}
else
{
// pMax 10.8=10.8+0.6*0 with output power 0..15 so pOut is -4=10-(15-0)...10.8=10.8-(15-15)
regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Min;
regPAConfigValue |= (byte)(outputPower + 4);
}
_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Normal);
}
}
The formula for pOut and pMax in RegPaConfig documentation is included in the source code so I could manually calculate (including edge cases) the values as part of my testing. I ran the SX127XLoRaDeviceClient and inspected the PaConfig & RegPaDac in the Visual Studio 2022 debugger.
PABoost
Output power = 1
Output power = 21
Exception
Output power = 2
PaConfig = 192
RegPaDac = normal
1100 0000
Output power = 16
PaConfig = 206
RegPaDac = normal
1100 1110
Output power = 17
PaConfig = 204
RegPacDac = Normal
1100 1100
Output power = 18
PaConfig = 205
RegPacDac = Boost
1100 1101
Output power = 19
PaConfig = 206
RegPacDac = Boost
1100 1110
Output power = 20
PaConfig = 207
RegPacDac = Boost
1100 1111
RFO
Output power = -5
Output power = 16
Exception
Output power = -4
PAConfig = 0
0000 0000
Output power = -1
PAConfig = 3
0000 0011
Output power = 0
PAConfig = 4
0000 0100
Output power = 1
PAConfig = 113
0111 0001
OutputPower = 14
PAConfig = 126
0111 1110
OutputPower = 15
PAConfig = 127
0111 1111
I need to borrow some test gear to check my implementation
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.
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.
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).
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.
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).
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);
}
}
...
}
}
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.