Add generic mg_health report for mdash

This commit is contained in:
robert
2026-09-23 12:07:11 +03:00
parent 60c75a22b8
commit 4e7c664455
16 changed files with 1225 additions and 230 deletions
+376 -8
View File
@@ -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) {
+117 -72
View File
@@ -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
+19 -3
View File
@@ -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);
}
+41 -73
View File
@@ -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()
+344
View File
@@ -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
+77
View File
@@ -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 *);
+9 -5
View File
@@ -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) {
+1 -1
View File
@@ -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
+16 -2
View File
@@ -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
@@ -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("<I", w)):
mem[addr + i * 4 + j] = b
for name, addr in FAULT_REGS.items():
if name in crash.get("fault", {}):
w = value(crash["fault"][name], syms)
for j, b in enumerate(struct.pack("<I", w)):
mem[addr + j] = b
order = ["r%d" % n for n in range(13)] + ["sp", "lr", "pc", "xpsr"]
regvals = [value(regs[name], syms) for name in order]
regpkt = b"".join(struct.pack("<I", r) for r in regvals).hex()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(1)
print("fake target listening on 127.0.0.1:%d" % port, flush=True)
conn, _ = s.accept()
with conn:
while True:
req = parse_packet(conn)
if req.startswith("qSupported"):
rep = "PacketSize=4000"
elif req.startswith("qRcmd,"):
print("monitor %s" % hex_decode(req[6:]), flush=True)
rep = "OK"
elif req in ("?", "vStopped", "\x03", "c", "s") or \
req.startswith(("C", "S", "vCont;c", "vCont;s")):
rep = "T05thread:1;"
elif req == "vCont?":
rep = "vCont;c;s"
elif req == "qC":
rep = "QC1"
elif req == "qfThreadInfo":
rep = "m1"
elif req == "qsThreadInfo":
rep = "l"
elif req == "qAttached":
rep = "1"
elif req.startswith("T"):
rep = "OK"
elif req.startswith(("Hg", "Hc", "QStartNoAckMode")):
rep = "OK"
elif req == "g":
rep = regpkt
elif req.startswith("p"):
n = int(req[1:], 16)
rep = struct.pack("<I", regvals[n]).hex() if n < len(regvals) else "00000000"
elif req.startswith("m"):
a, n = req[1:].split(",", 1)
data = readmem(mem, int(a, 16), int(n, 16))
rep = data if data is not None else "E01"
elif req.startswith(("q", "v", "Z", "z")):
rep = ""
elif req in ("D", "k"):
conn.sendall(packet("OK").encode("ascii"))
break
else:
rep = ""
conn.sendall(packet(rep).encode("ascii"))
if __name__ == "__main__":
main()
@@ -93,6 +93,7 @@ __attribute__((weak)) int _getpid(void) {
__attribute__((weak)) int _write(int fd, char *ptr, int len) {
(void) fd, (void) ptr, (void) len;
hal_uart_write_buf(USART3, ptr, len);
return -1;
}
+11 -4
View File
@@ -9,12 +9,23 @@ MEMORY {
itcmram (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
}
_estack = ORIGIN(ram_d1) + LENGTH(ram_d1);
_sstack = ORIGIN(ram_d1);
SECTIONS {
.vectors : { KEEP(*(.isr_vector)) } > 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 = .;
}
+15 -60
View File
@@ -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);
@@ -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
@@ -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
@@ -0,0 +1,15 @@
The target architecture is set to "armv7e-m".
http_ev_handler (c=0x2400da68, ev=<optimized out>, ev_data=<optimized out>) 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=<optimized out>, ev_data=<optimized out>) at main.c:174
#1 0x080107aa in http_cb (c=0x2400da68, ev=<optimized out>, ev_data=<optimized out>) 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=<optimized out>, pkt=<optimized out>) at mongoose/mongoose.c:9907
#4 rx_tcp (ifp=<optimized out>, pkt=<optimized out>) 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 <mif_>, buf=<optimized out>, len=len@entry=139) at mongoose/mongoose.c:10260
#7 0x0801a082 in mg_tcpip_poll (ifp=0x24007e58 <mif_>, 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]