4.1 本章目标

在这章中, 我们会创建两个任务,并让这两个任务不断地切换, 任务的主体都是让一个变量按照一定的频率翻转,通过 KEIL 的软件仿真功能,在逻辑分析仪中观察变量的波形变化,最终的波形图具体见图:
image.png
其实, 图 7-1 的波形图的效果,并不是真正的多任务系统中任务切换的效果图,这个效果其实可以完全由裸机代码来实现。

  1. /* flag 必须定义成全局变量才能添加到逻辑分析仪里面观察波形
  2. * 在逻辑分析仪中要设置以 bit 的模式才能看到波形,不能用默认的模拟量
  3. */
  4. uint32_t flag1;
  5. uint32_t flag2;
  6. /* 软件延时,不必纠结具体的时间 */
  7. void delay( uint32_t count )
  8. {
  9. for (; count!=0; count--);
  10. }
  11. int main(void)
  12. {
  13. /* 无限循环,顺序执行 */
  14. for (;;) {
  15. flag1 = 1;
  16. delay( 100 );
  17. flag1 = 0;
  18. delay( 100 );
  19. flag2 = 1;
  20. delay( 100 );
  21. flag2 = 0;
  22. delay( 100 );
  23. }
  24. }

在多任务系统中,两个任务不断切换的效果图应该像图 7-2 所示那样,即两个变量的波形是完全一样的,就好像 CPU 在同时干两件事一样,这才是多任务的意义。虽然两者的波形图一样,但是,代码的实现方式是完全不一样的,由原来的顺序执行变成了任务的主动切换,这是根本区别。
我们先掌握好任务是如何切换,在后面章节中,我们会陆续的完善功能代码,加入系统调度,实现真正的多任务。

4.2 什么任务

在裸机系统中, 系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。

  1. void task_entry(void * parg)
  2. {
  3. /* 任务主体,无限循环且不能返回 */
  4. for(;;){
  5. /* 任务主体代码 */
  6. }
  7. }

4.3 创建任务

4.3.1 创建任务栈

  • 在裸机中,全局变量,函数调用,中断等都有系统帮忙弄好了。在操作系统不行。
  • 在RTOS中,必须搞清楚变量怎么存储的。
  • 裸机中有栈、堆。在_main中进行的初始化。
  • 在多任务中,每个人人呢我独立,互不干扰,要为每个任务分配独立的空间。这个栈一般都是预先定义好的全局数组,也可是动态分配的空间。

本章我们要实现两个变量按照一定的频率轮流的翻转,每个变量对应一个任务,那么就需要定义两个任务栈

  1. #define TASK1_STACK_SIZE 128 // (1)
  2. StackType_t Task1Stack[TASK1_STACK_SIZE]; // (2)
  3. #define TASK2_STACK_SIZE 128
  4. StackType_t Task2Stack[TASK2_STACK_SIZE];

(1):任务栈其实就是一个预先定义好的全局数据,数据类型为StackType_t,大小由 TASK1_STACK_SIZE 这个宏来定义, 默认为 128,单位为字,即 512字节,这也是 FreeRTOS 推荐的最小的任务栈。

4.3.2 定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。

  1. /* 任务1 */
  2. void Task1_Entry( void *p_arg )
  3. {
  4. for( ;; )
  5. {
  6. flag1 = 1;
  7. delay( 100 );
  8. flag1 = 0;
  9. delay( 100 );
  10. }
  11. }
  12. /* 任务2 */
  13. void Task2_Entry( void *p_arg )
  14. {
  15. for( ;; )
  16. {
  17. flag2 = 1;
  18. delay( 100 );
  19. flag2 = 0;
  20. delay( 100 );
  21. }
  22. }

4.3.3 定义任务控制块

程序的主体是 CPU 按照顺序执行的。而在多任务系统中, 任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称, 任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。

  1. typedef struct tskTaskControlBlock
  2. {
  3. volatile StackType_t *pxTopOfStack; /* 栈顶(1) */
  4. ListItem_t XstateListItem; /* 任务节点(2) */
  5. StackType_t *pxStack; /* 任务栈起始地址(3) */
  6. char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名称,字符串形式 */
  7. }tskTCB;
  8. typedef tskTCB TCB_t; //(5)
  1. 栈顶指针,作为 TCB 的第一个成员。
  2. 任务节点,这是一个内置在 TCB 控制块中的链表节点,通过这个节点,可以将任务控制块挂接到各种链表中。这个节点就类似晾衣架的钩子, TCB 就是衣服。
  3. 任务栈起始地址
    任务名称,字符串形式, 长度由宏 configMAX_TASK_NAME_LEN来控制, 该宏在 FreeRTOSConfig.h 中定义,默认为 16
  4. 数据类型重定义

添加任务块:

  1. TCB_t Task1TCB;
  2. TCB_t Task2TCB;

4.3.4 实现任务创建函数

任务的栈, 任务的函数实体, 任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic()来实现,该函数在 task.c(task.c 第一次使用需要自行在文件夹 freertos 中新建并添加到工程的 freertos/source 组)中定义, 在 task.h 中声明, 所有跟任务相关的函数都在这个文件定义。

1. xTaskCreateStaic函数

  1. #if (configSUPPORT_STATIC_ALLOCATION == 1 ) //1
  2. TaskFunction_t xTaskCreateStaic( TaskFunction_t pxTaskCode, //2
  3. const char * const pcName, //3
  4. const uint32_t ulStackDepth, //4
  5. void * const pvParameters, //5
  6. StackType_t * const puxStackBuffer,//6
  7. TCB_t * const pxTaskBuffer ) //7
  8. {
  9. TCB_t *pxNewTCB;
  10. TaskHandle_t xReturn; //8
  11. if( (pxTaskBuffer != NULL) && (puxStackBuffer != NULL))
  12. {
  13. pxNewTCB = (TCB_t *)pxTaskBuffer;
  14. pxNewTCB->pxStack = (StackType_t *)puxStackBuffer;
  15. /* 创建新的任务 */ //9
  16. prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
  17. pcName, /* 任务名称,字符串形式 */
  18. ulStackDepth, /* 任务栈大小,单位为字 */
  19. pvParameters, /* 任务形参 */
  20. &xReturn, /* 任务句柄 */
  21. pxNewTCB ); /* 任务栈起始地址 */
  22. }
  23. else
  24. {
  25. xReturn = NULL;
  26. }
  27. /* 返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块 */
  28. return xReturn;
  29. }
  30. #endif
  1. FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。 动态创建时,任务控制块和栈的内存是创建任务时动态分配的, 任务删除时,内存可以释放。 静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内 存 , 任 务 删 除 时 , 内 存 不 能 释 放 。 目 前 我 们 以 静 态 创 建 为 例 来 讲 解 ,
    configSUPPORT_STATIC_ALLOCATION 在 FreeRTOSConfig.h 中定义,我们配置为 1 。
  2. 任务入口,即任务的函数名称。 TaskFunction_t 是在 projdefs.h(projdefs.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件)中重定义的一个数据类型,实际就是空指针 ```c

    ifndef PROJDEFS_H

    define PROJDEFS_H

typedef void (TaskFunction_t)( void );

define pdFALSE ( ( BaseType_t ) 0 )

define pdTRUE ( ( BaseType_t ) 1 )

define pdPASS ( pdTRUE )

define pdFAIL ( pdFALSE )

endif / PROJDEFS_H /

  1. 3. 任务名称,字符串形式,方便调试。
  2. 3. 任务栈大小,单位为字。
  3. 3. 任务形参
  4. 3. 任务起始栈地址
  5. 3. 任务控制块指针
  6. 3. 定义一个任务句柄 xReturn 任务句柄用于指向任务的 TCB 任务句柄的数据类型为 TaskHandle_t task.h 中定义,实际上就是一个空指针。
  7. ```c
  8. /* 任务句柄 */
  9. typedef void * TaskHandle_t;
  1. 调用 prvInitialiseNewTask()函数,创建新任务,该函数在 task.c 实现。

2. prvInitialiseNewTask()函数

  1. static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, /* 任务入口 */
  2. const char * const pcName, /* 任务名称,字符串形式 */
  3. const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
  4. void * const pvParameters, /* 任务形参 */
  5. TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
  6. TCB_t *pxNewTCB ) /* 任务控制块指针 */ //6
  7. {
  8. StackType_t *pxTopOfStack;
  9. UBaseType_t x;
  10. /* 获取栈顶地址 */ //(7)
  11. pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - ( uint32_t )1);
  12. /* 向下做 8 字节对齐 */ //(8)
  13. pxTopOfStack = (StackType_t *) (((uint32_t) pxTopOfStack) & (~((uint32_t) 0x0007)));
  14. /* 将任务的名字存储在 TCB 中 */ //(9)
  15. for ( x = (UBaseType_t) 0; x < (UBaseType_t) configM_TASK_NAME_LEN; x++)
  16. {
  17. pxNewTCB->pcTaskName[ x ] = pcName[ x ];
  18. if( pcName[ x ] == 0x00)
  19. {
  20. break;
  21. }
  22. }
  23. /* 任务名字的长度不能超过 configMAX_TASK_NAME_LEN */ //(10)
  24. pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
  25. /* 初始化 TCB 中的 xStateListItem 节点 */ //(11)
  26. vListInitialiseItem( &(pxNewTCB)->xStateListItem);
  27. /* 设置 xStateListItem 节点的拥有者 */ //(12)
  28. listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
  29. /* 初始化任务栈 */ //(13)
  30. pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack,
  31. pxTaskCode,
  32. pvParameters);
  33. /* 让任务句柄指向任务控制块 */
  34. if ((void *) pxTopOfStack != NULL) //(14)
  35. {
  36. *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
  37. }
  38. }
  1. 任务入口
  2. 任务名称,字符串形式
  3. 任务栈大小,单位为字
  4. 任务形参
  5. 任务句柄
  6. 任务控制块指针
  7. 获取栈顶地址 使用的是满减栈
  8. 将栈顶指针向下做 8 字节对齐。 在 Cortex-M3(Cortex-M4 或Cortex-M7)内核的单片机中,因为总线宽是 32 位的,通常只要栈保持 4 字节对齐就行,可这样为啥要 8 字节?难道有哪些操作是 64 位的?确实有,那就是浮点运算,所以要 8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐的时候,指针不会移动,如果不是8 字节对齐的,在做向下 8 字节对齐的时候,就会空出几个字节,不会使用,比如当pxTopOfStack 是 33,明显不能整除 8,进行向下 8 字节对齐就是 32,那么就会空出一个字节不使用。

    1. 1、原理
    2. 2字节对齐:要求地址位为2, 4, 6, 8…,地址的二进制最后一位为021次方)。
    3. 4字节对齐:要求地址位为481216…,地址的二进制最后两位为022次方)。
    4. 8字节对齐:要求地址位为8162432…,地址的最后三位为023次方)。
    5. 16字节对齐:要求地址位为16324864…,地址的最后四位为024次方)。
  9. 将任务的名字存储在 TCB 中

  10. 任务名字的长度不能超过 configMAX_TASK_NAME_LEN, 并以’\0’结尾
  11. 初始化 TCB 中的 xStateListItem 节点, 即初始化该节点所在的链表为空,表示节点还没有插入任何链表
  12. 设置 xStateListItem 节点的拥有者, 即拥有这个节点本身的 TCB
  13. 调用 pxPortInitialiseStack()函数初始化任务栈, 并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。该函数在 port.c( port.c 第一次使用需要在freertos\portable\RVDS\ARM_CM3(ARM_CM4 或 ARM_CM7) 文件夹下面新建然后添加到工程 freertos/source 这个组文件)中定义,

之后的代码只注明比较关键的部分。

3. pxPortInitialiseStack()函数

  1. #include "FreeRTOS.h"
  2. #include "task.h"
  3. #include "ARMCM3.h"
  4. #define portINITIAL_XPSR ( 0x01000000 )
  5. #define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL )
  6. static void prvTaskExitError(void)
  7. {
  8. /* 函数停止在这里 */
  9. for(;;);
  10. }
  11. StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack,
  12. TaskFunction_t pxCode,
  13. void *pvParameters )
  14. {
  15. /* 异常发生时,自动加载到 CPU 寄存器的内容 */ // 1
  16. pxTopOfStack--;
  17. *pxTopOfStack = portINITIAL_XPSR; // 2
  18. pxTopOfStack--;
  19. *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; // 3
  20. pxTopOfStack--;
  21. // 任务的返回地址,通常任务是不会返回的,如果返回了就跳转到
  22. //prvTaskExitError, 该函数是一个无限循环
  23. *pxTopOfStack = ( StackType_t ) prvTaskExitError;
  24. pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为 0 */
  25. *pxTopOfStack = ( StackType_t ) pvParameters; // 4
  26. /* 异常发生时,手动加载到 CPU 寄存器的内容 */
  27. pxTopOfStack -= 8; //5
  28. /* 返回栈顶指针,此时 pxTopOfStack 指向空闲栈 */
  29. return pxTopOfStack;
  30. }

image.png

  1. 异常发生时, CPU 自动从栈中加载到 CPU 寄存器的内容。包括 8
    个寄存器,分别为 R0、 R1、 R2、 R3、 R12、 R14、 R15 和 xPSR 的位 24,且顺序不能变。
  2. xPSR 的 bit24 必须置 1,即 0x01000000
  3. 任务入口地址
  4. R12, R3, R2 and R1 默认初始化为 0。
  5. 异常发生时,需要手动加载到 CPU 寄存器的内容, 总共有 8 个,分别为 R4、 R5、 R6、 R7、 R8、 R9、 R10 和 R11,默认初始化为 0。
  6. 此时 PC 指针就指向了任务入口地址,从而成功跳转到第一个任务。

4.4 实现就绪列表

4.4.1 定义就绪列表

我们需要把任务添加到就绪列表里面, 表示任务已经就绪,系统随时可以调度。
就绪列表在 task.c 中定义。

  1. List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最 大 任 务 优 先 级 的 宏configMAX_PRIORITIES 决 定 , configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。 数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。
image.png

4.4.2 就绪列表初始化

就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists()里面实现。

  1. void prvInitialiseTaskLists( void )
  2. {
  3. UBaseType_t uxPriority;
  4. for(uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++)
  5. {
  6. vListInitialise(& (pxReadyTasksLists[ uxPriority ]));
  7. }
  8. }

image.png

4.4.3 将任务插入到就绪列表

任务控制块里面有一个 xStateListItem 成员, 数据类型为 ListItem_t, 我们将任务插入到就绪列表里面,就是通过将任务控制块的 xStateListItem 这个节点插入到就绪列表中来实现的。
我们在任务创建好之后,紧跟着将任务插入到就绪列表。

  1. int main(void)
  2. {
  3. /* 初始化与任务相关的列表,如就绪列表 */
  4. prvInitialiseTaskLists();
  5. /* 创建任务 */
  6. Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
  7. (char *)"Task1", /* 任务名称,字符串形式 */
  8. (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
  9. (void *) NULL, /* 任务形参 */
  10. (StackType_t *)Task1Stack, /* 任务栈起始地址 */
  11. (TCB_t *)&Task1TCB ); /* 任务控制块 */
  12. /* 将任务添加到就绪列表 */
  13. vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
  14. Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
  15. (char *)"Task2", /* 任务名称,字符串形式 */
  16. (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
  17. (void *) NULL, /* 任务形参 */
  18. (StackType_t *)Task2Stack, /* 任务栈起始地址 */
  19. (TCB_t *)&Task2TCB ); /* 任务控制块 */
  20. /* 将任务添加到就绪列表 */
  21. vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
  22. }

就绪列表的下标对应的是任务的优先级,但是目前我们的任务还不支持优先级,所以 Task1 和 Task2 任务在插入到就绪列表的时候,可以随便选择插入的位置。
我们选择将 Task1 任务插入到就绪列表下标为 1 的链表中, Task2 任务插入到就绪列表下标为 2 的链表中

image.png
4.5 实现调度器

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。

4.5.1 启动调度器

调度器的启动由 vTaskStartScheduler()函数来完成,该函数在 task.c 中定义

1. vTaskStartScheduler()函数

  1. /* 当前正在运行的任务的任务控制块指针,默认初始化为NULL */
  2. TCB_t * volatile pxCurrentTCB = NULL;
  3. extern TCB_t Task1TCB;
  4. extern TCB_t Task2TCB;
  5. void vTaskStartScheduler( void )
  6. {
  7. /* 手动指定第一个运行的任务 */
  8. pxCurrentTCB = &Task1TCB;
  9. if ( xPortStartScheduler() != pdFALSE)
  10. {
  11. /* 调度器启动成功,则不会返回,即不会来到这里 */
  12. }
  13. }
  1. pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。 目前我们还不支持优先级,则手动指定第一个要运行的任务。
  2. 调用函数 xPortStartScheduler()启动调度器, 调度器启动成功, 则不会返回。 该函数在 port.c 中实现。

2. xPortStartScheduler()函数

优先级参考文章
中断设置

  1. /*
  2. * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
  3. * 在Cortex-M中,内核外设SCB中SHPR3寄存器用于设置SysTick和PendSV的异常优先级
  4. * System handler priority register 3 (SCB_SHPR3) SCB_SHPR3:0xE000 ED20
  5. * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
  6. * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV
  7. */
  8. #define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
  9. // 配置成 最低优先级 0XFF << 16 0xFF << 24
  10. // #define configKERNEL_INTERRUPT_PRIORITY 255 /* 高四位有效,即等于0xff,或者是15 */
  11. #define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
  12. #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
  13. BaseType_t xPortStartScheduler( void )
  14. {
  15. /* 配置 PendSV 和 SysTick 的中断优先级为最低 */
  16. portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
  17. portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
  18. /* 启动第一个任务,不再返回 */
  19. prvStartFirstTask();
  20. /* 不应该运行到这里 */
  21. return 0;
  22. }

image.png
image.png
默认为4个位的抢占优先级:0~15的优先级 高4位有效,所以设置0xFF为最低优先级。

3. prvStartFirstTask()函数

prvStartFirstTask()函数用于开始第一个任务,主要做了两个动作,一个是更新 MSP 的值,二是产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务。

  1. __asm void prvStartFirstTask( void )
  2. {
  3. PRESERVE8 //1
  4. /* 在 Cortex-M 中, 0xE000ED08 是 SCB_VTOR 这个寄存器的地址,
  5. 里面存放的是向量表的起始地址,即 MSP 的地址 */
  6. ldr r0, =0xE000ED08 //3
  7. ldr r0, [r0] //4
  8. ldr r0, [r0] //5
  9. /* 设置主堆栈指针 msp 的值 */
  10. msr msp, r0 //6
  11. /* 使能全局中断 */
  12. cpsie i
  13. cpsie f
  14. dsb
  15. isb
  16. /* 调用 SVC 去启动第一个任务 */
  17. svc 0 //8 参数0通过r1寄存器传递
  18. nop
  19. nop
  20. }
  1. PRESERVE8 :当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。在 Cortex-M 中浮点运算是 8 字节的
  2. 在 Cortex-M 中, 0xE000ED08 是 SCB_VTOR 寄存器的地址, 里面存放的是向量表的起始地址,即 MSP 的地址。 向量表通常是从内部 FLASH 的起始地址开始存放(内存映射),那么可知 memory: 0x00000000 处存放的就是 MSP 的值
  3. 将 0xE000ED08 这个立即数加载到寄存器 R0。
  4. 将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0等于 SCB_VTOR 寄存器的值, 等于 0x00000000,即 memory 的起始地址。
  5. 将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于0x200008DB。(从内存里查看)
  6. 将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆栈的栈顶指针。起始这一步操作有点多余,因为当系统启动的时候,执行完 Reset_Handler的时候, 向量表已经初始化完毕, MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。

主堆栈指针( MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包
括中断服务例程) 。参考CotexM3 权威手册 157

  1. 使用 CPS 指令把全局中断打开。 为了快速地开关中断, Cortex-M内核 专门设置了一条 CPS指令,有 4 种用法。

    1. CPSID I ;PRIMASK=1 ;关中断
    2. CPSIE I ;PRIMASK=0 ;开中断
    3. CPSID F ;FAULTMASK=1 ;关异常
    4. CPSIE F ;FAULTMASK=0 ;开异常

    PRIMASK 和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI。
    image.png
    8. 产生系统调用,服务号 0 表示 SVC 中断,接下来将会执行 SVC 中断服务函数。


    4.vPortSVCHandler()函数(第一个任务开始运行)

    SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中, SVC 的中断服务函数注册的名称是 SVC_Handler, 所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler, 但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(),为了能够顺利的响应 SVC 中断,我们有两个选择,改中断向量表中 SVC 的注册的函数名称或者改 FreeRTOS 中 SVC 的中断服务名称。这里,我们采取第二种方法,即在FreeRTOSConfig.h 中添加添加宏定义的方法来修改。

    1. #define xPortPendSVHandler PendSV_Handler
    2. #define xPortSysTickHandler SysTick_Handler
    3. #define vPortSVCHandler SVC_Handler

    vPortSVCHandler()函数开始真正启动第一个任务,不再返回。

    1. __ASM void vPortSVCHandler( void )
    2. {
    3. //声明外部变量 pxCurrentTCB, pxCurrentTCB 是一个在 task.c 中定
    4. //义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块
    5. extern pxCurrentTCB;
    6. PRESERVE8
    7. ldr r3, =pxCurrentTCB
    8. ldr r1, [r3]
    9. ldr r0, [r1] // 1
    10. ldmia r0!, {r4-r11} // 2
    11. msr psp,r0 // 3
    12. //指令同步隔离(与流水线和 MPU 等有关——译注)
    13. isb
    14. mov r0, #0
    15. msr basepri, r0 //4
    16. orr r14, #0xd //5
    17. bx r14 //6
    18. }
  2. 加载 pxCurrentTCB 指向的任务控制块到 r0,任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针。 一个刚刚被创建还没有运行过的任务的栈空间分布具体如图7-8 所示,即 r0 等于图pxTopOfStack。

image.png

  1. 以 r0 为基地址,将栈中向上增长的 8 个字的内容加载到 CPU 寄存器 r4~r11,同时 r0 也会跟着自增。
  2. 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp。
  3. 设置 basepri 寄存器的值为 0,即打开所有中断。 basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。
  4. 当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态。在 SVC 中断服务里面,使用的是 MSP 堆栈指针, 是处在 ARM 状态。(这里栈就开始使用PSP了)

参考:
上下文切换
当 r14 为 0xFFFFFFFX,执行是中断返回指令, cortext-m3 的做法, X 的 bit0 为 1 表示返回 thumb 状态, bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还是用户模式

  1. 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR, PC(任务入口地址), R14, R12, R3, R2, R1, R0(任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶。

到了这里,任务就已经开始运行了。!!!!!!!!

4.5.2 任务切换

任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。但是目前我们还不支持优先级,仅实现两个任务轮流切换, 任务切换函数 taskYIELD()具体实现。

1. taskYIELD

  1. /* 在 task.h 中定义 */
  2. #define taskYIELD() portYIELD()
  3. /* 在 portmacro.h 中定义 */
  4. #define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *) 0xe000ed04)) //中断控制及状态寄存器ICSR
  5. #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL ) //挂起pendsv
  6. #define portSY_FULL_READ_WRITE ( 15 )
  7. #define portYIELD() \
  8. { \
  9. /* 触发PendSV,产生上下文切换 */ \
  10. portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
  11. //数据同步隔离(与流水线、 MPU 和 cache 等有关——译注)
  12. __dsb( portSY_FULL_READ_WRITE ); \
  13. __isb( portSY_FULL_READ_WRITE ); \
  14. }

portYIELD 的实现很简单,实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV中断服务函数,在里面实现任务切换。

2. xPortPendSVHandler()函数

PendSV 中断服务函数是真正实现任务切换的地方

  1. __asm void xPortPendSVHandler( void )
  2. {
  3. extern pxCurrentTCB;
  4. extern vTaskSwitchContext;
  5. PRESERVE8
  6. mrs r0, psp // 1
  7. isb
  8. // 加载 pxCurrentTCB 的地址到 r3
  9. ldr r3, = pxCurrentTCB
  10. // 加载 r3 指向的内容到 r2,即 r2 等于 pxCurrentTCB。
  11. ldr r2, [r3]
  12. stmdb r0!, {r4-r11} // 2
  13. str r0, [r2] // 3
  14. stmdb sp!, {r3, r14} //4
  15. mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY //5
  16. msr basepri, r0 // 6
  17. dsb
  18. isb
  19. bl vTaskSwitchContext
  20. mov r0, #0
  21. msr basepri, r0 //退出临界段, 开中断,直接往 BASEPRI 写 0。
  22. ldmia sp!, {r3, r14} // 8
  23. ldr r1,[r3] //9
  24. ldr r0,[r1] //10
  25. ldmia r0!, {r4-r11} //11
  26. msr psp, r0 //12
  27. isb
  28. bx r14 //13
  29. nop
  30. }
  1. 将 PSP 的值存储到 r0。 当进入 PendSVC Handler 时,上一个任务运行的环境即: xPSR, PC(任务入口地址), R14, R12, R3, R2, R1, R0(任务的形参)这些 CPU 寄存器的值会自动存储到任务的栈中,剩下的 r4~r11 需要手动保存,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶)

image.png

  1. 以 r0 作为基址(指针先递减,再操作, STMDB 的 DB 表示Decrease Befor),将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0的值,

image.png

  1. 将 r0 的值存储到 r2 指向的内容, r2 等于 pxCurrentTCB。 具体为将r0 的值存储到上一个任务的栈顶指针 pxTopOfStack。到此, 上下文切换中的上文保存就完成了
  1. 将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是 MSP),因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回地址自动保存到 R14 中,所以一旦调用发生, R14 的值会被覆盖(PendSV 中断服务函数执行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使用的是 PSP 还是 MSP) ,因此需要入栈保护。 R3 保存的是当前正在运行的任务(准确来说是上文,因为接下来即将要切换到新的任务)的 TCB 指针(pxCurrentTCB)地址,函数调用后 pxCurrentTCB 的值会被更新,后面我们还需要通过 R3 来操作 pxCurrentTCB,但是运行函数 vTaskSwitchContext 时不确定会不会使用 R3 寄存器作为中间变量, 所以为了保险起见, R3 也入栈保护起来。
  2. 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到r0,该宏在 FreeRTOSConfig.h 中定义,用来配置中断屏蔽寄存器 BASEPRI 的值, 高四位有效。 目前配置为 191,因为是高四位有效,所以实际值等于 11,即优先级高于或者等于11 的中断都将被屏蔽。在关中断方面, FreeRTOS 与其它的 RTOS 关中断不同,而是操作BASEPRI 寄存器来预留一部分中断,并不像 μC/OS 或者 RT-Thread 那样直接操作PRIMASK 把所有中断都关闭掉(除了硬 FAULT)
  3. 关中断,进入临界段,因为接下来要更新全局指针 pxCurrentTCB的值。
  4. 调用函数 vTaskSwitchContext。该函数在 task.c 中定义,作用只有一个,选择优先级最高的任务,然后更新 pxCurrentTCB。目前我们还不支持优先级,则手动切换,不是任务 1 就是任务 2。

    1. void vTaskSwitchContext( void )
    2. {
    3. /* 两个任务轮流切换 */
    4. if( pxCurrentTCB == &Task1TCB )
    5. {
    6. pxCurrentTCB = &Task2TCB;
    7. }
    8. else
    9. {
    10. pxCurrentTCB = &Task1TCB;
    11. }
    12. }
  5. 从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP

  6. 加载 r3 指向的内容到 r1。 r3 存放的是 pxCurrentTCB 的地址, 即让 r1 等于 pxCurrentTCB。 pxCurrentTCB 在上面的 vTaskSwitchContext 函数中被更新, 指向了下一个将要运行的任务的 TCB。
  7. 加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针。
  8. 以 r0 作为基地址(先取值,再递增指针, LDMIA 的 IA 表示Increase After),将下一个要运行的任务的任务栈的内容加载到 CPU 寄存器 r4~r11。
  9. 更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务栈中剩下的内容自动加载到 CPU 寄存器。
  10. 异常发生时, R14 中保存异常返回标志,包括返回后进入任务模式还是处理器模式、使用 PSP 堆栈指针还是 MSP 堆栈指针。此时的 r14 等于 0xfffffffd, 最表示异常返回后进入任务模式, SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向任务栈的栈顶。当调用 bx r14 指令后,系统以 PSP 作为 SP 指针出栈,把接下来要运行的新任务的任务栈中剩下的内容加载到 CPU 寄存器: R0(任务形参)、 R1、 R2、 R3、 R12、 R14
    (LR)、 R15(PC)和 xPSR,从而切换到新的任务。

4.6 main函数

  1. /**
  2. ************************************************************************
  3. * @file main.c
  4. * @author fire
  5. * @version V1.0
  6. * @date 2018-xx-xx
  7. * @brief 《FreeRTOS内核实现与应用开发实战指南》书籍例程
  8. * 新建FreeRTOS工程—软件仿真
  9. ************************************************************************
  10. * @attention
  11. *
  12. * 实验平台:野火 STM32 系列 开发板
  13. *
  14. * 官网 :www.embedfire.com
  15. * 论坛 :http://www.firebbs.cn
  16. * 淘宝 :https://fire-stm32.taobao.com
  17. *
  18. ************************************************************************
  19. */
  20. /*
  21. *************************************************************************
  22. * 包含的头文件
  23. *************************************************************************
  24. */
  25. #include "FreeRTOS.h"
  26. #include "task.h"
  27. /*
  28. *************************************************************************
  29. * 全局变量
  30. *************************************************************************
  31. */
  32. portCHAR flag1;
  33. portCHAR flag2;
  34. extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
  35. /*
  36. *************************************************************************
  37. * 任务控制块 & STACK
  38. *************************************************************************
  39. */
  40. TaskHandle_t Task1_Handle;
  41. #define TASK1_STACK_SIZE 20
  42. StackType_t Task1Stack[TASK1_STACK_SIZE];
  43. TCB_t Task1TCB;
  44. TaskHandle_t Task2_Handle;
  45. #define TASK2_STACK_SIZE 20
  46. StackType_t Task2Stack[TASK2_STACK_SIZE];
  47. TCB_t Task2TCB;
  48. /*
  49. *************************************************************************
  50. * 函数声明
  51. *************************************************************************
  52. */
  53. void delay (uint32_t count);
  54. void Task1_Entry( void *p_arg );
  55. void Task2_Entry( void *p_arg );
  56. /*
  57. ************************************************************************
  58. * main函数
  59. ************************************************************************
  60. */
  61. /*
  62. * 注意事项:1、该工程使用软件仿真,debug需选择 Ude Simulator
  63. * 2、在Target选项卡里面把晶振Xtal(Mhz)的值改为25,默认是12,
  64. * 改成25是为了跟system_ARMCM3.c中定义的__SYSTEM_CLOCK相同,确保仿真的时候时钟一致
  65. */
  66. int main(void)
  67. {
  68. /* 硬件初始化 */
  69. /* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
  70. /* 初始化与任务相关的列表,如就绪列表 */
  71. prvInitialiseTaskLists();
  72. /* 创建任务 */
  73. Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
  74. (char *)"Task1", /* 任务名称,字符串形式 */
  75. (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
  76. (void *) NULL, /* 任务形参 */
  77. (StackType_t *)Task1Stack, /* 任务栈起始地址 */
  78. (TCB_t *)&Task1TCB ); /* 任务控制块 */
  79. /* 将任务添加到就绪列表 */
  80. vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
  81. Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
  82. (char *)"Task2", /* 任务名称,字符串形式 */
  83. (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
  84. (void *) NULL, /* 任务形参 */
  85. (StackType_t *)Task2Stack, /* 任务栈起始地址 */
  86. (TCB_t *)&Task2TCB ); /* 任务控制块 */
  87. /* 将任务添加到就绪列表 */
  88. vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
  89. /* 启动调度器,开始多任务调度,启动成功则不返回 */
  90. vTaskStartScheduler();
  91. for(;;)
  92. {
  93. /* 系统启动成功不会到达这里 */
  94. }
  95. }
  96. /*
  97. *************************************************************************
  98. * 函数实现
  99. *************************************************************************
  100. */
  101. /* 软件延时 */
  102. void delay (uint32_t count)
  103. {
  104. for(; count!=0; count--);
  105. }
  106. /* 任务1 */
  107. void Task1_Entry( void *p_arg )
  108. {
  109. for( ;; )
  110. {
  111. flag1 = 1;
  112. delay( 100 );
  113. flag1 = 0;
  114. delay( 100 );
  115. /* 任务切换,这里是手动切换 */
  116. taskYIELD();
  117. }
  118. }
  119. /* 任务2 */
  120. void Task2_Entry( void *p_arg )
  121. {
  122. for( ;; )
  123. {
  124. flag2 = 1;
  125. delay( 100 );
  126. flag2 = 0;
  127. delay( 100 );
  128. /* 任务切换,这里是手动切换 */
  129. taskYIELD();
  130. }
  131. }

4.7 实验现象

  1. 进入调试模式

image.png

  1. 选择逻辑分析仪

image.png

  1. 选择变量

image.png

  1. 设置bit模式

image.png

  1. 查看波形

image.png

4.4 常用汇编

image.png
image.png
image.png