Random wanderings through Microsoft Azure esp. the IoT bits, AI on Micro controllers, .NET nanoFramework, .NET Core on *nix, and GHI Electronics TinyCLR
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 <
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
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.
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.
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);
}
}
}