| /* | |
| * Copyright (c) 2026 Raspberry Pi (Trading) Ltd. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| /* | |
| * Measurement harness for the FreeRTOS/SDK sync interop tests. | |
| * | |
| * Two things need measuring, and both are calibrated in-run against a deliberate | |
| * busy-wait and a deliberate proper block, so no threshold depends on assumptions about | |
| * DWT/mcycle behaviour or clock speed. If a calibration fails to separate the two poles, | |
| * the corresponding metric reports "unavailable" and its assertions are skipped rather | |
| * than producing numbers nobody should trust. | |
| * | |
| * 1. RTOS-level yielding -- did the blocked task give the CPU up, so other work runs? | |
| * Measured with a lowest-priority spinner task incrementing a counter. | |
| * | |
| * 2. Hardware-level sleeping -- did the core actually enter WFE, or spin? | |
| * Measured with the cycle counter (DWT CYCCNT on Arm, mcycle on RISC-V) compared | |
| * against wall clock. Not available on RP2040's M0+. | |
| */ | |
| #ifndef _INTEROP_HARNESS_H | |
| #define _INTEROP_HARNESS_H | |
| #include "pico.h" | |
| #include "pico/time.h" | |
| /* ---- result recording ------------------------------------------------------------- */ | |
| typedef enum { | |
| RESULT_PASS = 0, | |
| RESULT_FAIL, | |
| RESULT_SKIP, /* not meaningful in this configuration */ | |
| RESULT_EXPECTED_FAIL, /* known-broken; recorded, does not fail the run */ | |
| RESULT_INFO, /* measurement only, no pass/fail criterion */ | |
| } result_kind_t; | |
| #define MAX_RESULTS 48 | |
| void harness_record(const char *id, result_kind_t kind, const char *fmt, ...) __printflike(3, 4); | |
| /* returns non-zero if any RESULT_FAIL was recorded */ | |
| int harness_summary(void); | |
| /* ---- latency ---------------------------------------------------------------------- */ | |
| #define LATENCY_BUCKETS 20 /* log2 microsecond buckets: <1, <2, <4, ... */ | |
| typedef struct { | |
| uint32_t count; | |
| uint32_t early; /* samples that completed *before* the target - always a bug */ | |
| int64_t min_us; | |
| int64_t max_us; | |
| int64_t sum_us; | |
| uint32_t bucket[LATENCY_BUCKETS]; | |
| } latency_t; | |
| void latency_reset(latency_t *l); | |
| void latency_add(latency_t *l, int64_t lateness_us); | |
| void latency_print(const latency_t *l, const char *name); | |
| int64_t latency_mean_us(const latency_t *l); | |
| /* ---- spinner: RTOS-level yield detection ------------------------------------------ */ | |
| /* Create the lowest-priority spinner. In SMP it is pinned to the test core. */ | |
| void harness_spinner_start(void); | |
| uint32_t harness_spinner_count(void); | |
| /* | |
| * 0 = the core was fully occupied by something other than the spinner (busy-wait) | |
| * 100 = the core was as free as when idle (the waiter yielded properly) | |
| * -1 = calibration did not separate the two poles; metric unusable | |
| */ | |
| int harness_yield_pct(uint32_t spinner_delta, uint32_t window_ms); | |
| /* ---- cycle occupancy: hardware WFE detection -------------------------------------- */ | |
| uint32_t harness_cycles(void); | |
| /* DWT_SLEEPCNT gives a direct "did this core sleep" signal, unlike CYCCNT which counts | |
| * through WFE. Not implemented on the RP2350 M33; expected on future parts. | |
| * | |
| * 8-bit and WRAPPING, so read it asymmetrically: a change proves the core slept, but no | |
| * change is inconclusive rather than proof it did not (~1/256 of sleeps are an exact | |
| * multiple of 256 cycles and read back unchanged, whatever the duration). | |
| * Per core: must be read on the core being judged. */ | |
| /* | |
| * Whether this platform is expected to have the DWT profiling counters (of which SLEEPCNT is | |
| * one). Positively specified - name the architectures that have them - so an unrecognised | |
| * future platform defaults to "not expected" rather than raising a false failure. | |
| * | |
| * Absent on Armv6-M (Cortex-M0+) and, importantly, on Armv8-M *Baseline* (Cortex-M23) - the | |
| * profiling counters come with Mainline, which is why the SDK gates its software spin lock on | |
| * __ARM_ARCH_8M_MAIN__ rather than on Armv8-M generally. Absent on RISC-V, which has no DWT. | |
| */ | |
| #if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7EM__) | |
| #define HARNESS_EXPECT_SLEEP_COUNTER 1 | |
| #else | |
| #define HARNESS_EXPECT_SLEEP_COUNTER 0 | |
| #endif | |
| /* 1 only where a DWT sleep counter exists at all; 0 elsewhere, where | |
| * harness_sleep_counter() is a constant 0. Should agree with the expectation above. */ | |
| bool harness_sleep_counter_present(void); | |
| /* false where the part has no usable cycle counter, so D0 reports rather than fails */ | |
| bool harness_cycle_counter_present(void); | |
| uint32_t harness_sleep_counter(void); | |
| /* 8-bit wrapping delta between two harness_sleep_counter() reads */ | |
| uint32_t harness_sleep_delta(uint32_t before, uint32_t after); | |
| /* | |
| * The only signal worth acting on: was DWT_SLEEPCNT *observed to move* across a known sleep | |
| * during calibration? It cannot move unless it genuinely works. | |
| * | |
| * Deliberately not derived from DWT_CTRL.NOPRFCNT. That bit covers the profiling counters as | |
| * a group, and the RP2350 M33 implements EXCCNT/FOLDCNT/LSUCNT but not SLEEPCNT - so it | |
| * reads clear (truthfully) while the counter we want is absent. m33.h reflects this: it | |
| * defines M33_DWT_{EXCCNT,FOLDCNT,LSUCNT}_OFFSET but no SLEEPCNT offset at all. | |
| */ | |
| bool harness_sleep_counter_usable(void); | |
| /* Counters differ in width and direction (SysTick counts down), so deltas go through this. */ | |
| uint32_t harness_cycle_delta(uint32_t before, uint32_t after); | |
| /* The counter is per core, and on Arm SysTick belongs to FreeRTOS - so this may only be | |
| * called on a core that is not running FreeRTOS. */ | |
| void harness_cycles_enable_this_core(void); | |
| /* Measured cycles/us while spinning, from the calibration. Should be ~clk_sys in MHz; a | |
| * wildly wrong value (notably 0) means the counter is not running. */ | |
| uint32_t harness_cal_cycles_per_us(void); | |
| /* | |
| * A counter verdict belongs to the core that measured it - DWT is per core, and on Arm | |
| * SysTick belongs to FreeRTOS. Keeping the measurement in a value rather than in harness | |
| * globals is what stops one core's result being reported as a program-wide fact: that is how | |
| * a run once printed "DWT_SLEEPCNT ... not usable" in its banner and then "SLEEPCNT agrees" | |
| * in D1.4, from two different cores' counters. | |
| */ | |
| typedef struct { | |
| uint32_t busy_per_us; /* cycles/us while spinning */ | |
| uint32_t sleep_per_us; /* cycles/us while in WFE */ | |
| uint32_t dwt_ctrl; /* 0 where there is no DWT; diagnoses a dead counter */ | |
| uint32_t demcr; | |
| bool sleepcnt_moved; /* moved across a known sleep */ | |
| bool sleepcnt_moved_awake; /* moved while busy => not a sleep counter at all */ | |
| } harness_cal_t; | |
| /* Measure this core's counters; must run on the core being judged. Writes nothing global. */ | |
| void harness_calibrate_cycles_local(harness_cal_t *out); | |
| /* Install the agent core's measurement. The queries above report the agent's numbers, since | |
| * the agent core is the only one whose sleeping the cases ever judge. */ | |
| void harness_set_agent_calibration(const harness_cal_t *cal); | |
| uint32_t harness_agent_dwt_ctrl(void); | |
| uint32_t harness_agent_demcr(void); | |
| /* False if the agent core never reported one - its probe arms an alarm and waits on it, so | |
| * this fails whenever alarms are not reaching that core. Distinguishing that from "counters | |
| * read zero" is the difference between a broken timer and a disabled counter. */ | |
| bool harness_agent_calibrated(void); | |
| /* | |
| * 100 = the core executed instructions for the whole window (spinning) | |
| * 0 = the core executed almost nothing (asleep in WFE) | |
| * -1 = unavailable on this platform, or calibration failed | |
| */ | |
| int harness_occupancy_pct(uint32_t cycle_delta, uint64_t elapsed_us); | |
| /* ---- setup ------------------------------------------------------------------------ */ | |
| /* Calibrates the spinner. Must be called from a task, after the scheduler starts. | |
| * The cycle calibration is separate and runs on the agent core. */ | |
| void harness_calibrate(void); | |
| void harness_print_calibration(void); | |
| #endif |