From 4e7c664455e19c677f97d439b3c46e0cd64acd69 Mon Sep 17 00:00:00 2001 From: robert Date: Wed, 9 Sep 2026 15:52:19 +0300 Subject: [PATCH] Add generic mg_health report for mdash --- mongoose.c | 384 +++++++++++++++++- mongoose.h | 189 +++++---- src/health.c | 22 +- src/health.h | 114 ++---- src/health_cortex.c | 344 ++++++++++++++++ src/health_cortex.h | 77 ++++ src/mdash.c | 14 +- test/Makefile | 2 +- .../stm32/nucleo-h723zg/minimal/Makefile | 18 +- .../nucleo-h723zg/minimal/fake_target.py | 164 ++++++++ tutorials/stm32/nucleo-h723zg/minimal/hal.c | 1 + tutorials/stm32/nucleo-h723zg/minimal/link.ld | 15 +- tutorials/stm32/nucleo-h723zg/minimal/main.c | 75 +--- .../nucleo-h723zg/minimal/mongoose_config.h | 7 +- .../nucleo-h723zg/minimal/stacktrace.gdb | 14 + .../nucleo-h723zg/minimal/stacktrace.txt | 15 + 16 files changed, 1225 insertions(+), 230 deletions(-) create mode 100644 src/health_cortex.c create mode 100644 src/health_cortex.h create mode 100644 tutorials/stm32/nucleo-h723zg/minimal/fake_target.py create mode 100644 tutorials/stm32/nucleo-h723zg/minimal/stacktrace.gdb create mode 100644 tutorials/stm32/nucleo-h723zg/minimal/stacktrace.txt diff --git a/mongoose.c b/mongoose.c index 51b54f57..505ae6d6 100644 --- a/mongoose.c +++ b/mongoose.c @@ -3604,9 +3604,373 @@ struct mg_fs mg_fs_posix = {p_stat, p_list, p_open, p_close, p_read, #endif -// The one health record. Lives in RAM that survives a warm reset, see -// MG_HEALTH_RAM and the .mg_health region in the linker script -struct mg_health mg_health_record MG_HEALTH_RAM; + +struct mg_health mg_health_record; + +void mg_health_init(void) { + static const char magic[] = MG_HEALTH_MAGIC; + memset(&mg_health_record, 0, sizeof(mg_health_record)); + memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic)); +#if MG_HEALTH == MG_HEALTH_CORTEX + mg_health_cortex_init(&mg_health_cortex); + mg_health_record.get_blob = mg_health_cortex_get_blob; + mg_health_record.fn_data = &mg_health_cortex; +#endif +} + +struct mg_str mg_health_get_blob(void) { + return !mg_health_valid() || mg_health_record.get_blob == NULL + ? mg_str("") + : mg_health_record.get_blob(mg_health_record.fn_data); +} + +#ifdef MG_ENABLE_LINES +#line 1 "src/health_cortex.c" +#endif + + + +#if MG_HEALTH == MG_HEALTH_CORTEX + +#define MG_SCB_CPUID (*(volatile uint32_t *) 0xe000ed00) +#define MG_SCB_CFSR (*(volatile uint32_t *) 0xe000ed28) +#define MG_SCB_HFSR (*(volatile uint32_t *) 0xe000ed2c) +#define MG_SCB_DFSR (*(volatile uint32_t *) 0xe000ed30) +#define MG_SCB_MMFAR (*(volatile uint32_t *) 0xe000ed34) +#define MG_SCB_BFAR (*(volatile uint32_t *) 0xe000ed38) +#define MG_SCB_AFSR (*(volatile uint32_t *) 0xe000ed3c) +#define MG_SCB_ABFSR (*(volatile uint32_t *) 0xe000efa8) + +#define MG_CPU_CORTEX_M0 0xc20 +#define MG_CPU_CORTEX_M23 0xd20 +#define MG_CPU_CORTEX_M3 0xc23 +#define MG_CPU_CORTEX_M4 0xc24 +#define MG_CPU_CORTEX_M7 0xc27 +#define MG_CPU_CORTEX_M0P 0xc60 +#define MG_CPU_CORTEX_M33 0xd21 +#define MG_CPU_CORTEX_M35P 0xd31 +#define MG_CPU_CORTEX_M52 0xd24 +#define MG_CPU_CORTEX_M55 0xd22 +#define MG_CPU_CORTEX_M85 0xd23 + +extern uint32_t __StackLimit __attribute__((weak)); +extern uint32_t __StackTop __attribute__((weak)); +extern uint32_t _sstack __attribute__((weak)); +extern uint32_t _estack __attribute__((weak)); +extern bool mg_health_cortex_stack_range(const void *, uintptr_t *, uintptr_t *) + __attribute__((weak)); + +static struct mg_health_cortex_record s_record; +static char s_report[MG_HEALTH_CORTEX_REPORT_SIZE]; + +static bool storage_init(void *storage, size_t size) { + // Note: actual init is handled in main() + (void) storage; + (void) size; + return true; +} + +static bool storage_read(const void *storage, size_t offset, void *buf, + size_t len) { + volatile const uint8_t *src = (volatile const uint8_t *) storage + offset; + uint8_t *dst = (uint8_t *) buf; + size_t i; + for (i = 0; i < len; i++) dst[i] = src[i]; + return true; +} + +static bool storage_write(void *storage, size_t offset, const void *buf, + size_t len) { + volatile uint8_t *dst = (volatile uint8_t *) storage + offset; + const uint8_t *src = (const uint8_t *) buf; + size_t i; + for (i = 0; i < len; i++) dst[i] = src[i]; + return true; +} + +struct mg_health_cortex mg_health_cortex = { + MG_HEALTH_CORTEX_STORAGE, + MG_HEALTH_CORTEX_STORAGE_SIZE, + storage_init, + storage_read, + storage_write, +}; + +static bool storage_bounds(const struct mg_health_cortex *ctx, size_t offset, + size_t len) { + return ctx != NULL && ctx->storage != NULL && offset <= ctx->storage_size && + len <= ctx->storage_size - offset; +} + +static bool storage_configured(const struct mg_health_cortex *ctx) { + return ctx != NULL && ctx->storage != NULL && ctx->storage_size != 0 && + ctx->storage_size >= sizeof(struct mg_health_cortex_record) && + ctx->init != NULL && ctx->read != NULL && ctx->write != NULL; +} + +static bool record_read(struct mg_health_cortex *ctx, + struct mg_health_cortex_record *record) { + static const char magic[] = MG_HEALTH_MAGIC; + if (ctx == NULL || ctx->read == NULL || + !storage_bounds(ctx, 0, sizeof(*record)) || + !ctx->read(ctx->storage, 0, record, sizeof(*record))) { + return false; + } + return memcmp(record->magic, magic, sizeof(record->magic)) == 0 && + record->version == MG_HEALTH_CORTEX_VERSION && + record->stack_word_count <= MG_HEALTH_CORTEX_STACK_WORDS; +} + +static bool record_write(struct mg_health_cortex *ctx, + const struct mg_health_cortex_record *record) { + char invalid[sizeof(record->magic)] = {0}; + size_t offset = sizeof(record->magic); + if (ctx == NULL || ctx->write == NULL || + !storage_bounds(ctx, 0, sizeof(*record)) || + !ctx->write(ctx->storage, 0, &invalid, sizeof(invalid))) { + return false; + } + if (!ctx->write(ctx->storage, offset, (const uint8_t *) record + offset, + sizeof(*record) - offset)) { + return false; + } + if (!ctx->write(ctx->storage, 0, record->magic, sizeof(record->magic))) + return false; + return true; +} + +static bool stack_range(const void *ptr, uintptr_t *lo, uintptr_t *hi) { + if (mg_health_cortex_stack_range != NULL && + mg_health_cortex_stack_range(ptr, lo, hi)) { + return *lo < *hi; + } else if (&__StackLimit != NULL && &__StackTop != NULL) { + *lo = (uintptr_t) &__StackLimit; + *hi = (uintptr_t) &__StackTop; + } else if (&_sstack != NULL && &_estack != NULL) { + *lo = (uintptr_t) &_sstack; + *hi = (uintptr_t) &_estack; + } else { + return false; + } + return *lo < *hi; +} + +static int stack_valid(const void *ptr, size_t words) { + uintptr_t lo, hi, p = (uintptr_t) ptr; + if ((p & 3U) != 0) return 0; + if (!stack_range(ptr, &lo, &hi)) return -1; + return p >= lo && p <= hi && words <= (hi - p) / sizeof(uint32_t); +} + +static bool has_fault_status(uint32_t part) { + return part == MG_CPU_CORTEX_M3 || part == MG_CPU_CORTEX_M4 || + part == MG_CPU_CORTEX_M7 || part == MG_CPU_CORTEX_M33 || + part == MG_CPU_CORTEX_M35P || part == MG_CPU_CORTEX_M52 || + part == MG_CPU_CORTEX_M55 || part == MG_CPU_CORTEX_M85; +} + +static const char *core_name(uint32_t part) { + switch (part) { + case MG_CPU_CORTEX_M0: + return "cortex-m0"; + case MG_CPU_CORTEX_M0P: + return "cortex-m0+"; + case MG_CPU_CORTEX_M23: + return "cortex-m23"; + case MG_CPU_CORTEX_M3: + return "cortex-m3"; + case MG_CPU_CORTEX_M4: + return "cortex-m4"; + case MG_CPU_CORTEX_M7: + return "cortex-m7"; + case MG_CPU_CORTEX_M33: + return "cortex-m33"; + case MG_CPU_CORTEX_M35P: + return "cortex-m35p"; + case MG_CPU_CORTEX_M52: + return "cortex-m52"; + case MG_CPU_CORTEX_M55: + return "cortex-m55"; + case MG_CPU_CORTEX_M85: + return "cortex-m85"; + default: + return "unknown"; + } +} + +static const char *arch_name(uint32_t part) { + if (part == MG_CPU_CORTEX_M0 || part == MG_CPU_CORTEX_M0P) return "armv6-m"; + if (part == MG_CPU_CORTEX_M3) return "armv7-m"; + if (part == MG_CPU_CORTEX_M4 || part == MG_CPU_CORTEX_M7) return "armv7e-m"; + return "armv8-m"; +} + +static bool json_append(char *buf, size_t size, size_t *offset, const char *fmt, + ...) { + va_list ap; + size_t available, n; + if (*offset >= size) return false; + available = size - *offset; + va_start(ap, fmt); + n = mg_vsnprintf(buf + *offset, available, fmt, &ap); + va_end(ap); + if (n >= available) { + *offset = size; + return false; + } + *offset += n; + return true; +} + +void mg_health_cortex_init(struct mg_health_cortex *ctx) { + if (storage_configured(ctx)) { + (void) ctx->init(ctx->storage, ctx->storage_size); + } +} + +struct mg_str mg_health_cortex_get_blob(void *data) { + struct mg_health_cortex *ctx = (struct mg_health_cortex *) data; + struct mg_health_cortex_record *r = &s_record; + uint32_t part; + size_t n = 0, i; + bool ok; + + if (!storage_configured(ctx)) { + return mg_str( + "{\"valid\":false,\"error\":\"health storage is not configured\"}"); + } + if (!record_read(ctx, r)) return mg_str("{\"valid\":false}"); + part = (r->cpuid >> 4) & 0xfffU; + ok = json_append( + s_report, sizeof(s_report), &n, + "{\"valid\":true,\"version\":%lu,\"arch\":\"%s\"," + "\"core\":\"%s\",\"image\":\"firmware.elf\"," + "\"map\":\"firmware.elf.map\",\"binary\":\"firmware.bin\"," + "\"cpuid\":\"0x%08lx\",\"exception_sp\":\"0x%08lx\"," + "\"exc_return\":\"0x%08lx\",\"exception_valid\":%s," + "\"stack_valid\":%s,\"regs\":{" + "\"r0\":\"0x%08lx\",\"r1\":\"0x%08lx\"," + "\"r2\":\"0x%08lx\",\"r3\":\"0x%08lx\"," + "\"r4\":\"0x%08lx\",\"r5\":\"0x%08lx\"," + "\"r6\":\"0x%08lx\",\"r7\":\"0x%08lx\"," + "\"r8\":\"0x%08lx\",\"r9\":\"0x%08lx\"," + "\"r10\":\"0x%08lx\",\"r11\":\"0x%08lx\"," + "\"r12\":\"0x%08lx\",\"sp\":\"0x%08lx\"," + "\"lr\":\"0x%08lx\",\"pc\":\"0x%08lx\"," + "\"xpsr\":\"0x%08lx\"},\"fault\":{" + "\"cfsr\":\"0x%08lx\",\"hfsr\":\"0x%08lx\"," + "\"dfsr\":\"0x%08lx\",\"afsr\":\"0x%08lx\"," + "\"mmfar\":\"0x%08lx\",\"bfar\":\"0x%08lx\"," + "\"abfsr\":\"0x%08lx\"},\"stack\":{" + "\"addr\":\"0x%08lx\",\"word_size\":4,\"words\":[", + (unsigned long) r->version, arch_name(part), core_name(part), + (unsigned long) r->cpuid, (unsigned long) r->exception_sp, + (unsigned long) r->exc_return, r->exception_valid > 0 ? "true" : "false", + r->stack_valid > 0 ? "true" : "false", (unsigned long) r->r0, + (unsigned long) r->r1, (unsigned long) r->r2, (unsigned long) r->r3, + (unsigned long) r->r4, (unsigned long) r->r5, (unsigned long) r->r6, + (unsigned long) r->r7, (unsigned long) r->r8, (unsigned long) r->r9, + (unsigned long) r->r10, (unsigned long) r->r11, (unsigned long) r->r12, + (unsigned long) r->sp, (unsigned long) r->lr, (unsigned long) r->pc, + (unsigned long) r->psr, (unsigned long) r->cfsr, (unsigned long) r->hfsr, + (unsigned long) r->dfsr, (unsigned long) r->afsr, + (unsigned long) r->mmfar, (unsigned long) r->bfar, + (unsigned long) r->abfsr, (unsigned long) r->sp); + for (i = 0; ok && i < r->stack_word_count; i++) { + ok = json_append(s_report, sizeof(s_report), &n, "%s\"0x%08lx\"", + i == 0 ? "" : ",", (unsigned long) r->stack_words[i]); + } + if (ok) ok = json_append(s_report, sizeof(s_report), &n, "]}}"); + return ok ? mg_str_n(s_report, n) + : mg_str("{\"valid\":false,\"error\":\"JSON overflow\"}"); +} + +static void __attribute__((used)) mg_health_cortex_handler( + uint32_t *exception_stack, uint32_t lr, + const uint32_t *callee_saved) { + static const char magic[] = MG_HEALTH_MAGIC; + uint32_t cpuid = MG_SCB_CPUID; + uint32_t part = (cpuid >> 4) & 0xfffU; + size_t fp_words = (lr & (1U << 4)) == 0 ? 18U : 0U; + int valid = stack_valid(exception_stack, fp_words + 8U); + struct mg_health_cortex_record *r = &s_record; + volatile uint32_t *words = (volatile uint32_t *) r; + size_t i; + + for (i = 0; i < sizeof(*r) / sizeof(*words); i++) words[i] = 0; + r->version = MG_HEALTH_CORTEX_VERSION; + r->cpuid = cpuid; + r->exception_sp = (uint32_t) (uintptr_t) exception_stack; + r->exc_return = lr; + r->exception_valid = valid; + r->r4 = callee_saved[0]; + r->r5 = callee_saved[1]; + r->r6 = callee_saved[2]; + r->r7 = callee_saved[3]; + r->r8 = callee_saved[4]; + r->r9 = callee_saved[5]; + r->r10 = callee_saved[6]; + r->r11 = callee_saved[7]; + if (valid > 0) { + uint32_t *core_frame = exception_stack + fp_words; + uint32_t *fault_sp; + uintptr_t lo, hi, p; + size_t count; + r->r0 = core_frame[0]; + r->r1 = core_frame[1]; + r->r2 = core_frame[2]; + r->r3 = core_frame[3]; + r->r12 = core_frame[4]; + r->lr = core_frame[5]; + r->pc = core_frame[6]; + r->psr = core_frame[7]; + fault_sp = core_frame + 8; + if (r->psr & (1U << 9)) fault_sp++; + r->sp = (uint32_t) (uintptr_t) fault_sp; + r->stack_valid = stack_valid(fault_sp, 0); + p = (uintptr_t) fault_sp; + if (r->stack_valid > 0 && stack_range(fault_sp, &lo, &hi)) { + (void) lo; + count = (hi - p) / sizeof(uint32_t); + if (count > MG_HEALTH_CORTEX_STACK_WORDS) + count = MG_HEALTH_CORTEX_STACK_WORDS; + r->stack_word_count = (uint32_t) count; + for (i = 0; i < count; i++) r->stack_words[i] = fault_sp[i]; + } + } + if (has_fault_status(part)) { + r->cfsr = MG_SCB_CFSR; + r->hfsr = MG_SCB_HFSR; + r->dfsr = MG_SCB_DFSR; + r->afsr = MG_SCB_AFSR; + r->bfar = MG_SCB_BFAR; + r->mmfar = MG_SCB_MMFAR; + if (part == MG_CPU_CORTEX_M7) r->abfsr = MG_SCB_ABFSR; + } + memcpy(r->magic, magic, sizeof(r->magic)); + (void) record_write(&mg_health_cortex, r); + for (;;) (void) 0; +} + +void HardFault_Handler(void); +__attribute__((naked)) void HardFault_Handler(void) { + __asm volatile( + "mov r0, lr \n" + "movs r1, #4 \n" + "tst r0, r1 \n" + "beq 1f \n" + "mrs r0, psp \n" + "b 2f \n" + "1: \n" + "mrs r0, msp \n" + "2: \n" + "mov r1, lr \n" + "stmdb sp!, {r4-r11} \n" + "mov r2, sp \n" + "b mg_health_cortex_handler \n"); +} + +#endif #ifdef MG_ENABLE_LINES #line 1 "src/http.c" @@ -7192,11 +7556,15 @@ static void mg_mdash_fn(struct mg_connection *c, int ev, void *ev_data) { } static void mg_mdash_rpc_get_info(struct mg_rpc_req *r) { - mg_rpc_ok(r, "{%m:%m,%m:%llu,%m:%m,%m:\"mws.%d\"}", MG_ESC("fw_version"), - MG_ESC(MG_MDASH_FIRMWARE_VERSION), MG_ESC("uptime"), - (uint64_t) (mg_millis() / 1000), MG_ESC("reboot_reason"), - MG_ESC(mg_health_reason_str(mg_health_reason())), MG_ESC("arch"), - MG_ARCH); + struct mg_str blob = mg_health_get_blob(); + if (blob.buf == NULL) blob.len = 0; + mg_rpc_ok(r, "{%m:%m,%m:%llu,%m:%m,%m:\"mws.%d\",%m:{%m:%m,%m:%llu,%m:%m}}", + MG_ESC("fw_version"), MG_ESC(MG_MDASH_FIRMWARE_VERSION), MG_ESC("uptime"), + (uint64_t) (mg_millis() / 1000), MG_ESC("reboot_reason"), + MG_ESC(mg_health_reason_str(mg_health_reason())), MG_ESC("arch"), MG_ARCH, + MG_ESC("health"), MG_ESC("encoding"), MG_ESC("base64"), MG_ESC("size"), + (uint64_t) blob.len, MG_ESC("data"), mg_print_base64, (int) blob.len, + (uint8_t *) (blob.buf == NULL ? "" : blob.buf)); } static void mg_mdash_rpc_ota_begin(struct mg_rpc_req *r) { diff --git a/mongoose.h b/mongoose.h index 13bd8fec..c76b850d 100644 --- a/mongoose.h +++ b/mongoose.h @@ -4945,44 +4945,25 @@ void mg_ota_device_id(char *buf, size_t len); void mg_ota_poll(struct mg_mgr *); // Device health monitoring // -// Keeps a small record in a RAM region that survives a warm reset, so a device -// can tell why it restarted. The record holds a boot counter, the uptime this -// boot has reached, and the reset reason reported by the hardware. A crash -// loop, a hang loop and a marginal power supply each leave a different mark. -// -// Put MG_HEALTH_INIT() in main(), right after clock and memory init: -// -// ```c -// int main(void) { -// hal_clock_init(); -// MG_HEALTH_INIT(); -// ... -// } -// ``` -// -// The record must live in RAM that startup code neither copies nor zeroes. -// See tutorials/stm32/nucleo-h723zg/minimal/link.ld for the .mg_health region, -// and tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h for the -// reset-reason hooks. -// -// On Cortex-M, a fault handler can record the crash: it sets reset_reason to -// MG_HEALTH_RESET_FAULT and walks the stack into backtrace[]. See -// tutorials/stm32/nucleo-h723zg/minimal/main.c for a sample HardFault_Handler. +// A health provider owns the report format and returns it as an opaque blob. +// Applications and transports use mg_health_get_blob() without knowing how the +// report was collected or encoded. +#define MG_HEALTH_NONE 0 +#define MG_HEALTH_CORTEX 1 + +#ifndef MG_HEALTH +#define MG_HEALTH MG_HEALTH_NONE +#endif + #ifndef MG_HEALTH_MAGIC #define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version #endif -// Places the record in RAM that survives a reset. Define in mongoose_config.h -// to match the linker script, e.g. __attribute__((section(".mg_health"))) -#ifndef MG_HEALTH_RAM -#define MG_HEALTH_RAM -#endif - // Why the device restarted. Hardware usually reports several flags at once, // so these are ordered by how much they tell you: the most specific cause a // device reports wins @@ -4997,15 +4978,16 @@ enum mg_health_reason { MG_HEALTH_RESET_FAULT // Fault handler ran, see regs[] and saved_stack }; -#define MG_HEALTH_BACKTRACE 20 // Return addresses in mg_health::backtrace +typedef struct mg_str mg_health_blob; struct mg_health { - char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data - uint32_t reset_reason; // enum mg_health_reason that started this boot - uint32_t backtrace[MG_HEALTH_BACKTRACE]; // Crash backtrace, frame 0 first + char magic[4]; + mg_health_blob (*get_blob)(void *); + void *fn_data; + uint32_t reset_reason; }; -extern struct mg_health mg_health_record; // Defined in health.c +extern struct mg_health mg_health_record; // Return true if the record holds valid data, i.e. it survived the reset and // was written by this firmware layout @@ -5015,6 +4997,14 @@ static inline bool mg_health_valid(void) { sizeof(mg_health_record.magic)) == 0; } +// Select and initialise the configured health provider. Call after +// initializing the board-specific persistent-storage +void mg_health_init(void); + +// Return the provider's opaque report. The returned memory is provider-owned +// and remains valid until the next call to this function +struct mg_str mg_health_get_blob(void); + static inline int mg_health_reason(void) { return mg_health_valid() ? (int) mg_health_record.reset_reason : MG_HEALTH_RESET_UNKNOWN; @@ -5022,48 +5012,103 @@ static inline int mg_health_reason(void) { static inline const char *mg_health_reason_str(int reason) { switch (reason) { - case MG_HEALTH_RESET_POWER: return "power"; - case MG_HEALTH_RESET_BROWNOUT: return "brownout"; - case MG_HEALTH_RESET_PIN: return "pin"; - case MG_HEALTH_RESET_SOFTWARE: return "software"; - case MG_HEALTH_RESET_LOWPOWER: return "lowpower"; - case MG_HEALTH_RESET_WATCHDOG: return "watchdog"; - case MG_HEALTH_RESET_FAULT: return "fault"; - default: return "unknown"; + case MG_HEALTH_RESET_POWER: + return "power"; + case MG_HEALTH_RESET_BROWNOUT: + return "brownout"; + case MG_HEALTH_RESET_PIN: + return "pin"; + case MG_HEALTH_RESET_SOFTWARE: + return "software"; + case MG_HEALTH_RESET_LOWPOWER: + return "lowpower"; + case MG_HEALTH_RESET_WATCHDOG: + return "watchdog"; + case MG_HEALTH_RESET_FAULT: + return "fault"; + default: + return "unknown"; } } -// Start a new boot: validate the record, count the boot, store the reset -// reason. The current boot moves to prev_* slots first, so the previous -// boot's uptime and reason stay readable. Called by MG_HEALTH_INIT() -static inline void mg_health_init(void) { - char magic[] = MG_HEALTH_MAGIC; - // struct mg_health *h = mg_health_get(); - if (!mg_health_valid()) { // First boot, or RAM lost its contents - memset(&mg_health_record, 0, sizeof(mg_health_record)); - memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic)); - } - // h->prev_uptime = h->uptime; - // h->prev_reason = h->reason; - // h->counter++; - // h->uptime = 0; - // h->reason = mg_health_decode_reason(); - // MG_HEALTH_RESET_CLEAR(); -} - -// // Record that the main loop is still alive. Called from mg_mgr_poll(), so a -// // hung application leaves uptime frozen at the hang -// extern uint64_t mg_health_next_ms; // Defined in health.c - -// static inline void mg_health_uptime(uint64_t now_ms) { -// struct mg_health *h = mg_health_get(); -// if (now_ms >= mg_health_next_ms && mg_health_valid()) { -// mg_health_next_ms = now_ms + 1000; -// h->uptime++; -// } -// } - #define MG_HEALTH_INIT() mg_health_init() + + + + +#define MG_HEALTH_CORTEX_VERSION 1U + +#ifndef MG_HEALTH_CORTEX_STORAGE +#define MG_HEALTH_CORTEX_STORAGE NULL +#endif + +#ifndef MG_HEALTH_CORTEX_STORAGE_SIZE +#define MG_HEALTH_CORTEX_STORAGE_SIZE 0 +#endif + +#ifndef MG_HEALTH_CORTEX_STACK_WORDS +#define MG_HEALTH_CORTEX_STACK_WORDS 980U +#endif + +#ifndef MG_HEALTH_CORTEX_REPORT_SIZE +#define MG_HEALTH_CORTEX_REPORT_SIZE 16384U +#endif + +// Storage operations are deliberately format-agnostic. H723 uses the default +// direct-memory implementation; boards with different retained storage can +// replace these callbacks before calling MG_HEALTH_INIT() +struct mg_health_cortex { + void *storage; + size_t storage_size; + bool (*init)(void *, size_t); + bool (*read)(const void *, size_t, void *, size_t); + bool (*write)(void *, size_t, const void *, size_t); +}; + +struct mg_health_cortex_record { + char magic[4]; + uint32_t version; + + uint32_t cpuid; + uint32_t exception_sp; + uint32_t sp; + uint32_t exc_return; + int32_t exception_valid; + int32_t stack_valid; + + uint32_t r0; + uint32_t r1; + uint32_t r2; + uint32_t r3; + uint32_t r4; + uint32_t r5; + uint32_t r6; + uint32_t r7; + uint32_t r8; + uint32_t r9; + uint32_t r10; + uint32_t r11; + uint32_t r12; + uint32_t lr; + uint32_t pc; + uint32_t psr; + + uint32_t cfsr; + uint32_t hfsr; + uint32_t dfsr; + uint32_t afsr; + uint32_t bfar; + uint32_t mmfar; + uint32_t abfsr; + + uint32_t stack_word_count; + uint32_t stack_words[MG_HEALTH_CORTEX_STACK_WORDS]; +}; + +extern struct mg_health_cortex mg_health_cortex; + +void mg_health_cortex_init(struct mg_health_cortex *); +struct mg_str mg_health_cortex_get_blob(void *); // Cloud connector API to https://mdash.net - device management cloud diff --git a/src/health.c b/src/health.c index bc82c602..ea77c786 100644 --- a/src/health.c +++ b/src/health.c @@ -1,5 +1,21 @@ #include "health.h" +#include "health_cortex.h" -// The one health record. Lives in RAM that survives a warm reset, see -// MG_HEALTH_RAM and the .mg_health region in the linker script -struct mg_health mg_health_record MG_HEALTH_RAM; +struct mg_health mg_health_record; + +void mg_health_init(void) { + static const char magic[] = MG_HEALTH_MAGIC; + memset(&mg_health_record, 0, sizeof(mg_health_record)); + memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic)); +#if MG_HEALTH == MG_HEALTH_CORTEX + mg_health_cortex_init(&mg_health_cortex); + mg_health_record.get_blob = mg_health_cortex_get_blob; + mg_health_record.fn_data = &mg_health_cortex; +#endif +} + +struct mg_str mg_health_get_blob(void) { + return !mg_health_valid() || mg_health_record.get_blob == NULL + ? mg_str("") + : mg_health_record.get_blob(mg_health_record.fn_data); +} diff --git a/src/health.h b/src/health.h index 3fc1ed92..0e0d469d 100644 --- a/src/health.h +++ b/src/health.h @@ -1,43 +1,24 @@ // Device health monitoring // -// Keeps a small record in a RAM region that survives a warm reset, so a device -// can tell why it restarted. The record holds a boot counter, the uptime this -// boot has reached, and the reset reason reported by the hardware. A crash -// loop, a hang loop and a marginal power supply each leave a different mark. -// -// Put MG_HEALTH_INIT() in main(), right after clock and memory init: -// -// ```c -// int main(void) { -// hal_clock_init(); -// MG_HEALTH_INIT(); -// ... -// } -// ``` -// -// The record must live in RAM that startup code neither copies nor zeroes. -// See tutorials/stm32/nucleo-h723zg/minimal/link.ld for the .mg_health region, -// and tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h for the -// reset-reason hooks. -// -// On Cortex-M, a fault handler can record the crash: it sets reset_reason to -// MG_HEALTH_RESET_FAULT and walks the stack into backtrace[]. See -// tutorials/stm32/nucleo-h723zg/minimal/main.c for a sample HardFault_Handler. +// A health provider owns the report format and returns it as an opaque blob. +// Applications and transports use mg_health_get_blob() without knowing how the +// report was collected or encoded. #pragma once -#include "arch.h" +#include "str.h" + +#define MG_HEALTH_NONE 0 +#define MG_HEALTH_CORTEX 1 + +#ifndef MG_HEALTH +#define MG_HEALTH MG_HEALTH_NONE +#endif #ifndef MG_HEALTH_MAGIC #define MG_HEALTH_MAGIC {'M', 'G', 'H', '3'} // '3' is the layout version #endif -// Places the record in RAM that survives a reset. Define in mongoose_config.h -// to match the linker script, e.g. __attribute__((section(".mg_health"))) -#ifndef MG_HEALTH_RAM -#define MG_HEALTH_RAM -#endif - // Why the device restarted. Hardware usually reports several flags at once, // so these are ordered by how much they tell you: the most specific cause a // device reports wins @@ -52,15 +33,16 @@ enum mg_health_reason { MG_HEALTH_RESET_FAULT // Fault handler ran, see regs[] and saved_stack }; -#define MG_HEALTH_BACKTRACE 20 // Return addresses in mg_health::backtrace +typedef struct mg_str mg_health_blob; struct mg_health { - char magic[4]; // MG_HEALTH_MAGIC when the record holds valid data - uint32_t reset_reason; // enum mg_health_reason that started this boot - uint32_t backtrace[MG_HEALTH_BACKTRACE]; // Crash backtrace, frame 0 first + char magic[4]; + mg_health_blob (*get_blob)(void *); + void *fn_data; + uint32_t reset_reason; }; -extern struct mg_health mg_health_record; // Defined in health.c +extern struct mg_health mg_health_record; // Return true if the record holds valid data, i.e. it survived the reset and // was written by this firmware layout @@ -70,6 +52,14 @@ static inline bool mg_health_valid(void) { sizeof(mg_health_record.magic)) == 0; } +// Select and initialise the configured health provider. Call after +// initializing the board-specific persistent-storage +void mg_health_init(void); + +// Return the provider's opaque report. The returned memory is provider-owned +// and remains valid until the next call to this function +struct mg_str mg_health_get_blob(void); + static inline int mg_health_reason(void) { return mg_health_valid() ? (int) mg_health_record.reset_reason : MG_HEALTH_RESET_UNKNOWN; @@ -77,45 +67,23 @@ static inline int mg_health_reason(void) { static inline const char *mg_health_reason_str(int reason) { switch (reason) { - case MG_HEALTH_RESET_POWER: return "power"; - case MG_HEALTH_RESET_BROWNOUT: return "brownout"; - case MG_HEALTH_RESET_PIN: return "pin"; - case MG_HEALTH_RESET_SOFTWARE: return "software"; - case MG_HEALTH_RESET_LOWPOWER: return "lowpower"; - case MG_HEALTH_RESET_WATCHDOG: return "watchdog"; - case MG_HEALTH_RESET_FAULT: return "fault"; - default: return "unknown"; + case MG_HEALTH_RESET_POWER: + return "power"; + case MG_HEALTH_RESET_BROWNOUT: + return "brownout"; + case MG_HEALTH_RESET_PIN: + return "pin"; + case MG_HEALTH_RESET_SOFTWARE: + return "software"; + case MG_HEALTH_RESET_LOWPOWER: + return "lowpower"; + case MG_HEALTH_RESET_WATCHDOG: + return "watchdog"; + case MG_HEALTH_RESET_FAULT: + return "fault"; + default: + return "unknown"; } } -// Start a new boot: validate the record, count the boot, store the reset -// reason. The current boot moves to prev_* slots first, so the previous -// boot's uptime and reason stay readable. Called by MG_HEALTH_INIT() -static inline void mg_health_init(void) { - char magic[] = MG_HEALTH_MAGIC; - // struct mg_health *h = mg_health_get(); - if (!mg_health_valid()) { // First boot, or RAM lost its contents - memset(&mg_health_record, 0, sizeof(mg_health_record)); - memcpy(mg_health_record.magic, magic, sizeof(mg_health_record.magic)); - } - // h->prev_uptime = h->uptime; - // h->prev_reason = h->reason; - // h->counter++; - // h->uptime = 0; - // h->reason = mg_health_decode_reason(); - // MG_HEALTH_RESET_CLEAR(); -} - -// // Record that the main loop is still alive. Called from mg_mgr_poll(), so a -// // hung application leaves uptime frozen at the hang -// extern uint64_t mg_health_next_ms; // Defined in health.c - -// static inline void mg_health_uptime(uint64_t now_ms) { -// struct mg_health *h = mg_health_get(); -// if (now_ms >= mg_health_next_ms && mg_health_valid()) { -// mg_health_next_ms = now_ms + 1000; -// h->uptime++; -// } -// } - #define MG_HEALTH_INIT() mg_health_init() diff --git a/src/health_cortex.c b/src/health_cortex.c new file mode 100644 index 00000000..4a984189 --- /dev/null +++ b/src/health_cortex.c @@ -0,0 +1,344 @@ +#include "health_cortex.h" +#include "printf.h" + +#if MG_HEALTH == MG_HEALTH_CORTEX + +#define MG_SCB_CPUID (*(volatile uint32_t *) 0xe000ed00) +#define MG_SCB_CFSR (*(volatile uint32_t *) 0xe000ed28) +#define MG_SCB_HFSR (*(volatile uint32_t *) 0xe000ed2c) +#define MG_SCB_DFSR (*(volatile uint32_t *) 0xe000ed30) +#define MG_SCB_MMFAR (*(volatile uint32_t *) 0xe000ed34) +#define MG_SCB_BFAR (*(volatile uint32_t *) 0xe000ed38) +#define MG_SCB_AFSR (*(volatile uint32_t *) 0xe000ed3c) +#define MG_SCB_ABFSR (*(volatile uint32_t *) 0xe000efa8) + +#define MG_CPU_CORTEX_M0 0xc20 +#define MG_CPU_CORTEX_M23 0xd20 +#define MG_CPU_CORTEX_M3 0xc23 +#define MG_CPU_CORTEX_M4 0xc24 +#define MG_CPU_CORTEX_M7 0xc27 +#define MG_CPU_CORTEX_M0P 0xc60 +#define MG_CPU_CORTEX_M33 0xd21 +#define MG_CPU_CORTEX_M35P 0xd31 +#define MG_CPU_CORTEX_M52 0xd24 +#define MG_CPU_CORTEX_M55 0xd22 +#define MG_CPU_CORTEX_M85 0xd23 + +extern uint32_t __StackLimit __attribute__((weak)); +extern uint32_t __StackTop __attribute__((weak)); +extern uint32_t _sstack __attribute__((weak)); +extern uint32_t _estack __attribute__((weak)); +extern bool mg_health_cortex_stack_range(const void *, uintptr_t *, uintptr_t *) + __attribute__((weak)); + +static struct mg_health_cortex_record s_record; +static char s_report[MG_HEALTH_CORTEX_REPORT_SIZE]; + +static bool storage_init(void *storage, size_t size) { + // Note: actual init is handled in main() + (void) storage; + (void) size; + return true; +} + +static bool storage_read(const void *storage, size_t offset, void *buf, + size_t len) { + volatile const uint8_t *src = (volatile const uint8_t *) storage + offset; + uint8_t *dst = (uint8_t *) buf; + size_t i; + for (i = 0; i < len; i++) dst[i] = src[i]; + return true; +} + +static bool storage_write(void *storage, size_t offset, const void *buf, + size_t len) { + volatile uint8_t *dst = (volatile uint8_t *) storage + offset; + const uint8_t *src = (const uint8_t *) buf; + size_t i; + for (i = 0; i < len; i++) dst[i] = src[i]; + return true; +} + +struct mg_health_cortex mg_health_cortex = { + MG_HEALTH_CORTEX_STORAGE, + MG_HEALTH_CORTEX_STORAGE_SIZE, + storage_init, + storage_read, + storage_write, +}; + +static bool storage_bounds(const struct mg_health_cortex *ctx, size_t offset, + size_t len) { + return ctx != NULL && ctx->storage != NULL && offset <= ctx->storage_size && + len <= ctx->storage_size - offset; +} + +static bool storage_configured(const struct mg_health_cortex *ctx) { + return ctx != NULL && ctx->storage != NULL && ctx->storage_size != 0 && + ctx->storage_size >= sizeof(struct mg_health_cortex_record) && + ctx->init != NULL && ctx->read != NULL && ctx->write != NULL; +} + +static bool record_read(struct mg_health_cortex *ctx, + struct mg_health_cortex_record *record) { + static const char magic[] = MG_HEALTH_MAGIC; + if (ctx == NULL || ctx->read == NULL || + !storage_bounds(ctx, 0, sizeof(*record)) || + !ctx->read(ctx->storage, 0, record, sizeof(*record))) { + return false; + } + return memcmp(record->magic, magic, sizeof(record->magic)) == 0 && + record->version == MG_HEALTH_CORTEX_VERSION && + record->stack_word_count <= MG_HEALTH_CORTEX_STACK_WORDS; +} + +static bool record_write(struct mg_health_cortex *ctx, + const struct mg_health_cortex_record *record) { + char invalid[sizeof(record->magic)] = {0}; + size_t offset = sizeof(record->magic); + if (ctx == NULL || ctx->write == NULL || + !storage_bounds(ctx, 0, sizeof(*record)) || + !ctx->write(ctx->storage, 0, &invalid, sizeof(invalid))) { + return false; + } + if (!ctx->write(ctx->storage, offset, (const uint8_t *) record + offset, + sizeof(*record) - offset)) { + return false; + } + if (!ctx->write(ctx->storage, 0, record->magic, sizeof(record->magic))) + return false; + return true; +} + +static bool stack_range(const void *ptr, uintptr_t *lo, uintptr_t *hi) { + if (mg_health_cortex_stack_range != NULL && + mg_health_cortex_stack_range(ptr, lo, hi)) { + return *lo < *hi; + } else if (&__StackLimit != NULL && &__StackTop != NULL) { + *lo = (uintptr_t) &__StackLimit; + *hi = (uintptr_t) &__StackTop; + } else if (&_sstack != NULL && &_estack != NULL) { + *lo = (uintptr_t) &_sstack; + *hi = (uintptr_t) &_estack; + } else { + return false; + } + return *lo < *hi; +} + +static int stack_valid(const void *ptr, size_t words) { + uintptr_t lo, hi, p = (uintptr_t) ptr; + if ((p & 3U) != 0) return 0; + if (!stack_range(ptr, &lo, &hi)) return -1; + return p >= lo && p <= hi && words <= (hi - p) / sizeof(uint32_t); +} + +static bool has_fault_status(uint32_t part) { + return part == MG_CPU_CORTEX_M3 || part == MG_CPU_CORTEX_M4 || + part == MG_CPU_CORTEX_M7 || part == MG_CPU_CORTEX_M33 || + part == MG_CPU_CORTEX_M35P || part == MG_CPU_CORTEX_M52 || + part == MG_CPU_CORTEX_M55 || part == MG_CPU_CORTEX_M85; +} + +static const char *core_name(uint32_t part) { + switch (part) { + case MG_CPU_CORTEX_M0: + return "cortex-m0"; + case MG_CPU_CORTEX_M0P: + return "cortex-m0+"; + case MG_CPU_CORTEX_M23: + return "cortex-m23"; + case MG_CPU_CORTEX_M3: + return "cortex-m3"; + case MG_CPU_CORTEX_M4: + return "cortex-m4"; + case MG_CPU_CORTEX_M7: + return "cortex-m7"; + case MG_CPU_CORTEX_M33: + return "cortex-m33"; + case MG_CPU_CORTEX_M35P: + return "cortex-m35p"; + case MG_CPU_CORTEX_M52: + return "cortex-m52"; + case MG_CPU_CORTEX_M55: + return "cortex-m55"; + case MG_CPU_CORTEX_M85: + return "cortex-m85"; + default: + return "unknown"; + } +} + +static const char *arch_name(uint32_t part) { + if (part == MG_CPU_CORTEX_M0 || part == MG_CPU_CORTEX_M0P) return "armv6-m"; + if (part == MG_CPU_CORTEX_M3) return "armv7-m"; + if (part == MG_CPU_CORTEX_M4 || part == MG_CPU_CORTEX_M7) return "armv7e-m"; + return "armv8-m"; +} + +static bool json_append(char *buf, size_t size, size_t *offset, const char *fmt, + ...) { + va_list ap; + size_t available, n; + if (*offset >= size) return false; + available = size - *offset; + va_start(ap, fmt); + n = mg_vsnprintf(buf + *offset, available, fmt, &ap); + va_end(ap); + if (n >= available) { + *offset = size; + return false; + } + *offset += n; + return true; +} + +void mg_health_cortex_init(struct mg_health_cortex *ctx) { + if (storage_configured(ctx)) { + (void) ctx->init(ctx->storage, ctx->storage_size); + } +} + +struct mg_str mg_health_cortex_get_blob(void *data) { + struct mg_health_cortex *ctx = (struct mg_health_cortex *) data; + struct mg_health_cortex_record *r = &s_record; + uint32_t part; + size_t n = 0, i; + bool ok; + + if (!storage_configured(ctx)) { + return mg_str( + "{\"valid\":false,\"error\":\"health storage is not configured\"}"); + } + if (!record_read(ctx, r)) return mg_str("{\"valid\":false}"); + part = (r->cpuid >> 4) & 0xfffU; + ok = json_append( + s_report, sizeof(s_report), &n, + "{\"valid\":true,\"version\":%lu,\"arch\":\"%s\"," + "\"core\":\"%s\",\"image\":\"firmware.elf\"," + "\"map\":\"firmware.elf.map\",\"binary\":\"firmware.bin\"," + "\"cpuid\":\"0x%08lx\",\"exception_sp\":\"0x%08lx\"," + "\"exc_return\":\"0x%08lx\",\"exception_valid\":%s," + "\"stack_valid\":%s,\"regs\":{" + "\"r0\":\"0x%08lx\",\"r1\":\"0x%08lx\"," + "\"r2\":\"0x%08lx\",\"r3\":\"0x%08lx\"," + "\"r4\":\"0x%08lx\",\"r5\":\"0x%08lx\"," + "\"r6\":\"0x%08lx\",\"r7\":\"0x%08lx\"," + "\"r8\":\"0x%08lx\",\"r9\":\"0x%08lx\"," + "\"r10\":\"0x%08lx\",\"r11\":\"0x%08lx\"," + "\"r12\":\"0x%08lx\",\"sp\":\"0x%08lx\"," + "\"lr\":\"0x%08lx\",\"pc\":\"0x%08lx\"," + "\"xpsr\":\"0x%08lx\"},\"fault\":{" + "\"cfsr\":\"0x%08lx\",\"hfsr\":\"0x%08lx\"," + "\"dfsr\":\"0x%08lx\",\"afsr\":\"0x%08lx\"," + "\"mmfar\":\"0x%08lx\",\"bfar\":\"0x%08lx\"," + "\"abfsr\":\"0x%08lx\"},\"stack\":{" + "\"addr\":\"0x%08lx\",\"word_size\":4,\"words\":[", + (unsigned long) r->version, arch_name(part), core_name(part), + (unsigned long) r->cpuid, (unsigned long) r->exception_sp, + (unsigned long) r->exc_return, r->exception_valid > 0 ? "true" : "false", + r->stack_valid > 0 ? "true" : "false", (unsigned long) r->r0, + (unsigned long) r->r1, (unsigned long) r->r2, (unsigned long) r->r3, + (unsigned long) r->r4, (unsigned long) r->r5, (unsigned long) r->r6, + (unsigned long) r->r7, (unsigned long) r->r8, (unsigned long) r->r9, + (unsigned long) r->r10, (unsigned long) r->r11, (unsigned long) r->r12, + (unsigned long) r->sp, (unsigned long) r->lr, (unsigned long) r->pc, + (unsigned long) r->psr, (unsigned long) r->cfsr, (unsigned long) r->hfsr, + (unsigned long) r->dfsr, (unsigned long) r->afsr, + (unsigned long) r->mmfar, (unsigned long) r->bfar, + (unsigned long) r->abfsr, (unsigned long) r->sp); + for (i = 0; ok && i < r->stack_word_count; i++) { + ok = json_append(s_report, sizeof(s_report), &n, "%s\"0x%08lx\"", + i == 0 ? "" : ",", (unsigned long) r->stack_words[i]); + } + if (ok) ok = json_append(s_report, sizeof(s_report), &n, "]}}"); + return ok ? mg_str_n(s_report, n) + : mg_str("{\"valid\":false,\"error\":\"JSON overflow\"}"); +} + +static void __attribute__((used)) mg_health_cortex_handler( + uint32_t *exception_stack, uint32_t lr, + const uint32_t *callee_saved) { + static const char magic[] = MG_HEALTH_MAGIC; + uint32_t cpuid = MG_SCB_CPUID; + uint32_t part = (cpuid >> 4) & 0xfffU; + size_t fp_words = (lr & (1U << 4)) == 0 ? 18U : 0U; + int valid = stack_valid(exception_stack, fp_words + 8U); + struct mg_health_cortex_record *r = &s_record; + volatile uint32_t *words = (volatile uint32_t *) r; + size_t i; + + for (i = 0; i < sizeof(*r) / sizeof(*words); i++) words[i] = 0; + r->version = MG_HEALTH_CORTEX_VERSION; + r->cpuid = cpuid; + r->exception_sp = (uint32_t) (uintptr_t) exception_stack; + r->exc_return = lr; + r->exception_valid = valid; + r->r4 = callee_saved[0]; + r->r5 = callee_saved[1]; + r->r6 = callee_saved[2]; + r->r7 = callee_saved[3]; + r->r8 = callee_saved[4]; + r->r9 = callee_saved[5]; + r->r10 = callee_saved[6]; + r->r11 = callee_saved[7]; + if (valid > 0) { + uint32_t *core_frame = exception_stack + fp_words; + uint32_t *fault_sp; + uintptr_t lo, hi, p; + size_t count; + r->r0 = core_frame[0]; + r->r1 = core_frame[1]; + r->r2 = core_frame[2]; + r->r3 = core_frame[3]; + r->r12 = core_frame[4]; + r->lr = core_frame[5]; + r->pc = core_frame[6]; + r->psr = core_frame[7]; + fault_sp = core_frame + 8; + if (r->psr & (1U << 9)) fault_sp++; + r->sp = (uint32_t) (uintptr_t) fault_sp; + r->stack_valid = stack_valid(fault_sp, 0); + p = (uintptr_t) fault_sp; + if (r->stack_valid > 0 && stack_range(fault_sp, &lo, &hi)) { + (void) lo; + count = (hi - p) / sizeof(uint32_t); + if (count > MG_HEALTH_CORTEX_STACK_WORDS) + count = MG_HEALTH_CORTEX_STACK_WORDS; + r->stack_word_count = (uint32_t) count; + for (i = 0; i < count; i++) r->stack_words[i] = fault_sp[i]; + } + } + if (has_fault_status(part)) { + r->cfsr = MG_SCB_CFSR; + r->hfsr = MG_SCB_HFSR; + r->dfsr = MG_SCB_DFSR; + r->afsr = MG_SCB_AFSR; + r->bfar = MG_SCB_BFAR; + r->mmfar = MG_SCB_MMFAR; + if (part == MG_CPU_CORTEX_M7) r->abfsr = MG_SCB_ABFSR; + } + memcpy(r->magic, magic, sizeof(r->magic)); + (void) record_write(&mg_health_cortex, r); + for (;;) (void) 0; +} + +void HardFault_Handler(void); +__attribute__((naked)) void HardFault_Handler(void) { + __asm volatile( + "mov r0, lr \n" + "movs r1, #4 \n" + "tst r0, r1 \n" + "beq 1f \n" + "mrs r0, psp \n" + "b 2f \n" + "1: \n" + "mrs r0, msp \n" + "2: \n" + "mov r1, lr \n" + "stmdb sp!, {r4-r11} \n" + "mov r2, sp \n" + "b mg_health_cortex_handler \n"); +} + +#endif diff --git a/src/health_cortex.h b/src/health_cortex.h new file mode 100644 index 00000000..84625ec3 --- /dev/null +++ b/src/health_cortex.h @@ -0,0 +1,77 @@ +#pragma once + +#include "health.h" + +#define MG_HEALTH_CORTEX_VERSION 1U + +#ifndef MG_HEALTH_CORTEX_STORAGE +#define MG_HEALTH_CORTEX_STORAGE NULL +#endif + +#ifndef MG_HEALTH_CORTEX_STORAGE_SIZE +#define MG_HEALTH_CORTEX_STORAGE_SIZE 0 +#endif + +#ifndef MG_HEALTH_CORTEX_STACK_WORDS +#define MG_HEALTH_CORTEX_STACK_WORDS 980U +#endif + +#ifndef MG_HEALTH_CORTEX_REPORT_SIZE +#define MG_HEALTH_CORTEX_REPORT_SIZE 16384U +#endif + +// Storage operations are deliberately format-agnostic. H723 uses the default +// direct-memory implementation; boards with different retained storage can +// replace these callbacks before calling MG_HEALTH_INIT() +struct mg_health_cortex { + void *storage; + size_t storage_size; + bool (*init)(void *, size_t); + bool (*read)(const void *, size_t, void *, size_t); + bool (*write)(void *, size_t, const void *, size_t); +}; + +struct mg_health_cortex_record { + char magic[4]; + uint32_t version; + + uint32_t cpuid; + uint32_t exception_sp; + uint32_t sp; + uint32_t exc_return; + int32_t exception_valid; + int32_t stack_valid; + + uint32_t r0; + uint32_t r1; + uint32_t r2; + uint32_t r3; + uint32_t r4; + uint32_t r5; + uint32_t r6; + uint32_t r7; + uint32_t r8; + uint32_t r9; + uint32_t r10; + uint32_t r11; + uint32_t r12; + uint32_t lr; + uint32_t pc; + uint32_t psr; + + uint32_t cfsr; + uint32_t hfsr; + uint32_t dfsr; + uint32_t afsr; + uint32_t bfar; + uint32_t mmfar; + uint32_t abfsr; + + uint32_t stack_word_count; + uint32_t stack_words[MG_HEALTH_CORTEX_STACK_WORDS]; +}; + +extern struct mg_health_cortex mg_health_cortex; + +void mg_health_cortex_init(struct mg_health_cortex *); +struct mg_str mg_health_cortex_get_blob(void *); diff --git a/src/mdash.c b/src/mdash.c index 9ec5a4a3..f4665c04 100644 --- a/src/mdash.c +++ b/src/mdash.c @@ -91,11 +91,15 @@ static void mg_mdash_fn(struct mg_connection *c, int ev, void *ev_data) { } static void mg_mdash_rpc_get_info(struct mg_rpc_req *r) { - mg_rpc_ok(r, "{%m:%m,%m:%llu,%m:%m,%m:\"mws.%d\"}", MG_ESC("fw_version"), - MG_ESC(MG_MDASH_FIRMWARE_VERSION), MG_ESC("uptime"), - (uint64_t) (mg_millis() / 1000), MG_ESC("reboot_reason"), - MG_ESC(mg_health_reason_str(mg_health_reason())), MG_ESC("arch"), - MG_ARCH); + struct mg_str blob = mg_health_get_blob(); + if (blob.buf == NULL) blob.len = 0; + mg_rpc_ok(r, "{%m:%m,%m:%llu,%m:%m,%m:\"mws.%d\",%m:{%m:%m,%m:%llu,%m:%m}}", + MG_ESC("fw_version"), MG_ESC(MG_MDASH_FIRMWARE_VERSION), MG_ESC("uptime"), + (uint64_t) (mg_millis() / 1000), MG_ESC("reboot_reason"), + MG_ESC(mg_health_reason_str(mg_health_reason())), MG_ESC("arch"), MG_ARCH, + MG_ESC("health"), MG_ESC("encoding"), MG_ESC("base64"), MG_ESC("size"), + (uint64_t) blob.len, MG_ESC("data"), mg_print_base64, (int) blob.len, + (uint8_t *) (blob.buf == NULL ? "" : blob.buf)); } static void mg_mdash_rpc_ota_begin(struct mg_rpc_req *r) { diff --git a/test/Makefile b/test/Makefile index 94800a37..4fd82997 100644 --- a/test/Makefile +++ b/test/Makefile @@ -106,7 +106,7 @@ mongoose.c: Makefile $(wildcard ../src/*.c) $(wildcard ../src/drivers/*.c) cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#include "mongoose.h"' ; (for F in src/*.c src/drivers/*.c ; do echo; echo '#ifdef MG_ENABLE_LINES'; echo "#line 1 \"$$F\""; echo '#endif'; cat $$F | sed -e 's,#include ".*,,'; done))> $@ mongoose.h: $(HDRS) Makefile - cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_x25519.h src/tls_aes128.h src/tls_uecc.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ + cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_x25519.h src/tls_aes128.h src/tls_uecc.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/health_cortex.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ # Check that all external (exported) symbols have "mg_" prefix mg_prefix: mongoose.c mongoose.h diff --git a/tutorials/stm32/nucleo-h723zg/minimal/Makefile b/tutorials/stm32/nucleo-h723zg/minimal/Makefile index 876fc6aa..c2d4a002 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/Makefile +++ b/tutorials/stm32/nucleo-h723zg/minimal/Makefile @@ -1,12 +1,13 @@ # Copyright (c) 2026 Cesanta Software Limited # Environment setup: https://mongoose.ws/docs/getting-started/build-environment/ -MGDIR = ../../../.. +MGDIR = mongoose CFLAGS = -W -Wall -Wextra -Wundef -Wshadow -Wdouble-promotion CFLAGS += -Wformat-truncation -fno-common -Wconversion -Wno-sign-conversion CFLAGS += -g3 -Os -ffunction-sections -fdata-sections -CFLAGS += -I. -I$(MGDIR) -Icmsis_core/CMSIS/Core/Include -Icmsis_h7/Include +CFLAGS += -fno-omit-frame-pointer -funwind-tables +CFLAGS += -I. -Icmsis_core/CMSIS/Core/Include -Icmsis_h7/Include -Imongoose CFLAGS += -mcpu=cortex-m7 -mthumb -mfloat-abi=hard -mfpu=fpv5-d16 $(CFLAGS_EXTRA) LDFLAGS ?= -Tlink.ld -nostdlib -nostartfiles --specs nosys.specs -lc -lgcc -Wl,--gc-sections -Wl,-Map=$@.map @@ -16,6 +17,10 @@ SOURCES += $(MGDIR)/mongoose.c all build example: firmware.bin +mongoose/mongoose.c mongoose/mongoose.h: + mkdir -p mongoose/ + cp ../../../../mongoose.[ch] mongoose/ + firmware.elf: cmsis_core cmsis_h7 hal.h link.ld Makefile $(SOURCES) $(MGDIR)/mongoose.h mongoose_config.h arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(CFLAGS_EXTRA) $(LDFLAGS) -o $@ @@ -24,6 +29,15 @@ firmware.bin: firmware.elf @echo @echo "To flash, run 'make flash', or use STM32CubeProgrammer" +crash.json: + @echo "To get crash.json, curl http://DEVICE_IP/api/report after recovering from \ + a hardfault and store the result in crash.json" + +stacktrace.txt: firmware.bin firmware.elf.map crash.json fake_target.py stacktrace.gdb + python3 -u fake_target.py 3334 > fake_target.log 2>&1 & + sleep 0.2 + gdb-multiarch -q firmware.elf -batch -x stacktrace.gdb > $@ + flash: firmware.bin STM32_Programmer_CLI -c port=swd -w firmware.elf -hardRst diff --git a/tutorials/stm32/nucleo-h723zg/minimal/fake_target.py b/tutorials/stm32/nucleo-h723zg/minimal/fake_target.py new file mode 100644 index 00000000..3a7ef88e --- /dev/null +++ b/tutorials/stm32/nucleo-h723zg/minimal/fake_target.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +import json +import socket +import struct +import sys + + +FLASH_BASE = 0x08000000 +FAULT_REGS = { + "cfsr": 0xE000ED28, + "hfsr": 0xE000ED2C, + "dfsr": 0xE000ED30, + "afsr": 0xE000ED3C, + "mmfar": 0xE000ED34, + "bfar": 0xE000ED38, + "abfsr": 0xE000EFA8, +} + + +def checksum(s): + return sum(s.encode("ascii")) & 0xff + + +def packet(s): + return "$%s#%02x" % (s, checksum(s)) + + +def parse_packet(conn): + while True: + c = conn.recv(1) + if c == b"$": + break + if c == b"\x03": + return "\x03" + data = bytearray() + while True: + c = conn.recv(1) + if c == b"#": + conn.recv(2) + conn.sendall(b"+") + return data.decode("ascii") + data += c + + +def symbols(path): + syms = {} + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + for line in lines: + fields = line.split() + if len(fields) >= 2 and fields[0].startswith("0x"): + syms[fields[-1]] = int(fields[0], 16) + return syms + + +def value(v, syms): + if isinstance(v, int): + return v & 0xffffffff + s = str(v).replace(" ", "") + for op in ("+", "-"): + if op in s: + name, off = s.split(op, 1) + n = int(off, 0) + return (syms[name] + n if op == "+" else syms[name] - n) & 0xffffffff + if s.startswith("0x"): + return int(s, 16) & 0xffffffff + return syms[s] & 0xffffffff + + +def readmem(mem, addr, size): + data = bytearray() + for i in range(size): + b = mem.get(addr + i) + if b is None: + return None + data.append(b) + return data.hex() + + +def hex_decode(s): + return bytes.fromhex(s).decode("ascii") + + +def main(): + port = int(sys.argv[1]) if len(sys.argv) > 1 else 3333 + with open("crash.json", "r", encoding="utf-8") as f: + crash = json.load(f) + image = crash.get("image", "crash.elf") + syms = symbols(crash.get("map", image + ".map")) + with open(crash.get("binary", image.rsplit(".", 1)[0] + ".bin"), "rb") as f: + flash = f.read() + + regs = crash["regs"] + stack = crash["stack"] + mem = {} + for i, b in enumerate(flash): + mem[FLASH_BASE + i] = b + addr = value(stack["addr"], syms) + for i, word in enumerate(stack["words"]): + w = value(word, syms) + for j, b in enumerate(struct.pack(" flash .text : { *(.text* .text.*) } > flash .rodata : { *(.rodata*) } > flash + .ARM.extab : { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > flash + + .ARM.exidx : { + __exidx_start = .; + KEEP(*(.ARM.exidx* .gnu.linkonce.armexidx.*)) + __exidx_end = .; + } > flash + /* Note the .iram section, for functions copied to RAM. Required for MG_IRAM OTA support */ .data : { _sdata = .; *(.first_data) *(.iram .iram* .iram.*) *(.data SORT(.data.*)) _edata = .; } > ram_d1 AT > flash _sidata = LOADADDR(.data); @@ -23,10 +34,6 @@ SECTIONS { .eth_ram : { *(.eth_ram .eth_ram*) } > ram_d1 AT > flash - /* Health record. NOLOAD and in unused DTCM, so startup code neither copies - nor zeroes it and the contents survive a warm reset */ - .mg_health (NOLOAD) : { KEEP(*(.mg_health .mg_health*)) } > dtcmram - . = ALIGN(8); _end = .; } diff --git a/tutorials/stm32/nucleo-h723zg/minimal/main.c b/tutorials/stm32/nucleo-h723zg/minimal/main.c index a08edaf9..d630dbb2 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/main.c +++ b/tutorials/stm32/nucleo-h723zg/minimal/main.c @@ -17,6 +17,14 @@ #define LED2 PIN('E', 1) #define LED3 PIN('B', 14) +void hal_storage_init(void) { + hal_backup_domain_init(); + RCC->AHB4ENR |= RCC_AHB4ENR_BKPRAMEN; + (void) RCC->AHB4ENR; + PWR->CR2 |= PWR_CR2_BREN; + while ((PWR->CR2 & PWR_CR2_BRRDY) == 0) (void) 0; +} + static void log_fn(char ch, void *param) { hal_uart_write_buf(param, &ch, 1); } @@ -28,50 +36,6 @@ static void blink_task(void) { } } -// Fault handler body. Runs in exception context: no printf, no malloc, no -// blocking calls. Records the crash reason and a backtrace into the health -// record, then resets. -// "used" keeps the linker from garbage-collecting this section: the only -// reference is the "b fault_c" branch in the naked handler below -__attribute__((used, noinline)) static void fault_c(uint32_t *sp) { - extern uint8_t _estack; // End of the main RAM region, defined in link.ld - size_t n = 0; - mg_health_record.reset_reason = MG_HEALTH_RESET_FAULT; - // Frame 0 is the faulting PC, frame 1 the caller's LR. Deeper frames come - // from a heuristic stack walk: BL pushes an odd return address that lives - // in flash. A stack word that merely looks like an address shows up as a - // bogus frame when symbolised, which is easy to filter by eye - mg_health_record.backtrace[n++] = sp[6] & ~1U; // Stacked PC - mg_health_record.backtrace[n++] = sp[5] & ~1U; // Stacked LR - for (uint32_t *p = sp + 8; - n < MG_HEALTH_BACKTRACE && - (uintptr_t) p < (uintptr_t) sp + 4096U && // Bound the scan - (uintptr_t) p < (uintptr_t) &_estack; // Stay in RAM - p++) { - uint32_t v = *p; - if ((v & 1U) && v >= 0x08000000U && v < 0x08000000U + 1024U * 1024U) { - mg_health_record.backtrace[n++] = v & ~1U; - } - } - NVIC_SystemReset(); -} - -// Common fault entry. EXC_RETURN bit 2 tells which stack was in use: -// 0 = MSP, 1 = PSP. Load the faulting SP into r0 and hand it to fault_c() -__attribute__((naked)) void HardFault_Handler(void) { - __asm volatile( - "tst lr, #4\n\t" // Test EXC_RETURN bit 2 - "ite eq\n\t" // If zero, use MSP; else PSP - "mrseq r0, msp\n\t" - "mrsne r0, psp\n\t" - "b fault_c\n\t"); -} - -// Route the other fault types through the same entry point -void MemManage_Handler(void) __attribute__((alias("HardFault_Handler"))); -void BusFault_Handler(void) __attribute__((alias("HardFault_Handler"))); -void UsageFault_Handler(void) __attribute__((alias("HardFault_Handler"))); - uint64_t mg_millis(void) { return hal_get_tick(); } @@ -92,6 +56,10 @@ static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } else if (mg_match(hm->uri, mg_str("/api/kill"), NULL)) { SCB->SHCSR &= ~SCB_SHCSR_USGFAULTENA_Msk; __asm volatile("udf #0"); + } else if (mg_match(hm->uri, mg_str("/api/report"), NULL)) { + struct mg_str report = mg_health_get_blob(); + mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%.*s\n", + (int) report.len, report.buf); } else { mg_http_reply(c, 200, "", "Hi from Mongoose, tick %llu\n", hal_get_tick()); @@ -101,9 +69,8 @@ static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { int main(void) { hal_clock_init(); - - MG_HEALTH_INIT(); // Must be called after clock init - + hal_storage_init(); + MG_HEALTH_INIT(); hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); mg_log_set_fn(log_fn, UART_DEBUG); hal_rng_init(); @@ -114,19 +81,7 @@ int main(void) { MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); - // Report the previous boot's crash backtrace, if any - if (mg_health_reason() == MG_HEALTH_RESET_FAULT) { - char buf[MG_HEALTH_BACKTRACE * 10 + 100]; - mg_snprintf(buf, sizeof(buf), "%s", - "arm-none-eabi-addr2line -pfiaC -e firmware.elf"); - for (int i = 0; i < MG_HEALTH_BACKTRACE; i++) { - if (mg_health_record.backtrace[i] == 0) break; - mg_snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), " 0x%08lx", - mg_health_record.backtrace[i]); - } - // mg_snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "\n"); - MG_INFO(("Previous boot crashed! Analyse with: %s", buf)); - } + // MG_OTA_BOOT_CHECK(); // Must be called after clock init struct mg_mgr mgr; mg_mgr_init(&mgr); diff --git a/tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h b/tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h index 097774f1..efa24b54 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h +++ b/tutorials/stm32/nucleo-h723zg/minimal/mongoose_config.h @@ -26,8 +26,11 @@ mac[5] = MGUID[2] & 255; \ } while (0) -// Crash report support. Health record lives in the .mg_health region, see link.ld -#define MG_HEALTH_RAM __attribute__((section(".mg_health"))) +// Persist Cortex-M crash reports in the 4 KB backup SRAM. hal_storage_init() +// enables this memory before MG_HEALTH_INIT() is called. +#define MG_HEALTH MG_HEALTH_CORTEX +#define MG_HEALTH_CORTEX_STORAGE ((void *) D3_BKPSRAM_BASE) +#define MG_HEALTH_CORTEX_STORAGE_SIZE (4U * 1024U) // mdash.net device management service support #define MG_ENABLE_MDASH 1 diff --git a/tutorials/stm32/nucleo-h723zg/minimal/stacktrace.gdb b/tutorials/stm32/nucleo-h723zg/minimal/stacktrace.gdb new file mode 100644 index 00000000..94de15f8 --- /dev/null +++ b/tutorials/stm32/nucleo-h723zg/minimal/stacktrace.gdb @@ -0,0 +1,14 @@ +set architecture armv7e-m +target remote 127.0.0.1:3334 +set $cfsr = *(unsigned int *) 0xe000ed28 +set $hfsr = *(unsigned int *) 0xe000ed2c +set $bfar = *(unsigned int *) 0xe000ed38 +set $abfsr = *(unsigned int *) 0xe000efa8 +printf "Fault registers: CFSR=0x%08x HFSR=0x%08x BFAR=0x%08x ABFSR=0x%08x\n", $cfsr, $hfsr, $bfar, $abfsr +if $cfsr & 0x00000400 + printf "Imprecise BusFault: exception PC is asynchronous; offending instruction may be earlier.\n" +end +if $cfsr & 0x00008000 + printf "BFAR valid: 0x%08x\n", $bfar +end +bt diff --git a/tutorials/stm32/nucleo-h723zg/minimal/stacktrace.txt b/tutorials/stm32/nucleo-h723zg/minimal/stacktrace.txt new file mode 100644 index 00000000..c153b8c5 --- /dev/null +++ b/tutorials/stm32/nucleo-h723zg/minimal/stacktrace.txt @@ -0,0 +1,15 @@ +The target architecture is set to "armv7e-m". +http_ev_handler (c=0x2400da68, ev=, ev_data=) at main.c:174 +174 __asm volatile ("udf #0"); +Fault registers: CFSR=0x00010000 HFSR=0x40000000 BFAR=0x00000000 ABFSR=0x00000000 +#0 http_ev_handler (c=0x2400da68, ev=, ev_data=) at main.c:174 +#1 0x080107aa in http_cb (c=0x2400da68, ev=, ev_data=) at mongoose/mongoose.c:4842 +#2 0x08007812 in mg_call (c=0x2400da68, ev=7, ev_data=0x2404fdd4) at mongoose/mongoose.c:2619 +#3 0x080194b8 in read_conn (c=, pkt=) at mongoose/mongoose.c:9907 +#4 rx_tcp (ifp=, pkt=) at mongoose/mongoose.c:10022 +#5 rx_ip (ifp=0x2400da68, pkt=0x2404fdc8) at mongoose/mongoose.c:10133 +#6 mg_tcpip_rx (ifp=ifp@entry=0x24007e58 , buf=, len=len@entry=139) at mongoose/mongoose.c:10260 +#7 0x0801a082 in mg_tcpip_poll (ifp=0x24007e58 , now=32453) at mongoose/mongoose.c:10413 +#8 mg_mgr_poll (mgr=mgr@entry=0x2404ffac, ms=ms@entry=0) at mongoose/mongoose.c:10642 +#9 0x08000a64 in main () at main.c:212 +[Inferior 1 (Remote target) detached]