Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_lr1121.cpp
Go to the documentation of this file.
1/// @file radio_lr1121.cpp
2/// @brief LR1121 radio driver implementation for IO-Homecontrol.
3/// @ingroup hioc_radio
4///
5/// See radio_lr1121.h for the architectural context: this driver is a re-plumbed
6/// RadioSX1262 over a different SPI command set. Everything about the software UART
7/// PHY (TX bit-encoding, RX probe/CRC recovery) is identical to the SX1262 and lives
8/// in radio_soft_phy.{h,cpp}; the IRQ-driven RX/TX orchestration both drivers share is
9/// in SoftPhyDriverBase (radio_soft_phy_driver_base.{h,cpp}). This file only has to
10/// reproduce the LR1121-specific transport (16-bit opcodes, two-transaction
11/// command/response, 32-bit IRQ word) and the chip's own GFSK/RF-switch/TCXO/PA
12/// configuration.
13
14// Opcode payloads, line-coding widths, and recovery thresholds are written in the same shape
15// as the chip protocol and on-air framing they reproduce.
16// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
17
18#include "radio_lr1121.h"
19#include "esphome/core/log.h"
20#include "esphome/core/application.h"
21
22#include <cinttypes>
23
24namespace esphome {
25namespace home_io_control {
26
27static const char *const TAG = "home_io_control.lr1121";
28
29// The LR11xx runs an internal boot ROM after reset before BUSY is meaningful — cross-checked
30// against RadioLib's LRxxxx::reset(), which waits ~300ms ("typical transition duration should
31// be 273 ms") and then polls BUSY with a 3s timeout; matched here exactly rather than picking
32// an arbitrary shorter value, since the only cost of a longer timeout is on the already-failing
33// path (a genuinely dead chip takes a few seconds longer to report failure).
34static const uint32_t LR1121_BUSY_TIMEOUT_MS = 3000;
35
36// === SPI Communication (16-bit opcode, two-transaction) ===
37
39 // Once a BUSY timeout has failed the driver, every later wait_busy_() call would otherwise
40 // re-run the same timeout again — init()'s remaining ~16 configure_radio_() steps would each
41 // block for LR1121_BUSY_TIMEOUT_MS before init() finally returns false. Short-circuit instead:
42 // the chip is already known-unresponsive, so there's nothing to wait for.
43 if (this->failed_)
44 return;
45 uint32_t const start = millis();
46 while (this->busy_pin_->digital_read()) {
47 if (millis() - start > LR1121_BUSY_TIMEOUT_MS) {
48 ESP_LOGE(TAG, "BUSY timeout");
49 this->failed_ = true;
50 return;
51 }
52 App.feed_wdt();
53 }
54}
55
56void RadioLR1121::write_command_(uint16_t opcode, const uint8_t *params, uint8_t len) {
57 // Write-only phase: MISO is don't-care while we send the opcode + params, so spi_write()
58 // (MOSI only) is used rather than spi_transfer() — the two are equivalent on real hardware,
59 // but only spi_write() correctly signals "no response byte here" to the SpiAccess contract.
60 this->wait_busy_();
61 this->spi_->spi_enable();
62 this->spi_->spi_write((opcode >> 8) & 0xFF);
63 this->spi_->spi_write(opcode & 0xFF);
64 for (uint8_t i = 0; i < len; i++)
65 this->spi_->spi_write(params[i]);
66 this->spi_->spi_disable();
67}
68
69void RadioLR1121::read_command_(uint16_t opcode, const uint8_t *params, uint8_t params_len, uint8_t *out,
70 uint8_t out_len) {
71 // First transaction: send opcode + request params, NSS up. The chip raises BUSY while it
72 // prepares the response.
73 this->write_command_(opcode, params, params_len);
74 // Second transaction: wait BUSY low again, then clock out Stat1 followed by the response
75 // (MOSI don't-care here, so spi_read() rather than spi_transfer(0x00)).
76 this->wait_busy_();
77 this->spi_->spi_enable();
78 this->last_stat1_ = this->spi_->spi_read(); // Stat1 — command status byte.
79 for (uint8_t i = 0; i < out_len; i++)
80 out[i] = this->spi_->spi_read();
81 this->spi_->spi_disable();
82 this->log_command_status_(opcode);
83}
84
85void RadioLR1121::log_command_status_(uint16_t opcode) const {
86 // Stat1 bits [3:1] encode the previous command's fate (0=CMD_FAIL, 1=CMD_PERR "processing
87 // error", 2=CMD_OK, 3=CMD_DAT); bit 0 is a separate IRQ-active flag. Any FAIL/PERR means the
88 // chip silently rejected the command — surfaced unconditionally (not gated on
89 // IOHOME_FRAME_LOG) since this whole driver has been blind to command rejections until now.
90 uint8_t const cmd_status = (this->last_stat1_ >> 1) & 0x07;
91 if (cmd_status == 0x00 || cmd_status == 0x01) {
92 ESP_LOGW(TAG, "cmd 0x%04X rejected: stat1=0x%02X (%s)", opcode, this->last_stat1_,
93 cmd_status == 0x00 ? "CMD_FAIL" : "CMD_PERR");
94 }
95#ifdef IOHOME_FRAME_LOG
96 ESP_LOGV(TAG, "cmd 0x%04X stat1=0x%02X", opcode, this->last_stat1_);
97#endif
98}
99
100void RadioLR1121::write_reg_mem_mask32_(uint32_t addr, uint32_t mask, uint32_t value) {
101 uint8_t params[12] = {
102 (uint8_t) (addr >> 24), (uint8_t) (addr >> 16), (uint8_t) (addr >> 8), (uint8_t) addr,
103 (uint8_t) (mask >> 24), (uint8_t) (mask >> 16), (uint8_t) (mask >> 8), (uint8_t) mask,
104 (uint8_t) (value >> 24), (uint8_t) (value >> 16), (uint8_t) (value >> 8), (uint8_t) value,
105 };
106 this->write_command_(LR1121_CMD_WRITE_REG_MEM_MASK32, params, sizeof(params));
107}
108
113
122
124 uint8_t params[2] = {LR1121_IMAGE_CAL_FREQ1, LR1121_IMAGE_CAL_FREQ2};
125 this->write_command_(LR1121_CMD_CALIBRATE_IMAGE, params, sizeof(params));
126}
127
128void RadioLR1121::write_buffer_(const uint8_t *data, uint8_t len) {
129 this->write_command_(LR1121_CMD_WRITE_BUFFER, data, len);
130}
131
132void RadioLR1121::read_buffer_(uint8_t offset, uint8_t len, uint8_t *data) {
133 uint8_t params[2] = {offset, len};
134 this->read_command_(LR1121_CMD_READ_BUFFER, params, sizeof(params), data, len);
135}
136
138 // GetStatus returns Stat1, Stat2, then the 32-bit IRQ status word (design §3.2:
139 // "read IRQ status non-destructively via GetStatus") — cross-checked against RadioLib's
140 // LRxxxx::getIrqStatus(): 6 bytes total [Stat1, Stat2, IRQ_b3..IRQ_b0]. read_command_
141 // already consumes Stat1 into last_stat1_, so only 5 more bytes remain: Stat2 (resp[0],
142 // unused) followed by the 4 IRQ bytes (resp[1..4]).
143 uint8_t resp[5] = {0};
144 this->read_command_(LR1121_CMD_GET_STATUS, nullptr, 0, resp, sizeof(resp));
145 return (static_cast<uint32_t>(resp[1]) << 24) | (static_cast<uint32_t>(resp[2]) << 16) |
146 (static_cast<uint32_t>(resp[3]) << 8) | static_cast<uint32_t>(resp[4]);
147}
148
149// === Packet params helper ===
150
151void RadioLR1121::set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type,
152 uint8_t crc_type) {
153 // Same 9-field shape as RadioSX1262::set_packet_params_ (design §3.2 step 7 gives the LR1121
154 // field values as a direct match to the SX1262's), just written via the LR1121 opcode.
155 //
156 // preamble_len arrives in bytes (see RadioSX1262::set_packet_params_ for why); this chip's
157 // field is bit-denominated too — Semtech names it lr11xx_radio_pkt_params_gfsk_t.pbl_len_in_bit
158 // (reference/SWTL001/lr11xx_driver/src/lr11xx_radio_types.h:545) — so convert here as well.
159 const uint16_t preamble_bits = preamble_len * 8;
160 uint8_t params[9] = {
161 (uint8_t) (preamble_bits >> 8), // Preamble length MSB
162 (uint8_t) preamble_bits, // Preamble length LSB
163 LR1121_PREAMBLE_DETECTOR_16_BIT, // Preamble detector: 16 bits
164 LR1121_SYNC_WORD_PARAM_24_BITS, // Sync word length: 24 bits
165 0x00, // Address comparison: off
166 packet_type, // GFSK packet type: known length or variable size
167 payload_len, // Configured payload length
168 crc_type, // CRC type
169 0x00, // Whitening: off
170 };
171 this->write_command_(LR1121_CMD_SET_PACKET_PARAMS, params, sizeof(params));
172}
173
175 // Same fixed-length raw-probe strategy as SX1262 (SOFT_PHY_RX_PROBE_PACKET_LEN, shared —
176 // see its doc comment in radio_soft_phy_driver_base.h): the LR1121 gets the identical
177 // treatment because the reasoning is about UART-packed frame sizes, not chip-specific
178 // packet-engine behavior.
180}
181
183 // LR1121 GFSK modulation parameters for the IO-Homecontrol 868 MHz waveform, written as
184 // plain Hz/bps 32-bit values (design §1.2: "RF frequency / bitrate / fdev are plain Hz /
185 // bit/s 32-bit values" — no PLL-step math needed, unlike SX1262). Field order (bitrate,
186 // pulse shape, bandwidth, fdev) cross-checked against RadioLib/Semtech and confirmed on
187 // real hardware.
188 uint32_t const bitrate_bps = 38400;
189 uint32_t const fdev_hz = 19200;
190 uint8_t mod_params[10] = {
191 (uint8_t) (bitrate_bps >> 24),
192 (uint8_t) (bitrate_bps >> 16),
193 (uint8_t) (bitrate_bps >> 8),
194 (uint8_t) bitrate_bps, // Bitrate: 38400 bps
195 0x0B, // Pulse shape: Gaussian BT=1.0 (same encoding as SX126x)
196 static_cast<uint8_t>(this->rx_bandwidth_), // Bandwidth: runtime-tunable (same encoding as SX126x)
197 (uint8_t) (fdev_hz >> 24),
198 (uint8_t) (fdev_hz >> 16),
199 (uint8_t) (fdev_hz >> 8),
200 (uint8_t) fdev_hz, // Fdev: 19200 Hz
201 };
202 this->write_command_(LR1121_CMD_SET_MODULATION_PARAMS, mod_params, sizeof(mod_params));
203
204 // GFSK workaround register trio (analysis §4.2) — RadioLib/Semtech apply this after every
205 // modulation-params write, not just once at init, so it must re-run on every bandwidth retune.
207}
208
210 uint8_t errors_raw[2] = {0};
211 this->read_command_(LR1121_CMD_GET_ERRORS, nullptr, 0, errors_raw, sizeof(errors_raw));
212 return (static_cast<uint16_t>(errors_raw[0]) << 8) | errors_raw[1];
213}
214
216
217void RadioLR1121::clear_irq_status(uint32_t irq_mask) {
218 uint8_t clear_irq[4] = {
219 (uint8_t) ((irq_mask >> 24) & 0xFF),
220 (uint8_t) ((irq_mask >> 16) & 0xFF),
221 (uint8_t) ((irq_mask >> 8) & 0xFF),
222 (uint8_t) (irq_mask & 0xFF),
223 };
224 this->write_command_(LR1121_CMD_CLEAR_IRQ, clear_irq, sizeof(clear_irq));
225}
226
227void RadioLR1121::fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
228 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
229 // GetPktStatus (0x0204), not GetRssiInst (0x0205): the latter is a live, instantaneous RSSI
230 // read unrelated to any specific frame, whereas GetPktStatus is atomically tied to the
231 // last-received packet — the reported RSSI must reflect the frame that was actually received,
232 // not the channel's state whenever this function happens to run. Mirrors
233 // RadioSX1262::fill_capture_info()'s use of its own GetPacketStatus. byte[0] is rssi_sync
234 // (RSSI at sync-word detection), matching the sync-not-avg field SX1262 uses
235 // (packet_status[1] there).
236 uint8_t pkt_status[4] = {0};
237 this->read_command_(LR1121_CMD_GET_PKT_STATUS, nullptr, 0, pkt_status, sizeof(pkt_status));
238
239 this->populate_capture_base_(blocking_wait, this->current_freq_, -(int16_t) pkt_status[0] / 2, raw, raw_len, frame,
240 frame_len);
241 this->last_capture_.rx_done = (irq_status & LR1121_IRQ_RX_DONE) != 0;
242 this->last_capture_.crc_error = (irq_status & LR1121_IRQ_CRC_ERR) != 0;
243 // RadioCaptureInfo::irq_status is uint16_t; map the 32-bit word down by taking bits [2..10]
244 // and shifting right by 2 (design §3.2 "IRQ-width note" — one documented place for this).
245 this->last_capture_.irq_status = static_cast<uint16_t>((irq_status >> 2) & 0x01FF);
246 this->last_capture_.packet_status = pkt_status[3];
247 this->last_capture_.rx_offset = rx_offset;
248 this->last_capture_.reported_len = reported_len;
249}
250
252 uint8_t raw = 0;
253 this->read_command_(LR1121_CMD_GET_RSSI_INST, nullptr, 0, &raw, 1);
254 return raw;
255}
256
257void RadioLR1121::get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) {
258 // GetRxBufferStatus response layout mirrors SX1262: [length, offset].
259 uint8_t rx_status[2] = {0};
260 this->read_command_(LR1121_CMD_GET_RX_BUFFER_STATUS, nullptr, 0, rx_status, sizeof(rx_status));
261 reported_len = rx_status[0];
262 rx_offset = rx_status[1];
263}
264
266 // Tick base is 30.52us (32.768kHz RTC), so 0x03E800 is ~7.8s, not the ~4s the field was
267 // originally assumed to encode; the software 4000ms guard in the shared send_packet() is what
268 // actually bounds TX and is independent of this chip-level value.
269 uint8_t tx_timeout[3] = {0x03, 0xE8, 0x00};
270 this->write_command_(LR1121_CMD_SET_TX, tx_timeout, sizeof(tx_timeout));
271}
272
273// === ISR ===
274
276
277// === Initialization ===
278
280 // --- Pin setup ---
281 this->rst_pin_->setup();
282 this->irq_pin_->setup();
283 this->busy_pin_->setup();
284
285 // --- Hardware reset ---
286 this->reset_hardware_();
287 this->wait_busy_();
288 if (this->failed_)
289 return false;
290
291 this->configure_radio_();
292 if (this->failed_)
293 return false;
294
295 ESP_LOGI(TAG, "LR1121 initialized");
296 return true;
297}
298
300 uint8_t version[4] = {0};
301 this->read_command_(LR1121_CMD_GET_VERSION, nullptr, 0, version, sizeof(version));
302 uint32_t const irq = this->read_irq_status_raw();
303 uint16_t const errors = this->get_errors_();
304
305 ESP_LOGCONFIG(TAG, " LR1121 Diagnostic:");
306 ESP_LOGCONFIG(TAG, " Device type: 0x%02X (expect 0x%02X)", version[1], LR1121_DEVICE_TYPE);
307 ESP_LOGCONFIG(TAG, " HW version: 0x%02X, FW version: %u.%u", version[0], version[2], version[3]);
308 if (lr1121_firmware_is_outdated(version[2], version[3])) {
309 ESP_LOGCONFIG(TAG,
310 " Newer firmware available (%u.%u), see "
311 "https://github.com/Lora-net/radio_firmware_images/blob/master/lr1121/transceiver/README.md",
313 }
314 ESP_LOGCONFIG(TAG, " BUSY=%d IRQ=%d", this->busy_pin_->digital_read(), this->irq_pin_->digital_read());
315 ESP_LOGCONFIG(TAG, " IRQ status: 0x%08" PRIX32, irq);
316 ESP_LOGCONFIG(TAG, " Device errors: 0x%04X", errors);
317 ESP_LOGCONFIG(TAG, " Last Stat1: 0x%02X", this->last_stat1_);
318}
319
321 // 1. Identity check: GetVersion — device type must be LR1121 (0x03). Response layout
322 // cross-checked against RadioLib's LR11x0::getVersion(): [hw_version, device_type,
323 // fw_major, fw_minor] (4 bytes) — device type is byte 1, not byte 0.
324 uint8_t version[4] = {0};
325 this->read_command_(LR1121_CMD_GET_VERSION, nullptr, 0, version, sizeof(version));
326 if (version[1] != LR1121_DEVICE_TYPE) {
327 ESP_LOGE(TAG, "Unexpected device type 0x%02X (expected 0x%02X) — not an LR1121?", version[1], LR1121_DEVICE_TYPE);
328 this->failed_ = true;
329 return;
330 }
331 ESP_LOGI(TAG, "LR1121 detected: hw=0x%02X fw=%u.%u", version[0], version[2], version[3]);
332
333 // 2. TCXO: map the YAML TCXO_VOLTAGE_OPTIONS code (1_6V=0x01 .. 3_3V=0x08, __init__.py) to the
334 // LR1121's own 0x00-0x07 voltage code (design §0.5.3: "codes run 0x00-0x07 for 1.6-3.3V" —
335 // one less than the YAML/SX1262 numbering, so a simple -1 mapping is exact if the two
336 // tables are both linear 0.1V-ish steps in the same order; verify in Step 6).
337 auto const tcxo_code = static_cast<uint8_t>(this->tcxo_voltage_yaml_code_ - 1);
340 this->write_command_(LR1121_CMD_SET_TCXO_MODE, tcxo_params, sizeof(tcxo_params));
341
342 // 3. Clear errors, then calibrate all blocks — order matters: calibration must run on the
343 // TCXO clock, which was just configured (design §3.2 step 3).
344 this->clear_errors_();
345 uint8_t const cal_all = LR1121_CALIBRATE_ALL_BLOCKS;
346 this->write_command_(LR1121_CMD_CALIBRATE, &cal_all, 1);
347 delay(5); // Wait for calibration to complete (same margin as SX1262).
348
349 // 3b. Banded image calibration (analysis §4.3) — Calibrate(0x3F) calibrates the IMG block at
350 // the chip's default band, not ours; both reference drivers issue an explicit banded call.
351 this->calibrate_image_();
352
353 // 4. RF switch: T3-S3 DIO5/DIO6 table (secondhand, see radio_lr1121.h constants).
354 uint8_t rfswitch_params[8] = {
363 };
364 this->write_command_(LR1121_CMD_SET_DIO_AS_RF_SWITCH, rfswitch_params, sizeof(rfswitch_params));
365
366 // 5. GFSK packet type.
367 uint8_t const pkt_type = LR1121_PACKET_TYPE_GFSK;
368 this->write_command_(LR1121_CMD_SET_PACKET_TYPE, &pkt_type, 1);
369
370 // 6. Set frequency to channel 2 (868.95 MHz) — plain Hz, no PLL-step conversion.
372
373 // 7. Apply GFSK modulation parameters (detailed values live in write_modulation_params_()).
375
376 // 8. Default RX packet params: fixed-length GFSK with software CRC (hardware CRC unusable —
377 // same reasoning as SX1262, see radio_sx1262.cpp file header).
378 this->set_rx_packet_params();
379
380 // 9. Sync word: 0x57 0xFD 0x99 + zero padding (identical bytes to the SX1262/SX1276 UART sync
381 // word hypothesis — written via the LR1121's own SetGfskSyncWord opcode rather than a raw
382 // register write).
383 uint8_t sync_word[8] = {0x57, 0xFD, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00};
384 this->write_command_(LR1121_CMD_SET_GFSK_SYNC_WORD, sync_word, sizeof(sync_word));
385
386 // 10. PA config: select LP or HP PA path based on the configured power — cross-checked against
387 // RadioLib's LR1120::setOutputPower()/checkOutputPower(): the LP path only covers -17..14dBm
388 // (regPaSupply=internal regulator); anything above requires the HP path (regPaSupply=VBAT,
389 // -9..22dBm). This board's config requests 17dBm, which is out of the LP path's valid
390 // range — always configuring LP here regardless of tx_power_ was invalid, and is the likely
391 // reason TX completed digitally (TX_DONE fired, no error) while the awning never received
392 // anything: the PA was asked to output power outside the range its selected path supports.
393 bool const use_hp_pa = this->tx_power_ > 14;
394 uint8_t pa_config[4] = {
395 (uint8_t) (use_hp_pa ? 0x01 : 0x00), // paSel: 0=LP, 1=HP
396 (uint8_t) (use_hp_pa ? 0x01 : 0x00), // regPaSupply: 0=internal regulator (LP), 1=VBAT (HP)
397 0x04, // paDutyCycle
398 0x07, // paHpSel — RadioLib always sets this, even for LP
399 };
400 this->write_command_(LR1121_CMD_SET_PA_CONFIG, pa_config, sizeof(pa_config));
401
402 // 11. TX params: power in dBm, clamped to the selected PA path's valid range (see step 10).
403 // Ramp register is LR11x0-specific, not the SX126x encoding this used to assume: RadioLib's
404 // LR1120::setOutputPower() passes `roundRampTime(us) - 3` — the shared ramp-time enum
405 // starts with three LR2021-only fast steps (2/4/8us) that don't exist on the LR11x0, so its
406 // own register value for a given ramp time is 3 less than the shared table's index. 0x0C is
407 // ~200us on this chip (the previous 0x04 was actually ~80us).
408 int8_t const min_power = use_hp_pa ? -9 : -17;
409 int8_t const max_power = use_hp_pa ? 22 : 14;
410 int8_t const power = std::max(min_power, std::min(max_power, (int8_t) this->tx_power_));
411 uint8_t tx_params[2] = {(uint8_t) power, 0x0C};
412 this->write_command_(LR1121_CMD_SET_TX_PARAMS, tx_params, sizeof(tx_params));
413
414 // 12. Keep the crystal path alive after RX/TX completion.
415 uint8_t const fallback_mode = LR1121_FALLBACK_STDBY_XOSC;
416 this->write_command_(LR1121_CMD_SET_RX_TX_FALLBACK_MODE, &fallback_mode, 1);
417
418 // 13. IRQ config: two 32-bit masks — first DIO's mask (routed to DIO9) + second DIO's mask
419 // (unused, all zero). Cross-checked against RadioLib's LRxxxx::setDioIrqParams(irq1, irq2):
420 // the command takes exactly these two fields, no separate "enable" field (design §Step 2
421 // item 2: "mask 0 for the second IRQ output").
422 uint8_t irq_params[8] = {
423 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 24),
424 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 16),
425 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 8),
426 (uint8_t) LR1121_IRQ_DIO_ENABLE_MASK, // DIO9 (irq1) mask
427 0x00,
428 0x00,
429 0x00,
430 0x00, // second IRQ output — unused
431 };
432 this->write_command_(LR1121_CMD_SET_DIO_IRQ_PARAMS, irq_params, sizeof(irq_params));
433
434 // 14. Attach the DIO9 interrupt.
435 this->irq_pin_->attach_interrupt(&RadioLR1121::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
436
437 // 15. Clear any pending IRQs / errors.
438 this->clear_irq_status(0xFFFFFFFF);
439 this->clear_errors_();
440
441 // 16. Enter continuous receive.
442 this->set_mode_rx();
443}
444
445// === Mode control ===
446
448 uint8_t const stdby_xosc = 0x01; // Same small sequential enum as SetRxTxFallbackMode (0x01=STDBY_XOSC there too).
449 this->write_command_(LR1121_CMD_SET_STANDBY, &stdby_xosc, 1);
450}
451
453 // High-ACP workaround (analysis §4.1) — Semtech applies this unconditionally before every
454 // SetRx, so it lives here rather than only at init to cover every set_mode_rx() call site.
456 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF}; // 0xFFFFFF = continuous, same sentinel as SX1262.
457 this->write_command_(LR1121_CMD_SET_RX, rx_continuous, sizeof(rx_continuous));
458}
459
460// === Frequency control ===
461
463 uint8_t params[4] = {
464 (uint8_t) (freq_hz >> 24),
465 (uint8_t) (freq_hz >> 16),
466 (uint8_t) (freq_hz >> 8),
467 (uint8_t) freq_hz,
468 };
469 this->write_command_(LR1121_CMD_SET_RF_FREQUENCY, params, sizeof(params));
470 this->current_freq_ = freq_hz;
471}
472
474 this->rx_bandwidth_ = bandwidth;
476}
477
478} // namespace home_io_control
479} // namespace esphome
480
481// NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
void populate_capture_base_(bool blocking_wait, uint32_t freq_hz, int16_t rssi_dbm, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len)
Populate the common fields of RadioCaptureInfo from raw telemetry.
void reset_hardware_()
Shared hardware reset sequence for chips with an active-low RST pin.
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.
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,...
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.
void apply_high_acp_workaround_()
Apply the Semtech high-ACP TX-quality erratum workaround (analysis §4.1).
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...
void write_command_(uint16_t opcode, const uint8_t *params, uint8_t len)
Write-only command: opcode + params, single NSS cycle.
void set_mode_rx() override
Switch to continuous receive mode.
void write_modulation_params_()
Apply the runtime bandwidth setting to the LR1121 modulation parameters.
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 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.
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).
void calibrate_image_()
Issue a banded image calibration for the 868MHz operating range (analysis §4.3).
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 uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN
Fixed raw-RX probe length: chosen from captures of 23-25 byte protocol frames after UART packing and ...
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 const char * TAG
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 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_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_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_REG_GFSK_WORKAROUND_1_VALUE
static constexpr uint16_t LR1121_CMD_CALIBRATE
cross-checked
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
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 const uint32_t LR1121_BUSY_TIMEOUT_MS
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_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
LR1121 radio driver for IO-Homecontrol.