diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index c90f7c9d..a658899a 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -54,7 +54,7 @@ jobs:
 
     - name: Build example
       run:
-        arduino-cli compile --libraries /home/runner/work/RadioLib --fqbn arduino:avr:uno $PWD/examples/SX126x/SX126x_Transmit_Blocking/SX126x_Transmit_Blocking.ino --warnings=all
+        arduino-cli compile --libraries /home/runner/work/RadioLib --fqbn arduino:avr:uno $PWD/examples/SX123x/SX123x_Transmit_Blocking/SX123x_Transmit_Blocking.ino --warnings=all
 
     - name: Perform CodeQL Analysis
       uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 642e8471..7c518526 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -46,7 +46,7 @@ jobs:
         # platform-dependent settings - extra board options, board index URLs, skip patterns etc.
         include:
           - id: arduino:avr:uno
-            run: echo "skip-pattern=(STM32WL|SSTV|LoRaWAN|LR11x0_Firmware_Update|Pager|APRS|Morse)" >> $GITHUB_OUTPUT
+            run: echo "skip-pattern=(STM32WL|SSTV|LoRaWAN|LR11x0_Firmware_Update|Pager|APRS|Morse|SX126x)" >> $GITHUB_OUTPUT
           - id: arduino:avr:mega
             run: |
               echo "options=':cpu=atmega2560'" >> $GITHUB_OUTPUT
diff --git a/examples/SX126x/SX126x_LR_FHSS_Modem/SX126x_LR_FHSS_Modem.ino b/examples/SX126x/SX126x_LR_FHSS_Modem/SX126x_LR_FHSS_Modem.ino
new file mode 100644
index 00000000..d0f45f8d
--- /dev/null
+++ b/examples/SX126x/SX126x_LR_FHSS_Modem/SX126x_LR_FHSS_Modem.ino
@@ -0,0 +1,94 @@
+/*
+  RadioLib SX126x LR-FHSS Modem Example
+
+  This example shows how to use LR-FHSS modem in SX126x chips.
+  This modem can only transmit data, and is not able to receive.
+
+  NOTE: The sketch below is just a guide on how to use
+        LR-FHSS modem, so this code should not be run directly!
+        Instead, modify the other examples to use LR-FHSS
+        modem and use the appropriate configuration
+        methods.
+
+  For default module settings, see the wiki page
+  https://github.com/jgromes/RadioLib/wiki/Default-configuration#sx126x---lr-fhss-modem
+
+  For full API reference, see the GitHub Pages
+  https://jgromes.github.io/RadioLib/
+*/
+
+// include the library
+#include <RadioLib.h>
+
+// SX1262 has the following connections:
+// NSS pin:   10
+// IRQ pin:   2
+// NRST pin:  3
+// BUSY pin:  9
+SX1262 radio = new Module(10, 2, 3, 9);
+
+// or using RadioShield
+// https://github.com/jgromes/RadioShield
+//SX1262 radio = RadioShield.ModuleA;
+
+// or using CubeCell
+//SX1262 radio = new Module(RADIOLIB_BUILTIN_MODULE);
+
+void setup() {
+  Serial.begin(9600);
+
+  // initialize SX1262 with default settings
+  Serial.print(F("[SX1262] Initializing ... "));
+  int state = radio.beginLRFHSS();
+  if (state == RADIOLIB_ERR_NONE) {
+    Serial.println(F("success!"));
+  } else {
+    Serial.print(F("failed, code "));
+    Serial.println(state);
+    while (true) { delay(10); }
+  }
+
+  // if needed, you can switch between any of the modems
+  //
+  // radio.begin()       start LoRa modem (and disable LR-FHSS)
+  // radio.beginLRFHSS() start LR-FHSS modem (and disable LoRa)
+
+  // the following settings can also
+  // be modified at run-time
+  state = radio.setFrequency(433.5);
+  state = radio.setLrFhssConfig(RADIOLIB_SX126X_LR_FHSS_BW_1523_4,  // bandwidth
+                                RADIOLIB_SX126X_LR_FHSS_CR_1_2,     // coding rate
+                                3,                                  // header count
+                                0x13A);                             // hopping sequence seed
+  state = radio.setOutputPower(10.0);
+  state = radio.setSyncWord(0x12345678);
+  if (state != RADIOLIB_ERR_NONE) {
+    Serial.print(F("Unable to set configuration, code "));
+    Serial.println(state);
+    while (true) { delay(10); }
+  }
+
+  #warning "This sketch is just an API guide! Read the note at line 6."
+}
+
+void loop() {
+  // LR-FHSS modem can only transmit!
+  // transmit LR-FHSS packet
+  int state = radio.transmit("Hello World!");
+  /*
+    byte byteArr[] = {0x01, 0x23, 0x45, 0x67,
+                      0x89, 0xAB, 0xCD, 0xEF};
+    int state = radio.transmit(byteArr, 8);
+  */
+  if (state == RADIOLIB_ERR_NONE) {
+    Serial.println(F("[SX1262] Packet transmitted successfully!"));
+  } else if (state == RADIOLIB_ERR_PACKET_TOO_LONG) {
+    Serial.println(F("[SX1262] Packet too long!"));
+  } else if (state == RADIOLIB_ERR_TX_TIMEOUT) {
+    Serial.println(F("[SX1262] Timed out while transmitting!"));
+  } else {
+    Serial.println(F("[SX1262] Failed to transmit packet, code "));
+    Serial.println(state);
+  }
+
+}
diff --git a/src/modules/SX126x/SX1262.cpp b/src/modules/SX126x/SX1262.cpp
index 232ad638..43c97063 100644
--- a/src/modules/SX126x/SX1262.cpp
+++ b/src/modules/SX126x/SX1262.cpp
@@ -47,6 +47,24 @@ int16_t SX1262::beginFSK(float freq, float br, float freqDev, float rxBw, int8_t
   return(state);
 }
 
+int16_t SX1262::beginLRFHSS(float freq, uint8_t bw, uint8_t cr, bool narrowGrid, int8_t power, float tcxoVoltage, bool useRegulatorLDO) {
+  // execute common part
+  int16_t state = SX126x::beginLRFHSS(bw, cr, narrowGrid, tcxoVoltage, useRegulatorLDO);
+  RADIOLIB_ASSERT(state);
+
+  // configure publicly accessible settings
+  state = setFrequency(freq);
+  RADIOLIB_ASSERT(state);
+
+  state = SX126x::fixPaClamping();
+  RADIOLIB_ASSERT(state);
+
+  state = setOutputPower(power);
+  RADIOLIB_ASSERT(state);
+
+  return(state);
+}
+
 int16_t SX1262::setFrequency(float freq) {
   return(setFrequency(freq, true));
 }
diff --git a/src/modules/SX126x/SX1262.h b/src/modules/SX126x/SX1262.h
index 7ae6789f..c05ee4b1 100644
--- a/src/modules/SX126x/SX1262.h
+++ b/src/modules/SX126x/SX1262.h
@@ -62,6 +62,21 @@ class SX1262: public SX126x {
     */
     int16_t beginFSK(float freq = 434.0, float br = 4.8, float freqDev = 5.0, float rxBw = 156.2, int8_t power = 10, uint16_t preambleLength = 16, float tcxoVoltage = 1.6, bool useRegulatorLDO = false);
     
+    /*!
+      \brief Initialization method for LR-FHSS modem. This modem only supports transmission!
+      \param freq Carrier frequency in MHz. Defaults to 434.0 MHz.
+      \param bw LR-FHSS bandwidth, one of RADIOLIB_SX126X_LR_FHSS_BW_* values. Defaults to 722.66 kHz.
+      \param cr LR-FHSS coding rate, one of RADIOLIB_SX126X_LR_FHSS_CR_* values. Defaults to 2/3 coding rate.
+      \param narrowGrid Whether to use narrow (3.9 kHz) or wide (25.39 kHz) grid spacing. Defaults to true (narrow/non-FCC) grid.
+      \param power Output power in dBm. Defaults to 10 dBm.
+      \param tcxoVoltage TCXO reference voltage to be set. Defaults to 1.6 V.
+      If you are seeing -706/-707 error codes, it likely means you are using non-0 value for module with XTAL.
+      To use XTAL, either set this value to 0, or set SX126x::XTAL to true.
+      \param useRegulatorLDO Whether to use only LDO regulator (true) or DC-DC regulator (false). Defaults to false.
+      \returns \ref status_codes
+    */
+    int16_t beginLRFHSS(float freq = 434.0, uint8_t bw = RADIOLIB_SX126X_LR_FHSS_BW_722_66, uint8_t cr = RADIOLIB_SX126X_LR_FHSS_CR_2_3, bool narrowGrid = true, int8_t power = 10, float tcxoVoltage = 1.6, bool useRegulatorLDO = false);
+    
     // configuration methods
 
     /*!
diff --git a/src/modules/SX126x/SX1268.cpp b/src/modules/SX126x/SX1268.cpp
index c50baaf1..78cfdae3 100644
--- a/src/modules/SX126x/SX1268.cpp
+++ b/src/modules/SX126x/SX1268.cpp
@@ -47,6 +47,24 @@ int16_t SX1268::beginFSK(float freq, float br, float freqDev, float rxBw, int8_t
   return(state);
 }
 
+int16_t SX1268::beginLRFHSS(float freq, uint8_t bw, uint8_t cr, bool narrowGrid, int8_t power, float tcxoVoltage, bool useRegulatorLDO) {
+  // execute common part
+  int16_t state = SX126x::beginLRFHSS(bw, cr, narrowGrid, tcxoVoltage, useRegulatorLDO);
+  RADIOLIB_ASSERT(state);
+
+  // configure publicly accessible settings
+  state = setFrequency(freq);
+  RADIOLIB_ASSERT(state);
+
+  state = SX126x::fixPaClamping();
+  RADIOLIB_ASSERT(state);
+
+  state = setOutputPower(power);
+  RADIOLIB_ASSERT(state);
+
+  return(state);
+}
+
 int16_t SX1268::setFrequency(float freq) {
   return(setFrequency(freq, true));
 }
diff --git a/src/modules/SX126x/SX1268.h b/src/modules/SX126x/SX1268.h
index 503c74e8..ad411ee7 100644
--- a/src/modules/SX126x/SX1268.h
+++ b/src/modules/SX126x/SX1268.h
@@ -61,6 +61,21 @@ class SX1268: public SX126x {
     */
     int16_t beginFSK(float freq = 434.0, float br = 4.8, float freqDev = 5.0, float rxBw = 156.2, int8_t power = 10, uint16_t preambleLength = 16, float tcxoVoltage = 1.6, bool useRegulatorLDO = false);
 
+    /*!
+      \brief Initialization method for LR-FHSS modem. This modem only supports transmission!
+      \param freq Carrier frequency in MHz. Defaults to 434.0 MHz.
+      \param bw LR-FHSS bandwidth, one of RADIOLIB_SX126X_LR_FHSS_BW_* values. Defaults to 722.66 kHz.
+      \param cr LR-FHSS coding rate, one of RADIOLIB_SX126X_LR_FHSS_CR_* values. Defaults to 2/3 coding rate.
+      \param narrowGrid Whether to use narrow (3.9 kHz) or wide (25.39 kHz) grid spacing. Defaults to true (narrow/non-FCC) grid.
+      \param power Output power in dBm. Defaults to 10 dBm.
+      \param tcxoVoltage TCXO reference voltage to be set. Defaults to 1.6 V.
+      If you are seeing -706/-707 error codes, it likely means you are using non-0 value for module with XTAL.
+      To use XTAL, either set this value to 0, or set SX126x::XTAL to true.
+      \param useRegulatorLDO Whether to use only LDO regulator (true) or DC-DC regulator (false). Defaults to false.
+      \returns \ref status_codes
+    */
+    int16_t beginLRFHSS(float freq = 434.0, uint8_t bw = RADIOLIB_SX126X_LR_FHSS_BW_722_66, uint8_t cr = RADIOLIB_SX126X_LR_FHSS_CR_2_3, bool narrowGrid = true, int8_t power = 10, float tcxoVoltage = 1.6, bool useRegulatorLDO = false);
+    
     // configuration methods
 
     /*!
diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp
index 17ad0663..7109d6d9 100644
--- a/src/modules/SX126x/SX126x.cpp
+++ b/src/modules/SX126x/SX126x.cpp
@@ -119,6 +119,47 @@ int16_t SX126x::beginFSK(float br, float freqDev, float rxBw, uint16_t preambleL
   return(state);
 }
 
+int16_t SX126x::beginLRFHSS(uint8_t bw, uint8_t cr, bool narrowGrid, float tcxoVoltage, bool useRegulatorLDO) {
+  this->lrFhssGridNonFcc = narrowGrid;
+  
+  // set module properties and perform initial setup
+  int16_t state = this->modSetup(tcxoVoltage, useRegulatorLDO, RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS);
+  RADIOLIB_ASSERT(state);
+
+  // set publicly accessible settings that are not a part of begin method
+  state = setCurrentLimit(60.0);
+  RADIOLIB_ASSERT(state);
+
+  state = setDio2AsRfSwitch(true);
+  RADIOLIB_ASSERT(state);
+
+  // set all packet params to 0 (packet engine is disabled in LR-FHSS mode)
+  state = setPacketParamsFSK(0, 0, 0, 0, 0, 0, 0, 0);
+  RADIOLIB_ASSERT(state);
+
+  // set bit rate
+  this->rxBandwidth = 0;
+  this->frequencyDev = 0;
+  this->pulseShape = RADIOLIB_SX126X_GFSK_FILTER_GAUSS_1;
+  state = setBitRate(0.48828125f);
+  RADIOLIB_ASSERT(state);
+
+  return(setLrFhssConfig(bw, cr));
+}
+
+int16_t SX126x::setLrFhssConfig(uint8_t bw, uint8_t cr, uint8_t hdrCount, uint16_t hopSeqId) {
+  // check and cache all parameters
+  RADIOLIB_CHECK_RANGE((int8_t)cr, (int8_t)RADIOLIB_SX126X_LR_FHSS_CR_5_6, (int8_t)RADIOLIB_SX126X_LR_FHSS_CR_1_3, RADIOLIB_ERR_INVALID_CODING_RATE);
+  this->lrFhssCr = cr;
+  RADIOLIB_CHECK_RANGE((int8_t)bw, (int8_t)RADIOLIB_SX126X_LR_FHSS_BW_39_06, (int8_t)RADIOLIB_SX126X_LR_FHSS_BW_1574_2, RADIOLIB_ERR_INVALID_BANDWIDTH);
+  this->lrFhssBw = bw;
+  RADIOLIB_CHECK_RANGE(hdrCount, 1, 4, RADIOLIB_ERR_INVALID_BIT_RANGE);
+  this->lrFhssHdrCount = hdrCount;
+  RADIOLIB_CHECK_RANGE((int16_t)hopSeqId, (int16_t)0x000, (int16_t)0x1FF, RADIOLIB_ERR_INVALID_DATA_SHAPING);
+  this->lrFhssHopSeqId = hopSeqId;
+  return(RADIOLIB_ERR_NONE);
+}
+
 int16_t SX126x::reset(bool verify) {
   // run the reset sequence
   this->mod->hal->pinMode(this->mod->getRst(), this->mod->hal->GpioModeOutput);
@@ -171,13 +212,34 @@ int16_t SX126x::transmit(const uint8_t* data, size_t len, uint8_t addr) {
   RADIOLIB_ASSERT(state);
 
   // wait for packet transmission or timeout
+  uint8_t modem = getPacketType();
   RadioLibTime_t start = this->mod->hal->millis();
-  while(!this->mod->hal->digitalRead(this->mod->getIrq())) {
+  while(true) {
+    // yield for  multi-threaded platforms
     this->mod->hal->yield();
+
+    // check timeout
     if(this->mod->hal->millis() - start > timeout) {
       finishTransmit();
       return(RADIOLIB_ERR_TX_TIMEOUT);
     }
+
+    // poll the interrupt pin
+    if(this->mod->hal->digitalRead(this->mod->getIrq())) {
+      // in LoRa or GFSK, only Tx done interrupt is enabled
+      if(modem != RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS) {
+        break;
+      }
+
+      // in LR-FHSS, IRQ signals both Tx done as frequency hop request
+      if(this->getIrqFlags() & RADIOLIB_SX126X_IRQ_TX_DONE) {
+        break;
+      } else {
+        // handle frequency hop
+        this->setLRFHSSHop(this->lrFhssHopNum % 16);
+        clearIrqStatus();
+      }
+    }
   }
 
   // update data rate
@@ -467,13 +529,17 @@ int16_t SX126x::startTransmit(const uint8_t* data, size_t len, uint8_t addr) {
     state = setPacketParams(this->preambleLengthLoRa, this->crcTypeLoRa, len, this->headerType, this->invertIQEnabled);
   } else if(modem == RADIOLIB_SX126X_PACKET_TYPE_GFSK) {
     state = setPacketParamsFSK(this->preambleLengthFSK, this->crcTypeFSK, this->syncWordLength, this->addrComp, this->whitening, this->packetType, len);
-  } else {
+  } else if(modem != RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS) {
     return(RADIOLIB_ERR_UNKNOWN);
   }
   RADIOLIB_ASSERT(state);
 
   // set DIO mapping
-  state = setDioIrqParams(RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT, RADIOLIB_SX126X_IRQ_TX_DONE);
+  if(modem != RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS) {
+    state = setDioIrqParams(RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT, RADIOLIB_SX126X_IRQ_TX_DONE);
+  } else {
+    state = setDioIrqParams(RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_LR_FHSS_HOP, RADIOLIB_SX126X_IRQ_TX_DONE | RADIOLIB_SX126X_IRQ_LR_FHSS_HOP);
+  }
   RADIOLIB_ASSERT(state);
 
   // set buffer pointers
@@ -481,7 +547,49 @@ int16_t SX126x::startTransmit(const uint8_t* data, size_t len, uint8_t addr) {
   RADIOLIB_ASSERT(state);
 
   // write packet to buffer
+  if(modem != RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS) {
     state = writeBuffer(const_cast<uint8_t*>(data), len);
+  
+  } else {
+    // first, reset the LR-FHSS state machine
+    state = resetLRFHSS();
+    RADIOLIB_ASSERT(state);
+
+    // skip hopping for the first 4 - lrFhssHdrCount blocks
+    for(int i = 0; i < 4 - this->lrFhssHdrCount; ++i ) {
+      stepLRFHSS();
+    }
+
+    // in LR-FHSS mode, we need to build the entire packet manually
+    uint8_t frame[RADIOLIB_SX126X_MAX_PACKET_LENGTH] = { 0 };
+    size_t frameLen = 0;
+    this->lrFhssFrameBitsRem = 0;
+    this->lrFhssFrameHopsRem = 0;
+    this->lrFhssHopNum = 0;
+    state = buildLRFHSSPacket(const_cast<uint8_t*>(data), len, frame, &frameLen, &this->lrFhssFrameBitsRem, &this->lrFhssFrameHopsRem);
+    RADIOLIB_ASSERT(state);
+
+    // FIXME check max len for FHSS
+    state = writeBuffer(frame, frameLen);
+    RADIOLIB_ASSERT(state);
+
+    // activate hopping
+    uint8_t hopCfg[] = { RADIOLIB_SX126X_HOPPING_ENABLED, (uint8_t)frameLen, (uint8_t)this->lrFhssFrameHopsRem };
+    state = writeRegister(RADIOLIB_SX126X_REG_HOPPING_ENABLE, hopCfg, 3);
+    RADIOLIB_ASSERT(state);
+
+    // write the initial hopping table
+    uint8_t initHops = this->lrFhssFrameHopsRem;
+    if(initHops > 16) {
+      initHops = 16;
+    };
+    for(size_t i = 0; i < initHops; i++) {
+      // set the hop frequency and symbols
+      state = this->setLRFHSSHop(i);
+      RADIOLIB_ASSERT(state);
+    }
+  
+  }
   RADIOLIB_ASSERT(state);
 
   // clear interrupt flags
@@ -1025,7 +1133,8 @@ int16_t SX126x::setRxBoostedGainMode(bool rxbgm, bool persist) {
 
 int16_t SX126x::setDataShaping(uint8_t sh) {
   // check active modem
-  if(getPacketType() != RADIOLIB_SX126X_PACKET_TYPE_GFSK) {
+  uint8_t modem = getPacketType();
+  if((modem != RADIOLIB_SX126X_PACKET_TYPE_GFSK) && (modem != RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS)) {
     return(RADIOLIB_ERR_WRONG_MODEM);
   }
 
@@ -1079,6 +1188,14 @@ int16_t SX126x::setSyncWord(uint8_t* syncWord, size_t len) {
       return(RADIOLIB_ERR_INVALID_SYNC_WORD);
     }
     return(setSyncWord(syncWord[0]));
+
+  } else if(modem == RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS) {
+    // with length set to 4 and LR-FHSS modem active, assume it is the LR-FHSS sync word
+    if(len != sizeof(uint32_t)) {
+      return(RADIOLIB_ERR_INVALID_SYNC_WORD);
+    }
+    memcpy(this->lrFhssSyncWord, syncWord, sizeof(uint32_t));
+  
   }
 
   return(RADIOLIB_ERR_WRONG_MODEM);
@@ -1346,7 +1463,8 @@ int16_t SX126x::variablePacketLengthMode(uint8_t maxLen) {
 RadioLibTime_t SX126x::getTimeOnAir(size_t len) {
   // everything is in microseconds to allow integer arithmetic
   // some constants have .25, these are multiplied by 4, and have _x4 postfix to indicate that fact
-  if(getPacketType() == RADIOLIB_SX126X_PACKET_TYPE_LORA) {
+  uint8_t modem = getPacketType();
+  if(modem == RADIOLIB_SX126X_PACKET_TYPE_LORA) {
     uint32_t symbolLength_us = ((uint32_t)(1000 * 10) << this->spreadingFactor) / (this->bandwidthKhz * 10) ;
     uint8_t sfCoeff1_x4 = 17; // (4.25 * 4)
     uint8_t sfCoeff2 = 8;
@@ -1373,9 +1491,44 @@ RadioLibTime_t SX126x::getTimeOnAir(size_t len) {
     uint32_t nSymbol_x4 = (this->preambleLengthLoRa + 8) * 4 + sfCoeff1_x4 + nPreCodedSymbols * (this->codingRate + 4) * 4;
 
     return((symbolLength_us * nSymbol_x4) / 4);
-  } else {
+ 
+  } else if(modem == RADIOLIB_SX126X_PACKET_TYPE_GFSK) {
     return(((uint32_t)len * 8 * this->bitRate) / (RADIOLIB_SX126X_CRYSTAL_FREQ * 32));
+  
+  } else if(modem == RADIOLIB_SX126X_PACKET_TYPE_LR_FHSS) {
+    // calculate the number of bits based on coding rate
+    uint16_t N_bits;
+    switch(this->lrFhssCr) {
+      case RADIOLIB_SX126X_LR_FHSS_CR_5_6:
+        N_bits = ((len * 6) + 4) / 5; // this is from the official LR11xx driver, but why the extra +4?
+        break;
+      case RADIOLIB_SX126X_LR_FHSS_CR_2_3:
+        N_bits = (len * 3) / 2;
+        break;
+      case RADIOLIB_SX126X_LR_FHSS_CR_1_2:
+        N_bits = len * 2;
+        break;
+      case RADIOLIB_SX126X_LR_FHSS_CR_1_3:
+        N_bits = len * 3;
+        break;
+      default:
+        return(RADIOLIB_ERR_INVALID_CODING_RATE);
+    }
+
+    // calculate number of bits when accounting for unaligned last block
+    uint16_t N_payBits = (N_bits / RADIOLIB_SX126X_LR_FHSS_FRAG_BITS) * RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS;
+    uint16_t N_lastBlockBits = N_bits % RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
+    if(N_lastBlockBits) {
+      N_payBits += N_lastBlockBits + 2;
+    }
+
+    // add header bits
+    uint16_t N_totalBits = (RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount) + N_payBits;
+    return(((uint32_t)N_totalBits * 8 * 1000000UL) / 488.28215f);
+  
   }
+
+  return(RADIOLIB_ERR_UNKNOWN);
 }
 
 RadioLibTime_t SX126x::calculateRxTimeout(RadioLibTime_t timeoutUs) {
@@ -1908,8 +2061,8 @@ int16_t SX126x::clearDeviceErrors() {
 
 int16_t SX126x::setFrequencyRaw(float freq) {
   // calculate raw value
-  uint32_t frf = (freq * (uint32_t(1) << RADIOLIB_SX126X_DIV_EXPONENT)) / RADIOLIB_SX126X_CRYSTAL_FREQ;
-  return(setRfFrequency(frf));
+  this->frf = (freq * (uint32_t(1) << RADIOLIB_SX126X_DIV_EXPONENT)) / RADIOLIB_SX126X_CRYSTAL_FREQ;
+  return(setRfFrequency(this->frf));
 }
 
 int16_t SX126x::fixSensitivity() {
diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h
index 0c7d17b7..a64356aa 100644
--- a/src/modules/SX126x/SX126x.h
+++ b/src/modules/SX126x/SX126x.h
@@ -8,6 +8,8 @@
 #include "../../Module.h"
 
 #include "../../protocols/PhysicalLayer/PhysicalLayer.h"
+#include "../../utils/FEC.h"
+#include "../../utils/CRC.h"
 
 // SX126X physical layer properties
 #define RADIOLIB_SX126X_FREQUENCY_STEP_SIZE                     0.9536743164
@@ -434,6 +436,36 @@
 // size of the spectral scan result
 #define RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE                  (33)
 
+// LR-FHSS configuration
+#define RADIOLIB_SX126X_LR_FHSS_CR_5_6                          (0x00UL << 0)   //  7     0     LR FHSS coding rate: 5/6
+#define RADIOLIB_SX126X_LR_FHSS_CR_2_3                          (0x01UL << 0)   //  7     0                          2/3
+#define RADIOLIB_SX126X_LR_FHSS_CR_1_2                          (0x02UL << 0)   //  7     0                          1/2
+#define RADIOLIB_SX126X_LR_FHSS_CR_1_3                          (0x03UL << 0)   //  7     0                          1/3
+#define RADIOLIB_SX126X_LR_FHSS_MOD_TYPE_GMSK                   (0x00UL << 0)   //  7     0     LR FHSS modulation: GMSK
+#define RADIOLIB_SX126X_LR_FHSS_GRID_STEP_FCC                   (0x00UL << 0)   //  7     0     LR FHSS step size: 25.390625 kHz (FCC)
+#define RADIOLIB_SX126X_LR_FHSS_GRID_STEP_NON_FCC               (0x01UL << 0)   //  7     0                        3.90625 kHz (non-FCC)
+#define RADIOLIB_SX126X_LR_FHSS_HOPPING_DISABLED                (0x00UL << 0)   //  7     0     LR FHSS hopping: disabled
+#define RADIOLIB_SX126X_LR_FHSS_HOPPING_ENABLED                 (0x01UL << 0)   //  7     0                      enabled
+#define RADIOLIB_SX126X_LR_FHSS_BW_39_06                        (0x00UL << 0)   //  7     0     LR FHSS bandwidth: 39.06 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_85_94                        (0x01UL << 0)   //  7     0                        85.94 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_136_72                       (0x02UL << 0)   //  7     0                        136.72 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_183_59                       (0x03UL << 0)   //  7     0                        183.59 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_335_94                       (0x04UL << 0)   //  7     0                        335.94 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_386_72                       (0x05UL << 0)   //  7     0                        386.72 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_722_66                       (0x06UL << 0)   //  7     0                        722.66 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_773_44                       (0x07UL << 0)   //  7     0                        773.44 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_1523_4                       (0x08UL << 0)   //  7     0                        1523.4 kHz
+#define RADIOLIB_SX126X_LR_FHSS_BW_1574_2                       (0x09UL << 0)   //  7     0                        1574.2 kHz
+
+// LR-FHSS packet lengths
+#define RADIOLIB_SX126X_LR_FHSS_MAX_ENC_SIZE                    (608)
+#define RADIOLIB_SX126X_LR_FHSS_HEADER_BITS                     (114)
+#define RADIOLIB_SX126X_LR_FHSS_HDR_BYTES                       (10)
+#define RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES                 (4)
+#define RADIOLIB_SX126X_LR_FHSS_FRAG_BITS                       (48)
+#define RADIOLIB_SX126X_LR_FHSS_BLOCK_PREAMBLE_BITS             (2)
+#define RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS                      (RADIOLIB_SX126X_LR_FHSS_FRAG_BITS + RADIOLIB_SX126X_LR_FHSS_BLOCK_PREAMBLE_BITS)
+
 /*!
   \class SX126x
   \brief Base class for %SX126x series. All derived classes for %SX126x (e.g. SX1262 or SX1268) inherit from this base class.
@@ -489,6 +521,27 @@ class SX126x: public PhysicalLayer {
     */
     int16_t beginFSK(float br, float freqDev, float rxBw, uint16_t preambleLength, float tcxoVoltage, bool useRegulatorLDO = false);
 
+    /*!
+      \brief Initialization method for LR-FHSS modem. This modem only supports transmission!
+      \param bw LR-FHSS bandwidth, one of RADIOLIB_SX126X_LR_FHSS_BW_* values.
+      \param cr LR-FHSS coding rate, one of RADIOLIB_SX126X_LR_FHSS_CR_* values.
+      \param narrowGrid Whether to use narrow (3.9 kHz) or wide (25.39 kHz) grid spacing.
+      \param tcxoVoltage TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip.
+      \param useRegulatorLDO Whether to use only LDO regulator (true) or DC-DC regulator (false). Defaults to false.
+      \returns \ref status_codes
+    */
+    int16_t beginLRFHSS(uint8_t bw, uint8_t cr, bool narrowGrid, float tcxoVoltage, bool useRegulatorLDO = false);
+
+    /*!
+      \brief Sets LR-FHSS configuration.
+      \param bw LR-FHSS bandwidth, one of RADIOLIB_SX126X_LR_FHSS_BW_* values.
+      \param cr LR-FHSS coding rate, one of RADIOLIB_SX126X_LR_FHSS_CR_* values.
+      \param hdrCount Header packet count, 1 - 4. Defaults to 3.
+      \param hopSeqId 9-bit seed number for PRNG generation of the hopping sequence. Defaults to 0x13A.
+      \returns \ref status_codes
+    */
+    int16_t setLrFhssConfig(uint8_t bw, uint8_t cr, uint8_t hdrCount = 3, uint16_t hopSeqId = 0x100);
+
     /*!
       \brief Reset method. Will reset the chip to the default state using RST pin.
       \param verify Whether correct module startup should be verified. When set to true, RadioLib will attempt to verify the module has started correctly
@@ -830,6 +883,7 @@ class SX126x: public PhysicalLayer {
 
     /*!
       \brief Sets FSK sync word in the form of array of up to 8 bytes.
+      Can also set LR-FHSS sync word, but its length must be 4 bytes.
       \param syncWord FSK sync word to be set.
       \param len FSK sync word length in bytes.
       \returns \ref status_codes
@@ -1214,10 +1268,26 @@ class SX126x: public PhysicalLayer {
 
     uint32_t tcxoDelay = 0;
     uint8_t pwr = 0;
+    uint32_t frf = 0;
 
     size_t implicitLen = 0;
     uint8_t invertIQEnabled = RADIOLIB_SX126X_LORA_IQ_STANDARD;
 
+    // LR-FHSS stuff - there's a lot of it because all the encoding happens in software
+    uint8_t lrFhssCr = RADIOLIB_SX126X_LR_FHSS_CR_2_3;
+    uint8_t lrFhssBw = RADIOLIB_SX126X_LR_FHSS_BW_722_66;
+    uint8_t lrFhssHdrCount = 3;
+    uint8_t lrFhssSyncWord[RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES] = { 0x12, 0xAD, 0x10, 0x1B };
+    bool lrFhssGridNonFcc = false;
+    uint16_t lrFhssNgrid = 0;
+    uint16_t lrFhssLfsrState = 0;
+    uint16_t lrFhssPoly = 0;
+    uint16_t lrFhssSeed = 0;
+    uint16_t lrFhssHopSeqId = 0;
+    size_t lrFhssFrameBitsRem = 0;
+    size_t lrFhssFrameHopsRem = 0;
+    size_t lrFhssHopNum = 0;
+
     int16_t modSetup(float tcxoVoltage, bool useRegulatorLDO, uint8_t modem);
     int16_t config(uint8_t modem);
     bool findChip(const char* verStr);
@@ -1232,6 +1302,11 @@ class SX126x: public PhysicalLayer {
     int16_t fixImplicitTimeout();
     int16_t fixInvertedIQ(uint8_t iqConfig);
 
+    // LR-FHSS utilities
+    int16_t buildLRFHSSPacket(const uint8_t* in, size_t in_len, uint8_t* out, size_t* out_len, size_t* out_bits, size_t* out_hops);
+    int16_t resetLRFHSS();
+    uint16_t stepLRFHSS();
+    int16_t setLRFHSSHop(uint8_t index);
 
     void regdump();
     void effectEvalPre(uint8_t* buff, uint32_t start);
diff --git a/src/modules/SX126x/SX126x_LR_FHSS.cpp b/src/modules/SX126x/SX126x_LR_FHSS.cpp
new file mode 100644
index 00000000..f7cae378
--- /dev/null
+++ b/src/modules/SX126x/SX126x_LR_FHSS.cpp
@@ -0,0 +1,392 @@
+#include "SX126x.h"
+#include <string.h>
+#include <math.h>
+#if !RADIOLIB_EXCLUDE_SX126X
+
+/*
+  LR-FHSS implementation in this file is adapted from Setmech's LR-FHSS demo:
+  https://github.com/Lora-net/SWDM001/tree/master/lib/sx126x_driver
+
+  Its SX126x driver is distributed under the Clear BSD License,
+  and to comply with its terms, it is reproduced below.
+
+  The Clear BSD License
+  Copyright Semtech Corporation 2021. All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted (subject to the limitations in the disclaimer
+  below) provided that the following conditions are met:
+      * Redistributions of source code must retain the above copyright
+        notice, this list of conditions and the following disclaimer.
+      * Redistributions in binary form must reproduce the above copyright
+        notice, this list of conditions and the following disclaimer in the
+        documentation and/or other materials provided with the distribution.
+      * Neither the name of the Semtech corporation nor the
+        names of its contributors may be used to endorse or promote products
+        derived from this software without specific prior written permission.
+
+  NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
+  THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+  NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+// header interleaver
+static const uint8_t LrFhssHeaderInterleaver[80] = {
+  0,  18, 36, 54, 72, 4,  22, 40,
+  58, 76, 8,  26, 44, 62, 12, 30,
+  48, 66, 16, 34, 52, 70, 1,  19,
+  37, 55, 73, 5,  23, 41, 59, 77,
+  9,  27, 45, 63, 13, 31, 49, 67,
+  17, 35, 53, 71, 2,  20, 38, 56,
+  74, 6,  24, 42, 60, 78, 10, 28,
+  46, 64, 14, 32, 50, 68, 3,  21,
+  39, 57, 75, 7,  25, 43, 61, 79,
+  11, 29, 47, 65, 15, 33, 51, 69,
+};
+
+int16_t SX126x::buildLRFHSSPacket(const uint8_t* in, size_t in_len, uint8_t* out, size_t* out_len, size_t* out_bits, size_t* out_hops) {
+  // perform payload whitening
+  uint8_t lfsr = 0xFF;
+  for(size_t i = 0; i < in_len; i++) {
+    uint8_t u = in[i] ^ lfsr;
+
+    // we really shouldn't reuse the caller's memory in this way ...
+    // but since this is a private method it should be at least controlled, if not safe
+    out[i] = ((u & 0x0F) << 4 ) | ((u & 0xF0) >> 4);
+    lfsr = (lfsr << 1) | (((lfsr & 0x80) >> 7) ^ (((lfsr & 0x20) >> 5) ^ (((lfsr & 0x10) >> 4) ^ ((lfsr & 0x08) >> 3))));
+  }
+
+  // calculate the CRC-16 over the whitened data, looks like something custom
+  RadioLibCRCInstance.size = 16;
+  RadioLibCRCInstance.poly = 0x755B;
+  RadioLibCRCInstance.init = 0xFFFF;
+  RadioLibCRCInstance.out = 0x0000;
+  uint16_t crc16 = RadioLibCRCInstance.checksum(out, in_len);
+
+  // add payload CRC
+  out[in_len] = (crc16 >> 8) & 0xFF;
+  out[in_len + 1] = crc16 & 0xFF;
+  out[in_len + 2] = 0;
+
+  // encode the payload with CRC using convolutional coding with 1/3 rate into a temporary buffer
+  uint8_t tmp[RADIOLIB_SX126X_LR_FHSS_MAX_ENC_SIZE] = { 0 };
+  size_t nb_bits = 0;
+  RadioLibConvCodeInstance.begin(3);
+  RadioLibConvCodeInstance.encode(out, 8 * (in_len + 2) + 6, tmp, &nb_bits);
+  memset(out, 0, RADIOLIB_SX126X_MAX_PACKET_LENGTH);
+
+  // for rates other than the 1/3 base, puncture the code
+  if(this->lrFhssCr != RADIOLIB_SX126X_LR_FHSS_CR_1_3) {
+    uint32_t matrix_index = 0;
+    uint8_t  matrix[15]   = { 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0 };
+    uint8_t  matrix_len   = 0;
+    switch(this->lrFhssCr) {
+      case RADIOLIB_SX126X_LR_FHSS_CR_5_6:
+        matrix_len = 15;
+        break;
+      case RADIOLIB_SX126X_LR_FHSS_CR_2_3:
+        matrix_len = 6;
+        break;
+      case RADIOLIB_SX126X_LR_FHSS_CR_1_2:
+        matrix_len = 3;
+        break;
+    }
+
+    uint32_t j = 0;
+    for(uint32_t i = 0; i < nb_bits; i++) {
+      if(matrix[matrix_index]) {
+        if(TEST_BIT_IN_ARRAY_LSB(tmp, i)) {
+          SET_BIT_IN_ARRAY_LSB(out, j);
+        } else {
+          CLEAR_BIT_IN_ARRAY_LSB(out, j);
+        }
+        j++;
+      }
+
+      if(++matrix_index == matrix_len) {
+        matrix_index = 0;
+      }
+    }
+
+    nb_bits = j;
+    memcpy(tmp, out, (nb_bits + 7) / 8);
+  }
+
+  // interleave the payload into output buffer
+  uint16_t step = 0;
+  while(step * step < nb_bits) {
+    // probably the silliest sqrt() I ever saw
+    step++;
+  }
+
+  const uint16_t step_v = step >> 1;
+  step <<= 1;
+
+  uint16_t pos           = 0;
+  uint16_t st_idx        = 0;
+  uint16_t st_idx_init   = 0;
+  int16_t  bits_left     = nb_bits;
+  uint16_t out_row_index = RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount;
+
+  while(bits_left > 0) {
+    int16_t in_row_width = bits_left;
+    if(in_row_width > RADIOLIB_SX126X_LR_FHSS_FRAG_BITS) {
+      in_row_width = RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
+    }
+
+    // guard bits
+    CLEAR_BIT_IN_ARRAY_LSB(out, out_row_index);
+    CLEAR_BIT_IN_ARRAY_LSB(out, out_row_index + 1);
+        
+    for(int16_t j = 0; j < in_row_width; j++) {
+      // guard bit
+      if(TEST_BIT_IN_ARRAY_LSB(tmp, pos)) {
+        SET_BIT_IN_ARRAY_LSB(out, j + 2 + out_row_index);
+      } else {
+        CLEAR_BIT_IN_ARRAY_LSB(out, j + 2 + out_row_index);
+      }
+
+      pos += step;
+      if(pos >= nb_bits) {
+        st_idx += step_v;
+        if(st_idx >= step) {
+          st_idx_init++;
+          st_idx = st_idx_init;
+        }
+        pos = st_idx;
+      }
+    }
+
+    bits_left -= RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
+    out_row_index += 2 + in_row_width;
+  }
+
+  nb_bits = out_row_index - RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount;
+
+  // build the header
+  uint8_t raw_header[RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2];
+  raw_header[0] = in_len;
+  raw_header[1] = (this->lrFhssCr << 3) | ((uint8_t)this->lrFhssGridNonFcc << 2) |
+    (RADIOLIB_SX126X_LR_FHSS_HOPPING_ENABLED << 1) | (this->lrFhssBw >> 3);
+  raw_header[2] = ((this->lrFhssBw & 0x07) << 5) | (this->lrFhssHopSeqId >> 4);
+  raw_header[3] = ((this->lrFhssHopSeqId & 0x000F) << 4);
+
+  // CRC-8 used seems to based on 8H2F, but without final XOR
+  RadioLibCRCInstance.size = 8;
+  RadioLibCRCInstance.poly = 0x2F;
+  RadioLibCRCInstance.init = 0xFF;
+  RadioLibCRCInstance.out = 0x00;
+
+  uint16_t header_offset = 0;
+  for(size_t i = 0; i < this->lrFhssHdrCount; i++) {
+    // insert index and calculate the header CRC
+    raw_header[3] = (raw_header[3] & ~0x0C) | ((this->lrFhssHdrCount - i - 1) << 2);
+    raw_header[4] = RadioLibCRCInstance.checksum(raw_header, (RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2 - 1));
+
+    // convolutional encode
+    uint8_t coded_header[RADIOLIB_SX126X_LR_FHSS_HDR_BYTES] = { 0 };
+    RadioLibConvCodeInstance.begin(2);
+    RadioLibConvCodeInstance.encode(raw_header, 8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2, coded_header);
+    // tail-biting seems to just do this twice ...?
+    RadioLibConvCodeInstance.encode(raw_header, 8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2, coded_header);
+
+    // clear guard bits
+    CLEAR_BIT_IN_ARRAY_LSB(out, header_offset);
+    CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 1);
+
+    // interleave the header directly to the physical payload buffer
+    for(size_t j = 0; j < (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2); j++) {
+      if(TEST_BIT_IN_ARRAY_LSB(coded_header, LrFhssHeaderInterleaver[j])) {
+        SET_BIT_IN_ARRAY_LSB(out, header_offset + 2 + j);
+      } else {
+        CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 2 + j);
+      }
+    }
+    for(size_t j = 0; j < (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2); j++) {
+      if(TEST_BIT_IN_ARRAY_LSB(coded_header, LrFhssHeaderInterleaver[(8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + j])) {
+        SET_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + (8*RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES) + j);
+      } else {
+        CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + (8*RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES) + j);
+      }
+    }
+
+    // copy the sync word to the physical payload buffer
+    for(size_t j = 0; j < (8*RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES); j++) {
+      if(TEST_BIT_IN_ARRAY_LSB(this->lrFhssSyncWord, j)) {
+        SET_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + j);
+      } else {
+        CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + j);
+      }
+    }
+
+    header_offset += RADIOLIB_SX126X_LR_FHSS_HEADER_BITS;
+  }
+
+  // calculate the number of hops and total number of bits
+  uint16_t length_bits = (in_len + 2) * 8 + 6;
+  switch(this->lrFhssCr) {
+    case RADIOLIB_SX126X_LR_FHSS_CR_5_6:
+      length_bits = ( ( length_bits * 6 ) + 4 ) / 5;
+      break;
+    case RADIOLIB_SX126X_LR_FHSS_CR_2_3:
+      length_bits = length_bits * 3 / 2;
+      break;
+    case RADIOLIB_SX126X_LR_FHSS_CR_1_2:
+      length_bits = length_bits * 2;
+      break;
+    case RADIOLIB_SX126X_LR_FHSS_CR_1_3:
+      length_bits = length_bits * 3;
+      break;
+  }
+
+  *out_hops = (length_bits + 47) / 48 + this->lrFhssHdrCount;
+
+  // calculate total number of payload bits, after breaking into blocks
+  uint16_t payload_bits = length_bits / RADIOLIB_SX126X_LR_FHSS_FRAG_BITS * RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS;
+  uint16_t last_block_bits = length_bits % RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
+  if(last_block_bits > 0) {
+    // add the 2 guard bits for the last block + the actual remaining payload bits
+    payload_bits += last_block_bits + 2;
+  }
+
+  *out_bits = (RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount) + payload_bits;
+  *out_len = (*out_bits + 7) / 8;
+
+  return(RADIOLIB_ERR_NONE);
+}
+
+int16_t SX126x::resetLRFHSS() {
+  // initialize hopping configuration
+  const uint16_t numChan[] = { 80, 176, 280, 376, 688, 792, 1480, 1584, 3120, 3224 };
+  
+  // LFSR polynomials for different ranges of lrFhssNgrid
+  const uint8_t lfsrPoly1[] = { 33, 45, 48, 51, 54, 57 };
+  const uint8_t lfsrPoly2[] = { 65, 68, 71, 72 };
+  const uint8_t lfsrPoly3[] = { 142, 149 };
+
+  uint32_t nb_channel_in_grid = this->lrFhssGridNonFcc ? 8 : 52;
+  this->lrFhssNgrid = numChan[this->lrFhssBw] / nb_channel_in_grid;
+  this->lrFhssLfsrState = 6;
+  switch(this->lrFhssNgrid) {
+    case 10:
+    case 22:
+    case 28:
+    case 30:
+    case 35:
+    case 47:
+      this->lrFhssPoly = lfsrPoly1[this->lrFhssHopSeqId >> 6];
+      this->lrFhssSeed = this->lrFhssHopSeqId & 0x3F;
+      if(this->lrFhssHopSeqId >= 384) {
+        return(RADIOLIB_ERR_INVALID_DATA_SHAPING);
+      }
+      break;
+    
+    case 60:
+    case 62:
+      this->lrFhssLfsrState = 56;
+      this->lrFhssPoly = lfsrPoly1[this->lrFhssHopSeqId >> 6];
+      this->lrFhssSeed = this->lrFhssHopSeqId & 0x3F;
+      if(this->lrFhssHopSeqId >= 384) {
+        return(RADIOLIB_ERR_INVALID_DATA_SHAPING);
+      }
+      break;
+    
+    case 86:
+    case 99:
+      this->lrFhssPoly = lfsrPoly2[this->lrFhssHopSeqId >> 7];
+      this->lrFhssSeed = this->lrFhssHopSeqId & 0x7F;
+      break;
+    
+    case 185:
+    case 198:
+      this->lrFhssPoly = lfsrPoly3[this->lrFhssHopSeqId >> 8];
+      this->lrFhssSeed = this->lrFhssHopSeqId & 0xFF;
+      break;
+    
+    case 390:
+    case 403:
+      this->lrFhssPoly = 264;
+      this->lrFhssSeed = this->lrFhssHopSeqId;
+      break;
+    
+    default:
+      return(RADIOLIB_ERR_INVALID_DATA_SHAPING);
+  }
+
+  return(RADIOLIB_ERR_NONE);
+}
+
+uint16_t SX126x::stepLRFHSS() {
+  uint16_t hop;
+  do {
+    uint16_t lsb = this->lrFhssLfsrState & 1;
+    this->lrFhssLfsrState >>= 1;
+    if(lsb) {
+     this->lrFhssLfsrState ^= this->lrFhssPoly;
+    }
+    hop = this->lrFhssSeed;
+    if(hop != this->lrFhssLfsrState) {
+      hop ^= this->lrFhssLfsrState;
+    }
+  } while(hop > this->lrFhssNgrid);
+  return(hop);
+}
+
+int16_t SX126x::setLRFHSSHop(uint8_t index) {
+  if(!this->lrFhssFrameHopsRem) {
+    return(RADIOLIB_ERR_NONE);
+  }
+
+  uint16_t hop = stepLRFHSS();
+  int16_t freq_table = hop - 1;
+  if(freq_table >= (int16_t)(this->lrFhssNgrid >> 1)) {
+    freq_table -= this->lrFhssNgrid;
+  }
+
+  uint32_t nb_channel_in_grid = this->lrFhssGridNonFcc ? 8 : 52;
+  uint32_t grid_offset = (1 + (this->lrFhssNgrid % 2)) * (nb_channel_in_grid / 2);
+  uint32_t grid_in_pll_steps = this->lrFhssGridNonFcc ? 4096 : 26624;
+  uint32_t freq_raw = this->frf - freq_table * grid_in_pll_steps - grid_offset * 512;
+
+  if((this->lrFhssHopNum < this->lrFhssHdrCount)) {
+    if((((this->lrFhssHdrCount - this->lrFhssHopNum) % 2) == 0)) {
+      freq_raw += 256;
+    }
+  }
+
+  uint8_t frq[4] = { (uint8_t)((freq_raw >> 24) & 0xFF), (uint8_t)((freq_raw >> 16) & 0xFF), (uint8_t)((freq_raw >> 8) & 0xFF), (uint8_t)(freq_raw & 0xFF) };
+  int16_t state = writeRegister(RADIOLIB_SX126X_REG_LR_FHSS_FREQX_0(index), frq, sizeof(freq_raw));
+  RADIOLIB_ASSERT(state);
+
+  // (LR_FHSS_HEADER_BITS + pulse_shape_compensation) symbols on first sync_word, LR_FHSS_HEADER_BITS on
+  // next sync_words, LR_FHSS_BLOCK_BITS on payload
+  uint16_t numSymbols = RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS;
+  if(index == 0) {
+    numSymbols = RADIOLIB_SX126X_LR_FHSS_HEADER_BITS + 1; // the +1 is "pulse_shape_compensation", but it's constant in the demo
+  } else if(index < this->lrFhssHdrCount) {
+    numSymbols = RADIOLIB_SX126X_LR_FHSS_HEADER_BITS;
+  } else if(this->lrFhssFrameBitsRem < RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS) {
+    numSymbols = this->lrFhssFrameBitsRem;
+  }
+
+  // write hop length in symbols
+  uint8_t sym[2] = { (uint8_t)((numSymbols >> 8) & 0xFF), (uint8_t)(numSymbols & 0xFF) };
+  state = writeRegister(RADIOLIB_SX126X_REG_LR_FHSS_NUM_SYMBOLS_FREQX_MSB(index), sym, sizeof(uint16_t));
+  RADIOLIB_ASSERT(state);
+
+  this->lrFhssFrameBitsRem -= numSymbols;
+  this->lrFhssFrameHopsRem--;
+  this->lrFhssHopNum++;
+  return(RADIOLIB_ERR_NONE);
+}
+
+#endif
diff --git a/src/utils/FEC.cpp b/src/utils/FEC.cpp
index 515014d0..7aadf4ab 100644
--- a/src/utils/FEC.cpp
+++ b/src/utils/FEC.cpp
@@ -307,3 +307,59 @@ uint32_t RadioLibBCH::encode(uint32_t dataword) {
 }
 
 RadioLibBCH RadioLibBCHInstance;
+
+RadioLibConvCode::RadioLibConvCode() {
+
+}
+
+void RadioLibConvCode::begin(uint8_t rt) {
+  this->enc_state = 0;
+  this->rate = rt;
+}
+
+int16_t RadioLibConvCode::encode(const uint8_t* in, size_t in_bits, uint8_t* out, size_t* out_bits) {
+  if(!in || !out) {
+    return(RADIOLIB_ERR_UNKNOWN);
+  }
+
+  size_t ind_bit;
+  uint16_t data_out_bitcount = 0;
+  uint32_t bin_out_word = 0;
+
+  // iterate over the provided bits
+  for(ind_bit = 0; ind_bit < in_bits; ind_bit++) {
+    uint8_t cur_bit = GET_BIT_IN_ARRAY_LSB(in, ind_bit);
+    const uint32_t* lut_ptr = (this->rate == 2) ? ConvCodeTable1_2 : ConvCodeTable1_3;
+    uint8_t word_pos = this->enc_state / 4;
+    uint8_t byte_pos = (3 - (this->enc_state % 4)) * 8;
+    uint8_t nibble_pos = (1 - cur_bit) * 4;
+    uint8_t g1g0 = (lut_ptr[word_pos] >> (byte_pos + nibble_pos)) & 0x0F;
+
+    uint8_t mod = this->rate == 2 ? 16 : 64;
+    this->enc_state = (this->enc_state * 2 + cur_bit) % mod;
+    bin_out_word |= (g1g0 << ((7 - (ind_bit % 8)) * this->rate));
+    if(ind_bit % 8 == 7) {
+      if(this->rate == 3) {
+        *out++ = (uint8_t)(bin_out_word >> 16);
+      }
+      *out++ = (uint8_t)(bin_out_word >> 8);
+      *out++ = (uint8_t)bin_out_word;
+      bin_out_word  = 0;
+    }
+    data_out_bitcount += this->rate;
+  }
+
+  if(ind_bit % 8) {
+    if(this->rate == 3) {
+      *out++ = (uint8_t)(bin_out_word >> 16);
+    }
+    *out++ = (uint8_t)(bin_out_word >> 8);
+    *out++ = (uint8_t)bin_out_word;
+  }
+
+  if(out_bits) { *out_bits = data_out_bitcount; }
+
+  return(RADIOLIB_ERR_NONE);
+}
+
+RadioLibConvCode RadioLibConvCodeInstance;
diff --git a/src/utils/FEC.h b/src/utils/FEC.h
index cf201b11..788b0b52 100644
--- a/src/utils/FEC.h
+++ b/src/utils/FEC.h
@@ -31,7 +31,7 @@ class RadioLibBCH {
     RadioLibBCH();
 
     /*!
-      \brief Default detructor.
+      \brief Default destructor.
     */
     ~RadioLibBCH();
 
@@ -76,6 +76,92 @@ extern RadioLibBCH RadioLibBCHInstance;
 #define CLEAR_BIT_IN_ARRAY_MSB(A, k)                            ( A[((k)/8)] &= ~(1 << ((k)%8)) )
 #define TEST_BIT_IN_ARRAY_MSB(A, k)                             ( A[((k)/8)] & (1 << ((k)%8)) )
 #define GET_BIT_IN_ARRAY_MSB(A, k)                              ( (A[((k)/8)] & (1 << ((k)%8))) ? 1 : 0 )
+#define SET_BIT_IN_ARRAY_LSB(A, k)                              ( A[((k)/8)] |= (1 << (7 - ((k)%8))) )
+#define CLEAR_BIT_IN_ARRAY_LSB(A, k)                            ( A[((k)/8)] &= ~(1 << (7 - ((k)%8))) )
+#define TEST_BIT_IN_ARRAY_LSB(A, k)                             ( A[((k)/8)] & (1 << (7 - ((k)%8))) )
+#define GET_BIT_IN_ARRAY_LSB(A, k)                              ( (A[((k)/8)] & (1 << (7 - ((k)%8)))) ? 1 : 0 )
 
+/*!
+  \class RadioLibConvCode
+  \brief Class to perform convolutional coding wtih variable rates.
+  Only 1/2 and 1/3 rate is currently supported.
+
+  Copnvolutional coder implementation in this class is adapted from Setmech's LR-FHSS demo:
+  https://github.com/Lora-net/SWDM001/tree/master/lib/sx126x_driver
+
+  Its SX126x driver is distributed under the Clear BSD License,
+  and to comply with its terms, it is reproduced below.
+
+  The Clear BSD License
+  Copyright Semtech Corporation 2021. All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted (subject to the limitations in the disclaimer
+  below) provided that the following conditions are met:
+      * Redistributions of source code must retain the above copyright
+        notice, this list of conditions and the following disclaimer.
+      * Redistributions in binary form must reproduce the above copyright
+        notice, this list of conditions and the following disclaimer in the
+        documentation and/or other materials provided with the distribution.
+      * Neither the name of the Semtech corporation nor the
+        names of its contributors may be used to endorse or promote products
+        derived from this software without specific prior written permission.
+
+  NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
+  THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+  NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+class RadioLibConvCode {
+  public:
+    /*!
+      \brief Default constructor.
+    */
+    RadioLibConvCode();
+
+    /*!
+      \brief Initialization method.
+      \param rt Encoding rate denominator (1/x). Only 1/2 and 1/3 encoding is currently supported.
+    */
+    void begin(uint8_t rt);
+
+    /*!
+      \brief Encoding method.
+      \param in Input buffer (a byte array).
+      \param in_bits Input length in bits.
+      \param out Output buffer (a byte array). It is up to the caller
+      to ensure the buffer is large enough to fit the encoded data!
+      \param out_bits Pointer to a variable to save the number of encoded bits.
+      Ignored if set to NULL.
+      \returns \ref status_codes 
+    */
+    int16_t encode(const uint8_t* in, size_t in_bits, uint8_t* out, size_t* out_bits = NULL);
+
+  private:
+    uint8_t enc_state = 0;
+    uint8_t rate = 0;
+};
+
+// each 32-bit word stores 8 values, one per each nibble
+static const uint32_t ConvCodeTable1_3[16] = {
+  0x07347043, 0x61521625, 0x16256152, 0x70430734,
+  0x43703407, 0x25165261, 0x52612516, 0x34074370,
+  0x70430734, 0x16256152, 0x61521625, 0x07347043,
+  0x34074370, 0x52612516, 0x25165261, 0x43703407,
+};
+
+static const uint32_t ConvCodeTable1_2[4] = { 
+  0x03122130, 0x21300312, 0x30211203, 0x12033021,
+};
+
+extern RadioLibConvCode RadioLibConvCodeInstance;
 
 #endif