Rfm9xLoRaDevice NetMF Payload CRCs

To ensure I was only handling messages with valid contents I added code to appended a cyclic redundancy check(CRC) onto outbound messages and validate the CRC on inbound messages.

First step was to update the initialise method parameter list (the parameter list is huge but for most scenarios the defaults are fine)

public void Initialise(RegOpModeMode regOpModeAfterInitialise, // RegOpMode
         double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
         bool rxDoneignoreIfCrcMissing = true, bool rxDoneignoreIfCrcInvalid = true,
         bool paBoost = false, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
         bool ocpOn = true, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
         RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoost = false, // RegLna
         RegModemConfigBandwidth bandwidth = RegModemConfigBandwidthDefault, RegModemConfigCodingRate codingRate = RegModemConfigCodingRateDefault, RegModemConfigImplicitHeaderModeOn implicitHeaderModeOn = RegModemConfigImplicitHeaderModeOnDefault, //RegModemConfig1
         RegModemConfig2SpreadingFactor spreadingFactor = RegModemConfig2SpreadingFactorDefault, bool txContinuousMode = false, bool rxPayloadCrcOn = false,
         ushort symbolTimeout = SymbolTimeoutDefault,
         ushort preambleLength = PreambleLengthDefault,
         byte payloadLength = PayloadLengthDefault,
         byte payloadMaxLength = PayloadMaxLengthDefault,
         byte freqHoppingPeriod = FreqHoppingPeriodDefault,
         bool lowDataRateOptimize = false, bool agcAutoOn = false,
         byte ppmCorrection = ppmCorrectionDefault,
         RegDetectOptimizeDectionOptimize detectionOptimize = RegDetectOptimizeDectionOptimizeDefault,
         bool invertIQ = false,
         RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
         byte syncWord = RegSyncWordDefault)

The rxPayloadCrcOn needs to be set to True for outbound messages to have a CRC.

Then in the RxDone interrupt handler the CRC is checked (regHopChannel & regIrqFlagsMask) if this feature is enabled. Any messages with missing\invalid CRCs will currently be silently discarded and I’m not certain this is a good idea.

 // Check to see if payload has CRC
         if (RxDoneIgnoreIfCrcMissing)
         {
            byte regHopChannel = this.Rfm9XLoraModem.ReadByte((byte)Registers.RegHopChannel);
            if ((regHopChannel & (byte)RegHopChannelFlags.CrcOnPayload) != (byte)RegHopChannelFlags.CrcOnPayload)
            {
               return;
            }
         }

         // Check to see if payload CRC is valid
         if (RxDoneIgnoreIfCrcInvalid)
         {
            if (((byte)IrqFlags & (byte)RegIrqFlagsMask.PayLoadCrcErrorMask) == (byte)RegIrqFlagsMask.PayLoadCrcErrorMask)
            {
               return;
            }
         }

The conversion of the payload from an array of bytes to a string for display stopped failing with an exception. When I had a number of clients running up to 10% of the messages were getting corrupted.

Rfm9xLoRaDevice NetMF LNA

While fixing up the Signal to Noise Ratio(SNR) and Received Signal Strength Indication(RSSI) in a previous posts. I noted the Low Noise Amplifier(LNA) had High Frequency(LF) and Low Frequency settings.

First step was to update the initialise method parameter list (the parameter list is huge but for most scenarios the defaults are fine)

 public void Initialise(RegOpModeMode regOpModeAfterInitialise, // RegOpMode
       double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
         bool paBoost = false, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
         bool ocpOn = true, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
         RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoost = false, // RegLna
         RegModemConfigBandwidth bandwidth = RegModemConfigBandwidthDefault, RegModemConfigCodingRate codingRate = RegModemConfigCodingRateDefault, RegModemConfigImplicitHeaderModeOn implicitHeaderModeOn = RegModemConfigImplicitHeaderModeOnDefault, //RegModemConfig1
         RegModemConfig2SpreadingFactor spreadingFactor = RegModemConfig2SpreadingFactorDefault, bool txContinuousMode = false, bool rxPayloadCrcOn = false,
         ushort symbolTimeout = SymbolTimeoutDefault,
         ushort preambleLength = PreambleLengthDefault,
         byte payloadLength = PayloadLengthDefault,
         byte payloadMaxLength = PayloadMaxLengthDefault,
         byte freqHoppingPeriod = FreqHoppingPeriodDefault,
         bool lowDataRateOptimize = false, bool agcAutoOn = false,
         byte ppmCorrection = ppmCorrectionDefault,
         RegDetectOptimizeDectionOptimize detectionOptimize = RegDetectOptimizeDectionOptimizeDefault,
         bool invertIQ = false,
         RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
         byte syncWord = RegSyncWordDefault)
      {

The SX127X RegLNA configuration code and the SetMode method required modification

if ((lnaGain != LnaGainDefault) || (lnaBoost != false))
         {
            byte regLnaValue = (byte)lnaGain;
            if (lnaBoost)
            {
               if (Frequency > RFMidBandThreshold)
               {
                  regLnaValue |= RegLnaLnaBoostHfOn;
               }
               else
               {
                  regLnaValue |= RegLnaLnaBoostLfOn;
               }
            }
            Rfm9XLoraModem.WriteByte((byte)Registers.RegLna, regLnaValue);
         }
public void SetMode(RegOpModeMode mode)
      {
         byte regOpModeValue;

         regOpModeValue = RegOpModeLongRangeModeLoRa;
         regOpModeValue |= RegOpModeAcessSharedRegLoRa;
         if (Frequency > RFMidBandThreshold)
         {
            regOpModeValue |= RegOpModeLowFrequencyModeOnHighFrequency;
         }
         else
         {
            regOpModeValue |= RegOpModeLowFrequencyModeOnLowFrequency;
         }
         regOpModeValue |= (byte)mode;
         Rfm9XLoraModem.WriteByte((byte)Registers.RegOpMode, regOpModeValue);
      }

Having to convert all the Flags & masks from binary to hexadecimal values was a bit painful

// RegDioMapping1
[Flags]
public enum RegDioMapping1
{
Dio0RxDone = 0x00,
Dio0TxDone = 0x40,
Dio0CadDone = 0x80,
}

The HF & LF differences where not obviously handled in Arduino-LoRa library and the Semtech LoRaMac node GitHub repository wasn’t so helpful.

When I stress tested this code the UTF8Encoding.UTF8.GetChars kept on throwing exceptions as the messages were corrupt. Need to add CRC presence and validity checking to next version.

 static void rfm9XDevice_OnDataReceived(float packetSnr, int packetRssi, int rssi,  byte[] data)
{
   try
   {
      string messageText = new string(UTF8Encoding.UTF8.GetChars(data));

      Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""") ;
   }
   catch (Exception ex)
   {
      Debug.Print(ex.Message);
   }
}

Rfm9xLoRaDevice NetMF SNR and RSSI

The signal to noise Ratio (SNR) and Received Signal Strength Indication(RSSI) for inbound messages required reading values from three registers
•RegPktSnrValue
•RegPktRssiValue
•RegRssiValue

I had to modify the OnDataRecievedHandler method signature so the values could be returned

 public delegate void OnDataRecievedHandler(float packetSnr, int packetRssi, int rssi, byte[] data);

I was inspired by the RSSI adjustment approach used in the Arduino-LoRa library

// Get the RSSI HF vs. LF port adjustment section 5.5.5 RSSI and SNR in LoRa Mode
float packetSnr = this.Rfm9XLoraModem.ReadByte((byte)Registers.RegPktSnrValue) * 0.25f;

int rssi = this.Rfm9XLoraModem.ReadByte((byte)Registers.RegRssiValue);
if (Frequency > RFMidBandThreshold)
{
  rssi = RssiAdjustmentHF + rssi;
}
else
{
  rssi = RssiAdjustmentLF + rssi;
}

int packetRssi = this.Rfm9XLoraModem.ReadByte((byte)Registers.RegPktRssiValue);
if (Frequency > RFMidBandThreshold)
{
  packetRssi = RssiAdjustmentHF + packetRssi;
}
else
{
  packetRssi = RssiAdjustmentLF + packetRssi;
}

OnDataReceived?.Invoke( packetSnr, packetRssi, rssi, messageBytes);

The values displayed in the Rfm9xLoRaDeviceClient application looked reasonable, but will need further checking

00:06:14-Rfm9X PacketSnr 9.8 Packet RSSI -47dBm RSSI -111dBm = 28 byte message "Hello W10 IoT Core LoRa! 182"
Sending 20 bytes message Hello NetMF LoRa! 38
Transmit-Done
00:06:24-Rfm9X PacketSnr 9.8 Packet RSSI -48dBm RSSI -111dBm = 28 byte message "Hello W10 IoT Core LoRa! 181"
Sending 20 bytes message Hello NetMF LoRa! 39
Transmit-Done
00:06:34-Rfm9X PacketSnr 9.8 Packet RSSI -47dBm RSSI -112dBm = 28 byte message "Hello W10 IoT Core LoRa! 180"
Sending 20 bytes message Hello NetMF LoRa! 40
Transmit-Done
00:06:44-Rfm9X PacketSnr 10.0 Packet RSSI -48dBm RSSI -111dBm = 28 byte message "Hello W10 IoT Core LoRa! 179"

 

Rfm9xLoRaDevice LNA gain revisited

While fixing up the Signal to Noise Ratio(SNR) and Received Signal Strength Indication(RSSI) in a previous post I noted the Low Noise Amplifier(LNA) had High Frequency(LF) and Low Frequency settings.

First step was to update the initialise method parameter list (the parameter list is huge but for most values the defaults are fine)

		public void Initialise(RegOpModeMode modeAfterInitialise, // RegOpMode
			double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
			bool rxDoneignoreIfCrcMissing = true, bool rxDoneignoreIfCrcInvalid = true,
			bool paBoost = false, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
			bool ocpOn = true, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
			RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoostLF = false, bool lnaBoostHf = false, // RegLna
			RegModemConfigBandwidth bandwidth = RegModemConfigBandwidthDefault, RegModemConfigCodingRate codingRate = RegModemConfigCodingRateDefault, RegModemConfigImplicitHeaderModeOn implicitHeaderModeOn = RegModemConfigImplicitHeaderModeOnDefault, //RegModemConfig1
			RegModemConfig2SpreadingFactor spreadingFactor = RegModemConfig2SpreadingFactorDefault, bool txContinuousMode = false, bool rxPayloadCrcOn = false,
			ushort symbolTimeout = SymbolTimeoutDefault,
			ushort preambleLength = PreambleLengthDefault,
			byte payloadLength = PayloadLengthDefault,
			byte payloadMaxLength = PayloadMaxLengthDefault,
			byte freqHoppingPeriod = FreqHoppingPeriodDefault,
			bool lowDataRateOptimize = false, bool agcAutoOn = false,
			byte ppmCorrection = ppmCorrectionDefault,
			RegDetectOptimizeDectionOptimize detectionOptimize = RegDetectOptimizeDectionOptimizeDefault,
			bool invertIQ = false,
			RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
			byte syncWord = RegSyncWordDefault)
		{

which became

public void Initialise(RegOpModeMode modeAfterInitialise, // RegOpMode
	double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
	bool rxDoneignoreIfCrcMissing = true, bool rxDoneignoreIfCrcInvalid = true,
	bool paBoost = false, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
	bool ocpOn = true, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
	RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoost = false, // RegLna
	RegModemConfigBandwidth bandwidth = RegModemConfigBandwidthDefault, RegModemConfigCodingRate codingRate = RegModemConfigCodingRateDefault, RegModemConfigImplicitHeaderModeOn implicitHeaderModeOn = RegModemConfigImplicitHeaderModeOnDefault, //RegModemConfig1
	RegModemConfig2SpreadingFactor spreadingFactor = RegModemConfig2SpreadingFactorDefault, bool txContinuousMode = false, bool rxPayloadCrcOn = false,
	ushort symbolTimeout = SymbolTimeoutDefault,
	ushort preambleLength = PreambleLengthDefault,
	byte payloadLength = PayloadLengthDefault,
	byte payloadMaxLength = PayloadMaxLengthDefault,
	byte freqHoppingPeriod = FreqHoppingPeriodDefault,
	bool lowDataRateOptimize = false, bool agcAutoOn = false,
	byte ppmCorrection = ppmCorrectionDefault,
	RegDetectOptimizeDectionOptimize detectionOptimize = RegDetectOptimizeDectionOptimizeDefault,
	bool invertIQ = false,
	RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
	byte syncWord = RegSyncWordDefault)
{

Then in the code for configuring the RegLna the frequency band is checked

		// Set RegLna if any of the settings not defaults
			if ((lnaGain != LnaGainDefault) || (lnaBoost != false))
			{
				byte regLnaValue = (byte)lnaGain;
				if (lnaBoost)
				{
					if (Frequency > RFMidBandThreshold)
					{
						regLnaValue |= RegLnaLnaBoostHfOn;
					}
					else
					{
						regLnaValue |= RegLnaLnaBoostLfOn;
					}
				}
				RegisterManager.WriteByte((byte)Registers.RegLna, regLnaValue);
			}

The HF & LF differences where not obviously handled in Arduino-LoRa library and the Semtech LoRaMac node GitHub repository wasn’t so helpful this time.

Not so confident with these changes need to test with my 434MHz devices

Rfm9xLoRaDevice SNR and RSSI revisited

The magic numbers for the high frequency(HF) vs. low frequency(LF) adjustment bugged me

int rssi = this.RegisterManager.ReadByte((byte)Registers.RegRssiValue);
if (Frequency < 868E6)
	rssi = -164 + rssi; // LF output port
else
	rssi = -157 + rssi; // HF output port

int packetRssi = this.RegisterManager.ReadByte((byte)Registers.RegPktRssiValue);
if (Frequency < 868E6)
	packetRssi = -164 + rssi; // LF output port
else
	packetRssi = -157 + rssi; // HF output port

After some searching I ended up at the Semetch LoRaMac node github repository. In that code they had a number of constants which looked like a better approach.

if( SX1276.Settings.Channel > RF_MID_BAND_THRESH )
{
   rssi = RSSI_OFFSET_HF + SX1276Read( REG_LR_RSSIVALUE );
}
else
{
   rssi = RSSI_OFFSET_LF + SX1276Read( REG_LR_RSSIVALUE );
}

I also fixed a couple of bugs I noticed with the maths and the code now looks like this

int rssi = this.RegisterManager.ReadByte((byte)Registers.RegRssiValue);
if (Frequency > RFMidBandThreshold)
   rssi = RssiAdjustmentHF + rssi;
else
   rssi = RssiAdjustmentLF + rssi;

int packetRssi = this.RegisterManager.ReadByte((byte)Registers.RegPktRssiValue);
if (Frequency > RFMidBandThreshold)
   packetRssi = RssiAdjustmentHF + packetRssi;
else
   packetRssi = RssiAdjustmentLF + packetRssi;

This has also got me thing about how RegLna – LnaBoostLf/LnaBoostHf should be handled as well.

Rfm9xLoRaDevice SNR and RSSI

The signal to noise Ratio (SNR) and Received Signal Strength Indication(RSSI) for inbound messages required reading values from three registers

  • RegPktSnrValue
  • RegPktRssiValue
  • RegRssiValue

I had to modify the EventArgs returned to the customer

public class OnDataReceivedEventArgs : EventArgs
{
	public float PacketSnr { get; set; }
	public int PacketRssi { get; set; }
	public int Rssi { get; set; }
	public byte[] Data { get; set; }
}

I was inspired by the RSSI adjustment approach used in the Arduino-LoRa library

// Get the RSSI HF vs. LF port adjustment section 5.5.5 RSSI and SNR in LoRa Mode
float packetSnr = this.RegisterManager.ReadByte((byte)Registers.RegPktSnrValue) * 0.25f;

int rssi = this.RegisterManager.ReadByte((byte)Registers.RegRssiValue);
if (Frequency < 868E6)
	rssi = -164 - rssi; // LF output port
else
	rssi = -157 + rssi; // HF output port

int packetRssi = this.RegisterManager.ReadByte((byte)Registers.RegPktRssiValue);
if (Frequency < 868E6)
	packetRssi = -164 - rssi; // LF output port
else
	packetRssi = -157 - rssi; // HF output port

OnDataReceivedEventArgs receiveArgs = new OnDataReceivedEventArgs
{
	PacketSnr = packetSnr,
	Rssi = rssi,
	PacketRssi = packetRssi,
	Data = messageBytes
};

The values displayed in the Rfm9xLoRaDeviceClient application looked reasonable, but will need further checking

M2M Low power LoRaWan Node Model B1284

Along with the M2M LoRaWan Gateway Shield for Raspberry Pi I also purchased a Low power LoRaWan Node Model B1284. After configuring Arduino IDE then downloading the necessary board configuration files (link to instructions was provided) I could down upload my Arduino-Lora based test application .

LoRaWanNodeV1_0.jpg
Initially the program failed with “LoRa init failed. Check your connections.” so I went back and checked the board configuration details and noticed that the chip select line was different.

const int csPin = 14;          // 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

byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send

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

  Serial.println("LoRa Duplex - Set sync word");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.enableCrc();

  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs

  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}

void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "11 Hello Arduino LoRa! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    //interval = random(2000) + 1000;    // 2-3 seconds
    interval = 1000;
  }

  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}

void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return

  // read packet header bytes:
  String incoming = "";

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

  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

When I uploaded my application I found the device had significantly more memory available

Sketch uses 8456 bytes (27%) of program storage space. Maximum is 30720 bytes.
vs..
Sketch uses 10424 bytes (8%) of program storage space. Maximum is 130048 bytes.

With the size of the LMIC stack this additional extra headroom could be quite useful. For most my LoRa applications (which tend to be a couple of simple sensors) I think the Low Power LoRaWan Node Model A328 should be sufficient.

M2M LoRaWan Node Model A328

Along with the M2M LoRaWan Gateway Shield for Raspberry Pi I also purchased a Low power LoRaWan Node Model A328. After setting the Board in Arduino IDE to Arduino pro mini 8Mhz 3V the device fired up and worked first time.

LoRaWanNodeV3_5
The device is intended for LoRaWan applications so the samples provided (including a link to application template generator) were not that applicable for my LoRa project so I used the Arduino LoRa library.

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

byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send

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

  Serial.println("LoRa Duplex - Set sync word");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.enableCrc();

  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs

  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}

void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "0 Hello Arduino LoRa! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    //interval = random(2000) + 1000;    // 2-3 seconds
    interval = 1000;
  }

  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}

void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return

  // read packet header bytes:
  String incoming = "";

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

  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

I did find the “grove” connectors weren’t compatible with any of my sensors, but the vendor does include a number of cables DIY connection.

GroveConnectorIssue20180822

Next I’ll use power conservation modes and see how long I can get a set of AAA batteries to last. The device looks like a good option (esp. with solar power for devices with higher power consumption sensors) for some of the SmartAg projects my students are building.

In my Windows 10 IoT Core test application I could see the enableCrc() method was working according to the RegHopChannel CrcOnPayload flag.

For real deployments of the field gateway I think packets which have no CRC or a corrupted one will be dropped.

RFM95/96/97/98 shield library Part 10

Enums & Masks

The code was filled with “magic numbers” so it was time to get rid of them. In C# there are bit constants which I missed for my backport to .NetMF.

I sat down with the Semtech SX1276 datasheet and started typing in register names and adding constants and enums for all the bit masks, flags and defaults.

The initialisation of the RFM9X is now done in one of two constructors and an initialise method. Much like the approach used in the nRF24L01P libraries I use on Windows 10 IoT Core and .NetMF.

A few weeks ago I had a failed attempt at building a library which tried to hide as much of the hardware and wireless implementation details from the user as possible. Realistically if you’re building systems using LoRa, a basic understanding of the technology plus any regional regulatory requirements (frequency use, duty cycles etc.) is necessary.

	sealed class Rfm9XDevice
	{
		// Registers from SemTech SX127X Datasheet
		enum Registers : byte
		{
			MinValue = RegOpMode,
			RegFifo = 0x0,
			RegOpMode = 0x01,
			//Reserved 0x02-0x06
			RegFrMsb = 0x06,
			RegFrMid = 0x7,
			RegFrLsb = 0x08,
			RegPAConfig = 0x09,
			//RegPARamp = 0x0A, // not inlcuded as FSK/OOK functionality
			RegOcp = 0x0B,
			RegLna = 0x0C,
			RegFifoAddrPtr = 0x0D,
			//RegFifoTxBaseAddr = 0x0E
			RegFifoRxCurrent =0x10,
			RegIrqFlagsMask = 0x11,
			RegIrqFlags = 0x12,
			// RegRxNdBytes = 0x13
			// RegRxHeaderCnValueMsb=0x14
			// RegRxHeaderCnValueLsb=0x15
			// RegRxPacketCntValueMsb=0x16
			// RegRxPacketCntValueMsb=0x17
			// RegModemStat=0x18
			// RegPktSnrVale=0x19
			// RegPktRssiValue=0x1A
			// RegRssiValue=0x1B
			// RegHopChannel=0x1C
			RegModemConfig1 = 0x1D,
			RegModemConfig2 = 0x1E,
			RegSymbTimeout = 0x1F,
			RegPreambleMsb = 0x20,
			RegPreambleLsb = 0x21,
			RegPayloadLength = 0x22,
			RegMaxPayloadLength = 0x23,
			RegHopPeriod = 0x24,
			// RegFifiRxByteAddr = 0x25
			RegModemConfig3 = 0x26,
			RegPpmCorrection = 0x27,
			// RegFeiMsb = 0x28
			// RegFeiMid = 0x29
			// RegFeiLsb = 0x2A
			// Reserved 0x2B
			// RegRssiWideband = 0x2C
			// Reserved 0x2D-0x30
			RegDetectOptimize = 0x31,
			// Reserved 0x32
			RegInvertIQ = 0x33,
			// Reserved 0x34-0x36
			RegDetectionThreshold = 0x37,
			// Reserved 0x38
			RegSyncWord = 0x39,
			RegDioMapping1 = 0x40,
			RegVersion = 0x42,

			MaxValue = RegVersion,
		}

		// RegOpMode mode flags
		private const byte RegOpModeLongRangeModeLoRa = 0b10000000;
		private const byte RegOpModeLongRangeModeFskOok = 0b00000000;
		private const byte RegOpModeLongRangeModeDefault = RegOpModeLongRangeModeFskOok;

		private const byte RegOpModeAcessSharedRegLoRa = 0b00000000;
		private const byte RegOpModeAcessSharedRegFsk = 0b01000000;
		private const byte RegOpModeAcessSharedRegDefault = RegOpModeAcessSharedRegLoRa;

		private const byte RegOpModeLowFrequencyModeOnHighFrequency = 0b00000000;
		private const byte RegOpModeLowFrequencyModeOnLowFrequency = 0b00001000;
		private const byte RegOpModeLowFrequencyModeOnDefault = RegOpModeLowFrequencyModeOnLowFrequency;

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

		// Frequency configuration magic numbers from Semtech SX127X specs
		private const double RH_RF95_FXOSC = 32000000.0;
		private const double RH_RF95_FSTEP = RH_RF95_FXOSC / 524288.0;

		// RegFrMsb, RegFrMid, RegFrLsb
		private const double FrequencyDefault = 434000000.0;

One constructor is for shields where the chip select pin is connected to one of the two standard lines CS0/CS1.

// Constructor for shields with chip select connected to CS0/CS1 e.g. Elecrow/Electronic tricks
		public Rfm9XDevice(ChipSelectPin chipSelectPin, int resetPinNumber, int interruptPinNumber)
		{
			RegisterManager = new RegisterManager(chipSelectPin);

			// Check that SX127X chip is present
			Byte regVersionValue = RegisterManager.ReadByte((byte)Registers.RegVersion);
			if (regVersionValue != RegVersionValueExpected)
			{
				throw new ApplicationException("Semtech SX127X not found");
			}

			GpioController gpioController = GpioController.GetDefault();

The other is for shields with the chip select connected to another pin (the chip select has to be set to one of the default pins even though I am implementing the drive logic in code

	// Constructor for shields with chip select not connected to CS0/CS1 (but needs to be configured anyway) e.g. Dragino
		public Rfm9XDevice(ChipSelectPin chipSelectPin, int chipSelectPinNumber, int resetPinNumber, int interruptPinNumber)
		{
			RegisterManager = new RegisterManager(chipSelectPin, chipSelectPinNumber);

			// Check that SX127X chip is present
			Byte regVersionValue = RegisterManager.ReadByte((byte)Registers.RegVersion);
			if (regVersionValue != RegVersionValueExpected)
			{
				throw new ApplicationException("Semtech SX127X not found");	
			}

			GpioController gpioController = GpioController.GetDefault();

The Initialise method has a large number of parameters (most of them can be ignored and defaults used). I only set registers if the configuration has been changed from the default value. This is fine for most settings, but some (like RegSymbTimeoutMsb & RegSymbTimeoutLsb span two registers and are combined with other settings.

public void Initialise(RegOpModeMode modeAfterInitialise, // RegOpMode
			double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
			bool paBoost = false, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
			bool ocpOn = true, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
			RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoostLF = false, bool lnaBoostHf = false, // RegLna
			RegModemConfigBandwidth bandwidth = RegModemConfigBandwidthDefault, RegModemConfigCodingRate codingRate = RegModemConfigCodingRateDefault, RegModemConfigImplicitHeaderModeOn implicitHeaderModeOn = RegModemConfigImplicitHeaderModeOnDefault, //RegModemConfig1
         RegModemConfig2SpreadingFactor spreadingFactor = RegModemConfig2SpreadingFactorDefault, bool txContinuousMode = false, bool rxPayloadCrcOn = false,
			ushort symbolTimeout = SymbolTimeoutDefault,
			ushort preambleLength = PreambleLengthDefault,
			byte payloadLength = PayloadLengthDefault,
			byte payloadMaxLength = PayloadMaxLengthDefault,
			byte freqHoppingPeriod = FreqHoppingPeriodDefault,
			bool lowDataRateOptimize = false, bool agcAutoOn = false,
			byte ppmCorrection = ppmCorrectionDefault,
			RegDetectOptimizeDectionOptimize detectionOptimize=RegDetectOptimizeDectionOptimizeDefault,
         bool invertIQ = false,
			RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
         byte syncWord = RegSyncWordDefault )
		{
			Frequency = frequency; // Store this away for RSSI adjustments
			RegOpModeModeCurrent = modeAfterInitialise;

			// Strobe Reset pin briefly to factory reset SX127X chip
			ResetGpioPin.Write(GpioPinValue.Low);
			Task.Delay(10);
			ResetGpioPin.Write(GpioPinValue.High);
			Task.Delay(10);

			// Put the device into sleep mode so registers can be changed
			SetMode(RegOpModeMode.Sleep);

			// Configure RF Carrier frequency
			if (frequency != FrequencyDefault)
			{
				byte[] bytes = BitConverter.GetBytes((long)(frequency / RH_RF95_FSTEP));
				RegisterManager.WriteByte((byte)Registers.RegFrMsb, bytes[2]);
				RegisterManager.WriteByte((byte)Registers.RegFrMid, bytes[1]);
				RegisterManager.WriteByte((byte)Registers.RegFrLsb, bytes[0]);
			}

Next step is add event handlers for inbound and outbound messages, then the finally split the device specific code into a stand alone library.

 

.Net MicroFramework LoRa library Part8

Receive Interrupt

After getting interrupts to work for outbound messages I changed the interrupt pin D10 mapping and the interrupt mask.

Getting this working with my RPI meant the process went relatively smoothly.

//---------------------------------------------------------------------------------
// Copyright (c) August 2018, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.NetMF.Rfm9X.ReceiveInterrupt
{
   using System;
   using System.Text;
   using System.Threading;
   using Microsoft.SPOT;
   using Microsoft.SPOT.Hardware;
   using SecretLabs.NETMF.Hardware.Netduino;

   public sealed class Rfm9XDevice
   {
      private const byte RegisterAddressReadMask = 0X7f;
      private const byte RegisterAddressWriteMask = 0x80;

      private SPI Rfm9XLoraModem = null;
      private OutputPort ResetGpioPin = null;
      private InterruptPort InterruptPin = null;

      public Rfm9XDevice(Cpu.Pin chipSelect, Cpu.Pin resetPin, Cpu.Pin interruptPin)
      {
         // Factory reset pin configuration
         ResetGpioPin = new OutputPort(Pins.GPIO_PIN_D9, true);
         ResetGpioPin.Write(false);
         Thread.Sleep(10);
         ResetGpioPin.Write(true);
         Thread.Sleep(10);

         this.Rfm9XLoraModem = new SPI(new SPI.Configuration(chipSelect, false, 0, 0, false, false, 2000, SPI.SPI_module.SPI1));

         InterruptPin = new InterruptPort(interruptPin, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeHigh);

         InterruptPin.OnInterrupt += InterruptPin_OnInterrupt;

         Thread.Sleep(100);
      }

      public Rfm9XDevice(Cpu.Pin chipSelect, Cpu.Pin reset)
      {
         // Factory reset pin configuration
         ResetGpioPin = new OutputPort(Pins.GPIO_PIN_D9, true);
         ResetGpioPin.Write(false);
         Thread.Sleep(10);
         ResetGpioPin.Write(true);
         Thread.Sleep(10);

         this.Rfm9XLoraModem = new SPI(new SPI.Configuration(chipSelect, false, 0, 0, false, false, 2000, SPI.SPI_module.SPI1));

         Thread.Sleep(100);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress };
         byte[] readBuffer = new byte[1];
         Debug.Assert(Rfm9XLoraModem != null);

         Rfm9XLoraModem.WriteRead(writeBuffer, readBuffer, 1);

         return readBuffer[0];
      }

      public ushort RegisterReadWord(byte address)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask };
         byte[] readBuffer = new byte[2];
         Debug.Assert(Rfm9XLoraModem != null);

         readBuffer[0] = RegisterReadByte(address);
         readBuffer[1] = RegisterReadByte(address += 1);

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

      public byte[] RegisterRead(byte address, int length)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask };
         byte[] readBuffer = new byte[length];
         Debug.Assert(Rfm9XLoraModem != null);

         for (byte index = 0; index < length; index++)
         {
            readBuffer[index] = RegisterReadByte(address += 1);
         }

         return readBuffer;
      }

      public void RegisterWriteByte(byte address, byte value)
      {
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
         Debug.Assert(Rfm9XLoraModem != null);

         Rfm9XLoraModem.Write(writeBuffer);
      }

      public void RegisterWriteWord(byte address, ushort value)
      {
         byte[] valueBytes = BitConverter.GetBytes(value);
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
         Debug.Assert(Rfm9XLoraModem != null);

         Rfm9XLoraModem.Write(writeBuffer);
      }

      public void RegisterWrite(byte address, byte[] bytes)
      {
         byte[] writeBuffer = new byte[1 + bytes.Length];
         Debug.Assert(Rfm9XLoraModem != null);

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

         Rfm9XLoraModem.Write(writeBuffer);
      }

      public void RegisterDump()
      {
         Debug.Print("---Registers 0x00 thru 0x42---");
         for (byte registerIndex = 0; registerIndex  4];

         // Mask off the upper 4 bits to get the rest of it.
         hexString += hexChars[singlebyte & 0x0F];

         return hexString;
      }

      private static string WordToHexString(ushort singleword)
      {
         string hexString = string.Empty;

         byte[] bytes = BitConverter.GetBytes(singleword);

         hexString += ByteToHexString(bytes[1]);

         hexString += ByteToHexString(bytes[0]);

         return hexString;
      }

      public class Program
      {
         public static void Main()
         {
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D2);
            byte MessageCount = byte.MinValue;

            // Put device into LoRa + Sleep mode
            rfm9XDevice.RegisterWriteByte(0x01, 0x80); // RegOpMode 

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

            rfm9XDevice.RegisterWriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

            rfm9XDevice.RegisterWriteByte(0x40, 0x0); // RegDioMapping1 0b00000000 DIO0 RxReady & TxReady

            rfm9XDevice.RegisterWriteByte(0x01, 0x85); // RegOpMode set LoRa & RxContinuous

            Thread.Sleep(Timeout.Infinite);
         }
      }
   }
}

In the Visual Studio debug output window I could see received packets

'Microsoft.SPOT.Debugger.CorDebug.dll' (Managed): Loaded 'C:\Program Files (x86)\Secret Labs\Netduino SDK\Assemblies\v4.3\le\SecretLabs.NETMF.Hardware.dll', Symbols loaded.
The thread '' (0x2) has exited with code 0 (0x0).
RegIrqFlags 50
Receive-Message
Received 28 byte message Hello W10 IoT Core LoRa! 247
RegIrqFlags 50
Receive-Message
Received 28 byte message Hello W10 IoT Core LoRa! 246
RegIrqFlags 50
Receive-Message

Next, I’ll integrate the .NetMF receive and transmit interrupt examples, and then refactor the code to extract the RFM9X code into a reusable module.