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