Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_soft_phy_driver_base.h
Go to the documentation of this file.
1#pragma once
2
3/// @file radio_soft_phy_driver_base.h
4/// @brief Shared driver flow for radios using the software PHY (SX1262, LR1121).
5/// @ingroup hioc_radio
6///
7/// RadioSX1262 and RadioLR1121 both lack the SX1276's IoHomeOn hardware framing, so both
8/// reproduce IO-Homecontrol framing in software on top of generic GFSK support
9/// (`radio_soft_phy.h`'s UART bit-encode/probe). Beyond that shared bit-level codec, the two
10/// drivers' IRQ-driven RX state machine and TX orchestration are identical in every detail that
11/// isn't chip-specific transport or register encoding, so this class holds that shared flow once
12/// instead of each driver maintaining its own copy.
13///
14/// This class holds everything the two drivers do identically: `wait_for_packet()`/
15/// `check_for_packet()`'s IRQ polling and sync/RX-done race resolution, `read_rx_packet()`'s
16/// buffer-read and UART-probe recovery, `send_packet()`'s TX orchestration, `read_rssi()`'s
17/// formula, and the response-preamble/post-TX-settle tuning fields. What genuinely differs
18/// between the two chips — SPI opcode encoding/transport, IRQ bit values and word width,
19/// register-level packet/modulation parameter encoding, and the handful of one-off steps one
20/// chip needs that the other doesn't (SX1262's buffer-base-address write, LR1121's high-ACP
21/// pre-TX workaround and preamble-tolerant activity check) — stays behind virtual primitives and
22/// hooks implemented by `RadioSX1262`/`RadioLR1121`.
23
24#include "radio_interface.h"
25#include "radio_soft_phy.h"
26
27#include <cstdint>
28
29namespace esphome {
30namespace home_io_control {
31
32/// Fixed raw-RX probe length: chosen from captures of 23-25 byte protocol frames after UART
33/// packing and CRC appending — the longest frame (25 bytes + 2 CRC) UART-packs to 34 raw bytes,
34/// so 48 bytes preserves complete traffic (with margin for leading noise before the frame start)
35/// without relying on either chip's variable-length engine. This is a protocol-frame-size
36/// property, not a chip quirk, so both drivers share one value — used here for the raw-probe
37/// threshold in @ref SoftPhyDriverBase::read_rx_packet and by each driver's own
38/// `set_rx_packet_params()` for the configured RX payload length.
39static constexpr uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN = 48;
40
41/// Sentinel meaning "every IRQ bit counts as activity" — the default for @ref
42/// SoftPhyDriverBase::activity_irq_mask, correct for chips (SX1262) whose hardware-level IRQ
43/// mask already excludes the one bit (PreambleDetected) that would need special handling.
44static constexpr uint32_t SOFT_PHY_ALL_IRQ_BITS = 0xFFFFFFFF;
45
46/// Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART
47/// cell (10 bits) at any of the probe's bit alignments (up to 9 bits of slack).
48static constexpr uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES = 3;
49
50/// Air-time margin added before every mid-reception buffer read, in raw bytes.
51///
52/// Covers the lag between a byte finishing on air and the chip having it in its data buffer, plus
53/// the granularity of the polled sync-word observation. Two byte-times is generous at this line
54/// rate and still leaves a length-driven receive far ahead of the fixed-length RX_DONE.
55static constexpr uint8_t SOFT_PHY_EARLY_READ_MARGIN_BYTES = 2;
56
57/// Poll interval while waiting out a frame's remaining air time, in microseconds.
58static constexpr uint32_t SOFT_PHY_EARLY_POLL_US = 100;
59
60/// Smallest receive window a length-driven receive will be attempted in, in milliseconds.
61///
62/// The longest possible frame (FRAME_MAX_SIZE + CRC, UART-packed) occupies ~9.4 ms of air time, so
63/// a caller with less than this left cannot finish one either way. Declining up front keeps the
64/// early path from spending a short window's whole budget on a receive it cannot complete.
65static constexpr uint32_t SOFT_PHY_EARLY_MIN_WINDOW_MS = 12;
66
67/// Protocol line rate. The same 38400 bps every driver programs into its own bitrate register.
68static constexpr uint32_t SOFT_PHY_LINE_RATE_BPS = 38400;
69/// Microseconds in a second, for the air-time arithmetic below.
70static constexpr uint32_t SOFT_PHY_US_PER_SECOND = 1000000;
71
72/// @brief On-air time in microseconds for `raw_bytes` bytes at the protocol's line rate.
73///
74/// One byte is 8 / 38400 s = 208.333 µs. Computed as an integer division rounded *up*, so the
75/// result never falls short of a whole byte's air time and a caller that waits on it never reads
76/// the chip's buffer early. The numerator peaks around 360 million for the longest frame this is
77/// ever asked about, well inside uint32_t.
78constexpr uint32_t soft_phy_air_time_us(uint32_t raw_bytes) {
79 const uint32_t bit_periods = raw_bytes * BITS_PER_BYTE * SOFT_PHY_US_PER_SECOND;
80 return (bit_periods + SOFT_PHY_LINE_RATE_BPS - 1) / SOFT_PHY_LINE_RATE_BPS;
81}
82
83/// @brief Shared RX/TX driver flow for the software-PHY radios (SX1262, LR1121).
84/// @ingroup hioc_radio
86 public:
87 /// @param rst_pin Active-low hardware reset pin, forwarded to RadioDriver.
88 /// @param default_response_preamble Chip-specific default for @ref response_preamble (each
89 /// concrete driver passes its own validated constant — this class has no opinion on the
90 /// value, only on where it's stored).
91 /// @param default_post_tx_settle_us Chip-specific default post-TX settling delay, same rationale.
92 SoftPhyDriverBase(InternalGPIOPin *rst_pin, uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
93 : RadioDriver(rst_pin),
94 response_preamble_(default_response_preamble),
95 post_tx_settle_us_(default_post_tx_settle_us) {}
96
97 /// @copydoc RadioDriver::send_packet
98 bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override;
99 /// @copydoc RadioDriver::wait_for_packet
100 bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override;
101 /// @copydoc RadioDriver::check_for_packet
102 bool check_for_packet(RadioRxPacket &packet) override;
103 /// @copydoc RadioDriver::change_frequency
104 void change_frequency(uint32_t freq_hz) override;
105 /// @copydoc RadioDriver::read_rssi
106 ///
107 /// Same formula on both chips (`-(int16_t) raw / 2`); only the opcode used to read the single
108 /// raw byte differs, via @ref read_rssi_raw_byte.
109 int16_t read_rssi() override;
110 /// @copydoc RadioDriver::is_sync_detected
111 bool is_sync_detected() override;
112 /// @copydoc RadioDriver::is_preamble_detected
113 bool is_preamble_detected() override;
114 /// @brief Preamble for response/continuation frames — shared storage, see the concrete
115 /// drivers' constructors/tuning defaults for the chip-specific rationale and value.
116 [[nodiscard]] uint16_t response_preamble() const override { return this->response_preamble_; }
117
118 protected:
119 // --- Tuning helpers shared by both drivers (values/defaults stay chip-specific) ---
120 /// Set the preamble length used for response/continuation frames within an exchange.
121 void set_response_preamble_(uint16_t preamble) { this->response_preamble_ = preamble; }
122 /// Set the delay between TX completion and re-entering RX.
123 void set_post_tx_settle_us_(uint16_t delay_us) { this->post_tx_settle_us_ = delay_us; }
124
125 // --- Shared RX/TX orchestration (moved verbatim from RadioSX1262/RadioLR1121) ---
126 /// Read a received packet from the buffer and return the raw bytes reported by the chip.
127 /// Virtual to allow test doubles (both concrete drivers' tests override this).
128 virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status);
129 /// Reset RX state machine and buffer. Optionally force standby first.
130 void reset_rx_state_(bool force_standby = true);
131
132 /// @brief Read the raw IRQ status word from the radio.
133 /// Virtual to allow test doubles (both concrete drivers' tests override this).
134 virtual uint32_t read_irq_status_raw() = 0;
135 /// Clear IRQ status bits.
136 /// @param irq_mask Bitmask of IRQs to clear (each driver narrows to its own IRQ word width).
137 virtual void clear_irq_status(uint32_t irq_mask) = 0;
138
139 /// @name Chip-specific IRQ bit values
140 /// Each driver's own IRQ bit constants, exposed as accessors so the shared RX/TX orchestration
141 /// never needs to name a chip-specific constant directly.
142 ///@{
143 [[nodiscard]] virtual uint32_t sync_word_valid_bit() const = 0;
144 [[nodiscard]] virtual uint32_t rx_done_bit() const = 0;
145 [[nodiscard]] virtual uint32_t tx_done_bit() const = 0;
146 [[nodiscard]] virtual uint32_t preamble_detected_bit() const = 0;
147 ///@}
148
149 /// @brief IRQ bits that count as "activity" for the internal `poll_until_activity_()` helper
150 /// and @ref check_for_packet.
151 ///
152 /// Default is "any bit" — correct for SX1262, whose `SetDioIrqParams` mask already excludes
153 /// `PreambleDetected` system-wide, so a preamble-only reading can never reach this check in the
154 /// first place. LR1121 routes `PreambleDetected` to its IRQ pin for other reasons and overrides
155 /// this to exclude it: a preamble-only reading means a frame may still be arriving, and treating
156 /// it as terminal activity would tear down RX mid-reception.
157 [[nodiscard]] virtual uint32_t activity_irq_mask() const { return SOFT_PHY_ALL_IRQ_BITS; }
158
159 /// @brief Data-buffer offset an in-flight reception is being written to, or a negative value
160 /// when this chip must not be read before RX_DONE.
161 ///
162 /// Neither chip's RX_DONE marks the end of the *frame*: with no hardware framing, RX runs in
163 /// fixed-length mode at @ref SOFT_PHY_RX_PROBE_PACKET_LEN, so RX_DONE arrives a fixed ~10 ms
164 /// after the sync word no matter how short the frame actually was. That delay lands squarely on
165 /// the protocol's tightest turnaround — the hub's reply to a device's challenge — so a driver
166 /// that can read its buffer while reception is still running opts in here and the shared flow
167 /// finishes on the frame's own air time instead (see `try_early_completion_`).
168 ///
169 /// Default is -1: wait for RX_DONE exactly as before. SX1262 overrides it with the RX base
170 /// address it programs in configure_buffer_base(), which is where a single in-flight packet
171 /// always starts. LR1121 is left on the RX_DONE path pending hardware validation.
172 [[nodiscard]] virtual int16_t early_rx_read_offset() const { return -1; }
173
174 /// Set RF frequency via the chip's own frequency register/opcode encoding, and update
175 /// `current_freq_`. Called from both @ref change_frequency and the shared `send_packet()`.
176 virtual void set_frequency_register(uint32_t freq_hz) = 0;
177 /// Configure RX-specific packet parameters (preamble detector length, fixed probe length).
178 virtual void set_rx_packet_params() = 0;
179 /// Configure TX packet parameters for one outgoing UART-encoded frame.
180 /// @param preamble_len Preamble length in symbols, from the caller's RadioTxConfig.
181 /// @param payload_len UART-encoded payload length in bytes.
182 virtual void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) = 0;
183 /// Read the single raw RSSI byte (chip-specific opcode); formula is shared, see @ref read_rssi.
184 virtual uint8_t read_rssi_raw_byte() = 0;
185 /// Write the UART-encoded TX payload into the chip's TX buffer.
186 virtual void write_tx_buffer(const uint8_t *data, uint8_t len) = 0;
187 /// Read the chip-reported RX length and buffer offset (raw, before any clamping).
188 virtual void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) = 0;
189 /// Read `len` bytes from the RX buffer starting at `offset`.
190 virtual void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) = 0;
191 /// Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,
192 /// differing only in opcode/transport, so this stays a thin chip-specific wrapper.
193 virtual void start_tx() = 0;
194 /// Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,
195 /// and IRQ-word-width narrowing all differ per chip).
196 virtual void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
197 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) = 0;
198
199 /// @brief Hook run immediately before every `SetTx`. No-op by default; LR1121 overrides this to
200 /// apply its high-ACP TX-quality workaround, which Semtech's own reference applies unconditionally
201 /// before every SetRx/SetTx.
202 virtual void before_tx_arm() {}
203 /// @brief Hook run as part of @ref reset_rx_state_, before re-entering RX. No-op by default;
204 /// SX1262 overrides this to (re-)write its buffer base address, which LR1121 doesn't need.
205 virtual void configure_buffer_base() {}
206
207 private:
208 // === wait_for_packet/check_for_packet state-machine helpers (private) ===
209 /// Poll for first *terminal* radio activity (IRQ pin or an IRQ status bit within
210 /// @ref activity_irq_mask) within timeout.
211 bool poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq);
212 /// Resolve the SYNC_WORD_VALID → RX_DONE race condition common to both chips, and — on a chip
213 /// that opts into @ref early_rx_read_offset — give the length-driven receive its chance first.
214 /// @param early_completed Set when a whole CRC-valid frame was recovered without waiting for
215 /// RX_DONE; `packet` is then already populated and the caller is done.
216 bool resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq, RadioRxPacket &packet,
217 bool &early_completed);
218 /// Finish a reception on the frame's own air time rather than on the chip's fixed-length
219 /// RX_DONE. Returns true only when a CRC-valid frame was recovered.
220 bool try_early_completion_(RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status, uint32_t start_ms,
221 uint32_t timeout_ms);
222 /// Finalize receive: read the packet if RX_DONE is set, otherwise record failure.
223 bool finalize_receive_(RadioRxPacket &packet, uint32_t irq);
224
225 uint16_t response_preamble_;
226 uint16_t post_tx_settle_us_;
227};
228
229} // namespace home_io_control
230} // namespace esphome
RadioDriver(InternalGPIOPin *rst_pin=nullptr)
virtual uint32_t read_irq_status_raw()=0
Read the raw IRQ status word from the radio.
virtual void set_frequency_register(uint32_t freq_hz)=0
Set RF frequency via the chip's own frequency register/opcode encoding, and update current_freq_.
virtual void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset)=0
Read the chip-reported RX length and buffer offset (raw, before any clamping).
SoftPhyDriverBase(InternalGPIOPin *rst_pin, uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
bool is_preamble_detected() override
Check if preamble has been detected (while in RX).
virtual void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len)=0
Read len bytes from the RX buffer starting at offset.
virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status)
Read a received packet from the buffer and return the raw bytes reported by the chip.
virtual uint32_t rx_done_bit() const =0
bool is_sync_detected() override
Check if sync word has been detected (while in RX).
virtual uint32_t preamble_detected_bit() const =0
virtual void configure_buffer_base()
Hook run as part of reset_rx_state_, before re-entering RX.
virtual void set_rx_packet_params()=0
Configure RX-specific packet parameters (preamble detector length, fixed probe length).
virtual uint32_t sync_word_valid_bit() const =0
virtual 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)=0
Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,...
virtual void clear_irq_status(uint32_t irq_mask)=0
Clear IRQ status bits.
void set_post_tx_settle_us_(uint16_t delay_us)
Set the delay between TX completion and re-entering RX.
virtual void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len)=0
Configure TX packet parameters for one outgoing UART-encoded frame.
void reset_rx_state_(bool force_standby=true)
Reset RX state machine and buffer. Optionally force standby first.
virtual uint32_t tx_done_bit() const =0
virtual uint8_t read_rssi_raw_byte()=0
Read the single raw RSSI byte (chip-specific opcode); formula is shared, see read_rssi.
int16_t read_rssi() override
Read instantaneous RSSI (in dBm) while in RX mode.
virtual void before_tx_arm()
Hook run immediately before every SetTx.
uint16_t response_preamble() const override
Preamble for response/continuation frames — shared storage, see the concrete drivers' constructors/tu...
virtual void write_tx_buffer(const uint8_t *data, uint8_t len)=0
Write the UART-encoded TX payload into the chip's TX buffer.
void set_response_preamble_(uint16_t preamble)
Set the preamble length used for response/continuation frames within an exchange.
virtual int16_t early_rx_read_offset() const
Data-buffer offset an in-flight reception is being written to, or a negative value when this chip mus...
virtual uint32_t activity_irq_mask() const
IRQ bits that count as "activity" for the internal poll_until_activity_() helper and check_for_packet...
bool check_for_packet(RadioRxPacket &packet) override
Non-blocking check for a received packet.
bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override
Wait (blocking) for a packet with timeout.
bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override
Send a packet using the specified carrier frequency and preamble settings.
virtual void start_tx()=0
Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,...
void change_frequency(uint32_t freq_hz) override
Change the carrier frequency using fast hop (no standby transition needed).
static constexpr uint8_t BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_sizes.h:27
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 ...
constexpr uint32_t soft_phy_air_time_us(uint32_t raw_bytes)
On-air time in microseconds for raw_bytes bytes at the protocol's line rate.
static constexpr uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES
Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART cell (10 b...
static constexpr uint32_t SOFT_PHY_US_PER_SECOND
Microseconds in a second, for the air-time arithmetic below.
static constexpr uint32_t SOFT_PHY_EARLY_MIN_WINDOW_MS
Smallest receive window a length-driven receive will be attempted in, in milliseconds.
static constexpr uint32_t SOFT_PHY_LINE_RATE_BPS
Protocol line rate. The same 38400 bps every driver programs into its own bitrate register.
static constexpr uint8_t SOFT_PHY_EARLY_READ_MARGIN_BYTES
Air-time margin added before every mid-reception buffer read, in raw bytes.
static constexpr uint32_t SOFT_PHY_EARLY_POLL_US
Poll interval while waiting out a frame's remaining air time, in microseconds.
static constexpr uint32_t SOFT_PHY_ALL_IRQ_BITS
Sentinel meaning "every IRQ bit counts as activity" — the default for SoftPhyDriverBase::activity_irq...
Radio abstraction layer for IO-Homecontrol.
Software PHY for radios without IoHomeOn hardware framing.
Raw packet received from the radio.
Configuration for transmitting a packet: carrier frequency and preamble length.