mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 03:08:27 +00:00
fix LNA/FEM control
This commit is contained in:
@@ -260,7 +260,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
| `get path.hash.mode` | Path hashing algorithm: `0`, `1`, or `2` |
|
||||
| `get loop.detect` | Loop detection level: `off`, `minimal`, `moderate`, or `strict` |
|
||||
| `get radio.rxgain` | RX gain boost: `on` or `off` |
|
||||
| `get radio.fem.rxgain` | External FEM/LNA active during RX: `on` or `off` (default `on`) |
|
||||
| `get radio.fem.rxgain` | External FEM's LNA in the RX path: `on` (through the LNA) or `off` (bypassed). Default `on` |
|
||||
| `get rxduty` | RX duty cycle mode: `0` or `1` |
|
||||
| `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
|
||||
| `get gps diag` | What the last GPS module-configuration attempt did — which path ran, bytes sent, and tracked satellites per constellation. See **GPS configuration diagnostics** in the GPS section for the field reference |
|
||||
@@ -312,7 +312,7 @@ Changes are persisted immediately unless noted. Some require a reboot.
|
||||
| `set path.hash.mode <mode>` | 0, 1, or 2 | Path hashing algorithm |
|
||||
| `set loop.detect <mode>` | `off`, `minimal`, `moderate`, `strict` | Loop detection sensitivity |
|
||||
| `set radio.rxgain <0\|1\|on\|off>` | | RX gain boost, applied live. Replies `Error: unsupported` on radios without RX boost (SX127x); the pref is still saved. |
|
||||
| `set radio.fem.rxgain <0\|1\|on\|off>` | default **1** | External FEM/LNA gating in the RX direction, applied live. Supported only where the FEM's chip-enable is wired to the radio node as `antenna-enable-gpios` — today `heltec_t096`, `heltec_wireless_tracker_v2`, `heltec_wifi_lora32_v4` and `heltec_wifi_lora32_v43`. Every other board reports `Error: unsupported`, for one of three reasons: it has no external amplifier at all; its amplifier has no separate receive-side enable, only the DIO2/TXEN/RXEN transmit-receive switch (`station_g2`, `gat562_30s`, `ikoka_nano_30dbm`, `promicro_sx1262`); or its amplifier enable is a standalone always-on regulator outside the radio node (`rak3401_1watt` — moving that pin onto the lora node as `antenna-enable-gpios` is what would enable it). Non-SX126x radios (LR1110, LR2021, SX127x) never support it. Setting `0` drops the FEM's RX gain (~16 dB on the SKY66122) and its supply current (~6.5 mA) — sensitivity for battery life, not a tuning knob. Transmit always uses the FEM, and the driver's idle/sleep gating is unchanged. Replies `Error: unsupported` on non-SX126x radios and on boards with no FEM wired; the pref is still saved. |
|
||||
| `set radio.fem.rxgain <0\|1\|on\|off>` | default **1** | Routes receive through the external FEM's LNA (`1`) or around it via the FEM's bypass path (`0`), applied live. Sensitivity for battery life — `0` costs roughly 17 dB and saves the LNA's supply current. Transmit, and the driver's idle/sleep gating of the FEM, are unaffected either way. Supported only where the FEM's receive path is software-selectable and that select line is wired to the radio node as `lna-bypass-gpios` — today the three KCT8103L boards, `heltec_t096`, `heltec_wireless_tracker_v2` and `heltec_wifi_lora32_v43`. Every other board reports `Error: unsupported`: `heltec_wifi_lora32_v4`'s GC1109 has no receive-path select (its CPS is don't-care in RX, same as MeshCore); `station_g2`, `gat562_30s`, `ikoka_nano_30dbm` and `promicro_sx1262` have only the DIO2/TXEN/RXEN transmit-receive switch; `rak3401_1watt`'s SKY66122 is enabled by a standalone always-on regulator outside the radio node; and non-SX126x radios (LR1110, LR2021, SX127x) never implement it. The pref is still saved when unsupported. **Do not expect the FEM's chip-enable to be the knob** — deasserting `antenna-enable-gpios` in RX shuts the part down and takes the through path with it (~69 dB measured on a V4.3), which is what 1.17.2 did before this moved to `lna-bypass-gpios`. |
|
||||
| `set rxduty <0\|1\|on\|off>` | | RX duty cycle mode *(reboot required)*. Window timing auto-sized per SF/BW/preamble from the SX126x datasheet constraints (boot log line `rxduty:` shows the result). Zero-loss guarantee assumes senders on preamble-32 firmware (current MeshCore at SF≤8); legacy preamble-16 senders are only caught ~50% worst-phase — keep off until the local mesh has converted. Presets with 16-symbol preambles (SF≥9) fall back to continuous RX automatically. |
|
||||
| `set adc.multiplier <mult>` | `0` (use board default) or 100–30000 | Battery voltage ADC calibration multiplier, set directly. Rejects non-numeric input, NaN/inf and negatives. |
|
||||
| `set adc.multiplier target <mv>` | 3000–4400 mV | Calibrate against a voltage you measured with a multimeter: rescales the current multiplier so the ADC reads `<mv>`. Replies with the old and new multiplier plus the before/after reading. `Error: no ADC reading on this board` if the board has no battery ADC. |
|
||||
|
||||
+54
-3
@@ -35,10 +35,61 @@ function(zephcore_apply_patches PATCH_DIR TARGET_DIR LABEL)
|
||||
file(READ "${_stamp}" _existing_hash)
|
||||
string(STRIP "${_existing_hash}" _existing_hash)
|
||||
if(_existing_hash STREQUAL _stamp_hash)
|
||||
message(STATUS " [${LABEL}] Patches already applied, skipping.")
|
||||
return()
|
||||
# The stamp hashes HEAD + patch contents and says nothing about the
|
||||
# working tree. A `git reset --hard` / `git checkout -- .` in the
|
||||
# target tree reverts our patched files but leaves this stamp behind
|
||||
# (it is untracked), and `west update` at an already-current
|
||||
# revision does not move HEAD -- so the hash still matches and the
|
||||
# build would skip patching and compile a pristine tree. Depending
|
||||
# on which patch was lost that is either a confusing configure-time
|
||||
# failure or, worse, a silently unpatched binary.
|
||||
#
|
||||
# Verify instead of trusting: every patch here modifies tracked
|
||||
# files only (none create or delete), so a patch that is applied
|
||||
# always leaves its own paths dirty against HEAD. Paths clean
|
||||
# against HEAD therefore means "not applied" exactly. Note this
|
||||
# deliberately does NOT compare tree content to the patch: a dirty
|
||||
# path is left alone, so the edit-the-driver-then-regenerate-the-
|
||||
# patch workflow still builds your in-progress edits untouched.
|
||||
set(_stamp_stale FALSE)
|
||||
foreach(_pf ${PATCH_FILES})
|
||||
execute_process(
|
||||
COMMAND git apply --numstat "${_pf}"
|
||||
WORKING_DIRECTORY "${TARGET_DIR}"
|
||||
OUTPUT_VARIABLE _vfy_numstat
|
||||
ERROR_QUIET
|
||||
)
|
||||
string(REGEX MATCHALL "[^\t\n]+\t[^\t\n]+\t[^\t\n]+"
|
||||
_vfy_lines "${_vfy_numstat}")
|
||||
set(_vfy_paths "")
|
||||
foreach(_line ${_vfy_lines})
|
||||
string(REGEX REPLACE "^[^\t]+\t[^\t]+\t" "" _p "${_line}")
|
||||
list(APPEND _vfy_paths "${_p}")
|
||||
endforeach()
|
||||
if(_vfy_paths)
|
||||
execute_process(
|
||||
COMMAND git diff --quiet -- ${_vfy_paths}
|
||||
WORKING_DIRECTORY "${TARGET_DIR}"
|
||||
RESULT_VARIABLE _vfy_clean
|
||||
OUTPUT_QUIET ERROR_QUIET
|
||||
)
|
||||
if(_vfy_clean EQUAL 0)
|
||||
get_filename_component(_vfy_name "${_pf}" NAME)
|
||||
message(STATUS
|
||||
" [${LABEL}] Stamp says applied but ${_vfy_name} "
|
||||
"is missing from the tree — re-applying all.")
|
||||
set(_stamp_stale TRUE)
|
||||
break()
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
if(NOT _stamp_stale)
|
||||
message(STATUS " [${LABEL}] Patches already applied, skipping.")
|
||||
return()
|
||||
endif()
|
||||
else()
|
||||
message(STATUS " [${LABEL}] Patch set changed, re-applying...")
|
||||
endif()
|
||||
message(STATUS " [${LABEL}] Patch set changed, re-applying...")
|
||||
endif()
|
||||
foreach(PATCH_FILE ${PATCH_FILES})
|
||||
get_filename_component(PATCH_NAME ${PATCH_FILE} NAME)
|
||||
|
||||
@@ -85,14 +85,15 @@ public:
|
||||
virtual bool setRxBoost(bool enable);
|
||||
bool isRxBoostEnabled() const { return _rx_boost_enabled; }
|
||||
|
||||
/* External FEM/LNA gain during RX. Boards carrying a front-end module
|
||||
* wire its chip-enable to the radio's antenna-enable line, which the
|
||||
* driver asserts for RX and TX alike; disabling this withholds it in the
|
||||
* RX direction only, trading the FEM's RX gain for its supply current.
|
||||
* The transmit path and the driver's idle gating are unaffected.
|
||||
* Returns false on radios that do not implement the knob (everything but
|
||||
* the native SX126x today); on an SX126x board with no FEM wired it
|
||||
* succeeds and does nothing, since there is no pin to withhold. */
|
||||
/* External FEM/LNA gain during RX. Boards whose front-end module has a
|
||||
* software-selectable receive path wire that select line to the radio
|
||||
* (lna-bypass-gpios); disabling this routes RX around the FEM's LNA,
|
||||
* trading its gain for its supply current while leaving the antenna
|
||||
* connected. The transmit path and the driver's idle gating are
|
||||
* unaffected. Returns false on radios that do not implement the knob
|
||||
* (everything but the native SX126x today) and on SX126x boards that
|
||||
* wire no such line -- including boards whose only FEM control is the
|
||||
* chip enable, where there is no gain to trade, only a path to cut. */
|
||||
virtual bool setFemRxEnable(bool enable) { (void)enable; return false; }
|
||||
|
||||
/* Multi-SF receive via LoRa side detectors. Only the LR2021 has them;
|
||||
|
||||
@@ -112,11 +112,13 @@ void SX126xRadio::hwSetRxBoost(bool enable)
|
||||
|
||||
bool SX126xRadio::setFemRxEnable(bool enable)
|
||||
{
|
||||
/* Pure driver-side flag -- no SPI, no chip state. It takes effect at the
|
||||
* next RX/TX/sleep transition, and startReceive() goes through one, so
|
||||
* there is nothing to re-apply here. Returns false when this board wires
|
||||
* no antenna-enable line, so the CLI reports "unsupported" rather than
|
||||
* acknowledging a setting that cannot do anything. */
|
||||
/* Pure driver-side -- no SPI, no chip state. The driver applies it to
|
||||
* lna-bypass-gpios straight away when the radio is parked in RX, and
|
||||
* otherwise at the next RX/TX/sleep transition. Returns false when this
|
||||
* board wires no receive-path select, so the CLI reports "unsupported"
|
||||
* rather than acknowledging a setting that cannot do anything -- note the
|
||||
* gate is that line, not antenna-enable-gpios, so a board with a FEM whose
|
||||
* only control is the chip enable (heltec_wifi_lora32_v4) lands here too. */
|
||||
if (!sx126x_set_fem_rx_enable(_dev, enable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ public:
|
||||
|
||||
void begin() override;
|
||||
|
||||
/* Gate the external FEM's LNA in the RX direction (radio.fem.rxgain).
|
||||
* Delegates to the driver, which owns the antenna-enable pin; returns
|
||||
* false on a board that has no such pin, i.e. no FEM to gate. */
|
||||
/* Select the external FEM's LNA or its bypass path for RX
|
||||
* (radio.fem.rxgain). Delegates to the driver, which owns the
|
||||
* receive-path select pin; returns false on a board that wires no such
|
||||
* pin, i.e. one whose FEM gain is not software-selectable. */
|
||||
bool setFemRxEnable(bool enable) override;
|
||||
|
||||
/* Duty-cycle preamble false-positive stats (SX126x-specific) */
|
||||
|
||||
@@ -223,29 +223,41 @@
|
||||
|
||||
/* External PA/FEM control (KCT8103L, V4.3):
|
||||
* GPIO2 = CSD (chip enable): driven HIGH during radio, LOW in sleep
|
||||
* GPIO5 = CTX (TX/RX select): HIGH during TX, LOW during RX/LNA
|
||||
* V4.3 CHANGE: CTX moved from GPIO46 (GC1109 CPS) to GPIO5 (KCT8103L CTX). */
|
||||
* GPIO5 = CTX (receive-path select): LOW = through the LNA,
|
||||
* HIGH = bypass. Held HIGH during TX, where the branch
|
||||
* is inert because DIO2/CPS has selected the TX path.
|
||||
* V4.3 CHANGE: CTX moved from GPIO46 (GC1109 CPS) to GPIO5 (KCT8103L CTX).
|
||||
* CTX is lna-bypass-gpios, NOT tx-enable-gpios: it enables nothing,
|
||||
* and binding it as a TX enable is what made `set radio.fem.rxgain
|
||||
* off` gate CSD instead in 1.17.2, shutting the FEM down and taking
|
||||
* the receive path with it (~69 dB, measured). */
|
||||
/*
|
||||
* ESP32_GPIO_SLEEP_HOLD_EN on both: light sleep powers down
|
||||
* RTC_PERIPH (see boards/common/pm_esp32.overlay for the full
|
||||
* chain), and GPIO2/GPIO5 are RTC-capable pads (0-21), so their
|
||||
* state is not retained across a sleep unless explicitly held.
|
||||
* These two lines are the FEM's entire control surface — CSD low
|
||||
* disables the PA and the LNA with it, CTX low parks the part in
|
||||
* its receive path — so losing them means transmitting through a
|
||||
* bypassed PA, tens of dB down, with RX sensitivity gone too,
|
||||
* while the node otherwise looks healthy and keeps relaying.
|
||||
* disables the PA and the LNA with it, CTX high bypasses the LNA —
|
||||
* so losing them means transmitting through a bypassed PA, tens of
|
||||
* dB down, with RX sensitivity gone too, while the node otherwise
|
||||
* looks healthy and keeps relaying.
|
||||
* The flag is ignored when CONFIG_PM=n, so this is inert on
|
||||
* builds that never light-sleep.
|
||||
*/
|
||||
antenna-enable-gpios = <&gpio0 2 (GPIO_ACTIVE_HIGH |
|
||||
ESP32_GPIO_SLEEP_HOLD_EN)>; /* KCT8103L CSD = GPIO2 */
|
||||
tx-enable-gpios = <&gpio0 5 (GPIO_ACTIVE_HIGH |
|
||||
lna-bypass-gpios = <&gpio0 5 (GPIO_ACTIVE_HIGH |
|
||||
ESP32_GPIO_SLEEP_HOLD_EN)>; /* KCT8103L CTX = GPIO5 */
|
||||
|
||||
/* DIO2 drives the SX1262 internal RF switch (TX path select).
|
||||
* Combined with tx-enable-gpios, this keeps both the internal and
|
||||
* external PA switch in sync with no software intervention needed. */
|
||||
/* DIO2 drives the SX1262 RF switch output, which on a KCT8103L
|
||||
* board is the part's CPS pin — the TX/RX path select. MeshCore's
|
||||
* heltec_v4 target defines CSD=2 and CTX=5 for the KCT8103L, no
|
||||
* CPS GPIO, and SX126X_DIO2_AS_RF_SWITCH=true; same on t096 and
|
||||
* tracker_v2. Note the V4.2 GC1109 inverts these two pin names —
|
||||
* there CPS is GPIO46 (an MCU pin, see heltec_wifi_lora32_v4) and
|
||||
* DIO2 drives the pin that part calls CTX. On the KCT8103L, DIO2
|
||||
* doing the path select is what leaves CTX free to mean nothing
|
||||
* but LNA-or-bypass. */
|
||||
dio2-tx-enable;
|
||||
|
||||
/* TCXO 1.8V via DIO3 — same as V4.2 */
|
||||
|
||||
@@ -43,8 +43,9 @@ esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash \
|
||||
| SX1262 DIO3 TCXO | SX1262 DIO3 | `dio3-tcxo-voltage = 1.8V` | MeshCore |
|
||||
| VFEM_Ctrl | GPIO7 | `fem_power` regulator | Heltec schematic, MeshCore |
|
||||
| PA_CSD | GPIO4 | `antenna-enable-gpios` | Heltec schematic, MeshCore |
|
||||
| PA_CTX | GPIO5 | `tx-enable-gpios` | Heltec schematic, MeshCore |
|
||||
| PA_CPS | GPIO46 | Unused/unclaimed | Heltec schematic/pin map |
|
||||
| PA_CTX | GPIO5 | `lna-bypass-gpios` | Heltec schematic, MeshCore |
|
||||
| PA_CPS | SX1262 DIO2 | `dio2-tx-enable` | MeshCore (`SX126X_DIO2_AS_RF_SWITCH`) |
|
||||
| GPIO46 | — | Unused/unclaimed (V4.2 pin-map carry-over, see notes) | Heltec pin map |
|
||||
| UC6580 GNSS UART | RX GPIO33, TX GPIO34 | `&uart2`, `gnss-nmea-generic` | Heltec schematic, MeshCore |
|
||||
| GNSS_RST | GPIO35 | `gps-enable` alias, active-high run | Heltec schematic, MeshCore |
|
||||
| GNSS PPS | GPIO36 | Unused input | Heltec schematic |
|
||||
@@ -72,8 +73,10 @@ esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash \
|
||||
physical Wireless Tracker V2.
|
||||
- Battery ADC is on ESP32-S3 ADC unit 1 (`&adc0`) channel 0, GPIO1. ADC unit 2
|
||||
channel 0 is GPIO11 and must stay disabled because GPIO11 is LoRa MISO.
|
||||
- MeshCore's V2 target drives `PA_CSD` and `PA_CTX` but does not drive
|
||||
`PA_CPS`/GPIO46. ZephCore leaves GPIO46 unclaimed pending hardware validation
|
||||
of that KCT8103L control path.
|
||||
- MeshCore's V2 target drives `PA_CSD` and `PA_CTX` only, defines no `PA_CPS`
|
||||
GPIO, and sets `SX126X_DIO2_AS_RF_SWITCH=true`. On a KCT8103L the CPS pin is
|
||||
the SX1262 DIO2 net, so it is not an MCU line here; GPIO46 carries the
|
||||
`PA_CPS` label only as a carry-over from the V4.2/GC1109 pin map, where the
|
||||
two FEM pin names swap roles. ZephCore leaves GPIO46 unclaimed.
|
||||
- `GNSS_RST` is exposed through ZephCore's `gps-enable` alias as an active-high
|
||||
logical "run" line: high releases reset, low holds UC6580 off.
|
||||
|
||||
+24
-13
@@ -14,9 +14,12 @@
|
||||
* - Heltec Wireless Tracker V2.3 schematic and V2 datasheet
|
||||
* - MeshCore variants/heltec_tracker_v2
|
||||
*
|
||||
* PA_CPS appears on GPIO46 in the Heltec pin map/schematic but MeshCore's V2
|
||||
* variant does not drive it. ZephCore leaves GPIO46 unclaimed until the exact
|
||||
* KCT8103L CPS behavior for this revision is confirmed.
|
||||
* GPIO46 appears as "PA_CPS" in the Heltec pin map. It is not a FEM control
|
||||
* on this board and is left unclaimed. That label is a carry-over from the
|
||||
* V4.2 map, where the GC1109's CPS genuinely is GPIO46; on a KCT8103L the two
|
||||
* pin names swap roles and CPS is the SX1262 DIO2 net (dio2-tx-enable), which
|
||||
* is why MeshCore's heltec_tracker_v2 target defines no CPS GPIO and sets
|
||||
* SX126X_DIO2_AS_RF_SWITCH=true. See the FEM comment on the &lora node.
|
||||
*/
|
||||
|
||||
/dts-v1/;
|
||||
@@ -250,23 +253,31 @@
|
||||
busy-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
|
||||
dio1-gpios = <&gpio0 14 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;
|
||||
|
||||
/* KCT8103L control per MeshCore:
|
||||
* - PA_CSD/GPIO4: active-HIGH FEM enable
|
||||
* - PA_CTX/GPIO5: HIGH=TX, LOW=RX/LNA
|
||||
* - SX1262 DIO2: internal RF switch TX select */
|
||||
/* KCT8103L control per MeshCore (variants/heltec_tracker_v2/
|
||||
* LoRaFEMControl.cpp):
|
||||
* - PA_CSD/GPIO4: active-HIGH FEM enable, low only in sleep
|
||||
* - PA_CTX/GPIO5: receive-path select, LOW=LNA, HIGH=bypass;
|
||||
* held HIGH in TX, where the receive branch is inert
|
||||
* - SX1262 DIO2 -> KCT8103L CPS: TX/RX path select
|
||||
* CTX is lna-bypass-gpios, not tx-enable-gpios: it enables
|
||||
* nothing. MeshCore's target defines CSD=4 and CTX=5, defines
|
||||
* no CPS GPIO, and sets SX126X_DIO2_AS_RF_SWITCH=true -- so the
|
||||
* path select is DIO2 in hardware, which is what leaves CTX
|
||||
* free to mean LNA-or-bypass. */
|
||||
/*
|
||||
* ESP32_GPIO_SLEEP_HOLD_EN on both: light sleep powers down
|
||||
* RTC_PERIPH (chain documented in boards/common/pm_esp32.overlay)
|
||||
* and GPIO4/GPIO5 are RTC-capable pads (0-21), so without the
|
||||
* hold the FEM comes out of a sleep disabled and parked in its
|
||||
* receive path — transmit goes out through a bypassed PA, tens of
|
||||
* dB down, and RX loses the LNA, while the node keeps relaying
|
||||
* and looks healthy. Inert when CONFIG_PM=n.
|
||||
* hold the FEM comes out of a sleep disabled and with an
|
||||
* indeterminate receive path — transmit goes out through a
|
||||
* bypassed PA, tens of dB down, and RX may lose the LNA, while
|
||||
* the node keeps relaying and looks healthy. Inert when
|
||||
* CONFIG_PM=n.
|
||||
*/
|
||||
antenna-enable-gpios = <&gpio0 4 (GPIO_ACTIVE_HIGH |
|
||||
ESP32_GPIO_SLEEP_HOLD_EN)>;
|
||||
tx-enable-gpios = <&gpio0 5 (GPIO_ACTIVE_HIGH |
|
||||
ESP32_GPIO_SLEEP_HOLD_EN)>;
|
||||
lna-bypass-gpios = <&gpio0 5 (GPIO_ACTIVE_HIGH |
|
||||
ESP32_GPIO_SLEEP_HOLD_EN)>;
|
||||
dio2-tx-enable;
|
||||
|
||||
dio3-tcxo-voltage = <SX126X_DIO3_TCXO_1V8>;
|
||||
|
||||
@@ -31,7 +31,7 @@ Sources: official Heltec Mesh Node T096 datasheet/schematic, with MeshCore `vari
|
||||
| LoRa DIO1 | P0.21 | DIO1 | `dio1-gpios` |
|
||||
| SX1262 DIO2 / FEM CPS | SX1262 DIO2 | PA_CPS | `dio2-tx-enable` |
|
||||
| FEM enable | P0.12 | PA_CSD | `antenna-enable-gpios` |
|
||||
| FEM TX select | P1.09 | PA_CTX | `tx-enable-gpios` |
|
||||
| FEM RX path select | P1.09 | PA_CTX | `lna-bypass-gpios` |
|
||||
| FEM rail enable | P0.30 | VFEM_Ctrl | fixed regulator `vfem_enable` |
|
||||
| GNSS UART RX into MCU | P0.23 | GNSS_TX | `uart0` RX |
|
||||
| GNSS UART TX from MCU | P0.25 | GNSS_RX | `uart0` TX |
|
||||
|
||||
@@ -333,15 +333,17 @@
|
||||
|
||||
/*
|
||||
* KCT8103L PA/FEM control:
|
||||
* - PA_CSD P0.12 -> CSD, active-HIGH FEM enable.
|
||||
* - PA_CTX P1.09 -> CTX, HIGH for TX in the existing ZephCore
|
||||
* SX1262 tx-enable model. MeshCore uses the same net for
|
||||
* TX/LNA-bypass control.
|
||||
* - PA_CSD P0.12 -> CSD, active-HIGH FEM enable, low only in sleep.
|
||||
* - PA_CTX P1.09 -> CTX, the receive-path select: LOW routes RX
|
||||
* through the LNA, HIGH bypasses it. Held HIGH in TX, where
|
||||
* the receive branch is inert. This is lna-bypass-gpios, not
|
||||
* tx-enable-gpios — it enables nothing.
|
||||
* - PA_CPS is not an MCU GPIO on T096; it is the SX1262 DIO2
|
||||
* net into the KCT8103L CPS pin, so use dio2-tx-enable.
|
||||
* net into the KCT8103L CPS pin, so use dio2-tx-enable. That
|
||||
* is what does the TX/RX switching and leaves CTX free.
|
||||
*/
|
||||
antenna-enable-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; /* PA_CSD */
|
||||
tx-enable-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; /* PA_CTX */
|
||||
lna-bypass-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; /* PA_CTX */
|
||||
dio2-tx-enable; /* PA_CPS */
|
||||
|
||||
dio3-tcxo-voltage = <SX126X_DIO3_TCXO_1V8>;
|
||||
|
||||
@@ -51,22 +51,28 @@ bool sx126x_is_receiving(const struct device *dev);
|
||||
void sx126x_set_rx_boost(const struct device *dev, bool enable);
|
||||
|
||||
/**
|
||||
* @brief Enable/disable an external FEM's LNA in the RX direction
|
||||
* @brief Select an external FEM's LNA or its bypass path for RX
|
||||
*
|
||||
* Boards with an external front-end module wire its chip-enable to
|
||||
* antenna-enable-gpios (KCT8103L CSD, SKY66122 CSD+CPS). The driver normally
|
||||
* asserts that line for both RX and TX. Passing false withholds it for RX
|
||||
* only: the FEM's LNA and its supply current (~6.5 mA on SKY66122) drop out,
|
||||
* at the cost of the FEM's RX gain (~16 dB on the same part). TX always
|
||||
* asserts the line, and sleep/idle always clears it, so neither the transmit
|
||||
* path nor the driver's idle gating is affected.
|
||||
* Acts on lna-bypass-gpios, the FEM's receive-path select -- KCT8103L CTX on
|
||||
* the Heltec boards, where DIO2 into the FEM's CPS pin does the TX/RX
|
||||
* switching and leaves this line meaning nothing but "LNA or bypass".
|
||||
* Passing false routes RX around the LNA: its gain (~17 dB measured) and its
|
||||
* supply current both drop out, but the antenna stays connected to the
|
||||
* receiver. TX and the driver's idle/sleep gating are unaffected.
|
||||
*
|
||||
* Default is enabled.
|
||||
* This is deliberately NOT antenna-enable-gpios. That line is the FEM's chip
|
||||
* enable, owned by the RX/TX/sleep state machine; deasserting it during RX
|
||||
* shuts the part down, and a shut-down FEM passes nothing -- the node goes
|
||||
* deaf by tens of dB rather than losing the LNA's share. ZephCore 1.17.2
|
||||
* shipped that mistake; see the KCT8103L handling in MeshCore's
|
||||
* variants/heltec_v4/LoRaFEMControl.cpp for the reference behaviour.
|
||||
*
|
||||
* Default is enabled (LNA in the path).
|
||||
*
|
||||
* @param dev LoRa device
|
||||
* @param enable true to keep the FEM active during RX
|
||||
* @return true if this board has an antenna-enable line to gate, false if it
|
||||
* has none (in which case the call did nothing)
|
||||
* @param enable true for the LNA path, false for the bypass path
|
||||
* @return true if this board wires a receive-path select, false if it has
|
||||
* none (in which case the call did nothing)
|
||||
*/
|
||||
bool sx126x_set_fem_rx_enable(const struct device *dev, bool enable);
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
Subject: [PATCH] lora: sx126x: ZephCore native driver
|
||||
|
||||
Sole owner of every ZephCore change under drivers/lora/native/sx126x/ --
|
||||
sx126x.c, sx126x.h, sx126x_hal.c, sx126x_hal_common.c and sx126x_regs.h. No
|
||||
other patch in this directory touches those files; see "Why this is one patch"
|
||||
at the end. The extension-API header sx126x_ext.h is not patched in, it is
|
||||
copied from patches/zephyr-new/.
|
||||
sx126x.c, sx126x.h, sx126x_hal.c, sx126x_hal.h, sx126x_hal_common.c,
|
||||
sx126x_hal_stm32wl.c and sx126x_regs.h -- plus the driver's devicetree binding
|
||||
dts/bindings/lora/semtech,sx126x-base.yaml, which it extends with
|
||||
lna-bypass-gpios (see "FEM receive-path select" below). No other patch in this
|
||||
directory touches those files; see "Why this is one patch" at the end. The
|
||||
extension-API header sx126x_ext.h is not patched in, it is copied from
|
||||
patches/zephyr-new/.
|
||||
|
||||
== Base driver ==
|
||||
|
||||
@@ -38,24 +41,16 @@ top of upstream:
|
||||
C++ caller. sx126x_lora_recv_async() has an idempotent fast path, so the
|
||||
failure-path startReceive() on that side is a no-op.
|
||||
|
||||
* External FEM/LNA RX gating -- sx126x_set_fem_rx_enable() plus the
|
||||
fem_rx_off flag it sets. antenna-enable-gpios is the chip-enable line of
|
||||
the external FEM on the boards that route it there (KCT8103L CSD on
|
||||
heltec_t096, heltec_wireless_tracker_v2 and heltec_wifi_lora32_v43;
|
||||
GC1109 CSD on heltec_wifi_lora32_v4), and the driver asserts it for RX and
|
||||
TX alike. With the flag set, sx126x_set_rf_path() withholds it in the RX
|
||||
direction only, so the FEM's LNA and its supply current drop out while TX
|
||||
still gets the PA path and sleep/idle keeps clearing the line as before.
|
||||
The setter returns false where the board declares no antenna-enable line,
|
||||
so the CLI can report that rather than acknowledging a no-op -- that is
|
||||
the rak3401_1watt case, where the SKY66122 enable is a standalone
|
||||
regulator-boot-on rail outside the radio node.
|
||||
|
||||
The pin stays driver-owned throughout. Taking it over from devicetree
|
||||
instead would cost the idle gating, drop the ESP32 sleep-hold flags those
|
||||
nodes carry, and on the boards whose CTX net is a separate tx-enable-gpios
|
||||
it would desynchronise the external switch from DIO2. Backs
|
||||
`set radio.fem.rxgain` (default on = historical behaviour).
|
||||
* External FEM receive-path select -- the lna-bypass-gpios devicetree
|
||||
property, sx126x_set_fem_rx_enable() and the fem_rx_off flag it sets.
|
||||
The line is the FEM's LNA-or-bypass select (KCT8103L CTX on heltec_t096,
|
||||
heltec_wireless_tracker_v2 and heltec_wifi_lora32_v43), driven as
|
||||
(tx || fem_rx_off) and deliberately independent of the chip enable.
|
||||
Backs `set radio.fem.rxgain` (default on = LNA in the path = historical
|
||||
behaviour). Read the "FEM receive-path select" section below before
|
||||
touching any of it: this is emphatically NOT antenna-enable-gpios, and
|
||||
an earlier implementation that gated that pin instead shipped in 1.17.2
|
||||
and cost roughly 69 dB of receive sensitivity.
|
||||
|
||||
* Extension API for the C++ adapter (sx126x_ext.h): sx126x_is_chip_busy,
|
||||
sx126x_is_receiving, sx126x_set_rx_boost, sx126x_set_fem_rx_enable,
|
||||
@@ -387,6 +382,85 @@ transmit side. The branch is now gated on state != SX126X_STATE_TX; a Tx
|
||||
timeout falls through to the tail, which sleeps the chip (restoring REST_STATE)
|
||||
and reports -ETIMEDOUT, and the C++ failure path re-arms RX.
|
||||
|
||||
== FEM receive-path select (lna-bypass-gpios) ==
|
||||
|
||||
Adds a devicetree property, lna-bypass-gpios, and the driver plumbing behind
|
||||
sx126x_set_fem_rx_enable() -- the radio.fem.rxgain CLI knob. This replaces the
|
||||
first implementation of that knob, which acted on the wrong pin and shipped in
|
||||
1.17.2.
|
||||
|
||||
Three of our boards carry a KCT8103L (t096, wireless_tracker_v2,
|
||||
wifi_lora32_v43), and MeshCore's LoRaFEMControl.cpp for each of them is the
|
||||
reference for what its two MCU lines mean:
|
||||
|
||||
CSD chip enable. HIGH whenever the radio is up; LOW only in sleep --
|
||||
MeshCore's setSleepModeEnable() drives exactly this pin and nothing
|
||||
else.
|
||||
CTX receive-path select. LOW routes RX through the LNA, HIGH bypasses it
|
||||
(variants/heltec_v4/LoRaFEMControl.cpp spells both branches out in
|
||||
comments). Held HIGH during TX, where the receive branch is inert.
|
||||
CPS TX/RX path select -- not an MCU GPIO on any KCT8103L board. It is the
|
||||
SX1262 DIO2 net, which is why every one of MeshCore's KCT8103L targets
|
||||
sets SX126X_DIO2_AS_RF_SWITCH=true and defines no CPS pin at all
|
||||
(t096 CSD=12/CTX=41, tracker_v2 CSD=4/CTX=5, heltec_v4 CSD=2/CTX=5),
|
||||
and why we set dio2-tx-enable on all three.
|
||||
|
||||
Beware the naming inversion between the two Heltec front ends: on the GC1109
|
||||
(V4.2) it is *CTX* that hangs off DIO2 and *CPS* that is an MCU pin (GPIO46,
|
||||
"HIGH=full PA, LOW=bypass", don't-care in RX), which is why heltec_wifi_lora32_v4
|
||||
keeps GPIO46 on tx-enable-gpios and has no LNA knob. On the KCT8103L the two
|
||||
names swap roles. Where GPIO46 still appears as "PA_CPS" in a Heltec pin map
|
||||
for a KCT8103L board, it is a carry-over from that V4.2 map, not a FEM control:
|
||||
MeshCore's tracker_v2 target does not define or drive it.
|
||||
|
||||
Because DIO2/CPS does the path switching in hardware, CTX enables nothing. Its
|
||||
only software-visible job is LNA-or-bypass. Binding it as tx-enable-gpios
|
||||
produced the right waveform by coincidence -- tx_enable is driven (enable &&
|
||||
tx), which matches "HIGH in TX, LOW in RX with the LNA" -- and that coincidence
|
||||
is what sent the first implementation of the knob to the other pin: with CTX
|
||||
apparently spoken for, "FEM RX gain off" became
|
||||
|
||||
sx126x_hal_set_antenna_enable(dev, enable && (tx || !data->fem_rx_off));
|
||||
|
||||
i.e. CSD low during RX. That is MeshCore's setSleepModeEnable(), not its
|
||||
LNA-off state. A shut-down FEM has no through path: the loss is the LNA's gain
|
||||
plus the off-state isolation of the part, measured on a V4.3 at roughly 69 dB
|
||||
(-17 dBm to -86 dBm on a 4 m link) against the ~17 dB the bypass path costs.
|
||||
Reported by a user who could not raise their own repeater from more than 50 m
|
||||
away after turning the knob off.
|
||||
|
||||
So the pin moves to a property named for what it is. antenna_enable goes back
|
||||
to plain `enable` -- the RX/TX/sleep state machine owns it -- and the driver
|
||||
drives lna_bypass as (tx || fem_rx_off), deliberately independent of `enable`:
|
||||
while CSD is deasserted this line only decides which path the FEM will select
|
||||
when it comes back, and leaving it in the receive state across sleep is what
|
||||
lets an ESP32 GPIO sleep-hold keep the RX path intact.
|
||||
|
||||
Support is per-board and automatic: sx126x_set_fem_rx_enable() reports false
|
||||
when lna_bypass.port is NULL, so the CLI answers "Error: unsupported" on every
|
||||
board whose FEM gain is not software-selectable. That correctly includes
|
||||
wifi_lora32_v4 (GC1109, whose CPS is don't-care in RX -- MeshCore's
|
||||
LoRaFEMControl leaves lna_can_control false on that branch) and excludes, by
|
||||
construction, boards with a genuine TXEN on tx-enable-gpios (lilygo_tlora_c6,
|
||||
lilygo_timpulse_plus), where asserting a line during RX would key the PA into a
|
||||
receive window.
|
||||
|
||||
Audited against MeshCore 2026-08-25. It ships LoRaFEMControl for six variants;
|
||||
exactly four expose an LNA knob -- heltec_t096, heltec_tracker_v2, heltec_v4
|
||||
(KCT8103L branch only) and heltec_v4_r8, all KCT8103L, plus station_g3_esp32 on
|
||||
a different part (P_PRIMARY_LNA_EN). heltec_tower_v2 uses a KCT8103L but keeps
|
||||
the same net as a plain TX/RX select and exposes nothing. Our three
|
||||
lna-bypass-gpios boards map onto MeshCore's KCT8103L set exactly; we ship no
|
||||
board where we claim the knob and MeshCore does not, so there is nothing to
|
||||
scrub. heltec_v4_r8, heltec_tower_v2 and station_g3 have no ZephCore board.
|
||||
|
||||
The knob also applies immediately now, under data->lock -- the same lock
|
||||
sx126x_set_rf_path()'s callers hold, since this runs on the CLI thread and an
|
||||
unlocked state read of RX can be overtaken by send_async() taking the chip to
|
||||
TX. It is a bare GPIO with no SPI and no chip state behind it, and a node
|
||||
parked in continuous RX may not make an RX/TX/sleep transition for minutes --
|
||||
long enough for the CLI to acknowledge a change the radio had not made.
|
||||
|
||||
== Why this is one patch ==
|
||||
|
||||
Five of the sections above shipped as separate files (0011 band-rssi-cal, 0012
|
||||
@@ -423,12 +497,13 @@ filenames into the section headings above.
|
||||
|
||||
Regenerate with:
|
||||
|
||||
git -C zephyr diff -- drivers/lora/native/sx126x/
|
||||
git -C zephyr diff -- drivers/lora/native/sx126x/ \
|
||||
'dts/bindings/lora/semtech,sx126x-base.yaml'
|
||||
|
||||
appended to this preamble.
|
||||
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
|
||||
index 30243ba5dc7..3a8bc017a52 100644
|
||||
index 30243ba5dc7..0e870e37468 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.c
|
||||
@@ -6,72 +6,218 @@
|
||||
@@ -1110,7 +1185,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
LOG_INF("SX126x initialized");
|
||||
return 0;
|
||||
}
|
||||
@@ -398,7 +892,7 @@ static void sx126x_dio1_callback(const struct device *dev)
|
||||
@@ -398,15 +892,44 @@ static void sx126x_dio1_callback(const struct device *dev)
|
||||
{
|
||||
struct sx126x_data *data = dev->data;
|
||||
|
||||
@@ -1119,18 +1194,27 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
}
|
||||
|
||||
static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx)
|
||||
@@ -406,7 +900,26 @@ static void sx126x_set_rf_path(const struct device *dev, bool enable, bool tx)
|
||||
{
|
||||
const struct sx126x_hal_config *config = dev->config;
|
||||
+ struct sx126x_data *data = dev->data;
|
||||
|
||||
- sx126x_hal_set_antenna_enable(dev, enable);
|
||||
sx126x_hal_set_antenna_enable(dev, enable);
|
||||
- if (!config->dio2_tx_enable) {
|
||||
+ /* An external FEM's enable line is asserted for TX unconditionally. For
|
||||
+ * RX it follows fem_rx_off (the radio.fem.rxgain pref, applied through
|
||||
+ * sx126x_set_fem_rx_enable): clearing it drops the FEM's LNA and its
|
||||
+ * supply current, at the cost of that gain. `enable` still wins, so
|
||||
+ * sleep and idle keep gating the line exactly as they did before. */
|
||||
+ sx126x_hal_set_antenna_enable(dev, enable && (tx || !data->fem_rx_off));
|
||||
+
|
||||
+ /* FEM receive-path select, on boards that wire one (lna-bypass-gpios).
|
||||
+ * HIGH = bypass, LOW = through the LNA. Deliberately independent of
|
||||
+ * `enable`: the FEM's own chip-enable is antenna_enable, so while that
|
||||
+ * is deasserted this line only decides which path the part will select
|
||||
+ * when it comes back. Leaving it in the receive state across sleep is
|
||||
+ * what lets an ESP32 GPIO sleep-hold keep the RX path intact.
|
||||
+ *
|
||||
+ * TX drives it HIGH for the same reason the FEM vendors do: with the
|
||||
+ * TX/RX path itself selected elsewhere (DIO2 into CPS), bypass is the
|
||||
+ * inert state for the receive branch while the PA is keyed. */
|
||||
+ if (config->lna_bypass.port != NULL) {
|
||||
+ gpio_pin_set_dt(&config->lna_bypass, tx || data->fem_rx_off);
|
||||
+ }
|
||||
+
|
||||
+ if (config->dio2_tx_enable) {
|
||||
+ /* DIO2 handles TX enable in hardware -- but rx-enable-gpios
|
||||
+ * (e.g. E22-900M30S RXEN) and some board-specific tx-enable
|
||||
@@ -1148,7 +1232,25 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
sx126x_hal_set_rf_switch(dev, enable, tx);
|
||||
}
|
||||
}
|
||||
@@ -455,6 +962,228 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev)
|
||||
@@ -426,6 +949,7 @@ static void sx126x_disconnect_rf_gpios(const struct device *dev)
|
||||
sx126x_disconnect_gpio(&config->antenna_enable);
|
||||
sx126x_disconnect_gpio(&config->tx_enable);
|
||||
sx126x_disconnect_gpio(&config->rx_enable);
|
||||
+ sx126x_disconnect_gpio(&config->lna_bypass);
|
||||
}
|
||||
|
||||
static int sx126x_reconnect_rf_gpios(const struct device *dev)
|
||||
@@ -451,10 +975,238 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev)
|
||||
return ret;
|
||||
}
|
||||
|
||||
+ ret = sx126x_hal_configure_gpio(&config->lna_bypass,
|
||||
+ GPIO_OUTPUT_INACTIVE, "FEM LNA bypass");
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
return 0;
|
||||
}
|
||||
#endif /* CONFIG_PM_DEVICE */
|
||||
|
||||
@@ -1377,7 +1479,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
static int sx126x_set_sleep(const struct device *dev)
|
||||
{
|
||||
struct sx126x_data *data = dev->data;
|
||||
@@ -533,8 +1262,16 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
@@ -533,8 +1285,16 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
struct sx126x_data *data = dev->data;
|
||||
struct sx126x_rx_result result = { 0 };
|
||||
uint8_t payload_len = 0, offset = 0;
|
||||
@@ -1394,7 +1496,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
/* Get received packet info */
|
||||
ret = sx126x_get_rx_buffer_status(dev, &payload_len, &offset);
|
||||
if (ret < 0) {
|
||||
@@ -544,9 +1281,26 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
@@ -544,9 +1304,26 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
/* Get signal quality */
|
||||
sx126x_get_packet_status(dev, &result.rssi, &result.snr);
|
||||
|
||||
@@ -1424,7 +1526,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
result.status = -EIO;
|
||||
} else {
|
||||
/* Read payload into shared buffer */
|
||||
@@ -564,23 +1318,55 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
@@ -564,23 +1341,55 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1493,7 +1595,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
sx126x_set_sleep(dev);
|
||||
k_msgq_put(&data->rx_msgq, &result, K_NO_WAIT);
|
||||
}
|
||||
@@ -589,8 +1375,83 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
@@ -589,8 +1398,83 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta
|
||||
static void sx126x_handle_irq_timeout(const struct device *dev)
|
||||
{
|
||||
struct sx126x_data *data = dev->data;
|
||||
@@ -1577,7 +1679,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
sx126x_set_sleep(dev);
|
||||
|
||||
if (data->tx_async_signal != NULL) {
|
||||
@@ -633,10 +1494,54 @@ static void sx126x_irq_work_handler(struct k_work *work)
|
||||
@@ -633,10 +1517,54 @@ static void sx126x_irq_work_handler(struct k_work *work)
|
||||
sx126x_handle_irq_rx_done(dev, irq_status);
|
||||
}
|
||||
|
||||
@@ -1632,7 +1734,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
/* Re-enable the DIO1 interrupt for the next event (unless sleeping) */
|
||||
if (atomic_get(&data->state) != SX126X_REST_STATE) {
|
||||
sx126x_hal_dio1_irq_enable(dev);
|
||||
@@ -648,6 +1553,7 @@ static int sx126x_lora_config(const struct device *dev,
|
||||
@@ -648,6 +1576,7 @@ static int sx126x_lora_config(const struct device *dev,
|
||||
{
|
||||
struct sx126x_data *data = dev->data;
|
||||
const struct sx126x_hal_config *hal_config = dev->config;
|
||||
@@ -1640,7 +1742,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
bool ldro;
|
||||
int ret;
|
||||
|
||||
@@ -679,25 +1585,45 @@ static int sx126x_lora_config(const struct device *dev,
|
||||
@@ -679,25 +1608,45 @@ static int sx126x_lora_config(const struct device *dev,
|
||||
goto out;
|
||||
}
|
||||
|
||||
@@ -1688,7 +1790,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
/* Set sync word */
|
||||
ret = sx126x_set_sync_word(dev, config->public_network);
|
||||
if (ret < 0) {
|
||||
@@ -721,6 +1647,10 @@ out:
|
||||
@@ -721,6 +1670,10 @@ out:
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1699,7 +1801,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
static int sx126x_lora_send_async(const struct device *dev,
|
||||
uint8_t *data_buf, uint32_t data_len,
|
||||
struct k_poll_signal *async)
|
||||
@@ -738,11 +1668,27 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
@@ -738,11 +1691,27 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
@@ -1729,7 +1831,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
k_mutex_lock(&data->lock, K_FOREVER);
|
||||
|
||||
ret = sx126x_ensure_ready(dev);
|
||||
@@ -752,6 +1698,59 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
@@ -752,6 +1721,59 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1789,7 +1891,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
data->tx_async_signal = async;
|
||||
k_msgq_purge(&data->tx_msgq);
|
||||
|
||||
@@ -777,8 +1776,99 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
@@ -777,8 +1799,99 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
/* Enable antenna and set TX path */
|
||||
sx126x_set_rf_path(dev, true, true);
|
||||
|
||||
@@ -1891,7 +1993,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
if (ret < 0) {
|
||||
goto out_error;
|
||||
}
|
||||
@@ -847,6 +1937,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf,
|
||||
@@ -847,6 +1960,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf,
|
||||
}
|
||||
|
||||
data->rx_cb = NULL;
|
||||
@@ -1900,7 +2002,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
k_msgq_purge(&data->rx_msgq);
|
||||
|
||||
/* Set packet parameters for variable length reception */
|
||||
@@ -918,6 +2010,8 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
@@ -918,6 +2033,8 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
/* Stop async reception */
|
||||
data->rx_cb = NULL;
|
||||
data->rx_cb_user_data = NULL;
|
||||
@@ -1909,7 +2011,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
if (atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) {
|
||||
sx126x_set_standby(dev, SX126X_STANDBY_RC);
|
||||
sx126x_set_sleep(dev);
|
||||
@@ -932,6 +2026,19 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
@@ -932,6 +2049,19 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
@@ -1929,7 +2031,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_RX)) {
|
||||
LOG_ERR("Busy");
|
||||
k_mutex_unlock(&data->lock);
|
||||
@@ -945,8 +2052,18 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
@@ -945,8 +2075,18 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1948,7 +2050,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
|
||||
/* Set packet parameters */
|
||||
ret = sx126x_set_packet_params(dev,
|
||||
@@ -959,6 +2076,7 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
@@ -959,6 +2099,7 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
SX126X_LORA_IQ_INVERTED : SX126X_LORA_IQ_STANDARD);
|
||||
if (ret < 0) {
|
||||
data->rx_cb = NULL;
|
||||
@@ -1956,7 +2058,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
sx126x_set_sleep(dev);
|
||||
k_mutex_unlock(&data->lock);
|
||||
return ret;
|
||||
@@ -971,11 +2089,21 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
@@ -971,11 +2112,21 @@ static int sx126x_lora_recv_async(const struct device *dev,
|
||||
ret = sx126x_set_rx(dev, 0);
|
||||
if (ret < 0) {
|
||||
data->rx_cb = NULL;
|
||||
@@ -1978,7 +2080,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
k_mutex_unlock(&data->lock);
|
||||
return 0;
|
||||
}
|
||||
@@ -993,7 +2121,9 @@ static uint32_t sx126x_lora_airtime(const struct device *dev, uint32_t data_len)
|
||||
@@ -993,7 +2144,9 @@ static uint32_t sx126x_lora_airtime(const struct device *dev, uint32_t data_len)
|
||||
}
|
||||
|
||||
/* Calculate symbol time in microseconds */
|
||||
@@ -1989,7 +2091,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
sf = data->config.datarate;
|
||||
|
||||
/* Symbol time = 2^SF / BW (seconds) */
|
||||
@@ -1051,7 +2181,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
|
||||
@@ -1051,7 +2204,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
|
||||
|
||||
/* Set PA config and TX power */
|
||||
ret = sx126x_hal_configure_tx_params(dev, tx_power, frequency,
|
||||
@@ -1998,7 +2100,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
if (ret < 0) {
|
||||
sx126x_set_sleep(dev);
|
||||
k_mutex_unlock(&data->lock);
|
||||
@@ -1083,14 +2213,828 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
|
||||
@@ -1083,14 +2236,855 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2158,15 +2260,42 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
+ struct sx126x_data *data = dev->data;
|
||||
+ const struct sx126x_hal_config *config = dev->config;
|
||||
+
|
||||
+ /* Stored inverted so the zero-initialised default is "FEM on in RX",
|
||||
+ * which is what the driver did before this knob existed. No SPI and no
|
||||
+ * chip state: sx126x_set_rf_path() reads it on the next RX/TX/sleep
|
||||
+ * transition, and every RX (re)start passes through there. */
|
||||
+ /* False when the board wires no receive-path select: there is no FEM
|
||||
+ * RX gain to trade away, so the caller can say so instead of
|
||||
+ * acknowledging a no-op. Boards whose only FEM line is the chip
|
||||
+ * enable land here too -- dropping that would not lower the gain, it
|
||||
+ * would shut the part down and open the receive path entirely. */
|
||||
+ if (config->lna_bypass.port == NULL) {
|
||||
+ /* Still record it: a board can gain the line later without the
|
||||
+ * caller having to re-apply the pref. */
|
||||
+ data->fem_rx_off = !enable;
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ /* Under data->lock, which is what sx126x_set_rf_path()'s callers hold.
|
||||
+ * This runs on the CLI thread: without it, a state read of RX can be
|
||||
+ * overtaken by send_async() taking the chip to TX and driving the line
|
||||
+ * HIGH, after which the write below pulls it back LOW and leaves the
|
||||
+ * FEM's LNA in circuit for the whole transmission. */
|
||||
+ k_mutex_lock(&data->lock, K_FOREVER);
|
||||
+
|
||||
+ /* Stored inverted so the zero-initialised default is "LNA in the RX
|
||||
+ * path", which is what the driver did before this knob existed. */
|
||||
+ data->fem_rx_off = !enable;
|
||||
+
|
||||
+ /* False when the board wires no antenna-enable line: there is no FEM to
|
||||
+ * gate, so the caller can say so instead of acknowledging a no-op. */
|
||||
+ return config->antenna_enable.port != NULL;
|
||||
+ /* Apply now rather than at the next sx126x_set_rf_path(). This is a
|
||||
+ * bare GPIO with no SPI and no chip state behind it, and a node parked
|
||||
+ * in continuous RX may not transition for minutes -- long enough for
|
||||
+ * the CLI to have acknowledged a change the radio had not made. Only
|
||||
+ * in RX: every other state either drives the line itself on the way in
|
||||
+ * (TX) or wants it left where sleep parked it. */
|
||||
+ if (atomic_get(&data->state) == SX126X_STATE_RX) {
|
||||
+ gpio_pin_set_dt(&config->lna_bypass, data->fem_rx_off);
|
||||
+ }
|
||||
+
|
||||
+ k_mutex_unlock(&data->lock);
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+void sx126x_reset_agc(const struct device *dev)
|
||||
@@ -2834,7 +2963,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
};
|
||||
|
||||
#ifdef CONFIG_PM_DEVICE
|
||||
@@ -1112,6 +3040,7 @@ static int sx126x_pm_action(const struct device *dev,
|
||||
@@ -1112,6 +3106,7 @@ static int sx126x_pm_action(const struct device *dev,
|
||||
static int sx126x_init(const struct device *dev)
|
||||
{
|
||||
struct sx126x_data *data = dev->data;
|
||||
@@ -2842,7 +2971,7 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
int ret;
|
||||
|
||||
/* Initialize data structures */
|
||||
@@ -1121,9 +3050,33 @@ static int sx126x_init(const struct device *dev)
|
||||
@@ -1121,9 +3116,33 @@ static int sx126x_init(const struct device *dev)
|
||||
k_msgq_init(&data->rx_msgq, (char *)&data->rx_result,
|
||||
sizeof(struct sx126x_rx_result), 1);
|
||||
k_work_init(&data->irq_work, sx126x_irq_work_handler);
|
||||
@@ -2876,11 +3005,29 @@ index 30243ba5dc7..3a8bc017a52 100644
|
||||
|
||||
/* Initialize HAL */
|
||||
ret = sx126x_hal_init(dev);
|
||||
@@ -1185,6 +3204,8 @@ static int sx126x_init(const struct device *dev)
|
||||
{0}), \
|
||||
.rx_enable = GPIO_DT_SPEC_INST_GET_OR(inst, rx_enable_gpios, \
|
||||
{0}), \
|
||||
+ .lna_bypass = GPIO_DT_SPEC_INST_GET_OR(inst, lna_bypass_gpios, \
|
||||
+ {0}), \
|
||||
.dio2_tx_enable = DT_INST_PROP(inst, dio2_tx_enable), \
|
||||
.dio3_tcxo_enable = DT_INST_NODE_HAS_PROP(inst, dio3_tcxo_voltage), \
|
||||
.dio3_tcxo_voltage = DT_INST_PROP_OR(inst, dio3_tcxo_voltage, 0), \
|
||||
@@ -1239,6 +3260,8 @@ DT_INST_FOREACH_STATUS_OKAY_VARGS(SX126X_INIT, true)
|
||||
{0}), \
|
||||
.rx_enable = GPIO_DT_SPEC_INST_GET_OR(inst, rx_enable_gpios, \
|
||||
{0}), \
|
||||
+ .lna_bypass = GPIO_DT_SPEC_INST_GET_OR(inst, lna_bypass_gpios, \
|
||||
+ {0}), \
|
||||
.dio2_tx_enable = DT_INST_PROP(inst, dio2_tx_enable), \
|
||||
.dio3_tcxo_enable = DT_INST_NODE_HAS_PROP(inst, dio3_tcxo_voltage), \
|
||||
.dio3_tcxo_voltage = DT_INST_PROP_OR(inst, dio3_tcxo_voltage, 0), \
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h
|
||||
index 9dbf3f26586..b6df07ce7ab 100644
|
||||
index 9dbf3f26586..675f3eca8a7 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x.h
|
||||
+++ b/drivers/lora/native/sx126x/sx126x.h
|
||||
@@ -56,13 +56,104 @@ struct sx126x_data {
|
||||
@@ -56,13 +56,107 @@ struct sx126x_data {
|
||||
/* Async RX callback */
|
||||
lora_recv_cb rx_cb;
|
||||
void *rx_cb_user_data;
|
||||
@@ -2898,12 +3045,15 @@ index 9dbf3f26586..b6df07ce7ab 100644
|
||||
+ bool rx_duty_cycle_enabled;
|
||||
+ bool rx_boost_enabled;
|
||||
+
|
||||
+ /* External FEM/LNA gating for the RX direction, set by
|
||||
+ * sx126x_set_fem_rx_enable(). Negative logic so the zero-initialised
|
||||
+ * default preserves the historical behaviour -- antenna-enable-gpios
|
||||
+ * asserted for both RX and TX. When set, sx126x_set_rf_path() drops
|
||||
+ * the line for RX only: TX still asserts it and sleep/idle still
|
||||
+ * clears it, so the driver keeps sole ownership of the pin. */
|
||||
+ /* External FEM receive-path select, set by sx126x_set_fem_rx_enable()
|
||||
+ * and acted on through lna-bypass-gpios. Negative logic so the
|
||||
+ * zero-initialised default is the LNA path, which is what every board
|
||||
+ * wants and what the driver did before the knob existed.
|
||||
+ *
|
||||
+ * NOT the FEM chip enable. antenna-enable-gpios stays owned by the
|
||||
+ * RX/TX/sleep state machine: deasserting it in RX would shut the part
|
||||
+ * down, which on the KCT8103L boards costs the LNA gain *and* the
|
||||
+ * through path -- tens of dB, not the ~17 dB the bypass costs. */
|
||||
+ bool fem_rx_off;
|
||||
+
|
||||
+ /* AgcSensiAdjust value for the configured band (DS Table 6-4), cached
|
||||
@@ -2986,7 +3136,7 @@ index 9dbf3f26586..b6df07ce7ab 100644
|
||||
|
||||
#endif /* ZEPHYR_DRIVERS_LORA_SX126X_SX126X_INTERNAL_H_ */
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x_hal.c b/drivers/lora/native/sx126x/sx126x_hal.c
|
||||
index 34f7089584f..6f4e2a4434c 100644
|
||||
index 34f7089584f..394578956de 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x_hal.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x_hal.c
|
||||
@@ -150,7 +150,13 @@ int sx126x_hal_init(const struct device *dev)
|
||||
@@ -3004,7 +3154,22 @@ index 34f7089584f..6f4e2a4434c 100644
|
||||
if (ret < 0) {
|
||||
LOG_ERR("Failed to configure DIO1 GPIO: %d", ret);
|
||||
return ret;
|
||||
@@ -220,15 +226,54 @@ int sx126x_hal_configure_tx_params(const struct device *dev, int8_t power,
|
||||
@@ -183,6 +189,14 @@ int sx126x_hal_init(const struct device *dev)
|
||||
return ret;
|
||||
}
|
||||
|
||||
+ /* Inactive = LOW = FEM LNA in the receive path, which is the state
|
||||
+ * every board using this line wants by default. */
|
||||
+ ret = sx126x_hal_configure_gpio(&config->lna_bypass, GPIO_OUTPUT_INACTIVE,
|
||||
+ "FEM LNA bypass");
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
LOG_DBG("HAL initialized");
|
||||
return 0;
|
||||
}
|
||||
@@ -220,15 +234,54 @@ int sx126x_hal_configure_tx_params(const struct device *dev, int8_t power,
|
||||
}
|
||||
tx_power = CLAMP(power, SX1261_MIN_POWER, SX1261_MAX_POWER_TX_PARAM);
|
||||
} else {
|
||||
@@ -3063,6 +3228,21 @@ index 34f7089584f..6f4e2a4434c 100644
|
||||
}
|
||||
|
||||
uint8_t buf[2] = { (uint8_t)tx_power, ramp_time };
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x_hal.h b/drivers/lora/native/sx126x/sx126x_hal.h
|
||||
index e4b26ba60bd..558890ad54c 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x_hal.h
|
||||
+++ b/drivers/lora/native/sx126x/sx126x_hal.h
|
||||
@@ -29,6 +29,10 @@ struct sx126x_hal_config {
|
||||
struct gpio_dt_spec antenna_enable;
|
||||
struct gpio_dt_spec tx_enable;
|
||||
struct gpio_dt_spec rx_enable;
|
||||
+ /* ZephCore: external FEM receive-path select (lna-bypass-gpios). Not
|
||||
+ * an RF switch -- LOW selects the FEM's LNA, HIGH its bypass path. See
|
||||
+ * the binding, and sx126x_set_fem_rx_enable() for the runtime knob. */
|
||||
+ struct gpio_dt_spec lna_bypass;
|
||||
uint16_t tcxo_startup_delay_ms;
|
||||
uint8_t dio3_tcxo_voltage;
|
||||
bool dio2_tx_enable;
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x_hal_common.c b/drivers/lora/native/sx126x/sx126x_hal_common.c
|
||||
index b2dfc0d75b5..8ab8a0f5738 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x_hal_common.c
|
||||
@@ -3100,6 +3280,26 @@ index b2dfc0d75b5..8ab8a0f5738 100644
|
||||
}
|
||||
|
||||
return 0;
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x_hal_stm32wl.c b/drivers/lora/native/sx126x/sx126x_hal_stm32wl.c
|
||||
index c99a7204a72..08d20e44ae4 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x_hal_stm32wl.c
|
||||
+++ b/drivers/lora/native/sx126x/sx126x_hal_stm32wl.c
|
||||
@@ -246,6 +246,15 @@ int sx126x_hal_init(const struct device *dev)
|
||||
return ret;
|
||||
}
|
||||
|
||||
+ /* No STM32WL board here wires one today, but sx126x_set_rf_path() will
|
||||
+ * drive the pin if a board declares it -- configure it so that is never
|
||||
+ * a write to an unconfigured GPIO. Inactive = LOW = LNA path. */
|
||||
+ ret = sx126x_hal_configure_gpio(&config->lna_bypass, GPIO_OUTPUT_INACTIVE,
|
||||
+ "FEM LNA bypass");
|
||||
+ if (ret < 0) {
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
LOG_DBG("STM32WL HAL initialized");
|
||||
return 0;
|
||||
}
|
||||
diff --git a/drivers/lora/native/sx126x/sx126x_regs.h b/drivers/lora/native/sx126x/sx126x_regs.h
|
||||
index 7f55c9b96e2..155d7dc2d81 100644
|
||||
--- a/drivers/lora/native/sx126x/sx126x_regs.h
|
||||
@@ -3174,3 +3374,32 @@ index 7f55c9b96e2..155d7dc2d81 100644
|
||||
#define SX126X_TX_TIMEOUT_NONE 0x000000
|
||||
|
||||
/* Convert milliseconds to timeout ticks */
|
||||
diff --git a/dts/bindings/lora/semtech,sx126x-base.yaml b/dts/bindings/lora/semtech,sx126x-base.yaml
|
||||
index 2d13d758695..04390be84e5 100644
|
||||
--- a/dts/bindings/lora/semtech,sx126x-base.yaml
|
||||
+++ b/dts/bindings/lora/semtech,sx126x-base.yaml
|
||||
@@ -35,6 +35,24 @@ properties:
|
||||
Antenna switch RX enable GPIO. If set, the driver tracks the
|
||||
state of the radio and controls the RF switch.
|
||||
|
||||
+ lna-bypass-gpios:
|
||||
+ type: phandle-array
|
||||
+ description: |
|
||||
+ Receive-path select of an external front-end module whose RX gain is
|
||||
+ software-selectable, such as the KCT8103L CTX pin on Heltec boards.
|
||||
+ LOW routes the receive path through the FEM's LNA, HIGH bypasses it.
|
||||
+
|
||||
+ This is NOT an RF switch control. It belongs only on a FEM where the
|
||||
+ TX/RX path is selected by something else -- on the KCT8103L boards,
|
||||
+ DIO2 into the FEM's CPS pin -- which is what leaves this line free to
|
||||
+ mean "LNA or bypass". Never point it at a PA or TXEN enable: the
|
||||
+ driver asserts it during receive whenever the LNA is switched off,
|
||||
+ which on a real TXEN would key the amplifier into a receive window.
|
||||
+
|
||||
+ Held HIGH while transmitting; in receive it follows
|
||||
+ sx126x_set_fem_rx_enable() -- LOW for the LNA (the default), HIGH to
|
||||
+ trade its gain for its supply current.
|
||||
+
|
||||
dio1-gpios:
|
||||
type: phandle-array
|
||||
description: |
|
||||
|
||||
Reference in New Issue
Block a user