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. 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 against RadioLib and the LR1121 datasheet.
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/// Deliberately excludes CrcErr from the DIO-routed set, unlike SX1262; CrcErr is still visible
97/// 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 against RadioLib and the datasheet
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 against RadioLib
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/// Offset a reception is written to in the LR1121's data buffer.
162///
163/// Unlike the SX1262 there is no SetBufferBaseAddress on this chip — the whole 256-byte buffer is
164/// one area whose pointers the chip manages, and a fresh reception after SetRx starts at the
165/// bottom of it (the same buffer model @ref RadioLR1121::write_buffer_'s own doc comment records
166/// for the TX direction). Used as the read offset for the length-driven receive; see
167/// @ref RadioLR1121::early_rx_read_offset.
168static constexpr uint8_t LR1121_RX_BUFFER_BASE = 0x00;
169
170// ============================================================================
171// Vendor errata / calibration workaround registers — Semtech's own driver (lr11xx_radio.c) and
172// RadioLib both apply these unconditionally. Register addresses/masks/values are byte-for-byte
173// matches to `lr11xx_radio_apply_high_acp_workaround` and `lr11xx_workaround_gfsk_reset`.
174// ============================================================================
175
176/// High-ACP (adjacent channel power) TX-quality erratum: clear bit 30 of this register before
177/// every SetRx/SetTx. Without it the chip's TX spectrum has excess spectral regrowth — plausibly
178/// why our own (tolerant) monitors decode our frames byte-exact while the awning's spec-compliant
179/// receiver mostly can't.
180static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_ADDR = 0x00F30054;
181static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_MASK = 1UL << 30;
182static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_VALUE = 0x00000000;
183
184/// GFSK modulation workaround register trio, standard (non-0.6/1.2kbps) values for our 38.4kbps
185/// config — applied after every modulation-params write, mirroring RadioLib's workaroundGFSK()
186/// ("always the first step, even when resetting" per its own comment on the first write).
187static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_ADDR = 0x00F20344;
188static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_MASK = 0x00000030;
189static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_VALUE = 0x00000010;
190static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_ADDR = 0x00F20348;
191static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_MASK = 0x00000005;
192static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_VALUE = 0x00000001;
193static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_ADDR = 0x00F20244;
194static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_MASK = 0x0001FF03;
195static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_VALUE = 0x00000A01;
196
197/// Banded image calibration for 868.25-869.85MHz +/-4MHz (~860-876MHz), matching RadioLib's
198/// setFrequency() margin. CalibImage params are {floor((fmin-1)/4), ceil((fmax+1)/4)}.
199static constexpr uint8_t LR1121_IMAGE_CAL_FREQ1 = 0xD7;
200static constexpr uint8_t LR1121_IMAGE_CAL_FREQ2 = 0xDB;
201
202// ============================================================================
203// T3-S3 RF-switch table — sourced from the Meshtastic `tlora_t3s3_v1` variant's rfswitch.h,
204// cross-checked against LilyGo's own T3-S3 LR1121 example and confirmed on real hardware.
205// ============================================================================
206//
207// SetDioAsRfSwitch parameter layout: [enable_mask, standby, rx, tx, tx_hp, tx_hf, gnss, wifi]
208// (8 bytes total), matching RadioLib's Lr11x0::setDioAsRfSwitch() signature. Each mode byte is
209// a bitmask of which switch-controlled DIOs (bit0=DIO5, bit1=DIO6, ...) should be driven high
210// in that mode.
211static constexpr uint8_t LR1121_RFSWITCH_ENABLE_DIO5_DIO6 = 0x03; ///< DIO5 + DIO6 are switch pins.
212static constexpr uint8_t LR1121_RFSWITCH_STANDBY = 0x00; ///< Both low.
213static constexpr uint8_t LR1121_RFSWITCH_RX = 0x01; ///< DIO5 high.
214static constexpr uint8_t LR1121_RFSWITCH_TX = 0x02; ///< DIO6 high (both LP and HP PA).
215static constexpr uint8_t LR1121_RFSWITCH_TX_HP = 0x02; ///< Same as TX (LP PA only used today).
216static constexpr uint8_t LR1121_RFSWITCH_TX_HF = 0x00; ///< 2.4GHz path unused; both low.
217static constexpr uint8_t LR1121_RFSWITCH_GNSS = 0x00; ///< Unused; both low.
218static constexpr uint8_t LR1121_RFSWITCH_WIFI = 0x00; ///< Unused; both low.
219
220/// How long SoftPhyDriverBase::wait_busy_() waits for BUSY to drop before declaring the chip
221/// failed. The LR11xx runs an internal boot ROM after reset before BUSY is meaningful —
222/// cross-checked against RadioLib's LRxxxx::reset(), which waits ~300ms ("typical transition
223/// duration should be 273 ms") and then polls BUSY with a 3s timeout; matched here exactly rather
224/// than picking an arbitrary shorter value, since the only cost of a longer timeout is on the
225/// already-failing path (a genuinely dead chip takes a few seconds longer to report failure).
226/// Much longer than SX1262's (see SX1262_BUSY_TIMEOUT_MS in radio_sx1262.h) — different chips,
227/// different boot behavior, not a value that should be unified.
228static constexpr uint32_t LR1121_BUSY_TIMEOUT_MS = 3000;
229
230/// LR1121 TCXO voltage on the T3-S3 board — 3.0V, confirmed on real hardware.
231static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MSB = 0x00;
232static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MID = 0x01;
233/// 0x140 ticks at 30.52us/tick (32.768kHz RTC) is ~9.8ms (the SX1262's tick base differs, so the
234/// same tick count means a different time there) — harmless either way, since a longer startup is safe.
235static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_LSB = 0x40;
236
237// ============================================================================
238// LR1121 Radio Driver
239// ============================================================================
240
241/// @brief LR1121 implementation of RadioDriver.
242/// @ingroup hioc_radio
243///
244/// Manages the LR1121 via 16-bit-opcode, two-transaction SPI using the SpiAccess
245/// interface. Configures the chip in GFSK mode with software CRC-CCITT to match the
246/// IO-Homecontrol protocol (the LR1121, like the SX1262, lacks the SX1276's IoHomeOn
247/// mode). The IRQ-driven RX/TX orchestration is inherited from SoftPhyDriverBase; this
248/// class supplies the SPI transport and every LR1121-specific register/opcode encoding
249/// underneath it. See the file header for the driver's relationship to RadioSX1262.
251 public:
252 RadioLR1121(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *irq_pin, InternalGPIOPin *busy_pin,
253 uint8_t tx_power, uint8_t tcxo_voltage_yaml_code)
256 spi_(spi),
257 irq_pin_(irq_pin),
258 tx_power_(tx_power),
259 tcxo_voltage_yaml_code_(tcxo_voltage_yaml_code) {}
260
261 /// @copydoc RadioDriver::init
262 bool init() override;
263 /// @brief Apply LR1121 runtime tuning: RX bandwidth, response preamble, post-TX settle delay.
269 /// @brief Per-channel dwell for a rotating listen (LR1121).
270 ///
271 /// See @ref LR1121_DISCOVERY_HOP_SLICE_MS for why this is measured independently rather than
272 /// inherited from SX1262, and @ref SX1262_DISCOVERY_HOP_SLICE_MS for the shared short-dwell
273 /// reasoning. Governs discovery and the broadcast roll-call alike (see
274 /// @ref RadioDriver::hop_dwell_ms). The value comes from the user-facing
275 /// `lr1121_discovery_hop_slice_ms` tuning field.
276 [[nodiscard]] uint16_t hop_dwell_ms(const TuningConfig &tuning) const override {
277 return tuning.lr1121_discovery_hop_slice_ms;
278 }
279 /// @brief TX→RX turnaround capability (LR1121): slow, same as SX1262.
280 ///
281 /// See @ref RadioSX1262::has_fast_tx_rx_turnaround for the rationale — the LR1121 needs
282 /// the same standby/settle cycle between TX and RX.
283 [[nodiscard]] bool has_fast_tx_rx_turnaround() const override { return false; }
284 /// @copydoc RadioDriver::set_mode_rx
285 void set_mode_rx() override;
286 /// @copydoc RadioDriver::set_mode_standby
287 void set_mode_standby() override;
288 [[nodiscard]] const char *chip_name() const override { return "lr1121"; }
289 /// @brief Dump LR1121-specific debug info.
290 void dump_debug() override;
291
292 protected:
293 // --- Tuning helper unique to this chip (its RX-bandwidth enum is not shared with SX1262 —
294 // see tuning_config.h's LR1121RxBandwidth comment: two of the five borrowed SX1262 byte values
295 // turned out wrong for this chip during 2026-07-17 bring-up) ---
296 /// Apply the RX bandwidth selector and rewrite the modulation parameters.
297 void set_rx_bandwidth_(LR1121RxBandwidth bandwidth);
298
299 // --- SPI communication (16-bit opcode, two-transaction) ---
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).
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 (a FAIL or PERR command-status code in Stat1).
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.
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. 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. 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.
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.
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::early_rx_read_offset
388 ///
389 /// Opted in (issue #81). The LR1121's ReadBuffer8 is the same non-destructive, offset-addressed
390 /// read the SX1262's is, so the length-driven receive ports unchanged; only the offset differs,
391 /// and this chip's has to be reasoned about rather than programmed (see @ref
392 /// LR1121_RX_BUFFER_BASE).
393 ///
394 /// Not yet confirmed against a real LR1121, and deliberately shipped that way: a wrong offset
395 /// here is safe, but *not* merely because try_early_completion_() gates on CRC-CCITT. On this
396 /// chip's single shared TX/RX buffer, the bytes sitting at a wrong offset right after a
397 /// transmission would otherwise be the hub's own last-sent frame — genuinely CRC-valid, not
398 /// noise, so the CRC gate alone would not catch it. What actually makes a wrong guess harmless
399 /// is @ref invalidate_stale_rx_content_after_tx overwriting that content after every TX, so a
400 /// wrong offset degrades to reading real garbage instead — which the CRC gate (or the stage-1
401 /// length peek) then correctly rejects, falling through to the RX_DONE path exactly as it does
402 /// today. Either way it cannot cost a frame, only the latency saving. `RadioCaptureInfo::rx_offset`
403 /// (logged in @ref SoftPhyDriverBase::read_rx_packet under `IOHOME_FRAME_LOG`) carries the
404 /// chip's own post-RX_DONE answer if the assumption ever needs checking on hardware.
405 [[nodiscard]] int16_t early_rx_read_offset() const override { return LR1121_RX_BUFFER_BASE; }
406 /// @copydoc SoftPhyDriverBase::read_rssi_raw_byte
407 uint8_t read_rssi_raw_byte() override;
408 /// @copydoc SoftPhyDriverBase::write_tx_buffer
409 void write_tx_buffer(const uint8_t *data, uint8_t len) override { this->write_buffer_(data, len); }
410 /// @copydoc SoftPhyDriverBase::get_rx_buffer_status
411 void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) override;
412 /// @copydoc SoftPhyDriverBase::read_rx_buffer
413 void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) override { this->read_buffer_(offset, len, data); }
414 /// @copydoc SoftPhyDriverBase::start_tx
415 void start_tx() override;
416 /// @copydoc SoftPhyDriverBase::before_tx_arm
417 ///
418 /// High-ACP workaround — Semtech applies this unconditionally before every
419 /// SetTx, same as before every SetRx (see set_mode_rx()).
420 void before_tx_arm() override { this->apply_high_acp_workaround_(); }
421 /// @copydoc SoftPhyDriverBase::invalidate_stale_rx_content_after_tx
422 ///
423 /// Overwrites the header-peek window at @ref LR1121_RX_BUFFER_BASE with non-frame-shaped bytes
424 /// (all-zero: a run of zero bits can never satisfy `decode_uart_probe`'s stop-bit check, so
425 /// `soft_phy_peek_frame_length()` fails deterministically on it, not merely with high
426 /// probability). `write_buffer_()` always starts from the chip's own buffer base — the same
427 /// address `early_rx_read_offset()` reads from — so this needs no separate offset parameter.
428 /// Sized to exactly the bytes `try_early_completion_()`'s stage 1 reads, so it stays a single
429 /// short SPI write rather than clearing the whole probe window.
431 uint8_t const zeros[SOFT_PHY_EARLY_HEADER_RAW_BYTES] = {0};
432 this->write_buffer_(zeros, sizeof(zeros));
433 }
434
435 private:
436 SpiAccess *spi_;
437 InternalGPIOPin *irq_pin_;
438 uint8_t tx_power_;
439 uint8_t tcxo_voltage_yaml_code_; ///< Raw TCXO_VOLTAGE_OPTIONS code from YAML (1_6V=0x01 .. 3_3V=0x08).
440 uint8_t last_stat1_{0}; ///< Most recent Stat1 byte (diagnostics).
441 LR1121RxBandwidth rx_bandwidth_{LR1121RxBandwidth::BW_117_3_KHZ}; ///< Runtime-tunable RX bandwidth.
442};
443
444} // namespace home_io_control
445} // 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
int16_t early_rx_read_offset() const override
Data-buffer offset an in-flight reception is being written to, or a negative value when this chip mus...
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.
void invalidate_stale_rx_content_after_tx() override
Hook run from rearm_rx_after_tx_, after a transmission and before re-entering RX.
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 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.
void apply_gfsk_workaround_()
Apply the GFSK modulation workaround register trio.
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.
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).
uint16_t hop_dwell_ms(const TuningConfig &tuning) const override
Per-channel dwell for a rotating listen (LR1121).
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.
SoftPhyDriverBase(InternalGPIOPin *rst_pin, InternalGPIOPin *busy_pin, uint32_t busy_timeout_ms, 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 against RadioLib and the datasheet
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_VALUE
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 uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES
Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART cell (10 b...
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_RX_BUFFER_BASE
Offset a reception is written to in the LR1121's data buffer.
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 against RadioLib
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 (the SX1262's tick base differs,...
static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_VALUE
static constexpr uint32_t LR1121_BUSY_TIMEOUT_MS
How long SoftPhyDriverBase::wait_busy_() waits for BUSY to drop before declaring the chip failed.
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.
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.