Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_sx1262.h
Go to the documentation of this file.
1#pragma once
2
3/// @file radio_sx1262.h
4/// @brief SX1262 radio driver for IO-Homecontrol.
5/// @ingroup hioc_radio
6///
7/// Implements the RadioDriver interface for the Semtech SX1262 transceiver.
8/// Unlike the SX1276, the SX1262 uses opcode-based SPI commands and requires a
9/// BUSY pin check before every SPI transaction. The capture path preserves the
10/// radio-reported RX metadata so offline analysis can work from trustworthy data.
11/// The IRQ-driven RX/TX orchestration this driver shares with RadioLR1121 lives in
12/// SoftPhyDriverBase (see radio_soft_phy_driver_base.h) — this file has the SX1262-specific
13/// SPI transport and register/opcode encoding underneath it.
14/// @todo Validate the Heltec V4.2 on real hardware, especially its assumed front-end module
15/// enable pins and TCXO voltage selection. The V4.3 is validated and does not settle this:
16/// its GC1109 is a different part from the V4.3's KCT8103L, and the two are wired the other
17/// way round (see ADR 0035).
18
20#include "esphome/core/hal.h"
21
22#include <cstddef>
23
24namespace esphome {
25namespace home_io_control {
26
27// ============================================================================
28// SX1262 Opcode Constants
29// ============================================================================
30
31static constexpr uint8_t SX1262_SET_STANDBY = 0x80;
32static constexpr uint8_t SX1262_SET_RX = 0x82;
33static constexpr uint8_t SX1262_SET_TX = 0x83;
34static constexpr uint8_t SX1262_SET_RF_FREQUENCY = 0x86;
35static constexpr uint8_t SX1262_SET_RX_TX_FALLBACK_MODE = 0x93;
36static constexpr uint8_t SX1262_WRITE_BUFFER = 0x0E;
37static constexpr uint8_t SX1262_READ_BUFFER = 0x1E;
38static constexpr uint8_t SX1262_SET_DIO_IRQ_PARAMS = 0x08;
39static constexpr uint8_t SX1262_GET_IRQ_STATUS = 0x12;
40static constexpr uint8_t SX1262_GET_PACKET_STATUS = 0x14;
41static constexpr uint8_t SX1262_GET_DEVICE_ERRORS = 0x17;
42static constexpr uint8_t SX1262_CLEAR_IRQ_STATUS = 0x02;
43static constexpr uint8_t SX1262_CLEAR_DEVICE_ERRORS = 0x07;
44static constexpr uint8_t SX1262_SET_PACKET_TYPE = 0x8A;
45static constexpr uint8_t SX1262_SET_MODULATION_PARAMS = 0x8B;
46static constexpr uint8_t SX1262_SET_PACKET_PARAMS = 0x8C;
47static constexpr uint8_t SX1262_SET_BUFFER_BASE_ADDRESS = 0x8F;
48static constexpr uint8_t SX1262_SET_PA_CONFIG = 0x95;
49static constexpr uint8_t SX1262_SET_TX_PARAMS = 0x8E;
50static constexpr uint8_t SX1262_SET_DIO2_AS_RF_SWITCH_CTRL = 0x9D;
51static constexpr uint8_t SX1262_SET_DIO3_AS_TCXO_CTRL = 0x97;
52static constexpr uint8_t SX1262_CALIBRATE = 0x89;
53static constexpr uint8_t SX1262_CALIBRATE_IMAGE = 0x98;
54static constexpr uint8_t SX1262_GET_RX_BUFFER_STATUS = 0x13;
55static constexpr uint8_t SX1262_GET_RSSI_INST = 0x15;
56static constexpr uint8_t SX1262_SET_REGULATOR_MODE = 0x96;
57static constexpr uint8_t SX1262_GET_STATUS = 0xC0;
58
59// SPI register access opcodes
60static constexpr uint8_t SX1262_WRITE_REGISTER = 0x0D;
61static constexpr uint8_t SX1262_READ_REGISTER = 0x1D;
62
63// ============================================================================
64// SX1262 IRQ Bit Masks
65// ============================================================================
66
67static constexpr uint16_t SX1262_IRQ_TX_DONE = 0x0001;
68static constexpr uint16_t SX1262_IRQ_RX_DONE = 0x0002;
69static constexpr uint16_t SX1262_IRQ_PREAMBLE_DETECTED = 0x0004;
70static constexpr uint16_t SX1262_IRQ_SYNC_WORD_VALID = 0x0008;
71static constexpr uint16_t SX1262_IRQ_CRC_ERR = 0x0040;
72
73/// IRQ bits that represent a *terminal* radio event — a frame finished decoding, an outbound
74/// frame completed, or the chip reported a bad CRC. Deliberately excludes PREAMBLE_DETECTED even
75/// though `configure_radio_()` now includes it in `irqMask`: a preamble alone means a frame may
76/// still be arriving, so SoftPhyDriverBase's poll_until_activity_()/check_for_packet() (gated by
77/// @ref RadioSX1262::activity_irq_mask) must not treat it as terminal — doing so would tear down
78/// RX mid-reception. Same trap, same fix shape as @ref LR1121_IRQ_ACTIVITY_MASK.
79static constexpr uint16_t SX1262_IRQ_ACTIVITY_MASK =
81
82// ============================================================================
83// SX1262 GetDeviceErrors bit masks
84// ============================================================================
85// The word returned by GetDeviceErrors (SX1262 datasheet §13.3.7, cross-checked against RadioLib's
86// RADIOLIB_SX126X_*_ERR). XOSC_START_ERR and IMG_CALIB_ERR are expected at a cold start with a
87// TCXO fitted and are simply cleared before calibration; the others are genuine faults.
88static constexpr uint16_t SX1262_DEV_ERR_RC64K_CALIB = 0x0001; ///< 64 kHz RC oscillator calibration failed.
89static constexpr uint16_t SX1262_DEV_ERR_RC13M_CALIB = 0x0002; ///< 13 MHz RC oscillator calibration failed.
90static constexpr uint16_t SX1262_DEV_ERR_PLL_CALIB = 0x0004; ///< PLL calibration failed.
91static constexpr uint16_t SX1262_DEV_ERR_ADC_CALIB = 0x0008; ///< ADC calibration failed.
92static constexpr uint16_t SX1262_DEV_ERR_IMG_CALIB = 0x0010; ///< Image calibration failed.
93static constexpr uint16_t SX1262_DEV_ERR_XOSC_START = 0x0020; ///< Crystal/TCXO failed to start.
94static constexpr uint16_t SX1262_DEV_ERR_PLL_LOCK = 0x0040; ///< PLL failed to lock.
95static constexpr uint16_t SX1262_DEV_ERR_PA_RAMP = 0x0100; ///< PA ramping failed (bit 0x0080 is unused).
96
97/// Receive-side gain of a FEM profile's LNA, in the SX1262's RSSI scale.
98struct FemRxGain {
99 int8_t db; ///< Gain (dB) the RSSI includes and the driver removes; 0 when unknown or absent.
100 const char *basis; ///< Where the figure comes from and how far to trust it, for the config dump.
101};
102
103/// @brief The receive gain to remove from RSSI for a FEM profile.
104/// @param profile Any profile; `FemProfile::NONE` has no front end and yields 0.
106
107/// Antenna-port power estimate for a configured `tx_power` through a FEM profile's chain.
109 uint8_t tx_power; ///< The `tx_power` actually programmed: the configured value clamped to SetTxParams' range.
110 int antenna_dbm; ///< Estimated antenna-port power in dBm — a rough bound, not a calibrated figure.
111};
112
113/// @brief Estimate what a FEM profile radiates for a configured `tx_power`.
114/// @param profile A real FEM profile (not `FemProfile::NONE`, which has no gain table).
115/// @param tx_power Configured power setting; clamped like the value written to SetTxParams.
116///
117/// The gain tables behind it carry an uncertainty of about ±5–7 dB for the Heltec profiles; see
118/// docs/adr/0035-fem-support-is-a-behaviour-profile-boards-always-supply-pins.md.
119FemTxEstimate sx1262_fem_tx_estimate(FemProfile profile, uint8_t tx_power);
120
121/// @brief Expand a GetDeviceErrors word into a human-readable `NAME|NAME|...` string.
122/// @param errors Raw device-error bitmask from GetDeviceErrors.
123/// @param buf Caller-owned output buffer; always NUL-terminated on return.
124/// @param buf_size Size of @p buf. Use @ref DEVICE_ERROR_STR_SIZE.
125///
126/// Writes `"none"` when @p errors is zero, and appends `UNKNOWN_0x%04X` for any set bit with no
127/// name in the table so an undocumented flag still shows up in the log.
128void sx1262_format_device_errors(uint16_t errors, char *buf, size_t buf_size);
129
130/// @brief Name of the chip mode in a GetStatus byte (bits 6:4).
131/// @param chip_status The byte GetStatus returned.
132/// @return `STDBY_RC`, `STDBY_XOSC`, `FS`, `RX`, `TX`, or `?` for a reserved or unused value.
133const char *sx1262_chip_mode_name(uint8_t chip_status);
134
135/// @brief Command status field of a GetStatus byte (bits 3:1).
136/// @param chip_status The byte GetStatus returned.
137/// @return The 3-bit field (2 = data available, 3 = command timeout, 4 = processing error,
138/// 5 = execution failure, 6 = TX done).
139uint8_t sx1262_command_status(uint8_t chip_status);
140
141// Sync word register base address
142static constexpr uint16_t SX1262_REG_SYNC_WORD = 0x06C0;
143static constexpr uint16_t SX1262_REG_RX_GAIN = 0x08AC;
144static constexpr uint16_t SX1262_REG_TX_CLAMP_CONFIG = 0x08D8;
145
146/// TX modulation-quality erratum register (SX1262 datasheet §15.1, "Modulation Quality with
147/// 500 kHz LoRa Bandwidth" — the title names LoRa, but the workaround table covers every
148/// modulation). Bit 2 must be cleared *only* for LoRa at BW 500 kHz and set to 1 for everything
149/// else, explicitly including any (G)FSK configuration — which is all this driver ever uses.
150/// Semtech's own driver and RadioLib apply it as `TxModulation` / `fixModulationQuality()`.
151/// Leaving it at its reset value degrades transmitted modulation quality, which on this protocol
152/// shows up as a peer that intermittently fails to decode an otherwise strong frame.
153/// Counterpart to the already-applied TxClamp erratum (@ref SX1262_REG_TX_CLAMP_CONFIG).
154static constexpr uint16_t SX1262_REG_TX_MODULATION = 0x0889;
155// Code span, not \ref: doxygen 1.18 can't resolve \ref to this in a whole-project build
156// (details in proto_sizes.h). Autolinking still links it.
157/// Bit 2 of `SX1262_REG_TX_MODULATION` — the (G)FSK-correct value is 1.
158static constexpr uint8_t SX1262_TX_MODULATION_GFSK_BIT = 0x04;
159
160/// Data-buffer split programmed by configure_buffer_base(): TX packets build from 0x00, RX
161/// packets land at 0x80. The RX base doubles as the read offset for a length-driven receive —
162/// a single in-flight packet always starts exactly there.
163static constexpr uint8_t SX1262_TX_BUFFER_BASE = 0x00;
164static constexpr uint8_t SX1262_RX_BUFFER_BASE = 0x80;
165
166static constexpr uint8_t SX1262_GFSK_PACKET_TYPE_KNOWN_LENGTH = 0x00;
167static constexpr uint8_t SX1262_GFSK_CRC_OFF = 0x01;
168/// Preamble detector length selector: 16 bits (0x04 = 8, 0x05 = 16, 0x06 = 24, 0x07 = 32 bits).
169/// The 8-bit detector fired on noise 3-4 times per second of listening on a Heltec V3, and each
170/// false detection held a rotating listen on an empty channel for 25-35 ms; at 16 bits those were
171/// gone and every answer was still caught. A real preamble runs 14 ms or more (500+ bits), so 16
172/// bits costs it nothing. 24 bits stopped all sync detection on the same bench (cause not known),
173/// so do not raise this further without measuring. The LR1121 driver uses the same 16-bit value.
174static constexpr uint8_t SX1262_PREAMBLE_DETECTOR_16_BIT = 0x05;
175static constexpr uint8_t SX1262_FALLBACK_STDBY_XOSC = 0x30;
176
177/// Ceiling on how long SoftPhyDriverBase::wait_busy_() waits for BUSY to drop before declaring the
178/// chip failed. A healthy chip never waits for it, and the wait is judged on a fresh sample, so a
179/// task preemption cannot trip it; this is headroom for the RC-oscillator-clocked commands (unlike
180/// LR1121's post-reset boot ROM, see LR1121_BUSY_TIMEOUT_MS in radio_lr1121.h). It must stay below
181/// the smallest TCXO bring-up rung budget (30 ms) so every rung still widens it.
182static constexpr uint32_t SX1262_BUSY_TIMEOUT_MS = 25;
183
184// ============================================================================
185// SX1262 Radio Driver
186// ============================================================================
187
188/// @brief SX1262 implementation of RadioDriver.
189/// @ingroup hioc_radio
190///
191/// Manages the SX1262 via opcode‑based SPI using the SpiAccess interface.
192/// Configures the chip in FSK mode with software CRC‑CCITT to match the
193/// IO‑Homecontrol protocol (the SX1262 lacks the SX1276's IoHomeOn mode).
194/// The IRQ-driven RX/TX orchestration is inherited from SoftPhyDriverBase; this class supplies
195/// the SPI transport and every SX1262-specific register/opcode encoding underneath it.
197 public:
198 RadioSX1262(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio1_pin, InternalGPIOPin *busy_pin,
199 uint8_t tx_power, uint8_t tcxo_voltage, InternalGPIOPin *fem_en_pin = nullptr,
200 InternalGPIOPin *vfem_pin = nullptr, InternalGPIOPin *fem_pa_pin = nullptr,
201 FemProfile fem_profile = FemProfile::NONE)
202 : SoftPhyDriverBase(rst_pin, busy_pin, SX1262_BUSY_TIMEOUT_MS, SX1262_RESPONSE_PREAMBLE,
203 SX1262_POST_TX_SETTLE_US),
204 spi_(spi),
205 dio1_pin_(dio1_pin),
206 fem_en_pin_(fem_en_pin),
207 vfem_pin_(vfem_pin),
208 fem_pa_pin_(fem_pa_pin),
209 fem_profile_(fem_profile),
210 tx_power_(tx_power),
211 tcxo_voltage_(tcxo_voltage) {}
212
213 /// @copydoc RadioDriver::init
214 bool init() override;
215 /// @brief Apply SX1262 runtime tuning: RX bandwidth, response preamble, post-TX settle delay.
221 /// @brief Per-channel dwell for a rotating listen (SX1262).
222 ///
223 /// See `SX1262_DISCOVERY_HOP_SLICE_MS` for why a short dwell is correct here despite SX1262's
224 /// slower per-channel retune than the SX1276's FastHop. Governs discovery and the broadcast
225 /// roll-call alike (see @ref RadioDriver::hop_dwell_ms). The value comes from the user-facing
226 /// `sx1262_discovery_hop_slice_ms` tuning field.
227 [[nodiscard]] uint16_t hop_dwell_ms(const TuningConfig &tuning) const override {
228 return tuning.sx1262_discovery_hop_slice_ms;
229 }
230 /// @brief TX→RX turnaround capability (SX1262): slow.
231 ///
232 /// The TX→standby→RX transition plus the post-TX settle delay is too slow to
233 /// catch a device's immediate reply through the standard exchange wait; in
234 /// pairing, the key-confirm (0x33) is missed that way. PairingEngine therefore
235 /// uses its dedicated key-confirm wait with key-init re-trigger on this driver.
236 [[nodiscard]] bool has_fast_tx_rx_turnaround() const override { return false; }
237 /// @copydoc RadioDriver::set_mode_rx
238 void set_mode_rx() override;
239 /// @copydoc RadioDriver::set_mode_standby
240 void set_mode_standby() override;
241 [[nodiscard]] const char *chip_name() const override { return "sx1262"; }
242 /// @brief Dump the FEM profile and its antenna-power estimate (nothing without a FEM).
243 void dump_front_end() override;
244 /// @brief Dump SX1262‑specific debug info.
245 void dump_debug() override;
246
247 protected:
248 // --- Tuning helper unique to this chip (its RX-bandwidth enum is not shared, see
249 // tuning_config.h's LR1121RxBandwidth comment for why the two chips' encodings diverged) ---
250 /// Apply the RX bandwidth selector and rewrite the modulation parameters.
251 /// @param bandwidth Bandwidth selector enum.
252 void set_rx_bandwidth_(SX1262RxBandwidth bandwidth);
253
254 // --- SPI communication (opcode‑based) ---
255 /// Write an opcode with optional parameter bytes.
256 /// @param opcode SX1262 opcode.
257 /// @param params Pointer to parameter buffer (may be nullptr).
258 /// @param len Parameter length.
259 void write_opcode_(uint8_t opcode, const uint8_t *params, uint8_t len);
260 /// Read response from an opcode.
261 /// @param opcode Opcode that was previously written.
262 /// @param data Output buffer.
263 /// @param len Expected number of bytes to read.
264 void read_opcode_(uint8_t opcode, uint8_t *data, uint8_t len);
265 /// Write to a register (SX1262 uses opcodes for register access).
266 /// @param addr 16‑bit register address.
267 /// @param data Pointer to data bytes.
268 /// @param len Number of bytes.
269 void write_register_(uint16_t addr, const uint8_t *data, uint8_t len);
270 /// Read from a register.
271 /// @param addr 16‑bit register address.
272 /// @param data Output buffer.
273 /// @param len Number of bytes to read.
274 void read_register_(uint16_t addr, uint8_t *data, uint8_t len);
275 /// Write into the SX1262 TX/RX buffer at a given offset.
276 /// @param offset Buffer offset.
277 /// @param data Payload bytes.
278 /// @param len Payload length.
279 void write_buffer_(uint8_t offset, const uint8_t *data, uint8_t len);
280 /// Read from the SX1262 RX buffer.
281 /// @param offset Buffer offset.
282 /// @param data Output buffer.
283 /// @param len Number of bytes to read.
284 void read_buffer_(uint8_t offset, uint8_t *data, uint8_t len);
285
286 // --- Radio configuration ---
287 /// Full radio initialization (called from init()).
288 void configure_radio_();
289 /// @brief Bring up the DIO3-controlled TCXO and calibrate, with a bounded XOSC-start retry.
290 ///
291 /// Runs up to three attempts on a strictly increasing startup-delay ladder (5/10/50 ms). Each
292 /// attempt issues SetDIO3AsTCXOCtrl, clears the device-error latch (XOSC_START_ERR /
293 /// IMG_CALIB_ERR are expected at a cold TCXO start), calibrates, then re-reads the errors and
294 /// stops as soon as XOSC_START_ERR is clear. Records the attempt count and last delay in
295 /// `tcxo_startup_attempts_` / `tcxo_startup_delay_us_`.
296 ///
297 /// wait_busy_()'s budget is widened to each rung's startup window (plus a calibration margin)
298 /// for the duration, and — if the ladder had to escalate — a ceiling wide enough for the
299 /// successful rung is kept for the rest of the session, because `configure_radio_()` step 4
300 /// re-enters STDBY_XOSC and re-pays that same startup window. Skipped entirely when
301 /// `tcxo_voltage: none` selects a bare crystal.
302 void configure_tcxo_();
303 /// @copydoc SoftPhyDriverBase::set_frequency_register
304 void set_frequency_register(uint32_t freq_hz) override;
305 /// Configure packet parameters (preamble, payload length, CRC).
306 /// @param preamble_len Preamble length in symbols.
307 /// @param payload_len Expected payload length.
308 /// @param packet_type Fixed for GFSK.
309 /// @param crc_type CRC configuration (off or on).
310 void set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type, uint8_t crc_type);
311 /// Apply the runtime bandwidth setting to the SX1262 modulation parameters.
313 /// Apply the Semtech TX modulation-quality erratum workaround (datasheet §15.1). The datasheet
314 /// requires it "before any packet transmission", so it hangs off @ref before_tx_arm rather than
315 /// running once at init — same placement rationale as RadioLR1121's high-ACP workaround.
317 /// @copydoc SoftPhyDriverBase::set_rx_packet_params
318 void set_rx_packet_params() override;
319 /// @copydoc SoftPhyDriverBase::set_tx_packet_params
320 void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) override {
322 }
323 /// @copydoc SoftPhyDriverBase::clear_irq_status
324 void clear_irq_status(uint32_t irq_mask) override;
325 /// @brief Read device error flags (and clear them).
326 /// @return Error bitmask.
327 uint16_t get_device_errors_();
328 /// @brief Read the GetStatus byte (chip mode and last command status).
329 uint8_t read_chip_status_();
330 /// @brief Log GetStatus, whether the IRQ word holds TX-done, the device errors and the DIO1/BUSY
331 /// pin levels after a TX that never reported TX-done.
332 void log_tx_timeout_state(uint32_t irq_status) override;
333 /// @brief Clear device error flags.
335 /// @copydoc SoftPhyDriverBase::front_end_rx_gain_db
336 ///
337 /// The LNA gain of the configured FEM profile; see sx1262_fem_rx_gain().
338 [[nodiscard]] int8_t front_end_rx_gain_db() const override { return sx1262_fem_rx_gain(this->fem_profile_).db; }
339 /// @copydoc SoftPhyDriverBase::configure_buffer_base
340 ///
341 /// SX1262's buffer base addresses (TX=0x00, RX=0x80) are re-asserted on every RX reset — LR1121
342 /// has no equivalent register.
343 void configure_buffer_base() override;
344 /// @copydoc SoftPhyDriverBase::early_rx_read_offset
345 ///
346 /// The SX1262 writes demodulated bytes into its data buffer as they arrive, and a single
347 /// in-flight packet always starts at the RX base address programmed by configure_buffer_base(),
348 /// so the shared flow can read the frame out on its own air time rather than waiting the fixed
349 /// ~10 ms for the 48-byte RX_DONE.
350 [[nodiscard]] int16_t early_rx_read_offset() const override { return SX1262_RX_BUFFER_BASE; }
351 /// @copydoc SoftPhyDriverBase::fill_capture_info
352 ///
353 /// Note: `crc_error` is set from the CrcErr IRQ bit — unlike the SX1276, this
354 /// driver sees and reports frames with a bad CRC.
355 void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
356 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) override;
357
358 /// DIO1 ISR — sets dio_fired flag. Runs in interrupt context.
359 static void gpio_intr(RadioSX1262 *arg);
360
361 /// @copydoc SoftPhyDriverBase::read_irq_status_raw
362 uint32_t read_irq_status_raw() override;
363 /// @copydoc SoftPhyDriverBase::sync_word_valid_bit
364 [[nodiscard]] uint32_t sync_word_valid_bit() const override { return SX1262_IRQ_SYNC_WORD_VALID; }
365 /// @copydoc SoftPhyDriverBase::rx_done_bit
366 [[nodiscard]] uint32_t rx_done_bit() const override { return SX1262_IRQ_RX_DONE; }
367 /// @copydoc SoftPhyDriverBase::tx_done_bit
368 [[nodiscard]] uint32_t tx_done_bit() const override { return SX1262_IRQ_TX_DONE; }
369 /// @copydoc SoftPhyDriverBase::preamble_detected_bit
370 [[nodiscard]] uint32_t preamble_detected_bit() const override { return SX1262_IRQ_PREAMBLE_DETECTED; }
371 /// @copydoc SoftPhyDriverBase::activity_irq_mask
372 ///
373 /// The base class default ("any bit") stopped being safe the moment `configure_radio_()`
374 /// unmasked PreambleDetected — see @ref SX1262_IRQ_ACTIVITY_MASK's doc comment.
375 [[nodiscard]] uint32_t activity_irq_mask() const override { return SX1262_IRQ_ACTIVITY_MASK; }
376 /// @copydoc SoftPhyDriverBase::read_rssi_raw_byte
377 uint8_t read_rssi_raw_byte() override;
378 /// @copydoc SoftPhyDriverBase::write_tx_buffer
379 void write_tx_buffer(const uint8_t *data, uint8_t len) override {
380 this->write_buffer_(SX1262_TX_BUFFER_BASE, data, len);
381 }
382 /// @copydoc SoftPhyDriverBase::get_rx_buffer_status
383 void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) override;
384 /// @copydoc SoftPhyDriverBase::read_rx_buffer
385 void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) override { this->read_buffer_(offset, data, len); }
386 /// @copydoc SoftPhyDriverBase::start_tx
387 void start_tx() override;
388 /// @copydoc SoftPhyDriverBase::before_tx_arm
389 ///
390 /// TX modulation-quality erratum (`SX1262_REG_TX_MODULATION`): the datasheet requires bit 2
391 /// be set for every (G)FSK transmission, so it is re-asserted before each SetTx rather than
392 /// assumed to survive from init. Also drives the FEM mode pin to its TX-active level — see
393 /// @ref fem_set_tx_mode_.
394 void before_tx_arm() override {
396 this->fem_set_tx_mode_();
397 }
398
399 /// @brief The level `fem_pa_pin_` is driven to for the duration of a transmission, for the
400 /// current `fem_profile_`. `true` (HIGH) for GC1109/KCT8103L, whose shared truth table treats the
401 /// mode pin as CPS/CTX -- active HIGH selects the TX/full-PA path. `false` (LOW) for XY16P35,
402 /// whose "LNA Ctrl" pin is the receive-side enable: HIGH turns the LNA *on* (receive), LOW turns
403 /// it off (transmit or idle) -- the inverse sense, per LilyGO's own datasheet note (ADR 0035).
404 /// Never called for FemProfile::NONE (both callers guard on that first).
405 [[nodiscard]] bool fem_tx_active_level_() const;
406 /// @brief Drive the FEM mode pin to its transmit state (full PA), a no-op when no FEM profile
407 /// is configured.
408 ///
409 /// "Mode pin" is fem_pa_pin_ -- CPS on GC1109 (V4.2), CTX on KCT8103L (V4.3), the LNA-control pin
410 /// on XY16P35 (T-Beam 1W) -- see the shared truth table in
411 /// docs/adr/0035-fem-support-is-a-behaviour-profile-boards-always-supply-pins.md. Called from
412 /// @ref before_tx_arm, right before SetTx.
413 void fem_set_tx_mode_();
414 /// @brief Drive the FEM mode pin to its receive state (LNA), a no-op when no FEM profile is
415 /// configured.
416 ///
417 /// Called from both @ref set_mode_rx and @ref set_mode_standby (not just the former) because a
418 /// TX timeout in SoftPhyDriverBase::send_packet() calls set_mode_standby() directly and never
419 /// reaches set_mode_rx() — without this hook there too, a timed-out TX would leave the mode pin
420 /// stranded in its TX-active state indefinitely (nothing else would ever drop it back).
421 void fem_set_rx_mode_();
422
423 private:
424 SpiAccess *spi_;
425 InternalGPIOPin *dio1_pin_;
426 InternalGPIOPin *fem_en_pin_;
427 InternalGPIOPin *vfem_pin_;
428 InternalGPIOPin *fem_pa_pin_;
429 FemProfile fem_profile_;
430 uint8_t tx_power_;
431 uint8_t tcxo_voltage_; ///< 0-based chip voltage code, or @ref TCXO_VOLTAGE_NONE for a bare crystal.
432 uint8_t tcxo_startup_attempts_{0}; ///< SetDIO3AsTCXOCtrl attempts the last bring-up took (0 = skipped/none).
433 uint32_t tcxo_startup_delay_us_{0}; ///< Startup delay of the last rung tried, in µs — the rung that started the
434 ///< TCXO on success, or the one reached before giving up.
435 SX1262RxBandwidth rx_bandwidth_{SX1262RxBandwidth::BW_58_6_KHZ}; ///< Runtime-tunable RX bandwidth.
436};
437
438} // namespace home_io_control
439} // namespace esphome
SX1262 implementation of RadioDriver.
void set_rx_packet_params() override
Configure RX-specific packet parameters (preamble detector length, fixed probe length).
void before_tx_arm() override
Hook run immediately before every SetTx.
void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) override
Read len bytes from the RX buffer starting at offset.
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 apply_tuning(const TuningConfig &tuning) override
Apply SX1262 runtime tuning: RX bandwidth, response preamble, post-TX settle delay.
bool has_fast_tx_rx_turnaround() const override
TX→RX turnaround capability (SX1262): slow.
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.
const char * chip_name() const override
Get a human‑readable chip name.
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.
void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) override
Configure TX packet parameters for one outgoing UART-encoded frame.
uint32_t tx_done_bit() const override
void write_tx_buffer(const uint8_t *data, uint8_t len) override
Write the UART-encoded TX payload into the chip's TX buffer.
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.
uint32_t rx_done_bit() const override
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.
uint32_t sync_word_valid_bit() const override
uint16_t hop_dwell_ms(const TuningConfig &tuning) const override
Per-channel dwell for a rotating listen (SX1262).
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.
int8_t front_end_rx_gain_db() const override
Gain (dB) of the board's external receive front end that the chip's RSSI includes.
bool init() override
Initialize the radio hardware. Returns true on success.
uint8_t read_chip_status_()
Read the GetStatus byte (chip mode and last command status).
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 log_tx_timeout_state(uint32_t irq_status) override
Log GetStatus, whether the IRQ word holds TX-done, the device errors and the DIO1/BUSY pin levels aft...
uint32_t activity_irq_mask() const override
IRQ bits that count as "activity" for the internal poll_until_activity_() helper and check_for_packet...
int16_t early_rx_read_offset() const override
Data-buffer offset an in-flight reception is being written to, or a negative value when this chip mus...
void read_register_(uint16_t addr, uint8_t *data, uint8_t len)
Read from a register.
uint32_t preamble_detected_bit() const override
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).
SoftPhyDriverBase(InternalGPIOPin *rst_pin, InternalGPIOPin *busy_pin, uint32_t busy_timeout_ms, uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
void set_post_tx_settle_us_(uint16_t delay_us)
Set the delay between TX completion and re-entering RX.
void set_response_preamble_(uint16_t preamble)
Set the preamble length used for response/continuation frames within an exchange.
Interface for SPI bus access.
static constexpr uint8_t SX1262_SET_DIO3_AS_TCXO_CTRL
static constexpr uint8_t SX1262_CALIBRATE
static constexpr uint16_t SX1262_DEV_ERR_PLL_CALIB
PLL calibration failed.
static constexpr uint16_t SX1262_REG_RX_GAIN
static constexpr uint8_t SX1262_SET_BUFFER_BASE_ADDRESS
static constexpr uint16_t SX1262_DEV_ERR_PLL_LOCK
PLL failed to lock.
static constexpr uint8_t SX1262_CLEAR_DEVICE_ERRORS
static constexpr uint8_t SX1262_GFSK_CRC_OFF
SX1262RxBandwidth
Valid SX1262 RX bandwidth options (kHz register values).
@ BW_58_6_KHZ
58.6 kHz — default; the narrowest value validated on real hardware here.
static constexpr uint8_t SX1262_READ_REGISTER
static constexpr uint8_t SX1262_SET_PACKET_TYPE
static constexpr uint16_t SX1262_DEV_ERR_IMG_CALIB
Image calibration failed.
static constexpr uint16_t SX1262_IRQ_SYNC_WORD_VALID
static constexpr uint8_t SX1262_GET_DEVICE_ERRORS
const char * sx1262_chip_mode_name(uint8_t chip_status)
Name of the chip mode in a GetStatus byte (bits 6:4).
FemProfile
Which RF front-end module (if any) sits between the SX1262 and the antenna.
@ NONE
No FEM, or a board with the FEM pins wired but no behaviour profile set – see the NONE-specific note ...
static constexpr uint16_t SX1262_DEV_ERR_RC13M_CALIB
13 MHz RC oscillator calibration failed.
static constexpr uint16_t SX1262_IRQ_CRC_ERR
static constexpr uint8_t SX1262_SET_MODULATION_PARAMS
static constexpr uint8_t SX1262_GET_RX_BUFFER_STATUS
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.
static constexpr uint16_t SX1262_REG_TX_MODULATION
TX modulation-quality erratum register (SX1262 datasheet §15.1, "Modulation Quality with500 kHz LoRa ...
static constexpr uint8_t SX1262_CLEAR_IRQ_STATUS
static constexpr uint8_t SX1262_SET_RX
static constexpr uint8_t SX1262_SET_TX
static constexpr uint8_t SX1262_GFSK_PACKET_TYPE_KNOWN_LENGTH
static constexpr uint32_t SX1262_BUSY_TIMEOUT_MS
Ceiling on how long SoftPhyDriverBase::wait_busy_() waits for BUSY to drop before declaring the chip ...
uint8_t sx1262_command_status(uint8_t chip_status)
Command status field of a GetStatus byte (bits 3:1).
static constexpr uint8_t SX1262_CALIBRATE_IMAGE
static constexpr uint8_t SX1262_SET_DIO2_AS_RF_SWITCH_CTRL
static constexpr uint8_t SX1262_WRITE_BUFFER
static constexpr uint16_t SX1262_DEV_ERR_PA_RAMP
PA ramping failed (bit 0x0080 is unused).
static constexpr uint8_t SX1262_READ_BUFFER
static constexpr uint16_t SX1262_IRQ_RX_DONE
static constexpr uint8_t SX1262_SET_PA_CONFIG
static constexpr uint8_t SX1262_SET_TX_PARAMS
static constexpr uint16_t SX1262_IRQ_TX_DONE
static constexpr uint16_t SX1262_DEV_ERR_XOSC_START
Crystal/TCXO failed to start.
static constexpr uint8_t SX1262_SET_DIO_IRQ_PARAMS
static constexpr uint16_t SX1262_DEV_ERR_ADC_CALIB
ADC calibration failed.
static constexpr uint8_t SX1262_PREAMBLE_DETECTOR_16_BIT
Preamble detector length selector: 16 bits (0x04 = 8, 0x05 = 16, 0x06 = 24, 0x07 = 32 bits).
static constexpr uint8_t SX1262_SET_PACKET_PARAMS
static constexpr uint8_t SX1262_TX_MODULATION_GFSK_BIT
Bit 2 of SX1262_REG_TX_MODULATION — the (G)FSK-correct value is 1.
static constexpr uint8_t SX1262_GET_STATUS
static constexpr uint16_t SX1262_REG_TX_CLAMP_CONFIG
static constexpr uint16_t SX1262_IRQ_PREAMBLE_DETECTED
static constexpr uint8_t SX1262_SET_RX_TX_FALLBACK_MODE
static constexpr uint8_t SX1262_TX_BUFFER_BASE
Data-buffer split programmed by configure_buffer_base(): TX packets build from 0x00,...
static constexpr uint16_t SX1262_DEV_ERR_RC64K_CALIB
64 kHz RC oscillator calibration failed.
static constexpr uint8_t SX1262_RX_BUFFER_BASE
static constexpr uint16_t SX1262_IRQ_ACTIVITY_MASK
IRQ bits that represent a terminal radio event — a frame finished decoding, an outbound frame complet...
static constexpr uint8_t SX1262_GET_IRQ_STATUS
static constexpr uint8_t SX1262_SET_REGULATOR_MODE
static constexpr uint8_t SX1262_GET_PACKET_STATUS
FemTxEstimate sx1262_fem_tx_estimate(FemProfile profile, uint8_t tx_power)
Estimate what a FEM profile radiates for a configured tx_power.
static constexpr uint16_t SX1262_REG_SYNC_WORD
static constexpr uint8_t SX1262_FALLBACK_STDBY_XOSC
FemRxGain sx1262_fem_rx_gain(FemProfile profile)
The receive gain to remove from RSSI for a FEM profile.
static constexpr uint8_t SX1262_SET_RF_FREQUENCY
static constexpr uint8_t SX1262_SET_STANDBY
static constexpr uint8_t SX1262_WRITE_REGISTER
static constexpr uint8_t SX1262_GET_RSSI_INST
Shared driver flow for radios using the software PHY (SX1262, LR1121).
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.
All runtime tunable parameters for pairing and radio diagnostics.
SX1262RxBandwidth sx1262_rx_bandwidth
SX1262 RX bandwidth selector.
uint16_t sx1262_post_tx_settle_us
Delay after SX1262 TX before RX (µs).
uint16_t sx1262_response_preamble
SX1262 response preamble in bytes.
uint16_t sx1262_discovery_hop_slice_ms
Per-channel dwell while SX1262 discovery hops.