Featured image of post Zephyr RTOS 兩種建立Thread的方式

Zephyr RTOS 兩種建立Thread的方式

Zephyr RTOS 兩種建立Thread的方式

Zephyr RTOS 兩種建立Thread的方式

Thread (執行緒)

在 Zephyr OS中,每個獨立的功能或任務可以放在不同的執行緒中執行,內部使用了輕量級的排程器,可以根據不同執行緒的優先級,以及執行緒本身的狀態(ready, running, pending 等),來決定哪一個執行緒能夠先被 CPU 執行,執行緒可以有以下幾個性質:

  1. 執行緒優先級(Priority):數值越小表示優先級越高(Preemptive scheduling 的情況下)。
  2. 堆疊大小(Stack Size):每個執行緒必須設定自己的堆疊空間,Zephyr 會在執行緒切換時儲存或恢復執行緒的上下文。
  3. 執行緒生命週期(Lifecycle):可以在編譯期就被預先配置好,或在執行期動態配置與建立。

Zephyr 建立執行緒的兩種常見方法

在編譯期間(Compile Time)建立執行緒

Zephyr 提供了一個巨集 K_THREAD_DEFINE() 用於在編譯期就宣告並建立執行緒。只要程式一開啟並初始化 RTOS 後,這些執行緒就會自動被建立並進入排程器的管理。

K_THREAD_DEFINE()

1
2
#define K_THREAD_DEFINE(name, stack_size, entry_fn, p1, p2, p3,       \
                        prio, options, delay)
  • name:此執行緒的識別名稱,同時會生成一個 k_tid_t 型態的變數。
  • stack_size:此執行緒的堆疊大小(以 byte 為單位)。
  • entry_fn:此執行緒進入點函式(thread function),開始執行時要跑哪個函式。
  • p1, p2, p3:最多可傳入三個參數給進入點函式使用(都為 void* 型態)。
  • prio:優先級(數值越小,優先級越高)。
  • options:執行緒選項,通常先給 0 表示沒有特別的額外設定。
  • delay:執行緒開始之前的延遲時間(ticks 或毫秒),若填 0 表示不延遲,立即就緒。

在執行期間(Run Time)建立執行緒

另一種是在程式運行期間建立執行緒,使用 API 函數k_thread_create() 建立執行緒。這種方法在有些情況下會更具彈性,例如依據狀態或條件,動態啟動或終止執行緒,以免靜態建立過多浪費資源。

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:指向使用者自行宣告的 struct k_thread 物件,用來儲存此執行緒的控制區塊。
  • stack:指向此執行緒對應的堆疊空間(需先以 K_THREAD_STACK_DEFINE() 靜態或動態配置)。
  • stack_size:此堆疊的大小。
  • entry:執行緒執行的進入點函式。
  • p1, p2, p3:最多可傳入三個參數到執行緒函式。
  • prio:優先級。
  • options:執行緒選項,常用為 0。
  • delay:執行緒開始前是否要延遲。

範例

以下兩段程式碼,分別示範用 K_THREAD_DEFINE()(在編譯期間)或 k_thread_create()(在執行期間)建立兩個執行緒,分別執行 LED 漸亮漸暗(fade_led) 與 LED 閃爍(toggle_led) 的功能 github repo 以下是在Nucleo-F303K8的執行結果,左邊LED執行toggle,右邊執行fading

在編譯期間(Compile Time)動態建立執行緒範例

  1. K_THREAD_DEFINE(fade_tid, 512, fade_led, NULL, NULL, NULL, 5, 0, 0);
    • 建立一個名為 fade_tid 的執行緒,堆疊大小 512 bytes,執行函式為 fade_led,優先級 5。
    • p1, p2, p3 都填 NULL,表示不需要傳遞參數。
  2. K_THREAD_DEFINE(toggle_tid, 512, toggle_led, NULL, NULL, NULL, 5, 0, 0);
    • 同樣建立名為 toggle_tid 的執行緒,stack大小、優先級參數與上面一致,執行函式為 toggle_led。

這些執行緒在編譯的時候就已經被「靜態配置」了,所以當系統啟動並執行到 main() 時,它們也就由排程器自動啟動並不斷執行。

 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);

// 利用 DT_ALIAS(pwm_led0) 與 DT_ALIAS(led1) 取得裝置樹中的設備訊息
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 NUM_STEPS       256U
#define SLEEP_MSEC      5U
#define SLEEP_TIME_MS   500

// 在編譯期間靜態建立兩個執行緒
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 會透過 PWM 設定脈寬來控制 LED 的亮度,製造 "呼吸燈" 效果
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) {
        // 設定當前的脈寬
        pwm_set_pulse_dt(&pwm_led0, pulse_width);

        // 調整脈寬,控制 LED 往漸亮或漸暗方向
        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 會透過 GPIO pin toggle 的方式,使 LED 閃爍
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)
{
    // 檢查 PWM 裝置是否 ready
    if (!pwm_is_ready_dt(&pwm_led0)) {
        printk("Error: PWM device %s is not ready\n", pwm_led0.dev->name);
        return 0;
    }

    // 設定 GPIO pin 為輸出
    gpio_pin_configure_dt(&led1, GPIO_OUTPUT_ACTIVE);

    // main 函式結束後,兩個 K_THREAD_DEFINE 建立的執行緒依然會持續執行
    return 0;
}

在執行期(Run Time)動態建立執行緒範例

若想要在程式執行的過程中(例如:由某個事件觸發)再建立執行緒,可以使用以下的做法: 1. 用 K_THREAD_STACK_DEFINE(my_stack_area, STACK_SIZE); 定義stack。 2. 定義一個 struct k_thread my_thread_data; 作為執行緒控制區塊(Thread Control Block, TCB)。 3. 呼叫 k_thread_create(),傳入上述的stack、控制區塊、進入點函式及其他參數來完成建立。

以下是一個範例,展示如何在 main() 函式中動態建立執行緒(此示例僅顯示與執行緒有關的部分,省略週邊初始化等重複程式碼):

 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
\* 
前面的程式碼相同
*\

// 宣告執行緒stack size
static K_THREAD_STACK_DEFINE(fade_stack, 512);
static K_THREAD_STACK_DEFINE(toggle_stack, 512);

// 宣告執行緒控制區塊
static struct k_thread fade_thread_data;
static struct k_thread toggle_thread_data;

// 這兩個函式和之前範例雷同
void fade_led(void *p1, void *p2, void *p3)
{
    // ...
}

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

int main(void)
{
    // 在執行期動態建立 fade_led thread
    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
    );

    // 在執行期動態建立 toggle_led thread
    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 回傳 k_tid_t,可用於後續管理 thread
    // 例如 k_thread_suspend(fade_tid), k_thread_resume(fade_tid), k_thread_abort(fade_tid)...

    return 0;
}

比較

建立方式特點典型使用時機
Compile Time 建立K_THREAD_DEFINE()一開始已知需要的執行緒,以及系統資源充足時
Run Time 動態建立k_thread_create(),在程式執行時期再分配資源不確定需要多少執行緒,或需要彈性創建/釋放
  1. 如果確定在整個系統的生命週期中,執行緒數量固定,或是系統比較單純(例如一個簡單的多任務控制),用編譯期方式能減少程式碼的複雜度,也能讓程式一啟動就快速就緒。
  2. 如果系統中會動態產生/取消某些任務(如:偵測到新裝置才開新任務等),就可以使用動態建立的方式,提高系統彈性。

總結

  • 靜態(編譯期)建立執行緒:
    • 使用 K_THREAD_DEFINE()
    • 簡單、快速、程式架構清晰
    • 適用於固定且少數的長期執行緒
  • 動態(執行期)建立執行緒:
    • 使用 k_thread_create()
    • 需要自行管理堆疊、控制區塊等
    • 適用於需要彈性創建和銷毀大量或不定數量執行緒的應用場景
comments powered by Disqus