diff --git a/Boards.h b/Boards.h index ef2b8c5..0b19de3 100644 --- a/Boards.h +++ b/Boards.h @@ -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 diff --git a/Display.h b/Display.h index 7d903b9..4764050 100644 --- a/Display.h +++ b/Display.h @@ -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 #else #include @@ -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 diff --git a/Makefile b/Makefile index 8ba24a9..ce2bd03 100644 --- a/Makefile +++ b/Makefile @@ -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\"" diff --git a/Modem.h b/Modem.h index 027e314..8b2d9f7 100644 --- a/Modem.h +++ b/Modem.h @@ -2,3 +2,4 @@ #define SX1278 0x02 #define SX1262 0x03 #define SX1280 0x04 +#define LR11XX 0x05 diff --git a/Power.h b/Power.h index 7ce6d8a..9c58ba8 100644 --- a/Power.h +++ b/Power.h @@ -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 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) { diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index 5649206..cdb1c4c 100644 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -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) { diff --git a/Utilities.h b/Utilities.h index 8892f59..ed2b66a 100644 --- a/Utilities.h +++ b/Utilities.h @@ -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 diff --git a/lr11xx.cpp b/lr11xx.cpp new file mode 100644 index 0000000..285a6d8 --- /dev/null +++ b/lr11xx.cpp @@ -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, ®_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; diff --git a/lr11xx.h b/lr11xx.h new file mode 100644 index 0000000..21776a5 --- /dev/null +++ b/lr11xx.h @@ -0,0 +1,141 @@ +// Copyright 2025 +// Licensed under the MIT license. + +#ifndef LR11XX_H +#define LR11XX_H + +#include +#include +#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