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
4// This file is mostly chip-register programming and packed bit masks. Naming every literal that
5// mirrors the datasheet hurts readability more than it helps, so the magic-number checks are
6// suppressed at file scope here.
7// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
8
9#include "radio_sx1276.h"
10#include "log_frame.h"
11#include "esphome/core/log.h"
12#include "esphome/core/application.h"
13
14namespace esphome {
15namespace home_io_control {
16
17static const char *const TAG = "home_io_control.sx1276";
18
19// === SPI register access ===
20
21uint8_t RadioSX1276::read_register_(uint8_t reg) {
22 this->spi_->spi_enable();
23 this->spi_->spi_write(reg & 0x7F);
24 uint8_t const value = this->spi_->spi_read();
25 this->spi_->spi_disable();
26 return value;
27}
28
29void RadioSX1276::write_register_(uint8_t reg, uint8_t value) {
30 this->spi_->spi_enable();
31 this->spi_->spi_write(reg | 0x80);
32 this->spi_->spi_write(value);
33 this->spi_->spi_disable();
34}
35
36// === Radio mode control ===
37
38void RadioSX1276::set_mode_(uint8_t mode) {
39 uint8_t const current = this->read_register_(REG_OP_MODE);
40 this->write_register_(REG_OP_MODE, (current & ~MODE_MASK) | (mode & MODE_MASK));
41 uint32_t const start = millis();
42 while (true) {
43 uint8_t const cur_mode = this->read_register_(REG_OP_MODE) & MODE_MASK;
44 if (cur_mode == mode || (mode == MODE_RX && cur_mode == 0x04))
45 break;
46 if (millis() - start > 50) {
47 ESP_LOGE(TAG, "Radio mode change timeout");
48 this->failed_ = true;
49 return;
50 }
51 }
52}
53
56
58 this->set_mode_(MODE_STDBY);
59 this->write_register_(REG_IMAGE_CAL, 0x40);
60 uint32_t const start = millis();
61 while ((this->read_register_(REG_IMAGE_CAL) & 0x20) != 0) {
62 if (millis() - start > 20) {
63 ESP_LOGE(TAG, "Image calibration timeout");
64 this->failed_ = true;
65 return;
66 }
67 }
68}
69
71
72void RadioSX1276::fill_capture_info_(bool blocking_wait, uint8_t irq1, uint8_t irq2, uint8_t rssi, const uint8_t *raw,
73 uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
74 this->populate_capture_base_(blocking_wait, this->current_freq_, -(int16_t) (rssi) / 2, raw, raw_len, frame,
75 frame_len);
76 this->last_capture_.rx_done = (irq2 & 0x04) != 0;
77 this->last_capture_.irq_flags1 = irq1;
78 this->last_capture_.irq_flags2 = irq2;
79 // IRQ_FLAGS2 bit 1 = PayloadCrcError. In IoHomeOn mode the chip filters
80 // bad-CRC frames in hardware before raising RxDone, so this bit is never
81 // set when a packet is delivered. Always false on SX1276.
82 this->last_capture_.crc_error = false;
83 this->last_capture_.reported_len = raw_len;
84}
85
86void RadioSX1276::change_frequency(uint32_t freq_hz) {
87 uint64_t const frf = ((uint64_t) freq_hz << 19) / FXOSC;
88 this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
89 this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
90 this->write_register_(REG_FRF_LSB, (uint8_t) (frf & 0xFF));
91 this->current_freq_ = freq_hz;
92}
93
94int16_t RadioSX1276::read_rssi() { return -(int16_t) this->read_register_(REG_RSSI_VALUE) / 2; }
95
96// === Initialization ===
97
99 this->rst_pin_->setup();
100 this->dio0_pin_->setup();
101 // Attach DIO0 interrupt
102 this->dio0_pin_->attach_interrupt(&RadioSX1276::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
103 if (this->dio4_pin_ != nullptr)
104 this->dio4_pin_->setup();
105
106 // Hardware reset
107 this->reset_hardware_();
108
109 // Version check — SX1276 should return 0x12
110 if (this->read_register_(REG_VERSION) != 0x12) {
111 ESP_LOGE(TAG, "SX1276 not found");
112 this->failed_ = true;
113 return false;
114 }
115
116 this->configure_radio_();
117 if (this->failed_)
118 return false;
119
120 ESP_LOGI(TAG, "SX1276 initialized");
121 return true;
122}
123
125 // This routine programs the SX1276 into FSK mode with the IoHomeOn feature enabled.
126 // IoHomeOn is critical: it enables hardware CRC (CCITT) and enforces the exact
127 // frame structure expected by the IO‑Homecontrol protocol. Without it, the radio
128 // would not recognize frames or compute correct CRCs. Every register value below
129 // has been validated against working Somfy/Velux captures and Semtech's recommended
130 // configuration for IoHomeOn. Do not change unless you have on‑air captures to
131 // prove compatibility.
132 this->write_register_(REG_OP_MODE, 0x00); // FSK + Sleep
133 delay(10);
134
135 // Frequency: channel 2 (868.95 MHz)
136 uint64_t const frf = ((uint64_t) FREQ_CH2 << 19) / FXOSC;
137 this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
138 this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
139 this->write_register_(REG_FRF_LSB, (uint8_t) (frf & 0xFF));
140
141 this->set_mode_(MODE_STDBY);
142 this->run_image_cal_();
143 if (this->failed_)
144 return;
145 this->set_mode_(MODE_STDBY);
146
147 this->write_register_(REG_OSC, 0x07); // Clock out off
149 0x90); // Variable len, CRC on, CCITT
150 // IoHomeOn is the crucial difference from a generic FSK setup: Semtech's SX1276 can
151 // speak the protocol natively enough to handle CRC and frame boundaries for us. This
152 // path serves as the baseline against which SX1262 captures are compared.
153 this->write_register_(REG_PACKET_CONFIG2, 0x70); // Packet mode, IoHomeOn, PowerFrame
154 this->write_register_(REG_SYNC_CONFIG, 0x50); // Auto restart PLL off, AA, sync on
155 this->write_register_(REG_DIO_MAPPING1, 0x39); // DIO0: PayloadReady/PacketSent
156 this->write_register_(REG_DIO_MAPPING2, 0xF1); // DIO4: PreambleDetect
157 this->write_register_(REG_PLLHOP, this->read_register_(REG_PLLHOP) | 0x80); // Fast hop
158 this->write_register_(REG_PA_RAMP, 0x0E); // No shaping, 12us
159 this->write_register_(REG_FIFO_THRESH, 0x80); // TX start FIFO not empty
160 this->write_register_(REG_PAYLOAD_LENGTH, 0xFF); // Max payload
161 this->write_register_(REG_RSSI_CONFIG, 0x02); // RSSI smoothing 8
162 this->write_register_(REG_RX_CONFIG, 0x9E); // Restart collision, AFC, AGC, preamble
163 this->write_register_(REG_AFC_FEI, 0x01); // AFC auto clear
164 this->write_register_(REG_LNA, 0x23); // Max gain, boost on
165 this->write_register_(REG_PREAMBLE_DETECT, 0xAA); // Detect on, 2 bytes, tol 10
166 this->write_register_(REG_RX_BW, 0x01); // 250 kHz
167
168 // Bitrate 38400 bps
169 uint32_t const br = FXOSC / 38400;
170 this->write_register_(REG_BITRATE_MSB, (br >> 8) & 0xFF);
171 this->write_register_(REG_BITRATE_LSB, br & 0xFF);
172
173 // Deviation 19200 Hz
174 auto fd = (uint32_t) ((19200.0F / FXOSC) * (1 << 19));
175 this->write_register_(REG_FDEV_MSB, (fd >> 8) & 0xFF);
176 this->write_register_(REG_FDEV_LSB, fd & 0xFF);
177
178 // PA config
179 if (this->pa_pin_ == 0x80) {
180 uint8_t p = std::max(this->tx_power_, (uint8_t) 2);
181 p = std::min(p, (uint8_t) 17);
182 this->write_register_(REG_PA_CONFIG, 0x80 | (p - 2));
183 } else {
184 this->write_register_(REG_PA_CONFIG, std::min(this->tx_power_, (uint8_t) 14));
185 }
186 this->write_register_(0x0B, 0x3B); // OCP on, 240mA
187
188 // Preamble 1024 bytes default (changed per-packet)
191
192 // Sync word: 0x55, 0xFF, 0x33 in registers → on‑air `55 FF 33`. This is the real-Deal confirmed-working sync for
193 // IO‑Homecontrol with the SX1276. SyncSize=2 (REG_SYNC_CONFIG bits 2:0 = 0x2) matches the first 2 bytes {0x55, 0xFF}.
194 // The 3rd byte 0x33 is transmitted on‑air but not compared by the chip in SyncSize=2 mode.
195 uint8_t const sc = this->read_register_(REG_SYNC_CONFIG);
196 this->write_register_(REG_SYNC_CONFIG, (sc & 0xF8) | 0x02); // SyncSize=2: 2 matched bytes `55 FF`; 3rd `33` unused
197 this->write_register_(REG_SYNC_VALUE1, 0x55);
198 this->write_register_(REG_SYNC_VALUE1 + 1, 0xFF);
199 this->write_register_(REG_SYNC_VALUE1 + 2, 0x33);
200
201 this->set_mode_rx();
202}
203
204// === Packet TX/RX ===
205
206bool RadioSX1276::send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) {
207 if (len == 0 || len > RADIO_PACKET_BUFFER_SIZE)
208 return false;
209#ifdef IOHOME_FRAME_LOG
210 log_frame("TX", data, len, tx_config.freq_hz, tx_config.preamble_len);
211#endif
212 this->set_mode_standby();
213 this->write_register_(REG_PREAMBLE_MSB, tx_config.preamble_len >> 8);
214 this->write_register_(REG_PREAMBLE_LSB, tx_config.preamble_len & 0xFF);
215 this->change_frequency(tx_config.freq_hz);
216
217 // Write data to FIFO
218 this->spi_->spi_enable();
219 this->spi_->spi_write(REG_FIFO | 0x80);
220 for (uint8_t i = 0; i < len; i++)
221 this->spi_->spi_transfer(data[i]);
222 this->spi_->spi_disable();
223
224 this->clear_dio_fired();
225 uint8_t const opmode = this->read_register_(REG_OP_MODE);
226 this->write_register_(REG_OP_MODE, (opmode & 0xF8) | MODE_TX);
227
228 uint32_t const start = millis();
229 while (!this->is_dio_fired()) {
230 if (millis() - start > 4000) {
231 ESP_LOGE(TAG, "TX timeout");
232 this->set_mode_standby();
233 return false;
234 }
235 App.feed_wdt();
236 delayMicroseconds(100);
237 }
238
239 // Reset preamble to short for RX
242 this->set_mode_rx();
243 return true;
244}
245
246bool RadioSX1276::poll_until_payload_ready_(uint32_t timeout_ms, bool &saw_dio0, uint8_t &irq1, uint8_t &irq2) {
247 uint32_t const start = millis();
248 while (true) {
249 if (this->is_dio_fired()) {
250 saw_dio0 = true;
251 break;
252 }
253 irq1 = this->read_register_(REG_IRQ_FLAGS1);
254 irq2 = this->read_register_(REG_IRQ_FLAGS2);
255 if ((irq2 & 0x04) != 0)
256 break;
257 if (millis() - start > timeout_ms)
258 return false;
259 App.feed_wdt();
260 delay(1);
261 }
262 this->clear_dio_fired();
263 if (saw_dio0) {
264 irq1 = this->read_register_(REG_IRQ_FLAGS1);
265 irq2 = this->read_register_(REG_IRQ_FLAGS2);
266 }
267 return true;
268}
269
270uint8_t RadioSX1276::read_fifo_packet_(uint8_t *buf, uint8_t buf_size) {
271 uint8_t len = 0;
272 while (((this->read_register_(REG_IRQ_FLAGS2) & 0x40) == 0) && len < buf_size)
273 buf[len++] = this->read_register_(REG_FIFO);
274 return len;
275}
276
277bool RadioSX1276::wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) {
278 this->prepare_blocking_receive_(packet);
279 this->clear_dio_fired();
280
281 bool saw_dio0 = false;
282 uint8_t irq1 = 0;
283 uint8_t irq2 = 0;
284 if (!this->poll_until_payload_ready_(timeout_ms, saw_dio0, irq1, irq2))
285 return false;
286
287 uint8_t const rssi = this->read_register_(REG_RSSI_VALUE);
288 if ((irq2 & 0x04) == 0) {
289 this->fill_capture_info_(true, irq1, irq2, rssi, nullptr, 0, nullptr, 0);
290 return false;
291 }
292
293 packet.len = this->read_fifo_packet_(packet.data, sizeof(packet.data));
294 packet.freq_hz = this->current_freq_;
295 this->fill_capture_info_(true, irq1, irq2, rssi, packet.data, packet.len, packet.data, packet.len);
296#ifdef IOHOME_FRAME_LOG
297 if (packet.len > 0)
298 log_frame("RX", packet.data, packet.len, this->current_freq_);
299#endif
300 return packet.len > 0;
301}
302
304 if (!this->is_dio_fired())
305 return false;
306 this->prepare_nonblocking_receive_(packet);
307
308 uint8_t const irq1 = this->read_register_(REG_IRQ_FLAGS1);
309 uint8_t const irq2 = this->read_register_(REG_IRQ_FLAGS2);
310 uint8_t const rssi = this->read_register_(REG_RSSI_VALUE);
311 if ((irq2 & 0x04) != 0) {
312 packet.len = this->read_fifo_packet_(packet.data, sizeof(packet.data));
313 packet.freq_hz = this->current_freq_;
314 this->fill_capture_info_(false, irq1, irq2, rssi, packet.data, packet.len, packet.data, packet.len);
315#ifdef IOHOME_FRAME_LOG
316 if (packet.len > 0)
317 log_frame("RX", packet.data, packet.len, this->current_freq_);
318#endif
319 return packet.len > 0;
320 }
321 this->fill_capture_info_(false, irq1, irq2, rssi, nullptr, 0, nullptr, 0);
322 if ((irq2 & 0x10) != 0) { // FifoOverrun — clear it
323 this->write_register_(REG_IRQ_FLAGS2, 0x10);
324 }
325 return false;
326}
327
329 ESP_LOGCONFIG(TAG, " SX1276 Diagnostic:");
330 ESP_LOGCONFIG(TAG, " Opmode=0x%02X irq1=0x%02X irq2=0x%02X freq=%u", this->read_register_(REG_OP_MODE),
332}
333
334} // namespace home_io_control
335} // namespace esphome
336
337// 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_()
Hardware reset sequence common to all SX chips.
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.
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_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.
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).
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 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_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).
Definition proto_frame.h:31
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).
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 const char *const TAG
Definition hub_core.cpp:34
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.