視覺設(shè)計(jì)網(wǎng)站推薦世界球隊(duì)最新排名
寫在前面
本文是基于野火 RTOS 教程對(duì)空閑任務(wù)和阻塞延時(shí)的詳解。
一、什么是任務(wù)中的阻塞延時(shí)
- 說到阻塞延時(shí),筆者的第一反應(yīng)就是在單片機(jī)的 while 循環(huán)中,使用一個(gè) for 循環(huán)不斷遞減一個(gè)大數(shù),通過 CPU 不斷執(zhí)行一條指令的耗時(shí)進(jìn)行延時(shí)。這種延時(shí)會(huì)占用 CPU 資源執(zhí)行指令,在延時(shí)的時(shí)候 CPU 不能執(zhí)行其他的指令。
- 但是注意,我們現(xiàn)在是想在 RTOS 中的任務(wù)實(shí)現(xiàn)阻塞延時(shí),RTOS 可以有多個(gè)任務(wù),所有所謂任務(wù)中的阻塞延時(shí)雖然也是阻塞其后的代碼運(yùn)行,但是只阻塞了他所在的那個(gè)任務(wù)中阻塞延時(shí)函數(shù)后面的代碼。
- 也就是說,RTOS 中,任務(wù)中的阻塞延時(shí)就是先阻塞一下這個(gè)任務(wù),然后把 CPU 使用權(quán)交給其他代碼,雖然也是阻塞下文的代碼執(zhí)行,但是只阻塞這個(gè)任務(wù)的下文,CPU 在這個(gè)過程中可以執(zhí)行其他任務(wù)中的指令,大大提高 CPU 利用率,和筆者印象中的阻塞延時(shí)并不一樣。
二、空閑任務(wù)有什么用
- 空閑任務(wù)的優(yōu)先級(jí)是所有任務(wù)中優(yōu)先級(jí)最低的,當(dāng)其他任務(wù)都在阻塞延時(shí)中,CPU 就會(huì)切換到空閑任務(wù)運(yùn)行。
- 一般來說在空閑任務(wù)里面運(yùn)行一些系統(tǒng)內(nèi)存的清理工作,或者在空閑任務(wù)中讓單片機(jī)休眠或者進(jìn)入低功耗模式。
三、空閑任務(wù)的實(shí)現(xiàn)
- 定義空閑任務(wù)的任務(wù)棧
- 定義空閑任務(wù)的 TCB
- 空閑任務(wù)的創(chuàng)建
注意,空閑任務(wù)的任務(wù)棧和 TCB 變量我們都在 main.c
中聲明為全局變量,但是同時(shí),我們想在開啟任務(wù)調(diào)度器的時(shí)候自動(dòng)創(chuàng)建一個(gè)空閑任務(wù),而 RTOS 的開發(fā)人員不用顯式地去創(chuàng)建空閑任務(wù),所以我們把空閑任務(wù)的創(chuàng)建集成在 void vTaskStartScheduler( void ) 這個(gè)函數(shù)中。這樣,我們?cè)趩?dòng)調(diào)度器的同時(shí)就會(huì)自動(dòng)創(chuàng)建一個(gè)空閑任務(wù)。代碼如下:
void vTaskStartScheduler( void )
{
/*======================================創(chuàng)建空閑任務(wù)start==============================================*/ TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空閑任務(wù)控制塊 */StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空閑任務(wù)棧起始地址 */uint32_t ulIdleTaskStackSize;/* 獲取空閑任務(wù)的內(nèi)存:任務(wù)棧和任務(wù)TCB */vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任務(wù)入口 */(char *)"IDLE", /* 任務(wù)名稱,字符串形式 */(uint32_t)ulIdleTaskStackSize , /* 任務(wù)棧大小,單位為字 */(void *) NULL, /* 任務(wù)形參 */(StackType_t *)pxIdleTaskStackBuffer, /* 任務(wù)棧起始地址 */(TCB_t *)pxIdleTaskTCBBuffer ); /* 任務(wù)控制塊 *//* 將任務(wù)添加到就緒列表 */ vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*======================================創(chuàng)建空閑任務(wù)end================================================*//* 手動(dòng)指定第一個(gè)運(yùn)行的任務(wù) */pxCurrentTCB = &Task1TCB;/* 初始化系統(tǒng)時(shí)基計(jì)數(shù)器 */xTickCount = ( TickType_t ) 0U;/* 啟動(dòng)調(diào)度器 */if( xPortStartScheduler() != pdFALSE ){/* 調(diào)度器啟動(dòng)成功,則不會(huì)返回,即不會(huì)來到這里 */}
}
上面這段代碼調(diào)用了 xTaskCreateStatic() 這個(gè)函數(shù)進(jìn)行空閑任務(wù)的創(chuàng)建,但是這個(gè)函數(shù)需要傳入空閑任務(wù)的任務(wù)棧和 TCB 變量,而我們把這些變量定義在了 main.c
中,所以需要使用 vApplicationGetIdleTaskMemory() 這個(gè)函數(shù)來使 vTaskStartScheduler() 函數(shù)中的任務(wù)指針等等變量指向定義在 main.c
中的任務(wù)棧和 TCB,然后再把這些任務(wù)指針等傳入 xTaskCreateStatic() 中。vApplicationGetIdleTaskMemory() 的具體代碼如下:
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize )
{*ppxIdleTaskTCBBuffer=&IdleTaskTCB;*ppxIdleTaskStackBuffer=IdleTaskStack; *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
四、任務(wù)中的阻塞延時(shí)怎么實(shí)現(xiàn)
具體想法如下:
- 為 TCB 添加記錄延時(shí)時(shí)間的參數(shù)
- 在任務(wù)中調(diào)用阻塞延時(shí)函數(shù)時(shí),會(huì)給 TCB 記錄延時(shí)時(shí)間的參數(shù)進(jìn)行賦值,然后調(diào)用任務(wù)切換函數(shù)
- 調(diào)用任務(wù)切換函數(shù)會(huì)產(chǎn)生 PendSV 中斷,在 PendSV中斷服務(wù)函數(shù)中會(huì)調(diào)用上下文切換函數(shù) vTaskSwitchContext()
- 在上下文切換函數(shù)中,我們更新當(dāng)前執(zhí)行任務(wù)的指針?,F(xiàn)在我們的思想是,如果當(dāng)前任務(wù)是空閑任務(wù),那么查看其他任務(wù)的延時(shí)是否結(jié)束,如果沒有結(jié)束就繼續(xù)執(zhí)行空閑任務(wù);如果當(dāng)前執(zhí)行的不是空閑任務(wù),那么檢查一下其他任務(wù)是否在延時(shí)中,如果不在延時(shí)中,就不忘初心進(jìn)行任務(wù)切換,如果在延時(shí)中,就判斷現(xiàn)在這個(gè)任務(wù)是否要延時(shí),如果要延時(shí)就切換到空閑任務(wù),否則就不進(jìn)行任何切換。
- 上面檢查任務(wù)是否在延時(shí)狀態(tài)都是通過檢查 TCB 的延時(shí)參數(shù)是否為 0 來實(shí)現(xiàn)的,我們使用 SysTick 中斷來對(duì) TCB 的延時(shí)參數(shù)進(jìn)行定時(shí)修改
- 在每次 SysTick 中斷觸發(fā)時(shí),我們更新一下系統(tǒng)時(shí)基計(jì)數(shù)器(以后有用),然后掃描一下就緒列表中所有 TCB 的延時(shí)參數(shù),不為 0 就減 1,最后嘗試任務(wù)切換
1. 為 TCB 添加記錄延時(shí)時(shí)間的參數(shù)
typedef struct tskTaskControlBlock
{volatile StackType_t *pxTopOfStack; /* 棧頂 */ListItem_t xStateListItem; /* 任務(wù)節(jié)點(diǎn) */StackType_t *pxStack; /* 任務(wù)棧起始地址 *//* 任務(wù)名稱,字符串形式 */char pcTaskName[ configMAX_TASK_NAME_LEN ]; TickType_t xTicksToDelay; /* 用于延時(shí) */
} tskTCB;
typedef tskTCB TCB_t;
2. 阻塞延時(shí)函數(shù) vTaskDelay()
給 TCB 記錄延時(shí)時(shí)間的參數(shù)進(jìn)行賦值,然后調(diào)用任務(wù)切換函數(shù)。
void vTaskDelay( const TickType_t xTicksToDelay )
{TCB_t *pxTCB = NULL;/* 獲取當(dāng)前任務(wù)的TCB */pxTCB = pxCurrentTCB;/* 設(shè)置延時(shí)時(shí)間 */pxTCB->xTicksToDelay = xTicksToDelay;/* 任務(wù)切換 */taskYIELD();
}
3. 上下文切換函數(shù) vTaskSwitchContext()
- 如果當(dāng)前任務(wù)是空閑任務(wù)
- 查看其他任務(wù)的延時(shí)是否結(jié)束
- 沒有結(jié)束 -> 繼續(xù)執(zhí)行空閑任務(wù)
- 結(jié)束 -> 跳轉(zhuǎn)到其他任務(wù)
- 查看其他任務(wù)的延時(shí)是否結(jié)束
- 如果當(dāng)前執(zhí)行的不是空閑任務(wù)
- 檢查一下其他任務(wù)是否在延時(shí)中
- 不在延時(shí)中 -> 進(jìn)行任務(wù)切換
- 在延時(shí)中 -> 判斷現(xiàn)在這個(gè)任務(wù)是否要延時(shí)
- 要延時(shí)就切換到空閑任務(wù)
- 否則就不進(jìn)行任何切換
- 檢查一下其他任務(wù)是否在延時(shí)中
void vTaskSwitchContext( void )
{/* 如果當(dāng)前線程是空閑線程,那么就去嘗試執(zhí)行線程1或者線程2,看看他們的延時(shí)時(shí)間是否結(jié)束,如果線程的延時(shí)時(shí)間均沒有到期,那就返回繼續(xù)執(zhí)行空閑線程 */if( pxCurrentTCB == &IdleTaskTCB ){if(Task1TCB.xTicksToDelay == 0){ pxCurrentTCB =&Task1TCB;}else if(Task2TCB.xTicksToDelay == 0){pxCurrentTCB =&Task2TCB;}else{return; /* 線程延時(shí)均沒有到期則返回,繼續(xù)執(zhí)行空閑線程 */} }else{/*如果當(dāng)前線程是線程1或者線程2的話,檢查下另外一個(gè)線程,如果另外的線程不在延時(shí)中,就切換到該線程否則,判斷下當(dāng)前線程是否應(yīng)該進(jìn)入延時(shí)狀態(tài),如果是的話,就切換到空閑線程。否則就不進(jìn)行任何切換 */if(pxCurrentTCB == &Task1TCB){if(Task2TCB.xTicksToDelay == 0){pxCurrentTCB =&Task2TCB;}else if(pxCurrentTCB->xTicksToDelay != 0){pxCurrentTCB = &IdleTaskTCB;}else {return; /* 返回,不進(jìn)行切換,因?yàn)閮蓚€(gè)線程都處于延時(shí)中 */}}else if(pxCurrentTCB == &Task2TCB){if(Task1TCB.xTicksToDelay == 0){pxCurrentTCB =&Task1TCB;}else if(pxCurrentTCB->xTicksToDelay != 0){pxCurrentTCB = &IdleTaskTCB;}else {return; /* 返回,不進(jìn)行切換,因?yàn)閮蓚€(gè)線程都處于延時(shí)中 */}}}
}
4. SysTick 中斷對(duì) TCB 的延時(shí)參數(shù)進(jìn)行定時(shí)修改
/*
*************************************************************************
* SysTick中斷服務(wù)函數(shù)
*************************************************************************
*/
void xPortSysTickHandler( void )
{/* 關(guān)中斷 */vPortRaiseBASEPRI();/* 更新系統(tǒng)時(shí)基 */xTaskIncrementTick();/* 開中斷 */vPortClearBASEPRIFromISR();
}
每次 SysTick 中斷觸發(fā)時(shí),我們更新一下系統(tǒng)時(shí)基計(jì)數(shù)器(以后有用),然后掃描一下就緒列表中所有 TCB 的延時(shí)參數(shù),不為 0 就減 1,最后嘗試任務(wù)切換:
void xTaskIncrementTick( void )
{TCB_t *pxTCB = NULL;BaseType_t i = 0;/* 更新系統(tǒng)時(shí)基計(jì)數(shù)器xTickCount,xTickCount是一個(gè)在port.c中定義的全局變量 */const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;/* 掃描就緒列表中所有線程的xTicksToDelay,如果不為0,則減1 */for(i=0; i<configMAX_PRIORITIES; i++){pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );if(pxTCB->xTicksToDelay > 0){pxTCB->xTicksToDelay --;}}/* 任務(wù)切換 */portYIELD();
}
關(guān)于上面這段代碼,有一段寫得很奇怪:
/* 更新系統(tǒng)時(shí)基計(jì)數(shù)器xTickCount,xTickCount是一個(gè)在port.c中定義的全局變量 */const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;
筆者剛開始看到的時(shí)候想問:直接遞增xTickCount不行嗎,為什么要寫成
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
這樣不是畫蛇添足嗎?使代碼更復(fù)雜。
其實(shí)不然,在任務(wù)調(diào)度器中,xTickCount 變量用于記錄系統(tǒng)的時(shí)基計(jì)數(shù)器。它的目的是跟蹤系統(tǒng)運(yùn)行的時(shí)間,并且根據(jù)需要遞增。
直接遞增 xTickCount 可能會(huì)導(dǎo)致并發(fā)問題。在多線程或多任務(wù)的情況下,如果有多個(gè)任務(wù)同時(shí)嘗試遞增 xTickCount,并且中間存在競(jìng)爭(zhēng)條件,可能會(huì)導(dǎo)致計(jì)數(shù)不準(zhǔn)確或不一致。
為了避免這種并發(fā)問題,代碼中將遞增操作分解為兩個(gè)步驟:
首先,通過 const TickType_t xConstTickCount = xTickCount + 1; 將 xTickCount 的值復(fù)制到一個(gè)中間變量 xConstTickCount 中,并遞增這個(gè)中間變量。
然后,將中間變量 xConstTickCount 的值賦回給 xTickCount,完成遞增操作。
這樣做的好處是,無論何時(shí)進(jìn)行遞增操作,代碼都使用了一個(gè)穩(wěn)定的中間值 xConstTickCount 來執(zhí)行計(jì)算和更新。這確保了計(jì)數(shù)器 xTickCount 在整個(gè)遞增過程中保持一致,并且不會(huì)受到其他任務(wù)的干擾。這樣可以避免并發(fā)問題,提高代碼的可靠性和正確性。
5. 最后是 SysTick 的相關(guān)初始化代碼
在調(diào)度器啟動(dòng)函數(shù) xPortStartScheduler() 函數(shù)中調(diào)用 vPortSetupTimerInterrupt():
/*
*************************************************************************
* 調(diào)度器啟動(dòng)函數(shù)
*************************************************************************
*/BaseType_t xPortStartScheduler( void )
{/*PendSV是一個(gè)用于低優(yōu)先級(jí)任務(wù)切換的軟件中斷。通過觸發(fā)PendSV中斷,可以請(qǐng)求處理器在合適的時(shí)間切換到更高優(yōu)先級(jí)的任務(wù)。PendSV中斷具有最低的中斷優(yōu)先級(jí),因此可以在其他中斷處理完成后立即執(zhí)行。*//* 配置PendSV 和 SysTick 的中斷優(yōu)先級(jí)為最低 */portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;//初始化SysTick中斷vPortSetupTimerInterrupt();/* 啟動(dòng)第一個(gè)任務(wù),不再返回 */prvStartFirstTask();/* 不應(yīng)該運(yùn)行到這里 */return 0;
}
初始化 SysTick 的函數(shù) vPortSetupTimerInterrupt():
/*
*************************************************************************
* 初始化SysTick
*************************************************************************
*/
void vPortSetupTimerInterrupt( void )
{/* 設(shè)置重裝載寄存器的值 */portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;/* 設(shè)置系統(tǒng)定時(shí)器的時(shí)鐘等于內(nèi)核時(shí)鐘使能SysTick 定時(shí)器中斷使能SysTick 定時(shí)器 */portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT |portNVIC_SYSTICK_ENABLE_BIT );
}
這里解釋一下重裝載寄存器的值怎么設(shè)置。計(jì)時(shí)器實(shí)際上是一個(gè)計(jì)數(shù)器,當(dāng)接收到設(shè)定數(shù)量的脈沖后進(jìn)行一次中斷,而這個(gè)設(shè)定的數(shù)量就是重裝載寄存器的值。
我們把計(jì)時(shí)器接入到 CPU 晶振后,由于晶振每隔一段固定時(shí)間發(fā)出一個(gè)脈沖信號(hào),此時(shí)計(jì)時(shí)器就將重裝載寄存器的值減 1,當(dāng)重裝載寄存器的值減到 0 后,就觸發(fā)一次中斷,由此完成了對(duì)晶振的高頻率信號(hào)的分頻。
注意,重裝載寄存器的值是從 0 開始減的,所以設(shè)置時(shí)要減 1。
可以看到,我們使用 configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL 進(jìn)行設(shè)置,configSYSTICK_CLOCK_HZ 實(shí)際上就是 CPU 的晶振頻率,而 configTICK_RATE_HZ 就是我們?cè)O(shè)置 SysTick 的中斷頻率。
其中的宏定義為:
#define configCPU_CLOCK_HZ ( ( unsigned long ) 25000000 )
#define configTICK_RATE_HZ ( ( TickType_t ) 100 )/* SysTick 配置寄存器 */
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )#ifndef configSYSTICK_CLOCK_HZ#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ/* 確保SysTick的時(shí)鐘與內(nèi)核時(shí)鐘一致 */#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 )
后記
如果您覺得本文寫得不錯(cuò),可以點(diǎn)個(gè)贊激勵(lì)一下作者!
如果您發(fā)現(xiàn)本文的問題,歡迎在評(píng)論區(qū)或者私信共同探討!
共勉!