FreeRTOS系統啓動過程主要分爲三部分:彙編部分、main函數初始化部分、開啓任務調度部分。
對於彙編部分主要是設置一些中斷向量表、設置堆和棧等一些C語言運行需要的條件,當這些部分設置完成時候,就會跳轉到main函數運行。對於main函數初始化部分,主要是做一些必要的硬件外設初始化、板級初始化、還有就是任務的創建。任務創建完成之後,就會開啓調度器,FreeRTOS開始運行。
下面就講一下FreeRTOS是怎麼開始運行的:
由於之前講過一篇關於apollo2 MCU的彙編啓動,關於Cortex-M4的彙編啓動部分基本一致。大家可以看之前博客。直接從main函數開始,上代碼:
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設置系統中斷優先級分組4
delay_init(168); //初始化延時函數
uart_init(115200); //初始化串口
LED_Init(); //初始化LED端口
KEY_Init(); //初始化按鍵
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化內部內存池
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 20-1");
LCD_ShowString(30,50,200,16,16,"Mem Manage");
LCD_ShowString(30,70,200,16,16,"KEY_UP:Malloc,KEY1:Free");
LCD_ShowString(30,90,200,16,16,"KEY0:Use Mem");
LCD_ShowString(30,110,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,130,200,16,16,"2016/11/14");
LCD_ShowString(30,170,200,16,16,"Total Mem: Bytes");
LCD_ShowString(30,190,200,16,16,"Free Mem: Bytes");
LCD_ShowString(30,210,200,16,16,"Message: ");
POINT_COLOR = BLUE;
//創建開始任務
xTaskCreate((TaskFunction_t )start_task, //任務函數
(const char* )"start_task", //任務名稱
(uint16_t )START_STK_SIZE, //任務堆棧大小
(void* )NULL, //傳遞給任務函數的參數
(UBaseType_t )START_TASK_PRIO, //任務優先級
(TaskHandle_t* )&StartTask_Handler); //任務句柄
vTaskStartScheduler(); //開啓任務調度
}
main函數第一部分會進行一些硬件初始化(串口、定時器、LED、LCD顯示器),之後會調用任務創建函數xTaskCreate,此函數會創建一個start任務。關於start_task函數代碼如下:
//開始任務任務函數
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //進入臨界區
//創建TASK1任務
xTaskCreate((TaskFunction_t )malloc_task,
(const char* )"malloc_task",
(uint16_t )MALLOC_STK_SIZE,
(void* )NULL,
(UBaseType_t )MALLOC_TASK_PRIO,
(TaskHandle_t* )&MallocTask_Handler);
vTaskDelete(StartTask_Handler); //刪除開始任務
taskEXIT_CRITICAL(); //退出臨界區
}
關於start任務會進行一系列的任務創建,當這些任務創建完成之後,start會將自己刪除。從上面代碼可以看出,在start任務中會創建一個malloc任務。具體的malloc任務的代碼如下:
//MALLOC任務函數
void malloc_task(void *pvParameters)
{
u8 *buffer;
u8 times,i,key=0;
u32 freemem;
LCD_ShowxNum(110,170,configTOTAL_HEAP_SIZE,5,16,0);//顯示內存總容量
while(1)
{
key=KEY_Scan(0);
switch(key)
{
case WKUP_PRES:
buffer=pvPortMalloc(30); //申請內存,30個字節
printf("申請到的內存地址爲:%#x\r\n",(int)buffer);
break;
case KEY1_PRES:
if(buffer!=NULL)vPortFree(buffer); //釋放內存
buffer=NULL;
break;
case KEY0_PRES:
if(buffer!=NULL) //buffer可用,使用buffer
{
times++;
sprintf((char*)buffer,"User %d Times",times);//向buffer中填寫一些數據
LCD_ShowString(94,210,200,16,16,buffer);
}
break;
}
freemem=xPortGetFreeHeapSize(); //獲取剩餘內存大小
LCD_ShowxNum(110,190,freemem,5,16,0);//顯示內存總容量
i++;
if(i==50)
{
i=0;
LED0=~LED0;
}
vTaskDelay(10);
}
}
回到main函數,當創建完成任務之後,FreeRTOS會調用vTaskStartScheduler(); 開啓任務調度器。
整體步驟:main函數硬件初始化->start任務創建->start創建各種任務->start任務自殺->開啓任務調度器。
下面就開始講解調度器的實現
函數void vTaskStartScheduler( void )代碼如下:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////// //////
////// 調度器 //////
////// //////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
#if( configSUPPORT_STATIC_ALLOCATION == 1 )//若是靜態方法創建任務,由於採用動態方法,不會執行
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
"IDLE",
ulIdleTaskStackSize,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else
{//動態方法創建任務
xReturn = xTaskCreate( prvIdleTask,//創建空閒任務,空閒任務是必須要有的,當沒有任務需要運行時,給FreeRTOS找點事幹
"IDLE", configMINIMAL_STACK_SIZE,//空閒任務的堆棧大小
( void * ) NULL,//空閒任務的參數
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),//設置空閒任務的優先級,爲系統的最低優先級
&xIdleTaskHandle ); //空閒任務的任務句柄
}
#endif
#if ( configUSE_TIMERS == 1 )//啓用軟件定時器
{
if( xReturn == pdPASS )
{//空閒任務創建成功
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
if( xReturn == pdPASS )
{
portDISABLE_INTERRUPTS();//屏蔽中斷,屏蔽掉FreeRTOS能夠管理的所有中斷
#if ( configUSE_NEWLIB_REENTRANT == 1 )//沒有使用嵌入式C語言專用庫
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
xNextTaskUnblockTime = portMAX_DELAY;//下一個需要解除阻塞的任務的時間
xSchedulerRunning = pdTRUE;//標識任務調度器開始工作
xTickCount = ( TickType_t ) 0U;//用於記錄系統運行時間,記錄的是運行的節拍數
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();//關於代碼運行時間和任務狀態收集相關功能函數。此時必須藉助MCU的系統定時器之外的一個定時器
if( xPortStartScheduler() != pdFALSE )
{
}
else
{
}
}
else
{
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
( void ) xIdleTaskHandle;
}
該函數主要是首先創建空閒任務,在FreeRTOS中空閒任務是必須要有的。之後根據配置決定是否開啓軟件定時器。再之後屏蔽FreeRTOS的能夠管理的中斷,設置相應的全局變量。這裏要注意一點,空閒任務是FreeRTOS必須要創建的,而且任務的優先級是最低的。全局變量xNextTaskUnblockTime表示下一個解除阻塞的任務;全局變量xSchedulerRunning標識任務調度器開啓;xTickCount表示記錄FreeRTOS運行時間。該函數最終會調用xPortStartScheduler()函數,具體代碼如下:
BaseType_t xPortStartScheduler( void )
{
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
#if( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );//0xE000E400是外部中斷0#優先級設置寄存器
ulOriginalPriority = *pucFirstUserPriorityRegister;//保存外部中斷0#原始優先級
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;//得到MCU支持的可編程優先級數量
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )//ucMaxSysCallPriority此時存儲的系統可以支持的最高優先級,如果我們設置最高優先級爲5,那麼此處變量ucMaxSysCallPriority數值爲0b01010000
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;//設置PENDSV中斷優先級,一般會設置MCU所支持的最低優先級
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;//設置系統滴答時鐘中斷優先級,一般設置MCU所支持的最低優先級
vPortSetupTimerInterrupt();//開啓滴答定時器
uxCriticalNesting = 0;//用於臨界區保護嵌套作用
prvEnableVFP();//使能FPU
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;//設置浮點上下文控制寄存器
prvStartFirstTask();//啓動第一個任務
return 0;
}
關於函數xPortStartScheduler主要是這是pendsv和滴答定時器的中斷優先級,一般會設置爲整個系統所支持的最低中斷優先級。開啓滴答定時器,滴答定時器的作用是產生系統節拍,進行任務切換。最後調用prvStartFirstTask()函數啓動第一個任務。
函數vPortSetupTimerInterrupt( void )代碼如下:
void vPortSetupTimerInterrupt( void )
{
#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
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;//portNVIC_SYSTICK_LOAD_REG是Systick重裝載寄存器,設置系統節拍
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );//portNVIC_SYSTICK_CTRL_REG是Systick控制和狀態寄存器,使用外部時鐘,定時器減到0產生中斷,定時器使能
}
上述函數主要是設置滴答定時器的裝載寄存器數值,配置滴答定時器的時鐘來源,定時器開啓等。
函數prvStartFirstTask( void )如下:
__asm void prvStartFirstTask( void )
{
PRESERVE8
ldr r0, =0xE000ED08 //向量表偏移寄存器地址
ldr r0, [r0] //取向量表地址
ldr r0, [r0] //取MSP初始值
msr msp, r0 //重置MSP指針
cpsie i //開中斷
cpsie f //開異常
dsb //數據同步隔離
isb //指令同步隔離
svc 0 //異常觸發,啓動第一個任務,SVC異常
nop
nop
}
該函數最主要的就是產生一個SVC中斷,以此啓動第一個任務。
函數vPortSVCHandler( void )代碼如下:
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
mov r0, #0
msr basepri, r0
bx r14
}
關於上面函數重點說明以下幾點:
1)pxCurrentTCB存儲的是當前任務的控制塊,任務控制塊中的第一個成員變量就是任務堆棧的指針。
2)ldmia r0!, {r4-r11, r14},通過上面代碼的處理,R0寄存器保存着當前任務的棧頂,r0!表示自增的意思,因此這步操作的內容是將棧頂的數據手動加載到寄存器r4~r11,以及r14寄存器中;msr psp, r0將PSP寄存器重新指向當前任務的新的棧頂。
3)msr basepri, r0,basepri寄存器是屏蔽優先級寄存器,當我們向該寄存器寫入某一個數值n之後,就會屏蔽優先級數值高於n的所有中斷和異常,這裏要注意數值越大優先級越低。當我們寫入0時,表示不屏蔽任何中斷和異常。
4)bx r14,跳轉到用戶寫的任務函數中運行,這時候FreeRTOS開始真正運行。
關於FreeRTOS的啓動過程大致總結如下:
首先就是創建一個start任務,在start任務中會創建一堆任務;當這些任務創建完成之後,start任務會進行自殺;
接着會啓動任務調度器,在任務調度器中會進行滴答定時器設置、開啓滴答定時器、產生SVC中斷、跳轉到用戶編寫的任務函數。
可能大家會有很多疑問,比如在《FreeRTOS內核源碼解讀之-------系統啓動(一)》這篇文章中提到的MSP和PSP之間是怎麼進行切換的?爲什麼通過bx r14指令就能夠跳轉到用戶任務函數?內部各個寄存器保存的的數值有什麼作用?這些問題我會在下一篇文章中進行介紹。