diff --git a/src/common/pico_sync/BUILD.bazel b/src/common/pico_sync/BUILD.bazel index 4b65e8921..1b9ff0bef 100644 --- a/src/common/pico_sync/BUILD.bazel +++ b/src/common/pico_sync/BUILD.bazel @@ -5,6 +5,7 @@ package(default_visibility = ["//visibility:public"]) cc_library( name = "pico_sync_headers", hdrs = [ + "include/pico/cond.h", "include/pico/critical_section.h", "include/pico/lock_core.h", "include/pico/mutex.h", @@ -21,6 +22,7 @@ cc_library( cc_library( name = "pico_sync", srcs = [ + "cond.c", "critical_section.c", "lock_core.c", "mutex.c", diff --git a/src/common/pico_sync/CMakeLists.txt b/src/common/pico_sync/CMakeLists.txt index 07e9291e2..28e8db6fc 100644 --- a/src/common/pico_sync/CMakeLists.txt +++ b/src/common/pico_sync/CMakeLists.txt @@ -8,7 +8,7 @@ endif() if (NOT TARGET pico_sync) pico_add_impl_library(pico_sync) target_include_directories(pico_sync_headers SYSTEM INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) - pico_mirrored_target_link_libraries(pico_sync INTERFACE pico_sync_sem pico_sync_mutex pico_sync_critical_section pico_time hardware_sync) + pico_mirrored_target_link_libraries(pico_sync INTERFACE pico_sync_cond pico_sync_sem pico_sync_mutex pico_sync_critical_section pico_time hardware_sync) endif() @@ -19,6 +19,14 @@ if (NOT TARGET pico_sync_core) ) endif() +if (NOT TARGET pico_sync_cond) + pico_add_library(pico_sync_cond) + target_sources(pico_sync_cond INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/cond.c + ) + pico_mirrored_target_link_libraries(pico_sync_cond INTERFACE pico_sync_core) +endif() + if (NOT TARGET pico_sync_sem) pico_add_library(pico_sync_sem) target_sources(pico_sync_sem INTERFACE diff --git a/src/common/pico_sync/cond.c b/src/common/pico_sync/cond.c new file mode 100644 index 000000000..04990833e --- /dev/null +++ b/src/common/pico_sync/cond.c @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022-2025 Paul Guyot + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "pico/cond.h" + +void cond_init(cond_t *cond) { + lock_init(&cond->core, next_striped_spin_lock_num()); + cond->waiter = LOCK_INVALID_OWNER_ID; + cond->broadcast_count = 0; + cond->signaled = false; + __mem_fence_release(); +} + +bool __time_critical_func(cond_wait_until)(cond_t *cond, mutex_t *mtx, absolute_time_t until) { + bool success = true; + bool broadcast = false; + bool mutex_acquired = false; + lock_owner_id_t caller = lock_get_caller_owner_id(); + + // waiter and mtx_core are protected by the cv spin_lock + uint32_t save = spin_lock_blocking(cond->core.spin_lock); + uint64_t current_broadcast = cond->broadcast_count; + if (lock_is_owner_id_valid(cond->waiter)) { + // There is a valid owner of the condition variable: we are not the + // first waiter. + assert(cond->mtx_core.spin_lock == mtx->core.spin_lock); + + // wait until it's released + lock_internal_spin_unlock_with_wait(&cond->core, save); + do { + save = spin_lock_blocking(cond->core.spin_lock); + if (cond->broadcast_count != current_broadcast) { + // Condition variable was broadcast while we were waiting to + // own it. + spin_unlock(cond->core.spin_lock, save); + broadcast = true; + break; + } + if (!lock_is_owner_id_valid(cond->waiter)) { + cond->waiter = caller; + cond->mtx_core = mtx->core; + spin_unlock(cond->core.spin_lock, save); + break; + } + if (is_at_the_end_of_time(until)) { + lock_internal_spin_unlock_with_wait(&cond->core, save); + } else if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&cond->core, save, until)) { + // timed out + success = false; + break; + } + } while (true); + } else { + cond->waiter = caller; + cond->mtx_core = mtx->core; + spin_unlock(cond->core.spin_lock, save); + } + + save = spin_lock_blocking(mtx->core.spin_lock); + assert(mtx->owner == caller); + + if (success && !broadcast) { + if (cond->signaled) { + // as an optimization, do not release the mutex. + cond->signaled = false; + mutex_acquired = true; + spin_unlock(mtx->core.spin_lock, save); + } else { + // release mutex + mtx->owner = LOCK_INVALID_OWNER_ID; + lock_internal_spin_unlock_with_notify(&mtx->core, save); + do { + if (cond->signaled) { + cond->signaled = false; + if (!lock_is_owner_id_valid(mtx->owner)) { + // As an optimization, acquire the mutex here + mtx->owner = caller; + mutex_acquired = true; + } + spin_unlock(mtx->core.spin_lock, save); + break; + } + if (!success) { + if (!lock_is_owner_id_valid(mtx->owner)) { + // As an optimization, acquire the mutex here + mtx->owner = caller; + mutex_acquired = true; + } + spin_unlock(mtx->core.spin_lock, save); + break; + } + if (is_at_the_end_of_time(until)) { + lock_internal_spin_unlock_with_wait(&mtx->core, save); + } else if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&mtx->core, save, until)) { + // timed out + success = false; + } + save = spin_lock_blocking(mtx->core.spin_lock); + } while (true); + } + } + + // free the cond var + save = spin_lock_blocking(cond->core.spin_lock); + if (cond->waiter == caller) { + cond->waiter = LOCK_INVALID_OWNER_ID; + } + lock_internal_spin_unlock_with_notify(&cond->core, save); + + if (!mutex_acquired) { + mutex_enter_blocking(mtx); + } + + return success; +} + +bool __time_critical_func(cond_wait_timeout_ms)(cond_t *cond, mutex_t *mtx, uint32_t timeout_ms) { + return cond_wait_until(cond, mtx, make_timeout_time_ms(timeout_ms)); +} + +bool __time_critical_func(cond_wait_timeout_us)(cond_t *cond, mutex_t *mtx, uint32_t timeout_us) { + return cond_wait_until(cond, mtx, make_timeout_time_us(timeout_us)); +} + +void __time_critical_func(cond_wait)(cond_t *cond, mutex_t *mtx) { + cond_wait_until(cond, mtx, at_the_end_of_time); +} + +void __time_critical_func(cond_signal)(cond_t *cond) { + uint32_t save = spin_lock_blocking(cond->core.spin_lock); + if (lock_is_owner_id_valid(cond->waiter)) { + lock_core_t mtx_core = cond->mtx_core; + // spin_locks can be identical + if (mtx_core.spin_lock != cond->core.spin_lock) { + spin_unlock(cond->core.spin_lock, save); + save = spin_lock_blocking(mtx_core.spin_lock); + } + cond->signaled = true; + lock_internal_spin_unlock_with_notify(&mtx_core, save); + } else { + spin_unlock(cond->core.spin_lock, save); + } +} + +void __time_critical_func(cond_broadcast)(cond_t *cond) { + uint32_t save = spin_lock_blocking(cond->core.spin_lock); + if (lock_is_owner_id_valid(cond->waiter)) { + cond->broadcast_count++; + lock_core_t mtx_core = cond->mtx_core; + // spin_locks can be identical + if (mtx_core.spin_lock != cond->core.spin_lock) { + lock_internal_spin_unlock_with_notify(&cond->core, save); + save = spin_lock_blocking(mtx_core.spin_lock); + } + cond->signaled = true; + lock_internal_spin_unlock_with_notify(&mtx_core, save); + } else { + spin_unlock(cond->core.spin_lock, save); + } +} diff --git a/src/common/pico_sync/include/pico/cond.h b/src/common/pico_sync/include/pico/cond.h new file mode 100644 index 000000000..dd8cf1a90 --- /dev/null +++ b/src/common/pico_sync/include/pico/cond.h @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2022-2025 Paul Guyot + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _PLATFORM_COND_H +#define _PLATFORM_COND_H + +#include "pico/mutex.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file cond.h + * \defgroup cond cond + * \ingroup pico_sync + * \brief Condition variable API for non IRQ mutual exclusion between cores + * + * Condition variables complement mutexes by providing a way to atomically + * wait and release a held mutex. Then, the task on the other core can signal + * the variable, which ends the wait. Often, the other core would also hold + * the shared mutex, so the signaled task waits until the mutex is released. + * In this implementation, the signaling core does not need to hold the mutex. + * + * The implementation is compatible with more than two cores, with the following + * effects: + * - there could be a race condition if two cores try to signal at the same + * time (this would be solved by having them hold a shared mutex when signaling) + * - every core that waits should wait using the same mutex. There is an + * assert if this is not the case. + * - broadcast is implemented and releases every waiting cores. + * + * The condition variables only work with non-recursive mutexes. + * + * Limitations of mutexes also apply to condition variables. See \ref mutex.h + */ + +typedef struct __packed_aligned +{ + lock_core_t core; + lock_owner_id_t waiter; + lock_core_t mtx_core; + uint32_t broadcast_count; // Overflow is unlikely + bool signaled; +} cond_t; + +/*! \brief Initialize a condition variable structure + * \ingroup cond + * + * \param cv Pointer to condition variable structure + */ +void cond_init(cond_t *cv); + +/*! \brief Wait on a condition variable + * \ingroup cond + * + * Wait until a condition variable is signaled or broadcast. The mutex should + * be owned and is released atomically. It is reacquired when this function + * returns. + * + * \param cv Condition variable to wait on + * \param mtx Currently held mutex + */ +void cond_wait(cond_t *cv, mutex_t *mtx); + +/*! \brief Wait on a condition variable with a timeout. + * \ingroup cond + * + * Wait until a condition variable is signaled or broadcast until a given + * time. The mutex is released atomically and reacquired even if the wait + * timed out. + * + * \param cv Condition variable to wait on + * \param mtx Currently held mutex + * \param until The time after which to return if the condition variable was + * not signaled. + * \return true if the condition variable was signaled, false otherwise + */ +bool cond_wait_until(cond_t *cv, mutex_t *mtx, absolute_time_t until); + +/*! \brief Wait on a condition variable with a timeout. + * \ingroup cond + * + * Wait until a condition variable is signaled or broadcast until a given + * time. The mutex is released atomically and reacquired even if the wait + * timed out. + * + * \param cv Condition variable to wait on + * \param mtx Currently held mutex + * \param timeout_ms The timeout in milliseconds. + * \return true if the condition variable was signaled, false otherwise + */ +bool cond_wait_timeout_ms(cond_t *cv, mutex_t *mtx, uint32_t timeout_ms); + +/*! \brief Wait on a condition variable with a timeout. + * \ingroup cond + * + * Wait until a condition variable is signaled or broadcast until a given + * time. The mutex is released atomically and reacquired even if the wait + * timed out. + * + * \param cv Condition variable to wait on + * \param mtx Currently held mutex + * \param timeout_ms The timeout in microseconds. + * \return true if the condition variable was signaled, false otherwise + */ +bool cond_wait_timeout_us(cond_t *cv, mutex_t *mtx, uint32_t timeout_us); + +/*! \brief Signal on a condition variable and wake the waiter + * \ingroup cond + * + * \param cv Condition variable to signal + */ +void cond_signal(cond_t *cv); + +/*! \brief Broadcast a condition variable and wake every waiters + * \ingroup cond + * + * \param cv Condition variable to signal + */ +void cond_broadcast(cond_t *cv); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/common/pico_sync/include/pico/sync.h b/src/common/pico_sync/include/pico/sync.h index 3ee97bbcb..c330be7bc 100644 --- a/src/common/pico_sync/include/pico/sync.h +++ b/src/common/pico_sync/include/pico/sync.h @@ -15,5 +15,6 @@ #include "pico/sem.h" #include "pico/mutex.h" #include "pico/critical_section.h" +#include "pico/cond.h" #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 81647ee93..144ff3b45 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,4 +13,5 @@ if (PICO_ON_DEVICE) add_subdirectory(cmsis_test) add_subdirectory(pico_sem_test) add_subdirectory(pico_sha256_test) + add_subdirectory(pico_cond_test) endif() diff --git a/test/pico_cond_test/BUILD.bazel b/test/pico_cond_test/BUILD.bazel new file mode 100644 index 000000000..f2ba68e4c --- /dev/null +++ b/test/pico_cond_test/BUILD.bazel @@ -0,0 +1,16 @@ +load("//bazel:defs.bzl", "compatible_with_rp2") + +package(default_visibility = ["//visibility:public"]) + +cc_binary( + name = "pico_cond_test", + testonly = True, + srcs = ["pico_cond_test.c"], + # Host doesn't support multicore + target_compatible_with = compatible_with_rp2(), + deps = [ + "//src/rp2_common/pico_multicore", + "//src/rp2_common/pico_stdlib", + "//test/pico_test", + ], +) diff --git a/test/pico_cond_test/CMakeLists.txt b/test/pico_cond_test/CMakeLists.txt new file mode 100644 index 000000000..e7a94c183 --- /dev/null +++ b/test/pico_cond_test/CMakeLists.txt @@ -0,0 +1,6 @@ +if (TARGET pico_multicore) + add_executable(pico_cond_test pico_cond_test.c) + + target_link_libraries(pico_cond_test PRIVATE pico_test pico_sync pico_multicore pico_stdlib ) + pico_add_extra_outputs(pico_cond_test) +endif() diff --git a/test/pico_cond_test/pico_cond_test.c b/test/pico_cond_test/pico_cond_test.c new file mode 100644 index 000000000..03c85eac9 --- /dev/null +++ b/test/pico_cond_test/pico_cond_test.c @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2022-2023 Paul Guyot + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +#include "pico/cond.h" +#include "pico/test.h" +#include "pico/multicore.h" +#include "pico/stdio.h" + +PICOTEST_MODULE_NAME("COND", "condition variable test"); + +static cond_t cond; +static mutex_t mutex; + +static volatile bool test_cond_wait_done; +static volatile bool test_cond_wait_ready; +static void test_cond_wait(void) { + busy_wait_ms(100); + mutex_enter_blocking(&mutex); + test_cond_wait_ready = true; + cond_wait(&cond, &mutex); + test_cond_wait_done = true; + mutex_exit(&mutex); +} + +static volatile bool test_cond_wait_timedout; +static void test_cond_wait_timeout(void) { + busy_wait_ms(100); + mutex_enter_blocking(&mutex); + test_cond_wait_ready = true; + test_cond_wait_timedout = cond_wait_timeout_ms(&cond, &mutex, 200); + test_cond_wait_done = true; + mutex_exit(&mutex); +} + +int main() { + stdio_init_all(); + mutex_init(&mutex); + cond_init(&cond); + + PICOTEST_START(); + + PICOTEST_CHECK(cond.core.spin_lock != mutex.core.spin_lock, "spinlock are identical"); + + PICOTEST_START_SECTION("test cond wait / signal with mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + mutex_enter_blocking(&mutex); + cond_signal(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for mutex release"); + mutex_exit(&mutex); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("test cond wait / signal without mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + cond_signal(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("test cond wait / broadcast with mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + mutex_enter_blocking(&mutex); + cond_broadcast(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for mutex release"); + mutex_exit(&mutex); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("test cond wait / broadcast without mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + cond_broadcast(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("test cond wait with timeout and signal"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + test_cond_wait_timedout = false; + multicore_launch_core1(test_cond_wait_timeout); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait"); + mutex_enter_blocking(&mutex); + cond_signal(&cond); + mutex_exit(&mutex); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_timedout, "core1 did time out"); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("test cond wait with timeout and no signal"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + test_cond_wait_timedout = false; + multicore_launch_core1(test_cond_wait_timeout); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait"); + busy_wait_ms(200); + PICOTEST_CHECK(!test_cond_wait_timedout, "core1 did not time out"); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + int tests = 0; + while (cond.core.spin_lock != mutex.core.spin_lock && tests < (PICO_SPINLOCK_ID_STRIPED_LAST - PICO_SPINLOCK_ID_STRIPED_FIRST)) { + cond_init(&cond); + tests++; + } + + PICOTEST_CHECK(cond.core.spin_lock == mutex.core.spin_lock, "spinlock are different"); + + PICOTEST_START_SECTION("same spinlock -- test cond wait / signal with mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + mutex_enter_blocking(&mutex); + cond_signal(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for mutex release"); + mutex_exit(&mutex); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("same spinlock -- test cond wait / signal without mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + cond_signal(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("same spinlock -- test cond wait / broadcast with mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + mutex_enter_blocking(&mutex); + cond_broadcast(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for mutex release"); + mutex_exit(&mutex); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("same spinlock -- test cond wait / broadcast without mutex"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + multicore_launch_core1(test_cond_wait); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait for signal"); + cond_broadcast(&cond); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("same spinlock -- test cond wait with timeout and signal"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + test_cond_wait_timedout = false; + multicore_launch_core1(test_cond_wait_timeout); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait"); + mutex_enter_blocking(&mutex); + cond_signal(&cond); + mutex_exit(&mutex); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_timedout, "core1 did time out"); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_START_SECTION("same spinlock -- test cond wait with timeout and no signal"); + test_cond_wait_ready = false; + test_cond_wait_done = false; + test_cond_wait_timedout = false; + multicore_launch_core1(test_cond_wait_timeout); + busy_wait_ms(200); + PICOTEST_CHECK(test_cond_wait_ready, "core1 is not ready"); + PICOTEST_CHECK(!test_cond_wait_done, "core1 did not wait"); + busy_wait_ms(200); + PICOTEST_CHECK(!test_cond_wait_timedout, "core1 did not time out"); + PICOTEST_CHECK(test_cond_wait_done, "core1 isn't done"); + multicore_reset_core1(); + PICOTEST_END_SECTION(); + + PICOTEST_END_TEST(); +} diff --git a/tools/bazel_build.py b/tools/bazel_build.py index 522f28310..abc963568 100755 --- a/tools/bazel_build.py +++ b/tools/bazel_build.py @@ -39,6 +39,7 @@ "//test/kitchen_sink:kitchen_sink_cpp", "//test/kitchen_sink:kitchen_sink_lwip_poll", "//test/kitchen_sink:kitchen_sink_lwip_background", + "//test/pico_cond_test:pico_cond_test", "//test/pico_divider_test:pico_divider_test", "//test/pico_divider_test:pico_divider_nesting_test", "//test/pico_float_test:pico_double_test",