前言

基于ESP32S+ESP-IDF的FreeRTOS笔记。

FreeRTOS 里函数名前缀通常表示返回值类型返回值含义,属于一种命名约定,常见如下:

前缀典型返回类型含义
vvoid无返回值
xBaseType_t基础有符号类型,常用于返回状态、布尔值或错误码
uxUBaseType_t无符号基础类型,常用于返回数量、索引、优先级等
uluint32_t / unsigned long32 位无符号整数
usuint16_t / unsigned short16 位无符号整数
ucuint8_t / unsigned char8 位无符号整数
cchar字符类型
pcchar *指向字符的指针,常用于字符串
pucuint8_t *指向无符号 8 位数据的指针
pvvoid *通用指针
px指向某种结构体的指针常用于 FreeRTOS 内部结构体指针
e枚举类型返回枚举值
prv通常是 static 私有函数FreeRTOS 内部私有函数,不强调返回值类型

系统启动流程

flowchart LR
    entry["ENTRY(call_start_cpu0)<br/><small>esp_system/sections.ld.in</small>"]
    call_start_cpu0["call_start_cpu0<br/><small>esp_system/cpu_start.c</small>"]
    sys_startup_fn["SYS_STARTUP_FN<br/><small>esp_system/startup_internal.h</small>"]
    g_startup_fn["g_startup_fn<br/><small>esp_system/startup.c</small>"]
    start_cpu0["start_cpu0<br/><small>esp_system/startup.c</small>"]
    start_cpu0_default["start_cpu0_default<br/><small>esp_system/startup.c</small>"]
    esp_startup_start_app["esp_startup_start_app<br/><small>freertos/app_startup.c</small>"]
    main_task["main_task<br/><small>freertos/app_startup.c</small>"]
    app_main["app_main<br/><small>main.c</small>"]

    entry --> call_start_cpu0 --> sys_startup_fn --> g_startup_fn --> start_cpu0 --> start_cpu0_default --> esp_startup_start_app --> main_task --> app_main

应用程序的启动流程

Task创建与删除

xTaskCreate

 BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
                         const char * const pcName,
                         const configSTACK_DEPTH_TYPE uxStackDepth,
                         void *pvParameters,
                         UBaseType_t uxPriority,
                         TaskHandle_t *pxCreatedTask
                       );

参数:

  • pvTaskCode 指向任务入口函数的指针
  • pcName 任务的描述性名称。
  • uxStackDepth 要用作任务堆栈的
  • pvParameters 作为参数传递给所创建任务的值
  • uxPriority 任务优先级
  • pxCreatedTask 用于将句柄传递至由 xTaskCreate() 函数创建的任务

返回:

  • 如果任务创建成功,则返回 pdPASS
  • 否则返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY

vTaskDelete

void vTaskDelete( TaskHandle_t xTask );

参数:

  • xTask 要删除的任务的句柄。如果传递 NULL,会删除调用任务

Task输入参数

typedef struct {
	int id;
	char *name;
} TaskParam_t;

// 整型
void intTask(void *pvParam){
	int *pInt = (int *)pvParam;
	printf("int: %d \n", *pInt);
	vTaskDelay(1000 / portTick_PERIOD_MS);
	vTaskDelete(NULL);
}

// 数组
void arrayTask(void *pvParam){
	int *pInt = (int *)pvParam;
	printf("array[0]: %d \n", *pInt);
	printf("array[1]: %d \n", *(pInt + 1));
	vTaskDelay(1000 / portTick_PERIOD_MS);
	vTaskDelete(NULL);
}

// 字符串
void stringTask(void *pvParam){
	char *pStr = (char *)pvParam;
	printf("string: %s \n", pStr);
	vTaskDelay(1000 / portTick_PERIOD_MS);
	vTaskDelete(NULL);
}

// 结构体
void structTask(void *pvParam){
	TaskParam_t *param = (TaskParam_t *)pvParam;
	printf("id: %d \n", param->id);
	printf("name: %s \n", param->name);
	vTaskDelay(1000 / portTick_PERIOD_MS);
	vTaskDelete(NULL);
}

int testN = 5;
int testArray[] = {5, 6};
char *testString = "Hello FreeRTOS";

TaskParam_t taskParam = {
	.id = 5,
	.name = "FreeRTOS"
};

void app_main(void){
	xTaskCreate(intTask, "intTask", 1024, (void *)&testN, 1, NULL);
	xTaskCreate(arrayTask, "arrayTask", 1024, (void *)testArray, 1, NULL);
	xTaskCreate(stringTask, "stringTask", 1024, (void *)testString, 1, NULL);
	xTaskCreate(structTask, "structTask", 1024, (void *)&taskParam, 1, NULL);
}
如果变量本身不是指针:(void \*)&variable
否则:(void \*)variable

任务调度

核心规则

  1. 不同优先级任务:高优先级任务优先运行。
  2. 相同优先级任务:开启时间片后,多个就绪任务轮流运行。

调度器只会从 就绪态 Ready 的任务中选择一个任务进入 运行态 Running

任务状态转换图

优先级

优先级数量由 configMAX_PRIORITIES 决定。

components/freertos/config/include/freertos/FreeRTOSConfig.h

#define configMAX_PRIORITIES                         ( 25 )

因此任务优先级范围为:

0 ~ 24

数字越大,优先级越高。

例如:

优先级含义
0最低优先级
24最高优先级

获取任务优先级:

UBaseType_t uxTaskBasePriorityGet( const TaskHandle_t xTask );

修改任务优先级:

void vTaskPrioritySet( TaskHandle_t xTask,
                       UBaseType_t uxNewPriority );

调度相关配置

抢占式调度

components/freertos/config/include/freertos/FreeRTOSConfig.h

#define configUSE_PREEMPTION                         1

开启抢占式调度后,如果一个更高优先级任务进入就绪态,它可以立即抢占当前正在运行的低优先级任务。

时间片调度

components/freertos/config/include/freertos/FreeRTOSConfig.h

#define configUSE_TIME_SLICING 1

开启时间片调度后,多个 相同优先级 的就绪任务会轮流运行。

任务状态

任务常见状态包括:

状态说明
Running运行态,当前正在占用 CPU
Ready就绪态,已经具备运行条件,等待调度器选择
Blocked阻塞态,正在等待延时、队列、信号量、事件等
Suspended挂起态,不参与调度,直到被恢复

不同优先级任务的调度

当多个任务同时处于就绪态时,FreeRTOS 会选择:

优先级最高的任务运行。

例如:

任务优先级状态
Task A1Ready
Task B3Ready
Task C2Ready

此时运行的是:

Task B

因为 Task B 的优先级最高。

高优先级任务抢占低优先级任务

如果当前正在运行低优先级任务,而一个高优先级任务变为就绪态,那么高优先级任务会抢占当前任务。

常见触发场景包括:

  • 中断释放信号量
  • 队列收到数据
  • 延时结束
  • 事件标志满足

例如:

Task A:优先级 1,正在运行
Task B:优先级 3,原本处于阻塞态

Task B 等待的事件发生后,Task B 进入就绪态:

Task B 抢占 Task A
Task B 开始运行

同优先级任务的调度

如果多个任务优先级相同,并且都处于就绪态,则 FreeRTOS 通常采用 时间片轮转调度

前提是开启: components/freertos/config/include/freertos/FreeRTOSConfig.h

#define configUSE_TIME_SLICING 1

例如:

任务优先级状态
Task A2Ready
Task B2Ready
Task C2Ready

它们优先级相同,调度器会让它们轮流运行:

Task A → Task B → Task C → Task A → ...

每个任务运行一个时间片。

同优先级任务的特殊情况

任务主动阻塞

如果某个任务调用了可能导致阻塞的 API,它会让出 CPU。

例如:

vTaskDelay();
xQueueReceive();
xSemaphoreTake();

执行过程示例:

Task A 运行
Task A 调用 vTaskDelay()
Task A 进入 Blocked
Task B 开始运行

此时不是因为 Task B 优先级更高,而是因为 Task A 主动进入阻塞态,调度器需要选择其他就绪任务运行。

任务主动让出 CPU

任务可以调用:

taskYIELD();

主动让出 CPU。

此时调度器会在 同优先级的就绪任务 中选择下一个任务运行。

例如:

Task A 运行
Task A 调用 taskYIELD()
Task B 开始运行

关闭时间片调度

如果关闭时间片调度:

components/freertos/config/include/freertos/FreeRTOSConfig.h

#define configUSE_TIME_SLICING 0

同优先级任务之间不会因为系统节拍自动轮转。

此时一个任务会一直运行,直到发生以下情况之一:

  • 主动阻塞
  • 主动让出 CPU
  • 被更高优先级任务抢占

!!!待完全 堆栈

uxTaskGetStackHighWaterMark

UBaseType_t uxTaskGetStackHighWaterMark
 ( TaskHandle_t xTask );

configSTACK_DEPTH_TYPE uxTaskGetStackHighWaterMark2
 ( TaskHandle_t xTask );

返回任务开始执行后任务可用的最小剩余堆栈空间量—— 即任务堆栈达到最大(最深)值时未使用的堆栈量。返回值以为单位。

看门狗

看门狗

中断看门狗定时器 (IWDT)

IWDT 的目的是,确保中断服务例程 (ISR) 运行不会受到长时间阻塞(即 IWDT 超时)。

Task看门狗

任务看门狗定时器 (TWDT) 用于监视特定任务,确保任务在配置的超时时间内执行。

调用以下函数,用 TWDT 监视任务:

  • esp_task_wdt_init() 初始化 TWDT 并订阅ILDE任务。
  • esp_task_wdt_add() 为其他任务订阅 TWDT。
  • 订阅后,应从任务中调用 esp_task_wdt_reset()来喂 TWDT。
  • esp_task_wdt_delete() 可以取消之前订阅的任务。
  • esp_task_wdt_deinit()取消订阅空闲任务并反初始化 TWDT。

队列

队列管理 对于队列及信号量的用于任务间通信的函数,它们大多都具有普通版与ISR版。唯有ISR版可以在中断服务函数中使用。

基本使用

队列是先进先出的。

  • xQueueCreate():创建队列。
  • xQueueSend():向队列发送项目。发送的方式是复制而不是引用项目。
  • xQueueReceive:从队列接收项目。依然通过复制接收项目。
  • uxQueueMessagesWaiting:返回队列中存储的消息数量。

传递结构体

发送:

typedef struct {
    int id;
    int value;
} SensorData_t;

QueueHandle_t sensorQueue;

void app_main(void)
{
    sensorQueue = xQueueCreate(5, sizeof(SensorData_t));

    SensorData_t data = {
        .id = 1,
        .value = 25
    };

    xQueueSend(sensorQueue, &data, portMAX_DELAY);
}

接收:

SensorData_t recvData;

xQueueReceive(sensorQueue, &recvData, portMAX_DELAY);

printf("id = %d, value = %d\n", recvData.id, recvData.value);

传递结构体指针

#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

typedef struct {
    int id;
    char name[20];
} SensorData_t;

void sendTask(void *pvParam){
	QueueHandle_t xQueue = (QueueHandle_t) pvParam;
    while(1) {
		SensorData_t *dataToSend = (SensorData_t *)malloc(sizeof(SensorData_t));
		
		if (dataToSend == NULL) {
			printf("Failed to allocate memory for sensor data\n");
			vTaskDelay(pdMS_TO_TICKS(1000));
			continue;
	
		}
	
		dataToSend->id = 1;
		snprintf(dataToSend->name, sizeof(dataToSend->name), "Temperature Sensor");
	
		if(xQueueSend(xQueue, &dataToSend, portMAX_DELAY) != pdPASS) {
			free(dataToSend);
		}
		vTaskDelay(pdMS_TO_TICKS(1000)); // Send data every 1 second

	}
}

void receiveTask(void *pvParam){
    QueueHandle_t xQueue = (QueueHandle_t) pvParam;

    SensorData_t *receivedData;

  

    while(1) {

        if(xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) {
            printf("Received data: ID=%d, Name=%s\n", receivedData->id, receivedData->name);
            free(receivedData);
        } else {
            printf("Failed to receive data from queue\n");
        }
    }
}

void app_main(void){
    QueueHandle_t xQueue = xQueueCreate(10, sizeof(SensorData_t *));
    if (xQueue == NULL) {
        printf("Failed to create queue\n");
        return;
    }

    xTaskCreate(sendTask, "Send Task", 2048, (void *)xQueue, 1, NULL);
    xTaskCreate(receiveTask, "Receive Task", 2048, (void *)xQueue, 1, NULL);
}

队列多进一出

图片

对于单进单出的队列:若接收任务优先级高于发送任务,则队列总是几乎空的,因此接收任务必须要有阻塞时间;若接收任务优先级低于发送任务,则队列总是几乎满的,因此发送任务必须要有阻塞时间。

对于多进单出的队列,假设接收任务优先级最低。则当所有发送任务进入阻塞时,接收任务才成功执行。 以下是当两个发送任务优先级都为2,接收任务优先级为1时的情况:

图片

队列集合

队列集合用于单个任务从不同数据源接收数据的情况。

xQueueCreateSet

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength);

其中 uxEventQueueLength 表示所有加入队列集合的队列长度之和。

xQueueAddToSet

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );

xQueueSelectFromSet

用于从队列集合中选择一个已经就绪的队列或信号量。

  • 不要在 xQueueSelectFromSet 返回之前再次调用它
  • 一次只能读取一个项目
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
const TickType_t xTicksToWait );

示例

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

QueueHandle_t queue1;
QueueHandle_t queue2;
QueueSetHandle_t queueSet;

void sendTask1(void *pvParam){
    int count = 0;

    while(1){
        count++;
        xQueueSend(queue1, &count, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void sendTask2(void *pvParam){
    int count = 100;

    while(1){
        count++;
        xQueueSend(queue2, &count, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void receiveTask(void *pvParam){
    QueueSetMemberHandle_t activeQueue;
    int value;

    while(1){
        activeQueue = xQueueSelectFromSet(queueSet, portMAX_DELAY);

        if(activeQueue == queue1){
            xQueueReceive(queue1, &value, 0);
            printf("Receive from queue1: %d\n", value);
        }else if(activeQueue == queue2){
            xQueueReceive(queue2, &value, 0);
            printf("Receive from queue2: %d\n", value);
        }
    }
}

void app_main(void){
    queue1 = xQueueCreate(5, sizeof(int));
    queue2 = xQueueCreate(5, sizeof(int));

    queueSet = xQueueCreateSet(10);

    xQueueAddToSet(queue1, queueSet);
    xQueueAddToSet(queue2, queueSet);

    xTaskCreate(sendTask1, "sendTask1", 2048, NULL, 1, NULL);
    xTaskCreate(sendTask2, "sendTask2", 2048, NULL, 1, NULL);
    xTaskCreate(receiveTask, "receiveTask", 2048, NULL, 2, NULL);
}

队列邮箱

队列邮箱是指长度为1的队列。对于这样的队列,有特殊的发送/接收函数。

发送:

BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue );

它和xQueueSend的区别在于,它会直接覆盖队列中的值。它只能用于长度为1的队列。

接收:

BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );

软件定时器

软件定时器用于定时或循环执行函数。定时器有两类:单次定时器和周期定时器。 单次定时器:

图片

周期定时器:

图片

软件定时器的上下文

所有软件计时器回调函数都在同一RTOS守护进程(或“计时器服务”)任务的上下文中执行。不能在其中执行阻塞或非常耗时的操作。 守护进程的优先级由 configTIMER_TASK_PRIORITY 配置。

守护进程优先级较低的情况:

图片

守护进程优先级较高的情况:

图片

xTimerStart()被调用时会先发送到队列中,待守护进程被调度才会开始处理。

正在启动的软件计时器将到期的时间,是从向计时器命令队列发送“启动计时器”命令开始计算的。

使用定时器

创建定时器

TimerHandle_t xTimerCreate( const char * const pcTimerName, 
                                TickType_t xTimerPeriodInTicks, 
                                UBaseType_t uxAutoReload, 
                                void * pvTimerID, 
                                TimerCallbackFunction_t pxCallbackFunction );
  • uxAutoReload:设定为pdFALSE以创建单次定时器

启动定时器

BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
  • xTicksToWait:最多等待命令队列有空间的时间

其他函数

  • xTimerReset():重置定时器
  • xTimerChangePeriod():修改定时器周期
  • vTimerSetTimerID():设定TimerID
  • pvTimerGetTimerID:获取TimerID

示例

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void timerTask(TimerHandle_t xTimer){
    printf("Timer callback executed, time: %lu\n", xTaskGetTickCount());
}

void app_main(void){
    // Create a timer
    TimerHandle_t timer = xTimerCreate("Timer", pdMS_TO_TICKS(5000), pdTRUE, 0, timerTask);
    xTimerStart(timer, 0);
    
}

中断管理

从ISR使用FreeRTOS API

不要从ISR中调用普通的FreeRTOS函数,应当调用他们的FromISR版本。

xHigherPriorityTaskWoken

如果上下文切换由中断执行,那么中断退出和进入时的任务可能不同。 一些FreeRTOS API函数可以将任务从阻塞状态移动到就绪状态(如xQueueSendToBack())。如果这些函数解锁的任务优先级高于运行状态的任务,那么应该切换到那个任务。何时切换取决于调用API函数的上下文:

  • 在任务中调用:若设定了configUSE_PREEMPTION为1,则会直接切换。在该函数还未退出之前,就会切换。
  • 在中断中调用:在中断中调用时,不会立即进行上下文切换,而是通过xHigherPriorityTaskWoken参数来标记是否需要切换,并在中断退出时根据该参数决定是否执行任务切换。

portYIELD_FROM_ISR和portEND_SWITCHING_ISR

这两个宏用于从ISR请求上下文切换。

延迟中断处理

延迟中断处理意为仅在ISR中执行设定标志位等耗时短的操作,而具体的操作放到任务中。 保持ISR尽可能短的原因:

  1. 中断会阻塞其他中断,导致其他中断响应延迟。
  2. 长时间占用CPU会延长任务调度延迟,降低系统实时性。
  3. 可能丢失后续中断(如果硬件不支持中断排队或嵌套)。
  4. 高频率中断中执行耗时操作容易造成看门狗超时。
  5. 需要考虑变量、外设和内存缓冲区等资源同时被任务和ISR访问的后果。

如果延迟中断处理的任务优先级最高,那么将立即执行。就像是在ISR里执行一样。 图片

二进制信号量

图片

延迟中断处理函数调用xSemaphoreTake()从而进入阻塞,直到ISR调用xSemaphoreGiveFromISR()后解除阻塞。

ISR 中调用 xSemaphoreGiveFromISR(queue, &xHigherPriorityTaskWoken)
    ↓
xHigherPriorityTaskWoken 被标记为 pdTRUE(表示有更高优先级的任务就绪)
    ↓
在 ISR 末尾调用 portYIELD_FROM_ISR(xHigherPriorityTaskWoken)
    ↓
若 xHigherPriorityTaskWoken == pdTRUE,触发 PendSV 异常 → 退出 ISR 后立即任务切换
若为 pdFALSE,什么都不做 → 退出 ISR 后回到原来的任务

如果有多个ISR,并且操作同一个信号量。该信号量就必须是计数信号量,不能是二进制信号量。

计数信号量

计数信号量用于管理有限数量的资源。它维护一个计数值,任务通过 xSemaphoreTake() 获取信号量,使计数值减一;通过 xSemaphoreGive() 释放信号量,使计数值加一。它常用于计数事件资源管理。对于上一节的情况,它用于计数事件。

函数原型:

SemaphoreHandle_t xSemaphoreCreateCounting (UBaseType_t uxMaxCount, UBaseType_t uxInitialCount );

延迟中断处理到RTOS守护任务

函数原型:

BaseType_t xTimerPendFunctionCallFromISR ( PendedFunction_t uint32 xFunctionToPend,void* pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken );

pxHigherPriorityTaskWokenxTimerPendFunctionCallFromISR() 用于在中断中向定时器命令队列排队一个函数调用,并可选择触发上下文切换,让守护任务立即处理该调用。

示例

static uint32_t ulExampleInterruptHandler( void )
{
	static uint32_t ulParameterValue = 0;
	BaseType_t xHigherPriorityTaskWoken;
	/* xHigherPriorityTaskWoken 参数必须初始化为 pdFALSE,
	因为如果需要上下文切换,它将在中断安全 API 函数内设置为 pdTRUE。 */
	xHigherPriorityTaskWoken = pdFALSE;

	xTimerPendFunctionCallFromISR(
	vDeferredHandlingFunction, /* 处理函数 */
	NULL,
	ulParameterValue,
	&xHigherPriorityTaskWoken );
	ulParameterValue++;
	/* 将 xHigherPriorityTaskWoken 值传递到 portYIELD_FROM_ISR() 中。如果 xHigherPriorityTaskWoken 在 xTimerPendFunctionCallFromISR() 内设置为 pdTRUE,则调用 portYIELD_FROM_ISR() 将控制 FreeRTOS 实时内核请求上下文切换。如果 xHigherPriorityTaskWoken 仍为 pdFALSE,则调用 portYIELD_FROM_ISR() 将无效。
	*/
	portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

static void vDeferredHandlingFunction( void *pvParameter1, uint32_t ulParameter2 ){
	vPrintStringAndNumber( "Handler function - Processing event ", ulParameter2 );
}

中断嵌套

硬件决定ISR何时执行,而软件决定任务何时执行。

  • 数字优先级:如果分配给一个中断源的优先级为7,则它的数字优先级为7。
  • 逻辑优先级:决定当两个中断同时发生时谁先执行ISR。根据处理器不同,可能数字优先级越高,逻辑优先级越高,也可能反过来。

当一个中断正在执行时,另一个优先级更高的中断打断了它,等更高优先级的中断处理完后,再回到原来的中断继续执行。

示例

该示例中Priority 7的逻辑优先级大于Priority 1。 图片

高优先级区(Priority 4~7)

  • 不能调用任何 FreeRTOS API 函数
  • 不会被内核或临界区阻塞/延迟(响应最快)
  • 可以任意嵌套
  • 适用于对实时性要求极高的硬件中断(如高速定时器、紧急故障处理)

可调用 API 区(Priority 1~3)

  • 可以调用 FreeRTOS API(如 xQueueSendFromISR() 等)
  • 可以被临界区(Critical Section)屏蔽/延迟
  • 也可以嵌套
  • 适用于需要与内核交互的普通外设中断(如串口接收完成、GPIO 中断)

内核优先级(Priority 1)

  • FreeRTOS 内核滴答定时器(Tick Timer)等内核中断运行在此优先级
  • 是最低的中断优先级

资源管理

如果从多个任务调用函数是安全的,那么该函数就是可重入的。如果函数只访问自己的堆栈和寄存器的数据,那么它就是可重入、线程安全的。

如果应用程序使用newlib C库,它必须在FreeRTOSConfig.h中将configUSE_NEWLIB_REENTRANT设置为1,以确保正确分配newlib所需的线程本地存储。 如果应用程序使用C库,它必须在FreeRTOSConfig.h中将configUSE_PICOLIBC_TLS设置为1,以确保configUSE_PICOLIBC_TLS所需的线程本地存储正确分配。 如果应用程序使用任何其他C库并且需要线程本地存储(TLS),它必须在FreeRTOSConfig.h中将configUSE_C_RUNTIME_TLS_SUPPORT设置为1,并且必须实现以下操作:

  • configTLS_BLOCK_TYPE:每个任务TLS块的类型。
  • configINIT_TLS_BLOCK:初始化每个任务TLS块。
  • configSET_TLS_BLOCK:更新当前的TLS块。在上下文切换期间被调用,以确保使用了正确的TLS块。
  • configDEINIT_TLS_BLOCK:释放TLS块。

TLS是一种让每个任务(Task)都拥有其独立全局变量副本的机制。

互斥

一旦一个任务开始访问不可重入、非线程安全的资源,它对该资源具有独占权,此时其他任务不能访问该资源,这种特性被称为互斥

临界区

临界区会阻止任务切换或中断导致数据竞争。FreeRTOS 中可使用 taskENTER_CRITICAL()taskEXIT_CRITICAL() 进入/退出临界区,内部会屏蔽一定优先级的中断。临界区应尽量短,不能在其中调用可能阻塞的 API。

taskENTER_CRITICAL();

/*中断仍然可以执行,但只允许优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY,并且这些中断不允许调用FreeRTOS API函数。*/
PORTA |= 0x01;

taskEXIT_CRITICAL();

挂起调度器

暂停调度器的执行,此时不能调用FreeRTOS API函数。

void vPrintString( const char *pcString )
{
 	vTaskSuspendScheduler();
 	{
 		printf( "%s", pcString );
 		fflush( stdout );
 	}
 	xTaskResumeScheduler();
}

互斥锁

互斥锁是特殊的二进制信号量。

特性互斥锁 (Mutex)二进制信号量 (Binary Semaphore)
设计核心资源保护(排他性访问控制)行为同步(任务间或中断与任务间的通知)
所有权 (Ownership)有所有权。哪个任务 Take 了锁,就必须由同一个任务 Give 释放。无所有权。任务 A 可以 Take,任务 B 也可以 Give,甚至中断中也可以 Give。
优先级翻转解决支持优先级继承机制。不支持,容易引发恶性优先级翻转。
中断中使用不能在中断服务程序(ISR)中使用(因为中断没有任务上下文,无法拥有锁)。可以且经常在中断中使用(如 xSemaphoreGiveFromISR)。
初始状态创建后通常默认是 可用(Unlocked) 状态。创建后通常默认是 空(Locked) 状态,等待被触发。
SemaphoreHandle_t xSemaphoreCreateMutex( void );

图片

优先级反转

图片

优先级较高的Task必须阻塞等待低优先级的Task解除对锁的持有,这被称为优先级反转

优先级继承

它是用来改善优先级反转的。它的的工作原理是暂时将互斥锁持有者的优先级提高到尝试获取相同互斥锁的最高优先级任务的优先级。持有互斥锁的低优先级任务“继承”等待互斥锁的任务的优先级图片

死锁

当两个任务都无法继续等待对方持有的资源时,就会发生死锁

递归互斥锁

如果一个任务多次尝试使用相同的互斥锁,而没有首先返回互斥锁,就可能和自身发生死锁。递归互斥锁可以避免这种状况。同一任务可以多次“获取”递归互斥锁,并且只 有在之前每次“获取”递归互斥锁的调用都执行了一次“给予”递归互斥锁后,才会返回该互斥锁。

函数标准互斥锁递归互斥锁
创建xSemaphoreCreateMutexxSemaphoreCreateRecursiveMutex
获取xSemaphoreTakexSemaphoreTakeRecursive
给出xSemaphoreGivexSemaphoreGiveRecursive

互斥锁与任务调度

图片

当两个任务使用相同互斥锁时,若任务2给出互斥锁,则任务1不会抢占任务2,而只会进入就绪态而在下一个Tick才能被调用。这种情况可能导致任务1无法得到充分的执行时间。 例如:

/*在紧密循环中使用互斥体的任务的实现。该任务在本地缓冲区中创建一个文本字符串,然后将该字符串
写入显示器。 对显示器的访问受到互斥锁的保护*/
void vATask( void *pvParameter )
{
	extern SemaphoreHandle_t xMutex;
	char cTextBuffer[ 128 ];
	for( ;; ) {
		/*生成文本字符串——这是一个快速的操作。*/
		vGenerateTextInALocalBuffer ( cTextBuffer );
		xSemaphoreTake( xMutex, portMAX_DELAY );
		/*将生成的文本写入到显示器中-这是一个缓慢的操作。*/
		vCopyTextToFrameBuffer (cTextBuffer );
		xSemaphoreGive ( xMutex );
	}
}

图片

因此Task2应该主动切换上下文(使用taskYIELD())。

/*在紧密循环中使用互斥体的任务的实现。该任务在本地缓冲区中创建一个文本字符串,然后将该字符串写入显示器。 对显示器的访问受到互斥锁的保护*/
void vATask( void *pvParameter )
{
	extern SemaphoreHandle_t xMutex;
	char cTextBuffer[ 128 ];
	for( ;; ) {
		/*生成文本字符串——这是一个快速的操作。*/
		vGenerateTextInALocalBuffer ( cTextBuffer );
		xSemaphoreTake( xMutex, portMAX_DELAY );
		/*将生成的文本写入到显示器中-这是一个缓慢的操作。*/
		vCopyTextToFrameBuffer (cTextBuffer );
		xSemaphoreGive ( xMutex );
		
		/*如果每次迭代都调用taskYIELD(),那么该任务只会在短时间内保持运行状态,并且任务之间的快速切换会浪费处理时间。因此,只有在持有互斥体时时钟计数发生变化时才调用taskYIELD()*/
		if( xTaskGetTickCount() != xTimeAtWhichMutexWasTaken )
		{
			taskYIELD();
		}
	}
}

看门人任务

看门人任务(Gatekeeper Task)提供了一种实现互斥的干净方法,而没有优先级反转或死锁的风险。它是拥有资源唯一所有权的任务。仅允许它直接访问资源,任何其他需要访问资源的任务只能通过使用它的服务间接访问资源。

事件组

事件组( Event Gropus ) 是处理发生事件的一种机制,它和队列与信号量的区别在于:

  • 事件组可以让所有等待它的任务都解除阻塞;而队列/信号量只能解锁最高优先级的任务。
  • 事件组可以等待多个任务的组合;而队列/信号量只能等待单个事件。 因此其在:同步多个任务向多个任务广播事件允许任务在阻塞时等待事件组中的单/多个事件发生上非常有用。并且对RAM的占用相比使用多个二进制信号量更少。

事件组的特性

事件标志是一个布尔值,用于指示事件是否发生(1为发生)。事件组是一组事件标志。事件组的所有事件标志存储在单个EventBits_t变量中。 图片

事件组中的事件位数取决于FreeRTOSConfig.h中的configTICK_TYPE_WIDTH_IN_BITS如(configTICK_TYPE_WIDTH_IN_BITS为TICK_TYPE_WIDTH_32_BITS)。

函数

  • xEventGroupCreate:创建事件组。
  • xEventGroupGetStaticBuffer:获取静态事件组的指针。
  • xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet):设置一个或多个位。事件组的现有值与uxBitsToSet进行按位或运算。
  • xEventGroupSetBitsFromISR:它不会在ISR内设置事件位,而是将其延迟到RTOS守护任务。

xEventGroupWaitBits

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit,const BaseType_t xWaitForAllBits,TickType_t xTicksToWait );

xClearOnExit:如果满足调用任务的解锁条件,并且 xClearOnExit 设置为 pdTRUE,则在调用任务退出xEventGroupWaitBits() API 函数之前,事件组中由 uxBitsToWaitFor 指定的事件位将被清除回0如果 xClearOnExit 设置为 pdFALSE,则 xEventGroupWaitBits() API 函数不会修改事件组中事件位的状态

现有事件组值uxBitsToWaitForxWaitForAllBits结果
00000101pdFALSE由于事件组中的位 0 或位 2 均未设置,调用任务将进入阻塞状态,并且当事件组中的位 0 或位 2 被设置时
,调用任务将离开阻塞状态
01000101pdTRUE调用任务将进入阻塞状态,因为事件组中的位 0 和位 2未同时设置,并且当事件组中的位 0 和位 2 均设置时,调用任务将离开阻塞状态
01000110pdFALSE调用任务将不会进入阻塞状态,因为xWaitForAllBit是pdFALSE,并且uxBitsTowait指定的两位之一已经在事件组中设置
01000110pdTRUE调用任务将进入阻塞状态,因为 xWaitForAllBits 为pdTRUE,并且事件组中仅设置了 uxBitsToWaitFor 指定
的两个位之一。当事件组中的位 1 和位 2 均被设置时,任务将离开阻塞状态。

xEventGroupSync

xEventGroupSync() 用于多个任务同步:每个任务在到达同步点时设置自己的标志位,并等待所有参与任务均设置完毕。常用场景为多任务启动时统一开始执行,或周期性协同工作。函数会阻塞当前任务直至指定位全部置1,然后返回事件组当前的值。

为何不能使用xEventGroupSetBits+xEventGroupWaitBits:因为xEventGroupSetBitsxEventGroupWaitBits是分离的非原子操作。在设置位之后、等待之前,其他任务可能已检测到位全部置1并清除然后继续运行,导致当前任务永远等不到完整位组而卡死。而xEventGroupSync将设置与等待合并为原子操作,确保任务在设置自身位的同时开始等待,避免竞态条件。、

EventBits t xEventGroupSync ( EventGroupHandle_t xEventGroup, EventBits_t uxBitsToSet, TickType_t xTicksToWait, const EventBits_t, uxBitsToWaitFor);

示例

#define BIT_SENSOR  (1 << 0)
#define BIT_PROCESS (1 << 1)
#define BOTH_BITS   (BIT_SENSOR | BIT_PROCESS)

EventGroupHandle_t xEventGroup;

void sensorTask(void *pvParam) {
    while (1) {
        // 采集传感器数据...
        printf("Sensor: data collected\n");

        // 等 processing task 也完成后再进入下一轮
        xEventGroupSync(xEventGroup, BIT_SENSOR, BOTH_BITS, portMAX_DELAY);

        printf("Sensor: next cycle\n");
    }
}

void processTask(void *pvParam) {
    while (1) {
        // 处理数据...
        printf("Process: data processed\n");

        // 等 sensor task 也完成后再进入下一轮
        xEventGroupSync(xEventGroup, BIT_PROCESS, BOTH_BITS, portMAX_DELAY);

        printf("Process: next cycle\n");
    }
}

任务通知

任务通知相较于信号量,是一种高效的任务间通信方式,它允许一个任务直接通知另一个任务,而不经过中介。 任务通知的优点在于,它的RAM成本默认每个任务8字节;缺点在于,它只能用于单次点对点通信,无法像信号量那样支持多个任务等待同一事件;且通知值仅32位,计数有限,在复杂同步场景下可能丢失事件。

其他有用的函数

  • vTaskList():列出所有任务的状态

参考资料

掌握FreeRTOS实时内核

FreeRTOS™