Featured image of post Two Ways to Create Threads in Zephyr RTOS

Two Ways to Create Threads in Zephyr RTOS

Two Ways to Create Threads in Zephyr RTOS

Two Ways to Create Threads in Zephyr RTOS

Thread

In Zephyr OS, each independent function or task can be placed in a different thread for execution. A lightweight scheduler is used internally to decide which thread can be executed by the CPU first, based on the priority of different threads and the state of the thread itself (ready, running, pending, etc.). Threads can have the following properties:

  1. Thread Priority: The smaller the value, the higher the priority (in the case of Preemptive scheduling).
  2. Stack Size: Each thread must configure its own stack space. Zephyr saves or restores the thread context when switching threads.
  3. Thread Lifecycle: Can be pre-configured at compile time, or dynamically configured and created at run time.

Two Common Methods to Create Threads in Zephyr

Create Thread at Compile Time

Zephyr provides a macro K_THREAD_DEFINE() to declare and create threads at compile time. As long as the program starts and initializes RTOS, these threads will be automatically created and managed by the scheduler.

K_THREAD_DEFINE()

1
2
#define K_THREAD_DEFINE(name, stack_size, entry_fn, p1, p2, p3,       \
                        prio, options, delay)
  • name: Identifier name of this thread, which will also generate a variable of type k_tid_t.
  • stack_size: Stack size of this thread (in bytes).
  • entry_fn: Entry point function of this thread (thread function), which function to run when execution starts.
  • p1, p2, p3: Up to three parameters can be passed to the entry point function (all void* type).
  • prio: Priority (smaller value means higher priority).
  • options: Thread options, usually 0 implies no special settings.
  • delay: Delay time before the thread starts (ticks or milliseconds). If 0, it means no delay and ready immediately.

Create Thread at Run Time

Another way is to create threads during program execution, using the API function k_thread_create(). This method is more flexible in some cases, for example, dynamically starting or terminating threads based on states or conditions, to avoid wasting resources by statically creating too many.

k_thread_create()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
k_tid_t k_thread_create(
    struct k_thread *new_thread,
    k_thread_stack_t *stack,
    size_t stack_size,
    k_thread_entry_t entry,
    void *p1,
    void *p2,
    void *p3,
    int prio,
    uint32_t options,
    k_timeout_t delay
);
  • new_thread: Points to a struct k_thread object declared by the user, used to store the Thread Control Block (TCB) of this thread.
  • stack: Points to the stack space corresponding to this thread (needs to be statically or dynamically allocated with K_THREAD_STACK_DEFINE() first).
  • stack_size: Size of this stack.
  • entry: Entry point function executed by the thread.
  • p1, p2, p3: Up to three parameters to pass to the thread function.
  • prio: Priority.
  • options: Thread options, commonly 0.
  • delay: Whether to delay before the thread starts.

Example

The following two code snippets demonstrate creating two threads using K_THREAD_DEFINE() (at compile time) or k_thread_create() (at run time) respectively, executing LED fading (fade_led) and LED blinking (toggle_led) functions. github repo The following is the execution result on Nucleo-F303K8, the left LED executes toggle, the right one executes fading.

Example of Dynamic Thread Creation at Compile Time

  1. K_THREAD_DEFINE(fade_tid, 512, fade_led, NULL, NULL, NULL, 5, 0, 0);
    • Create a thread named fade_tid, stack size 512 bytes, executing function fade_led, priority 5.
    • p1, p2, p3 are all NULL, indicating no parameters need to be passed.
  2. K_THREAD_DEFINE(toggle_tid, 512, toggle_led, NULL, NULL, NULL, 5, 0, 0);
    • Create a thread named toggle_tid similarly, stack size and priority parameters consistent with above, executing function toggle_led.

These threads are already “statically allocated” during compilation, so when the system starts and executes to main(), they will be automatically started and continuously executed by the scheduler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/device.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/gpio.h>

void fade_led(void *p1, void *p2, void *p3);
void toggle_led(void *p1, void *p2, void *p3);

// Use DT_ALIAS(pwm_led0) and DT_ALIAS(led1) to get device info from device tree
static const struct pwm_dt_spec pwm_led0 = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led0));
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);

// Define some constants for controlling fading speed or beat
#define NUM_STEPS       256U
#define SLEEP_MSEC      5U
#define SLEEP_TIME_MS   500

// Statically create two threads at compile time
K_THREAD_DEFINE(fade_tid, 512, fade_led, NULL, NULL, NULL, 5, 0, 0);
K_THREAD_DEFINE(toggle_tid, 512, toggle_led, NULL, NULL, NULL, 5, 0, 0);

// fade_led will control LED brightness by setting pulse width via PWM, creating a "breathing light" effect
void fade_led(void *p1, void *p2, void *p3)
{
    uint32_t pulse_width = 0U;
    uint32_t step = pwm_led0.period / NUM_STEPS;
    uint8_t dir = 1U;

    while (1) {
        // Set current pulse width
        pwm_set_pulse_dt(&pwm_led0, pulse_width);

        // Adjust pulse width, control LED to fade in or out
        if (dir) {
            pulse_width += step;
            if (pulse_width >= pwm_led0.period) {
                pulse_width = pwm_led0.period - step;
                dir = 0U;
            }
        } else {
            if (pulse_width >= step) {
                pulse_width -= step;
            } else {
                pulse_width = step;
                dir = 1U;
            }
        }
        k_sleep(K_MSEC(SLEEP_MSEC));
    }
}

// toggle_led will cause LED to blink by toggling GPIO pin
void toggle_led(void *p1, void *p2, void *p3)
{
    while (1) {
        gpio_pin_toggle_dt(&led1);
        k_sleep(K_MSEC(SLEEP_TIME_MS));
    }
}

int main(void)
{
    // Check if PWM device is ready
    if (!pwm_is_ready_dt(&pwm_led0)) {
        printk("Error: PWM device %s is not ready\n", pwm_led0.dev->name);
        return 0;
    }

    // Configure GPIO pin as output
    gpio_pin_configure_dt(&led1, GPIO_OUTPUT_ACTIVE);

    // After main function ends, the two threads created by K_THREAD_DEFINE will continue to execute
    return 0;
}

Example of Dynamic Thread Creation at Run Time

If you want to create threads during program execution (e.g., triggered by an event), you can use the following method: 1. Use K_THREAD_STACK_DEFINE(my_stack_area, STACK_SIZE); to define stack. 2. Define a struct k_thread my_thread_data; as Thread Control Block (TCB). 3. Call k_thread_create(), passing the above stack, control block, entry point function and other parameters to complete creation.

The following is an example showing how to dynamically create threads in the main() function (this example only shows parts related to threads, omitting repetitive code like peripheral initialization):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* 
Previous code is same
*/

// Declare thread stack size
static K_THREAD_STACK_DEFINE(fade_stack, 512);
static K_THREAD_STACK_DEFINE(toggle_stack, 512);

// Declare thread control block
static struct k_thread fade_thread_data;
static struct k_thread toggle_thread_data;

// These two functions are similar to previous example
void fade_led(void *p1, void *p2, void *p3)
{
    // ...
}

void toggle_led(void *p1, void *p2, void *p3)
{
    // ...
}

int main(void)
{
    // Dynamically create fade_led thread at run time
    k_tid_t fade_tid = k_thread_create(
        &fade_thread_data,
        my_fade_stack,
        K_THREAD_STACK_SIZEOF(my_fade_stack),
        fade_led,
        NULL, NULL, NULL,
        5, 0,
        K_NO_WAIT
    );

    // Dynamically create toggle_led thread at run time
    k_tid_t toggle_tid = k_thread_create(
        &toggle_thread_data,
        my_toggle_stack,
        K_THREAD_STACK_SIZEOF(my_toggle_stack),
        toggle_led,
        NULL, NULL, NULL,
        5, 0,
        K_NO_WAIT
    );

    // k_thread_create returns k_tid_t, which can be used for subsequent thread management
    // e.g. k_thread_suspend(fade_tid), k_thread_resume(fade_tid), k_thread_abort(fade_tid)...

    return 0;
}

Comparison

Creation MethodFeaturesTypical Use Cases
Compile Time CreationK_THREAD_DEFINE()When required threads are known from the start, and system resources are sufficient
Run Time Dynamic Creationk_thread_create(), allocate resources during program executionWhen unsure how many threads are needed, or need flexible creation/release
  1. If it is certain that the number of threads is fixed throughout the system lifecycle, or the system is relatively simple (such as a simple multi-tasking control), using compile-time method can reduce code complexity and allow the program to be ready quickly upon startup.
  2. If tasks are dynamically generated/cancelled in the system (e.g., detecting a new device starts a new task), you can use the dynamic creation method to improve system flexibility.

Summary

  • Static (Compile Time) Thread Creation:
    • Use K_THREAD_DEFINE()
    • Simple, fast, clear program architecture
    • Suitable for fixed and few long-term threads
  • Dynamic (Run Time) Thread Creation:
    • Use k_thread_create()
    • Need to manage stack, control block etc. yourself
    • Suitable for application scenarios requiring flexible creation and destruction of large or indefinite number of threads