Skip to content

Commit ee1abdf

Browse files
committed
Add condition variables to pico_sync (fix #1093)
1 parent bddd20f commit ee1abdf

File tree

10 files changed

+579
-1
lines changed

10 files changed

+579
-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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright (c) 2022-2025 Paul Guyot <pguyot@kallisys.net>
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+
lock_owner_id_t caller = lock_get_caller_owner_id();
20+
uint32_t save = save_and_disable_interrupts();
21+
// Acquire the mutex spin lock
22+
spin_lock_unsafe_blocking(mtx->core.spin_lock);
23+
assert(lock_is_owner_id_valid(mtx->owner));
24+
assert(caller == mtx->owner);
25+
26+
// Mutex and cond spin locks can be the same as spin locks are attributed
27+
// using `next_striped_spin_lock_num()`. To avoid any deadlock, we only
28+
// acquire the condition variable spin lock if it is different from the
29+
// mutex spin lock
30+
bool same_spinlock = mtx->core.spin_lock == cond->core.spin_lock;
31+
32+
// Acquire the condition variable spin_lock
33+
if (!same_spinlock) {
34+
spin_lock_unsafe_blocking(cond->core.spin_lock);
35+
}
36+
37+
// Release the mutex
38+
mtx->owner = LOCK_INVALID_OWNER_ID;
39+
40+
uint64_t current_broadcast = cond->broadcast_count;
41+
42+
if (lock_is_owner_id_valid(cond->waiter)) {
43+
// Release the mutex spin lock but without restoring interrupts and notify.
44+
if (!same_spinlock) {
45+
spin_unlock_unsafe(mtx->core.spin_lock);
46+
}
47+
48+
// There is a valid owner of the condition variable: we are not the
49+
// first waiter.
50+
// First iteration: notify
51+
lock_internal_spin_unlock_with_notify(&cond->core, save);
52+
save = spin_lock_blocking(cond->core.spin_lock);
53+
// Further iterations: wait
54+
do {
55+
if (!lock_is_owner_id_valid(cond->waiter)) {
56+
break;
57+
}
58+
if (cond->broadcast_count != current_broadcast) {
59+
break;
60+
}
61+
if (is_at_the_end_of_time(until)) {
62+
lock_internal_spin_unlock_with_wait(&cond->core, save);
63+
} else {
64+
if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&cond->core, save, until)) {
65+
// timed out
66+
success = false;
67+
break;
68+
}
69+
}
70+
save = spin_lock_blocking(cond->core.spin_lock);
71+
} while (true);
72+
}
73+
74+
if (success && cond->broadcast_count == current_broadcast) {
75+
// We are the first waiter
76+
cond->waiter = caller;
77+
78+
// Release the mutex spin lock but without restoring interrupts
79+
if (!same_spinlock) {
80+
uint32_t disabled_ints = save_and_disable_interrupts();
81+
lock_internal_spin_unlock_with_notify(&mtx->core, disabled_ints);
82+
}
83+
84+
// Wait for the signal
85+
do {
86+
if (cond->signaled) {
87+
cond->waiter = LOCK_INVALID_OWNER_ID;
88+
cond->signaled = false;
89+
break;
90+
}
91+
if (is_at_the_end_of_time(until)) {
92+
lock_internal_spin_unlock_with_wait(&cond->core, save);
93+
} else {
94+
if (lock_internal_spin_unlock_with_best_effort_wait_or_timeout(&cond->core, save, until)) {
95+
// timed out
96+
cond->waiter = LOCK_INVALID_OWNER_ID;
97+
success = false;
98+
break;
99+
}
100+
}
101+
save = spin_lock_blocking(cond->core.spin_lock);
102+
} while (true);
103+
104+
// Acquire the mutex spin lock
105+
if (!same_spinlock) {
106+
spin_lock_unsafe_blocking(mtx->core.spin_lock);
107+
}
108+
}
109+
110+
// We got the signal (or timed out)
111+
112+
if (lock_is_owner_id_valid(mtx->owner)) {
113+
// Release the core spin lock.
114+
if (!same_spinlock) {
115+
spin_unlock_unsafe(cond->core.spin_lock);
116+
}
117+
118+
// Another core holds the mutex.
119+
// First iteration: notify
120+
lock_internal_spin_unlock_with_notify(&mtx->core, save);
121+
save = spin_lock_blocking(mtx->core.spin_lock);
122+
// Further iterations: wait
123+
do {
124+
if (!lock_is_owner_id_valid(mtx->owner)) {
125+
break;
126+
}
127+
// We always wait for the mutex.
128+
lock_internal_spin_unlock_with_wait(&mtx->core, save);
129+
save = spin_lock_blocking(mtx->core.spin_lock);
130+
} while (true);
131+
} else {
132+
// Release the core spin lock
133+
// with notify but without restoring interrupts
134+
if (!same_spinlock) {
135+
uint32_t disabled_ints = save_and_disable_interrupts();
136+
lock_internal_spin_unlock_with_notify(&cond->core, disabled_ints);
137+
}
138+
}
139+
140+
// Eventually hold the mutex.
141+
mtx->owner = caller;
142+
143+
// Restore the interrupts now
144+
spin_unlock(mtx->core.spin_lock, save);
145+
146+
return success;
147+
}
148+
149+
bool __time_critical_func(cond_wait_timeout_ms)(cond_t *cond, mutex_t *mtx, uint32_t timeout_ms) {
150+
return cond_wait_until(cond, mtx, make_timeout_time_ms(timeout_ms));
151+
}
152+
153+
bool __time_critical_func(cond_wait_timeout_us)(cond_t *cond, mutex_t *mtx, uint32_t timeout_us) {
154+
return cond_wait_until(cond, mtx, make_timeout_time_us(timeout_us));
155+
}
156+
157+
void __time_critical_func(cond_wait)(cond_t *cond, mutex_t *mtx) {
158+
cond_wait_until(cond, mtx, at_the_end_of_time);
159+
}
160+
161+
void __time_critical_func(cond_signal)(cond_t *cond) {
162+
uint32_t save = spin_lock_blocking(cond->core.spin_lock);
163+
if (lock_is_owner_id_valid(cond->waiter)) {
164+
// We have a waiter, we can signal.
165+
cond->signaled = true;
166+
lock_internal_spin_unlock_with_notify(&cond->core, save);
167+
} else {
168+
spin_unlock(cond->core.spin_lock, save);
169+
}
170+
}
171+
172+
void __time_critical_func(cond_broadcast)(cond_t *cond) {
173+
uint32_t save = spin_lock_blocking(cond->core.spin_lock);
174+
if (lock_is_owner_id_valid(cond->waiter)) {
175+
// We have a waiter, we can broadcast.
176+
cond->signaled = true;
177+
cond->broadcast_count++;
178+
lock_internal_spin_unlock_with_notify(&cond->core, save);
179+
} else {
180+
spin_unlock(cond->core.spin_lock, save);
181+
}
182+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (c) 2022-2023 Paul Guyot <pguyot@kallisys.net>
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+
*
26+
* Condition variables can also be broadcast.
27+
*
28+
* In this implementation, it is not mandatory. The condition variables only
29+
* work with non-recursive mutexes.
30+
*
31+
* Limitations of mutexes also apply to condition variables. See \ref mutex.h
32+
*/
33+
34+
typedef struct __packed_aligned
35+
{
36+
lock_core_t core;
37+
lock_owner_id_t waiter;
38+
uint32_t broadcast_count; // Overflow is unlikely
39+
bool signaled;
40+
} cond_t;
41+
42+
/*! \brief Initialize a condition variable structure
43+
* \ingroup cond
44+
*
45+
* \param cv Pointer to condition variable structure
46+
*/
47+
void cond_init(cond_t *cv);
48+
49+
/*! \brief Wait on a condition variable
50+
* \ingroup cond
51+
*
52+
* Wait until a condition variable is signaled or broadcast. The mutex should
53+
* be owned and is released atomically. It is reacquired when this function
54+
* returns.
55+
*
56+
* \param cv Condition variable to wait on
57+
* \param mtx Currently held mutex
58+
*/
59+
void cond_wait(cond_t *cv, mutex_t *mtx);
60+
61+
/*! \brief Wait on a condition variable with a timeout.
62+
* \ingroup cond
63+
*
64+
* Wait until a condition variable is signaled or broadcast until a given
65+
* time. The mutex is released atomically and reacquired even if the wait
66+
* timed out.
67+
*
68+
* \param cv Condition variable to wait on
69+
* \param mtx Currently held mutex
70+
* \param until The time after which to return if the condition variable was
71+
* not signaled.
72+
* \return true if the condition variable was signaled, false otherwise
73+
*/
74+
bool cond_wait_until(cond_t *cv, mutex_t *mtx, absolute_time_t until);
75+
76+
/*! \brief Wait on a condition variable with a timeout.
77+
* \ingroup cond
78+
*
79+
* Wait until a condition variable is signaled or broadcast until a given
80+
* time. The mutex is released atomically and reacquired even if the wait
81+
* timed out.
82+
*
83+
* \param cv Condition variable to wait on
84+
* \param mtx Currently held mutex
85+
* \param timeout_ms The timeout in milliseconds.
86+
* \return true if the condition variable was signaled, false otherwise
87+
*/
88+
bool cond_wait_timeout_ms(cond_t *cv, mutex_t *mtx, uint32_t timeout_ms);
89+
90+
/*! \brief Wait on a condition variable with a timeout.
91+
* \ingroup cond
92+
*
93+
* Wait until a condition variable is signaled or broadcast until a given
94+
* time. The mutex is released atomically and reacquired even if the wait
95+
* timed out.
96+
*
97+
* \param cv Condition variable to wait on
98+
* \param mtx Currently held mutex
99+
* \param timeout_ms The timeout in microseconds.
100+
* \return true if the condition variable was signaled, false otherwise
101+
*/
102+
bool cond_wait_timeout_us(cond_t *cv, mutex_t *mtx, uint32_t timeout_us);
103+
104+
/*! \brief Signal on a condition variable and wake the waiter
105+
* \ingroup cond
106+
*
107+
* \param cv Condition variable to signal
108+
*/
109+
void cond_signal(cond_t *cv);
110+
111+
/*! \brief Broadcast a condition variable and wake every waiters
112+
* \ingroup cond
113+
*
114+
* \param cv Condition variable to signal
115+
*/
116+
void cond_broadcast(cond_t *cv);
117+
118+
#ifdef __cplusplus
119+
}
120+
#endif
121+
#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)