In real-time embedded systems, managing the flow of information between concurrent tasks is critical to achieving both functional correctness and predictable behavior. Whether you are reading data from sensors, responding to user inputs, or communicating over wireless interfaces, your software architecture must support safe and efficient task coordination.
Zephyr RTOS offers a rich set of kernel primitives for this purpose. One particularly useful primitive is the message queue (k_msgq
), which allows multiple threads to exchange fixed-size messages without requiring dynamic memory allocation. This model is ideal for embedded systems where performance and determinism matter.
This article provides a comprehensive tutorial on using message queues in Zephyr RTOS on Nordic Semiconductor’s nRF7002 Development Kit. We’ll implement a multithreaded system where one thread simulates sensor data generation, and another simulates Wi-Fi transmission. All development will be done using the nRF Connect SDK with the nRF Connect for VS Code extension.
Introduction to Message Queues in Zephyr
Message queues in Zephyr allow fixed-size messages to be passed between threads. Each queue is a statically allocated buffer capable of holding a predefined number of messages. The message size and number of messages are both configured at compile-time. This makes message queues highly predictable and deterministic, ideal for real-time applications where dynamic memory allocation is discouraged or unavailable.
Zephyr’s message queue API is defined in kernel.h
and includes the following key functions:
k_msgq_put()
: Adds a message to the queue.k_msgq_get()
: Retrieves a message from the queue.k_msgq_purge()
: Clears the queue.k_msgq_num_free_get()
: Returns the number of free slots.
Each function supports timeouts, allowing developers to choose between blocking, non-blocking, or timed waits. This flexibility enables developers to build communication mechanisms that are robust against congestion, latency, or unresponsive tasks.
Benefits of k_msgq
:
- Deterministic timing: All memory is preallocated.
- Thread-safe: Supports multiple producers and consumers.
- Flexible timeout support: Threads can block, timeout, or return immediately.
- Simple to use: Minimal API, easy to debug.
Use Case: Sensor-to-Wi-Fi Pipeline
To understand how message queues can be applied in a real system, consider a device that samples data from an onboard sensor and periodically transmits this data over Wi-Fi. This pattern is common in telemetry systems, environmental monitoring, and IoT gateways.
We will implement two threads:
- Sensor Thread (Producer): Simulates a sensor by generating temperature readings at regular intervals. It writes each data point to a message queue.
- Wi-Fi Thread (Consumer): Reads messages from the queue and simulates sending them over a Wi-Fi connection.
By decoupling the producer and consumer threads, we achieve better modularity, reduced blocking, and improved responsiveness. The producer can continue sampling even if the consumer is busy or delayed, up to the limit of the queue’s capacity.
Tools and Environment
To follow this tutorial, install the following tools:
- nRF Connect for Desktop
- nRF Connect SDK (v3.0.2 at the time of writing)
- nRF Connect for VS Code extension
- nRF7002 Development Kit
Once installed, the nRF Connect for VS Code extension provides a graphical interface to configure, build, flash, and monitor Zephyr-based firmware projects without needing to run west
from the command line.
Project Setup with nRF Connect for VS Code
Step 1: Open the VS Code Extension
- Launch VS Code.
- Click on the nRF Connect icon in the Activity Bar.
- Open the Welcome panel and click Create a new application.
Step 2: Create a New Application
- Choose a name such as
nrf7002_msgq_demo
. - Select a workspace folder.
- Choose nrf7002dk_nrf5340_cpuapp as the board target.
- Use the nRF Connect SDK version 3.0.2 or newer.
- Click Create Application.
This will generate a skeleton project with default configuration files.
Project Files
Navigate to your newly created project directory. Replace the contents of the default source and configuration files with the following:
prj.conf
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_LOG_MODE_DEFERRED=y
CONFIG_LOG_BACKEND_UART=y
This configuration enables UART output and ensures the application has enough stack space to run two threads.
src/main.c
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main);
#define MSGQ_MAX_MSGS 10
#define MSG_SIZE sizeof(struct sensor_msg)
struct sensor_msg {
uint32_t timestamp;
int16_t temperature;
};
K_MSGQ_DEFINE(sensor_msgq, MSG_SIZE, MSGQ_MAX_MSGS, 4);
void sensor_thread(void)
{
struct sensor_msg msg;
int counter = 0;
while (1) {
msg.timestamp = k_uptime_get_32();
msg.temperature = 250 + (counter++ % 10); // Simulated temperature
int ret = k_msgq_put(&sensor_msgq, &msg, K_NO_WAIT);
if (ret != 0) {
LOG_WRN("Message queue full, dropping message");
}
k_sleep(K_SECONDS(1));
}
}
void wifi_thread(void)
{
struct sensor_msg msg;
while (1) {
int ret = k_msgq_get(&sensor_msgq, &msg, K_FOREVER);
if (ret == 0) {
LOG_INF("Uploading: time=%u temp=%d", msg.timestamp, msg.temperature);
}
}
}
K_THREAD_DEFINE(sensor_tid, 1024, sensor_thread, NULL, NULL, NULL, 7, 0, 0);
K_THREAD_DEFINE(wifi_tid, 1024, wifi_thread, NULL, NULL, NULL, 5, 0, 0);
Building and Flashing the Application
Step 1: Configure Build Settings
- Click the Build Configuration button in the VS Code Status Bar.
- Select Add Build Configuration.
-
Choose:
- Board:
nrf7002dk_nrf5340_cpuapp
- Application Directory: your project root
- Build Directory:
build
- Board:
Step 2: Build the Project
Click the Build button in the status bar or press Ctrl+Shift+B
. If configured correctly, VS Code will run west build
behind the scenes using your selected SDK and board.
Step 3: Flash to the Device
Click the Flash button in the status bar. This runs west flash
and programs the compiled firmware onto your nRF7002 DK.
Step 4: Open the Serial Monitor
Click the Terminal icon or choose Terminal → New UART Terminal to open a live console. The output should show messages from the Wi-Fi thread as it receives data:
Uploading: time=1000 temp=250
Uploading: time=2000 temp=251
Uploading: time=3000 temp=252
How the Message Queue Works
The system uses a fixed-size message queue to store sensor_msg
structures. Each message contains a timestamp and a simulated temperature reading. When the sensor thread calls k_msgq_put()
, it attempts to enqueue the message. If the queue is full, the message is dropped, and a warning is printed.
The Wi-Fi thread blocks on k_msgq_get()
with K_FOREVER
, meaning it will sleep until a message is available. Once it receives a message, it simulates an upload by printing the data.
This architecture ensures that the sensor thread can continue running independently of the consumer’s speed, within the constraints of the queue size.
Thread Priority and Stack Management
The threads are created using K_THREAD_DEFINE
, which allocates each thread a specific stack size and priority. In Zephyr:
- Lower numerical values mean higher priority.
- The Wi-Fi thread has a higher priority (
5
) than the sensor thread (7
).
This ensures that message consumption is prioritized, reducing the risk of queue overflow.
If you observe frequent dropped messages, you can experiment with increasing the queue size or modifying thread priorities to balance the producer and consumer workloads.
Monitoring and Debugging
Zephyr offers several functions to monitor queue usage:
k_msgq_num_used_get(&sensor_msgq)
: Number of messages currently in the queue.k_msgq_num_free_get(&sensor_msgq)
: Available slots in the queue.k_msgq_purge(&sensor_msgq)
: Clears all messages in the queue.
You can use these functions to implement diagnostics, alerts, or adaptive scheduling strategies in more complex systems.
Advanced Use Cases and Extensions
Integrating with Real Sensors
Instead of simulating temperature values, you can connect a sensor using I2C or SPI and use the actual readings as your message content. This is a natural next step toward building a production IoT system.
Sending Data Over Wi-Fi
The nRF7002 supports Wi-Fi connectivity via the wifi_mgmt
subsystem. You can enhance the Wi-Fi thread to connect to a network and use BSD sockets or the net_app
API to send data to a remote server.
Adding Power Management
You can configure the system to enter a low-power sleep mode when the queue is empty or after a period of inactivity. Zephyr's power management subsystem supports deep sleep states and automatic context restoration.
Using k_poll()
for Event Multiplexing
If your thread needs to wait on multiple events (e.g., queue input and external interrupts), consider replacing k_msgq_get()
with a k_poll()
call. This allows more efficient multitasking with minimal blocking.
Conclusion
Message queues in Zephyr RTOS provide a lightweight, efficient, and deterministic method for inter-thread communication. By separating the concerns of data production and data transmission, they allow your system to be more modular, scalable, and robust against timing variability.
The example in this tutorial demonstrates a common real-world pattern — one thread sampling data and another handling output — and shows how to implement it using the nRF Connect for VS Code workflow. This modern development environment simplifies Zephyr application development, making it more accessible for both beginners and experienced embedded developers.
As you advance your project, you can build on this structure to support richer message formats, error handling, and real-time scheduling. Whether you're building a connected sensor node, a wireless gateway, or a custom telemetry device, mastering Zephyr’s message queue mechanism is a powerful step toward building reliable RTOS-based applications.