mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-08-29 01:28:22 +00:00
lr1110 updater final
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user