Inter-Thread Communication
oveRTOS provides three IPC primitives for coordinating work across threads and interrupt service routines. Each is optimised for a distinct communication pattern:
| Primitive | Best for | Data model |
|---|---|---|
| Queue | Passing discrete, fixed-size items between threads | N typed items, FIFO |
| EventGroup | Signalling state transitions with bit flags | Up to 32 boolean bits |
| Stream | Moving a continuous byte stream with a flow-control threshold | Circular byte buffer |
Choosing a Primitive
flowchart TD
Q{What are you\ntransferring?}
Q -->|"Fixed-size structs\nor pointers"| QUE["Use Queue\nove_queue_*"]
Q -->|"Boolean state flags\nor event notifications"| EVG["Use EventGroup\nove_eventgroup_*"]
Q -->|"Raw bytes / serial data\nor audio samples"| STR["Use Stream\nove_stream_*"]
QUE --> QISR{Need to send\nfrom an ISR?}
QISR -->|Yes| QISRN["ove_queue_send_from_isr()"]
QISR -->|No| QTHRD["ove_queue_send()"]
EVG --> EVGWAIT{Wait for all\nbits or any?}
EVGWAIT -->|All| EVGALL["OVE_EG_WAIT_ALL flag"]
EVGWAIT -->|Any| EVGANY["flags = 0 (default)"]
STR --> STRISR{Producer is\nan ISR?}
STRISR -->|Yes| STRISRN["ove_stream_write_from_isr()"]
STRISR -->|No| STRTHRD["ove_stream_write()"]
Queue
A thread-safe FIFO that transfers fixed-size items by value. The sender copies the item into the queue; the receiver copies it back out. Both blocking and non-blocking variants are available, and ISR-safe variants allow interrupt handlers to enqueue items without blocking.
graph LR
subgraph Threads
PROD["Producer Thread"]
ISR["ISR"]
CONS["Consumer Thread"]
end
subgraph Q["Queue (depth N, item size S)"]
direction LR
SLOT0["[item 0]"]
SLOT1["[item 1]"]
SLOTN["[item N-1]"]
SLOT0 --> SLOT1 --> SLOTN
end
PROD -->|"ove_queue_send()\n(blocks if full)"| Q
ISR -->|"ove_queue_send_from_isr()\n(never blocks)"| Q
Q -->|"ove_queue_receive()\n(blocks if empty)"| CONS
style PROD fill:#4a9,stroke:#333,color:#fff
style ISR fill:#a54,stroke:#333,color:#fff
style CONS fill:#48b,stroke:#333,color:#fff
API
| Function | Description |
|---|---|
ove_queue_init(cfg) |
Initialise queue subsystem (called once at startup) |
ove_queue_deinit() |
Tear down queue subsystem |
ove_queue_create(depth, item_size) |
Allocate a queue with depth slots of item_size bytes; returns handle |
ove_queue_destroy(q) |
Free a queue and all pending items |
ove_queue_send(q, item, timeout_ms) |
Copy item into queue; blocks up to timeout_ms (-1 = forever) if full |
ove_queue_receive(q, item, timeout_ms) |
Copy oldest item out of queue; blocks if empty |
ove_queue_send_from_isr(q, item, woken) |
ISR-safe send; sets *woken if a higher-priority task was unblocked |
ove_queue_receive_from_isr(q, item, woken) |
ISR-safe receive |
All functions return OVE_OK on success. ove_queue_send / ove_queue_receive return OVE_ERR_TIMEOUT when the timeout expires.
Example: IR Load Requests
The hIRoic app uses a queue to pass cabinet IR load requests from the control task (UI/CLI) to the DSP task without blocking the audio path.
/* Shared handle, created at startup */
static ove_queue_t g_ir_req_q;
struct ir_request {
uint8_t slot; /* IR preset slot 0–7 */
uint32_t file_offset; /* byte offset in flash */
uint32_t length; /* IR length in samples */
};
/* Startup */
g_ir_req_q = ove_queue_create(4, sizeof(struct ir_request));
/* Control task — posts a load request */
struct ir_request req = { .slot = 2, .file_offset = 0x8000, .length = 1024 };
ove_queue_send(g_ir_req_q, &req, OVE_WAIT_FOREVER);
/* DSP task — drains requests between audio cycles */
struct ir_request req;
while (ove_queue_receive(g_ir_req_q, &req, 0) == OVE_OK) {
hiroic_load_ir(req.slot, req.file_offset, req.length);
}
EventGroup
An EventGroup holds up to 32 independent boolean bits. Any thread (or ISR) can set or clear bits; waiting threads are unblocked when a target bit pattern is satisfied. EventGroups are ideal for broadcasting state transitions without copying data.
graph TB
subgraph Setters
T1["Load Task"]
T2["Bypass Task"]
T3["Clip Detect ISR"]
end
subgraph EG["EventGroup (32 bits)"]
B0["bit 0: IR_LOADED"]
B1["bit 1: BYPASS_ACTIVE"]
B2["bit 2: CLIP_DETECTED"]
end
subgraph Waiters
DSP["DSP Task\n(waits for IR_LOADED)"]
UI["UI Task\n(waits for BYPASS or CLIP)"]
end
T1 -->|"ove_eventgroup_set(eg, BIT_IR_LOADED)"| B0
T2 -->|"ove_eventgroup_set(eg, BIT_BYPASS)"| B1
T3 -->|"ove_eventgroup_set_from_isr(eg, BIT_CLIP)"| B2
B0 -->|"OVE_EG_WAIT_ALL\nbit 0 satisfied"| DSP
B1 -->|"any bit"| UI
B2 -->|"any bit"| UI
style T1 fill:#4a9,stroke:#333,color:#fff
style T2 fill:#4a9,stroke:#333,color:#fff
style T3 fill:#a54,stroke:#333,color:#fff
style DSP fill:#48b,stroke:#333,color:#fff
style UI fill:#48b,stroke:#333,color:#fff
Flags
| Flag | Value | Behaviour |
|---|---|---|
OVE_EG_WAIT_ALL |
0x01 |
Block until all bits in the mask are set simultaneously |
OVE_EG_CLEAR_ON_EXIT |
0x02 |
Atomically clear the waited bits when unblocked |
Combining both flags (OVE_EG_WAIT_ALL | OVE_EG_CLEAR_ON_EXIT) is the recommended pattern for one-shot synchronisation barriers: the thread wakes exactly once and the bits are cleared automatically.
API
| Function | Description |
|---|---|
ove_eventgroup_init(cfg) |
Initialise EventGroup subsystem |
ove_eventgroup_deinit() |
Tear down subsystem |
ove_eventgroup_create() |
Allocate a new EventGroup (all bits clear); returns handle |
ove_eventgroup_destroy(eg) |
Free EventGroup |
ove_eventgroup_set(eg, bits) |
Set one or more bits (bitwise OR); unblocks matching waiters |
ove_eventgroup_clear(eg, bits) |
Clear one or more bits |
ove_eventgroup_get(eg) |
Read current bit mask without blocking |
ove_eventgroup_wait(eg, bits, flags, timeout_ms) |
Block until bit pattern satisfied; returns actual bits at wake |
ove_eventgroup_set_from_isr(eg, bits, woken) |
ISR-safe set |
Example: Coordinating Load and Bypass State
#define BIT_IR_LOADED (1u << 0)
#define BIT_BYPASS_ACTIVE (1u << 1)
#define BIT_CLIP_DETECTED (1u << 2)
static ove_eventgroup_t g_state_eg;
/* Startup */
g_state_eg = ove_eventgroup_create();
/* Load task — signal that a new IR is ready */
hiroic_load_ir_blocking(slot);
ove_eventgroup_set(g_state_eg, BIT_IR_LOADED);
/* DSP task — wait for first IR before processing audio */
ove_eventgroup_wait(g_state_eg, BIT_IR_LOADED,
OVE_EG_WAIT_ALL, OVE_WAIT_FOREVER);
/* Bypass button ISR — toggle bypass bit */
bool bypass = !!(ove_eventgroup_get(g_state_eg) & BIT_BYPASS_ACTIVE);
uint32_t new_bit = bypass ? 0 : BIT_BYPASS_ACTIVE;
ove_eventgroup_clear(g_state_eg, BIT_BYPASS_ACTIVE);
ove_eventgroup_set_from_isr(g_state_eg, new_bit, NULL);
Stream
A Stream is a lock-free ring buffer that transfers an unframed byte sequence from a producer to a consumer. An optional threshold allows the consumer to sleep until a minimum number of bytes are available, reducing task wake-up overhead for bursty producers.
graph LR
subgraph Producer
SER["Serial / DMA ISR"]
end
subgraph RingBuf["Stream (capacity C bytes)"]
direction LR
WP(["write\npointer"])
BYTES["… bytes …"]
RP(["read\npointer"])
WP --> BYTES --> RP
end
subgraph Consumer
THRESH{{"available ≥\nthreshold?"}}
TASK["Consumer Task"]
end
SER -->|"ove_stream_write_from_isr()\nbyte-by-byte or burst"| RingBuf
RingBuf -->|"ove_stream_available()"| THRESH
THRESH -->|"yes, unblock"| TASK
THRESH -->|"no, sleep"| THRESH
style SER fill:#a54,stroke:#333,color:#fff
style THRESH fill:#888,stroke:#333,color:#fff
style TASK fill:#48b,stroke:#333,color:#fff
API
| Function | Description |
|---|---|
ove_stream_init(cfg) |
Initialise Stream subsystem |
ove_stream_deinit() |
Tear down subsystem |
ove_stream_create(capacity, threshold) |
Allocate ring buffer; threshold bytes triggers consumer wake |
ove_stream_destroy(s) |
Free stream |
ove_stream_write(s, buf, len, timeout_ms) |
Write up to len bytes; blocks if buffer full |
ove_stream_read(s, buf, len, timeout_ms) |
Read up to len bytes; blocks until threshold bytes available |
ove_stream_write_from_isr(s, buf, len, woken) |
ISR-safe write (non-blocking) |
ove_stream_read_from_isr(s, buf, len, woken) |
ISR-safe read (non-blocking) |
ove_stream_available(s) |
Bytes currently readable |
ove_stream_space(s) |
Free bytes remaining in buffer |
ove_stream_write returns the number of bytes actually written (may be less than len if the buffer fills and the timeout expires). ove_stream_read returns the number of bytes read.
Example: Serial Data Buffering
#define STREAM_CAP 512 /* ring buffer capacity in bytes */
#define STREAM_THRESH 64 /* wake consumer after 64 bytes */
static ove_stream_t g_uart_stream;
/* Startup */
g_uart_stream = ove_stream_create(STREAM_CAP, STREAM_THRESH);
/* UART RX ISR — one byte at a time */
void USART1_IRQHandler(void)
{
uint8_t byte = USART1->RDR;
bool woken = false;
ove_stream_write_from_isr(g_uart_stream, &byte, 1, &woken);
portYIELD_FROM_ISR(woken);
}
/* Consumer task — process frames in bulk */
static void uart_task(void *arg)
{
uint8_t buf[64];
for (;;) {
int n = ove_stream_read(g_uart_stream, buf, sizeof(buf),
OVE_WAIT_FOREVER);
if (n > 0)
process_frame(buf, n);
}
}
Kconfig Options
| Option | Default | Description |
|---|---|---|
CONFIG_OVE_QUEUE |
y |
Enable Queue primitive |
CONFIG_OVE_EVENTGROUP |
y |
Enable EventGroup primitive |
CONFIG_OVE_STREAM |
n |
Enable Stream primitive (adds ring-buffer memory overhead) |
Headers
| Header | Contents |
|---|---|
ove/queue.h |
Queue handle type, create/destroy, send/receive API |
ove/eventgroup.h |
EventGroup handle type, set/clear/wait API, flag constants |
ove/stream.h |
Stream handle type, create/destroy, read/write API |