FreeRTOS簡單內核實現3 任務管理

来源:https://www.cnblogs.com/lc-guo/p/18249061
-Advertisement-
Play Games

0、思考與回答 0.1、思考一 對於 Cortex-M4 內核的 MCU 在發生異常/中斷時,哪些寄存器會自動入棧,哪些需要手動入棧? 會自動入棧的寄存器如下 R0 - R3:通用寄存器 R12:通用寄存器 LR (Link Register):鏈接寄存器,保存返回地址 PC (Program Co ...


0、思考與回答

0.1、思考一

對於 Cortex-M4 內核的 MCU 在發生異常/中斷時,哪些寄存器會自動入棧,哪些需要手動入棧?

會自動入棧的寄存器如下

  1. R0 - R3:通用寄存器
  2. R12:通用寄存器
  3. LR (Link Register):鏈接寄存器,保存返回地址
  4. PC (Program Counter):程式計數器,保存當前執行指令的地址
  5. xPSR (Program Status Register):程式狀態寄存器

需要手動入棧的寄存器如下

  1. R4 - R11:其他通用寄存器

0.2、思考二

這些入棧的寄存器是怎麼找到要入棧的地址的呢?

依靠堆棧指針來確定入棧的地址,Cortex-M4 的堆棧指針寄存器 SP 在同一物理位置上有 MSP 和 PSP 兩個堆棧指針,預設情況下會使用主堆棧指針 MSP

0.3、思考三

自動入棧的寄存器什麼時候入棧,什麼時候出棧恢複原來的處理器狀態?

進入異常處理時會發生自動入棧,當異常處理完成並執行異常返回指令(bx r14)時,處理器會自動從堆棧中彈出這些寄存器的值

1、任務控制塊

通常使用名為任務控制塊(TCB)的結構體來對每個任務進行管理,該結構體中包含了任務管理所需要的一些重要成員,其中棧頂指針 pxTopOfStack 作為結構體的第一個成員,其地址也即任務控制塊的地址,具體如下所示

/* task.h */
// 任務控制塊
typedef struct tskTaskControlBlock
{
    volatile StackType_t  *pxTopOfStack;                        // 棧頂
    ListItem_t            xStateListItem;                       // 任務節點
    StackType_t           *pxStack;                             // 任務棧起始地址
    char                  pcTaskName[configMAX_TASK_NAME_LEN];  // 任務名稱
}tskTCB;
typedef tskTCB TCB_t;

configMAX_TASK_NAME_LEN 是一個巨集,用於設置任務名稱長度,具體定義如下

/* FreeRTOSConfig.h */
// 任務名稱字元串長度
#define configMAX_TASK_NAME_LEN                 24

2、創建任務

2.1、xTaskCreateStatic( )

靜態創建任務時需要指定任務棧(地址和大小)、任務函數、任務控制塊等參數,該函數就是負責將這些分散的變數聯繫在一起,方便後續對任務進行管理,但是其並不是真正的創建任務的函數,具體如下所示

/* task.c */
// 靜態創建任務函數
#if (configSUPPORT_STATIC_ALLOCATION == 1)
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,     // 任務函數
                            const char* const pcName,         // 任務名稱
                            const uint32_t ulStackDepth,      // 任務棧深度
                            void* const pvParameters,         // 任務參數
                            StackType_t* const puxTaskBuffer, // 任務棧起始指針
                            TCB_t* const pxTaskBuffer)        // 任務棧控制指針
{
	TCB_t* pxNewTCB;
	TaskHandle_t xReturn;
	// 任務棧控制指針和任務棧起始指針不為空
	if((pxTaskBuffer != NULL) && (puxTaskBuffer != NULL))
	{
		pxNewTCB = (TCB_t*)pxTaskBuffer;
		// 將任務控制塊的 pxStack 指針指向任務棧起始地址
		pxNewTCB->pxStack = (StackType_t*)puxTaskBuffer;
		
		// 真正的創建任務函數
		prvInitialiseNewTask(pxTaskCode,
							 pcName,
							 ulStackDepth,
							 pvParameters,
							 &xReturn,
							 pxNewTCB);
	}
	else
	{
		xReturn = NULL;
	}
	// 任務創建成功後應該返回任務句柄,否則返回 NULL
	return xReturn;
}
#endif

/* task.h */
// 函數聲明
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,
                            const char* const pcName,
                            const uint32_t ulStackDepth,
                            void* const pvParameters,
                            StackType_t* const puxTaskBuffer,
                            TCB_t* const pxTaskBuffer);

configSUPPORT_STATIC_ALLOCATION 是一個巨集,用於配置和裁剪 RTOS 靜態創建任務的功能,具體定義如下所示

/* FreeRTOSConfig.h */
// 是否支持靜態方式創建任務
#define configSUPPORT_STATIC_ALLOCATION         1

TaskHandle_t 是一個 void * 類型的變數,用於表示任務句柄,TaskFunction_t 是一個函數指針,用於表示任務函數,具體定義如下所示

/* task.h */
// 任務句柄指針
typedef void* TaskHandle_t;
// 任務函數指針
typedef void (*TaskFunction_t)(void *);

2.2、prvInitialiseNewTask( )

函數 xTaskCreateStatic() 最終調用了真正的創建任務函數 prvInitialiseNewTask() ,該函數主要是對任務棧記憶體、任務控制塊成員等進行初始化,具體如下所示

/* task.c */
// 使用的外部函數聲明
extern StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, 
                                          TaskFunction_t pxCode, 
                                          void* pvParameters);
// 真正的創建任務函數																 
static void prvInitialiseNewTask(TaskFunction_t pxTaskCode,    // 任務函數
                            const char* const pcName,          // 任務名稱
                            const uint32_t ulStackDepth,       // 任務棧深度
                            void* const pvParameters,          // 任務參數
                            TaskHandle_t* const pxCreatedTask, // 任務句柄
                            TCB_t* pxNewTCB)                   // 任務棧控制指針
{
	StackType_t *pxTopOfStack;
	UBaseType_t x;
	
	// 棧頂指針,用於指向分配的任務棧空間的最高地址
	pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1);
	// 8 位元組對齊
	pxTopOfStack = (StackType_t*)(((uint32_t)pxTopOfStack) 
	                             & (~((uint32_t)0x0007)));
	// 保存任務名稱到TCB中
	for(x = (UBaseType_t)0;x < (UBaseType_t)configMAX_TASK_NAME_LEN;x++)
	{
		pxNewTCB->pcTaskName[x] = pcName[x];
		if(pcName[x] == 0x00)
			break;
	}
	pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN-1] = '\0';
	
	// 初始化鏈表項
	vListInitialiseItem(&(pxNewTCB->xStateListItem));
	
	// 設置該鏈表項的擁有者為 pxNewTCB
	listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
	
	// 初始化任務棧
	pxNewTCB->pxTopOfStack = 
	          pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
	
	if((void*)pxCreatedTask != NULL)
	{
	    *pxCreatedTask = (TaskHandle_t)pxNewTCB;
	}
}

2.3、pxPortInitialiseStack( )

初始化任務棧函數,記憶體具體被初始化為什麼樣可以閱讀 "2.4 任務記憶體詳解" 小節

/* port.c */
// 錯誤出口
static void prvTaskExitError(void)
{
    for(;;);
}
// 初始化棧記憶體
StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, 
								   TaskFunction_t pxCode, 
								   void* pvParameters)
{
    // 異常發生時,自動載入到CPU的內容
    pxTopOfStack --;
    *pxTopOfStack = portINITIAL_XPSR;
    pxTopOfStack --;
    *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;
    pxTopOfStack --;
    *pxTopOfStack = (StackType_t)prvTaskExitError;
	
    // r12、r3、r2 和 r1 預設初始化為 0
    pxTopOfStack -= 5;
    *pxTopOfStack = (StackType_t)pvParameters;
	
    // 異常發生時,手動載入到 CPU 的內容
    pxTopOfStack -= 8;
	
    // 返回棧頂指針,此時 pxTopOfStack 指向空閑棧
    return pxTopOfStack;
}
/* portMacro.h */
#define portINITIAL_XPSR            (0x01000000)
#define portSTART_ADDRESS_MASK      ((StackType_t) 0xFFFFFFFEUL)

2.4、任務記憶體詳解

使用靜態方式創建任務之前需要提前定義好任務句柄、任務棧空間、任務控制塊和任務函數,具體如下程式所示

// 任務句柄
TaskHandle_t Task_Handle;
// 任務棧大小
#define TASK_STACK_SIZE                    128
// 任務棧空間
StackType_t TaskStack[TASK_STACK_SIZE];
// 任務控制塊
TCB_t TaskTCB;
// 任務函數
void TaskFunction(void *parg)
{
	for(;;)
	{
	}
}

定義好這些變數之後都會在 RAM 中占用一定的空間,其中任務控制塊 TaskTCB 的地址和結構體第一個成員 pxTopOfStack 是同一個地址,假設這些變數在空間中的占用情況如下圖所示

將這些定義好的變數作為參數傳遞給靜態創建任務函數 xTaskCreateStatic() ,具體如下程式所示

// 靜態方式創建任務
Task_Handle = xTaskCreateStatic(TaskFunction,
							   "Task",
							   128,
							   TaskParameters,
							   TaskStack,
							   TaskTCB);

該函數執行完畢之後,最終這些變數在空間中的占用情況如下圖所示

為什麼要按照順序這麼存放?

因為在 ARM Cortex-M 處理器中,當異常發生時硬體會自動按照上圖的順序將寄存器推入當前的棧中,同時退出異常時也會按照其相反的順序從當前棧中取值載入到 MCU 寄存器中

為什麼任務棧頂 PSR 的值為 0x01000000 ?

PSR 是程式狀態寄存器,對於異常來說具體的寄存器為 EPSR ,其第 24 位為 Thumb 狀態位(T位),在 ARM Cortex-M 架構中,所有代碼都必須在 Thumb 模式下執行,寄存器定義可以在 Cortex-M4 Devices Generic User Guide 手冊中找到,具體如下圖所示

可以發現當執行 BX 指令退出異常時(後面啟動任務 / 任務調度會頻繁使用到異常退出指令),會將 T 位清零,而一旦清零並嘗試執行指令就會導致 MCU 故障或鎖定,因此必須在異常退出自動恢復 MCU 寄存器時將 T 位重新置 1

為什麼任務棧存放任務函數入口地址的內容為 pxCode & portSTART_ADDRESS_MASK?

簡單來說這步操作其實是為了記憶體對齊portSTART_ADDRESS_MASK 是一個巨集,pxCode & portSTART_ADDRESS_MASK 只是為了確保 pxCode 任務函數入口地址最低位置為 0

3、就緒鏈表

3.1、定義

為什麼要定義鏈表?

使用鏈表可以將各個獨立的任務鏈接起來,對於多任務時會方便任務管理

/* task.c */
// 就緒鏈表
List_t pxReadyTasksLists;

3.2、prvInitialiseTaskLists( )

鏈表創建後不能直接使用,需要調用 vListInitialise() 函數對鏈表進行初始化,由於後續會創建多個鏈表,因此將鏈表初始化操作包裝為一個函數,具體如下所示

/* task.c */
// 就緒列表初始化函數
void prvInitialiseTaskLists(void)
{
	vListInitialise(&pxReadyTasksLists);
}

/* task.h */
// 函數聲明
void prvInitialiseTaskLists(void);

4、任務調度器

任務調度流程如下所示

  1. 啟動調度器
    1. vTaskStartScheduler( )
    2. xPortStartScheduler( )
  2. 啟動第一個任務
    1. prvStartFirstTask( )
    2. vPortSVCHandler( )
  3. 產生任務調度
    1. taskYIELD( )
    2. xPortPendSVHandler( )
    3. vTaskSwitchContext( )

4.1、vTaskStartScheduler( )

任務創建好了怎麼執行呢?

這就是啟動調度器函數的工作,選擇一個任務然後啟動第一個任務的執行

怎麼選擇任務?

通過一個名為 pxCurrentTCBTCB_t * 類型的指針選擇要執行的任務,該指針始終指向需要運行的任務的任務控制塊

選擇哪一個?

在創建多個任務時,可以通過一些方法(比如根據任務優先順序)來選擇先執行的任務,但是這裡我們的 RTOS 內核還不支持優先順序,因此可以先手動指定要執行的第一個任務

/* task.c */
// 當前 TCB 指針
TCB_t volatile *pxCurrentTCB = NULL;
// 使用的外部函數聲明
extern BaseType_t xPortStartScheduler(void);
// 在 main.c 中定義的兩個任務聲明
extern TCB_t Task1TCB;
extern TCB_t Task2TCB;

// 啟動任務調度器
void vTaskStartScheduler(void)
{
	// 手動指定第一個運行的任務
	pxCurrentTCB = &Task1TCB;
	// 啟動調度器
	if(xPortStartScheduler() != pdFALSE)
	{
		// 調度器啟動成功則不會到這裡
	}
}

/* task.h */
// 函數聲明
void vTaskStartScheduler(void);

4.2、xPortStartScheduler( )

vTaskStartScheduler() 函數選擇了要執行的任務,啟動第一個任務執行的重任交給了 4.2 ~ 4.4 小節的三個函數,xPortStartScheduler() 函數主要設置 PendSV 和 SysTick 的中斷優先順序為最低,然後調用了 prvStartFirstTask() 函數

/* port.c */
// 啟動調度器
BaseType_t xPortStartScheduler(void)
{
	// 設置 PendSV 和 SysTick 中斷優先順序為最低
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
	
	// 初始化滴答定時器
	
	// 啟動第一個任務,不再返回
	prvStartFirstTask();
	
	// 正常不會運行到這裡
	return 0;
}

為什麼要設置 PendSV 和 SysTick 中斷優先順序最低?

在 RTOS 中,PendSV 和 SysTick 中斷服務函數均與任務調度有關,而任務調度優先順序需要小於外部硬體中斷的優先順序,所以要將其設置為最低優先順序

怎麼設置 PendSV 和 SysTick 中斷優先順序為最低?

這裡是直接操作寄存器的方法來設置 Cortex-M4 的 PendSV 和 SysTick 中斷優先順序的,可以在 Cortex-M4 Devices Generic User Guide 手冊中找到有關 SHPR3 寄存器的地址與其每一位的含義,具體如下圖所示

根據上圖應該很好理解程式是怎麼將 PendSV 和 SysTick 優先順序設置為 15 的(NVIC 為 4 位搶占優先順序,最低優先順序就是 15)

/* portMacro.h */
#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)
/* FreeRTOSConfig.h */
// 設置內核中斷優先順序(最低優先順序)
#define configKERNEL_INTERRUPT_PRIORITY         15

4.3、prvStartFirstTask( )

prvStartFirstTask 是使用彙編語言編寫的函數,我們要實現的 RTOS 內核中一共有三個函數是由彙編語言編寫的,除了這個外還有 4.4 和 4.5 兩個小節的函數,該函數實際上仍然沒有真正的啟動第一個任務,實質上是觸發了 SVC 中斷

Keil 版本

/* port.c */
// 啟動第一個任務,實際上是觸發 SVC 中斷
__asm void prvStartFirstTask(void)
{
	// 8 位元組對齊
    PRESERVE8
	
	// 在 cortex-M 中,0xE000ED08 是 SCB_VTOR 這個寄存器的地址,
	// 裡面放的是向量表的起始地址 ,其第一項為 __initial_sp,也即 msp 的初始地址 
    ldr r0,=0xE000ED08
    ldr r0,[r0]
    ldr r0,[r0]
	// 設置主棧指針 msp 的值
    msr msp,r0
    // 啟用全局中斷
    cpsie i
    cpsie f
    dsb
    isb
    // 調用 SVC 啟動第一個任務
    svc 0
    nop
    nop
}

// 函數聲明
void prvStartFirstTask(void);

CLion 版本

void prvStartFirstTask(void)  
{  
    __asm volatile  
    (
		".align 8             \n"  
		"ldr r0,=0xE000ED08   \n"  
		"ldr r0,[r0]          \n"  
		"ldr r0,[r0]          \n"  
		"msr msp,r0           \n"  
		"cpsie i              \n"  
		"cpsie f              \n"  
		"dsb                  \n"  
		"isb                  \n"  
		"svc 0                \n"  
		"nop                  \n"  
		"nop                  \n"  
    );
}

4.4、vPortSVCHandler( )

SVC 中斷服務函數,真正執行第一個任務的函數

Keil 版本

/* port.c */
// SVC 中斷服務函數
__asm void vPortSVCHandler(void)
{
    extern pxCurrentTCB;
	// 8 位元組對齊
    PRESERVE8
    // 將 pxCurrentTCB 指針的地址載入到寄存器 r3
    ldr r3,=pxCurrentTCB
    // 將 pxCurrentTCB 指針指向的地址(存儲 TCB 的地址)載入到寄存器 r1
    ldr r1,[r3]
    // 將 pxCurrentTCB 指針指向的地址里的值(當前運行的 TCB 結構體)載入到寄存器 r0
    // 也即寄存器 r0 中存儲了當前運行的任務的棧頂指針地址
    ldr r0,[r1]
    // 以寄存器 r0 為基地址,將任務棧中存儲的值載入到寄存器 r4 ~ r11 中
    // 同時寄存器 r0 的值會自動更新
    ldmia r0!,{r4-r11}
    // 將更新後的寄存器 r0 的值載入給 psp
    msr psp,r0
    isb
    // 開中斷
    mov r0,#0
    msr basepri,r0
    // 指示處理器在異常返回時使用 psp 作為堆棧指針
    orr r14,#0xd
	// 異常返回,自動載入剩下的 xPSR、PC、R14、R12 和 R3 ~ R0 寄存器
    bx r14
}

CLion 版本

void vPortSVCHandler(void)  
{  
    extern uint32_t pxCurrentTCB;  
  
    __asm volatile  
    (  
		".align 8                    \n"  
		"ldr r3, =pxCurrentTCB       \n"
		"ldr r1, [r3]                \n"
		"ldr r0, [r1]                \n"
		"ldmia r0!, {r4-r11}         \n"
		"msr psp, r0                 \n"
		"isb                         \n"
		"mov r0, #0                  \n"
		"msr basepri, r0             \n"
		"orr lr, lr, #0x0d           \n"     // 筆者未驗證,可能有誤,請註意分辨
		"bx lr                       \n"
		:
		: 
		: "r0", "r1", "r3", "memory"
    );  
}

熟悉 STM32 的讀者應該瞭解,NVIC 中所有中斷服務函數都有一個固定的函數名,該函數名可以在 startup_stm32f407xx.s 文件中找到,這裡 vPortSVCHandler 其實是 SVC_Handler 的別名,具體巨集定義如下所示

/* portMacro.h */
#define vPortSVCHandler             SVC_Handler

註意:由於我們重新實現了 SVC 中斷服務函數,因此在 stm32f4xx_it.c 中自動生成的 SVC_Handler 函數需要註釋或者直接刪除

4.5、xPortPendSVHandler( )

PendSV 中斷服務函數,實現任務切換的函數

Keil 版本

/* port.c */
// PendSV 中斷服務函數,真正實現任務切換的函數
__asm void xPortPendSVHandler(void)
{
	// 只要觸發 PendSV 中斷,寄存器 xPSR、PC、R14、R12 和 R3 ~ R0,
	// 這 8 個寄存器的值會自動從 psp 指針指向的地址開始載入到任務棧中
	// 並且 psp 指針也會自動更新
    extern pxCurrentTCB;
    extern vTaskSwitchContext;
	
    PRESERVE8
	// 將 psp 的值存儲到寄存器 r0 中 
    mrs r0,psp
    isb
	// 將 pxCurrentTCB 的地址存儲到寄存器 r3 中
    ldr r3,=pxCurrentTCB
    // 將寄存器 r3 存儲的地址里的內容存儲到寄存器 r2 中
    // 此時寄存器 r2 中存儲了當前執行任務的 TCB 地址
    ldr r2,[r3]
	// 以寄存器 r0 為基地址(r0 中現在存儲了 psp 指針的值)
	// 將寄存器 r11 ~ r4 中存儲的值存儲到任務棧
	// 並且 r0 中存儲的地址指針也會自動更新
    stmdb r0!,{r4-r11}
    // 將寄存器 r0 中的內容存儲到上一個執行任務的 TCB 地址
    // 也即上一個任務的棧頂指針 pxTopOfStack 指向寄存器 r0 現在存儲的地址
    str r0,[r2]
	// 上文保存完成
	// 開始載入下文
	// 將寄存器 r3 和 r14 壓入棧
    stmdb sp!,{r3,r14}
    // 屏蔽 優先順序值 高於或者等於 11 的中斷
    mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri,r0
    dsb
    isb
    // 跳轉到 vTaskSwitchContext 函數,得到下一個要執行任務的指針 pxCurrentTCB
    bl vTaskSwitchContext
    // 開中斷
    mov r0,#0
    msr basepri,r0
    // 恢復寄存器 r3 和 r14
    ldmia sp!,{r3,r14}
	
	// 將 pxCurrentTCB 載入到寄存器 r1 
    ldr r1,[r3]
    // 將下一個要執行任務的棧頂指針載入到寄存器 r0 
    ldr r0,[r1]
    // 以寄存器 r0 為基地址(r0 中現在存儲了任務的棧頂指針)
    // 將任務棧中存儲的值載入到寄存器 r4 ~ r11 中
    // 同時 r0 中的值會自動更新
    ldmia r0!,{r4-r11}
    // 將更新後的 r0 的值載入到 psp
    msr psp,r0
    isb
    // 異常退出,自動載入剩下的 xPSR、PC、R14、R12 和 R3 ~ R0 寄存器
    bx r14
    nop
}

CLion 版本

void xPortPendSVHandler(void)  
{  
    extern uint32_t pxCurrentTCB;  
    extern void vTaskSwitchContext(void);  
  
    uint32_t temp;  
  
    __asm volatile  
    (  
		".align 8                     \n"  
		"mrs r0, psp                  \n"  
		"isb                          \n"  
		"ldr r3, =pxCurrentTCB        \n"  
		"ldr r2, [r3]                 \n"  
		"stmdb r0!, {r4-r11}          \n"  
		"str r0, [r2]                 \n"  
		"stmdb sp!, {r3, lr}          \n"  
		"mov r0, %[max_syscall_prio]  \n"  
		"msr basepri, r0              \n"  
		"dsb                          \n"  
		"isb                          \n"  
		"bl vTaskSwitchContext        \n"  
		"mov r0, #0                   \n"  
		"msr basepri, r0              \n"  
		"ldmia sp!, {r3, lr}          \n"  
		"ldr r1, [r3]                 \n"  
		"ldr r0, [r1]                 \n"  
		"ldmia r0!, {r4-r11}          \n"  
		"msr psp, r0                  \n"  
		"isb                          \n"  
		"bx lr                        \n"  
		"nop                          \n"  
		:  
		: [max_syscall_prio] "i" (configMAX_SYSCALL_INTERRUPT_PRIORITY)  
		: "r0", "r1", "r2", "r3", "memory"  
    );  
}

xPortPendSVHandlerPendSV_Handler 的別名,用於表示 PendSV 中斷服務函數入口地址

/* portMacro.h */
#define xPortPendSVHandler          PendSV_Handler

關於如何屏蔽中斷的請讀者閱讀後續 FreeRTOS簡單內核實現4 臨界段 文章中 ”思考三“ ,FreeRTOS 的中斷管理策略是將所有的搶占優先順序分為兩組,一組可以通過 RTOS 的臨界區進行屏蔽,另外一組不受 RTOS 的影響,這兩組的邊界則由 configMAX_SYSCALL_INTERRUPT_PRIORITY 巨集確定,如下所示

/* FreeRTOSConfig.h */
// 設置 RTOS 可管理的最大中斷優先順序
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    11<<4

註意:由於我們重新實現了 PendSV 中斷服務函數,因此在 stm32f4xx_it.c 中自動生成的 PendSV_Handler 函數需要註釋或者直接刪除

4.6、vTaskSwitchContext( )

任務調度函數中通常應該使用某一調度策略來選擇下一個要執行的任務(比如始終保證最高優先順序的任務得到執行),但這裡我們只簡單的進行兩個任務的交替,後面會對該函數完善,具體如下所示

/* task.c */
// 任務調度函數
void vTaskSwitchContext(void)
{
	// 兩個任務輪流切換
	if(pxCurrentTCB == &Task1TCB)
	{
		pxCurrentTCB = &Task2TCB;
	}
	else
	{
		pxCurrentTCB = &Task1TCB;
	}
}

/* task.h */
// 函數聲明
void vTaskSwitchContext(void);

4.7、taskYIELD( )

taskYIELD() 是主動產生任務調度的巨集函數,其實質上是觸發了 PendSV 中斷,然後在 PendSV 中斷服務函數中產生了任務調度,具體如下所示

/* task.h */
// 主動產生任務調度
#define taskYIELD() portYIELD()
/* portMacro.h */
#define portNVIC_INT_CTRL_REG       (*((volatile uint32_t*)0xE000ED04))
#define portNVIC_PENDSVSET_BIT      (1UL << 28UL)
// 觸發 PendSV,產生上下文切換
#define portYIELD() \
{ \
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
    __DSB(); \
    __ISB(); \
};

同樣這裡是直接操作 Cortex-M4 內核 ICSR 寄存器的 PENDSVSET 位置 1 來產生 PendSV 中斷的,關於該寄存器的相關內容仍然可以在 Cortex-M4 Devices Generic User Guide 手冊中找到,具體的原理如下圖所示

5、兩個重要問題

5.1、啟動第一個任務流程

通過上面的學習讀者應初步瞭解到啟動第一個任務的函數調用流程如下

  1. vTaskStartScheduler()
  2. -> xPortStartScheduler()
  3. -> prvStartFirstTask()
  4. -> vPortSVCHandler()

已知 prvStartFirstTask() 函數實質是觸發了 SVC 中斷,然後在 vPortSVCHandler() 這個 SVC 中斷服務函數中啟動了第一個任務,於是有如下問題

問題 1 :為什麼在觸發 SVC 中斷前進行了設置主堆棧指針 msp 值的操作?

問題 2 :vPortSVCHandler() 函數究竟是怎麼把第一個任務載入到 MCU 的寄存器中運行的?

5.1.1、問題 1 解答

這個問題的答案很簡單,就是因為之後的程式將不再使用主堆棧指針 msp,因此將其初始化為預設狀態,設置主棧指針的程式如下所示

ldr r0,=0xE000ED08
ldr r0,[r0]
ldr r0,[r0]
msr msp,r0

0xE000ED08 是 Cortex-M4 內核 VTOR (中斷向量偏移寄存器)的地址,這個寄存器是不是聽起來很熟悉?

讀者可以在 STM32 工程的啟動文件中找到其中斷向量偏移表,可以發現中斷向量偏移表中第一項為 __initial_sp ,也即主堆棧指針 msp 的初始值,以 startup_stm32f407xx.s 為例,具體如下圖所示

5.1.2、問題 2 解答

當一個任務創建好之後,應該滿足 &TaskTCB == &TaskTCB.pxTopOfStack ,並且他們都指向了任務棧中用於存放寄存器 r4 記憶體的地址,具體可以閱讀 “2.4、任務記憶體詳解” 小節內容

同時由於我們手動指定了 pxCurrentTCB 指針指向任務控制塊,因此經過下麵幾句程式後,寄存器 r0 中值實際上已經是任務棧中用於存放寄存器 r4 記憶體的地址

ldr r3,=pxCurrentTCB
ldr r1,[r3]
ldr r0,[r1]

然後經過下麵的程式,此時保存在任務棧中的 r4 ~ r11 的值就會載入到 MCU 的寄存器中,同時 r0 中的值也會自動更新,最終指向任務棧中用於存放寄存器 r0 記憶體的地址

ldmia r0!,{r4-r11}

接著將 r0 的值載入給 psp 指針,這時候 psp 指針指向了任務棧中用於存放寄存器 r0 記憶體的地址

msr psp,r0
isb

下一步在打開中斷後執行了很重要的一步程式

orr r14,#0xd

執行完這條語句後,SVC 中斷服務函數返回時就不再使用 msp 作為堆棧指針了,而是切換到了 psp 堆棧指針,這樣 MCU 會從 psp 堆棧指針處將可以自動載入的寄存器進行載入,同時 psp 指針也會更新,最終 psp 指針會指向任務棧的棧頂

值得提醒的是由於任務創建時已經提前在任務棧用於存放寄存器 PC 記憶體處存儲了任務函數的入口地址,因此第一個任務函數便會開始運行

5.2、任務調度原理

已知 RTOS 中發生任務調度其實質為觸發 PendSV 中斷,而 xPortPendSVHandler() 是 PendSV 中斷服務函數,因此任務調度時上文的保存和下文的載入都是在該函數中發生的,於是有如下問題

問題 1 :上文是如何保存的?

問題 2 :下文是如何載入到 MCU 寄存器中的?

5.2.1、問題 1 解答

首先需要知道的是啟動第一個任務後 psp 堆棧指針在哪裡?它現在正指向任務棧的棧頂

其次需要知道當發生任務調度產生 PendSV 中斷時,MCU 會從 psp 堆棧指針處自動保存那些可以自動保存的寄存器,並且 psp 也會跟著更新

於是當進入 PendSV 中斷服務函數時,psp 指針已經指向了任務棧中用於存放寄存器 r0 記憶體的地址,然後將 psp 指針的值載入到 r0 寄存器中,並執行以下程式從 MCU 保存 r4 ~ r11 寄存器的值到任務棧對應位置,同時 r0 中的值也會更新,執行完之後 r0 此時應該指向任務棧中用於存放寄存器 r4 記憶體的地址

mrs r0,psp
isb
stmdb r0!,{r4-r11}

如下所示的幾條程式則是將當前任務的任務控制塊指向了 r0 中的值描述的地址處,也即當前任務的任務控制塊指向了任務棧中用於存放寄存器 r4 記憶體的地址,可以發現和剛創建完成任務時一樣,這樣當前任務的上文就保存好了

ldr r3,=pxCurrentTCB
ldr r2,[r3]
str r0,[r2]

5.2.2、問題 2 解答

在保存好上文後載入下文前,中間的程式是為了找到下一個需要執行的任務,當找到下一個要執行的任務之後,載入下文的過程其實和啟動第一個任務時大致相同,不再詳細分析

6、實驗

6.1、測試

使用 xTaskCreateStatic() 函數創建兩個任務

  1. Task1 負責每隔 100ms 翻轉一次 GREEN_LED 燈引腳的電平
  2. Task2 負責每隔 500ms 翻轉一次 ORANGE_LED 燈引腳的電平

任務創建好之後將其添加到就緒鏈表中,然後啟動任務調度器即可,具體代碼如下所示

/* main.c */
/* USER CODE BEGIN Includes */
#include "FreeRTOS.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
extern List_t pxReadyTasksLists;

TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE                    128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;

TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE                    128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;

// 任務 1 入口函數
void Task1_Entry(void *parg)
{
	for(;;)
	{
		HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
		HAL_Delay(100);
		taskYIELD();
	}
}
// 任務 2 入口函數
void Task2_Entry(void *parg)
{
	for(;;)
	{
		HAL_GPIO_TogglePin(ORANGE_LED_GPIO_Port, ORANGE_LED_Pin);
		HAL_Delay(500);
		taskYIELD();
	}
}
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
// 使用鏈表前手動初始化
prvInitialiseTaskLists();
// 創建任務 1 和 2
Task1_Handle = xTaskCreateStatic((TaskFunction_t)Task1_Entry,
								 (char *)"Task1",
								 (uint32_t)TASK1_STACK_SIZE,
								 (void *)NULL,
								 (StackType_t *)Task1Stack,
								 (TCB_t *)&Task1TCB);
														
Task2_Handle = xTaskCreateStatic((TaskFunction_t)Task2_Entry,
								 (char *)"Task2",
								 (uint32_t)TASK2_STACK_SIZE,
								 (void *) NULL,
								 (StackType_t *)Task2Stack,
								 (TCB_t *)&Task2TCB );
// 將兩個任務插入到就緒鏈表中
vListInsertEnd(&(pxReadyTasksLists),&(((TCB_t *)(&Task1TCB))->xStateListItem));
vListInsertEnd(&(pxReadyTasksLists),&(((TCB_t *)(&Task2TCB))->xStateListItem));
// 啟動任務調度器,永不返回
vTaskStartScheduler();
/* USER CODE END 2 */

啟動任務調度器後的程式執行流程如下所示,第一次執行完步驟 6 後會不斷重覆步驟 3 ~ 6

  1. vTaskStartScheduler() -> pxCurrentTCB = &Task1TCB; -> xPortStartScheduler()
  2. -> prvStartFirstTask() -> vPortSVCHandler() -> Task1_Entry()
  3. -> taskYIELD() -> xPortPendSVHandler() -> vTaskSwitchContext()
  4. -> Task2_Entry()
  5. -> taskYIELD() -> xPortPendSVHandler() -> vTaskSwitchContext()
  6. -> Task1_Entry()

使用邏輯分析儀捕獲 GREEN_LED 和 ORANGE_LED 兩個引腳的電平變化,具體如下圖所示

可以發現兩個任務不是並行運行的,而是一個任務執行完,第二個任務才會得到執行,所以兩個 LED 燈引腳電平都是每隔 600ms 翻轉一次,不是我們期待的 Task1 引腳電平每隔 100 ms 翻轉一次,Task2 引腳電平每隔 500ms 翻轉一次,這樣的效果其實可被如下簡單代碼取代

while(1)
{
	HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
	HAL_Delay(100);
	HAL_GPIO_TogglePin(ORANGE_LED_GPIO_Port, ORANGE_LED_Pin);
	HAL_Delay(500);
}

6.2、待改進

當前 RTOS 簡單內核已實現的功能有

  1. 靜態方式創建任務
  2. 手動切換任務

當前 RTOS 簡單內核存在的缺點有

  1. 不支持任務優先順序
  2. 任務不能並行運行
  3. 無中斷臨界段保護

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這個庫提供了在啟動期間實例化已註冊的單例,而不是在首次使用它時實例化。 單例通常在首次使用時創建,這可能會導致響應傳入請求的延遲高於平時。在註冊時創建實例有助於防止第一次Request請求的SLA 以往我們要在註冊的時候實例單例可能會這樣寫: //註冊: services.AddSingleton< ...
  • 下麵是一個標準的IDistributedCache用例: public class SomeService(IDistributedCache cache) { public async Task<SomeInformation> GetSomeInformationAsync (string na ...
  • 0、思考與回答 0.1、思考一 如何實現 RTOS 內核支持多優先順序? 因為不支持優先順序,所以所有的任務都插入了一個名為 pxReadyTasksLists 的就緒鏈表中,相當於所有任務的優先順序都是一致的,那如果我們創建一個就緒鏈表數組,數組下標代表優先順序,優先順序為 x 的任務就插入到 pxRead ...
  • 本文介紹瞭如何根據所使用的不同開發板配置不同的交叉編譯環境. 由於在移植LVGL到不同開發板上時遇到了一些問題, 故在問題解決後整理和總結和該文章. ...
  • 我想用SecureFX(以及SecureCRT),但是FX安裝過程各種問題,導致安裝/卸載了大概4、5次,非常磨人。這裡記錄解決過程。 問題 secureFX註冊機缺少dll secureFX破解失敗,提示“the license is for a different version” 版本 系統: ...
  • iPerf 是一個網路性能測試工具,用於測量最大 TCP 和 UDP 帶寬性能。它支持多種平臺,包括 Windows、Linux、macOS 等。以下是 iPerf 的基本使用方法: 安裝 iPerf 在 Linux 系統中,你可以使用包管理器來安裝 iPerf。在 Ubuntu 或 Debian ...
  • @目錄0、思考與回答0.1、思考一0.2、思考二0.3、思考三1、關中斷1.1、帶返回值1.2、不帶返回值2、開中斷3、臨界段4、應用 0、思考與回答 0.1、思考一 為什麼需要臨界段? 有時候我們需要部分代碼一旦這開始執行,則不允許任何中斷打斷,這段代碼稱為臨界段 0.2、思考二 如何實現臨界段? ...
  • 目錄為什麼要學習使用make工具?什麼是make工具?make工具的學習過程1. 安裝make:sudo apt install make;並學習使用make安裝make流程學習使用make指令make指令的相關特點make只會對修改過的或者可執行目標文件不存在的.c文件進行編譯使用make時,若不 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...