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