本節屬於操作系統中基礎中的基礎,包括任務的創建與切換。以兩個任務爲例,在多任務系統中,兩個變量波形完全一樣,就好像CPU在同時做兩件事,這纔是多任務的意義。
一、什麼是任務
裸機系統中,系統的主體是main函數裏順序執行的無限循環,這個無限循環裏面CPU按照順序完成各種事情。在多任務系統中,我們可以根據功能的不同,把整個系統分割爲一個個獨立的且無法返回的函數,這個函數我們就稱爲任務。如下:
void task_entry (void *parg)
{
/* 任務主體,無限循環且不能返回 */
for (;;) {
/* 任務主體代碼 */
}
}
二、創建任務
1、定義任務棧
在裸機系統中,局部變量、全局變量和函數返回地址統統放在一個叫棧的地方,棧是單片機裏一段連續的內存空間,棧的大小一般在啓動文件或鏈接腳本里指定,最後由C庫函數_main進行初始化。
在多任務系統中,每個任務都是獨立的,互不干擾,所以要爲每個任務都分配棧空間,這個棧空間通常是預先定義好的一個全局數組,也可以是動態分配的一段內存空間,但他們都存在與RAM中。
首先定義任務棧:
#define TASK1_STACK_SIZE 128 (2)
StackType_t Task1Stack[TASK1_STACK_SIZE]; (1)
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
(1)任務棧是一個預先定義好的全局數組,數據類型爲StackType_t ,大小爲TASK1_STACK_SIZE,默認128,單位字,也是FreeRTOS推薦的最小任務棧。
FreeRTOS中,凡是涉及到的的數據類型,都會用typedef重新取名。這些經過重定義的數據類型放在 portmacro.h頭文件中。
#ifndef PORTMACRO_H
#define PORTMACRO_H
/* 包含標準庫頭文件 */
#include "stdint.h"
#include "stddef.h"
/* 數據類型重定義 */
#define portCHAR char
#define portFLOAT float
#define portDOUBLE double
#define portLONG long
#define portSHORT short
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
#endif /* PORTMACRO_H */
2、定義任務函數
/* 軟件延時 */
void delay (uint32_t count)
{
for (; count!=0; count--);
}
/* 任務 1 */
void Task1_Entry( void *p_arg ) (1)
{
for ( ;; )
{
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
}
}
/* 任務 2 */
void Task2_Entry( void *p_arg ) (2)
{
for ( ;; )
{
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
}
}
以上就是兩個獨立、無限循環且不能返回的任務函數。
3、定義任務控制塊
多任務系統中,任務的執行由系統調度。系統爲了順利調度任務,爲每個任務額外定義了一個任務控制塊,這個任務控制塊就相當於任務的身份證,包含任務的所有信息,比如任務的棧指針、任務名稱、任務的形參等。有了任務控制塊後,系統對任務的全部操作都可以通過這個任務塊實現。以下爲一個任務控制塊的聲明:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 棧頂指針 */(1)
ListItem_t xStateListItem; /* 任務節點 */(2)
StackType_t *pxStack; /* 任務棧起始地址 */ (3)
char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任務名稱,字符串形式 */(4)
} tskTCB;
typedef tskTCB TCB_t; (5)
(2)作爲內置在TCB控制塊中的鏈表節點,通過這個節點,可以將這個任務控制塊掛接到各種鏈表中。
在main.c中定義兩個任務控制塊:
/* 定義任務控制塊 */
TCB_t Task1TCB;
TCB_t Task2TCB;
4、定義任務創建函數
任務的棧、任務的函數實體、任務的控制塊最終需要聯繫起來才能由系統進行統一調度。這個聯繫工作就由任務創建函數xTaskCreateStatic()來實現。
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, (2)
const char * const pcName, (3)
const uint32_t ulStackDepth, (4)
void * const pvParameters, (5)
StackType_t * const puxStackBuffer, (6)
TCB_t * const pxTaskBuffer ) (7)
{
TCB_t *pxNewTCB;
TaskHandle_t xReturn; (8)
if ( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 創建新的任務 */ (9)
prvInitialiseNewTask( pxTaskCode, /* 任務入口 */
pcName, /* 任務名稱,字符串形式 */
ulStackDepth, /* 任務棧大小,單位爲字 */
pvParameters, /* 任務形參 */
&xReturn, /* 任務句柄 */
pxNewTCB); /* 任務棧起始地址 */
}
else
{
xReturn = NULL;
}
/* 返回任務句柄,如果任務創建成功,此時 xReturn 應該指向任務控制塊 */
return xReturn; (10)
}
FreeRTOS中,任務的創建有兩種方法,一是動態創建,另一種是靜態創建。動態創建時,任務控制塊和棧的內存是動態分配的,任務刪除時,內存可以釋放。而靜態創建時,任務控制塊和棧的內存是事先定義好的,是靜態內存,任務刪除時,內存不能釋放。
(2):任務入口,即任務的函數名稱。TaskFunction_t 是在 projdefs.h中重定義的一個數據類型,實際就是空指針,如下:
#ifndef PROJDEFS_H
#define PROJDEFS_H
typedef void (*TaskFunction_t)( void * );
#define pdFALSE ( ( BaseType_t ) 0 )
#define pdTRUE ( ( BaseType_t ) 1 )
#define pdPASS ( pdTRUE )
#define pdFAIL ( pdFALSE )
#endif /* PROJDEFS_H */
(9):調用 prvInitialiseNewTask()函數,創建新任務,該函數在 task.c 實現。prvInitialiseNewTask()函數如下:
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /*任務入口,任務函數名*/(1)
const char * const pcName, /*任務名稱,字符串形式*/(2)
const uint32_t ulStackDepth, /*任務棧大小,單位爲字*/(3)
void * const pvParameters, /*任務形參,沒有時可以爲NULL*/(4)
TaskHandle_t * const pxCreatedTask, /*任務句柄*/(5)
TCB_t *pxNewTCB ) /*任務控制塊指針*/(6)
三、實現就緒列表
任務創建好後,我們需要把任務添加到就緒列表中,表示任務已經就緒,系統隨時可以調度。
1、定義就緒列表
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
任務就緒列表是一個List_t類型的數組,數組大小有決定最大任務優先級宏configMAX_PRIORITIES決定,最大支持256個優先級,這裏默認定義爲5。數組的下表對應了任務的優先級,同一優先級的任務統一插入到同一就緒列表的鏈表中。一個空的就緒列表如下:
2、就緒列表初始化
就緒列表初始化工作在函數prvInitialiseTaskLists()裏面實現。
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for ( uxPriority = ( UBaseType_t ) 0U;
uxPriority < ( UBaseType_t ) configMAX_PRIORITIES;
uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
初始化後如下圖:
2、將任務插入到就緒列表中
我們通過任務控制塊中的xStateListItem
這個節點插入到就緒列表中來實現的。如果把就緒列表比作是晾衣架,任務是衣服,那xStateListItem
就是晾衣架上面的鉤子,每個任務都自帶晾衣架鉤子,就是爲了把自己掛在各種不同的鏈表中。
任務創建好後,緊跟着就要實現任務插入到就緒列表中。
/* 初始化與任務相關的列表,如就緒列表 */
prvInitialiseTaskLists();
Task1_Handle = /* 任務句柄 */
xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任務入口 */
(char *)"Task1", /* 任務名稱,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任務棧大小,單位爲字 */
(void *) NULL, /* 任務形參 */
(StackType_t *)Task1Stack, /* 任務棧起始地址 */
(TCB_t *)&Task1TCB ); /* 任務控制塊 */
/* 將任務添加到就緒列表 */
vListInsertEnd( &( pxReadyTasksLists[1] ),&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle = /* 任務句柄 */
xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任務入口 */
(char *)"Task2", /* 任務名稱,字符串形式 */
(uint32_t)TASK2_STACK_SIZE , /* 任務棧大小,單位爲字 */
(void *) NULL, /* 任務形參 */
(StackType_t *)Task2Stack, /* 任務棧起始地址 */
(TCB_t *)&Task2TCB ); /* 任務控制塊 */
/* 將任務添加到就緒列表 */
vListInsertEnd( &( pxReadyTasksLists[2] ),&( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
在這裏,我們把Task1任務插入到就緒列表下標爲1的鏈表中,把Task2任務插入到就緒列表下標爲2的鏈表中,如下:
四、實現調度器
調度器是操作系統的核心,其主要功能就是實現任務的切換,即從就緒列表裏面找到優先級最高的任務,然後去執行該任務。
1、啓動調度器
由函數vTaskStartScheduler()
完成
void vTaskStartScheduler( void )
{
/* 手動指定第一個運行的任務 */
pxCurrentTCB = &Task1TCB; (1)
/* 啓動調度器 */
if ( xPortStartScheduler() != pdFALSE )
{
/* 調度器啓動成功,則不會返回,即不會來到這裏 */ (2)
}
}
(1):pxCurrentTCB 是一個在 task.c 定義的全局指針,用於指向正在運行或即將運行的任務的任務控制指針。
(2):調用函數 xPortStartScheduler()啓動調度器, 調度器啓動成功, 則不會返回。
xPortStartScheduler()函數
/*
* 參考資料《STM32F10xxx Cortex-M3 programming manual》 4.4.3,百度搜索“PM0056”即可找到這個文檔
* 在 Cortex-M 中,內核外設 SCB 中 SHPR3 寄存器用於設置 SysTick 和 PendSV 的異常優先級
* System handler priority register 3 (SCB_SHPR3) SCB_SHPR3: 0xE000 ED20
* Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
* Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV
*/
#define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20))
#define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL)
#define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
BaseType_t xPortStartScheduler( void )
{
/* 配置 PendSV 和 SysTick 的中斷優先級爲最低 */ (1)
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 啓動第一個任務,不再返回 */
prvStartFirstTask(); (2)
/* 不應該運行到這裏 */
return 0;
}
(1):配置 PendSV 和 SysTick 的中斷優先級爲最低。 SysTick 和PendSV 都會涉及到系統調度,系統調度的優先級要低於系統的其它硬件中斷優先級, 即優先響應系統中的外部硬件中斷, 所以 SysTick 和 PendSV 的中斷優先級配置爲最低。
(2)調用函數 prvStartFirstTask()啓動第一個任務, 啓動成功後, 則不再返回, 該函數由彙編編寫,此處不貼出。
2、任務切換
任務切換就是在就緒列表中尋找到優先級最高的就緒任務,然後去執行該任務。
2.1 TaskYIELD():
/* 在 task.h 中定義 */
#define taskYIELD() portYIELD()
/* 在 portmacro.h 中定義 */
/* 中斷控制狀態寄存器: 0xe000ed04
* Bit 28 PENDSVSET: PendSV 懸起位
*/
#define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *) 0xe000ed04))
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
#define portSY_FULL_READ_WRITE ( 15 )
#define portYIELD() \
{ \
/* 觸發 PendSV,產生上下文切換 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (1) \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
(1):portYIELD 的實現實際就是將 PendSV 的懸起位置 1,當
沒有其它中斷運行的時候響應 PendSV 中斷,去執行我們寫好的 PendSV 中斷服務函數,在裏面實現任務切換。
2.2 xPortPendSVHandler()函數是PendSV 中斷服務函數是真正實現任務切換的地方。
參考:[野火®]《FreeRTOS 內核實現與應用開發實戰—基於STM32》