Add LR1121 radio support for T-Beam Supreme

Add driver and board support for the Semtech LR1121 radio on the LilyGo
T-Beam Supreme V3.0 (BOARD_TBEAM_S_LR_V1 0x43).

New files:
- lr11xx.h/cpp: LR1121 driver extending Stream, matching the sx126x
  interface for duck-typed LoRa pointer compatibility. Implements 2-byte
  opcode SPI protocol with two-phase reads, inline MISO status capture,
  DIO9 interrupt handling, and carrier detection via preamble/header
  IRQ polling.

Key LR1121 differences from SX126x handled in the driver:
- SPI uses 2-byte opcodes with NSS release between command and response
  phases (vs single-phase on SX126x)
- SetPacketParams PayloadLen is enforced in explicit header mode (SX126x
  ignores it) — receive() sets PayloadLen=0 to accept any length
- ESP32S3 requires explicit SPI pin assignment via SPI.begin(sclk, miso,
  mosi, cs)
- RF switch configured via SetDioAsRfSwitch command (DIO5/DIO6) rather
  than external GPIO control
- TCXO at 3.0V via SetTcxoMode command
- High Power PA with configurable output up to +22dBm

Modified files:
- Modem.h: add LR11XX modem type
- Boards.h: add BOARD_TBEAM_S_LR_V1 with pin definitions and model
  codes (MODEL_D3/DF/D7 for 433/868/2400 MHz)
- Utilities.h: add LR11XX modem selection, LED stubs, model validation,
  and kiss_indicate_log() for KISS-framed debug output
- RNode_Firmware.ino: add LR11XX cases for setPins, symbol calculation,
  TX power clamping, and serial wait exclusion
- Display.h: add BOARD_TBEAM_S_LR_V1 alongside BOARD_TBEAM_S_V1
- Power.h: add BOARD_TBEAM_S_LR_V1 alongside BOARD_TBEAM_S_V1
- Makefile: add firmware-tbeam_supreme_lr1121 build target
This commit is contained in:
Ben Agricola 2026-03-31 16:52:26 +01:00
commit 5888438d82
9 changed files with 1106 additions and 19 deletions

View file

@ -61,9 +61,13 @@
#define MODEL_D9 0xD9 // LilyGO T-Deck, 868 MHz
#define PRODUCT_TBEAM_S_V1 0xEA
#define BOARD_TBEAM_S_V1 0x3D
#define MODEL_DB 0xDB // LilyGO T-Beam Supreme, 433 MHz
#define MODEL_DC 0xDC // LilyGO T-Beam Supreme, 868 MHz
#define BOARD_TBEAM_S_V1 0x3D // SX1262 variant
#define MODEL_DB 0xDB // LilyGO T-Beam Supreme SX1262, 433 MHz
#define MODEL_DC 0xDC // LilyGO T-Beam Supreme SX1262, 868 MHz
#define BOARD_TBEAM_S_LR_V1 0x43 // LR1121 variant
#define MODEL_D3 0xD3 // LilyGO T-Beam Supreme LR1121, 433 MHz
#define MODEL_DF 0xDF // LilyGO T-Beam Supreme LR1121, 868 MHz
#define MODEL_D7 0xD7 // LilyGO T-Beam Supreme LR1121, 2.4 GHz
#define PRODUCT_XIAO_S3 0xEB
#define BOARD_XIAO_S3 0x3E
@ -665,6 +669,60 @@
#endif
#endif
#elif BOARD_MODEL == BOARD_TBEAM_S_LR_V1
#define IS_ESP32S3 true
#define MODEM LR11XX
// TODO: Re-enable firmware validation once proper signing is implemented
#undef VALIDATE_FIRMWARE
#define VALIDATE_FIRMWARE false
#define HAS_BUSY true
#define HAS_TCXO true
#define HAS_DISPLAY true
#define HAS_CONSOLE true
#define HAS_WIFI true
#define HAS_BLUETOOTH false
#define HAS_BLE true
#define HAS_PMU true
#define HAS_NP false
#define HAS_SD false
#define HAS_EEPROM true
#define HAS_INPUT true
#define HAS_SLEEP false
#define PMU_IRQ 40
#define I2C_SCL 41
#define I2C_SDA 42
const int pin_btn_usr1 = 0;
const int pin_cs = 10;
const int pin_reset = 5;
const int pin_sclk = 12;
const int pin_mosi = 11;
const int pin_miso = 13;
const int pin_tcxo_enable = -1;
const int pin_dio = 1;
const int pin_busy = 4;
const int SD_MISO = 37;
const int SD_MOSI = 35;
const int SD_CLK = 36;
const int SD_CS = 47;
const int IMU_CS = 34;
#if HAS_NP == false
#if defined(EXTERNAL_LEDS)
const int pin_led_rx = 43;
const int pin_led_tx = 43;
#else
const int pin_led_rx = 43;
const int pin_led_tx = 43;
#endif
#endif
#elif BOARD_MODEL == BOARD_XIAO_S3
#define IS_ESP32S3 true
#define MODEM SX1262

View file

@ -22,7 +22,7 @@
#elif BOARD_MODEL == BOARD_HELTEC_T114
#include "ST7789.h"
#define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
#include <Adafruit_SH110X.h>
#else
#include <Wire.h>
@ -84,7 +84,7 @@
#define DISP_W 128
#define DISP_H 64
#define DISP_ADDR -1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
#define DISP_RST -1
#define DISP_ADDR 0x3C
#define SCL_OLED 18
@ -112,7 +112,7 @@
ST7789Spi display(&SPI1, DISPLAY_RST, DISPLAY_DC, DISPLAY_CS);
#define SSD1306_WHITE ST77XX_WHITE
#define SSD1306_BLACK ST77XX_BLACK
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);
#define SSD1306_WHITE SH110X_WHITE
#define SSD1306_BLACK SH110X_BLACK
@ -225,7 +225,7 @@ void update_area_positions() {
}
uint8_t display_contrast = 0x00;
#if BOARD_MODEL == BOARD_TBEAM_S_V1
#if BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
void set_contrast(Adafruit_SH1106G *display, uint8_t value) {
}
#elif BOARD_MODEL == BOARD_HELTEC_T114
@ -319,7 +319,7 @@ bool display_init() {
pinMode(pin_backlight, OUTPUT);
analogWrite(pin_backlight, 0);
#endif
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
Wire.begin(SDA_OLED, SCL_OLED);
#elif BOARD_MODEL == BOARD_XIAO_S3
Wire.begin(SDA_OLED, SCL_OLED);
@ -376,7 +376,7 @@ bool display_init() {
// set white as default pixel colour for Heltec T114
display.setRGB(COLOR565(0xFF, 0xFF, 0xFF));
if (false) {
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
if (!display.begin(display_address, true)) {
#else
if (!display.begin(SSD1306_SWITCHCAPVCC, display_address)) {
@ -410,7 +410,7 @@ bool display_init() {
#elif BOARD_MODEL == BOARD_TBEAM
disp_mode = DISP_MODE_LANDSCAPE;
display.setRotation(0);
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
disp_mode = DISP_MODE_PORTRAIT;
display.setRotation(1);
#elif BOARD_MODEL == BOARD_HELTEC32_V2

View file

@ -98,6 +98,9 @@ firmware-tdeck:
firmware-tbeam_supreme:
arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x3D"
firmware-tbeam_supreme_lr1121:
arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x43"
firmware-lora32_v10: check_bt_buffers
arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\""

View file

@ -2,3 +2,4 @@
#define SX1278 0x02
#define SX1262 0x03
#define SX1280 0x04
#define LR11XX 0x05

View file

@ -19,12 +19,12 @@
bool pmu_temp_sensor_ready = false;
float pmu_temperature = PMU_TEMP_MIN-1;
#if BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1
#if BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
#include <XPowersLib.h>
XPowersLibInterface* PMU = NULL;
#ifndef PMU_WIRE_PORT
#if BOARD_MODEL == BOARD_TBEAM_S_V1
#if BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
#define PMU_WIRE_PORT Wire1
#else
#define PMU_WIRE_PORT Wire
@ -308,7 +308,7 @@ void measure_battery() {
// }
}
#elif BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
if (PMU) {
float discharge_current = 0;
float charge_current = 0;
@ -564,7 +564,7 @@ bool init_pmu() {
PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
return true;
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
Wire1.begin(I2C_SDA, I2C_SCL);
if (!PMU) {

View file

@ -129,7 +129,7 @@ void setup() {
boot_seq();
#endif
#if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4
#if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_TBEAM_S_LR_V1 && BOARD_MODEL != BOARD_HELTEC32_V4
// Some boards need to wait until the hardware UART is set up before booting
// the full firmware. In the case of the RAK4631 and Heltec T114, the line below will wait
// until a serial connection is actually established with a master. Thus, it
@ -176,7 +176,7 @@ void setup() {
// pins for the LoRa module
#if MODEM == SX1276 || MODEM == SX1278
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy);
#elif MODEM == SX1262
#elif MODEM == SX1262 || MODEM == LR11XX
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen);
#elif MODEM == SX1280
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen);
@ -664,7 +664,7 @@ void add_airtime(uint16_t written) {
lora_symbols += lora_preamble_symbols + 0.25 + 8;
packet_cost_ms += lora_symbols * lora_symbol_time_ms;
#elif MODEM == SX1262 || MODEM == SX1280
#elif MODEM == SX1262 || MODEM == SX1280 || MODEM == LR11XX
if (lora_sf < 7) {
lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + PHY_HEADER_LORA_SYMBOLS);
lora_symbols /= 4*lora_sf;
@ -859,7 +859,7 @@ void serial_callback(uint8_t sbyte) {
kiss_indicate_txpower();
} else {
int txp = sbyte;
#if MODEM == SX1262
#if MODEM == SX1262 || MODEM == LR11XX
#if HAS_LORA_PA
if (txp > PA_MAX_OUTPUT) txp = PA_MAX_OUTPUT;
#else
@ -1699,6 +1699,7 @@ void loop() {
tx_queue_handler();
check_modem_status();
} else {
if (hw_ready) {

View file

@ -38,6 +38,9 @@ sx127x *LoRa = &sx127x_modem;
#elif MODEM == SX1280
#include "sx128x.h"
sx128x *LoRa = &sx128x_modem;
#elif MODEM == LR11XX
#include "lr11xx.h"
lr11xx *LoRa = &lr11xx_modem;
#endif
#include "ROM.h"
@ -243,7 +246,7 @@ uint8_t boot_vector = 0x00;
void led_tx_off() { }
void led_id_on() { }
void led_id_off() { }
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_TBEAM_S_LR_V1
void led_rx_on() { }
void led_rx_off() { }
void led_tx_on() { }
@ -841,6 +844,18 @@ void kiss_indicate_error(uint8_t error_code) {
serial_write(FEND);
}
void kiss_indicate_log(const char *msg) {
serial_write(FEND);
serial_write(CMD_LOG);
while (*msg) {
if (*msg == FEND) { serial_write(FESC); serial_write(TFEND); }
else if (*msg == FESC) { serial_write(FESC); serial_write(TFESC); }
else { serial_write(*msg); }
msg++;
}
serial_write(FEND);
}
void kiss_indicate_radiostate() {
serial_write(FEND);
serial_write(CMD_RADIO_STATE);
@ -1631,6 +1646,8 @@ bool eeprom_model_valid() {
if (model == MODEL_16 || model == MODEL_17) {
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
if (model == MODEL_DB || model == MODEL_DC) {
#elif BOARD_MODEL == BOARD_TBEAM_S_LR_V1
if (model == MODEL_D3 || model == MODEL_DF || model == MODEL_D7) {
#elif BOARD_MODEL == BOARD_XIAO_S3
if (model == MODEL_DD || model == MODEL_DE) {
#elif BOARD_MODEL == BOARD_LORA32_V1_0

866
lr11xx.cpp Normal file
View file

@ -0,0 +1,866 @@
// Copyright 2025
// Licensed under the MIT license.
// LR11xx radio driver for RNode Firmware
// Supports LR1121 (and potentially LR1110/LR1120)
// Uses 2-byte opcode SPI protocol with two-phase reads
#include "lr11xx.h"
#if PLATFORM == PLATFORM_ESP32
#if defined(ESP32) and !defined(CONFIG_IDF_TARGET_ESP32S3)
#include "soc/rtc_wdt.h"
#endif
#define ISR_VECT IRAM_ATTR
#else
#define ISR_VECT
#endif
// ---- LR11xx System Opcodes (0x01xx) ----
#define OP_GET_STATUS_11XX 0x0100
#define OP_GET_VERSION_11XX 0x0101
#define OP_WRITE_REG_MEM_11XX 0x0105
#define OP_READ_REG_MEM_11XX 0x0106
#define OP_WRITE_BUFFER_11XX 0x0109
#define OP_READ_BUFFER_11XX 0x010A
#define OP_GET_ERRORS_11XX 0x010D
#define OP_CLEAR_ERRORS_11XX 0x010E
#define OP_CALIBRATE_11XX 0x010F
#define OP_SET_REG_MODE_11XX 0x0110
#define OP_CALIBRATE_IMAGE_11XX 0x0111
#define OP_SET_DIO_AS_RF_SWITCH_11XX 0x0112
#define OP_SET_DIO_IRQ_PARAMS_11XX 0x0113
#define OP_CLEAR_IRQ_11XX 0x0114
#define OP_SET_TCXO_MODE_11XX 0x0117
#define OP_SET_SLEEP_11XX 0x011B
#define OP_SET_STANDBY_11XX 0x011C
#define OP_SET_FS_11XX 0x011D
// ---- LR11xx Radio Opcodes (0x02xx) ----
#define OP_GET_RX_BUFFER_STATUS_11XX 0x0203
#define OP_GET_PACKET_STATUS_11XX 0x0204
#define OP_GET_RSSI_INST_11XX 0x0205
#define OP_SET_RX_11XX 0x0209
#define OP_SET_TX_11XX 0x020A
#define OP_SET_RF_FREQUENCY_11XX 0x020B
#define OP_SET_CAD_PARAMS_11XX 0x020D
#define OP_SET_PACKET_TYPE_11XX 0x020E
#define OP_SET_MODULATION_PARAMS_11XX 0x020F
#define OP_SET_PACKET_PARAMS_11XX 0x0210
#define OP_SET_TX_PARAMS_11XX 0x0211
#define OP_SET_RX_TX_FALLBACK_11XX 0x0213
#define OP_SET_PA_CONFIG_11XX 0x0215
#define OP_SET_CAD_11XX 0x0218
#define OP_SET_RX_BOOSTED_11XX 0x0227
#define OP_SET_LORA_SYNC_WORD_11XX 0x022B
// ---- LR11xx IRQ Masks (32-bit) ----
#define IRQ_TX_DONE_11XX 0x00000004 // bit 2
#define IRQ_RX_DONE_11XX 0x00000008 // bit 3
#define IRQ_PREAMBLE_DET_11XX 0x00000010 // bit 4
#define IRQ_SYNC_HEADER_VALID_11XX 0x00000020 // bit 5
#define IRQ_HEADER_ERR_11XX 0x00000040 // bit 6
#define IRQ_CRC_ERR_11XX 0x00000080 // bit 7
#define IRQ_CAD_DONE_11XX 0x00000100 // bit 8
#define IRQ_CAD_DETECTED_11XX 0x00000200 // bit 9
#define IRQ_TIMEOUT_11XX 0x00000400 // bit 10
// Only documented IRQ bits (2-11, 21-25)
#define IRQ_ALL_11XX 0x03E00FFC
// ---- LR11xx Mode Constants ----
#define MODE_STDBY_RC_11XX 0x00
#define MODE_STDBY_XOSC_11XX 0x01
#define MODE_PACKET_TYPE_LORA_11XX 0x02
#define MODE_FALLBACK_STDBY_RC_11XX 0x01
// ---- LR11xx PA Constants ----
#define PA_SEL_LP_11XX 0x00
#define PA_SEL_HP_11XX 0x01
#define PA_SEL_HF_11XX 0x02
#define PA_REG_SUPPLY_VREG_11XX 0x00
#define PA_REG_SUPPLY_VBAT_11XX 0x01
// ---- LR11xx TCXO Voltage ----
#define MODE_TCXO_3_0V_11XX 0x06
// ---- LR11xx Register Addresses ----
#define REG_HIGH_ACP_11XX 0x00F30054
// ---- LR11xx Device Types (from GetVersion) ----
#define DEVICE_LR1121 0x03
// ---- LR11xx Sync Word ----
#define SYNC_WORD_PRIVATE_11XX 0x12
lr11xx::lr11xx() :
_spiSettings(8E6, MSBFIRST, SPI_MODE0),
_ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN),
_dio0(LORA_DEFAULT_DIO0_PIN), _rxen(LORA_DEFAULT_RXEN_PIN),
_busy(LORA_DEFAULT_BUSY_PIN),
_frequency(0), _txp(17), _sf(0x07), _bw(0x04), _cr(0x01), _ldro(0x00),
_packetIndex(0), _preambleLength(18), _implicitHeaderMode(0),
_payloadLength(255), _crcMode(1), _fifo_rx_addr_ptr(0),
_preinit_done(false), _preamble_detected_at(0), _onReceive(NULL)
{
}
// --- SPI Primitives (2-byte opcodes, two-phase reads) ---
void lr11xx::waitOnBusy() {
unsigned long time = millis();
if (_busy != -1) {
while (digitalRead(_busy) == HIGH) {
if (millis() >= (time + 100)) { break; }
}
}
}
void lr11xx::executeOpcode(uint16_t opcode, uint8_t *buffer, uint8_t size) {
waitOnBusy();
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
// Capture MISO: during writes the chip returns Stat1, Stat2, IrqStatus
// inline (per user manual Section 3.1). Store for use by handleDio0Rise().
int misoIdx = 0;
_lastMiso[misoIdx++] = SPI.transfer((opcode >> 8) & 0xFF);
_lastMiso[misoIdx++] = SPI.transfer(opcode & 0xFF);
for (int i = 0; i < size; i++) {
if (misoIdx < 6) {
_lastMiso[misoIdx++] = SPI.transfer(buffer[i]);
} else {
SPI.transfer(buffer[i]);
}
}
SPI.endTransaction();
digitalWrite(_ss, HIGH);
}
void lr11xx::executeOpcodeRead(uint16_t opcode, uint8_t *buffer, uint8_t size) {
waitOnBusy();
// Phase 1: send command
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer((opcode >> 8) & 0xFF);
SPI.transfer(opcode & 0xFF);
SPI.endTransaction();
digitalWrite(_ss, HIGH);
// Wait for chip to finish processing the command
waitOnBusy();
// Phase 2: read response
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer(0x00); // dummy/status byte
for (int i = 0; i < size; i++) {
buffer[i] = SPI.transfer(0x00);
}
SPI.endTransaction();
digitalWrite(_ss, HIGH);
}
uint32_t lr11xx::readRegister32(uint32_t address) {
uint8_t addr_buf[5];
addr_buf[0] = (address >> 24) & 0xFF;
addr_buf[1] = (address >> 16) & 0xFF;
addr_buf[2] = (address >> 8) & 0xFF;
addr_buf[3] = address & 0xFF;
addr_buf[4] = 0x01;
waitOnBusy();
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer((OP_READ_REG_MEM_11XX >> 8) & 0xFF);
SPI.transfer(OP_READ_REG_MEM_11XX & 0xFF);
for (int i = 0; i < 5; i++) SPI.transfer(addr_buf[i]);
SPI.endTransaction();
digitalWrite(_ss, HIGH);
waitOnBusy();
uint8_t val[4];
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer(0x00);
for (int i = 0; i < 4; i++) val[i] = SPI.transfer(0x00);
SPI.endTransaction();
digitalWrite(_ss, HIGH);
return ((uint32_t)val[0] << 24) | ((uint32_t)val[1] << 16) |
((uint32_t)val[2] << 8) | val[3];
}
void lr11xx::writeRegister32(uint32_t address, uint32_t value) {
uint8_t buf[8];
buf[0] = (address >> 24) & 0xFF;
buf[1] = (address >> 16) & 0xFF;
buf[2] = (address >> 8) & 0xFF;
buf[3] = address & 0xFF;
buf[4] = (value >> 24) & 0xFF;
buf[5] = (value >> 16) & 0xFF;
buf[6] = (value >> 8) & 0xFF;
buf[7] = value & 0xFF;
executeOpcode(OP_WRITE_REG_MEM_11XX, buf, 8);
}
void lr11xx::writeBuffer(const uint8_t* buffer, size_t size) {
waitOnBusy();
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer((OP_WRITE_BUFFER_11XX >> 8) & 0xFF);
SPI.transfer(OP_WRITE_BUFFER_11XX & 0xFF);
for (size_t i = 0; i < size; i++) {
SPI.transfer(buffer[i]);
}
SPI.endTransaction();
digitalWrite(_ss, HIGH);
}
void lr11xx::readBuffer(uint8_t* buffer, size_t size) {
uint8_t cmd_buf[2];
cmd_buf[0] = _fifo_rx_addr_ptr;
cmd_buf[1] = (uint8_t)size;
waitOnBusy();
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer((OP_READ_BUFFER_11XX >> 8) & 0xFF);
SPI.transfer(OP_READ_BUFFER_11XX & 0xFF);
SPI.transfer(cmd_buf[0]);
SPI.transfer(cmd_buf[1]);
SPI.endTransaction();
digitalWrite(_ss, HIGH);
waitOnBusy();
digitalWrite(_ss, LOW);
SPI.beginTransaction(_spiSettings);
SPI.transfer(0x00);
for (size_t i = 0; i < size; i++) {
buffer[i] = SPI.transfer(0x00);
}
SPI.endTransaction();
digitalWrite(_ss, HIGH);
}
void lr11xx::clearIrqFlags(uint32_t mask) {
uint8_t buf[4];
buf[0] = (mask >> 24) & 0xFF;
buf[1] = (mask >> 16) & 0xFF;
buf[2] = (mask >> 8) & 0xFF;
buf[3] = mask & 0xFF;
executeOpcode(OP_CLEAR_IRQ_11XX, buf, 4);
}
// --- Lifecycle ---
void lr11xx::setPins(int ss, int reset, int dio0, int busy, int rxen) {
_ss = ss;
_reset = reset;
_dio0 = dio0;
_busy = busy;
_rxen = rxen;
}
void lr11xx::reset() {
if (_reset != -1) {
pinMode(_reset, OUTPUT);
digitalWrite(_reset, LOW);
delay(1);
digitalWrite(_reset, HIGH);
delay(10);
}
}
bool lr11xx::preInit() {
pinMode(_ss, OUTPUT);
digitalWrite(_ss, HIGH);
#if BOARD_MODEL == BOARD_TBEAM_S_LR_V1
SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
#else
SPI.begin();
#endif
reset();
if (_busy != -1) { pinMode(_busy, INPUT); }
long start = millis();
uint8_t device_type = 0;
while (((millis() - start) < 2000) && (millis() >= start)) {
uint8_t version_buf[4] = {0};
executeOpcodeRead(OP_GET_VERSION_11XX, version_buf, 4);
device_type = version_buf[1];
if (device_type == DEVICE_LR1121) {
break;
}
delay(100);
}
if (device_type != DEVICE_LR1121) {
return false;
}
_preinit_done = true;
return true;
}
int lr11xx::begin(long frequency) {
_frequency = frequency;
reset();
if (_busy != -1) { pinMode(_busy, INPUT); }
if (!_preinit_done) {
if (!preInit()) {
return 0;
}
}
standby();
// DC-DC regulator
uint8_t reg_mode = 0x01;
executeOpcode(OP_SET_REG_MODE_11XX, &reg_mode, 1);
configureRfSwitch();
enableTCXO();
uint8_t clear_buf[4] = {0};
executeOpcode(OP_CLEAR_ERRORS_11XX, clear_buf, 4);
calibrate();
calibrateImage(frequency);
loraMode();
setFrequency(frequency);
setSyncWord(SYNC_WORD_PRIVATE_11XX);
uint8_t fallback = MODE_FALLBACK_STDBY_RC_11XX;
executeOpcode(OP_SET_RX_TX_FALLBACK_11XX, &fallback, 1);
setModulationParams(_sf, _bw, _cr, _ldro);
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
setTxPower(_txp);
setRxBoosted(true);
clearIrqFlags(IRQ_ALL_11XX);
return 1;
}
void lr11xx::end() {
sleep();
SPI.end();
_preinit_done = false;
}
// --- TX Path ---
int lr11xx::beginPacket(int implicitHeader) {
standby();
if (implicitHeader) {
implicitHeaderMode();
} else {
explicitHeaderMode();
}
_payloadLength = 0;
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
return 1;
}
int lr11xx::endPacket() {
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
// Write accumulated packet data to TX buffer in one shot
writeBuffer(_packet, _payloadLength);
applyHighAcpWorkaround();
uint8_t timeout[3] = {0x00, 0x00, 0x00};
executeOpcode(OP_SET_TX_11XX, timeout, 3);
// Poll for TX_DONE
// GetStatus Phase 2 returns: [stat] [IRQ3] [IRQ2] [IRQ1] [IRQ0]
bool timed_out = false;
// Use a simple timeout based on payload length
uint32_t w_timeout = millis() + LORA_MODEM_TIMEOUT_MS;
while (millis() < w_timeout) {
uint8_t irq_buf[5] = {0};
executeOpcodeRead(OP_GET_STATUS_11XX, irq_buf, 5);
uint32_t irq_status = ((uint32_t)irq_buf[1] << 24) | ((uint32_t)irq_buf[2] << 16) |
((uint32_t)irq_buf[3] << 8) | irq_buf[4];
if (irq_status & IRQ_TX_DONE_11XX) break;
yield();
}
if (millis() >= w_timeout) { timed_out = true; }
clearIrqFlags(IRQ_ALL_11XX);
return !timed_out;
}
size_t lr11xx::write(uint8_t byte) {
return write(&byte, sizeof(byte));
}
size_t lr11xx::write(const uint8_t *buffer, size_t size) {
if ((_payloadLength + size) > 255) {
size = 255 - _payloadLength;
}
// Buffer locally in _packet. LR11xx WriteBuffer8 always writes from offset 0
// (no auto-incrementing FIFO pointer like SX126x), so we accumulate here and
// write all at once in endPacket().
memcpy(_packet + _payloadLength, buffer, size);
_payloadLength += size;
return size;
}
// --- RX Path ---
int lr11xx::parsePacket(int size) {
// Not used in RNode firmware (uses onReceive callback instead)
return 0;
}
int ISR_VECT lr11xx::available() {
uint8_t buf[2] = {0};
executeOpcodeRead(OP_GET_RX_BUFFER_STATUS_11XX, buf, 2);
return buf[0] - _packetIndex;
}
int ISR_VECT lr11xx::read() {
if (!available()) { return -1; }
if (_packetIndex == 0) {
uint8_t rxbuf[2] = {0};
executeOpcodeRead(OP_GET_RX_BUFFER_STATUS_11XX, rxbuf, 2);
int size = rxbuf[0];
_fifo_rx_addr_ptr = rxbuf[1];
readBuffer(_packet, size);
}
uint8_t byte = _packet[_packetIndex];
_packetIndex++;
return byte;
}
int lr11xx::peek() {
if (!available()) { return -1; }
if (_packetIndex == 0) {
uint8_t rxbuf[2] = {0};
executeOpcodeRead(OP_GET_RX_BUFFER_STATUS_11XX, rxbuf, 2);
int size = rxbuf[0];
_fifo_rx_addr_ptr = rxbuf[1];
readBuffer(_packet, size);
}
return _packet[_packetIndex];
}
void lr11xx::flush() {
}
void lr11xx::onReceive(void(*callback)(int)) {
_onReceive = callback;
if (callback) {
pinMode(_dio0, INPUT);
// Route only RX_DONE to DIO9
uint8_t irq_buf[8];
uint32_t irq_mask = IRQ_RX_DONE_11XX;
irq_buf[0] = (irq_mask >> 24) & 0xFF;
irq_buf[1] = (irq_mask >> 16) & 0xFF;
irq_buf[2] = (irq_mask >> 8) & 0xFF;
irq_buf[3] = irq_mask & 0xFF;
irq_buf[4] = (irq_mask >> 24) & 0xFF;
irq_buf[5] = (irq_mask >> 16) & 0xFF;
irq_buf[6] = (irq_mask >> 8) & 0xFF;
irq_buf[7] = irq_mask & 0xFF;
executeOpcode(OP_SET_DIO_IRQ_PARAMS_11XX, irq_buf, 8);
attachInterrupt(digitalPinToInterrupt(_dio0), lr11xx::onDio0Rise, RISING);
} else {
detachInterrupt(digitalPinToInterrupt(_dio0));
}
}
void lr11xx::receive(int size) {
if (size > 0) {
implicitHeaderMode();
_payloadLength = size;
} else {
explicitHeaderMode();
_payloadLength = 0;
}
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
clearIrqFlags(IRQ_ALL_11XX);
uint8_t mode[3] = {0xFF, 0xFF, 0xFF}; // continuous RX
executeOpcode(OP_SET_RX_11XX, mode, 3);
}
// Named handleDio0Rise for consistency with the sx126x driver convention.
// On the LR1121 this actually handles DIO9 (the primary interrupt pin),
// not DIO0 (which is the BUSY signal on LR11xx).
void ISR_VECT lr11xx::handleDio0Rise() {
// ClearIrq is a single-phase write command (ISR-safe). The chip returns
// Stat1, Stat2, and IrqStatus inline on MISO during the transaction
// (per user manual Section 3.1), captured by executeOpcode into _lastMiso.
clearIrqFlags(IRQ_RX_DONE_11XX | IRQ_CRC_ERR_11XX | IRQ_HEADER_ERR_11XX);
uint32_t irq = ((uint32_t)_lastMiso[2] << 24) | ((uint32_t)_lastMiso[3] << 16) |
((uint32_t)_lastMiso[4] << 8) | _lastMiso[5];
// Reject if header CRC (bit 6) or payload CRC (bit 7) failed
if ((irq & (IRQ_CRC_ERR_11XX | IRQ_HEADER_ERR_11XX)) == 0) {
// Reset read position so read()/peek() fetch from the start of the new packet
_packetIndex = 0;
uint8_t rxbuf[2] = {0};
executeOpcodeRead(OP_GET_RX_BUFFER_STATUS_11XX, rxbuf, 2);
int packetLength = rxbuf[0];
_fifo_rx_addr_ptr = rxbuf[1];
// Guard against spurious interrupts delivering zero-length packets
if (packetLength > 0 && _onReceive) {
_onReceive(packetLength);
}
}
}
void ISR_VECT lr11xx::onDio0Rise() {
lr11xx_modem.handleDio0Rise();
}
// --- Modem Control ---
void lr11xx::standby() {
uint8_t mode = MODE_STDBY_RC_11XX;
executeOpcode(OP_SET_STANDBY_11XX, &mode, 1);
}
void lr11xx::sleep() {
uint8_t buf[5] = {0};
buf[0] = 0x01;
executeOpcode(OP_SET_SLEEP_11XX, buf, 5);
}
// --- RF Configuration ---
uint32_t lr11xx::getFrequency() {
return _frequency;
}
void lr11xx::setFrequency(long frequency) {
_frequency = frequency;
uint8_t buf[4];
buf[0] = ((uint32_t)frequency >> 24) & 0xFF;
buf[1] = ((uint32_t)frequency >> 16) & 0xFF;
buf[2] = ((uint32_t)frequency >> 8) & 0xFF;
buf[3] = (uint32_t)frequency & 0xFF;
executeOpcode(OP_SET_RF_FREQUENCY_11XX, buf, 4);
}
void lr11xx::setSpreadingFactor(int sf) {
if (sf < 5) sf = 5;
else if (sf > 12) sf = 12;
_sf = sf;
handleLowDataRate();
setModulationParams(sf, _bw, _cr, _ldro);
}
long lr11xx::getSignalBandwidth() {
switch (_bw) {
case 0x03: return 62.5E3;
case 0x04: return 125E3;
case 0x05: return 250E3;
case 0x06: return 500E3;
}
return 0;
}
void lr11xx::setSignalBandwidth(long sbw) {
if (sbw <= 62.5E3) {
_bw = 0x03;
} else if (sbw <= 125E3) {
_bw = 0x04;
} else if (sbw <= 250E3) {
_bw = 0x05;
} else {
_bw = 0x06;
}
handleLowDataRate();
setModulationParams(_sf, _bw, _cr, _ldro);
}
void lr11xx::setCodingRate4(int denominator) {
if (denominator < 5) denominator = 5;
else if (denominator > 8) denominator = 8;
_cr = denominator - 4;
setModulationParams(_sf, _bw, _cr, _ldro);
}
void lr11xx::setPreambleLength(long length) {
_preambleLength = length;
setPacketParams(length, _implicitHeaderMode, _payloadLength, _crcMode);
}
// --- Power Control ---
uint8_t lr11xx::getTxPower() {
return _txp;
}
void lr11xx::setTxPower(int level, int outputPin) {
uint8_t pa_buf[4];
pa_buf[0] = PA_SEL_HP_11XX;
pa_buf[1] = PA_REG_SUPPLY_VBAT_11XX;
pa_buf[2] = 0x04;
pa_buf[3] = 0x07;
executeOpcode(OP_SET_PA_CONFIG_11XX, pa_buf, 4);
if (level > 22) level = 22;
else if (level < -9) level = -9;
_txp = level;
uint8_t tx_buf[2];
tx_buf[0] = (uint8_t)level;
tx_buf[1] = 0x02;
executeOpcode(OP_SET_TX_PARAMS_11XX, tx_buf, 2);
}
// --- Signal Quality ---
uint8_t lr11xx::currentRssiRaw() {
uint8_t byte = 0;
executeOpcodeRead(OP_GET_RSSI_INST_11XX, &byte, 1);
return byte;
}
int lr11xx::currentRssi() {
uint8_t byte = 0;
executeOpcodeRead(OP_GET_RSSI_INST_11XX, &byte, 1);
return -(int(byte)) / 2;
}
uint8_t lr11xx::packetRssiRaw() {
uint8_t buf[3] = {0};
executeOpcodeRead(OP_GET_PACKET_STATUS_11XX, buf, 3);
return buf[0];
}
int lr11xx::packetRssi() {
uint8_t buf[3] = {0};
executeOpcodeRead(OP_GET_PACKET_STATUS_11XX, buf, 3);
return -buf[0] / 2;
}
int lr11xx::packetRssi(uint8_t pkt_snr_raw) {
return packetRssi();
}
uint8_t ISR_VECT lr11xx::packetSnrRaw() {
uint8_t buf[3] = {0};
executeOpcodeRead(OP_GET_PACKET_STATUS_11XX, buf, 3);
return buf[1];
}
float lr11xx::packetSnr() {
uint8_t buf[3] = {0};
executeOpcodeRead(OP_GET_PACKET_STATUS_11XX, buf, 3);
return float((int8_t)buf[1]) * 0.25;
}
long lr11xx::packetFrequencyError() {
return 0;
}
// --- Channel Monitoring ---
bool lr11xx::dcd() {
uint8_t irq_buf[5] = {0};
executeOpcodeRead(OP_GET_STATUS_11XX, irq_buf, 5);
// GetStatus returns extra status byte at position 0, IRQ flags at bytes 1-4
uint32_t irq = ((uint32_t)irq_buf[1] << 24) | ((uint32_t)irq_buf[2] << 16) |
((uint32_t)irq_buf[3] << 8) | irq_buf[4];
uint32_t now = millis();
bool preamble = irq & IRQ_PREAMBLE_DET_11XX;
bool header = irq & IRQ_SYNC_HEADER_VALID_11XX;
bool carrier_detected = false;
bool false_preamble = false;
// Header without preamble is a stranded flag from a previous detection
// where ClearIrq cleared the preamble but not the header. Clear it to
// prevent permanent carrier detection deadlock.
if (header && !preamble) {
clearIrqFlags(IRQ_SYNC_HEADER_VALID_11XX);
} else if (header && preamble) {
carrier_detected = true;
}
if (preamble) {
carrier_detected = true;
if (_preamble_detected_at == 0) { _preamble_detected_at = now; }
if (now - _preamble_detected_at > (uint32_t)(_preambleLength * (1 << _sf) / (getSignalBandwidth() / 1000) + 8 * (1 << _sf) / (getSignalBandwidth() / 1000))) {
_preamble_detected_at = 0;
if (!header) {
false_preamble = true;
clearIrqFlags(IRQ_PREAMBLE_DET_11XX | IRQ_SYNC_HEADER_VALID_11XX);
} else {
clearIrqFlags(IRQ_PREAMBLE_DET_11XX);
}
}
}
// Note: unlike SX126x, we don't call receive() on false preamble.
// The LR11xx stays in continuous RX mode and DIO9 is re-armed in
// handleDio0Rise() via SetRx. Calling receive() here would disrupt
// any in-progress reception because dcd() runs inside a critical
// section during active packet reception.
return carrier_detected;
}
// --- CRC ---
void lr11xx::enableCrc() {
_crcMode = 1;
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
}
void lr11xx::disableCrc() {
_crcMode = 0;
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
}
// --- TCXO ---
void lr11xx::enableTCXO() {
uint8_t buf[4];
buf[0] = MODE_TCXO_3_0V_11XX;
buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0xFF;
executeOpcode(OP_SET_TCXO_MODE_11XX, buf, 4);
}
void lr11xx::disableTCXO() {
}
// --- Sync Word ---
void lr11xx::setSyncWord(uint8_t sw) {
executeOpcode(OP_SET_LORA_SYNC_WORD_11XX, &sw, 1);
}
// --- Misc ---
byte lr11xx::random() {
return currentRssiRaw();
}
void lr11xx::setSPIFrequency(uint32_t frequency) {
_spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0);
}
void lr11xx::dumpRegisters(Stream& out) {
}
// --- LR11xx-specific internal methods ---
void lr11xx::loraMode() {
uint8_t mode = MODE_PACKET_TYPE_LORA_11XX;
executeOpcode(OP_SET_PACKET_TYPE_11XX, &mode, 1);
}
void lr11xx::setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro) {
uint8_t buf[4];
buf[0] = sf;
buf[1] = bw;
buf[2] = cr;
buf[3] = ldro;
executeOpcode(OP_SET_MODULATION_PARAMS_11XX, buf, 4);
}
void lr11xx::setPacketParams(long preamble, uint8_t headermode, uint8_t length, uint8_t crc) {
uint8_t buf[6];
buf[0] = (preamble >> 8) & 0xFF;
buf[1] = preamble & 0xFF;
buf[2] = headermode;
buf[3] = length;
buf[4] = crc;
buf[5] = 0x00;
executeOpcode(OP_SET_PACKET_PARAMS_11XX, buf, 6);
}
void lr11xx::handleLowDataRate() {
if (long((1 << _sf) / (getSignalBandwidth() / 1000)) > 16) {
_ldro = 0x01;
} else {
_ldro = 0x00;
}
}
void lr11xx::calibrate() {
uint8_t mode = MODE_STDBY_RC_11XX;
executeOpcode(OP_SET_STANDBY_11XX, &mode, 1);
uint8_t cal = 0x3F;
executeOpcode(OP_CALIBRATE_11XX, &cal, 1);
delay(5);
waitOnBusy();
}
void lr11xx::calibrateImage(long frequency) {
uint8_t image_freq[2] = {0};
if (frequency >= 430E6 && frequency <= 440E6) { image_freq[0] = 0x6B; image_freq[1] = 0x6F; }
else if (frequency >= 470E6 && frequency <= 510E6) { image_freq[0] = 0x75; image_freq[1] = 0x81; }
else if (frequency >= 779E6 && frequency <= 787E6) { image_freq[0] = 0xC1; image_freq[1] = 0xC5; }
else if (frequency >= 863E6 && frequency <= 870E6) { image_freq[0] = 0xD7; image_freq[1] = 0xDB; }
else if (frequency >= 902E6 && frequency <= 928E6) { image_freq[0] = 0xE1; image_freq[1] = 0xE9; }
executeOpcode(OP_CALIBRATE_IMAGE_11XX, image_freq, 2);
waitOnBusy();
}
void lr11xx::configureRfSwitch() {
uint8_t buf[8];
buf[0] = 0x03; // enable DIO5 (bit 0) + DIO6 (bit 1)
buf[1] = 0x00; // standby
buf[2] = 0x01; // RX: DIO5=HIGH, DIO6=LOW
buf[3] = 0x02; // TX: DIO5=LOW, DIO6=HIGH
buf[4] = 0x02; // TX_HP
buf[5] = 0x00; // TX_HF
buf[6] = 0x00; // GNSS
buf[7] = 0x00; // WiFi
executeOpcode(OP_SET_DIO_AS_RF_SWITCH_11XX, buf, 8);
}
void lr11xx::applyHighAcpWorkaround() {
uint32_t val = readRegister32(REG_HIGH_ACP_11XX);
val &= ~(1UL << 30);
writeRegister32(REG_HIGH_ACP_11XX, val);
}
void lr11xx::setRxBoosted(bool enable) {
uint8_t val = enable ? 0x01 : 0x00;
executeOpcode(OP_SET_RX_BOOSTED_11XX, &val, 1);
}
void lr11xx::explicitHeaderMode() {
_implicitHeaderMode = 0;
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
}
void lr11xx::implicitHeaderMode() {
_implicitHeaderMode = 1;
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
}
lr11xx lr11xx_modem;

141
lr11xx.h Normal file
View file

@ -0,0 +1,141 @@
// Copyright 2025
// Licensed under the MIT license.
#ifndef LR11XX_H
#define LR11XX_H
#include <Arduino.h>
#include <SPI.h>
#include "Modem.h"
#define LORA_DEFAULT_SS_PIN 10
#define LORA_DEFAULT_RESET_PIN 9
#define LORA_DEFAULT_DIO0_PIN 2
#define LORA_DEFAULT_RXEN_PIN -1
#define LORA_DEFAULT_BUSY_PIN -1
#define LORA_MODEM_TIMEOUT_MS 20E3
#define PA_OUTPUT_RFO_PIN 0
#define PA_OUTPUT_PA_BOOST_PIN 1
#define RSSI_OFFSET 157
class lr11xx : public Stream {
public:
lr11xx();
int begin(long frequency);
void end();
int beginPacket(int implicitHeader = false);
int endPacket();
int parsePacket(int size = 0);
int packetRssi();
int packetRssi(uint8_t pkt_snr_raw);
int currentRssi();
uint8_t packetRssiRaw();
uint8_t currentRssiRaw();
uint8_t packetSnrRaw();
float packetSnr();
long packetFrequencyError();
// from Print
virtual size_t write(uint8_t byte);
virtual size_t write(const uint8_t *buffer, size_t size);
// from Stream
virtual int available();
virtual int read();
virtual int peek();
virtual void flush();
void onReceive(void(*callback)(int));
void receive(int size = 0);
void standby();
void sleep();
void reset(void);
bool preInit();
uint8_t getTxPower();
void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN);
uint32_t getFrequency();
void setFrequency(long frequency);
void setSpreadingFactor(int sf);
long getSignalBandwidth();
void setSignalBandwidth(long sbw);
void setCodingRate4(int denominator);
void setPreambleLength(long preamble_symbols);
void setSyncWord(uint8_t sw);
bool dcd();
void enableCrc();
void disableCrc();
void enableTCXO();
void disableTCXO();
void loraMode();
void waitOnBusy();
// LR11xx SPI layer (2-byte opcodes, two-phase reads)
void executeOpcode(uint16_t opcode, uint8_t *buffer, uint8_t size);
void executeOpcodeRead(uint16_t opcode, uint8_t *buffer, uint8_t size);
void writeBuffer(const uint8_t* buffer, size_t size);
void readBuffer(uint8_t* buffer, size_t size);
void setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc);
void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro);
byte random();
void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN);
void setSPIFrequency(uint32_t frequency);
void dumpRegisters(Stream& out);
private:
void explicitHeaderMode();
void implicitHeaderMode();
void handleDio0Rise();
static void onDio0Rise();
void handleLowDataRate();
void calibrate(void);
void calibrateImage(long frequency);
void configureRfSwitch();
void applyHighAcpWorkaround();
void setRxBoosted(bool enable);
void clearIrqFlags(uint32_t mask);
// LR11xx register access (32-bit addresses)
uint32_t readRegister32(uint32_t address);
void writeRegister32(uint32_t address, uint32_t value);
private:
SPISettings _spiSettings;
int _ss;
int _reset;
int _dio0; // DIO9 on LR1121 (interrupt pin)
int _rxen;
int _busy; // DIO0 on LR1121 (busy indicator)
long _frequency;
int _txp;
uint8_t _sf;
uint8_t _bw;
uint8_t _cr;
uint8_t _ldro;
int _packetIndex;
int _preambleLength;
int _implicitHeaderMode;
int _payloadLength;
int _crcMode;
int _fifo_rx_addr_ptr;
uint8_t _packet[255];
bool _preinit_done;
uint8_t _lastMiso[6]; // Inline MISO capture from write commands
uint32_t _preamble_detected_at;
void (*_onReceive)(int);
};
extern lr11xx lr11xx_modem;
#endif