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
+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]