.Net MicroFramework LoRa library Part1

After getting my Windows 10 IoT Core RFM9X library well under way I figured that writing a library for .NetMF devices (like Netduino and Ingenuity Micro ones) shouldn’t be “rocket science”.

To get started I used a Dragino LoRa shield for Arduino which looked compatible with my Netduino devices. I was initially worried that the shield might not work with a 3v3 device but I tested it with a Seeeduino Lite (which has a switch to select 3v3 or 5v operation) and it worked fine.

The shield uses D10 for chip select, D2 for RFM9X DI0 interrupt and D9 for Reset.
Lora_sheild_sch
The shield ships with the SPI lines configured for ICSP so the three jumpers diagonally across the shield from the antenna connector need to be swapped to the side closest to the edge of the shield.
NetduinoDraginoShield
First step was to confirm I could (using the Netduino SPI interface and .NetMF library)read a couple of the Semtech SX1276 registers. I implemented both “automagic” and manual chip select operation in my test harness.

//---------------------------------------------------------------------------------
// 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.NetMF.Rfm9X.DraginoShield
{
   using System;
   using System.Threading;
   using Microsoft.SPOT;
   using Microsoft.SPOT.Hardware;
   using SecretLabs.NETMF.Hardware;
   using SecretLabs.NETMF.Hardware.Netduino;

   public class Program
   {

      public static void Main()
      {
         //OutputPort reset = new OutputPort(Pins.GPIO_PIN_D9, true);
         OutputPort chipSelect = null;
         //chipSelect = new OutputPort(Pins.GPIO_PIN_D10, true);
         //SPI spiPort = new SPI(new SPI.Configuration(Pins.GPIO_NONE, false, 0, 0, true, true, 500, SPI.SPI_module.SPI1));
         SPI spiPort = new SPI(new SPI.Configuration(Pins.GPIO_PIN_D10, false, 0, 0, false, true, 500, SPI.SPI_module.SPI1));

         Thread.Sleep(100);

         while (true)
         {
            //byte[] writeBuffer = new byte[] { 0x42 }; // RegVersion exptecing 0x12
            byte[] writeBuffer = new byte[] { 0x06 }; // RegFreqMsb expecting 0x6C
            //byte[] writeBuffer = new byte[] { 0x07 }; // RegFreqMid expecting 0x80
            //byte[] writeBuffer = new byte[] { 0x08 }; // RegFreqLsb expecting 0x00
            byte[] readBuffer = new byte[1];

            if (chipSelect != null)
            {
               chipSelect.Write(false);
            }
            spiPort.WriteRead(writeBuffer, readBuffer,1);
            if (chipSelect != null)
            {
               chipSelect.Write(true);
            }

            Debug.Print("Value = 0x" + BytesToHexString(readBuffer));

            Thread.Sleep(1000);
         }
      }

      private static string BytesToHexString(byte[] bytes)
      {
         string hexString = string.Empty;

         // Create a character array for hexidecimal conversion.
         const string hexChars = "0123456789ABCDEF";

         // Loop through the bytes.
         for (byte b = 0; b  0)
               hexString += "-";

            // Grab the top 4 bits and append the hex equivalent to the return string.
            hexString += hexChars[bytes[b] >> 4];

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

         return hexString;
      }
   }
}

I could successfully read the RegVersion and default frequency values

'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).
Value = 0x6C
Value = 0x6C
Value = 0x6C
The program '[43] Micro Framework application: Managed' has exited with code 0 (0x0).

 

RFM95/96/97/98 shield library Part9

RegisterManager Extraction Refactor

The code was getting really repetitive and nasty so figured it was time to extract the register manager class into a separate module. In an abandoned refactor I tried introducing an enumeration for the RFM9X Register addresses and extract the RegisterManager in one go but that caused too much churn.

//---------------------------------------------------------------------------------
// 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.Rfm9x.RefactorRegisterManager
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Threading.Tasks;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Gpio;

	sealed class Rfm9XDevice
	{
		private GpioPin InterruptGpioPin = null;
		public RegisterManager RegisterManager = null; // Future refactor this will be made private

		public Rfm9XDevice(byte chipSelectPin, byte resetPin, byte interruptPin)
		{
			RegisterManager = new RegisterManager(chipSelectPin);

			// Setup the reset and interrupt pins
			GpioController gpioController = GpioController.GetDefault();

			// Reset pin configuration then strobe briefly to factory reset
			GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			resetGpioPin.Write(GpioPinValue.Low);
			Task.Delay(10);
			resetGpioPin.Write(GpioPinValue.High);
			Task.Delay(10);

			// Interrupt pin for RX message, TX done etc. notifications
			InterruptGpioPin = gpioController.OpenPin(interruptPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Input);

			InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;
		}

		public void RegisterDump()
		{
			RegisterManager.Dump(0x0, 0x40);
		}

		private void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
		{
			if (args.Edge != GpioPinEdge.RisingEdge)
			{
				return;
			}

			byte IrqFlags = this.RegisterManager.ReadByte(0x12); // RegIrqFlags
			Debug.WriteLine(string.Format("RegIrqFlags {0}", Convert.ToString(IrqFlags, 2).PadLeft(8, '0')));

			if ((IrqFlags & 0b01000000) == 0b01000000)  // RxDone
			{
				Debug.WriteLine("Receive-Message");
				byte currentFifoAddress = this.RegisterManager.ReadByte(0x10); // RegFifiRxCurrent
				this.RegisterManager.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

				byte numberOfBytes = this.RegisterManager.ReadByte(0x13); // RegRxNbBytes

				// Allocate buffer for message
				byte[] messageBytes = new byte[numberOfBytes];

				for (int i = 0; i < numberOfBytes; i++)
				{
					messageBytes[i] = this.RegisterManager.ReadByte(0x00); // RegFifo
				}

				string messageText = UTF8Encoding.UTF8.GetString(messageBytes);
				Debug.WriteLine("Received {0} byte message {1}", messageBytes.Length, messageText);
			}

			if ((IrqFlags & 0b00001000) == 0b00001000)  // TxDone
			{
				this.RegisterManager.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
				Debug.WriteLine("Transmit-Done");
			}

			this.RegisterManager.WriteByte(0x12, 0xff);// RegIrqFlags
		}
	}

	public sealed class StartupTask : IBackgroundTask
	{
		private const byte ChipSelectLine = 25;
		private const byte ResetLine = 17;
		private const byte InterruptLine = 4;
		private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectLine, ResetLine, InterruptLine);
		private byte NessageCount = Byte.MaxValue;

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			// Put device into LoRa + Sleep mode
			rfm9XDevice.RegisterManager.WriteByte(0x01, 0b10000000); // RegOpMode 

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

			rfm9XDevice.RegisterManager.WriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

			// More power PA Boost
			rfm9XDevice.RegisterManager.WriteByte(0x09, 0b10000000); // RegPaConfig

			rfm9XDevice.RegisterManager.WriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady

			rfm9XDevice.RegisterManager.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

			rfm9XDevice.RegisterDump();

			while (true)
			{
				rfm9XDevice.RegisterManager.WriteByte(0x0E, 0x0); // RegFifoTxBaseAddress 

				// Set the Register Fifo address pointer
				rfm9XDevice.RegisterManager.WriteByte(0x0D, 0x0); // RegFifoAddrPtr 

				string messageText = "W10 IoT Core LoRa! " + NessageCount.ToString();
				NessageCount -= 1;

				// load the message into the fifo
				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				foreach (byte b in messageBytes)
				{
					rfm9XDevice.RegisterManager.WriteByte(0x0, b); // RegFifo
				}

				// Set the length of the message in the fifo
				rfm9XDevice.RegisterManager.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
				rfm9XDevice.RegisterManager.WriteByte(0x01, 0b10000011); // RegOpMode 

				Debug.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);

				Task.Delay(10000).Wait();
			}
		}
	}
}

I also started some basic modifications to the register manager class to make it reusable for other SPI devices. In a future refactor the slave select logic will need to be made more flexible to support shields which use the standard CS0/CS1 pins

//---------------------------------------------------------------------------------
// 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.Rfm9x.RefactorRegisterManager
{
	using System;
	using System.Diagnostics;
	using System.Runtime.InteropServices.WindowsRuntime;
	using Windows.Devices.Gpio;
	using Windows.Devices.Spi;

	public sealed class RegisterManager
	{
		private GpioPin ChipSelectGpioPin = null;
		private SpiDevice Device = null;
		private const byte RegisterAddressReadMask = 0X7f;
		private const byte RegisterAddressWriteMask = 0x80;

		public RegisterManager(int chipSelectPin)
		{
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(0)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,
			};

			// Chip select pin configuration
			GpioController gpioController = GpioController.GetDefault();
			ChipSelectGpioPin = gpioController.OpenPin(chipSelectPin);
			ChipSelectGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			Device = spiController.GetDevice(settings);
		}

		public Byte ReadByte(byte address)
		{
			byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask };
			byte[] readBuffer = new byte[1];
			Debug.Assert(Device != null);

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Device.Write(writeBuffer);
			Device.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			return readBuffer[0];
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Device.Write(writeBuffer);
			Device.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

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

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Device.Write(writeBuffer);
			Device.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			return readBuffer;
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Device.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Device.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

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

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Device.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		public void Dump(byte start, byte finish)
		{
			Debug.Assert(Device != null);

			Debug.WriteLine("Register dump");

			for (byte registerIndex = start; registerIndex <= finish; registerIndex++)
			{
				byte registerValue = this.ReadByte(registerIndex);

				Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
			}
		}
	}
}

I think the next steps will be introducing an enumeration for the SX127X registers, moving the initialisation code into the Rfm9XDevice class, along with the interrupt handler.

After looking at other Windows 10 IoT Core and Arduino RFM9X libraries I think I'll try and keep the number of getter/setters to a minimum and focus on configuring the device at startup. The initialise method may end up having a lot of parameters but I should be able to hide ones a user doesn't need for their application with named and optional parameters.

RFM95/96/97/98 shield library Part7

Transmit Interrupt

Starting with the TransmitBasic sample application I modified the code so that a hardware interrupt (specified in RegDioMapping1) was generated on TxDone (FIFO Payload Transmission completed).

My first couple of attempts didn’t work as I had the RegDioMapping1 wrong and the RegIrqFlags bitmasks in the interrupt handler weren’t consistent (need to get rid of the magic numbers & bitmasks soon)

The application inserts a message into the RFM95 transmit FIFO every 10 seconds with confirmation of transmission displayed shortly afterwards

//---------------------------------------------------------------------------------
// 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.Rfm9x.TransmitInterrupt
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Runtime.InteropServices.WindowsRuntime;
	using System.Threading.Tasks;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;
	using Windows.Devices.Gpio;

	public sealed class Rfm9XDevice
	{
		private SpiDevice Rfm9XLoraModem = null;
		private GpioPin ChipSelectGpioPin = null;
		private GpioPin InterruptGpioPin = null;
		private const byte RegisterAddressReadMask = 0X7f;
		private const byte RegisterAddressWriteMask = 0x80;

		public Rfm9XDevice(byte chipSelectPin, byte resetPin, byte interruptPin)
		{
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(0)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,
			};

			// Chip select pin configuration
			GpioController gpioController = GpioController.GetDefault();
			ChipSelectGpioPin = gpioController.OpenPin(chipSelectPin);
			ChipSelectGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			// Reset pin configuration to do factory reset
			GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			resetGpioPin.Write(GpioPinValue.Low);
			Task.Delay(10);
			resetGpioPin.Write(GpioPinValue.High);
			Task.Delay(10);

			// Interrupt pin for RX message & TX done notification
			InterruptGpioPin = gpioController.OpenPin(interruptPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Input);

			InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;

			Rfm9XLoraModem = spiController.GetDevice(settings);
		}

		private void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
		{
			if (args.Edge != GpioPinEdge.RisingEdge)
			{
				return;
			}

			byte IrqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
			Debug.WriteLine(string.Format("RegIrqFlags {0}", Convert.ToString(IrqFlags, 2).PadLeft(8, '0')));

			if ((IrqFlags & 0b00001000) == 0b00001000)  // TxDone
			{
				Debug.WriteLine("Transmit-Done");
			}

			this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			Rfm9XLoraModem.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			return readBuffer[0];
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			Rfm9XLoraModem.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			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);

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			Rfm9XLoraModem.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			return readBuffer;
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		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);

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		public void RegisterWrite(byte address, [ReadOnlyArray()] 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;

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		public void RegisterDump()
		{
			Debug.WriteLine("Register dump");
			for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
			{
				byte registerValue = this.RegisterReadByte(registerIndex);

				Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
			}
		}
	}

	public sealed class StartupTask : IBackgroundTask
	{
		private const byte ChipSelectLine = 25;
		private const byte ResetLine = 17;
		private const byte InterruptLine = 4;
		private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectLine, ResetLine, InterruptLine);

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			// Put device into LoRa + Sleep mode
			rfm9XDevice.RegisterWriteByte(0x01, 0b10000000); // RegOpMode 

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

			// More power PA Boost
			rfm9XDevice.RegisterWriteByte(0x09, 0b10000000); // RegPaConfig

			// Interrupt on TxDone
			rfm9XDevice.RegisterWriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 TxDone

			while (true)
			{
				// Set the Register Fifo address pointer
				rfm9XDevice.RegisterWriteByte(0x0E, 0x80); // RegFifoTxBaseAddress 

				// Set the Register Fifo address pointer
				rfm9XDevice.RegisterWriteByte(0x0D, 0x0); // RegFifoAddrPtr 

				string messageText = "Hello LoRa!";

				// load the message into the fifo
				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				foreach (byte b in messageBytes)
				{
					rfm9XDevice.RegisterWriteByte(0x0, b); // RegFifo
				}

				// Set the length of the message in the fifo
				rfm9XDevice.RegisterWriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
				Debug.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);
				rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

				Task.Delay(10000).Wait();
			}
		}
	}
}

The output in the debug window

Sending 11 bytes message Hello LoRa!
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\TransmitInterrupt-uwpVS.Debug_ARM.Bryn.Lewis\System.Runtime.Extensions.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
RegIrqFlags 00001000
Transmit-Done
The thread 0xe38 has exited with code 0 (0x0).
Sending 11 bytes message Hello LoRa!
RegIrqFlags 00001000
Transmit-Done
The thread 0x92c has exited with code 0 (0x0).
Sending 11 bytes message Hello LoRa!
RegIrqFlags 00001000
Transmit-Done
The thread 0xcc4 has exited with code 0 (0x0).
Sending 11 bytes message Hello LoRa!
RegIrqFlags 00001000
Transmit-Done
The thread 0xb54 has exited with code 0 (0x0).

On the Arduino test client the serial monitor displayed

ending HeLoRa World! 0
Sending HeLoRa World! 2
Sending HeLoRa World! 4
Sending HeLoRa World! 6
Sending HeLoRa World! 8
Sending HeLoRa World! 10
Message: Hello LoRa!
RSSI: -67
Snr: 9.00

Sending HeLoRa World! 12
Sending HeLoRa World! 14
Sending HeLoRa World! 16
Sending HeLoRa World! 18
Message: Hello LoRa!
RSSI: -67
Snr: 9.50

Sending HeLoRa World! 20
Sending HeLoRa World! 22

The next step is most probably either chopping apart the existing code, modularising it,  and removing the magic numbers etc. or getting bidirectional interrupt driven connectivity working.

RFM95/96/97/98 shield library Part6

Receive Interrupt

The HopeRF datasheet coverage of how the hardware interrupts should be configured (RegDioMapping1 & RegDioMapping2) was a bit confusing and some of the other values where wrong e.g. RegVersion returned 0x12 rather than 0x11.

I figured that getting interrupts working was a good choice to do next while the code was still relatively small. From the LMIC LoRaWAN stack implementations I have looked at the mapping of digital pins in hardware and software appeared critical.

I modified my test application and set what I though was the correct RegDioMapping1 for DIO0 but the application didn’t work the way I expected. After some searching then tinkering I went back and checked my approach against the Semtech SX1276 datasheet. I found the byte ordering was backwards to what I assumed (The two DIO0 bits were bits 7&6 of RegDioMapping1 not bits 1&0)

I also added an extra parameter to the constructor to pass in the interrupt pin number, replaced the message sending code, with an infinite sleep, then wired up the interrupt handler. In this proof of concept the interrupt handler just displays the received message which is a bit fugly.

//---------------------------------------------------------------------------------
// 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.Rfm9x.ReceiveInterrupt
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Runtime.InteropServices.WindowsRuntime;
	using System.Threading.Tasks;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;
	using Windows.Devices.Gpio;

	public sealed class Rfm9XDevice
	{
		private SpiDevice Rfm9XLoraModem = null;
		private GpioPin ChipSelectGpioPin = null;
		private GpioPin InterruptGpioPin = null;
		private const byte RegisterAddressReadMask = 0X7f;
		private const byte RegisterAddressWriteMask = 0x80;

		public Rfm9XDevice(byte chipSelectPin, byte resetPin, byte interruptPin)
		{
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(0)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,
			};

			// Chip select pin configuration
			GpioController gpioController = GpioController.GetDefault();
			ChipSelectGpioPin = gpioController.OpenPin(chipSelectPin);
			ChipSelectGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			// Reset pin configuration to do factory reset
			GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			resetGpioPin.Write(GpioPinValue.Low);
			Task.Delay(10);
			resetGpioPin.Write(GpioPinValue.High);
			Task.Delay(10);

			// Interrupt pin for RX message & TX done notification
			InterruptGpioPin = gpioController.OpenPin(interruptPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Input);

			InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;

			Rfm9XLoraModem = spiController.GetDevice(settings);
		}

		private void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
		{
			if (args.Edge != GpioPinEdge.RisingEdge)
			{
				return;
			}

			byte IrqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
			Debug.WriteLine( string.Format("RegIrqFlags {0}", Convert.ToString(IrqFlags, 2).PadLeft(8, '0')));
			if ((IrqFlags & 0b01000000) == 0b01000000)  // RxDone
			{
				Debug.WriteLine("Receive-Message");
				byte currentFifoAddress = this.RegisterReadByte(0x10); // RegFifiRxCurrent
				this.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

				byte numberOfBytes = this.RegisterReadByte(0x13); // RegRxNbBytes

				// Allocate buffer for message
				byte[] messageBytes = new byte[numberOfBytes];

				for (int i = 0; i < numberOfBytes; i++)
				{
					messageBytes[i] = this.RegisterReadByte(0x00); // RegFifo
				}

				string messageText = UTF8Encoding.UTF8.GetString(messageBytes);
				Debug.WriteLine("Received {0} byte message {1}", messageBytes.Length, messageText);
			}

			this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			Rfm9XLoraModem.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			return readBuffer[0];
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			Rfm9XLoraModem.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			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);

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			Rfm9XLoraModem.Read(readBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);

			return readBuffer;
		}

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

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		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);

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		public void RegisterWrite(byte address, [ReadOnlyArray()] 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;

			ChipSelectGpioPin.Write(GpioPinValue.Low);
			Rfm9XLoraModem.Write(writeBuffer);
			ChipSelectGpioPin.Write(GpioPinValue.High);
		}

		public void RegisterDump()
		{
			Debug.WriteLine("Register dump");
			for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
			{
				byte registerValue = this.RegisterReadByte(registerIndex);

				Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
			}
		}
	}

	public sealed class StartupTask : IBackgroundTask
	{
		private const byte ChipSelectLine = 25;
		private const byte ResetLine = 17;
		private const byte InterruptLine = 4;
		private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectLine, ResetLine, InterruptLine);

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			// Put device into LoRa + Sleep mode
			rfm9XDevice.RegisterWriteByte(0x01, 0b10000000); // 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, 0b00000000); // RegDioMapping1 0b00000000 DIO0 RxReady & TxReady

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

			rfm9XDevice.RegisterDump();

			Debug.WriteLine("Receive-Wait");
			Task.Delay(-1).Wait();
		}
	}
}

The output in the output debug window looked like this

RegIrqFlags 01010000
Receive-Message
Received 15 byte message HeLoRa World! 0
RegIrqFlags 01010000
Receive-Message
Received 15 byte message HeLoRa World! 2
RegIrqFlags 01010000
Receive-Message
Received 15 byte message HeLoRa World! 4
RegIrqFlags 01010000
Receive-Message
Received 15 byte message HeLoRa World! 6

Next step will be wiring up the transmit done interrupt.

 

Windows 10 IoT Core LoRa library

I have a pair of Windows 10 IoT Core nRF24L01 field gateway projects, one for AdaFruit.IO and the other for Azure IoTHub (Including Azure IoT Central). I use these field gateways for small scale indoor and outdoor deployments.

For larger systems e.g a school campus I was looking for something with a bit more range (line of site + in building penetration) and clients with lower power consumption (suitable for long term battery or solar power).

Other makers had had success with RFM69(proprietary) and RFM9X (LoRA) based devices and shields/hats so I had a look at both technologies.

To kick things off I purchased

I then did some searching and downloaded two commonly used libraries

Initially I trialled the emmellsoft Windows 10 IoT Core Dragino.LoRa code on a couple of Raspberry PI devices.

RPIDraginoP2P

After updating the Windows 10 Min/Max versions, plus the NuGet packages and setting the processor type to ARM the code compiled, downloaded and ran which was a pretty good start.

I could see messages being transmitted and received by the two devices

Packet RSSI: -33, RSSI: -91, SNR: 8, Length: 5
Message Received: CRC OK, Rssi=-91, PacketRssi=-33, PacketSnr=8, Buffer:[55, ff, 00, aa, 01], 2018-07-30 09:27:48
Successfully sent in 110 milliseconds.
Packet RSSI: -15, RSSI: -100, SNR: 9.2, Length: 5
Message Received: CRC OK, Rssi=-100, PacketRssi=-15, PacketSnr=9.2, Buffer:[55, ff, 00, aa, 02], 2018-07-30 09:27:53
Successfully sent in 36 milliseconds.
Packet RSSI: -35, RSSI: -101, SNR: 9, Length: 5
Message Received: CRC OK, Rssi=-101, PacketRssi=-35, PacketSnr=9, Buffer:[55, ff, 00, aa, 03], 2018-07-30 09:27:58
Successfully sent in 36 milliseconds.

I added my first attempt at device configuration for New Zealand (based on EU settings) in Dragino.LoRa\Radio\TransceiverSettings.cs

public static readonly TransceiverSettings ANZ915 = new TransceiverSettings(
             RadioModemKind.Lora,
             915000000,
             BandWidth.BandWidth_125_00_kHz,
             SpreadingFactor.SF7,
             CodingRate.FourOfFive,
             8,
             true,
             false,
             LoraSyncWord.Public);

The LoraSyncWord.Public would turn out to be a problem later!

Then I modified the sender and receiver sample application MainPage.xaml.cs files to reference my settings

private static TransceiverSettings GetRadioSettings()
{
   // *********************************************************************************************
   // #1/2. YOUR EDITING IS REQUIRED HERE!
   //
   // Choose transeiver settings:
   // *********************************************************************************************

   return TransceiverSettings.Standard.ANZ915;
}

I modified one of the RadioHead sample Arduino applications (centre frequency) and deployed it to a LoRa MiniDev device. I could see messages getting sent but they were not getting received by the RPI(s).

So I dumped the registers for the SX127X device in the HopeRF RFM95 module on both devices,

DraginoLoraMinDev

From the device on RPI Hat

SX1276/77/78/79 detected, starting.
1-85
2-1A
3-B
4-0
5-52
6-E4
7-C0
8-0
9-85
A-9
B-2B
C-23
D-0
E-80
F-0
10-0
11-0
12-0
13-0
14-0
15-0
16-0
17-0
18-4
19-0
1A-0
1B-42
1C-0
1D-72
1E-74
1F-9F
20-0
21-8
22-1
23-FF
24-0
25-0
26-4
27-0
28-0
29-0
2A-0
2B-0
2C-9
2D-50
2E-14
2F-45
30-55
31-C3
32-5
33-27
34-1C
35-A
36-3
37-A
38-42
39-34
The LoRa transceiver is initiated successfully.

I printed out the Radiohead and emmellsoft registers then manually compared them using the SX1275 datasheet for reference.

I found the 3 registers which contain the MSB, ISB and LSB for the centre frequency weren’t being calculated correctly (checked this by changing the frequency to 434MHz and comparing the register values to the worked example in the datasheet).

I manually “brute forced” the centre frequency registers in LoRaTransceiver.cs Init() and the RPI could then detect a signal but couldn’t decode the messages.

I went back to the Register dumps and found the SyncWord (odd name as it is a byte) were different. After updating the RPI settings the devices could exchange packets..

const double RH_RF95_FXOSC = 32000000.0;
const double RH_RF95_FSTEP = RH_RF95_FXOSC / 524288.0;
long frf = (long)(Settings.Frequency / RH_RF95_FSTEP);

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

byte[] x6 = { bytes[2] };
RegisterManager.WriteRegister(6, x6);
byte[] x7 = { bytes[1] };
RegisterManager.WriteRegister(7, x7);
byte[] x8 = { bytes[0] };
RegisterManager.WriteRegister(8, x8);

RegisterManager.Write(new LoraRegisterSyncWord(Settings.LoraSyncWord.Value));

This was not a long term solution, lots of code, and register setting changes with limited explanation…