netMF Electric Longboard Part 1

Wiichuck connectivity

Roughly four years ago I build myself an electric longboard as summer transport. It initially had a controller built with a devDuino V2.2 which after a while I “upgraded” to a GHI Electronics .NET Microframework device.

Configuring the original netMF based longboard

Now that GHI Electronics no longer supports the FEZ Panda III I figured upgrading to a device that runs the nanoFramework would be a good compromise.

I control the speed of the longboard with a generic wireless wii nunchuk. So my first project is porting the .NET Micro Framework Toolbox code to the nanoFramework.

wireless controller test rig

My test rig uses (prices as at Aug 2020) the following parts

  • Netduino 3 Wifi
  • Grove-Base Shield V2.0 for Arduino USD4.45
  • Grove-Universal 4 Pin Bucked 5cm cable(5 PCs Pack) USD1.90
  • Grove-Nunchuck USD2.90
  • Generic wireless WII nunchuk

My changes were mainly related to the Inter Integrated Circuit(I2C) configuration and the reading+writing of registers.

/// <summary>
/// Initialises a new Wii Nunchuk
/// </summary>
/// <param name="busId">The unique identifier of the I²C to use.</param>
/// <param name="slaveAddress">The I²C address</param>
/// <param name="busSpeed">The bus speed, an enumeration that defaults to StandardMode</param>
/// <param name="sharingMode">The sharing mode, an enumeration that defaults to Shared.</param>
public WiiNunchuk(string busId, ushort slaveAddress = 0x52, I2cBusSpeed busSpeed = I2cBusSpeed.StandardMode, I2cSharingMode sharingMode = I2cSharingMode.Shared)
   {
      I2cTransferResult result;

      // This initialisation routine seems to work. I got it at http://wiibrew.org/wiki/Wiimote/Extension_Controllers#The_New_Way
      Device = I2cDevice.FromId(busId, new I2cConnectionSettings(slaveAddress)
      {
         BusSpeed = busSpeed,
         SharingMode = sharingMode,
      });

      result = Device.WritePartial(new byte[] { 0xf0, 0x55 });
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      result = Device.WritePartial(new byte[] { 0xfb, 0x00 });
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      this.Device.Write(new byte[] { 0xf0, 0x55 });
      this.Device.Write(new byte[] { 0xfb, 0x00 });
   }

   /// <summary>
   /// Reads all data from the nunchuk
   /// </summary>
   public void Read()
   {
      byte[] WaitWriteBuffer = { 0 };
      I2cTransferResult result;

      result = Device.WritePartial(WaitWriteBuffer);
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      byte[] ReadBuffer = new byte[6];
      result = Device.ReadPartial(ReadBuffer);
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      // Parses data according to http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck#Data_Format

      // Analog stick
      this.AnalogStickX = ReadBuffer[0];
      this.AnalogStickY = ReadBuffer[1];

      // Accelerometer
      ushort AX = (ushort)(ReadBuffer[2] << 2);
      ushort AY = (ushort)(ReadBuffer[3] << 2);
      ushort AZ = (ushort)(ReadBuffer[4] << 2);
      AZ += (ushort)((ReadBuffer[5] & 0xc0) >> 6); // 0xc0 = 11000000
      AY += (ushort)((ReadBuffer[5] & 0x30) >> 4); // 0x30 = 00110000
      AX += (ushort)((ReadBuffer[5] & 0x0c) >> 2); // 0x0c = 00001100
      this.AcceleroMeterX = AX;
      this.AcceleroMeterY = AY;
      this.AcceleroMeterZ = AZ;

      // Buttons
      ButtonC = (ReadBuffer[5] & 0x02) != 0x02;    // 0x02 = 00000010
      ButtonZ = (ReadBuffer[5] & 0x01) != 0x01;    // 0x01 = 00000001
}

The nanoFramework code polls for the joystick position and accelerometer values every 100mSec

public class Program
{
   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard.WiiNunchuckTest starting");
      Debug.WriteLine(I2cDevice.GetDeviceSelector());

      try
      {
         WiiNunchuk nunchuk = new WiiNunchuk("I2C1");

         while (true)
         {
            nunchuk.Read();

            Debug.WriteLine($"JoyX: {nunchuk.AnalogStickX} JoyY:{nunchuk.AnalogStickY} AX:{nunchuk.AcceleroMeterX} AY:{nunchuk.AcceleroMeterY} AZ:{nunchuk.AcceleroMeterZ} BtnC:{nunchuk.ButtonC} BtnZ:{nunchuk.ButtonZ}");

            Thread.Sleep(100);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}

The setup to use for the I2C port was determined by looking at the board.h and target_windows_devices_I2C_config.cpp file

//
// Copyright (c) 2018 The nanoFramework project contributors
// See LICENSE file in the project root for full license information.
//

#include <win_dev_i2c_native_target.h>

//////////
// I2C1 //
//////////

// pin configuration for I2C1
// port for SCL pin is: GPIOB
// port for SDA pin is: GPIOB
// SCL pin: is GPIOB_6
// SDA pin: is GPIOB_7
// GPIO alternate pin function is 4 (see alternate function mapping table in device datasheet)
I2C_CONFIG_PINS(1, GPIOB, GPIOB, 6, 7, 4)

Then checking this against the Netduino 3 Wifi schematic.

This image has an empty alt attribute; its file name is netduinoschematic-1.jpg

After some experimentation with how to detect if an I2C read or write had failed the debugging console output began displaying reasonable value

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.Longboard.WiiNunchuckTest starting
I2C1
JoyX: 128 JoyY:128 AX:520 AY:508 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:520 AY:504 AZ:716 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:524 AY:508 AZ:716 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:524 AY:536 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:516 AY:528 AZ:724 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:492 AY:524 AZ:720 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:508 AY:528 AZ:700 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:504 AY:532 AZ:716 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:512 AY:532 AZ:724 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:516 AY:532 AZ:712 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:520 AY:532 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:524 AY:532 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:480 AY:504 AZ:688 BtnC:True BtnZ:True
JoyX: 128 JoyY:128 AX:480 AY:520 AZ:728 BtnC:False BtnZ:True
JoyX: 128 JoyY:128 AX:512 AY:520 AZ:704 BtnC:False BtnZ:True
JoyX: 128 JoyY:128 AX:512 AY:548 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:504 AY:516 AZ:728 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:548 AY:536 AZ:704 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:500 AY:528 AZ:728 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:496 AY:524 AZ:716 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:528 AY:536 AZ:696 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:540 AY:540 AZ:720 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:500 AY:520 AZ:684 BtnC:False BtnZ:False
JoyX: 128 JoyY:0 AX:520 AY:508 AZ:696 BtnC:False BtnZ:False
JoyX: 29 JoyY:0 AX:488 AY:576 AZ:716 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:532 AY:540 AZ:700 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:492 AY:512 AZ:708 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:492 AY:516 AZ:708 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:504 AY:512 AZ:708 BtnC:False BtnZ:False
JoyX: 27 JoyY:128 AX:508 AY:520 AZ:700 BtnC:False BtnZ:False
JoyX: 106 JoyY:128 AX:504 AY:516 AZ:700 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:496 AY:520 AZ:700 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:512 AY:532 AZ:716 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:500 AY:516 AZ:708 BtnC:False BtnZ:False
JoyX: 85 JoyY:113 AX:500 AY:536 AZ:720 BtnC:False BtnZ:False
JoyX: 128 JoyY:110 AX:512 AY:532 AZ:712 BtnC:False BtnZ:False
JoyX: 128 JoyY:90 AX:516 AY:528 AZ:716 BtnC:False BtnZ:False
JoyX: 128 JoyY:43 AX:508 AY:468 AZ:660 BtnC:False BtnZ:False
JoyX: 128 JoyY:0 AX:508 AY:532 AZ:712 BtnC:False BtnZ:False
JoyX: 128 JoyY:0 AX:496 AY:524 AZ:716 BtnC:False BtnZ:False

The next test rig will be getting Pulse Width Modulation(PWM) working.

nanoFramework nRF24L01 library Part1

After porting then debugging Windows 10 IoT Core, .NetMF, Wilderness Labs Meadow and GHI Electronics TinyCLR nRF24L01P libraries I figured yet another port, this time to a nanoFramework powered devices should be low risk.

My initial test rig uses a Netduino 3 Wifi and an Embedded Coolness nRF24 shield as I didn’t need to use jumper wires.

//---------------------------------------------------------------------------------
// Copyright (c) July 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.
//
//---------------------------------------------------------------------------------
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update

namespace devMobile.IoT.nRf24L01.ModuleSPI
{
   using System;
   using System.Threading;
   using System.Diagnostics;
   using System.Text;
   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

   public class Program
   {
      const byte SETUP_AW = 0x03;
      const byte RF_CH = 0x05;
      const byte RX_ADDR_P0 = 0x0A;
      const byte R_REGISTER = 0b00000000;
      const byte W_REGISTER = 0b00100000;
      const string P0_Address = "ZYXWV";

#if NETDUINO3_WIFI
      private const string SpiBusId = "SPI2";
#endif

      public static void Main()
      {
#if NETDUINO3_WIFI
         // Arduino D7->PD7
         int chipSelectPinNumber = PinNumber('A', 1);
#endif
         Debug.WriteLine("devMobile.IoT.nRf24L01.ModuleSPI starting");

         Debug.WriteLine(Windows.Devices.Spi.SpiDevice.GetDeviceSelector());

         try
         {
            GpioController gpioController = GpioController.GetDefault();

            var settings = new SpiConnectionSettings(chipSelectPinNumber)
            {
               ClockFrequency = 2000000,
               Mode = SpiMode.Mode0,
               SharingMode = SpiSharingMode.Shared,
            };

            using (SpiDevice device = SpiDevice.FromId(SpiBusId, settings))
            {
               Debug.WriteLine("nrf24L01Device Device...");
               if (device == null)
               {
                  Debug.WriteLine("nrf24L01Device == null");
               }

               Thread.Sleep(100);

               Debug.WriteLine("ConfigureSpiPort Done...");
               Debug.WriteLine("");

               Thread.Sleep(500);
               try
               {
                  // Read the Address width
                  Debug.WriteLine("Read address width");
                  byte[] txBuffer1 = new byte[] { SETUP_AW | R_REGISTER, 0x0 };
                  byte[] rxBuffer1 = new byte[txBuffer1.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...SETUP_AW");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer1));
                  device.TransferFullDuplex(txBuffer1, rxBuffer1);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer1));

                  // Extract then adjust the address width
                  byte addressWidthValue = rxBuffer1[1];
                  addressWidthValue &= 0b00000011;
                  addressWidthValue += 2;
                  Debug.WriteLine($"Address width 0x{SETUP_AW:x2} - Value 0X{rxBuffer1[1]:x2} Value adjusted {addressWidthValue}");
                  Debug.WriteLine("");

                  // Write Pipe0 Receive address
                  Debug.WriteLine($"Write Pipe0 Receive Address {P0_Address}");
                  byte[] txBuffer2 = new byte[addressWidthValue + 1];
                  byte[] rxBuffer2 = new byte[txBuffer2.Length];
                  txBuffer2[0] = RX_ADDR_P0 | W_REGISTER;
                  Array.Copy(Encoding.UTF8.GetBytes(P0_Address), 0, txBuffer2, 1, addressWidthValue);

                  Debug.WriteLine(" nrf24L01Device.Write...RX_ADDR_P0");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer2));
                  device.TransferFullDuplex(txBuffer2, rxBuffer2);
                  Debug.WriteLine("");

                  // Read Pipe0 Receive address
                  Debug.WriteLine("Read Pipe0 Receive address");
                  byte[] txBuffer3 = new byte[addressWidthValue + 1];
                  txBuffer3[0] = RX_ADDR_P0 | R_REGISTER;
                  byte[] rxBuffer3 = new byte[txBuffer3.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RX_ADDR_P0");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer3));
                  device.TransferFullDuplex(txBuffer3, rxBuffer3);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer3));
                  Debug.WriteLine($"Address 0x{RX_ADDR_P0:x2} Address {UTF8Encoding.UTF8.GetString(rxBuffer3, 1, addressWidthValue)}");
                  Debug.WriteLine("");

                  // Read the RF Channel
                  Debug.WriteLine("RF Channel read 1");
                  byte[] txBuffer4 = new byte[] { RF_CH | R_REGISTER, 0x0 };
                  byte[] rxBuffer4 = new byte[txBuffer4.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer4));
                  device.TransferFullDuplex(txBuffer4, rxBuffer4);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer4));

                  byte rfChannel1 = rxBuffer4[1];
                  Debug.WriteLine($"RF Channel 1 0x{RF_CH:x2} - Value 0X{rxBuffer4[1]:x2} - Value adjusted {rfChannel1+2400}");
                  Debug.WriteLine("");

                  // Write the RF Channel
                  Debug.WriteLine("RF Channel write");
                  byte[] txBuffer5 = new byte[] { RF_CH | W_REGISTER, rfChannel1+=1};
                  byte[] rxBuffer5 = new byte[txBuffer5.Length];

                  Debug.WriteLine(" nrf24L01Device.Write...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer5));
                  //device.Write(txBuffer5);
                  device.TransferFullDuplex(txBuffer5, rxBuffer5);
                  Debug.WriteLine("");

                  // Read the RF Channel
                  Debug.WriteLine("RF Channel read 2");
                  byte[] txBuffer6 = new byte[] { RF_CH | R_REGISTER, 0x0 };
                  byte[] rxBuffer6 = new byte[txBuffer6.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer6));
                  device.TransferFullDuplex(txBuffer6, rxBuffer6);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer6));

                  byte rfChannel2 = rxBuffer6[1];
                  Debug.WriteLine($"RF Channel 2 0x{RF_CH:x2} - Value 0X{rxBuffer6[1]:x2} - Value adjusted {rfChannel2+2400}");
                  Debug.WriteLine("");
               }
               catch (Exception ex)
               {
                  Debug.WriteLine("Configure Port0 " + ex.Message);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

#if NETDUINO3_WIFI
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}

After bit of tinkering with SPI configuration options and checking device.Write vs. device.TransferFullDuplex usage. I can reliably read and write my nRF24L01 device’s receive port address and channel configuration.

devMobile.IoT.nRf24L01.ModuleSPI starting
SPI1,SPI2,SPI3,SPI4
nrf24L01Device Device...
ConfigureSpiPort Done...

Read address width
 nrf24L01Device.TransferFullDuplex...SETUP_AW
 txBuffer:03-00
 rxBuffer:0E-03
Address width 0x03 - Value 0X03 Value adjusted 5

Write Pipe0 Receive Address ZYXWV
 nrf24L01Device.Write...RX_ADDR_P0
 txBuffer:2A-5A-59-58-57-56

Read Pipe0 Receive address
 nrf24L01Device.TransferFullDuplex...RX_ADDR_P0
 txBuffer:0A-00-00-00-00-00
 rxBuffer:0E-5A-59-58-57-56
Address 0x0A Address ZYXWV

RF Channel read 1
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-02
RF Channel 1 0x05 - Value 0X02 - Value adjusted 2402

RF Channel write
 nrf24L01Device.Write...RF_CH
 txBuffer:25-03

RF Channel read 2
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-03
RF Channel 2 0x05 - Value 0X03 - Value adjusted 2403

The thread '<No Name>' (0x1) has exited with code 0 (0x0).
Done.

Next step is to port my TinyCLR nRF24L01 library which is based on the Techfoonina Windows 10 IoT Core port which is based on .NetMF library by Gralin.

nanoFramework LoRa library Part6

Transmit and Receive with Interrupts

For the final revision my nanoFramework SX127X Library test harness I checked interrupts were working for the interleaved transmission and reception of messages.

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

         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         Console.WriteLine($"RegIrqFlags 0X{irqFlags:x2}");

         if ((irqFlags & 0b01000000) == 0b01000000)  // RxDone 
         {
            Console.WriteLine("Receive-Message");
            byte currentFifoAddress = this.RegisterReadByte(0x10); // RegFifiRxCurrent
            this.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

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

            // Allocate buffer for message
            byte[] messageBytes = this.RegisterRead(0X0, numberOfBytes);

            // Remove unprintable characters from messages
            for (int index = 0; index < messageBytes.Length; index++)
            {
               if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
               {
                  messageBytes[index] = 0x20;
               }
            }

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

         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            this.RegisterWriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
            Console.WriteLine("Transmit-Done");
         }

         this.RegisterWriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
      }

…
class Program
{
      static void Main()
      {
         int SendCount = 0;
#if ST_STM32F429I_DISCOVERY
         int chipSelectPinNumber = PinNumber('C', 2);
         int resetPinNumber = PinNumber('C', 3);
         int interruptPinNumber = PinNumber('A', 4);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
         int interruptPinNumber = Gpio.IO26;
#endif

         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, interruptPinNumber);
#endif
#if ST_STM32F429I_DISCOVERY
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber, interruptPinNumber);
#endif
            Thread.Sleep(500);

            // Put device into LoRa + Standby 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 {SendCount += 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 {messageBytes.Length} bytes message {messageText}");
               rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

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

The diagnostic output shows inbound and outbound messages

'nanoFramework.Tools.VS2019.Extension.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.NetNF\packages\nanoFramework.Hardware.Esp32.1.2.1-preview.10\lib\nanoFramework.Hardware.Esp32.dll', Symbols loaded.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Sending 13 bytes message Hello LoRa 1!
RegIrqFlags 0X08
Transmit-Done
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 136
Sending 13 bytes message Hello LoRa 2!
RegIrqFlags 0X08
Transmit-Done
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 138
Sending 13 bytes message Hello LoRa 3!
RegIrqFlags 0X08
Transmit-Done
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 140
20:32:58.079 -> Sending HeLoRa World! 134
20:33:04.553 -> Message: Hello LoRa 1!
20:33:04.553 -> Length: 13
20:33:04.587 -> FirstChar: 72
20:33:04.587 -> RSSI: -52
20:33:04.587 -> Snr: 9.50
20:33:04.622 -> 
20:33:08.137 -> Sending HeLoRa World! 136
20:33:14.568 -> Message: Hello LoRa 2!
20:33:14.568 -> Length: 13
20:33:14.602 -> FirstChar: 72
20:33:14.602 -> RSSI: -53
20:33:14.602 -> Snr: 9.75
20:33:14.635 -> 
20:33:19.135 -> Sending HeLoRa World! 138
20:33:24.560 -> Message: Hello LoRa 3!
20:33:24.560 -> Length: 13
20:33:24.594 -> FirstChar: 72
20:33:24.594 -> RSSI: -52
20:33:24.594 -> Snr: 9.25
20:33:24.628 -> 

There did appear to be some oddness (leading to corrupted first message) with the RegOpMode setting(0b10000000 vs. 0b10000001) for my STM32F429 Discovery and Sparkfun LoRa Gateway 1 Channel ESP32.

I think it maybe due to the Discovery having the rest line connected but unlike the Sparkfun LoRa Gateway.

nanoFramework LoRa library Part5

Receive Basic

This code implements the reception of messages builds on my transmit basic sample. I had to add a simple for loop to replace un-printable characters in the received message with spaces as nanoFramework UTF8Encoding.UTF8.GetString was throwing exceptions.

23:06:19.172 -> Sending HeLoRa World! 94
23:06:29.556 -> Sending HeLoRa World! 96
23:06:40.274 -> Sending HeLoRa World! 98
23:06:51.064 -> Sending HeLoRa World! 100
23:07:02.012 -> Sending HeLoRa World! 102
23:07:12.534 -> Sending HeLoRa World! 104
23:07:17.657 -> Message: ⸮LoRaIoT1Maduino2at 50.2,ah 90,wsa 4,wsg 11,wd 184.13,r 0.00,
23:07:17.725 -> Length: 61
23:07:17.725 -> FirstChar: 136
23:07:17.793 -> RSSI: -81
23:07:17.793 -> Snr: 9.50
23:07:17.793 -> 
23:07:23.216 -> Sending HeLoRa World! 106
23:07:34.228 -> Sending HeLoRa World! 108
23:07:44.907 -> Sending HeLoRa World! 110
23:07:55.930 -> Sending HeLoRa World! 112

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

   class Program
   {
#if ST_STM32F429I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const string SpiBusId = "SPI1";
#endif

      static void Main()
      {
#if ST_STM32F429I_DISCOVERY
         int chipSelectPinNumber = PinNumber('C', 2);
         int resetPinNumber = PinNumber('C', 3);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
#endif

         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber);
#endif
#if ST_STM32F429I_DISCOVERY
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber);
#endif
             Thread.Sleep(500);

            // 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
               {
                  Thread.Sleep(100);
                  irqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
                  Console.Write(".");
               }
               Console.WriteLine("");
               Console.WriteLine($"RegIrqFlags 0X{irqFlags:X2}");
               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

               // Remove unprintable characters from messages
               for( int index = 0; index < messageBytes.Length; index++)
               {
                  if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
                  {
                     messageBytes[index] = 0x20;
                  }
               }

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

               Console.WriteLine("Receive-Done");
            }
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }
      }

#if ST_STM32F429I_DISCOVERY
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }

The receive code works reliably but has no error detection or correction capability.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Receive-Wait
................................................................
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 114
Receive-Done
Receive-Wait
.......................................................................................................
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 116
Receive-Done
Receive-Wait
.....................
RegIrqFlags 0X50
Receive-Message
Received 60 byte message  LoRaIoT1Maduino2at 50.2,ah 90,wsa 3,wsg 5,wd 178.50,r 0.00,
Receive-Done
Receive-Wait
.......................................................................................
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 118
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.

nanoFramework LoRa library Part4B

Transmit Basic Revisited

After finding some possible SPI library issues (April 2020) with my STM32F429 Discovery + Dragino LoRa shield for Arduino test rig I wanted to trial my code on another nanoFramework platform.

I had ordered a Sparkfun LoRa Gateway 1 Channel ESP32 for a LoRaWAN research project from a local supplier and an unexpected “bonus” was that the ESP32 WROOM platform is supported by the nanoFramework.

Sparkfun LoRa Gateway 1 Channel with wire antenna

I am using this in conjunction with my Armtronix IA005 SX1276 loRa node and my STM32F429 Discovery + Dragino LoRa shield for Arduino test rig.

STM32F429 Discovery+ Dragino LoRa shield with Armtronix device

The code now works on STM32F429 Discovery and ESP32 WROOM platforms. (manual update nanoFramework.Hardware.Esp32 NuGet reference required)

Sparkfun LoRa Gateway 1 Channel schematic

One disadvantage of the SparkFun device is that the reset pin on the SX127X doesn’t appear to be connected to the ESP32 so I can’t factory reset the device in code.

//#define ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
#define ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
namespace devMobile.IoT.Rfm9x.TransmitBasic
{
   using System;
   using System.Text;
   using System.Threading;

   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

#if ESP32_WROOM_32_LORA_1_CHANNEL
   using nanoFramework.Hardware.Esp32;
#endif

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

      public Rfm9XDevice(string spiPort, int chipSelectPin, int resetPin)
      {
         var settings = new SpiConnectionSettings(chipSelectPin)
         {
            ClockFrequency = 1000000,
            //DataBitLength = 8,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared,
         };

         rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);

         // Factory reset pin configuration
         GpioController gpioController = GpioController.GetDefault();
         GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
         resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
         resetGpioPin.Write(GpioPinValue.Low);
         Thread.Sleep(10);
         resetGpioPin.Write(GpioPinValue.High);
         Thread.Sleep(10);
      }

      public Rfm9XDevice(string spiPort, int chipSelectPin)
      {
         var settings = new SpiConnectionSettings(chipSelectPin)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared,
         };

         rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress &= RegisterAddressReadMask, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         return readBuffer[1];
      }

      public ushort RegisterReadWord(byte address)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

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

      public byte[] RegisterRead(byte address, int length)
      {
         byte[] writeBuffer = new byte[length + 1];
         byte[] readBuffer = new byte[writeBuffer.Length];
         byte[] repyBuffer = new byte[length];

         writeBuffer[0] = address &= RegisterAddressReadMask;

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         Array.Copy(readBuffer, 1, repyBuffer, 0, length);

         return repyBuffer;
      }

      public void RegisterWriteByte(byte address, byte value)
      {
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void RegisterWriteWord(byte address, ushort value)
      {
         byte[] valueBytes = BitConverter.GetBytes(value);
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer,readBuffer);
      }

      public void RegisterWrite(byte address, byte[] bytes)
      {
         byte[] writeBuffer = new byte[1 + bytes.Length];
         byte[] readBuffer = new byte[writeBuffer.Length];

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

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
      }

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

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

   class Program
   {
#if ST_STM32F429I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const string SpiBusId = "SPI1";
#endif

      static void Main()
      {
         int SendCount = 0;
#if ST_STM32F429I_DISCOVERY
         int chipSelectPinNumber = PinNumber('C', 2);
         int resetPinNumber = PinNumber('C', 3);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
#endif
         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber);
#endif
#if ST_STM32F429I_DISCOVERY
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber);
#endif
            Thread.Sleep(500);

            // Put device into LoRa + Standby mode
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000001); // 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

            rfm9XDevice.RegisterDump();

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

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

               string messageText = $"Hello LoRa {SendCount += 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 {messageBytes.Length} bytes message {messageText}");
               /// Set the mode to LoRa + Transmit
               rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

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

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

#if ST_STM32F429I_DISCOVERY
      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
#endif
   }
}

When I initially ran the application in Visual Studio 2019 the text below was displayed in the output window.

Register dump
Register 0x00 - Value 0X00
Register 0x01 - Value 0X80
Register 0x02 - Value 0X1A
Register 0x03 - Value 0X0B
Register 0x04 - Value 0X00
…
Register 0x3E - Value 0X00
Register 0x3F - Value 0X00
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12
Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
.
.
.
.
.
Send-Done

I could the see the messages arriving at the Armtronix device in the Arduino monitor.

18:21:46.299 -> Sending HeLoRa World! 188
18:21:48.700 -> Message: p8V⸮⸮⸮⸮⸮Kg
18:21:48.700 -> Length: 13
18:21:48.734 -> FirstChar: 112
18:21:48.734 -> RSSI: -70
18:21:48.734 -> Snr: 9.50
18:21:48.769 -> 
18:21:50.193 -> Message: Hello LoRa 10!
18:21:50.193 -> Length: 14
18:21:50.226 -> FirstChar: 72
18:21:50.226 -> RSSI: -49
18:21:50.226 -> Snr: 10.00
18:21:50.260 -> 
18:21:56.652 -> Sending HeLoRa World! 190
18:21:58.765 -> Message: Hello LoRa 2!
18:21:58.765 -> Length: 13
18:21:58.798 -> FirstChar: 72
18:21:58.798 -> RSSI: -71
18:21:58.798 -> Snr: 9.75
18:21:58.832 -> 
18:22:00.268 -> Message: Hello LoRa 11!
18:22:00.268 -> Length: 14
18:22:00.302 -> FirstChar: 72
18:22:00.302 -> RSSI: -49
18:22:00.302 -> Snr: 10.00
18:22:00.336 -> 

The first message was getting corrupted (only when running in the debugger) which after some trial and error I think was most probably due to my RegOpMode register mode configuration.

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

// Put device into LoRa + Standby mode
rfm9XDevice.RegisterWriteByte(0x01, 0b10000001); 

After a couple of years and half a dozen platform ports still finding bugs in my samples…

nanoFramework LoRa library Part4A

Transmit Basic

I had a couple of Armtronix IA005 SX1276 loRa nodes sitting on my desk from a recent post so I used one of them running a modified version of the Arduino LoRa library LoRaSetSyncWord example to receive messages from my STM32F429 Discovery + Dragino LoRa shield for Arduino test rig.

STM32F429 Discovery+ Dragino LoRa shield with Armtronix device
/*
  LoRa Duplex communication with Sync Word
 
  Sends a message every half second, and polls continually
  for new incoming messages. Sets the LoRa radio's Sync Word.
 
  Spreading factor is basically the radio's network ID. Radios with different
  Sync Words will not receive each other's transmissions. This is one way you
  can filter out radios you want to ignore, without making an addressing scheme.
 
  See the Semtech datasheet, http://www.semtech.com/images/datasheet/sx1276.pdf
  for more on Sync Word.
 
  created 28 April 2017
  by Tom Igoe
*/
#include <stdlib.h>
#include <LoRa.h>
const int csPin = PA4;          // LoRa radio chip select
const int resetPin = PC13;       // LoRa radio reset
const int irqPin = PA11;         // change for your board; must be a hardware interrupt pin
 
byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send
 
void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);
 
  Serial.println("LoRa Duplex - Set sync word");
 
  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin
 
  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }
 
  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs
 
  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}
 
void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "HeLoRa World! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    interval = random(1000) + 10000;    // 10-11 seconds
    msgCount++;
  }
 
  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}
 
void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}
 
void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return
 
  // read packet header bytes:
  String incoming = "";
 
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }
 
  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

The STM32F429 Discovery application

namespace devMobile.IoT.Rfm9x.TransmitBasic
{
   using System;
   using System.Text;
   using System.Threading;

   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

   public sealed class Rfm9XDevice
   {
      private SpiDevice rfm9XLoraModem;
      private GpioPin chipSelectGpioPin;
      private const byte RegisterAddressReadMask = 0X7f;
      private const byte RegisterAddressWriteMask = 0x80;

      public Rfm9XDevice(string spiPort, int chipSelectPin, int resetPin)
      {
         var settings = new SpiConnectionSettings(chipSelectPin)
         {
            ClockFrequency = 500000,
//            DataBitLength = 8,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared,
         };

         rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);

         GpioController gpioController = GpioController.GetDefault();

         // Chip select pin configuration
         chipSelectGpioPin = gpioController.OpenPin(chipSelectPin);
         chipSelectGpioPin.SetDriveMode(GpioPinDriveMode.Output);

         // Factory reset pin configuration
         GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
         resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
         resetGpioPin.Write(GpioPinValue.Low);
         Thread.Sleep(10);
         resetGpioPin.Write(GpioPinValue.High);
         Thread.Sleep(10);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress &= RegisterAddressReadMask, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         return readBuffer[1];
      }

      public ushort RegisterReadWord(byte address)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

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

      public byte[] RegisterRead(byte address, int length)
      {
         byte[] writeBuffer = new byte[length + 1];
         byte[] readBuffer = new byte[length + 1];
         byte[] repyBuffer = new byte[length];

         writeBuffer[0] = address &= RegisterAddressReadMask;

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         Array.Copy(readBuffer, 1, repyBuffer, 0, length);

         return repyBuffer;
      }

      public void RegisterWriteByte(byte address, byte value)
      {
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };

         rfm9XLoraModem.Write(writeBuffer);
      }

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

         rfm9XLoraModem.Write(writeBuffer);
      }

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

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

         rfm9XLoraModem.Write(writeBuffer);
      }

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

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

   class Program
   {
      static void Main()
      {
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice("SPI5", PinNumber('C', 2), PinNumber('C', 3));
         int SendCount = 0;

         // 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

         rfm9XDevice.RegisterDump();

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

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

            string messageText = $"Hello LoRa {SendCount += 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 {messageBytes.Length} bytes message {messageText}");
            /// Set the mode to LoRa + Transmit
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

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

            Thread.Sleep(10000);
         }
      }

      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
   }

When I ran the nanoFramework application in Visual Studio 2019 the text below was displayed in the output window.

Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
.
.
.
.
.
Send-Done

I could the see the messages arriving at the Armtronix device in the Arduino monitor.

10:48:31.215 -> Sending HeLoRa World! 202
10:48:40.870 -> Message: ⸮Hello LoRa 1
10:48:40.870 -> Length: 13
10:48:40.905 -> FirstChar: 143
10:48:40.905 -> RSSI: -41
10:48:40.905 -> Snr: 9.00
10:48:40.940 -> 
10:48:41.630 -> Sending HeLoRa World! 204
10:48:50.946 -> Message: ⸮Hello LoRa 2
10:48:50.946 -> Length: 13
10:48:50.981 -> FirstChar: 143
10:48:50.981 -> RSSI: -34
10:48:50.981 -> Snr: 9.25

This nano Frameowork proof of concept (PoC) code is not working as expected. There is a single byte containing 0X8F (the ⸮) prepended to each message.

I downloaded the nanoFramework Windows.Devices.Spi project, and removed the Nerdbank.GitVersioning library. I could then build, deploy and single step through the nanoFramework SPI library.

Bytes to be sent in the Transmit Basic code
Bytes to be sent in the Windows.Devices.Spi library just before the firmware call

The extra byte prepended to the message is the write mask which is expected.

nanoFramework LoRa library Part2

Register Dump

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

I’m using “SPI5” for my testing as I could be confident in the pin numbers I copied from the nanoFramework SPI sample application.

namespace devMobile.IoT.Rfm9x.RegisterScan
{
   using System;
   using System.Threading;

   using Windows.Devices.Spi;

   public sealed class Rfm9XDevice
   {
      private SpiController spiController = SpiController.GetDefault();
      private SpiDevice rfm9XLoraModem;

      public Rfm9XDevice(string SpiPort, int chipSelectPin)
      {

         var settings = new SpiConnectionSettings(chipSelectPin)
         {
            ClockFrequency = 500000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared
         };

         rfm9XLoraModem = SpiDevice.FromId(SpiPort, settings);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress };
         byte[] readBuffer = new byte[1];

         rfm9XLoraModem.TransferSequential(writeBuffer, readBuffer);

         return readBuffer[0];
      }
   }

   public class Program
   {
      public static void Main()
      {
         try
         {
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice("SPI5", PinNumber('C', 2));

            Thread.Sleep(500);

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

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

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

      static int PinNumber(char port, byte pin)
      {
         if (port < 'A' || port > 'J')
            throw new ArgumentException();

         return ((port - 'A') * 16) + pin;
      }
   }
}

The output of the application looked like this

Attaching to nanoDevice...
Waiting for nanoDevice to initialize...
Updating nanoDevice debugger engine.
The nanoDevice runtime is loading the application assemblies and starting execution.
'nanoFramework.Tools.VS2019.Extension.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.NetNF\RegisterScan\bin\Debug\RegisterScan.exe', Symbols loaded.
'nanoFramework.Tools.VS2019.Extension.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.NetNF\packages\nanoFramework.Windows.Devices.Spi.1.3.0-preview.12\lib\Windows.Devices.Spi.dll', Symbols loaded.
'nanoFramework.Tools.VS2019.Extension.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.NetNF\packages\nanoFramework.Runtime.Events.1.4.2-preview.1\lib\nanoFramework.Runtime.Events.dll', Symbols loaded.
'nanoFramework.Tools.VS2019.Extension.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.NetNF\packages\nanoFramework.Windows.Devices.Gpio.1.4.1-preview.13\lib\Windows.Devices.Gpio.dll', 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 0x20 - Value 0X00
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 0XFB
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.

I’m running (April 2020) preview versions NuGets (1.7.0-preview.12) and having to re-flash the device more often than is desirable.