mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 04:02:54 +00:00
[zephyr_coredump] Store a coredump in flash on nRF52 and print it to the next log reader (#13122)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Copilot
J. Nick Koston
parent
c2c8cb5eb3
commit
5f8f075e76
@@ -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
|
||||
|
||||
@@ -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 <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
@@ -12,12 +15,6 @@
|
||||
#include <esphome/components/zephyr/reset_reason.h>
|
||||
#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<char, zephyr::RESET_REASON_BUFFER_SIZE>(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" {
|
||||
|
||||
@@ -93,6 +93,7 @@ CONFIG_NVS=y
|
||||
CONFIG_SETTINGS=y
|
||||
""",
|
||||
),
|
||||
_TidySubsystem("coredump", "CONFIG_DEBUG_COREDUMP=y\n"),
|
||||
)
|
||||
|
||||
_TIDY_PRJ_CONF = "".join(
|
||||
|
||||
@@ -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)
|
||||
@@ -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 <cinttypes>
|
||||
#include <zephyr/version.h>
|
||||
// The header has no C++ linkage guards of its own
|
||||
extern "C" {
|
||||
#include <zephyr/debug/coredump.h>
|
||||
}
|
||||
|
||||
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<size_t>(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<off_t>(this->offset_), buf,
|
||||
std::min<size_t>(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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
zephyr_coredump:
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
zephyr_coredump: !include common.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
zephyr_coredump: !include common.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
zephyr_coredump: !include common.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
zephyr_coredump: !include common.yaml
|
||||
Reference in New Issue
Block a user