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
31#include <cstddef>
32
33namespace esphome {
34namespace home_io_control {
35
36// ============================================================================
37// LR1121 Opcode Constants (16-bit)
38// ============================================================================
39//
40// The opcodes below marked "cross-checked" were verified against Semtech's SWDR001 driver and
41// RadioLib's LR11x0_commands.h before first use, rather than inferred from observed traffic.
42
43static constexpr uint16_t LR1121_CMD_GET_STATUS = 0x0100; ///< cross-checked
44static constexpr uint16_t LR1121_CMD_GET_VERSION = 0x0101; ///< cross-checked
45static constexpr uint16_t LR1121_CMD_GET_ERRORS = 0x010D; ///< cross-checked (2-byte response)
46static constexpr uint16_t LR1121_CMD_CLEAR_ERRORS =
47 0x010E; ///< hardware-verified (called on every init/RX cycle, never rejected — see log_command_status_())
48static constexpr uint16_t LR1121_CMD_WRITE_BUFFER = 0x0109; ///< cross-checked
49static constexpr uint16_t LR1121_CMD_READ_BUFFER = 0x010A; ///< cross-checked
50static constexpr uint16_t LR1121_CMD_WRITE_REG_MEM_MASK32 =
51 0x010C; ///< cross-checked (Semtech SWDR001 lr11xx_radio.c / RadioLib LR11x0_commands.h)
52static constexpr uint16_t LR1121_CMD_CALIBRATE = 0x010F; ///< cross-checked
53static constexpr uint16_t LR1121_CMD_CALIBRATE_IMAGE = 0x0111; ///< cross-checked (RadioLib calibrateImageRejection)
54static constexpr uint16_t LR1121_CMD_SET_DIO_AS_RF_SWITCH = 0x0112; ///< cross-checked
55static constexpr uint16_t LR1121_CMD_SET_DIO_IRQ_PARAMS = 0x0113; ///< cross-checked
56static constexpr uint16_t LR1121_CMD_CLEAR_IRQ = 0x0114; ///< cross-checked
57static constexpr uint16_t LR1121_CMD_SET_TCXO_MODE = 0x0117; ///< cross-checked
58static constexpr uint16_t LR1121_CMD_SET_STANDBY = 0x011C; ///< cross-checked
59static constexpr uint16_t LR1121_CMD_GET_RX_BUFFER_STATUS = 0x0203; ///< cross-checked
60static constexpr uint16_t LR1121_CMD_GET_PKT_STATUS =
61 0x0204; ///< cross-checked (Semtech SWDR001 lr11xx_radio.c :: lr11xx_radio_get_gfsk_pkt_status —
62 ///< 0 request params, 4-byte GFSK response: [rssi_sync, rssi_avg, rx_len, status_flags],
63 ///< rssi_sync/avg both decode via -(raw>>1) dBm, same formula as LR1121_CMD_GET_RSSI_INST)
64static constexpr uint16_t LR1121_CMD_GET_RSSI_INST = 0x0205; ///< cross-checked
65static constexpr uint16_t LR1121_CMD_SET_GFSK_SYNC_WORD = 0x0206; ///< cross-checked
66static constexpr uint16_t LR1121_CMD_SET_RX = 0x0209; ///< cross-checked
67static constexpr uint16_t LR1121_CMD_SET_TX = 0x020A; ///< cross-checked
68static constexpr uint16_t LR1121_CMD_SET_RF_FREQUENCY = 0x020B; ///< cross-checked
69static constexpr uint16_t LR1121_CMD_SET_PACKET_TYPE = 0x020E; ///< cross-checked
70static constexpr uint16_t LR1121_CMD_SET_MODULATION_PARAMS = 0x020F; ///< cross-checked
71static constexpr uint16_t LR1121_CMD_SET_PACKET_PARAMS = 0x0210; ///< cross-checked
72static constexpr uint16_t LR1121_CMD_SET_TX_PARAMS = 0x0211; ///< cross-checked
73static constexpr uint16_t LR1121_CMD_SET_RX_TX_FALLBACK_MODE = 0x0213; ///< cross-checked
74static constexpr uint16_t LR1121_CMD_SET_PA_CONFIG = 0x0215; ///< cross-checked
75
76// ============================================================================
77// LR1121 IRQ Bit Masks (32-bit word)
78// ============================================================================
79// Bit positions are cross-checked against RadioLib and the LR1121 datasheet.
80
81static constexpr uint32_t LR1121_IRQ_TX_DONE = 1UL << 2;
82static constexpr uint32_t LR1121_IRQ_RX_DONE = 1UL << 3;
83static constexpr uint32_t LR1121_IRQ_PREAMBLE_DETECTED = 1UL << 4;
84static constexpr uint32_t LR1121_IRQ_SYNC_WORD_VALID = 1UL << 5;
85static constexpr uint32_t LR1121_IRQ_CRC_ERR = 1UL << 7;
86static constexpr uint32_t LR1121_IRQ_TIMEOUT = 1UL << 10;
87
88/// IRQ bits that represent a *terminal* radio event — a frame finished decoding, an outbound
89/// frame completed, or the chip gave up on its own. Deliberately excludes PREAMBLE_DETECTED,
90/// even though it's part of LR1121_IRQ_DIO_ENABLE_MASK: a preamble alone means a frame may
91/// still be arriving, so SoftPhyDriverBase's poll_until_activity_()/check_for_packet() (gated by
92/// @ref RadioLR1121::activity_irq_mask) must not treat it as terminal — doing so would tear down
93/// RX mid-reception.
94static constexpr uint32_t LR1121_IRQ_ACTIVITY_MASK =
96
97/// DIO-routed IRQ enable mask: TxDone|RxDone|PreambleDetected|SyncWordValid|Timeout.
98/// Deliberately excludes CrcErr from the DIO-routed set, unlike SX1262; CrcErr is still visible
99/// in the raw status word for capture diagnostics.
103
104// ============================================================================
105// LR1121 chip identity / GFSK / packet constants
106// ============================================================================
107
108static constexpr uint8_t LR1121_DEVICE_TYPE = 0x03; ///< cross-checked against RadioLib and the datasheet
109
110// GetErrors bit masks — Semtech's SWTL001 `lr11xx_system_errors_e`. Bits 8+ are undocumented there.
111static constexpr uint16_t LR1121_ERR_LF_RC_CALIB = 0x0001; ///< Low-frequency RC oscillator calibration failed.
112static constexpr uint16_t LR1121_ERR_HF_RC_CALIB = 0x0002; ///< High-frequency RC oscillator calibration failed.
113static constexpr uint16_t LR1121_ERR_ADC_CALIB = 0x0004; ///< ADC calibration failed.
114static constexpr uint16_t LR1121_ERR_PLL_CALIB = 0x0008; ///< PLL calibration failed.
115static constexpr uint16_t LR1121_ERR_IMG_CALIB = 0x0010; ///< Image calibration failed.
116static constexpr uint16_t LR1121_ERR_HF_XOSC_START = 0x0020; ///< High-frequency crystal/TCXO failed to start.
117static constexpr uint16_t LR1121_ERR_LF_XOSC_START = 0x0040; ///< Low-frequency crystal failed to start.
118static constexpr uint16_t LR1121_ERR_PLL_LOCK = 0x0080; ///< PLL failed to lock.
119
120/// @brief Expand a GetErrors word into a human-readable `NAME|NAME|...` string.
121/// @param errors Raw GetErrors bitmask.
122/// @param buf Caller-owned output buffer; always NUL-terminated on return.
123/// @param buf_size Size of @p buf. Use @ref DEVICE_ERROR_STR_SIZE.
124///
125/// Writes `"none"` when @p errors is zero, and appends `UNKNOWN_0x%04X` for any set bit with no
126/// name in the table so an undocumented flag still shows up in the log.
127void lr1121_format_device_errors(uint16_t errors, char *buf, size_t buf_size);
128
129/// Newest LR1121 transceiver firmware known at the time this file was last updated, per the
130/// version-numbered filenames and CHANGELOG.md at
131/// https://github.com/Lora-net/radio_firmware_images/tree/master/lr1121/transceiver — the same
132/// two bytes GetVersion reports as fw_major/fw_minor (e.g. `lr1121_transceiver_0104.bin` is
133/// major=0x01, minor=0x04). Deliberately NOT queried live (baked in, per design decision) — this
134/// is a point-in-time snapshot that will go stale as Semtech ships new firmware, and needs a
135/// manual refresh here when it does.
136///
137/// Last checked 2026-08-05: latest is 0x0104 (2026-04-01), which per that changelog fixes
138/// CVE-2025-14857/-14858/-14859 (see https://www.semtech.com/company/security/security-bulletins)
139/// on top of earlier feature/bugfix releases — not just a cosmetic version bump.
140///
141/// Reachability caveat: this project's `lr1121_firmware_update:` feature (docs/lr1121-firmware.md)
142/// can flash any published image, but 0x0104 requires bootloader 0x2101. Reaching it from the
143/// common 0x2100 bootloader needs the separate, opt-in bootloader rewrite (ADR 0021) — an
144/// irreversible operation gated behind its own arming switch — so without that opted in, 0x0103
145/// is the practical ceiling. The "update available" line below can therefore point at a version
146/// that is not reachable on a given chip as currently configured; the flash feature's own
147/// boot-time bootloader-version report is what tells a user which case they are in.
148static constexpr uint8_t LR1121_KNOWN_LATEST_FW_MAJOR = 0x01;
149static constexpr uint8_t LR1121_KNOWN_LATEST_FW_MINOR = 0x04;
150
151/// Pure comparison against the known-latest constants above — no I/O, host-testable. Only ever
152/// flags a version as outdated when it is strictly older than what this file knows about;
153/// versions equal to or newer than LR1121_KNOWN_LATEST_FW_* (including any real future firmware
154/// this snapshot predates) are never flagged, so a stale baked-in constant fails silent rather
155/// than crying wolf.
156[[nodiscard]] constexpr bool lr1121_firmware_is_outdated(uint8_t fw_major, uint8_t fw_minor) {
157 return (fw_major < LR1121_KNOWN_LATEST_FW_MAJOR) ||
159}
160
161static constexpr uint8_t LR1121_PACKET_TYPE_GFSK = 0x01; ///< cross-checked against RadioLib
162static constexpr uint8_t LR1121_GFSK_CRC_OFF = 0x01; ///< cross-checked (same encoding as SX126x)
163static constexpr uint8_t LR1121_GFSK_PACKET_FIXED_LENGTH = 0x00; ///< cross-checked (same encoding as SX126x)
164/// Preamble detector length selector: 16 bits. Shares the SX126x GFSK preamble-detector enum
165/// (0x00 off, 0x04=8bit, 0x05=16bit, 0x06=24bit, 0x07=32bit) — the LR11xx sub-GHz GFSK modem
166/// descends from the same core.
167static constexpr uint8_t LR1121_PREAMBLE_DETECTOR_16_BIT = 0x05;
168/// Sync word length: 24 bits, encoded as a literal bit count — cross-checked, identical encoding
169/// to SX1262's SX1262_SYNC_WORD_PARAM_24_BITS.
170static constexpr uint8_t LR1121_SYNC_WORD_PARAM_24_BITS = 0x18;
171
172/// Calibrate "all blocks" bitmask. LR11xx calibration blocks are LF-RC(0)/HF-RC(1)/PLL(2)/
173/// ADC(3)/IMG(4)/PLL-TX(5), so "all" = 0x3F — NOT the SX126x value (0x7F), which uses a
174/// different bit layout for a different chip.
175static constexpr uint8_t LR1121_CALIBRATE_ALL_BLOCKS = 0x3F;
176
177/// SetRxTxFallbackMode value for STDBY_XOSC. LR11xx fallback-mode is a small sequential enum
178/// (FS=0x00, STDBY_RC=0x01, STDBY_XOSC=0x02), unlike SX126x's raw standby-mode byte (0x30)
179/// reused directly in that field.
180static constexpr uint8_t LR1121_FALLBACK_STDBY_XOSC = 0x02;
181
182/// Offset a reception is written to in the LR1121's data buffer.
183///
184/// Unlike the SX1262 there is no SetBufferBaseAddress on this chip — the whole 256-byte buffer is
185/// one area whose pointers the chip manages, and a fresh reception after SetRx starts at the
186/// bottom of it (the same buffer model @ref RadioLR1121::write_buffer_'s own doc comment records
187/// for the TX direction). Used as the read offset for the length-driven receive; see
188/// @ref RadioLR1121::early_rx_read_offset.
189static constexpr uint8_t LR1121_RX_BUFFER_BASE = 0x00;
190
191// ============================================================================
192// Vendor errata / calibration workaround registers — Semtech's own driver (lr11xx_radio.c) and
193// RadioLib both apply these unconditionally. Register addresses/masks/values are byte-for-byte
194// matches to `lr11xx_radio_apply_high_acp_workaround` and `lr11xx_workaround_gfsk_reset`.
195// ============================================================================
196
197/// High-ACP (adjacent channel power) TX-quality erratum: clear bit 30 of this register before
198/// every SetRx/SetTx. Without it the chip's TX spectrum has excess spectral regrowth — plausibly
199/// why our own (tolerant) monitors decode our frames byte-exact while the awning's spec-compliant
200/// receiver mostly can't.
201static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_ADDR = 0x00F30054;
202static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_MASK = 1UL << 30;
203static constexpr uint32_t LR1121_REG_HIGH_ACP_WORKAROUND_VALUE = 0x00000000;
204
205/// GFSK modulation workaround register trio, standard (non-0.6/1.2kbps) values for our 38.4kbps
206/// config — applied after every modulation-params write, mirroring RadioLib's workaroundGFSK()
207/// ("always the first step, even when resetting" per its own comment on the first write).
208static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_ADDR = 0x00F20344;
209static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_MASK = 0x00000030;
210static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_1_VALUE = 0x00000010;
211static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_ADDR = 0x00F20348;
212static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_MASK = 0x00000005;
213static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_2_VALUE = 0x00000001;
214static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_ADDR = 0x00F20244;
215static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_MASK = 0x0001FF03;
216static constexpr uint32_t LR1121_REG_GFSK_WORKAROUND_3_VALUE = 0x00000A01;
217
218/// Banded image calibration for 868.25-869.85MHz +/-4MHz (~860-876MHz), matching RadioLib's
219/// setFrequency() margin. CalibImage params are {floor((fmin-1)/4), ceil((fmax+1)/4)}.
220static constexpr uint8_t LR1121_IMAGE_CAL_FREQ1 = 0xD7;
221static constexpr uint8_t LR1121_IMAGE_CAL_FREQ2 = 0xDB;
222
223// ============================================================================
224// T3-S3 RF-switch table — sourced from the Meshtastic `tlora_t3s3_v1` variant's rfswitch.h,
225// cross-checked against LilyGo's own T3-S3 LR1121 example and confirmed on real hardware.
226// ============================================================================
227//
228// SetDioAsRfSwitch parameter layout: [enable_mask, standby, rx, tx, tx_hp, tx_hf, gnss, wifi]
229// (8 bytes total), matching RadioLib's Lr11x0::setDioAsRfSwitch() signature. Each mode byte is
230// a bitmask of which switch-controlled DIOs (bit0=DIO5, bit1=DIO6, ...) should be driven high
231// in that mode.
232static constexpr uint8_t LR1121_RFSWITCH_ENABLE_DIO5_DIO6 = 0x03; ///< DIO5 + DIO6 are switch pins.
233static constexpr uint8_t LR1121_RFSWITCH_STANDBY = 0x00; ///< Both low.
234static constexpr uint8_t LR1121_RFSWITCH_RX = 0x01; ///< DIO5 high.
235static constexpr uint8_t LR1121_RFSWITCH_TX = 0x02; ///< DIO6 high (both LP and HP PA).
236static constexpr uint8_t LR1121_RFSWITCH_TX_HP = 0x02; ///< Same as TX (LP PA only used today).
237static constexpr uint8_t LR1121_RFSWITCH_TX_HF = 0x00; ///< 2.4GHz path unused; both low.
238static constexpr uint8_t LR1121_RFSWITCH_GNSS = 0x00; ///< Unused; both low.
239static constexpr uint8_t LR1121_RFSWITCH_WIFI = 0x00; ///< Unused; both low.
240
241/// How long SoftPhyDriverBase::wait_busy_() waits for BUSY to drop before declaring the chip
242/// failed. The LR11xx runs an internal boot ROM after reset before BUSY is meaningful —
243/// cross-checked against RadioLib's LRxxxx::reset(), which waits ~300ms ("typical transition
244/// duration should be 273 ms") and then polls BUSY with a 3s timeout; matched here exactly rather
245/// than picking an arbitrary shorter value, since the only cost of a longer timeout is on the
246/// already-failing path (a genuinely dead chip takes a few seconds longer to report failure).
247/// Much longer than SX1262's (see SX1262_BUSY_TIMEOUT_MS in radio_sx1262.h) — different chips,
248/// different boot behavior, not a value that should be unified.
249static constexpr uint32_t LR1121_BUSY_TIMEOUT_MS = 3000;
250
251/// LR1121 TCXO voltage on the T3-S3 board — 3.0V, confirmed on real hardware.
252static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MSB = 0x00;
253static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_MID = 0x01;
254/// 0x140 ticks at 30.52us/tick (32.768kHz RTC) is ~9.8ms (the SX1262's tick base differs, so the
255/// same tick count means a different time there) — harmless either way, since a longer startup is safe.
256static constexpr uint8_t LR1121_TCXO_STARTUP_DELAY_TICKS_LSB = 0x40;
257
258// ============================================================================
259// LR1121 Radio Driver
260// ============================================================================
261
262/// @brief LR1121 implementation of RadioDriver.
263/// @ingroup hioc_radio
264///
265/// Manages the LR1121 via 16-bit-opcode, two-transaction SPI using the SpiAccess
266/// interface. Configures the chip in GFSK mode with software CRC-CCITT to match the
267/// IO-Homecontrol protocol (the LR1121, like the SX1262, lacks the SX1276's IoHomeOn
268/// mode). The IRQ-driven RX/TX orchestration is inherited from SoftPhyDriverBase; this
269/// class supplies the SPI transport and every LR1121-specific register/opcode encoding
270/// underneath it. See the file header for the driver's relationship to RadioSX1262.
272 public:
273 RadioLR1121(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *irq_pin, InternalGPIOPin *busy_pin,
274 uint8_t tx_power, uint8_t tcxo_voltage_code)
275 : SoftPhyDriverBase(rst_pin, busy_pin, LR1121_BUSY_TIMEOUT_MS, LR1121_RESPONSE_PREAMBLE,
276 LR1121_POST_TX_SETTLE_US),
277 spi_(spi),
278 irq_pin_(irq_pin),
279 tx_power_(tx_power),
280 tcxo_voltage_code_(tcxo_voltage_code) {}
281
282 /// @copydoc RadioDriver::init
283 bool init() override;
284 /// @brief Apply LR1121 runtime tuning: RX bandwidth, response preamble, post-TX settle delay.
290 // The chip constants below are code spans, not \ref: doxygen 1.18 can't resolve \ref to them
291 // in a whole-project build (details in proto_sizes.h). Autolinking still links them.
292 /// @brief Per-channel dwell for a rotating listen (LR1121).
293 ///
294 /// See `LR1121_DISCOVERY_HOP_SLICE_MS` for why this is measured independently rather than
295 /// inherited from SX1262, and `SX1262_DISCOVERY_HOP_SLICE_MS` for the shared short-dwell
296 /// reasoning. Governs discovery and the broadcast roll-call alike (see
297 /// @ref RadioDriver::hop_dwell_ms). The value comes from the user-facing
298 /// `lr1121_discovery_hop_slice_ms` tuning field.
299 [[nodiscard]] uint16_t hop_dwell_ms(const TuningConfig &tuning) const override {
300 return tuning.lr1121_discovery_hop_slice_ms;
301 }
302 /// @brief TX→RX turnaround capability (LR1121): slow, same as SX1262.
303 ///
304 /// See @ref RadioSX1262::has_fast_tx_rx_turnaround for the rationale — the LR1121 needs
305 /// the same standby/settle cycle between TX and RX.
306 [[nodiscard]] bool has_fast_tx_rx_turnaround() const override { return false; }
307 /// @copydoc RadioDriver::set_mode_rx
308 void set_mode_rx() override;
309 /// @copydoc RadioDriver::set_mode_standby
310 void set_mode_standby() override;
311 [[nodiscard]] const char *chip_name() const override { return "lr1121"; }
312 /// @brief Dump LR1121-specific debug info.
313 void dump_debug() override;
314
315 protected:
316 // --- Tuning helper unique to this chip (its RX-bandwidth enum is not shared with SX1262 —
317 // see tuning_config.h's LR1121RxBandwidth comment: two of the five borrowed SX1262 byte values
318 // turned out wrong for this chip during 2026-07-17 bring-up) ---
319 /// Apply the RX bandwidth selector and rewrite the modulation parameters.
320 void set_rx_bandwidth_(LR1121RxBandwidth bandwidth);
321
322 // --- SPI communication (16-bit opcode, two-transaction) ---
323 /// Write-only command: opcode + params, single NSS cycle.
324 /// @param opcode LR1121 16-bit opcode.
325 /// @param params Pointer to parameter buffer (may be nullptr).
326 /// @param len Parameter length.
327 void write_command_(uint16_t opcode, const uint8_t *params, uint8_t len);
328 /// Read-type command: write transaction, wait BUSY, then a second NSS cycle clocks out
329 /// a Stat1 status byte followed by `out_len` response bytes.
330 /// @param opcode LR1121 16-bit opcode.
331 /// @param params Pointer to request parameter buffer (may be nullptr).
332 /// @param params_len Request parameter length.
333 /// @param out Output buffer for the response bytes (excludes Stat1).
334 /// @param out_len Number of response bytes to read.
335 void read_command_(uint16_t opcode, const uint8_t *params, uint8_t params_len, uint8_t *out, uint8_t out_len);
336 /// Write into the LR1121 TX buffer (always from the chip's internal write pointer, which
337 /// resets to the buffer base for a fresh WriteBuffer sequence — unlike SX1262 there is no
338 /// separate offset parameter here).
339 void write_buffer_(const uint8_t *data, uint8_t len);
340 /// Read from the LR1121 RX buffer at a given offset (as reported by GetRxBufferStatus).
341 void read_buffer_(uint8_t offset, uint8_t len, uint8_t *data);
342 /// Log a warning if the most recently observed Stat1 command-status byte indicates the
343 /// chip rejected the previous command (a FAIL or PERR command-status code in Stat1).
344 void log_command_status_(uint16_t opcode) const;
345 /// WriteRegMemMask32: read-modify-write a 32-bit register through `mask`/`value`. Used by the
346 /// vendor errata workarounds below; opcode 0x010C.
347 void write_reg_mem_mask32_(uint32_t addr, uint32_t mask, uint32_t value);
348 /// Apply the Semtech high-ACP TX-quality erratum workaround. Must run before
349 /// every SetRx/SetTx — called from set_mode_rx() and @ref before_tx_arm rather than once at
350 /// init, mirroring Semtech's own call sites.
352 /// Apply the GFSK modulation workaround register trio. Called at the end of
353 /// write_modulation_params_() so it re-applies on every bandwidth retune too.
355 /// Issue a banded image calibration for the 868MHz operating range.
356 void calibrate_image_();
357
358 // --- Radio configuration ---
359 /// Full radio initialization (called from init()).
360 void configure_radio_();
361 /// @copydoc SoftPhyDriverBase::set_frequency_register
362 ///
363 /// Plain Hz, 32-bit — no PLL-step conversion needed on this chip.
364 void set_frequency_register(uint32_t freq_hz) override;
365 /// Configure GFSK packet parameters (preamble, payload length, CRC).
366 void set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type, uint8_t crc_type);
367 /// Apply the runtime bandwidth setting to the LR1121 modulation parameters.
369 /// @copydoc SoftPhyDriverBase::set_rx_packet_params
370 void set_rx_packet_params() override;
371 /// @copydoc SoftPhyDriverBase::set_tx_packet_params
372 void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) override {
374 }
375 /// @copydoc SoftPhyDriverBase::clear_irq_status
376 void clear_irq_status(uint32_t irq_mask) override;
377 /// @brief Read device error flags (for diagnostics only — see dump_debug()).
378 /// @return Error bitmask (2-byte response, cross-checked against RadioLib).
379 uint16_t get_errors_();
380 /// @brief Clear device error flags. Called both during init (after TCXO configuration, before
381 /// calibration) and at the end of configure_radio_(), which records them first.
382 void clear_errors_();
383 /// @copydoc SoftPhyDriverBase::fill_capture_info
384 ///
385 /// Note: RadioCaptureInfo::irq_status is uint16_t; the 32-bit IRQ word is mapped down by
386 /// taking bits [2..10] and shifting right by 2.
387 void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
388 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) override;
389
390 /// IRQ pin (DIO9) ISR — sets dio_fired flag. Runs in interrupt context.
391 static void gpio_intr(RadioLR1121 *arg);
392
393 /// @copydoc SoftPhyDriverBase::read_irq_status_raw
394 ///
395 /// Reads the raw 32-bit IRQ status word from the radio (via GetStatus).
396 uint32_t read_irq_status_raw() override;
397 /// @copydoc SoftPhyDriverBase::sync_word_valid_bit
398 [[nodiscard]] uint32_t sync_word_valid_bit() const override { return LR1121_IRQ_SYNC_WORD_VALID; }
399 /// @copydoc SoftPhyDriverBase::rx_done_bit
400 [[nodiscard]] uint32_t rx_done_bit() const override { return LR1121_IRQ_RX_DONE; }
401 /// @copydoc SoftPhyDriverBase::tx_done_bit
402 [[nodiscard]] uint32_t tx_done_bit() const override { return LR1121_IRQ_TX_DONE; }
403 /// @copydoc SoftPhyDriverBase::preamble_detected_bit
404 [[nodiscard]] uint32_t preamble_detected_bit() const override { return LR1121_IRQ_PREAMBLE_DETECTED; }
405 /// @copydoc SoftPhyDriverBase::activity_irq_mask
406 ///
407 /// Excludes PREAMBLE_DETECTED, unlike the base's "any bit" default — see
408 /// LR1121_IRQ_ACTIVITY_MASK's doc comment for why this chip needs the distinction.
409 [[nodiscard]] uint32_t activity_irq_mask() const override { return LR1121_IRQ_ACTIVITY_MASK; }
410 /// @copydoc SoftPhyDriverBase::early_rx_read_offset
411 ///
412 /// Opted in (issue #81). The LR1121's ReadBuffer8 is the same non-destructive, offset-addressed
413 /// read the SX1262's is, so the length-driven receive ports unchanged; only the offset differs,
414 /// and this chip's has to be reasoned about rather than programmed (see @ref
415 /// LR1121_RX_BUFFER_BASE).
416 ///
417 /// Not yet confirmed against a real LR1121, and deliberately shipped that way: a wrong offset
418 /// here is safe, but *not* merely because try_early_completion_() gates on CRC-CCITT. On this
419 /// chip's single shared TX/RX buffer, the bytes sitting at a wrong offset right after a
420 /// transmission would otherwise be the hub's own last-sent frame — genuinely CRC-valid, not
421 /// noise, so the CRC gate alone would not catch it. What actually makes a wrong guess harmless
422 /// is @ref invalidate_stale_rx_content_after_tx overwriting that content after every TX, so a
423 /// wrong offset degrades to reading real garbage instead — which the CRC gate (or the stage-1
424 /// length peek) then correctly rejects, falling through to the RX_DONE path exactly as it does
425 /// today. Either way it cannot cost a frame, only the latency saving. `RadioCaptureInfo::rx_offset`
426 /// (logged in @ref SoftPhyDriverBase::read_rx_packet under `IOHOME_FRAME_LOG`) carries the
427 /// chip's own post-RX_DONE answer if the assumption ever needs checking on hardware.
428 [[nodiscard]] int16_t early_rx_read_offset() const override { return LR1121_RX_BUFFER_BASE; }
429 /// @copydoc SoftPhyDriverBase::read_rssi_raw_byte
430 uint8_t read_rssi_raw_byte() override;
431 /// @copydoc SoftPhyDriverBase::write_tx_buffer
432 void write_tx_buffer(const uint8_t *data, uint8_t len) override { this->write_buffer_(data, len); }
433 /// @copydoc SoftPhyDriverBase::get_rx_buffer_status
434 void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) override;
435 /// @copydoc SoftPhyDriverBase::read_rx_buffer
436 void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) override { this->read_buffer_(offset, len, data); }
437 /// @copydoc SoftPhyDriverBase::start_tx
438 void start_tx() override;
439 /// @copydoc SoftPhyDriverBase::before_tx_arm
440 ///
441 /// High-ACP workaround — Semtech applies this unconditionally before every
442 /// SetTx, same as before every SetRx (see set_mode_rx()).
443 void before_tx_arm() override { this->apply_high_acp_workaround_(); }
444 /// @copydoc SoftPhyDriverBase::invalidate_stale_rx_content_after_tx
445 ///
446 /// Overwrites the header-peek window at @ref LR1121_RX_BUFFER_BASE with non-frame-shaped bytes
447 /// (all-zero: a run of zero bits can never satisfy `decode_uart_probe`'s stop-bit check, so
448 /// `soft_phy_peek_frame_length()` fails deterministically on it, not merely with high
449 /// probability). `write_buffer_()` always starts from the chip's own buffer base — the same
450 /// address `early_rx_read_offset()` reads from — so this needs no separate offset parameter.
451 /// Sized to exactly the bytes `try_early_completion_()`'s stage 1 reads, so it stays a single
452 /// short SPI write rather than clearing the whole probe window.
454 uint8_t const zeros[SOFT_PHY_EARLY_HEADER_RAW_BYTES] = {0};
455 this->write_buffer_(zeros, sizeof(zeros));
456 }
457
458 private:
459 SpiAccess *spi_;
460 InternalGPIOPin *irq_pin_;
461 uint8_t tx_power_;
462 uint8_t tcxo_voltage_code_; ///< TCXO_VOLTAGE_OPTIONS code from YAML (1_6V=0x00 .. 3_3V=0x07); the chip's
463 ///< SetTcxoMode voltage code directly. TCXO_VOLTAGE_NONE means no TCXO (bare crystal).
464 uint8_t last_stat1_{0}; ///< Most recent Stat1 byte (diagnostics).
465 LR1121RxBandwidth rx_bandwidth_{LR1121RxBandwidth::BW_117_3_KHZ}; ///< Runtime-tunable RX bandwidth.
466};
467
468} // namespace home_io_control
469} // 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,...
RadioLR1121(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *irq_pin, InternalGPIOPin *busy_pin, uint8_t tx_power, uint8_t tcxo_voltage_code)
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.
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_ERR_HF_RC_CALIB
High-frequency RC oscillator calibration failed.
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 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_ERR_IMG_CALIB
Image calibration failed.
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 uint16_t LR1121_ERR_LF_XOSC_START
Low-frequency crystal failed to start.
static constexpr uint32_t LR1121_IRQ_CRC_ERR
static constexpr uint16_t LR1121_CMD_GET_VERSION
cross-checked
static constexpr uint16_t LR1121_ERR_ADC_CALIB
ADC calibration failed.
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.
void lr1121_format_device_errors(uint16_t errors, char *buf, size_t buf_size)
Expand a GetErrors word into a human-readable NAME|NAME|... string.
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 uint16_t LR1121_ERR_PLL_LOCK
PLL failed to lock.
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 uint16_t LR1121_ERR_HF_XOSC_START
High-frequency crystal/TCXO failed to start.
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_ERR_PLL_CALIB
PLL calibration failed.
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
static constexpr uint16_t LR1121_ERR_LF_RC_CALIB
Low-frequency RC oscillator calibration failed.
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.