A thread is an independent execution sequence that the scheduler can alternate with others based on their state and priority.
So far, we have been running all our code inside the main() function. If you come from the Arduino world, this will feel familiar: an infinite loop where we do things one after another.
But we came here to use an RTOS (Real-Time Operating System), and the beauty of an RTOS is being able to do several things “at the same time”.
Imagine you need to read a sensor every 10ms, control a PID motor every 1ms, and send data to the cloud via Wi-Fi whenever possible. If you do everything in a single loop (while(1)), the moment Wi-Fi gets blocked sending a packet, the motor will go haywire.
To solve this, Zephyr uses Threads. Today we will learn how to create them, assign priorities, and most critically, allocate memory for them.
What is a thread in Zephyr?
A Thread is an independent execution sequence. You can imagine it as your microcontroller having multiple virtual CPUs. Each Thread thinks it has the processor all to itself.
The Kernel’s Scheduler is the “referee” that decides which Thread runs at each physical instant, based on priority rules and timings.
To function, each thread needs two fundamental things:
- Stack: A reserved chunk of RAM for its local variables and function call history.
- Entry Point: The C function it will execute.
Priorities: The kernel’s game of thrones
Zephyr does not treat all threads equally. Some are more important than others. That’s what the priority system is for, and it’s important to understand it well because it works opposite to human intuition.
In Zephyr, the LOWER the number, the HIGHER the priority. Priority 1 is more important than priority 10.
Zephyr divides threads into two categories based on their priority:
Cooperative threads (negative priority)
- Range: -1, -2, …
- Behavior: A cooperative thread continues until it blocks, yields, or finishes. Interrupts and special MetaIRQ type threads are relevant exceptions.
- Usage: Critical tasks that must not be interrupted by anything in the software.
Preemptive threads (non-negative priority)
- Range: 0, 1, 2, … (configurable in
prj.conf). - Behavior: They are “expropriable”. If a thread with priority 5 is running and suddenly a thread with priority 2 wakes up, the Kernel pauses the priority 5 thread immediately and runs the priority 2 thread.
- Usage: Most sensor, communication, and logic tasks.
Creating a thread (K_THREAD_DEFINE)
Threads can also be created at runtime with k_thread_create. Static declaration makes it easier to know in advance which stacks and objects are reserved, useful in systems with constrained memory.
For this, we use the K_THREAD_DEFINE macro.
Syntax:
K_THREAD_DEFINE(thread_name, stack_size, entry_function,
arg1, arg2, arg3, priority, options, delay);Practical example: The LED dance
Let’s create an example with two extra threads besides main.
- Thread A (Priority 7): Prints “Ping” every second.
- Thread B (Priority 2): Prints “PONG!” every 5 seconds. Since it has priority 2 (lower number), it is more important than A.
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(demo_threads, LOG_LEVEL_INF);
/* 1. Define the Stack size */
/* 1024 bytes is usually a safe starting point for simple logic */
#define MY_STACK_SIZE 1024
/* 2. Define the priorities */
#define PRIORITY_A 7
#define PRIORITY_B 2
/* 3. Define the thread functions */
void thread_a_entry(void *p1, void *p2, void *p3)
{
while (1) {
LOG_INF("Thread A (Prio %d): Ping", PRIORITY_A);
k_msleep(1000); /* Yield control for 1s */
}
}
void thread_b_entry(void *p1, void *p2, void *p3)
{
while (1) {
/* Notice this thread will run OVER A if they coincide */
LOG_WRN("Thread B (Prio %d): PONG! (I'm priority)", PRIORITY_B);
k_msleep(5000);
}
}
/* 4. Create the threads statically */
K_THREAD_DEFINE(my_thread_a, MY_STACK_SIZE, thread_a_entry, NULL, NULL, NULL,
PRIORITY_A, 0, 0);
K_THREAD_DEFINE(my_thread_b, MY_STACK_SIZE, thread_b_entry, NULL, NULL, NULL,
PRIORITY_B, 0, 0);
/* main is also a thread; its priority depends on CONFIG_MAIN_THREAD_PRIORITY */
int main(void)
{
LOG_INF("Starting system...");
/* main ends, but the other threads remain alive */
return 0;
}Sizing the stack
The MY_STACK_SIZE parameter is the hardest to tune.
- If you set too little stack: Your thread will try to write variables into memory that is not its own. Result: Stack Overflow and the system crashes (or worse, silently corrupts data).
- If you set too much stack: You are wasting RAM, which is the most scarce resource in a microcontroller.
How do I calculate the size?
By rule of thumb… no, just kidding.
- Estimation: Start from the architecture’s requirements, the calls made, and the subsystem’s recommendations.
1024bytes may work for this example, but it is not a universal number. - Protection: Activate hardware protection in
prj.conf. If you exceed the limit, the system will throw a controlled error instead of going crazy.
CONFIG_HW_STACK_PROTECTION=y- Analysis: Use the Thread Analyzer. Zephyr has a tool that tells you exactly how much stack each thread is using.
Add this to prj.conf:
CONFIG_THREAD_ANALYZER=y
CONFIG_THREAD_ANALYZER_USE_PRINTK=y
CONFIG_THREAD_ANALYZER_AUTO=y
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=10When running, you will see a report on the console every 10 seconds:
Thread analyze:
Thread name Stack usage Stack size % used
my_thread_a 340 1024 33 %
my_thread_b 340 1024 33 %If you see you are using 33%, you can lower the stack to 512 bytes and save memory. If you see 95%, increase it immediately!
Thread states
It’s important to know that a thread is not always running. It can be:
- Running: It has the CPU right now.
- Ready: It wants to run, but another thread with higher priority is occupying the CPU. It is in the ready queue.
- Waiting: It is waiting for a time, a semaphore, or another kernel object.
- Suspended: Another piece of code has explicitly suspended it, and it will not compete for the CPU again until resumed.
Manual control
We can control threads from other threads using their ID (the name we gave in K_THREAD_DEFINE).
/* Suspend thread A */
k_thread_suspend(my_thread_a);
/* Resume thread A */
k_thread_resume(my_thread_a);
/* Kill thread A (Be careful with this) */
k_thread_abort(my_thread_a);With multiple threads running, the next step is to coordinate when they wake up using delays and timers.