Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_sx1276.cpp
Go to the documentation of this file.
1/// @file radio_sx1276.cpp
2/// @brief SX1276 radio driver implementation for IO-Homecontrol.
3/// @ingroup hioc_radio
4
5// This file is mostly chip-register programming and packed bit masks. Naming every literal that
6// mirrors the datasheet hurts readability more than it helps, so the magic-number checks are
7// suppressed at file scope here.
8// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
9
10#include "radio_sx1276.h"
11#include "log_frame.h"
12#include "esphome/core/log.h"
13#include "esphome/core/application.h"
14
15#include <cinttypes>
16
17namespace esphome {
18namespace home_io_control {
19
20static const char *const TAG = "home_io_control.sx1276";
21
22// === SPI register access ===
23
24uint8_t RadioSX1276::read_register_(uint8_t reg) {
25 this->spi_->spi_enable();
26 this->spi_->spi_write(reg & 0x7F);
27 uint8_t const value = this->spi_->spi_read();
28 this->spi_->spi_disable();
29 return value;
30}
31
32void RadioSX1276::write_register_(uint8_t reg, uint8_t value) {
33 this->spi_->spi_enable();
34 this->spi_->spi_write(reg | 0x80);
35 this->spi_->spi_write(value);
36 this->spi_->spi_disable();
37}
38
39// === Radio mode control ===
40
41void RadioSX1276::set_mode_(uint8_t mode) {
42 uint8_t const current = this->read_register_(REG_OP_MODE);
43 this->write_register_(REG_OP_MODE, (current & ~MODE_MASK) | (mode & MODE_MASK));
44 uint32_t const start = millis();
45 while (true) {
46 uint8_t const cur_mode = this->read_register_(REG_OP_MODE) & MODE_MASK;
47 if (cur_mode == mode || (mode == MODE_RX && cur_mode == 0x04))
48 break;
49 if (millis() - start > 50) {
50 ESP_LOGE(TAG, "Radio mode change timeout");
51 this->failed_ = true;
52 return;
53 }
54 App.feed_wdt();
55 }
56}
57
60
62 this->set_mode_(MODE_STDBY);
63 this->write_register_(REG_IMAGE_CAL, 0x40);
64 uint32_t const start = millis();
65 while ((this->read_register_(REG_IMAGE_CAL) & 0x20) != 0) {
66 if (millis() - start > 20) {
67 ESP_LOGE(TAG, "Image calibration timeout");
68 this->failed_ = true;
69 return;
70 }
71 App.feed_wdt();
72 }
73}
74
76
77void RadioSX1276::fill_capture_info_(bool blocking_wait, uint8_t irq1, uint8_t irq2, uint8_t rssi, const uint8_t *raw,
78 uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
79 this->populate_capture_base_(blocking_wait, this->current_freq_, -(int16_t) rssi / 2, raw, raw_len, frame, frame_len);
80 this->last_capture_.rx_done = (irq2 & 0x04) != 0;
81 this->last_capture_.irq_flags1 = irq1;
82 this->last_capture_.irq_flags2 = irq2;
83 // IRQ_FLAGS2 bit 1 = PayloadCrcError. In IoHomeOn mode the chip filters
84 // bad-CRC frames in hardware before raising RxDone, so this bit is never
85 // set when a packet is delivered. Always false on SX1276.
86 this->last_capture_.crc_error = false;
87 this->last_capture_.reported_len = raw_len;
88}
89
90void RadioSX1276::change_frequency(uint32_t freq_hz) {
91 uint64_t const frf = ((uint64_t) freq_hz << 19) / FXOSC;
92 this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
93 this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
94 this->write_register_(REG_FRF_LSB, (uint8_t) (frf & 0xFF));
95 this->current_freq_ = freq_hz;
96}
97
98int16_t RadioSX1276::read_rssi() { return -(int16_t) this->read_register_(REG_RSSI_VALUE) / 2; }
99
101 return (this->read_register_(REG_IRQ_FLAGS1) & 0x01) != 0; // Bit 0 = SyncAddressMatch
102}
103
105 return (this->read_register_(REG_IRQ_FLAGS1) & 0x02) != 0; // Bit 1 = PreambleDetect
106}
107
108// === Initialization ===
109
111 this->rst_pin_->setup();
112 this->dio0_pin_->setup();
113 // Attach DIO0 interrupt
114 this->dio0_pin_->attach_interrupt(&RadioSX1276::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
115 if (this->dio4_pin_ != nullptr)
116 this->dio4_pin_->setup();
117
118 // Hardware reset
119 this->reset_hardware_();
120
121 // Version check — SX1276 should return 0x12
122 if (this->read_register_(REG_VERSION) != 0x12) {
123 ESP_LOGE(TAG, "SX1276 not found");
124 this->failed_ = true;
125 return false;
126 }
127
128 this->configure_radio_();
129 if (this->failed_)
130 return false;
131
132 ESP_LOGI(TAG, "SX1276 initialized");
133 return true;
134}
135
137 // This routine programs the SX1276 into FSK mode with the IoHomeOn feature enabled.
138 // IoHomeOn is critical: it enables hardware CRC (CCITT) and enforces the exact
139 // frame structure expected by the IO‑Homecontrol protocol. Without it, the radio
140 // would not recognize frames or compute correct CRCs. Every register value below
141 // has been validated against working Somfy/Velux captures and Semtech's recommended
142 // configuration for IoHomeOn. Do not change unless you have on‑air captures to
143 // prove compatibility.
144 this->write_register_(REG_OP_MODE, 0x00); // FSK + Sleep
145 delay(10);
146
147 // Frequency: channel 2 (868.95 MHz)
148 uint64_t const frf = ((uint64_t) FREQ_CH2 << 19) / FXOSC;
149 this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
150 this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
151 this->write_register_(REG_FRF_LSB, (uint8_t) (frf & 0xFF));
152
153 this->set_mode_(MODE_STDBY);
154 this->run_image_cal_();
155 if (this->failed_)
156 return;
157 this->set_mode_(MODE_STDBY);
158
159 this->write_register_(REG_OSC, 0x07); // Clock out off
161 0x90); // Variable len, CRC on, CCITT
162 // IoHomeOn is the crucial difference from a generic FSK setup: Semtech's SX1276 can
163 // speak the protocol natively enough to handle CRC and frame boundaries for us. This
164 // path serves as the baseline against which SX1262 captures are compared.
165 this->write_register_(REG_PACKET_CONFIG2, 0x70); // Packet mode, IoHomeOn, PowerFrame
166 this->write_register_(REG_SYNC_CONFIG, 0x50); // Auto restart PLL off, AA, sync on
167 this->write_register_(REG_DIO_MAPPING1, 0x3D); // DIO0: PayloadReady/PacketSent, DIO2: SyncAddress (for gated hop)
168 this->write_register_(REG_DIO_MAPPING2, 0xF1); // DIO4: PreambleDetect
169 this->write_register_(REG_PLLHOP, this->read_register_(REG_PLLHOP) | 0x80); // Fast hop
170 this->write_register_(REG_PA_RAMP, 0x2D); // Gaussian BT=1.0 shaping (0x20) | 15μs ramp (0x0D)
171 this->write_register_(REG_FIFO_THRESH, 0x80); // TX start FIFO not empty
172 this->write_register_(REG_PAYLOAD_LENGTH, 0xFF); // Max payload
173 this->write_register_(REG_RSSI_CONFIG, 0x02); // RSSI smoothing 8
174 this->write_register_(REG_RX_CONFIG, 0x9E); // Restart collision, AFC, AGC, preamble
175 this->write_register_(REG_AFC_FEI, 0x01); // AFC auto clear
176 this->write_register_(REG_LNA, 0x23); // Max gain, boost on
177 this->write_register_(REG_PREAMBLE_DETECT, 0xAA); // Detect on, 2 bytes, tol 10
178 // RX/AFC bandwidth: sourced from the runtime-tunable rx_bandwidth_ (default BW_41_7_KHZ = 0x13).
179 // Carson-rule bandwidth for 38400 bps + 19200 Hz deviation is ~77 kHz; the default is tighter
180 // than theoretical but maximizes sensitivity by rejecting out-of-band noise. Validated against
181 // real devices. See SX1276RxBandwidth for the selectable options.
182 this->set_rx_bandwidth_(this->rx_bandwidth_);
183
184 // Bitrate 38400 bps
185 uint32_t const br = FXOSC / 38400;
186 this->write_register_(REG_BITRATE_MSB, (br >> 8) & 0xFF);
187 this->write_register_(REG_BITRATE_LSB, br & 0xFF);
188
189 // Deviation 19200 Hz
190 auto fd = (uint32_t) ((19200.0F / FXOSC) * (1 << 19));
191 this->write_register_(REG_FDEV_MSB, (fd >> 8) & 0xFF);
192 this->write_register_(REG_FDEV_LSB, fd & 0xFF);
193
194 // PA config
195 if (this->pa_pin_ == 0x80) {
196 uint8_t p = std::max(this->tx_power_, (uint8_t) 2);
197 p = std::min(p, (uint8_t) 17);
198 this->write_register_(REG_PA_CONFIG, 0x80 | (p - 2));
199 } else {
200 this->write_register_(REG_PA_CONFIG, std::min(this->tx_power_, (uint8_t) 14));
201 }
202 this->write_register_(0x0B, 0x3B); // OCP on, 240mA
203
204 // Preamble 1024 bytes default (changed per-packet)
207
208 // Sync word: 0x55, 0xFF, 0x33 in registers → on‑air `55 FF 33`. This is the real-Deal confirmed-working sync for
209 // IO‑Homecontrol with the SX1276. SyncSize=2 (REG_SYNC_CONFIG bits 2:0 = 0x2) matches the first 2 bytes {0x55, 0xFF}.
210 // The 3rd byte 0x33 is transmitted on‑air but not compared by the chip in SyncSize=2 mode.
211 uint8_t const sc = this->read_register_(REG_SYNC_CONFIG);
212 this->write_register_(REG_SYNC_CONFIG, (sc & 0xF8) | 0x02); // SyncSize=2: 2 matched bytes `55 FF`; 3rd `33` unused
213 this->write_register_(REG_SYNC_VALUE1, 0x55);
214 this->write_register_(REG_SYNC_VALUE1 + 1, 0xFF);
215 this->write_register_(REG_SYNC_VALUE1 + 2, 0x33);
216
217 this->set_mode_rx();
218}
219
221 this->rx_bandwidth_ = bandwidth;
222 // The enum value is the RegRxBw register byte; AFC bandwidth uses the same encoding.
223 this->write_register_(REG_RX_BW, static_cast<uint8_t>(bandwidth));
224 this->write_register_(REG_AFC_BW, static_cast<uint8_t>(bandwidth));
225}
226
227// === Packet TX/RX ===
228
229bool RadioSX1276::send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) {
230 if (len == 0 || len > RADIO_PACKET_BUFFER_SIZE)
231 return false;
232#ifdef IOHOME_FRAME_LOG
233 log_frame("TX", data, len, tx_config.freq_hz, tx_config.preamble_len);
234#endif
235 this->set_mode_standby();
236 this->write_register_(REG_PREAMBLE_MSB, tx_config.preamble_len >> 8);
237 this->write_register_(REG_PREAMBLE_LSB, tx_config.preamble_len & 0xFF);
238 this->change_frequency(tx_config.freq_hz);
239
240 // Write data to FIFO
241 this->spi_->spi_enable();
242 this->spi_->spi_write(REG_FIFO | 0x80);
243 for (uint8_t i = 0; i < len; i++)
244 this->spi_->spi_transfer(data[i]);
245 this->spi_->spi_disable();
246
247 this->clear_dio_fired();
248 uint8_t const opmode = this->read_register_(REG_OP_MODE);
249 this->write_register_(REG_OP_MODE, (opmode & 0xF8) | MODE_TX);
250
251 uint32_t const start = millis();
252 while (!this->is_dio_fired()) {
253 if (millis() - start > 4000) {
254 ESP_LOGE(TAG, "TX timeout");
255 this->set_mode_standby();
256 return false;
257 }
258 App.feed_wdt();
259 delayMicroseconds(100);
260 }
261
262 // Reset preamble to short for RX
265 this->set_mode_rx();
266 return true;
267}
268
269bool RadioSX1276::poll_until_payload_ready_(uint32_t timeout_ms, bool &saw_dio0, uint8_t &irq1, uint8_t &irq2) {
270 uint32_t const start = millis();
271 while (true) {
272 if (this->is_dio_fired()) {
273 saw_dio0 = true;
274 break;
275 }
276 irq1 = this->read_register_(REG_IRQ_FLAGS1);
277 irq2 = this->read_register_(REG_IRQ_FLAGS2);
278 if ((irq2 & 0x04) != 0)
279 break;
280 if (millis() - start > timeout_ms)
281 return false;
282 App.feed_wdt();
283 delay(1);
284 }
285 this->clear_dio_fired();
286 if (saw_dio0) {
287 irq1 = this->read_register_(REG_IRQ_FLAGS1);
288 irq2 = this->read_register_(REG_IRQ_FLAGS2);
289 }
290 return true;
291}
292
293uint8_t RadioSX1276::read_fifo_packet_(uint8_t *buf, uint8_t buf_size) {
294 uint8_t len = 0;
295 while (((this->read_register_(REG_IRQ_FLAGS2) & 0x40) == 0) && len < buf_size)
296 buf[len++] = this->read_register_(REG_FIFO);
297 return len;
298}
299
300bool RadioSX1276::wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) {
301 this->prepare_blocking_receive_(packet);
302 this->clear_dio_fired();
303
304 bool saw_dio0 = false;
305 uint8_t irq1 = 0;
306 uint8_t irq2 = 0;
307 if (!this->poll_until_payload_ready_(timeout_ms, saw_dio0, irq1, irq2))
308 return false;
309
310 uint8_t const rssi = this->read_register_(REG_RSSI_VALUE);
311 if ((irq2 & 0x04) == 0) {
312 this->fill_capture_info_(true, irq1, irq2, rssi, nullptr, 0, nullptr, 0);
313 return false;
314 }
315
316 packet.len = this->read_fifo_packet_(packet.data, sizeof(packet.data));
317 packet.freq_hz = this->current_freq_;
318 this->fill_capture_info_(true, irq1, irq2, rssi, packet.data, packet.len, packet.data, packet.len);
319#ifdef IOHOME_FRAME_LOG
320 if (packet.len > 0)
321 log_frame("RX", packet.data, packet.len, this->current_freq_);
322#endif
323 return packet.len > 0;
324}
325
327 if (!this->is_dio_fired())
328 return false;
329 this->prepare_nonblocking_receive_(packet);
330
331 uint8_t const irq1 = this->read_register_(REG_IRQ_FLAGS1);
332 uint8_t const irq2 = this->read_register_(REG_IRQ_FLAGS2);
333 uint8_t const rssi = this->read_register_(REG_RSSI_VALUE);
334 if ((irq2 & 0x04) != 0) {
335 packet.len = this->read_fifo_packet_(packet.data, sizeof(packet.data));
336 packet.freq_hz = this->current_freq_;
337 this->fill_capture_info_(false, irq1, irq2, rssi, packet.data, packet.len, packet.data, packet.len);
338#ifdef IOHOME_FRAME_LOG
339 if (packet.len > 0)
340 log_frame("RX", packet.data, packet.len, this->current_freq_);
341#endif
342 return packet.len > 0;
343 }
344 this->fill_capture_info_(false, irq1, irq2, rssi, nullptr, 0, nullptr, 0);
345 if ((irq2 & 0x10) != 0) { // FifoOverrun — clear it
346 this->write_register_(REG_IRQ_FLAGS2, 0x10);
347 }
348 return false;
349}
350
352 ESP_LOGCONFIG(TAG, " SX1276 Diagnostic:");
353 ESP_LOGCONFIG(TAG, " Opmode=0x%02X irq1=0x%02X irq2=0x%02X freq=%" PRIu32, this->read_register_(REG_OP_MODE),
355}
356
357} // namespace home_io_control
358} // namespace esphome
359
360// 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 is_dio_fired() const
Set by the ISR when DIO fires.
void prepare_nonblocking_receive_(RadioRxPacket &packet)
Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
void prepare_blocking_receive_(RadioRxPacket &packet)
Common preamble for blocking receive: clear diagnostics and output packet.
void configure_radio_()
Perform full radio configuration (called during init).
int16_t read_rssi() override
Read instantaneous RSSI (dBm) while in RX mode (used for LBT).
bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override
Blocking wait for a packet with timeout.
uint8_t read_register_(uint8_t reg)
Read an SX1276 register over SPI.
SX1276RxBandwidth rx_bandwidth_
Runtime-tunable RX bandwidth.
void fill_capture_info_(bool blocking_wait, uint8_t irq1, uint8_t irq2, uint8_t rssi, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len)
Populate last_capture_ from raw telemetry.
void set_rx_bandwidth_(SX1276RxBandwidth bandwidth)
Apply the RX bandwidth selector to REG_RX_BW and REG_AFC_BW.
void set_mode_(uint8_t mode)
Set the operating mode (sleep/standby/tx/rx) and wait for mode completion.
RadioSX1276(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio0_pin, InternalGPIOPin *dio4_pin, uint8_t tx_power, uint8_t pa_pin)
void set_mode_standby() override
Switch radio into standby mode.
static void gpio_intr(RadioSX1276 *arg)
DIO0 ISR — sets dio_fired flag. Runs in interrupt context.
bool poll_until_payload_ready_(uint32_t timeout_ms, bool &saw_dio0, uint8_t &irq1, uint8_t &irq2)
Wait until FIFO payload is ready (polling for TX/RX readiness).
void dump_debug() override
Dump radio‑specific debug info to log.
void change_frequency(uint32_t freq_hz) override
Change RF frequency using fast hop (no standby needed).
bool init() override
Initialize the SX1276 radio (reset, calibrate, configure registers).
void write_register_(uint8_t reg, uint8_t value)
Write an SX1276 register over SPI.
void set_mode_rx() override
Switch radio into continuous receive mode.
bool is_sync_detected() override
Check if sync word has been detected (while in RX).
uint8_t read_fifo_packet_(uint8_t *buf, uint8_t buf_size)
Read a packet from the RX FIFO into a buffer.
bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override
Transmit a frame with specified frequency and preamble.
bool check_for_packet(RadioRxPacket &packet) override
Non‑blocking check for a received packet (called from loop).
void run_image_cal_()
Run image calibration routine (required after reset).
bool is_preamble_detected() override
Check if preamble has been detected (while in RX).
Shared frame logging helpers for IO-Homecontrol.
static constexpr uint8_t REG_RX_CONFIG
Receiver configuration (AFC, AGC, trigger).
static constexpr uint8_t REG_IRQ_FLAGS1
IRQ flags: mode ready, preamble detect, etc.
static constexpr uint8_t REG_FRF_MSB
Carrier frequency MSB (freq = FRF * FXOSC / 2^19).
static constexpr uint8_t REG_PACKET_CONFIG2
Packet mode, IoHomeOn, PowerFrame.
static constexpr uint8_t REG_PA_CONFIG
Power amplifier config (pin select + power level).
static constexpr uint8_t REG_IRQ_FLAGS2
IRQ flags: FIFO full/empty, payload ready, CRC ok.
static constexpr const char * TAG
static constexpr uint8_t MODE_RX
static constexpr uint8_t REG_DIO_MAPPING1
DIO0-DIO3 pin mapping.
static constexpr uint8_t REG_FDEV_LSB
static constexpr uint8_t REG_DIO_MAPPING2
DIO4-DIO5 pin mapping.
static constexpr uint8_t REG_FIFO_THRESH
FIFO threshold for TX start condition.
static constexpr uint8_t REG_FIFO
FIFO read/write access.
static constexpr uint8_t REG_OP_MODE
Operating mode (sleep/standby/tx/rx).
static constexpr uint8_t REG_FDEV_MSB
Frequency deviation MSB.
static constexpr uint8_t MODE_TX
static constexpr uint8_t REG_PA_RAMP
PA ramp time and modulation shaping.
static constexpr uint8_t MODE_STDBY
static constexpr uint8_t REG_PAYLOAD_LENGTH
Max payload length.
static constexpr uint8_t REG_AFC_BW
AFC bandwidth.
static constexpr uint8_t REG_RX_BW
Receiver bandwidth.
static constexpr uint8_t REG_PACKET_CONFIG1
Packet format, CRC, encoding.
static constexpr uint8_t REG_BITRATE_MSB
Bit rate MSB = FXOSC / bitrate.
static constexpr uint8_t REG_SYNC_CONFIG
Sync word config (size, polarity, enable).
static constexpr uint8_t REG_AFC_FEI
AFC auto clear.
static constexpr uint8_t REG_PREAMBLE_LSB
static constexpr uint8_t REG_PLLHOP
PLL hop: fast frequency change without standby.
static constexpr uint8_t REG_LNA
Low noise amplifier gain and boost.
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
static constexpr uint8_t MODE_MASK
static constexpr uint32_t FXOSC
SX1276 crystal oscillator frequency (32 MHz).
static constexpr uint8_t REG_IMAGE_CAL
Image calibration.
static constexpr uint8_t REG_RSSI_CONFIG
RSSI smoothing.
static constexpr uint8_t REG_OSC
Oscillator / clock output.
static constexpr uint8_t REG_VERSION
Chip version (should read 0x12 for SX1276).
SX1276RxBandwidth
Valid SX1276 RX bandwidth options (RegRxBw register bytes).
static constexpr uint8_t REG_PREAMBLE_DETECT
Preamble detector config.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
static constexpr uint8_t REG_SYNC_VALUE1
Sync word byte 1 (registers 0x28-0x2F for bytes 1-8).
static constexpr uint8_t REG_RSSI_VALUE
Instant RSSI value in FSK mode.
static constexpr uint8_t REG_FRF_MID
static constexpr uint8_t REG_PREAMBLE_MSB
TX preamble length MSB.
static constexpr uint8_t REG_BITRATE_LSB
static constexpr uint8_t REG_FRF_LSB
SX1276 radio driver for IO-Homecontrol.
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint32_t freq_hz
Frequency the packet was received on (Hz).
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.
Configuration for transmitting a packet: carrier frequency and preamble length.
uint16_t preamble_len
Preamble length in symbol periods (bytes).
uint32_t freq_hz
Carrier frequency in Hz.