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