mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 04:08:22 +00:00
lr updater test
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 ── */
|
||||
|
||||
Reference in New Issue
Block a user