Lightweight SX1276 radio library for Arduino supporting LoRa, FSK, and OOK modulations, optimized for memory-constrained devices.
This is a simplified port of RadioLib's SX1276 implementation, specifically optimized for the Adafruit Feather 32u4 with RFM95 LoRa Radio, while maintaining compatibility with other Arduino architectures. This lightweight variant focuses on memory efficiency for AVR devices.
📖 Migrating from RadioLib? See the RadioLib Compatibility Guide for detailed migration instructions.
- Memory Efficient: Optimized for AVR ATmega32u4 with limited RAM
- No Dynamic Allocation: All memory is statically allocated
- Flat Class Hierarchy: No inheritance for reduced overhead
- Multiple Modulations: Support for LoRa, FSK, and OOK
- Optional Modes: Enable LoRa with
#define LORA_ENABLEDand/or FSK/OOK with#define FSK_OOK_ENABLED - RadioLib-Compatible API: Provides RadioLib-compatible methods for easy migration
- Multi-Architecture: Compatible with AVR, ESP32, ESP8266, and RP2040
- Simple API: Easy-to-use interface for all modulation types
- Debug Support: Optional debug output with
#define SX1276_DEBUG
- Download this repository as a ZIP file
- In Arduino IDE: Sketch → Include Library → Add .ZIP Library
- Select the downloaded ZIP file
Add to your platformio.ini:
lib_deps =
https://github.com/matthias-bs/SX1276.gitNote: LoRa and FSK/OOK modulations are enabled by default. To disable them for minimal memory usage, edit src/SX1276.h and comment out #define LORA_ENABLED and/or #define FSK_OOK_ENABLED.
This library provides two API styles to suit different needs:
Direct and memory-efficient - pins specified in begin():
#include <SX1276.h>
SX1276 radio;
void setup() {
Serial.begin(115200);
// Initialize: freq in Hz, then pins
int16_t state = radio.begin(868000000L, 8, 4, 7); // freq, cs, rst, dio0
if (state == SX1276_ERR_NONE) {
Serial.println("Radio initialized!");
}
// Configure parameters
radio.setSpreadingFactor(SX1276_SF_7);
radio.setBandwidth(SX1276_BW_125_KHZ);
}
void loop() {
const char* message = "Hello LoRa!";
radio.transmit((uint8_t*)message, strlen(message));
delay(5000);
}Familiar to RadioLib users - pins in constructor, frequency in MHz:
#include <SX1276.h>
SX1276 radio(8, 7, 4); // cs, irq, rst (like RadioLib's Module)
void setup() {
Serial.begin(115200);
// RadioLib-style begin with MHz and optional parameters
int16_t state = radio.begin(868.0); // Frequency in MHz
// Or with full parameters:
// state = radio.begin(868.0, 125.0, 9, 7, 0x12, 10, 8, 0);
if (state == SX1276_ERR_NONE) {
Serial.println("Radio initialized!");
}
}
void loop() {
const char* message = "Hello LoRa!";
radio.transmit((uint8_t*)message, strlen(message));
delay(5000);
}See the BasicExample for simplified API or RadioLibCompatible for RadioLib-compatible API.
#include <SX1276.h>
// Pin definitions for Adafruit Feather 32u4 RFM95
#define LORA_CS 8
#define LORA_RST 4
#define LORA_DIO0 7
SX1276 radio;
void setup() {
Serial.begin(115200);
// Initialize radio at 868 MHz
int16_t state = radio.begin(868000000L, LORA_CS, LORA_RST, LORA_DIO0);
if (state == SX1276_ERR_NONE) {
Serial.println("Radio initialized!");
} else {
Serial.println("Radio initialization failed!");
}
// Optional: Configure LoRa parameters
radio.setSpreadingFactor(SX1276_SF_7);
radio.setBandwidth(SX1276_BW_125_KHZ);
radio.setCodingRate(SX1276_CR_4_5);
radio.setPower(17, true); // 17 dBm with PA_BOOST
}
void loop() {
// Transmit a message
const char* message = "Hello LoRa!";
int16_t state = radio.transmit((uint8_t*)message, strlen(message));
if (state == SX1276_ERR_NONE) {
Serial.println("Transmission successful!");
}
delay(5000);
}See the BasicExample for a complete LoRa example.
#include <SX1276.h>
SX1276 radio;
void setup() {
Serial.begin(115200);
// Initialize radio at 868 MHz
radio.begin(868000000L, 8, 4, 7);
// Set modulation to FSK
radio.setModulation(SX1276_MODULATION_FSK);
// Configure FSK parameters
radio.setBitrate(4800); // 4.8 kbps
radio.setFrequencyDeviation(5000); // 5 kHz
radio.setRxBandwidth(SX1276_RX_BW_10_4_KHZ_FSK);
uint8_t syncWord[] = {0x2D, 0xD4};
radio.setSyncWord(syncWord, 2);
}
void loop() {
const char* message = "Hello FSK!";
radio.transmit((uint8_t*)message, strlen(message));
delay(5000);
}See the FSKExample for a complete FSK example.
#include <SX1276.h>
SX1276 radio;
void setup() {
Serial.begin(115200);
// Initialize radio at 868 MHz
radio.begin(868000000L, 8, 4, 7);
// Set modulation to OOK
radio.setModulation(SX1276_MODULATION_OOK);
// Configure OOK parameters
radio.setBitrate(4800); // 4.8 kbps
radio.setFrequencyDeviation(0); // 0 Hz (OOK)
radio.setRxBandwidth(SX1276_RX_BW_10_4_KHZ_FSK);
uint8_t syncWord[] = {0x69, 0x81};
radio.setSyncWord(syncWord, 2);
}
void loop() {
const char* message = "Hello OOK!";
radio.transmit((uint8_t*)message, strlen(message));
delay(5000);
}See the OOKExample for a complete OOK example.
Note: Typically OOK would be used with 433 MHz in the EU. The OOK example uses 868 MHz because suitable 433 MHz test hardware was not available.
int16_t begin(long freq, int cs, int rst, int dio0);Initialize the radio module.
freq: Frequency in Hz (e.g., 915000000 for 915 MHz)cs: Chip select pinrst: Reset pindio0: DIO0 interrupt pin- Returns:
SX1276_ERR_NONEon success, error code otherwise
int16_t setModulation(uint8_t modulation);Set modulation type.
modulation:SX1276_MODULATION_LORA,SX1276_MODULATION_FSK, orSX1276_MODULATION_OOK- Returns:
SX1276_ERR_NONEon success, error code otherwise
int16_t transmit(const uint8_t* data, size_t len);Transmit data packet (blocking).
- Returns:
SX1276_ERR_NONEon success, error code otherwise
int16_t receive(uint8_t* data, size_t maxLen, uint32_t timeout_ms = 10000);Receive data packet (blocking).
timeout_ms: Maximum wait time in milliseconds (default: 10 000)- Returns: Number of bytes received, or error code (< 0)
When LORA_ENABLED is defined:
int16_t setSpreadingFactor(uint8_t sf); // SF6-SF12
int16_t setBandwidth(uint8_t bw); // Use SX1276_BW_* constants
int16_t setCodingRate(uint8_t cr); // Use SX1276_CR_* constants
int16_t setPreambleLength(uint16_t len); // In symbols
int16_t setSyncWord(uint8_t sw); // 0x12 private, 0x34 LoRaWAN
int16_t setCRC(bool enable); // Enable/disable CRCWhen FSK_OOK_ENABLED is defined:
int16_t setBitrate(uint32_t bitrate); // 1200-300000 bps
int16_t setFrequencyDeviation(uint32_t freqDev); // 600-200000 Hz (0 for OOK)
int16_t setRxBandwidth(uint8_t rxBw); // Use SX1276_RX_BW_* constants
int16_t setSyncWord(const uint8_t* syncWord, uint8_t len); // 1-8 bytes
int16_t setPreambleLength(uint16_t len); // In bits
int16_t setPacketConfig(bool fixedLength, bool crcOn); // Packet formatLoRa mode:
int16_t getRSSI(); // Get RSSI in dBm
int8_t getSNR(); // Get SNR (divide by 4 for actual dB)
int32_t getFrequencyError(); // Get frequency error in HzFSK/OOK mode:
int16_t getRSSI_FSK(); // Get RSSI in dBmint16_t setPower(int8_t power, bool useBoost); // Set TX power
int16_t standby(); // Enter standby mode
int16_t sleep(); // Enter sleep modeThe library supports three modulation types:
- LoRa: Long Range, requires
LORA_ENABLEDdefine - FSK: Frequency Shift Keying, requires
FSK_OOK_ENABLEDdefine - OOK: On-Off Keying, requires
FSK_OOK_ENABLEDdefine
You can enable both LoRa and FSK/OOK in the same project and switch between them using setModulation().
| Signal | Pin |
|---|---|
| CS | 8 |
| RST | 4 |
| DIO0 | 7 |
| MOSI | 16 |
| MISO | 14 |
| SCK | 15 |
Configure pins according to your hardware setup. The library uses the default SPI pins.
This library is specifically designed for memory-constrained devices:
- No
mallocornew: All allocations are static - Minimal RAM usage: ~50-100 bytes of instance data (depending on enabled modes)
- No floating point: All calculations use integers (except one unused constant)
- Compile-time options: Enable only the modes you need
- Define
LORA_ENABLEDto enable LoRa modulation - Define
FSK_OOK_ENABLEDto enable FSK/OOK modulation - Define both to enable all modes with runtime switching
- Define
- Debug macros: Debug output compiled out when not needed
⚠️ Warning: Transmitting on ISM/SRD bands is subject to local radio regulations.
- The 868.0–868.6 MHz g1 sub-band (EU SRD) limits all modulation types (LoRa, FSK, OOK) to 1% duty cycle and 25 mW ERP (ETSI EN 300 220).
- The 433.05–434.79 MHz ISM band (EU) is typically used for OOK and allows up to 10% duty cycle and 10 mW ERP (but always check local regulations).
- Other sub-bands and regions have different limits.
When writing your own sketch, you must ensure the time-on-air of each transmission divided by the TX interval stays below the applicable duty cycle limit. The formula is:
Factors that affect airtime: spreading factor (LoRa), bit rate (FSK/OOK), preamble length, payload size, and coding rate.
All examples in this library enforce the 1 % limit via a TX_MIN_INTERVAL_MS guard.
See any example sketch (e.g. BasicExample) for
the pattern.
Bidirectional sketches (e.g. FSKExample, RadioLibCompatible) that both transmit and receive face a synchronization challenge: each node alternates between TX and RX windows, and the duty cycle enforcement delay increases the total cycle time. If both nodes happen to transmit at the same moment, a collision occurs; if they listen at the same moment, neither sends.
Mitigations used in the examples:
- RX timeout ≥ peer's worst-case TX cycle — ensures at least one peer transmission falls inside each listen window.
- Jittered timing —
randomSeed(micros())plus randomized delays decorrelate the two nodes so they gradually drift out of phase-lock. - Probabilistic beaconing — on RX timeout, transmit with < 100 % probability to break symmetric deadlocks.
Even so, the first few exchanges after power-on may experience timeouts until the nodes de-synchronize. This is expected behaviour and not a fault.
For real applications, prefer asymmetric roles over symmetric ping-pong:
| Approach | Description | Complexity |
|---|---|---|
| Initiator / Responder | One node (primary) always initiates; the other (secondary) stays in continuous RX and only transmits a short reply after being polled. No timing conflict — exactly one transmitter at a time. This is how LoRaWAN Class A works. | Low |
| Listen-Before-Talk (LBT) | Check RSSI before transmitting; back off if the channel is busy. Avoids collisions but does not break the "both listening" deadlock alone — still needs one side to initiate. | Medium |
| TDMA | Assign fixed time slots to each node. Requires synchronized clocks (GPS, NTP, or a coordinator beacon). Scales to many nodes but is overkill for two. | High |
| Slotted ALOHA + Backoff | Transmit at random times; on collision (no ACK), wait a random, exponentially increasing delay before retrying. Simple, no roles, but lower throughput. | Medium |
Recommendation: Use the Initiator / Responder pattern. The primary transmits, then opens a short RX window for the reply; the secondary stays in continuous RX. This eliminates synchronization problems entirely and is inherently duty-cycle-friendly because the responder only transmits briefly after being polled.
The existing TransmitExample + ReceiveExample pair already demonstrates one-way
Initiator / Responder communication. For bidirectional data exchange, extend the primary
to listen for a reply immediately after its transmission (similar to LoRaWAN Class A RX
windows).
Tested and compatible with:
- ✅ AVR ATmega32u4 (Adafruit Feather 32u4 RFM95)
- ✅ ESP32
- ✅ ESP8266 (limited testing)
- ✅ RP2040 (limited testing)
This library is licensed under the MIT License. See LICENSE for details.
Based on RadioLib by Jan Gromeš and RadioLib contributors.
Contributions are welcome! Please open an issue or pull request on GitHub.