轉載自 https://blog.csdn.net/zhoutaopower/article/details/107057528
在使用 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 的代碼;
