在裸機運行中,我們是使用軟件延時來實現延時的功能(delay()),即是讓CPU空等來達到延時的目的。使用RTOS的很大優勢就是榨乾CPU性能,永遠不讓他閒着,任務需要延時也就不需要讓CPU空等來實現延時的效果。
RTOS中的延時叫做阻塞延時,即任務需要延時的時候,任務會放棄CPU的使用權,CPU可以去幹其他的事情,當任務延時時間到,重新獲取CPU使用權,任務繼續運行。這樣就可以充分利用CPU資源了。
當任務進入阻塞狀態,如果沒有其他任務可以運行,RTOS都會爲CPU創建一個空閒任務,這個時候CPU就去運行空閒任務。在FreeRTOS中,空閒任務是系統在【啓動調度器】的時候創建的優先級最低的任務,空閒任務主要做一些系統內存清理的任務。
一、實現空閒任務
目前我們在創建任務時使用的棧和TCB都是使用的靜態內存,即預先定義好的內存,空閒任務也不例外。
1、定義空閒任務的棧
空閒任務的棧在main.c 中定義:
/* 定義空閒任務的棧 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) (2)
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; (1)
(1):空閒任務的棧是一個定義好的數組,大小由FreeRTOSConfig.h 中
定義的宏 configMINIMAL_STACK_SIZE 控制,大小爲128,單位是字。
2、定義空閒任務的任務控制塊
空閒任務的任務控制塊在main.c中定義:
/* 定義空閒任務的任務控制塊 */
TCB_t IdleTaskTCB;
3、定義空閒任務的任務控制塊
空閒任務在調度器啓動函數vTaskStartScheduler()中創建:
extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize );
void vTaskStartScheduler( void )
{
/*=======================創建空閒任務 start=======================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用於指向空閒任務控制塊 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用於空閒任務棧起始地址 */
uint32_t ulIdleTaskStackSize;
/* 獲取空閒任務的內存:任務棧和任務 TCB */ (1)
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
/* 創建空閒任務 */ (2)
xIdleTaskHandle =
xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任務入口 */
(char *)"IDLE", /* 任務名稱,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任務棧大小,單位爲字 */
(void *) NULL, /* 任務形參 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任務棧起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任務控制塊 */
/* 將任務添加到就緒列表開頭 */ (3)
vListInsertEnd( &( pxReadyTasksLists[0] ),&( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*==========================創建空閒任務 end=====================*/
/* 手動指定第一個運行的任務 */
pxCurrentTCB = &Task1TCB;
/* 啓動調度器 */
if ( xPortStartScheduler() != pdFALSE )
{
/* 調度器啓動成功,則不會返回,即不會來到這裏 */
}
}
(1):獲 取 空 閒 任 務 的 內 存 , 即 將 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 這兩個接下來要作爲形參傳到xTaskCreateStatic()函數的指針分別指向空閒任務的 TCB 和棧的起始地址,這個操作由函數 vApplicationGetIdleTaskMemory()來實現:
vApplicationGetIdleTaskMemory():
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
(2):調用 xTaskCreateStatic()函數創建空閒任務。
(3):將空閒任務插入到就緒列表的開頭。
二、實現阻塞延時
1、vTaskDelay ()函數
阻塞延時的阻塞是指任務調用該延時函數後,任務會被剝離CPU使用權,然後進入阻塞狀態,直到延時結束,任務重新獲取CPU使用權纔可以繼續運行。在任務阻塞這段時間,CPU可以去執行其他的任務,如果其他任務也處於阻塞狀態,CPU就會執行空閒任務。
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 獲取當前任務的 TCB */
pxTCB = pxCurrentTCB; (1)
/* 設置延時時間 */
pxTCB->xTicksToDelay = xTicksToDelay; (2)
/* 任務切換 */
taskYIELD(); (3)
}
(1):獲取當前任務控制塊。
(2):xTicksToDelay是任務控制塊的一個成員,用於記錄需要延時的時間,單位爲SysTick的中斷週期。
xTicksToDelay定義:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 棧頂 */
ListItem_t xStateListItem; /* 任務節點 */
StackType_t *pxStack; /* 任務棧起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任務名稱,字符串形式 */
TickType_t xTicksToDelay; /* 用於延時 */
} tskTCB;
(3):任務切換。調用taskYIELD()會產生PendSV中斷,在PendSV中斷服務函數終會調用任務切換函數vTaskSwitchContext(),該任務的作用是尋找最高優先級的就緒任務,然後更新pxCurrentTCB.因爲這一章比上一章多一個空閒任務,需要讓pxCurrentTCB 在這三個任務中切換, 算法需要改變,vTaskSwitchContext()函數修改如下:
void vTaskSwitchContext( void )
{
/* 如果當前任務是空閒任務,那麼就去嘗試執行任務 1 或者任務 2,
看看他們的延時時間是否結束,如果任務的延時時間均沒有到期,
那就返回繼續執行空閒任務 */
if ( pxCurrentTCB == &IdleTaskTCB ) (1)
{
if (Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if (Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return; /* 任務延時均沒有到期則返回,繼續執行空閒任務 */
}
}
else /* 當前任務不是空閒任務則會執行到這裏 */ (2)
{
/*如果當前任務是任務 1 或者任務 2 的話,檢查下另外一個任務,
如果另外的任務不在延時中,就切換到該任務
否則,判斷下當前任務是否應該進入延時狀態,
如果是的話,就切換到空閒任務。否則就不進行任何切換 */
if (pxCurrentTCB == &Task1TCB)
{
if (Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else if (pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不進行切換,因爲兩個任務都處於延時中 */
}
}
else if (pxCurrentTCB == &Task2TCB)
{
if (Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if (pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不進行切換,因爲兩個任務都處於延時中 */
}
}
}
}
三、SysTick 中斷服務函數
1、SysTick 中斷服務函數
在任務上下文切換函數 vTaskSwitchContext ()中,會判斷每個任務的任務控制塊中的延時成員xTicksToDelay的值是否爲0,如果爲0就要將對應的任務就緒,如果不爲0就繼續延時。如果一個任務延時,一開始xTicksToDelay不爲0,當xTicksToDelay變爲0後表示延時結束。在FreeRTOS中,操作系統中的最小時間單位是SysTick的中斷週期,我們稱之爲一個tick ,xTicksToDelay就是以tick爲週期遞減的。
SysTick 中斷服務函數:
void xPortSysTickHandler( void )
{
/* 進入臨界段,關中斷 */
vPortRaiseBASEPRI();
/* 更新系統時基 */
xTaskIncrementTick();
/* 退出臨界中斷,開中斷 */
vPortClearBASEPRIFromISR();
}
xTaskIncrementTick()函數
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系統時基計數器 xTickCount, xTickCount 是一個在 port.c 中定義的全局變量 */(1)
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 掃描就緒列表中所有任務的 xTicksToDelay,如果不爲 0,則減 1 */(2)
for (i=0; i<configMAX_PRIORITIES; i++)
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
if (pxTCB->xTicksToDelay > 0)
{
pxTCB->xTicksToDelay --;
}
}
/* 執行一次任務切換 */(3)
portYIELD();
}
2、SysTick 初始化函數
SysTick 的中斷服務函數要想被順利執行,則 SysTick 必須先初始化SysTick 初始化函數在 port.c 中定義。
vPortSetupTimerInterrupt()函數:
/* SysTick 控制寄存器 */ (1)
#define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))
/* SysTick 重裝載寄存器寄存器 */
#define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))
/* SysTick 時鐘源選擇 */
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
/* 確保 SysTick 的時鐘與內核時鐘一致 */
#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
#else
#define portNVIC_SYSTICK_CLK_BIT ( 0 )
#endif
#define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
void vPortSetupTimerInterrupt( void ) (2)
{
/* 設置重裝載寄存器的值 */ (2)-①
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
/* 設置系統定時器的時鐘等於內核時鐘 (2)-②
使能 SysTick 定時器中斷
使能 SysTick 定時器 */
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
portNVIC_SYSTICK_INT_BIT |
portNVIC_SYSTICK_ENABLE_BIT );
}
SysTick 初 始 化 函 數 vPortSetupTimerInterrupt() , 在
xPortStartScheduler()中被調用。
BaseType_t xPortStartScheduler( void )
{
/* 配置 PendSV 和 SysTick 的中斷優先級爲最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化 SysTick */
vPortSetupTimerInterrupt();
/* 啓動第一個任務,不再返回 */
prvStartFirstTask();
/* 不應該運行到這裏 */
return 0;
}
(2)-①:設置重裝載寄存器的值,決定SysTick 的中斷週期。
configCPU_CLOCK_HZ 與 configTICK_RATE_HZ 宏定義:
#define configCPU_CLOCK_HZ (( unsigned long ) 25000000) (1)
#define configTICK_RATE_HZ (( TickType_t ) 100) (2)
(1):系統時鐘大小,目前是軟件仿真,需要配置成與system_ARMCM3.c文件中的 SYSTEM_CLOCK的一樣, 即等於 25M。如果有具體的硬件,則配置成與硬件系統時鐘一樣。
(2):SysTick 每秒中斷多少次,目前配置爲 100,即每 10ms 中斷一次。
(2)-②:設置系統定時器的時鐘等於內核時鐘,使能 SysTick 定時器中斷,使能 SysTick 定時器。
參考:[野火®]《FreeRTOS 內核實現與應用開發實戰—基於STM32》