Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_sx1262.cpp
Go to the documentation of this file.
1/// @file radio_sx1262.cpp
2/// @brief SX1262 radio driver implementation for IO-Homecontrol.
3/// @ingroup hioc_radio
4///
5/// Unlike the SX1276, the SX1262 does not provide Semtech's IoHomeOn mode for
6/// IO-Homecontrol. On SX1276 that mode handles key protocol details in hardware:
7/// CRC generation and checking, packet boundary handling, and delivery of
8/// already-decoded protocol bytes in the FIFO. On SX1262 those pieces are reproduced
9/// in software by SoftPhyDriverBase (shared with RadioLR1121) on top of generic GFSK
10/// support; this file supplies the SPI transport and every register/opcode encoding
11/// SoftPhyDriverBase's shared RX/TX orchestration calls through virtual primitives.
12///
13/// The chip interface itself is opcode-based SPI instead of the SX1276 register model, and
14/// every transaction must respect the BUSY line. For experiment builds, the RX path
15/// prioritizes preserving the chip-reported bytes and metadata verbatim.
16
17// The SX1262 path is intentionally low-level: opcode payloads, line-coding widths, and recovery
18// thresholds are written in the same shape as the chip protocol and on-air framing.
19// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
20
21#include "radio_sx1262.h"
22#include "esphome/core/log.h"
23#include "esphome/core/application.h"
24
25#include <cinttypes>
26
27namespace esphome {
28namespace home_io_control {
29
30static const char *const TAG = "home_io_control.sx1262";
31static const uint8_t SX1262_SYNC_WORD_PARAM_24_BITS = 0x18;
32
33namespace {
34
35/// GetDeviceErrors bit → name table used by sx1262_format_device_errors().
36constexpr DeviceErrorBit SX1262_DEVICE_ERROR_BITS[] = {
37 {SX1262_DEV_ERR_RC64K_CALIB, "RC64K_CALIB_ERR"}, {SX1262_DEV_ERR_RC13M_CALIB, "RC13M_CALIB_ERR"},
38 {SX1262_DEV_ERR_PLL_CALIB, "PLL_CALIB_ERR"}, {SX1262_DEV_ERR_ADC_CALIB, "ADC_CALIB_ERR"},
39 {SX1262_DEV_ERR_IMG_CALIB, "IMG_CALIB_ERR"}, {SX1262_DEV_ERR_XOSC_START, "XOSC_START_ERR"},
40 {SX1262_DEV_ERR_PLL_LOCK, "PLL_LOCK_ERR"}, {SX1262_DEV_ERR_PA_RAMP, "PA_RAMP_ERR"},
41};
42
43/// One tick of the SX1262 TCXO startup-delay field is 15.625 µs = 1000/64 µs, so
44/// ticks = ceil(delay_us * 64 / 1000). The field is 24-bit; the largest delay this driver ever
45/// asks for (50 ms) is 3200 ticks, well inside it.
46constexpr uint32_t sx1262_tcxo_startup_ticks(uint32_t delay_us) { return (delay_us * 64U + 999U) / 1000U; }
47
48/// Net gain (TX-path pad + PA, in dB) of the GC1109/KCT8103L front-end chain (Heltec V4.2/V4.3),
49/// indexed by the SX1262 SetTxParams power setting (0..21 — the table doesn't extend to 22, the
50/// schema's max). Measured gain flattens at low drive and falls off from PA compression above
51/// ~14; see Meshtastic's `LoRaFEMInterface::powerConversion()`. XY16P35's own gain constant,
52/// below, is a single measured figure rather than a table of this shape — see its own doc comment.
53constexpr int8_t SX1262_FEM_GAIN_GC1109_DB[22] = {
54 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7,
55};
56constexpr int8_t SX1262_FEM_GAIN_KCT8103L_DB[22] = {
57 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7,
58};
59
60/// Net gain (dB) for the XY16P35 chain (LilyGO T-Beam 1W SX1262), as a single representative
61/// constant rather than a 22-entry table. LilyGO's own measured output-power figures: tx_power
62/// -9 -> 3 dBm, -6 -> 8.6 dBm, -5 -> 10 dBm, 0 -> 14 dBm, 5 -> 18 dBm (gain = measured - setting:
63/// 12/14.6/15/14/13 dB). +14 anchors on the tx_power:0 point this profile's board package ships;
64/// treat this as a rougher bound than the other two chips' 22-point tables -- five sparse
65/// measured points, not a real curve. LilyGO also publishes a flat +12dBm "Sub1G PA Gain" spec
66/// for the HM06S006P transistor inside this module -- that is the component's nominal rating, not
67/// the module's measured net gain; +14 is used because it is measured (not a datasheet nominal)
68/// and the more conservative of the two for the compliance warning below.
69constexpr int8_t SX1262_FEM_GAIN_XY16P35_DB = 14;
70
71constexpr size_t SX1262_FEM_GAIN_TABLE_LEN = 22;
72
73/// Receive-side LNA gain each FEM adds to the SX1262's RSSI, removed so readings are
74/// antenna-referred (see RadioSX1262::front_end_rx_gain_db()). Unlike the TX tables above this is
75/// one figure per profile: the LNA has no gain setting the driver controls.
76///
77/// KCT8103L, 25 dB: measured. The same dimmer at 7 m, seen from a Heltec V4.3 and from a
78/// Heltec V3 (no front end) at the same spot, read -36.5 dBm against -61 dBm (+24.5 dB); an
79/// LR1121 board read -63 dBm, so the setup repeats within about 2-4 dB. That places the gain at
80/// 25 +/- 4 dB, matching the vendor's 28 dB RX LNA gain less a few dB of insertion loss.
81///
82/// GC1109, 15 dB: quoted, not measured here. Heltec's own V3-versus-V4 comparison at 868 MHz reads
83/// -94 dBm on the V4 against -109..-110 dBm on the V3 for the same link.
84///
85/// XY16P35, 0 dB: not characterised. The module has an LNA, but no RX gain is published for it,
86/// so its RSSI (and listen-before-talk) still include an unknown amount of gain.
87constexpr FemRxGain SX1262_FEM_RX_GAIN_KCT8103L = {25, "measured on a V4.3 against a V3, +/-4 dB"};
88constexpr FemRxGain SX1262_FEM_RX_GAIN_GC1109 = {15, "quoted by Heltec, not measured here"};
89constexpr FemRxGain SX1262_FEM_RX_GAIN_XY16P35 = {0, "not characterised: RSSI still includes the LNA gain"};
90constexpr FemRxGain SX1262_FEM_RX_GAIN_NONE = {0, "no front end"};
91
92/// Upper bound of SetTxParams' power setting; `tx_power` is clamped to it before both the register
93/// write and the FEM estimate lookup, so the estimate always describes what was actually programmed.
94constexpr uint8_t SX1262_MAX_TX_POWER_DBM = 22;
95
96/// Net gain (dB) added to `tx_power` for the given profile's chain, at the given (already-
97/// clamped-into-table-domain) tx_power setting. GC1109/KCT8103L look up their 22-entry table;
98/// XY16P35 returns its single constant regardless of the index. Never called for
99/// FemProfile::NONE -- the only caller (sx1262_fem_estimated_antenna_dbm()) is itself only
100/// called when fem_profile_ != NONE.
101int8_t sx1262_fem_gain_db(FemProfile profile, uint8_t tx_power_idx) {
102 switch (profile) {
104 return SX1262_FEM_GAIN_GC1109_DB[tx_power_idx];
106 return SX1262_FEM_GAIN_KCT8103L_DB[tx_power_idx];
108 return SX1262_FEM_GAIN_XY16P35_DB;
109 case FemProfile::NONE:
110 return 0; // unreached; listed so -Werror=switch catches a future profile that forgets this function.
111 }
112 return 0; // unreachable; satisfies compilers that don't recognize the switch above as exhaustive.
113}
114
115/// Boot-log profile name and gain-estimate confidence caveat for the given profile. A `switch`
116/// that returns from every case (rather than assigning through a local) so the compiler can see
117/// every path is covered -- an assign-then-break shape here previously tripped both
118/// -Wmaybe-uninitialized (GCC can't prove FemProfile's underlying uint8_t is limited to the four
119/// named enumerators) and clang-tidy's dead-store check, for opposite reasons.
120const char *sx1262_fem_profile_name(FemProfile profile) {
121 switch (profile) {
123 return "gc1109";
125 return "kct8103l";
127 return "xy16p35";
128 case FemProfile::NONE:
129 return "none"; // unreached; this whole ESP_LOGW block is itself guarded on != NONE.
130 }
131 return "none"; // unreachable; satisfies compilers that don't recognize the switch above as exhaustive.
132}
133
134/// GC1109/KCT8103L's gain estimate rests on three low-drive sources that disagree by ~5-7 dB
135/// (SX1262_FEM_GAIN_GC1109_DB's own doc comment); XY16P35's rests on a single, sparser evidence
136/// base -- five of LilyGO's own measured points, not a real curve (SX1262_FEM_GAIN_XY16P35_DB's
137/// own doc comment) -- with no comparable numeric error bar published anywhere this project has
138/// access to. Rather than inventing one, XY16P35's boot-log caveat says plainly that this estimate
139/// is rougher than the other two profiles' equivalent line instead of quoting a number that would
140/// overstate how well-characterized it is.
141const char *sx1262_fem_confidence_caveat(FemProfile profile) {
142 switch (profile) {
145 return "+-5..7 dB, unmeasured";
147 return "vendor-measured but from five sparse points, not a real curve -- rougher than the other profiles' "
148 "estimate";
149 case FemProfile::NONE:
150 return ""; // unreached; this whole ESP_LOGW block is itself guarded on != NONE.
151 }
152 return ""; // unreachable; satisfies compilers that don't recognize the switch above as exhaustive.
153}
154
155/// Estimated antenna-port power (dBm) for `tx_power` through the named FEM profile's chain, from
156/// the gain tables/constant above. `tx_power` is clamped into the table's domain first — the same
157/// clamp init() applies before writing SetTxParams — so a caller need not re-derive it.
158int sx1262_fem_estimated_antenna_dbm(FemProfile profile, uint8_t tx_power) {
159 const uint8_t idx =
160 tx_power < SX1262_FEM_GAIN_TABLE_LEN ? tx_power : static_cast<uint8_t>(SX1262_FEM_GAIN_TABLE_LEN - 1);
161 return static_cast<int>(tx_power) + static_cast<int>(sx1262_fem_gain_db(profile, idx));
162}
163
164constexpr const char *SX1262_FEM_COMPLIANCE_HINT =
165 "Verify against your local 868 MHz SRD/EIRP limit before relying on this for compliance";
166
167} // namespace
168
170 switch (profile) {
172 return SX1262_FEM_RX_GAIN_GC1109;
174 return SX1262_FEM_RX_GAIN_KCT8103L;
176 return SX1262_FEM_RX_GAIN_XY16P35;
177 case FemProfile::NONE:
178 return SX1262_FEM_RX_GAIN_NONE;
179 }
180 return SX1262_FEM_RX_GAIN_NONE; // unreachable; satisfies compilers that don't see the switch as exhaustive.
181}
182
184 const uint8_t clamped = std::min<uint8_t>(tx_power, SX1262_MAX_TX_POWER_DBM);
185 return {clamped, sx1262_fem_estimated_antenna_dbm(profile, clamped)};
186}
187
188void sx1262_format_device_errors(uint16_t errors, char *buf, size_t buf_size) {
189 format_device_error_bits(errors, SX1262_DEVICE_ERROR_BITS, buf, buf_size);
190}
191
192// === SPI Communication (opcode-based) ===
193
194void RadioSX1262::write_opcode_(uint8_t opcode, const uint8_t *params, uint8_t len) {
195 this->wait_busy_();
196 this->spi_->spi_enable();
197 this->spi_->spi_transfer(opcode);
198 for (uint8_t i = 0; i < len; i++)
199 this->spi_->spi_transfer(params[i]);
200 this->spi_->spi_disable();
201}
202
203void RadioSX1262::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t len) {
204 this->wait_busy_();
205 this->spi_->spi_enable();
206 this->spi_->spi_transfer(opcode);
207 this->spi_->spi_transfer(0x00); // NOP — status byte
208 for (uint8_t i = 0; i < len; i++)
209 data[i] = this->spi_->spi_transfer(0x00);
210 this->spi_->spi_disable();
211}
212
214 uint8_t irq_raw[2] = {0};
215 this->read_opcode_(SX1262_GET_IRQ_STATUS, irq_raw, 2);
216 return (static_cast<uint32_t>(irq_raw[0]) << 8) | static_cast<uint32_t>(irq_raw[1]);
217}
218
219void RadioSX1262::write_register_(uint16_t addr, const uint8_t *data, uint8_t len) {
220 this->wait_busy_();
221 this->spi_->spi_enable();
222 this->spi_->spi_transfer(SX1262_WRITE_REGISTER);
223 this->spi_->spi_transfer((addr >> 8) & 0xFF); // Address MSB
224 this->spi_->spi_transfer(addr & 0xFF); // Address LSB
225 for (uint8_t i = 0; i < len; i++)
226 this->spi_->spi_transfer(data[i]);
227 this->spi_->spi_disable();
228}
229
230void RadioSX1262::read_register_(uint16_t addr, uint8_t *data, uint8_t len) {
231 this->wait_busy_();
232 this->spi_->spi_enable();
233 this->spi_->spi_transfer(SX1262_READ_REGISTER);
234 this->spi_->spi_transfer((addr >> 8) & 0xFF); // Address MSB
235 this->spi_->spi_transfer(addr & 0xFF); // Address LSB
236 this->spi_->spi_transfer(0x00); // NOP — status byte
237 for (uint8_t i = 0; i < len; i++)
238 data[i] = this->spi_->spi_transfer(0x00);
239 this->spi_->spi_disable();
240}
241
242void RadioSX1262::write_buffer_(uint8_t offset, const uint8_t *data, uint8_t len) {
243 this->wait_busy_();
244 this->spi_->spi_enable();
245 this->spi_->spi_transfer(SX1262_WRITE_BUFFER);
246 this->spi_->spi_transfer(offset);
247 for (uint8_t i = 0; i < len; i++)
248 this->spi_->spi_transfer(data[i]);
249 this->spi_->spi_disable();
250}
251
252void RadioSX1262::read_buffer_(uint8_t offset, uint8_t *data, uint8_t len) {
253 this->wait_busy_();
254 this->spi_->spi_enable();
255 this->spi_->spi_transfer(SX1262_READ_BUFFER);
256 this->spi_->spi_transfer(offset);
257 this->spi_->spi_transfer(0x00); // NOP — status byte
258 for (uint8_t i = 0; i < len; i++)
259 data[i] = this->spi_->spi_transfer(0x00);
260 this->spi_->spi_disable();
261}
262
263// === Packet params helper ===
264
265void RadioSX1262::set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type,
266 uint8_t crc_type) {
267 // Field order and the byte->bit preamble conversion are shared with LR1121 — see
268 // SoftPhyDriverBase::build_gfsk_packet_params. Only the detector length (8 bits / 1 byte here),
269 // the sync-word selector, and the opcode/transport are SX1262-specific.
270 uint8_t params[GFSK_PACKET_PARAMS_LEN];
271 build_gfsk_packet_params({.preamble_bytes = preamble_len,
272 .preamble_detector = 0x04, // 8 bits (1 byte)
273 .sync_word_param = SX1262_SYNC_WORD_PARAM_24_BITS,
274 .packet_type = packet_type,
275 .payload_len = payload_len,
276 .crc_type = crc_type},
277 params);
278 this->write_opcode_(SX1262_SET_PACKET_PARAMS, params, sizeof(params));
279}
280
282 // The variable-size packet engine recovers a stable 15-byte boundary, but that boundary is
283 // too short for a full software UART decode and buffer reads past it are not trustworthy.
284 // Probe again with a fixed raw packet size that matches typical UART-packed 23-25 byte
285 // IO-homecontrol frames (SOFT_PHY_RX_PROBE_PACKET_LEN, shared with LR1121 — see its doc
286 // comment in radio_soft_phy_driver_base.h).
287 this->set_packet_params_(8, SOFT_PHY_RX_PROBE_PACKET_LEN, SX1262_GFSK_PACKET_TYPE_KNOWN_LENGTH, SX1262_GFSK_CRC_OFF);
288}
289
291 // SX1262 GFSK modulation parameters for the IO-Homecontrol 868 MHz waveform.
292 //
293 // BitRate = 32 * Fxosc / BR_reg → BR_reg = 32 * 32MHz / 38400 = 26667 = 0x00682B
294 // Pulse shape: Gaussian BT=1.0 (0x0B) — reduces TX spectral occupation.
295 // Bandwidth: the register byte is runtime-tunable via rx_bandwidth_ — see
296 // TuningConfig::sx1262_rx_bandwidth (tuning_config.h) for the current default (58.6 kHz) and
297 // why. The value is double-sideband, and with no AFC in GFSK mode this filter alone has to
298 // absorb the peer's carrier offset.
299 // Fdev = fdev_hz * 2^25 / 32e6 → 19200 * 2^25 / 32e6 = 20133 = 0x004EA5
300 uint8_t mod_params[8] = {
301 0x00,
302 0x68,
303 0x2B, // Bitrate: 38400 bps
304 0x0B, // Pulse shape: Gaussian BT=1.0
305 static_cast<uint8_t>(this->rx_bandwidth_), // Bandwidth: runtime-tunable
306 0x00,
307 0x4E,
308 0xA5, // Fdev: 19200 Hz
309 };
310 this->write_opcode_(SX1262_SET_MODULATION_PARAMS, mod_params, sizeof(mod_params));
311}
312
313void RadioSX1262::clear_irq_status(uint32_t irq_mask) {
314 uint8_t clear_irq[2] = {
315 (uint8_t) ((irq_mask >> 8) & 0xFF),
316 (uint8_t) (irq_mask & 0xFF),
317 };
318 this->write_opcode_(SX1262_CLEAR_IRQ_STATUS, clear_irq, sizeof(clear_irq));
319}
320
322 uint8_t errors_raw[2] = {0};
323 this->read_opcode_(SX1262_GET_DEVICE_ERRORS, errors_raw, sizeof(errors_raw));
324 return (uint16_t) (((uint16_t) errors_raw[0] << 8) | errors_raw[1]);
325}
326
328 uint8_t clear_errors[2] = {0x00, 0x00};
329 this->write_opcode_(SX1262_CLEAR_DEVICE_ERRORS, clear_errors, sizeof(clear_errors));
330}
331
333 uint8_t buf_base[2] = {SX1262_TX_BUFFER_BASE, SX1262_RX_BUFFER_BASE};
334 this->write_opcode_(SX1262_SET_BUFFER_BASE_ADDRESS, buf_base, sizeof(buf_base));
335}
336
337void RadioSX1262::fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
338 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
339 uint8_t packet_status[3] = {0};
340 this->read_opcode_(SX1262_GET_PACKET_STATUS, packet_status, sizeof(packet_status));
341
342 this->populate_capture_base_(blocking_wait, this->current_freq_, this->raw_rssi_to_dbm_(packet_status[1]), raw,
343 raw_len, frame, frame_len);
344 this->last_capture_.rx_done = (irq_status & SX1262_IRQ_RX_DONE) != 0;
345 this->last_capture_.crc_error = (irq_status & SX1262_IRQ_CRC_ERR) != 0;
346 this->last_capture_.irq_status = static_cast<uint16_t>(irq_status);
347 this->last_capture_.packet_status = packet_status[0];
348 this->last_capture_.rx_offset = rx_offset;
349 this->last_capture_.reported_len = reported_len;
350}
351
353 uint8_t raw = 0;
354 this->read_opcode_(SX1262_GET_RSSI_INST, &raw, 1);
355 return raw;
356}
357
358void RadioSX1262::get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) {
359 uint8_t rx_status[2] = {0};
360 this->read_opcode_(SX1262_GET_RX_BUFFER_STATUS, rx_status, sizeof(rx_status));
361 reported_len = rx_status[0];
362 rx_offset = rx_status[1];
363}
364
366 // Read-modify-write so the reserved bits of the register keep whatever the chip put there.
367 uint8_t tx_modulation = 0;
368 this->read_register_(SX1262_REG_TX_MODULATION, &tx_modulation, 1);
369 tx_modulation |= SX1262_TX_MODULATION_GFSK_BIT;
370 this->write_register_(SX1262_REG_TX_MODULATION, &tx_modulation, 1);
371}
372
374 // Start TX with 4s timeout (256000 ticks at 15.625us/tick = 0x03E800).
375 uint8_t tx_timeout[3] = {0x03, 0xE8, 0x00};
376 this->write_opcode_(SX1262_SET_TX, tx_timeout, sizeof(tx_timeout));
377}
378
379// === ISR ===
380
382
383// === Initialization ===
384
386 // --- Pin setup ---
387 this->rst_pin_->setup();
388 this->dio1_pin_->setup();
389 this->busy_pin_->setup();
390
391 // Front-end module pins (e.g., Heltec V4). Order matters for every FEM
392 // config, legacy raw-pin included: the parts that have a secondary chip-enable pin (CSD --
393 // GC1109/KCT8103L; XY16P35 has none) require VBAT (here: VFEM) to be raised before any control
394 // pin is driven, so vfem_pin_ goes first with a brief settle delay, then fem_en_pin_, then the
395 // mode pin last.
396 if (this->vfem_pin_ != nullptr) {
397 this->vfem_pin_->setup();
398 this->vfem_pin_->digital_write(true);
399 delay(1); // GC1109/KCT8103L power-on sequencing: VBAT must precede CSD/CPS/CTX
400 }
401 if (this->fem_en_pin_ != nullptr) {
402 this->fem_en_pin_->setup();
403 this->fem_en_pin_->digital_write(true);
404 }
405 if (this->fem_pa_pin_ != nullptr) {
406 this->fem_pa_pin_->setup();
407 if (this->fem_profile_ == FemProfile::NONE) {
408 // No FEM behaviour configured: strap HIGH once, never touched again.
409 this->fem_pa_pin_->digital_write(true);
410 } else {
411 // A configured profile starts the mode pin in its receive/idle level.
412 this->fem_set_rx_mode_();
413 }
414 }
415
416 // --- Hardware reset ---
417 this->reset_hardware_();
418 this->wait_busy_();
419 if (this->failed_)
420 return false;
421
422 this->configure_radio_();
423 if (this->failed_)
424 return false;
425
426 if (this->fem_profile_ != FemProfile::NONE) {
427 // The estimate is a rough bound, not a calibrated one — see sx1262_fem_tx_estimate().
428 const FemTxEstimate estimate = sx1262_fem_tx_estimate(this->fem_profile_, this->tx_power_);
429 ESP_LOGW(TAG,
430 "FEM profile %s active: tx_power %u is amplified by the front-end module to an "
431 "estimated ~%d dBm at the antenna port (%s). %s",
432 sx1262_fem_profile_name(this->fem_profile_), estimate.tx_power, estimate.antenna_dbm,
433 sx1262_fem_confidence_caveat(this->fem_profile_), SX1262_FEM_COMPLIANCE_HINT);
434 }
435
436 ESP_LOGI(TAG, "SX1262 initialized");
437 return true;
438}
439
441 this->wait_busy_();
442 this->spi_->spi_enable();
443 uint8_t const chip_status = this->spi_->spi_transfer(SX1262_GET_STATUS);
444 this->spi_->spi_transfer(0x00);
445 this->spi_->spi_disable();
446
447 uint8_t const chip_mode = (chip_status >> 4) & 0x07;
448 uint8_t const cmd_status = (chip_status >> 1) & 0x07;
449 const char *mode_str = "?";
450 switch (chip_mode) {
451 case 2:
452 mode_str = "STDBY_RC";
453 break;
454 case 3:
455 mode_str = "STDBY_XOSC";
456 break;
457 case 4:
458 mode_str = "FS";
459 break;
460 case 5:
461 mode_str = "RX";
462 break;
463 case 6:
464 mode_str = "TX";
465 break;
466 default:
467 break;
468 }
469
470 uint8_t sync[3];
471 this->read_register_(SX1262_REG_SYNC_WORD, sync, 3);
472
473 uint8_t irq_raw[2];
474 this->read_opcode_(SX1262_GET_IRQ_STATUS, irq_raw, 2);
475 uint16_t const irq = ((uint16_t) irq_raw[0] << 8) | irq_raw[1];
476 uint16_t const errors = this->get_device_errors_();
477
478 ESP_LOGCONFIG(TAG, " SX1262 Diagnostic:");
479 ESP_LOGCONFIG(TAG, " TCXO voltage: %s", tcxo_voltage_label(this->tcxo_voltage_));
480 ESP_LOGCONFIG(TAG, " Chip status: 0x%02X (mode=%s, cmd=%u)", chip_status, mode_str, cmd_status);
481 ESP_LOGCONFIG(TAG, " BUSY=%d DIO1=%d", this->busy_pin_->digital_read(), this->dio1_pin_->digital_read());
482 ESP_LOGCONFIG(TAG, " Sync word: %02X %02X %02X (expect 57 FD 99)", sync[0], sync[1], sync[2]);
483 ESP_LOGCONFIG(TAG, " IRQ status: 0x%04X", irq);
484 char errbuf[DEVICE_ERROR_STR_SIZE];
485 sx1262_format_device_errors(errors, errbuf, sizeof(errbuf));
486 ESP_LOGCONFIG(TAG, " Device errors: 0x%04X (%s)", errors, errbuf);
487 if (this->tcxo_startup_attempts_ > 1) {
488 ESP_LOGCONFIG(TAG, " TCXO startup: %u attempts, %" PRIu32 " ms final delay",
489 static_cast<unsigned>(this->tcxo_startup_attempts_), this->tcxo_startup_delay_us_ / 1000);
490 }
492}
493
495 if (this->fem_profile_ == FemProfile::NONE)
496 return;
497 // init() warns about this once at boot, before the API connects, so most users never see that
498 // line; dump_config is replayed to every new log client, which makes this the copy they read.
499 const FemTxEstimate estimate = sx1262_fem_tx_estimate(this->fem_profile_, this->tx_power_);
500 ESP_LOGCONFIG(TAG, " Front-end module (FEM):");
501 ESP_LOGCONFIG(TAG, " Profile: %s", sx1262_fem_profile_name(this->fem_profile_));
502 LOG_PIN(" VFEM Pin: ", this->vfem_pin_);
503 LOG_PIN(" Enable Pin: ", this->fem_en_pin_);
504 LOG_PIN(" PA/Mode Pin: ", this->fem_pa_pin_);
505 ESP_LOGCONFIG(TAG, " TX estimate: tx_power %u is amplified to ~%d dBm at the antenna port (%s)", estimate.tx_power,
506 estimate.antenna_dbm, sx1262_fem_confidence_caveat(this->fem_profile_));
507 ESP_LOGCONFIG(TAG, " %s", SX1262_FEM_COMPLIANCE_HINT);
508 const FemRxGain rx_gain = sx1262_fem_rx_gain(this->fem_profile_);
509 ESP_LOGCONFIG(TAG, " RX gain removed from RSSI: %d dB (%s)", static_cast<int>(rx_gain.db), rx_gain.basis);
510}
511
513 // Strictly increasing startup-delay ladder. A slow TCXO that misses the first (nominal 5 ms)
514 // window often starts inside a longer one; each rung must actually be longer than the last, so
515 // a retry buys the oscillator real extra time.
516 static constexpr uint32_t TCXO_STARTUP_DELAYS_US[] = {5000, 10000, 50000};
517 // Margin over the programmed startup window for wait_busy_() to also cover CALIBRATE running on
518 // the fresh clock, in milliseconds.
519 static constexpr uint32_t TCXO_BUSY_MARGIN_MS = 25;
520
521 this->tcxo_startup_attempts_ = 0;
522 this->tcxo_startup_delay_us_ = 0;
523
524 // The chip holds BUSY through the whole programmed TCXO startup window (up to 50 ms on the last
525 // rung) plus calibration before it can latch XOSC_START_ERR. wait_busy_()'s normal
526 // SX1262_BUSY_TIMEOUT_MS (10 ms, sized for RC-oscillator timing) is far shorter than that, so a
527 // slow TCXO would trip a BUSY timeout, latch failed_, and brick the component in exactly the
528 // case this retry exists to rescue. Widen the budget for the bring-up and restore it after.
529 const uint32_t saved_busy_timeout_ms = this->get_busy_timeout_ms_();
530
531 uint16_t last_errors = 0;
532 for (const uint32_t delay_us : TCXO_STARTUP_DELAYS_US) {
533 this->set_busy_timeout_ms_((delay_us / 1000) + TCXO_BUSY_MARGIN_MS);
534
535 const uint32_t ticks = sx1262_tcxo_startup_ticks(delay_us);
536 uint8_t tcxo_params[4] = {this->tcxo_voltage_, static_cast<uint8_t>((ticks >> 16) & 0xFF),
537 static_cast<uint8_t>((ticks >> 8) & 0xFF), static_cast<uint8_t>(ticks & 0xFF)};
538 this->write_opcode_(SX1262_SET_DIO3_AS_TCXO_CTRL, tcxo_params, sizeof(tcxo_params));
539
540 // Sleep out most of the startup window on the host before touching the chip again so
541 // wait_busy_() only has to absorb the remainder plus calibration.
542 delay((delay_us / 1000) + 2);
543
544 // XOSC_START_ERR / IMG_CALIB_ERR are the expected POR state with a TCXO fitted — clear the
545 // latch here so the post-CALIBRATE read below reflects only this attempt, which is what lets
546 // the early break work. Our LR1121 driver clears in the same spot for the same reason.
547 this->clear_device_errors_();
548
549 uint8_t const cal = 0x7F; // calibrate all blocks
550 this->write_opcode_(SX1262_CALIBRATE, &cal, 1);
551 delay(5); // wait for calibration to complete (same margin as the LR1121 driver); the widened
552 // busy_timeout_ absorbs any overrun on the next transaction's wait_busy_()
553
554 this->tcxo_startup_attempts_++;
555 this->tcxo_startup_delay_us_ = delay_us;
556
557 if (this->failed_)
558 break; // a BUSY timeout already failed the chip — escalating further just reads garbage
559
560 last_errors = this->get_device_errors_();
561 if ((last_errors & SX1262_DEV_ERR_XOSC_START) == 0)
562 break; // TCXO is running — stop escalating
563 }
564
565 // Restore the steady-state BUSY budget — but if the ladder had to escalate, keep a ceiling wide
566 // enough for the startup window that actually worked. configure_radio_() step 4 re-enters
567 // STDBY_XOSC immediately after this, which powers the TCXO again and holds BUSY for that same
568 // window, so step 5's wait_busy_() would otherwise time out and latch failed_ on a board the
569 // ladder just rescued. busy_timeout_ms_ is a ceiling, never a delay, so a permanently wider
570 // value costs a healthy board nothing; runtime never re-pays the startup wait (step 6b sets the
571 // XOSC standby fallback and set_mode_standby() uses STDBY_XOSC, so the chip never drops to RC).
572 if (this->tcxo_startup_attempts_ > 1) {
573 const uint32_t escalated = (this->tcxo_startup_delay_us_ / 1000) + TCXO_BUSY_MARGIN_MS;
574 this->set_busy_timeout_ms_(saved_busy_timeout_ms > escalated ? saved_busy_timeout_ms : escalated);
575 } else {
576 this->set_busy_timeout_ms_(saved_busy_timeout_ms);
577 }
578
579 // A BUSY timeout during the ladder already logged "BUSY timeout" and will fail init()
580 if (this->failed_)
581 return;
582
583 // Say nothing on the happy path (one attempt): the log stays identical to a single-shot bring-up.
584 if (this->tcxo_startup_attempts_ <= 1)
585 return;
586 if ((last_errors & SX1262_DEV_ERR_XOSC_START) != 0) {
587 ESP_LOGE(TAG,
588 "SX1262 TCXO never started after %u attempts (XOSC_START_ERR persists) — check "
589 "tcxo_voltage for this board and the TCXO part itself",
590 static_cast<unsigned>(this->tcxo_startup_attempts_));
591 } else {
592 ESP_LOGW(TAG, "SX1262 TCXO started after %u attempts (%" PRIu32 " ms startup delay)",
593 static_cast<unsigned>(this->tcxo_startup_attempts_), this->tcxo_startup_delay_us_ / 1000);
594 }
595}
596
598 // 1. Standby on RC oscillator (safe starting point)
599 uint8_t const stdby_rc = 0x00;
600 this->write_opcode_(SX1262_SET_STANDBY, &stdby_rc, 1);
601
602 // 2-3. Bring up the reference clock and calibrate. A board with a bare crystal (tcxo_voltage:
603 // none) has no DIO3-controlled TCXO: skip that programming and calibrate straight off the
604 // crystal. Otherwise configure the TCXO with a bounded XOSC-start retry (see configure_tcxo_()).
605 if (this->tcxo_voltage_ == TCXO_VOLTAGE_NONE) {
606 // Clear the expected POR device-error latch before calibrating, same as the TCXO path does
607 // per attempt, so the step-18 read below only ever reports a genuine post-init fault.
608 this->clear_device_errors_();
609 uint8_t const cal = 0x7F;
610 this->write_opcode_(SX1262_CALIBRATE, &cal, 1);
611 delay(5); // Wait for calibration to complete
612 } else {
613 this->configure_tcxo_();
614 }
615
616 // 4. Standby on XOSC (reference clock now running)
617 uint8_t const stdby_xosc = 0x01;
618 this->write_opcode_(SX1262_SET_STANDBY, &stdby_xosc, 1);
619
620 // 5. Use DC-DC regulator for better efficiency
621 uint8_t const reg_mode = 0x01;
622 this->write_opcode_(SX1262_SET_REGULATOR_MODE, &reg_mode, 1);
623
624 // 6. DIO2 as RF switch control (for boards with integrated RF switch)
625 uint8_t const dio2_rf = 0x01;
626 this->write_opcode_(SX1262_SET_DIO2_AS_RF_SWITCH_CTRL, &dio2_rf, 1);
627
628 // 6b. Keep the crystal path alive after RX/TX completion instead of relying on the chip default.
629 uint8_t const fallback_mode = SX1262_FALLBACK_STDBY_XOSC;
630 this->write_opcode_(SX1262_SET_RX_TX_FALLBACK_MODE, &fallback_mode, 1);
631
632 // 7. FSK packet type
633 uint8_t const pkt_type = 0x00;
634 this->write_opcode_(SX1262_SET_PACKET_TYPE, &pkt_type, 1);
635
636 // 8. Set frequency to channel 2 (868.95 MHz)
637 this->set_frequency_register(FREQ_CH2);
638
639 // 9. Calibrate image for 863-870 MHz band
640 uint8_t cal_img[2] = {0xD7, 0xDB};
641 this->write_opcode_(SX1262_CALIBRATE_IMAGE, cal_img, sizeof(cal_img));
642
643 // 9b. Use boosted RX gain for maximum sensitivity.
644 uint8_t const rx_gain = 0x96;
645 this->write_register_(SX1262_REG_RX_GAIN, &rx_gain, 1);
646
647 // 10. Apply GFSK modulation parameters (detailed values live in write_modulation_params_()).
649
650 // 11. Default RX packet params: variable-size GFSK with hardware CCITT CRC validation.
651 this->set_rx_packet_params();
652
653 // 12. Sync word: 0x57 0xFD 0x99 (24 bits). Not an independent value: it is exactly
654 // uart_encode_packet({0x55, 0xFF, 0x33}) — SX1276's own raw, confirmed-working sync bytes — read
655 // starting 6 bits into the first UART cell instead of at the start bit. SyncWordDerivation
656 // (radio_soft_phy_test.cpp) confirms this is the unique 10-bit-cell offset that reproduces it.
657 uint8_t sync_word[8] = {0x57, 0xFD, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00};
658 this->write_register_(SX1262_REG_SYNC_WORD, sync_word, sizeof(sync_word));
659
660 // 12b. CRC registers are configured for potential future hardware-CRC use, but RX uses
661 // CRC_OFF because the UART encoding makes hardware CRC checking impossible — the chip
662 // sees UART-packed bits, not raw protocol bytes.
663 uint8_t crc_init[2] = {0x1D, 0x0F};
664 this->write_register_(0x06BC, crc_init, 2);
665 uint8_t crc_poly[2] = {0x10, 0x21};
666 this->write_register_(0x06BE, crc_poly, 2);
667
668 // 13. Buffer base addresses: TX at 0x00, RX at 0x80
669 this->configure_buffer_base();
670
671 // 14. PA config: SX1262 high power PA (paDutyCycle=0x04, hpMax=0x07, deviceSel=0x00=SX1262, paLut=0x01)
672 uint8_t pa_config[4] = {0x04, 0x07, 0x00, 0x01};
673 this->write_opcode_(SX1262_SET_PA_CONFIG, pa_config, sizeof(pa_config));
674
675 // 14b. Apply Semtech's SX1262 clamp workaround for better tolerance of RF mismatch.
676 uint8_t tx_clamp = 0;
677 this->read_register_(SX1262_REG_TX_CLAMP_CONFIG, &tx_clamp, 1);
678 tx_clamp |= 0x1E;
679 this->write_register_(SX1262_REG_TX_CLAMP_CONFIG, &tx_clamp, 1);
680
681 // 15. TX params: power in dBm (SX1262 accepts -9 to +22 directly), ramp 200us (0x04)
682 int8_t const power = std::max((int8_t) -9, std::min((int8_t) SX1262_MAX_TX_POWER_DBM, (int8_t) this->tx_power_));
683 uint8_t tx_params[2] = {(uint8_t) power, 0x04};
684 this->write_opcode_(SX1262_SET_TX_PARAMS, tx_params, sizeof(tx_params));
685
686 // 16. IRQ config: map TxDone + RxDone + SyncWordValid + CrcErr to DIO1. irqMask additionally
687 // enables PreambleDetected system-wide (SX126x irqMask gates GetIrqStatus itself, not just DIO
688 // routing — confirmed against Semtech's own driver docs) so is_preamble_detected() can finally
689 // return true on this chip; dio1Mask deliberately leaves it out so the ISR still only wakes on
690 // a terminal event, not on every preamble. See
691 // SX1262_IRQ_ACTIVITY_MASK's doc comment (radio_sx1262.h) for the other half of this change —
692 // poll_until_activity_() must not treat a bare preamble as terminal, or this unmask tears down
693 // RX mid-reception instead of fixing anything.
694 uint8_t irq_params[8] = {
695 0x00, 0x4F, // irqMask: TxDone(0x0001) | RxDone(0x0002) | PreambleDetected(0x0004) |
696 // SyncWordValid(0x0008) | CrcErr(0x0040)
697 0x00, 0x4B, // dio1Mask: unchanged — TxDone|RxDone|SyncWordValid|CrcErr only
698 0x00, 0x00, // dio2Mask: none
699 0x00, 0x00, // dio3Mask: none
700 };
701 this->write_opcode_(SX1262_SET_DIO_IRQ_PARAMS, irq_params, sizeof(irq_params));
702
703 // 17. Attach DIO1 interrupt
704 this->dio1_pin_->attach_interrupt(&RadioSX1262::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
705
706 // 18. Clear any pending IRQs, and report the device-error word before clearing it. Both the TCXO
707 // and bare-crystal paths in step 2-3 clear the expected POR flags (XOSC_START_ERR, IMG_CALIB_ERR
708 // with a TCXO fitted) before calibrating, so a non-zero word here is a genuine post-init fault —
709 // a PLL that would not lock, calibration that failed, or a TCXO that never started even after
710 // the retry ladder. Such a chip still initializes and still transmits, just off-frequency or
711 // off-calibration, so surfacing the decoded flags is the one cheap piece of evidence for it.
712 this->clear_irq_status(0xFFFF);
714 this->clear_device_errors_();
715
716 // 19. Enter continuous receive. Written inline rather than via set_mode_rx() — correct only
717 // because the FEM mode pin was already left in its RX level by the pin-setup block above (HIGH
718 // for a legacy FemProfile::NONE strap, its computed receive/idle level for a configured profile
719 // (see fem_set_rx_mode_())); a future refactor that reorders init() must not move this ahead of
720 // that block, or route through set_mode_rx() instead.
721 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF}; // 0xFFFFFF = continuous
722 this->write_opcode_(SX1262_SET_RX, rx_continuous, sizeof(rx_continuous));
723}
724
725// === Mode control ===
726
728 this->fem_set_rx_mode_();
729 uint8_t const stdby = 0x01; // STDBY_XOSC
730 this->write_opcode_(SX1262_SET_STANDBY, &stdby, 1);
731}
732
734 this->fem_set_rx_mode_();
735 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF};
736 this->write_opcode_(SX1262_SET_RX, rx_continuous, sizeof(rx_continuous));
737}
738
739// === Front-end module mode control ===
740
741bool RadioSX1262::fem_tx_active_level_() const { return this->fem_profile_ != FemProfile::XY16P35; }
742
744 if (this->fem_profile_ == FemProfile::NONE || this->fem_pa_pin_ == nullptr)
745 return;
746 this->fem_pa_pin_->digital_write(this->fem_tx_active_level_());
747}
748
750 if (this->fem_profile_ == FemProfile::NONE || this->fem_pa_pin_ == nullptr)
751 return;
752 this->fem_pa_pin_->digital_write(!this->fem_tx_active_level_());
753}
754
755// === Frequency control ===
756
758 auto freq_reg = (uint32_t) ((double) freq_hz * (1 << 25) / 32e6);
759 uint8_t params[4] = {
760 (uint8_t) (freq_reg >> 24),
761 (uint8_t) (freq_reg >> 16),
762 (uint8_t) (freq_reg >> 8),
763 (uint8_t) freq_reg,
764 };
765 this->write_opcode_(SX1262_SET_RF_FREQUENCY, params, sizeof(params));
766 this->current_freq_ = freq_hz;
767}
768
770 this->rx_bandwidth_ = bandwidth;
772}
773
774} // namespace home_io_control
775} // namespace esphome
776
777// 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.
bool failed_
Latched by fail_; see is_failed.
void set_rx_packet_params() override
Configure RX-specific packet parameters (preamble detector length, fixed probe length).
RadioSX1262(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio1_pin, InternalGPIOPin *busy_pin, uint8_t tx_power, uint8_t tcxo_voltage, InternalGPIOPin *fem_en_pin=nullptr, InternalGPIOPin *vfem_pin=nullptr, InternalGPIOPin *fem_pa_pin=nullptr, FemProfile fem_profile=FemProfile::NONE)
void set_mode_rx() override
Switch to continuous receive mode.
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 write_opcode_(uint8_t opcode, const uint8_t *params, uint8_t len)
Write an opcode with optional parameter bytes.
void set_mode_standby() override
Switch to standby mode.
void clear_irq_status(uint32_t irq_mask) override
Clear IRQ status bits.
void write_modulation_params_()
Apply the runtime bandwidth setting to the SX1262 modulation parameters.
void write_buffer_(uint8_t offset, const uint8_t *data, uint8_t len)
Write into the SX1262 TX/RX buffer at a given offset.
void fem_set_rx_mode_()
Drive the FEM mode pin to its receive state (LNA), a no-op when no FEM profile is configured.
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 clear_device_errors_()
Clear device error flags.
void fem_set_tx_mode_()
Drive the FEM mode pin to its transmit state (full PA), a no-op when no FEM profile is configured.
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 write_register_(uint16_t addr, const uint8_t *data, uint8_t len)
Write to a register (SX1262 uses opcodes for register access).
void configure_buffer_base() override
Hook run as part of reset_rx_state_, before re-entering RX.
void set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type, uint8_t crc_type)
Configure packet parameters (preamble, payload length, CRC).
void dump_debug() override
Dump SX1262‑specific debug info.
void configure_radio_()
Full radio initialization (called from init()).
void start_tx() override
Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,...
void apply_tx_modulation_workaround_()
Apply the Semtech TX modulation-quality erratum workaround (datasheet §15.1).
void configure_tcxo_()
Bring up the DIO3-controlled TCXO and calibrate, with a bounded XOSC-start retry.
static void gpio_intr(RadioSX1262 *arg)
DIO1 ISR — sets dio_fired flag. Runs in interrupt context.
void read_opcode_(uint8_t opcode, uint8_t *data, uint8_t len)
Read response from an opcode.
bool init() override
Initialize the radio hardware. Returns true on success.
void set_rx_bandwidth_(SX1262RxBandwidth bandwidth)
Apply the RX bandwidth selector and rewrite the modulation parameters.
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 read_buffer_(uint8_t offset, uint8_t *data, uint8_t len)
Read from the SX1262 RX buffer.
void read_register_(uint16_t addr, uint8_t *data, uint8_t len)
Read from a register.
void dump_front_end() override
Dump the FEM profile and its antenna-power estimate (nothing without a FEM).
bool fem_tx_active_level_() const
The level fem_pa_pin_ is driven to for the duration of a transmission, for the current fem_profile_.
uint16_t get_device_errors_()
Read device error flags (and clear them).
void wait_busy_()
Wait until busy_pin_ reads low, feeding the watchdog while polling.
void set_busy_timeout_ms_(uint32_t timeout_ms)
Set the wait_busy_ timeout, in milliseconds.
uint32_t get_busy_timeout_ms_() const
Current wait_busy_ timeout, in milliseconds.
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.
SX1262RxBandwidth
Valid SX1262 RX bandwidth options (kHz register values).
static constexpr const char * TAG
FemProfile
Which RF front-end module (if any) sits between the SX1262 and the antenna.
@ XY16P35
LilyGO T-Beam 1W SX1262 (868 MHz).
@ KCT8103L
Heltec WiFi LoRa 32 V4.3 / V4 R8 – Kangxi KCT8103L. Mode pin active-HIGH during TX.
@ GC1109
Heltec WiFi LoRa 32 V4.2 – Geo-chip GC1109. Mode pin active-HIGH during TX.
@ NONE
No FEM, or a board with the FEM pins wired but no behaviour profile set – see the NONE-specific note ...
void sx1262_format_device_errors(uint16_t errors, char *buf, size_t buf_size)
Expand a GetDeviceErrors word into a human-readable NAME|NAME|... string.
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.
FemTxEstimate sx1262_fem_tx_estimate(FemProfile profile, uint8_t tx_power)
Estimate what a FEM profile radiates for a configured tx_power.
FemRxGain sx1262_fem_rx_gain(FemProfile profile)
The receive gain to remove from RSSI for a FEM profile.
constexpr uint8_t TCXO_VOLTAGE_NONE
Sentinel tcxo_voltage code (YAML none) for a board with a bare crystal and no TCXO.
static const uint8_t SX1262_SYNC_WORD_PARAM_24_BITS
static const char *const TAG
SX1262 radio driver for IO-Homecontrol.
One named bit of a chip's device-error word.
Receive-side gain of a FEM profile's LNA, in the SX1262's RSSI scale.
int8_t db
Gain (dB) the RSSI includes and the driver removes; 0 when unknown or absent.
const char * basis
Where the figure comes from and how far to trust it, for the config dump.
Antenna-port power estimate for a configured tx_power through a FEM profile's chain.
int antenna_dbm
Estimated antenna-port power in dBm — a rough bound, not a calibrated figure.
uint8_t tx_power
The tx_power actually programmed: the configured value clamped to SetTxParams' range.
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).