lr1110 updater final

This commit is contained in:
liquidraver
2026-07-21 14:43:39 +02:00
parent 4001b3f130
commit 3e04c11621
8 changed files with 314 additions and 6 deletions
+8
View File
@@ -51,6 +51,14 @@ rtt_*.log
# packages (see formatter/README.md, PROVIDER_CATALOG.md), so they must be tracked.
!formatter/*.zip
# ...and the prebuilt LR1110 radio-firmware updaters. Also shipped artifacts:
# updating an LR1110 needs Semtech's encrypted images compiled in, so handing
# someone a prebuilt binary is far more practical than asking them to build the
# tool. See LR1110_updater/README.md.
!LR1110_updater/
!LR1110_updater/*.uf2
!LR1110_updater/*.bin
# OS junk
.DS_Store
Thumbs.db
+81
View File
@@ -0,0 +1,81 @@
LR1110 radio firmware updater
Prebuilt tools that update the **LR1110 radio chip's own firmware** to `0x0402`.
This is the radio's internal firmware — not ZephCore. Flash one of these, let it
run, then flash normal ZephCore firmware afterwards.
You almost certainly do **not** need this. It exists for the Semtech H1_2026
security release (CVE-2025-14857 / 14858 / 14859), which is only exploitable with
physical access to the radio's SPI pins. Boards shipping `0x0401` work fine with
ZephCore and can be left alone.
## Files
| File | Board | How to flash |
|------|-------|--------------|
| **`T1000-E_LR1110_updater_FW0402.uf2`** | SenseCAP T1000-E | Double-tap reset, drag onto the UF2 drive |
| **`ThinkNode-M9_LR1110_updater_FW0402.bin`** | ThinkNode M9 | `esptool --chip esp32s3 -p COMx write_flash 0x0 <file>` |
Watch the serial console while it runs (115200) — it prints every step, and a
per-chunk trace of the flash write. The whole thing takes about 20 seconds.
## Before you flash
- **Remove the SD card** (M9). The slot shares SPI2 with the radio, and a card
present during flashing corrupts the image *while every write still reports
success* — the radio ends up running nothing, with no error to point at. The
tool detects a card and refuses to start, so you'll be told rather than bitten.
- **The bootloader update is one-way.** Firmware `0x0402` only runs on chip
bootloader `0x1001`, so a chip on the original `0x6500` gets its bootloader
rewritten first. Semtech ships no loader in the reverse direction, so that part
cannot be undone. It is safe — verified end-to-end — but it is permanent.
- **Don't interrupt it.** If it fails partway, power-cycle and run it again; the
tool detects whatever state the chip landed in and resumes appropriately.
Re-running once the radio is already on `0x0402` is harmless — it detects the
target firmware and exits without touching flash.
## Reading the output
```
[3/8] Reading current firmware version...
HW = 0x22 <- V2C production silicon, normal
TYPE = 0x01 <- 0x01 transceiver, 0xDF bootloader, 0xDE loader running
FW = 0x0402
```
Success looks like `TYPE=0x01 FW=0x0402` at step 8. If it ends at `TYPE=0xDF`,
the firmware was written but isn't running — power-cycle and re-run, and check
the SD card and the radio's supply.
During Stage A the chip briefly stays in bootloader mode after the bootloader is
rewritten. That is expected, not a failure — the loader image left in flash was
built for the old bootloader, so the new one declines to run it. Stage B follows.
## Rebuilding
Source is `zephcore/tools/lr1110_updater`.
```bash
west build -b t1000_e zephcore/tools/lr1110_updater --pristine -d build_t1000
cp build_t1000/zephyr/zephyr.uf2 LR1110_updater/T1000-E_LR1110_updater_FW0402.uf2
west build -b thinknode_m9/esp32s3/procpu zephcore/tools/lr1110_updater --pristine -d build_m9
cp build_m9/zephyr/zephyr.bin LR1110_updater/ThinkNode-M9_LR1110_updater_FW0402.bin
```
The M9 image is a plain simple-boot ESP32-S3 binary loaded from `0x0` — no
MCUboot, nothing to merge, despite what the flash offset might suggest.
Useful build options:
| Option | Effect |
|--------|--------|
| `-DUPDATER_TARGET_FW=0x0401` | Flash the older image; skips the bootloader update entirely |
| `-DUPDATER_ALLOW_BOOTLOADER_UPDATE=0` | Firmware only — never touch the chip bootloader |
| `-DUPDATER_IGNORE_SDCARD=1` | Flash anyway with a card inserted (not advised) |
| `-DUPDATER_CHUNK_DELAY_MS=10` | Space out page writes; diagnostic for a marginal supply |
Adding a board needs a `boards/<platform>/<board>/board.overlay` under the source
directory that disables unused peripherals and parks anything sharing the radio's
SPI bus. See the M9's overlay for what a shared-bus board requires.
Binary file not shown.
@@ -104,6 +104,26 @@ if(DEFINED UPDATER_ALLOW_BOOTLOADER_UPDATE)
UPDATER_ALLOW_BOOTLOADER_UPDATE=${UPDATER_ALLOW_BOOTLOADER_UPDATE})
endif()
# Idle gap after every flash page program, in ms. Diagnostic for a marginal
# supply — see the note in src/main.c.
# west build ... -- -DUPDATER_CHUNK_DELAY_MS=10
if(DEFINED UPDATER_CHUNK_DELAY_MS)
target_compile_definitions(app PRIVATE
UPDATER_CHUNK_DELAY_MS=${UPDATER_CHUNK_DELAY_MS})
endif()
# Flash even with an SD card in the slot (shared SPI bus — see src/main.c).
# west build ... -- -DUPDATER_IGNORE_SDCARD=1
if(DEFINED UPDATER_IGNORE_SDCARD)
target_compile_definitions(app PRIVATE
UPDATER_IGNORE_SDCARD=${UPDATER_IGNORE_SDCARD})
endif()
# Let a pasted log identify which build produced it.
if(UPDATER_SPI_DMA)
target_compile_definitions(app PRIVATE UPDATER_SPI_DMA=1)
endif()
# Include paths for Semtech driver headers and firmware image
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -266,6 +266,111 @@ int lr1110_updater_reset_to_bootloader(void)
return ret;
}
/* ── SD card presence probe ───────────────────────────────────
*
* On boards where the SD slot shares the radio's SPI bus, an inserted card
* breaks LR1110 flashing: every write still reports OK and the programmed
* image fails its integrity check at boot, which is indistinguishable from a
* dozen other faults and cost days to track down. There is no card-detect pin
* wired on the M9, so presence is established over the bus.
*
* Standard SPI-mode detection: >=74 dummy clocks with CS high to bring the
* card up in SPI mode, then CMD0 (GO_IDLE_STATE). A present card answers R1
* with the MSB clear (0x01 = idle). An empty slot leaves MISO pulled high, so
* every byte reads 0xFF and nothing else. False "absent" is possible if a card
* ignores CMD0 — no worse than not probing; false "present" essentially cannot
* happen, since 0xFF is all an empty slot can produce.
*
* Runs at 400 kHz (SD init is specified at 100-400 kHz; the radio path stays
* at its own clock) and is safe on the shared bus: the LR1110's NSS is parked
* inactive by hal_init, and the TFT is held in reset and is write-only.
*/
#if defined(CONFIG_BOARD_THINKNODE_M9)
/* No DT node exists for the slot — the base board DTS only parks its CS with
* a gpio-hog. GPIO48 = gpio1 pin 16. */
#define SDCARD_CS_PORT_NODE DT_NODELABEL(gpio1)
#define SDCARD_CS_PIN 16
#endif
int lr1110_updater_probe_sdcard(void)
{
#ifdef SDCARD_CS_PORT_NODE
const struct device *cs_port = DEVICE_DT_GET(SDCARD_CS_PORT_NODE);
struct spi_config slow_cfg = {
.frequency = 400000,
.operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
};
/* CMD0: GO_IDLE_STATE, arg 0, CRC7 0x95 (valid, and required while the
* card is still in its CRC-checked power-up state). */
static const uint8_t cmd0[6] = { 0x40, 0x00, 0x00, 0x00, 0x00, 0x95 };
uint8_t tx[8], rx[8];
int ret, present = 0;
if (!device_is_ready(cs_port)) {
return -ENODEV;
}
if (gpio_pin_configure(cs_port, SDCARD_CS_PIN, GPIO_OUTPUT_HIGH) < 0) {
return -EIO;
}
/* Wake-up clocks, CS HIGH (deselected) — 10 bytes = 80 cycles. */
memset(tx, 0xFF, sizeof(tx));
const struct spi_buf wake_buf = { .buf = tx, .len = sizeof(tx) };
const struct spi_buf_set wake = { .buffers = &wake_buf, .count = 1 };
ret = spi_write(spi_dev, &slow_cfg, &wake);
if (ret == 0) {
ret = spi_write(spi_dev, &slow_cfg, &wake); /* >=74 clocks total */
}
if (ret < 0) {
goto out;
}
/* Select the card and issue CMD0. */
gpio_pin_set(cs_port, SDCARD_CS_PIN, 0);
const struct spi_buf cmd_buf = { .buf = (uint8_t *)cmd0, .len = sizeof(cmd0) };
const struct spi_buf_set cmd = { .buffers = &cmd_buf, .count = 1 };
ret = spi_write(spi_dev, &slow_cfg, &cmd);
if (ret == 0) {
/* Clock the response out with MOSI held high, as the spec
* requires — spi_read() would drive zeros instead. */
memset(tx, 0xFF, sizeof(tx));
const struct spi_buf rtx_buf = { .buf = tx, .len = sizeof(rx) };
const struct spi_buf_set rtx = { .buffers = &rtx_buf, .count = 1 };
const struct spi_buf rrx_buf = { .buf = rx, .len = sizeof(rx) };
const struct spi_buf_set rrx = { .buffers = &rrx_buf, .count = 1 };
ret = spi_transceive(spi_dev, &slow_cfg, &rtx, &rrx);
if (ret == 0) {
for (size_t i = 0; i < sizeof(rx); i++) {
if ((rx[i] & 0x80) == 0) { /* valid R1 token */
present = 1;
break;
}
}
}
}
/* Deselect, then one more byte so the card releases the bus. */
gpio_pin_set(cs_port, SDCARD_CS_PIN, 1);
memset(tx, 0xFF, sizeof(tx));
const struct spi_buf rel_buf = { .buf = tx, .len = 1 };
const struct spi_buf_set rel = { .buffers = &rel_buf, .count = 1 };
spi_write(spi_dev, &slow_cfg, &rel);
out:
/* Leave CS parked HIGH exactly as the gpio-hog had it. */
gpio_pin_configure(cs_port, SDCARD_CS_PIN, GPIO_OUTPUT_HIGH);
return (ret < 0) ? ret : present;
#else
return -ENOTSUP;
#endif
}
/* ── Semtech HAL interface ────────────────────────────────── */
lr11xx_hal_status_t lr11xx_hal_write(const void *context, const uint8_t *command,
@@ -66,6 +66,18 @@ void *lr1110_updater_get_context(void);
* - busy_seen: false if BUSY never rose within the bounded watch window.
* - spi_ret: return code of the last spi_write().
*/
/**
* @brief Probe the shared SPI bus for an inserted SD card.
*
* Boards where the SD slot shares the radio's bus cannot flash the LR1110
* reliably with a card inserted. No card-detect pin is available, so presence
* is established with an SPI-mode CMD0 exchange.
*
* @return 1 if a card responded, 0 if the slot is empty, -ENOTSUP on boards
* with no shared SD slot, or a negative errno on bus failure.
*/
int lr1110_updater_probe_sdcard(void);
uint32_t lr1110_updater_last_busy_rise_us(void);
uint32_t lr1110_updater_last_busy_hold_us(void);
bool lr1110_updater_last_busy_seen(void);
+88 -6
View File
@@ -56,12 +56,20 @@
#endif
/*
* The 0x6500 -> 0x1001 chip-bootloader update is ONE-WAY and, on every chip
* we have observed take it, leaves the radio unable to boot ANY transceiver
* image (0x0402, 0x0401 and 0x0303 all write without error and never run,
* across two independent flashers). Chips still on bootloader 0x6500 flash
* and run normally. So it must be opted into explicitly, never performed as
* a side effect of asking for firmware 0x0402.
* Perform the 0x6500 -> 0x1001 chip-bootloader update when firmware 0x0402 is
* the target (0x0402 will not run on the old bootloader).
*
* Verified end-to-end on a T1000-E 2026-07-21: Stage A then Stage B, chip
* afterwards reports TYPE=0x01 FW=0x0402 and runs from flash. An earlier
* belief that this update bricks radios came from a sample where every
* observed 0x1001 chip was on one board (ThinkNode M9) — that board has a
* separate, still-open problem, so a board fault was being read as a
* bootloader fault.
*
* It IS one-way: Semtech ships loaders in the forward direction only, and the
* new bootloader lives inside the encrypted loader payload, so there is
* nothing to flash back. Set this to 0 to flash firmware only and leave the
* chip bootloader untouched.
*/
#ifndef UPDATER_ALLOW_BOOTLOADER_UPDATE
#define UPDATER_ALLOW_BOOTLOADER_UPDATE 1
@@ -116,6 +124,36 @@ static void led_off(void) {}
static void led_toggle(void) {}
#endif
/* Idle gap inserted after every flash page program.
*
* Diagnostic for a marginal supply. A page program pulls current for ~3.6 ms
* and we issue 959 of them back to back, so a rail that cannot sustain that
* burst will sag — and a sagging rail corrupts what gets programmed while the
* write still reports OK (the bootloader never reads back). Spacing the pages
* lets the supply recover between them. 10 ms stretches a full image write
* from ~6.6 s to ~16 s, which costs nothing.
* west build ... -- -DUPDATER_CHUNK_DELAY_MS=10
*/
#ifndef UPDATER_CHUNK_DELAY_MS
#define UPDATER_CHUNK_DELAY_MS 0
#endif
/* Refuse to flash with an SD card in the slot.
*
* On the M9 the slot shares SPI2 with the radio, and a card present during
* flashing produces a silently corrupt image: every write reports OK, the
* chip spends real time programming, and the result fails its integrity check
* at boot with no error anywhere to point at. Field-confirmed 2026-07-21 —
* removing the card was what finally made that board flash.
*
* Gated rather than warned because the asymmetry is stark: the cost of the
* gate is ejecting a card, the cost of missing it is days of debugging.
* west build ... -- -DUPDATER_IGNORE_SDCARD=1
*/
#ifndef UPDATER_IGNORE_SDCARD
#define UPDATER_IGNORE_SDCARD 0
#endif
/* ── Helpers ──────────────────────────────────────────────────── */
static void updater_done(void)
@@ -153,6 +191,37 @@ static void fatal_error(const char *msg)
}
}
static void check_sdcard(void)
{
int rc = lr1110_updater_probe_sdcard();
if (rc == -ENOTSUP) {
return; /* board has no SD slot on the radio's bus */
}
if (rc < 0) {
printk(" SD slot: probe failed (%d) — continuing\n", rc);
return;
}
if (rc == 0) {
printk(" SD slot: empty\n");
return;
}
printk(" SD slot: CARD PRESENT\n");
if (UPDATER_IGNORE_SDCARD) {
printk(" WARNING: flashing anyway (UPDATER_IGNORE_SDCARD=1).\n");
return;
}
printk("\n");
printk(" The SD slot shares SPI2 with the radio. Flashing with a\n");
printk(" card inserted corrupts the image while every write still\n");
printk(" reports success — the chip ends up running nothing and\n");
printk(" there is no error to point at.\n");
printk("\n");
printk(" Remove the SD card and try again.\n");
fatal_error("SD card present — refusing to flash");
}
/* Map a millivolt value from devicetree to the chip's TCXO supply code. */
static uint8_t tcxo_code_from_mv(uint16_t mv)
{
@@ -413,6 +482,10 @@ static void erase_and_flash_image(void *ctx, const char *what,
led_toggle();
}
if (UPDATER_CHUNK_DELAY_MS > 0) {
k_msleep(UPDATER_CHUNK_DELAY_MS);
}
offset += this_chunk * sizeof(uint32_t);
remaining -= this_chunk;
chunk_idx++;
@@ -587,6 +660,14 @@ int main(void)
printk(" Image: %u words (%u KB)\n",
LR11XX_FIRMWARE_IMAGE_SIZE,
(LR11XX_FIRMWARE_IMAGE_SIZE * 4) / 1024);
printk(" Build: SPI=%s chunk-delay=%ums bl-update=%s\n",
#ifdef UPDATER_SPI_DMA
"DMA",
#else
"PIO",
#endif
(unsigned)UPDATER_CHUNK_DELAY_MS,
UPDATER_ALLOW_BOOTLOADER_UPDATE ? "yes" : "no");
printk("============================================\n");
printk("\n");
@@ -599,6 +680,7 @@ int main(void)
fatal_error("HAL init failed");
}
ctx = lr1110_updater_get_context();
check_sdcard();
/* ── Step 2: Hardware reset ── */
printk("[2/8] Hardware reset LR1110...\n");