任务调度器开启

在 main()函数中先创建一个开始任务,后面紧接着调用函数 vTaskStartScheduler()。这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c中有定义。

  1. void vTaskStartScheduler( void )
  2. {
  3. BaseType_t xReturn;
  4. /**
  5. * 创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,
  6. * 优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低
  7. */
  8. xReturn = xTaskCreate( prvIdleTask,
  9. "IDLE", configMINIMAL_STACK_SIZE,
  10. ( void * ) NULL,
  11. ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
  12. &xIdleTaskHandle );
  13. //使用软件定时器使能
  14. #if ( configUSE_TIMERS == 1 )
  15. {
  16. if( xReturn == pdPASS )
  17. {
  18. //如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务
  19. xReturn = xTimerCreateTimerTask();
  20. }
  21. else
  22. {
  23. mtCOVERAGE_TEST_MARKER();
  24. }
  25. }
  26. #endif /* configUSE_TIMERS */
  27. //空闲任务和定时器任务创建成功。
  28. if( xReturn == pdPASS )
  29. {
  30. //关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
  31. portDISABLE_INTERRUPTS();
  32. //使能 NEWLIB
  33. #if ( configUSE_NEWLIB_REENTRANT == 1 )
  34. {
  35. _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
  36. }
  37. #endif /* configUSE_NEWLIB_REENTRANT */
  38. xNextTaskUnblockTime = portMAX_DELAY;
  39. //变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。
  40. xSchedulerRunning = pdTRUE;
  41. xTickCount = ( TickType_t ) 0U;
  42. /**
  43. * 当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时
  44. * 需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。
  45. */
  46. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
  47. //调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等。
  48. if( xPortStartScheduler() != pdFALSE ) {
  49. //如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
  50. }
  51. else
  52. {
  53. //不会运行到这里,除非调用函数 xTaskEndScheduler()。
  54. }
  55. }
  56. else
  57. {
  58. //程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
  59. //空闲任务或者定时器任务的时候没有足够的内存。
  60. configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
  61. }
  62. //防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提
  63. //示 xIdleTaskHandle 未使用。
  64. ( void ) xIdleTaskHandle;
  65. }

初始化调度器启动的硬件

FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成。

  1. BaseType_t xPortStartScheduler( void )
  2. {
  3. /******************************************************************/
  4. /****************此处省略一大堆的条件编译代码**********************/
  5. /*****************************************************************/
  6. //设置 PendSV 的中断优先级,为最低优先级
  7. portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
  8. //设置滴答定时器的中断优先级,为最低优先级。
  9. portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
  10. //调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断
  11. vPortSetupTimerInterrupt();
  12. //初始化临界区嵌套计数器。
  13. uxCriticalNesting = 0;
  14. //调用函数 prvEnableVFP()使能 FPU。
  15. prvEnableVFP();
  16. // 设置寄存器 FPCCR 的 bit31 和 bit30 都为 1,这样 S0~S15 和 FPSCR 寄存器在异常入口和退出时的壮态自动保存和恢复。并且异常流程使用惰性压栈的特性以保证中断等待。
  17. *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
  18. //启动第一个任务
  19. prvStartFirstTask();
  20. //代码正常执行的话是不会到这里的!
  21. return 0;
  22. }

使能 FPU

在函数 xPortStartScheduler()中会通过调用 prvEnableVFP()来使能 FPU,这个函数是汇编形式的,在文件 port.c 中有定义

  1. __asm void prvEnableVFP( void )
  2. {
  3. PRESERVE8
  4. ldr.w r0, =0xE000ED88 ;R0=0XE000ED88
  5. ldr r1, [r0] ;从 R0 的地址读取数据赋给 R1
  6. orr r1, r1, #( 0xf << 20 ) ;R1=R1|(0xf<<20)
  7. str r1, [r0] ;R1 中的值写入 R0 保存的地址中
  8. bx r14
  9. nop
  10. }

利用寄存器 CPACR 可以使能或禁止 FPU,此寄存器的地址为 0XE000ED88(具体参考《权威指南》 第13章浮点运算),此寄存器的 CP10(bit20 和 bit21)和 CP11(bit22和 bit23)用于控制 FPU。这 4 个 bit 的具体含义请参考《权威指南》,通常将这 4 个 bit 都设置为1 来开启 FPU,表示全访问。此行代码将地址 0XE000ED88 保存在寄存器 R0 中。

启动第一个任务

函数 prvStartFirstTask()用于启动第一个任务,这是一个汇编函数。

  1. __asm void prvStartFirstTask( void )
  2. {
  3. PRESERVE8
  4. ldr r0, =0xE000ED08 ;R0=0XE000ED08
  5. ldr r0, [r0] ;取 R0 所保存的地址处的值赋给 R0
  6. ldr r0, [r0] ;获取 MSP 初始值
  7. msr msp, r0 ;复位 MSP
  8. cpsie I ;使能中断(清除 PRIMASK)
  9. cpsie f ;使能中断(清除 FAULTMASK)
  10. dsb ;数据同步屏障
  11. isb ;指令同步屏障
  12. svc 0 ;触发 SVC 中断(异常)
  13. nop
  14. nop
  15. }
  • 将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000)开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定义向量表。
  • 向量表的起始地址保存的就是主栈指针MSP 的初始值
  • 调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。

    SVC 中断服务函数

    在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的,SVC 中断服务函数应该为 SVC_Handler(),但是FreeRTOSConfig.h 中通过#define的方式重新定义为了xPortPendSVHandler()

    1. #define xPortPendSVHandler PendSV_Handler
    1. __asm void vPortSVCHandler( void )
    2. {
    3. PRESERVE8
    4. ldr r3, =pxCurrentTCB ;R3=pxCurrentTCB 的地址
    5. ldr r1, [r3] ;取 R3 所保存的地址处的值赋给 R1
    6. ldr r0, [r1] ;取 R1 所保存的地址处的值赋给 R0
    7. ldmia r0!, {r4-r11, r14} ;出栈 R4~R11 R14
    8. msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈
    9. isb ;指令同步屏障
    10. mov r0, #0 ;R0=0
    11. msr basepri, r0 ;寄存器 basepri=0,开启中断
    12. bx r14
    13. }
  • 获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。

  • 取 R3 所保存的地址处的值赋给 R1。通过这一步就获取到了当前任务的任务控制块的存储地址。
  • 取 R3 所保存的地址处的值赋给 R0,我们知道任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址得到的就是栈顶指针所指向的地址
  • 任务所对应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄存器值
  • R4~R11,R14 这些寄存器出栈。通过这一步我们就从任务堆栈中将 R4~R11,R14 这几个寄存器的值给恢复了,注意 R14 的值为 0XFFFFFFFD,这个值就是我们在初始化任务堆栈的时候保存的 EXC_RETURN 的值。R0~R3,R12,PC,xPSR 这些寄存器怎么没有恢复?这是因为这些寄存器会在退出中断的时候 MCU 自动出栈(恢复)的,而 R4~R11 需要由用户手动出栈。
    08.任务的调度开启过程 - 图1
  • 设置进程栈指针 PSP
  • 设置寄存器 R0 为 0。
  • 设置寄存器 BASEPRI 为 R0,也就是 0,打开中断
  • 执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开始运行

    空闲任务

    vTaskStartScheduler()说过,此函数会创建一个名为“IDLE”的任务,这个任务叫做空闲任务。顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是 FreeRTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他的事情,如下:

  • 判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。

  • 运行用户设置的空闲任务钩子函数。
  • 判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理

空闲任务的任务优先级是最低的,为 0,任务函数为 prvIdleTask()