Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_lr1121.h
Go to the documentation of this file.
1#pragma once
2
3/// @file radio_lr1121.h
4/// @brief LR1121 radio driver for IO-Homecontrol.
5/// @ingroup hioc_radio
6///
7/// Implements the RadioDriver interface for the Semtech LR1121 transceiver on the
8/// LilyGo T3-S3 board. Like the SX1262, the LR1121 has no hardware equivalent of the
9/// SX1276's IoHomeOn framing, so this driver reuses the shared software PHY
10/// (`radio_soft_phy.h`) wholesale: software UART bit-encoding on TX, a UART-decode
11/// probe with CRC-CCITT validation on RX, fixed-length GFSK packets, software CRC.
12/// This is a re-plumbed RadioSX1262 with a different SPI command set, not a new
13/// bring-up from first principles (design doc §1.2). The IRQ-driven RX/TX orchestration
14/// this driver shares with RadioSX1262 lives in SoftPhyDriverBase (see
15/// radio_soft_phy_driver_base.h).
16///
17/// The chip-level difference from the SX1262 that actually changes the driver's
18/// shape is the SPI transport: the LR1121 uses 16-bit opcodes and a two-transaction
19/// command/response protocol (write opcode+params, wait BUSY, then a second
20/// transaction clocks out a status byte + response), instead of the SX1262's
21/// single-transaction opcode+NOP-then-data pattern.
22///
23/// Hardware bring-up (2026-08) confirmed this driver end-to-end against a real Somfy Sunea IO
24/// awning motor: authenticated open/close/stop exchanges complete reliably, with correct
25/// position/state feedback decoded from the device's real responses. Every constant below is
26/// therefore hardware-verified unless its own comment says otherwise.
27
29#include "esphome/core/hal.h"
30
31namespace esphome {
32namespace home_io_control {
33
34// ============================================================================
35// LR1121 Opcode Constants (16-bit)
36// ============================================================================
37//
38// The opcodes below marked "cross-checked" were verified against Semtech's SWDR001 driver and
39// RadioLib's LR11x0_commands.h before first use, rather than inferred from observed traffic.
40
41static constexpr uint16_t LR1121_CMD_GET_STATUS = 0x0100; ///< cross-checked
42static constexpr uint16_t LR1121_CMD_GET_VERSION = 0x0101; ///< cross-checked
43static constexpr uint16_t LR1121_CMD_GET_ERRORS = 0x010D; ///< cross-checked (2-byte response)
44static constexpr uint16_t LR1121_CMD_CLEAR_ERRORS =
45 0x010E; ///< hardware-verified (called on every init/RX cycle, never rejected — see log_command_status_())
46static constexpr uint16_t LR1121_CMD_WRITE_BUFFER = 0x0109; ///< cross-checked
47static constexpr uint16_t LR1121_CMD_READ_BUFFER = 0x010A; ///< cross-checked
48static constexpr uint16_t LR1121_CMD_WRITE_REG_MEM_MASK32 =
49 0x010C; ///< cross-checked (Semtech SWDR001 lr11xx_radio.c / RadioLib LR11x0_commands.h)
50static constexpr uint16_t LR1121_CMD_CALIBRATE = 0x010F; ///< cross-checked
51static constexpr uint16_t LR1121_CMD_CALIBRATE_IMAGE = 0x0111; ///< cross-checked (RadioLib calibrateImageRejection)
52static constexpr uint16_t LR1121_CMD_SET_DIO_AS_RF_SWITCH = 0x0112; ///< cross-checked
53static constexpr uint16_t LR1121_CMD_SET_DIO_IRQ_PARAMS = 0x0113; ///< cross-checked
54static constexpr uint16_t LR1121_CMD_CLEAR_IRQ = 0x0114; ///< cross-checked
55static constexpr uint16_t LR1121_CMD_SET_TCXO_MODE = 0x0117; ///< cross-checked
56static constexpr uint16_t LR1121_CMD_SET_STANDBY = 0x011C; ///< cross-checked
57static constexpr uint16_t LR1121_CMD_GET_RX_BUFFER_STATUS = 0x0203; ///< cross-checked
58static constexpr uint16_t LR1121_CMD_GET_PKT_STATUS =
59 0x0204; ///< cross-checked (Semtech SWDR001 lr11xx_radio.c :: lr11xx_radio_get_gfsk_pkt_status —
60 ///< 0 request params, 4-byte GFSK response: [rssi_sync, rssi_avg, rx_len, status_flags],
61 ///< rssi_sync/avg both decode via -(raw>>1) dBm, same formula as LR1121_CMD_GET_RSSI_INST)
62static constexpr uint16_t LR1121_CMD_GET_RSSI_INST = 0x0205; ///< cross-checked
63static constexpr uint16_t LR1121_CMD_SET_GFSK_SYNC_WORD = 0x0206; ///< cross-checked
64static constexpr uint16_t LR1121_CMD_SET_RX = 0x0209; ///< cross-checked
65static constexpr uint16_t LR1121_CMD_SET_TX = 0x020A; ///< cross-checked
66static constexpr uint16_t LR1121_CMD_SET_RF_FREQUENCY = 0x020B; ///< cross-checked
67static constexpr uint16_t LR1121_CMD_SET_PACKET_TYPE = 0x020E; ///< cross-checked
68static constexpr uint16_t LR1121_CMD_SET_MODULATION_PARAMS = 0x020F; ///< cross-checked
69static constexpr uint16_t LR1121_CMD_SET_PACKET_PARAMS = 0x0210; ///< cross-checked
70static constexpr uint16_t LR1121_CMD_SET_TX_PARAMS = 0x0211; ///< cross-checked
71static constexpr uint16_t LR1121_CMD_SET_RX_TX_FALLBACK_MODE = 0x0213; ///< cross-checked
72static constexpr uint16_t LR1121_CMD_SET_PA_CONFIG = 0x0215; ///< cross-checked
73
74// ============================================================================
75// LR1121 IRQ Bit Masks (32-bit word)
76// ============================================================================
77// Bit positions are cross-checked (design §1.2 chip comparison table).
78
79static constexpr uint32_t LR1121_IRQ_TX_DONE = 1UL << 2;
80static constexpr uint32_t LR1121_IRQ_RX_DONE = 1UL << 3;
81static constexpr uint32_t LR1121_IRQ_PREAMBLE_DETECTED = 1UL << 4;
82static constexpr uint32_t LR1121_IRQ_SYNC_WORD_VALID = 1UL << 5;
83static constexpr uint32_t LR1121_IRQ_CRC_ERR = 1UL << 7;
84static constexpr uint32_t LR1121_IRQ_TIMEOUT = 1UL << 10;
85
86/// IRQ bits that represent a *terminal* radio event — a frame finished decoding, an outbound
87/// frame completed, or the chip gave up on its own. Deliberately excludes PREAMBLE_DETECTED,
88/// even though it's part of LR1121_IRQ_DIO_ENABLE_MASK: a preamble alone means a frame may
89/// still be arriving, so SoftPhyDriverBase's poll_until_activity_()/check_for_packet() (gated by
90/// @ref RadioLR1121::activity_irq_mask) must not treat it as terminal — doing so would tear down
91/// RX mid-reception.
92static constexpr uint32_t LR1121_IRQ_ACTIVITY_MASK =
94
95/// DIO-routed IRQ enable mask: TxDone|RxDone|PreambleDetected|SyncWordValid|Timeout
96/// (design implementation §Step 2, item 2 — deliberately excludes CrcErr from the DIO-routed
97/// set, unlike SX1262; CrcErr is still visible in the raw status word for capture diagnostics).
101
102// ============================================================================
103// LR1121 chip identity / GFSK / packet constants
104// ============================================================================
105
106static constexpr uint8_t LR1121_DEVICE_TYPE = 0x03; ///< cross-checked (design §1.2 "Identity" row)
107
108/// Newest LR1121 transceiver firmware known at the time this file was last updated, per the
109/// version-numbered filenames and CHANGELOG.md at
110/// https://github.com/Lora-net/radio_firmware_images/tree/master/lr1121/transceiver — the same
111/// two bytes GetVersion reports as fw_major/fw_minor (e.g. `lr1121_transceiver_0104.bin` is
112/// major=0x01, minor=0x04). Deliberately NOT queried live (baked in, per design decision) — this
113/// is a point-in-time snapshot that will go stale as Semtech ships new firmware, and needs a
114/// manual refresh here when it does.
115///
116/// Last checked 2026-08-05: latest is 0x0104 (2026-04-01), which per that changelog fixes
117/// CVE-2025-14857/-14858/-14859 (see https://www.semtech.com/company/security/security-bulletins)
118/// on top of earlier feature/bugfix releases — not just a cosmetic version bump.
119///
120/// Reachability caveat: this project's `lr1121_firmware_update:` feature (docs/home_io_control.md)
121/// can flash any published image, but 0x0104 requires bootloader 0x2101. Reaching it from the
122/// common 0x2100 bootloader needs the separate, opt-in bootloader rewrite (ADR 0021) — an
123/// irreversible operation gated behind its own arming switch — so without that opted in, 0x0103
124/// is the practical ceiling. The "update available" line below can therefore point at a version
125/// that is not reachable on a given chip as currently configured; the flash feature's own
126/// boot-time bootloader-version report is what tells a user which case they are in.
127static constexpr uint8_t LR1121_KNOWN_LATEST_FW_MAJOR = 0x01;
128static constexpr uint8_t LR1121_KNOWN_LATEST_FW_MINOR = 0x04;
129
130/// Pure comparison against the known-latest constants above — no I/O, host-testable. Only ever
131/// flags a version as outdated when it is strictly older than what this file knows about;
132/// versions equal to or newer than LR1121_KNOWN_LATEST_FW_* (including any real future firmware
133/// this snapshot predates) are never flagged, so a stale baked-in constant fails silent rather
134/// than crying wolf.
135[[nodiscard]] constexpr bool lr1121_firmware_is_outdated(uint8_t fw_major, uint8_t fw_minor) {
136 return (fw_major < LR1121_KNOWN_LATEST_FW_MAJOR) ||
138}
139
140static constexpr uint8_t LR1121_PACKET_TYPE_GFSK = 0x01; ///< cross-checked (design §3.2 step 5)
141static constexpr uint8_t LR1121_GFSK_CRC_OFF = 0x01; ///< cross-checked (same encoding as SX126x)
142static constexpr uint8_t LR1121_GFSK_PACKET_FIXED_LENGTH = 0x00; ///< cross-checked (same encoding as SX126x)
143/// Preamble detector length selector: 16 bits. Shares the SX126x GFSK preamble-detector enum
144/// (0x00 off, 0x04=8bit, 0x05=16bit, 0x06=24bit, 0x07=32bit) — the LR11xx sub-GHz GFSK modem
145/// descends from the same core.
146static constexpr uint8_t LR1121_PREAMBLE_DETECTOR_16_BIT = 0x05;
147/// Sync word length: 24 bits, encoded as a literal bit count — cross-checked, identical encoding
148/// to SX1262's SX1262_SYNC_WORD_PARAM_24_BITS.
149static constexpr uint8_t LR1121_SYNC_WORD_PARAM_24_BITS = 0x18;
150
151/// Calibrate "all blocks" bitmask. LR11xx calibration blocks are LF-RC(0)/HF-RC(1)/PLL(2)/
152/// ADC(3)/IMG(4)/PLL-TX(5), so "all" = 0x3F — NOT the SX126x value (0x7F), which uses a
153/// different bit layout for a different chip.
154static constexpr uint8_t LR1121_CALIBRATE_ALL_BLOCKS = 0x3F;
155
156/// SetRxTxFallbackMode value for STDBY_XOSC. LR11xx fallback-mode is a small sequential enum
157/// (FS=0x00, STDBY_RC=0x01, STDBY_XOSC=0x02), unlike SX126x's raw standby-mode byte (0x30)
158/// reused directly in that field.
159static constexpr uint8_t LR1121_FALLBACK_STDBY_XOSC = 0x02;
160
161// ============================================================================
162// Vendor errata / calibration workaround registers — Semtech's own driver (lr11xx_radio.c) and
163// RadioLib both apply these unconditionally. Register addresses/masks/values are byte-for-byte
164// matches to `lr11xx_radio_apply_high_acp_workaround` and `lr11xx_workaround_gfsk_reset`.
165// ============================================================================
166
167/// High-ACP (adjacent channel power) TX-quality erratum: clear bit 30 of this register before
168/// every SetRx/SetTx. Without it the chip's TX spectrum has excess spectral regrowth — plausibly
169/// why our own (tolerant) monitors decode our frames byte-exact while the awning's spec-compliant
170/// receiver mostly can't.
171static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_ADDR = 0x00F30054;
172static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_MASK = 1UL << 30;
173static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_VALUE = 0x00000000;
174
175/// GFSK modulation workaround register trio, standard (non-0.6/1.2kbps) values for our 38.4kbps
176/// config — applied after every modulation-params write, mirroring RadioLib's workaroundGFSK()
177/// ("always the first step, even when resetting" per its own comment on the first write).
178static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_ADDR = 0x00F20344;
179static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_MASK = 0x00000030;
180static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_VALUE = 0x00000010;
181static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_ADDR = 0x00F20348;
182static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_MASK = 0x00000005;
183static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_VALUE = 0x00000001;
184static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_ADDR = 0x00F20244;
185static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_MASK = 0x0001FF03;
186static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_VALUE = 0x00000A01;
187
188/// Banded image calibration for 868.25-869.85MHz +/-4MHz (~860-876MHz), matching RadioLib's
189/// setFrequency() margin. CalibImage params are {floor((fmin-1)/4), ceil((fmax+1)/4)}.
190static constexpr uint8_t LR1121_IMAGE_CAL_FREQ1 = 0xD7;
191static constexpr uint8_t LR1121_IMAGE_CAL_FREQ2 = 0xDB;
192
193// ============================================================================
194// T3-S3 RF-switch table — sourced from the Meshtastic `tlora_t3s3_v1` variant's rfswitch.h,
195// cross-checked against LilyGo's own T3-S3 LR1121 example and confirmed on real hardware.
196// ============================================================================
197//
198// SetDioAsRfSwitch parameter layout: [enable_mask, standby, rx, tx, tx_hp, tx_hf, gnss, wifi]
199// (8 bytes total), matching RadioLib's Lr11x0::setDioAsRfSwitch() signature. Each mode byte is
200// a bitmask of which switch-controlled DIOs (bit0=DIO5, bit1=DIO6, ...) should be driven high
201// in that mode.
202static constexpr uint8_t LR1121_RFSWITCH_ENABLE_DIO5_DIO6 = 0x03; ///< DIO5 + DIO6 are switch pins.
203static constexpr uint8_t LR1121_RFSWITCH_STANDBY = 0x00; ///< Both low.
204static constexpr uint8_t LR1121_RFSWITCH_RX = 0x01; ///< DIO5 high.
205static constexpr uint8_t LR1121_RFSWITCH_TX = 0x02; ///< DIO6 high (both LP and HP PA).
206static constexpr uint8_t LR1121_RFSWITCH_TX_HP = 0x02; ///< Same as TX (LP PA only used today).
207static constexpr uint8_t LR1121_RFSWITCH_TX_HF = 0x00; ///< 2.4GHz path unused; both low.
208static constexpr uint8_t LR1121_RFSWITCH_GNSS = 0x00; ///< Unused; both low.
209static constexpr uint8_t LR1121_RFSWITCH_WIFI = 0x00; ///< Unused; both low.
210
211/// LR1121 TCXO voltage on the T3-S3 board — 3.0V, confirmed on real hardware.
212static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MSB = 0x00;
213static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MID = 0x01;
214/// 0x140 ticks at 30.52us/tick (32.768kHz RTC) is ~9.8ms, not the ~5ms the SX1262-derived comment
215/// used to claim (that chip's tick base differs) — harmless either way (longer startup is safe).
216static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_LSB = 0x40;
217
218/// LR1121-specific per-channel dwell while waiting for authenticated exchange responses.
219///
220/// NOT seeded from the SX1262 value anymore (was 90, matching SX1262_EXCHANGE_RESPONSE_WAIT_SLICE_MS
221/// exactly) — see 2026-07-17 hardware bring-up: golden captures for this exact device
222/// (tests/corpus/captures/somfy_awning/exchange_open_sx1276.yaml) show the device's first reply
223/// arriving ~287ms after the request, on the *same* channel the request went out on, against a
224/// 300ms total wait budget split into 90ms per-channel hop slices (CH2→CH3→CH1→CH2). That only
225/// leaves the *last* ~30ms hop-back-to-CH2 slice to catch it — survivable for SX1262/SX1276, but
226/// LR1121's two-transaction 16-bit-opcode SPI protocol makes each hop's SetStandby/SetRfFrequency/
227/// SetRx round-trip slower, which can push the hop-back-to-CH2 moment past 287ms and cause a clean
228/// miss every try. Set above the largest response-wait budget (RESPONSE_WAIT_MS=500 in
229/// proto_timing.h) so this driver never hops away from the request's channel while waiting for a
230/// reply — real replies for this device only ever arrive on that same channel anyway, so hopping
231/// during the wait was actively counterproductive, not just slow.
232static constexpr int32_t LR1121_EXCHANGE_RESPONSE_WAIT_SLICE_MS = 600;
233
234// ============================================================================
235// LR1121 Radio Driver
236// ============================================================================
237
238/// @brief LR1121 implementation of RadioDriver.
239/// @ingroup hioc_radio
240///
241/// Manages the LR1121 via 16-bit-opcode, two-transaction SPI using the SpiAccess
242/// interface. Configures the chip in GFSK mode with software CRC-CCITT to match the
243/// IO-Homecontrol protocol (the LR1121, like the SX1262, lacks the SX1276's IoHomeOn
244/// mode). The IRQ-driven RX/TX orchestration is inherited from SoftPhyDriverBase; this
245/// class supplies the SPI transport and every LR1121-specific register/opcode encoding
246/// underneath it. See the file header for the driver's relationship to RadioSX1262.
248 public:
249 RadioLR1121(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *irq_pin, InternalGPIOPin *busy_pin,
250 uint8_t tx_power, uint8_t tcxo_voltage_yaml_code)
252 spi_(spi),
253 irq_pin_(irq_pin),
254 busy_pin_(busy_pin),
255 tx_power_(tx_power),
256 tcxo_voltage_yaml_code_(tcxo_voltage_yaml_code) {}
257
258 /// @copydoc RadioDriver::init
259 bool init() override;
260 /// @brief Apply LR1121 runtime tuning: RX bandwidth, response preamble, post-TX settle delay.
266 /// @brief Per-channel dwell while waiting for exchange responses (LR1121).
267 [[nodiscard]] uint32_t exchange_wait_slice_ms() const override { return LR1121_EXCHANGE_RESPONSE_WAIT_SLICE_MS; }
268 /// @brief Per-channel dwell while pairing discovery hops (LR1121).
269 ///
270 /// LR1121 frequency changes require a standby→SetRfFrequency→RX cycle (no fast hop),
271 /// same as the SX1262, so discovery needs the equivalent longer dwell. The value comes
272 /// from the user-facing `lr1121_discovery_hop_slice_ms` tuning field.
273 [[nodiscard]] uint16_t discovery_hop_slice_ms(const TuningConfig &tuning) const override {
274 return tuning.lr1121_discovery_hop_slice_ms;
275 }
276 /// @brief TX→RX turnaround capability (LR1121): slow, same as SX1262.
277 ///
278 /// See @ref RadioSX1262::has_fast_tx_rx_turnaround for the rationale — the LR1121 needs
279 /// the same standby/settle cycle between TX and RX.
280 [[nodiscard]] bool has_fast_tx_rx_turnaround() const override { return false; }
281 /// @copydoc RadioDriver::set_mode_rx
282 void set_mode_rx() override;
283 /// @copydoc RadioDriver::set_mode_standby
284 void set_mode_standby() override;
285 [[nodiscard]] bool is_failed() const override { return this->failed_; }
286 [[nodiscard]] const char *chip_name() const override { return "lr1121"; }
287 /// @brief Dump LR1121-specific debug info.
288 void dump_debug() override;
289
290 protected:
291 // --- Tuning helper unique to this chip (its RX-bandwidth enum is not shared with SX1262 —
292 // see tuning_config.h's LR1121RxBandwidth comment: two of the five borrowed SX1262 byte values
293 // turned out wrong for this chip during 2026-07-17 bring-up) ---
294 /// Apply the RX bandwidth selector and rewrite the modulation parameters.
295 void set_rx_bandwidth_(LR1121RxBandwidth bandwidth);
296
297 // --- SPI communication (16-bit opcode, two-transaction) ---
298 /// Wait until BUSY pin is low before any SPI transaction.
299 void wait_busy_();
300 /// Write-only command: opcode + params, single NSS cycle.
301 /// @param opcode LR1121 16-bit opcode.
302 /// @param params Pointer to parameter buffer (may be nullptr).
303 /// @param len Parameter length.
304 void write_command_(uint16_t opcode, const uint8_t *params, uint8_t len);
305 /// Read-type command: write transaction, wait BUSY, then a second NSS cycle clocks out
306 /// a Stat1 status byte followed by `out_len` response bytes.
307 /// @param opcode LR1121 16-bit opcode.
308 /// @param params Pointer to request parameter buffer (may be nullptr).
309 /// @param params_len Request parameter length.
310 /// @param out Output buffer for the response bytes (excludes Stat1).
311 /// @param out_len Number of response bytes to read.
312 void read_command_(uint16_t opcode, const uint8_t *params, uint8_t params_len, uint8_t *out, uint8_t out_len);
313 /// Write into the LR1121 TX buffer (always from the chip's internal write pointer, which
314 /// resets to the buffer base for a fresh WriteBuffer sequence — unlike SX1262 there is no
315 /// separate offset parameter here; see design §3.2 "SPI transport").
316 void write_buffer_(const uint8_t *data, uint8_t len);
317 /// Read from the LR1121 RX buffer at a given offset (as reported by GetRxBufferStatus).
318 void read_buffer_(uint8_t offset, uint8_t len, uint8_t *data);
319 /// Log a warning if the most recently observed Stat1 command-status byte indicates the
320 /// chip rejected the previous command (FAIL/PERR — see radio_lr1121.h §4.4).
321 void log_command_status_(uint16_t opcode) const;
322 /// WriteRegMemMask32: read-modify-write a 32-bit register through `mask`/`value`. Used by the
323 /// vendor errata workarounds below; opcode 0x010C (design analysis §4.1/§4.2).
324 void write_reg_mem_mask32_(uint32_t addr, uint32_t mask, uint32_t value);
325 /// Apply the Semtech high-ACP TX-quality erratum workaround (analysis §4.1). Must run before
326 /// every SetRx/SetTx — called from set_mode_rx() and @ref before_tx_arm rather than once at
327 /// init, mirroring Semtech's own call sites.
329 /// Apply the GFSK modulation workaround register trio (analysis §4.2). Called at the end of
330 /// write_modulation_params_() so it re-applies on every bandwidth retune too.
332 /// Issue a banded image calibration for the 868MHz operating range (analysis §4.3).
333 void calibrate_image_();
334
335 // --- Radio configuration ---
336 /// Full radio initialization (called from init()).
337 void configure_radio_();
338 /// @copydoc SoftPhyDriverBase::set_frequency_register
339 ///
340 /// Plain Hz, 32-bit — no PLL-step conversion needed on this chip.
341 void set_frequency_register(uint32_t freq_hz) override;
342 /// Configure GFSK packet parameters (preamble, payload length, CRC).
343 void set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type, uint8_t crc_type);
344 /// Apply the runtime bandwidth setting to the LR1121 modulation parameters.
346 /// @copydoc SoftPhyDriverBase::set_rx_packet_params
347 void set_rx_packet_params() override;
348 /// @copydoc SoftPhyDriverBase::set_tx_packet_params
349 void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) override {
351 }
352 /// @copydoc SoftPhyDriverBase::clear_irq_status
353 void clear_irq_status(uint32_t irq_mask) override;
354 /// @brief Read device error flags (for diagnostics only — see dump_debug()).
355 /// @return Error bitmask (2-byte response, cross-checked against RadioLib).
356 uint16_t get_errors_();
357 /// @brief Clear device error flags. Called both during init (after TCXO configuration, before
358 /// calibration) and at the end of configure_radio_() to discard init-time noise.
359 void clear_errors_();
360 /// @copydoc SoftPhyDriverBase::fill_capture_info
361 ///
362 /// Note: RadioCaptureInfo::irq_status is uint16_t; the 32-bit IRQ word is mapped down by
363 /// taking bits [2..10] and shifting right by 2 (design §3.2 "IRQ-width note").
364 void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
365 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) override;
366
367 /// IRQ pin (DIO9) ISR — sets dio_fired flag. Runs in interrupt context.
368 static void gpio_intr(RadioLR1121 *arg);
369
370 /// @copydoc SoftPhyDriverBase::read_irq_status_raw
371 ///
372 /// Reads the raw 32-bit IRQ status word from the radio (via GetStatus).
373 uint32_t read_irq_status_raw() override;
374 /// @copydoc SoftPhyDriverBase::sync_word_valid_bit
375 [[nodiscard]] uint32_t sync_word_valid_bit() const override { return LR1121_IRQ_SYNC_WORD_VALID; }
376 /// @copydoc SoftPhyDriverBase::rx_done_bit
377 [[nodiscard]] uint32_t rx_done_bit() const override { return LR1121_IRQ_RX_DONE; }
378 /// @copydoc SoftPhyDriverBase::tx_done_bit
379 [[nodiscard]] uint32_t tx_done_bit() const override { return LR1121_IRQ_TX_DONE; }
380 /// @copydoc SoftPhyDriverBase::preamble_detected_bit
381 [[nodiscard]] uint32_t preamble_detected_bit() const override { return LR1121_IRQ_PREAMBLE_DETECTED; }
382 /// @copydoc SoftPhyDriverBase::activity_irq_mask
383 ///
384 /// Excludes PREAMBLE_DETECTED, unlike the base's "any bit" default — see
385 /// LR1121_IRQ_ACTIVITY_MASK's doc comment for why this chip needs the distinction.
386 [[nodiscard]] uint32_t activity_irq_mask() const override { return LR1121_IRQ_ACTIVITY_MASK; }
387 /// @copydoc SoftPhyDriverBase::read_rssi_raw_byte
388 uint8_t read_rssi_raw_byte() override;
389 /// @copydoc SoftPhyDriverBase::write_tx_buffer
390 void write_tx_buffer(const uint8_t *data, uint8_t len) override { this->write_buffer_(data, len); }
391 /// @copydoc SoftPhyDriverBase::get_rx_buffer_status
392 void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) override;
393 /// @copydoc SoftPhyDriverBase::read_rx_buffer
394 void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) override { this->read_buffer_(offset, len, data); }
395 /// @copydoc SoftPhyDriverBase::start_tx
396 void start_tx() override;
397 /// @copydoc SoftPhyDriverBase::before_tx_arm
398 ///
399 /// High-ACP workaround (analysis §4.1) — Semtech applies this unconditionally before every
400 /// SetTx, same as before every SetRx (see set_mode_rx()).
401 void before_tx_arm() override { this->apply_high_acp_workaround_(); }
402
403 private:
404 SpiAccess *spi_;
405 InternalGPIOPin *irq_pin_;
406 InternalGPIOPin *busy_pin_;
407 uint8_t tx_power_;
408 uint8_t tcxo_voltage_yaml_code_; ///< Raw TCXO_VOLTAGE_OPTIONS code from YAML (1_6V=0x01 .. 3_3V=0x08).
409 bool failed_{false};
410 uint8_t last_stat1_{0}; ///< Most recent Stat1 byte (diagnostics).
411 LR1121RxBandwidth rx_bandwidth_{LR1121RxBandwidth::BW_117_3_KHZ}; ///< Runtime-tunable RX bandwidth.
412};
413
414} // namespace home_io_control
415} // namespace esphome
LR1121 implementation of RadioDriver.
void write_reg_mem_mask32_(uint32_t addr, uint32_t mask, uint32_t value)
WriteRegMemMask32: read-modify-write a 32-bit register through mask/value.
uint32_t preamble_detected_bit() const override
uint8_t read_rssi_raw_byte() override
Read the single raw RSSI byte (chip-specific opcode); formula is shared, see read_rssi.
uint32_t read_irq_status_raw() override
Read the raw IRQ status word from the radio.
void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) override
Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,...
void configure_radio_()
Full radio initialization (called from init()).
void set_frequency_register(uint32_t freq_hz) override
Set RF frequency via the chip's own frequency register/opcode encoding, and update current_freq_.
void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) override
Read the chip-reported RX length and buffer offset (raw, before any clamping).
void clear_irq_status(uint32_t irq_mask) override
Clear IRQ status bits.
void set_rx_packet_params() override
Configure RX-specific packet parameters (preamble detector length, fixed probe length).
void set_rx_bandwidth_(LR1121RxBandwidth bandwidth)
Apply the RX bandwidth selector and rewrite the modulation parameters.
void start_tx() override
Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,...
void before_tx_arm() override
Hook run immediately before every SetTx.
static void gpio_intr(RadioLR1121 *arg)
IRQ pin (DIO9) ISR — sets dio_fired flag. Runs in interrupt context.
void set_mode_standby() override
Switch to standby mode.
uint32_t sync_word_valid_bit() const override
void apply_high_acp_workaround_()
Apply the Semtech high-ACP TX-quality erratum workaround (analysis §4.1).
bool is_failed() const override
Returns true if the radio failed to initialize or encountered a fatal error.
void dump_debug() override
Dump LR1121-specific debug info.
RadioLR1121(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *irq_pin, InternalGPIOPin *busy_pin, uint8_t tx_power, uint8_t tcxo_voltage_yaml_code)
void write_buffer_(const uint8_t *data, uint8_t len)
Write into the LR1121 TX buffer (always from the chip's internal write pointer, which resets to the b...
bool has_fast_tx_rx_turnaround() const override
TX→RX turnaround capability (LR1121): slow, same as SX1262.
uint32_t activity_irq_mask() const override
IRQ bits that count as "activity" for the internal poll_until_activity_() helper and check_for_packet...
void write_command_(uint16_t opcode, const uint8_t *params, uint8_t len)
Write-only command: opcode + params, single NSS cycle.
uint32_t tx_done_bit() const override
void set_mode_rx() override
Switch to continuous receive mode.
void write_modulation_params_()
Apply the runtime bandwidth setting to the LR1121 modulation parameters.
void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) override
Configure TX packet parameters for one outgoing UART-encoded frame.
void apply_tuning(const TuningConfig &tuning) override
Apply LR1121 runtime tuning: RX bandwidth, response preamble, post-TX settle delay.
uint16_t get_errors_()
Read device error flags (for diagnostics only — see dump_debug()).
void wait_busy_()
Wait until BUSY pin is low before any SPI transaction.
void clear_errors_()
Clear device error flags.
void read_command_(uint16_t opcode, const uint8_t *params, uint8_t params_len, uint8_t *out, uint8_t out_len)
Read-type command: write transaction, wait BUSY, then a second NSS cycle clocks out a Stat1 status by...
void write_tx_buffer(const uint8_t *data, uint8_t len) override
Write the UART-encoded TX payload into the chip's TX buffer.
uint16_t discovery_hop_slice_ms(const TuningConfig &tuning) const override
Per-channel dwell while pairing discovery hops (LR1121).
void apply_gfsk_workaround_()
Apply the GFSK modulation workaround register trio (analysis §4.2).
void set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type, uint8_t crc_type)
Configure GFSK packet parameters (preamble, payload length, CRC).
bool init() override
Initialize the radio hardware. Returns true on success.
uint32_t exchange_wait_slice_ms() const override
Per-channel dwell while waiting for exchange responses (LR1121).
void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) override
Read len bytes from the RX buffer starting at offset.
uint32_t rx_done_bit() const override
void log_command_status_(uint16_t opcode) const
Log a warning if the most recently observed Stat1 command-status byte indicates the chip rejected the...
void read_buffer_(uint8_t offset, uint8_t len, uint8_t *data)
Read from the LR1121 RX buffer at a given offset (as reported by GetRxBufferStatus).
const char * chip_name() const override
Get a human‑readable chip name.
void calibrate_image_()
Issue a banded image calibration for the 868MHz operating range (analysis §4.3).
SoftPhyDriverBase(InternalGPIOPin *rst_pin, uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
void set_post_tx_settle_us_(uint16_t delay_us)
Set the delay between TX completion and re-entering RX.
void set_response_preamble_(uint16_t preamble)
Set the preamble length used for response/continuation frames within an exchange.
Interface for SPI bus access.
static constexpr uint16_t LR1121_CMD_SET_TX_PARAMS
cross-checked
static constexpr uint16_t LR1121_CMD_CALIBRATE_IMAGE
cross-checked (RadioLib calibrateImageRejection)
static constexpr uint16_t LR1121_CMD_SET_MODULATION_PARAMS
cross-checked
static constexpr uint16_t LR1121_CMD_GET_PKT_STATUS
cross-checked (Semtech SWDR001 lr11xx_radio.c :: lr11xx_radio_get_gfsk_pkt_status — 0 request params,...
static constexpr uint32_t LR1121_IRQ_TIMEOUT
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_MASK
static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_MASK
static constexpr uint16_t LR1121_CMD_SET_RX
cross-checked
static constexpr uint8_t LR1121_KNOWN_LATEST_FW_MAJOR
Newest LR1121 transceiver firmware known at the time this file was last updated, per the version-numb...
static constexpr uint8_t LR1121_KNOWN_LATEST_FW_MINOR
constexpr bool lr1121_firmware_is_outdated(uint8_t fw_major, uint8_t fw_minor)
Pure comparison against the known-latest constants above — no I/O, host-testable.
static constexpr uint8_t LR1121_SYNC_WORD_PARAM_24_BITS
Sync word length: 24 bits, encoded as a literal bit count — cross-checked, identical encoding to SX12...
static constexpr uint16_t LR1121_CMD_WRITE_REG_MEM_MASK32
cross-checked (Semtech SWDR001 lr11xx_radio.c / RadioLib LR11x0_commands.h)
static constexpr uint8_t LR1121_DEVICE_TYPE
cross-checked (design §1.2 "Identity" row)
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_VALUE
static constexpr int32_t LR1121_EXCHANGE_RESPONSE_WAIT_SLICE_MS
LR1121-specific per-channel dwell while waiting for authenticated exchange responses.
static constexpr uint16_t LR1121_CMD_SET_DIO_AS_RF_SWITCH
cross-checked
static constexpr uint16_t LR1121_CMD_SET_DIO_IRQ_PARAMS
cross-checked
LR1121RxBandwidth
Valid LR1121 RX bandwidth options (register values).
static constexpr uint32_t LR1121_IRQ_ACTIVITY_MASK
IRQ bits that represent a terminal radio event — a frame finished decoding, an outbound frame complet...
static constexpr uint8_t LR1121_CALIBRATE_ALL_BLOCKS
Calibrate "all blocks" bitmask.
static constexpr uint16_t LR1121_CMD_SET_PACKET_TYPE
cross-checked
static constexpr uint32_t LR1121_IRQ_TX_DONE
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_ADDR
static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MSB
LR1121 TCXO voltage on the T3-S3 board — 3.0V, confirmed on real hardware.
static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MID
static constexpr uint16_t LR1121_CMD_SET_GFSK_SYNC_WORD
cross-checked
static constexpr uint8_t LR1121_RFSWITCH_STANDBY
Both low.
static constexpr uint16_t LR1121_CMD_SET_PACKET_PARAMS
cross-checked
static constexpr uint16_t LR1121_CMD_WRITE_BUFFER
cross-checked
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_ADDR
GFSK modulation workaround register trio, standard (non-0.6/1.2kbps) values for our 38....
static constexpr uint16_t LR1121_CMD_SET_TX
cross-checked
static constexpr uint8_t LR1121_FALLBACK_STDBY_XOSC
SetRxTxFallbackMode value for STDBY_XOSC.
static constexpr uint8_t LR1121_RFSWITCH_ENABLE_DIO5_DIO6
DIO5 + DIO6 are switch pins.
static constexpr uint32_t LR1121_IRQ_CRC_ERR
static constexpr uint16_t LR1121_CMD_GET_VERSION
cross-checked
static constexpr uint16_t LR1121_RESPONSE_PREAMBLE
LR1121-specific preamble for response/continuation frames within an exchange.
static constexpr uint16_t LR1121_CMD_GET_RX_BUFFER_STATUS
cross-checked
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_ADDR
static constexpr uint8_t LR1121_RFSWITCH_TX_HP
Same as TX (LP PA only used today).
static constexpr uint8_t LR1121_RFSWITCH_WIFI
Unused; both low.
static constexpr uint16_t LR1121_CMD_CLEAR_ERRORS
hardware-verified (called on every init/RX cycle, never rejected — see log_command_status_())
static constexpr uint32_t LR1121_IRQ_PREAMBLE_DETECTED
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_VALUE
static constexpr uint16_t LR1121_CMD_CALIBRATE
cross-checked
static constexpr uint16_t LR1121_CMD_GET_ERRORS
cross-checked (2-byte response)
static constexpr uint8_t LR1121_GFSK_PACKET_FIXED_LENGTH
cross-checked (same encoding as SX126x)
static constexpr uint8_t LR1121_PACKET_TYPE_GFSK
cross-checked (design §3.2 step 5)
static constexpr uint8_t LR1121_RFSWITCH_TX_HF
2.4GHz path unused; both low.
static constexpr uint16_t LR1121_CMD_CLEAR_IRQ
cross-checked
static constexpr uint16_t LR1121_CMD_SET_STANDBY
cross-checked
static constexpr uint16_t LR1121_CMD_SET_RX_TX_FALLBACK_MODE
cross-checked
static constexpr uint8_t LR1121_RFSWITCH_GNSS
Unused; both low.
static constexpr uint8_t LR1121_RFSWITCH_TX
DIO6 high (both LP and HP PA).
static constexpr uint8_t LR1121_RFSWITCH_RX
DIO5 high.
static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_LSB
0x140 ticks at 30.52us/tick (32.768kHz RTC) is ~9.8ms, not the ~5ms the SX1262-derived comment used t...
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_VALUE
static constexpr uint8_t LR1121_PREAMBLE_DETECTOR_16_BIT
Preamble detector length selector: 16 bits.
static constexpr uint8_t LR1121_GFSK_CRC_OFF
cross-checked (same encoding as SX126x)
static constexpr uint16_t LR1121_CMD_SET_RF_FREQUENCY
cross-checked
static constexpr uint16_t LR1121_CMD_SET_PA_CONFIG
cross-checked
static constexpr uint32_t LR1121_IRQ_SYNC_WORD_VALID
static constexpr uint16_t LR1121_POST_TX_SETTLE_US
LR1121-specific post-TX settling delay before re-entering RX.
static constexpr uint32_t LR1121_IRQ_RX_DONE
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_MASK
static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_ADDR
High-ACP (adjacent channel power) TX-quality erratum: clear bit 30 of this register before every SetR...
static constexpr uint16_t LR1121_CMD_GET_RSSI_INST
cross-checked
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_MASK
static constexpr uint32_t LR1121_IRQ_DIO_ENABLE_MASK
DIO-routed IRQ enable mask: TxDone|RxDone|PreambleDetected|SyncWordValid|Timeout (design implementati...
static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_VALUE
static constexpr uint16_t LR1121_CMD_READ_BUFFER
cross-checked
static constexpr uint16_t LR1121_CMD_SET_TCXO_MODE
cross-checked
static constexpr uint16_t LR1121_CMD_GET_STATUS
cross-checked
static constexpr uint8_t LR1121_IMAGE_CAL_FREQ1
Banded image calibration for 868.25-869.85MHz +/-4MHz (~860-876MHz), matching RadioLib's setFrequency...
static constexpr uint8_t LR1121_IMAGE_CAL_FREQ2
Shared driver flow for radios using the software PHY (SX1262, LR1121).
All runtime tunable parameters for pairing and radio diagnostics.
uint16_t lr1121_discovery_hop_slice_ms
Per-channel dwell while LR1121 discovery hops.
LR1121RxBandwidth lr1121_rx_bandwidth
LR1121 RX bandwidth selector.
uint16_t lr1121_post_tx_settle_us
Delay after LR1121 TX before RX (µs).
uint16_t lr1121_response_preamble
LR1121 response preamble in bytes.