From af9d05a9bb303c29e6d30712abf1771efd992a03 Mon Sep 17 00:00:00 2001 From: Byeongjee Kang Date: Mon, 10 Aug 2026 16:57:57 -0400 Subject: [PATCH] Detect mid-region power failures via NVM in-region flag --- passes/runtime/boot_common.inc | 66 +++++++++++++++++++++++++++++- passes/runtime/milp_boot.S | 6 +++ passes/runtime/milp_runtime.c | 13 ++++++ passes/runtime/rockclimb_boot.S | 6 +++ passes/runtime/rockclimb_runtime.c | 13 ++++++ passes/runtime/schematic_boot.S | 6 +++ passes/runtime/schematic_runtime.c | 13 ++++++ scripts/ckpt/bench/milp.py | 1 + scripts/ckpt/bench/rockclimb.py | 1 + scripts/ckpt/bench/runner.py | 34 +++++++++++---- scripts/ckpt/bench/schematic.py | 1 + scripts/ckpt/device/flash.py | 26 +++++++++++- scripts/ckpt/errors.py | 9 ++++ scripts/ckpt/verify/common.py | 33 ++++++++++++++- 14 files changed, 217 insertions(+), 11 deletions(-) diff --git a/passes/runtime/boot_common.inc b/passes/runtime/boot_common.inc index 4709b31d..cc3ec003 100644 --- a/passes/runtime/boot_common.inc +++ b/passes/runtime/boot_common.inc @@ -21,6 +21,9 @@ .extern __nvm_done #endif + .extern __nvm_in_region + .extern __nvm_violation + /* * park_if_done — Freeze completed runs (DEVICE_DEBUG only). * @@ -49,6 +52,58 @@ #endif .endm +/* + * check_region_violation — Detect a reset that arrived mid-region. + * + * __nvm_in_region is 1 from the moment execution (re)enters a region + * until the next boundary's checkpoint is committed. Every region is + * sized so the capacitor can always finish it, so no reset should ever + * arrive while the flag is set. If one does, the energy estimate was + * wrong: record it in __nvm_violation (read by the host) and park + * blinking LED1 (P1.0). The flag is deliberately left set, so every + * subsequent reset re-enters this park and the NVM evidence survives + * until the next reflash (which zeroes .nvm). + * + * Invoked after park_if_done, before the recovery check. + */ +.macro check_region_violation + tst.w &__nvm_in_region + jz 6f + hw_init + mov.w #1, &__nvm_violation + bis.b #0x01, &0x0204 /* P1DIR |= BIT0 (LED1) */ +5: + xor.b #0x01, &0x0202 /* P1OUT ^= BIT0 */ +#if F_CPU == 16000000 + mov.w #13, R15 +#elif F_CPU == 8000000 + mov.w #7, R15 +#else + mov.w #1, R15 +#endif +4: + mov.w #0xFFFF, R14 /* ~200k cycles per outer iteration */ +3: + dec.w R14 + jnz 3b + dec.w R15 + jnz 4b + jmp 5b +6: +.endm + +/* + * mark_in_region — Flag that execution is (re)entering a region. + * + * Placed immediately before control returns to instrumented code: + * at the end of recovery, at the end of a fresh boot, and after the + * wait-mode boundary continues. Cleared by save_pc_sp_and_halt once + * the next boundary's checkpoint is committed. + */ +.macro mark_in_region + mov.w #1, &__nvm_in_region +.endm + /* * hw_init — Disable WDT, unlock GPIO, configure FRAM wait states + DCO. * @@ -153,6 +208,11 @@ mov.w SP, &__nvm_sp add.w #2, &__nvm_sp + /* Checkpoint is committed: the region completed within its energy + budget. A reset from here until mark_in_region is a legitimate + at-boundary death. */ + mov.w #0, &__nvm_in_region + #if defined(HALT_BOR) /* Software BOR: PMMCTL0 = PMMPW (0xA500) | PMMSWBOR (0x0004). CPU resets immediately, FRAM survives. */ @@ -187,13 +247,17 @@ pop R11 pop SR + /* Full capacitor: continuing into the next region without a reboot. */ + mark_in_region + #endif /* BOR vs SWBOR vs WAIT */ .endm /* - * zero_nvm_state — Clear __nvm_pc and __nvm_sp for fresh boot. + * zero_nvm_state — Clear __nvm_pc, __nvm_sp, __nvm_in_region for fresh boot. */ .macro zero_nvm_state mov.w #0, &__nvm_pc mov.w #0, &__nvm_sp + mov.w #0, &__nvm_in_region .endm diff --git a/passes/runtime/milp_boot.S b/passes/runtime/milp_boot.S index f540b162..e7faa6d1 100644 --- a/passes/runtime/milp_boot.S +++ b/passes/runtime/milp_boot.S @@ -34,6 +34,7 @@ _milp_boot: park_if_done + check_region_violation /* Check if this is a recovery boot: nvm_pc != 0 */ mov.w &__nvm_pc, R14 @@ -72,6 +73,8 @@ _do_recovery: at IR level. After regalloc, these become physical register saves/restores automatically. */ + mark_in_region + /* Jump to saved PC (pops from stack). After ret: PC = saved PC, SP = __nvm_sp (push -2, ret +2 cancel). */ ret @@ -91,6 +94,9 @@ _normal_boot: call #wait_until_vcc_full #endif + /* Entering the first region (CRT init + entry code count toward it). */ + mark_in_region + /* Fall through to next CRT section (do NOT use ret here) */ .size _milp_boot, .-_milp_boot diff --git a/passes/runtime/milp_runtime.c b/passes/runtime/milp_runtime.c index 7d6fa04f..9ef2cd98 100644 --- a/passes/runtime/milp_runtime.c +++ b/passes/runtime/milp_runtime.c @@ -30,6 +30,15 @@ __attribute__((section(".nvm"))) uint16_t __nvm_pc = 0; /* Saved stack pointer */ __attribute__((section(".nvm"))) uint16_t __nvm_sp = 0; +/* 1 while a region is executing; cleared once a boundary's checkpoint + is committed. Boot code treats a reset with this flag set as an + energy-budget violation (see check_region_violation in boot_common.inc). */ +__attribute__((section(".nvm"))) volatile uint16_t __nvm_in_region = 0; + +/* Set by boot code when a reset arrived mid-region. Read by the host + after every run; only reflashing clears it. */ +__attribute__((section(".nvm"))) volatile uint16_t __nvm_violation = 0; + /* __region_boundary is provided by milp_boot.S */ /* Halt CPU after benchmark completes. @@ -37,6 +46,9 @@ __attribute__((section(".nvm"))) uint16_t __nvm_sp = 0; that inflate Saleae timing measurements. */ #include void bench_halt(void) { + /* Run complete — a later reset (e.g. mspdebug attach) is not a + mid-region death. */ + __nvm_in_region = 0; __bis_SR_register(LPM4_bits); } @@ -60,6 +72,7 @@ __attribute__((section(".nvm"))) volatile uint16_t __nvm_result = 0; __attribute__((section(".nvm"))) volatile uint16_t __nvm_done = 0; void debug_exit(int result) { + __nvm_in_region = 0; debug_exit_begin(result); uart_puts(" __region_boundary: "); uart_put_u32(cnt_boundary); diff --git a/passes/runtime/rockclimb_boot.S b/passes/runtime/rockclimb_boot.S index ddde3ed5..3b460023 100644 --- a/passes/runtime/rockclimb_boot.S +++ b/passes/runtime/rockclimb_boot.S @@ -30,6 +30,7 @@ _rockclimb_boot: park_if_done + check_region_violation /* Check if this is a recovery boot: nvm_pc != 0 */ mov.w &__nvm_pc, R14 @@ -89,6 +90,8 @@ _do_recovery: counter update is omitted. */ #endif + mark_in_region + /* Jump to saved PC (pops from stack). After ret: PC = saved PC, SP = __nvm_sp (push -2, ret +2 cancel). */ ret @@ -108,6 +111,9 @@ _normal_boot: call #wait_until_vcc_full #endif + /* Entering the first region (CRT init + entry code count toward it). */ + mark_in_region + /* Fall through to next CRT section (do NOT use ret here) */ .size _rockclimb_boot, .-_rockclimb_boot diff --git a/passes/runtime/rockclimb_runtime.c b/passes/runtime/rockclimb_runtime.c index 45d74af9..bede4461 100644 --- a/passes/runtime/rockclimb_runtime.c +++ b/passes/runtime/rockclimb_runtime.c @@ -31,10 +31,22 @@ __attribute__((section(".nvm"))) uint16_t __nvm_pc = 0; /* Saved stack pointer */ __attribute__((section(".nvm"))) uint16_t __nvm_sp = 0; +/* 1 while a region is executing; cleared once a boundary's checkpoint + is committed. Boot code treats a reset with this flag set as an + energy-budget violation (see check_region_violation in boot_common.inc). */ +__attribute__((section(".nvm"))) volatile uint16_t __nvm_in_region = 0; + +/* Set by boot code when a reset arrived mid-region. Read by the host + after every run; only reflashing clears it. */ +__attribute__((section(".nvm"))) volatile uint16_t __nvm_violation = 0; + /* __region_boundary is provided by rockclimb_boot.S */ #include void bench_halt(void) { + /* Run complete — a later reset (e.g. mspdebug attach) is not a + mid-region death. */ + __nvm_in_region = 0; __bis_SR_register(LPM4_bits); } @@ -54,6 +66,7 @@ __attribute__((section(".nvm"))) volatile uint16_t __nvm_result = 0; __attribute__((section(".nvm"))) volatile uint16_t __nvm_done = 0; void debug_exit(int result) { + __nvm_in_region = 0; debug_exit_begin(result); uart_puts(" __region_boundary: "); uart_put_u32(cnt_boundary); diff --git a/passes/runtime/schematic_boot.S b/passes/runtime/schematic_boot.S index c1a8a4bd..6cc12fd9 100644 --- a/passes/runtime/schematic_boot.S +++ b/passes/runtime/schematic_boot.S @@ -34,6 +34,7 @@ _schematic_boot: park_if_done + check_region_violation /* Check if this is a recovery boot: nvm_pc != 0 */ mov.w &__nvm_pc, R14 @@ -93,6 +94,8 @@ _do_recovery: pop SR #endif + mark_in_region + /* Jump to saved PC (pops from stack). After ret: PC = saved PC, SP = __nvm_sp (push -2, ret +2 cancel). */ ret @@ -112,6 +115,9 @@ _normal_boot: call #wait_until_vcc_full #endif + /* Entering the first region (CRT init + entry code count toward it). */ + mark_in_region + /* Fall through to next CRT section (do NOT use ret here) */ .size _schematic_boot, .-_schematic_boot diff --git a/passes/runtime/schematic_runtime.c b/passes/runtime/schematic_runtime.c index c2b8f992..9b29fd6f 100644 --- a/passes/runtime/schematic_runtime.c +++ b/passes/runtime/schematic_runtime.c @@ -31,10 +31,22 @@ __attribute__((section(".nvm"))) uint16_t __nvm_pc = 0; /* Saved stack pointer */ __attribute__((section(".nvm"))) uint16_t __nvm_sp = 0; +/* 1 while a region is executing; cleared once a boundary's checkpoint + is committed. Boot code treats a reset with this flag set as an + energy-budget violation (see check_region_violation in boot_common.inc). */ +__attribute__((section(".nvm"))) volatile uint16_t __nvm_in_region = 0; + +/* Set by boot code when a reset arrived mid-region. Read by the host + after every run; only reflashing clears it. */ +__attribute__((section(".nvm"))) volatile uint16_t __nvm_violation = 0; + /* __region_boundary is provided by schematic_boot.S */ #include void bench_halt(void) { + /* Run complete — a later reset (e.g. mspdebug attach) is not a + mid-region death. */ + __nvm_in_region = 0; __bis_SR_register(LPM4_bits); } @@ -60,6 +72,7 @@ __attribute__((section(".nvm"))) volatile uint16_t __nvm_result = 0; __attribute__((section(".nvm"))) volatile uint16_t __nvm_done = 0; void debug_exit(int result) { + __nvm_in_region = 0; debug_exit_begin(result); uart_puts(" __region_boundary: "); uart_put_u32(cnt_boundary); diff --git a/scripts/ckpt/bench/milp.py b/scripts/ckpt/bench/milp.py index e87ebdaf..5a207e96 100644 --- a/scripts/ckpt/bench/milp.py +++ b/scripts/ckpt/bench/milp.py @@ -68,6 +68,7 @@ NVM_SYMBOLS: list[str] = [ "__nvm_done", "__nvm_result", + "__nvm_violation", "cnt_boundary", "cnt_store_mem", "cnt_restore_mem", diff --git a/scripts/ckpt/bench/rockclimb.py b/scripts/ckpt/bench/rockclimb.py index d249d2c9..4f2bbc1d 100644 --- a/scripts/ckpt/bench/rockclimb.py +++ b/scripts/ckpt/bench/rockclimb.py @@ -57,6 +57,7 @@ NVM_SYMBOLS: list[str] = [ "__nvm_done", "__nvm_result", + "__nvm_violation", "cnt_boundary", ] diff --git a/scripts/ckpt/bench/runner.py b/scripts/ckpt/bench/runner.py index fc1948eb..0a36080e 100644 --- a/scripts/ckpt/bench/runner.py +++ b/scripts/ckpt/bench/runner.py @@ -24,7 +24,7 @@ from saleae.automation import Manager from ..env import ProjectEnv -from ..errors import CkptError, CompilationError, DeviceError +from ..errors import CkptError, CompilationError, DeviceError, RegionViolationError from ..output_parser import ( NvmCounters, PassStatistics, @@ -334,30 +334,48 @@ def run_benchmark_matrix( # ----- Saleae timing + NVM read (only with a device) ----- nvm: NvmCounters | None = None execution_time_us: float | None = None + region_violation = False if output_dir is not None and saleae_manager is not None: elf = output_dir / f"{bench_name}.elf" if elf.is_file(): - try: - from ..device.flash import read_nvm + from ..device.flash import ( + check_region_violation, + raise_if_region_violation, + read_nvm, + ) + try: execution_time_us = measure_execution_time( elf, saleae_manager, capture_timeout_seconds, ) + except DeviceError as exc: + logger.error(" DEVICE ERROR: %s", exc) + # Check the violation flag even when the capture + # failed: a mid-region reset parks the device + # blinking, so the stop pulse never fires and the + # capture times out. + try: + time.sleep(POST_CAPTURE_SETTLE_SECONDS) if device_debug and nvm_symbols: - time.sleep(POST_CAPTURE_SETTLE_SECONDS) nvm_dict = read_nvm( tc, elf, FLASH_TIMEOUT, nvm_symbols, ) + raise_if_region_violation(nvm_dict, elf) nvm_text = "\n".join( f"{k}={v}" for k, v in nvm_dict.items() ) nvm = parse_nvm_output(nvm_text) + else: + check_region_violation(tc, elf, FLASH_TIMEOUT) + except RegionViolationError as exc: + logger.error(" REGION ENERGY VIOLATION: %s", exc) + region_violation = True except DeviceError as exc: logger.error(" DEVICE ERROR: %s", exc) @@ -426,12 +444,14 @@ def run_benchmark_matrix( if execution_time_us is not None: row_fields["execution_time_us"] = str(round(execution_time_us, 2)) + if had_compilation_error and row_status == "ok": + row_status = "link_failed" + if region_violation: + row_status = "region_violation" row = BenchmarkRow( benchmark=row_name, capacitor=cap.label, - status="link_failed" - if had_compilation_error and row_status == "ok" - else row_status, + status=row_status, fields=row_fields, ) write_csv_row(writer, row, csv_header) diff --git a/scripts/ckpt/bench/schematic.py b/scripts/ckpt/bench/schematic.py index c446cdd8..f811d674 100644 --- a/scripts/ckpt/bench/schematic.py +++ b/scripts/ckpt/bench/schematic.py @@ -72,6 +72,7 @@ NVM_SYMBOLS: list[str] = [ "__nvm_done", "__nvm_result", + "__nvm_violation", "cnt_boundary", "cnt_save_reg", "cnt_restore_reg", diff --git a/scripts/ckpt/device/flash.py b/scripts/ckpt/device/flash.py index b952c191..fefe3d4b 100644 --- a/scripts/ckpt/device/flash.py +++ b/scripts/ckpt/device/flash.py @@ -7,7 +7,7 @@ import threading from pathlib import Path -from ..errors import DeviceError +from ..errors import DeviceError, RegionViolationError from ..runner import run from ..toolchain import Toolchain from . import nvm @@ -183,3 +183,27 @@ def read_nvm( raise DeviceError(f"Expected {total_len} bytes from hex dump, got {len(data)}") return nvm.extract_symbol_values(data, sym_info, symbols, base_addr) + + +def raise_if_region_violation(values: dict[str, int], elf_path: Path) -> None: + """Raise RegionViolationError if an NVM readback carries the violation flag.""" + if values.get("__nvm_violation", 0) != 0: + raise RegionViolationError( + f"{elf_path.name}: device reset while a region was executing " + "(__nvm_violation set) — a region exceeded its energy budget" + ) + + +def check_region_violation( + tc: Toolchain, + elf_path: Path, + timeout: int, +) -> None: + """Read the ``__nvm_violation`` flag and raise RegionViolationError if set. + + Boot code sets the flag and parks the device blinking LED1 when a + reset arrives mid-region, so this works even when the run never + completed (e.g. the Saleae capture timed out). + """ + values = read_nvm(tc, elf_path, timeout, ["__nvm_violation"]) + raise_if_region_violation(values, elf_path) diff --git a/scripts/ckpt/errors.py b/scripts/ckpt/errors.py index 0cc4aa8d..57c3e144 100644 --- a/scripts/ckpt/errors.py +++ b/scripts/ckpt/errors.py @@ -56,6 +56,15 @@ class DeviceError(CkptError): """A device interaction failed.""" +class RegionViolationError(CkptError): + """The device reset while a region was executing. + + Regions are sized so the capacitor can always finish them, so a + mid-region reset means the energy estimate was violated. Reported + by boot code via the ``__nvm_violation`` NVM flag. + """ + + class ConfigError(CkptError): """A configuration error.""" diff --git a/scripts/ckpt/verify/common.py b/scripts/ckpt/verify/common.py index 6212e7c3..cf924432 100644 --- a/scripts/ckpt/verify/common.py +++ b/scripts/ckpt/verify/common.py @@ -28,10 +28,10 @@ UninstrumentedCompileOptions, compile_uninstrumented, ) -from ..device.flash import read_nvm +from ..device.flash import check_region_violation, raise_if_region_violation, read_nvm from ..device.saleae import discover_saleae, saleae_run from ..env import ProjectEnv -from ..errors import CompilationError, ConfigError, DeviceError +from ..errors import CompilationError, ConfigError, DeviceError, RegionViolationError from ..output_parser import detect_infeasibility from ..runner import StepResult from ..tempdir import compilation_workdir @@ -394,7 +394,36 @@ def _verify_instrumented( ) time.sleep(POST_CAPTURE_SETTLE_SECONDS) inst_nvm = read_nvm(tc, inst_elf, FLASH_TIMEOUT, spec.nvm_symbols) + raise_if_region_violation(inst_nvm, inst_elf) + except RegionViolationError as exc: + msg = f"{algorithm} region energy violation: {exc}" + logger.error(" %s", msg) + return BenchResult( + bench_name, + cap_label, + Status.ERROR, + msg, + baseline_result=baseline_result, + algorithm_result=None, + ) except (DeviceError, OSError) as exc: + # A mid-region reset parks the device blinking, so the capture + # times out — check the violation flag before blaming the device. + try: + check_region_violation(tc, inst_elf, FLASH_TIMEOUT) + except RegionViolationError as vexc: + msg = f"{algorithm} region energy violation: {vexc}" + logger.error(" %s", msg) + return BenchResult( + bench_name, + cap_label, + Status.ERROR, + msg, + baseline_result=baseline_result, + algorithm_result=None, + ) + except DeviceError, OSError: + pass msg = f"{algorithm} flash/read failed: {exc}" logger.error(" %s", msg) return BenchResult(