diff --git a/AGENTS.md b/AGENTS.md index 183b3cb..677b81f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,9 +108,9 @@ The `scripts/ckpt/` package provides the compilation, benchmarking, and device i ```bash # Compilation pipelines (INPUT can be a benchmark name or path to .c file) # --csv PATH writes a one-row CSV of compile-time stats only (no device/runtime columns). -ckpt compile milp INPUT --cap CAP [--link] [--estimator-mode assembly|ir] [--save-temps] [--halt-mode bor|lpm4|swbor] [--cpu-freq 1|8|16] [--device-debug] [--accumulate-keys FILE] [--csv CSV] ... -ckpt compile rockclimb INPUT --cap CAP [--link] [--no-precomputed-energy] [--save-temps] [--halt-mode bor|lpm4|swbor] [--cpu-freq 1|8|16] [--device-debug] [--accumulate-keys FILE] [--csv CSV] ... -ckpt compile schematic INPUT --cap CAP [--link] [--trace-file FILE] [--trace-only] [--save-temps] [--halt-mode bor|lpm4|swbor] [--cpu-freq 1|8|16] [--device-debug] [--accumulate-keys FILE] [--csv CSV] ... +ckpt compile milp INPUT --cap CAP [--link] [--estimator-mode assembly|ir] [--save-temps] [--halt-mode bor|swbor|wait] [--cpu-freq 1|8|16] [--device-debug] [--accumulate-keys FILE] [--csv CSV] ... +ckpt compile rockclimb INPUT --cap CAP [--link] [--no-precomputed-energy] [--save-temps] [--halt-mode bor|swbor|wait] [--cpu-freq 1|8|16] [--device-debug] [--accumulate-keys FILE] [--csv CSV] ... +ckpt compile schematic INPUT --cap CAP [--link] [--trace-file FILE] [--trace-only] [--save-temps] [--halt-mode bor|swbor|wait] [--cpu-freq 1|8|16] [--device-debug] [--accumulate-keys FILE] [--csv CSV] ... ckpt compile uninstrumented INPUT [--link] [--save-temps] [--cpu-freq 1|8|16] [--device-debug] [--csv CSV] ... # Explicit config paths also accepted: -e ENERGY_CONFIG -m/-c/-s ALGO_CONFIG diff --git a/passes/runtime/boot_common.inc b/passes/runtime/boot_common.inc index a45022a..4709b31 100644 --- a/passes/runtime/boot_common.inc +++ b/passes/runtime/boot_common.inc @@ -5,14 +5,50 @@ * Requires F_CPU to be defined via -D at assembly time. */ -#if !defined(HALT_BOR) && !defined(HALT_LPM4) && !defined(HALT_SWBOR) -#error "Define one of HALT_BOR, HALT_LPM4, or HALT_SWBOR" +#if !defined(HALT_BOR) && !defined(HALT_SWBOR) && !defined(HALT_WAIT) +#error "Define one of HALT_BOR, HALT_SWBOR, or HALT_WAIT" #endif -#if (defined(HALT_BOR) + defined(HALT_LPM4) + defined(HALT_SWBOR)) > 1 +#if (defined(HALT_BOR) + defined(HALT_SWBOR) + defined(HALT_WAIT)) > 1 #error "Define only one halt mode" #endif +#ifdef HALT_WAIT + .extern wait_until_vcc_full +#endif + +#ifdef DEVICE_DEBUG + .extern __nvm_done +#endif + +/* + * park_if_done — Freeze completed runs (DEVICE_DEBUG only). + * + * Invoked at the very top of each boot entry, before the recovery + * check. Once __nvm_done is set, any later boot parks in LPM4 without + * touching NVM, so results and counters stay exactly as the completed + * run wrote them — resets from mspdebug readback attach, FET replug, + * or power cycles cannot re-execute the program. Only reflashing + * (which zeroes __nvm_done) starts a new run. + * + * debug_init() has the same guard for fresh boots, but recovery boots + * bypass main, so the check must also live here. + */ +.macro park_if_done +#ifdef DEVICE_DEBUG + cmp.w #1, &__nvm_done + jne 8f + /* WDT is still running this early after reset — stop it so it + cannot reset us out of the park. */ + mov.w #0x5A80, &0x015C +9: + bis.w #0xF0, SR + nop + jmp 9b +8: +#endif +.endm + /* * hw_init — Disable WDT, unlock GPIO, configure FRAM wait states + DCO. * @@ -105,7 +141,8 @@ * save_pc_sp_and_halt — Save return address + caller SP to NVM, then halt. * * Expects __nvm_pc and __nvm_sp to be .extern'd by the caller. - * Halt mode selected by HALT_BOR / HALT_LPM4 / HALT_SWBOR defines. + * Halt mode selected by HALT_BOR / HALT_SWBOR / HALT_WAIT. + * HALT_WAIT does not halt: it waits for a full capacitor and continues. */ .macro save_pc_sp_and_halt /* Save return address as recovery PC. @@ -129,14 +166,28 @@ br #_start /* Unreachable — jump is unconditional */ -#else /* HALT_LPM4 */ - /* Enter LPM4 deep sleep (CPUOFF+OSCOFF+SCG0+SCG1 = bits 4-7 of SR). - System powers off. On energy harvest, reset vector fires. */ - bis.w #0xF0, SR - nop - /* If woken by interrupt (not full reboot), fall through to ret */ +#elif defined(HALT_WAIT) + /* Real intermittent power: the checkpoint above is complete, so wait + until the capacitor is full and continue into the next region + without rebooting. A power failure during the wait recovers to + this boundary. RockClimb inserts this call post-regalloc, where + caller-saved registers and SR flags may be live — preserve them + around the C call. */ + push SR + push R11 + push R12 + push R13 + push R14 + push R15 + call #wait_until_vcc_full + pop R15 + pop R14 + pop R13 + pop R12 + pop R11 + pop SR -#endif /* BOR vs SWBOR vs LPM4 */ +#endif /* BOR vs SWBOR vs WAIT */ .endm /* diff --git a/passes/runtime/milp_boot.S b/passes/runtime/milp_boot.S index ce9f612..f540b16 100644 --- a/passes/runtime/milp_boot.S +++ b/passes/runtime/milp_boot.S @@ -33,6 +33,8 @@ .type _milp_boot, @function _milp_boot: + park_if_done + /* Check if this is a recovery boot: nvm_pc != 0 */ mov.w &__nvm_pc, R14 tst.w R14 @@ -44,9 +46,19 @@ _do_recovery: hw_init poison_bor_sram +#ifdef HALT_WAIT + /* Real power failure: the board woke as soon as VCC crossed the + brownout level. Wait for a full capacitor before resuming. + __nvm_pc stays set so a death during this wait re-enters + recovery on the next boot. SP is the CRT-initialized stack; + the call clobbers R11-R15, so reload the saved PC after. */ + call #wait_until_vcc_full + mov.w &__nvm_pc, R14 +#else /* R14 holds saved PC. Clear recovery marker first to prevent infinite recovery loops on next reboot. */ mov.w #0, &__nvm_pc +#endif /* Restore SP */ mov.w &__nvm_sp, SP @@ -72,6 +84,13 @@ _normal_boot: hw_init zero_nvm_state +#ifdef HALT_WAIT + /* Fresh boot: wait for a full capacitor before the program starts. + Nothing persistent has been written yet, so a death during this + wait is simply another clean fresh boot. */ + call #wait_until_vcc_full +#endif + /* Fall through to next CRT section (do NOT use ret here) */ .size _milp_boot, .-_milp_boot @@ -81,15 +100,13 @@ _normal_boot: * __region_boundary — Save PC + SP and halt at region boundaries * * Called at the start of each energy region (except the entry block). - * Saves the return address (= region body start) and SP to NVM, - * then enters deep sleep (LPM4) or triggers BOR. The system powers off. - * On next power-on, the reset vector fires, CRT runs, and boot - * recovery (above) resumes from saved state. + * Saves the return address (= region body start) and SP to NVM, then + * triggers BOR (or simulates one, or waits for a full capacitor — + * per halt mode). After a reboot, the reset vector fires, CRT runs, + * and boot recovery (above) resumes from saved state. * * Unlike RockClimb/SCHEMATIC, no register bulk-save is needed here. * MILP handles all live state at IR level. - * - * If woken from LPM4 by an interrupt (not a full reboot), just returns. */ .section .text .global __region_boundary diff --git a/passes/runtime/rockclimb_boot.S b/passes/runtime/rockclimb_boot.S index 7a320cf..ddde3ed 100644 --- a/passes/runtime/rockclimb_boot.S +++ b/passes/runtime/rockclimb_boot.S @@ -29,6 +29,8 @@ .type _rockclimb_boot, @function _rockclimb_boot: + park_if_done + /* Check if this is a recovery boot: nvm_pc != 0 */ mov.w &__nvm_pc, R14 tst.w R14 @@ -40,9 +42,19 @@ _do_recovery: hw_init poison_bor_sram +#ifdef HALT_WAIT + /* Real power failure: the board woke as soon as VCC crossed the + brownout level. Wait for a full capacitor before resuming. + __nvm_pc stays set so a death during this wait re-enters + recovery on the next boot. SP is the CRT-initialized stack; + the call clobbers R11-R15, so reload the saved PC after. */ + call #wait_until_vcc_full + mov.w &__nvm_pc, R14 +#else /* R14 holds saved PC. Clear recovery marker first to prevent infinite recovery loops on next reboot. */ mov.w #0, &__nvm_pc +#endif /* Restore SP */ mov.w &__nvm_sp, SP @@ -89,6 +101,13 @@ _normal_boot: hw_init zero_nvm_state +#ifdef HALT_WAIT + /* Fresh boot: wait for a full capacitor before the program starts. + Nothing persistent has been written yet, so a death during this + wait is simply another clean fresh boot. */ + call #wait_until_vcc_full +#endif + /* Fall through to next CRT section (do NOT use ret here) */ .size _rockclimb_boot, .-_rockclimb_boot @@ -98,15 +117,13 @@ _normal_boot: * __region_boundary — Save state and halt at region boundaries * * Called at the start of each energy region (except the entry block). - * Saves the return address (= region body start) and SP to NVM, - * then enters deep sleep (LPM4). The system powers off. - * On next power-on, the reset vector fires, CRT runs, and boot - * recovery (above) resumes from saved state. + * Saves the return address (= region body start) and SP to NVM, then + * triggers BOR (or simulates one, or waits for a full capacitor — + * per halt mode). After a reboot, the reset vector fires, CRT runs, + * and boot recovery (above) resumes from saved state. * * RockClimb's machine pass inserts per-register saves distributed * at definition points — no bulk-save needed here. - * - * If woken from LPM4 by an interrupt (not a full reboot), just returns. */ .section .text .global __region_boundary @@ -114,7 +131,7 @@ _normal_boot: __region_boundary: #ifdef DEVICE_DEBUG - /* Preserve SR in case LPM4 wakes without a reset. */ + /* Preserve SR flags for the wait-mode continue path. */ push SR add_u32_to_nvm_counter cnt_boundary, 1 pop SR diff --git a/passes/runtime/rockclimb_runtime.h b/passes/runtime/rockclimb_runtime.h index 317b254..2eab8d4 100644 --- a/passes/runtime/rockclimb_runtime.h +++ b/passes/runtime/rockclimb_runtime.h @@ -58,7 +58,8 @@ extern uint32_t cnt_boundary NVM_SECTION; * depends on the compile-time mode: * - swbor (HALT_SWBOR, default): jumps to _start without destroying state * - bor (HALT_BOR): triggers reset and destroys modeled volatile state - * - lpm4 (HALT_LPM4): enters LPM4 deep sleep (real deployment) + * - wait (HALT_WAIT): waits until the capacitor is full, then continues + * (real intermittent power) * * On reboot, boot.S recovers from saved NVM state. * diff --git a/passes/runtime/schematic_boot.S b/passes/runtime/schematic_boot.S index 4acc980..c1a8a4b 100644 --- a/passes/runtime/schematic_boot.S +++ b/passes/runtime/schematic_boot.S @@ -33,6 +33,8 @@ .type _schematic_boot, @function _schematic_boot: + park_if_done + /* Check if this is a recovery boot: nvm_pc != 0 */ mov.w &__nvm_pc, R14 tst.w R14 @@ -44,9 +46,19 @@ _do_recovery: hw_init poison_bor_sram +#ifdef HALT_WAIT + /* Real power failure: the board woke as soon as VCC crossed the + brownout level. Wait for a full capacitor before resuming. + __nvm_pc stays set so a death during this wait re-enters + recovery on the next boot. SP is the CRT-initialized stack; + the call clobbers R11-R15, so reload the saved PC after. */ + call #wait_until_vcc_full + mov.w &__nvm_pc, R14 +#else /* R14 holds saved PC. Clear recovery marker first to prevent infinite recovery loops on next reboot. */ mov.w #0, &__nvm_pc +#endif /* Restore SP */ mov.w &__nvm_sp, SP @@ -93,6 +105,13 @@ _normal_boot: hw_init zero_nvm_state +#ifdef HALT_WAIT + /* Fresh boot: wait for a full capacitor before the program starts. + Nothing persistent has been written yet, so a death during this + wait is simply another clean fresh boot. */ + call #wait_until_vcc_full +#endif + /* Fall through to next CRT section (do NOT use ret here) */ .size _schematic_boot, .-_schematic_boot @@ -109,8 +128,8 @@ _normal_boot: * Sequence: * 1. Save R4-R15 to __nvm_regs (12 MOV instructions) * 2. Save return address (PC) and caller SP to NVM - * 3. If halt mode: enter LPM4 or trigger BOR - * 4. Return (if woken by interrupt or halt disabled) + * 3. Trigger BOR, simulate one, or wait for a full capacitor + * (per halt mode) — the wait mode returns and continues */ .section .text .global __region_boundary diff --git a/passes/runtime/schematic_runtime.h b/passes/runtime/schematic_runtime.h index cb4d413..3501e1a 100644 --- a/passes/runtime/schematic_runtime.h +++ b/passes/runtime/schematic_runtime.h @@ -59,11 +59,11 @@ extern uint32_t cnt_restore_mem NVM_SECTION; * ============================================================================ */ /** - * Region boundary: bulk-save registers, save state, and halt (deep sleep). + * Region boundary: bulk-save registers, save state, and halt. * * Saves R4-R15 to NVM, then saves return address (region body start) and SP, - * then enters LPM4. System powers off. On reboot, boot.S recovers from - * saved state. + * then triggers BOR, simulates one, or waits for a full capacitor (per halt + * mode). On reboot, boot.S recovers from saved state. * * Provided by schematic_boot.S (assembly). */ diff --git a/passes/runtime/vcc_wait.c b/passes/runtime/vcc_wait.c new file mode 100644 index 0000000..005aedd --- /dev/null +++ b/passes/runtime/vcc_wait.c @@ -0,0 +1,95 @@ +/* + * wait_until_vcc_full — Block until the supply capacitor reaches 3.3 V. + * + * Used by the HALT_WAIT halt mode for real intermittent-power evaluation: + * called at every region boundary (after the PC/SP checkpoint), at + * recovery boot (the board wakes as soon as VCC crosses the ~1.8 V + * brownout level, well below a full capacitor), and at fresh boot. + * + * VCC is measured with the ADC12_B battery monitor: ADC12BATMAP routes + * AVCC/2 to input channel 31, converted against the internal 2.0 V + * reference. Between samples the CPU sleeps in LPM3, woken every ~10 ms + * by Timer_A0 clocked from VLO (~9.4 kHz). The reference and ADC are + * powered up around each sample and shut off before sleeping (they draw + * ~150 uA combined, which would fight the capacitor's charging current), + * so the sleeping board draws only LPM3-level current (~1 uA). + * + * Called from the .crt_0010 boot path BEFORE data/BSS initialization, + * so this file must not use any global variables. + */ + +#include +#include + +#ifdef HALT_WAIT + +/* Full capacitor = 3.3 V supply. The battery monitor sees AVCC/2 = 1.65 V; + against the 2.0 V reference: 4095 * 1.65 / 2.0 = 3378 counts. */ +#define VCC_FULL_ADC_COUNTS 3378 + +/* Sample period in VLO (~9.4 kHz) ticks: ~10 ms. */ +#define SAMPLE_PERIOD_TICKS 94 + +/* Power up REF + ADC, take one battery-monitor sample, power both down. + The ~75 us reference settling cost per sample is negligible against + the 10 ms sample period. Long sample time (256 clocks) accommodates + the high-impedance internal AVCC/2 divider. */ +static uint16_t sample_avcc_half(void) { + uint16_t sample; + + while (REFCTL0 & REFGENBUSY) + ; + REFCTL0 = REFVSEL_1 | REFON; + + ADC12CTL0 = ADC12SHT0_8 | ADC12ON; + ADC12CTL1 = ADC12SHP; + ADC12CTL2 = ADC12RES_2; + ADC12CTL3 = ADC12BATMAP; + ADC12MCTL0 = ADC12INCH_31 | ADC12VRSEL_1; + + while (!(REFCTL0 & REFGENRDY)) + ; + + ADC12CTL0 |= ADC12ENC | ADC12SC; + while (ADC12CTL1 & ADC12BUSY) + ; + sample = ADC12MEM0; + + ADC12CTL0 &= ~ADC12ENC; + ADC12CTL0 &= ~ADC12ON; + REFCTL0 &= ~REFON; + + return sample; +} + +void wait_until_vcc_full(void) { + /* ACLK <- VLO so Timer_A keeps running in LPM3 (no crystal needed). + SELS/SELM stay on DCO, matching timing_gpio_init in benchmark.h. */ + CSCTL0_H = CSKEY_H; + CSCTL2 = SELA__VLOCLK | SELS__DCOCLK | SELM__DCOCLK; + CSCTL0_H = 0; + + /* Below ~2.2 V supply the 2.0 V reference is out of spec, but such + samples read far below the threshold and VCC only rises while + waiting, so early inaccuracy is harmless. */ + if (sample_avcc_half() < VCC_FULL_ADC_COUNTS) { + TA0CCR0 = SAMPLE_PERIOD_TICKS; + TA0CCTL0 = CCIE; + TA0CTL = TASSEL__ACLK | MC__UP | TACLR; + + do { + __bis_SR_register(LPM3_bits | GIE); + } while (sample_avcc_half() < VCC_FULL_ADC_COUNTS); + + __disable_interrupt(); + TA0CTL = MC__STOP; + TA0CCTL0 = 0; + TA0CTL |= TACLR; + } +} + +__attribute__((interrupt(TIMER0_A0_VECTOR))) void __vcc_wait_timer_isr(void) { + __bic_SR_register_on_exit(LPM3_bits); +} + +#endif /* HALT_WAIT */ diff --git a/scripts/ckpt/bench/schematic.py b/scripts/ckpt/bench/schematic.py index 87a5bbd..c446cdd 100644 --- a/scripts/ckpt/bench/schematic.py +++ b/scripts/ckpt/bench/schematic.py @@ -210,7 +210,7 @@ def run_schematic_benchmarks( device_debug: Link the debug-counter runtime and attempt NVM readback. halt_mode: - Halt mode at region boundaries (bor/lpm4/swbor). + Halt mode at region boundaries (bor/swbor/wait). """ bench_paths = discover_benchmarks(env, benchmarks) if not bench_paths: diff --git a/scripts/ckpt/cli.py b/scripts/ckpt/cli.py index c155d80..a44a883 100644 --- a/scripts/ckpt/cli.py +++ b/scripts/ckpt/cli.py @@ -14,6 +14,7 @@ from .analysis.plot import ALGORITHMS, METRICS from .bench.all import ALL_ALGORITHMS, DEFAULT_ALGORITHMS +from .compile.common import HALT_MODES from .errors import ( CkptError, CompilationError, @@ -26,8 +27,6 @@ logger = logging.getLogger(__name__) -HALT_MODES = ("bor", "lpm4", "swbor") - _saleae_timeout_option = click.option( "--timeout", type=click.FloatRange(min=0, min_open=True), diff --git a/scripts/ckpt/compile/common.py b/scripts/ckpt/compile/common.py index 18cb42d..79f63ba 100644 --- a/scripts/ckpt/compile/common.py +++ b/scripts/ckpt/compile/common.py @@ -21,6 +21,11 @@ MATH_LINK_FLAGS = ["-lm"] +# Halt modes for instrumented binaries. "wait" is the real intermittent-power +# mode: region boundaries block until the capacitor is recharged to 3.3 V +# instead of halting (see passes/runtime/vcc_wait.c). +HALT_MODES = ("bor", "swbor", "wait") + def raises_compilation_error(fn): """Convert ToolError from subprocess steps into CompilationError. @@ -543,10 +548,10 @@ def build_boot_defines( defines = [f"F_CPU={cpu_freq}"] if halt_mode == "bor": defines.append("HALT_BOR") - elif halt_mode == "lpm4": - defines.append("HALT_LPM4") elif halt_mode == "swbor": defines.append("HALT_SWBOR") + elif halt_mode == "wait": + defines.append("HALT_WAIT") if device_debug: defines.append("DEVICE_DEBUG") return defines @@ -740,6 +745,18 @@ def link_algorithm( link_objs = [main_object, boot_o, runtime_o] + if boot_defines and "HALT_WAIT" in boot_defines: + vcc_wait_o = stem.with_suffix(".vcc_wait.o") + compile_runtime_c( + tc, + env, + env.vcc_wait_c, + vcc_wait_o, + gcc_opt_level=gcc_opt_level, + extra_defines=runtime_defines + ["HALT_WAIT"], + ) + link_objs.append(vcc_wait_o) + if device_debug: debug_common_o = stem.with_suffix(".debug_common.o") compile_runtime_c( diff --git a/scripts/ckpt/compile/milp.py b/scripts/ckpt/compile/milp.py index d0507a7..ac43215 100644 --- a/scripts/ckpt/compile/milp.py +++ b/scripts/ckpt/compile/milp.py @@ -85,9 +85,9 @@ def compile_milp( Both modes optionally link with milp_boot.S + milp_runtime.c. """ - # bor/lpm4/swbor halt modes and debug-counters imply linking + # Halt modes and debug-counters imply linking link = opts.link - if opts.halt_mode in ("bor", "lpm4", "swbor"): + if opts.halt_mode in common.HALT_MODES: link = True if opts.device_debug: link = True diff --git a/scripts/ckpt/compile/rockclimb.py b/scripts/ckpt/compile/rockclimb.py index e2105b8..5e5f4e1 100644 --- a/scripts/ckpt/compile/rockclimb.py +++ b/scripts/ckpt/compile/rockclimb.py @@ -76,9 +76,9 @@ def compile_rockclimb( strip .cfi_* -> (if link: assemble + link with rockclimb_boot.S + rockclimb_runtime.c) """ - # bor/lpm4/swbor and debug-counters imply linking + # Halt modes and debug-counters imply linking link = opts.link - if opts.halt_mode in ("bor", "lpm4", "swbor"): + if opts.halt_mode in common.HALT_MODES: link = True if opts.device_debug: link = True diff --git a/scripts/ckpt/compile/schematic.py b/scripts/ckpt/compile/schematic.py index 8747293..6b0b531 100644 --- a/scripts/ckpt/compile/schematic.py +++ b/scripts/ckpt/compile/schematic.py @@ -89,9 +89,9 @@ def compile_schematic( schematic pass -> compile to object -> (if link: assemble + link with schematic_boot.S + schematic_runtime.c) """ - # bor/lpm4/swbor halt modes and debug-counters imply linking + # Halt modes and debug-counters imply linking link = opts.link - if opts.halt_mode in ("bor", "lpm4", "swbor"): + if opts.halt_mode in common.HALT_MODES: link = True if opts.device_debug: link = True diff --git a/scripts/ckpt/env.py b/scripts/ckpt/env.py index 4c3764b..facabac 100644 --- a/scripts/ckpt/env.py +++ b/scripts/ckpt/env.py @@ -44,6 +44,7 @@ class ProjectEnv: bb_freq_runtime: Path = field(init=False) debug_common_c: Path = field(init=False) + vcc_wait_c: Path = field(init=False) sysroot_flags: list[str] = field(init=False) @@ -83,6 +84,7 @@ def __post_init__(self) -> None: ) object.__setattr__(self, "bb_freq_runtime", rt / "bb_freq_runtime.c") object.__setattr__(self, "debug_common_c", rt / "debug_common.c") + object.__setattr__(self, "vcc_wait_c", rt / "vcc_wait.c") # macOS SDK detection flags: list[str] = []