.NET Core 5 SX127X library Part3

Transmit Basic

Next step was proving I could send a message to an Arduino device running the LoRaSimpleNode example from the SandeepMistry Arduino LoRa library.

Seeeduino V4.2 with Dragino Tech

My first attempt didn’t have much range so I tried turning on the PA_BOOST pin (in RegPaConfig) which improved the range and Received Signal Strength Indication (RSSI).

Arduino Monitor displaying received messages

There was quite a bit of code to configure the SX127X to Transmit messages. I had to put the device into sleep mode (RegOpMode), set the frequency to 915MHz(RegFrMsb, RegFrMid, RegFrLsb), and set the output power(RegPaConfig). Then for each message reset the pointer to the start of the message buffer(RegFifoTxBaseAddress, RegFifoAddrPtr), load the message into the buffer (RegPayloadLength), then turn on the transmitter(RegOpMode), and then finally poll (RegIrqFlags) until the message was sent(TxDone).

class Program
{
	static void Main(string[] args)
	{
		Byte regOpMode;
		ushort preamble;
		byte[] frequencyBytes;
		// Uptronics has no reset pin uses CS0 or CS1
		//SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 0); 
		//SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 1); 

		// M2M device has reset pin uses non standard chip select 
		//SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 0, chipSelectLogicalPinNumber: 25, resetPin: 17);
		SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 1, chipSelectLogicalPinNumber: 25, resetPin: 17);

		// Put device into LoRa + Sleep mode
		sX127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

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

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

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

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

			string messageText = "Hello LoRa from .NET Core!";

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

			// Set the length of the message in the fifo
			sX127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength

			Debug.WriteLine($"Sending {messageBytes.Length} bytes message \"{messageText}\"");
			/// Set the mode to LoRa + Transmit
			sX127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode 

			// Wait until send done, no timeouts in PoC
			Debug.WriteLine("Send-wait");
			byte IrqFlags = sX127XDevice.ReadByte(0x12); // RegIrqFlags
			while ((IrqFlags & 0b00001000) == 0)  // wait until TxDone cleared
			{
				Thread.Sleep(10);
				IrqFlags = sX127XDevice.ReadByte(0x12); // RegIrqFlags
				Debug.Write(".");
			}
			Debug.WriteLine("");
			sX127XDevice.WriteByte(0x12, 0b00001000); // clear TxDone bit
			Debug.WriteLine("Send-Done");

			Thread.Sleep(30000);
		}
	}
}

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Memory.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
....
Send-Done
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
...
Send-Done
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
...
Send-Done
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
...
Send-Done
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
...
Send-Done
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
...
Send-Done
Sending 26 bytes message "Hello LoRa from .NET Core!"
Send-wait
...
Send-Done

Summary

In this iteration I sent a message from my  .Net Core 5 dotnet/iot powered Raspberry PI to a Dragino LoRa Shield 915MHz on a Seeeduino V4.2 device. Every so often the payload was corrupted becuase I had not enabled the payload Cyclic Redundancy Check(CRC) functionality.

.NET Core 5 SX127X library Part2

Register Reading and Writing

Now that Serial Peripheral(SPI) connectivity for my .Net Core 5 dotnet/iot SX127X library is working, the next step is to build a “generic” class for my two reference Rapsberry Pi HATS.

The Uputronics Raspberry PiZero LoRa(TM) Expansion Board supports both standard Chip Select(CS) lines (switch selectable which is really useful) and the reset pin is not connected.

Uputronics Raspberry PIZero LoRa Expansion board on a Raspberry PI 3 device

The M2M 1 Channel LoRaWan Gateway Shield for Raspberry PI has a “non-standard” CS pin and the reset pin is connected to pin 17.

M2M Single channel shield on Raspberry Pi 3 Device

In my previous post the spiDevice.TransferFullDuplex method worked for a standard CS line (CS0 or CS1), and for a non-standard CS pin, though the CS line configured in SpiConnectionSettings was “unusable” by other applications.

static void Main(string[] args)
{
	Byte regOpMode;
	ushort preamble;
	byte[] frequencyBytes;
	// Uptronics has no reset pin uses CS0 or CS1
	//SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 0); 
	//SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 1); 

	// M2M device has reset pin uses non standard chip select 
	//SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 0, chipSelectLogicalPinNumber: 25, resetPin: 17);
	SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 1, chipSelectLogicalPinNumber:25, resetPin: 17);

	Console.WriteLine("In FSK mode");
	sX127XDevice.RegisterDump();


	Console.WriteLine("Read RegOpMode (read byte)");
	regOpMode = sX127XDevice.ReadByte(0x1);
	Debug.WriteLine($"RegOpMode 0x{regOpMode:x2}");

	Console.WriteLine("Set LoRa mode and sleep mode (write byte)");
	sX127XDevice.WriteByte(0x01, 0b10000000);

	Console.WriteLine("Read RegOpMode (read byte)");
	regOpMode = sX127XDevice.ReadByte(0x1);
	Debug.WriteLine($"RegOpMode 0x{regOpMode:x2}");


	Console.WriteLine("In LoRa mode");
	sX127XDevice.RegisterDump();


	Console.WriteLine("Read the preamble (read word)"); // Should be 0x08
	preamble = sX127XDevice.ReadWordMsbLsb(0x20);
	Debug.WriteLine($"Preamble 0x{preamble:x2} - Bits {Convert.ToString(preamble, 2).PadLeft(16, '0')}");

	Console.WriteLine("Set the preamble to 0x8000 (write word)");
	sX127XDevice.WriteWordMsbLsb(0x20, 0x8000);

	Console.WriteLine("Read the preamble (read word)"); // Should be 0x08
	preamble = sX127XDevice.ReadWordMsbLsb(0x20);
	Debug.WriteLine($"Preamble 0x{preamble:x2} - Bits {Convert.ToString(preamble, 2).PadLeft(16, '0')}");


	Console.WriteLine("Read the centre frequency"); // RegFrfMsb 0x6c RegFrfMid 0x80 RegFrfLsb 0x00 which is 433MHz
	frequencyBytes = sX127XDevice.ReadBytes(0x06, 3);
	Console.WriteLine($"Frequency Msb 0x{frequencyBytes[0]:x2} Mid 0x{frequencyBytes[1]:x2} Lsb 0x{frequencyBytes[2]:x2}");

	Console.WriteLine("Set the centre frequency"); 
	byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 };
	sX127XDevice.WriteBytes(0x06, frequencyWriteBytes);

	Console.WriteLine("Read the centre frequency"); // RegFrfMsb 0xE4 RegFrfMid 0xC0 RegFrfLsb 0x00 which is 915MHz
	frequencyBytes = sX127XDevice.ReadBytes(0x06, 3);
	Console.WriteLine($"Frequency Msb 0x{frequencyBytes[0]:x2} Mid 0x{frequencyBytes[1]:x2} Lsb 0x{frequencyBytes[2]:x2}");


	sX127XDevice.RegisterDump();

	// Sleep forever
	Thread.Sleep(-1);
}

I use RegisterDump multiple times to show the updates working.

...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
In FSK mode
Register dump
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x01 - Value 0X09 - Bits 00001001
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x07 - Value 0X80 - Bits 10000000
...
Register 0x1f - Value 0X40 - Bits 01000000
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X00 - Bits 00000000
Register 0x22 - Value 0X00 - Bits 00000000
...
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010

Read RegOpMode (read byte)
RegOpMode 0x09
Set LoRa mode and sleep mode (write byte)
Read RegOpMode (read byte)
RegOpMode 0x80
In LoRa mode
Register dump
Register 0x00 - Value 0Xdf - Bits 11011111
Register 0x01 - Value 0X80 - Bits 10000000
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x07 - Value 0X80 - Bits 10000000
...
Register 0x1f - Value 0X64 - Bits 01100100
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X08 - Bits 00001000
Register 0x22 - Value 0X01 - Bits 00000001
...
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010

Read the preamble (read word)
Preamble 0x08 - Bits 0000000000001000
Set the preamble to 0x8000 (write word)
Read the preamble (read word)
Preamble 0x8000 - Bits 1000000000000000
Read the centre frequency
Frequency Msb 0x6c Mid 0x80 Lsb 0x00
Set the centre frequency
Read the centre frequency
Frequency Msb 0xe4 Mid 0xc0 Lsb 0x00
Register dump
Register 0x00 - Value 0Xb9 - Bits 10111001
Register 0x01 - Value 0X80 - Bits 10000000
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0Xe4 - Bits 11100100
Register 0x07 - Value 0Xc0 - Bits 11000000
...
Register 0x1f - Value 0X64 - Bits 01100100
Register 0x20 - Value 0X80 - Bits 10000000
Register 0x21 - Value 0X00 - Bits 00000000
Register 0x22 - Value 0X01 - Bits 00000001
...
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000

Summary

In this iteration I added support for resetting the SX127X module (where supported by the Raspberry PI HAT) and an spiDevice.TransferFullDuplex based implementation for reading/writing individual bytes/words and reading/writing arrays of bytes.

public byte[] ReadBytes(byte registerAddress, byte length)
{
	Span<byte> writeBuffer = stackalloc byte[length + 1];
	Span<byte> readBuffer = stackalloc byte[writeBuffer.Length];

	if (SX127XTransceiver == null)
	{
		throw new ApplicationException("SX127XDevice is not initialised");
	}

	writeBuffer[0] = registerAddress &= RegisterAddressReadMask;

	if (this.ChipSelectLogicalPinNumber != 0)
	{
		gpioController.Write(ChipSelectLogicalPinNumber, PinValue.Low);
	}

	this.SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

	if (this.ChipSelectLogicalPinNumber != 0)
	{
		gpioController.Write(ChipSelectLogicalPinNumber, PinValue.High);
	}

	return readBuffer[1..readBuffer.Length].ToArray();
}

I used stackalloc so the memory for the writeBuffer and readBuffer doesn’t have to be tidied up by the .Net Garbage Collector(GC).

public void WriteBytes(byte address, byte[] bytes)
{
	Span<byte> writeBuffer = stackalloc byte[bytes.Length + 1];
	Span<byte> readBuffer = stackalloc byte[writeBuffer.Length];

	if (SX127XTransceiver == null)
	{
		throw new ApplicationException("SX127XDevice is not initialised");
	}

	writeBuffer[0] = address |= RegisterAddressWriteMask;
	for (byte index = 0; index < bytes.Length; index++)
	{
		writeBuffer[index + 1] = bytes[index];
	}

	if (this.ChipSelectLogicalPinNumber != 0)
	{
		gpioController.Write(ChipSelectLogicalPinNumber, PinValue.Low);
	}

	this.SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);

	if (this.ChipSelectLogicalPinNumber != 0)
	{
		gpioController.Write(ChipSelectLogicalPinNumber, PinValue.High);
	}
}

In the WriteBytes method copying the bytes from the bytes[] parameter to the span with a for loop is a bit ugly but I couldn’t find a better way. One odd thing I noticed was that if I wrote a lot of debug output the text would be truncated in the output window

Frequency Msb 0xe4 Mid 0xc0 Lsb 0x00
Register dump
Register 0x00 - Value 0Xb9 - Bits 10111001
Register 0x01 - Value 0X80 - Bits 10000000
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0Xe4 - Bits 11100100
Register 0x07 - Value 0Xc0 - Bits 11000000
Register 0x08 - Value 0X00 - Bits 00000000
Register 0x09 - Value 0X4f - Bits 01001111
Register 0x0a - Value 0X09 - Bits 00001001
Register 0x0b - Value 0X2b - Bits 00101011
Register 0x0c - Value 0X20 - Bits 00100000
Register 0x0d - Value 0X02 - Bits 00000010
Register 0x0e - Value 0X80 - Bits 10000000
Register 0x0f - Value 0X00 - Bits 00000000
Register 0x10 - Value 0X00 - Bits 00000000
Register 0x11 - Value 0X00 - Bits 00000000
Register 0x12 - Value 0X00 - Bits 00000000
Register 0x13 - Value 0X00 - Bits 00000000
Register 0x14 - Value 0X00 - Bits 00000000
Register 0x15 - Value 0X00 - Bits 00000000
Register 0x16 - Value 0X00 - Bits 00000000
Register 0x17 - Value 0X00 - Bits 00000000
Register 0x18 - Value 0X10 - Bits 00010000
Register 0x19 - Value 0X00 - Bits 00000000
Register 0x1a - Value 0X00 - Bits 00000000
Register 0x1b - Value 0X00 - Bits 00000000
Register 0x1c - Value 0X00 - Bits 00000000
Register 0x1d - Value 0X72 - Bits 01110010
Register 0x1e - Value 0X70 - Bits 01110000
Register 0x1f - Value 0X64 - Bits 01100100
Register 0x20 - Value 0X80 - Bits 10000000
Register 0x21 - Value 0X00 - Bits 00000000
Register 0x22 - Value 0X01 - Bits 00000001
Register 0x23 - Value 0Xff - Bits 11111111
Register 0x24 - Value 0X00 - Bits 00000000
Register 0x25 - Value 0X00 - Bits 00000000
Register 0x26 - Value 0X04 - Bits 00000100
Register 0x27 - Value 0X00 - Bits 00000000
Register 0x28 - Value 0X00 - Bits 00000000
Register 0x29 - Value 0X00 - Bits 00000000
Register 0x2a - Value 0X00 - Bits 00000000
Register 0x2b - Value 0X00 - Bits 00000000
Register 0x2c - Value 0X00 - Bits 00000000
Register 0x2d - Value 0X50 - Bits 01010000
Register 0x2e - Value 0X14 - Bits 00010100
Register 0x2f - Value 0X45 - Bits 01000101
Register 0x30 - Value 0X55 - Bits 01010101
Register 0x31 - Value 0Xc3 - Bits 11000011
Register 0x32 - Value 0X05 - Bits 00000101
Register 0x33 - Value 0X27 - Bits 00100111
Register 0x34 - Value 0X1c - Bits 00011100
Register 0x35 - Value 0X0a - Bits 00001010
Register 0x36 - Value 0X03 - Bits 00000011
Register 0x37 - Value 0X0a - Bits 00001010
Register 0x38 - Value 0X42 - Bits 01000010
Register 0x39 - Value 0X12 - Bits 00010010
Register 0x3a - Value 0X49 - Bits 01001001
Register 0x3b - Value 0X1d - Bits 00011101
Register 0x3c - Value 0X00 - Bits 00000000
Register 0x3d - Value 0Xaf - Bits 10101111
Register 0x3e - Value 0X00 - Bits 00000000
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000

.NET Core 5 SX127X library Part1

TransferFullDuplex vs. ReadWrite

For testing the initial versions of my .Net Core 5 dotnet/iot SX127X library I’m using a Uputronics Raspberry PiZero LoRa(TM) Expansion Board which supports both standard Chip Select(CS) pins (switch selectable which is really useful) and an M2M 1 Channel LoRaWan Gateway Shield for Raspberry PI which has a “non-standard” CS pin.

Uputronics Raspberry PIZero LoRa Expansion board on a Raspberry PI 3 device
M2M Single channel shield on Raspberry Pi 3 Device

The spiDevice.ReadByte() and spiDevice.WriteBye() version worked with a custom chip select pin(25) and CS0 or CS1 selected in the SpiConnectionSettings (but this CS line was “unusable” by other applications). This approach also worked with standard select line (CS01 or CS1) if the SpiConnectionSettings was configured to use the “other” CS line and the selected CS pin managed by the application.

namespace devMobile.IoT.SX127x.ShieldSPIWriteRead
{
	class Program
	{
		private const int SpiBusId = 0;
		private const int ChipSelectLine = 1; // 0 or 1 for Uputronics depends on the switch, for the others choose CS pin not already in use
#if ChipSelectNonStandard
		private const int ChipSelectPinNumber = 25; // 25 for M2M, Dragino etc.
#endif
		private const byte RegisterAddress = 0x6; // RegFrfMsb 0x6c
		//private const byte RegisterAddress = 0x7; // RegFrfMid 0x80
		//private const byte RegisterAddress = 0x8; // RegFrfLsb 0x00
		//private const byte RegisterAddress = 0x42; // RegVersion 0x12

		static void Main(string[] args)
		{
#if ChipSelectNonStandard
			GpioController controller = null;

			controller = new GpioController(PinNumberingScheme.Logical);

			controller.OpenPin(ChipSelectPinNumber, PinMode.Output);
			controller.Write(ChipSelectPinNumber, PinValue.High);
#endif

			var settings = new SpiConnectionSettings(SpiBusId, ChipSelectLine)
			{
				ClockFrequency = 5000000,
				Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
			};

			SpiDevice spiDevice = SpiDevice.Create(settings);

			Thread.Sleep(500);

			while (true)
			{
#if ChipSelectNonStandard
				controller.Write(ChipSelectPinNumber, PinValue.Low);
#endif

				spiDevice.WriteByte(RegisterAddress);
				byte registerValue = spiDevice.ReadByte();

#if ChipSelectNonStandard
				controller.Write(ChipSelectPinNumber, PinValue.High);
#endif

				byte registerValue = readBuffer[writeBuffer.Length - 1];

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

				Thread.Sleep(5000);
			}
		}
	}
}

The spiDevice.TransferFullDuplex worked for a standard CS line (CS0 or CS1), and for a non-standard CS line, though the CS line configured in SpiConnectionSettings was “unusable” by other applications “.

namespace devMobile.IoT.SX127x.ShieldSPITransferFullDuplex
{
	class Program
	{
		private const int SpiBusId = 0;
		private const int ChipSelectLine = 0; // 0 or 1 for Uputronics depends on the switch, for the others choose CS pin not already in use
#if ChipSelectNonStandard
		private const int ChipSelectPinNumber = 25; // 25 for M2M, Dragino etc.
#endif
		private const byte RegisterAddress = 0x6; // RegFrfMsb 0x6c
		//private const byte RegisterAddress = 0x7; // RegFrfMid 0x80
		//private const byte RegisterAddress = 0x8; // RegFrfLsb 0x00
		//private const byte RegisterAddress = 0x42; // RegVersion 0x12

		static void Main(string[] args)
		{
#if ChipSelectNonStandard
			GpioController controller = null;

			controller = new GpioController(PinNumberingScheme.Logical);

			controller.OpenPin(ChipSelectPinNumber, PinMode.Output);
			controller.Write(ChipSelectPinNumber, PinValue.High);
#endif

			var settings = new SpiConnectionSettings(SpiBusId, ChipSelectLine)
			{
				ClockFrequency = 5000000,
				Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
			};

			SpiDevice spiDevice = SpiDevice.Create(settings);

			Thread.Sleep(500);

			while (true)
			{
				byte[] writeBuffer = new byte[] { RegisterAddress, 0 };
				byte[] readBuffer = new byte[writeBuffer.Length];

#if ChipSelectNonStandard
				controller.Write(ChipSelectPinNumber, PinValue.Low);
#endif

				spiDevice.TransferFullDuplex(writeBuffer, readBuffer);

#if ChipSelectNonStandard
				controller.Write(ChipSelectPinNumber, PinValue.High);
#endif

				byte registerValue = readBuffer[writeBuffer.Length - 1];

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

				Thread.Sleep(5000);
			}
		}
	}
}

The output when the application was working as expected

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x06 - Value 0X6c - Bits 01101100
The program 'dotnet' has exited with code 0 (0x0).

Summary

Though the spiDevice.TransferFullDuplex code was slightly more complex it worked with both standard and non-standard CS pins.

.NET Core 5 Raspberry PI SPI

I have spent a lot of time debugging Serial Peripheral Interface(SPI) device libraries and the .Net Core 5 dotnet/iot library will have its own subtleties(with SPI it’s all about timing). I have written GHI Electronics TinyCLR, Wilderness Labs Meadow, Windows 10 IoT Core, .NET MicroFramework and .NET nanoFramework libraries the SX127X family of devices so building a .Net Core 5 one seemed like a good place to start.

I’m using a Uputronics Raspberry PiZero LoRa(TM) Expansion Board which supports both standard Chip Select(CS) pins (switch selectable which is really useful) and an M2M 1 Channel LoRaWan Gateway Shield for Raspberry PI which has a “non-standard” CS pin.

Uputronics Raspberry PIZero LoRa Expansion board on a Raspberry 3 device

The Uputronics pHat has a pair of Light Emitting Diodes(LEDs) so I adapted some code from a previous post to flash these to confirm the card was working.

static void UputronicsLeds()
{
	const int RedLedPinNumber = 6;
	const int GreenLedPinNumber = 13;

	GpioController controller = new GpioController(PinNumberingScheme.Logical);

	controller.OpenPin(RedLedPinNumber, PinMode.Output);
	controller.OpenPin(GreenLedPinNumber, PinMode.Output);

	while (true)
	{
		if (controller.Read(RedLedPinNumber) == PinValue.Low)
		{
			controller.Write(RedLedPinNumber, PinValue.High);
			controller.Write(GreenLedPinNumber, PinValue.Low);
		}
		else
		{
			controller.Write(RedLedPinNumber, PinValue.Low);
			controller.Write(GreenLedPinNumber, PinValue.High);
		}

		Thread.Sleep(1000);
	}
}

The first Uputronics pHat version using spiDevice.TransferFullDuplex didn’t work. I tried allocating memory for the buffers with new and stackalloc which didn’t seem to make any difference in my trivial example. I tried different Chip Select(CS) pin options, frequencies and modes (the mode used is based on the timings specified in the SX127X datasheet).

static void TransferFullDuplex()
{
	//byte[] writeBuffer = new byte[1]; // Memory allocation didn't seem to make any difference
    //byte[] readBuffer = new byte[1];
	Span<byte> writeBuffer = stackalloc byte[1];
	Span<byte> readBuffer = stackalloc byte[1];

	//var settings = new SpiConnectionSettings(0)
	var settings = new SpiConnectionSettings(0, 0)
	//var settings = new SpiConnectionSettings(0, 1)
	{
		ClockFrequency = 5000000,
		//ClockFrequency = 500000, // Frequency didn't seem to make any difference
		Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
	};

	SpiDevice spiDevice = SpiDevice.Create(settings);

	Thread.Sleep(500);

	while (true)
	{
		try
		{
			for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
			{
				writeBuffer[0] = registerIndex;
				spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
				//Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0')); // Debug output stopped after roughly 3 times round for loop often debugger would barf as well
				Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));

				// Would be nice if SpiDevice has a TransferSequential
				/* 
				writeBuffer[0] = registerIndex;
				spiDevice.TransferSequential(writeBuffer, readBuffer);
				Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
				*/
			}

			Console.WriteLine("");
			Thread.Sleep(5000);
		}
		catch (Exception ex)
		{
			Console.WriteLine(ex.Message);
		}
	}
}

The second Uputronics pHat version using spiDevice.ReadByte() and spiDevice.WriteBye() didn’t work either.

static void ReadWriteChipSelectStandard()
{
	var settings = new SpiConnectionSettings(0) // Doesn't work
	//	var settings = new SpiConnectionSettings(0, 0) // Doesn't work
	//var settings = new SpiConnectionSettings(0, 1) // Doesn't Work
	{
		ClockFrequency = 5000000,
		ChipSelectLineActiveState = PinValue.Low,
		Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
	};

	SpiDevice spiDevice = SpiDevice.Create(settings);

	Thread.Sleep(500);

	while (true)
	{
		try
		{
			for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
			{
				spiDevice.WriteByte(registerIndex);
				//Thread.Sleep(5); These made no difference
				//Thread.Sleep(10);
				//Thread.Sleep(20);
				//Thread.Sleep(40);
				byte registerValue = spiDevice.ReadByte();

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

			Thread.Sleep(5000);
		}
		catch (Exception ex)
		{
			Console.WriteLine(ex.Message);
		}
	}
}

The third Uputronics pHat version using spiDevice.ReadByte() and spiDevice.WriteByte() with DIY Chip Select(CS) worked. In previous SPI device libraries I have found that “managing” the CS line in code can be easier to get working The MicroFramework also has more connectionSettings options for better control of CS line timings which reduces the need for DIY.

static void ReadWriteChipSelectDiy()
{
	const int CSPinNumber = 8; // CS0
	//const int CSPinNumber = 7; // CS1

	// DIY CS0 implented with GPIO pin application controls
	GpioController controller = new GpioController(PinNumberingScheme.Logical);

	controller.OpenPin(CSPinNumber, PinMode.Output);
	//controller.Write(CSPinNumber, PinValue.High);

	//var settings = new SpiConnectionSettings(0) // Doesn't work
	var settings = new SpiConnectionSettings(0, 1) // Works, have to point at unused CS1, this could be a problem is other device on CS1
	//var settings = new SpiConnectionSettings(0, 0) // Works, have to point at unused CS0, this could be a problem is other device on CS0
	{
		ClockFrequency = 5000000,
		Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
	};

	SpiDevice spiDevice = SpiDevice.Create(settings);

	Thread.Sleep(500);

	while (true)
	{
		try
		{
			for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
			{
				controller.Write(CSPinNumber, PinValue.Low);
				spiDevice.WriteByte(registerIndex);
				//Thread.Sleep(2); // This maybe necessary
				byte registerValue = spiDevice.ReadByte();
				controller.Write(CSPinNumber, PinValue.High);

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

			Thread.Sleep(5000);
		}
		catch (Exception ex)
		{
			Console.WriteLine(ex.Message);
		}
	}
}

The dotNet/IoT doesn’t support (July2021) the option to “exclusively” open a port so there could be issues with other applications assuming they control CS0/CS1.

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x01 - Value 0X09 - Bits 00001001
Register 0x02 - Value 0X1a - Bits 00011010
Register 0x03 - Value 0X0b - Bits 00001011
Register 0x04 - Value 0X00 - Bits 00000000
Register 0x05 - Value 0X52 - Bits 01010010
Register 0x06 - Value 0X6c - Bits 01101100
Register 0x07 - Value 0X80 - Bits 10000000
Register 0x08 - Value 0X00 - Bits 00000000
Register 0x09 - Value 0X4f - Bits 01001111
Register 0x0a - Value 0X09 - Bits 00001001
Register 0x0b - Value 0X2b - Bits 00101011
Register 0x0c - Value 0X20 - Bits 00100000
Register 0x0d - Value 0X08 - Bits 00001000
Register 0x0e - Value 0X02 - Bits 00000010
Register 0x0f - Value 0X0a - Bits 00001010
Register 0x10 - Value 0Xff - Bits 11111111
Register 0x11 - Value 0X70 - Bits 01110000
Register 0x12 - Value 0X15 - Bits 00010101
Register 0x13 - Value 0X0b - Bits 00001011
Register 0x14 - Value 0X28 - Bits 00101000
Register 0x15 - Value 0X0c - Bits 00001100
Register 0x16 - Value 0X12 - Bits 00010010
Register 0x17 - Value 0X47 - Bits 01000111
Register 0x18 - Value 0X32 - Bits 00110010
Register 0x19 - Value 0X3e - Bits 00111110
Register 0x1a - Value 0X00 - Bits 00000000
Register 0x1b - Value 0X00 - Bits 00000000
Register 0x1c - Value 0X00 - Bits 00000000
Register 0x1d - Value 0X00 - Bits 00000000
Register 0x1e - Value 0X00 - Bits 00000000
Register 0x1f - Value 0X40 - Bits 01000000
Register 0x20 - Value 0X00 - Bits 00000000
Register 0x21 - Value 0X00 - Bits 00000000
Register 0x22 - Value 0X00 - Bits 00000000
Register 0x23 - Value 0X00 - Bits 00000000
Register 0x24 - Value 0X05 - Bits 00000101
Register 0x25 - Value 0X00 - Bits 00000000
Register 0x26 - Value 0X03 - Bits 00000011
Register 0x27 - Value 0X93 - Bits 10010011
Register 0x28 - Value 0X55 - Bits 01010101
Register 0x29 - Value 0X55 - Bits 01010101
Register 0x2a - Value 0X55 - Bits 01010101
Register 0x2b - Value 0X55 - Bits 01010101
Register 0x2c - Value 0X55 - Bits 01010101
Register 0x2d - Value 0X55 - Bits 01010101
Register 0x2e - Value 0X55 - Bits 01010101
Register 0x2f - Value 0X55 - Bits 01010101
Register 0x30 - Value 0X90 - Bits 10010000
Register 0x31 - Value 0X40 - Bits 01000000
Register 0x32 - Value 0X40 - Bits 01000000
Register 0x33 - Value 0X00 - Bits 00000000
Register 0x34 - Value 0X00 - Bits 00000000
Register 0x35 - Value 0X0f - Bits 00001111
Register 0x36 - Value 0X00 - Bits 00000000
Register 0x37 - Value 0X00 - Bits 00000000
Register 0x38 - Value 0X00 - Bits 00000000
Register 0x39 - Value 0Xf5 - Bits 11110101
Register 0x3a - Value 0X20 - Bits 00100000
Register 0x3b - Value 0X82 - Bits 10000010
Register 0x3c - Value 0Xf6 - Bits 11110110
Register 0x3d - Value 0X02 - Bits 00000010
Register 0x3e - Value 0X80 - Bits 10000000
Register 0x3f - Value 0X40 - Bits 01000000
Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010

The fourth Uputronics pHat version using spiDevice.TransferFullDuplex with read and write buffers two bytes long and the leading bye of the response ignored worked.

...
while (true)
{
	try
	{
		for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
		{
			// Doesn't work
			writeBuffer[0] = registerIndex;
			spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
			Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));

			// Does work
			writeBuffer[0] = registerIndex;
			spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
			Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, readBuffer[1], Convert.ToString(readBuffer[1], 2).PadLeft(8, '0'));

			// Does work
			writeBuffer[1] = registerIndex;
			spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
			Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, readBuffer[1], Convert.ToString(readBuffer[1], 2).PadLeft(8, '0'));

			Console.WriteLine("");
		}

		Console.WriteLine("");
		Thread.Sleep(5000);
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
}

Register 0x00 - Value 0X00 - Bits 00000000
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x00 - Value 0X00 - Bits 00000000

...

Register 0x42 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010
Register 0x42 - Value 0X12 - Bits 00010010

M2M Single channel shield on Raspberry Pi 3 Device

The first M2M pHat version using SpiDevice.Read and SpiDevice.Write with a “custom” CS pin worked.

...
// Chip select with pin which isn't CS0 or CS1 needs M2M shield
static void ReadWriteDiyChipSelectNonStandard()
{
	const int CSPinNumber = 25;

	// DIY CS0 implented with GPIO pin application controls
	GpioController controller = new GpioController(PinNumberingScheme.Logical);

	controller.OpenPin(CSPinNumber, PinMode.Output);
	//controller.Write(CSPinNumber, PinValue.High);

	// Work, this could be a problem is other device on CS0/CS1
	var settings = new SpiConnectionSettings(0)
	//var settings = new SpiConnectionSettings(0, 0) 
	//var settings = new SpiConnectionSettings(0, 1) 
	{
		ClockFrequency = 5000000,
		Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
	};

	SpiDevice spiDevice = SpiDevice.Create(settings);

	Thread.Sleep(500);

	while (true)
	{
		try
		{
			for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
			{
				controller.Write(CSPinNumber, PinValue.Low);
				spiDevice.WriteByte(registerIndex);
				//Thread.Sleep(2); // This maybe necessary
				byte registerValue = spiDevice.ReadByte();
				controller.Write(CSPinNumber, PinValue.High);

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

			Thread.Sleep(5000);
		}
		catch (Exception ex)
		{
			Console.WriteLine(ex.Message);
		}
	}
}

The second M2M pHat version using SpiDevice.TransferFullDuplex with a “custom” CS pin also worked.

while (true)
{
	try
	{
		for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
		{
			writeBuffer[0] = registerIndex;
			//writeBuffer[1] = registerIndex;

			controller.Write(CSPinNumber, PinValue.Low);
			spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
			controller.Write(CSPinNumber, PinValue.High);

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

		Thread.Sleep(5000);
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
}

The next step was to read an array of bytes, using spiDevice.TransferFullDuplex. The SX127X transmit/receive frequency is specified in registers 0x06 RegFrMSB, 0x07 RegFrMid, and 0x08 RegFrLsb. The default frequency is 868MHz which is 0xE4, 0xC0, 0x00

static void TransferFullDuplexBufferBytesRead()
{ 
	const byte length = 3;
	byte[] writeBuffer = new byte[length + 1];
	byte[] readBuffer = new byte[length + 1];

	// Read the frequency which is 3 bytes RegFrMsb 0x6c, RegFrMid 0x80, RegFrLsb 0x00
	writeBuffer[0] = 0x06; //

	// Works, have to point at unused CS0/CS1, others could be a problem is another another SPI device is on on CS0/CS1
	//var settings = new SpiConnectionSettings(0)
	var settings = new SpiConnectionSettings(0, 0) 
	//var settings = new SpiConnectionSettings(0, 1) 
	{
		ClockFrequency = 5000000,
		Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
	};

	SpiDevice spiDevice = SpiDevice.Create(settings);

	spiDevice.TransferFullDuplex(writeBuffer, readBuffer);

	Console.WriteLine($"Register 0x06-0x{readBuffer[1]:x2} 0x07-0x{readBuffer[2]:x2} 0x08-0x{readBuffer[3]:x2}");
}
-------------------------------------------------------------------
You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Private.CoreLib.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x06-0xe4 0x07-0xc0 0x08-0x00

The final step was write an array of bytes, using spiDevice.TransferFullDuplex to change the transmit/receive frequency to 915MHz. To write a value the first bit of the address byte must be set to 1 hence the 0x86 RegFrMsb address.

static void TransferFullDuplexBufferBytesWrite()
{
	const byte length = 3;
	byte[] writeBuffer = new byte[length + 1];
	byte[] readBuffer = new byte[length + 1];

	// Write the frequency which is 3 bytes RegFrMsb 0x6c, RegFrMid 0x80, RegFrLsb or with 0x00 the write mask
	writeBuffer[0] = 0x86 ;

	// Works, have to point at unused CS0/CS1, others could be a problem is another another SPI device is on on CS0/CS1
	//var settings = new SpiConnectionSettings(0)
	var settings = new SpiConnectionSettings(0, 0)
	//var settings = new SpiConnectionSettings(0, 1) 
	{
		ClockFrequency = 5000000,
		Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
	};

	SpiDevice spiDevice = SpiDevice.Create(settings);

	// Set the frequency to 915MHz
	writeBuffer[1] = 0xE4;
	writeBuffer[2] = 0xC0;
	writeBuffer[3] = 0x00;

	spiDevice.TransferFullDuplex(writeBuffer, readBuffer);
}

-------------------------------------------------------------------
You may only use the Microsoft .NET Core Debugger (vsdbg) with
Visual Studio Code, Visual Studio or Visual Studio for Mac software
to help you develop and test your applications.
-------------------------------------------------------------------
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Private.CoreLib.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
...
Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Register 0x06-0x6c 0x07-0x80 0x08-0x00
Register 0x06-0xe4 0x07-0xc0 0x08-0x00
The program 'dotnet' has exited with code 0 (0x0).

Summary

This exceptionally long post was to highlight that with SPI it’s all about timing, first read the datasheet, then build code to validate your understanding.

SX127X SPI interface timing diagram

Some platforms have native TransferSequential implementations but the dotNet/IoT library only has TransferFullDuplex. SPI hardware is always full duplex, if “sequential” is available the implementation will write the provided bytes and then follow them with zeros to read the requested bytes.

RFM9X.IoTCore Uputronics Raspberry PI LoRa(TM) Expansion Board

The Raspberry Pi+ LoRa(TM) Expansion Board has two RF modules. In my setup CE0 was 915MHz and CE1 was 433MHz so I modified the demo application so I could run both ports independently or simultaneously.

#if UPUTRONICS_RPIPLUS_CS0 && !UPUTRONICS_RPIPLUS_CS1
		private const byte InterruptLine = 25;
		private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS0, InterruptLine);
#endif
#if !UPUTRONICS_RPIPLUS_CS0 && UPUTRONICS_RPIPLUS_CS1
		private const byte InterruptLine = 16;
		private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS1, InterruptLine);
#endif
#if UPUTRONICS_RPIPLUS_CS0 && UPUTRONICS_RPIPLUS_CS1 // 433MHz and 915MHz in my setup
	private const byte InterruptLineCS0 = 25;
	private Rfm9XDevice rfm9XDeviceCS0 = new Rfm9XDevice(ChipSelectPin.CS0, InterruptLineCS0);
	private const byte InterruptLineCS1 = 16;
	private Rfm9XDevice rfm9XDeviceCS1 = new Rfm9XDevice(ChipSelectPin.CS1, InterruptLineCS1);
#endif

The in the run method

#if UPUTRONICS_RPIPLUS_CS0 && UPUTRONICS_RPIPLUS_CS1
		public void Run(IBackgroundTaskInstance taskInstance)
		{
			rfm9XDeviceCS0.Initialise(915000000.0, paBoost: true, rxPayloadCrcOn: true);
			rfm9XDeviceCS1.Initialise(433000000.0, paBoost: true, rxPayloadCrcOn: true);
#if DEBUG
			rfm9XDeviceCS0.RegisterDump();
			rfm9XDeviceCS1.RegisterDump();
#endif

			rfm9XDeviceCS0.OnReceive += Rfm9XDevice_OnReceive;
			rfm9XDeviceCS1.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
			rfm9XDeviceCS0.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
			rfm9XDeviceCS1.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
#else
			rfm9XDeviceCS0.Receive();
			rfm9XDeviceCS1.Receive();
#endif
			rfm9XDeviceCS0.OnTransmit += Rfm9XDevice_OnTransmit;
			rfm9XDeviceCS1.OnTransmit += Rfm9XDevice_OnTransmit;

			Task.Delay(10000).Wait();

			while (true)
			{
				string messageText = string.Format("Hello from {0} ! {1}", Environment.MachineName, MessageCount);
				MessageCount -= 1;

				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				Debug.WriteLine("{0:HH:mm:ss}-TX {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
#if ADDRESSED_MESSAGES_PAYLOAD
				this.rfm9XDeviceCS0.Send(UTF8Encoding.UTF8.GetBytes("Netduino"), messageBytes);
				this.rfm9XDeviceCS1.Send(UTF8Encoding.UTF8.GetBytes("Arduino1"), messageBytes);
#else
				this.rfm9XDeviceCS0.Send(messageBytes);
				this.rfm9XDeviceCS1.Send(messageBytes);
#endif
				Task.Delay(10000).Wait();
			}
		}

#else

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			rfm9XDevice.Initialise(433000000, paBoost: true, rxPayloadCrcOn : true);
			rfm9XDevice.Initialise(915000000, paBoost: true, rxPayloadCrcOn : true);
#if DEBUG
			rfm9XDevice.RegisterDump();
#endif

		rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
			rfm9XDevice.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
#else
			rfm9XDevice.Receive();
#endif
			rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;

			Task.Delay(10000).Wait();

			while (true)
			{
				string messageText = string.Format("Hello from {0} ! {1}", Environment.MachineName, MessageCount);
				MessageCount -= 1;

				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				Debug.WriteLine("{0:HH:mm:ss}-TX {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
#if ADDRESSED_MESSAGES_PAYLOAD
				this.rfm9XDevice.Send(UTF8Encoding.UTF8.GetBytes("AddressHere"), messageBytes);
#else
				this.rfm9XDevice.Send(messageBytes);
#endif
				Task.Delay(10000).Wait();
			}
		}
#endif

In the debugger output I could see addressed messages being sent to and arriving from a 433MHz Arduino device and a 915MHz Netduino device.

The thread 0x5f8 has exited with code 0 (0x0).
22:06:44-RX From Netduino PacketSnr 9.5 Packet RSSI -49dBm RSSI -110dBm = 20 byte message "Hello NetMF LoRa! 82"
22:06:47-RX From Arduino1 PacketSnr 9.8 Packet RSSI -56dBm RSSI -101dBm = 11 byte message "t 20.2,h 90"
22:06:48-TX 25 byte message Hello from LoRaIoT1 ! 163
22:06:49-TX Done
22:06:49-TX Done
The thread 0xe9c has exited with code 0 (0x0).
22:06:54-RX From Netduino PacketSnr 9.8 Packet RSSI -46dBm RSSI -111dBm = 20 byte message "Hello NetMF LoRa! 83"
22:06:57-RX From Arduino1 PacketSnr 9.8 Packet RSSI -61dBm RSSI -93dBm = 11 byte message "t 20.3,h 90"
22:06:58-TX 25 byte message Hello from LoRaIoT1 ! 162
22:06:59-TX Done
22:06:59-TX Done
The thread 0xf88 has exited with code 0 (0x0).
22:07:04-RX From Netduino PacketSnr 9.5 Packet RSSI -48dBm RSSI -110dBm = 20 byte message "Hello NetMF LoRa! 84"
22:07:07-RX From Arduino1 PacketSnr 9.8 Packet RSSI -61dBm RSSI -93dBm = 11 byte message "t 20.2,h 90"
22:07:09-TX 25 byte message Hello from LoRaIoT1 ! 161
22:07:09-TX Done
22:07:09-TX Done

This particular configuration has not been extensively tested yet and should be treated as early Beta (11 Sept 2018)

 

Uputronics Raspberry Pi+ LoRa(TM) Expansion Board

The second package to arrive was a Raspberry Pi+ LoRa(TM) Expansion Board populated with HopeRF 434MHz & 915MHz modules. It was in a small cardboard box with bolts+spacers and had a small set of printed instructions.

The shield has four user controlable Light Emitting Diodes(LED) connected to General Purpose Input Output(GPIO) pins which will be useful  for providing feedback when trying to debug faults etc..

uputronicsPiPlusHelp

Some of the pin numbers are also printed on the shield silk screen.UputronicsRPIPlusShield
This time the first step was to check the pin assignments of the 4 LEDs

//---------------------------------------------------------------------------------
// Copyright (c) September 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.UputronicsRPIPlusLed
{
	using System;
	using System.Threading;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Gpio;

	public sealed class StartupTask : IBackgroundTask
	{
		public void Run(IBackgroundTaskInstance taskInstance)
		{
			GpioController gpioController = GpioController.GetDefault();

			GpioPin ce01LedPin = gpioController.OpenPin(5);
			ce01LedPin.SetDriveMode(GpioPinDriveMode.Output);
			ce01LedPin.Write(GpioPinValue.Low);

			GpioPin ceo2LedPin = gpioController.OpenPin(21);
			ceo2LedPin.SetDriveMode(GpioPinDriveMode.Output);
			ceo2LedPin.Write(GpioPinValue.High);

			GpioPin lanLedPin = gpioController.OpenPin(6);
			lanLedPin.SetDriveMode(GpioPinDriveMode.Output);
			lanLedPin.Write(GpioPinValue.Low);

			GpioPin internetLedPin = gpioController.OpenPin(13);
			internetLedPin.SetDriveMode(GpioPinDriveMode.Output);
			internetLedPin.Write(GpioPinValue.High);

			while (true)
			{
				if (ce01LedPin.Read() == GpioPinValue.High)
				{
					ce01LedPin.Write(GpioPinValue.Low);
				}
				else
				{
					ce01LedPin.Write(GpioPinValue.High);
				}

				if (ceo2LedPin.Read() == GpioPinValue.High)
				{
					ceo2LedPin.Write(GpioPinValue.Low);
				}
				else
				{
					ceo2LedPin.Write(GpioPinValue.High);
				}

				if (lanLedPin.Read() == GpioPinValue.High)
				{
					lanLedPin.Write(GpioPinValue.Low);
				}
				else
				{
					lanLedPin.Write(GpioPinValue.High);
				}

				if (internetLedPin.Read() == GpioPinValue.High)
				{
					internetLedPin.Write(GpioPinValue.Low);
				}
				else
				{
					internetLedPin.Write(GpioPinValue.High);
				}

				Thread.Sleep(500);
			}
		}
	}
}

I think there is a small issue with the internet LED it should be GPIO13 (which matches the pin number)

The next step was to get the Serial Peripheral Interface (SPI) interface for both modules working.

//---------------------------------------------------------------------------------
// Copyright (c) September 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.UputronicsRPIPlusSPI
{
	using System;
	using System.Diagnostics;
	using System.Threading;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;

	public sealed class StartupTask : IBackgroundTask
	{
		public void Run(IBackgroundTaskInstance taskInstance)
		{
#if CS0
			const int chipSelectPinNumber = 0;
#endif
#if CS1
			const int chipSelectPinNumber = 1;
#endif
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(chipSelectPinNumber)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
			};
			SpiDevice Device = spiController.GetDevice(settings);

			while (true)
			{
				byte[] writeBuffer = new byte[] { 0x42 }; // RegVersion
				byte[] readBuffer = new byte[1];

				// Read the RegVersion silicon ID to check SPI works
				Device.TransferSequential(writeBuffer, readBuffer);

#if CS0
				Debug.WriteLine("CS0 Register RegVer 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
#endif
#if CS1
				Debug.WriteLine("CS1 Register RegVer 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
#endif
				Thread.Sleep(10000);
			}
		}
	}
}

Like the other uputronics shield I have tested this appears not to have the reset line of the RFM9X connected.

The output confirmed the code worked with both CS0 and CS1 defined

CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010

 

CS1 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS1 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS1 Register RegVer 0x42 - Value 0X12 - Bits 00010010

Would have been more useful to read RegFrMsb = 0x06, RegFrMid = 0x7, and RegFrLsb = 0x08 so I could see the different default frequencies of the two HopeRF modules. The next step is to build support for this shield into my RFM9X.IoTCore library.

RFM9X.IoTCore Uputronics Raspberry PiZero LoRa(TM) Expansion Board

I had to make some modifications to my RFM9X.IoT core library to support the Uputronics Raspberry PiZero LoRa(TM) Expansion Board as it doesn’t appear to have the HopeRF 9X reset pin connected.

I create another overload of the class constructor

// Constructor for RPI shields with chip select connected to CS0/CS1 and no reset pin e.g. Uputronics
public Rfm9XDevice(ChipSelectPin chipSelectPin, 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();

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

	InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;
}

Then disabled the strobing of the reset pin if it was not configured in the Initialise method

ublic void Initialise(
	double frequency = FrequencyDefault, // RegFrMsb, RegFrMid, RegFrLsb
	bool rxDoneignoreIfCrcMissing = true, bool rxDoneignoreIfCrcInvalid = true,
	bool paBoost = PABoostDefault, byte maxPower = RegPAConfigMaxPowerDefault, byte outputPower = RegPAConfigOutputPowerDefault, // RegPaConfig
	bool ocpOn = RegOcpDefault, byte ocpTrim = RegOcpOcpTrimDefault, // RegOcp
	RegLnaLnaGain lnaGain = LnaGainDefault, bool lnaBoost = LnaBoostDefault, // 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 = LowDataRateOptimizeDefault, bool agcAutoOn = AgcAutoOnDefault,
	byte ppmCorrection = ppmCorrectionDefault,
	RegDetectOptimizeDectionOptimize detectionOptimize = RegDetectOptimizeDectionOptimizeDefault,
	bool invertIQ = InvertIqDefault,
	RegisterDetectionThreshold detectionThreshold = RegisterDetectionThresholdDefault,
	byte syncWord = RegSyncWordDefault)
{
	Frequency = frequency; // Store this away for RSSI adjustments
	RxDoneIgnoreIfCrcMissing = rxDoneignoreIfCrcMissing;
	RxDoneIgnoreIfCrcInvalid = rxDoneignoreIfCrcInvalid;

	// If the HopeRF module doesn't have the reset pin connected (e.g. uputroncis) not point in resetting it
	if (ResetGpioPin != null)
	{
		// Strobe Reset pin briefly to factory reset SX127X chip
		ResetGpioPin.Write(GpioPinValue.Low);
		Task.Delay(10);
		ResetGpioPin.Write(GpioPinValue.High);
		Task.Delay(10);
	}

In the calling application the constructor is called when UPUTRONICS_RPIZERO_CS0 or UPUTRONICS_RPIZERO_CS0 is defined.

#endif
#if UPUTRONICS_RPIZERO_CS0
	private const byte InterruptLine = 25;
	private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS0, InterruptLine);
#endif
#if UPUTRONICS_RPIZERO_CS1
	private const byte InterruptLine = 16;
	private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS1, InterruptLine);
#endif

I rebuilt the test application with the necessary uputronics definitions and it worked.

Register 0x40 - Value 0X00 - Bits 00000000
Register 0x41 - Value 0X00 - Bits 00000000
Register 0x42 - Value 0X12 - Bits 00010010
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): <span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\Rfm9xLoRaDeviceClient-uwpVS.Debug_ARM.Bryn.Lewis\System.Threading.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
14:26:42-TX 25 byte message Hello from LoRaIoT1 ! 255
14:26:42-TX Done
14:26:42-RX PacketSnr 9.5 Packet RSSI -55dBm RSSI -112dBm = 24 byte message "11 Hello Arduino LoRa! 0"
14:26:43-RX PacketSnr 9.3 Packet RSSI -56dBm RSSI -110dBm = 24 byte message "11 Hello Arduino LoRa! 1"
14:26:44-RX PacketSnr 9.8 Packet RSSI -58dBm RSSI -111dBm = 24 byte message "11 Hello Arduino LoRa! 2"
14:26:45-RX PacketSnr 9.5 Packet RSSI -58dBm RSSI -111dBm = 24 byte message "11 Hello Arduino LoRa! 3"
14:26:46-RX PacketSnr 9.3 Packet RSSI -58dBm RSSI -112dBm = 24 byte message "11 Hello Arduino LoRa! 4"
The thread 0x154 has exited with code 0 (0x0).
14:26:47-RX PacketSnr 9.8 Packet RSSI -58dBm RSSI -114dBm = 24 byte message "11

I back integrated the code into my Adafruit.IO LoRa gateway and it worked (second time after I fixed the conditional compile directive) just need to do some further stress and soak testing.

 

Uputronics Raspberry PiZero LoRa(TM) Expansion Board

During the week another couple of Raspberry PI2/3/Zero shields arrived from uputronics. The two Raspberry PiZero LoRa(TM) Expansion Boards had arrived earlier so I unpacked them first. They were in small cardboard boxes with bolts+spacers and had a small set of printed instructions which was quite professional.uputronicsPiZeroLoRaHelp.png
These shields also have a switch for configuring the chip select line which is quite a neat feature and means they can be stacked. Unlike the other shields I have tested these appear not to have the reset line of the RFM9X connected.

UputronicsRPIZeroShield

The first step was to get the SPI connectivity sorted

//---------------------------------------------------------------------------------
// 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.UputronicsRPZeroSPI
{
	using System;
	using System.Diagnostics;
	using System.Threading;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;

	public sealed class StartupTask : IBackgroundTask
	{
		public void Run(IBackgroundTaskInstance taskInstance)
		{
#if CS0
			const int chipSelectPinNumber = 0;
#endif
#if CS1
			const int chipSelectPinNumber = 1;
#endif
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(chipSelectPinNumber)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
			};
			SpiDevice Device = spiController.GetDevice(settings);

			while (true)
			{
				byte[] writeBuffer = new byte[] { 0x42 }; // RegVersion
				byte[] readBuffer = new byte[1];

				// Read the RegVersion silicon ID to check SPI works
				Device.TransferSequential(writeBuffer, readBuffer);

				Debug.WriteLine("Register RegVer 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));

				Thread.Sleep(10000);
			}
		}
	}
}

The output confirmed the code worked with both CS0 and CS1 defined

Register RegVer 0x42 - Value 0X12 - Bits 00010010
Register RegVer 0x42 - Value 0X12 - Bits 00010010
Register RegVer 0x42 - Value 0X12 - Bits 00010010
The program '[2144] backgroundTaskHost.exe' has exited with code -1 (0xffffffff).

The shield has two onboard Light Emitting Diodes (LEDs) so I wrote a simple test application to flash them alternately.

//---------------------------------------------------------------------------------
// Copyright (c) July 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.UputronicsRPZeroLed
{
	using System;
	using System.Threading;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Gpio;

	public sealed class StartupTask : IBackgroundTask
	{
		public void Run(IBackgroundTaskInstance taskInstance)
		{
			GpioController gpioController = GpioController.GetDefault();
			GpioPin dataLedPin = gpioController.OpenPin(13);
			dataLedPin.SetDriveMode(GpioPinDriveMode.Output);
			dataLedPin.Write(GpioPinValue.Low);
			GpioPin linkLedPin = gpioController.OpenPin(6);
			linkLedPin.SetDriveMode(GpioPinDriveMode.Output);
			linkLedPin.Write(GpioPinValue.High);

			while (true)
			{

				if (dataLedPin.Read() == GpioPinValue.High)
				{
					dataLedPin.Write(GpioPinValue.Low);
				}
				else
				{
					dataLedPin.Write(GpioPinValue.High);
				}

				if (linkLedPin.Read() == GpioPinValue.High)
				{
					linkLedPin.Write(GpioPinValue.Low);
				}
				else
				{
					linkLedPin.Write(GpioPinValue.High);
				}

				Thread.Sleep(500);
			}
		}
	}
}

The two LEDs are labelled Data and Link but the pin numbers in the documentation were for an RPI Zero so didn’t match the ones I had to configure in code for my RPI3.

Overall the shield was professionally packaged and appears well engineered.