sx1276: fix loramac-node direction fast-path, resetAGC, add BW 62.5kHz

Three correctness fixes for the SX127x radio adapter on top of
Rastislav's initial sx1276 + patch commits:

1. _loramac_node flag (LoRaRadioBase + SX127xRadio):
   Guard the direction-only fast path in configureTx()/configureRx()
   so loramac-node always calls both Radio.SetTxConfig() and
   Radio.SetRxConfig().  Skipping either leaves TxTimeout=0 in the
   library, causing an immediate TX timeout callback, which prevents
   the _tx_signal from ever being raised and blocks the TX wait thread
   for TX_TIMEOUT_MS (5 s) — a 5-second RX blackout per transmission.
   This was the root cause of "boots but no RX".

2. SX127xRadio::resetAGC() override:
   The base-class resetAGC() calls startReceive() after hwResetAGC().
   During async RX the loramac-node modem mutex is STATE_BUSY, so
   lora_recv_async() returns -EBUSY, which sets _in_recv_mode = 0 and
   corrupts the receive state machine.  Override skips the call
   entirely — the loramac-node driver self-manages AGC.

3. BW 62.5 kHz support in loramac-node/sx1276.c + patch:
   Rastislav's 0004-lora-sx127x-62k5-bandwidth.patch adds BW_62_KHZ
   support to sx12xx_common.c (Zephyr side) but not to the loramac-
   node library itself.  Without this fix bandwidth index 3 hits the
   "while(1)" fatal guard in SX1276SetRxConfig/SX1276SetTxConfig.
   This commit patches sx1276.c directly and adds
   patches/modules/loramac-node/0001-sx1276-bw62k5.patch so the fix
   survives west update.  CMakeLists.txt is extended to apply module
   patches via the existing zephcore_apply_patches() mechanism.
This commit is contained in:
liquidraver
2026-04-08 08:30:17 +02:00
parent f718fae057
commit 5f9c8b0d99
6 changed files with 134 additions and 6 deletions
+10
View File
@@ -121,6 +121,16 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr)
)
endif()
# Apply patches to loramac-node module
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/loramac-node)
message(STATUS "Applying ZephCore patches to loramac-node...")
zephcore_apply_patches(
"${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/loramac-node"
"${MODULES_DIR}/lib/loramac-node"
"loramac-node"
)
endif()
# Copy new files into Zephyr tree
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr-new)
file(GLOB_RECURSE ZEPHCORE_NEW_FILES
+10 -4
View File
@@ -29,6 +29,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
_rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)),
_rx_boost_enabled(true),
_tx_power_reduction_db(0),
_loramac_node(false),
_config_cached(false),
_rx_cb(nullptr), _rx_cb_user_data(nullptr),
_tx_done_cb(nullptr), _tx_done_cb_user_data(nullptr),
@@ -272,8 +273,12 @@ void LoRaRadioBase::configureRx()
* RX config (RadioSetRxConfig) from a previous cycle — Radio.Rx(0)
* in hwStartReceive() will use those register values directly.
* This avoids the modem_acquire → modem_release → Radio.Sleep()
* round-trip that wastes ~5 ms on every TX→RX transition. */
if (_config_cached && onlyDirectionDiffers(cfg, _last_cfg)) {
* round-trip that wastes ~5 ms on every TX→RX transition.
*
* Not used for loramac-node: Radio.SetTxConfig() and Radio.SetRxConfig()
* configure completely disjoint internal state (including TxTimeout).
* Skipping either on a direction change leaves that state uninitialized. */
if (!_loramac_node && _config_cached && onlyDirectionDiffers(cfg, _last_cfg)) {
LOG_DBG("configureRx: direction-only change, skip hwConfigure");
_last_cfg = cfg;
return;
@@ -300,8 +305,9 @@ void LoRaRadioBase::configureTx()
/* Fast path: direction-only change (RX→TX). The driver already
* has a valid TX config (RadioSetTxConfig with TxTimeout=4000)
* from a previous cycle — Radio.Send() will use those values. */
if (_config_cached && onlyDirectionDiffers(cfg, _last_cfg)) {
* from a previous cycle — Radio.Send() will use those values.
* Not used for loramac-node (see configureRx comment above). */
if (!_loramac_node && _config_cached && onlyDirectionDiffers(cfg, _last_cfg)) {
LOG_DBG("configureTx: direction-only change, skip hwConfigure");
_last_cfg = cfg;
return;
+7
View File
@@ -90,6 +90,13 @@ protected:
/** GPIO-only BUSY check (no SPI). Default false for chips without duty-cycle sleep. */
virtual bool hwIsChipBusy() { return false; }
/* Set to true by subclasses using the loramac-node driver backend.
* Disables the direction-only fast path in configureTx()/configureRx():
* loramac-node calls Radio.SetTxConfig() and Radio.SetRxConfig() which
* configure completely disjoint internal state — skipping either leaves
* TxTimeout/RxConfig uninitialized in the loramac-node library. */
bool _loramac_node;
/* ── Shared helpers available to subclasses ────────────────── */
void buildModemConfig(struct lora_modem_config &cfg, bool tx);
+16 -2
View File
@@ -32,6 +32,10 @@ SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board,
{
/* SX127x has no RX boost feature — start with boost disabled */
_rx_boost_enabled = false;
/* loramac-node requires full lora_config() on every TX/RX direction
* change — Radio.SetTxConfig() and Radio.SetRxConfig() configure
* completely disjoint internal state in the loramac-node library. */
_loramac_node = true;
}
void SX127xRadio::begin()
@@ -91,8 +95,18 @@ void SX127xRadio::hwResetAGC()
{
/* The loramac-node SX127x driver manages AGC recalibration internally
* (RadioSetRxConfig re-programs all gain registers on every RX config
* call). No explicit AGC reset is needed or possible via the
* standard API. The base class will restart RX after this call. */
* call). No explicit AGC reset is needed or possible via the standard
* API. */
}
void SX127xRadio::resetAGC()
{
/* hwResetAGC() is a no-op, so skip the base-class resetAGC() entirely.
* The base class calls startReceive() after hwResetAGC(), but the
* loramac-node modem mutex (STATE_BUSY during async RX) causes
* lora_recv_async() to return -EBUSY, setting _in_recv_mode = 0 and
* corrupting the state machine. The loramac-node driver self-manages
* AGC, so nothing needs to happen here. */
}
} /* namespace mesh */
+6
View File
@@ -43,6 +43,12 @@ protected:
/* SX127x loramac-node driver manages AGC automatically. No-op. */
void hwResetAGC() override;
/* Override public resetAGC(): hwResetAGC() is a no-op, and the
* loramac-node modem mutex makes the base-class startReceive() call
* fail with -EBUSY (modem STATE_BUSY during async RX), which would
* set _in_recv_mode = 0 and corrupt the state machine. */
void resetAGC() override;
/* SX127x has no BUSY pin. Default (false) from base is correct. */
/* bool hwIsChipBusy() — inherited, returns false */
};
@@ -0,0 +1,85 @@
diff --git a/src/radio/sx1276/sx1276.c b/src/radio/sx1276/sx1276.c
index 7dd9b122..3ea5320d 100644
--- a/src/radio/sx1276/sx1276.c
+++ b/src/radio/sx1276/sx1276.c
@@ -523,12 +523,14 @@ void SX1276SetRxConfig( RadioModems_t modem, uint32_t bandwidth,
break;
case MODEM_LORA:
{
- if( bandwidth > 2 )
+ if( bandwidth > 3 )
{
- // Fatal error: When using LoRa modem only bandwidths 125, 250 and 500 kHz are supported
+ // Fatal error: When using LoRa modem only bandwidths 62.5, 125, 250 and 500 kHz are supported
while( 1 );
}
- bandwidth += 7;
+ // Map loramac-node index to SX1276 BW register value:
+ // 0=125kHz->7, 1=250kHz->8, 2=500kHz->9, 3=62.5kHz->6
+ bandwidth = ( bandwidth == 3 ) ? 6 : ( bandwidth + 7 );
SX1276.Settings.LoRa.Bandwidth = bandwidth;
SX1276.Settings.LoRa.Datarate = datarate;
SX1276.Settings.LoRa.Coderate = coderate;
@@ -550,7 +552,9 @@ void SX1276SetRxConfig( RadioModems_t modem, uint32_t bandwidth,
datarate = 6;
}
- if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ // BW 62.5kHz (reg 6): LDRO required for SF10/11/12 (symbol time >16ms)
+ if( ( ( bandwidth == 6 ) && ( ( datarate == 10 ) || ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
( ( bandwidth == 8 ) && ( datarate == 12 ) ) )
{
SX1276.Settings.LoRa.LowDatarateOptimize = 0x01;
@@ -687,12 +691,14 @@ void SX1276SetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev,
case MODEM_LORA:
{
SX1276.Settings.LoRa.Power = power;
- if( bandwidth > 2 )
+ if( bandwidth > 3 )
{
- // Fatal error: When using LoRa modem only bandwidths 125, 250 and 500 kHz are supported
+ // Fatal error: When using LoRa modem only bandwidths 62.5, 125, 250 and 500 kHz are supported
while( 1 );
}
- bandwidth += 7;
+ // Map loramac-node index to SX1276 BW register value:
+ // 0=125kHz->7, 1=250kHz->8, 2=500kHz->9, 3=62.5kHz->6
+ bandwidth = ( bandwidth == 3 ) ? 6 : ( bandwidth + 7 );
SX1276.Settings.LoRa.Bandwidth = bandwidth;
SX1276.Settings.LoRa.Datarate = datarate;
SX1276.Settings.LoRa.Coderate = coderate;
@@ -712,7 +718,9 @@ void SX1276SetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev,
{
datarate = 6;
}
- if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ // BW 62.5kHz (reg 6): LDRO required for SF10/11/12 (symbol time >16ms)
+ if( ( ( bandwidth == 6 ) && ( ( datarate == 10 ) || ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
+ ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
( ( bandwidth == 8 ) && ( datarate == 12 ) ) )
{
SX1276.Settings.LoRa.LowDatarateOptimize = 0x01;
@@ -1437,6 +1445,9 @@ static uint32_t SX1276GetLoRaBandwidthInHz( uint32_t bw )
case 2: // 500 kHz
bandwidthInHz = 500000UL;
break;
+ case 3: // 62.5 kHz
+ bandwidthInHz = 62500UL;
+ break;
}
return bandwidthInHz;
@@ -1475,8 +1486,11 @@ static uint32_t SX1276GetLoRaTimeOnAirNumerator( uint32_t bandwidth,
}
}
+ // index 0=125kHz: LDRO for SF11/12; index 1=250kHz: LDRO for SF12
+ // index 3=62.5kHz: LDRO for SF10/11/12 (symbol time >16ms)
if( ( ( bandwidth == 0 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) ||
- ( ( bandwidth == 1 ) && ( datarate == 12 ) ) )
+ ( ( bandwidth == 1 ) && ( datarate == 12 ) ) ||
+ ( ( bandwidth == 3 ) && ( ( datarate == 10 ) || ( datarate == 11 ) || ( datarate == 12 ) ) ) )
{
lowDatareOptimize = true;
}