Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_lr1121_firmware_updater.cpp
Go to the documentation of this file.
1/// @file radio_lr1121_firmware_updater.cpp
2/// @brief LR1121 bootloader-mode SPI transport implementation.
3/// @ingroup hioc_radio
4
5// See radio_lr1121_firmware_updater.h's comment on include-before-ifdef ordering: this #include
6// must run before the #ifdef check below so IOHOME_LR1121_FIRMWARE_UPDATE (defined via
7// esphome/core/defines.h, pulled in transitively) is visible by the time it's tested.
9
10#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
11
12#include "radio_poll.h"
13#include "esphome/core/application.h"
14
15#include <algorithm>
16
17namespace esphome {
18namespace home_io_control {
19
20namespace {
21
22/// Reset low/high pulse width for bootloader entry — same value as RadioDriver::reset_hardware_()
23/// uses for the ordinary transceiver-mode reset (radio_interface.cpp); this is a different
24/// sequence (BUSY is also driven here) but there is no reason for the pulse itself to differ.
25constexpr uint32_t LR1121_UPDATER_RESET_PULSE_MS = 10;
26/// Wait after the reset pulse while BUSY is still held as an output. Semtech's value, from
27/// lr11xx_reset_to_bootloader() (SWTL001 application/src/lr11xx_firmware_update.c).
28constexpr uint32_t LR1121_UPDATER_BOOTLOADER_ENTRY_WAIT_MS = 500;
29/// Further wait after BUSY is returned to input, before the chip is treated as ready. Also
30/// Semtech's value, from the same function.
31constexpr uint32_t LR1121_UPDATER_POST_ENTRY_SETTLE_MS = 100;
32/// Settle time after Reboot is sent, before reboot() returns control to the caller — see
33/// reboot()'s comment for the BUSY race this closes. Unlike the two entry-sequence waits above,
34/// this one has no counterpart in Semtech's source: their reference tool
35/// (lr11xx_update_post_flash_reboot_and_verification()) issues the post-reboot GetVersion
36/// immediately with no wait of its own for the transceiver-firmware path. It is defensive against
37/// this project's own HAL/BUSY timing, sized as half of LR1121_UPDATER_POST_ENTRY_SETTLE_MS since
38/// Reboot drives the same internal chip reset without the external RST/BUSY strapping that makes
39/// the entry sequence heavier.
40constexpr uint32_t LR1121_UPDATER_POST_REBOOT_SETTLE_MS = 50;
41
42/// RAII guard for the bootloader-entry BUSY-as-output-LOW trick: drives BUSY as a GPIO output LOW
43/// on construction, restores it to its actual configured mode on destruction (including on every
44/// early-return path through enter_bootloader(), since a stuck-as-output BUSY pin would break every
45/// later normal-mode read that depends on it reporting real chip state).
46///
47/// Restoring the *configured* mode, not a hardcoded gpio::FLAG_INPUT, matters because busy_pin
48/// comes from pins.internal_gpio_input_pin_schema, which allows `pullup:`/`pulldown:` — a
49/// hardcoded FLAG_INPUT would silently drop whatever pull the user configured. get_flags() is
50/// safe to read here specifically because pin_mode() never mutates it: on ESP32,
51/// InternalGPIOPin::pin_mode() only programs the IDF GPIO driver (direction/pull registers) and
52/// the pin's own flags_ member stays whatever setup() set it to from YAML. So capturing
53/// get_flags() before flipping the pin to an output and restoring exactly that value afterward
54/// reproduces the pin's real configuration, pulls included, not just its direction.
55class BusyPinAsResetStrapGuard {
56 public:
57 explicit BusyPinAsResetStrapGuard(InternalGPIOPin *busy_pin)
58 : busy_pin_(busy_pin), restore_flags_(busy_pin->get_flags()) {
59 this->busy_pin_->pin_mode(gpio::FLAG_OUTPUT);
60 this->busy_pin_->digital_write(false);
61 }
62 ~BusyPinAsResetStrapGuard() { this->busy_pin_->pin_mode(this->restore_flags_); }
63 BusyPinAsResetStrapGuard(const BusyPinAsResetStrapGuard &) = delete;
64 BusyPinAsResetStrapGuard &operator=(const BusyPinAsResetStrapGuard &) = delete;
65
66 private:
67 InternalGPIOPin *busy_pin_;
68 gpio::Flags restore_flags_;
69};
70
71} // namespace
72
73Lr1121FirmwareUpdater::Lr1121FirmwareUpdater(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *busy_pin)
74 : spi_(spi), rst_pin_(rst_pin), busy_pin_(busy_pin) {
75 // This class exists specifically to run before RadioLR1121::init() has ever executed (the
76 // boot-time bootloader-version read), so the pins it
77 // needs cannot rely on the driver having set them up already. GPIOPin::setup() is idempotent —
78 // init() calling it again afterward on the same pins is harmless.
79 this->rst_pin_->setup();
80 this->busy_pin_->setup();
81}
82
83bool Lr1121FirmwareUpdater::wait_busy_(uint32_t timeout_ms) {
84 return poll_until([this] { return !this->busy_pin_->digital_read(); }, timeout_ms).ready;
85}
86
87bool Lr1121FirmwareUpdater::write_command_(uint16_t opcode, const uint8_t *params, size_t len,
88 uint32_t busy_timeout_ms) {
89 if (!this->wait_busy_(busy_timeout_ms))
90 return false;
91 this->spi_->spi_enable();
92 this->spi_->spi_write((opcode >> 8) & 0xFF);
93 this->spi_->spi_write(opcode & 0xFF);
94 for (size_t i = 0; i < len; i++)
95 this->spi_->spi_write(params[i]);
96 this->spi_->spi_disable();
97 return true;
98}
99
100bool Lr1121FirmwareUpdater::read_command_(uint16_t opcode, const uint8_t *params, size_t params_len, uint8_t *out,
101 size_t out_len, uint32_t busy_timeout_ms) {
102 if (!this->write_command_(opcode, params, params_len, busy_timeout_ms))
103 return false;
104 if (!this->wait_busy_(busy_timeout_ms))
105 return false;
106 this->spi_->spi_enable();
107 this->spi_->spi_read(); // Stat1 — this class has no diagnostic use for it, unlike RadioLR1121.
108 for (size_t i = 0; i < out_len; i++)
109 out[i] = this->spi_->spi_read();
110 this->spi_->spi_disable();
111 return true;
112}
113
114bool Lr1121FirmwareUpdater::read_normal_version(uint8_t &device_type, uint8_t &fw_major, uint8_t &fw_minor) {
115 uint8_t resp[4] = {0};
116 if (!this->read_command_(LR1121_UPDATER_CMD_GET_VERSION, nullptr, 0, resp, sizeof(resp),
117 LR1121_UPDATER_BUSY_TIMEOUT_MS))
118 return false;
119 // Response layout [hw, device_type, fw_major, fw_minor] — same as RadioLR1121::configure_radio_()
120 // reads in normal mode; device type is byte 1, not byte 0.
121 device_type = resp[1];
122 fw_major = resp[2];
123 fw_minor = resp[3];
124 return true;
125}
126
127bool Lr1121FirmwareUpdater::enter_bootloader(uint8_t &type, uint16_t &bootloader_version) {
128 {
129 BusyPinAsResetStrapGuard const busy_guard(this->busy_pin_);
130 this->rst_pin_->digital_write(false);
131 delay(LR1121_UPDATER_RESET_PULSE_MS);
132 this->rst_pin_->digital_write(true);
133 delay(LR1121_UPDATER_BOOTLOADER_ENTRY_WAIT_MS);
134 } // Guard destructor returns BUSY to input here, before the post-entry settle wait.
135 delay(LR1121_UPDATER_POST_ENTRY_SETTLE_MS);
136
137 return this->read_bootloader_version(type, bootloader_version);
138}
139
140bool Lr1121FirmwareUpdater::read_bootloader_version(uint8_t &type, uint16_t &bootloader_version) {
141 uint8_t resp[4] = {0};
142 if (!this->read_command_(LR1121_UPDATER_CMD_GET_VERSION, nullptr, 0, resp, sizeof(resp),
143 LR1121_UPDATER_BUSY_TIMEOUT_MS))
144 return false;
145 // Same response layout as normal-mode GetVersion; in bootloader mode byte 1 is
146 // LR1121_UPDATER_BOOTLOADER_TYPE (0xDF) and bytes 2-3 are the bootloader version, not a
147 // transceiver firmware version.
148 type = resp[1];
149 bootloader_version = (static_cast<uint16_t>(resp[2]) << 8) | resp[3];
150 return true;
151}
152
153#ifdef IOHOME_LR1121_BOOTLOADER_UPDATE
154
155bool Lr1121FirmwareUpdater::update_bootloader() {
156 if (!this->write_command_(LR1121_UPDATER_CMD_UPDATE_BOOTLOADER, nullptr, 0, LR1121_UPDATER_BUSY_TIMEOUT_MS))
157 return false;
158 // Semtech's own reference tool calls GetStatus exactly once here and calls that "waiting for
159 // bootloader update termination" (lr11xx_bootloader_update.c:138) -- a single poll, not a wait
160 // loop. This project's erase_flash() already handles an operation that holds BUSY for an
161 // internal flash write correctly; do the same here, so a hung write surfaces as a timeout rather
162 // than as a verify_bootloader() read taken before the chip is actually ready.
163 return this->wait_busy_(LR1121_UPDATER_BOOTLOADER_UPDATE_BUSY_TIMEOUT_MS);
164}
165
166bool Lr1121FirmwareUpdater::read_updater_status(Lr1121UpdaterStatus &status) {
167 if (!this->wait_busy_(LR1121_UPDATER_BUSY_TIMEOUT_MS))
168 return false;
169 // No opcode: a bare NSS-low/clock/NSS-high read returns Stat1, Stat2 and the 4-byte IrqStatus,
170 // matching Semtech's lr11xx_hal_direct_read() with LR11XX_BL_UPDATER_GET_STATUS_CMD_LENGTH (6).
171 uint8_t data[6] = {0};
172 this->spi_->spi_enable();
173 for (unsigned char &byte : data)
174 byte = this->spi_->spi_read();
175 this->spi_->spi_disable();
176
177 // Bit layout matches lr11xx_bootloader_updater_get_status() verbatim.
178 status.interrupt_active = (data[0] & 0x01) != 0;
179 status.command_status = static_cast<Lr1121UpdaterCommandStatus>(data[0] >> 1);
180 status.running_from_flash = (data[1] & 0x01) != 0;
181 status.chip_mode = static_cast<uint8_t>((data[1] & 0x0F) >> 1);
182 status.reset_status = static_cast<uint8_t>((data[1] & 0xF0) >> 4);
183 status.irq_status = (static_cast<uint32_t>(data[2]) << 24) | (static_cast<uint32_t>(data[3]) << 16) |
184 (static_cast<uint32_t>(data[4]) << 8) | static_cast<uint32_t>(data[5]);
185 return true;
186}
187
188bool Lr1121FirmwareUpdater::verify_bootloader(Lr1121BootloaderVerification &report) {
189 uint8_t resp[4] = {0};
190 if (!this->read_command_(LR1121_UPDATER_CMD_VERIFY_BOOTLOADER, nullptr, 0, resp, sizeof(resp),
191 LR1121_UPDATER_BUSY_TIMEOUT_MS))
192 return false;
193 // Bit layout matches SWTL001's lr11xx_bootloader_updater_verify_bootloader() verbatim
194 // (bootloader_updater_driver/src/lr11xx_bootloader_updater.c:170-179) -- the only public
195 // specification for this response; the User Manual predates the bootloader updater entirely.
196 const uint8_t check_byte = resp[0];
197 report.signature_verified = (check_byte & 0x01) != 0;
198 report.version_verified = (check_byte & 0x02) != 0;
199 report.use_case_verified = (check_byte & 0x04) != 0;
200 report.version_major_verified = (check_byte & 0x08) != 0;
201 report.version_minor_verified = (check_byte & 0x10) != 0;
202 report.anti_rollback_verified = (check_byte & 0x20) != 0;
203 report.use_case = resp[1];
204 report.version_major = resp[2];
205 report.version_minor = resp[3];
206 return true;
207}
208
209bool Lr1121FirmwareUpdater::updater_reboot(bool stay_in_bootloader) {
210 uint8_t const param = stay_in_bootloader ? 0x03 : 0x00;
211 if (!this->write_command_(LR1121_UPDATER_CMD_UPDATER_REBOOT, &param, 1, LR1121_UPDATER_BUSY_TIMEOUT_MS))
212 return false;
213 // Same BUSY race as reboot() -- see its comment. The caller's very next step is always a
214 // read_bootloader_version() to check whether the chip stayed in the bootloader, which is exactly
215 // the kind of immediately-following wait_busy_() reboot()'s comment warns about.
216 delay(LR1121_UPDATER_POST_REBOOT_SETTLE_MS);
217 return true;
218}
219
220#endif // IOHOME_LR1121_BOOTLOADER_UPDATE
221
222bool Lr1121FirmwareUpdater::erase_flash() {
223 if (!this->write_command_(LR1121_UPDATER_CMD_ERASE_FLASH, nullptr, 0, LR1121_UPDATER_BUSY_TIMEOUT_MS))
224 return false;
225 // EraseFlash holds BUSY high for the whole erase (seconds-scale) rather than for the brief
226 // processing window ordinary commands need, so this waits it out with its own generous budget
227 // before handing control back — callers should never need to know this detail.
228 return this->wait_busy_(LR1121_UPDATER_ERASE_BUSY_TIMEOUT_MS);
229}
230
231bool Lr1121FirmwareUpdater::write_image(const uint32_t *image, size_t word_count,
232 const std::function<void(size_t, size_t)> &on_progress) {
233 size_t written = 0;
234 while (written < word_count) {
235 size_t const chunk_words = std::min<size_t>(LR1121_UPDATER_FLASH_CHUNK_WORDS, word_count - written);
236 uint32_t const offset_bytes = static_cast<uint32_t>(written * sizeof(uint32_t));
237
238 uint8_t params[4 + LR1121_UPDATER_FLASH_CHUNK_WORDS * 4];
239 params[0] = static_cast<uint8_t>(offset_bytes >> 24);
240 params[1] = static_cast<uint8_t>(offset_bytes >> 16);
241 params[2] = static_cast<uint8_t>(offset_bytes >> 8);
242 params[3] = static_cast<uint8_t>(offset_bytes);
243 for (size_t i = 0; i < chunk_words; i++) {
244 uint32_t const word = image[written + i];
245 params[4 + i * 4 + 0] = static_cast<uint8_t>(word >> 24);
246 params[4 + i * 4 + 1] = static_cast<uint8_t>(word >> 16);
247 params[4 + i * 4 + 2] = static_cast<uint8_t>(word >> 8);
248 params[4 + i * 4 + 3] = static_cast<uint8_t>(word);
249 }
250
251 if (!this->write_command_(LR1121_UPDATER_CMD_WRITE_FLASH_ENCRYPTED, params, 4 + chunk_words * 4,
252 LR1121_UPDATER_BUSY_TIMEOUT_MS))
253 return false;
254
255 written += chunk_words;
256 // 255 chunks of SPI without yielding is itself long enough to matter for the watchdog, on
257 // top of whatever wait_busy_() already fed while waiting for each chunk's BUSY.
258 App.feed_wdt();
259 if (on_progress)
260 on_progress(written, word_count);
261 }
262 return true;
263}
264
265bool Lr1121FirmwareUpdater::read_hash(uint8_t *out, size_t out_len) {
266 if (out_len < LR1121_UPDATER_HASH_LENGTH)
267 return false;
268 return this->read_command_(LR1121_UPDATER_CMD_GET_HASH, nullptr, 0, out, LR1121_UPDATER_HASH_LENGTH,
269 LR1121_UPDATER_BUSY_TIMEOUT_MS);
270}
271
272bool Lr1121FirmwareUpdater::reboot(bool stay_in_bootloader) {
273 uint8_t const param = stay_in_bootloader ? 0x03 : 0x00;
274 if (!this->write_command_(LR1121_UPDATER_CMD_REBOOT, &param, 1, LR1121_UPDATER_BUSY_TIMEOUT_MS))
275 return false;
276 // wait_busy_() samples BUSY the instant it's called; nothing guarantees the chip has reacted to
277 // Reboot and reasserted BUSY yet, so a caller's very next wait_busy_() (e.g. the post-flash
278 // read_normal_version() verification) could sample BUSY before the chip drives it, return
279 // immediately, and clock a command into a chip that is still mid-reset -- reading garbage. See
280 // LR1121_UPDATER_POST_REBOOT_SETTLE_MS above for why this specific value and why it applies
281 // here rather than only at the post-flash call site.
282 delay(LR1121_UPDATER_POST_REBOOT_SETTLE_MS);
283 return true;
284}
285
286} // namespace home_io_control
287} // namespace esphome
288
289#endif // IOHOME_LR1121_FIRMWARE_UPDATE
Interface for SPI bus access.
PollResult poll_until(Ready ready, uint32_t timeout_ms)
Poll ready() until it is true or timeout_ms has passed, feeding the watchdog between samples.
Definition radio_poll.h:40
LR1121 bootloader-mode-*and*-loader-mode SPI transport, standalone from the running RadioDriver.
Preemption-safe "poll a condition until it holds or a deadline passes" helper.