目錄
在使用 FreeRTOS 的時候,一般的,先創建若干任務,但此刻任務並沒有被調度起來,僅僅是創建了,如果想要真正的跑起來,那麼還需要調用讓調度器跑起來的函數:
vTaskStartScheduler
典型的用法是:
xTaskCreate(.."task_1"..);
xTaskCreate(.."task_2"..);
xTaskCreate(.."task_3"..);
vTaskStartScheduler();
// Never reach here
DUMP_ERROR();
現在就來看看 vTaskStartScheduler 具體做了些什麼;
1、vTaskStartScheduler
vTaskStartScheduler 的實現在 task.c 中:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/* Add the idle task at the lowest priority. */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* The Idle task is created using user provided RAM - obtain the
address of the RAM then create the idle task. */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL,
portPRIVILEGE_BIT,
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else
{
/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT,
&xIdleTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS )
{
/* freertos_tasks_c_additions_init() should only be called if the user
definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
the only macro called by the function. */
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif
/* Interrupts are turned off here, to ensure a tick does not occur
before or during the call to xPortStartScheduler(). The stacks of
the created tasks contain a status word with interrupts switched on
so interrupts will automatically get re-enabled when the first task
starts to run. */
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
structure specific to the task that will run first. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
/* If configGENERATE_RUN_TIME_STATS is defined then the following
macro must be defined to configure the timer/counter used to generate
the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS
is set to 0 and the following line fails to build then ensure you do not
have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
FreeRTOSConfig.h file. */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
traceTASK_SWITCHED_IN();
/* Setting up the timer tick is hardware specific and thus in the
portable interface. */
if( xPortStartScheduler() != pdFALSE )
{
/* Should not reach here as if the scheduler is running the
function will not return. */
}
else
{
/* Should only reach here if a task calls xTaskEndScheduler(). */
}
}
else
{
/* This line will only be reached if the kernel could not be started,
because there was not enough FreeRTOS heap to create the idle task
or the timer task. */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
meaning xIdleTaskHandle is not used anywhere else. */
( void ) xIdleTaskHandle;
}
在 vTaskStartScheduler 函數中,首先通過 xTaskCreate 創建了一個 Idle 任務,優先級爲最低 0;(關於 Idle 任務,後面專門來講);
調用 portDISABLE_INTERRUPTS(); 關閉全局中斷,因爲後面要初始化 TICK 中斷;
接着初始化了全局變量:
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
下一個未阻塞的任務時間爲 0xFFFF_FFFF;調度器啓動的標誌位爲 TRUE,代表調度器已經初始化,Tick 的計數器爲 0;
2、xPortStartScheduler
接着調用 xPortStartScheduler 來配置和體系結構相關調度,這裏主要是一些和處理器相關的寄存器(比如 SYSTICK 等);這裏還是以 Cortex-M3 作爲例子,xPortStartScheduler 實現在 port.c 文件:
/* Constants required to check the validity of an interrupt priority. */
#define portFIRST_USER_INTERRUPT_NUMBER ( 16 )
#define portNVIC_IP_REGISTERS_OFFSET_16 ( 0xE000E3F0 )
#define portAIRCR_REG ( * ( ( volatile uint32_t * ) 0xE000ED0C ) )
#define portMAX_8_BIT_VALUE ( ( uint8_t ) 0xff )
#define portTOP_BIT_OF_BYTE ( ( uint8_t ) 0x80 )
#define portMAX_PRIGROUP_BITS ( ( uint8_t ) 7 )
#define portPRIORITY_GROUP_MASK ( 0x07UL << 8UL )
#define portPRIGROUP_SHIFT ( 8UL )
BaseType_t xPortStartScheduler( void )
{
#if(configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
/* 中斷優先級寄存器0: PRI_0 */
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
/* 這一大段代碼用來確定一個最高ISR優先級,在這個ISR或者更低優先級的ISR中可以安全的調用以FromISR結尾的API函數.*/
/* 保存中斷優先級值,因爲下面要覆寫這個寄存器(PRI_0) */
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* 確定有效的優先級位個數. 首先向所有位寫1,然後再讀出來,由於無效的優先級位讀出爲0,然後數一數有多少個1,就能知道有多少位優先級.*/
*pucFirstUserPriorityRegister= portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* 冗餘代碼,用來防止用戶不正確的設置RTOS可屏蔽中斷優先級值 */
ucMaxSysCallPriority =configMAX_SYSCALL_INTERRUPT_PRIORITY &ucMaxPriorityValue;
/* 計算最大優先級組值 */
ulMaxPRIGROUPValue =portMAX_PRIGROUP_BITS;
while( (ucMaxPriorityValue &portTOP_BIT_OF_BYTE ) ==portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
ulMaxPRIGROUPValue <<=portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &=portPRIORITY_GROUP_MASK;
/* 將PRI_0寄存器的值復原*/
*pucFirstUserPriorityRegister= ulOriginalPriority;
}
#endif /*conifgASSERT_DEFINED */
/* 將PendSV和SysTick中斷設置爲最低優先級*/
portNVIC_SYSPRI2_REG |=portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |=portNVIC_SYSTICK_PRI;
/* 啓動系統節拍定時器,即SysTick定時器,初始化中斷週期並使能定時器*/
vPortSetupTimerInterrupt();
/* 初始化臨界區嵌套計數器 */
uxCriticalNesting = 0;
/* 啓動第一個任務 */
prvStartFirstTask();
/* 永遠不會到這裏! */
return 0;
}
首先,如果定義 configASSERT_DEFINED 了的話,那麼先:
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
根據 Cortex-M3 的數據手冊可以知道,這個地方獲取到的是:NVIC 中斷優先級寄存器的基地址,Cortex-M3 的 NVIC 中斷優先級寄存器定義如下:
Name | Access | Base Address | Reset Value | Description |
PRI_0 | R/W | 0xE000_E400 | 0 (8 bits) | 外中斷 #0 的優先級 |
PRI_1 | R/W | 0xE000_E401 | 0 (8 bits) | 外中斷 #1 的優先級 |
..... | R/W | ..... | 0 (8 bits) | ..... |
PRI_239 | R/W | 0xE000_E4EF | 0 (8 bits) | 外中斷 #239 的優先級 |
這裏注意一下,每個優先級 8 bit;
這裏,首先將中斷 0 優先級讀出來,放置到 ulOriginalPriority;
在往這個 PRI_0 優先級寄存器中寫全 1,也就是 0xFF;
再將這個寄存器讀出來讀到 ucMaxPriorityValue 中;
這樣做的目的是爲了判斷這個中斷優先級寄存器哪些 bit 是可寫(可配置)的;這個和處理器對這方面的定義相關;
在 Cortex-M3 處理器上,NVIC 中斷控制器支持中斷優選級配置,它分爲了組優先級和組內優先級概念,雖然看起來每個優先級使用 8 bits 來表示,看似最大配置到 0xFF,也就是 255,其實不然,因爲引入了組優先級和組內優先級的概念,讓這個 8 bits 配置得有點玄機;
優先級組也叫搶佔優先級,組內優先級也叫子優先級;搶佔優先級高的中斷可以嵌套搶佔優先級低的,同樣搶佔優先級的中斷,則比較的是子優先級;
Cortex-M3 只是處理器的架構,實際上,芯片公司在利用這種架構實現芯片設計的時候,優先級組和組內優先級,並不是這 8 個 bit 都用到了,因爲大量的優先級會增加 NVIC 的複雜度;所以,一般的,具體芯片,PRI_0 的 8 bit 只會用到一部分的 bit,其中中用幾個 bit 來表示搶佔優先級,用幾個 bit 來表示子優先級,這個是可以配置的;具體一點的話,比如:
一款 Cortex-M3 內核做的處理器,它在設計的時候,就定義了 PRI_x 優先級的 8 bit,只有高 3 bit 有效:
一款 Cortex-M3 內核做的處理器,它在設計的時候,就定義了 PRI_x 優先級的 8 bit,只有高 4 bit 有效:
比如ST的STM32F1xx和F4xx只使用了這個8位中的高4位[7:4],低四位取零,這樣2^4=16,只能表示16級中斷嵌套。
那麼用高 4 bit 表示優先級,那麼這 4 個 bit 哪幾個 bit 代表搶佔優先級?哪幾個 bit 代表子優先級呢?
這個由另一個寄存器的值說了算,SCB->AIRCR[10:8](0xE000_ED00) 寄存器的 PRIGROUP 的值說了算;
#define NVIC_PriorityGroup_0 ((u32)0x700) /* 0 bits for pre-emption priority 4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((u32)0x600) /* 1 bits for pre-emption priority 3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((u32)0x500) /* 2 bits for pre-emption priority 2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((u32)0x400) /* 3 bits for pre-emption priority 1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((u32)0x300) /* 4 bits for pre-emption priority 0 bits for subpriority */
Group | AIRCR[10:8] Value | PRI_x bit[7:4] 分配情況 | 分配結果 |
0 | 3‘b111 | 0:4 | 0位搶佔優先級,4位響應優先級 |
1 | 3‘b110 | 1:3 | 1位搶佔優先級,3位響應優先級 |
2 | 3‘b101 | 2:2 | 2位搶佔優先級,2位響應優先級 |
3 | 3‘b100 | 3:1 | 3位搶佔優先級,1位響應優先級 |
4 | 3‘b011 | 4:0 | 4位搶佔優先級,0位響應優先 |
Cortex-M3 中斷優先級數值越大,表示優先級越低。而 FreeRTOS 的任務優先級則與之相反:優先級數值越大的任務,優先級越高。
好了,言歸正傳,這裏應該說清楚爲何要寫進去 0xF,在讀出來,就是因爲這 8bit 並不是全部都用了,這樣便可以得到最大支持的優先級個數 ucMaxPriorityValue;
接着計算最大優先級組的值,從讀出來有效的 PRI_0 的最高位開始判斷(因爲)優先級組和子優先級是從最高位開始;
接着配置 PendSV 和 SysTick 的優先級爲最低:
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
3、vPortSetupTimerInterrupt
調用 vPortSetupTimerInterrupt 配置 SysTick,這個是操作系統的心跳,和體系架構相關:
void vPortSetupTimerInterrupt( void )
{
/* Calculate the constants required to configure the tick interrupt. */
#if( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif /* configUSE_TICKLESS_IDLE */
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
STM32 的 SysTick 是一個向下計數的計數器,可以配置產生 Tick 中斷;
configSYSTICK_CLOCK_HZ 定義了 CPU 的時鐘頻率,需要和處理器同步;
configTICK_RATE_HZ 定義了 Tick 來的頻率,比如:
configTICK_RATE_HZ爲100,則系統節拍時鐘週期爲10ms,設置宏configTICK_RATE_HZ爲1000,則系統節拍時鐘週期爲1ms
太頻繁的 Tick 中斷會導致過頻繁的上下文切換,增加系統負擔,過於長的上下文切換,會導致任務響應不及時;
典型的,STM32 的 configTICK_RATE_HZ 爲 1000,也就是 1ms 一次 Tick 中斷;
然後配置寄存器,使能了 SysTick,使能了 SysTick 中斷;
接着初始化了嵌套深度:
uxCriticalNesting = 0;
4、prvStartFirstTask
最後調用了 prvStartFirstTask(); 啓動第一個任務,它的實現使用匯編寫的:
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* Cortext-M3硬件中,0xE000ED08 地址處爲VTOR(向量表偏移量)寄存器,存儲向量表起始地址*/
/* 將 0xE000ED08 加載到 R0 */
ldr r0, =0xE000ED08
/* 將 0xE000ED08 中的值,也就是向量表的實際地址加載到 R0 */
ldr r0, [r0]
/* 根據向量表實際存儲地址,取出向量表中的第一項,向量表第一項存儲主堆棧指針MSP的初始值*/
ldr r0, [r0]
/* 將堆棧地址寫入主堆棧指針 */
msr msp, r0
/* 使能全局中斷*/
cpsie i
cpsie f
dsb
isb
/* 調用SVC啓動第一個任務 */
svc 0
nop
nop
}
PRESERVE8 用於 8 字節對齊;
從 0xE000ED08 獲取向量表的偏移,爲啥要獲得向量表呢?因爲向量表的第一個是 MSP 指針!
取 MSP 的初始值的思路是先根據向量表的位置寄存器 VTOR (0xE000ED08) 來獲取向量表存儲的地址;
在根據向量表存儲的地址,來訪問第一個元素,也就是初始的 MSP;
此刻呢,將初始的 MSP 存入到了 R0 中,通過 MSR 指令,寫到 MSP 中:
打個比方,Cortex-M3 處理器,上電默認進入線程的特權模式,使用 MSP 作爲堆棧指針,從上電跑到這裏,經過一系列的函數調用,出棧,入棧,MSP 自然已經不是最開始的初始化的位置,這裏通過 MSR 重新複製了 MSP,豈不是堆棧都沒了麼?是的,因爲這是一條不歸路,代碼跑到這裏,首先不會返回,之前壓棧的內容再也不會用到,所以破壞之前的堆棧也沒關係;其次既然不會用到,那麼豈不是之前的壓棧空間都廢了,如果把 MSP 重新初始化到頭,就 OK 了嘛,大不了就是破壞了堆棧,反正再也回不去啦;
OK,堆棧指針 MSP 刷完,賦予了新的生命,此刻開中斷,開異常,刷流水線;
調用 svc 並傳入系統調用號爲 0 手動拉 SVC 中斷;
5、vPortSVCHandler
手動拉了 SVC 中斷,而且開啓了中斷,那麼就會進入它的 ISR:vPortSVCHandler,它的實現也是和處理器體系結構相關,在 port.c 中實現:
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB /* pxCurrentTCB指向處於最高優先級的就緒任務TCB */
ldr r1, [r3] /* 獲取任務TCB地址 */
ldr r0, [r1] /* 獲取任務TCB的第一個成員,即當前堆棧棧頂pxTopOfStack */
ldmia r0!, {r4-r11} /* 出棧,將寄存器r4~r11出棧 */
msr psp, r0 /* 最新的棧頂指針賦給線程堆棧指針PSP */
isb
mov r0, #0
msr basepri, r0
orr r14, #0xd /* 這裏0x0d表示:返回後進入線程模式,從進程堆棧中做出棧操作,返回Thumb狀態*/
bx r14
}
首先還是 PRESERVE8 的 8字節對齊操作;
還記得嗎,pxCurrentTCB 指向的是最高優先級的 Ready 狀態的任務指針;
根據 pxCurrentTCB 獲取到對應 TCB 的地址;然後獲取第一個成員變量,也就是當前棧頂地址 pxTopOfStack;這個值在任務分配的時候,就已經計算好,並且模擬的 Cortex-M3 的異常入棧順序,手動入棧了;
使用 LDMIA 指令,以 pxTopOfStack 開始順序出棧,先出 R4~R11(在創建任務的時候,最後入棧的就是這些個),同時 R0 遞增;
將此刻的 R0 賦值給 PSP(因爲彈棧的時候,處理器會按照入棧的順序去取 xPSR、PC、LR、R12、R3、R2、R1、R0,而這些寄存器在我們創建任務的時候已經手動壓棧);
ISB 指令屏障,刷流水線;
將 BASEPRI 寄存器賦值爲 0,也就是允許任何中斷;
ORR 指令時按位或,所以 ORR R14, #0xd 相當於 R14 |= 0xd;這個操作也和體系架構相關,R14 是鏈接寄存器 LR,在 ISR 中(此刻我們在 SVC 的 ISR 中),它記錄了異常返回值 EXC_RETURN(更多細節參考《Cortex-M3 處理器窺探》Chapter 7.4);
因爲當前在 ISR 中還是使用的 MSP,啓動任務後,我們期望在任務執行過程中,處於線程模式,並使用 PSP(前面幾行已經給 PSP 賦值了),所以我們需要將 LR 設計成爲 0xFFFF_FFFD,讓處理器知道返回的時候呢,使用線程模式+PSP堆棧;
最後執行 bx R14,告訴處理器 ISR 完成,需要返回,此刻處理器便會使用 PSP 做爲堆棧指針,進行出棧操作,將xPSR、PC、LR、R12、R3~R0 出棧,初始化的時候,PC 被我們賦值成爲了執行任務的函數的入口,所以呢,就正常跳入到了優先級最高的 Ready 狀態的第一個任務的入口函數了;
處理器相關的部分,可以參考《Cortex-M3 處理器窺探》,創建任務的部分參考《FreeRTOS --(8)任務管理之創建任務》
大致的流程如下:
紫色部分,是和體系架構相關的,黑色的是開關中斷的地方,藍色的是 FreeRTOS 的代碼;