5. FreeRTOS的启动流程

在目前的RTOS中,主要有两种比较流行的启动方式,暂时还没有看到第三种,接下来将通过伪代码的方式来讲解下这两种启动方式的区别, 然后再具体分析下FreeRTOS的启动流程。

5.1. 万事俱备,只欠东风

第一种称之为万事俱备,只欠东风法。这种方法是在main函数中将硬件初始化,RTOS系统初始化,所有任务的创建这些都弄好, 这个称之为万事都已经准备好。最后只欠一道东风,即启动RTOS的调度器,开始多任务的调度,具体的伪代码实现见 代码清单1。

代码清单1万事俱备,只欠东风法伪代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 int main (void)
 {
     /* 硬件初始化 */
     HardWare_Init();

     /* RTOS 系统初始化 */
     RTOS_Init();

     /* 创建任务,但任务不会执行,因为调度器还没有开启
     RTOS_TaskCreate(Task);

     /* ......创建各种任务 */

     /* 启动RTOS,开始调度 */
     RTOS_Start();
 }

 voidTask1( void *arg )
 {
     while (1)
     {
         /* 任务实体,必须有阻塞的情况出现 */
     }
 }
  • 第4行:硬件初始化。硬件初始化这一步还属于裸机的范畴,我们可以把需要使用到的硬件都初始化好而且测试好,确保无误。

  • 第7行:RTOS系统初始化。比如RTOS里面的全局变量的初始化,空闲任务的创建等。不同的RTOS,它们的初始化有细微的差别。

  • 第10行 :创建各种任务。这里把所有要用到的任务都创建好,但还不会进入调度,因为这个时候RTOS的调度器还没有开启。

  • 第15行:启动RTOS调度器,开始任务调度。这个时候调度器就从刚刚创建好的任务中选择一个优先级最高的任务开始运行。

  • 第18-24行 :任务实体通常是一个不带返回值的无限循环的C函数,函数体必须有阻塞的情况出现, 不然任务(如果优先权恰好是最高)会一直在while循环里面执行,导致其他任务没有执行的机会。

5.2. 小心翼翼,十分谨慎

第二种称之为小心翼翼,十分谨慎法。这种方法是在main函数中将硬件和RTOS系统先初始化好, 然后创建一个启动任务后就启动调度器,然后在启动任务里面创建各种应用任务,当所有任务都创建成功后, 启动任务把自己删除,具体的伪代码实现见 代码清单2

代码清单2小心翼翼,十分谨慎法伪代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 int main (void)
 {
     /* 硬件初始化 */
     HardWare_Init();

     /* RTOS 系统初始化 */
     RTOS_Init();

     /* 创建一个任务 */
     RTOS_TaskCreate(AppTaskCreate);

     /* 启动RTOS,开始调度 */
     RTOS_Start();
 }

 /* 起始任务,在里面创建任务 */
 voidAppTaskCreate( void *arg )
 {
     /* 创建任务1,然后执行 */
     RTOS_TaskCreate(Task1);

     /* 当任务1阻塞时,继续创建任务2,然后执行 */
     RTOS_TaskCreate(Task2);

     /* ......继续创建各种任务 */

     /* 当任务创建完成,删除起始任务 */
     RTOS_TaskDelete(AppTaskCreate);
 }

 void Task1( void *arg )
 {
     while (1)
     {
         /* 任务实体,必须有阻塞的情况出现 */
     }
 }

 void Task2( void *arg )
 {
     while (1)
     {
         /* 任务实体,必须有阻塞的情况出现 */
     }
 }
  • 第4行 :硬件初始化。来到硬件初始化这一步还属于裸机的范畴, 我们可以把需要使用到的硬件都初始化好而且测试好,确保无误。

  • 第7行 :RTOS系统初始化。比如RTOS里面的全局变量的初始化, 空闲任务的创建等。不同的RTOS,它们的初始化有细微的差别。

  • 第10行 :创建一个开始任务。然后在这个初始任务里面创建各种应用任务。

  • 第13行 :启动RTOS调度器,开始任务调度。这个时候调度器就去执行刚刚创建好的初始任务。

  • 第17行:我们通常说任务是一个不带返回值的无限循环的C函数, 但是因为初始任务的特殊性,它不能是无限循环的,只执行一次后就关闭。在初始任务里面我们创建我们需要的各种任务。

  • 第20-25行:创建任务。每创建一个任务后它都将进入就绪态, 系统会进行一次调度,如果新创建的任务的优先级比初始任务的优先级高的话,那将去执行新创建的任务, 当新的任务阻塞时再回到初始任务被打断的地方继续执行。反之,则继续往下创建新的任务,直到所有任务创建完成。

  • 第28行:各种应用任务创建完成后,初始任务自己关闭自己,使命完成。

  • 第31-45行:任务实体通常是一个不带返回值的无限循环的C函数,函数体必须有阻塞的情况出现, 不然任务(如果优先权恰好是最高)会一直在while循环里面执行,其他任务没有执行的机会。

5.3. 孰优孰劣

那有关这两种方法孰优孰劣?暂时没发现,我个人还是比较喜欢使用第一种。STM32CubeIDE生成的FreeRTOS工程也是使用第一种方式, LiteOS和ucos第一种和第二种都可以使用,由用户选择,RT-Thread默认使用第二种。 在了解FreeRTOS启动之前先了解下CMSIS-RTOS与线程相关的数据结构与API。

5.4. CMSIS-RTOS与线程相关的数据结构与API

5.4.1. osThreadAttr_t结构体

CMSIS-RTOS使用osThreadAttr_t结构体描述线程属性,其定义如下

cmsis_os2.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 //描述线程属性的结构体
 typedef struct {
     const char                   *name;   ///< 线程名
     uint32_t                 attr_bits;   ///< 在FreeRTOS内核中没有使用
     void                      *cb_mem;    ///< 控制块内存
     uint32_t                   cb_size;   ///< 控制块大小
     void                   *stack_mem;    ///< 栈地址
     uint32_t                stack_size;   ///< 栈大小
     osPriority_t              priority;   ///< 最初的线程优先级 (默认值: osPriorityNormal)
     TZ_ModuleId_t            tz_module;   ///< 在FreeRTOS内核中没有使用
     uint32_t                  reserved;   ///< 保留 (必须为0)
 } osThreadAttr_t;
  • name :由于描述线程名。默认为NULL

  • attr_bits :用于描述线程的类型,在FreeRTOS内核中并没有使用

  • cb_memcb_size :用于静态创建线程时内存控制块需手动指定。

  • stack_mem :用于静态创建线程需手动指定。

  • stack_size :栈大小。默认栈大小为128字节

  • priority :描述线程优先级。默认任务优先级为osPriorityNormal(24)。

  • tz_module :在FreeRTOS内核中没有使用

  • reserved :保留必须为0。

5.4.2. osKernelInitialize函数

CMSIS-RTOS使用osKernelInitialize函数用来初始化RTOS内核状态,其函数定义如下

cmsis_os2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 osStatus_t osKernelInitialize (void) {
 osStatus_t stat;

 if (IS_IRQ()) {
     stat = osErrorISR;
 }
 else {
     if (KernelState == osKernelInactive) {
     #if defined(USE_FREERTOS_HEAP_5) && (HEAP_5_REGION_SETUP == 1)
         vPortDefineHeapRegions (configHEAP_5_REGIONS);
     #endif
     KernelState = osKernelReady;
     stat = osOK;
     } else {
     stat = osError;
     }
 }

 return (stat);
 }
  • 第4行,使用IS_IRQ判断当前的运行环境是不是在中断中,当在中断函数中调用osKernelInitialize函数则直接返回osErrorISR。

  • 第8-16行,判断当前内核的状态,在程序中只有内核处于未激活状态时调用osKernelInitialize函数才能生效。 否则直接返回osErrorISR。

  • 第9行,FreeRTOS使用5种不同的动态内存分配策略,当使用heap_5时,则需要先使用vPortDefineHeapRegions进行初始化, 关于FreeRTOS的动态内存分配在以后章节或有详细说明。

  • 第12行,将内核状态设置为就绪态。

5.4.3. osThreadNew函数

CMSIS-RTOS使用osThreadNew函数用途创建线程,其函数原型如下

cmsis_os2.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
 //创建新线程  传入参数为:线程函数、线程参数、线程控制块
 osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr) {
 const char *name;
 uint32_t stack;
 TaskHandle_t hTask;
 UBaseType_t prio;
 int32_t mem;                        //标记创建线程方式(动态、静态)

 hTask = NULL;

 //判断是否在中断中使用该函数以及线程函数是否为空
 if (!IS_IRQ() && (func != NULL)) {
     stack = configMINIMAL_STACK_SIZE;               //默认线程栈大小为128
     prio  = (UBaseType_t)osPriorityNormal;  //默认线程优先级为24

     name = NULL;            //默认线程名为空
     mem  = -1;

     //线程控制块不为空
     if (attr != NULL) {

     //设置线程名
     if (attr->name != NULL) {
         name = attr->name;
     }

     //设置线程优先级
     if (attr->priority != osPriorityNone) {
         prio = (UBaseType_t)attr->priority;
     }

     //线程优先级出错判断
     if ((prio < osPriorityIdle) || (prio > osPriorityISR) || ((attr->attr_bits & osThreadJoinable) == osThreadJoinable)) {
         return (NULL);
     }

     //设置栈大小
     if (attr->stack_size > 0U) {
         /* In FreeRTOS stack is not in bytes, but in sizeof(StackType_t) which is 4 on ARM ports.       */
         /* Stack size should be therefore 4 byte aligned in order to avoid division caused side effects */
         stack = attr->stack_size / sizeof(StackType_t);
     }

     //判断线程控制块创建线程的方式
     if ((attr->cb_mem    != NULL) && (attr->cb_size    >= sizeof(StaticTask_t)) &&
         (attr->stack_mem != NULL) && (attr->stack_size >  0U)) {
         mem = 1;
     }
     else {
         if ((attr->cb_mem == NULL) && (attr->cb_size == 0U) && (attr->stack_mem == NULL)) {
         mem = 0;
         }
     }
     }
     else {
     mem = 0;
     }

     //使用静态创建线程
     if (mem == 1) {
     hTask = xTaskCreateStatic ((TaskFunction_t)func, name, stack, argument, prio, (StackType_t  *)attr->stack_mem,
                                                                                     (StaticTask_t *)attr->cb_mem);
     }
     //使用动态创建线程
     else {
     if (mem == 0) {
         if (xTaskCreate ((TaskFunction_t)func, name, (uint16_t)stack, argument, prio, &hTask) != pdPASS) {
         hTask = NULL;
         }
     }
     }
 }
 //将使用FreeRTOS创建线程得到的线程id返回
 return ((osThreadId_t)hTask);
 }

参数

  • func : 线程的函数入口

  • argument :线程函数参数

  • attr :osThreadAttr_t结构体(线程控制块)

返回值 :成功则返回线程ID,失败返回NULL。

当attr形参中的cb_mem、cb_size、stack_mem、stack_size四个成员中只要有一个成员为空则使用动态内存分配的 形式创建线程,相反如果我们需要使用静态内存分配则需要对以上四个成员赋予正确的值。

5.4.4. osKernelStart函数

osKernelStart函数定义如下所示

cmsis_os2.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 osStatus_t osKernelStart (void) {
 osStatus_t stat;

 //判读是否在中断中执行osKernelStart函数
 if (IS_IRQ()) {
     stat = osErrorISR;
 }
 else {
     if (KernelState == osKernelReady) {
     /* Ensure SVC priority is at the reset value */
     SVC_Setup();
     /* Change state to enable IRQ masking check */
     KernelState = osKernelRunning;   // 设置内核状态为运行态
     /* Start the kernel scheduler */
     vTaskStartScheduler();          //FreeRTOS启动任务调度器函数
     stat = osOK;
     } else {
     stat = osError;
     }
 }

 return (stat);
 }

osKernelStart函数用于启动任务调度器,该函数不能在中断中使用, 在创建完任务的时候,我们需要开启调度器,因为创建仅仅是把任务添加到系统中,还没真正调度, 并且空闲任务也没实现,定时器任务也没实现,这些都是在开启调度函数vTaskStartScheduler()中实现的。 为什么要有空闲任务?因为FreeRTOS一旦启动,就必须保证系统中每时每刻都有一个任务处于运行态(Runing), 并且空闲任务不可以被挂起与删除,空闲任务的优先级是最低的,以便系统中其他任务能随时抢占空闲任务的CPU使用权。 这些都是系统必要的东西,也无需用户自己实现,FreeRTOS全部帮我们搞定了。 处理完这些必要的东西之后,系统才真正开始启动。

在Cortex-M架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick:

SVC(系统服务调用,亦简称系统调用)用于任务启动,有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数, 用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个 SVC 异常。

PendSV(可挂起系统调用)用于完成任务切换,它是可以像普通的中断一样被挂起的, 它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕, 这样子产生的PendSV中断就不会打断其他中断的运行。

SysTick用于产生系统节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次SysTick中断, 下一个任务将获得一个时间片。关于详细的SVC、PendSV异常描述,推荐《Cortex-M3/M4权威指南》一书的“异常”部分。

5.5. FreeRTOS的启动流程

本小节内容主要以上个章节的实验代码进行讲解。

当系统开始工作时,第一个执行的是由汇编语言编写的.s启动文件,最后会调用C库函数__main, __main函数的主要工作是初始化系统的堆和栈,最后调用C中的main函数,从而去到C的世界。

5.5.1. main函数

main.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 int main(void)
 {
     HAL_Init();                    //初始化HAL库

     if(IS_ENGINEERING_BOOT_MODE())  //判断是否处于工程模式下运行
     {
         SystemClock_Config();   //配置时钟
     }

     MX_GPIO_Init();         //初始化LED相关GPIO引脚,

     osKernelInitialize();   //初始化内核状态
     MX_FREERTOS_Init();             //创建任务
     osKernelStart();        //启动任务调度,当启动任务调度之后,便跳转到任务函数中。

     while (1)
     {
     }
 }

main函数的内容并不多,前半部分的内容主要用于初始化硬件相关的内容如时钟、GPIO引脚配置。 后半部分初始化内核状态、创建任务并启动任务调度。相关函数内容的作用我们在前面已经讲到, 此处我们需要关心的是MX_FREERTOS_Init函数的内容。MX_FREERTOS_Init函数所在的文件为 app_freertos.c。

5.5.2. app_freertos.c文件

app_freertos.c文件是我们使用FreeRTOS工程的核心,当我们在STM32CubeIDE上对FreeRTOS进行一切 配置都会生成到这个文件中,我们的编程重点也是放在此处。这个app_freertos.c文件如下所示。

app_freertos.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 #include "FreeRTOS.h"
 #include "task.h"
 #include "main.h"
 #include "cmsis_os.h"

 osThreadId_t LED1TaskHandle;
 const osThreadAttr_t LED1Task_attributes = {
     .name = "LED1Task",
     .priority = (osPriority_t) osPriorityNormal,
     .stack_size = 128 * 4
 };

 osThreadId_t LED2TaskHandle;
 const osThreadAttr_t LED2Task_attributes = {
     .name = "LED2Task",
     .priority = (osPriority_t) osPriorityLow,
     .stack_size = 128 * 4
 };

 void LED1_Task(void *argument);
 void LED2_Task(void *argument);

 void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

 void MX_FREERTOS_Init(void) {

     LED1TaskHandle = osThreadNew(LED1_Task, NULL, &LED1Task_attributes);
     LED2TaskHandle = osThreadNew(LED2_Task, NULL, &LED2Task_attributes);
 }

 void LED1_Task(void *argument)
 {
     for(;;)
     {
         HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
         osDelay(200);
     }
 }

 void LED2_Task(void *argument)
 {
     for(;;)
     {
         HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
         osDelay(500);
     }
 }
  • 第6-18行,定义了与线程相关的线程句柄以及线程控制块。

  • 第20-21行,定义了两个线程入口。

  • 第25–29行,使用osThreadNew函数创建LED1_Task和LED2_Task两个线程。

  • 第31-47行,实现两个线程的具体内容。

在app_freertos.c文件中,大部分的内容STM32CubeIDE已经帮我们做好了, 我们需要做的内容很简单,那便是往LED1_Task以及LED2_Task线程函数中添加我们的功能代码。