上文說到 RT-Thread 對臨界區的處理方式有多種,其中已經分析了關閉調度器和屏蔽中斷的方式,
本文就來學學另外的線程同步方式。
目錄
前言
一、IPC機制
二、信號量
2.1 信號量控制塊
2.2 信號量操作
2.2.1 創建和刪除
2.2.2 初始化和脫離
2.2.3 獲取信號量
2.2.4 釋放信號量
2.2.5 信號量控制
2.3 示例(典型停車場模型)
三、互斥量
3.1 優先級翻轉
3.2 優先級繼承
3.3 互斥量控制塊
3.4 互斥量操作
3.2.1 創建和刪除
3.2.2 初始化和脫離
3.2.3 獲取互斥量
3.2.4 釋放互斥量
3.5 示例(優先級繼承)
四、事件集
4.1 事件集控制塊
4.2 事件集操作
4.2.1 創建和刪除
4.2.2 初始化和脫離
4.2.3 發送事件
4.2.4 接收事件
4.3 示例(邏輯與和邏輯或)
結語
前言
在我們專欄前面的文章中,已經學習過 RT-Thread 線程操作函數、軟件定時器、臨界區的保護,我們都進行了一些底層的分析,能讓我們更加理解 RT-Thread 的內核,但是也不要忽略了上層的函數使用 要理解 RT-Thread 面向對象的思想,對所有的這些線程啊,定時器,包括要介紹的信號量,郵箱這些,都是以 對象 來操作,直白的說來就是 對于所有這些對象,都是以結構體的形式來表示,然后通過對這個對象結構體的操作來進行的。
本文所要介紹的內容屬于 IPC機制,這些內容相對來說比較簡單,我們重點在于學會如何使用以及了解他們的使用場合。
本 RT-Thread 專欄記錄的開發環境:
RT-Thread記錄(一、版本開發環境及配合CubeMX) + http://www.nxhydt.com/d/1850333.html
RT-Thread記錄(二、RT-Thread內核啟動流程)+ http://www.nxhydt.com/d/1850347.html
RT-Thread 內核篇系列博文鏈接:
RT-Thread記錄(三、RT-Thread線程操作函數)+ http://www.nxhydt.com/d/1850351.html
RT-Thread記錄(四、RTT時鐘節拍和軟件定時器)+ http://www.nxhydt.com/d/1850554.html
RT-Thread記錄(五、RT-Thread 臨界區保護) + http://www.nxhydt.com/d/1850712.html
一、IPC機制
在嵌入式操作系統中,運行代碼主要包括線程 和 ISR,在他們的運行過程中,因為應用或者多線程模型帶來的需求,有時候需要同步,有時候需要互斥,有時候也需要彼此交換數據。操作系統必須提供相應的機制來完成這些功能,這些機制統稱為 線程間通信(IPC機制)。
本文所要介紹的就是關于線程同步的信號量、互斥量、事件 也屬于 IPC機制。
RT-Thread 中的 IPC機制包括信號量、互斥量、事件、郵箱、消息隊列。對于學習 RT-Thread ,這些IPC機制我們必須要學會靈活的使用。
為什么要說一下這個IPC機制?
我們前面說到過,RT-Thread 面向對象的思想,所有的這些 IPC 機制都被當成一個對象,都有一個結構體控制塊,我們用信號量結構體來看一看:
Kernel object
有哪些,我們可以從基礎內核對象結構體定義下面的代碼找到:
本節說明了 RT-Thread 的 IPC 機制,同時通過 信號量的結構體控制塊再一次的認識了 RT-Thread 面向對象的設計思想。
在我的 FreeRTOS 專欄中,對于FreeRTOS 的信號量,互斥量,事件集做過說明和測試。在這個部分,實際上 RT-Thread 與 FreeRTOS 是類似的,都是一樣的思想。所以如果屬熟悉FreeRTOS的話,這部分是簡單的,我們要做的就是記錄一下 對象的控制塊,和操作函數,加以簡單的示例測試。
二、信號量
信號量官方的說明是:信號量是一種輕型的用于解決線程間同步問題的內核對象,線程可以獲取或釋放它,從而達到同步或互斥的目的。
信號量非常靈活,可以使用的場合也很多:
- 比如 一個典型的應用場合就是停車位模型,總共有多少個車位,就是多少個信號量,入口進入一輛車信號量-1,出口離開一輛車信號量+1。
- 比如 兩個線程之間的同步,信號量的值初始化成 0,而嘗試獲得該信號量的線程,一定需要等待另一個釋放信號量的線程先執行完。
在 FreeRTOS 中存在二值信號量,但是 RT-Thread 中已經沒有了,官方有說明:
信號量記住一句話基本就可以,釋放一次信號量就+1,獲取一次就-1,如果信號量數據為0,那么嘗試獲取的線程就會掛機,直到有線程釋放信號量使得信號量大于0。
2.1 信號量控制塊
老規矩用源碼,解釋看注釋(使用起來也方便復制 ~ ~!):
#ifdef RT_USING_SEMAPHORE
/**
* Semaphore structure
* value 信號量的值,直接表明目前信號量的數量
*/
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of semaphore. */
rt_uint16_t reserved; /**< reserved field */
};
/*
rt_sem_t 是指向 semaphore 結構體的指針類型
*/
typedef struct rt_semaphore *rt_sem_t;
#endif
2.2 信號量操作
2.2.1 創建和刪除
同以前的線程那些一樣,動態的方式,先定義一個信號量結構體的指針變量,接收創建好的句柄。
創建信號量:
/*
參數的含義:
1、name 信號量名稱
2、value 信號量初始值
3、flag 信號量標志,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
信號量創建成功,返回信號量的控制塊指針
信號量創建失敗,返回RT_BULL
*/
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)
對于最后的參數 flag,決定了當信號量不可用時(就是當信號量為0的時候),多個線程等待的排隊方式。只有RT_IPC_FLAG_FIFO
(先進先出)或 RT_IPC_FLAG_PRIO
(優先級等待)兩種 flag。
關于用哪一個,要看具體的情況,官方有特意說明:
刪除信號量:
/*
參數:
sem rt_sem_create() 創建的信號量對象,信號量句柄
返回值:
RT_EOK 刪除成功
*/
rt_err_t rt_sem_delete(rt_sem_t sem)
2.2.2 初始化和脫離
靜態的方式,先定義一個信號量結構體,然后對他進行初始化。
初始化信號量:
/**
參數的含義:
1、sem 信號量對象的句柄,就是開始定義的信號量結構體變量
2、name 信號量名稱
3、value 信號量初始值
4、flag 信號量標志,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
RT_EOK 初始化成功
*/
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
脫離信號量:
/*
參數:
sem 信號量對象的句柄
返回值:
RT_EOK 脫離成功
*/
rt_err_t rt_sem_detach(rt_sem_t sem);
2.2.3 獲取信號量
當信號量值大于零時,線程將獲得信號量,并且相應的信號量值會減 1。
/**
參數:
1、sem 信號量對象的句柄
2、time 指定的等待時間,單位是操作系統時鐘節拍(OS Tick)
返回值:
RT_EOK 成功獲得信號量
-RT_ETIMEOUT 超時依然未獲得信號量
-RT_ERROR 其他錯誤
*/
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
注意!要等待的時間是系統時鐘節拍(OS Tick)。
無等待獲取信號量:
//就是上面獲取的等待時間為0的方式
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
return rt_sem_take(sem, 0);
}
當線程申請的信號量資源實例為0時,直接返回 - RT_ETIMEOUT。
2.2.4 釋放信號量
釋放信號量可以使得該信號量+1,如果有線程在等待這個信號量,可以喚醒這個線程。
/**
參數:
sem 信號量對象的句柄
返回值:
RT_EOK 成功釋放信號量
*/
rt_err_t rt_sem_release(rt_sem_t sem)
2.2.5 信號量控制
信號量控制函數,用來重置信號量,使得信號量恢復為設定的值:
/**
* This function can get or set some extra attributions of a semaphore object.
參數:
sem 信號量對象的句柄
cmd 信號量控制命令 ,支持命令:RT_IPC_CMD_RESET
arg 暫時不知道
返回值:
RT_EOK 成功釋放信號量
*/
rt_err_t rt_sem_control(rt_sem_t sem, int cmd, void *arg)
{
rt_ubase_t level;
/* parameter check */
RT_ASSERT(sem != RT_NULL);
RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
if (cmd == RT_IPC_CMD_RESET)
{
rt_ubase_t value;
/* get value */
value = (rt_ubase_t)arg;
/* disable interrupt */
level = rt_hw_interrupt_disable();
/* resume all waiting thread */
rt_ipc_list_resume_all(&sem->parent.suspend_thread);
/* set new value */
sem->value = (rt_uint16_t)value;
/* enable interrupt */
rt_hw_interrupt_enable(level);
rt_schedule();
return RT_EOK;
}
return -RT_ERROR;
}
使用示例:
rt_err_t result;
rt_uint32_t value;
value = 10; /* 重置的值,即重置為10 */
result = rt_sem_control(sem, RT_IPC_CMD_RESET, (void*)value)
/* 重置為0 */
rt_sem_control(sem, RT_IPC_CMD_RESET, RT_NULL)
對sem重置后,會先把sem上掛起的所有任務進行喚醒(任務的error是-RT_ERROR),然后把sem的值會重新初始化成設定的值。
在官方論壇有如下說明:
在rt_sem_release后使用rt_sem_control的目的是因為在某些應用中必須rt_sem_take和rt_sem_release依次出現,而不允許rt_sem_release被連續多次調用,一旦出現這種情況會被認為是出現了異常,通過調用rt_sem_control接口來重新初始化 sem_ack恢復異常。
2.3 示例(典型停車場模型)
前面說到過,信號量非常靈活,可以使用的場合也很多,官方也有很多例子,我們這里做個典型的示例
— 停車場模型(前面用截圖做解釋,后面會附帶源碼)。
示例中,我們使用兩個不同的按鍵來模擬車輛的進出,但是考慮到我們還沒有學設備和驅動,沒有添加按鍵驅動,所以我們用古老的方式來實現按鍵操作:
按鍵key3,代表車輛離開:
按鍵key2,代表車輛進入:
信號量的創建,初始10個車位:
當然不要忘了,車輛進入和車輛離開(兩個按鍵)是需要兩個線程的。
我們來看看測試效果,說明如圖:
注意上圖測試最后的細節,雖然 one car get out!
但是打印出來的停車位還是0,可以這么理解,key3_thread_entry
線程釋放了信號量以后還沒來得及打印,等待信號量的線程key2_thread_entry
就獲取到了信號量。
具體的分析需要看rt_sem_release
函數源碼,里面會判斷是否需要值+1,以及是否需要調度:
附上上面測試代碼:
/*
* Copyright (c) 2006-2022, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2022-02-16 RT-Thread first version
*/
#include
#include "main.h"
#include "usart.h"
#include "gpio.h"
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include
static struct rt_thread led1_thread; //led1線程
static char led1_thread_stack[256];
static rt_thread_t led2_thread = RT_NULL; //led2線程
static rt_thread_t key2_thread = RT_NULL; //
static rt_thread_t key3_thread = RT_NULL; //
rt_sem_t mysem;
static void led1_thread_entry(void *par){
while(1){
LED1_ON;
rt_thread_mdelay(1000);
LED1_OFF;
rt_thread_mdelay(1000);
}
}
static void led2_thread_entry(void *par){
while(1){
LED2_ON;
rt_thread_mdelay(500);
LED2_OFF;
rt_thread_mdelay(500);
}
}
static void key2_thread_entry(void *par){
static rt_err_t result;
while(1){
if(key2_read == 0){
rt_thread_mdelay(10); //去抖動
if(key2_read == 0){
result = rt_sem_take(mysem, 1000);
if (result != RT_EOK)
{
rt_kprintf("the is no parking spaces now...\r\n");
}
else
{
rt_kprintf("one car get in!,we have %d parking spaces now...\r\n",mysem->value);
}
while(key2_read == 0){rt_thread_mdelay(10);}
}
}
rt_thread_mdelay(1);
}
}
static void key3_thread_entry(void *par){
while(1){
if(key3_read == 0){
rt_thread_mdelay(10); //去抖動
if(key3_read == 0){
if(mysem->value < 10){
rt_sem_release(mysem);
rt_kprintf("one car get out!,we have %d parking spaces now...\r\n",mysem->value);
}
while(key3_read == 0){rt_thread_mdelay(10);} //去抖動
}
}
rt_thread_mdelay(1);
}
}
int main(void)
{
MX_GPIO_Init();
MX_USART1_UART_Init();
rt_err_t rst2;
rst2 = rt_thread_init(&led1_thread,
"led1_blink ",
led1_thread_entry,
RT_NULL,
&led1_thread_stack[0],
sizeof(led1_thread_stack),
RT_THREAD_PRIORITY_MAX -1,
50);
if(rst2 == RT_EOK){
rt_thread_startup(&led1_thread);
}
mysem = rt_sem_create("my_sem1", 10, RT_IPC_FLAG_FIFO);
if(RT_NULL == mysem){
LOG_E("create sem failed!...\n");
}
else LOG_D("we have 10 parking spaces now...\n");
key2_thread = rt_thread_create("key2_control",
key2_thread_entry,
RT_NULL,
512,
RT_THREAD_PRIORITY_MAX -2,
50);
/* 如果獲得線程控制塊,啟動這個線程 */
if (key2_thread != RT_NULL)
rt_thread_startup(key2_thread);
key3_thread = rt_thread_create("key3_control",
key3_thread_entry,
RT_NULL,
512,
RT_THREAD_PRIORITY_MAX -2,
50);
/* 如果獲得線程控制塊,啟動這個線程 */
if (key3_thread != RT_NULL)
rt_thread_startup(key3_thread);
return RT_EOK;
}
void led2_Blink(){
led2_thread = rt_thread_create("led2_blink",
led2_thread_entry,
RT_NULL,
256,
RT_THREAD_PRIORITY_MAX -1,
50);
/* 如果獲得線程控制塊,啟動這個線程 */
if (led2_thread != RT_NULL)
rt_thread_startup(led2_thread);
}
MSH_CMD_EXPORT(led2_Blink, Led2 sample);
三、互斥量
互斥量是一種特殊的二值信號量。互斥量的狀態只有兩種,開鎖或閉鎖(兩種狀態值)。
互斥量支持遞歸,持有該互斥量的線程也能夠再次獲得這個鎖而不被掛起。自己能夠再次獲得互斥量。
互斥量可以解決優先級翻轉問題,它能夠實現優先級繼承。
互斥量互斥量不能在中斷服務例程中使用。
3.1 優先級翻轉
優先級翻轉,我以前寫過:
再用官方的圖加深理解:
3.2 優先級繼承
優先級繼承,以前也寫過:
再用官方的圖加深理解:
需要切記的是互斥量不能在中斷服務例程中使用。
3.3 互斥量控制塊
#ifdef RT_USING_MUTEX
/**
* Mutual exclusion (mutex) structure
* parent 繼承ipc類
* value 互斥量的值
* original_priority 持有線程的原始優先級
* hold 持有線程的持有次數,可以多次獲得
* *owner 當前擁有互斥量的線程
*/
struct rt_mutex
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of mutex */
rt_uint8_t original_priority; /**< priority of last thread hold the mutex */
rt_uint8_t hold; /**< numbers of thread hold the mutex */
struct rt_thread *owner; /**< current owner of mutex */
};
/* rt_mutext_t 為指向互斥量結構體的指針類型 */
typedef struct rt_mutex *rt_mutex_t;
#endif
3.4 互斥量操作
3.4.1 創建和刪除
先定義一個指向互斥量結構體的指針變量,接收創建好的句柄。
創建互斥量:
/**
參數的含義:
1、name 互斥量名稱
2、flag 該標志已經作廢,無論用戶選擇 RT_IPC_FLAG_PRIO 還是 RT_IPC_FLAG_FIFO,
內核均按照 RT_IPC_FLAG_PRIO 處理
返回值:
互斥量創建成功,返回互斥量的控制塊指針
互斥量創建失敗,返回RT_BULL
*/
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)
刪除互斥量:
/**
參數:
mutex 互斥量對象的句柄
返回值:
RT_EOK 刪除成
*/
rt_err_t rt_mutex_delete(rt_mutex_t mutex)
3.4.2 初始化和脫離
靜態的方式,先定義一個互斥量結構體,然后對他進行初始化。
初始化互斥量:
/**
參數的含義:
1、mutex 互斥量對象的句柄,指向互斥量對象的內存塊,開始定義的結構體
2、name 互斥量名稱
3、flag 該標志已經作廢,按照 RT_IPC_FLAG_PRIO (優先級)處理
返回值:
RT_EOK 初始化成功
*/
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)
脫離互斥量:
/**
參數:
mutex 互斥量對象的句柄
返回值:
RT_EOK 成功
*/
rt_err_t rt_mutex_detach(rt_mutex_t mutex)
3.4.3 獲取互斥量
一個時刻一個互斥量只能被一個線程持有。
如果互斥量沒有被其他線程控制,那么申請該互斥量的線程將成功獲得該互斥量。如果互斥量已經被當前線程線程控制,則該互斥量的持有計數加 1,當前線程也不會掛起等待。
/**
參數:
1、mutex 互斥量對象的句柄
2、time 指定的等待時間,單位是操作系統時鐘節拍(OS Tick)
返回值:
RT_EOK 成功獲得互斥量
-RT_ETIMEOUT 超時依然未獲得互斥量
-RT_ERROR 獲取失敗
*/
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
3.4.4 釋放互斥量
在獲得互斥量后,應該盡可能的快釋放互斥量。
/**
參數:
mutex 互斥量對象的句
返回值:
RT_EOK 成功
*/
rt_err_t rt_mutex_release(rt_mutex_t mutex)
3.5 示例(優先級繼承)
互斥量做一個簡單的示例,但是即便簡單,也能體現出優先級繼承這個機制。
示例中,我們使用兩個按鍵,key2按鍵,按一次獲取互斥量,再按一次釋放互斥量,打印自己初始優先級,當前優先級,互斥量占有線程優先級這幾個量。key3按鍵,按一次,獲取互斥量,立馬就釋放,也打印幾個優先級。
互斥量的創建,和兩個線程的優先級:
key2操作:
key3操作:
測試結果說明圖:
示例中為了更好的演示并沒有快進快出,實際使用還是需要快進快出,除非你自己就是有這種特出需求。
還有一個細節,就是 RT-Thread 中對象的 名字,只能顯示8個字符長度,長了會截斷,并不影響使用。
四、事件集
事件集這部分與 FreeRTOS 基本一樣。
事件集主要用于線程間的同步,它的特點是可以實現一對多,多對多的同步。即一個線程與多個事件的關系可設置為:其中任意一個事件喚醒線程,或幾個事件都到達后才喚醒線程進行后續的處理;同樣,事件也可以是多個線程同步多個事件。
RT-Thread 定義的事件集有以下特點:
- 事件只與線程相關,事件間相互獨立:每個線程可擁有 32 個事件標志,采用一個 32 bit 無符號整型數進行記錄,每一個 bit 代表一個事件;
- 事件僅用于同步,不提供數據傳輸功能;
- 事件無排隊性,即多次向線程發送同一事件 (如果線程還未來得及讀走),其效果等同于只發送一次。
4.1 事件集控制塊
#ifdef RT_USING_EVENT
/**
* flag defintions in event
* 邏輯與
* 邏輯或
* 清除標志位
*/
#define RT_EVENT_FLAG_AND 0x01 /**< logic and */
#define RT_EVENT_FLAG_OR 0x02 /**< logic or */
#define RT_EVENT_FLAG_CLEAR 0x04 /**< clear flag */
/*
* event structure
* set:事件集合,每一 bit 表示 1 個事件,bit 位的值可以標記某事件是否發生
*/
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /**< event set */
};
/* rt_event_t 是指向事件結構體的指針類型 */
typedef struct rt_event *rt_event_t;
#endif
4.2 事件集操作
4.2.1 創建和刪除
先定義一個指向事件集結構體的指針變量,接收創建好的句柄。
創建事件集:
/**
參數的含義:
1、name 事件集的名稱
2、flag 事件集的標志,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO理
返回值:
事件集創建成功,返回事件集的控制塊指針
事件集創建失敗,返回RT_BULL
*/
rt_event_t rt_event_create(const char *name, rt_uint8_t flag)
flag 使用哪一個,解釋和信號量一樣,可參考信號量創建部分說明。
刪除事件集:
/**
參數:
event 事件集對象的句柄
返回值:
RT_EOK 成功
*/
rt_err_t rt_event_delete(rt_event_t event)
4.2.2 初始化和脫離
靜態的方式,先定義一個事件集結構體,然后對他進行初始化。
初始化事件集:
/**
參數的含義:
1、event 事件集對象的句柄
2、name 事件集的名稱
3、flag 事件集的標志,它可以取如下數值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
RT_EOK 初始化成功
*/
rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag)
脫離事件集:
/**
參數:
event 事件集對象的句柄
返回值:
RT_EOK 成功
*/
rt_err_t rt_event_detach(rt_event_t event)
4.2.3 發送事件
發送事件函數可以發送事件集中的一個或多個事件。
/**
參數的含義:
1、event 事件集對象的句柄
2、set 發送的一個或多個事件的標志值
返回值:
RT_EOK 成功
*/
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
4.2.4 接收事件
內核使用 32 位的無符號整數來標識事件集,它的每一位代表一個事件,因此一個事件集對象可同時等待接收 32 個事件,內核可以通過指定選擇參數 “邏輯與” 或“邏輯或”來選擇如何激活線程。
/**
參數的含義:
1、event 事件集對象的句柄
2、set 接收線程感的事件
3、option 接收選項,可取的值為
#define RT_EVENT_FLAG_AND 0x01 邏輯與
#define RT_EVENT_FLAG_OR 0x02 邏輯或
#define RT_EVENT_FLAG_CLEAR 0x04 選擇清除重置事件標志位
4、timeout 指定超時時間
5、recved 指向接收到的事件,如果不在意,可以使用 NULL
返回值:
RT_EOK 成功
-RT_ETIMEOUT 超時
-RT_ERROR 錯誤
*/
rt_err_t rt_event_recv(rt_event_t event,
rt_uint32_t set,
rt_uint8_t option,
rt_int32_t timeout,
rt_uint32_t *recved)
4.3 示例(邏輯與和邏輯或)
事件集通過示例可以很好的理解怎么使用,我們示例中,用按鈕發送事件,其他線程接收事件,進行對應的處理。
按鍵操作:
線程邏輯或處理:
邏輯或測試結果:
線程邏輯與處理:
邏輯與測試結果:
結語
本文雖然只是介紹了信號量、互斥量和事件集這幾個比較簡單的線程同步操作,但是最終完成了后發現內容還是很多的。
洋洋灑灑這么多字,最終看下來自己還是挺滿意的,希望我把該表述的都表達清楚了,希望大家多多提意見,讓博主能給大家帶來更好的文章。
那么下一篇的 RT-Thread 記錄,就要來說說與線程通訊 有關的 郵箱、消息隊列和信號內容了。
謝謝!
-
IPC
+關注
關注
3文章
345瀏覽量
51828 -
RT-Thread
+關注
關注
31文章
1272瀏覽量
39923 -
信號量
+關注
關注
0文章
53瀏覽量
8314
發布評論請先 登錄
相關推薦
評論