FreeRTOS-基于stm32标准库

117 阅读31分钟

1 任务创建和删除

在FreeRTOS中,线程(Thread)和任务(Task)的概念是相同的。每个任务就是一个线程,有着自己的一个程序。

任务的创建有两种方式

  1. 动态创建任务:任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配
  2. 静态创建任务:任务的任务控制块以及任务的栈空间所需的内存,需用户分配提供

1.1 动态任务创建函数

BaseType_t xTaskCreate (
  TaskFunction_t  pxTaskCode, /* 指向任务函数的指针 */
  const char * const pcName, /* 任务名字,最大长度configMAX_TASK_NAME_LEN */
  const configSTACK_DEPTH_TYPE usStackDepth, /* 任务堆栈大小,注意字为单位 */
  void * const pvParameters, /* 传递给任务函数的参数 */
  UBaseType_t uxPriority, /* 任务优先级,范围:0 ~ configMAX_PRIORITIES(32) - 1 */
  TaskHandle_t * const pxCreatedTask /* 任务句柄,就是任务的任务控制块 */
)

//返回值:
//pdPASS:创建成功
//errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:创建失败

动态创建任务的流程:

image.png

任务控制块结构体成员

image.png

任务栈栈顶,在任务切换时的任务上下文保存、任务恢复息息相关

1.2 静态创建任务

TaskHandle_t xTaskCreateStatic
(
  TaskFunction_t pxTaskCode,  /* 指向任务函数的指针 */
  const char * const pcName,  /* 任务函数名 */
  const uint32_t ulStackDepth,  /* 任务堆栈大小注意字为单位 */
  void * const pvParameters,  /* 传递的任务函数参数 */
  UBaseType_t uxPriority,  /* 任务优先级 */
  StackType_t * const puxStackBuffer,  /* 任务堆栈,一般为数组,由用户分配 */
  StaticTask_t * const pxTaskBuffer  /* 任务控制块指针,由用户分配 */
); 

//返回值
//NULL:用户没有提供相应的内存,任务创建失败
//其他值:任务句柄,任务创建成功

静态创建任务的流程

image.png

1.3 任务删除

void vTaskDelete(
  TaskHandle_t xTaskToDelete /* 待删除任务的任务句柄 */
);

被删除的任务将从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除

注意:

  1. 当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)
  2. 空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存, 则需要由用户在任务被删除前提前释放,否则将导致内存泄露

删除任务流程 image.png

1.4 任务创建删除实验

设计四个任务:start_task、task1、task2、task3

  • start_task:用来创建其他的三个任务
  • task1:实现LED1每500ms闪烁一次
  • task2:实现LED2每500ms闪烁一次
  • task3:判断按键KEY是否按下,按下则删掉task1
#include "stm32f10x.h"

#include "freertos.h"
#include "task.h"

#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Delay.h"

/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler; // 任务句柄
void Start_Task(void *pvParameters);

/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);

/* Task2 任务 配置 */
#define TASK2_PRIORITY 3
#define TASK2_STACK_DEPTH 128
TaskHandle_t task2_handler;
void Task2(void *pvParameters);

/* Task3 任务 配置 */
#define TASK3_PRIORITY 4
#define TASK3_STACK_DEPTH 128
TaskHandle_t task3_handler;
void Task3(void *pvParameters);

/**
 * @description: FreeRTOS入口函数:创建任务函数并开始调度
 * @return {*}
 */
void FreeRTOS_Start(void)
{
    xTaskCreate((TaskFunction_t)Start_Task,
                (char *)"Start_Task",
                (configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIORITY,
                (TaskHandle_t *)&start_task_handler);
    vTaskStartScheduler(); // 开启任务调度器,Start_Task才会开始执行
}

void Start_Task( void * pvParameters )
{
    taskENTER_CRITICAL(); /* 进入临界区,临界区的代码不会被打断阻塞,任务切换是发生在中断的,进入临界区会关闭中断*/

    xTaskCreate((TaskFunction_t) Task1,
                (char *) "Task1",
                (configSTACK_DEPTH_TYPE ) TASK1_STACK_DEPTH,
                (void *) NULL,
                (UBaseType_t) TASK1_PRIORITY,
                (TaskHandle_t *) &task1_handler);
    xTaskCreate((TaskFunction_t) Task2,
                (char *) "Task2",
                (configSTACK_DEPTH_TYPE ) TASK2_STACK_DEPTH,
                (void *) NULL,
                (UBaseType_t) TASK2_PRIORITY,
                (TaskHandle_t *) &task2_handler);
    xTaskCreate((TaskFunction_t) Task3,
                (char *) "Task3",
                (configSTACK_DEPTH_TYPE ) TASK3_STACK_DEPTH,
                (void *) NULL,
                (UBaseType_t) TASK3_PRIORITY,
                (TaskHandle_t *) &task3_handler);

    vTaskDelete(NULL); // 删除Start_Task任务
    taskEXIT_CRITICAL(); /* 退出临界区,退出后才会开始任务切换*/
}

/**
 * @description: LED1每500ms翻转一次
 * @param {void *} pvParameters
 * @return {*}
 */
void Task1(void * pvParameters)
{
    
    while (1){
        LED1_Turn();
        vTaskDelay(500); //  freeRTOS专用的延时函数,如果用其他的会会一直阻塞在这里
    }
}
/**
 * @description: LED2每500ms翻转一次
 * @param {void *} pvParameters
 * @return {*}
 */
void Task2(void * pvParameters)
{
    
    while (1){
        LED2_Turn();
        vTaskDelay(250);
    }
}
/**
 * @description: 按下KEY1删除task1
 * @param {void *} pvParameters
 * @return {*}
 */
void Task3(void * pvParameters)
{
    uint8_t key = 0;
    uint8_t time = 0;
    while(1){
        // 判断是否按了一次,key=1表示按下且松开
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) {//读PB11输入寄存器的状态,如果为0,则代表按键1按下
            vTaskDelay(20); //延时消抖
            while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); //等待按键松手
            vTaskDelay(20); //延时消抖
            key = 1; //置键码为1
        }
        // 按下后删除task1
        if(key == 1) {
            if(task1_handler != NULL) {
                vTaskDelete(task1_handler);
                task1_handler = NULL;
            }
            key = 0;
        }
        vTaskDelay(500);
    }
}

int main(void)
{
    LED_Init();
    Key_Init();
    OLED_Init();

    FreeRTOS_Start();
}

2 任务的挂起与恢复

挂起任务类似暂停,可恢复; 删除任务,无法恢复

2.1 函数API

挂起任务

void vTaskSuspend(
    TaskHandle_t xTaskToSuspend // 待挂起任务的任务句柄,传入的参数为NULL,则代表挂起任务自身
) 

此函数用于挂起任务,使用时需将宏 INCLUDE_vTaskSuspend 配置为 1

无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复。

任务恢复

void vTaskResume(
    TaskHandle_t xTaskToResume // 待恢复任务的任务句柄
) 

使用该函数注意宏:INCLUDE_vTaskSuspend 必须定义为 1

任务无论被 vTaskSuspend() 挂起多少次,只需在任务中调用 vTakResume() 恢复一次,就可以继续运行。且被恢复的任务会进入就绪态!

中断中恢复被挂起函数

BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)  
// 返回值
// pdTRUE:任务恢复后需要进行任务切换
// pdFALSE:任务恢复后不需要进行任务切换

使用该函数注意宏:INCLUDE_vTaskSuspendINCLUDE_xTaskResumeFromISR 必须定义为 1

该函数专用于中断服务函数中,用于解挂被挂起任务,中断服务程序中要调用freeRTOS的API函数则中断优先级不能高于FreeRTOS所管理的最高优先级

在中断处理函数中调用:

image.png

2.2 挂起和恢复流程

image.png image.png

2.3 任务调度器的挂起和恢复

挂起任务调度器,防止任务之间进行资源的抢夺,有点类似于临界区

image.png
  1. 与临界区不一样的是,挂起任务调度器,未关闭中断;
  2. 它仅仅是防止了任务之间的资源争夺,中断照样可以直接响应;
  3. 挂起调度器的方式,适用于临界区位于任务与任务之间;既不用去延时中断,又可以做到临界区的安全

3 中断管理

3.1 中断优先级

STM32 的中断优先级可以分为抢占优先级和子优先级

  • 抢占优先级: 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断
  • 子优先级:当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行

stm32优先级分组有五种方式 image.png freertos为了方便优先级的管理,建议选择最后一种

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 调用该函数可以设置

3.2 中断相关寄存器

三个系统中断优先级配置寄存器,分别为 SHPR1、 SHPR2、 SHPR3

image.png freertos需要先把PendSV和SysTick设置最低优先级,保证系统任务切换(PendSV)不会阻塞系统其他中断的响应 image.png FreeRTOS所使用的中断管理是利用的BASEPRI这个寄存器(作用屏蔽优先级低于某一个阈值的中断 )

BASEPRI设置为0x50,代表中断优先级在5-15内的均被屏蔽,0-4的中断优先级正常执行 image.png 在中断服务函数中调度FreeRTOS的API函数需注意:

  1. 中断服务函数的优先级需在FreeRTOS所管理的范围内
  2. 在中断服务函数里边需调用FreeRTOS的API函数,必须使用带“FromISR”后缀的函数

3.3 中断实验

使用两个定时器,一个优先级为4,一个优先级为6,设置系统所管理的优先级范围:5~15

现象:两个定时器每1s,打印一段字符串,按下按钮关中断时,停止打印,开中断时持续打印

配置FreeRTOSConfig.h

/* 设置 RTOS 内核自身使用的中断优先级。 一般设置为最低优先级, 不至于屏蔽其他优先级程序,既设置PendSV和SysTick最低优先级*/
#define configKERNEL_INTERRUPT_PRIORITY (15 << 4)
/* 设置了 调用中断安全的 FreeRTOS API 函数的最高中断优先级。 FreeRTOS 的管理的最高优先级,设置为5,既5-15优先级的中断受管理*/        
#define configMAX_SYSCALL_INTERRUPT_PRIORITY  (5 << 4)

主程序代码

#include "stm32f10x.h"                  // Device header
#include "freertos.h"
#include "task.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Delay.h"
#include "Timer.h"
#include "Serial.h"
/* Timer2中断处理函数 */
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		Serial_SendString("Timer2 send data!\r\n");
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}
/* Timer3中断处理函数 */
void TIM3_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
	{
		Serial_SendString("Timer3 send data!\r\n");
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
	}
}
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);

/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);

/**
 * @description: FreeRTOS入口函数:创建任务函数并开始调度
 * @return {*}
 */
void FreeRTOS_Start(void)
{
    xTaskCreate((TaskFunction_t)Start_Task,
                (char *)"Start_Task",
                (configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIORITY,
                (TaskHandle_t *)&start_task_handler);
    vTaskStartScheduler();
}

void Start_Task(void *pvParameters)
{
    taskENTER_CRITICAL(); /* 进入临界区 */
    xTaskCreate((TaskFunction_t)Task1,
                (char *)"Task1",
                (configSTACK_DEPTH_TYPE)TASK1_STACK_DEPTH,
                (void *)NULL,
                (UBaseType_t)TASK1_PRIORITY,
                (TaskHandle_t *)&task1_handler);

    vTaskDelete(NULL);  
    taskEXIT_CRITICAL(); /* 退出临界区 */
}
/**
 * @description: 开关中断
 * @param {void *} pvParameters
 * @return {*}
 */
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = Key_GetNum();
        if (key == 1)
        {
            /* 关中断 */
            Serial_SendString(">>>>Close Timer3.....\r\n");
            portDISABLE_INTERRUPTS();
        }
        else if (key == 2)
        {
            /* 开中断 */
            Serial_SendString(">>>>Open Timer3.....\r\n");
            portENABLE_INTERRUPTS();
        }

        /* 为了观察实验现象,不能调用freertos的延时函数,底层会去开关中断,影响现象 */
        // vTaskDelay(500);
        Delay_ms(500);
    }
}

int main(void)
{
    Timer2_Init();
	Timer3_Init();
	Serial_Init();
	Key_Init();
	FreeRTOS_Start();
}

现象

image.png

4 列表和列表项

列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务

在OS中任务的数量是不确定的,并且任务状态是会发生改变的,所以非常适用列表(链表)这种数据结构

4.1 相关结构体

列表结构体:

typedef struct xLIST
{
  listFIRST_LIST_INTEGRITY_CHECK_VALUE  /* 校验值 */
  volatile UBaseType_t uxNumberOfItems  /* 列表中的列表项数量 */
  ListItem_t * configLIST_VOLATILE pxIndex  /* 用于遍历列表项的指针 */
  MiniListItem_t xListEnd  /* 末尾列表项,是个迷你列表项*/
  listSECOND_LIST_INTEGRITY_CHECK_VALUE  /* 校验值 */
} List_t;
// 通过检查这两个校验值值,来判断列表的数据在程序运行过程中,是否遭到破坏 ,该功能一般用于调试, 默认是不开启的 

列表项结构体:

struct xLIST_ITEM
{
  listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE // 用于检测列表项的数据完整性
  configLIST_VOLATILE TickType_t xItemValue // 列表项的值,多用于对列表进行排序
  struct xLIST_ITEM * configLIST_VOLATILE pxNext; // 下一个列表项
  struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; // 上一个列表项
  void * pvOwner; // 列表项的拥有者,通常是任务控制块
  struct xLIST * configLIST_VOLATILE pxContainer; // 列表项所在列表
  listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE // 用于检测列表项的数据完整性
};
typedef struct xLIST_ITEM ListItem_t; 	

迷你列表项结构体:

struct xMINI_LIST_ITEM
{
  listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE  /* 用于检测数据完整性 */
  configLIST_VOLATILE TickType_t xItemValue;  /* 列表项的值 */
  struct xLIST_ITEM * configLIST_VOLATILE pxNext;  /* 上一个列表项 */
  struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;  /* 下一个列表项 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
// 迷你列表项只用于标记列表的末尾和挂载其他插入列表中的列表项,因此不需要成员变量 pxOwner 和 pxContainer,以节省内存开销 

4.2 相关API

列表的操作就是链表的相关操作

4 任务调度

FreeRTOS对任务的调度采用基于时间片(time slicing)的方式。时间片,顾名思义,把一段时间等分成了很多个时间段,在每一个时间段保证优先级最高的任务能执行,同时如果几个任务拥有相等的优先级,则它们会轮流使用每个时间段占用CPU资源。调度器会在每个时间片结束的时候通过周期中断(tick interrupt)执行一次,调度器根据设置的抢占式还是合作式模式选择哪个任务在下一个时间片会运行

4.1 开启任务调度器

vTaskStartScheduler()

作用:用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度

该函数主要做了下面的事情:

  1. 创建空闲任务,确保在没有其他任务运行时,系统仍然能够正常工作并保持稳定
  2. 如果使能软件定时器,则创建定时器任务
  3. 关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
  4. 初始化全局变量,并将任务调度器的运行标志设置为已运行
  5. 初始化任务运行时间统计功能的时基定时器
  6. 调用函数 xPortStartScheduler()

xPortStartScheduler()

BaseType_t xPortStartScheduler( void )
{
#if ( configASSERT_DEFINED == 1 )
{
 /* 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误,代码省略 */
}
#endif
 
 /* 设置 PendSV 和 SysTick 的中断优先级为最低优先级 */
 portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
 
 /* 配置 SysTick
 * 清空 SysTick 的计数值
 * 根据 configTICK_RATE_HZ 配置 SysTick 的重装载值
 * 开启 SysTick 计数和中断
 */
 vPortSetupTimerInterrupt();
 
 /* 初始化临界区嵌套次数计数器为 0 */
 uxCriticalNesting = 0;
 
 /* 使能 FPU
 * 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码
 * ARM Cortex-M3 内核 MCU 无 FPU
 */
 // prvEnableVFP();
 
 /* 在进出异常时,自动保存和恢复 FPU 相关寄存器
 * 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码
 * ARM Cortex-M3 内核 MCU 无 FPU
 */
// *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
 
 /* 启动第一个任务 */
 prvStartFirstTask();
 
 /* 不会返回这里 */
 return 0;
}

作用:该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务

  1. 检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误。
  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级。
  3. 调用函数 vPortSetupTimerInterrupt()配置 SysTick。
  4. 初始化临界区嵌套计数器为 0。
  5. 调用函数 prvEnableVFP()使能 FPU。
  6. 调用函数 prvStartFirstTask()启动第一个任务。

4.2 启动第一个任务

prvStartFirstTask()

用于初始化启动第一个任务前的环境,主要是重新设置 MSP 指针,并使能全局中断,它是一段汇编代码

__asm void prvStartFirstTask( void )
{
    /* 8字节对齐 */
    PRESERVE8

    /* 下面三行代码是为了获得 MSP 指针的初始值,使用 NVIC 偏移寄存器来定位堆栈 */
    ldr r0, =0xE000ED08  /* 0xE000ED08为VTOR地址 */
    ldr r0, [ r0 ]       /* 获取VTOR的值,即向量表的首地址*/
    ldr r0, [ r0 ]       /* 获取MSP的初始值,向量表中第一个字的数据,也就是栈底指*/

    /* 将 MSP 设置回堆栈的起始位置,将 MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据 */
    msr msp, r0
    /* 使能全局中断,因为之前在函数 vTaskStartScheduler()中关闭了受 FreeRTOS 的中断*/
    cpsie i
    cpsie f
    dsb
    isb
    /* 调用 SVC 来启动第一个任务 */
    svc 0
    nop
    nop
}
  • 八字节对齐:PRESERVE8 是 FreeRTOS 中的一个宏,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
  • 0xE000ED08: ARM Cortex-M 中的 VTOR(Vector Table Offset Register) 地址,VTOR 寄存器保存了向量表的基地址(通常是中断向量表或异常处理函数的入口地址),而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址。
  • MSP:程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针,使 SP 指针指向最后一个入栈的元素,那么程序就可以根据 SP 指针来从栈中存取信息。ARM Cortex-M 提供两个栈指针:MSP(主堆栈指针)和 PSP(进程堆栈指针)。在 FreeRTOS 中 MSP 是给系统栈空间使用的,而 PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过 PSP 指向的,而在进入中断服务函数时,则是使用 MSP 指针。当使用不同的堆栈指针时,SP 会等于当前使用的堆栈指针。
  • 使能全局中断,因为之前在函数 vTaskStartScheduler()中关闭了受 FreeRTOS 的中断。 使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。

vPortSVCHandler()

当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中。注意,SVC中断只在启动第一次任务时会调用一次,以后均不调用。

__asm void vPortSVCHandler( void )
{
    /* 8字节对齐 */
    PRESERVE8

    /* 获取任务栈地址 */
    ldr r3, = pxCurrentTCB   /* 恢复上下文,r3指向优先级最高的就绪任务的任务控制块 */
    ldr r1, [ r3 ]           /* 使用 pxCurrentTCBConst 获取 pxCurrentTCB 的地址*/
    ldr r0, [ r1 ]           /* pxCurrentTCB 中的第一个项是任务的栈顶(TCB结构体的第一个成员存放的就是栈顶地址,这也是为什么注释要说该成员必须放在第一个成员位置) */
  
    /* 出栈,并设置PSP */
    ldmia r0 !, { r4 - r11 } /* 任务栈弹出到 CPU 寄存器 */
    msr psp, r0 /* 设置PSP为任务栈指针 */
    isb
    
    /* 使能所有中断 */
    mov r0, # 0
    msr basepri, r0
    
    /* 使用PSP指针,并跳转到任务函数 */
    orr r14, # 0xd
    bx r14
}
  1. 首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务就是系统将要运行的任务。pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块
  2. 通过获取任务控制块中的第一个元素,得到该任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中。由于任务栈中的内容在调用任务创建函数的时候,已经初始化了,只要设置 PSP 指针,任务的运行环境就准备好了
  3. 通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已经初始化了。然后再设置 PSP 指针,那么,这么一来,任务的运行环境就准备好了
  4. 往 BASEPRI 寄存器中写 0,允许中断
  5. 使 CPU 跳转到任务的函数中去执行

4.3 任务切换

每个任务(Task)都有一个与之相关联的控制块(Task Control Block,TCB),其中包含了任务的状态、堆栈指针、任务优先级等信息。当 CPU 执行任务时,它会根据任务调度策略在多个任务之间进行切换。

image.png
  • 当执行任务A时,要进行任务切换到任务B时,需要保存任务A的寄存器到任务堆栈中,即保存现场
  • 然后将任务B存在任务堆栈中的寄存器值恢复到CPU寄存器中,即恢复现场

任务切换的过程在PendSV中断服务函数里边完成,通过向中断控制及状态寄存器ICSR(地址:0xE000_ED04)的Bit28写入1挂起PendSV来启动PendSV中断,主要有一下两种方式:

  1. 滴答定时器中断调用
  2. 执行FreeRTOS提供的相关API函数:portYIELD()

5 消息队列

任务之间或任务于中断之间需要进行“沟通交流”,这里的“沟通交流”就是消息传递的过程。

在操作系统中,因为会涉及“资源管理”的问题,比方说读写冲突,因此使用全局变量在任务与任务或任务与中断之间进行消息传递,并不是很好的解决方案,FreeRTOS为此提供了“队列”的机制

FreeRTOS基于队列,实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量

FreeRTOS队列特点:

  • 数据入队出队方式:队列通常采用“先进先出”(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取,FreeRTOS中也可以配置为“后进先出”LIFO方式
  • 数据传递方式:FreeRTOS中队列可以采用实际值传递,即将数据拷贝到队列中进行传递, 也可以传递指针,所以在传递较大的数据的时候采用指针传递
  • 多任务访问:队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息
  • 出队、入队阻塞:当任务向一个队列发送消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队,需要等待指定阻塞时间

5.1 队列结构体

image.png
typedef struct QueueDefinition 
{
    int8_t * pcHead /* 存储区域的起始地址 */
    int8_t * pcWriteTo; /* 下一个写入的位置 */
    union
    {
        QueuePointers_t xQueue; /* 把结构体当队列使用 */
        SemaphoreData_t xSemaphore; /* 把结构体当互斥信号量和递归信号量使用 */
    } u ;
    List_t xTasksWaitingToSend;  /* 等待发送列表(队列满的时候,还想写入队列的任务就会进入这个列表) */
    List_t xTasksWaitingToReceive;  /* 等待接收列表(同理,队列为空想要读取的任务就会进入) */
    volatile UBaseType_t uxMessagesWaiting;  /* 非空闲队列项目的数量 */
    UBaseType_t uxLength;  /* 队列长度 */
    UBaseType_t uxItemSize;  /* 队列项目的大小 */
    volatile int8_t cRxLock;  /* 读取上锁计数器,锁上后是可以对队列进行正常的读写,但是不能操作等待发送列表 */
    volatile int8_t cTxLock;  /* 写入上锁计数器,不能操作等待接收列表*/
    
   /* 还有其他的一些条件编译。。。 */
} xQUEUE;

QueuePointers_t

typedef struct QueuePointers
{
     int8_t * pcTail;  /* 存储区的结束地址 */
     int8_t * pcReadFrom;  /* 最后一个读取队列的地址 */
} QueuePointers_t;

SemaphoreData_t

typedef struct SemaphoreData
{
    TaskHandle_t xMutexHolder;  /* 互斥信号量持有者 */
    UBaseType_t uxRecursiveCallCount;  /* 递归互斥信号量的获取计数器 */
} SemaphoreData_t;

5.2 队列相关API

创建队列:

使用 xQueueCreate()创建队列时,使用的是动态内存分配,所以要想使用该函数必须在 FreeRTOSConfig.h 中把 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 来使能(默认已经设置为1)

image.png

读队列:

使用xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用

BaseType_t xQueueReceive( 
  QueueHandle_t xQueue,
  void * const pvBuffer,
  TickType_t xTicksToWait
);
 
BaseType_t xQueueReceiveFromISR(
  QueueHandle_t    xQueue,
  void             *pvBuffer,
  BaseType_t       *pxTaskWoken
);
image.png

还有一个函数xQueuePeek使用和xQueueReceive一样,但是此函数在成功读取消息后,并不会移除已读取的消息!

写入队列:

可以把数据写到队列头部,也可以写到尾部,这些函数也有两个版本:在任务中使用、在ISR中使用

xQueueSend() //往队列的尾部写入消息
xQueueSendToBack() //同 xQueuesend()
xQueueSendToFront()//往队列的头部写入消息
xQueueOverwrite()//覆写队列消息(只用于队列长度为1的情况)

xQueueSendFromlSR()//在中断中往队列的尾部写入消息
xQueueSendToBackFromlSR()
xQueueSendToFrontFromlSR()
xQueueOverwriteFromlSR()
image.png

删除队列:

队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空

消息队列删除函数vQueueDelete()的使用也是很简单的,只需传入要删除的消息队列的句柄即可,

5.3 队列操作实验

  • task1:当按键key1或key2按下,将键值拷贝到队列queue1(入队);当按键key3按下,拷贝大数据的地址到队列big_queue中。
  • task2:读取队列queue1中的消息(出队),打印出接收到的键值。
  • task3:从队列big_queue读取大数据地址,通过地址访问大数据。

入口函数

QueueHandle_t queue1;    /* 小数据句柄 */
QueueHandle_t big_queue; /* 大数据句柄 */
//description: FreeRTOS入口函数:创建任务函数并开始调度
void FreeRTOS_Start(void)
{
    /* 创建queue1队列 */
    queue1 = xQueueCreate(2, sizeof(uint8_t));
    if (queue1 != NULL){
        printf("queue1队列创建成功\r\n");
    }
    else{
        printf("queue1队列创建失败\r\n");
    }
    
    /* 创建big_queue队列 */
    big_queue = xQueueCreate(1, sizeof(char *)); // 对于数据比较大,传入数据的地址就行,所以大小设置成地址的大小就行
    if (big_queue != NULL){
        printf("big_queue队列创建成功\r\n");
    }
    else{
        printf("big_queue队列创建失败\r\n");
    }
    
    // Start_Task里面创建三个任务,这里省略。。。
    xTaskCreate((TaskFunction_t)Start_Task,
                (char *)"Start_Task",
                (configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIORITY,
                (TaskHandle_t *)&start_task_handler);
    vTaskStartScheduler();
}

task1

// 入队
void Task1(void *pvParameters)
{
    uint8_t key = 0;
    char buff[100] = {"大大大fdahjk324hjkhfjksdahjk#$@!@#jfaskdfhjka"};
    BaseType_t err = 0;
    
    while (1){
        key = Key_Detect();
        if (key == KEY1_PRESS || key == KEY2_PRESS){
            err = xQueueSend(queue1, &key, portMAX_DELAY);
            if (err != pdTRUE){
                printf("queue1队列发送失败\r\n");
            }
        }
        else if (key == KEY3_PRESS){
            err = xQueueSend(big_queue, &buff, portMAX_DELAY);
            if (err != pdTRUE){
                printf("big_queue队列发送失败\r\n");
            }
        }
        vTaskDelay(10);
    }
}

task2

// 小数据出队
void Task2(void *pvParameters)
{
    uint8_t key = 0;
    BaseType_t err = 0;
    while (1) {
        err = xQueueReceive(queue1, &key, portMAX_DELAY);
        if (err != pdTRUE) {
            printf("queue1队列读取失败\r\n");
        }
        else {
            printf("queue1读取队列成功,数据:%d\r\n", key);
        }
    }
}

task3

// 大数据出队
void Task3(void *pvParameters)
{
    char *buf;
    BaseType_t err = 0;
    while (1) {
        err = xQueueReceive(big_queue, &buf, portMAX_DELAY); // portMAX_DELAY表示死等
        if (err != pdTRUE) {
            printf("big_queue队列读取失败\r\n");
        }
        else {
            printf("数据:%s\r\n", buf);
        }
    }
}

6 信号量

消息队列主要用于传输数据:任务与任务之间、任务与中断之间,在有些情况下,不需要传输数据,只需要传递状态即可

  • 车开出停车位,你的车可以停进来了

信号量是一种实现任务间通信的机制,可以实现任务之间的同步或临界资源的互斥访问,可以实现对共享资源的有序访问(用于传递状态)

共享资源的访问

  • 汽车驶入或离开停车位,停车位的个数(计数型信号量)
  • 公共电话的使用(二值信号量)

任务之间的同步(任务与任务、任务与中断)

在执行中断服务函数的时候,可以通过释放信号量(不做具体的处理,以提高系统的实时性)来通知某个任务所期待的事件发生了。当退出中断后,通过调度器,同步的任务(做出相应的处理)就会执行。

image.png

6.1 二值信号量

本质:一个队列长度为 1 ,队列项大小为0的队列 ,该队列就只有空和满两种情况

通常用于互斥访问或任务同步(一个任务等待另一个任务完成)

使用二值信号量的过程:

  1. 创建二值信号量
  2. 释放二值信号量(队满)
  3. 获取二值信号量(队空)

注:创建二值信号量时,初始值为0(表示队空),所以先要释放信号量。在队空的情况下有其他任务想要获取信号量就会被阻塞,如果多个任务在同一个信号量上阻塞,那么具有最高优先级的任务将在下次信号量可用时最先解除阻塞。

相关API

xSemaphoreCreateBinary()// 使用动态方式创建二值信号量
xSemaphoreCreateBinaryStatic()// 使用静态方式创建二值信号量
xSemaphoreGive()// 释放信号量
xSemaphoreGiveFromISR()// 在中断中释放信号量
xSemaphoreTake()// 获取信号量
xSemaphoreTakeFromISR()// 在中断中获取信号量

优先级反转问题

二值信号量用于互斥访问时,会出现优先级反转的情况

优先级翻转:高优先级的任务反而慢执行,低优先级的任务反而优先执行

一个具体的例子:假定一个进程中有三个线程Thread1(高)、Thread2(中)和Thread3(低),考虑下图的执行情况。

image.png
  • T0时刻,Thread3运行,并获取信号量(队满);
  • T1时刻,Thread2开始运行,由于优先级高于Thread3,Thread3被抢占(Thread2不需要获取信号量),Thread2被调度执行;
  • T2时刻,Thread1抢占Thread2;
  • T3时刻,Thread1需要获取信号量但是被更低优先级的Thread3所拥有,Thread1被挂起等待信号量
  • 而此时线程Thread2和Thread3都处于可运行状态,Thread2的优先级大于Thread3的优先级,Thread2被调度执行。最终的结果是高优先级的Thread1迟迟无法得到调度,而中优先级的Thread2却能抢到CPU资源

上述现象中,优先级最高的Thread1要得到调度,需要等Thread3释放信号量(这个很正常),但是还需要等待另外一个毫不相关的中优先级线程Thread2执行完成(这个就不合理了),会导致调度的实时性变差。

后面有空把代码补上!

解决方案:

  1. 优先级继承:其大致原理是让低优先级线程在获得同步资源的时候(如果有高优先级的线程也需要使用该同步资源时),临时提升其优先级。以前其能更快的执行并释放同步资源。释放同步资源后再恢复其原来的优先级。
  2. 优先级天花板:当线程申请某共享资源时,把该线程的优先级提升到可访问这个资源的所有线程中的最高优先级,这个优先级称为该资源的优先级天花板。这种方法简单易行,不必进行复杂的判断,不管线程是否阻塞了高优先级线程的运行,只要线程访问共享资源都会提升线程的优先级。

互斥信号量

互斥信号量是包含优先级继承机制的二进制信号量

image.png

  • 与优先级反转情况下相比,到了T3时刻,Thread1需要获取信号量,操作系统检测到这种情况后,把Thread3的优先级提高到Thread1的优先级。此时处于可运行状态的线程Thread2和Thread3中,Thread3的优先级大于Thread2的优先级,Thread3被调度执行。
  • Thread3执行到T4时刻,释放信号量,系统恢复了Thread3的优先级,Thread1获取信号量,重新进入可执行队列。处于可运行状态的线程Thread1和Thread2中,Thread1的优先级大于Thread2的优先级,所以Thread1被调度执行。

优先级继承无法完全解决优先级翻转,只是在某些情况下将影响降至最低。因为不能在中断中使用互斥信号量,原因如下:

  1. 互斥信号量使用的优先级继承机制要求从任务中(而不是从中断中)获取和释放互斥信号量。
  2. 中断无法保持阻塞来等待一个被互斥信号量保护的资源。

6.2 计数型信号量

计数信号量可以被认为是长度大于1的队列。信号量的用户对存储在队列中的数据不感兴趣,他们只关心队列是否为空。

计数信号量通常用于两种情况:

  • 事件计数:在此使用方案中,每次事件发生时,事件处理程序将释放一个信号量(信号量计数值递增),并且处理程序任务每次处理事件(信号量计数值递减)时获取一个信号量。因此,计数值是已发生的事件数与已处理的事件数之间的差值。这种情况初始值一般被设为0
  • 资源管理:在此使用情景中,计数值表示可用资源的数量。获得对资源的控制权,任务必须先获取信号量(信号量计数减一)。当计数值达到零时,表示没有空闲资源可用。当任务使用完资源时, 释放信号量(信号量计数加一)。在这种情况下,创建信号量时计数值可以等于最大计数值。

相关API

xSemaphoreCreateCounting() // 使用动态方法创建计数型信号量。
xSemaphoreCreateCountingStatic() // 使用静态方法创建计数型信号量
uxSemaphoreGetCount() // 获取信号量的计数值

计数型信号量的释放和获取与二值信号量相同

7 内存管理

除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()和函数 free()来实现动态地申请和释放内存 。

不用标准的 C 库自带的内存管理算法,是因为它有以下缺点:

  • 占用大量的代码空间不适合用在资源紧缺的嵌入式系统中
  • 没有线程安全的相关机制
  • 运行有不确定性,每次调用这些函数时花费的时间可能都不相同
  • 内存碎片化

FreeRTOS 提供了多种动态内存管理的算法,可针对不同的嵌入式系统

image.png

heap_1

heap_1只实现了pvPortMalloc,没有实现vPortFree;也就是说,它只能申请内存,无法释放内存!

如果工程,创建好的任务、队列、信号量等都不需要被删除,那么可以使用heap_1内存管理算法

heap_1的实现最为简单,管理的内存堆是一个数组,在申请内存的时候, heap_1 内存管理算法只是简单地从数组中分出合适大小的内存

heap_2

相比于 heap_1 内存管理算法, heap_2 内存管理算法使用最适应算法,并且支持释放内存

适用场景:频繁的创建和删除任务,且所创建的任务堆栈都相同,这类场景下 heap_2 没有碎片化的问题

最适应算法

假设heap有3块空闲内存(按内存块大小由小到大排序):5字节、25字节、50字节

现在新创建一个任务需要申请20字节的内存

第一步:找出最小的、能满足pvPortMalloc的内存:25字节

第二步:把它划分为20字节、5字节;返回这20字节的地址,剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用

image.png

内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的

heap_4

heap_4 内存管理算法使用了首次适应算法,也支持内存的申请与释放,并且能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。

适用于这种场景:频繁地分配、释放不同大小的内存。

首次适应算法

假设heap有3块空闲内存(按内存块地址由低到高排序):5字节、50字节、25字节

现在新创建一个任务需要申请20字节的内存

第一步:找出第一个能满足pvPortMalloc的内存:50字节

第二步:把它划分为20字节、30字节;返回这20字节的地址,剩下30字节仍然是空闲状态,留给后续的pvPortMalloc使用

image.png

heap_5

heap_5 内存管理算法在 heap_4 内存管理算法的基础上实现的,但是 heap_5 内存管理算法在 heap_4 内存管理算法的基础上实现了管理多个非连续内存区域的能力

heap_5 内存管理算法默认并没有定义内存堆,需要用户手动指定内存区域的信息,对其进行初始化。

适用场景:在嵌入式系统中,那些内存的地址并不连续的场景。