TinyCLR OS LoRa library Part2

Register Dump

Next step was to dump all registers (0x00 thru 0x42) of the SX1276/7/8/9 device.

//---------------------------------------------------------------------------------
// Copyright (c) March 2020, 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.RegisterScan
{
   using System;
   using System.Diagnostics;
   using System.Threading;
   using GHIElectronics.TinyCLR.Devices.Spi;
   using GHIElectronics.TinyCLR.Pins;

   public sealed class Rfm9XDevice
   {
      private SpiDevice rfm9XLoraModem;

      public Rfm9XDevice(int chipSelectPin)
      {
         var settings = new SpiConnectionSettings()
         {
            ChipSelectType = SpiChipSelectType.Gpio,
            ChipSelectLine = chipSelectPin,
            Mode = SpiMode.Mode0,
            ClockFrequency = 500000,
            DataBitLength = 8,
            ChipSelectActiveState = false,
         };

         SpiController spiCntroller = SpiController.FromName(FEZ.SpiBus.Spi1);

         rfm9XLoraModem = spiCntroller.GetDevice(settings);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];
         Debug.Assert(rfm9XLoraModem != null);

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         return readBuffer[1];
      }
   }

   class Program
   {
      static void Main()
      {
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(FEZ.GpioPin.D10);

         while (true)
         {
            for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
            {
               byte registerValue = rfm9XDevice.RegisterReadByte(registerIndex);

               Debug.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
            }
            Debug.WriteLine("");

            Thread.Sleep(10000);
         }
      }
   }
}


The output of the application looked like this

Found debugger!

Create TS.

Loading Deployment Assemblies.

Attaching deployed file.

   Assembly: mscorlib (1.0.0.0)  Attaching deployed file.

   Assembly: GHIElectronics.TinyCLR.Devices.Spi (1.0.0.0)  Attaching deployed file.

   Assembly: GHIElectronics.TinyCLR.Devices.Gpio (1.0.0.0)  Attaching deployed file.

   Assembly: GHIElectronics.TinyCLR.Native (1.0.0.0)  Attaching deployed file.

   Assembly: RegisterScan (1.0.0.0)  Resolving.

The debugging target runtime is loading the application assemblies and starting execution.
Ready.

'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\RegisterScan\bin\Debug\pe\..\GHIElectronics.TinyCLR.Native.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\RegisterScan\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Gpio.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\RegisterScan\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Spi.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\RegisterScan\bin\Debug\pe\..\RegisterScan.exe', Symbols loaded.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Register 0x00 - Value 0X00
Register 0x01 - Value 0X09
Register 0x02 - Value 0X1a
Register 0x03 - Value 0X0b
Register 0x04 - Value 0X00
Register 0x05 - Value 0X52
Register 0x06 - Value 0X6c
Register 0x07 - Value 0X80
Register 0x08 - Value 0X00
Register 0x09 - Value 0X4f
Register 0x0a - Value 0X09
Register 0x0b - Value 0X2b
Register 0x0c - Value 0X20
Register 0x0d - Value 0X08
Register 0x0e - Value 0X02
Register 0x0f - Value 0X0a
Register 0x10 - Value 0Xff
Register 0x11 - Value 0X70
Register 0x12 - Value 0X15
Register 0x13 - Value 0X0b
Register 0x14 - Value 0X28
Register 0x15 - Value 0X0c
Register 0x16 - Value 0X12
Register 0x17 - Value 0X47
Register 0x18 - Value 0X32
Register 0x19 - Value 0X3e
Register 0x1a - Value 0X00
Register 0x1b - Value 0X00
Register 0x1c - Value 0X00
Register 0x1d - Value 0X00
Register 0x1e - Value 0X00
Register 0x1f - Value 0X40
Register 0x21 - Value 0X00
Register 0x22 - Value 0X00
Register 0x23 - Value 0X00
Register 0x24 - Value 0X05
Register 0x25 - Value 0X00
Register 0x26 - Value 0X03
Register 0x27 - Value 0X93
Register 0x28 - Value 0X55
Register 0x29 - Value 0X55
Register 0x2a - Value 0X55
Register 0x2b - Value 0X55
Register 0x2c - Value 0X55
Register 0x2d - Value 0X55
Register 0x2e - Value 0X55
Register 0x2f - Value 0X55
Register 0x30 - Value 0X90
Register 0x31 - Value 0X40
Register 0x32 - Value 0X40
Register 0x33 - Value 0X00
Register 0x34 - Value 0X00
Register 0x35 - Value 0X0f
Register 0x36 - Value 0X00
Register 0x37 - Value 0X00
Register 0x38 - Value 0X00
Register 0x39 - Value 0Xf5
Register 0x3a - Value 0X20
Register 0x3b - Value 0X82
Register 0x3c - Value 0Xf7
Register 0x3d - Value 0X02
Register 0x3e - Value 0X80
Register 0x3f - Value 0X40
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12

The device was not in LoRa mode (Bit 7 of RegOpMode 0x01) so the next step was to read and write registers so I could change its configuration.

Overall the SPI implementation was closer to Windows 10 IoT Core model than expected.

TinyCLR OS LoRa library Part1

After writing my Windows 10 IoT Core RFM9X library and porting it to .NetMF and Wilderness Labs Meadow I figured another port to GHI Electronics TinyCLR-OS on a FEZ device shouldn’t take too long.

To get started I used a Dragino LoRa shield for Arduino which looked compatible with my FEZT18-N and FEZT18-W devices.

Dragino Arduino LoRa Shield Schematic

The shield uses D10 for chip select, D2 for RFM9X DI0 interrupt and D9 for Reset. 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.

FEZT18-N device with Dragino LoRa Shield

First step was to confirm I could (using the TinyCLR SPI NuGet library) read a couple of the Semtech SX1276 registers.

//---------------------------------------------------------------------------------
// Copyright (c) March 2020, 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.ShieldSpi
{
   using System;
   using System.Diagnostics;

   using System.Threading;
   using GHIElectronics.TinyCLR.Devices.Spi;
   using GHIElectronics.TinyCLR.Pins;

   class Program
   {
      static void Main()
      {
         var settings = new SpiConnectionSettings()
         {
            ChipSelectType = SpiChipSelectType.Gpio,
            ChipSelectLine = FEZ.GpioPin.D10,
            Mode = SpiMode.Mode0,
            ClockFrequency = 500000,
            DataBitLength = 8,
            ChipSelectActiveState = false,
         };

         var controller = SpiController.FromName(FEZ.SpiBus.Spi1);
         var device = controller.GetDevice(settings);

         Thread.Sleep(500);

         while (true)
         {
            byte register;
            byte[] writeBuffer;
            byte[] readBuffer;

            // Silicon Version info
            register = 0x42; // RegVersion expecting 0x12

            // Frequency
            //register = 0x06; // RegFrfMsb expecting 0x6C
            //register = 0x07; // RegFrfMid expecting 0x80
            //register = 0x08; // RegFrfLsb expecting 0x00

            //register = 0x17; //RegPayoadLength expecting 0x47

            // Preamble length 
            //register = 0x18; // RegPreambleMsb expecting 0x32
            //register = 0x19; // RegPreambleLsb expecting 0x3E

            writeBuffer = new byte[] { register,  0x0 };
            readBuffer = new byte[writeBuffer.Length];

            device.TransferFullDuplex(writeBuffer, readBuffer);

            Debug.WriteLine("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 < bytes.Length; b++)
         {
            if (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;
      }
   }
}

After trying many permutations of settings I could successfully read the RegVersion and default frequency values

The debugging target runtime is loading the application assemblies and starting execution.
Ready.

'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\ShieldSPI\bin\Debug\pe\..\GHIElectronics.TinyCLR.Native.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\ShieldSPI\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Gpio.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\ShieldSPI\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Spi.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\ShieldSPI\bin\Debug\pe\..\ShieldSPI.exe', Symbols loaded.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Value = 0x00-12
Value = 0x00-12
Value = 0x00-12
Value = 0x00-12

Overall the SPI implementation felt closer to Windows 10 IoT Core model than expected.

RFM9X.Meadow on Github

After a month or so of posts the source code of V1.0 of my Wilderness Labs Meadow RFM9X/SX127X library is on GitHub. I included all of the source for my test harness and proof of concept(PoC) application so other people can follow along with “my meadow learning experience”.

I initially started with a Dragino LoRa Shield for Arduino and jumper cables. I did this so only the pins I was using on the shield were connected to the Meadow.

Dragino LoRa Shield for Arduino based test harness

I then moved to an Adafruit LoRa Radio FeatherWing RFM95W 900MHz RadioFruit and Adafruit LoRa Radio FeatherWing – RFM95W 433 MHz – RadioFruit.

Adafruit FeatherWing based test harness

Using the jumper configuration above, the RFM9X constructor parameters are

  • Chip Select D9 (yellow wire)
  • Reset Pin D10 (grey wire)
  • Interrupt pin D12 (brown wire)
//---------------------------------------------------------------------------------
// Copyright (c) January 2020, 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.LoRaDeviceClient
{
   using System;
   using System.Diagnostics;
   using System.Text;
   using System.Threading.Tasks;

   using Meadow;
   using Meadow.Devices;
   using Meadow.Hardware;

   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      private const double Frequency = 915000000.0;
      private byte MessageCount = Byte.MaxValue;
      private Rfm9XDevice rfm9XDevice;

      public MeadowApp()
      {
         try
         {
            ISpiBus spiBus = Device.CreateSpiBus(500);
            if (spiBus == null)
            {
               Console.WriteLine("spiBus == null");
            }
            rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D12);

            rfm9XDevice.Initialise(Frequency, paBoost: true, rxPayloadCrcOn: true);
#if DEBUG
            rfm9XDevice.RegisterDump();
#endif

            rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
            rfm9XDevice.Receive(UTF8Encoding.UTF8.GetBytes("AddressHere"));
#else
            rfm9XDevice.Receive();
#endif
            rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }

         Task.Delay(10000).Wait();

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

            byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
            Console.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();
         }
      }

      private void Rfm9XDevice_OnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
      {
         try
         {
            string messageText = UTF8Encoding.UTF8.GetString(e.Data);

#if ADDRESSED_MESSAGES_PAYLOAD
            string addressText = UTF8Encoding.UTF8.GetString(e.Address);

            Console.WriteLine(@"{0:HH:mm:ss}-RX From {1} PacketSnr {2:0.0} Packet RSSI {3}dBm RSSI {4}dBm = {5} byte message ""{6}""", DateTime.Now, addressText, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText);
#else
            Console.WriteLine(@"{0:HH:mm:ss}-RX PacketSnr {1:0.0} Packet RSSI {2}dBm RSSI {3}dBm = {4} byte message ""{5}""", DateTime.Now, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText);
#endif
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }
      }

      private void Rfm9XDevice_OnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e)
      {
         Console.WriteLine("{0:HH:mm:ss}-TX Done", DateTime.Now);
      }
   }
}

My library works but some issues (Dec 2019) with the Serial Peripheral Interface (SPI) ReadRegister method, Debug.WriteLine and Console.WriteLine mean it should be treated as late beta.

The wilderness labs developers are regularly releasing updates which I will test with as soon as they are available.

.Net Meadow RFM95/96/97/98 LoRa library Part8

Transmit and Receive with Interrupts

For the final iteration of the “nasty” test harness I got the interrupts working for the transmitting and receiving of messages. It’s not quite simultaneous, the code sends a message every 10 seconds then goes back to receive continuous mode after each message has been sent.

      public Rfm9XDevice(IIODevice device, ISpiBus spiBus, IPin chipSelectPin, IPin resetPin, IPin interruptPin)
      {
         // Chip select pin configuration
         ChipSelectGpioPin = device.CreateDigitalOutputPort(chipSelectPin, initialState: true);
         if (ChipSelectGpioPin == null)
         {
            Console.WriteLine("ChipSelectGpioPin == null");
         }

         // Factory reset pin configuration
         IDigitalOutputPort resetGpioPin = device.CreateDigitalOutputPort(resetPin);
         if (resetGpioPin == null)
         {
            Console.WriteLine("resetGpioPin == null");
         }
         resetGpioPin.State = false;
         Task.Delay(10);
         resetGpioPin.State = true;
         Task.Delay(10);

         // Interrupt pin for RX message & TX done notification 
         InterruptGpioPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeRising);
         InterruptGpioPin.Changed += InterruptGpioPin_ValueChanged;

         Rfm9XLoraModem = new SpiPeripheral(spiBus, ChipSelectGpioPin);
         if (Rfm9XLoraModem == null)
         {
            Console.WriteLine("Rfm9XLoraModem == null");
         }
      }

      private void InterruptGpioPin_ValueChanged(object sender, DigitalInputPortEventArgs args)
      {
         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         byte numberOfBytes = 0;
         string messageText = "";
         bool transmitDone = false;

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

            numberOfBytes = this.RegisterReadByte(0x13); // RegRxNbBytes
            byte[] messageBytes = this.RegisterRead(0x00, numberOfBytes); // RegFifo
            messageText = UTF8Encoding.UTF8.GetString(messageBytes);
         }

         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            this.RegisterWriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
            transmitDone = true;
         }

         this.RegisterWriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
         if (numberOfBytes > 0)
         {
            Console.WriteLine("Received {0} byte message {1}", numberOfBytes, messageText);
         }
         if(transmitDone)
         {
            Console.WriteLine("Transmit-Done");
         }
      }
...
   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      private Rfm9XDevice rfm9XDevice;
      private byte NessageCount = Byte.MaxValue;

      public MeadowApp()
      {
         ISpiBus spiBus = Device.CreateSpiBus(500);
         if (spiBus == null)
         {
            Console.WriteLine("spiBus == null");
         }

         rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11, Device.Pins.D10);

         // 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(0x09, 0b10000000); // RegPaConfig

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

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

            // Set the Register Fifo address pointer
            rfm9XDevice.RegisterWriteByte(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);
            rfm9XDevice.RegisterWrite(0x0, messageBytes); // RegFifo 

            // Set the length of the message in the fifo
            rfm9XDevice.RegisterWriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
            Console.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);
            rfm9XDevice.RegisterWriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

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

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

The diagnostic output shows inbound and outbound messages (Arduino then Meadow diagnostics)

21:15:56.070 -> 0x1: 0x81
21:15:56.070 -> 0x2: 0x1A
…
21:15:57.527 -> 0x7E: 0x0
21:15:57.527 -> 0x7F: 0x0
21:15:57.527 -> LoRa init succeeded.
21:15:58.101 -> Sending HeLoRa World! 0
21:16:06.958 -> Message: W10 IoT Core LoRa! 254
21:16:06.991 -> RSSI: -60
21:16:06.991 -> Snr: 9.50
21:16:06.991 ->
21:16:09.062 -> Sending HeLoRa World! 2
21:16:17.184 -> Message: W10 IoT Core LoRa! 253
21:16:17.218 -> RSSI: -61
21:16:17.218 -> Snr: 9.75
21:16:17.218 ->
21:16:19.876 -> Sending HeLoRa World! 4
21:16:21.946 -> Message: ⸮LoRaIoT1Maduino2at 65.3,ah 70,wsa 6,wsg 12,wd 167.25,r 0.00,
21:16:22.014 -> RSSI: -76
21:16:22.014 -> Snr: 9.50
21:16:22.014 ->
21:16:27.406 -> Message: W10 IoT Core LoRa! 252
21:16:27.429 -> RSSI: -60
21:16:27.429 -> Snr: 9.50
21:16:27.429 ->
21:16:30.153 -> Sending HeLoRa World! 6
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveTransmitInterrupt\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveTransmitInterrupt\bin\Debug\net472\Meadow.dll'. 
The program '[42104] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[42104] App.exe' has exited with code 0 (0x0).
 .
 .
 DirectRegisterAccess = True
 .
 .
 Sending 22 bytes message W10 IoT Core LoRa! 255
 Transmit-Done
 Received 15 byte message HeLoRa World! 0
 Sending 22 bytes message W10 IoT Core LoRa! 254
 Transmit-Done
 Received 15 byte message HeLoRa World! 2
 Sending 22 bytes message W10 IoT Core LoRa! 253
 Transmit-Done
 Received 15 byte message HeLoRa World! 4
 Received 61 byte message ???LoRaIoT1Maduino2at 65.3,ah 70,wsa 6,wsg 12,wd 167.25,r 0.00,
 Sending 22 bytes message W10 IoT Core LoRa! 252
 Transmit-Done
 Received 15 byte message HeLoRa World! 6
 Sending 22 bytes message W10 IoT Core LoRa! 251
 Transmit-Done
 Received 15 byte message HeLoRa World! 8
 Sending 22 bytes message W10 IoT Core LoRa! 250
 Transmit-Done
 Received 16 byte message HeLoRa World! 10
 Sending 22 bytes message W10 IoT Core LoRa! 249
 Transmit-Done
 Received 16 byte message HeLoRa World! 12
 Received 40 byte message ???LoRaIoT1#)#???c???h 55,t 24.9,s 3,v 4.01
 Sending 22 bytes message W10 IoT Core LoRa! 248
 Transmit-Done
 Received 16 byte message HeLoRa World! 14
 Sending 22 bytes message W10 IoT Core LoRa! 247
 Transmit-Done
 Received 16 byte message ???LoRaIoT1Maduino
 Sending 22 bytes message W10 IoT Core LoRa! 246
 Transmit-Done
 Received 16 byte message HeLoRa World! 18
 Sending 22 bytes message W10 IoT Core LoRa! 245
 Transmit-Done
 Received 16 byte message HeLoRa World! 20
 Sending 22 bytes message W10 IoT Core LoRa! 244
 Transmit-Done
 Received 16 byte message HeLoRa World! 22
 Sending 22 bytes message W10 IoT Core LoRa! 243
 Transmit-Done
 Received 16 byte message HeLoRa World! 24
 Sending 22 bytes message W10 IoT Core LoRa! 242
 Transmit-Done

The RegIrqFlags 01011000 indicates the RxDone, ValidHeader, and TxDone flags were set which was what I was expecting. Note the interference, the 46 byte packet

Next step is some refactoring to extract the register access code and merging with my Windows 10 IoT Core RMF9X library code base.

.Net Meadow RFM95/96/97/98 LoRa 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).

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

      public Rfm9XDevice(IIODevice device, ISpiBus spiBus, IPin chipSelectPin, IPin resetPin, IPin interruptPin)
      {
         // Chip select pin configuration
         ChipSelectGpioPin = device.CreateDigitalOutputPort(chipSelectPin, initialState: true);
         if (ChipSelectGpioPin == null)
         {
            Console.WriteLine("ChipSelectGpioPin == null");
         }

         // Factory reset pin configuration
         IDigitalOutputPort resetGpioPin = device.CreateDigitalOutputPort(resetPin);
         if (resetGpioPin == null)
         {
            Console.WriteLine("resetGpioPin == null");
         }
         resetGpioPin.State = false;
         Task.Delay(10);
         resetGpioPin.State = true;
         Task.Delay(10);

         // Interrupt pin for RX message & TX done notification 
         InterruptGpioPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeRising);
         InterruptGpioPin.Changed += InterruptGpioPin_ValueChanged;

         Rfm9XLoraModem = new SpiPeripheral(spiBus, ChipSelectGpioPin);
         if (Rfm9XLoraModem == null)
         {
            Console.WriteLine("Rfm9XLoraModem == null");
         }
      }

      private void InterruptGpioPin_ValueChanged(object sender, DigitalInputPortEventArgs args)
      {
        byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
        this.RegisterWriteByte(0x12, 0xff);// Clear RegIrqFlags

        //Console.WriteLine(string.Format("RegIrqFlags:{0}", Convert.ToString(irqFlags, 2).PadLeft(8, '0')));
         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            Console.WriteLine("Transmit-Done");
         }
      }
…
   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      private Rfm9XDevice rfm9XDevice;

      public MeadowApp()
      {
         ISpiBus spiBus = Device.CreateSpiBus(500);
         if (spiBus == null)
         {
            Console.WriteLine("spiBus == null");
         }

         rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11, Device.Pins.D10);

         // 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, 0x00); // 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);
            rfm9XDevice.RegisterWrite(0x0, messageBytes); // RegFifo 

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

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

The output in the debug window

'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\TransmitInterrupt\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\TransmitInterrupt\bin\Debug\net472\Meadow.dll'. 
The program '[11164] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[11164] App.exe' has exited with code 0 (0x0).
.
.
DirectRegisterAccess = True
.
.
Sending 11 bytes message Hello LoRa!
Transmit-Done
Sending 11 bytes message Hello LoRa!
Transmit-Done
Sending 11 bytes message Hello LoRa!
Transmit-Done

On the Arduino test client the serial monitor displayed

13:02:09.098 -> Sending HeLoRa World! 4
13:02:19.130 -> Message: ⸮LoRaIoT1Maduino2at 79.7,ah 39,wsa 6,wsg 13,wd 28.13,r 0.00,
13:02:19.177 -> RSSI: -72
13:02:19.177 -> Snr: 9.25
13:02:19.177 -> 
13:02:19.431 -> Sending HeLoRa World! 6
13:02:29.994 -> Sending HeLoRa World! 8
13:02:32.000 -> Message: Hello LoRa!
13:02:32.000 -> RSSI: -46
13:02:32.047 -> Snr: 9.50
13:02:32.047 -> 
13:02:40.750 -> Sending HeLoRa World! 10
13:02:42.260 -> Message: Hello LoRa!
13:02:42.260 -> RSSI: -45
13:02:42.314 -> Snr: 9.50
13:02:42.314 -> 
13:02:51.286 -> Sending HeLoRa World! 12
13:02:52.541 -> Message: Hello LoRa!
13:02:52.541 -> RSSI: -45
13:02:52.541 -> Snr: 9.75
13:02:52.541 -> 
13:03:02.112 -> Sending HeLoRa World! 14
13:03:02.745 -> Message: Hello LoRa!
13:03:02.745 -> RSSI: -45
13:03:02.792 -> Snr: 9.50
13:03:02.792 -> 

Now that I’m confident my hardware is all working the next step will be building a full featured client based on my Windows 10 IoT Core library.

.Net Meadow RFM95/96/97/98 LoRa library Part6

Receive Interrupt

This proof of concept (PoC) was to confirm I could configure the Semtech 127X then handle the received messages using a Meadow event handler. The event handler code is based on the implementation in my Windows IoT 10 Core library.

I had added a few Console.WriteLine statements (Debug.Print currently doesn’t work Dec 2019) so I could see what was going on. But, using Console.WriteLine in the event handler caused me some problems which I had to debug. The irqFlags bit mask indicated there was a message in the FIFO but it wasn’t displayed and the interrupt mask wasn’t getting reset. As a temporary fix I refactored the code so the Console.WriteLine was the last statement in the EventHandler(which may cause other issues).

public Rfm9XDevice(IIODevice device, ISpiBus spiBus, IPin chipSelectPin, IPin resetPin, IPin interruptPin)
      {
         // Chip select pin configuration
         ChipSelectGpioPin = device.CreateDigitalOutputPort(chipSelectPin, initialState: true);
         if (ChipSelectGpioPin == null)
         {
            Console.WriteLine("ChipSelectGpioPin == null");
         }

         // Factory reset pin configuration
         IDigitalOutputPort resetGpioPin = device.CreateDigitalOutputPort(resetPin);
         if (resetGpioPin == null)
         {
            Console.WriteLine("resetGpioPin == null");
         }
         resetGpioPin.State = false;
         Task.Delay(10);
         resetGpioPin.State = true;
         Task.Delay(10);

         // Interrupt pin for RX message & TX done notification 
         InterruptGpioPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeRising);
         InterruptGpioPin.Changed += InterruptGpioPin_ValueChanged;

         Rfm9XLoraModem = new SpiPeripheral(spiBus, ChipSelectGpioPin);
         if (Rfm9XLoraModem == null)
         {
            Console.WriteLine("Rfm9XLoraModem == null");
         }
      }
...
      private void InterruptGpioPin_ValueChanged(object sender, DigitalInputPortEventArgs args)
      {
         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         byte numberOfBytes = 0;
         string messageText = "";

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

            numberOfBytes = this.RegisterReadByte(0x13); // RegRxNbBytes
            byte[] messageBytes = this.RegisterRead(0x00, numberOfBytes); // RegFifo
            messageText = UTF8Encoding.UTF8.GetString(messageBytes);
         }

         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
         if (numberOfBytes > 0)
         {
            Console.WriteLine("Received {0} byte message {1}", numberOfBytes, messageText);
         }
      }
...
public class MeadowApp : App
{
private Rfm9XDevice rfm9XDevice;
  public MeadowApp()
  {
     ISpiBus spiBus = Device.CreateSpiBus(500);
     if (spiBus == null)
     {
        Console.WriteLine("spiBus == null");
     }

     rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11, Device.Pins.D10);

     // 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 DI0 RxReady & TxReady

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

     rfm9XDevice.RegisterDump();

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

The output in the output debug window looked like this

'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveInterrupt\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveInterrupt\bin\Debug\net472\Meadow.dll'. 
The program '[40348] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[40348] App.exe' has exited with code 0 (0x0).
.
.
DirectRegisterAccess = True
.
.
Register dump
Register 0x00 - Value 0X6c - Bits 01101100
…
Register 0x42 - Value 0X12 - Bits 00010010
Receive-Wait
Received 15 byte message HeLoRa World! 0
Received 59 byte message ???LoRaIoT1Maduino2at 57.2,ah 67,wsa 1,wsg 3,wd 56.63,r 0.00,
Received 15 byte message HeLoRa World! 2
Received 15 byte message HeLoRa World! 4
Received 15 byte message HeLoRa World! 6
Received 15 byte message HeLoRa World! 8
Received 16 byte message HeLoRa World! 10

Next steps will be wiring up the transmit done interrupt, then building a full featured client based on my Windows 10 IoT Core library.

.Net Meadow RFM95/96/97/98 LoRa library Part5

Receive Basic

For testing this code I used the same version of the LoRaSetSyncWord example as Transmit Basic

20:57:40.239 -> LoRa init succeeded.
20:57:40.759 -> Sending HeLoRa World! 0
20:57:40.968 -> Message: ⸮LoRaIoT1Maduino2at 57.7,ah 64,wsa 1,wsg 2,wd 88.13,r 0.00,
20:57:41.036 -> RSSI: -72
20:57:41.036 -> Snr: 9.50
20:57:41.036 -> 
20:57:51.766 -> Sending HeLoRa World! 2
20:58:02.532 -> Sending HeLoRa World! 4
20:58:12.845 -> Sending HeLoRa World! 6
20:58:23.434 -> Sending HeLoRa World! 8
20:58:34.190 -> Sending HeLoRa World! 10
20:58:42.005 -> Message: ⸮LoRaIoT1Maduino2at 57.6,ah 64,wsa 2,wsg 5,wd 74.25,r 0.00,
20:58:42.074 -> RSSI: -72
20:58:42.074 -> Snr: 9.75

The Meadow code builds on my Windows 10 IoT Core Receive Basic and the Meadow Transmit Basic samples.

public class MeadowApp : App<F7Micro, MeadowApp>
{
	private Rfm9XDevice rfm9XDevice;

	public MeadowApp()
	{
		ISpiBus spiBus = Device.CreateSpiBus(500);
		if (spiBus == null)
		{
			Console.WriteLine("spiBus == null");
		}

		rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11);

		// 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(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

		while (true)
		{
			// Wait until a packet is received, no timeouts in PoC
			Console.WriteLine("Receive-Wait");
			byte IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
			while ((IrqFlags & 0b01000000) == 0)  // wait until RxDone cleared
			{
				Task.Delay(100).Wait();
				IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
				Console.Write(".");
			}
			Console.WriteLine("");
			Console.WriteLine(string.Format("RegIrqFlags {0}", Convert.ToString((byte)IrqFlags, 2).PadLeft(8, '0')));
			Console.WriteLine("Receive-Message");
			byte currentFifoAddress = rfm9XDevice.RegisterReadByte(0x10); // RegFifiRxCurrent
			rfm9XDevice.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

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

			byte[] messageBytes = rfm9XDevice.RegisterRead(0x00, numberOfBytes); // RegFifo

			rfm9XDevice.RegisterWriteByte(0x0d, 0);
			rfm9XDevice.RegisterWriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits

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

			Console.WriteLine("Receive-Done");
		}
	}
}

The receive code works reliably but has no error detection or correction capability, so every so often a message gets corrupted. Which is can be seen in the Debug output below.

'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveBasic\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveBasic\bin\Debug\net472\Meadow.dll'. 
The program '[18208] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[18208] App.exe' has exited with code 0 (0x0).
.
.
DirectRegisterAccess = True
.
.
Receive-Wait
RegIrqFlags 01010000
Receive-Message
Received 61 byte message ???LoRaIoT1Maduino2at 57.7,ah 64,wsa 2,wsg 4,wd 254.25,r 0.00,
Receive-Done
Receive-Wait
RegIrqFlags 01110000
Receive-Message
Received 60 byte message ???LoReE??????!???ngvyno2at,57/7-ah 6???,w???a 2,w?????????6,7$.13,r 0.00-
Receive-Done
Receive-Wait
RegIrqFlags 01010000
Receive-Message
Received 16 byte message HeLoRa World! 0
Receive-Done
Receive-Wait
RegIrqFlags 01000000
Receive-Message
Received 60 byte message ???LoRaIoT1Maduino2at 57.7,ah 64,wsa 1,wsg 2,wd 88.13,r 0.00,
Receive-Done
Receive-Wait
RegIrqFlags 01010000
Receive-Message
Received 16 byte message HeLoRa World! 2
Receive-Done
Receive-Wait

I will look at implementing some sort of carrier-sense multiple access with collision avoidance solution to reduce the number of corrupted messages when two (or possibly more devices) transmit at the same time.

Transmitting/receiving with interrupts or design goals next.