Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/intermittent.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ board supply rail, without the 5.6 kΩ resistor and the schottky diode: at
cannot hold the rail. Without an Otii both commands fall back to running on
the ez-FET's 3V3 rail.

Every such run starts from a cold supply, so the Saleae sees a sub-microsecond
glitch on P3.4 while VCC ramps, ahead of the real start pulse (see
[saleae.md](saleae.md)); the extractor ignores pulses narrower than 2 µs.

## How a Run Works

For each (benchmark, capacitor, trace):
Expand Down
16 changes: 14 additions & 2 deletions docs/saleae.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ pulses on P3.4:

| Event | Pulse width | Role |
|-------|-------------|------|
| Start | ~10 µs | Marks execution start; below the trigger threshold |
| Stop | ~5 ms | Marks execution end; fires the capture trigger |
| Start | ~10 µs (nominal) | Marks execution start; below the trigger threshold |
| Stop | ~5 ms (nominal) | Marks execution end; fires the capture trigger |

The widths are nominal: `_timing_delay_cycles` counts iterations, not cycles,
so the pulses come out several times wider (~70 µs and ~34 ms at 16 MHz).
Only their classification matters — well apart on both sides of the 1 ms
trigger threshold.

The capture triggers on `PULSE_HIGH` with a 1 ms minimum pulse width, so only
the stop pulse ends the capture. Execution time is extracted in
Expand All @@ -39,6 +44,13 @@ The GPIO stays LOW during execution, so brown-out resets — which reset port
registers to LOW — are invisible to the capture (pulse-based, BOR-safe
signalling).

A run that starts from a cold supply — every run powered from the Otii main
output, see [intermittent.md](intermittent.md) — emits one more pulse: while
VCC ramps, P3.4 is high-impedance (the firmware cannot clear `LOCKLPM5` and
drive it low until the CPU runs), so the line follows the rail across the
analyzer threshold for ~0.4 µs. Pulses narrower than 2 µs are therefore not
counted as start pulses.

Per measurement, the runner arms a capture, flashes the ELF via mspdebug,
lets the target free-run, waits for the stop-pulse trigger, and exports the
digital data to compute the delta. Ambiguous captures are retried up to
Expand Down
4 changes: 4 additions & 0 deletions passes/runtime/benchmark.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
* Execution time is measured in post-processing as:
* falling edge of the unique short start pulse before the stop pulse →
* rising edge of the first long stop pulse.
*
* A cold power-up adds a sub-microsecond glitch before the start pulse:
* P3.4 is high-impedance until timing_gpio_init() clears LOCKLPM5, so it
* follows the rising supply rail. The extractor filters pulses below 2 us.
*/
#ifdef __MSP430__
#ifndef F_CPU
Expand Down
44 changes: 32 additions & 12 deletions scripts/ckpt/device/saleae.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def saleae_run(
mspdebug session so it resets and free-runs exactly once, or —
with an Otii in the loop — isolate it from the ez-FET and power
it from the Otii main output
4. Start pulse (~10 us) is captured but does not trigger
4. Start pulse (~10 us) is captured but does not trigger; a cold
power-up first emits a sub-us glitch, which is filtered out
5. Program runs (GPIO LOW — BOR resets are invisible)
6. Stop pulse (~5 ms) fires the trigger
7. Record *after_trigger_seconds* more, then capture ends
Expand Down Expand Up @@ -243,6 +244,12 @@ def saleae_run(

_SHORT_PULSE_THRESHOLD = 0.001 # 1 ms — start pulse is ~10 us, stop pulse is ~5 ms

# Narrower than this is a cold power-up glitch, not a start pulse: while VCC
# ramps, P3.4 is high-impedance — the code cannot clear LOCKLPM5 and drive the
# pin low until the CPU runs, milliseconds later — so the line follows the rail
# across the analyzer threshold for ~0.4 us. Every Otii-powered run sees it.
_GLITCH_PULSE_THRESHOLD = 0.000002 # 2 us

# Deadline of a single completion poll: bounds how long the watcher thread
# keeps running after it was told to stop.
_WATCH_POLL_SECONDS = 0.5
Expand Down Expand Up @@ -310,6 +317,20 @@ def _collect_pulses(csv_path: Path) -> list[tuple[float, float]]:
return pulses


def _start_pulses(pulses: list[tuple[float, float]]) -> list[tuple[float, float]]:
"""Short pulses wide enough to be a start pulse, not a power-up glitch."""
return [
(r, f)
for r, f in pulses
if _GLITCH_PULSE_THRESHOLD <= (f - r) < _SHORT_PULSE_THRESHOLD
]


def _stop_pulses(pulses: list[tuple[float, float]]) -> list[tuple[float, float]]:
"""Long pulses — the ones that fire the capture trigger."""
return [(r, f) for r, f in pulses if (f - r) >= _SHORT_PULSE_THRESHOLD]


def _extract_timing(csv_path: Path) -> float:
"""Parse Saleae digital CSV export to find execution time.

Expand All @@ -321,9 +342,10 @@ def _extract_timing(csv_path: Path) -> float:
the short pulse falling edge immediately before the first
long pulse rising edge.

Multiple short pulses before the stop pulse are treated as ambiguous:
they indicate the target likely restarted during capture, so the
extractor refuses to guess which partial run is "correct".
Pulses below _GLITCH_PULSE_THRESHOLD are power-up glitches and do not
count as start pulses. Multiple start pulses before the stop pulse are
treated as ambiguous: they indicate the target likely restarted during
capture, so the extractor refuses to guess which partial run is "correct".

Raises DeviceError if edges are missing.
"""
Expand All @@ -337,13 +359,13 @@ def _extract_timing(csv_path: Path) -> float:
)

# Classify pulses by duration.
short_pulses = [(r, f) for r, f in pulses if (f - r) < _SHORT_PULSE_THRESHOLD]
long_pulses = [(r, f) for r, f in pulses if (f - r) >= _SHORT_PULSE_THRESHOLD]
short_pulses = _start_pulses(pulses)
long_pulses = _stop_pulses(pulses)

if not short_pulses:
raise DeviceError(
"No start pulse (short HIGH < 1 ms) detected on Saleae channel "
f"{_SALEAE_CHANNEL}. Check benchmark GPIO signalling."
"No start pulse (short HIGH between 2 us and 1 ms) detected on "
f"Saleae channel {_SALEAE_CHANNEL}. Check benchmark GPIO signalling."
)
if not long_pulses:
raise DeviceError(
Expand Down Expand Up @@ -486,15 +508,13 @@ def finish_pulse_capture(capture, wait_seconds: float) -> tuple[bool, float | No

def _replay_timing_from_pulses(pulses: list[tuple[float, float]]) -> float | None:
"""First start-pulse falling edge -> first stop-pulse rising edge, in us."""
long_pulses = [(r, f) for r, f in pulses if (f - r) >= _SHORT_PULSE_THRESHOLD]
long_pulses = _stop_pulses(pulses)
if not long_pulses:
logger.warning("Capture triggered but no stop pulse found in the export")
return None
stop_time = long_pulses[0][0]

start_falls = [
f for r, f in pulses if (f - r) < _SHORT_PULSE_THRESHOLD and f < stop_time
]
start_falls = [f for _, f in _start_pulses(pulses) if f < stop_time]
if not start_falls:
logger.warning("Stop pulse captured but no start pulse precedes it")
return None
Expand Down
7 changes: 7 additions & 0 deletions tests/test_intermittent.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ def test_multiple_start_pulses_uses_first(self):
us = _replay_timing_from_pulses([(1.0, 1.00001), (3.0, 3.00001), (5.0, 5.005)])
assert us == pytest.approx((5.0 - 1.00001) * 1e6)

def test_power_up_glitch_is_not_a_start_pulse(self):
# Each cold power-up glitches P3.4 for ~0.4 us before the real pulse.
us = _replay_timing_from_pulses(
[(0.9, 0.90000038), (1.0, 1.00001), (2.0, 2.005)]
)
assert us == pytest.approx((2.0 - 1.00001) * 1e6)

def test_no_stop_pulse(self):
assert _replay_timing_from_pulses([(1.0, 1.00001)]) is None

Expand Down
16 changes: 16 additions & 0 deletions tests/test_saleae_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ def test_multiple_start_pulses_before_stop_is_ambiguous(self, tmp_path: Path):
with pytest.raises(DeviceError, match="Ambiguous Saleae capture"):
_extract_timing(csv_path)

def test_power_up_glitch_is_not_a_start_pulse(self, tmp_path: Path):
csv_path = write_capture_csv(
tmp_path,
[
(0.000000000, 0),
(0.000500000, 1),
(0.000500380, 0), # 0.38 us glitch while VCC ramps
(0.001000000, 1),
(0.001010000, 0),
(0.379125720, 1),
(0.384125720, 0),
],
)

assert _extract_timing(csv_path) == pytest.approx(378115.72)

def test_short_pulses_after_stop_are_ignored(self, tmp_path: Path):
csv_path = write_capture_csv(
tmp_path,
Expand Down
Loading