blob: 6ff6a4d7d081c8626586c273fb16b15e199a671a [file]
/*
* 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