diff --git a/.gitignore b/.gitignore index 33965a5..920476b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/LR1110_updater/README.md b/LR1110_updater/README.md new file mode 100644 index 0000000..1fdd02a --- /dev/null +++ b/LR1110_updater/README.md @@ -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 ` | + +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///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. diff --git a/LR1110_updater/T1000-E_LR1110_updater_FW0402.uf2 b/LR1110_updater/T1000-E_LR1110_updater_FW0402.uf2 new file mode 100644 index 0000000..5c04025 Binary files /dev/null and b/LR1110_updater/T1000-E_LR1110_updater_FW0402.uf2 differ diff --git a/LR1110_updater/ThinkNode-M9_LR1110_updater_FW0402.bin b/LR1110_updater/ThinkNode-M9_LR1110_updater_FW0402.bin new file mode 100644 index 0000000..708aa2a Binary files /dev/null and b/LR1110_updater/ThinkNode-M9_LR1110_updater_FW0402.bin differ diff --git a/zephcore/tools/lr1110_updater/CMakeLists.txt b/zephcore/tools/lr1110_updater/CMakeLists.txt index 9892b69..ba941a2 100644 --- a/zephcore/tools/lr1110_updater/CMakeLists.txt +++ b/zephcore/tools/lr1110_updater/CMakeLists.txt @@ -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 diff --git a/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c b/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c index bcfc100..af46515 100644 --- a/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c +++ b/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c @@ -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, diff --git a/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.h b/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.h index dd34d7a..8b93041 100644 --- a/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.h +++ b/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.h @@ -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); diff --git a/zephcore/tools/lr1110_updater/src/main.c b/zephcore/tools/lr1110_updater/src/main.c index e904bd4..09c2c07 100644 --- a/zephcore/tools/lr1110_updater/src/main.c +++ b/zephcore/tools/lr1110_updater/src/main.c @@ -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");