Skip to content

Commit b333635

Browse files
committed
Add condition variables to pico_sync (fix #1093)
Implement condition variables as a companion to mutexes. Condition variables can be signaled and broadcast and implementation should work with any number of cores. To prevent deadlocks, at most a single spin lock is held at a given time. As a result there can be a race condition if several cores call cond_signal at the same time (without holding the mutex).
1 parent bddd20f commit b333635

File tree

10 files changed

+567
-1
lines changed

10 files changed

+567
-1
lines changed

src/common/pico_sync/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package(default_visibility = ["//visibility:public"])
55
cc_library(
66
name = "pico_sync_headers",
77
hdrs = [
8+
"include/pico/cond.h",
89
"include/pico/critical_section.h",
910
"include/pico/lock_core.h",
1011
"include/pico/mutex.h",
@@ -21,6 +22,7 @@ cc_library(
2122
cc_library(
2223
name = "pico_sync",
2324
srcs = [
25+
"cond.c",
2426
"critical_section.c",
2527
"lock_core.c",
2628
"mutex.c",

src/common/pico_sync/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ endif()
88
if (NOT TARGET pico_sync)
99
pico_add_impl_library(pico_sync)
1010
target_include_directories(pico_sync_headers SYSTEM INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include)
11-
pico_mirrored_target_link_libraries(pico_sync INTERFACE pico_sync_sem pico_sync_mutex pico_sync_critical_section pico_time hardware_sync)
11+
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)
1212
endif()
1313

1414

@@ -19,6 +19,14 @@ if (NOT TARGET pico_sync_core)
1919
)
2020
endif()
2121

22+
if (NOT TARGET pico_sync_cond)
23+
pico_add_library(pico_sync_cond)
24+
target_sources(pico_sync_cond INTERFACE
25+
${CMAKE_CURRENT_LIST_DIR}/cond.c
26+
)
27+
pico_mirrored_target_link_libraries(pico_sync_cond INTERFACE pico_sync_core)
28+
endif()
29+
2230
if (NOT TARGET pico_sync_sem)
2331
pico_add_library(pico_sync_sem)
2432
target_sources(pico_sync_sem INTERFACE

src/common/pico_sync/cond.c

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright (c) 2022-2025 Paul Guyot <[email protected]>
3+
*
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
7+
#include "pico/cond.h"
8+
9+
void cond_init(cond_t *cond) {
10+
lock_init(&cond->core, next_striped_spin_lock_num());
11+
cond->waiter = LOCK_INVALID_OWNER_ID;
12+
cond->broadcast_count = 0;
13+
cond->signaled = false;
14+
__mem_fence_release();
15+
}
16+
17+
bool __time_critical_func(cond_wait_until)(cond_t *cond, mutex_t *mtx, absolute_time_t until) {
18+
bool success = true;
19+
bool broadcast = false;
20+
bool mutex_acquired = false;
21+
lock_owner_id_t caller = lock_get_caller_owner_id();
22+
23+
// waiter and mtx_core are protected by the cv spin_lock
24+
uint32_t save = spin_lock_blocking(cond->core.spin_lock);
25+
uint64_t current_broadcast = cond->broadcast_count;
26+
if (lock_is_owner_id_valid(cond->waiter)) {
27+
// There is a valid owner of the condition variable: we are not the
28+
// first waiter.
29+
assert(cond->mtx_core.spin_lock == mtx->core.spin_lock);
30+
31+
// wait until it's released
32+
lock_internal_spin_unlock_with_wait(&cond->core, save);
33+
do {
34+
save = spin_lock_blocking(cond->core.spin_lock);
35+
if (cond->broadcast_count != current_broadcast) {
36+
// Condition variable was broadcast while we were waiting to
37+
// own it.
38+
spin_unlock(cond->core.spin_lock, save);
39+
broadcast = true;
40+
break;
41+
}
42+
if (!lock_is_owner_id_valid(cond->waiter)) {
43+
cond->waiter = caller;
44+
cond->mtx_core = mtx->core;
45+
spin_unlock(cond->core.spin_lock, save);
46+
break;
47+
}
48+
if (is_at_the_end_of_time(until)) {
49+
lock_internal_spin_unlock_with_wait(&cond->core, save);
50+
} else if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&cond->core, save, until)) {
51+
// timed out
52+
success = false;
53+
break;
54+
}
55+
} while (true);
56+
} else {
57+
cond->waiter = caller;
58+
cond->mtx_core = mtx->core;
59+
spin_unlock(cond->core.spin_lock, save);
60+
}
61+
62+
save = spin_lock_blocking(mtx->core.spin_lock);
63+
assert(mtx->owner == caller);
64+
65+
if (success && !broadcast) {
66+
if (cond->signaled) {
67+
// as an optimization, do not release the mutex.
68+
cond->signaled = false;
69+
mutex_acquired = true;
70+
spin_unlock(mtx->core.spin_lock, save);
71+
} else {
72+
// release mutex
73+
mtx->owner = LOCK_INVALID_OWNER_ID;
74+
lock_internal_spin_unlock_with_notify(&mtx->core, save);
75+
do {
76+
if (cond->signaled) {
77+
cond->signaled = false;
78+
if (!lock_is_owner_id_valid(mtx->owner)) {
79+
// As an optimization, acquire the mutex here
80+
mtx->owner = caller;
81+
mutex_acquired = true;
82+
}
83+
spin_unlock(mtx->core.spin_lock, save);
84+
break;
85+
}
86+
if (!success) {
87+
if (!lock_is_owner_id_valid(mtx->owner)) {
88+
// As an optimization, acquire the mutex here
89+
mtx->owner = caller;
90+
mutex_acquired = true;
91+
}
92+
spin_unlock(mtx->core.spin_lock, save);
93+
break;
94+
}
95+
if (is_at_the_end_of_time(until)) {
96+
lock_internal_spin_unlock_with_wait(&mtx->core, save);
97+
} else if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&mtx->core, save, until)) {
98+
// timed out
99+
success = false;
100+
}
101+
save = spin_lock_blocking(mtx->core.spin_lock);
102+
} while (true);
103+
}
104+
}
105+
106+
// free the cond var
107+
save = spin_lock_blocking(cond->core.spin_lock);
108+
if (cond->waiter == caller) {
109+
cond->waiter = LOCK_INVALID_OWNER_ID;
110+
}
111+
lock_internal_spin_unlock_with_notify(&cond->core, save);
112+
113+
if (!mutex_acquired) {
114+
mutex_enter_blocking(mtx);
115+
}
116+
117+
return success;
118+
}
119+
120+
bool __time_critical_func(cond_wait_timeout_ms)(cond_t *cond, mutex_t *mtx, uint32_t timeout_ms) {
121+
return cond_wait_until(cond, mtx, make_timeout_time_ms(timeout_ms));
122+
}
123+
124+
bool __time_critical_func(cond_wait_timeout_us)(cond_t *cond, mutex_t *mtx, uint32_t timeout_us) {
125+
return cond_wait_until(cond, mtx, make_timeout_time_us(timeout_us));
126+
}
127+
128+
void __time_critical_func(cond_wait)(cond_t *cond, mutex_t *mtx) {
129+
cond_wait_until(cond, mtx, at_the_end_of_time);
130+
}
131+
132+
void __time_critical_func(cond_signal)(cond_t *cond) {
133+
uint32_t save = spin_lock_blocking(cond->core.spin_lock);
134+
if (lock_is_owner_id_valid(cond->waiter)) {
135+
lock_core_t mtx_core = cond->mtx_core;
136+
// spin_locks can be identical
137+
if (mtx_core.spin_lock != cond->core.spin_lock) {
138+
spin_unlock(cond->core.spin_lock, save);
139+
save = spin_lock_blocking(mtx_core.spin_lock);
140+
}
141+
cond->signaled = true;
142+
lock_internal_spin_unlock_with_notify(&mtx_core, save);
143+
} else {
144+
spin_unlock(cond->core.spin_lock, save);
145+
}
146+
}
147+
148+
void __time_critical_func(cond_broadcast)(cond_t *cond) {
149+
uint32_t save = spin_lock_blocking(cond->core.spin_lock);
150+
if (lock_is_owner_id_valid(cond->waiter)) {
151+
cond->broadcast_count++;
152+
lock_core_t mtx_core = cond->mtx_core;
153+
// spin_locks can be identical
154+
if (mtx_core.spin_lock != cond->core.spin_lock) {
155+
lock_internal_spin_unlock_with_notify(&cond->core, save);
156+
save = spin_lock_blocking(mtx_core.spin_lock);
157+
}
158+
cond->signaled = true;
159+
lock_internal_spin_unlock_with_notify(&mtx_core, save);
160+
} else {
161+
spin_unlock(cond->core.spin_lock, save);
162+
}
163+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright (c) 2022-2025 Paul Guyot <[email protected]>
3+
*
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
7+
#ifndef _PLATFORM_COND_H
8+
#define _PLATFORM_COND_H
9+
10+
#include "pico/mutex.h"
11+
12+
#ifdef __cplusplus
13+
extern "C" {
14+
#endif
15+
16+
/** \file cond.h
17+
* \defgroup cond cond
18+
* \ingroup pico_sync
19+
* \brief Condition variable API for non IRQ mutual exclusion between cores
20+
*
21+
* Condition variables complement mutexes by providing a way to atomically
22+
* wait and release a held mutex. Then, the task on the other core can signal
23+
* the variable, which ends the wait. Often, the other core would also hold
24+
* the shared mutex, so the signaled task waits until the mutex is released.
25+
* In this implementation, the signaling core does not need to hold the mutex.
26+
*
27+
* The implementation is compatible with more than two cores, with the following
28+
* effects:
29+
* - there could be a race condition if two cores try to signal at the same
30+
* time (this would be solved by having them hold a shared mutex when signaling)
31+
* - every core that waits should wait using the same mutex. There is an
32+
* assert if this is not the case.
33+
* - broadcast is implemented and releases every waiting cores.
34+
*
35+
* The condition variables only work with non-recursive mutexes.
36+
*
37+
* Limitations of mutexes also apply to condition variables. See \ref mutex.h
38+
*/
39+
40+
typedef struct __packed_aligned
41+
{
42+
lock_core_t core;
43+
lock_owner_id_t waiter;
44+
lock_core_t mtx_core;
45+
uint32_t broadcast_count; // Overflow is unlikely
46+
bool signaled;
47+
} cond_t;
48+
49+
/*! \brief Initialize a condition variable structure
50+
* \ingroup cond
51+
*
52+
* \param cv Pointer to condition variable structure
53+
*/
54+
void cond_init(cond_t *cv);
55+
56+
/*! \brief Wait on a condition variable
57+
* \ingroup cond
58+
*
59+
* Wait until a condition variable is signaled or broadcast. The mutex should
60+
* be owned and is released atomically. It is reacquired when this function
61+
* returns.
62+
*
63+
* \param cv Condition variable to wait on
64+
* \param mtx Currently held mutex
65+
*/
66+
void cond_wait(cond_t *cv, mutex_t *mtx);
67+
68+
/*! \brief Wait on a condition variable with a timeout.
69+
* \ingroup cond
70+
*
71+
* Wait until a condition variable is signaled or broadcast until a given
72+
* time. The mutex is released atomically and reacquired even if the wait
73+
* timed out.
74+
*
75+
* \param cv Condition variable to wait on
76+
* \param mtx Currently held mutex
77+
* \param until The time after which to return if the condition variable was
78+
* not signaled.
79+
* \return true if the condition variable was signaled, false otherwise
80+
*/
81+
bool cond_wait_until(cond_t *cv, mutex_t *mtx, absolute_time_t until);
82+
83+
/*! \brief Wait on a condition variable with a timeout.
84+
* \ingroup cond
85+
*
86+
* Wait until a condition variable is signaled or broadcast until a given
87+
* time. The mutex is released atomically and reacquired even if the wait
88+
* timed out.
89+
*
90+
* \param cv Condition variable to wait on
91+
* \param mtx Currently held mutex
92+
* \param timeout_ms The timeout in milliseconds.
93+
* \return true if the condition variable was signaled, false otherwise
94+
*/
95+
bool cond_wait_timeout_ms(cond_t *cv, mutex_t *mtx, uint32_t timeout_ms);
96+
97+
/*! \brief Wait on a condition variable with a timeout.
98+
* \ingroup cond
99+
*
100+
* Wait until a condition variable is signaled or broadcast until a given
101+
* time. The mutex is released atomically and reacquired even if the wait
102+
* timed out.
103+
*
104+
* \param cv Condition variable to wait on
105+
* \param mtx Currently held mutex
106+
* \param timeout_ms The timeout in microseconds.
107+
* \return true if the condition variable was signaled, false otherwise
108+
*/
109+
bool cond_wait_timeout_us(cond_t *cv, mutex_t *mtx, uint32_t timeout_us);
110+
111+
/*! \brief Signal on a condition variable and wake the waiter
112+
* \ingroup cond
113+
*
114+
* \param cv Condition variable to signal
115+
*/
116+
void cond_signal(cond_t *cv);
117+
118+
/*! \brief Broadcast a condition variable and wake every waiters
119+
* \ingroup cond
120+
*
121+
* \param cv Condition variable to signal
122+
*/
123+
void cond_broadcast(cond_t *cv);
124+
125+
#ifdef __cplusplus
126+
}
127+
#endif
128+
#endif

src/common/pico_sync/include/pico/sync.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
#include "pico/sem.h"
1616
#include "pico/mutex.h"
1717
#include "pico/critical_section.h"
18+
#include "pico/cond.h"
1819

1920
#endif

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ if (PICO_ON_DEVICE)
1313
add_subdirectory(cmsis_test)
1414
add_subdirectory(pico_sem_test)
1515
add_subdirectory(pico_sha256_test)
16+
add_subdirectory(pico_cond_test)
1617
endif()

test/pico_cond_test/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("//bazel:defs.bzl", "compatible_with_rp2")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
cc_binary(
6+
name = "pico_cond_test",
7+
testonly = True,
8+
srcs = ["pico_cond_test.c"],
9+
# Host doesn't support multicore
10+
target_compatible_with = compatible_with_rp2(),
11+
deps = [
12+
"//src/rp2_common/pico_multicore",
13+
"//src/rp2_common/pico_stdlib",
14+
"//test/pico_test",
15+
],
16+
)

test/pico_cond_test/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
if (TARGET pico_multicore)
2+
add_executable(pico_cond_test pico_cond_test.c)
3+
4+
target_link_libraries(pico_cond_test PRIVATE pico_test pico_sync pico_multicore pico_stdlib )
5+
pico_add_extra_outputs(pico_cond_test)
6+
endif()

0 commit comments

Comments
 (0)