diff --git a/CODEOWNERS b/CODEOWNERS index 65ed44ee30b..539730a4a67 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -727,6 +727,7 @@ esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 +esphome/components/zephyr_coredump/* @tomaszduda23 esphome/components/zephyr_mcumgr/ota/* @tomaszduda23 esphome/components/zephyr_pwm/* @wiomoc esphome/components/zhlt01/* @cfeenstra1024 diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 95cc5d4a14b..c74943940c8 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -3,6 +3,9 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "logger.h" +#ifdef USE_CRASH_HANDLER +#include "esphome/components/zephyr_coredump/coredump.h" +#endif #include #include @@ -12,12 +15,6 @@ #include #endif -namespace esphome::zephyr_coredump { - -__attribute__((weak)) void print_coredump() {} - -} // namespace esphome::zephyr_coredump - namespace esphome::logger { // Zephyr 3.7 renamed z_arch_esf_t to struct arch_esf; the old name was later removed. @@ -56,6 +53,14 @@ void Logger::cdc_loop_() { if (!opened) { App.schedule_dump_config(); } +#ifdef USE_CRASH_HANDLER + // The new reader also gets a coredump stored by the last crash; a closed port keeps it for the next one + if (!opened) { + log_crash_to_client(); + } else { + zephyr_coredump::stop_coredump(); + } +#endif opened = !opened; } #endif @@ -103,7 +108,16 @@ void Logger::pre_setup() { const char *reset_reason = zephyr::get_reset_reason(std::span(reason_buffer)); ESP_LOGI(TAG, "Reset reason: %s", reset_reason); dump_crash_(); - zephyr_coredump::print_coredump(); +#endif +#ifdef USE_CRASH_HANDLER +#ifdef USE_LOGGER_UART_SELECTION_USB_CDC + // Nobody reads a USB port this early; cdc_loop_() reports the dump once the port is opened + if (this->uart_ == UART_SELECTION_USB_CDC) + return; +#endif + // Without a UART (baud_rate: 0, or one that did not come up) nothing reaches the console + if (this->uart_dev_ != nullptr) + log_crash_to_client(); #endif } @@ -195,6 +209,16 @@ void k_sys_fatal_error_handler(unsigned int reason, const FatalErrorEsf *esf) { arch_restart(); } +#ifdef USE_CRASH_HANDLER +bool crash_log_pending() { return zephyr_coredump::coredump_pending(); } +void log_crash_to_client() { + // A level that drops the lines would erase the dump unseen; keep it for a client that can see it + if (global_logger->get_log_level(zephyr_coredump::CRASH_LOG_TAG) < ESPHOME_LOG_LEVEL_ERROR) + return; + zephyr_coredump::print_coredump(); +} +#endif + } // namespace esphome::logger extern "C" { diff --git a/esphome/components/nrf52/clang_tidy.py b/esphome/components/nrf52/clang_tidy.py index d1f95979767..f4afafa0e49 100644 --- a/esphome/components/nrf52/clang_tidy.py +++ b/esphome/components/nrf52/clang_tidy.py @@ -93,6 +93,7 @@ CONFIG_NVS=y CONFIG_SETTINGS=y """, ), + _TidySubsystem("coredump", "CONFIG_DEBUG_COREDUMP=y\n"), ) _TIDY_PRJ_CONF = "".join( diff --git a/esphome/components/zephyr_coredump/__init__.py b/esphome/components/zephyr_coredump/__init__.py new file mode 100644 index 00000000000..cc36ac7dc4c --- /dev/null +++ b/esphome/components/zephyr_coredump/__init__.py @@ -0,0 +1,111 @@ +import esphome.codegen as cg +from esphome.components.nrf52 import NRF52840_FLASH_SIZE, TESTING_FLASH_SIZE +from esphome.components.nrf52.boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG +from esphome.components.zephyr import ( + Section, + zephyr_add_overlay, + zephyr_add_pm_static, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import ( + BOOTLOADER_MCUBOOT, + KEY_BOARD, + KEY_BOOTLOADER, + KEY_PM_STATIC, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID, KEY_CORE, KEY_FRAMEWORK_VERSION +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.types import ConfigType + +CODEOWNERS = ["@tomaszduda23"] +DEPENDENCIES = ["logger"] +DOMAIN = "zephyr_coredump" + +Coredump = cg.esphome_ns.namespace(DOMAIN).class_("Coredump", cg.Component) + +# SDK 2.9.2 and later can store every thread's stack; older ones only the faulting thread +DUMP_THREADS_MIN_VERSION = cv.Version(2, 9, 2) +# Zephyr 4.0 (from SDK 3.0) carries the fault stack pointer fix (zephyr PR 79622) +SP_FIX_MIN_VERSION = cv.Version(3, 0, 0) +# One size for every SDK, so enabling the component moves the OTA image slots the same way everywhere +PARTITION_SIZE = 0x8000 + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(Coredump)}).extend(cv.COMPONENT_SCHEMA), + cv.only_on_nrf52, +) + + +def _framework_version() -> cv.Version: + return CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + + +def _partition_address() -> int: + """Place the partition directly below the lowest section reserved above the + SoftDevice (the bootloader), or at the end of flash with MCUboot.""" + data = zephyr_data() + # The SoftDevice (or MBR) is the only section that can start at 0 + reserved = [s.address for s in data[KEY_PM_STATIC] if s.address > 0] + if reserved: + return min(reserved) - PARTITION_SIZE + if (bootloader := data[KEY_BOOTLOADER]) != BOOTLOADER_MCUBOOT: + raise cv.Invalid( + f"{DOMAIN} does not know the flash layout of bootloader '{bootloader}', " + f"select one of: {', '.join(BOOTLOADER_CONFIG)}" + ) + # MCUboot reserves nothing, so the flash size has to come from the board + board = data[KEY_BOARD] + if "nrf52840" not in board and board not in BOARDS_ZEPHYR: + raise cv.Invalid( + f"{DOMAIN} does not know the flash size of board '{board}', " + "only nRF52840 boards are supported" + ) + flash_size = TESTING_FLASH_SIZE if CORE.testing_mode else NRF52840_FLASH_SIZE + return flash_size - PARTITION_SIZE + + +def _final_validate(config: ConfigType) -> ConfigType: + _partition_address() + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +# Runs before zephyr_mcumgr OTA (OTA_UPDATES), which sizes its image slots below this partition +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) +async def to_code(config: ConfigType) -> None: + # The logger's crash handler hooks: the dump reaches serial, API and MQTT log clients + cg.add_define("USE_CRASH_HANDLER") + zephyr_add_prj_conf("DEBUG_COREDUMP", True) + zephyr_add_prj_conf("DEBUG_COREDUMP_BACKEND_FLASH_PARTITION", True) + framework_ver = _framework_version() + if framework_ver >= DUMP_THREADS_MIN_VERSION: + zephyr_add_prj_conf("DEBUG_COREDUMP_MEMORY_DUMP_THREADS", True) + else: + zephyr_add_prj_conf("DEBUG_COREDUMP_MEMORY_DUMP_MIN", True) + if framework_ver < SP_FIX_MIN_VERSION: + cg.add_define("USE_ZEPHYR_COREDUMP_SP_FIX") + cg.add_build_flag("-Wl,--wrap=z_arm_fatal_error") + cg.add_build_flag("-Wl,--wrap=z_arm_fault") + zephyr_add_prj_conf("PARTITION_MANAGER_ENABLED", True) + addr = _partition_address() + zephyr_add_pm_static( + [Section("coredump_partition", addr, PARTITION_SIZE, "flash_primary")] + ) + zephyr_add_overlay( + f""" + &flash0 {{ + partitions {{ + coredump_partition: partition@{addr:x} {{ + label = "coredump-partition"; + reg = <{addr:#x} {PARTITION_SIZE:#x}>; + }}; + }}; + }}; + """ + ) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/esphome/components/zephyr_coredump/coredump.cpp b/esphome/components/zephyr_coredump/coredump.cpp new file mode 100644 index 00000000000..7bfbcc927a0 --- /dev/null +++ b/esphome/components/zephyr_coredump/coredump.cpp @@ -0,0 +1,191 @@ +#ifdef USE_ZEPHYR +#include "coredump.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include +// The header has no C++ linkage guards of its own +extern "C" { +#include +} + +namespace esphome::zephyr_coredump { + +ESPHOME_LOG_TAG(TAG, "zephyr_coredump"); + +// Lines in the "#CD:" format that Zephyr's coredump_serial_log_parser.py reads: the log header (time, level, +// tag, line number and colour codes, under 64 characters), "#CD:" and two hex characters per byte must fit the +// logger's line buffer, so a small buffer gets shorter lines +static constexpr size_t LINE_OVERHEAD = 64; +static constexpr size_t CHUNK_SIZE = + std::min(64, (ESPHOME_LOGGER_TX_BUFFER_SIZE - LINE_OVERHEAD) / 2 / 16 * 16); +static_assert(CHUNK_SIZE >= 16, "logger line buffer too small for a coredump line"); +// A USB host starts reading the port only some 100 ms after opening it; until then the CDC driver discards output +static constexpr uint32_t START_DELAY_MS = 1000; +static constexpr uint32_t START_DELAY_ID = 0; +// Between lines, so a loop that runs faster than usual (a stepper moving) cannot overrun the USB CDC ring buffer +static constexpr uint32_t LINE_INTERVAL_MS = 10; + +// On a UART the logger reports the reader before this component exists, so the flag outlives the pointer +static Coredump *global_coredump = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool reader_present = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +bool coredump_pending() { return global_coredump != nullptr && global_coredump->stored_; } + +void print_coredump() { + reader_present = true; + if (global_coredump != nullptr) + global_coredump->start_print_(); +} + +void stop_coredump() { + reader_present = false; + if (global_coredump != nullptr) + global_coredump->stop_print_(); +} + +void Coredump::setup() { + global_coredump = this; + this->disable_loop(); + // The size comes from the header alone; "has stored dump" also checks the checksum and would hide a + // corrupt dump instead of letting it be reported and erased + const int size = coredump_query(COREDUMP_QUERY_GET_STORED_DUMP_SIZE, nullptr); + if (size <= 0) { + return; + } + this->stored_ = true; + this->total_ = size; + // A bad dump is reported and erased from loop() like a good one, once somebody can read the message + this->verify_ = coredump_cmd(COREDUMP_CMD_VERIFY_STORED_DUMP, nullptr); + if (reader_present) + this->start_print_(); +} + +// A new reader gets the whole dump: a print cut short by the port closing starts over +void Coredump::start_print_() { + if (!this->stored_ || this->printing_) + return; + this->printing_ = true; + this->offset_ = 0; + this->set_timeout(START_DELAY_ID, START_DELAY_MS, [this]() { this->enable_loop(); }); +} + +void Coredump::stop_print_() { + this->printing_ = false; + this->cancel_timeout(START_DELAY_ID); + this->disable_loop(); +} + +void Coredump::loop() { + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->last_line_ms_ < LINE_INTERVAL_MS) + return; + this->last_line_ms_ = now; + if (this->verify_ != 1) { + ESP_LOGE(TAG, "Stored coredump verification failed: %d", this->verify_); + this->finish_(0); + return; + } + if (this->offset_ == 0) { + ESP_LOGE(TAG, "#CD:BEGIN#"); + } + uint8_t buf[CHUNK_SIZE]; + char hex[format_hex_size(CHUNK_SIZE)]; + coredump_cmd_copy_arg arg{static_cast(this->offset_), buf, + std::min(CHUNK_SIZE, this->total_ - this->offset_)}; + const int ret = coredump_cmd(COREDUMP_CMD_COPY_STORED_DUMP, &arg); + if (ret > 0) { + ESP_LOGE(TAG, "#CD:%s", format_hex_to(hex, buf, ret)); + this->offset_ += ret; + } + if (ret <= 0 || this->offset_ >= this->total_) { + ESP_LOGE(TAG, "#CD:END#"); + this->finish_(ret < 0 ? ret : 0); + } +} + +// Erase so the dump is not reported again; the next crash writes a new one. Erasing the partition halts the +// CPU for about 85 ms per 4 KB page, so this one call takes most of a second +void Coredump::finish_(int err) { + if (err < 0) { + ESP_LOGE(TAG, "Failed to copy coredump: %d", err); + } + const int erase = coredump_cmd(COREDUMP_CMD_ERASE_STORED_DUMP, nullptr); + if (erase < 0) { + ESP_LOGE(TAG, "Failed to erase coredump: %d", erase); + } + this->stored_ = false; + this->total_ = 0; + this->verify_ = 0; + this->printing_ = false; + this->disable_loop(); +} + +void Coredump::dump_config() { + ESP_LOGCONFIG(TAG, + "Coredump:\n" + " Stored dump: %s\n" + " Size: %" PRIu32 "\n" + " Verified: %s\n" + " Error: %d", + YESNO(this->stored_), this->total_, YESNO(this->verify_ == 1), + coredump_query(COREDUMP_QUERY_GET_ERROR, nullptr)); +} + +} // namespace esphome::zephyr_coredump + +// Backport of https://github.com/zephyrproject-rtos/zephyr/pull/79622 (Zephyr 4.0): give GDB the stack +// pointer from before the exception frame so it can unwind. The Python side links it in with --wrap. +#ifdef USE_ZEPHYR_COREDUMP_SP_FIX +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +#if KERNEL_VERSION_NUMBER >= 0x030700 +using esf_t = struct arch_esf; +#else +using esf_t = z_arch_esf_t; +#endif + +static constexpr uint8_t EXC_RETURN_STACK_FRAME_TYPE_POS = 4; +static constexpr uint32_t EXC_RETURN_STACK_FRAME_TYPE_EXTENDED = 0; + +static uint32_t global_exc_return; + +void __real_z_arm_fault(uint32_t msp, uint32_t psp, uint32_t exc_return, _callee_saved_t *callee_regs); +void __wrap_z_arm_fault(uint32_t msp, uint32_t psp, uint32_t exc_return, _callee_saved_t *callee_regs) { + global_exc_return = exc_return; + __real_z_arm_fault(msp, psp, exc_return, callee_regs); +} + +// Faults from z_arm_fault() come here with the frame z_arm_fault() saved; a spurious interrupt has none. +// Software fatal errors (k_panic, k_oops) reach z_arm_fatal_error() inside its own object, which --wrap cannot +// intercept, so their dumps keep Zephyr's stack pointer. +void __real_z_arm_fatal_error(unsigned int reason, const esf_t *esf); +void __wrap_z_arm_fatal_error(unsigned int reason, const esf_t *esf) { +#if defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE) || defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE) + if (esf == nullptr) { + __real_z_arm_fatal_error(reason, esf); + return; + } + z_arm_coredump_fault_sp += sizeof(esf->basic); +#if defined(CONFIG_FPU) && defined(CONFIG_FPU_SHARING) + // The thread used the FP registers, so the frame also holds them + if ((global_exc_return & BIT(EXC_RETURN_STACK_FRAME_TYPE_POS)) == EXC_RETURN_STACK_FRAME_TYPE_EXTENDED) { + z_arm_coredump_fault_sp += sizeof(esf->fpu); + } +#endif +#ifndef CONFIG_ARMV8_M_MAINLINE + // PSR bit 9 set: the frame was padded for alignment + if ((esf->basic.xpsr & SCB_CCR_STKALIGN_Msk) == SCB_CCR_STKALIGN_Msk) { + z_arm_coredump_fault_sp |= 0x4; + } +#endif +#endif + __real_z_arm_fatal_error(reason, esf); +} +} +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +#endif +#endif diff --git a/esphome/components/zephyr_coredump/coredump.h b/esphome/components/zephyr_coredump/coredump.h new file mode 100644 index 00000000000..f56568e3555 --- /dev/null +++ b/esphome/components/zephyr_coredump/coredump.h @@ -0,0 +1,43 @@ +#pragma once +#include "esphome/core/component.h" + +namespace esphome::zephyr_coredump { + +/// The tag the dump is logged under, so a client can check the level in force for it +inline constexpr const char *CRASH_LOG_TAG = "zephyr_coredump"; + +/// Whether a dump from before this boot is waiting to be printed. +bool coredump_pending(); +/// A log reader is present: serial at boot, USB CDC when the port opens, an API or MQTT client subscribing. +/// The stored dump is printed from the start and erased once it has gone out; a print already under way +/// carries on. +void print_coredump(); +/// The reader went away (the USB port was closed); the print stops and the dump stays for the next one. +/// Readers are not told apart: closing the USB port also stops a print an API or MQTT client is receiving, +/// and a client that disconnects does not stop one, the same as the other platforms' crash records. +void stop_coredump(); + +class Coredump : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + + protected: + friend bool coredump_pending(); + friend void print_coredump(); + friend void stop_coredump(); + /// Start printing after a short delay, one line per loop so the USB CDC transmit buffer and the main loop keep up + void start_print_(); + void stop_print_(); + void finish_(int err); + + uint32_t offset_{0}; + uint32_t total_{0}; + uint32_t last_line_ms_{0}; + int verify_{0}; // result of verifying the stored dump, 1 when it is good + bool stored_{false}; + bool printing_{false}; +}; + +} // namespace esphome::zephyr_coredump diff --git a/esphome/components/zephyr_mcumgr/ota/__init__.py b/esphome/components/zephyr_mcumgr/ota/__init__.py index 5b438bbda1c..b6ef932e4f8 100644 --- a/esphome/components/zephyr_mcumgr/ota/__init__.py +++ b/esphome/components/zephyr_mcumgr/ota/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components.nrf52.boards import MCUMGR_OTA_BOOTLOADERS @@ -31,8 +33,11 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv from esphome.types import ConfigType +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@tomaszduda23"] DEPENDENCIES = ["zephyr"] @@ -114,6 +119,12 @@ def _validate_bootloader(config: ConfigType) -> None: def _final_validate(config: ConfigType) -> None: _validate_ble_server(config) _validate_bootloader(config) + if "zephyr_coredump" in fv.full_config.get(): + _LOGGER.warning( + "zephyr_coredump moves the OTA image slots. A device that already runs mcumgr OTA " + "needs one flash over USB after enabling or disabling it; an OTA update alone leaves " + "the installed MCUboot with the old slot layout" + ) if (uart := config[CONF_TRANSPORT].get(CONF_HARDWARE_UART)) is not None: cdc_id = UARTS[uart][1] if cdc_id >= 0: @@ -210,17 +221,18 @@ async def to_code(config: ConfigType) -> None: bootloader = zephyr_data()[KEY_BOOTLOADER] if bootloader != BOOTLOADER_MCUBOOT: - # Derive partition addresses from the SoftDevice and bootloader sections - # registered with the Partition Manager (moved in testing mode) so that - # the DTS flash map matches what it produces: - # MCUboot sits immediately after the SoftDevice, then slot0, then slot1. + # Derive partition addresses from the sections registered with the + # Partition Manager (moved in testing mode) so that the DTS flash map + # matches what it produces: + # MCUboot sits immediately after the SoftDevice, then slot0, then slot1, + # which ends at the lowest section reserved above (bootloader, coredump). sections = zephyr_data()[KEY_PM_STATIC] mcuboot_size = 0x9000 - sd_end = next(s.address + s.size for s in sections if "SoftDevice" in s.name) - bl_start = next(s.address for s in sections if "Adafruit" in s.name) + sd_end = next(s.end_address for s in sections if "SoftDevice" in s.name) + top = min(s.address for s in sections if s.address >= sd_end) slot0_start = sd_end + mcuboot_size # Align slot size down to a 4 KB sector boundary - slot_size = ((bl_start - slot0_start) // 2 // 0x1000) * 0x1000 + slot_size = ((top - slot0_start) // 2 // 0x1000) * 0x1000 slot1_start = slot0_start + slot_size def _mcuboot_partition_overlay() -> str: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index de4dfd2607b..331a637b6b3 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -756,6 +756,7 @@ #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 +#define USE_ZEPHYR_COREDUMP_SP_FIX #define USE_ZIGBEE #define USE_ZIGBEE_WIPE_ON_BOOT #define USE_ZIGBEE_WIPE_ON_BOOT_MAGIC 1 diff --git a/tests/component_tests/zephyr_coredump/__init__.py b/tests/component_tests/zephyr_coredump/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/zephyr_coredump/config/mcumgr_adafruit.yaml b/tests/component_tests/zephyr_coredump/config/mcumgr_adafruit.yaml new file mode 100644 index 00000000000..cb67b8ef9f7 --- /dev/null +++ b/tests/component_tests/zephyr_coredump/config/mcumgr_adafruit.yaml @@ -0,0 +1,15 @@ +esphome: + name: coredumptest + +nrf52: + board: adafruit_itsybitsy_nrf52840 + bootloader: adafruit_nrf52_sd140_v6 + +logger: + +zephyr_ble_server: + +ota: + - platform: zephyr_mcumgr + +zephyr_coredump: diff --git a/tests/component_tests/zephyr_coredump/test_zephyr_coredump.py b/tests/component_tests/zephyr_coredump/test_zephyr_coredump.py new file mode 100644 index 00000000000..19252188cb8 --- /dev/null +++ b/tests/component_tests/zephyr_coredump/test_zephyr_coredump.py @@ -0,0 +1,104 @@ +"""Tests for the placement of the zephyr_coredump flash partition.""" + +from collections.abc import Callable +import logging +from pathlib import Path +import re + +import pytest + +from esphome.components.nrf52 import set_core_data +from esphome.components.zephyr import zephyr_data +from esphome.components.zephyr.const import KEY_OVERLAY, KEY_PM_STATIC +from esphome.components.zephyr_coredump import ( + DUMP_THREADS_MIN_VERSION, + PARTITION_SIZE, + _partition_address, +) +import esphome.config_validation as cv +from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION +from esphome.core import CORE + + +def _core_data( + board: str, + bootloader: str, + testing_mode: bool = False, + version: cv.Version = DUMP_THREADS_MIN_VERSION, +) -> None: + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: version} + CORE.testing_mode = testing_mode + set_core_data({"board": board, "bootloader": bootloader}) + + +@pytest.mark.parametrize( + ("board", "bootloader", "testing_mode", "expected"), + [ + # Below the Adafruit bootloader at 0xF4000 + ("adafruit_itsybitsy_nrf52840", "adafruit_nrf52_sd140_v6", False, 0xEC000), + # Below the factory Open DFU bootloader at 0xE0000 + ("nrf52840dongle", "nrf", False, 0xD8000), + # Nothing reserved: the end of the 1 MB flash + ("adafruit_feather_nrf52840", "mcuboot", False, 0xF8000), + # An nRF52840 board whose name does not say so + ("xiao_ble", "mcuboot", False, 0xF8000), + # Testing mode moves the bootloader to the end of the faked 4 MB flash + ("adafruit_itsybitsy_nrf52840", "adafruit_nrf52_sd140_v6", True, 0x3EC000), + ("adafruit_feather_nrf52840", "mcuboot", True, 0x3F8000), + ], +) +def test_partition_address( + board: str, bootloader: str, testing_mode: bool, expected: int +) -> None: + _core_data(board, bootloader, testing_mode) + assert _partition_address() == expected + assert PARTITION_SIZE % 0x1000 == 0 + + +def test_partition_size_is_the_same_on_every_sdk() -> None: + _core_data("adafruit_feather_nrf52840", "mcuboot", version=cv.Version(2, 6, 1)) + assert _partition_address() == 0x100000 - PARTITION_SIZE + + +def test_unknown_flash_size_rejected() -> None: + _core_data("bbc_microbit", "mcuboot") + with pytest.raises(cv.Invalid, match="only nRF52840 boards are supported"): + _partition_address() + + +def test_bootloader_without_layout_rejected() -> None: + """The generic adafruit bootloader registers no sections, so the partition would + land inside the bootloader if it were treated like MCUboot.""" + _core_data("adafruit_itsybitsy_nrf52840", "adafruit") + with pytest.raises(cv.Invalid, match="flash layout of bootloader 'adafruit'"): + _partition_address() + + +def test_mcumgr_slot_ends_below_partition( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The OTA image slot1 must end at or before the coredump partition.""" + generate_main(component_config_path("mcumgr_adafruit.yaml")) + # The logger's crash handler hooks are compiled in for the dump + assert any(d.name == "USE_CRASH_HANDLER" for d in CORE.defines) + sections = {s.name: s for s in zephyr_data()[KEY_PM_STATIC]} + coredump = sections["coredump_partition"] + assert coredump.address == 0xEC000 + match = re.search( + r"slot1_partition: partition@\w+ \{\s*reg = <(0x\w+) (0x\w+)>", + zephyr_data()[KEY_OVERLAY][""], + ) + assert match is not None + start, size = (int(v, 16) for v in match.groups()) + assert start + size <= coredump.address + + +def test_mcumgr_warns_that_the_slots_move( + caplog: pytest.LogCaptureFixture, + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + with caplog.at_level(logging.WARNING): + generate_main(component_config_path("mcumgr_adafruit.yaml")) + assert "zephyr_coredump moves the OTA image slots" in caplog.text diff --git a/tests/components/zephyr_coredump/common.yaml b/tests/components/zephyr_coredump/common.yaml new file mode 100644 index 00000000000..e8d8a8b8954 --- /dev/null +++ b/tests/components/zephyr_coredump/common.yaml @@ -0,0 +1 @@ +zephyr_coredump: diff --git a/tests/components/zephyr_coredump/test.nrf52-adafruit.yaml b/tests/components/zephyr_coredump/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..1f0aefac131 --- /dev/null +++ b/tests/components/zephyr_coredump/test.nrf52-adafruit.yaml @@ -0,0 +1,2 @@ +packages: + zephyr_coredump: !include common.yaml diff --git a/tests/components/zephyr_coredump/test.nrf52-mcumgr.yaml b/tests/components/zephyr_coredump/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..1f0aefac131 --- /dev/null +++ b/tests/components/zephyr_coredump/test.nrf52-mcumgr.yaml @@ -0,0 +1,2 @@ +packages: + zephyr_coredump: !include common.yaml diff --git a/tests/components/zephyr_coredump/test.nrf52-nrf52840dongle.yaml b/tests/components/zephyr_coredump/test.nrf52-nrf52840dongle.yaml new file mode 100644 index 00000000000..1f0aefac131 --- /dev/null +++ b/tests/components/zephyr_coredump/test.nrf52-nrf52840dongle.yaml @@ -0,0 +1,2 @@ +packages: + zephyr_coredump: !include common.yaml diff --git a/tests/components/zephyr_coredump/test.nrf52-xiao-ble.yaml b/tests/components/zephyr_coredump/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..1f0aefac131 --- /dev/null +++ b/tests/components/zephyr_coredump/test.nrf52-xiao-ble.yaml @@ -0,0 +1,2 @@ +packages: + zephyr_coredump: !include common.yaml