Skip to content

Instantly share code, notes, and snippets.

@rlcamp
Last active June 1, 2022 16:59
Show Gist options
  • Save rlcamp/fe94a5b9c1a2e3109159e5bb75495b8b to your computer and use it in GitHub Desktop.
Save rlcamp/fe94a5b9c1a2e3109159e5bb75495b8b to your computer and use it in GitHub Desktop.
Minimalist nonblocking-ish RFM9x LORA transceiver driver

LoRa Minimalist Transparent Mode

This generic Arduino sketch implements something akin to XBee transparent mode, between two RFM9x LoRa modules. All of the necessary Arduino-specific functionality is encapsulated in one hardware abstraction layer file, such that porting to a non-Arduino environment should only require editing this file. Similarly, all of the RFM9x-specific code is encapsulated within a single file.

Modules

lora_minimalist.c/.h

Minimalist, entirely non-blocking interface to the RFM9x module for sending/receiving generic messages.

lora_miminalist_stream_transceiver.c/.h

Implements the necessary two-way handshaking and buffering to allow a half-duplex packetized link to appear to be a full-duplex streamlike link, suitable for serial cable replacement applications.

lora_minimalist_hal.h / lora_minimalist_hal_arduino.cpp

This encapsulates the Arduino-specific calls for reading/writing from serial and SPI, toggling pins, &c. needed by the other modules. A side benefit is that this encapsulates all of the Arduino C++ code, allowing the rest of the sketch to be C.

lora_minimalist_transparent_mode.c/.ino

An Arduino sketch which calls the lora_minimalist_stream_receiver module on the RF side, and uses the Arduino Serial output (which is likely a usb cable to a host machine) such that bytes received via serial are forwarded via RF, and vice versa.

#include "lora_minimalist.h"
#include "lora_minimalist_hal.h"
unsigned char lora_minimalist_recv_buffer[242];
volatile size_t lora_minimalist_recv_buffer_filled;
static struct spi_settings * spi_settings;
static volatile enum { STANDBY, TRANSMITTING, RECEIVING } mode, mode_after_tx;
static void spi_write_register(uint8_t reg, uint8_t val) {
spi_begin_transaction(spi_settings);
spi_transfer((uint8_t[2]) { reg | 0x80, val }, 2);
spi_end_transaction(spi_settings);
}
static uint8_t spi_read_register(uint8_t reg) {
spi_begin_transaction(spi_settings);
uint8_t buf[2] = { reg & ~0x80, 0 };
spi_transfer(buf, 2);
spi_end_transaction(spi_settings);
return buf[1];
}
static void spi_write_many(uint8_t reg, const uint8_t * src, size_t len) {
spi_begin_transaction(spi_settings);
spi_transfer(&(uint8_t) { reg | 0x80 }, 1);
for (size_t ibyte = 0; ibyte < len; ibyte++)
spi_transfer(&(uint8_t) { src[ibyte] }, 1);
spi_end_transaction(spi_settings);
}
static void spi_read_many(uint8_t reg, void * dest, size_t len) {
spi_begin_transaction(spi_settings);
spi_transfer(&(uint8_t) { reg & ~0x80 }, 1);
spi_transfer(dest, len);
spi_end_transaction(spi_settings);
}
static void set_tx_power(unsigned char high) {
/* if high, set +20 dBm on PA_BOOST pin */
spi_write_register(0x4D, high ? 0x07 : 0x04);
spi_write_register(0x09, 0x80 | (high ? 0x0f : 0));
}
static void set_standby(void) {
if (STANDBY == mode) return;
spi_write_register(0x01, 0x01);
mode = STANDBY;
}
void lora_minimalist_set_mode_rx(void) {
if (RECEIVING == mode) return;
/* request rx continuous mode */
spi_write_register(0x01, 0x05);
/* configure register 0x40 (RegDioMapping1), to generate an interrupt on dio0 on rxdone */
spi_write_register(0x40, 0x00);
mode = RECEIVING;
}
static volatile unsigned int wake = 0;
static void handle_interrupt(void) {
/* this is called whenever the given interrupt pin is driven high by dio0 */
wake++;
}
void lora_minimalist_react_to_interrupts(void) {
if (!wake) return;
atomic_decrement_unsigned_int(&wake);
/* which interrupt(s) fired */
const uint8_t irq_flags = spi_read_register(0x12);
if (RECEIVING == mode) {
/* read the reghopchannel register */
const uint8_t crc_present = spi_read_register(0x1C);
if ((irq_flags & (0x80 | 0x20)) | !(crc_present & 0x40)) {
/* crc error */
}
else if (irq_flags & 0x40) {
/* good crc */
const uint8_t size_filled = spi_read_register(0x13);
/* reset fifo */
spi_write_register(0x0D, spi_read_register(0x10));
/* read fifo */
spi_read_many(0x00, (void *)&lora_minimalist_recv_buffer, size_filled);
/* todo verify message is actually for us */
lora_minimalist_recv_buffer_filled = size_filled;
set_standby();
}
}
else if (TRANSMITTING == mode && irq_flags & 0x08) {
/* successfully finished transmitting */
if (RECEIVING == mode_after_tx) {
lora_minimalist_set_mode_rx();
mode_after_tx = STANDBY;
}
else if (STANDBY == mode_after_tx)
set_standby();
}
/* clear interrupts, gotta do this twice for reasons */
spi_write_register(0x12, 0xff);
spi_write_register(0x12, 0xff);
}
unsigned char lora_minimalist_send(const void * data, size_t len, unsigned char rx_after) {
/* verify len is not greater than maximum */
/* make sure we don't interrupt a prior outgoing message */
if (TRANSMITTING == mode) return 0;
set_standby();
/* possible todo: check for channel activity */
/* position fifo address pointer at beginning of fifo */
spi_write_register(0x0D, 0);
/* actual payload */
spi_write_many(0x00, data, len);
spi_write_register(0x22, len);
/* start transmitting and request an interrupt when finished */
mode = TRANSMITTING;
mode_after_tx = rx_after ? RECEIVING : STANDBY;
spi_write_register(0x01, 0x03);
/* configure register 0x40 (RegDioMapping1), to generate an interrupt on dio0 on txdone */
spi_write_register(0x40, 0x40);
return 1;
}
char lora_minimalist_init(const unsigned long millis_now, unsigned char pin_cs, unsigned char pin_reset, unsigned char pin_interrupt) {
static char state = 0;
static unsigned long millis_ref = 0;
if (0 == state) {
spi_settings = spi_init(1000000, 1, 0, pin_cs);
pin_init_for_output(pin_reset);
pin_set(pin_reset, 1);
millis_ref = millis_now;
state = 1;
}
if (1 == state) {
if (millis_now - millis_ref < 100) return 0;
pin_set(pin_reset, 0);
millis_ref = millis_now;
state++;
}
if (2 == state) {
if (millis_now - millis_ref < 10) return 0;
pin_set(pin_reset, 1);
millis_ref = millis_now;
state++;
}
if (3 == state) {
if (millis_now - millis_ref < 10) return 0;
/* enable lora mode, high frequency, stay in sleep */
spi_write_register(0x01, 0x80);
millis_ref = millis_now;
state++;
}
if (4 == state) {
if (millis_now - millis_ref < 10) return 0;
if (spi_read_register(0x01) != (0x00 | 0x80)) panic();
/* use entire fifo for either tx or rx */
spi_write_register(0x0E, 0);
spi_write_register(0x0F, 0);
/* set standby mode */
spi_write_register(0x01, 0x01);
/* set bandwidth to 125 kHz, cr 4/5 */
spi_write_register(0x1D, 0x72);
/* set spreading factor to 7 (128x), rx crc on */
spi_write_register(0x1E, 0x74);
/* enable auto agc */
spi_write_register(0x26, 0x04);
/* set preample length to 8 */
const uint16_t preamble_length = 8;
spi_write_register(0x20, preamble_length >> 8);
spi_write_register(0x21, preamble_length & 0xFF);
/* set centre frequency to 915 MHz, in steps of 61.035216 Hz */
const uint32_t centre_freq_in_steps = 14991360;
spi_write_register(0x06, (centre_freq_in_steps >> 16) & 0xFF);
spi_write_register(0x07, (centre_freq_in_steps >> 8) & 0xFF);
spi_write_register(0x08, centre_freq_in_steps & 0xFF);
/* set power to low */
set_tx_power(0);
pin_attach_rising_interrupt(pin_interrupt, handle_interrupt);
millis_ref = millis_now;
state++;
}
if (5 == state) {
if (millis_now - millis_ref < 10) return 0;
state++;
}
return 1;
}
unsigned long milliseconds_on_air_given_payload_length(const size_t payload_bytes) {
return (2592 + ( (8UL * payload_bytes + 99) / 28) * 640) / 128;
}
/* minimalist interface to rfm9x lora modules, delay-free and coroutine-friendly */
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* to initialize, call this until it returns 1 */
char lora_minimalist_init(const unsigned long millis_now, unsigned char pin_cs, unsigned char pin_reset, unsigned char pin_interrupt);
/* to send a packet, call this until it returns 1 */
unsigned char lora_minimalist_send(const void * data, size_t len, unsigned char rx_after);
/* call this in your main loop. the actual interrupt handler only increments a volatile that this routine consumes */
void lora_minimalist_react_to_interrupts(void);
/* check for this to be greater than zero to see if a packet has arrived */
extern volatile size_t lora_minimalist_recv_buffer_filled;
/* and consume it from here, and then zero the above value */
extern unsigned char lora_minimalist_recv_buffer[242];
void lora_minimalist_set_mode_rx(void);
unsigned long milliseconds_on_air_given_payload_length(const size_t payload_bytes);
#ifdef __cplusplus
}
#endif
/* dummy file to make arduino ide happy, see .c file with same filename for actual code */
/* wrappers for system-specific functionality */
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
void pin_set(unsigned char pin, unsigned char value);
void pin_init_for_output(unsigned char pin);
void pin_attach_rising_interrupt(unsigned char pin, void (*)(void));
struct spi_settings * spi_init(unsigned long spi_bus_speed, unsigned char msb_first, unsigned char spi_mode, unsigned char cs_pin);
void spi_begin_transaction(struct spi_settings *);
void spi_end_transaction(struct spi_settings *);
void spi_transfer(void * buf, const size_t size);
void atomic_decrement_unsigned_int(volatile unsigned int * p);
void panic(void);
void serial_begin(void);
void serial_end(void);
int serial_still_sending(void);
size_t serial_write(const void * buf, const size_t size);
size_t serial_available(void);
int serial_read_one_byte(void);
#ifdef __cplusplus
}
#endif
/* implementation of the hardware-specific routines (spi bus and pin toggling), assuming a generic
arduino. this implementation uses blocking spi transfers. a more board-specific implementation
could probably be done using nonblocking transfers to the benefit of the calling code without it
having to be written differently.
a side benefit is that, when used in the arduino ecosystem, this encapsulates all of the necessary
C++ code, allowing the rest of the sketch to be pure C */
#include "lora_minimalist_hal.h"
#include <Arduino.h>
#include <SPI.h>
void pin_init_for_output(unsigned char pin) {
pinMode(pin, OUTPUT);
}
void pin_set(unsigned char pin, unsigned char value) {
digitalWrite(pin, value ? HIGH : LOW);
}
static struct spi_settings {
SPISettings settings;
uint8_t cs_pin;
} settings_s;
struct spi_settings * spi_init(unsigned long spi_bus_speed, unsigned char msb_first, unsigned char spi_mode, unsigned char cs_pin) {
struct spi_settings * settings = &settings_s;
settings->settings = SPISettings(spi_bus_speed,
msb_first ? MSBFIRST : LSBFIRST,
0 == spi_mode ? SPI_MODE0 :
1 == spi_mode ? SPI_MODE1 :
2 == spi_mode ? SPI_MODE2 :
SPI_MODE3);
settings->cs_pin = cs_pin;
SPI.begin();
pinMode(cs_pin, OUTPUT);
return settings;
}
void spi_begin_transaction(struct spi_settings * settings) {
SPI.beginTransaction(settings->settings);
pin_set(settings->cs_pin, 0);
}
void spi_end_transaction(struct spi_settings * settings) {
pin_set(settings->cs_pin, 1);
SPI.endTransaction();
}
void spi_transfer(void * buf, const size_t size) {
SPI.transfer(buf, size);
}
void pin_attach_rising_interrupt(unsigned char pin, void (* interrupt)(void)) {
attachInterrupt(digitalPinToInterrupt(pin), interrupt, RISING);
}
void panic(void) {
SPI.end();
pinMode(LED_BUILTIN, OUTPUT);
while (1) {
digitalWrite(LED_BUILTIN, HIGH);
delay(50);
digitalWrite(LED_BUILTIN, LOW);
delay(50);
}
}
#ifdef __AVR
#include <avr/interrupt.h>
#define __disable_irq cli
#define __enable_irq sei
#elif defined(__IMXRT1062__)
#include <imxrt.h>
#else
#include <cmsis_gcc.h>
#endif
void atomic_decrement_unsigned_int(volatile unsigned int * p) {
__disable_irq();
(*p)--;
__enable_irq();
}
static int serial_available_for_write_max = 0;
void serial_begin(void) {
Serial.begin(115200); /* note that the baud rate is vestigial on usb cdc devices */
/* save this before we have enqueued anything, so that we can check against it later */
serial_available_for_write_max = Serial.availableForWrite();
}
int serial_still_sending(void) {
/* teensyduino punts on Serial.flush(), otherwise we could just do that and return 0 */
return Serial.availableForWrite() != serial_available_for_write_max;
}
size_t serial_write(const void * ptr, size_t size) {
return Serial.write((const unsigned char *)ptr, size);
}
void serial_end(void) {
Serial.end();
}
size_t serial_available(void) {
return Serial.available();
}
int serial_read_one_byte(void) {
return Serial.read();
}
#include "lora_minimalist_stream_transceiver.h"
#include "lora_minimalist.h"
#include <string.h>
#include <stdlib.h>
/* calling code checks that payload_to_send_size is zero, sets payload_to_send to the beginning of
a buffer, and sets payload_to_send_size */
char * payload_to_send;
size_t payload_to_send_size = 0;
/* calling code consumes received messages by reading payload_received_size bytes from
payload_received whenever the former is nonzero, and then sets it to zero */
const char * payload_received;
size_t payload_received_size = 0;
extern void led_on(void);
extern void led_off(void);
void lora_minimalist_stream_transceiver(const unsigned long millis_now, unsigned char pin_cs,
unsigned char pin_reset, unsigned char pin_interrupt) {
/* this is called from loop() with the current value of millis(), and always returns
immediately, and expects that everything else in loop() behaves similarly */
static char finished_with_nonblocking_init = 0;
if (!finished_with_nonblocking_init) {
/* repeatedly call this until it returns success (takes a while due to timing sequences
within it) */
if (!lora_minimalist_init(millis_now, pin_cs, pin_reset, pin_interrupt)) return;
lora_minimalist_set_mode_rx();
finished_with_nonblocking_init = 1;
}
static unsigned long millis_ack_timeout_length = 0;
static unsigned long millis_ack_timeout = 0;
static uint8_t sequence_number_last_acknowledged_by_other_end = 255;
static struct msg {
uint8_t seq_ack, seq;
char payload[sizeof(lora_minimalist_recv_buffer) - 2];
} msg_outgoing = { .seq_ack = 255, .seq = 255 };
static size_t outgoing_payload_size = 0;
static char ready_to_send = 0;
static unsigned long duty_cycle_numerator = 0, duty_cycle_denominator = 32768;
static unsigned long duty_cycle_last_increment = 0;
/* very crude moving average behaviour for the duty cycle calculation, inexpensive to compute on
all microcontrollers of interest, and gets the job done */
while (duty_cycle_denominator >= 65536) {
duty_cycle_denominator /= 2;
duty_cycle_numerator /= 2;
}
if (ready_to_send) {
const unsigned long millis_elapsed_since_last_increment = millis_now - duty_cycle_last_increment;
/* if we have been on air on average more than 1% of the time, wait until we haven't */
if (duty_cycle_denominator + millis_elapsed_since_last_increment < duty_cycle_numerator * 100) {
led_on();
return;
}
const size_t bytes_to_send = outgoing_payload_size ? offsetof(struct msg, payload) + outgoing_payload_size : offsetof(struct msg, seq);
if (!lora_minimalist_send(&msg_outgoing, bytes_to_send, 1)) return;
duty_cycle_numerator += milliseconds_on_air_given_payload_length(bytes_to_send);
duty_cycle_denominator += millis_elapsed_since_last_increment;
duty_cycle_last_increment = millis_now;
led_off();
// extern unsigned char blinks_remaining;
// blinks_remaining++;
millis_ack_timeout = millis_now;
ready_to_send = 0;
}
/* if we are ready to send a new message, and the calling code has provided one... */
if (payload_to_send_size && !outgoing_payload_size) {
msg_outgoing.seq++;
memcpy(msg_outgoing.payload, payload_to_send, payload_to_send_size);
outgoing_payload_size = payload_to_send_size;
/* reset the ack timeout length */
millis_ack_timeout_length = 1536;
payload_to_send_size = 0;
ready_to_send = 1;
return;
}
if (lora_minimalist_recv_buffer_filled) {
struct msg * msg_received = (void *)&lora_minimalist_recv_buffer;
if (lora_minimalist_recv_buffer_filled > offsetof(struct msg, seq)) {
/* got a packet with a nontrivial payload from other end */
if (msg_received->seq != msg_outgoing.seq_ack) {
payload_received_size = lora_minimalist_recv_buffer_filled - offsetof(struct msg, payload);
/* expose the actual buffer within the lora minimalist implementation to the calling
code, to save on sram. todo: evaluate consequences of this */
payload_received = msg_received->payload;
/* immediately reply */
ready_to_send = 1;
}
msg_outgoing.seq_ack = msg_received->seq;
}
/* is the other end acknowledging the last thing we sent? */
if (msg_received->seq_ack == msg_outgoing.seq && sequence_number_last_acknowledged_by_other_end != msg_outgoing.seq) {
sequence_number_last_acknowledged_by_other_end = msg_received->seq_ack;
/* zero the outgoing payload size so that we can send short acks */
outgoing_payload_size = 0;
}
/* "consume" the buffer from the lora receiver's point of view */
lora_minimalist_recv_buffer_filled = 0;
/* reset receiver to get the next packet, unless we are about to send one */
lora_minimalist_set_mode_rx();
return;
}
/* timed out waiting for other end to acknowledge the last thing we sent */
if (sequence_number_last_acknowledged_by_other_end != msg_outgoing.seq && millis_now - millis_ack_timeout >= millis_ack_timeout_length) {
/* re-send the payload */
ready_to_send = 1;
/* next timeout will be longer by a random amount */
millis_ack_timeout += millis_ack_timeout_length;
millis_ack_timeout_length += rand() % 512;
return;
}
}
#include <stddef.h>
/* calling code checks that payload_to_send_size is zero, sets payload_to_send to the beginning of
a buffer, and sets payload_to_send_size */
extern char * payload_to_send;
extern size_t payload_to_send_size;
/* calling code consumes received messages by reading payload_received_size bytes from
payload_received whenever the former is nonzero, and then sets it to zero */
extern const char * payload_received;
extern size_t payload_received_size;
/* this is called from loop() with the current value of millis(), and always returns
immediately, and expects that everything else in loop() behaves similarly */
void lora_minimalist_stream_transceiver(const unsigned long millis_now, unsigned char pin_cs, unsigned char pin_reset, unsigned char pin_interrupt);
#include <Arduino.h>
#include "lora_minimalist.h"
#include "lora_minimalist_stream_transceiver.h"
#include "lora_minimalist_hal.h"
#ifdef __AVR
/* pins on arduino uno */
#define PIN_CS 4
#define PIN_RESET 2
#define PIN_INTERRUPT 3
#define PIN_ALT_LED 5
#else
#if 1
/* pins on feather m0 with integrated rfm95 */
#define PIN_CS 8
#define PIN_RESET 4
#define PIN_INTERRUPT 3
#else
/* pins on feather m0/m4 */
#define PIN_CS 10
#define PIN_RESET 11
#define PIN_INTERRUPT 6
#endif
#define PIN_ALT_LED LED_BUILTIN
#endif
#ifdef __IMXRT1062__
/* teensy doesn't include definitions of __WFI() and some other stuff automatically */
#include <arm_math.h>
#elif defined(__AVR)
/* quick hack, doesn't actually sleep, code should behave the same, just use more power */
#define __WFI()
#endif
void setup() {
/* kick the serial line so we don't fail to read from it before finishing radio init */
serial_begin();
/* lol */
srand(F_CPU >> 16);
pinMode(PIN_ALT_LED, OUTPUT);
}
void led_on(void) {
digitalWrite(PIN_ALT_LED, HIGH);
}
void led_off(void) {
digitalWrite(PIN_ALT_LED, LOW);
}
/* this can be incremented from anywhere else, doesn't need to be atomic as long as it's not done inside an interrupt handler */
unsigned char blinks_remaining = 0;
static void blink_if_you_gotta_blink(const unsigned long millis_now) {
/* this is called unconditionally from loop() and passed the current value of millis() */
static unsigned long millis_prev = 0;
if (!blinks_remaining) {
millis_prev = millis_now;
return;
}
static char led_is_on = 0;
if (led_is_on) {
/* nonblocking wait a small number of milliseconds before turning led off */
if (millis_now - millis_prev < 20) return;
millis_prev += 20;
digitalWrite(PIN_ALT_LED, LOW);
led_is_on = 0;
blinks_remaining--;
} else {
/* nonblocking wait a larger number of milliseconds before turning led back on */
if (millis_now - millis_prev < 160) return;
millis_prev += 160;
digitalWrite(PIN_ALT_LED, HIGH);
led_is_on = 1;
}
}
void loop() {
/* sleep until any interrupt - usually the every-millisecond interrupt during which the value
returned by millis() is incremented */
__WFI();
const unsigned long millis_now = millis();
/* evaluate the various state-machine-like things */
lora_minimalist_react_to_interrupts();
blink_if_you_gotta_blink(millis_now);
lora_minimalist_stream_transceiver(millis_now, PIN_CS, PIN_RESET, PIN_INTERRUPT);
/* if the transceiver filled this buffer... */
if (payload_received_size) {
/* drain it to serial */
while (serial_still_sending()) yield();
serial_write(payload_received, payload_received_size);
payload_received_size = 0;
}
/* if the transceiver is ready for something to send... */
if (!payload_to_send_size) {
static char payload_staging[sizeof(lora_minimalist_recv_buffer) - 2];
static size_t bytes_buffered = 0;
static unsigned long millis_last_byte_read = 0;
/* get bytes from serial */
if (bytes_buffered < sizeof(payload_staging) && serial_available() > 0) {
payload_staging[bytes_buffered++] = serial_read_one_byte();
millis_last_byte_read = millis_now;
}
/* decide when to flush the buffer to the transceiver */
if (bytes_buffered >= sizeof(payload_staging) ||
(bytes_buffered > 0 && millis_now - millis_last_byte_read > 125) ||
(bytes_buffered > 0 && (payload_staging[bytes_buffered - 1] == '\n' || payload_staging[bytes_buffered - 1] == '\r') && !serial_available())) {
payload_to_send = payload_staging;
payload_to_send_size = bytes_buffered;
bytes_buffered = 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment