From af963beace3faa9cb31b3bdef4ffa7453d96379e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Amundsen?= Date: Thu, 4 Dec 2025 13:31:34 +0100 Subject: [PATCH] tests: support swapping UART MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstrate how to update the PERIPHCONF blob pointed to by UICR.PERIPHCONF in a system where Direct XIP is used. The application firmware is executed from one of two slots when Direct XIP is in use, this means that the global PERIPHCONF blob contained in the periphconf partition needs to be updated by the application firmware if it has to be updated. Firstly, we integrate the PERIPHCONF blob of each image within the firmware of the image. This way its contents is covered by the signature validation performed by MCUBoot, and any updates to the PERIPHCONF is contained within the firmware itself. Next, we add a function that is executed in SYS_INIT(EARLY), that is before any device driver setup is done, which ensures that the global PERIPHCONF matches the local one contained in the image firmware. If it does not match, the global PERIPHCONF is updated from the point of the first mismatch. We also add a mechanism showing how to re-order the PERIPHCONF so that the entries from MCUBoot are first. This is done to ensure that MCUBoot is not affected by any updates to the global PERIPHCONF, and that the system is power-failure safe during an update to the global PERIPHCONF. Some comments on the changes introduced in this commit: - The swap snippet simply swaps which UART is used for an image - We re-use an existing application (tests/subsys/bootloader/upgrade/ref_smp_svr/CMakeLists.txt) and create a CMake parameter 'PERIPHCONF_BIN' that is used to pass local PERIPHCONF to the image firmware. When this is set, the lib for copying the local PERIPHCONF to the global one is included. - The copy lib does not use any driver as that code is not available at the time of execution (SYS_INIT(EARLY)). - Add build configurations for this application to make build commands more understandable. - The rest of the commit are helper scripts to create the new PERIPHCONF blob and verify the functionality. The steps to build and verify the changes are: # cd to the test application cd $(west topdir)/nrf/tests/subsys/bootloader/upgrade/ref_smp_svr # Build the sample and generate all required artifacts ./move_mcuboot_periphconf_first.sh # Program the initial firmware west flash --build-dir build_no_mcuboot_uart --recover # Program the updated periphconf which has MCUBoot entries first nrfutil device program --firmware build_no_mcuboot_uart/periphconf_final.hex \ --options chip_erase_mode=ERASE_NONE # Reset and verify that app works as expected nrfutil device reset --reset-kind RESET_PIN --traits jlink # Close any UART connection at this point, otherwise next step will fail # Upload the update (takes some minutes) mcumgr --conntype serial --connstring "/dev/ttyACM0,baud=115200" image upload \ build_swapped_uart_v2/zephyr_secondary_app.signed.bin # Open UART to see logs, then trigger a reset nrfutil device reset --reset-kind RESET_PIN --traits jlink Signed-off-by: HÃ¥kon Amundsen --- snippets/swap_uart/snippet.yml | 15 ++++ snippets/swap_uart/use_uart135.overlay | 22 ++++++ snippets/swap_uart/use_uart136.overlay | 22 ++++++ .../upgrade/ref_smp_svr/CMakeLists.txt | 7 ++ .../move_mcuboot_periphconf_first.sh | 61 +++++++++++++++ .../upgrade/ref_smp_svr/periphconf_check.c | 76 +++++++++++++++++++ .../remove_periphconf_duplicates.py | 34 +++++++++ .../upgrade/ref_smp_svr/testcase.yaml | 41 ++++++++++ 8 files changed, 278 insertions(+) create mode 100644 snippets/swap_uart/snippet.yml create mode 100644 snippets/swap_uart/use_uart135.overlay create mode 100644 snippets/swap_uart/use_uart136.overlay create mode 100755 tests/subsys/bootloader/upgrade/ref_smp_svr/move_mcuboot_periphconf_first.sh create mode 100644 tests/subsys/bootloader/upgrade/ref_smp_svr/periphconf_check.c create mode 100755 tests/subsys/bootloader/upgrade/ref_smp_svr/remove_periphconf_duplicates.py diff --git a/snippets/swap_uart/snippet.yml b/snippets/swap_uart/snippet.yml new file mode 100644 index 000000000000..1a9359c6d6cb --- /dev/null +++ b/snippets/swap_uart/snippet.yml @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +name: swap-uart + +boards: + nrf54h20dk/nrf54h20/cpurad: + append: + EXTRA_DTC_OVERLAY_FILE: use_uart136.overlay + nrf54h20dk/nrf54h20/cpuapp: + append: + EXTRA_DTC_OVERLAY_FILE: use_uart135.overlay diff --git a/snippets/swap_uart/use_uart135.overlay b/snippets/swap_uart/use_uart135.overlay new file mode 100644 index 000000000000..ead186121040 --- /dev/null +++ b/snippets/swap_uart/use_uart135.overlay @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/ { + chosen { + zephyr,console = &uart135; + zephyr,shell-uart = &uart135; + zephyr,uart-mcumgr = &uart135; + }; +}; + +&uart135 { + status = "okay"; + memory-regions = <&cpuapp_dma_region>; +}; + +&uart136 { + status = "disabled"; +}; diff --git a/snippets/swap_uart/use_uart136.overlay b/snippets/swap_uart/use_uart136.overlay new file mode 100644 index 000000000000..3bb10fbba528 --- /dev/null +++ b/snippets/swap_uart/use_uart136.overlay @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/ { + chosen { + zephyr,console = &uart136; + zephyr,shell-uart = &uart136; + zephyr,uart-mcumgr = &uart136; + }; +}; + +&uart136 { + status = "okay"; + memory-regions = <&cpurad_dma_region>; +}; + +&uart135 { + status = "disabled"; +}; diff --git a/tests/subsys/bootloader/upgrade/ref_smp_svr/CMakeLists.txt b/tests/subsys/bootloader/upgrade/ref_smp_svr/CMakeLists.txt index dc3c14e504a9..fccfdca3c705 100644 --- a/tests/subsys/bootloader/upgrade/ref_smp_svr/CMakeLists.txt +++ b/tests/subsys/bootloader/upgrade/ref_smp_svr/CMakeLists.txt @@ -15,3 +15,10 @@ set(smp_srv_dir ${ZEPHYR_BASE}/samples/subsys/mgmt/mcumgr/smp_svr) # This project uses orginal sdk-zephyr C source code target_sources(app PRIVATE ${smp_srv_dir}/src/main.c) target_sources_ifdef(CONFIG_MCUMGR_TRANSPORT_BT app PRIVATE ${smp_srv_dir}/src/bluetooth.c) + +zephyr_get(PERIPHCONF_BIN) +if (DEFINED PERIPHCONF_BIN) + target_sources(app PRIVATE periphconf_check.c) + message("Including local periphconf '${PERIPHCONF_BIN}'") + generate_inc_file_for_target(app ${PERIPHCONF_BIN} ${ZEPHYR_BINARY_DIR}/include/generated/periphconf.bin.inc) +endif() diff --git a/tests/subsys/bootloader/upgrade/ref_smp_svr/move_mcuboot_periphconf_first.sh b/tests/subsys/bootloader/upgrade/ref_smp_svr/move_mcuboot_periphconf_first.sh new file mode 100755 index 000000000000..5d6a2a89b064 --- /dev/null +++ b/tests/subsys/bootloader/upgrade/ref_smp_svr/move_mcuboot_periphconf_first.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +# Function to process periphconf for a given build directory +move_mcuboot_periphconf_first() { + local build_dir="$1" + + # Create a bin file with the periphconf for MCUBoot + objcopy --set-section-flags uicr_periphconf_entry=alloc,load,readonly -I elf32-little -O binary \ + -j uicr_periphconf_entry "$build_dir/mcuboot/zephyr/zephyr.elf" "$build_dir/mcuboot_periphconf.bin" + + # Convert the full periphconf.hex to bin... + objcopy -I ihex -O binary "$build_dir/uicr/zephyr/periphconf.hex" "$build_dir/periphconf.bin" + + # ... and concatenate with MCUBoot, placing MCUBoot first + cat "$build_dir/mcuboot_periphconf.bin" "$build_dir/periphconf.bin" > "$build_dir/periphconf_cat.bin" + + # Remove duplicates + bash -c "./remove_periphconf_duplicates.py $build_dir/periphconf_cat.bin > $build_dir/periphconf_cat_nodups.bin" + + # Verify that the processed periphconf matches the original when sorted, this works since the + # original is sorted by regptr address in the first place. + local sorted_digest + local original_digest + + sorted_digest=$(xxd -e -g 4 -c 8 "$build_dir/periphconf_cat_nodups.bin" | awk '{print $2, $3}' | sort | md5sum) + original_digest=$(xxd -e -g 4 -c 8 "$build_dir/periphconf.bin" | awk '{print $2, $3}' | md5sum) + + if [ "$sorted_digest" != "$original_digest" ]; then + echo "ERROR: Digests don't match for $build_dir!" + exit 1 + fi + + # Create a hex file we can program to replace the default periphconf.hex + objcopy -I binary -O ihex --change-address 0x0E1AE000 \ + "$build_dir/periphconf_cat_nodups.bin" "$build_dir/periphconf_final.hex" + + echo "Generated new periphconf with mcuboot first: $build_dir/periphconf_final.hex" +} + +# Build initial image, enable radio, disable UART for MCUBoot +west build -p -b nrf54h20dk/nrf54h20/cpuapp -d build_no_mcuboot_uart -T no_mcuboot_uart . + +# Process periphconf for the initial build (includes digest comparison) +move_mcuboot_periphconf_first "build_no_mcuboot_uart" + +# Build of image with swapped UART, used to get new periphconf +west build -p -b nrf54h20dk/nrf54h20/cpuapp -d build_swapped_uart_v1 -T swapped_uart_v1 . + +# Process periphconf for the swapped UART build +move_mcuboot_periphconf_first "build_swapped_uart_v1" + +# Generate the periphconf that will be contained in the update image +objcopy --input-target=ihex --output-target=binary build_swapped_uart_v1/periphconf_final.hex \ +build_swapped_uart_v1/periphconf_swapped_uart.bin + +# Re-build the swapped image with integrated PERIPHCONF and bumped firmware version +west build -p -b nrf54h20dk/nrf54h20/cpuapp -d build_swapped_uart_v2 -T swapped_uart_v2 . -- \ +-DPERIPHCONF_BIN=build_swapped_uart_v1/periphconf_swapped_uart.bin diff --git a/tests/subsys/bootloader/upgrade/ref_smp_svr/periphconf_check.c b/tests/subsys/bootloader/upgrade/ref_smp_svr/periphconf_check.c new file mode 100644 index 000000000000..cad3de7bb3ae --- /dev/null +++ b/tests/subsys/bootloader/upgrade/ref_smp_svr/periphconf_check.c @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include + +#define PERIPHCONF_NODE DT_NODELABEL(periphconf_partition) +#define MRAM_NODE DT_GPARENT(PERIPHCONF_NODE) +#define MRAM_WORD_SIZE DT_PROP(MRAM_NODE, write_block_size) +#define GLOBAL_PERIPHCONF_ADDR DT_REG_ADDR(MRAM_NODE) + DT_REG_ADDR(PERIPHCONF_NODE) + +/* The global PERIPHCONF blob is the one pointed to by UICR.PERIPHCONF and hence the one which + * will be applied by IronSide SE. + */ +static struct uicr_periphconf_entry *global_periphconf = + (struct uicr_periphconf_entry *)(GLOBAL_PERIPHCONF_ADDR); + +/* The local PERIPHCONF contained within the image firmware blob. This could be out of sync with + * the global PERIPHCONF if a DFU has been updated with a change in PERIPHCONF. + */ +static const uint8_t local_periphconf_inc[] = { +#include +}; + +/* Since we are executing pre-kernel, we cannot use the proper MRAM driver. Hence we rely on + * the blob being MRAM word size aligned since write are committed when the last 4-byte word + * in the 16-byte MRAM word is written. + */ +BUILD_ASSERT(sizeof(local_periphconf_inc) % MRAM_WORD_SIZE == 0, + "PERIPHCONF blob is not MRAM word size aligned."); + +/* Compare the local PERIPHCONF blob with the global one. If an entry is found that differs, copy + * the remaining entries and trigger a restart. No action is taken if the blobs are equal. + */ +int check_and_copy_local_periphconf_to_global(void) +{ + const struct uicr_periphconf_entry *local_periphconf = + (struct uicr_periphconf_entry *)local_periphconf_inc; + const size_t local_periphconf_len = + sizeof(local_periphconf_inc) / sizeof(struct uicr_periphconf_entry); + + for (int i = 0; i < local_periphconf_len; i++) { + if (local_periphconf[i].regptr == 0xFFFFFFFF && + local_periphconf[i].value == 0xFFFFFFFF) { + return 0; + } else if (local_periphconf[i].regptr == global_periphconf[i].regptr && + local_periphconf[i].value == global_periphconf[i].value) { + continue; + } else { + for (int j = i; j < local_periphconf_len; j++) { + sys_write32(local_periphconf[j].regptr, + (uintptr_t)&global_periphconf[j].regptr); + sys_write32(local_periphconf[j].value, + (uintptr_t)&global_periphconf[j].value); + } + + /* IronSide applies the configuration in PERIPHCONF before booting the + * local domains, so a reboot is needed for the changes to be applied. + */ + sys_reboot(SYS_REBOOT_WARM); + + CODE_UNREACHABLE; + + return -1; + } + } + + return 0; +} + +/* Devices are initialized with PRE_KERNEL_1, and this needs to be fixed by then, so use EARLY */ +SYS_INIT(check_and_copy_local_periphconf_to_global, EARLY, 0); diff --git a/tests/subsys/bootloader/upgrade/ref_smp_svr/remove_periphconf_duplicates.py b/tests/subsys/bootloader/upgrade/ref_smp_svr/remove_periphconf_duplicates.py new file mode 100755 index 000000000000..cd36a8a651a9 --- /dev/null +++ b/tests/subsys/bootloader/upgrade/ref_smp_svr/remove_periphconf_duplicates.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Remove duplicate periphconf entries where regptr and value are identical and print to stdout""" + +import ctypes as c +import sys + + +class PeriphconfEntry(c.LittleEndianStructure): + _pack_ = 1 + _fields_ = [("regptr", c.c_uint32), ("value", c.c_uint32)] + + +def main(): + data = open(sys.argv[1], 'rb').read() + num_entries = len(data) // c.sizeof(PeriphconfEntry) + entries = (PeriphconfEntry * num_entries).from_buffer_copy(data) + + seen = set() + unique_entries = [] + for entry in entries: + if entry.regptr == 0xFFFFFFFF: # Don't remove the padding + unique_entries.append(entry) + else: + key = (entry.regptr, entry.value) + if key not in seen: + seen.add(key) + unique_entries.append(entry) + + result = (PeriphconfEntry * len(unique_entries))(*unique_entries) + sys.stdout.buffer.write(bytes(result)) + + +if __name__ == "__main__": + main() diff --git a/tests/subsys/bootloader/upgrade/ref_smp_svr/testcase.yaml b/tests/subsys/bootloader/upgrade/ref_smp_svr/testcase.yaml index 4563a96fc36b..c86b915af8ab 100644 --- a/tests/subsys/bootloader/upgrade/ref_smp_svr/testcase.yaml +++ b/tests/subsys/bootloader/upgrade/ref_smp_svr/testcase.yaml @@ -10,6 +10,47 @@ common: - ci_tests_subsys_bootloader tests: + no_mcuboot_uart: + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP=y + - FILE_SUFFIX=direct_xip + - CONFIG_SOC_NRF54H20_CPURAD_ENABLE=y + - ipc_radio_CONFIG_SERIAL=y + - ipc_radio_CONFIG_UART_CONSOLE=y + - ipc_radio_CONFIG_LOG=y + - mcuboot_CONFIG_SERIAL=n + + swapped_uart_v1: + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP=y + - FILE_SUFFIX=direct_xip + - CONFIG_SOC_NRF54H20_CPURAD_ENABLE=y + - ipc_radio_CONFIG_SERIAL=y + - ipc_radio_CONFIG_UART_CONSOLE=y + - ipc_radio_CONFIG_LOG=y + - mcuboot_CONFIG_SERIAL=n + required_snippets: + - swap-uart + + swapped_uart_v2: + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SB_CONFIG_MCUBOOT_MODE_DIRECT_XIP=y + - FILE_SUFFIX=direct_xip + - CONFIG_SOC_NRF54H20_CPURAD_ENABLE=y + - ipc_radio_CONFIG_SERIAL=y + - ipc_radio_CONFIG_UART_CONSOLE=y + - ipc_radio_CONFIG_LOG=y + - mcuboot_CONFIG_SERIAL=n + - CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="2.2.2+2" + required_snippets: + - swap-uart + mcuboot.upgrade.basic: platform_allow: - nrf52840dk/nrf52840