FreeRTOS --(4)任务管理之启动调度器

目录

1、vTaskStartScheduler

2、xPortStartScheduler

3、vPortSetupTimerInterrupt

4、prvStartFirstTask

5、vPortSVCHandler


 

在使用 FreeRTOS 的时候,一般的,先创建若干任务,但此刻任务并没有被调度起来,仅仅是创建了,如果想要真正的跑起来,那么还需要调用让调度器跑起来的函数:

vTaskStartScheduler

典型的用法是:


   
   
  1. xTaskCreate(.. "task_1"..);
  2. xTaskCreate(.. "task_2"..);
  3. xTaskCreate(.. "task_3"..);
  4. vTaskStartScheduler();
  5. // Never reach here
  6. DUMP_ERROR();

现在就来看看 vTaskStartScheduler 具体做了些什么;

 

1、vTaskStartScheduler

vTaskStartScheduler 的实现在 task.c 中:


   
   
  1. void vTaskStartScheduler( void )
  2. {
  3. BaseType_t xReturn;
  4. /* Add the idle task at the lowest priority. */
  5. #if( configSUPPORT_STATIC_ALLOCATION == 1 )
  6. {
  7. StaticTask_t *pxIdleTaskTCBBuffer = NULL;
  8. StackType_t *pxIdleTaskStackBuffer = NULL;
  9. uint32_t ulIdleTaskStackSize;
  10. /* The Idle task is created using user provided RAM - obtain the
  11. address of the RAM then create the idle task. */
  12. vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
  13. xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
  14. configIDLE_TASK_NAME,
  15. ulIdleTaskStackSize,
  16. ( void * ) NULL,
  17. portPRIVILEGE_BIT,
  18. pxIdleTaskStackBuffer,
  19. pxIdleTaskTCBBuffer );
  20. if( xIdleTaskHandle != NULL )
  21. {
  22. xReturn = pdPASS;
  23. }
  24. else
  25. {
  26. xReturn = pdFAIL;
  27. }
  28. }
  29. #else
  30. {
  31. /* The Idle task is being created using dynamically allocated RAM. */
  32. xReturn = xTaskCreate( prvIdleTask,
  33. configIDLE_TASK_NAME,
  34. configMINIMAL_STACK_SIZE,
  35. ( void * ) NULL,
  36. portPRIVILEGE_BIT,
  37. &xIdleTaskHandle );
  38. }
  39. #endif /* configSUPPORT_STATIC_ALLOCATION */
  40. #if ( configUSE_TIMERS == 1 )
  41. {
  42. if( xReturn == pdPASS )
  43. {
  44. xReturn = xTimerCreateTimerTask();
  45. }
  46. else
  47. {
  48. mtCOVERAGE_TEST_MARKER();
  49. }
  50. }
  51. #endif /* configUSE_TIMERS */
  52. if( xReturn == pdPASS )
  53. {
  54. /* freertos_tasks_c_additions_init() should only be called if the user
  55. definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
  56. the only macro called by the function. */
  57. #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
  58. {
  59. freertos_tasks_c_additions_init();
  60. }
  61. #endif
  62. /* Interrupts are turned off here, to ensure a tick does not occur
  63. before or during the call to xPortStartScheduler(). The stacks of
  64. the created tasks contain a status word with interrupts switched on
  65. so interrupts will automatically get re-enabled when the first task
  66. starts to run. */
  67. portDISABLE_INTERRUPTS();
  68. #if ( configUSE_NEWLIB_REENTRANT == 1 )
  69. {
  70. /* Switch Newlib's _impure_ptr variable to point to the _reent
  71. structure specific to the task that will run first. */
  72. _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
  73. }
  74. #endif /* configUSE_NEWLIB_REENTRANT */
  75. xNextTaskUnblockTime = portMAX_DELAY;
  76. xSchedulerRunning = pdTRUE;
  77. xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
  78. /* If configGENERATE_RUN_TIME_STATS is defined then the following
  79. macro must be defined to configure the timer/counter used to generate
  80. the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS
  81. is set to 0 and the following line fails to build then ensure you do not
  82. have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
  83. FreeRTOSConfig.h file. */
  84. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
  85. traceTASK_SWITCHED_IN();
  86. /* Setting up the timer tick is hardware specific and thus in the
  87. portable interface. */
  88. if( xPortStartScheduler() != pdFALSE )
  89. {
  90. /* Should not reach here as if the scheduler is running the
  91. function will not return. */
  92. }
  93. else
  94. {
  95. /* Should only reach here if a task calls xTaskEndScheduler(). */
  96. }
  97. }
  98. else
  99. {
  100. /* This line will only be reached if the kernel could not be started,
  101. because there was not enough FreeRTOS heap to create the idle task
  102. or the timer task. */
  103. configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
  104. }
  105. /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
  106. meaning xIdleTaskHandle is not used anywhere else. */
  107. ( void ) xIdleTaskHandle;
  108. }

在 vTaskStartScheduler 函数中,首先通过 xTaskCreate 创建了一个 Idle 任务,优先级为最低 0;(关于 Idle 任务,后面专门来讲);

调用 portDISABLE_INTERRUPTS(); 关闭全局中断,因为后面要初始化 TICK 中断;

接着初始化了全局变量:


   
   
  1. xNextTaskUnblockTime = portMAX_DELAY;
  2. xSchedulerRunning = pdTRUE;
  3. xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

下一个未阻塞的任务时间为 0xFFFF_FFFF;调度器启动的标志位为 TRUE,代表调度器已经初始化,Tick 的计数器为 0;

 

2、xPortStartScheduler

接着调用 xPortStartScheduler 来配置和体系结构相关调度,这里主要是一些和处理器相关的寄存器(比如 SYSTICK 等);这里还是以 Cortex-M3 作为例子,xPortStartScheduler 实现在 port.c 文件:


   
   
  1. /* Constants required to check the validity of an interrupt priority. */
  2. #define portFIRST_USER_INTERRUPT_NUMBER ( 16 )
  3. #define portNVIC_IP_REGISTERS_OFFSET_16 ( 0xE000E3F0 )
  4. #define portAIRCR_REG ( * ( ( volatile uint32_t * ) 0xE000ED0C ) )
  5. #define portMAX_8_BIT_VALUE ( ( uint8_t ) 0xff )
  6. #define portTOP_BIT_OF_BYTE ( ( uint8_t ) 0x80 )
  7. #define portMAX_PRIGROUP_BITS ( ( uint8_t ) 7 )
  8. #define portPRIORITY_GROUP_MASK ( 0x07UL << 8UL )
  9. #define portPRIGROUP_SHIFT ( 8UL )
  10. BaseType_t xPortStartScheduler( void )
  11. {
  12. #if(configASSERT_DEFINED == 1 )
  13. {
  14. volatile uint32_t ulOriginalPriority;
  15. /* 中断优先级寄存器0: PRI_0 */
  16. volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
  17. volatile uint8_t ucMaxPriorityValue;
  18. /* 这一大段代码用来确定一个最高ISR优先级,在这个ISR或者更低优先级的ISR中可以安全的调用以FromISR结尾的API函数.*/
  19. /* 保存中断优先级值,因为下面要覆写这个寄存器(PRI_0) */
  20. ulOriginalPriority = *pucFirstUserPriorityRegister;
  21. /* 确定有效的优先级位个数. 首先向所有位写1,然后再读出来,由于无效的优先级位读出为0,然后数一数有多少个1,就能知道有多少位优先级.*/
  22. *pucFirstUserPriorityRegister= portMAX_8_BIT_VALUE;
  23. ucMaxPriorityValue = *pucFirstUserPriorityRegister;
  24. /* 冗余代码,用来防止用户不正确的设置RTOS可屏蔽中断优先级值 */
  25. ucMaxSysCallPriority =configMAX_SYSCALL_INTERRUPT_PRIORITY &ucMaxPriorityValue;
  26. /* 计算最大优先级组值 */
  27. ulMaxPRIGROUPValue =portMAX_PRIGROUP_BITS;
  28. while( (ucMaxPriorityValue &portTOP_BIT_OF_BYTE ) ==portTOP_BIT_OF_BYTE )
  29. {
  30. ulMaxPRIGROUPValue--;
  31. ucMaxPriorityValue <<= ( uint8_t ) 0x01;
  32. }
  33. ulMaxPRIGROUPValue <<=portPRIGROUP_SHIFT;
  34. ulMaxPRIGROUPValue &=portPRIORITY_GROUP_MASK;
  35. /* 将PRI_0寄存器的值复原*/
  36. *pucFirstUserPriorityRegister= ulOriginalPriority;
  37. }
  38. #endif /*conifgASSERT_DEFINED */
  39. /* 将PendSV和SysTick中断设置为最低优先级*/
  40. portNVIC_SYSPRI2_REG |=portNVIC_PENDSV_PRI;
  41. portNVIC_SYSPRI2_REG |=portNVIC_SYSTICK_PRI;
  42. /* 启动系统节拍定时器,即SysTick定时器,初始化中断周期并使能定时器*/
  43. vPortSetupTimerInterrupt();
  44. /* 初始化临界区嵌套计数器 */
  45. uxCriticalNesting = 0;
  46. /* 启动第一个任务 */
  47. prvStartFirstTask();
  48. /* 永远不会到这里! */
  49. return 0;
  50. }

首先,如果定义 configASSERT_DEFINED 了的话,那么先:

volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) (portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );
   
   

根据 Cortex-M3 的数据手册可以知道,这个地方获取到的是:NVIC 中断优先级寄存器的基地址,Cortex-M3 的 NVIC 中断优先级寄存器定义如下:

CM3 中断优先级寄存器组  (0xE000_E400 ~ 0xE000_E4EF)
NameAccessBase AddressReset ValueDescription
PRI_0R/W0xE000_E4000 (8 bits)外中断 #0 的优先级
PRI_1R/W0xE000_E4010 (8 bits)外中断 #1 的优先级
.....R/W.....0 (8 bits).....
PRI_239R/W0xE000_E4EF0 (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 的值说了算;


   
   
  1. #define NVIC_PriorityGroup_0          ((u32)0x700) /* 0 bits for pre-emption priority 4 bits for subpriority */
  2. #define NVIC_PriorityGroup_1          ((u32)0x600) /* 1 bits for pre-emption priority 3 bits for subpriority */
  3. #define NVIC_PriorityGroup_2          ((u32)0x500) /* 2 bits for pre-emption priority 2 bits for subpriority */
  4. #define NVIC_PriorityGroup_3          ((u32)0x400) /* 3 bits for pre-emption priority 1 bits for subpriority */
  5. #define NVIC_PriorityGroup_4          ((u32)0x300) /* 4 bits for pre-emption priority 0 bits for subpriority */
SCB->AIRCR[10:8]
GroupAIRCR[10:8] ValuePRI_x bit[7:4] 分配情况分配结果
03‘b1110:40位抢占优先级,4位响应优先级
13‘b1101:31位抢占优先级,3位响应优先级
23‘b1012:22位抢占优先级,2位响应优先级
33‘b1003:13位抢占优先级,1位响应优先级
43‘b0114:04位抢占优先级,0位响应优先

Cortex-M3 中断优先级数值越大,表示优先级越低。而 FreeRTOS 的任务优先级则与之相反:优先级数值越大的任务,优先级越高。

好了,言归正传,这里应该说清楚为何要写进去 0xF,在读出来,就是因为这 8bit 并不是全部都用了,这样便可以得到最大支持的优先级个数 ucMaxPriorityValue

接着计算最大优先级组的值,从读出来有效的 PRI_0 的最高位开始判断(因为)优先级组和子优先级是从最高位开始;

接着配置 PendSV 和 SysTick 的优先级为最低:


   
   
  1. /* Make PendSV and SysTick the lowest priority interrupts. */
  2. portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
  3. portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

 

3、vPortSetupTimerInterrupt

调用 vPortSetupTimerInterrupt 配置 SysTick,这个是操作系统的心跳,和体系架构相关:


   
   
  1. void vPortSetupTimerInterrupt( void )
  2. {
  3. /* Calculate the constants required to configure the tick interrupt. */
  4. #if( configUSE_TICKLESS_IDLE == 1 )
  5. {
  6. ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
  7. xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
  8. ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
  9. }
  10. #endif /* configUSE_TICKLESS_IDLE */
  11. /* Stop and clear the SysTick. */
  12. portNVIC_SYSTICK_CTRL_REG = 0UL;
  13. portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
  14. /* Configure SysTick to interrupt at the requested rate. */
  15. portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
  16. portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
  17. }

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(); 启动第一个任务,它的实现使用汇编写的:


   
   
  1. __asm void prvStartFirstTask( void )
  2. {
  3. PRESERVE8
  4. /* Cortext-M3硬件中,0xE000ED08 地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址*/
  5. /* 将 0xE000ED08 加载到 R0 */
  6. ldr r0, = 0xE000ED08
  7. /* 将 0xE000ED08 中的值,也就是向量表的实际地址加载到 R0 */
  8. ldr r0, [r0]
  9. /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针MSP的初始值*/
  10. ldr r0, [r0]
  11. /* 将堆栈地址写入主堆栈指针 */
  12. msr msp, r0
  13. /* 使能全局中断*/
  14. cpsie i
  15. cpsie f
  16. dsb
  17. isb
  18. /* 调用SVC启动第一个任务 */
  19. svc 0
  20. nop
  21. nop
  22. }

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 中实现:


   
   
  1. __asm void vPortSVCHandler( void )
  2. {
  3. PRESERVE8
  4. ldr r3, =pxCurrentTCB /* pxCurrentTCB指向处于最高优先级的就绪任务TCB */
  5. ldr r1, [r3] /* 获取任务TCB地址 */
  6. ldr r0, [r1] /* 获取任务TCB的第一个成员,即当前堆栈栈顶pxTopOfStack */
  7. ldmia r0!, {r4-r11} /* 出栈,将寄存器r4~r11出栈 */
  8. msr psp, r0 /* 最新的栈顶指针赋给线程堆栈指针PSP */
  9. isb
  10. mov r0, # 0
  11. msr basepri, r0
  12. orr r14, # 0xd /* 这里0x0d表示:返回后进入线程模式,从进程堆栈中做出栈操作,返回Thumb状态*/
  13. bx r14
  14. }

首先还是 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 的代码;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值