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// === Device-error decoding ===
30
31namespace {
32
33/// GetErrors bit → name table used by lr1121_format_device_errors().
34constexpr DeviceErrorBit LR1121_DEVICE_ERROR_BITS[] = {
35 {LR1121_ERR_LF_RC_CALIB, "LF_RC_CALIB_ERR"}, {LR1121_ERR_HF_RC_CALIB, "HF_RC_CALIB_ERR"},
36 {LR1121_ERR_ADC_CALIB, "ADC_CALIB_ERR"}, {LR1121_ERR_PLL_CALIB, "PLL_CALIB_ERR"},
37 {LR1121_ERR_IMG_CALIB, "IMG_CALIB_ERR"}, {LR1121_ERR_HF_XOSC_START, "HF_XOSC_START_ERR"},
38 {LR1121_ERR_LF_XOSC_START, "LF_XOSC_START_ERR"}, {LR1121_ERR_PLL_LOCK, "PLL_LOCK_ERR"},
39};
40
41} // namespace
42
43void lr1121_format_device_errors(uint16_t errors, char *buf, size_t buf_size) {
44 format_device_error_bits(errors, LR1121_DEVICE_ERROR_BITS, buf, buf_size);
45}
46
47const char *lr1121_chip_mode_name(uint8_t stat2) {
48 // Stat2 bits 3:1 = chip mode; bit 0 = running from flash; bits 7:4 = reset cause, which every
49 // GetStatus clears (see log_tx_timeout_state()).
50 static constexpr const char *NAMES[] = {"SLEEP", "STDBY_RC", "STDBY_XOSC", "FS", "RX", "TX", "LOC"};
51 uint8_t const mode = (stat2 >> 1) & 0x07;
52 return mode < sizeof(NAMES) / sizeof(NAMES[0]) ? NAMES[mode] : "?";
53}
54
55// === SPI Communication (16-bit opcode, two-transaction) ===
56
57void RadioLR1121::write_command_(uint16_t opcode, const uint8_t *params, uint8_t len) {
58 // Write-only phase: MISO is don't-care while we send the opcode + params, so spi_write()
59 // (MOSI only) is used rather than spi_transfer() — the two are equivalent on real hardware,
60 // but only spi_write() correctly signals "no response byte here" to the SpiAccess contract.
61 this->wait_busy_();
62 this->spi_->spi_enable();
63 this->spi_->spi_write((opcode >> 8) & 0xFF);
64 this->spi_->spi_write(opcode & 0xFF);
65 for (uint8_t i = 0; i < len; i++)
66 this->spi_->spi_write(params[i]);
67 this->spi_->spi_disable();
68}
69
70void RadioLR1121::read_command_(uint16_t opcode, const uint8_t *params, uint8_t params_len, uint8_t *out,
71 uint8_t out_len) {
72 // First transaction: send opcode + request params, NSS up. The chip raises BUSY while it
73 // prepares the response.
74 this->write_command_(opcode, params, params_len);
75 // Second transaction: wait BUSY low again, then clock out Stat1 followed by the response
76 // (MOSI don't-care here, so spi_read() rather than spi_transfer(0x00)).
77 this->wait_busy_();
78 this->spi_->spi_enable();
79 this->last_stat1_ = this->spi_->spi_read(); // Stat1 — command status byte.
80 for (uint8_t i = 0; i < out_len; i++)
81 out[i] = this->spi_->spi_read();
82 this->spi_->spi_disable();
83 this->log_command_status_(opcode);
84}
85
86void RadioLR1121::log_command_status_(uint16_t opcode) const {
87 // Stat1 bits [3:1] encode the previous command's fate (0=CMD_FAIL, 1=CMD_PERR "processing
88 // error", 2=CMD_OK, 3=CMD_DAT); bit 0 is a separate IRQ-active flag. Any FAIL/PERR means the
89 // chip silently rejected the command — surfaced unconditionally (not gated on
90 // IOHOME_FRAME_LOG) since this whole driver has been blind to command rejections until now.
91 uint8_t const cmd_status = (this->last_stat1_ >> 1) & 0x07;
92 if (cmd_status == 0x00 || cmd_status == 0x01) {
93 ESP_LOGW(TAG, "cmd 0x%04X rejected: stat1=0x%02X (%s)", opcode, this->last_stat1_,
94 cmd_status == 0x00 ? "CMD_FAIL" : "CMD_PERR");
95 }
96#ifdef IOHOME_FRAME_LOG
97 ESP_LOGV(TAG, "cmd 0x%04X stat1=0x%02X", opcode, this->last_stat1_);
98#endif
99}
100
101void RadioLR1121::write_reg_mem_mask32_(uint32_t addr, uint32_t mask, uint32_t value) {
102 uint8_t params[12] = {
103 (uint8_t) (addr >> 24), (uint8_t) (addr >> 16), (uint8_t) (addr >> 8), (uint8_t) addr,
104 (uint8_t) (mask >> 24), (uint8_t) (mask >> 16), (uint8_t) (mask >> 8), (uint8_t) mask,
105 (uint8_t) (value >> 24), (uint8_t) (value >> 16), (uint8_t) (value >> 8), (uint8_t) value,
106 };
107 this->write_command_(LR1121_CMD_WRITE_REG_MEM_MASK32, params, sizeof(params));
108}
109
111 this->write_reg_mem_mask32_(LR1121_REG_HIGH_ACP_WORKAROUND_ADDR, LR1121_REG_HIGH_ACP_WORKAROUND_MASK,
112 LR1121_REG_HIGH_ACP_WORKAROUND_VALUE);
113}
114
116 this->write_reg_mem_mask32_(LR1121_REG_GFSK_WORKAROUND_1_ADDR, LR1121_REG_GFSK_WORKAROUND_1_MASK,
117 LR1121_REG_GFSK_WORKAROUND_1_VALUE);
118 this->write_reg_mem_mask32_(LR1121_REG_GFSK_WORKAROUND_2_ADDR, LR1121_REG_GFSK_WORKAROUND_2_MASK,
119 LR1121_REG_GFSK_WORKAROUND_2_VALUE);
120 this->write_reg_mem_mask32_(LR1121_REG_GFSK_WORKAROUND_3_ADDR, LR1121_REG_GFSK_WORKAROUND_3_MASK,
121 LR1121_REG_GFSK_WORKAROUND_3_VALUE);
122}
123
125 uint8_t params[2] = {LR1121_IMAGE_CAL_FREQ1, LR1121_IMAGE_CAL_FREQ2};
126 this->write_command_(LR1121_CMD_CALIBRATE_IMAGE, params, sizeof(params));
127}
128
129void RadioLR1121::write_buffer_(const uint8_t *data, uint8_t len) {
130 this->write_command_(LR1121_CMD_WRITE_BUFFER, data, len);
131}
132
133void RadioLR1121::read_buffer_(uint8_t offset, uint8_t len, uint8_t *data) {
134 uint8_t params[2] = {offset, len};
135 this->read_command_(LR1121_CMD_READ_BUFFER, params, sizeof(params), data, len);
136}
137
139 uint8_t stat2 = 0;
140 return this->read_status_(stat2);
141}
142
143uint32_t RadioLR1121::read_status_(uint8_t &stat2) {
144 // GetStatus reads the IRQ status non-destructively (it clears only Stat2's reset-cause bits, so
145 // they read back cleared here): it returns Stat1, Stat2, then the 32-bit
146 // IRQ status word — cross-checked against RadioLib's
147 // LRxxxx::getIrqStatus(): 6 bytes total [Stat1, Stat2, IRQ_b3..IRQ_b0]. read_command_
148 // already consumes Stat1 into last_stat1_, so only 5 more bytes remain: Stat2 (resp[0])
149 // followed by the 4 IRQ bytes (resp[1..4]).
150 uint8_t resp[5] = {0};
151 this->read_command_(LR1121_CMD_GET_STATUS, nullptr, 0, resp, sizeof(resp));
152 stat2 = resp[0];
153 return (static_cast<uint32_t>(resp[1]) << 24) | (static_cast<uint32_t>(resp[2]) << 16) |
154 (static_cast<uint32_t>(resp[3]) << 8) | static_cast<uint32_t>(resp[4]);
155}
156
157// === Packet params helper ===
158
159void RadioLR1121::set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type,
160 uint8_t crc_type) {
161 // Field order and the byte->bit preamble conversion are shared with SX1262 — see
162 // SoftPhyDriverBase::build_gfsk_packet_params. Only the detector length (16 bits here), the
163 // sync-word selector, and the opcode/transport are LR1121-specific.
164 uint8_t params[GFSK_PACKET_PARAMS_LEN];
165 build_gfsk_packet_params({.preamble_bytes = preamble_len,
166 .preamble_detector = LR1121_PREAMBLE_DETECTOR_16_BIT,
167 .sync_word_param = LR1121_SYNC_WORD_PARAM_24_BITS,
168 .packet_type = packet_type,
169 .payload_len = payload_len,
170 .crc_type = crc_type},
171 params);
172 this->write_command_(LR1121_CMD_SET_PACKET_PARAMS, params, sizeof(params));
173}
174
176 // Same fixed-length raw-probe strategy as SX1262 (SOFT_PHY_RX_PROBE_PACKET_LEN, shared —
177 // see its doc comment in radio_soft_phy_driver_base.h): the LR1121 gets the identical
178 // treatment because the reasoning is about UART-packed frame sizes, not chip-specific
179 // packet-engine behavior.
180 this->set_packet_params_(8, SOFT_PHY_RX_PROBE_PACKET_LEN, LR1121_GFSK_PACKET_FIXED_LENGTH, LR1121_GFSK_CRC_OFF);
181}
182
184 // LR1121 GFSK modulation parameters for the IO-Homecontrol 868 MHz waveform. RF frequency,
185 // bitrate and fdev are written as plain Hz / bit/s 32-bit values — no PLL-step math needed,
186 // unlike SX1262. Field order (bitrate, pulse shape, bandwidth, fdev) cross-checked against
187 // RadioLib/Semtech and confirmed on 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 — 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
215void RadioLR1121::clear_errors_() { this->write_command_(LR1121_CMD_CLEAR_ERRORS, nullptr, 0); }
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_, this->raw_rssi_to_dbm_(pkt_status[0]), raw, raw_len,
240 frame, 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. This is the one place that conversion happens.
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. TX is actually bounded by the
267 // shared send_packet()'s air-time-based guard (tx_done_timeout_ms()), independent of this chip-level value.
268 uint8_t tx_timeout[3] = {0x03, 0xE8, 0x00};
269 this->write_command_(LR1121_CMD_SET_TX, tx_timeout, sizeof(tx_timeout));
270}
271
272// === ISR ===
273
275
276// === Initialization ===
277
279 // --- Pin setup ---
280 this->rst_pin_->setup();
281 this->irq_pin_->setup();
282 this->busy_pin_->setup();
283
284 // --- Hardware reset ---
285 this->reset_hardware_();
286 this->wait_busy_();
287 if (this->failed_)
288 return false;
289
290 this->configure_radio_();
291 if (this->failed_)
292 return false;
293
294 ESP_LOGI(TAG, "LR1121 initialized");
295 return true;
296}
297
298void RadioLR1121::log_tx_timeout_state(uint32_t irq_status) {
299 uint8_t stat2 = 0;
300 (void) this->read_status_(stat2);
301 uint16_t const errors = this->get_errors_();
302 char errbuf[DEVICE_ERROR_STR_SIZE];
303 lr1121_format_device_errors(errors, errbuf, sizeof(errbuf));
304 // configure_radio_() sets the TX fallback to STDBY_XOSC, so: STDBY_XOSC with TX-done flagged = the
305 // frame ended and only the interrupt went missing (DIO1 path); STDBY_XOSC without it = SetTx never
306 // took effect; STDBY_RC = the chip reset or browned out and lost that setting with the rest of its
307 // configuration; TX = still sending long past the air time. A set error flag names the block that
308 // failed. The reset-cause bits of Stat2 are not decoded: GetStatus clears them, and the TX wait
309 // polls GetStatus right up to this call, so they would read "cleared" even after a reset.
310 ESP_LOGE(TAG, "TX timeout chip state: stat2 0x%02X (mode=%s), tx_done=%d, device errors 0x%04X (%s), DIO1=%d BUSY=%d",
311 stat2, lr1121_chip_mode_name(stat2), (irq_status & this->tx_done_bit()) != 0, errors, errbuf,
312 this->irq_pin_->digital_read(), this->busy_pin_->digital_read());
313}
314
316 uint8_t version[4] = {0};
317 this->read_command_(LR1121_CMD_GET_VERSION, nullptr, 0, version, sizeof(version));
318 uint32_t const irq = this->read_irq_status_raw();
319 uint16_t const errors = this->get_errors_();
320
321 ESP_LOGCONFIG(TAG, " LR1121 Diagnostic:");
322 ESP_LOGCONFIG(TAG, " TCXO voltage: %s", tcxo_voltage_label(this->tcxo_voltage_code_));
323 ESP_LOGCONFIG(TAG, " Device type: 0x%02X (expect 0x%02X)", version[1], LR1121_DEVICE_TYPE);
324 ESP_LOGCONFIG(TAG, " HW version: 0x%02X, FW version: %u.%u", version[0], version[2], version[3]);
325 if (lr1121_firmware_is_outdated(version[2], version[3])) {
326 ESP_LOGCONFIG(TAG,
327 " Newer firmware available (%u.%u), see "
328 "https://github.com/Lora-net/radio_firmware_images/blob/master/lr1121/transceiver/README.md",
329 LR1121_KNOWN_LATEST_FW_MAJOR, LR1121_KNOWN_LATEST_FW_MINOR);
330 }
331 ESP_LOGCONFIG(TAG, " BUSY=%d IRQ=%d", this->busy_pin_->digital_read(), this->irq_pin_->digital_read());
332 ESP_LOGCONFIG(TAG, " BUSY wait overruns (task preempted, chip ready): %" PRIu32, this->get_busy_overruns_());
333 ESP_LOGCONFIG(TAG, " IRQ status: 0x%08" PRIX32, irq);
334 char errbuf[DEVICE_ERROR_STR_SIZE];
335 lr1121_format_device_errors(errors, errbuf, sizeof(errbuf));
336 ESP_LOGCONFIG(TAG, " Device errors: 0x%04X (%s)", errors, errbuf);
337 ESP_LOGCONFIG(TAG, " Last Stat1: 0x%02X", this->last_stat1_);
339}
340
342 // 1. Identity check: GetVersion — device type must be LR1121 (0x03). Response layout
343 // cross-checked against RadioLib's LR11x0::getVersion(): [hw_version, device_type,
344 // fw_major, fw_minor] (4 bytes) — device type is byte 1, not byte 0.
345 uint8_t version[4] = {0};
346 this->read_command_(LR1121_CMD_GET_VERSION, nullptr, 0, version, sizeof(version));
347 if (version[1] != LR1121_DEVICE_TYPE) {
348 ESP_LOGE(TAG, "Unexpected device type 0x%02X (expected 0x%02X) — not an LR1121?", version[1], LR1121_DEVICE_TYPE);
349 this->fail_("chip does not identify as an LR1121 -- check radio_type, SPI wiring and the board");
350 return;
351 }
352 ESP_LOGI(TAG, "LR1121 detected: hw=0x%02X fw=%u.%u", version[0], version[2], version[3]);
353
354 // 2. TCXO: the YAML TCXO_VOLTAGE_OPTIONS code (1_6V=0x00 .. 3_3V=0x07, hub_validators.py) is already
355 // the LR1121's own SetTcxoMode voltage code — the same 0-based enum the SX1262's
356 // SetDIO3AsTCXOCtrl takes — so it goes straight through with no mapping. TCXO_VOLTAGE_NONE
357 // marks a bare-crystal board: skip SetTcxoMode entirely and let calibration run on the
358 // plain crystal.
359 if (this->tcxo_voltage_code_ != TCXO_VOLTAGE_NONE) {
360 uint8_t tcxo_params[4] = {this->tcxo_voltage_code_, LR1121_TCXO_STARTUP_DELAY_TICKS_MSB,
361 LR1121_TCXO_STARTUP_DELAY_TICKS_MID, LR1121_TCXO_STARTUP_DELAY_TICKS_LSB};
362 this->write_command_(LR1121_CMD_SET_TCXO_MODE, tcxo_params, sizeof(tcxo_params));
363 }
364
365 // 3. Clear errors, then calibrate all blocks — order matters: calibration must run on the
366 // reference clock just configured above (the TCXO, or the bare crystal when SetTcxoMode was
367 // skipped), and the clear wipes the expected POR error latch first.
368 this->clear_errors_();
369 uint8_t const cal_all = LR1121_CALIBRATE_ALL_BLOCKS;
370 this->write_command_(LR1121_CMD_CALIBRATE, &cal_all, 1);
371 delay(5); // Wait for calibration to complete (same margin as SX1262).
372
373 // 3b. Banded image calibration — Calibrate(0x3F) calibrates the IMG block at
374 // the chip's default band, not ours; both reference drivers issue an explicit banded call.
375 this->calibrate_image_();
376
377 // 4. RF switch: T3-S3 DIO5/DIO6 table (secondhand, see radio_lr1121.h constants).
378 uint8_t rfswitch_params[8] = {
379 LR1121_RFSWITCH_ENABLE_DIO5_DIO6,
380 LR1121_RFSWITCH_STANDBY,
381 LR1121_RFSWITCH_RX,
382 LR1121_RFSWITCH_TX,
383 LR1121_RFSWITCH_TX_HP,
384 LR1121_RFSWITCH_TX_HF,
385 LR1121_RFSWITCH_GNSS,
386 LR1121_RFSWITCH_WIFI,
387 };
388 this->write_command_(LR1121_CMD_SET_DIO_AS_RF_SWITCH, rfswitch_params, sizeof(rfswitch_params));
389
390 // 5. GFSK packet type.
391 uint8_t const pkt_type = LR1121_PACKET_TYPE_GFSK;
392 this->write_command_(LR1121_CMD_SET_PACKET_TYPE, &pkt_type, 1);
393
394 // 6. Set frequency to channel 2 (868.95 MHz) — plain Hz, no PLL-step conversion.
395 this->set_frequency_register(FREQ_CH2);
396
397 // 7. Apply GFSK modulation parameters (detailed values live in write_modulation_params_()).
399
400 // 8. Default RX packet params: fixed-length GFSK with software CRC (hardware CRC unusable —
401 // same reasoning as SX1262, see radio_sx1262.cpp file header).
402 this->set_rx_packet_params();
403
404 // 9. Sync word: 0x57 0xFD 0x99 + zero padding — same derivation as SX1262 (see the comment on
405 // RadioSX1262::configure_radio_()'s equivalent step): SX1276's raw sync bytes {0x55, 0xFF,
406 // 0x33} run through the UART encoder and read 6 bits into the first cell, not an independent
407 // hypothesis. Written via the LR1121's own SetGfskSyncWord opcode rather than a raw register
408 // write.
409 uint8_t sync_word[8] = {0x57, 0xFD, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00};
410 this->write_command_(LR1121_CMD_SET_GFSK_SYNC_WORD, sync_word, sizeof(sync_word));
411
412 // 10. PA config: always the HP PA (paSel=1, regPaSupply=VBAT), whatever tx_power_ is. The
413 // LR1121 has two sub-GHz PAs (cross-checked against RadioLib's LR1120::setOutputPower()/
414 // checkOutputPower()): LP covers -17..14dBm, HP -9..22dBm. On the LilyGo T3-S3 the LP path
415 // puts nothing on the air: at tx_power 10 the chip reports TX done, yet neither an SX1276
416 // next to it nor an SDR sees a single frame, while 17dBm on the same build is clean. The
417 // board most likely wires only the HP output to the antenna. HP's range covers the whole
418 // YAML range (0..22), so low settings keep working through it.
419 uint8_t pa_config[4] = {
420 0x01, // paSel: 1 = HP
421 0x01, // regPaSupply: 1 = VBAT (required by HP)
422 0x04, // paDutyCycle
423 0x07, // paHpSel
424 };
425 this->write_command_(LR1121_CMD_SET_PA_CONFIG, pa_config, sizeof(pa_config));
426
427 // 11. TX params: power in dBm, clamped to the HP PA's valid range (see step 10).
428 // The ramp register is LR11x0-specific, not the SX126x encoding. RadioLib's
429 // LR1120::setOutputPower() passes `roundRampTime(us) - 3` — the shared ramp-time enum
430 // starts with three LR2021-only fast steps (2/4/8us) that don't exist on the LR11x0, so its
431 // own register value for a given ramp time is 3 less than the shared table's index. 0x0C is
432 // ~200us on this chip (0x04 would be ~80us).
433 int8_t const min_power = -9; // HP PA's floor
434 int8_t const max_power = 22; // HP PA's ceiling
435 int8_t const power = std::max(min_power, std::min(max_power, (int8_t) this->tx_power_));
436 uint8_t tx_params[2] = {(uint8_t) power, 0x0C};
437 this->write_command_(LR1121_CMD_SET_TX_PARAMS, tx_params, sizeof(tx_params));
438
439 // 12. Keep the crystal path alive after RX/TX completion.
440 uint8_t const fallback_mode = LR1121_FALLBACK_STDBY_XOSC;
441 this->write_command_(LR1121_CMD_SET_RX_TX_FALLBACK_MODE, &fallback_mode, 1);
442
443 // 13. IRQ config: two 32-bit masks — first DIO's mask (routed to DIO9) + second DIO's mask
444 // (unused, mask 0). Cross-checked against RadioLib's LRxxxx::setDioIrqParams(irq1, irq2):
445 // the command takes exactly these two fields, no separate "enable" field.
446 uint8_t irq_params[8] = {
447 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 24),
448 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 16),
449 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 8),
450 (uint8_t) LR1121_IRQ_DIO_ENABLE_MASK, // DIO9 (irq1) mask
451 0x00,
452 0x00,
453 0x00,
454 0x00, // second IRQ output — unused
455 };
456 this->write_command_(LR1121_CMD_SET_DIO_IRQ_PARAMS, irq_params, sizeof(irq_params));
457
458 // 14. Attach the DIO9 interrupt.
459 this->irq_pin_->attach_interrupt(&RadioLR1121::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
460
461 // 15. Clear any pending IRQs / errors — recording the error word first, since that is the only
462 // trace of a calibration or TCXO fault from bring-up. A healthy chip leaves it zero (confirmed
463 // on the T3-S3), so a non-zero word is a genuine fault and is warned about.
464 this->clear_irq_status(0xFFFFFFFF);
466 this->clear_errors_();
467
468 // 16. Enter continuous receive.
469 this->set_mode_rx();
470}
471
472// === Mode control ===
473
475 uint8_t const stdby_xosc = 0x01; // Same small sequential enum as SetRxTxFallbackMode (0x01=STDBY_XOSC there too).
476 this->write_command_(LR1121_CMD_SET_STANDBY, &stdby_xosc, 1);
477}
478
480 // High-ACP workaround — Semtech applies this unconditionally before every
481 // SetRx, so it lives here rather than only at init to cover every set_mode_rx() call site.
483 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF}; // 0xFFFFFF = continuous, same sentinel as SX1262.
484 this->write_command_(LR1121_CMD_SET_RX, rx_continuous, sizeof(rx_continuous));
485}
486
487// === Frequency control ===
488
490 uint8_t params[4] = {
491 (uint8_t) (freq_hz >> 24),
492 (uint8_t) (freq_hz >> 16),
493 (uint8_t) (freq_hz >> 8),
494 (uint8_t) freq_hz,
495 };
496 this->write_command_(LR1121_CMD_SET_RF_FREQUENCY, params, sizeof(params));
497 this->current_freq_ = freq_hz;
498}
499
501 this->rx_bandwidth_ = bandwidth;
503}
504
505} // namespace home_io_control
506} // namespace esphome
507
508// 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 fail_(const char *reason)
Latch the failed state and record why, so is_failed and failure_reason can never disagree.
bool failed_
Latched by fail_; see is_failed.
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,...
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,...
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.
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...
void log_tx_timeout_state(uint32_t irq_status) override
Log Stat2 with the chip mode, whether the IRQ word holds TX-done, the device errors and the DIO1/BUSY...
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.
uint32_t read_status_(uint8_t &stat2)
Read GetStatus.
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 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.
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.
void wait_busy_()
Wait until busy_pin_ reads low, feeding the watchdog while polling.
void dump_init_device_errors_(const char *tag, DeviceErrorFormatter format) const
Log the init-time device errors under tag, or nothing when there were none.
int16_t raw_rssi_to_dbm_(uint8_t raw) const
Convert a chip's raw RSSI byte to an antenna-referred dBm reading.
static void build_gfsk_packet_params(const SoftPhyPacketParams &p, uint8_t out[GFSK_PACKET_PARAMS_LEN])
Fill the nine-byte GFSK SetPacketParams payload shared by both chips.
InternalGPIOPin * busy_pin_
BUSY line, read directly by both concrete drivers' own dump_debug() in addition to wait_busy_,...
static constexpr uint8_t GFSK_PACKET_PARAMS_LEN
Byte count of the GFSK SetPacketParams payload — identical on both software-PHY chips.
void record_init_device_errors_(const char *tag, const char *chip_label, uint16_t errors, DeviceErrorFormatter format)
Keep the device-error word read at the end of configure_radio_() and warn if it is non-zero.
uint32_t get_busy_overruns_() const
Number of wait_busy_ calls that ran past their timeout and still found the chip ready.
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 const char * TAG
LR1121RxBandwidth
Valid LR1121 RX bandwidth options (register values).
const char * tcxo_voltage_label(uint8_t code)
Human-readable voltage for a tcxo_voltage code, for the config dump.
void format_device_error_bits(uint16_t errors, const DeviceErrorBit *bits, size_t bit_count, char *buf, size_t buf_size)
Expand a device-error word into a human-readable NAME|NAME|... string.
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.
const char * lr1121_chip_mode_name(uint8_t stat2)
Name of the chip mode in a Stat2 byte (bits 3:1, LR1121 user manual "Status Registers").
constexpr uint8_t TCXO_VOLTAGE_NONE
Sentinel tcxo_voltage code (YAML none) for a board with a bare crystal and no TCXO.
static const char *const TAG
LR1121 radio driver for IO-Homecontrol.
One named bit of a chip's device-error word.
uint16_t irq_status
Raw IRQ status register value.
uint8_t packet_status
Packet status byte (chip-specific).
bool crc_error
True if a CRC error was detected.
uint8_t reported_len
Length reported by the radio chip.
bool rx_done
True if RxDone IRQ fired.
uint8_t rx_offset
RX buffer offset where the frame starts (0 for chips without offset reporting).