bump lr sdk and audit refactor

This commit is contained in:
liquidraver
2026-08-12 14:40:52 +02:00
parent 101e97f54a
commit b839c2c5d1
10 changed files with 1379 additions and 170 deletions
+3
View File
@@ -141,3 +141,6 @@ handover_thinknode_m9.md
# Vendor reference docs (datasheets, errata). Local only — not ours to redistribute.
/datasheets/
LR2021_AUDIT_INDEX.md
HANDOVER_lr2021_audit.md
lr2021_bench/cad_lbt_fast.patch
lr2021_bench/README.md
+199
View File
@@ -0,0 +1,199 @@
# LR2021 CAD changes — build, flash, measure
Local-only. Produced 2026-08-12 alongside the Batch 1 work from
`HANDOVER_lr2021_audit.md`. All paths are relative to the west workspace root
(`D:\zephcore`), which is also the git root.
## What is in the tree right now
Batch 1 **plus** the combined CAD change:
- **CAD_LBT is the normal route.** `CadExitMode = 0x10` (DS Table 6-18) — the
chip runs the CAD and, on a clear channel, goes straight to Tx with no host
round-trip. The old two-step CAD → host → `SetTx` path survives **only** as
the fallback above the 524 ms ceiling (see below).
- **Fast CAD**, `pnr_delta = 8`. DS §6.3.11: "CadDone time is shortened in case
of early no detection". Most LBT CADs find nothing, so this shortens the
common pre-TX path.
- **CAD → carrier-up latency instrumentation** (`CAD->TX latency: NNNN us`).
`lr2021_bench/cad_lbt_fast.patch` is that whole thing as one unit, generated
against Batch 1.
### Committing it separately
Do not use `git add -p` — the hunks interleave with Batch 1 in the same file.
Revert, commit Batch 1, re-apply, commit the CAD change:
```bash
git apply -R lr2021_bench/cad_lbt_fast.patch
```
Commit Batch 1, then:
```bash
git apply lr2021_bench/cad_lbt_fast.patch
```
## Build
Always with `debug.conf` — the verification lines below are compiled out
otherwise.
```bash
west build -b meshtracker_x1 zephcore --pristine -d build_all_x1 -- -DEXTRA_CONF_FILE="boards/common/debug.conf"
```
| Build | FLASH | RAM |
|---|---|---|
| Gate 0 (untouched) | 475,844 B / 68.74 % | 240,424 B / 91.71 % |
| Batch 1 (`build_b1_x1`) | 476,240 B / 68.80 % | 240,424 B / 91.71 % |
| Batch 1 + CAD (`build_all_x1`) | 476,996 B / 68.91 % | 240,424 B / 91.71 % |
RAM is unchanged throughout; the debug build's ~15.9 KB of headroom is intact.
## Flash and read it — no J-Link needed
`debug.conf`'s "RTT backend only" comment is stale. The generated config has
`CONFIG_LOG_BACKEND_UART=y` and the X1's chosen console is `cdc_acm_uart`, so
**logs come out over USB CDC**. One port carries both the log stream and the
CLI: binary companion frames are prefixed with `<`, and bare text lines go to
`CommonCLI`. So a plain serial terminal gives you logs and `get cad` in the
same window.
Double-tap reset, then drag the UF2:
```bash
explorer /select,"D:\zephcore\build_all_x1\zephyr\zephyr.uf2"
```
Find the port after it re-enumerates:
```bash
powershell -Command "Get-CimInstance Win32_SerialPort | Select-Object DeviceID,Description"
```
Then attach any terminal at 115200 (the rate is ignored by CDC). If you have
`pyserial`:
```bash
python -m serial.tools.miniterm COM7 115200
```
## What to look for
### Batch 1
- `PRAM loaded: type=0x03 version=0x13 (560 words)` — still present; both reset
paths reload it. Absent or an error line is a **Gate 0 failure**, and Gate 0
was never separately confirmed on this board.
- No `BUSY timeout!` and no `command REJECTED after ...` — the reset and
calibrate delay deletions (C7/C8).
- No `RXFREQ_NO_FRONT_END_CALIB` in any `chip ... error:` block, and RX working
at the operating frequency — C5. `get_errors` bit 9 clear.
- `ERR_EVENT_CAD_TIMEOUT` should stop appearing and TX should no longer stall
in 4 s multiples — C2, the main availability win.
- `RX header latched NNNN ms with no packet, releasing TX gate` — C2's payload
deadline. Rare is healthy; frequent means something else is wrong.
- `modem_cfg: set_tx_params(44 half-dBm = 22.0 dBm, ramp=0x05)=0` at max power
— C1.
- `get stats`: RX count should no longer stall after a burst of TX — C4.
### CAD_LBT
```
CAD_LBT: chip took CAD->TX itself (tx_timeout=NN ms)
```
on every transmit that found the channel clear. Then confirm a neighbour
actually receives the packets — this is the whole test.
**Judge on latency only.** Post-CAD collisions are not separable from any other
loss with CoreScope, so make no collision-rate claim in either direction.
Since the CAD → Tx transition is now chip-internal, the saving *is* the
host round-trip that used to sit there. To put a number on it, flash a
build carrying only the instrumentation (`bench2a_cad_latency_instrument.patch`
on Batch 1) and record `CAD->TX latency: NNNN us` over a few dozen transmits —
median and spread, it is scheduler latency and inherently jittery. On the
combined build that line now appears only on the >524 ms fallback path.
**Expect a small number.** Before C5 this gap contained a front-end calibration
that should not have been there; measuring against that would have flattered the
result. If it is large, suspect the measurement before believing it.
### Fast CAD
`get cad`, per-level line, before and after over comparable windows:
```
*+1(22) 22p 18b 16f 2t 72%
| | | | | | └─ false-positive rate
| | | | | └──── true positives
| | | | └─────── false positives
| | | └─────────── busy
| | └─────────────── probes
| └──────────────────── level (peak)
└─────────────────────── current operating level
```
A materially *lower* busy rate is a **red flag, not a win** — it more likely
means CAD stopped detecting than that the channel got quieter. What you want is
busy and false-positive rates roughly unchanged, with CAD completing sooner.
Side detectors are already off during CAD, so the datasheet's multi-SF caveat
("the main SF determines the not-detect condition") does not apply.
Note: stacked with CAD_LBT, a change in these counters is not attributable to
either mechanism alone. If a number looks wrong, split them before chasing it.
## The 524 ms ceiling
`cad_timeout` is 24 bits of 32 MHz periods — DS §6.3.11, `ds.txt:9435`:
"expressed in periods of 32MHz crystal oscillator". **Not** the "PLL step of
31.25us" the vendor header's doc comment claims, and not RTC steps. So
`0x00FFFFFF / 32e6` = **524 ms** of Tx timeout, against the 5000 ms the classic
path uses.
The code computes `airtime + 25 % + 50 ms` and takes the classic CAD → host →
`SetTx` path when that exceeds the ceiling, rather than letting the chip cut the
transmission off mid-packet. At SF7/BW250 a 100-byte frame is ~80 ms and fits
easily; SF10/BW250 at the same length is ~720 ms and takes the fallback.
**On a slow preset you may see no `CAD_LBT:` lines at all.** Check for
```
CAD_LBT: airtime NNN ms needs NNN ms Tx timeout, over the 524 ms cad_timeout ceiling — using host CAD->TX
```
at DBG before concluding the experiment failed.
## If it fails
Nobody has hardware CAD_LBT working on an LR2021. USP #125 reported the
`RAL_LORA_CAD_LBT` path non-functional; that entry is absent from the current
`KNOWN_LIMITATIONS.md`, which is the removal of a warning, not a statement that
it works. RadioLib never implemented LoRa CAD on this chip at all — its
`scanChannel()` uses the generic RSSI CAD. **Failure is an acceptable outcome.**
| Symptom | Meaning | Action |
|---|---|---|
| No `CAD_LBT:` line, no TX at all | Chip rejected `SetLoraCadParams` with exit mode 0x10. Look for `command REJECTED after configure_cad_params`. | Revert the CAD patch |
| `CAD_LBT: no CAD_DONE within budget` | Chip does not raise CAD_DONE in this exit mode. The code aborts, re-arms RX and returns `-EIO`; the mesh retries. | Revert the CAD patch |
| Neighbours receive truncated packets | The `cad_timeout` unit is wrong after all | Revert immediately |
`build_b1_x1/zephyr/zephyr.uf2` is Batch 1 without any CAD change — the fallback
image to drop back to.
## Individual patches (superseded by `cad_lbt_fast.patch`)
Kept because each isolates one delta, and because bench 2a alone is how you get
the latency baseline. `bench1` and `bench2b` conflict textually — both edit the
same `cad` struct initialiser — so apply 2a, then 2b, then bench1 by hand, or
just use the combined patch.
| Patch | Applies on top of |
|---|---|
| `bench1_pnr_delta_8.patch` | Batch 1 |
| `bench2a_cad_latency_instrument.patch` | Batch 1 |
| `bench2b_cad_lbt.patch` | Batch 1 + 2a |
+29
View File
@@ -0,0 +1,29 @@
diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
index d283ba4..ccbf5e3 100644
--- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
+++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
@@ -2111,7 +2111,23 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
uint8_t symb_nb = lr20xx_cad_symb_nb(mc);
lr20xx_radio_lora_cad_params_t cad = {
.cad_symb_nb = symb_nb,
- .pnr_delta = 0, /* exact symbol count, no best-effort */
+ /* BENCH EXPERIMENT — best-effort ("fast") CAD. DS §6.3.11:
+ * "CadDone time is shortened in case of early no detection";
+ * the vendor header calls 8 the recommended best-effort value.
+ * Most LBT CADs find nothing, so this shortens the common
+ * pre-TX blocking path.
+ *
+ * NOT a fix — revert unless the bench A/B justifies it. Judge
+ * on `get cad`, per-level line (probes / busy / false-pos /
+ * true-pos / fp-rate) over a comparable window. A materially
+ * LOWER busy rate is a RED FLAG, not a win: it more likely means
+ * CAD stopped detecting than that the channel went quiet.
+ * Side detectors are already off during CAD, so the DS's
+ * multi-SF "main SF determines the not-detect condition" caveat
+ * does not apply.
+ *
+ * Revert value: 0 (exact symbol count, no best-effort). */
+ .pnr_delta = 8,
.cad_exit_mode = LR20XX_RADIO_LORA_CAD_EXIT_MODE_STANDBYRC,
.cad_timeout_in_pll_step = 0,
/* Peak is looked up for the window actually programmed, so the
@@ -0,0 +1,60 @@
diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
index d283ba4..bf2bb39 100644
--- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
+++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
@@ -115,6 +115,19 @@ struct lr20xx_data {
int8_t cad_peak_offset;
uint8_t cad_probe_peak;
+ /* BENCH INSTRUMENTATION — CAD_DONE -> carrier-up latency.
+ *
+ * k_cycle_get_32() stamped where the DIO1 work handler observes
+ * CAD_DONE, read again immediately before SetTx. The delta is the
+ * whole host round-trip the chip's CAD_LBT exit mode would remove:
+ * IRQ -> work queue -> semaphore -> mesh thread -> apply_modem_config
+ * -> SetTx. 0 = no CAD preceded this transmit (nothing to report).
+ *
+ * This is measurement only, no behaviour change — flash it FIRST and
+ * record the baseline, because a CAD_LBT number is meaningless without
+ * one. Remove with the experiment. */
+ uint32_t cad_done_cycles;
+
/* Deferred hardware init */
bool hw_initialized;
@@ -1323,6 +1336,15 @@ static void lr20xx_dio1_work_handler(struct k_work *work)
if (irq & LR20XX_SYSTEM_IRQ_CAD_DONE) {
bool detected = (irq & LR20XX_SYSTEM_IRQ_CAD_DETECTED) != 0;
+ /* BENCH INSTRUMENTATION: stamp as early as possible in the
+ * handler — everything after this point is the host round-trip
+ * being measured. Only a free channel leads to a transmit, so
+ * only that case is worth stamping. */
+ data->cad_done_cycles = detected ? 0U : k_cycle_get_32();
+ if (!detected && data->cad_done_cycles == 0U) {
+ data->cad_done_cycles = 1U; /* 0 is the "none" sentinel */
+ }
+
LOG_DBG("CAD done: %s", detected ? "activity" : "free");
data->cad_active = false;
@@ -1641,6 +1663,19 @@ static int lr20xx_lora_send_async(const struct device *dev,
data->tx_signal = async;
data->tx_active = true;
+ /* BENCH INSTRUMENTATION: CAD_DONE -> carrier-up, the gap CAD_LBT would
+ * close. Reported in microseconds; k_cycle_get_32() wraps, but the
+ * unsigned subtraction is correct across one wrap and the interval is
+ * milliseconds against a 32-bit counter, so a double wrap is not
+ * reachable here. */
+ if (data->cad_done_cycles != 0U) {
+ uint32_t d = k_cycle_get_32() - data->cad_done_cycles;
+
+ LOG_INF("CAD->TX latency: %u us",
+ (unsigned)k_cyc_to_us_near32(d));
+ data->cad_done_cycles = 0U;
+ }
+
lr20xx_radio_common_set_tx(ctx, 5000);
/* Command status here is SET_TX's own (2=accepted, 1=rejected,
+235
View File
@@ -0,0 +1,235 @@
diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
index bf2bb39..7f037b4 100644
--- a/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
+++ b/zephcore/patches/zephyr-new/drivers/lora/lr20xx/lr20xx_lora.c
@@ -128,6 +128,16 @@ struct lr20xx_data {
* one. Remove with the experiment. */
uint32_t cad_done_cycles;
+ /* BENCH EXPERIMENT — CAD_LBT. When set, the next lr20xx_do_cad() asks
+ * the chip for CadExitMode = TX (0x10, DS Table 6-18: "The chip performs
+ * a CAD operation and if no activity is detected, it goes to Tx mode and
+ * takes cad_timeout as Tx timeout") instead of the STANDBY_RC fallback,
+ * and passes cad_lbt_tx_timeout as that Tx timeout. Both are cleared by
+ * do_cad once consumed, so an ordinary adaptive-CAD probe can never
+ * inherit them and accidentally transmit. */
+ bool cad_lbt_exit_tx;
+ uint32_t cad_lbt_tx_timeout; /* 32 MHz periods, <= 0x00FFFFFF */
+
/* Deferred hardware init */
bool hw_initialized;
@@ -1556,6 +1566,151 @@ static uint32_t lr20xx_cad_timeout_ms(struct lr20xx_data *data)
return MAX(ms, 200U);
}
+/* ── BENCH EXPERIMENT: hardware CAD_LBT ─────────────────────────────────
+ *
+ * DS Table 6-18, CadExitMode = 0x10: the chip runs the CAD and, if the channel
+ * is clear, goes straight to Tx with no host round-trip — removing the
+ * IRQ -> work queue -> semaphore -> mesh thread -> apply_modem_config -> SetTx
+ * path between "channel measured free" and "carrier up". Bench 2a measures
+ * exactly that gap; this is the thing that closes it.
+ *
+ * Judge on latency only (A7). Post-CAD collisions are not separable from any
+ * other loss with CoreScope, so make no collision-rate claim either way.
+ *
+ * Nobody has this working on an LR2021: USP #125 reported its RAL path
+ * non-functional (that entry is absent from the current KNOWN_LIMITATIONS.md,
+ * which is the removal of a warning, not a fix), and RadioLib never implemented
+ * LoRa CAD at all. FAILURE IS AN ACCEPTABLE OUTCOME — revert and report rather
+ * than sink time into it.
+ *
+ * Requirements the restructure exists to satisfy: the payload must be in the Tx
+ * FIFO and the packet params set BEFORE SetLoraCAD, since the chip transmits
+ * whatever is staged. DIO1 must stay enabled across the CAD (both CAD_DONE and
+ * TX_DONE arrive on it). The busy case exits to fallback STDBY_RC, not RX, so
+ * the host still re-arms RX there — that path is unchanged.
+ *
+ * HARD CEILING: cad_timeout is 24 bits of 32 MHz periods = 0x00FFFFFF / 32e6 =
+ * 524 ms of Tx timeout, against the 5000 ms the normal path uses. Anything
+ * whose airtime does not fit is transmitted the old way instead of being
+ * silently truncated mid-packet. At SF7/BW250 a 100-byte frame is ~80 us*1000
+ * and fits easily; SF10/BW250 at the same length is ~720 ms and does not.
+ */
+#define LR20XX_CAD_LBT_MAX_TX_TIMEOUT_STEPS 0x00FFFFFFU
+#define LR20XX_CAD_LBT_STEPS_PER_MS 32000U
+
+static int lr20xx_do_cad(struct lr20xx_data *data);
+
+static int lr20xx_lora_send_cad_lbt(const struct device *dev,
+ uint8_t *buf, uint32_t data_len,
+ struct k_poll_signal *async,
+ uint32_t tx_timeout_ms)
+{
+ struct lr20xx_data *data = dev->data;
+ const struct lr20xx_config *cfg = dev->config;
+ void *ctx = &data->hal_ctx;
+ bool was_in_rx = data->in_rx_mode;
+ int ret;
+
+ k_mutex_lock(&data->spi_mutex, K_FOREVER);
+
+ /* Same reasoning as the normal path: async_rx_cb stays registered. */
+ data->in_rx_mode = false;
+ lr20xx_reset_rx_busy_signals(data);
+
+ /* DIO1 deliberately NOT disabled — CAD_DONE and TX_DONE both arrive on
+ * it, and with CAD_LBT there is no host step in between to re-enable. */
+
+ if (lr20xx_system_set_standby_mode(ctx, LR20XX_SYSTEM_STANDBY_MODE_RC)
+ != LR20XX_STATUS_OK) {
+ LOG_ERR("CAD_LBT standby failed — HW reset");
+ lr20xx_hardware_reset(data, cfg);
+ }
+
+ lr20xx_system_clear_errors(ctx);
+ lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK);
+
+ /* Full TX-side modem setup must complete before the CAD. */
+ lr20xx_apply_modem_config(data, cfg, true);
+
+ lr20xx_radio_lora_pkt_params_t pkt = {
+ .preamble_len_in_symb = data->modem_cfg.preamble_len,
+ .pkt_mode = LR20XX_RADIO_LORA_PKT_EXPLICIT,
+ .pld_len_in_bytes = (uint8_t)data_len,
+ .crc = data->modem_cfg.packet_crc_disable
+ ? LR20XX_RADIO_LORA_CRC_DISABLED
+ : LR20XX_RADIO_LORA_CRC_ENABLED,
+ .iq = data->modem_cfg.iq_inverted
+ ? LR20XX_RADIO_LORA_IQ_INVERTED
+ : LR20XX_RADIO_LORA_IQ_STANDARD,
+ };
+ lr20xx_radio_lora_set_packet_params(ctx, &pkt);
+ lr20xx_radio_fifo_write_tx(ctx, buf, (uint16_t)data_len);
+
+ /* Armed before SetLoraCAD: on a clear channel the chip starts Tx
+ * autonomously, so TX_DONE can land before this function returns. */
+ data->tx_signal = async;
+ data->tx_active = true;
+
+ data->cad_lbt_exit_tx = true;
+ data->cad_lbt_tx_timeout = tx_timeout_ms * LR20XX_CAD_LBT_STEPS_PER_MS;
+
+ k_sem_reset(&data->cad_sem);
+ data->cad_result = -ETIMEDOUT;
+ data->cad_cb = NULL;
+
+ ret = lr20xx_do_cad(data);
+ k_mutex_unlock(&data->spi_mutex);
+
+ if (ret < 0) {
+ goto abort;
+ }
+
+ /* CAD_DONE still fires in CAD_LBT mode; it is how the host learns the
+ * outcome. Budget = the CAD window plus the transmit it may have
+ * already started. */
+ ret = k_sem_take(&data->cad_sem,
+ K_MSEC(lr20xx_cad_timeout_ms(data) + tx_timeout_ms));
+ if (ret == -EAGAIN) {
+ LOG_WRN("CAD_LBT: no CAD_DONE within budget — falling back");
+ data->cad_active = false;
+ ret = -EIO;
+ goto abort;
+ }
+
+ if (data->cad_result > 0) {
+ /* Busy: the chip is parked in the STDBY_RC fallback and never
+ * transmitted. Nothing raised tx_signal, so clear the TX state
+ * and re-arm RX exactly as the classic busy path does. */
+ LOG_DBG("CAD_LBT: channel busy");
+ data->tx_active = false;
+ data->tx_signal = NULL;
+ if (was_in_rx || data->rx_duty_cycle_enabled) {
+ k_mutex_lock(&data->spi_mutex, K_FOREVER);
+ lr20xx_start_rx(data, cfg);
+ k_mutex_unlock(&data->spi_mutex);
+ }
+ return -EBUSY;
+ }
+
+ /* Clear: the chip is transmitting (or has already finished). TX_DONE
+ * drives the rest, same as the normal path. */
+ LOG_INF("CAD_LBT: chip took CAD->TX itself (tx_timeout=%u ms)",
+ tx_timeout_ms);
+ return 0;
+
+abort:
+ data->tx_active = false;
+ data->tx_signal = NULL;
+ data->cad_lbt_exit_tx = false;
+ data->cad_lbt_tx_timeout = 0;
+ if (was_in_rx || data->rx_duty_cycle_enabled) {
+ k_mutex_lock(&data->spi_mutex, K_FOREVER);
+ lr20xx_start_rx(data, cfg);
+ k_mutex_unlock(&data->spi_mutex);
+ }
+ return ret;
+}
+
static int lr20xx_lora_send_async(const struct device *dev,
uint8_t *buf, uint32_t data_len,
struct k_poll_signal *async)
@@ -1572,6 +1727,23 @@ static int lr20xx_lora_send_async(const struct device *dev,
* commands it too, so this has to come first. */
lr20xx_dc_takeover(data);
+ /* BENCH EXPERIMENT: hand CAD->TX to the chip where the transmit fits
+ * inside cad_timeout's 524 ms ceiling. Above it, fall through to the
+ * classic two-step path rather than truncate the packet. */
+ if (data->modem_cfg.cad.mode == LORA_CAD_MODE_LBT) {
+ uint32_t airtime_ms = lr20xx_lora_airtime(dev, data_len);
+ uint32_t tx_timeout_ms = airtime_ms + (airtime_ms / 4U) + 50U;
+
+ if ((uint64_t)tx_timeout_ms * LR20XX_CAD_LBT_STEPS_PER_MS <=
+ LR20XX_CAD_LBT_MAX_TX_TIMEOUT_STEPS) {
+ return lr20xx_lora_send_cad_lbt(dev, buf, data_len,
+ async, tx_timeout_ms);
+ }
+ LOG_DBG("CAD_LBT: airtime %u ms needs %u ms Tx timeout, over the "
+ "524 ms cad_timeout ceiling — using host CAD->TX",
+ airtime_ms, tx_timeout_ms);
+ }
+
/* LBT: perform blocking CAD before transmitting. On CAD-busy, restore
* RX in-driver before returning -EBUSY so the C++ layer doesn't have
* to do a full cancel-then-restart round-trip. lr20xx_lora_cad
@@ -2147,8 +2319,22 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
lr20xx_radio_lora_cad_params_t cad = {
.cad_symb_nb = symb_nb,
.pnr_delta = 0, /* exact symbol count, no best-effort */
- .cad_exit_mode = LR20XX_RADIO_LORA_CAD_EXIT_MODE_STANDBYRC,
- .cad_timeout_in_pll_step = 0,
+ /* BENCH EXPERIMENT: CAD_LBT hands the CAD->TX transition to the
+ * chip. Note the two CAD commands have different exit-mode
+ * encodings — generic RSSI CAD (DS Table 6-22) is 0x01 for Tx,
+ * LoRa CAD (Table 6-18) is 0x10. Always the SDK enum, never
+ * RadioLib's constants: RadioLib calls the *other* command. */
+ .cad_exit_mode = data->cad_lbt_exit_tx
+ ? LR20XX_RADIO_LORA_CAD_EXIT_MODE_TX
+ : LR20XX_RADIO_LORA_CAD_EXIT_MODE_STANDBYRC,
+ /* DS §6.3.11 (ds.txt:9435): "cad_timeout defines the timeout
+ * duration after CAD for Rx (CAD_RX) or Tx (CAD_LBT), expressed
+ * in periods of 32MHz crystal oscillator." NOT the "PLL step of
+ * 31.25us" the vendor header's doc comment claims, and not RTC
+ * steps — getting this wrong truncates transmissions. */
+ .cad_timeout_in_pll_step = data->cad_lbt_exit_tx
+ ? data->cad_lbt_tx_timeout
+ : 0,
/* Peak is looked up for the window actually programmed, so the
* two can never drift apart (DS Table 6-19 is 2-D). */
.cad_detect_peak = lr20xx_cad_detect_peak(sf, symb_nb),
@@ -2188,6 +2374,12 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
lr20xx_radio_lora_set_cad(ctx);
CHECK_CMD(ctx, "set_cad");
+ /* One-shot: consumed here so no later CAD (an adaptive-CAD probe, or a
+ * plain LBT with the payload no longer staged) can inherit exit-to-TX
+ * and transmit whatever is left in the FIFO. */
+ data->cad_lbt_exit_tx = false;
+ data->cad_lbt_tx_timeout = 0;
+
return 0;
}
+9 -4
View File
@@ -85,10 +85,15 @@ int16_t LR2021Radio::hwGetCurrentRSSI()
bool LR2021Radio::hwIsReceiving()
{
/* MUST be non-destructive: never clear IRQ bits from this path.
* Foreign-preamble release is hardware-driven (chip-internal release
* on HEADER_ERROR / sync timeout). The driver's lr20xx_is_receiving()
* reads via get_status() without clearing. */
/* The poll itself is non-destructive: lr20xx_is_receiving() reads the
* IRQ register via get_status(), which clears nothing.
*
* Foreign-preamble release is NOT hardware-driven, whatever the comment
* that stood here (copy-pasted from LR1110Radio.cpp) claimed. DS §5.7:
* IRQ status bits are latched until ClearIrq, and continuous RX has no
* timeout so the driver releases them in software, on an SF-aware
* grace for the preamble and a max-airtime deadline for the header,
* exactly as the LR11xx and SX126x drivers do. */
return lr20xx_is_receiving(_dev);
}
+6 -3
View File
@@ -1033,9 +1033,12 @@ bool LoRaRadioBase::isReceiving()
return false;
}
/* Driver-side latch + non-destructive IRQ read covers the full
* payload phase. hwIsReceiving() never clears IRQ bits; foreign
* preambles release via hardware (SymbNumTimeout on SX126x non-DC
* or chip-internal sync timer on DC / LR11xx / LR20xx). */
* payload phase. hwIsReceiving() never clears IRQ bits on the poll
* path itself; foreign preambles are released by the driver in
* software, on an SF-aware grace plus a max-airtime header deadline
* (SX126x patch 0013, LR11xx, LR20xx alike). It is not a hardware
* release the chips latch these bits until ClearIrq, and continuous
* RX has no timeout to do it for them. */
if (hwIsReceiving()) {
return true;
}
@@ -14,6 +14,18 @@ LOG_MODULE_REGISTER(lr20xx_hal, CONFIG_LORA_LOG_LEVEL);
/* BUSY timeout; covers worst-case post-reset firmware boot (~300ms) with margin */
#define LR20XX_BUSY_TIMEOUT_MS 3000
/* This HAL is single-instance: everything below is file-scope, not per-context.
* The driver instantiates with DT_INST_FOREACH_STATUS_OKAY, so a second
* semtech,lr2021 node would silently share the DIO1 callback, the BUSY
* callback and the BUSY semaphore between two radios the second init would
* overwrite the first's callback and both would wait on one semaphore. No
* board does this today; the assert makes the assumption fail loudly at build
* time instead of mysteriously at run time. The fix, if a board ever needs
* two, is to move these into struct lr20xx_hal_context. */
BUILD_ASSERT(DT_NUM_INST_STATUS_OKAY(semtech_lr2021) <= 1,
"lr20xx_hal_zephyr.c holds its DIO1/BUSY state in file-scope "
"statics — it supports exactly one LR20xx instance");
/* Static state for DIO1 interrupt handling */
static struct gpio_callback dio1_gpio_cb;
static lr20xx_dio1_callback_t dio1_user_cb = NULL;
@@ -432,15 +444,26 @@ lr20xx_hal_status_t lr20xx_hal_reset(const void *context)
{
struct lr20xx_hal_context *ctx = (struct lr20xx_hal_context *)context;
LOG_INF("LR20xx reset: assert reset, hold 10ms");
LOG_INF("LR20xx reset: assert reset, hold 1ms");
/* DS §4.2: "Assert NRESET low for minimum 100 us." 1 ms is 10x the
* minimum and matches Semtech's reference HAL; the 10 ms here before
* was 100x it. */
gpio_pin_set_dt(&ctx->reset, 1);
k_msleep(10); /* ≥100us reset pulse per LR2021 datasheet §5.1 */
k_msleep(1);
gpio_pin_set_dt(&ctx->reset, 0);
k_msleep(300); /* LR2021 firmware boot time per datasheet §5.1 */
/* No blind boot delay. DS §4.2/§4.3 make BUSY the completion signal —
* "During each of the reset procedures … the BUSY signal remains high …
* As the device enters Standby RC mode, the BUSY signal returns to a low
* state" — and the datasheet prescribes no post-release wait. The
* wait_on_busy() below is interrupt-driven with a 3 s bound and logs on
* timeout, so the 300 ms that used to sit here was 300 ms of sleeping
* before asking the question that answers itself. It cost that on every
* one of up to 3 boot attempts, and again inside lr20xx_hardware_reset(),
* which runs on the DIO1 work queue holding spi_mutex and now also
* carries the PRAM block writes. */
LOG_INF("LR20xx reset complete, BUSY=%d", gpio_pin_get_dt(&ctx->busy));
ctx->radio_is_sleeping = false;
File diff suppressed because it is too large Load Diff
@@ -48,9 +48,17 @@ void lr20xx_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief GPIO-only check for "chip is in a duty-cycle sleep window" (no SPI)
*
* True only while an RX duty cycle is armed and BUSY is high. Deliberately
* not raw BUSY: this chip holds BUSY high during continuous RX too, so a raw
* reading would report the radio permanently unavailable.
* True only while an RX duty cycle is armed and BUSY is high the chip is in
* its own sleep window (DS Table 4-1: "Sleep: busy=1") and an NSS falling edge
* would end the cycle (DS §6.3.8).
*
* Deliberately gated on the duty cycle rather than reading raw BUSY, but NOT
* for the reason once given here: BUSY is *not* held high during continuous
* RX. DS §5.2 is explicit "In Rx mode, BUSY goes low as soon as the chip is
* ready to receive data." (The BUSY=1 readings the X1 bring-up logs showed
* came from sampling the pin after issuing SPI, i.e. the dump's own footprint;
* dump_chip_state() now samples before.) The gate is still right: without it
* a stray BUSY would gate TX as well as the probes and mute the node.
*
* @param dev LoRa device
* @return true if the host should not issue commands right now