mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-02 06:22:52 +07:00
Add generic mg_health report for mdash
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = .;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user