lr updater test

This commit is contained in:
liquidraver
2026-07-20 19:52:15 +02:00
parent 13b5e2fcf3
commit 8870f0ded1
15 changed files with 15535 additions and 43 deletions
+14 -1
View File
@@ -45,6 +45,15 @@ LOG_MODULE_REGISTER(ui_pages, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
* DOTS_Y..SEP_Y: Page indicator dots + separator line
* CONTENT_Y..end: Page-specific content
*/
/* Full-scale end of the radio page's TX power bar. The board's configured
* ceiling when it has one (same symbol CommonCLI clamps "set tx" against),
* else the common SX126x/LR11xx maximum. */
#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
#define TX_POWER_BAR_MAX_DBM CONFIG_ZEPHCORE_MAX_TX_POWER_DBM
#else
#define TX_POWER_BAR_MAX_DBM 22
#endif
#define FONT_W mc_display_font_width()
#define FONT_H mc_display_font_height()
#define DISP_W mc_display_width()
@@ -845,7 +854,11 @@ static void render_radio_color(void)
state.lora_radio_ready ? UI_COLOR_OK
: UI_COLOR_DISABLED;
uint16_t tx_color = UI_COLOR_OK;
int max_tx = state.lora_tx_power > 0 ? state.lora_tx_power : 22;
/* Scale the TX bar against the board's hardware ceiling, not
* against the current setting — the old max came from APC
* (effective vs. configured power) and with APC gone both ends
* would be lora_tx_power, pinning the bar permanently full. */
int max_tx = TX_POWER_BAR_MAX_DBM;
int badge_x;
mc_display_color_fill_rect(0, y - 1, DISP_W, COLOR_FONT_H + 2,
+15 -8
View File
@@ -40,18 +40,25 @@
#define KEY_PREV 0x06 /* single button prev page */
#define KEY_HOME 0x07 /* go to home screen */
#define KEY_SELECT 0x08 /* triple click / special */
#define KEY_ENTER_LONG 0xF1 /* long press enter */
#define KEY_TO_TOP 0xF2 /* long press up → page up */
#define KEY_TO_BOTTOM 0xF3 /* long press down → page down */
#define KEY_LOCK 0xF4 /* user button + joystick center held together → screen lock */
/* Keys above 0x7F MUST be cast to char here. They travel through the UI as a
* plain `char` (enqueueKey / handleInput), and plain char is SIGNED on Xtensa
* and RISC-V (ESP32-S3, ESP32-C6) while it is UNSIGNED on ARM. Without the
* cast, `char c = KEY_LED_TOGGLE` stores -8 but the bare constant promotes to
* int 248, so every `c == KEY_LED_TOGGLE` compares -8 == 248 and silently
* never matches on the ESP32 boards. Casting makes both sides the same char
* type, so the comparison holds under either signedness. */
#define KEY_ENTER_LONG ((char)0xF1) /* long press enter */
#define KEY_TO_TOP ((char)0xF2) /* long press up → page up */
#define KEY_TO_BOTTOM ((char)0xF3) /* long press down → page down */
#define KEY_LOCK ((char)0xF4) /* user button + joystick center held together → screen lock */
/* Global action keys emitted by multi tap filter, handled in loop().
* Kept above 0xF0 so they stay clear of the printable range — see the
* key-space contract above. */
#define KEY_FLOOD_ADVERT 0xF5 /* INPUT_KEY_B: 2 taps → flood advert (unused) */
#define KEY_BUZZ_TOGGLE 0xF6 /* INPUT_KEY_D: 3 taps → buzzer mute toggle */
#define KEY_GPS_TOGGLE 0xF7 /* INPUT_KEY_C: 4 taps → GPS on/off */
#define KEY_LED_TOGGLE 0xF8 /* INPUT_KEY_E: 5 taps → LED heartbeat toggle */
#define KEY_FLOOD_ADVERT ((char)0xF5) /* INPUT_KEY_B: 2 taps → flood advert (unused) */
#define KEY_BUZZ_TOGGLE ((char)0xF6) /* INPUT_KEY_D: 3 taps → buzzer mute toggle */
#define KEY_GPS_TOGGLE ((char)0xF7) /* INPUT_KEY_C: 4 taps → GPS on/off */
#define KEY_LED_TOGGLE ((char)0xF8) /* INPUT_KEY_E: 5 taps → LED heartbeat toggle */
/* ===== Layout constants (calibrated for 128x64 OLED, 6x8 font) ===== */
/* All hard-coded offsets from old Arduino code are preserved here.
@@ -56,6 +56,27 @@ if(UPD_BOARD_CONF_CANDIDATES)
endif()
endif()
# Optional: route SPI through DMA so a 262-byte WriteFlashEncrypted frame is
# ONE hardware transaction instead of five 64-byte ones. Diagnostic — see the
# header of the board's spi_dma.overlay.
# west build ... zephcore/tools/lr1110_updater -- -DUPDATER_SPI_DMA=1
if(UPDATER_SPI_DMA)
set(UPD_DMA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/boards/esp32/${BOARD_BASE}")
if(NOT EXISTS "${UPD_DMA_DIR}/spi_dma.overlay")
message(FATAL_ERROR
"UPDATER_SPI_DMA=1 requested but no spi_dma.overlay exists for "
"board '${BOARD_BASE}'. This option is ESP32-only — the nRF52 "
"SPIM driver has no 64-byte transaction limit to work around.")
endif()
set(EXTRA_DTC_OVERLAY_FILE
"${EXTRA_DTC_OVERLAY_FILE};${UPD_DMA_DIR}/spi_dma.overlay"
CACHE STRING "" FORCE)
set(EXTRA_CONF_FILE
"${EXTRA_CONF_FILE};${UPD_DMA_DIR}/spi_dma.conf"
CACHE STRING "" FORCE)
message(STATUS "LR1110 updater: SPI DMA ENABLED (single-transaction writes)")
endif()
message(STATUS "LR1110 updater board overlay: ${UPDATER_BOARD_OVERLAY}")
message(STATUS "LR1110 updater board conf: ${UPD_BOARD_CONF}")
@@ -69,6 +90,20 @@ target_sources(app PRIVATE
src/lr1110_bl_updater.c
)
# Optional: flash transceiver 0x0401 instead of the default 0x0402.
# west build ... zephcore/tools/lr1110_updater -- -DUPDATER_TARGET_FW=0x0401
if(DEFINED UPDATER_TARGET_FW)
target_compile_definitions(app PRIVATE UPDATER_TARGET_FW=${UPDATER_TARGET_FW})
endif()
# Opt in to the ONE-WAY chip-bootloader update (0x6500 -> 0x1001). Off by
# default: chips that took it have since refused to boot any transceiver
# image. See the note in src/main.c.
if(DEFINED UPDATER_ALLOW_BOOTLOADER_UPDATE)
target_compile_definitions(app PRIVATE
UPDATER_ALLOW_BOOTLOADER_UPDATE=${UPDATER_ALLOW_BOOTLOADER_UPDATE})
endif()
# Include paths for Semtech driver headers and firmware image
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -15,6 +15,24 @@
#include "../../../../../boards/common/esp32s3_console_uart0.dtsi"
/*
* Park the ST7789 chip-select (GPIO16) HIGH = deselected.
*
* SPI2 is shared by the LR1110, the TFT and the SD slot. This build has no
* display driver, so nothing would ever configure the TFT's CS and the pin
* would float across the whole firmware transfer — leaving the panel free
* to select itself on noise and drive MISO against the radio. (The SD
* slot's CS is parked by a hog in the base board DTS, which this overlay
* does not replace.)
*/
&gpio0 {
tft_cs_park {
gpio-hog;
gpios = <16 GPIO_ACTIVE_HIGH>;
output-high;
};
};
/* Disable UART1 (GPS) — not needed */
&uart1 {
status = "disabled";
@@ -0,0 +1,11 @@
# ThinkNode M9 updater — DMA support for the SPI2/GDMA path.
# Paired with spi_dma.overlay, pulled in by -DUPDATER_SPI_DMA=1.
CONFIG_DMA=y
CONFIG_DMA_ESP32=y
# The DMA path allocates a scratch buffer for RX-only transfers (it must drive
# MOSI with zeros, so it k_calloc's a TX buffer) and for any RX buffer that is
# not 4-byte aligned. The updater otherwise runs with no heap at all, so
# without this EVERY spi_read — GetVersion, GetStatus, the EUI reads — fails
# with -ENOMEM the moment DMA is enabled.
CONFIG_HEAP_MEM_POOL_SIZE=4096
@@ -0,0 +1,38 @@
/*
* ThinkNode M9 updater — route SPI2 through GDMA.
* SPDX-License-Identifier: MIT
*
* Opt-in via -DUPDATER_SPI_DMA=1. Purpose is diagnostic, not performance.
*
* WriteFlashEncrypted is a 6-byte header + 256-byte payload = 262 bytes, and
* it is the ONLY command this tool issues that exceeds 64 bytes. Without DMA,
* spi_esp32_spim.c clamps every chunk to SOC_SPI_MAXIMUM_BUFFER_SIZE (64 on
* the S3), so that one command is delivered as five hardware transactions
* with the peripheral reconfigured between them — while every command known
* to work on this board (GetVersion, GetStatus, EraseFlash, the EUI reads)
* fits in a single transaction.
*
* The payload is encrypted, so the chip cannot validate content at write
* time — only that 256 bytes arrived. If the split perturbed the bit stream,
* writes would still report OK and the signed image would fail its integrity
* check at boot, which is exactly the observed symptom.
*
* With DMA the cap becomes SPI_DMA_MAX_BUFFER_SIZE (4092), so the whole
* 262-byte frame goes out as ONE transaction and that variable is removed.
*
* Caveat: upstream's own ESP32 board files leave SPI DMA commented as a TODO,
* so this path is not well exercised in-tree. Treat a failure to build or run
* as information about the driver, not about the radio.
*/
/* GDMA controller is disabled in esp32s3_common.dtsi — enable it. */
&dma {
status = "okay";
};
&spi2 {
dma-enabled;
dma-host = <0>; /* 0 -> SPI2 */
dmas = <&dma 0>, <&dma 1>; /* #dma-cells = <1>: the cell is the channel */
dma-names = "tx", "rx";
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,76 @@
Semtech's software made available with this lr1110_transceiver_0303_license.txt
file includes or is provided with certain third-party components that are
subject to separate terms and conditions specified by applicable third-party
licenses (“Third-Party Components”). These Third-Party Components and applicable
licenses are set forth in this lr1110_transceiver_0303_license.txt file.
Your access and use of all Third-Party Components are at all times governed by
the applicable third-party licenses.
_______________________________________________________________________________
LR1110 transceiver firmware 0x0303
_______________________________________________________________________________
Semtech Corporation
-------------------
The Clear BSD License
Copyright Semtech Corporation 2020. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Semtech corporation nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Micro-ecc v1.0
--------------
https://github.com/kmackay/micro-ecc
Copyright (c) 2014, Kenneth MacKay
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
Semtech's software made available with this lr1110_transceiver_0401_license.txt
file includes or is provided with certain third-party components that are
subject to separate terms and conditions specified by applicable third-party
licenses (“Third-Party Components”). These Third-Party Components and applicable
licenses are set forth in this lr1110_transceiver_0401_license.txt file.
Your access and use of all Third-Party Components are at all times governed by
the applicable third-party licenses.
_______________________________________________________________________________
LR1110 transceiver firmware 0x0401
_______________________________________________________________________________
Semtech Corporation
-------------------
The Clear BSD License
Copyright Semtech Corporation 2021. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Semtech corporation nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Micro-ecc v1.0
--------------
https://github.com/kmackay/micro-ecc
Copyright (c) 2014, Kenneth MacKay
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -51,6 +51,9 @@
#define LR1110_BL_ERASE_FLASH_CMD_LENGTH LR1110_BL_CMD_NO_PARAM_LENGTH
#define LR1110_BL_WRITE_FLASH_ENCRYPTED_CMD_LENGTH (LR1110_BL_CMD_NO_PARAM_LENGTH + 4)
#define LR1110_BL_REBOOT_CMD_LENGTH (LR1110_BL_CMD_NO_PARAM_LENGTH + 1)
#define LR1110_BL_GET_HASH_CMD_LENGTH LR1110_BL_CMD_NO_PARAM_LENGTH
#define LR1110_BL_SET_TCXO_MODE_CMD_LENGTH (LR1110_BL_CMD_NO_PARAM_LENGTH + 4)
#define LR1110_BL_CALIBRATE_CMD_LENGTH (LR1110_BL_CMD_NO_PARAM_LENGTH + 1)
#define LR1110_BL_GET_PIN_CMD_LENGTH LR1110_BL_CMD_NO_PARAM_LENGTH
#define LR1110_BL_READ_CHIP_EUI_CMD_LENGTH LR1110_BL_CMD_NO_PARAM_LENGTH
#define LR1110_BL_READ_JOIN_EUI_CMD_LENGTH LR1110_BL_CMD_NO_PARAM_LENGTH
@@ -60,8 +63,14 @@
enum {
LR1110_BL_GET_STATUS_OC = 0x0100,
LR1110_BL_GET_VERSION_OC = 0x0101,
/* System commands the bootloader also answers (same space as
* GetStatus/GetVersion above) used to bring up the TCXO before
* flashing on boards whose 32 MHz reference is powered from DIO3. */
LR1110_BL_CALIBRATE_OC = 0x010F,
LR1110_BL_SET_TCXO_MODE_OC = 0x0117,
LR1110_BL_ERASE_FLASH_OC = 0x8000,
LR1110_BL_WRITE_FLASH_ENCRYPTED_OC = 0x8003,
LR1110_BL_GET_HASH_OC = 0x8004,
LR1110_BL_REBOOT_OC = 0x8005,
LR1110_BL_GET_PIN_OC = 0x800B,
LR1110_BL_READ_CHIP_EUI_OC = 0x800C,
@@ -139,6 +148,47 @@ lr1110_status_t lr1110_bootloader_erase_flash(const void *context)
lr11xx_hal_write(context, cbuffer, LR1110_BL_ERASE_FLASH_CMD_LENGTH, 0, 0);
}
lr1110_status_t lr1110_bootloader_get_hash(const void *context,
lr1110_bootloader_hash_t hash)
{
const uint8_t cbuffer[LR1110_BL_GET_HASH_CMD_LENGTH] = {
(uint8_t)(LR1110_BL_GET_HASH_OC >> 8),
(uint8_t)(LR1110_BL_GET_HASH_OC >> 0),
};
return (lr1110_status_t)
lr11xx_hal_read(context, cbuffer, LR1110_BL_GET_HASH_CMD_LENGTH,
hash, LR1110_BL_HASH_LENGTH);
}
lr1110_status_t lr1110_bootloader_set_tcxo_mode(const void *context,
uint8_t tune, uint32_t timeout)
{
const uint8_t cbuffer[LR1110_BL_SET_TCXO_MODE_CMD_LENGTH] = {
(uint8_t)(LR1110_BL_SET_TCXO_MODE_OC >> 8),
(uint8_t)(LR1110_BL_SET_TCXO_MODE_OC >> 0),
tune,
(uint8_t)(timeout >> 16),
(uint8_t)(timeout >> 8),
(uint8_t)(timeout >> 0),
};
return (lr1110_status_t)
lr11xx_hal_write(context, cbuffer, LR1110_BL_SET_TCXO_MODE_CMD_LENGTH, 0, 0);
}
lr1110_status_t lr1110_bootloader_calibrate(const void *context, uint8_t calib_param)
{
const uint8_t cbuffer[LR1110_BL_CALIBRATE_CMD_LENGTH] = {
(uint8_t)(LR1110_BL_CALIBRATE_OC >> 8),
(uint8_t)(LR1110_BL_CALIBRATE_OC >> 0),
calib_param,
};
return (lr1110_status_t)
lr11xx_hal_write(context, cbuffer, LR1110_BL_CALIBRATE_CMD_LENGTH, 0, 0);
}
lr1110_status_t lr1110_bootloader_write_flash_encrypted(const void *context,
uint32_t offset, const uint32_t *data, uint8_t length)
{
@@ -53,10 +53,12 @@ typedef lr11xx_status_t lr1110_status_t;
/* ── Types ─────────────────────────────────────────────────── */
#define LR1110_BL_VERSION_LENGTH 4
#define LR1110_BL_HASH_LENGTH 16
#define LR1110_BL_PIN_LENGTH 4
#define LR1110_BL_CHIP_EUI_LENGTH 8
#define LR1110_BL_JOIN_EUI_LENGTH 8
typedef uint8_t lr1110_bootloader_hash_t[LR1110_BL_HASH_LENGTH];
typedef uint8_t lr1110_bootloader_pin_t[LR1110_BL_PIN_LENGTH];
typedef uint8_t lr1110_bootloader_chip_eui_t[LR1110_BL_CHIP_EUI_LENGTH];
typedef uint8_t lr1110_bootloader_join_eui_t[LR1110_BL_JOIN_EUI_LENGTH];
@@ -119,6 +121,33 @@ lr1110_status_t lr1110_bootloader_get_version(const void *context,
lr1110_status_t lr1110_bootloader_erase_flash(const void *context);
/* 16-byte digest of the image currently in flash (opcode 0x8004). Lets the
* host confirm what actually landed, rather than trusting fire-and-forget
* WriteFlashEncrypted calls that never read back. */
lr1110_status_t lr1110_bootloader_get_hash(const void *context,
lr1110_bootloader_hash_t hash);
/* TCXO supply voltage codes for lr1110_bootloader_set_tcxo_mode() */
#define LR1110_TCXO_CTRL_1_6V 0x00
#define LR1110_TCXO_CTRL_1_7V 0x01
#define LR1110_TCXO_CTRL_1_8V 0x02
#define LR1110_TCXO_CTRL_2_2V 0x03
#define LR1110_TCXO_CTRL_2_4V 0x04
#define LR1110_TCXO_CTRL_2_7V 0x05
#define LR1110_TCXO_CTRL_3_0V 0x06
#define LR1110_TCXO_CTRL_3_3V 0x07
/* Calibrate all blocks (LF_RC|HF_RC|PLL|ADC|IMG|PLL_TX) */
#define LR1110_CALIB_ALL 0x3F
/* Power the TCXO from DIO3 and use it as the XOSC source. `timeout` is in
* 32.768 kHz RTC ticks (1 tick = 30.52 us). Must be followed by a
* calibration changing the clock source invalidates the previous one. */
lr1110_status_t lr1110_bootloader_set_tcxo_mode(const void *context,
uint8_t tune, uint32_t timeout);
lr1110_status_t lr1110_bootloader_calibrate(const void *context, uint8_t calib_param);
lr1110_status_t lr1110_bootloader_write_flash_encrypted(const void *context,
uint32_t offset, const uint32_t *data, uint8_t length);
@@ -10,6 +10,8 @@
#include "lr11xx_hal_updater.h"
#include "lr11xx_hal.h"
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(lr1110_hal, LOG_LEVEL_INF);
@@ -24,12 +26,35 @@ LOG_MODULE_REGISTER(lr1110_hal, LOG_LEVEL_INF);
/* SPI bus device */
static const struct device *spi_dev = DEVICE_DT_GET(DT_BUS(LR1110_NODE));
/* Flashing SPI clock cap.
*
* In bootloader mode the chip runs off its internal RC oscillator (the XOSC
* needs either a crystal or a powered TCXO), so it has far less timing
* margin than during normal operation. Flashing pushes ~1000 back-to-back
* 256-byte writes, and the images are encrypted+signed: a SINGLE corrupted
* byte anywhere makes the whole image fail its integrity check at boot,
* with every write still reporting OK (the bootloader never reads back).
* Cap the operational 8-16 MHz down to a conservative rate the entire
* 239 KB image still takes ~1 s of SPI time at 2 MHz. */
#define UPDATER_SPI_MAX_HZ 2000000
/* SPI config — manual CS (we toggle NSS via GPIO) */
static struct spi_config spi_cfg = {
.frequency = DT_PROP(LR1110_NODE, spi_max_frequency),
.frequency = MIN(DT_PROP(LR1110_NODE, spi_max_frequency), UPDATER_SPI_MAX_HZ),
.operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
};
/* TCXO configuration from devicetree (0 mV = crystal board, no TCXO) */
uint16_t lr1110_updater_tcxo_voltage_mv(void)
{
return DT_PROP_OR(LR1110_NODE, tcxo_voltage_mv, 0);
}
uint32_t lr1110_updater_tcxo_startup_delay_ms(void)
{
return DT_PROP_OR(LR1110_NODE, tcxo_startup_delay_ms, 5);
}
/* GPIO pins */
static const struct gpio_dt_spec pin_nss = GPIO_DT_SPEC_GET(DT_BUS(LR1110_NODE), cs_gpios);
static const struct gpio_dt_spec pin_reset = GPIO_DT_SPEC_GET(LR1110_NODE, reset_gpios);
@@ -42,6 +67,13 @@ static const struct gpio_dt_spec pin_busy = GPIO_DT_SPEC_GET(LR1110_NODE, busy_
* bootloader rewrite (0x8100) both keep BUSY high for seconds. */
#define LONG_BUSY_TIMEOUT_MS 10000
/* How long to watch for BUSY to RISE after a command before assuming the
* chip finished it too quickly to observe. See wait_command_complete(). */
#define BUSY_RISE_TIMEOUT_MS 2
/* Largest command+payload frame we ever put on the wire. */
#define LR11XX_HAL_MAX_FRAME 272
/* ── Context (opaque pointer for Semtech driver) ──────────── */
/* The Semtech driver passes 'context' to every HAL function.
@@ -69,6 +101,70 @@ static int wait_on_busy(uint32_t timeout_ms)
return 0;
}
/*
* Wait for a command the chip has just been given to actually COMPLETE.
*
* The chip does not raise BUSY the instant NSS deasserts it needs a few
* microseconds. Polling only for "BUSY is low" therefore has a race: on a
* fast host (ESP32-S3 at 240 MHz drives GPIO in nanoseconds) the poll can
* observe the *stale* pre-command LOW and conclude the command is already
* finished. The next transaction then starts clocking while the chip is
* still writing flash, and because WriteFlashEncrypted is fire-and-forget
* no read-back, no status check the resulting corruption is silent.
* One bad chunk anywhere invalidates the whole signed image, so the odds
* of a clean flash fall off a cliff as the image grows: a 19 KB loader is
* 77 transactions, a 239 KB firmware is 959.
*
* So: first watch for the rising edge (bounded a command that finishes
* faster than we can look is fine and simply never appears busy), then
* wait for the fall.
*/
/* ── Per-command instrumentation ───────────────────────────
*
* Captured for the most recent command so the caller can trace every chunk:
* how long the chip took to ASSERT busy (rise latency) and how long it then
* held it (the real flash-program time). A chunk that never asserts busy at
* all is the signature of the race this HAL exists to avoid worth seeing
* per chunk rather than inferring from a summary. */
static uint32_t last_busy_rise_us;
static uint32_t last_busy_hold_us;
static bool last_busy_seen;
static int last_spi_ret;
uint32_t lr1110_updater_last_busy_rise_us(void) { return last_busy_rise_us; }
uint32_t lr1110_updater_last_busy_hold_us(void) { return last_busy_hold_us; }
bool lr1110_updater_last_busy_seen(void) { return last_busy_seen; }
int lr1110_updater_last_spi_ret(void) { return last_spi_ret; }
static int wait_command_complete(uint32_t timeout_ms)
{
const uint32_t cyc_entry = k_cycle_get_32();
int64_t start = k_uptime_get();
last_busy_seen = false;
last_busy_rise_us = 0;
last_busy_hold_us = 0;
while (!gpio_pin_get_dt(&pin_busy)) {
if ((k_uptime_get() - start) > BUSY_RISE_TIMEOUT_MS) {
break; /* never went busy — nothing to wait for */
}
k_busy_wait(1);
}
if (gpio_pin_get_dt(&pin_busy)) {
last_busy_seen = true;
last_busy_rise_us =
k_cyc_to_us_floor32(k_cycle_get_32() - cyc_entry);
}
const uint32_t cyc_high = k_cycle_get_32();
int ret = wait_on_busy(timeout_ms);
last_busy_hold_us = k_cyc_to_us_floor32(k_cycle_get_32() - cyc_high);
return ret;
}
/* ── Public init/reset ────────────────────────────────────── */
int lr1110_updater_hal_init(void)
@@ -184,19 +280,46 @@ lr11xx_hal_status_t lr11xx_hal_write(const void *context, const uint8_t *command
return LR11XX_HAL_STATUS_ERROR;
}
const struct spi_buf tx_bufs[] = {
{ .buf = (uint8_t *)command, .len = command_length },
{ .buf = (uint8_t *)data, .len = data_length },
};
const struct spi_buf_set tx = {
.buffers = tx_bufs,
.count = (data_length > 0) ? 2 : 1,
/* Send the command and its payload as ONE contiguous buffer.
*
* Passing them as two spi_bufs makes Zephyr's ESP32 SPI driver walk
* the set buffer-by-buffer (spi_context_max_continuous_chunk() never
* spans a buffer boundary), so a 6-byte command and a 256-byte payload
* become separate hardware transactions and each is further split at
* SOC_SPI_MAXIMUM_BUFFER_SIZE (64 bytes on the S3, no DMA).
*
* That matters here because WriteFlashEncrypted is the ONLY command
* this tool issues with a payload: every command known to work
* (GetVersion, GetStatus, EraseFlash, the EUI reads) is a single
* sub-64-byte frame. Keeping the frame contiguous together with a
* flash chunk size chosen so command+payload stays under 64 bytes
* makes the write path look exactly like the paths already proven
* good on this hardware. */
static uint8_t txbuf[LR11XX_HAL_MAX_FRAME];
if ((size_t)command_length + (size_t)data_length > sizeof(txbuf)) {
printk("ERROR: SPI frame too large (%u + %u)\n",
command_length, data_length);
return LR11XX_HAL_STATUS_ERROR;
}
memcpy(txbuf, command, command_length);
if (data_length > 0) {
memcpy(txbuf + command_length, data, data_length);
}
const struct spi_buf tx_buf = {
.buf = txbuf,
.len = (size_t)command_length + (size_t)data_length,
};
const struct spi_buf_set tx = { .buffers = &tx_buf, .count = 1 };
gpio_pin_set_dt(&pin_nss, 1); /* Assert NSS (LOW) */
ret = spi_write(spi_dev, &spi_cfg, &tx);
gpio_pin_set_dt(&pin_nss, 0); /* Deassert NSS (HIGH) */
last_spi_ret = ret;
if (ret < 0) {
printk("ERROR: SPI write failed: %d\n", ret);
return LR11XX_HAL_STATUS_ERROR;
@@ -211,7 +334,9 @@ lr11xx_hal_status_t lr11xx_hal_write(const void *context, const uint8_t *command
uint32_t timeout = (opcode == 0x8000 || opcode == 0x8100)
? LONG_BUSY_TIMEOUT_MS : BUSY_TIMEOUT_MS;
if (wait_on_busy(timeout)) {
/* Rising-edge aware: the command must be seen through to completion,
* not merely observed to be "not busy yet". */
if (wait_command_complete(timeout)) {
return LR11XX_HAL_STATUS_ERROR;
}
@@ -244,12 +369,12 @@ lr11xx_hal_status_t lr11xx_hal_read(const void *context, const uint8_t *command,
}
if (data_length == 0) {
return (wait_on_busy(BUSY_TIMEOUT_MS) == 0)
return (wait_command_complete(BUSY_TIMEOUT_MS) == 0)
? LR11XX_HAL_STATUS_OK : LR11XX_HAL_STATUS_ERROR;
}
/* Step 2: Wait for device ready, then read response */
if (wait_on_busy(BUSY_TIMEOUT_MS)) {
/* Step 2: Wait for the command to complete, then read the response */
if (wait_command_complete(BUSY_TIMEOUT_MS)) {
return LR11XX_HAL_STATUS_ERROR;
}
@@ -22,6 +22,10 @@
*/
int lr1110_updater_hal_init(void);
/* TCXO configuration from devicetree (0 mV = crystal board, no TCXO) */
uint16_t lr1110_updater_tcxo_voltage_mv(void);
uint32_t lr1110_updater_tcxo_startup_delay_ms(void);
/**
* @brief Hardware reset the LR1110 (pulse RESET, wait for BUSY low).
*
@@ -49,4 +53,22 @@ int lr1110_updater_reset_to_bootloader(void);
*/
void *lr1110_updater_get_context(void);
/**
* @brief Instrumentation for the most recent command.
*
* WriteFlashEncrypted has no read-back and no per-command acknowledgement, so
* BUSY timing is the only direct evidence that the chip actually performed the
* flash program cycle. These expose it for per-chunk tracing:
*
* - rise_us: how long after NSS deassert the chip asserted BUSY.
* - hold_us: how long BUSY stayed high the real program time (~3.8 ms for a
* 256-byte page). A near-zero hold means nothing was programmed.
* - busy_seen: false if BUSY never rose within the bounded watch window.
* - spi_ret: return code of the last spi_write().
*/
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);
int lr1110_updater_last_spi_ret(void);
#endif /* LR11XX_HAL_UPDATER_H */
+384 -22
View File
@@ -38,7 +38,45 @@
#include "lr11xx_hal_updater.h"
#include "lr1110_bootloader.h"
#include "lr1110_bl_updater.h"
/*
* Which transceiver image to flash. 0x0402 is the current release (and the
* only one Semtech's own tool will attempt on bootloader 0x1001).
*
* 0x0401 is selectable to TEST whether the downgrade block is enforced by
* the CHIP or merely by the host tool: the compatibility table that pairs
* 0x0401 with bootloader 0x6500 lives in Semtech's reference *application*
* (lr11xx_update_utils.c), not in the silicon. Since 0x0402 is a CVE fix,
* anti-rollback in the new bootloader is plausible but unverified, and a
* chip that runs 0x0401 is fully usable by ZephCore (T1000-E ships it).
*
* west build ... -- -DUPDATER_TARGET_FW=0x0401
*/
#ifndef UPDATER_TARGET_FW
#define UPDATER_TARGET_FW 0x0402
#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.
*/
#ifndef UPDATER_ALLOW_BOOTLOADER_UPDATE
#define UPDATER_ALLOW_BOOTLOADER_UPDATE 1
#endif
#if UPDATER_TARGET_FW == 0x0401
#include "lr1110_transceiver_0401.h"
#elif UPDATER_TARGET_FW == 0x0303
/* 0x0303 is the version another M9/LR1110 in this exact broken state was
* recovered with (reported running as "Base FW 3.3" under RadioLib), which
* also demonstrates the bootloader does NOT enforce anti-rollback. */
#include "lr1110_transceiver_0303.h"
#else
#include "lr1110_transceiver_0402.h"
#endif
#if defined(CONFIG_SOC_SERIES_NRF52)
#include <hal/nrf_power.h>
@@ -115,6 +153,152 @@ static void fatal_error(const char *msg)
}
}
/* Map a millivolt value from devicetree to the chip's TCXO supply code. */
static uint8_t tcxo_code_from_mv(uint16_t mv)
{
if (mv >= 3300) return LR1110_TCXO_CTRL_3_3V;
if (mv >= 3000) return LR1110_TCXO_CTRL_3_0V;
if (mv >= 2700) return LR1110_TCXO_CTRL_2_7V;
if (mv >= 2400) return LR1110_TCXO_CTRL_2_4V;
if (mv >= 2200) return LR1110_TCXO_CTRL_2_2V;
if (mv >= 1800) return LR1110_TCXO_CTRL_1_8V;
if (mv >= 1700) return LR1110_TCXO_CTRL_1_7V;
return LR1110_TCXO_CTRL_1_6V;
}
/* Power the TCXO and switch the chip onto it before touching flash.
*
* On a crystal board the chip can start its 32 MHz XOSC by itself. On a
* TCXO board the oscillator is powered from DIO3, which stays OFF until
* SetTcxoMode is issued so the chip would otherwise run the ENTIRE
* update on its internal RC oscillator, which is both slower and far less
* accurate. Flash program/erase pulse timing and the charge-pump sequencing
* derive from that clock, so a marginal clock can produce writes that
* report OK but do not survive the image integrity check at boot.
*
* Not in Semtech's update documentation (their reference hardware does not
* need it), but field-reported to make LR1110 flashing succeed on boards
* where it otherwise fails. Best-effort: a chip that rejects the command
* simply stays on RC, exactly as before. */
/*
* Off by default. Neither Semtech's SWTL001 nor RadioLib touches the TCXO
* while flashing, and SetTcxoMode (0x0117) / Calibrate (0x010F) are *system*
* opcodes that the bootloader command set does not include so issuing them
* here is unverified behaviour on a bootloader we need to keep in a
* well-defined state. The field report that TCXO helps was not reproducible
* on this board (the failure predates and survives it). Set to 1 to re-test.
*/
#define UPDATER_ENABLE_TCXO 0
static void configure_tcxo(void *ctx)
{
const uint16_t mv = lr1110_updater_tcxo_voltage_mv();
if (mv == 0) {
printk(" No TCXO in devicetree — chip uses its own crystal\n");
return;
}
const uint8_t code = tcxo_code_from_mv(mv);
/* SetTcxoMode timeout is in 32.768 kHz ticks (1 tick = 30.52 us).
* Use a generous window at least 10 ms so a slow-starting TCXO
* is never declared failed. */
uint32_t delay_ms = lr1110_updater_tcxo_startup_delay_ms();
if (delay_ms < 10) {
delay_ms = 10;
}
const uint32_t ticks = delay_ms * 32768U / 1000U;
printk(" Powering TCXO: %u mV (code 0x%02X), startup %u ms\n",
mv, code, delay_ms);
if (lr1110_bootloader_set_tcxo_mode(ctx, code, ticks) != LR1110_STATUS_OK) {
printk(" WARNING: SetTcxoMode rejected — continuing on RC clock\n");
return;
}
k_msleep(delay_ms + 10);
/* Changing the clock source invalidates the factory calibration —
* recalibrate every block before using the chip. */
if (lr1110_bootloader_calibrate(ctx, LR1110_CALIB_ALL) != LR1110_STATUS_OK) {
printk(" WARNING: Calibrate rejected after TCXO enable\n");
return;
}
k_msleep(50);
printk(" TCXO active, chip recalibrated\n");
}
/* Dump the chip's flash digest at a labelled point in the sequence.
*
* Sampled before erase, after erase and after write, these three values
* answer the question no other command can: does anything we do actually
* change the flash? If all three match, the writes are not landing at all
* (or GetHash is inert on this bootloader). If erase changes it but the
* write does not, the payload is being discarded. */
static void dump_flash_hash(void *ctx, const char *when)
{
lr1110_bootloader_hash_t hash = { 0 };
if (lr1110_bootloader_get_hash(ctx, hash) != LR1110_STATUS_OK) {
printk(" Flash hash (%s): READ FAILED\n", when);
return;
}
printk(" Flash hash (%-12s): ", when);
for (int i = 0; i < LR1110_BL_HASH_LENGTH; i++) {
printk("%02X", hash[i]);
}
printk("\n");
}
/* Ask the chip how the last command went.
*
* WriteFlashEncrypted is fire-and-forget the HAL only knows the SPI
* transfer happened, not that the chip accepted it. Sampling the command
* status during the transfer turns a silent corruption into a located one.
* Returns false only for a definite chip-reported failure. */
static const char *cmd_status_name(uint8_t s)
{
switch (s) {
case LR1110_BOOTLOADER_CMD_STATUS_FAIL: return "FAIL";
case LR1110_BOOTLOADER_CMD_STATUS_PERR: return "PERR";
case LR1110_BOOTLOADER_CMD_STATUS_OK: return "OK";
case LR1110_BOOTLOADER_CMD_STATUS_DATA: return "DATA";
default: return "?";
}
}
static bool command_status_ok(void *ctx, uint32_t chunk_idx, uint8_t *status_out)
{
lr1110_bootloader_stat1_t stat1 = { 0 };
lr1110_bootloader_stat2_t stat2 = { 0 };
lr1110_bootloader_irq_mask_t irq = 0;
if (status_out) {
*status_out = 0xFF; /* unknown until GetStatus succeeds */
}
if (lr1110_bootloader_get_status(ctx, &stat1, &stat2, &irq) != LR1110_STATUS_OK) {
printk(" WARNING: GetStatus failed near chunk %u\n", chunk_idx);
return true; /* inconclusive — do not abort the flash */
}
if (status_out) {
*status_out = (uint8_t)stat1.command_status;
}
if (stat1.command_status == LR1110_BOOTLOADER_CMD_STATUS_FAIL ||
stat1.command_status == LR1110_BOOTLOADER_CMD_STATUS_PERR) {
printk(" ERROR: chip rejected a write near chunk %u (cmd_status=%u)\n",
chunk_idx, (unsigned)stat1.command_status);
return false;
}
return true;
}
/* Erase the chip flash, then write `total` words in 64-word chunks with
* progress output. Used for both the loader and the transceiver image. */
static void erase_and_flash_image(void *ctx, const char *what,
@@ -122,17 +306,50 @@ static void erase_and_flash_image(void *ctx, const char *what,
{
lr1110_status_t rc;
/* Bring the TCXO up here rather than once at startup: every reset —
* including the forced bootloader entries in Stage A drops the chip
* back onto its RC oscillator, so the clock must be re-established
* immediately before each erase/write pass. */
if (UPDATER_ENABLE_TCXO) {
configure_tcxo(ctx);
}
dump_flash_hash(ctx, "before erase");
printk(" Erasing LR1110 flash (~2.5 seconds)...\n");
led_toggle();
const int64_t erase_start = k_uptime_get();
rc = lr1110_bootloader_erase_flash(ctx);
if (rc != LR1110_STATUS_OK) {
fatal_error("Flash erase failed");
}
/* A full-flash erase genuinely takes ~2.5 s. If this reports a few
* milliseconds then BUSY was never observed and we are about to write
* into flash that is still erasing which would corrupt everything
* while every write still reports OK. */
printk(" Erase returned after %lld ms\n", k_uptime_get() - erase_start);
/* Let the flash controller settle before the first program cycle —
* the erase leaves the charge pump loaded and neither reference tool
* writes this promptly after a 2.5 s full-array erase. */
k_msleep(100);
dump_flash_hash(ctx, "after erase");
printk(" Writing %s (%u words = %u KB)...\n",
what, total, (total * 4) / 1024);
const uint32_t chunk_size = 64; /* 64 uint32_t words per write */
const int64_t write_start = k_uptime_get();
/* MUST be 64 words (256 bytes = one flash page). A 14-word write is
* rejected outright with PERR at chunk 0 the chip validates the byte
* count and only accepts whole pages (a short final chunk is fine).
*
* NOTE: that PERR result does NOT clear the SPI path, though it was
* once read that way. A 14-word write is a 62-byte frame one hardware
* transaction on the ESP32-S3 so it says nothing about whether a
* 262-byte frame survives being split across five. Matches Semtech's
* LR11XX_FLASH_DATA_MAX_LENGTH_UINT32 = 64 regardless. */
const uint32_t chunk_size = 64;
uint32_t num_chunks = (total + chunk_size - 1) / chunk_size;
uint32_t progress_step = num_chunks / 10; /* Print every 10% */
if (progress_step == 0) progress_step = 1;
@@ -157,9 +374,41 @@ static void erase_and_flash_image(void *ctx, const char *what,
fatal_error("Firmware write failed");
}
/* Check EVERY chunk, not just the progress points. Sampling at
* 10% intervals leaves ~95 unexamined writes between samples,
* and cmd_status only reflects the most recent command so a
* rejection in between is invisible. This is the diagnostic
* that pins down the exact chunk (and flash offset) where the
* chip first refuses data, if it refuses at all. */
/* Capture BUSY timing for THIS write before GetStatus below
* overwrites it with its own command's timing. */
const uint32_t busy_rise_us = lr1110_updater_last_busy_rise_us();
const uint32_t busy_hold_us = lr1110_updater_last_busy_hold_us();
const bool busy_seen = lr1110_updater_last_busy_seen();
const int spi_ret = lr1110_updater_last_spi_ret();
uint8_t cmd_status = 0xFF;
const bool accepted = command_status_ok(ctx, chunk_idx, &cmd_status);
/* Full per-chunk trace. The two columns that matter most are
* busy=... (a chunk that never asserted BUSY did no flash
* programming, however cleanly it "succeeded") and hold=...
* (a page program is ~3.8 ms; a near-zero hold is a write that
* did not happen). */
printk(" [%4u/%4u] off=0x%06X len=%2u spi=%d busy=%s rise=%uus hold=%uus stat=%s\n",
chunk_idx, num_chunks, offset, this_chunk, spi_ret,
busy_seen ? "yes" : "NO ", busy_rise_us, busy_hold_us,
cmd_status == 0xFF ? "??" : cmd_status_name(cmd_status));
if (!accepted) {
printk(" First rejection at chunk %u of %u, flash offset 0x%08X\n",
chunk_idx, num_chunks, offset);
fatal_error("Chip rejected firmware data mid-flash");
}
if ((chunk_idx % progress_step) == 0) {
uint32_t pct = (chunk_idx * 100) / num_chunks;
printk(" %3u%% (%u / %u words)\n",
printk(" ---- %3u%% (%u / %u words) ----\n",
pct, total - remaining + this_chunk, total);
led_toggle();
}
@@ -170,7 +419,9 @@ static void erase_and_flash_image(void *ctx, const char *what,
}
printk(" 100%% (%u / %u words)\n", total, total);
printk(" %s write complete!\n", what);
printk(" %s write complete in %lld ms\n", what, k_uptime_get() - write_start);
dump_flash_hash(ctx, "after write");
}
/* The loader is RUNNING (TYPE 0xDE) — command the bootloader rewrite,
@@ -184,6 +435,29 @@ static void loader_rewrite_and_verify(void *ctx)
lr1110_bl_updater_report_t report = { 0 };
lr1110_status_t rc;
/* No configure_tcxo() here, deliberately. The 0x8100 rewrite below is a
* flash write and the chip is back on its RC oscillator after the reboot
* into the loader, so this looks like a hole in the TCXO workaround that
* erase_and_flash_image() applies it is not:
*
* - SWTL001 configures no TCXO at ANY point of the update (its
* lr11xx_update_firmware() is just erase + write; the driver's
* set_tcxo_mode is never called by the update application). Our
* configure_tcxo() is a ZephCore-only field workaround with no
* counterpart in the reference flow.
* - The loader image's documented command set is only 0x8100/01/02 plus
* GetVersion/GetStatus. It exposes no system commands, so SetTcxoMode
* (0x0117) is undocumented against a RUNNING loader sending it is a
* guess, not a fix.
* - The failure modes are not symmetric. A bad transceiver flash leaves
* the chip falling back to the bootloader, which is retryable. A bad
* bootloader rewrite may not be recoverable at all. That asymmetry is
* what decides it: do not fire an undocumented opcode at the chip in
* the seconds before the one irreversible operation in this tool.
*
* If M9 bring-up ever shows the rewrite failing on a TCXO board, revisit
* with the GetStatus output below as evidence do not add it blind. */
/* Ask the loader to rewrite the bootloader — BUSY is held for the
* duration; the HAL grants opcode 0x8100 the long timeout. */
printk(" Rewriting chip bootloader (this takes a few seconds)...\n");
@@ -352,6 +626,28 @@ int main(void)
}
printk(" FW = 0x%04X\n", version.fw);
/* Seeing "bootloader mode" here does NOT prove the flash is empty: the
* hardware reset above releases NRESET while BUSY reads LOW, which is
* itself the bootloader-entry condition on this wiring. So ask the
* bootloader to run whatever is in flash and look again otherwise a
* chip that is already up to date gets pointlessly reflashed. */
if (version.type == LR1110_TYPE_BOOTLOADER) {
lr1110_bootloader_version_t booted = { 0 };
printk(" Probing for firmware in flash...\n");
lr1110_bootloader_reboot(ctx, false);
k_msleep(500);
if (lr1110_bootloader_get_version(ctx, &booted) == LR1110_STATUS_OK &&
booted.type == LR1110_TYPE_TRANSCEIVER) {
printk(" Flash holds bootable firmware: TYPE=0x%02X FW=0x%04X\n",
booted.type, booted.fw);
version = booted;
} else {
printk(" No bootable firmware in flash\n");
}
}
if (version.type == LR1110_TYPE_TRANSCEIVER && version.fw == TARGET_FW_VERSION) {
printk("\nAlready running target firmware 0x%04X — no update needed!\n",
TARGET_FW_VERSION);
@@ -383,16 +679,27 @@ int main(void)
fatal_error("Unknown chip type — cannot proceed");
}
/* ALWAYS force bootloader entry via hardware reset with BUSY held LOW —
* even when the chip already reports bootloader mode. With an erased or
* invalid flash the chip only FELL BACK into the bootloader, and that
* fallback state is not equivalent to a deliberate BUSY-held entry
* (field-confirmed on LR1110: flashing from the fallback state produces
* images that never boot). Semtech's SWTL001 reference likewise resets
* to bootloader unconditionally before every update attempt. */
printk("[4/8] Forcing LR1110 into bootloader mode (even if already there)...\n");
if (lr1110_updater_reset_to_bootloader() != 0) {
fatal_error("Failed to enter bootloader mode");
/* Enter the bootloader the way RadioLib does: the SOFTWARE reboot
* command with stay_in_bootloader=true.
*
* We previously used the BUSY-held hardware reset (what SWTL001
* does). Both are documented, but RadioLib is the reference that
* demonstrably flashes this chip family from an ESP32 host, and the
* entry method is the last structural difference left between its
* flow and ours opcodes, offsets, byte order and 64-word pages are
* all now verified identical. The BUSY-held reset stays as the
* fallback for a chip that will not answer commands at all. */
printk("[4/8] Entering bootloader (software reboot, stay=true)...\n");
lr1110_bootloader_reboot(ctx, true);
k_msleep(500);
rc = lr1110_bootloader_get_version(ctx, &version);
if (rc != LR1110_STATUS_OK || version.type != LR1110_TYPE_BOOTLOADER) {
printk(" Software entry did not land in bootloader —"
" falling back to BUSY-held hardware reset\n");
if (lr1110_updater_reset_to_bootloader() != 0) {
fatal_error("Failed to enter bootloader mode");
}
}
rc = lr1110_bootloader_get_version(ctx, &version);
@@ -428,14 +735,45 @@ int main(void)
/* ── Step 6: Chip bootloader — update if legacy ── */
printk("[6/8] Checking chip bootloader version...\n");
if (version.fw == LR1110_BL_VERSION_UPDATED) {
printk(" Bootloader 0x%04X — already up to date\n", version.fw);
} else if (version.fw == LR1110_BL_VERSION_LEGACY) {
run_bootloader_update(ctx);
} else {
if (version.fw != LR1110_BL_VERSION_UPDATED &&
version.fw != LR1110_BL_VERSION_LEGACY) {
printk(" Unexpected bootloader version 0x%04X\n", version.fw);
fatal_error("Unsupported chip bootloader — refusing to flash");
}
if (TARGET_FW_VERSION == 0x0402) {
if (version.fw == LR1110_BL_VERSION_UPDATED) {
printk(" Bootloader 0x%04X — already up to date\n", version.fw);
} else if (!UPDATER_ALLOW_BOOTLOADER_UPDATE) {
printk("\n");
printk(" Bootloader update DISABLED for this build.\n");
printk("\n");
printk(" Flash an image that runs on bootloader 0x6500:\n");
printk(" west build ... -- -DUPDATER_TARGET_FW=0x0401\n");
printk("\n");
printk(" Rebuild without -DUPDATER_ALLOW_BOOTLOADER_UPDATE=0\n");
printk(" to perform the bootloader update.\n");
fatal_error("Bootloader update disabled by build option");
} else {
printk("\n");
printk(" Performing the ONE-WAY bootloader update 0x%04X -> 0x%04X.\n",
version.fw, LR1110_BL_VERSION_UPDATED);
printk(" Semtech ships no reverse loader: this is permanent.\n");
printk(" (Build with -DUPDATER_ALLOW_BOOTLOADER_UPDATE=0 to\n");
printk(" flash firmware only and leave the bootloader alone.)\n");
printk("\n");
run_bootloader_update(ctx);
}
} else {
/* Older target image — never touch the bootloader. */
printk(" Bootloader 0x%04X, target FW 0x%04X\n",
version.fw, TARGET_FW_VERSION);
if (version.fw == LR1110_BL_VERSION_UPDATED) {
printk(" NOTE: Semtech's host-side table pairs FW 0x0401 with\n");
printk(" bootloader 0x6500 only. Trying anyway, to find out\n");
printk(" whether the chip itself enforces anti-rollback.\n");
}
}
}
/* ── Step 7: Flash transceiver firmware ── */
@@ -443,13 +781,37 @@ int main(void)
erase_and_flash_image(ctx, "transceiver firmware",
lr11xx_firmware_image, LR11XX_FIRMWARE_IMAGE_SIZE);
/* Reboot into firmware (273ms typical boot), then re-reset for a
* clean state before verification. */
/* Boot the freshly written firmware with the bootloader's own reboot
* command (0x8005, stay=false) and then DO NOT TOUCH NRESET.
*
* The LR1110 samples BUSY as NRESET is released and enters the
* bootloader whenever it reads LOW. Nothing holds that line high while
* the chip is in reset, so a hardware reset here lands back in the
* bootloader every time which is precisely what the old "re-reset for
* a clean state" did, making this step report "firmware not running"
* no matter how good the flash was. Stage A reboots the very same way
* *without* a trailing hardware reset, and its image always came up
* running: that asymmetry is what exposed this.
*
* Firmware boot takes ~273 ms (datasheet); 500 ms is comfortable. */
printk(" Rebooting LR1110 into new firmware...\n");
lr1110_bootloader_reboot(ctx, false);
k_msleep(500);
if (lr1110_updater_hw_reset() != 0) {
fatal_error("Reset after firmware flash failed");
/* The chip's own view of what it is executing. is_running_from_flash
* distinguishes "the image is bad" from "the image never got a chance
* to run", which the version read alone cannot. */
{
lr1110_bootloader_stat1_t stat1 = { 0 };
lr1110_bootloader_stat2_t stat2 = { 0 };
lr1110_bootloader_irq_mask_t irq = 0;
if (lr1110_bootloader_get_status(ctx, &stat1, &stat2, &irq) == LR1110_STATUS_OK) {
printk(" Post-reboot status: running_from_flash=%d chip_mode=%u "
"reset_status=%u cmd_status=%u\n",
stat2.is_running_from_flash, (unsigned)stat2.chip_mode,
(unsigned)stat2.reset_status, (unsigned)stat1.command_status);
}
}
/* ── Step 8: Verify ── */