FreeRTOS——任務的定義與切換

本節屬於操作系統中基礎中的基礎,包括任務的創建與切換。以兩個任務爲例,在多任務系統中,兩個變量波形完全一樣,就好像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》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章