My implementation was “inspired” by the myDevices C/C++ sample code. The first step was to allocate a buffer to store the byte encoded values. I pre allocated the buffer to try and reduce the impacts of garbage collection. The code uses a manually incremented index into the buffer for performance reasons, plus the inconsistent support of System.Collections.Generic and Language Integrated Query(LINQ) on my three embedded platforms. The maximum length message that can be sent is limited by coding rate, duty cycle and bandwidth of the LoRa channel.
public Encoder(byte bufferSize)
{
if ((bufferSize < BufferSizeMinimum) || ( bufferSize > BufferSizeMaximum))
{
throw new ArgumentException($"BufferSize must be between {BufferSizeMinimum} and {BufferSizeMaximum}", "bufferSize");
}
buffer = new byte[bufferSize];
}
For a simple data types like a digital input a single byte (True or False ) is used. The channel parameter is included so that multiple values of the same data type can be included in a message.
public void DigitalInputAdd(byte channel, bool value)
{
if ((index + DigitalInputSize) > buffer.Length)
{
throw new ApplicationException("DigitalInputAdd insufficent buffer capacity");
}
buffer[index++] = channel;
buffer[index++] = (byte)DataType.DigitalInput;
// I know this is fugly but it works on all platforms
if (value)
{
buffer[index++] = 1;
}
else
{
buffer[index++] = 0;
}
}
For more complex data types like a Global Positioning System(GPS) location (Latitude, Longitude and Altitude) the values are converted to 32bit signed integers and only 3 of the 4 bytes are used.
The nanoFrameworkcode polls the wii nunchuk for the joystick position every 100mSec and then updates the PWM duty cycle.
By convention the ESSC PWM frequency is 50Hz (a pulse ever 20mSec) and the duration of the pulse is 1000uSec(minimum throttle) to 2000uSec(maximum throttle), note the change of units.
After converting to the same units there is a pulse every 20mSec and its duration is 1mSec too 2mSec. Then converting the durations to the active duty cycle percentage (for the PWM SetActiveDutyCyclePercentage) the duration of the pulse is 5% to 10%.
I need to re-calibrate the ESC for these durations and ensure that reverse is disabled. Then tinker with the brake (braking percent & percent drag brake) and acceleration(initial acceleration low, medium, high, very high) configurations of my ESC to make the longboard easier to ride.
Next I will look at configurable throttle maps (to make it easier for new and different weight users), then using one of the wii-nunchuk buttons for cruise control (keeping the throttle steady when riding is difficult) and how the software reacts when the connection with nunchuk fails
The next step was to figure out how to configure a Pulse Width Modulation (PWM) output and an Analog Input so I could adjust the duty cycle and control the brightness of a Light Emitting Diode(LED).
Netduino 3 ADC & PWN test rig
My test rig uses (prices as at Aug 2020) the following parts
public class Program
{
public static void Main()
{
Debug.WriteLine("devMobile.Longboard.AdcTest starting");
Debug.WriteLine(AdcController.GetDeviceSelector());
try
{
AdcController adc = AdcController.GetDefault();
AdcChannel adcChannel = adc.OpenChannel(0);
while (true)
{
double value = adcChannel.ReadRatio();
Debug.WriteLine($"Value: {value:F2}");
Thread.Sleep(100);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
The nanoFramework code polls for the rotary angle sensor for its position value every 100mSec.
The setup to use for the Analog to Digital Convertor(ADC) port was determined by looking at the board.h and target_windows_devices_adc_config.cpp file.
//
// Copyright (c) 2018 The nanoFramework project contributors
// See LICENSE file in the project root for full license information.
//
#include <win_dev_adc_native_target.h>
const NF_PAL_ADC_PORT_PIN_CHANNEL AdcPortPinConfig[] = {
// ADC1
{1, GPIOC, 0, ADC_CHANNEL_IN10},
{1, GPIOC, 1, ADC_CHANNEL_IN11},
// ADC2
{2, GPIOC, 2, ADC_CHANNEL_IN14},
{2, GPIOC, 3, ADC_CHANNEL_IN15},
// ADC3
{3, GPIOC, 4, ADC_CHANNEL_IN12},
{3, GPIOC, 5, ADC_CHANNEL_IN13},
// these are the internal sources, available only at ADC1
{1, NULL, 0, ADC_CHANNEL_SENSOR},
{1, NULL, 0, ADC_CHANNEL_VREFINT},
{1, NULL, 0, ADC_CHANNEL_VBAT},
};
const int AdcChannelCount = ARRAYSIZE(AdcPortPinConfig);
The call to AdcController.GetDeviceSelector() only returned one controller
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.Longboard.AdcTest starting
ADC1
After some experimentation it appears that only A0 & A1 work on a Netduino. (Aug 2020).
My PWM test harness
public class Program
{
public static void Main()
{
Debug.WriteLine("devMobile.Longboard.PwmTest starting");
Debug.WriteLine(PwmController.GetDeviceSelector());
try
{
PwmController pwm = PwmController.FromId("TIM5");
AdcController adc = AdcController.GetDefault();
AdcChannel adcChannel = adc.OpenChannel(0);
PwmPin pwmPin = pwm.OpenPin(PinNumber('A', 0));
pwmPin.Controller.SetDesiredFrequency(1000);
pwmPin.Start();
while (true)
{
double value = adcChannel.ReadRatio();
Debug.WriteLine(value.ToString("F2"));
pwmPin.SetActiveDutyCyclePercentage(value);
Thread.Sleep(100);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private static int PinNumber(char port, byte pin)
{
if (port < 'A' || port > 'J')
throw new ArgumentException();
return ((port - 'A') * 16) + pin;
}
}
I had to refer to the Netduino schematic to figure out pin mapping
With my test rig (with easy access to D0 thru D8) I found that only D2,D3,D7 and D8 work as PWM outputs.
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.
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.
After some experimentation with how to detect if an I2C read or write had failed the debugging console output began displaying reasonable value
//---------------------------------------------------------------------------------
// 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.
For the final revision my nanoFrameworkSX127X 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
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 nanoFrameworkUTF8Encoding.UTF8.GetString was throwing exceptions.
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.
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…
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;
}
}