Multiple threads share resources: global variables, hardware peripherals, ring buffers. Without protection, two threads can corrupt shared data:
Thread A: reads x (x=5) Thread B: reads x (x=5)
Thread A: computes x+1=6 Thread B: computes x+1=6
Thread A: writes x=6 Thread B: writes x=6 ← x should be 7!
Zephyr provides several primitives. Each solves a different problem:
| Primitive | Problem it solves |
|---|---|
k_sem (semaphore) |
Signal between threads (“event happened”) |
k_mutex |
Protect shared data from simultaneous access |
k_msgq |
Pass data + signal between threads (queue) |
k_fifo |
Linked-list queue, zero-copy, variable-size items |
k_timer |
Periodic callbacks / timeouts |
k_event |
Multibit flags, wait for any/all |
A semaphore is a counter that threads use to coordinate:
- k_sem_give() increments the counter
- k_sem_take() decrements the counter — blocks if counter is 0
#include <zephyr/kernel.h>
K_SEM_DEFINE(data_ready_sem, 0, 1);
/* initial_count=0 (nothing ready), limit=1 (binary semaphore) */
/* ISR or producer thread — signals that data is ready */
void drdy_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
k_sem_give(&data_ready_sem); /* Non-blocking, safe from ISR */
}
/* Consumer thread — waits for signal */
void imu_reader_thread(void *a, void *b, void *c)
{
while (1) {
k_sem_take(&data_ready_sem, K_FOREVER); /* Block until ISR signals */
imu_burst_read(ACCEL_REG, raw, 12); /* Data guaranteed ready */
publish_to_zbus(raw);
}
}
K_SEM_DEFINE(free_buffers_sem, 4, 4);
/* 4 buffers available initially, max=4 */
void producer(void)
{
k_sem_take(&free_buffers_sem, K_FOREVER); /* Wait for free buffer */
write_to_buffer();
}
void consumer(void)
{
process_buffer();
k_sem_give(&free_buffers_sem); /* Return buffer to pool */
}
k_sem_give() is always ISR-safe. k_sem_take() is NOT ISR-safe (cannot block in ISR).
A mutex ensures only one thread accesses a resource at a time. Unlike semaphores, mutexes have ownership — only the thread that locked it can unlock it.
Zephyr mutexes support priority inheritance: if thread A (prio=0) is waiting for a mutex held by thread B (prio=10), Zephyr temporarily raises B’s priority to 0, so B finishes faster and A doesn’t starve.
K_MUTEX_DEFINE(spi_buf_mutex);
uint8_t shared_buffer[256];
/* Thread A — writing to buffer */
void writer_thread(void *a, void *b, void *c)
{
while (1) {
k_mutex_lock(&spi_buf_mutex, K_FOREVER);
memcpy(shared_buffer, new_data, sizeof(new_data));
k_mutex_unlock(&spi_buf_mutex);
k_msleep(10);
}
}
/* Thread B — reading from buffer */
void reader_thread(void *a, void *b, void *c)
{
uint8_t local_copy[256];
while (1) {
k_mutex_lock(&spi_buf_mutex, K_FOREVER);
memcpy(local_copy, shared_buffer, sizeof(local_copy));
k_mutex_unlock(&spi_buf_mutex);
process(local_copy); /* Process outside lock — minimize hold time */
}
}
/* Thread A */
k_mutex_lock(&mutex_a, K_FOREVER);
k_mutex_lock(&mutex_b, K_FOREVER); /* waits for B */
/* Thread B (simultaneously) */
k_mutex_lock(&mutex_b, K_FOREVER);
k_mutex_lock(&mutex_a, K_FOREVER); /* waits for A */
/* Both threads block forever — deadlock! */
/* Fix: always lock in the same order (mutex_a first, then mutex_b) */
A message queue passes fixed-size messages between threads — combines data transfer and signaling.
/* Define queue: element size = sizeof(struct imu_data), max 4 items */
K_MSGQ_DEFINE(imu_msgq, sizeof(struct imu_data), 4, 4);
/* Producer: push message */
void imu_reader(void *a, void *b, void *c)
{
struct imu_data msg;
while (1) {
read_imu(&msg);
if (k_msgq_put(&imu_msgq, &msg, K_NO_WAIT) != 0) {
LOG_WRN("Queue full — dropping IMU sample");
}
k_msleep(10);
}
}
/* Consumer: pop messages */
void packer_thread(void *a, void *b, void *c)
{
struct imu_data msg;
while (1) {
k_msgq_get(&imu_msgq, &msg, K_FOREVER); /* block until message arrives */
encode_and_send(&msg);
}
}
When to use k_msgq vs ZBus: - k_msgq: one-to-one pipe. Consumer gets events in order. - ZBus: one-to-many pub/sub. Multiple subscribers can see the same message. Subscribers only see “latest” unless they use queued subscription.
A timer calls a function after a timeout, or periodically.
void timer_callback(struct k_timer *timer)
{
/* This runs in ISR context (or system workqueue) — keep it short! */
k_sem_give(&tick_sem);
}
K_TIMER_DEFINE(tick_timer, timer_callback, NULL);
void main_init(void)
{
/* Start periodic timer: fire after 10ms, then every 10ms */
k_timer_start(&tick_timer, K_MSEC(10), K_MSEC(10));
}
void packer_thread(void *a, void *b, void *c)
{
while (1) {
k_sem_take(&tick_sem, K_FOREVER); /* exactly every 10ms */
pack_and_send_frame();
}
}
Timer callback rules:
- Runs in ISR context (if using K_TIMER_DEFINE) — keep it tiny
- Never call k_msleep, k_mutex_lock, or other blocking functions inside
- Use k_sem_give to wake a thread that does the real work
When you need to wait for any of several events, or all of several events simultaneously:
K_EVENT_DEFINE(system_events);
#define EVENT_IMU_READY BIT(0)
#define EVENT_CAN_READY BIT(1)
#define EVENT_GPS_READY BIT(2)
#define EVENT_SHUTDOWN BIT(15)
/* Post an event bit */
void imu_isr(void)
{
k_event_post(&system_events, EVENT_IMU_READY);
}
/* Wait for any sensor data */
void fusion_thread(void *a, void *b, void *c)
{
while (1) {
/* Wait until IMU or CAN data ready */
uint32_t events = k_event_wait(&system_events,
EVENT_IMU_READY | EVENT_CAN_READY,
false, /* false = any; true = all */
K_FOREVER);
k_event_clear(&system_events, events); /* clear what we consumed */
if (events & EVENT_IMU_READY) handle_imu();
if (events & EVENT_CAN_READY) handle_can();
}
}
When you need to defer work from an ISR to a thread context:
#include <zephyr/kernel.h>
static void process_data_work_fn(struct k_work *work)
{
/* Runs in system workqueue thread — can call blocking functions */
LOG_INF("Processing data");
}
static K_WORK_DEFINE(process_work, process_data_work_fn);
/* ISR: submit work item instead of doing heavy work in IRQ context */
void uart_isr(const struct device *dev, void *user_data)
{
/* Quick: read bytes into ring buffer */
// ...
/* Defer processing to thread context */
k_work_submit(&process_work); /* safe from ISR */
}
Signal only? ──Yes──► k_sem
│
No
│
Transfer data? ──Yes──► k_msgq (fixed size) or k_fifo (variable)
│
No
│
Protect shared data? ──Yes──► k_mutex
│
No
│
Multiple event flags? ──Yes──► k_event
│
No
│
Periodic operation? ──Yes──► k_timer
| Bug | Symptom | Fix |
|---|---|---|
| No mutex on shared buffer | Intermittent data corruption | Add k_mutex_lock/unlock |
k_mutex_lock in ISR |
HardFault / panic | Use k_sem_give in ISR |
Holding mutex across k_msleep |
Other threads starve | Release before sleeping |
k_msgq full, dropping |
LOG_WRN messages | Increase queue size or consume faster |
| Sem count > max | Ignored (capped at max) | Set max high enough |
| k_timer callback blocking | System timer miss | Never block in timer callback |