? ? ? ?在之前寫的另外一篇文章——<從0到1寫RT-Thread內核——線程定義及切換的實現>中線程體內的延時使用的是軟件延時,即還是讓CPU空等來達到延時的效果。RTOS中的延時叫阻塞延時,即線程需要延時的時候,線程會放棄CPU的使用權,CPU可以去干其他的事情,當線程延時時間到,重新獲取CPU使用權,線程繼續運行,這樣就充分利用了CPU的資源,而不是干等著。
? ? ? ?當某個線程需要延時,進入阻塞狀態,如果沒有其他線程可以運行,RTOS都會為CPU創建一個空閑線程,這個時候CPU就運行空閑線程。在RT-Thread中,空閑線程是系統在初始化的時候創建的優先級最低的線程,空閑線程主要是做一些系統內存的清理工作。但為了簡單起見,這里我們的空閑線程只對一個全局變量進行計數。在實際應用中,當系統進入空閑線程的時候,可在空閑線程中讓單片機進入休眠或者低功耗等操作。
我們把空閑線程與阻塞延時的實現分為以下兩大步:
一.實現空閑線程
1.定義空閑線程的棧
#include <rtthread.h>
#include <rthw.h>
#define IDLE_THREAD_STACK_SIZE 512
ALIGN(RT_ALIGN_SIZE)
static rt_uint8_t rt_thread_stack[IDLE_THREAD_STACK_SIZE];
2.定義空閑線程控制塊
struct rt_thread idle;
3.定義空閑線程函數
rt_ubase_t rt_idletask_ctr = 0;void rt_thread_idle_entry(void *parameter)
{parameter = parameter;while (1){rt_idletask_ctr ++;/* 進行系統調度,這個是我自己后來加的,野火官方的例程是沒有調用rt_schedule, *我覺得那樣是錯誤的,因為這里不加rt_schedule的話程序執行到空閑線程就回不去了*///rt_schedule();//這里補充一下,野火的例程并沒有錯,每次產生滴答定時器中斷都會調用調度器,所以這里不需要調用調度器的}
}
4.空閑線程初始化
/*** @ingroup SystemInit** 初始化空閑線程,啟動空閑線程** @note 當系統初始化的時候該函數必須被調用*/
void rt_thread_idle_init(void)
{/* 初始化線程 */rt_thread_init(&idle,"idle",rt_thread_idle_entry,RT_NULL,&rt_thread_stack[0],sizeof(rt_thread_stack));/* 將空閑線程插入到就緒列表中優先級最低的鏈表中 */rt_list_insert_before( &(rt_thread_priority_table[RT_THREAD_PRIORITY_MAX-1]),&(idle.tlist) );
}
以上4步在之前的文章——<從0到1寫RT-Thread內核——線程定義及切換的實現>有詳細介紹過了,這里就不再過多解釋。
二.實現阻塞延時
? ? ? 阻塞延時的阻塞是指線程調用該延時函數后,線程會被剝離CPU使用權,然后進入阻塞狀態,直到延時結束,線程會重新獲取CPU使用權才可繼續運行,在線程阻塞的這段時間,CPU可以去執行其他的線程,如果其他的線程也在延時狀態,那么CPU就將運行空閑線程。我們定義一個延時函數rt_thread_delay函數,其代碼清單如下圖:
? ? ? 上面的代碼中我們通過thread->remaining_tick來判斷某個線程的延時是否結束,thread->remaining_tick是在SysTick_Handler中遞減的,其代碼如下:
void SysTick_Handler(void)
{/* 進入中斷 */rt_interrupt_enter();rt_tick_increase();/* 離開中斷 */rt_interrupt_leave();
}
/* * rt_interrupt_nest為中斷計數器,是一個全局變量,用來記錄中斷嵌套次數。* 每進入一個中斷函數,就會加一* 每離開一個中斷函數,就會減一*/
volatile rt_uint8_t rt_interrupt_nest;/*** 當BSP文件的中斷服務函數進入時會調用該函數* * @note 請不要在應用程序中調用該函數** @see rt_interrupt_leave*/
void rt_interrupt_enter(void)
{rt_base_t level;/* 關中斷 */level = rt_hw_interrupt_disable();/* 中斷計數器++ */rt_interrupt_nest ++;/* 開中斷 */rt_hw_interrupt_enable(level);
}/*** 當BSP文件的中斷服務函數離開時會調用該函數** @note 請不要在應用程序中調用該函數** @see rt_interrupt_enter*/
void rt_interrupt_leave(void)
{rt_base_t level;/* 關中斷 */level = rt_hw_interrupt_disable();/* 中斷計數器-- */rt_interrupt_nest --;/* 開中斷 */rt_hw_interrupt_enable(level);
}
//rt_tick 為系統時基計數器,是一個全局變量,用來記錄產生了多少次SysTick中斷
static rt_tick_t rt_tick = 0;
extern rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];void rt_tick_increase(void)
{rt_ubase_t i;struct rt_thread *thread;rt_tick ++;/* 掃描就緒列表中所有線程的remaining_tick,如果不為0,則減1 */for(i=0; i<RT_THREAD_PRIORITY_MAX; i++){thread = rt_list_entry( rt_thread_priority_table[i].next,struct rt_thread,tlist);if(thread->remaining_tick > 0){thread->remaining_tick --;}}/* 系統調度 */rt_schedule();
}
? ? ? ?下面我們來看一下main函數和線程1和線程2的函數體,在main函數中我們分別初始化了空閑線程、線程1和線程2并將它們插入到對應就緒列表的鏈表中去,然后就開始了系統調度(rt_system_scheduler_start),先從線程1開始執行(這里不理解的話,可以看另外一篇博客:<從0到1寫RT-Thread內核——線程定義及切換的實現>),如果某個因為線程調用了rt_thread_delay函數而被阻塞了的話就運行另外的線程(此時線程阻塞剩余時長remaining_tick在SysTick_Handler中不斷遞減),如果非空閑線程都被阻塞了才運行空閑線程,如果某個線程的remaining_tick遞減到為0了,則又繼續運行該線程。
/************************************************************************* @brief main函數* @param 無* @retval 無** @attention*********************************************************************** */
int main(void)
{ /* 硬件初始化 *//* 將硬件相關的初始化放在這里,如果是軟件仿真則沒有相關初始化代碼 *//* 關中斷,在程序開始的時候把中斷關閉是一個好習慣,等系統初始化完畢,線程創建完畢,啟動系統 * 調度的時候會重新打開中斷(在rt_hw_context_switch_to函數中會再開啟中斷并設置中斷標位)。* 如果一開始不關閉中斷,接下來SysTick初始化完成,然后再初始化系統和創建線程,如果系統初始化* 和線程創建的時間大于SysTick中斷周期的話,那么就會出現系統或者線程還沒準備好的情況下就先執* 行了SysTick中斷服務函數,在該函數中進行了系統調度,顯示這是不合理的。*/rt_hw_interrupt_disable();/* 初始化SysTick,調用固件庫函數SysTick_Config來實現,* 配置中斷周期為10ms(為100),中斷優先級為最低*/SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND );/* 調度器初始化 */rt_system_scheduler_init();/* 初始化空閑線程 */ rt_thread_idle_init(); /* 初始化線程1 */rt_thread_init( &rt_flag1_thread, /* 線程控制塊 */"rt_flag1_thread", /* 線程名字,字符串形式 */flag1_thread_entry, /* 線程入口地址 */RT_NULL, /* 線程形參 */&rt_flag1_thread_stack[0], /* 線程棧起始地址 */sizeof(rt_flag1_thread_stack) ); /* 線程棧大小,單位為字節 *//* 將線程插入到就緒列表 */rt_list_insert_before( &(rt_thread_priority_table[0]),&(rt_flag1_thread.tlist) );/* 初始化線程2 */rt_thread_init( &rt_flag2_thread, /* 線程控制塊 */"rt_flag2_thread", /* 線程名字,字符串形式 */flag2_thread_entry, /* 線程入口地址 */RT_NULL, /* 線程形參 */&rt_flag2_thread_stack[0], /* 線程棧起始地址 */sizeof(rt_flag2_thread_stack) ); /* 線程棧大小,單位為字節 *//* 將線程插入到就緒列表 */rt_list_insert_before( &(rt_thread_priority_table[1]),&(rt_flag2_thread.tlist) );/* 啟動系統調度器 */rt_system_scheduler_start();
}
?
/* 線程1 */
void flag1_thread_entry( void *p_arg )
{for( ;; ){
#if 0flag1 = 1;delay( 100 ); flag1 = 0;delay( 100 );/* 線程切換,這里是手動切換 */ rt_schedule();
#elseflag1 = 1;rt_thread_delay(2); flag1 = 0;rt_thread_delay(2);
#endif }
}/* 線程2 */
void flag2_thread_entry( void *p_arg )
{for( ;; ){
#if 0flag2 = 1;delay( 100 ); flag2 = 0;delay( 100 );/* 線程切換,這里是手動切換 */rt_schedule();
#elseflag2 = 1;rt_thread_delay(2); flag2 = 0;rt_thread_delay(2);
#endif }
}
程序運行結果如下,其實這種阻塞延時的原理和我們在單片機裸機中采用的"前后臺輪詢"是非常相似的。
最后聲明一下,我這里只是對學習的知識點進行總結,本文章的大多數知識來自于野火公司出版的《RT-Thread 內核實現與應用開發實戰—基于STM32》,這本書非常不錯,有志學習RT-Thread物聯網操作系統的人可以考慮一下。