mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 19:38:20 +00:00
doc updates
This commit is contained in:
@@ -167,7 +167,7 @@ zephcore/
|
||||
│ ├── CAD (channel activity detection) │
|
||||
│ ├── Duty cycle enforcement (EU ETSI) │
|
||||
│ ├── RX delay (score-based prioritization) │
|
||||
│ └── Maintenance (noise floor, AGC reset) │
|
||||
│ └── Maintenance (noise floor, image cal) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ LoRaRadioBase │ Radio HAL
|
||||
│ ├── SX126xRadio ──► Zephyr SX126x driver │
|
||||
@@ -266,7 +266,8 @@ Called every ~5 seconds from the main event loop:
|
||||
|
||||
1. **Noise floor calibration**: EMA with alpha=1/8, jitter, threshold filtering, warmup
|
||||
2. **RX mode watchdog**: Flags error if radio stuck outside RX for >8 seconds
|
||||
3. **AGC reset**: Periodic warm sleep + recalibration (configurable interval, default off)
|
||||
3. **AGC reset** (`agcIdleMaintenance()`): warm sleep + recalibration, **SX126x only** — gated on `hwNeedsAgcReset()`, which only that family declares. Semtech prescribe it for a jammed AGC on the SX126x; neither the LR11xx UM nor the LR2021 DS describes such a fault, and running it there cost packets (T1000-E, 2026-08-24: 7.4% miss rate in the 60 s after a fire vs 0.6% elsewhere). Triggered by long silence **and** corroborating evidence — a frozen noise-floor reading — never by silence alone, which is a normal condition rather than a fault.
|
||||
4. **Image-calibration drift** (`imageCalMaintenance()`): unrelated to the AGC despite the shared hook — LR11xx/LR2021 only, where the datasheets give a temperature threshold. Temperature is read from the **board** (`Board::getMCUTemperature()`), never from the radio; only the delta matters and the die sensor tracks the same ambient without costing the radio an SPI command. Polled hourly, deferred after TX so PA self-heating is not read as ambient drift, and confirmed by a second reading before recalibrating.
|
||||
|
||||
### 4.7 Adaptive Contention Window
|
||||
|
||||
@@ -479,6 +480,12 @@ The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs:
|
||||
- **Header error**: Can shift buffer pointer → standby before RX restart
|
||||
- **DIO1 stuck HIGH**: 5-cycle detection → full hardware reset + recovery
|
||||
- **BUSY-high wedge**: a command racing the autonomous `SetRxDutyCycle` sleep phase can leave the chip BUSY-high with DIO1 low. No IRQ ever fires, so the event-driven driver never re-arms and the node goes permanently deaf. A wedge-recovery watchdog on its own work queue (`lr11xx_wedge`, `K_PRIO_COOP(7)`) checks every 3 s: after 12 s of DIO1 silence it polls the BUSY GPIO continuously for 250 ms, and a dwell with no low edge means a genuine wedge → hardware reset + RX restart. False-positive free by construction — a healthy chip, continuous or duty-cycled, always drops BUSY low within one cycle. Reads the GPIO only (no SPI), so it cannot itself disturb the chip or race the autonomous DC state machine.
|
||||
- **Duty-cycle ownership (BUSY is not a mode flag)**: UM §7.2.6 ends the RxDutyCycle loop on exactly three events — a packet (chip returns to the configured fallback, `STDBY_RC`), a host `SetStandby`, or **an NSS falling edge waking the chip from the sleep phase**, for which the manual adds "the user should send the `SetStandby(...)` command". Every host command is an NSS edge, so a command landing in the sleep phase silently ends the cycle: no IRQ, no flag, and every re-arm site hangs off RX_DONE or an error, which cannot fire on a receiver that is no longer listening. The node goes deaf until something independently calls `startReceive()` — which is precisely what the periodic housekeeping tick removed in `fe6e585` (2026-07-28, three days after the 1.16.7 tag) had been doing, and why the duty cycle "worked" in 1.16.7. Measured on a T1000-E 2026-08-24: **10 packets against an SX1262's 94** over 85 minutes with the cycle armed; parity with it off.
|
||||
|
||||
The guard this replaces was a BUSY read before each command, and it cannot be made correct here. BUSY is high in the sleep phase (command fatal) **and** through ordinary Rx (command harmless), so the pin does not distinguish them — 406 of 407 sampler bursts refused with the cycle armed, 76 of 76 with it off — and it is check-then-act regardless, since the chip can enter sleep between the read and the NSS assert. The driver takes ownership instead: `lr11xx_dc_suspend()` / `lr11xx_dc_resume()` bracket any work that must touch the chip, ending the cycle with the `SetStandby` the manual asks for and re-arming explicitly. `is_receiving()` is deliberately *not* bracketed — it runs on the TX gate, where standing the cycle down would end the reception being asked about — and answers from the DIO1-stamped latch with no bus access. Restored parity to 10/10, the sampler to zero refusals, and adaptive CAD from 2 probes in 9 h to ~20 per 5 min. Identical treatment in the LR2021 driver, whose DS §6.3.8 states the same three rules word for word (**untested on hardware** — no X1 available).
|
||||
|
||||
Two consequences worth keeping straight: the per-packet re-arm skips the standby after RX_DONE (`restart_rx(data, in_standby=true)`) because the chip has already performed it to spec, and the Rx-boost re-apply was dropped from the duty-cycle paths — §7.2.6 saves and restores the device configuration across each wake, so it was a redundant write inherited by analogy from the SX126x, which genuinely does need one (DS §9.6 retention list).
|
||||
|
||||
- **Stale SPI reply read as data**: a read is two NSS windows (command, then answer) with a BUSY wait between them. `wait_on_busy()` returns immediately on a BUSY that reads low and cannot distinguish "command finished" from "BUSY has not risen yet", so the answer window can clock out the chip's default status / IRQ stream instead of the payload — silently, since the caller parses IRQ bits as a plausible short integer. `lr11xx_hal_read()` therefore checks the stat1 command-status byte it used to discard and re-issues the command (3 attempts) unless it reports `CMD_DATA`. Same guard in the LR2021 HAL; upstream MeshCore hit this as [PR #3261](https://github.com/meshcore-dev/MeshCore/pull/3261).
|
||||
- **RX duty cycle**: wired via `SetRxDutyCycle` MODE_RX, sized by the shared adapter math (same as SX126x). The earlier "broken, 23-40% loss" verdict was a window-sizing bug (over-sleep + no header budget), not a chip defect — default-off, HW-verify before production use.
|
||||
|
||||
@@ -488,7 +495,7 @@ The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs:
|
||||
- **LR2021** (`CONFIG_ZEPHCORE_RADIO_LR2021`): custom driver in `patches/zephyr-new/drivers/lora/lr20xx/` (copied into the Zephyr tree at configure time, like LR11xx). **Validated on the SenseCAP MeshTracker X1** — RX, TX, LBT and RX duty cycle all confirmed on hardware after a full driver audit (2026-08-12). `promicro_lr2021` builds but is untested; its module was destroyed by overvoltage during bring-up. Notable properties that differ from the SX126x/LR11xx paths:
|
||||
- **Firmware Patch RAM.** DS §22.3 calls the PRAM "highly recommended"; without it the chip runs unpatched. `lr20xx_load_pram()` writes the 560-word image from `0x801000`, activates it with opcode `0x012D`, and verifies the magic word at `0x800FF8` — so the `PRAM loaded:` log line is proof the chip took it, not merely that the writes were accepted. Volatile: reloaded from both reset paths, survives every sleep this driver issues (all with retention).
|
||||
- **Hardware CAD→TX (`CadExitMode = 0x10`).** The chip runs the LBT CAD and, on a clear channel, transmits itself with no host round-trip. Payload and packet params are staged *before* `SetLoraCAD` and DIO1 stays enabled across it. Bounded by `cad_timeout`, which is 24 bits of 32 MHz periods = **524 ms max Tx timeout** — transmits whose airtime exceeds that take the classic CAD→host→`SetTx` route rather than being truncated (at SF7/BW62.5 the crossover is ~96 bytes).
|
||||
- **Front-end calibration is a point calibration, not a band.** `CalibFE` takes up to three individual frequencies (4 MHz steps, bit 15 = LF/HF), unlike the SX126x/LR11xx `CalibrateImage` freq1/freq2 band with datasheet-prescribed edges. It is issued only at config, after a hardware reset, and on AGC reset — never on the Tx/Rx path (DS §6.4.2 keeps the values on chip across retention sleep). Both 4 MHz neighbours of the operating frequency are calibrated, nearest first, because the SDK rounds the argument up where the chip's own default truncates down.
|
||||
- **Front-end calibration is a point calibration, not a band.** `CalibFE` takes up to three individual frequencies (4 MHz steps, bit 15 = LF/HF), unlike the SX126x/LR11xx `CalibrateImage` freq1/freq2 band with datasheet-prescribed edges. It is issued only at config, after a hardware reset, and on the temperature-drift recalibration — never on the Tx/Rx path (DS §6.4.2 keeps the values on chip across retention sleep). (It used to also ride the AGC-reset path; that path no longer exists on this family, and the caller it had explicitly skipped CalibFE anyway, so nothing was lost when it went.) Both 4 MHz neighbours of the operating frequency are calibrated, nearest first, because the SDK rounds the argument up where the chip's own default truncates down.
|
||||
- **Side detectors** (multi-SF receive) are LR2021-only; see `lr20xx_configure_side_detectors()`. Mutually exclusive with CAD, whose SF ordering constraint is the inverse.
|
||||
- **Per-packet frequency error** is decoded and accumulated (`get freqerr`) — diagnostic only, nothing acts on it.
|
||||
- **Reads are status-checked.** The two-window read (command, BUSY wait, answer) can clock its second window before BUSY rises, in which case the chip streams status / IRQ instead of the payload — `GetRxPacketLength` then returns `irq[31:16]`, exactly 4 with `RX_DONE` set, and a real frame is read out of the FIFO at the wrong length. `lr20xx_spi_read_frame()` accepts an answer only when the stat1 header reports `CMD_DATA`, re-issuing the command otherwise (3 attempts). Safe to retry because the Rx FIFO pop is not on this path (`lr20xx_hal_direct_read_fifo()`, single window, structurally immune).
|
||||
|
||||
@@ -267,7 +267,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
|
||||
| `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off |
|
||||
| `get dc.restarts` | Duty-cycle re-arm counter — RxTimeout re-arms **plus** parked-RX watchdog recoveries, sharing one total. **Read it as a rate: divide by uptime.** A bare count is not interpretable, and the two sources it merges cost very differently. An RxTimeout re-arm is ~7 ms of deaf time (the `Calibrate(ALL)` gap in the driver's `restart_rx`) after which the chip returns to duty cycle immediately — packets, not power. A watchdog recovery means the chip sat parked in *full RX* for one to two watchdog periods (`2·(preamble+8)` symbols, floored at 250 ms) — power, not packets, since parked RX still receives. The counter cannot tell you which, so read the worst case. **Measured normal: ~250/hr on a high site at SF8/BW 62.5** (one every ~14 s), where the worst case — every event a park — costs about 3.5% of the duty cycle's savings. Nothing to act on below roughly **2000/hr**; above that the parked-RX share starts eating a meaningful fraction of the saving and it becomes worth splitting the counter to find out. A high rate means the preamble detector is tripping without a decodable packet following, which on an elevated site is usually distant marginal traffic rather than interference — cross-check `get cad`, whose adaptive detPeak offset rises independently in a genuinely busy RF environment. Reset by `clear stats`. |
|
||||
| `get cad` | Adaptive-CAD status: header (`a` auto on/off, `o` operating detPeak offset, `pk` absolute peak with family base, `sp` noise-floor RSSI burst quality as `mean-spread-dB/zero-spread-%` (plus `(burst-count)` on the local USB console, omitted over the air to protect the 161 B reply budget) — a non-zero mean proves the 8 reads are independent however high the share climbs; only mean `0.0` with a high share indicts the sampler. See `ADAPTIVE_CAD.md`. `bc` busy cap), then a 3-rung window around the operating offset (`*` marks it) with probe/busy/fp/tp counts and false-positive rate — the three levels the knee controller reads. Probing runs even while `cad.auto` is off (dry-run), so this is the observation tool for picking a site-appropriate detPeak. See `ADAPTIVE_CAD.md`. Not available on SX127x boards (no hardware CAD). |
|
||||
| `get cad` | Adaptive-CAD status: header (`a` auto on/off, `o` operating detPeak offset, `pk` absolute peak with family base, `sp` noise-floor RSSI burst quality as `mean-spread-dB/zero-spread-%` (plus `(burst-count rN/bN/aN)` on the local USB console, omitted over the air to protect the 161 B reply budget, where `r` is completed RSSI reads, `b` reads the chip refused as busy, and `a` bursts abandoned because of one — on a healthy radio `b`/`a` stay at 0, and a large `a` against a near-zero burst count is the signature of a sampler being refused rather than one losing the odd read) — a non-zero mean proves the 8 reads are independent however high the share climbs; only mean `0.0` with a high share indicts the sampler. See `ADAPTIVE_CAD.md`. `bc` busy cap), then a 3-rung window around the operating offset (`*` marks it) with probe/busy/fp/tp counts and false-positive rate — the three levels the knee controller reads. Probing runs even while `cad.auto` is off (dry-run), so this is the observation tool for picking a site-appropriate detPeak. See `ADAPTIVE_CAD.md`. Not available on SX127x boards (no hardware CAD). |
|
||||
| `get extra.sf` | LR2021 side detectors: the extra spreading factors currently received alongside `sf`, comma-separated, or `none`. Reflects the saved prefs, not what the chip accepted — if the set became invalid after an `sf`/`bw` change it is reported here but was refused at boot (a `WRN` line says so). |
|
||||
| `get adc.multiplier` | Battery voltage ADC calibration multiplier |
|
||||
| `get bootloader.ver` | Bootloader version string |
|
||||
|
||||
@@ -38,7 +38,19 @@ static bool atomicWriteTempFile(const char *path, AtomicWriteFn write_fn, void *
|
||||
return false;
|
||||
}
|
||||
|
||||
fs_unlink(tmp_path);
|
||||
/* Best-effort cleanup of a leftover temp from an interrupted write.
|
||||
*
|
||||
* Guarded by fs_stat rather than unlinking blind: on the normal path the
|
||||
* file does not exist, fs_unlink() returns -ENOENT, and Zephyr's FS layer
|
||||
* logs that at ERR level regardless of us ignoring the return. That put
|
||||
* an <err> line on the happy path of every atomic save — 23 of them in a
|
||||
* 90-minute capture, one per save — which is exactly the noise that makes
|
||||
* a real filesystem error invisible. */
|
||||
struct fs_dirent tmp_ent;
|
||||
|
||||
if (fs_stat(tmp_path, &tmp_ent) == 0) {
|
||||
fs_unlink(tmp_path);
|
||||
}
|
||||
|
||||
struct fs_file_t file;
|
||||
fs_file_t_init(&file);
|
||||
@@ -231,7 +243,14 @@ bool ZephyrDataStore::copyFile(const char *src, const char *dst)
|
||||
return false;
|
||||
}
|
||||
|
||||
fs_unlink(tmp_path);
|
||||
/* Guarded for the same reason as atomicWriteTempFile(): a blind unlink of
|
||||
* a file that is normally absent logs -ENOENT at ERR level from the FS
|
||||
* layer, on the success path. */
|
||||
struct fs_dirent tmp_ent;
|
||||
|
||||
if (fs_stat(tmp_path, &tmp_ent) == 0) {
|
||||
fs_unlink(tmp_path);
|
||||
}
|
||||
|
||||
struct fs_file_t src_file, dst_file;
|
||||
fs_file_t_init(&src_file);
|
||||
|
||||
Reference in New Issue
Block a user