.NET nanoFramework SX127X LoRa library Refactoring

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.


Fork Refactoring Check-ins

The RegOpMode.cs file is a good example…

namespace devMobile.IoT.SX127xLoRaDevice
{
	using System;

	// RegOpMode bit flags from Semtech SX127X Datasheet
	[Flags]
	internal enum RegOpModeModeFlags : byte
	{
		LongRangeModeLoRa = 0b10000000,
		LongRangeModeFskOok = 0b00000000,
		LongRangeModeDefault = LongRangeModeFskOok,
		AcessSharedRegLoRa = 0b00000000,
		AcessSharedRegFsk = 0b01000000,
		AcessSharedRegDefault = AcessSharedRegLoRa,
		LowFrequencyModeOnHighFrequency = 0b00000000,
		LowFrequencyModeOnLowFrequency = 0b00001000,
		LowFrequencyModeOnDefault = LowFrequencyModeOnLowFrequency
	}

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

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 Semtech SX127X. 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.

.NET nanoFramework SX127X LoRa library RegPaConfig RegPaDac The never ending story

While trying different my NET nanoFramework Semtech 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.

Netduino3 Wifi with USB power consumption measurement

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.

RegPaConfig register configuration options
RegPaDac register configuration options

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 <

.NET nanoFramework SX127X LoRa library RegPaConfig RegPaDac

While updating my .NET nanoFramework Semtech 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…

SX127X Overview of transmission pipeline

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 AdaFruit version of the RadioHead library has differences to the Semtech Lora-net/LoRaMac-Node libraries)

RegPaConfig & RegOcp register configuration options

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.

High power mode overview
RegPaDac register configuration options

The RegOcp (over current protection) has to be relaxed for the higher power modes

RegPaConfig register configuration options

I started with the Semtech Lora-net/LoRaMac-Node library which reads the RegPaConfig, RegPaSelect and RegPaDac registers then does any updates required.

void SX1276SetRfTxPower( int8_t power )
{
    uint8_t paConfig = 0;
    uint8_t paDac = 0;

    paConfig = SX1276Read( REG_PACONFIG );
    paDac = SX1276Read( REG_PADAC );

    paConfig = ( paConfig & RF_PACONFIG_PASELECT_MASK ) | SX1276GetPaSelect( power );

    if( ( paConfig & RF_PACONFIG_PASELECT_PABOOST ) == RF_PACONFIG_PASELECT_PABOOST )
    {
        if( power > 17 )
        {
            paDac = ( paDac & RF_PADAC_20DBM_MASK ) | RF_PADAC_20DBM_ON;
        }
        else
        {
            paDac = ( paDac & RF_PADAC_20DBM_MASK ) | RF_PADAC_20DBM_OFF;
        }
        if( ( paDac & RF_PADAC_20DBM_ON ) == RF_PADAC_20DBM_ON )
        {
            if( power < 5 )
            {
                power = 5;
            }
            if( power > 20 )
            {
                power = 20;
            }
            paConfig = ( paConfig & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power - 5 ) & 0x0F );
        }
        else
        {
            if( power < 2 )
            {
                power = 2;
            }
            if( power > 17 )
            {
                power = 17;
            }
            paConfig = ( paConfig & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power - 2 ) & 0x0F );
        }
    }
    else
    {
        if( power > 0 )
        {
            if( power > 15 )
            {
                power = 15;
            }
            paConfig = ( paConfig & RF_PACONFIG_MAX_POWER_MASK & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( 7 << 4 ) | ( power );
        }
        else
        {
            if( power < -4 )
            {
                power = -4;
            }
            paConfig = ( paConfig & RF_PACONFIG_MAX_POWER_MASK & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( 0 << 4 ) | ( power + 4 );
        }
    }
    SX1276Write( REG_PACONFIG, paConfig );
    SX1276Write( REG_PADAC, paDac );
}

I also reviewed the Arduino-LoRa Semtech 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

.NET Core SX127X library Arduino LoRaSimpleNode & LoRaSimpleGateway

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

LoRaSimpleNode

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

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

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

    LoRa_sendMessage(message); // send a message

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

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

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

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

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

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

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

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

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

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

LoRaSimpleGateway

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

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

const long frequency = 915E6;  // LoRa Frequency

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

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

  LoRa.setPins(csPin, resetPin, irqPin);

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

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

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

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

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

    LoRa_sendMessage(message); // send a message

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

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

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

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

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

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

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

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

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

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

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

int messageCount = 1;

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

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

#if DEBUG
		sX127XDevice.RegisterDump();
#endif

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

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

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

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

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

			messageCount += 1;

			sX127XDevice.Send(messageBytes);

			Thread.Sleep(10000);
		}
	}

Summary

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

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

The Arduino-LoRa code has SetPower

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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