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
47// === SPI Communication (16-bit opcode, two-transaction) ===
48
49void RadioLR1121::write_command_(uint16_t opcode, const uint8_t *params, uint8_t len) {
50 // Write-only phase: MISO is don't-care while we send the opcode + params, so spi_write()
51 // (MOSI only) is used rather than spi_transfer() — the two are equivalent on real hardware,
52 // but only spi_write() correctly signals "no response byte here" to the SpiAccess contract.
53 this->wait_busy_();
54 this->spi_->spi_enable();
55 this->spi_->spi_write((opcode >> 8) & 0xFF);
56 this->spi_->spi_write(opcode & 0xFF);
57 for (uint8_t i = 0; i < len; i++)
58 this->spi_->spi_write(params[i]);
59 this->spi_->spi_disable();
60}
61
62void RadioLR1121::read_command_(uint16_t opcode, const uint8_t *params, uint8_t params_len, uint8_t *out,
63 uint8_t out_len) {
64 // First transaction: send opcode + request params, NSS up. The chip raises BUSY while it
65 // prepares the response.
66 this->write_command_(opcode, params, params_len);
67 // Second transaction: wait BUSY low again, then clock out Stat1 followed by the response
68 // (MOSI don't-care here, so spi_read() rather than spi_transfer(0x00)).
69 this->wait_busy_();
70 this->spi_->spi_enable();
71 this->last_stat1_ = this->spi_->spi_read(); // Stat1 — command status byte.
72 for (uint8_t i = 0; i < out_len; i++)
73 out[i] = this->spi_->spi_read();
74 this->spi_->spi_disable();
75 this->log_command_status_(opcode);
76}
77
78void RadioLR1121::log_command_status_(uint16_t opcode) const {
79 // Stat1 bits [3:1] encode the previous command's fate (0=CMD_FAIL, 1=CMD_PERR "processing
80 // error", 2=CMD_OK, 3=CMD_DAT); bit 0 is a separate IRQ-active flag. Any FAIL/PERR means the
81 // chip silently rejected the command — surfaced unconditionally (not gated on
82 // IOHOME_FRAME_LOG) since this whole driver has been blind to command rejections until now.
83 uint8_t const cmd_status = (this->last_stat1_ >> 1) & 0x07;
84 if (cmd_status == 0x00 || cmd_status == 0x01) {
85 ESP_LOGW(TAG, "cmd 0x%04X rejected: stat1=0x%02X (%s)", opcode, this->last_stat1_,
86 cmd_status == 0x00 ? "CMD_FAIL" : "CMD_PERR");
87 }
88#ifdef IOHOME_FRAME_LOG
89 ESP_LOGV(TAG, "cmd 0x%04X stat1=0x%02X", opcode, this->last_stat1_);
90#endif
91}
92
93void RadioLR1121::write_reg_mem_mask32_(uint32_t addr, uint32_t mask, uint32_t value) {
94 uint8_t params[12] = {
95 (uint8_t) (addr >> 24), (uint8_t) (addr >> 16), (uint8_t) (addr >> 8), (uint8_t) addr,
96 (uint8_t) (mask >> 24), (uint8_t) (mask >> 16), (uint8_t) (mask >> 8), (uint8_t) mask,
97 (uint8_t) (value >> 24), (uint8_t) (value >> 16), (uint8_t) (value >> 8), (uint8_t) value,
98 };
99 this->write_command_(LR1121_CMD_WRITE_REG_MEM_MASK32, params, sizeof(params));
100}
101
103 this->write_reg_mem_mask32_(LR1121_REG_HIGH_ACP_WORKAROUND_ADDR, LR1121_REG_HIGH_ACP_WORKAROUND_MASK,
104 LR1121_REG_HIGH_ACP_WORKAROUND_VALUE);
105}
106
108 this->write_reg_mem_mask32_(LR1121_REG_GFSK_WORKAROUND_1_ADDR, LR1121_REG_GFSK_WORKAROUND_1_MASK,
109 LR1121_REG_GFSK_WORKAROUND_1_VALUE);
110 this->write_reg_mem_mask32_(LR1121_REG_GFSK_WORKAROUND_2_ADDR, LR1121_REG_GFSK_WORKAROUND_2_MASK,
111 LR1121_REG_GFSK_WORKAROUND_2_VALUE);
112 this->write_reg_mem_mask32_(LR1121_REG_GFSK_WORKAROUND_3_ADDR, LR1121_REG_GFSK_WORKAROUND_3_MASK,
113 LR1121_REG_GFSK_WORKAROUND_3_VALUE);
114}
115
117 uint8_t params[2] = {LR1121_IMAGE_CAL_FREQ1, LR1121_IMAGE_CAL_FREQ2};
118 this->write_command_(LR1121_CMD_CALIBRATE_IMAGE, params, sizeof(params));
119}
120
121void RadioLR1121::write_buffer_(const uint8_t *data, uint8_t len) {
122 this->write_command_(LR1121_CMD_WRITE_BUFFER, data, len);
123}
124
125void RadioLR1121::read_buffer_(uint8_t offset, uint8_t len, uint8_t *data) {
126 uint8_t params[2] = {offset, len};
127 this->read_command_(LR1121_CMD_READ_BUFFER, params, sizeof(params), data, len);
128}
129
131 // GetStatus reads the IRQ status non-destructively: it returns Stat1, Stat2, then the 32-bit
132 // IRQ status word — cross-checked against RadioLib's
133 // LRxxxx::getIrqStatus(): 6 bytes total [Stat1, Stat2, IRQ_b3..IRQ_b0]. read_command_
134 // already consumes Stat1 into last_stat1_, so only 5 more bytes remain: Stat2 (resp[0],
135 // unused) followed by the 4 IRQ bytes (resp[1..4]).
136 uint8_t resp[5] = {0};
137 this->read_command_(LR1121_CMD_GET_STATUS, nullptr, 0, resp, sizeof(resp));
138 return (static_cast<uint32_t>(resp[1]) << 24) | (static_cast<uint32_t>(resp[2]) << 16) |
139 (static_cast<uint32_t>(resp[3]) << 8) | static_cast<uint32_t>(resp[4]);
140}
141
142// === Packet params helper ===
143
144void RadioLR1121::set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type,
145 uint8_t crc_type) {
146 // Field order and the byte->bit preamble conversion are shared with SX1262 — see
147 // SoftPhyDriverBase::build_gfsk_packet_params. Only the detector length (16 bits here), the
148 // sync-word selector, and the opcode/transport are LR1121-specific.
149 uint8_t params[GFSK_PACKET_PARAMS_LEN];
150 build_gfsk_packet_params({.preamble_bytes = preamble_len,
151 .preamble_detector = LR1121_PREAMBLE_DETECTOR_16_BIT,
152 .sync_word_param = LR1121_SYNC_WORD_PARAM_24_BITS,
153 .packet_type = packet_type,
154 .payload_len = payload_len,
155 .crc_type = crc_type},
156 params);
157 this->write_command_(LR1121_CMD_SET_PACKET_PARAMS, params, sizeof(params));
158}
159
161 // Same fixed-length raw-probe strategy as SX1262 (SOFT_PHY_RX_PROBE_PACKET_LEN, shared —
162 // see its doc comment in radio_soft_phy_driver_base.h): the LR1121 gets the identical
163 // treatment because the reasoning is about UART-packed frame sizes, not chip-specific
164 // packet-engine behavior.
165 this->set_packet_params_(8, SOFT_PHY_RX_PROBE_PACKET_LEN, LR1121_GFSK_PACKET_FIXED_LENGTH, LR1121_GFSK_CRC_OFF);
166}
167
169 // LR1121 GFSK modulation parameters for the IO-Homecontrol 868 MHz waveform. RF frequency,
170 // bitrate and fdev are written as plain Hz / bit/s 32-bit values — no PLL-step math needed,
171 // unlike SX1262. Field order (bitrate, pulse shape, bandwidth, fdev) cross-checked against
172 // RadioLib/Semtech and confirmed on real hardware.
173 uint32_t const bitrate_bps = 38400;
174 uint32_t const fdev_hz = 19200;
175 uint8_t mod_params[10] = {
176 (uint8_t) (bitrate_bps >> 24),
177 (uint8_t) (bitrate_bps >> 16),
178 (uint8_t) (bitrate_bps >> 8),
179 (uint8_t) bitrate_bps, // Bitrate: 38400 bps
180 0x0B, // Pulse shape: Gaussian BT=1.0 (same encoding as SX126x)
181 static_cast<uint8_t>(this->rx_bandwidth_), // Bandwidth: runtime-tunable (same encoding as SX126x)
182 (uint8_t) (fdev_hz >> 24),
183 (uint8_t) (fdev_hz >> 16),
184 (uint8_t) (fdev_hz >> 8),
185 (uint8_t) fdev_hz, // Fdev: 19200 Hz
186 };
187 this->write_command_(LR1121_CMD_SET_MODULATION_PARAMS, mod_params, sizeof(mod_params));
188
189 // GFSK workaround register trio — RadioLib/Semtech apply this after every
190 // modulation-params write, not just once at init, so it must re-run on every bandwidth retune.
192}
193
195 uint8_t errors_raw[2] = {0};
196 this->read_command_(LR1121_CMD_GET_ERRORS, nullptr, 0, errors_raw, sizeof(errors_raw));
197 return (static_cast<uint16_t>(errors_raw[0]) << 8) | errors_raw[1];
198}
199
200void RadioLR1121::clear_errors_() { this->write_command_(LR1121_CMD_CLEAR_ERRORS, nullptr, 0); }
201
202void RadioLR1121::clear_irq_status(uint32_t irq_mask) {
203 uint8_t clear_irq[4] = {
204 (uint8_t) ((irq_mask >> 24) & 0xFF),
205 (uint8_t) ((irq_mask >> 16) & 0xFF),
206 (uint8_t) ((irq_mask >> 8) & 0xFF),
207 (uint8_t) (irq_mask & 0xFF),
208 };
209 this->write_command_(LR1121_CMD_CLEAR_IRQ, clear_irq, sizeof(clear_irq));
210}
211
212void RadioLR1121::fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
213 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
214 // GetPktStatus (0x0204), not GetRssiInst (0x0205): the latter is a live, instantaneous RSSI
215 // read unrelated to any specific frame, whereas GetPktStatus is atomically tied to the
216 // last-received packet — the reported RSSI must reflect the frame that was actually received,
217 // not the channel's state whenever this function happens to run. Mirrors
218 // RadioSX1262::fill_capture_info()'s use of its own GetPacketStatus. byte[0] is rssi_sync
219 // (RSSI at sync-word detection), matching the sync-not-avg field SX1262 uses
220 // (packet_status[1] there).
221 uint8_t pkt_status[4] = {0};
222 this->read_command_(LR1121_CMD_GET_PKT_STATUS, nullptr, 0, pkt_status, sizeof(pkt_status));
223
224 this->populate_capture_base_(blocking_wait, this->current_freq_, this->raw_rssi_to_dbm_(pkt_status[0]), raw, raw_len,
225 frame, frame_len);
226 this->last_capture_.rx_done = (irq_status & LR1121_IRQ_RX_DONE) != 0;
227 this->last_capture_.crc_error = (irq_status & LR1121_IRQ_CRC_ERR) != 0;
228 // RadioCaptureInfo::irq_status is uint16_t; map the 32-bit word down by taking bits [2..10]
229 // and shifting right by 2. This is the one place that conversion happens.
230 this->last_capture_.irq_status = static_cast<uint16_t>((irq_status >> 2) & 0x01FF);
231 this->last_capture_.packet_status = pkt_status[3];
232 this->last_capture_.rx_offset = rx_offset;
233 this->last_capture_.reported_len = reported_len;
234}
235
237 uint8_t raw = 0;
238 this->read_command_(LR1121_CMD_GET_RSSI_INST, nullptr, 0, &raw, 1);
239 return raw;
240}
241
242void RadioLR1121::get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) {
243 // GetRxBufferStatus response layout mirrors SX1262: [length, offset].
244 uint8_t rx_status[2] = {0};
245 this->read_command_(LR1121_CMD_GET_RX_BUFFER_STATUS, nullptr, 0, rx_status, sizeof(rx_status));
246 reported_len = rx_status[0];
247 rx_offset = rx_status[1];
248}
249
251 // Tick base is 30.52us (32.768kHz RTC), so 0x03E800 is ~7.8s. TX is actually bounded by the
252 // software 4000ms guard in the shared send_packet(), independent of this chip-level value.
253 uint8_t tx_timeout[3] = {0x03, 0xE8, 0x00};
254 this->write_command_(LR1121_CMD_SET_TX, tx_timeout, sizeof(tx_timeout));
255}
256
257// === ISR ===
258
260
261// === Initialization ===
262
264 // --- Pin setup ---
265 this->rst_pin_->setup();
266 this->irq_pin_->setup();
267 this->busy_pin_->setup();
268
269 // --- Hardware reset ---
270 this->reset_hardware_();
271 this->wait_busy_();
272 if (this->failed_)
273 return false;
274
275 this->configure_radio_();
276 if (this->failed_)
277 return false;
278
279 ESP_LOGI(TAG, "LR1121 initialized");
280 return true;
281}
282
284 uint8_t version[4] = {0};
285 this->read_command_(LR1121_CMD_GET_VERSION, nullptr, 0, version, sizeof(version));
286 uint32_t const irq = this->read_irq_status_raw();
287 uint16_t const errors = this->get_errors_();
288
289 ESP_LOGCONFIG(TAG, " LR1121 Diagnostic:");
290 ESP_LOGCONFIG(TAG, " TCXO voltage: %s", tcxo_voltage_label(this->tcxo_voltage_code_));
291 ESP_LOGCONFIG(TAG, " Device type: 0x%02X (expect 0x%02X)", version[1], LR1121_DEVICE_TYPE);
292 ESP_LOGCONFIG(TAG, " HW version: 0x%02X, FW version: %u.%u", version[0], version[2], version[3]);
293 if (lr1121_firmware_is_outdated(version[2], version[3])) {
294 ESP_LOGCONFIG(TAG,
295 " Newer firmware available (%u.%u), see "
296 "https://github.com/Lora-net/radio_firmware_images/blob/master/lr1121/transceiver/README.md",
297 LR1121_KNOWN_LATEST_FW_MAJOR, LR1121_KNOWN_LATEST_FW_MINOR);
298 }
299 ESP_LOGCONFIG(TAG, " BUSY=%d IRQ=%d", this->busy_pin_->digital_read(), this->irq_pin_->digital_read());
300 ESP_LOGCONFIG(TAG, " IRQ status: 0x%08" PRIX32, irq);
301 char errbuf[DEVICE_ERROR_STR_SIZE];
302 lr1121_format_device_errors(errors, errbuf, sizeof(errbuf));
303 ESP_LOGCONFIG(TAG, " Device errors: 0x%04X (%s)", errors, errbuf);
304 ESP_LOGCONFIG(TAG, " Last Stat1: 0x%02X", this->last_stat1_);
306}
307
309 // 1. Identity check: GetVersion — device type must be LR1121 (0x03). Response layout
310 // cross-checked against RadioLib's LR11x0::getVersion(): [hw_version, device_type,
311 // fw_major, fw_minor] (4 bytes) — device type is byte 1, not byte 0.
312 uint8_t version[4] = {0};
313 this->read_command_(LR1121_CMD_GET_VERSION, nullptr, 0, version, sizeof(version));
314 if (version[1] != LR1121_DEVICE_TYPE) {
315 ESP_LOGE(TAG, "Unexpected device type 0x%02X (expected 0x%02X) — not an LR1121?", version[1], LR1121_DEVICE_TYPE);
316 this->fail_("chip does not identify as an LR1121 -- check radio_type, SPI wiring and the board");
317 return;
318 }
319 ESP_LOGI(TAG, "LR1121 detected: hw=0x%02X fw=%u.%u", version[0], version[2], version[3]);
320
321 // 2. TCXO: the YAML TCXO_VOLTAGE_OPTIONS code (1_6V=0x00 .. 3_3V=0x07, hub_validators.py) is already
322 // the LR1121's own SetTcxoMode voltage code — the same 0-based enum the SX1262's
323 // SetDIO3AsTCXOCtrl takes — so it goes straight through with no mapping. TCXO_VOLTAGE_NONE
324 // marks a bare-crystal board: skip SetTcxoMode entirely and let calibration run on the
325 // plain crystal.
326 if (this->tcxo_voltage_code_ != TCXO_VOLTAGE_NONE) {
327 uint8_t tcxo_params[4] = {this->tcxo_voltage_code_, LR1121_TCXO_STARTUP_DELAY_TICKS_MSB,
328 LR1121_TCXO_STARTUP_DELAY_TICKS_MID, LR1121_TCXO_STARTUP_DELAY_TICKS_LSB};
329 this->write_command_(LR1121_CMD_SET_TCXO_MODE, tcxo_params, sizeof(tcxo_params));
330 }
331
332 // 3. Clear errors, then calibrate all blocks — order matters: calibration must run on the
333 // reference clock just configured above (the TCXO, or the bare crystal when SetTcxoMode was
334 // skipped), and the clear wipes the expected POR error latch first.
335 this->clear_errors_();
336 uint8_t const cal_all = LR1121_CALIBRATE_ALL_BLOCKS;
337 this->write_command_(LR1121_CMD_CALIBRATE, &cal_all, 1);
338 delay(5); // Wait for calibration to complete (same margin as SX1262).
339
340 // 3b. Banded image calibration — Calibrate(0x3F) calibrates the IMG block at
341 // the chip's default band, not ours; both reference drivers issue an explicit banded call.
342 this->calibrate_image_();
343
344 // 4. RF switch: T3-S3 DIO5/DIO6 table (secondhand, see radio_lr1121.h constants).
345 uint8_t rfswitch_params[8] = {
346 LR1121_RFSWITCH_ENABLE_DIO5_DIO6,
347 LR1121_RFSWITCH_STANDBY,
348 LR1121_RFSWITCH_RX,
349 LR1121_RFSWITCH_TX,
350 LR1121_RFSWITCH_TX_HP,
351 LR1121_RFSWITCH_TX_HF,
352 LR1121_RFSWITCH_GNSS,
353 LR1121_RFSWITCH_WIFI,
354 };
355 this->write_command_(LR1121_CMD_SET_DIO_AS_RF_SWITCH, rfswitch_params, sizeof(rfswitch_params));
356
357 // 5. GFSK packet type.
358 uint8_t const pkt_type = LR1121_PACKET_TYPE_GFSK;
359 this->write_command_(LR1121_CMD_SET_PACKET_TYPE, &pkt_type, 1);
360
361 // 6. Set frequency to channel 2 (868.95 MHz) — plain Hz, no PLL-step conversion.
362 this->set_frequency_register(FREQ_CH2);
363
364 // 7. Apply GFSK modulation parameters (detailed values live in write_modulation_params_()).
366
367 // 8. Default RX packet params: fixed-length GFSK with software CRC (hardware CRC unusable —
368 // same reasoning as SX1262, see radio_sx1262.cpp file header).
369 this->set_rx_packet_params();
370
371 // 9. Sync word: 0x57 0xFD 0x99 + zero padding — same derivation as SX1262 (see the comment on
372 // RadioSX1262::configure_radio_()'s equivalent step): SX1276's raw sync bytes {0x55, 0xFF,
373 // 0x33} run through the UART encoder and read 6 bits into the first cell, not an independent
374 // hypothesis. Written via the LR1121's own SetGfskSyncWord opcode rather than a raw register
375 // write.
376 uint8_t sync_word[8] = {0x57, 0xFD, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00};
377 this->write_command_(LR1121_CMD_SET_GFSK_SYNC_WORD, sync_word, sizeof(sync_word));
378
379 // 10. PA config: select LP or HP PA path based on the configured power — cross-checked against
380 // RadioLib's LR1120::setOutputPower()/checkOutputPower(): the LP path only covers -17..14dBm
381 // (regPaSupply=internal regulator); anything above requires the HP path (regPaSupply=VBAT,
382 // -9..22dBm). This board's config requests 17dBm, which is out of the LP path's valid
383 // range — always configuring LP here regardless of tx_power_ was invalid, and is the likely
384 // reason TX completed digitally (TX_DONE fired, no error) while the awning never received
385 // anything: the PA was asked to output power outside the range its selected path supports.
386 bool const use_hp_pa = this->tx_power_ > 14;
387 uint8_t pa_config[4] = {
388 (uint8_t) (use_hp_pa ? 0x01 : 0x00), // paSel: 0=LP, 1=HP
389 (uint8_t) (use_hp_pa ? 0x01 : 0x00), // regPaSupply: 0=internal regulator (LP), 1=VBAT (HP)
390 0x04, // paDutyCycle
391 0x07, // paHpSel — RadioLib always sets this, even for LP
392 };
393 this->write_command_(LR1121_CMD_SET_PA_CONFIG, pa_config, sizeof(pa_config));
394
395 // 11. TX params: power in dBm, clamped to the selected PA path's valid range (see step 10).
396 // The ramp register is LR11x0-specific, not the SX126x encoding. RadioLib's
397 // LR1120::setOutputPower() passes `roundRampTime(us) - 3` — the shared ramp-time enum
398 // starts with three LR2021-only fast steps (2/4/8us) that don't exist on the LR11x0, so its
399 // own register value for a given ramp time is 3 less than the shared table's index. 0x0C is
400 // ~200us on this chip (0x04 would be ~80us).
401 int8_t const min_power = use_hp_pa ? -9 : -17;
402 int8_t const max_power = use_hp_pa ? 22 : 14;
403 int8_t const power = std::max(min_power, std::min(max_power, (int8_t) this->tx_power_));
404 uint8_t tx_params[2] = {(uint8_t) power, 0x0C};
405 this->write_command_(LR1121_CMD_SET_TX_PARAMS, tx_params, sizeof(tx_params));
406
407 // 12. Keep the crystal path alive after RX/TX completion.
408 uint8_t const fallback_mode = LR1121_FALLBACK_STDBY_XOSC;
409 this->write_command_(LR1121_CMD_SET_RX_TX_FALLBACK_MODE, &fallback_mode, 1);
410
411 // 13. IRQ config: two 32-bit masks — first DIO's mask (routed to DIO9) + second DIO's mask
412 // (unused, mask 0). Cross-checked against RadioLib's LRxxxx::setDioIrqParams(irq1, irq2):
413 // the command takes exactly these two fields, no separate "enable" field.
414 uint8_t irq_params[8] = {
415 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 24),
416 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 16),
417 (uint8_t) (LR1121_IRQ_DIO_ENABLE_MASK >> 8),
418 (uint8_t) LR1121_IRQ_DIO_ENABLE_MASK, // DIO9 (irq1) mask
419 0x00,
420 0x00,
421 0x00,
422 0x00, // second IRQ output — unused
423 };
424 this->write_command_(LR1121_CMD_SET_DIO_IRQ_PARAMS, irq_params, sizeof(irq_params));
425
426 // 14. Attach the DIO9 interrupt.
427 this->irq_pin_->attach_interrupt(&RadioLR1121::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
428
429 // 15. Clear any pending IRQs / errors — recording the error word first, since that is the only
430 // trace of a calibration or TCXO fault from bring-up. A healthy chip leaves it zero (confirmed
431 // on the T3-S3), so a non-zero word is a genuine fault and is warned about.
432 this->clear_irq_status(0xFFFFFFFF);
434 this->clear_errors_();
435
436 // 16. Enter continuous receive.
437 this->set_mode_rx();
438}
439
440// === Mode control ===
441
443 uint8_t const stdby_xosc = 0x01; // Same small sequential enum as SetRxTxFallbackMode (0x01=STDBY_XOSC there too).
444 this->write_command_(LR1121_CMD_SET_STANDBY, &stdby_xosc, 1);
445}
446
448 // High-ACP workaround — Semtech applies this unconditionally before every
449 // SetRx, so it lives here rather than only at init to cover every set_mode_rx() call site.
451 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF}; // 0xFFFFFF = continuous, same sentinel as SX1262.
452 this->write_command_(LR1121_CMD_SET_RX, rx_continuous, sizeof(rx_continuous));
453}
454
455// === Frequency control ===
456
458 uint8_t params[4] = {
459 (uint8_t) (freq_hz >> 24),
460 (uint8_t) (freq_hz >> 16),
461 (uint8_t) (freq_hz >> 8),
462 (uint8_t) freq_hz,
463 };
464 this->write_command_(LR1121_CMD_SET_RF_FREQUENCY, params, sizeof(params));
465 this->current_freq_ = freq_hz;
466}
467
469 this->rx_bandwidth_ = bandwidth;
471}
472
473} // namespace home_io_control
474} // namespace esphome
475
476// 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 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 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.
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.
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).