2026年2月28日 星期六

STM32無中斷排程器以及解釋

coroutine.h

#ifndef __COROUTINE_H__

#define __COROUTINE_H__
#include "pt.h"

#define PT_TIMER_USE_RTC 1

struct timer
{
#if PT_TIMER_USE_RTC
    unsigned int current;
#endif
    unsigned int interval;
    unsigned int last;
};
extern unsigned int timer_expired(struct timer *t);
extern void timer_set(struct timer *t, unsigned int Interval);
extern void set_task(unsigned char id, PT_THREAD((*func)(struct pt *pt)));
extern void add_task(unsigned char id, PT_THREAD((*func)(struct pt *pt)));
extern int check_pt_main(int id);
extern int Idle_enable;
extern struct pt pt_temp[16];
extern void scheduler(void);

#endif


#include "coroutine.h"
#include "main.h"
#include "fifofast.h"

#define SLEEP_DEBUG 0
#define TASK_NUM (16)
PT_THREAD((*task[TASK_NUM])(struct pt *pt));
struct pt pt_main[TASK_NUM];
#define TASK_FIFO_SIZE (32)
_fff_declare(int8_t, tid_fifo, TASK_FIFO_SIZE);
_fff_init(tid_fifo);
#define DIS_INTR() __disable_irq()
#define ENA_INTR() __enable_irq()
int Idle_enable = 0;

void set_task(unsigned char id, PT_THREAD((*func)(struct pt *pt)))
{
    task[id] = func;
}
void add_task(unsigned char id, PT_THREAD((*func)(struct pt *pt)))
{
    if (!pt_main[id].lc)
    {
        _fff_write(tid_fifo, id);
    }
    PT_INIT(&pt_main[id]);
    task[id] = func;
}
int check_pt_main(int id)
{
    return (int)pt_main[id].lc;
}
unsigned int check_cur_task_num(void)
{
    return (_fff_mem_level(tid_fifo));
}
#if PT_TIMER_USE_RTC
#define RTC_SS() (READ_REG(RTC->SSR))
// #define RTC_SS_MAX() ((unsigned int)1024)
#define RTC_SS_MAX() ((unsigned int)1000)
#define RTC_K() ((RTC_SS_MAX() << 8) / 1000)
#define RTC_KS8(n) (((n) * RTC_K()) >> 8)

unsigned int timer_expired(struct timer *t)
{
    // RTC_SS is down count
    unsigned int register pass_count;
    unsigned int register now;

    now = RTC_SS();
    pass_count = (t->last >= now) ? t->last - now : (RTC_SS_MAX() - now) + t->last;
    t->current += pass_count;
    t->last = now;
    return (t->current >= t->interval);
}

void timer_set(struct timer *t, unsigned int interval)
{
    t->interval = RTC_KS8(interval);
    t->last = RTC_SS();
    t->current = 0;
}
#else
unsigned int timer_expired(struct timer *t)
{
    return (unsigned int)(HAL_GetTick() - t->last) >= (unsigned int)t->interval;
}
void timer_set(struct timer *t, unsigned int Interval)
{
    t->interval = Interval;
    t->last = HAL_GetTick();
}
#endif

void scheduler(void)
{
    while (_fff_mem_level(tid_fifo) != 0)
    {
        unsigned char cur_id;
        cur_id = _fff_read(tid_fifo);
        if (PT_SCHEDULE(task[cur_id](&pt_main[cur_id])))
        {
            DIS_INTR();
            _fff_write(tid_fifo, cur_id);
            ENA_INTR();
        }
    }
    Idle_enable = 0;
}

# 協同排程器的運作原理 這個協同排程器(cooperative scheduler)是基於 **Protothreads** 函式庫實作的輕量級多工系統。讓我詳細說明它的運作機制: ## 1. 核心概念:合作式多工 與搶佔式(preemptive)作業系統不同,這個排程器採用**合作式**調度: - 每個任務必須**主動讓出控制權**(透過 `PT_YIELD()` 或 `PT_WAIT_UNTIL()`) - 不會強制中斷正在執行的任務 - 如果某個任務不讓出控制權,其他任務就會被「餓死」 ## 2. 資料結構 ```c PT_THREAD((*task[TASK_NUM])(struct pt *pt)); // 任務函數指標陣列(最多16個) struct pt pt_main[TASK_NUM]; // 每個任務的執行狀態 _fff_declare(int8_t, tid_fifo, TASK_FIFO_SIZE); // FIFO 佇列(最多32個任務ID) ``` - **`task[]`**:儲存每個任務的函數指標 - **`pt_main[]`**:儲存每個任務的 protothread 狀態,其中 `pt.lc`(line continuation)記錄任務上次中斷的位置 - **`tid_fifo`**:就緒佇列,存放準備執行的任務 ID ## 3. 排程器主迴圈 ```c void scheduler(void) { while (_fff_mem_level(tid_fifo) != 0) // 當佇列不為空 { unsigned char cur_id; cur_id = _fff_read(tid_fifo); // 從佇列取出任務ID if (PT_SCHEDULE(task[cur_id](&pt_main[cur_id]))) // 執行任務 { DIS_INTR(); // 關閉中斷 _fff_write(tid_fifo, cur_id); // 將任務ID放回佇列 ENA_INTR(); // 開啟中斷 } } Idle_enable = 0; } ``` **運作流程:** 1. 檢查就緒佇列是否有任務 2. 從佇列頭部取出一個任務 ID 3. 執行該任務的 protothread 函數 4. 如果任務返回非零值(表示還有工作要做)→ 將任務 ID 放回佇列尾部 5. 如果任務返回零(表示任務結束)→ 不放回佇列 6. 重複直到佇列清空 ## 4. Protothreads 的魔法:無堆疊協程 Protothreads 使用一個巧妙的技巧來實現「看起來會阻塞」但實際上不會消耗堆疊的程式碼: ```c PT_THREAD(example_task(struct pt *pt)) { PT_BEGIN(pt); // 執行一些工作 while(1) { PT_WAIT_UNTIL(pt, condition_is_true); // 在此「等待」 // 做某事 PT_YIELD(pt); // 讓出控制權 } PT_END(pt); } ``` **關鍵機制:** - `pt->lc` 儲存一個整數,代表上次讓出控制權的「程式碼位置」 - 每次呼叫任務函數時,會用 `switch-case` 跳到上次的位置繼續執行 - 這樣就能在沒有堆疊的情況下「記住」執行進度 ## 5. 任務管理 **新增任務:** ```c void add_task(unsigned char id, PT_THREAD((*func)(struct pt *pt))) { if (!pt_main[id].lc) // 如果任務尚未初始化 { _fff_write(tid_fifo, id); // 將任務ID加入就緒佇列 } PT_INIT(&pt_main[id]); // 初始化 protothread 狀態 task[id] = func; // 設定任務函數 } ``` **重要特性:** - 任務 ID 是陣列索引,必須保持穩定(0-15) - 如果 `pt_main[id].lc` 非零,表示任務「還活著」 - 任務可以從主程式或中斷服務常式中喚醒 ## 6. 計時器系統 本專案預設使用 **RTC(即時時鐘)** 的子秒暫存器來實現高精度計時: ```c unsigned int timer_expired(struct timer *t) { unsigned int pass_count; unsigned int now = RTC_SS(); // RTC 是遞減計數(999→0) // 計算經過的時間(處理環繞情況) pass_count = (t->last >= now) ? t->last - now : (RTC_SS_MAX() - now) + t->last; t->current += pass_count; t->last = now; return (t->current >= t->interval); } ``` **使用方式:** ```c struct timer my_timer; timer_set(&my_timer, 1000); // 設定 1000ms PT_WAIT_UNTIL(pt, timer_expired(&my_timer)); // 等待計時器到期 ``` ## 7. 關鍵設計要點 **為什麼需要關閉中斷?** ```c DIS_INTR(); _fff_write(tid_fifo, cur_id); ENA_INTR(); ``` 因為中斷服務常式也可能呼叫 `add_task()` 來喚醒任務,如果不保護 FIFO 寫入操作,可能會發生競爭條件導致資料損壞。 **限制:** - 最多 16 個並行任務(`TASK_NUM = 16`) - 就緒佇列最多 32 個條目(`TASK_FIFO_SIZE = 32`) - 如果 32 個插槽全滿,新任務會遺失 **效能考量:** - 任務必須頻繁讓出控制權,否則會增加中斷延遲 - 關鍵區間(critical section)越短越好 - 適合處理 I/O、通訊、LED 動畫等需要「等待」的任務 ## 總結 這個排程器實現了: ✅ 輕量級多工(每個任務只需少量記憶體) ✅ 易於閱讀的「線性」程式碼風格(看起來像會阻塞) ✅ 適合資源受限的 MCU(STM32F072 只有 16KB RAM) ✅ 可從主迴圈或中斷喚醒任務 但需要注意: ⚠️ 任務必須主動讓出控制權 ⚠️ 一個失控的任務會拖累整個系統 ⚠️ 無法利用多核心(單執行緒輪詢)