Don't copy/paste code into sync/time test cases (#3151)
* add blocked_waiter_wakeup() to time/sync primitives. rework test-cases to use vs copy/paste
fixel bazel compile flags. add PICO_PLATFORM_TEST_HEADER for test injections
* pass timed_out variable to blocked_waiter_wakeup
* add missing bazel srcs
diff --git a/src/common/pico_sync/mutex.c b/src/common/pico_sync/mutex.c
index 5e0f4c2..b2c5a84 100644
--- a/src/common/pico_sync/mutex.c
+++ b/src/common/pico_sync/mutex.c
@@ -66,7 +66,6 @@
return;
}
#endif
- // note: if you change the implementation here, please update the similar code in pico_sync_test.c
lock_owner_id_t caller = lock_get_caller_owner_id();
do {
uint32_t save = spin_lock_blocking(mtx->core.spin_lock);
@@ -76,6 +75,7 @@
break;
}
lock_internal_spin_unlock_with_wait(&mtx->core, save);
+ blocked_waiter_wakeup(false);
} while (true);
}
@@ -92,6 +92,7 @@
} else {
lock_internal_spin_unlock_with_wait(&mtx->core, save);
}
+ blocked_waiter_wakeup(false);
} while (true);
}
@@ -164,7 +165,6 @@
return recursive_mutex_enter_block_until(mtx, until);
}
#endif
- // note: if you change the implementation here, please update the similar code in pico_sync_test.c
assert(mtx->core.spin_lock);
lock_owner_id_t caller = lock_get_caller_owner_id();
do {
@@ -175,17 +175,17 @@
return true;
} else {
if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&mtx->core, save, until)) {
- // timed out
+ blocked_waiter_wakeup(true);
return false;
}
// not timed out; spin lock already unlocked, so loop again
}
+ blocked_waiter_wakeup(false);
} while (true);
}
bool __time_critical_func(recursive_mutex_enter_block_until)(recursive_mutex_t *mtx, absolute_time_t until) {
assert(mtx->core.spin_lock);
- // note: if you change the implementation here, please update the similar code in pico_sync_test.c
lock_owner_id_t caller = lock_get_caller_owner_id();
do {
uint32_t save = spin_lock_blocking(mtx->core.spin_lock);
@@ -197,11 +197,12 @@
return true;
} else {
if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&mtx->core, save, until)) {
- // timed out
+ blocked_waiter_wakeup(true);
return false;
}
// not timed out; spin lock already unlocked, so loop again
}
+ blocked_waiter_wakeup(false);
} while (true);
}
diff --git a/src/common/pico_sync/sem.c b/src/common/pico_sync/sem.c
index ee180f8..33dd58b 100644
--- a/src/common/pico_sync/sem.c
+++ b/src/common/pico_sync/sem.c
@@ -24,7 +24,6 @@
}
void __time_critical_func(sem_acquire_blocking)(semaphore_t *sem) {
- // note: if you change the implementation here, please update the similar code in pico_sync_test.c
// only a caller which waited can have consumed a notification, so only it can owe one to
// the waiters it did not exclude - taking one of several permits leaves the others able to
// proceed. Taking a permit on the first pass consumes no notification, so owes nothing.
@@ -38,6 +37,7 @@
}
lock_internal_spin_unlock_with_wait(&sem->core, save);
waited = true;
+ blocked_waiter_wakeup(false);
} while (true);
}
@@ -50,7 +50,6 @@
}
bool __time_critical_func(sem_acquire_block_until)(semaphore_t *sem, absolute_time_t until) {
- // note: if you change the implementation here, please update the similar code in pico_sync_test.c
bool __unused waited = false;
do {
uint32_t save = spin_lock_blocking(sem->core.spin_lock);
@@ -60,9 +59,11 @@
return true;
}
if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&sem->core, save, until)) {
+ blocked_waiter_wakeup(true);
return false;
}
waited = true;
+ blocked_waiter_wakeup(false);
} while (true);
}
diff --git a/src/common/pico_time/time.c b/src/common/pico_time/time.c
index 72f0bde..692a863 100644
--- a/src/common/pico_time/time.c
+++ b/src/common/pico_time/time.c
@@ -484,6 +484,7 @@
// sleep_until_callback() which also does a __sev() - the irq itself will wake us up if on the same core
__wfe();
#endif
+ blocked_waiter_wakeup(false);
}
}
}
diff --git a/src/common/pico_util/queue.c b/src/common/pico_util/queue.c
index 3307c7e..ba74d17 100644
--- a/src/common/pico_util/queue.c
+++ b/src/common/pico_util/queue.c
@@ -53,6 +53,7 @@
}
if (block) {
lock_internal_spin_unlock_with_wait(&q->core, save);
+ blocked_waiter_wakeup(false);
} else {
spin_unlock(q->core.spin_lock, save);
return false;
@@ -73,6 +74,7 @@
}
if (block) {
lock_internal_spin_unlock_with_wait(&q->core, save);
+ blocked_waiter_wakeup(false);
} else {
spin_unlock(q->core.spin_lock, save);
return false;
@@ -92,6 +94,7 @@
}
if (block) {
lock_internal_spin_unlock_with_wait(&q->core, save);
+ blocked_waiter_wakeup(false);
} else {
spin_unlock(q->core.spin_lock, save);
return false;
diff --git a/src/host/pico_platform/include/pico/platform.h b/src/host/pico_platform/include/pico/platform.h
index a4ed96c..1f54ddd 100644
--- a/src/host/pico_platform/include/pico/platform.h
+++ b/src/host/pico_platform/include/pico/platform.h
@@ -163,6 +163,8 @@
void busy_wait_at_least_cycles(uint32_t minimum_cycles);
+static inline void blocked_waiter_wakeup(__unused bool timed_out) {}
+
// PICO_CONFIG: PICO_NUM_VTABLE_IRQS, Number of IRQ handlers in the vector table - can be lowered to save space if you aren't using some higher IRQs, type=int, default=NUM_IRQS, group=hardware_irq
#ifndef PICO_NUM_VTABLE_IRQS
#define PICO_NUM_VTABLE_IRQS NUM_IRQS
diff --git a/src/rp2_common/pico_platform_common/include/pico/platform/common.h b/src/rp2_common/pico_platform_common/include/pico/platform/common.h
index c4645d6..45ba560 100644
--- a/src/rp2_common/pico_platform_common/include/pico/platform/common.h
+++ b/src/rp2_common/pico_platform_common/include/pico/platform/common.h
@@ -18,6 +18,11 @@
* but making an explicit library dependency does not make sense.
*/
+// PICO_CONFIG: PICO_PLATFORM_TEST_HEADER, Unquoted path to a header to include here so a test can inject code/defines into every translation unit, group=pico_platform
+#ifdef PICO_PLATFORM_TEST_HEADER
+#include __PICO_XSTRING(PICO_PLATFORM_TEST_HEADER)
+#endif
+
// PICO_CONFIG: PICO_MINIMAL_STORED_VECTOR_TABLE, Only store a very minimal vector table in the binary on Arm, type=bool, default=0, advanced=true, group=pico_crt0
#ifndef PICO_MINIMAL_STORED_VECTOR_TABLE
#define PICO_MINIMAL_STORED_VECTOR_TABLE 0
@@ -68,10 +73,23 @@
* \ingroup pico_platform
*
* No-op function intended to be called by any tight hardware polling loop. Using this ubiquitously
- * makes it much easier to find tight loops, but also in the future \#ifdef-ed support for lockup
- * debugging might be added
+ * makes it much easier to find tight loops, and may be #ifdef-ed for debugging
*/
+#ifndef tight_loop_contents
static __force_inline void tight_loop_contents(void) {}
+#endif
+
+/*! \brief No-op function to record that a blocked waiter has woken up
+ * \ingroup pico_platform
+ *
+ * No-op function intended to be called by any primitive which has just completed a wait for a condition to be
+ * satisfied - including a wait that ended in a timeout rather than the condition. This is distinct from
+ * \ref tight_loop_contents in that this operation follows some sort of actual wait (e.g. `__wfe()`) vs
+ * polling. This is mostly intended to be overridden for debugging or test cases
+ */
+#ifndef blocked_waiter_wakeup
+static __force_inline void blocked_waiter_wakeup(__unused bool timed_out) {}
+#endif
#define host_safe_hw_ptr(x) ((uintptr_t)(x))
#define native_safe_hw_ptr(x) host_safe_hw_ptr(x)
diff --git a/test/pico_sync_test/BUILD.bazel b/test/pico_sync_test/BUILD.bazel
index 9bc6734..cdab1df 100644
--- a/test/pico_sync_test/BUILD.bazel
+++ b/test/pico_sync_test/BUILD.bazel
@@ -1,15 +1,17 @@
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("//bazel:defs.bzl", "compatible_with_rp2")
+load("//bazel/util:transition.bzl", "extra_copts_for_all_deps")
package(default_visibility = ["//visibility:public"])
cc_binary(
- name = "pico_sync_test",
+ name = "pico_sync_test_actual",
testonly = True,
- srcs = ["pico_sync_test.c"],
- defines = [
- "PICO_USE_SW_SPIN_LOCKS=0",
+ srcs = [
+ "pico_sync_test.c",
+ "pico_sync_test_hook.h",
],
+ tags = ["manual"], # Built via pico_sync_test / pico_sync_test_sw.
target_compatible_with = compatible_with_rp2(),
deps = [
"//src/common/pico_sync",
@@ -21,20 +23,18 @@
}),
)
-cc_binary(
+extra_copts_for_all_deps(
+ name = "pico_sync_test",
+ testonly = True,
+ src = ":pico_sync_test_actual",
+ extra_copts = ["-DPICO_USE_SW_SPIN_LOCKS=0"],
+ target_compatible_with = compatible_with_rp2(),
+)
+
+extra_copts_for_all_deps(
name = "pico_sync_test_sw",
testonly = True,
- srcs = ["pico_sync_test.c"],
- defines = [
- "PICO_USE_SW_SPIN_LOCKS=1",
- ],
+ src = ":pico_sync_test_actual",
+ extra_copts = ["-DPICO_USE_SW_SPIN_LOCKS=1"],
target_compatible_with = ["//bazel/constraint:rp2350"],
- deps = [
- "//src/common/pico_sync",
- "//src/rp2_common/pico_multicore",
- "//test/pico_test",
- ] + select({
- "//bazel/constraint:host": ["//src/host/pico_stdlib"],
- "//conditions:default": ["//src/rp2_common/pico_stdlib"],
- }),
)
diff --git a/test/pico_sync_test/CMakeLists.txt b/test/pico_sync_test/CMakeLists.txt
index ea2056d..5ae5fcb 100644
--- a/test/pico_sync_test/CMakeLists.txt
+++ b/test/pico_sync_test/CMakeLists.txt
@@ -1,11 +1,16 @@
add_executable(pico_sync_test pico_sync_test.c)
target_compile_definitions(pico_sync_test PRIVATE PICO_USE_SW_SPIN_LOCKS=0)
+target_include_directories(pico_sync_test PRIVATE ${CMAKE_CURRENT_LIST_DIR})
+target_compile_definitions(pico_sync_test PRIVATE
+ PICO_PLATFORM_TEST_HEADER=pico_sync_test_hook.h)
target_link_libraries(pico_sync_test PRIVATE pico_test pico_sync pico_multicore)
pico_add_extra_outputs(pico_sync_test)
if (NOT PICO_RP2040)
add_executable(pico_sync_test_sw pico_sync_test.c)
target_compile_definitions(pico_sync_test_sw PRIVATE PICO_USE_SW_SPIN_LOCKS=1)
+ target_include_directories(pico_sync_test_sw PRIVATE ${CMAKE_CURRENT_LIST_DIR})
+ target_compile_definitions(pico_sync_test_sw PRIVATE PICO_PLATFORM_TEST_HEADER=pico_sync_test_hook.h)
target_link_libraries(pico_sync_test_sw PRIVATE pico_test pico_sync pico_multicore)
pico_add_extra_outputs(pico_sync_test_sw)
endif()
diff --git a/test/pico_sync_test/pico_sync_test.c b/test/pico_sync_test/pico_sync_test.c
index cc50dac..f53223e 100644
--- a/test/pico_sync_test/pico_sync_test.c
+++ b/test/pico_sync_test/pico_sync_test.c
@@ -18,22 +18,17 @@
PICOTEST_MODULE_NAME("SYNC", "sync test");
-typedef struct {
- lock_core_t lock;
- bool flag;
-} lock_with_flag_t;;
+// Counts wakeups from the wait loops inside pico_sync and pico_time, via the blocked_waiter_wakeup hook
+volatile int wakeups[NUM_CORES];
-int64_t notify_lock_with_flag(__unused alarm_id_t id, void *user_data) {
- lock_with_flag_t *lock_with_flag = (lock_with_flag_t *)user_data;
- uint32_t save = spin_lock_blocking(lock_with_flag->lock.spin_lock);
- lock_with_flag->flag = true;
- lock_internal_spin_unlock_with_notify(&lock_with_flag->lock, save);
- return 0;
+void pico_sync_test_wakeup(__unused bool timed_out) {
+ wakeups[get_core_num()]++;
}
-static int64_t sleep_until_callback(__unused alarm_id_t id, __unused void *user_data) {
- // note: this implementation is a copy of the code from pico_time/time.c and should be kept in sync
- __sev();
+static semaphore_t release_sem;
+
+static int64_t release_sem_callback(__unused alarm_id_t id, __unused void *user_data) {
+ sem_release(&release_sem);
return 0;
}
@@ -43,64 +38,43 @@
printf("=== Test on core %d ===\n", get_core_num());
PICOTEST_START_SECTION("check low power lock_core wait loop with timeout");
- // note: this implementation is implemented in a similar functions to mutex_enter_block_until(),
- // sem_acquire_block_until() etc. and should be updated if they are
- lock_core_t lock;
- lock_init(&lock, 0);
+ // A semaphore with no permits, so the acquire waits out the whole timeout
+ semaphore_t empty_sem;
+ sem_init(&empty_sem, 0, 1);
absolute_time_t until = make_timeout_time_ms(50);
- int wait_count = 0;
- do {
- uint32_t save = spin_lock_blocking(lock.spin_lock);
- wait_count++;
- if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&lock, save, until)) break;
- } while (true);
+ wakeups[get_core_num()] = 0;
+ bool acquired = sem_acquire_block_until(&empty_sem, until);
+ int wait_count = wakeups[get_core_num()];
printf("Waited %d times\n", wait_count);
-
+ PICOTEST_CHECK(!acquired, "Expected the acquire to time out");
+ PICOTEST_CHECK(time_reached(until), "Expected to have reached the timeout");
+ // a count of zero means the wait never happened, or blocked_waiter_wakeup is not reaching us
+ PICOTEST_CHECK(wait_count > 0, "Expected at least one wait");
PICOTEST_CHECK(wait_count <= MAX_WAITS, "Expected <= %d waits", MAX_WAITS);
PICOTEST_END_SECTION();
PICOTEST_START_SECTION("check low power lock_core wait loop without timeout");
- // note: this implementation is implemented in a similar functions to mutex_enter_blocking(),
- // sem_acquire_blocking() etc. and should be updated if they are
- lock_with_flag_t lock_with_flag;
- lock_init(&lock_with_flag.lock, 0);
- lock_with_flag.flag = false;
- add_alarm_in_ms(50, notify_lock_with_flag, &lock_with_flag, false);
- __wfe(); // consume outstanding one from adding alarm
- int wait_count = 0;
- do {
- uint32_t save = spin_lock_blocking(lock_with_flag.lock.spin_lock);
- if (lock_with_flag.flag) {
- spin_unlock(lock_with_flag.lock.spin_lock, save);
- break;
- }
- wait_count++;
- lock_internal_spin_unlock_with_wait(&lock_with_flag.lock, save);
- } while (true);
+ // Nothing to acquire until the alarm releases a permit, so the acquire has to wait
+ sem_init(&release_sem, 0, 1);
+ add_alarm_in_ms(50, release_sem_callback, NULL, false);
+ wakeups[get_core_num()] = 0;
+ sem_acquire_blocking(&release_sem);
+ int wait_count = wakeups[get_core_num()];
printf("Waited %d times\n", wait_count);
+ // a count of zero means the wait never happened, or blocked_waiter_wakeup is not reaching us
+ PICOTEST_CHECK(wait_count > 0, "Expected at least one wait");
PICOTEST_CHECK(wait_count <= MAX_WAITS, "Expected <= %d waits", MAX_WAITS);
PICOTEST_END_SECTION();
PICOTEST_START_SECTION("check low power sleep loop");
- // note: this sleep implementation is a copy of the code from pico_time/time.c and should be kept in sync
- int wait_count = 0;
- absolute_time_t t = make_timeout_time_ms(500);
- uint64_t t_us = to_us_since_boot(t);
- uint64_t t_before_us = t_us - PICO_TIME_SLEEP_OVERHEAD_ADJUST_US;
- // needs to work in the first PICO_TIME_SLEEP_OVERHEAD_ADJUST_US of boot
- if (t_before_us > t_us) t_before_us = 0;
- absolute_time_t t_before;
- update_us_since_boot(&t_before, t_before_us);
- if (absolute_time_diff_us(get_absolute_time(), t_before) > 0) {
- if (add_alarm_at(t_before, sleep_until_callback, NULL, false) >= 0) {
- // able to add alarm for just before the time
- while (!time_reached(t_before)) {
- __wfe();
- wait_count++;
- }
- }
- }
+ absolute_time_t sleep_target = make_timeout_time_ms(500);
+ wakeups[get_core_num()] = 0;
+ sleep_until(sleep_target);
+ int wait_count = wakeups[get_core_num()];
printf("Waited %d times\n", wait_count);
+ PICOTEST_CHECK(time_reached(sleep_target), "Expected to have slept until the target");
+ // a count of zero means the wait never happened, or blocked_waiter_wakeup is not reaching us
+ PICOTEST_CHECK(wait_count > 0, "Expected at least one wait");
PICOTEST_CHECK(wait_count <= MAX_WAITS, "Expected <= %d waits", MAX_WAITS);
PICOTEST_END_SECTION();
diff --git a/test/pico_sync_test/pico_sync_test_hook.h b/test/pico_sync_test/pico_sync_test_hook.h
new file mode 100644
index 0000000..659a731
--- /dev/null
+++ b/test/pico_sync_test/pico_sync_test_hook.h
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2026 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// Injected into every translation unit of this test via PICO_PLATFORM_TEST_HEADER, so that the
+// wait loops inside pico_sync and pico_time report each wakeup to the test.
+
+#ifndef _PICO_SYNC_TEST_HOOK_H
+#define _PICO_SYNC_TEST_HOOK_H
+
+#ifndef __ASSEMBLER__
+extern void pico_sync_test_wakeup(bool);
+#endif
+
+#define blocked_waiter_wakeup pico_sync_test_wakeup
+
+#endif
diff --git a/test/sync_interop_test/BUILD.bazel b/test/sync_interop_test/BUILD.bazel
index af557d7..58b1fdc 100644
--- a/test/sync_interop_test/BUILD.bazel
+++ b/test/sync_interop_test/BUILD.bazel
@@ -1,26 +1,23 @@
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("//bazel:defs.bzl", "compatible_with_rp2")
+load("//bazel/util:transition.bzl", "extra_copts_for_all_deps")
package(default_visibility = ["//visibility:public"])
# Only the baseline (no RTOS) variants are built here. The FreeRTOS variants need a kernel
# that the Bazel build does not provide, and are CMake-only - see CMakeLists.txt.
cc_binary(
- name = "sync_interop_test",
+ name = "sync_interop_test_actual",
testonly = True,
srcs = [
"interop_harness.c",
"interop_harness.h",
"interop_platform.c",
"interop_platform.h",
+ "interop_test_hook.h",
"sync_interop_test.c",
],
- defines = [
- "PICO_USE_SW_SPIN_LOCKS=0",
- # printf is instrumentation here, not the subject: the two disciplines are modelled
- # with the suite's own mutexes so no case depends on PICO_STDOUT_MUTEX.
- "PICO_STDOUT_MUTEX=0",
- ],
+ tags = ["manual"], # Built via sync_interop_test / sync_interop_test_sw.
target_compatible_with = compatible_with_rp2(),
deps = [
"//src/common/pico_sync",
@@ -31,29 +28,28 @@
],
)
-cc_binary(
+extra_copts_for_all_deps(
+ name = "sync_interop_test",
+ testonly = True,
+ src = ":sync_interop_test_actual",
+ extra_copts = [
+ "-DPICO_USE_SW_SPIN_LOCKS=0",
+ # printf is instrumentation here, not the subject: the two disciplines are modelled
+ # with the suite's own mutexes so no case depends on PICO_STDOUT_MUTEX.
+ "-DPICO_STDOUT_MUTEX=0",
+ ],
+ target_compatible_with = compatible_with_rp2(),
+)
+
+extra_copts_for_all_deps(
name = "sync_interop_test_sw",
testonly = True,
- srcs = [
- "interop_harness.c",
- "interop_harness.h",
- "interop_platform.c",
- "interop_platform.h",
- "sync_interop_test.c",
+ src = ":sync_interop_test_actual",
+ extra_copts = [
+ "-DPICO_USE_SW_SPIN_LOCKS=1",
+ "-DPICO_STDOUT_MUTEX=0",
],
- defines = [
- "PICO_USE_SW_SPIN_LOCKS=1",
- "PICO_STDOUT_MUTEX=0",
- ],
- # software spin locks need Armv8-M or RISC-V atomics, so there is no RP2040 variant
target_compatible_with = ["//bazel/constraint:rp2350"],
- deps = [
- "//src/common/pico_sync",
- "//src/common/pico_time",
- "//src/rp2_common/hardware_clocks",
- "//src/rp2_common/pico_multicore",
- "//src/rp2_common/pico_stdlib",
- ],
)
# Used only by the CMake FreeRTOS variants; listed so the file is accounted for.
diff --git a/test/sync_interop_test/CMakeLists.txt b/test/sync_interop_test/CMakeLists.txt
index 561142f..abfee6f 100644
--- a/test/sync_interop_test/CMakeLists.txt
+++ b/test/sync_interop_test/CMakeLists.txt
@@ -16,6 +16,7 @@
# with our own mutexes so that no case depends on the value of PICO_STDOUT_MUTEX.
target_compile_definitions(${NAME} PRIVATE
PICO_STDOUT_MUTEX=0
+ PICO_PLATFORM_TEST_HEADER=interop_test_hook.h
${ARGN}
)
# D1.7/D2.5 do a blocking lock_core wait from ISR context. That is legal bare (and so
diff --git a/test/sync_interop_test/interop_platform.c b/test/sync_interop_test/interop_platform.c
index 110f942..8111ffc 100644
--- a/test/sync_interop_test/interop_platform.c
+++ b/test/sync_interop_test/interop_platform.c
@@ -35,28 +35,31 @@
* distinguish the two. A wait that really blocks takes 1-5 iterations; a busy-poll runs to
* thousands.
*
- * NOTE: keep these in step with mutex.c, as pico_sync_test.c already has to.
*/
static uint32_t inline_wait_sleep_delta;
uint32_t last_inline_wait_sleep_delta(void) { return inline_wait_sleep_delta; }
-uint32_t counted_mutex_enter_blocking(mutex_t *mtx) {
- uint32_t waits = 0;
+static uint32_t interop_wakeups[NUM_CORES];
+static uint32_t interop_sleep_mark[NUM_CORES];
+
+void interop_wait_wakeup(__unused bool timed_out) {
+ uint core = get_core_num();
+ uint32_t sc = harness_sleep_counter();
+ inline_wait_sleep_delta += harness_sleep_delta(interop_sleep_mark[core], sc);
+ interop_sleep_mark[core] = sc;
+ interop_wakeups[core]++;
+}
+
+static void interop_count_reset(void) {
inline_wait_sleep_delta = 0;
- lock_owner_id_t caller = lock_get_caller_owner_id();
- do {
- uint32_t save = spin_lock_blocking(mtx->core.spin_lock);
- if (!lock_is_owner_id_valid(mtx->owner)) {
- mtx->owner = caller;
- spin_unlock(mtx->core.spin_lock, save);
- break;
- }
- waits++;
- uint32_t sc0 = harness_sleep_counter();
- lock_internal_spin_unlock_with_wait(&mtx->core, save);
- inline_wait_sleep_delta += harness_sleep_delta(sc0, harness_sleep_counter());
- } while (true);
- return waits;
+ interop_sleep_mark[get_core_num()] = harness_sleep_counter();
+ interop_wakeups[get_core_num()] = 0;
+}
+
+uint32_t counted_mutex_enter_blocking(mutex_t *mtx) {
+ interop_count_reset();
+ mutex_enter_blocking(mtx);
+ return interop_wakeups[get_core_num()];
}
uint32_t bare_mutex_enter_blocking(mutex_t *mtx) {
@@ -71,7 +74,7 @@
break;
}
waits++;
- /* deliberately NOT lock_internal_spin_unlock_with_wait() */
+ /* deliberately NOT lock_internal_spin_unlock_with_wait(), so this one stays a copy */
uint32_t sc0 = harness_sleep_counter();
spin_unlock(mtx->core.spin_lock, save);
__wfe();
@@ -82,22 +85,9 @@
uint32_t counted_mutex_enter_block_until(mutex_t *mtx, absolute_time_t until,
bool *acquired) {
- uint32_t waits = 0;
- lock_owner_id_t caller = lock_get_caller_owner_id();
- *acquired = false;
- do {
- uint32_t save = spin_lock_blocking(mtx->core.spin_lock);
- if (!lock_is_owner_id_valid(mtx->owner)) {
- mtx->owner = caller;
- spin_unlock(mtx->core.spin_lock, save);
- *acquired = true;
- return waits;
- }
- waits++;
- if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&mtx->core, save, until)) {
- return waits; /* timed out */
- }
- } while (true);
+ interop_count_reset();
+ *acquired = mutex_enter_block_until(mtx, until);
+ return interop_wakeups[get_core_num()];
}
#if INTEROP_HAS_SDK_CORE
diff --git a/test/sync_interop_test/interop_platform.h b/test/sync_interop_test/interop_platform.h
index b264c88..89db3f3 100644
--- a/test/sync_interop_test/interop_platform.h
+++ b/test/sync_interop_test/interop_platform.h
@@ -86,12 +86,13 @@
*/
void plat_hold_for_ms(uint32_t ms);
-/* ---- counted copies of the SDK primitives ------------------------------------------
- * Faithful copies of mutex_enter_blocking() / mutex_enter_block_until() with the wait
- * iterations counted. Counting is the only reliable way to tell sleeping from spinning:
- * a timeout that fires exactly on time looks the same either way. Available locally as
- * well as via the agent, so the driving core's waits can be judged too.
- * NOTE: keep in step with src/common/pico_sync/mutex.c.
+/* ---- counted calls of the SDK primitives -------------------------------------------
+ * mutex_enter_blocking() / mutex_enter_block_until() with the wait iterations counted, via
+ * the blocked_waiter_wakeup hook those primitives call. Counting is the only reliable way to tell
+ * sleeping from spinning: a timeout that fires exactly on time looks the same either way.
+ * Available locally as well as via the agent, so the driving core's waits can be judged too.
+ * Not reentrant, and it counts every contended wait in the program, so nothing else may wait
+ * while one of these is in progress.
*/
uint32_t counted_mutex_enter_blocking(mutex_t *mtx);
uint32_t counted_mutex_enter_block_until(mutex_t *mtx, absolute_time_t until, bool *acquired);
diff --git a/test/sync_interop_test/interop_test_hook.h b/test/sync_interop_test/interop_test_hook.h
new file mode 100644
index 0000000..8a7f933
--- /dev/null
+++ b/test/sync_interop_test/interop_test_hook.h
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2026 Raspberry Pi (Trading) Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// Injected into every translation unit via PICO_PLATFORM_TEST_HEADER so that the wait loops
+// inside pico_sync and pico_time can be recorded
+
+#ifndef _INTEROP_TEST_HOOK_H
+#define _INTEROP_TEST_HOOK_H
+
+#ifndef __ASSEMBLER__
+extern void interop_wait_wakeup(bool);
+#endif
+
+#define blocked_waiter_wakeup interop_wait_wakeup
+
+#endif