In my spare time I sometimes work with Arduino micro-controllers on little side projects. This time I made the mistake of wandering into a project using LoRa long-range wireless RF technology.

The problem

The goal was to build a sort of transmitter and receiver station for a race car for the 24 hours of lemons endurance race for $500 cars.

sleep sweet prince

In the past we’ve tried a simple walkie-talkie based solution to tell our crew in the pit or paddock when we need something. The problem is our radios are simply not powerful enough, and there is too much RF interference to hear what the driver is actually saying. What we get is usually something like “Glzzk…SSSSSSSSHHHHHHmmmmmmmmmSHHHHHHHHhhhhmmmm… Glzzk…” which is a lot less clear then “I am coming in for fuel”, or “I need to change drivers” or “I am on fire”, which are all things we’d like to know about in advance of them arriving.

The solution

The solution I came up with was to build a simple, long range signal station connected to a LoRa radio transmitter in the car.

Modem Plan

The transmitter would send back a very simple digital message that I would parse, and then based on its contents activate a light bulb on a pole to notify the team that something was happening.

Modem Receiver

Green means “I need gas”, blue means “I need to switch drivers”, red means “something very bad has happened”. Based on the light color we could get ready for whatever was coming our way.

The mysterious modem

Everything was going well until I got a little bit ahead of myself and wanted to try tinkering with the LoRa module’s modem configuration directly. You see the modem supports a range of different transmission bandwidth, spreading factor, and error correction options that all affect the signal speed and range.

The library I was using contained several of these configurations in a set of presets that I could drop in easily. It’s written in the dreaded C++ which is bit more cumbersome than a dynamic language, but its powerful memory management aspects make it ideal for running on ultra low-powered micro-controller devices.

 typedef enum
 {	Bw125Cr45Sf128 = 0, ///< Bw = 125 kHz,   Cr = 4/5,  Sf = 128chips/symbol,   CRC on. Default medium range
	Bw500Cr45Sf128,     ///< Bw = 500 kHz,   Cr = 4/5,  Sf = 128chips/symbol,   CRC on. Fast+short range
	Bw31_25Cr48Sf512,   ///< Bw = 31.25 kHz, Cr = 4/8,  Sf = 512chips/symbol,   CRC on. Slow+long range
	Bw125Cr48Sf4096,    ///< Bw = 125 kHz,   Cr = 4/8,  Sf = 4096chips/symbol,  CRC on. Slow+long range
 } ModemConfigChoice;

I wanted to modify the bandwidth and error coding rate values to maximize speed and range at the cost of bandwidth and reliability (think quantity over quality). Something like Bw = 250 kHz, Cr = 4/5, Sf = 4096chips/symbol, CRC on, which I thought would be a good compromise given my goal.

Exploring the code

To set these configuration values I use a helper method setModemConfig that matches a preset configuration and sends it to setModemRegisters which does the real work:

rf95.setModemConfig(RH_RF95::Bw31_25Cr48Sf512);

A quick search for one of the preset configurations revealed their source:

PROGMEM static const RH_RF95::ModemConfig MODEM_CONFIG_TABLE[] = 
{
    //  1d,     1e,      26
    { 0x72,   0x74,    0x04}, // Bw125Cr45Sf128 (the chip default), AGC enabled
    { 0x92,   0x74,    0x04}, // Bw500Cr45Sf128, AGC enabled
    { 0x48,   0x94,    0x04}, // Bw31_25Cr48Sf512, AGC enabled
    { 0x78,   0xc4,    0x0c}, // Bw125Cr48Sf4096, AGC enabled
    
};

What the heck are these? Without understanding how these were formatted there was no way I could write my own modem configuration. This is the mystery that would perplex me for days after discovering these weird 0x## values.

Did you read the manual?

Because I’ve dealt with Arduino libraries before, I recognized these strange things as hexadecimal notations, but I wasn’t quite sure what they would resolve to as values in my program or the modem hardware itself.

To get a clue I had to dig into the LoRa radio module’s manual and look up what values these map to inside the module’s registers.

Modem Register 0

Pulling up the register table there are dozens of registers for many different inputs, outputs and configuration options for the module. Finding out which of these values I was looking for to control the modem wasn’t very intuitive, but doing a little searching in the library revealed the answer:

#define RH_RF95_REG_1D_MODEM_CONFIG1	0x1d
#define RH_RF95_REG_1E_MODEM_CONFIG2	0x1e
#define RH_RF95_REG_26_MODEM_CONFIG3	0x26

// ...

// Sets registers from a canned modem configuration structure
void RH_RF95::setModemRegisters(const ModemConfig* config)
{
    spiWrite(RH_RF95_REG_1D_MODEM_CONFIG1,       config->reg_1d);
    spiWrite(RH_RF95_REG_1E_MODEM_CONFIG2,       config->reg_1e);
    spiWrite(RH_RF95_REG_26_MODEM_CONFIG3,       config->reg_26);
}

These are the 3 values that are written over SPI to the modem when I change its configuration settings. If I look at the register table for these values it confirms what each of them expects from my code.

Modem Registers

According to this table each of these registers takes an 8 bit (or 1 byte) value that configures a few different things based on its position in the byte. With this information I had my first big lead about how to read these values and solve the riddle of the hexadecimal modem configurations.

The binary in the coal mine

So after discovering that the register is expecting a byte, next I decided to convert all the hexadecimal values into binary.

// Into 8 bit binary
{
    // 1d,        1e,          26
    { 01110010,   01110100,    00000100},	// 0x72,   0x74,    0x04
    { 10010010,   01110100,    00000100},	// 0x92,   0x74,    0x04
    { 01001000,   10010100,    00000100}, 	// 0x48,   0x94,    0x04
    { 01111000,   11000100,    00001100}  	// 0x78,   0xc4,    0x0c
};

Great, now we have more numbers! If we compare these values with the register table in the manual we can see that each bit in the byte corresponds with a particular function. For example in the 0x1D register labeled RegModemConfig 1 we can see that bits 7-4 control bandwidth, bits 3-1 set the coding rate, and bit 0 turns on implicit header mode.

So which of these bits is position 0 and which is 7? Well I’m a natural left-to-right reader and mediocre programmer so let’s assume the first bit is on the left edge and split up the values into three ranges around bits 0, 1-3 & 4-7.

So I tried resolving the values for RegModemConfig1 parsing them from left-to-right:

{    
    // ImplicitHeaderModeOn CodingRate Bw
    { 0 111 0010 },     // false N/A 15.6kHz
    { 1 001 0010 },     // true  4/5 15.6kHz
    { 0 100 1000 },     // false 4/8 250kHz
    { 0 111 1000 }      // false N/A 250kHz
};

WEEPS OPENLY

If we compare these values with the RegModemConfig 1 register table we can see that this is definitely not correct. It’s resolving invalid values for coding rate which I don’t even want to imagine what that would do to the radio’s hardware. So left-to-right was definitely wrong, so let’s try right-to-left instead grouping the bits into ranges of 7-4, 3-1 & 0:

{    
    // Bw CodingRate ImplicitHeaderModeOn
    { 0111 001 0 },     // 125kHz   4/5 false
    { 1001 001 0 },     // 500kHz   4/5 false
    { 0100 100 0 },     // 31.25kHz 4/8 false
    { 0111 100 0 }      // 125kHz   4/8 false
};

WEEPS FOR JOY

This is exactly what I was looking for, this perfectly matches the values in the comments for the modem configuration presets.

Dawn of a new day

We now have confirmation what these values are and how they are parsed by the hardware. Let’s use this same approach for the RegModemConfig 2 & RegModemConfig 3 values:

// RegModemConfig 2
{
    // SpreadingFactor TxContinuousMode RxPayloadCrcOn SymbTimeout
    { 0111 0 1 00}, // 7  false true 0
    { 0111 0 1 00}, // 7  false true 0
    { 1001 0 1 00}, // 9  false true 0
    { 1100 0 1 00}  // 12 false true 0
};

// RegModemConfig 3
{
    // Unused MobileNode AcgAutoOn Reserved
    { 0000 0 1 00}, // N/A false true 0
    { 0000 0 1 00}, // N/A false true 0
    { 0000 0 1 00}, // N/A false true 0
    { 0000 1 1 00}  // N/A true  true 0
};

Now that we can read all the settings in the preset configurations, it’s possible to create a new custom modem configurations for our LoRa radio by performing a binary-to-hex conversion with the options we want:

 typedef enum
 {	
    TXBw250Cr45Sf12,  //< Bw = 250 kHz,   Cr = 4/5,  Sf = 4096chips/symbol,  CRC on. mobile long range
    RXBw250Cr45Sf12,  //< Bw = 250 kHz,   Cr = 4/5,  Sf = 4096chips/symbol,  CRC on.  stationary long range
 } ModemConfigChoice;


PROGMEM static const RH_RF95::ModemConfig MODEM_CONFIG_TABLE[] = 
{
    //  1d,     1e,      26
    { 0x82,   0xc4,    0x0c}, // TXBw250Cr45Sf12, AGC enabled
    { 0x78,   0xc4,    0x04}, // RXBw250Cr45Sf12, AGC enabled
};

In fact, I made 2 custom presets! One for the mobile transmitter, and another tailored for stationary receiver. I don’t know how well these custom settings work yet, but with any luck everything will work great when I test it out at the upcoming Where the Elite Meet the Cheat 24 hours of Lemons race at Gingerman raceway, in South Haven, Michigan.

Wish us luck!