【FreeRTOS】基于G431+Cubemx自用笔记

系列文章目录

留空


文章目录


前言

自用
猪猪猪:还在更新中


因为参加完蓝桥杯后,想学RTOS,所以直接无缝衔接,此笔记是基于蓝桥杯板子G431RBT6学习的!

一、从头开始创建一个FreeRTOS工程

基本的配置跳过,只记录有关FreeRTOS的创建!

1.1 在 “Timebase Source” 中,选择其他TIM

在 STM32 + FreeRTOS 项目中,FreeRTOS 默认使用 SysTick 作为时基,而 STM32CubeMX 默认的 HAL 库也是使用 SysTick这两个会冲突,导致系统运行不正常,尤其是出现任务调度异常、延时失效等问题。

所以把Timebase Source改成了TIM17
在这里插入图片描述

1.2 配置FreeRTOS的参数

在这里插入图片描述

关键参数(全部默认即可)

参数名称设置值描述
USE_PREEMPTIONEnabled启用抢占式调度,允许高优先级任务抢占低优先级任务的CPU时间。
CPU_CLOCK_HZSystemCoreClockCPU的时钟频率,通常由系统定义,表示处理器的时钟速度。
TICK_RATE_HZ1000系统的时基(tick)频率为1000Hz,即每1毫秒产生一个tick。
MAX_PRIORITIES56系统中任务的最大优先级数,FreeRTOS使用优先级来调度任务。
MINIMAL_STACK_SIZE128 Words任务的最小堆栈大小为128个词(word)。
MAX_TASK_NAME_LEN16任务名称的最大长度为16个字符。
TOTAL_HEAP_SIZE3072 Bytes为FreeRTOS堆分配的总内存大小为3072字节。
Memory Management schemeheap_4使用的内存管理方案,不同的方案可能有不同的内存分配和释放策略。

以下是 FreeRTOS Mode and Configuration 界面中全部参数,按功能模块分类(可跳过)

(1)Kernel Settings(内核设置)

参数名称当前配置值含义说明
USE_PREEMPTIONEnabled启用抢占式调度(高优先级任务可立即抢占低优先级任务)
CPU_CLOCK_HZSystemCoreClockCPU时钟频率(通常由MCU定义,如SystemCoreClock=16MHz
TICK_RATE_HZ1000系统Tick频率(1kHz=1ms一个Tick)
MAX_PRIORITIES56最大任务优先级数(0为最低,55为最高)
MINIMAL_STACK_SIZE128 Words空闲任务(Idle Task)的堆栈大小(单位:字,具体字节数需乘以字长)
MAX_TASK_NAME_LEN16任务名称的最大字符长度
USE_16_BIT_TICKSDisabled禁用16位Tick计数器(使用32位计数器,支持更长运行时间)
IDLE_SHOULD_YIELDEnabled空闲任务主动让出CPU给同等优先级的用户任务(节能场景可能需要禁用)
USE_PORT_OPTIMISED _TASK_SELECTIONDisabled禁用硬件优化任务选择(通用软件实现,兼容性更好)
USE_TICKLESS_IDLEDisabled禁用Tickless低功耗模式(始终维持Tick中断)

(2)Mutexes & Semaphores(互斥量与信号量)

参数名称当前配置值含义说明
USE_MUTEXESEnabled启用互斥量(Mutex)支持。
USE_RECURSIVE_MUTEXESEnabled启用递归互斥量(同一任务可重复加锁)。
USE_COUNTING_SEMAPHORESEnabled启用计数信号量。
QUEUE_REGISTRY_SIZE8队列注册表大小(用于调试工具跟踪队列/信号量)。

(3)Memory Management(内存管理)

参数名称当前配置值含义说明
TOTAL_HEAP_SIZE3072 Bytes动态内存堆总大小(根据任务和队列数量调整)。
Memory Management schemeheap_4使用动态内存分配方案4(合并空闲块,避免碎片化)。
Memory AllocationDynamic / Static支持动态和静态内存分配(需用户提供静态内存时需配置configSUPPORT_STATIC_ALLOCATION)。

1. 3 添加任务

下图是STM32CubeMX 的默认任务,可以修改它的名称和函数类型,但不能删除它。这是 CubeMX 提供的一个固定设置,用于初始化FreeRTOS和提供一个最基本的任务框架。
在这里插入图片描述

参数说明

配置项当前值解释说明
Task NamedefaultTask任务的名称,这里是 defaultTask。任务名称用于标识该任务
PriorityosPriorityNormal任务的优先级,osPriorityNormal 表示任务的优先级为正常(即中等优先级)
Stack Size (Words)128任务堆栈的大小,单位是字(Words),这里的 128 表示任务栈有128个字的空间。每个字的大小通常是4字节(32位系统)
Entry FunctionStartDefaultTask任务的入口函数,任务开始执行时会调用该函数。这里的 StartDefaultTask 是该任务的函数名称
Code Generation OptionDefault代码生成选项,设置为 Default 表示使用默认的代码生成设置
ParameterNULL传递给任务的参数,这里设置为 NULL,表示任务不需要传入任何参数
AllocationDynamic任务栈内存分配方式,设置为 Dynamic 表示任务栈的内存是在运行时动态分配的
Buffer NameNULL缓冲区名称,设置为 NULL 表示没有指定缓冲区。通常用于处理一些任务的输入输出缓冲区
Control Block NameNULL任务控制块名称,设置为 NULL 表示没有指定任务的控制块(在FreeRTOS中用于存储任务的元数据)

关于 STM32CubeMX 中的默认任务:

  • 默认任务:这是 CubeMX 在生成的代码中自动创建的第一个任务。它通常用于进行系统初始化、测试和调试。
  • 修改默认任务:虽然不能删除默认任务,但可以:
    • 修改任务的名称
    • 修改任务执行的函数(即默认任务执行的代码)
    • 修改任务的优先级

后面,我们手写代码时,我们可以通过 FreeRTOS 提供的 API 创建自己的任务、队列、信号量等对象。

最后!创建工程!

然后在,工程文件夹内,创建一个文件夹BSP,拿来放写好的底层驱动文件。

在这里插入图片描述

OK!完成!!(基本配置完成的文件放在最后了:LED KEY Usart Delay)

二、动态任务的创建/删除

2.1 函数介绍

2.1.1 创建动态任务xTaskCreate()

(1)函数原型

BaseType_t xTaskCreate(
    TaskFunction_t pxTaskCode,    // 任务函数
    const char * const pcName,    // 任务名称
    configSTACK_DEPTH_TYPE usStackDepth,  // 栈大小
    void *pvParameters,           // 传入任务的参数
    UBaseType_t uxPriority,       // 任务优先级
    TaskHandle_t *pxCreatedTask   // 返回任务句柄(可以是 NULL)
);

(2)参数解释

参数含义举例
pxTaskCode任务函数名(任务函数就是编写任务具体做什么)比如:任务函数为void Task_LED(void *pvParameters),任务函数名就是Task_LED
pcName给任务起个名字(调试查看用)"LED_Task"
usStackDepth分配给任务的栈大小(注意单位是“字”,不是字节一般 128~512 比较常见
pvParameters传递给任务函数的参数可以传结构体、变量、NULL
uxPriority任务优先级,值越大越重要通常范围 0~configMAX_PRIORITIES-1
pxCreatedTask返回这个任务的“身份证”(句柄),我们可以以后用它去操作这个任务。如想删掉、挂起这个任务,就需要通过句柄去操作&xxx_Handle,或者传 NULL 表示我不关心这个任务的句柄

(3)返回值说明

返回值含义
pdPASS创建成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY内存不足,创建失败(系统堆不够)

(4)示例代码

/***** (1)任务函数(任务是要做什么) ******/
void LED_Task(void *pvParameters)
{
    while (1)
    {
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);  // 翻转LED
        vTaskDelay(500);   // 延时500ms
    }
}

/***** (2)创建任务函数 ******/
// 创建的任务,系统会把它加入调度器,由 FreeRTOS 自动进行任务切换调度
xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);

这个任务的功能是:每隔 500ms 翻转一次 GPIOB 的 PIN_0 引脚,从而实现 LED 的闪烁效果

参数名类型示例值含义
1pxTaskCodeTaskFunction_tLED_Task任务函数指针,告诉 FreeRTOS 这个任务要做什么。这里是一个控制 LED 闪烁的函数。
2pcNameconst char *"LED"任务名称,用于调试和查看任务状态时显示的名字。
3usStackDepthuint16_t128栈大小,单位是“字”(word),不是字节。STM32 中 1 字 = 4 字节,因此此任务分配了 512 字节栈空间。
4pvParametersvoid *NULL传递给任务的参数。如果不需要传递参数,写 NULL
5uxPriorityUBaseType_t2任务优先级。值越大,优先级越高。
6pxCreatedTaskTaskHandle_t *NULL接收创建的任务句柄的指针。如果后续要操作该任务(如删除、挂起等),需传入句柄变量地址;后续不需要这些操作就传 NULL

如果我们要看任务是否创建成功:

/***** (1)任务函数:LED 闪烁 ******/
void LED_Task(void *pvParameters)
{
    while (1)
    {
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);  // 翻转 LED 引脚
        vTaskDelay(500);   // 延时 500ms
    }
}

/***** (2)任务创建函数:包含成功判断 ******/
void CreateTasks(void)
{
    BaseType_t xReturn;  // 用于接收任务创建结果

    xReturn = xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);

    if (xReturn == pdPASS) // 创建成功
    {
        printf("LED_Task 创建成功!\r\n");
    }
    else // 创建失败
    {
        printf("LED_Task 创建失败!\r\n");
    }
}

假设我们传入了任务句柄变量,例如 &LEDTaskHandle

// 定义一个任务句柄
TaskHandle_t LEDTaskHandle; 

///创建任务 `LED_Task`,并把这个任务的“控制权”交给变量 `LEDTaskHandle`
xTaskCreate(LED_Task, "LED", 128, NULL, 2, &LEDTaskHandle);

可以后续使用句柄对任务进行操作

  • 删除LED任务:vTaskDelete(LEDTaskHandle);
  • 挂起LED任务:vTaskSuspend(LEDTaskHandle);

2.1.2 创建静态任务xTaskCreateStatic()

这个函数是为 不使用动态内存分配(malloc) 的场景准备的。我们要自己准备好栈空间任务控制块

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       // 提前准备好的任务控制块
);

【后续没用到,我就是一个直接跳过!!】

2.1.3 删除任务 vTaskDelete()

(1)函数原型

void vTaskDelete(TaskHandle_t xTaskToDelete);

(2)参数解释

参数含义
xTaskToDelete要删除的任务的句柄。如果想删除当前任务,可以传入 NULL
  • vTaskDelete(NULL); → 删除当前正在运行的任务
  • vTaskDelete(xxx_Handle); → 删除指定句柄的任务

(3)示例代码

/*****(1)任务函数,运行后自删*****/
void LED_Task(void *pvParameters)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);	

    printf("LED_Task 自我删除!\r\n");
    vTaskDelete(NULL);  // 删除自己
}

/*****(2)创建任务*****/
xTaskCreate(LED_Task, "LED", 128, NULL, 2, NULL);

或由其他任务/定时器删除:

/***** 任务函数1,运行后删任务2 *****/
void LED_Task(void *pvParameters)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);	
    vTaskDelete(Task2_Handle);  // 删除任务2
}
/***** 任务函数2 *****/
void LED2_Task(void *pvParameters)
{
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);	
}

/*****(2)创建任务*****/
xTaskCreate(LED_Task, "LED", 128, NULL, 2, NULL);              // 不保存句柄
xTaskCreate(LED2_Task, "LED2", 128, NULL, 2, &Task2_Handle);   // 有句柄,用于后续删掉操作!

2.2 编写例题代码

这里参考正点原子例题!

在这里插入图片描述

2.2.1 添加任务

打开工程,这里一共四个任务,我们先创建好任务函数和添加任务

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */

// 定义三个任务句柄,用于后续管理和控制任务(如挂起、恢复等)
TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;

/* USER CODE END Variables */

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

// 四个任务函数的声明
void Task_Start(void *argument);   
void Task1_LED(void *argument);  
void Task2_LED(void *argument);    
void Task3_KEY(void *argument);    

/* USER CODE END FunctionPrototypes */

/****** (1) 创建四个任务函数 *******/

void Task_Start(void *argument)
{
	printf("Hello! Task Start!\r\n");

    // 创建 LED1 任务,优先级 26,堆栈大小 128
	xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);

	// 创建 LED2 任务,优先级 27,堆栈大小 128
	xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	

	// 创建 KEY 按键任务,优先级 28,堆栈大小 128
	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);

	// 删除当前任务
  	vTaskDelete(NULL); 
}

void Task1_LED(void *argument)
{
	while (1)
	{
		static int N1 = 0;
		N1++;
		printf("Task_1 -- %d\r\n", N1);

		vTaskDelay(500);
	}
}

void Task2_LED(void *argument)
{
	while (1)
	{
		static int N2 = 0;
		N2++;
		printf("Task_2 -- %d\r\n", N2);

		vTaskDelay(1000);		
	}
}

void Task3_KEY(void *argument)
{
	while (1)
	{
		static int N3 = 0;
		N3++;
		printf("Task_3 -- %d\r\n", N3);

		vTaskDelay(100);  
	}
}
... ...
void MX_FREERTOS_Init(void) {
  ... ...
  /* USER CODE BEGIN RTOS_THREADS */
  /* 添加 FreeRTOS 启动任务 */

  /****** (2) 添加任务 *******/
  // 创建启动任务,优先级 25,堆栈大小 128,启动时由调度器自动运行
  xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL);

  /* USER CODE END RTOS_THREADS */
}

创建好了四个任务,每个任务对应有任务函数添加任务,打印自增看看任务咋运行的

我们给任务分配了优先级

任务名称函数名优先级(数字越大优先级越高)说明
启动任务Task_Start25启动时创建其他任务后自删除
LED1任务Task1_LED26控制LED1,每500ms打印一次
LED2任务Task2_LED27控制LED2,每1000ms打印一次
按键处理任务Task3_KEY28处理按键输入,优先级最高

我们在第一章可以看到,优先级设置56个,为什么这是25到28呢???

我们看看默认任务的优先级是多少

/* Definitions for defaultTask */

osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
  .name = "defaultTask",
  .priority = (osPriority_t) osPriorityNormal,
  .stack_size = 128 * 4
};

.priority = (osPriority_t) osPriorityNormal,

点击进去看看这个普通优先级到底多少级

  osPriorityBelowNormal6  = 16+6,       ///< Priority: below normal + 6
  osPriorityBelowNormal7  = 16+7,       ///< Priority: below normal + 7
  osPriorityNormal        = 24,         ///< Priority: normal
  osPriorityNormal1       = 24+1,       ///< Priority: normal + 1
  osPriorityNormal2       = 24+2,       ///< Priority: normal + 2

哦哦哦,原来是24,那为了避免默认任务打扰我们,直接从25开始!

OKOK,说这么多,先把程序下载到板子看看啥情况,记得打开串口哦

在这里插入图片描述

怎么个事,我的Task3呢!!!

函数介绍里,xTaskCreate()会返回值,可以根据返回值判断任务是否成功

创建任务不成功的原因有很多,有一个可能就是给FreeRTOS分配的地方太小,装不下那么多任务

Task_Start添加几行代码,我们打印出来看看

void Task_Start(void *argument)
{
	printf("Hello!Task Start!\r\n");
	
	xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
	xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	
	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);
	
	BaseType_t xReturn;  // 用于接收任务3的创建结果
	
	xReturn = xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);;

	if (xReturn == pdPASS) // 创建成功
	{
			printf("Task3 创建成功!\r\n");
	}
	else // 创建失败
	{
			printf("Task3 创建失败!\r\n");
	}
	
	printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());
	
	// 删除自己
   	vTaskDelete(NULL); 
}

串口输出

在这里插入图片描述

Task3创建失败,但是Free Heap: 568不是还有空地方吗??

xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);

猪猪猪:我们创建任务时,任务堆栈实际占用的内存大小 = 128 words × 4 字节 = 512 字节

再看你当时打印的 Free Heap:568 字节,确实还剩下一点,但:

原因说明
剩余堆空间不够你还有 568 字节,但新任务创建至少要分配 堆栈空间 + TCB 控制块内存(约 100~200 字节),总共就超过 568 字节了。
堆碎片化即使堆总量看起来够用,但因为分散,可能没有一整块连续的大内存区域给任务使用,导致创建失败。

那么解决办法

  • 减小任务堆栈大小,一般简单任务(比如只打印或轮询按键)用不了这么大栈。
// 试试减小堆栈到 100 或 96(word 单位)
xTaskCreate(Task3_KEY, "Task3", 100, NULL, 28, &TaskKEY_Handle);
  • 增加堆大小,在 FreeRTOSConfig.h 中修改:
#define configTOTAL_HEAP_SIZE    ( ( size_t ) ( 5 * 1024 ) )  // 改为 5KB 或更大

选择了第二种,改为 5KB!

重新下载,看看是不是这个原因
在这里插入图片描述

OK!成功了

前四行是开始任务创建的三个任务,后面也可以看出来是优先级最高的Task3执行,然后就是Task2,最后是Task1。但是为什么创建任务的第一次打印不是Task3最开始呢???

我们看看开始任务里,我们最先创建的是Task1,而且它高于开始任务。

xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	
xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);

所以,Task1被创建完成后,直接就开始执行了,Task2是FreeRTOS执行完Task1后再回到Task_Start里创建的,Task3同理!那开始的时候怎么才能按优先级执行呢?临界区!后面我们会详细说明,这里只需要知道这个是停止执行任务的OK了。

void Task_Start(void *argument)
{
	printf("Hello!Task Start!\r\n");
	
	taskENTER_CRITICAL(); // 进入临界区
	
	xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
	xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	
	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);
  	vTaskDelete(NULL); // 删除自己
	
	taskEXIT_CRITICAL(); // 退出临界区
}

加上这两行代码即可,我们再下载,打开串口看看(截图太麻烦啦,直接复制粘贴了)

Hello!Task Start!
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_2--2
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
Task_2--3
Task_1--3

OKOK,这回就对了。

2.2.2 编写任务

根据题目要求我们把任务函数补充完整

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */

uint8_t LEDx = 0x00;
uint8_t LED1_Flag = 1;
uint8_t LED2_Flag = 1;

TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument);


/****** (1) 创建四个任务函数 *******/

void Task_Start(void *argument)
{
	printf("Hello!Task Start!\r\n");
	
	taskENTER_CRITICAL(); // 进入临界区
	
	xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
	xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	
	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);

	// 删除自己
    vTaskDelete(NULL); 
	
	taskEXIT_CRITICAL(); // 退出临界区
}


void Task1_LED(void *argument)
{
	while (1)
	{
		static int N1 = 0;
		N1 ++;
		
		printf("Task_1--%d\r\n",N1);
		
		switch(LED1_Flag) // LED1闪烁
		{
			case 1: LEDx |= 0x01; LED1_Flag = 2; break;
			case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;
			default: break;
		}
		LED_Disp(LEDx);
		vTaskDelay(500);
	}
}

void Task2_LED(void *argument)
{
	while (1)
	{
		static int N2 = 0;
		N2 ++;
		printf("Task_2--%d\r\n",N2);

		switch(LED2_Flag) // LED2闪烁
		{
			case 1: LEDx |= 0x02; LED2_Flag = 2; break;
			case 2: LEDx &= ~(1 << 1); LED2_Flag = 1; break;
			default: break;
		}
		LED_Disp(LEDx);	
		vTaskDelay(500);		
	}
}

void Task3_KEY(void *argument)
{
	while (1)
	{
		static int N3 = 0;
		N3 ++;
		printf("Task_3--%d\r\n",N3);
		
		KEY_Proc(); // 扫描检测按键
		if(KEY_Down == 1) // 按键1--删掉任务1
		{
			vTaskDelete(TaskLED1_Handle);
			printf("删掉了Task_1!!\r\n");
		}
		vTaskDelay(100);	
	}
}

然后下载,查看灯,按下按键1,打开串口看看

Hello!Task Start!
Free Heap: 1992
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_2--2
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
删掉了Task_1!!
Task_2--3
Task_3--11
Task_3--12
Task_3--13
Task_3--14
Task_2--4
Task_3--15
Task_3--16

可以看到,下载完成后,两个灯几乎同亮同灭,按下按键1后,LED1停止闪烁,串口输出已删掉提示

然后我再次按下按键1 ,遇到的问题:

第一次按键正常删除 Task1_LED,但按第二次后串口卡顿,Task3_KEY 不再打印,卡死

第二次进入 vTaskDelete(TaskLED1_Handle),但是任务1已经被删掉了,所以这时候的 TaskLED1_Handle == NULL ,问题根本在于:

vTaskDelete(NULL);  // 当句柄为 NULL 时,删除的是自己!

连续按两次后,**Task3_KEY 中自己把自己删了!**所以就“无了”,串口没输出、任务也不在了。

为了避免这个问题!

void Task3_KEY(void *argument)
{
	while (1)
	{
		static int N3 = 0;
		N3 ++;
		
		printf("Task_3--%d\r\n",N3);
		
		KEY_Proc();
		if(KEY_Val == 1 && TaskLED1_Handle != NULL) // 关键!防止再次误删
		if(KEY_Down == 1)
		{
			vTaskDelete(TaskLED1_Handle);
			printf("删掉了Task_1!!\r\n");
			TaskLED1_Handle = NULL;  // 关键!防止再次误删
		}
		vTaskDelay(100);	
	}
}

OK,解决!

2.2.3 完整代码

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "LED.h"
#include "lcd.h"
#include "KEY.h"
#include "usart.h"
#include "stdio.h"
/* USER CODE END Includes */

/* USER CODE BEGIN Variables */
uint8_t LEDx = 0x00;
uint8_t LED1_Flag = 1;
uint8_t LED2_Flag = 1;

TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument);

/****** (1) 创建四个任务函数 *******/

void Task_Start(void *argument)
{
	printf("Hello!Task Start!\r\n");
	
	taskENTER_CRITICAL(); // 进入临界区
	
	xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
	xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);	
	xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);
	
//	BaseType_t xReturn;  // 用于接收任务创建结果
//	
//	xReturn = xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);;

//	if (xReturn == pdPASS) // 创建成功
//	{
//			printf("Task3 创建成功!\r\n");
//	}
//	else // 创建失败
//	{
//			printf("Task3 创建失败!\r\n");
//	}
 
	printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());

  	vTaskDelete(NULL); // 删除自己
	
	taskEXIT_CRITICAL(); // 退出临界区
}

void Task1_LED(void *argument)
{
	while (1)
	{
		static int N1 = 0;
		N1 ++;
		
		printf("Task_1--%d\r\n",N1);
		
		switch(LED1_Flag)
		{
			case 1: LEDx |= 0x01; LED1_Flag = 2; break;
			case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;
			default: break;
		}
		LED_Disp(LEDx);
		vTaskDelay(500);
	}
}

void Task2_LED(void *argument)
{
	while (1)
	{
		static int N2 = 0;
		N2 ++;
		printf("Task_2--%d\r\n",N2);

		switch(LED2_Flag)
		{
			case 1: LEDx |= 0x02; LED2_Flag = 2; break;
			case 2: LEDx &= ~(1 << 1); LED2_Flag = 1; break;
			default: break;
		}
		LED_Disp(LEDx);	
		vTaskDelay(500);		
	}
}

void Task3_KEY(void *argument)
{
	while (1)
	{
		static int N3 = 0;
		N3 ++;
		
		printf("Task_3--%d\r\n",N3);
		
		KEY_Proc();
		if(KEY_Val == 1 && TaskLED1_Handle != NULL)
		if(KEY_Down == 1)
		{
			vTaskDelete(TaskLED1_Handle);
			printf("删掉了Task_1!!\r\n");
			TaskLED1_Handle = NULL;  // 关键!防止再次误删
		}
		vTaskDelay(100);	
	}
}

void MX_FREERTOS_Init(void) {
    
  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
	
  /****** (2) 添加任务 *******/
  xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL);
}

番外:

除了开始任务,其他三个任务都是死循环,如果放在main.cwhile函数中

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_I2C1_Init();

  osKernelInitialize();

  MX_FREERTOS_Init();

//  osKernelStart();

  while (1)
  {
		Led1_Test();
		LED2_Test();
      	KEY_Test();
  }
}

会发现,只有LED1灯闪,LED2和按键没反应,因为程序被卡死在LED1_Test();,进不到下一个程序了。

猪猪猪:FreeRTOS 启动后,main()主循环就被“弃用了”

  osKernelInitialize();    // 初始化 RTOS 内核
  MX_FREERTOS_Init();      // 创建任务
  osKernelStart();         // 启动 RTOS,开始多任务调度!

一旦执行到 osKernelStart()控制权就交给 FreeRTOS 的调度器了,程序不会再执行之后的代码,包括 while(1),所以要先注释掉osKernelStart(); !

三、任务挂起与恢复

3.1 函数介绍

通过本实验,掌握 FreeRTOS 中与 任务挂起与恢复 相关的 API 函数,包括:

  • vTaskSuspend() 挂起任务
  • vTaskResume() 恢复被挂起的任务
  • xTaskResumeFromISR() 从中断服务函数中恢复任务

3.1.1 任务挂起vTaskSuspend()

(1)函数原型

void vTaskSuspend(TaskHandle_t xTaskToSuspend);

(2)参数解释

参数说明
xTaskToSuspend要挂起的任务句柄。 如果传 NULL,表示挂起当前任务
  • 挂起任务后,该任务会停止运行,直到被恢复。
  • 被挂起的任务不会被调度器调度,CPU 不会再执行它。

(3)示例

// 挂起 LEDTask 任务
vTaskSuspend(LEDTaskHandle);  

// 自己挂起自己
vTaskSuspend(NULL); 

3.1.2 任务恢复 vTaskResume()

(1)函数原型

void vTaskResume(TaskHandle_t xTaskToResume);

(2)参数解释

参数说明
xTaskToResume要恢复的任务句柄
  • 将之前挂起的任务重新加入就绪队列,使其可以继续执行。
  • 只能用于恢复由 vTaskSuspend() 挂起的任务

(3)示例

vTaskResume(LEDTaskHandle);  // 让 LEDTask 任务恢复运行

3.1.3 从中断任务恢复 xTaskResumeFromISR()

(1)函数原型

BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);

(2)参数解释

参数名含义
xTaskToResume要恢复的任务的句柄,仅能用于被 vTaskSuspend() 挂起的任务

(3)返回值说明

返回值含义
pdTRUE任务恢复后就绪,建议在中断中进行一次任务切换
pdFALSE无需切换上下文(恢复任务未使更高优先级任务就绪)

(4)示例

void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    xTaskResumeFromISR(LEDTaskHandle);  // 恢复任务

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  // 判断是否需要任务切换
}

3.1.4 获取任务状态 eTaskGetState()

(1)函数原型

eTaskState eTaskGetState(TaskHandle_t xTask);

(2)参数解释

参数名含义
xTask要查询状态的任务的句柄

(3)返回值说明

返回值含义
eReady任务已准备好执行,但当前没有在运行。任务在就绪队列中等待调度。
eRunning任务当前正在运行。
eBlocked任务因等待某些资源(例如信号量、队列等)而被阻塞。
eSuspended任务已被挂起,不能被调度执行。
eDeleted任务已经被删除。

(4)示例

void Task3_KEY(void *argument)
{
    while (1)
    {
        static int N3 = 0;
        N3++;

        printf("Task_3--%d\r\n", N3);

        // 查询任务 1 (TaskLED1) 的状态
        eTaskState taskState = eTaskGetState(TaskLED1_Handle);

        if (taskState == eSuspended)
        {
            printf("TaskLED1 is suspended.\r\n");
        }
        else if (taskState == eRunning)
        {
            printf("TaskLED1 is running.\r\n");
        }
        else if (taskState == eBlocked)
        {
            printf("TaskLED1 is blocked.\r\n");
        }
        else if (taskState == eReady)
        {
            printf("TaskLED1 is ready.\r\n");
        }
        else
        {
            printf("TaskLED1 is deleted.\r\n");
        }

        // 延时
        vTaskDelay(100);
    }
}

说明eTaskGetState() 用来查询 xxx_Handle 的状态。根据返回的状态值 (eSuspended, eRunning, eBlocked, eReady, eDeleted),以便根据任务的当前状态做出适当的逻辑判断。

3.2 编写例题代码

正点原子例题

在这里插入图片描述

3.2.1 任务挂起/恢复

在任务3里进行任务1的挂起和恢复

在前一章的完整代码下,其他的函数不变,更改一下void Task3_KEY(void *argument)

void Task3_KEY(void *argument)
{
	while (1)
	{
		static int N3 = 0;
		N3 ++;
		
		printf("Task_3--%d\r\n",N3);
		
		KEY_Proc();
        // 按下按键1 挂起任务1
		if(KEY_Down == 1)
		{
			vTaskSuspend(TaskLED1_Handle);
			printf("-----挂起任务1-----\r\n");
		}
        // 按下按键2 恢复任务1
		else if(KEY_Down == 2)
		{
			vTaskResume(TaskLED1_Handle);
			printf("-----恢复任务1-----\r\n");
		}
		vTaskDelay(100);	
	}
}

下载到板子,打开串口助手,串口输出如下:

Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
... ...
Task_2--3
Task_1--5
Task_3--20
Task_3--21
Task_3--22
Task_3--23
-----挂起任务1-----
Task_3--24
Task_3--25
Task_3--26
Task_3--27
Task_3--28
Task_2--4
Task_3--29
Task_3--30
Task_3--31
Task_3--32
Task_3--33
Task_3--34
Task_3--35
Task_3--36
Task_3--37
Task_2--5
Task_3--38
Task_3--39
Task_3--40
Task_3--41
Task_3--42
Task_3--43
Task_3--44
-----恢复任务1-----
Task_1--6
Task_3--45
Task_3--46
Task_2--6
Task_3--47
Task_3--48
Task_1--7
Task_3--49
Task_3--50
Task_3--51
Task_3--52
Task_3--53
Task_1--8
Task_3--54
Task_3--55
Task_2--7
Task_3--56
-----挂起任务1-----
Task_3--57
Task_3--58
Task_3--59
Task_3--60
Task_3--61
Task_3--62
Task_3--63
Task_2--8
Task_3--65
Task_3--66
Task_3--67
Task_3--68
Task_3--69
Task_3--70
Task_3--71
Task_3--72
Task_3--73
Task_2--9
Task_3--74
Task_3--75
Task_3--76
Task_3--77
Task_3--78
Task_3--79
Task_3--80
Task_3--81
Task_3--82
Task_2--10
Task_3--83
Task_3--84
Task_3--85
Task_3--86
Task_3--87
-----恢复任务1-----
Task_1--9
Task_3--88
Task_3--89
Task_3--90
Task_2--11
Task_1--10
Task_3--92

可以看见按下按键1,挂起后任务1后就没有再执行过任务1,恢复后继续执行

第一次挂起前最后是Task_1--5,挂起后没有输出;恢复后继续之前的输出Task_1--6。第二次同理!

但是有一个问题,如果我们重复按下,就会一直显示“恢复任务“,但其实并没有,第一次按下时已经恢复了任务1,后面按下都是无效的!!所以我们改一下代码,避免无效恢复。

这个时候我们就会用到函数 eTaskGetState获取任务状态,如果被挂起,才执行恢复

void Task3_KEY(void *argument)
{
	while (1)
	{
		... ... 
		else if(KEY_Down == 2) 
		{
			switch(Task1_State)  // (2)根据判断执行
			{
				case 2: // 未被挂起
					printf("-----KEY2--已经恢复过啦-----\r\n");
                    //printf("-----KEY2--任务1没被挂起-----\r\n");
					break;
				case 1:	// 挂起
					vTaskResume(TaskLED1_Handle);
					printf("-----恢复任务1-----\r\n");
					break;
				default: 
					break;
			}						
		}
		
		// (1)判断任务1 是否被挂起
		if(eTaskGetState(TaskLED1_Handle) == eSuspended)
		{
			Task1_State = 1; // 挂起
		}
		else
		{
			Task1_State = 2; // 未被挂起
		}		
		
		vTaskDelay(100);	
	}
}

下载到板子,打开串口助手,串口输出如下:

Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
-----挂起任务1-----
Task_3--5
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
Task_2--2
Task_3--11
-----恢复任务1-----
Task_1--2
Task_3--12
Task_3--13
... ...
Task_1--5
Task_3--26
-----KEY2--已经恢复过啦-----
Task_3--27
Task_2--4
Task_3--29
Task_1--6
Task_3--30

OK!

3.2.2 从中断恢复任务

跟上面类似,只不过用的函数不同

首先在CubeMX打开按键中断,我是把PB2设置为上升沿中断触发

在这里插入图片描述

然后编写中断代码

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if(GPIO_Pin == GPIO_PIN_2) // 判断是否是 PB2 引脚触发的中断
  {
		BaseType_t xResume = pdFALSE; // 用来接收 xTaskResumeFromISR() 的返回值
		
		switch(Task1_State) // 判断任务1是否挂起
		{
			case 2: // 未被挂起
				printf("-----EXTI--已经恢复过啦-----\r\n");
                //printf("-----EXTI--任务1没被挂起-----\r\n");
			  break;
			case 1: // 挂起
				//如果该任务优先级高于当前运行任务,将返回 pdTRUE,否则返回 pdFALSE
				xResume = xTaskResumeFromISR(TaskLED1_Handle); 
				if(xResume == pdTRUE)
				{
					//pdTRUE,说明 ISR 中恢复的任务的优先级高,需要立即切换到该任务运行。
					portYIELD_FROM_ISR(xResume);
				}	
				printf("-----从中断中恢复任务1-----\r\n");
				break;
			default: 
				break;
		}	
  }
}

这里跟之前的有点不一样,多出了一个立即切换到该任务运行的判断,啥意思呢??

假设你在一个公司,正在做自己手头的工作。突然,老板交给你一个任务。

  • 如果老板说这个任务非常紧急,你就必须立刻去做老板的任务,等到老板的任务做完再去做你手头的工作。
  • 如果老板说这个任务不太紧急,可以等下再做,那么你就不需要立刻停止当前的工作,等你做完手头的工作,再去处理老板的任务。

代码中的 xTaskResumeFromISR()portYIELD_FROM_ISR()

  • xTaskResumeFromISR() :在中断中恢复任务,就像是老板突然发出“任务”,并且会返回值告诉你是否紧急
    • pdTRUE:表示中断被恢复的任务优先级更高,非常紧急
    • pdFALSE:表示当前任务优先级更高或相同,不急
  • portYIELD_FROM_ISR() :这个是立刻切换去执行的函数
    • portYIELD_FROM_ISR(pdTRUE):急急如律令,立刻去执行中断被恢复的任务
    • portYIELD_FROM_ISR(pdFALSE):不急,继续当前的任务,等会儿再说。

OK,下载到板子,打开串口助手,串口输出如下:

Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
-----挂起任务1-----
Task_2--2
Task_3--11
Task_3--12
Task_3--13
Task_3--14
Task_3--15
Task_3--16
Task_3--17
Task_3--18
-----从中断中恢复任务1-----
Task_1--3
Task_2--3
Task_3--20
Task_3--21
Task_3--22
Task_3--23
Task_1--4
Task_3--24
Task_3--25
Task_3--26
-----EXTI--已经恢复过啦-----
Task_3--27
Task_3--28
Task_1--5
Task_2--4
Task_3--29
Task_3--30
Task_3--31
Task_3--32

OK,完美!

四、FreeRTOS中断管理

4.1 概念理解

4.1.1 中断管理

STM32的中断优先级的两个组成部分:

  • 抢占优先级(Preemption Priority):决定一个中断是否可以“打断”另一个正在执行的中断。
  • 子优先级(Sub Priority):在抢占优先级相同的情况下,决定两个中断“谁先响应”。

我们可以打开Cubemx,点击NVIC查看,已经自动的帮我们把一些中断设置改了。

在这里插入图片描述

当前设置:4 bits for pre-emption priority, 0 bits for subpriority,即 NVIC_PRIORITYGROUP_4

  • 这意味着所有中断的优先级完全由抢占优先级决定,子优先级不起作用。
  • 在这种设置下,优先级范围为 0(最高优先级)到 15(最低优先级)。

这也是FreeRTOS官方建议的中断设置!

4.1.2 中断优先级推荐设置

为什么要用 NVIC_PRIORITYGROUP_4 呢??

  • FreeRTOS 内核只关注抢占优先级(Preemption Priority)
  • 子优先级对 FreeRTOS 是“透明”的,它不会参与调度判断
  • 如果设置了子优先级,FreeRTOS 不会管,结果就容易出“错”
  • 简化优先级配置逻辑,降低出错率

举个例子:假设你用了 2 位抢占、2 位子优先级(NVIC_PRIORITYGROUP_2)

  • 两个中断 A 和 B:
    • 抢占优先级相同
    • 子优先级不同
  • FreeRTOS 会认为它们优先级一样(只看抢占),但实际中:
    • Cortex-M 内核允许按子优先级执行顺序
    • 这可能让“低优先级中断”先执行 → 打乱预期调度!

4.1.3 FreeRTOS相关宏

打开FreeRTOSConfig.h ,有关中断定义的相关宏

/* Cortex-M 特定的设置 */
/* 检查是否已经定义了中断优先级位数 */
#ifdef __NVIC_PRIO_BITS
    /* 如果使用 CMSIS,直接使用系统定义的优先级位数 */
    #define configPRIO_BITS         __NVIC_PRIO_BITS
#else
    /* 如果没有使用 CMSIS,默认使用 4 位优先级 */
    #define configPRIO_BITS         4
#endif

/* 
 * 设置最低的中断优先级,这个优先级可以用来设置中断的优先级。
 * 数值越小,优先级越高。
 */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   15

/* 
 * 设置可以调用 FreeRTOS API 的最高中断优先级。
 * 优先级数值越低,优先级越高。
 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

/* 计算内核的中断优先级 */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

/* 
 * 配置系统调用的最大中断优先级,确保系统调用的优先级不为零。
 */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

/* 
 * 将 FreeRTOS 的中断处理函数映射到 CMSIS 标准中断处理函数。
 * SVC 用于系统调用,PendSV 用于上下文切换。
 */
#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler

/* 
 * 设置是否使用自定义的 SysTick 处理函数,0 表示使用默认处理函数。
 */
#define USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 0

这段配置是 FreeRTOS 和 Cortex-M 中断优先级对接的重要部分,可以总结为以下几信息:

  1. configPRIO_BITS 表示中断优先级一共用了几位,我们是NVIC_PRIORITYGROUP_4
  2. configLIBRARY_LOWEST_INTERRUPT_PRIORITYconfigLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 是 FreeRTOS允许参与调度(或调用 API)的中断优先级范围,它只能管5~15这部分!!
    • 数值 < 5 的高优中断:FreeRTOS不控制,也不允许在这些中断里用任何 FreeRTOS API。这些高优先级的中断可以写,比如紧急故障中断、DMA完成中断等;
    • 数值 ≥ 5 且 ≤ 15 的中断:可以在中断里调用 FreeRTOS 的函数(比如发消息、信号量)
    • FreeRTOS 自己的调度器用的是优先级 15(也就是最慢的调度中断);
  3. 后面的 configKERNEL_INTERRUPT_PRIORITYconfigMAX_SYSCALL_INTERRUPT_PRIORITY 是为了把上面这些优先级转换成芯片实际使用的格式,Cortex-M 的优先级是左对齐的,所以需要 << (8 - configPRIO_BITS) 来位移。
  4. vPortSVCHandlerxPortPendSVHandler 这些名字是把 FreeRTOS 的关键中断函数(系统调用和任务切换)映射到 CMSIS 的标准函数名,确保启动文件能识别。
  5. USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 设置为 0 表示用 FreeRTOS 默认的 SysTick_Handler。如果有特别需求,比如自己控制滴答定时器,设为 1 可以自己写这个中断函数。默认即可。

4.2 函数介绍

4.2.1 禁用中断 portDISABLE_INTERRUPTS()

(1)函数原型

void portDISABLE_INTERRUPTS(void);
  • 禁用所有可屏蔽中断,常用于进入临界区,保护关键代码不被打断。
  • 禁用中断期间,FreeRTOS 将不会进行任务切换,也不会响应中断服务。

4.2.2 启用中断 portENABLE_INTERRUPTS()

(1)函数原型

void portENABLE_INTERRUPTS(void);
  • 恢复中断响应,使系统能够再次处理中断和任务切换。
  • 通常用于临界区结束后,与 portDISABLE_INTERRUPTS() 配套使用。

(3)示例

portDISABLE_INTERRUPTS();  // 禁用中断

// 临界区域操作
buffer[index++] = data;
// 临界区域操作

portENABLE_INTERRUPTS();   // 恢复中断

4.3 编写例题代码

正点原子例题

在这里插入图片描述

主要就是看FreeRTOS能管理的中断范围,是在5~15之间。

现在打开CubeMX,增加两个定时器中断。

在这里插入图片描述

TIM6TIM7,配置相同!

在这里插入图片描述

然后在NVIC中使能,如果TIM6使能不了,就取消最后一栏的勾(FreeRTOS不允许设置0~4

首先,开启两个定时器,编写两个定时器代码(注:在main.c中写!!)

int main(void)
{
   ... ...
  /* USER CODE BEGIN 2 */
	HAL_TIM_Base_Start_IT(&htim6);
	HAL_TIM_Base_Start_IT(&htim7);
	
	delay_init(170); // 下一个代码用,用于阻塞延时
  /* USER CODE END 2 */
  ... ...
  while (1)
  {

  }
}
... ...
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim == &htim6)
  {
    printf("优先级4--TIM6--中断开启!!\r\n");
		
  }
  else if(htim == &htim7)
  {
    printf("优先级6--TIM7--中断开启!!\r\n");
  }
}

下载到板子,打开串口助手,串口输出如下:

优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
... ...

然后创建一个任务,去控制两个定时器中断的开关。

uint8_t Num = 0;

/****** (1) 创建一个任务函数 *******/
void Task1(void *argument)
{
	while (1)
	{
		if(++Num == 5)
		{
			Num = 0;
            
			portDISABLE_INTERRUPTS(); // 禁用中断
			printf("---关掉---中断啦---\r\n");

			delay_ms(5000); // 阻塞5s
	
			portENABLE_INTERRUPTS(); // 重新启用中断
			printf("---开启---中断啦---\r\n");
		}
		vTaskDelay(1000);
	}
}
/* USER CODE END FunctionPrototypes */
... ...
void MX_FREERTOS_Init(void) 
{
  ... ...

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
	
  /****** (2) 添加任务 *******/
  xTaskCreate(Task1, "Task1", 128, NULL, 25, NULL);
  ... ...
}

猪猪猪:这里的Delay是移植正点原子的,我已经放在了基础文件里。

下载,打开串口!

优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
---关掉--中断啦---
优先级4--TIM6--中断开启!!
优先级4--TIM6--中断开启!!
优先级4--TIM6--中断开启!!
优先级4--TIM6--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
---开启--中断啦---
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
优先级4--TIM6--中断开启!!
优先级6--TIM7--中断开启!!
---关掉--中断啦---
优先级4--TIM6--中断开启!!
优先级4--TIM6--中断开启!!
优先级4--TIM6--中断开启!!

由此可知!!FreeRTOS的任务也不能控制0~4优先级的任务。

优先级数值中断优先级含义是否受 FreeRTOS 管理能否调用 xxxFromISR
0~4高优先级❌ 不受 FreeRTOS 管理❌ 不可调用 FromISR
5~15低优先级✅ 可由 FreeRTOS 管理✅ 可调用 FromISR

configMAX_SYSCALL_INTERRUPT_PRIORITY 通常设置为 5,所以优先级 < 5 的中断不受 FreeRTOS 管理。

宏 / 指令关闭范围会关闭 TIM6(优先级4)吗
portDISABLE_INTERRUPTS()FreeRTOS 管理的中断(优先级5~15)❌ 不会
__disable_irq()所有中断(包括高优先级)✅ 会

__disable_irq()这个是 芯片级别 的中断屏蔽,不受 FreeRTOS 限制,也会关闭所有高优先级中断。

五、临界段代码保护及任务调度器的挂起和恢复

猪猪猪:前面我们学习的是任务的挂起和恢复,现在我们要看任务调度器!!

5.1 概念理解

5.1.1 临界段代码保护

(1)什么是临界段??

这段代码非常敏感,别人不能来打扰我,我要一口气干完!

(2)哪些场景需要??

  • 外设初始化:例如,在初始化 I2C、SPI 等通信外设时,需要确保时序正确。如果在初始化过程中被打断,可能导致设备处于不一致状态。
  • 数据传输:进行数据传输或硬件控制时,若操作被打断,可能会导致数据丢失或损坏。
  • 共享资源访问:多个任务可能同时访问共享资源,例如全局变量、硬件外设、队列等。如果没有适当的同步保护,会导致资源竞争和数据冲突。

(3)什么打断当前程序的运行

就是中断和任务调度

中断:你正在干活,比如说你在搬砖,结果手机响了(来了个中断),你就得先放下砖,去接电话。比如:定时器中断、串口中断、外部中断、DMA中断等,它随时可能打断你当前在干的事情!

任务调度:你是个低优先级任务,刚写一半代码,结果来了个高优先级任务,FreeRTOS 觉得你不够重要,于是暂停你,让别人先跑。

(4)怎么不被打断

关中断!禁止任务调度!

  • 关中断:关闭中断,确保当前任务执行期间不会被其他中断打扰。
  • 禁止任务调度:暂停任务调度,防止低优先级任务被高优先级任务抢占。

临界区直接屏蔽了中断,系统任务调度、ISR都得靠中断!

5.1.2 任务调度器

任务调度器是 FreeRTOS 的大脑,它负责决定系统中哪个任务什么时候运行。简单来说,它就像一个指挥官,按照任务的优先级和时间安排来指挥各个任务的执行。如果有多个任务在等待,调度器会决定哪个任务先执行,哪个任务稍后执行。

在任务执行过程中,有时候会有一些“临界段代码”,这段代码很重要,必须一气呵成执行完,不允许被打断。

但是,问题来了:任务调度器随时可能打断当前任务并切换到另一个任务。如果在执行临界段代码时被调度器打断,可能会导致任务未能完成这段关键操作。为了避免这种情况,可以使用可以“暂停”任务调度器,确保当前任务不会被打断,这样,任务可以放心地执行关键代码,不会因为调度器的干扰而导致错误。

5.2 函数介绍

5.2.1 临界段保护函数(任务级)

(1)函数原型

void taskENTER_CRITICAL(void);
void taskEXIT_CRITICAL(void);

说明:

函数作用使用场景
taskENTER_CRITICAL()进入临界段(任务级) 本质上是关闭中断,防止任务切换在任务函数中使用
taskEXIT_CRITICAL()退出临界段,恢复中断在任务函数中使用

这些函数用于在任务中关闭中断,保护临界代码不被打断。

(2)示例代码

taskENTER_CRITICAL();
IIC_Init();
taskEXIT_CRITICAL();

使用 taskENTER_CRITICAL()taskEXIT_CRITICAL() 包裹,确保在初始化期间不会被其他任务或中断打断。

5.2.2 临界段保护函数(中断级)

(1)函数原型

BaseType_t taskENTER_CRITICAL_FROM_ISR(void);
void taskEXIT_CRITICAL_FROM_ISR(UBaseType_t uxSavedStatusValue);

说明:

函数作用使用场景
taskENTER_CRITICAL_FROM_ISR()进入临界段,返回值当前中断状态,并关闭中断在中断服务函数中使用
taskEXIT_CRITICAL_FROM_ISR(xxx)退出临界段,恢复之前保存的中断状态在中断服务函数中使用

这些函数用于中断服务函数中保护临界代码。

(2)示例代码

UBaseType_t status;  // 定义一个变量,用于保存当前中断状态
status = taskENTER_CRITICAL_FROM_ISR(); // 关闭中断,并保存当前中断状态
IIC_WriteByte(0xA5);
taskEXIT_CRITICAL_FROM_ISR(status); // 开启中断,并恢复之前保存的中断状态

status 变量保存了进入临界段前的中断状态,确保在临界段内执行完关键操作后,可以正确地恢复系统的中断状态,避免不必要的中断丢失或系统行为异常。

5.2.3 任务调度器的挂起和恢复函数

(1)函数原型

void vTaskSuspendAll(void);
BaseType_t xTaskResumeAll(void);

(2)函数说明

函数作用
vTaskSuspendAll()挂起任务调度器,禁止任务调度器进行任务切换
xTaskResumeAll()恢复任务调度器,允许任务切换继续进行

(3)返回值说明

xTaskResumeAll():返回一个 BaseType_t 类型的值。

  • 返回值 pdTRUE 表示调度器已经成功恢复。
  • 返回值 pdFALSE 表示调度器没有恢复。

(4)示例代码

/*****(1)挂起任务调度器 ******/
void Critical_Section(void)
{
    vTaskSuspendAll();   // 挂起任务调度器,禁止任务调度

    IIC_WriteByte(0xA5);  // 比如在 I2C 总线中写入数据

    xTaskResumeAll();     // 恢复任务调度器,允许任务切换
}

/*****(2)调用函数******/
void Task_Function(void *pvParameters)
{
    while (1)
    {
        Critical_Section();  // 执行挂起调度器保护的临界段代码
        vTaskDelay(100);      // 延时100ms
    }
}

通过这两个函数,可以确保在某些重要操作中,任务调度不会打断重要操作。

【这章没有实验】

六、列表和列表项

6.1 概念理解

(1)什么是列表?什么是列表项?

列表可以类比为一个“容器”,专门用于存放和排序很多个列表项。它是 FreeRTOS 中管理调度、事件、延时等机制的基础容器。列表项就是存放在列表中的项目。

列表相当于链表,列表项相当于节点,FreeRTOS 中的列表是一个双向环形链表。

  • 每一个列表项也就是一个个的任务,如果中途增加任务,就插入到列表项,中途删掉任务,就从列表中移出。

  • 列表项的地址是非连续的,是人为链接的,所以数目可以后期改变。

列表项1  <--->  列表项2  <--->  列表项3  <---> ... ...  <---> 末尾列表项
 ^                                                            ^
 |                                                            |
 +------------------------------------------------------------+

假设,我们去医院看病,医院有多个科室,比如:

  • 内科排队列表(List_t)
  • 外科排队列表(List_t)
  • 急诊排队列表(List_t)

每个科室有自己的一个排队列表,用于管理等候的病人顺序,这就像 FreeRTOS 中的一个列表。

每次来挂号,护士会给你一张挂号单,上面写着:到你就诊的时间(或优先级)、你本人的信息、你现在在哪个队伍等等,这张挂号单就是 FreeRTOS 中的列表项。

6.2 函数介绍

6.2.1 列表/列表项结构体

首先!我们先看看每个结构体的定义和成员。

(1)列表项

ListItem_t 是 FreeRTOS 链表中的单个元素,每个元素就是链表中的一个节点。我们来看具体的定义:

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;        /* 指向拥有这个列表项的对象(如 TCB),从而形成一个双向链接 */
    struct xLIST * configLIST_VOLATILE pxContainer;     /* 指向包含该列表项的列表 */
    listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE          /* 校验值,确保数据完整性 */
};·
  • xItemValue: 这是列表项的核心值,通常用来决定排序!比如在任务调度中,可以通过 xItemValue 来表示任务的优先级,数值较小的任务具有较高的优先级(具体根据 FreeRTOS 的排序规则)。
  • pxNextpxPrevious: 指向列表中前/后一个 ListItem_t 元素的指针,这使得链表变成了双向链表,每个节点都知道自己前后节点的位置。
  • pvOwner: 这是一个指向实际拥有该列表项的对象的指针(指向我们的任务)。这样,可以在列表项和实际任务之间形成双向关联。
  • pxContainer: 这是一个指向该列表项所在列表的指针,表明这个列表项属于哪个列表

(2)迷你列表项

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; /* 指向上一个列表项的指针 */
};

迷你列表项也就是末尾列表项,它是一个精简版的列表项结构,只有最基本的字段。

(3)列表

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;
  • uxNumberOfItems: 列表中的项数。记录当前列表中有多少个列表项,但不算上末尾列表项!
  • pxIndex: 用于遍历列表的指针,它指向上次通过 listGET_OWNER_OF_NEXT_ENTRY() 获取的列表项。它帮助 FreeRTOS 在迭代时跟踪当前位置。
  • xListEnd: 这是一个特殊的迷你列表项(MiniListItem_t),标记了列表的结束。其 xItemValue 为最大值,用来作为列表的末尾节点。它不存储实际的数据,只起到标记作用,确保遍历时能正确停止。

所以,刚创建时,列表中的列表项的数量是0,但是列表中已经有了迷你列表项!

┌──────────── List_t ────────────┐
│                                │
│   ┌───────────────┐            │
│   │   xListEnd    │            │
│   │ pxNext → 自己 │◄────┐       │
│   │ pxPrev → 自己 │─────┘       │
│   └───────────────┘            │
└────────────────────────────────┘

创建一个列表项之后

┌──────────── List_t ────────────┐
│                                │
│   ┌─────────────┐              │
│   │ ListItem A  │◄────────┐    │
│   │ pxNext → End│         │    │
│   │ pxPrev → End│         │    │
│   └─────────────┘         │    │
│        ▲                  ▼    │
│   ┌───────────────┐       │    │
│   │   xListEnd    │───────┘    │
│   │ pxNext → A    │            │
│   │ pxPrev → A    │            │
│   └───────────────┘            │
└────────────────────────────────┘

创建两个之后

┌────────────┐     ┌────────────┐     ┌────────────┐
│ ListItem A │ ◄──►│ ListItem B │ ◄──►│  xListEnd  │
└────────────┘     └────────────┘     └────────────┘
     ▲                                      ▲
     └──────────────────────────────────────┘

以此类推!!

6.2.2 初始化列表

(1)函数原型

void vListInitialise( List_t *pxList );

参数解释:

参数名类型说明
pxListList_t*指向要初始化的列表结构体指针

完整函数:

void vListInitialise( List_t *pxList )
{
    /* 初始化列表中当前索引为末尾项 */
    pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );

    /* 初始化列表项的数量为 0 */
    pxList->uxNumberOfItems = ( UBaseType_t ) 0U;

    /* xListEnd 是 MiniListItem 类型,单独作为末尾项 */
    pxList->xListEnd.xItemValue = portMAX_DELAY;

    /* 双向连接:xListEnd 的前后指针都指向自己 */
    pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
    pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
}

(2)示例代码

List_t myList;
vListInitialise(&myList);

6.2.3 初始化列表项

(1)函数原型

void vListInitialiseItem( ListItem_t *pxItem );

参数解释:

参数名类型说明
pxItemListItem_t*指向要初始化的列表项指针

完整函数:

void vListInitialiseItem( ListItem_t *pxItem )
{
    /* 初始化时不属于任何列表 */
    pxItem->pxContainer = NULL;
}

(2)示例代码

ListItem_t myItem;
vListInitialiseItem(&myItem);

6.2.4 列表项插入列表

(1)函数原型

void vListInsert( List_t *pxList, ListItem_t *pxNewListItem );

参数解释:

参数名类型说明
pxListList_t*目标列表
pxNewListItemListItem_t*要插入的列表项,需预先设置 xItemValue

完整函数:

void vListInsert( List_t *pxList, ListItem_t *pxNewListItem )
{
    ListItem_t *pxIterator;
    TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

    /* 从列表的头开始,遍历每个列表项,直到找到插入点 */
    for (pxIterator = (ListItem_t *) &(pxList->xListEnd); 
         pxIterator->pxNext->xItemValue <= xValueOfInsertion; 
         pxIterator = pxIterator->pxNext)
    {
        /* 继续寻找插入位置,空循环体 */
    }

    /* 现在找到插入点,更新指针 */
    pxNewListItem->pxNext = pxIterator->pxNext;          // 新项的下一个项是当前项的下一个
    pxNewListItem->pxPrevious = pxIterator;              // 新项的前一个项是当前项
    pxIterator->pxNext->pxPrevious = pxNewListItem;      // 更新当前项下一个项的前一个指针
    pxIterator->pxNext = pxNewListItem;                  // 更新当前项的下一个指针为新项

    /* 设置新列表项的容器为目标列表 */
    pxNewListItem->pxContainer = pxList;

    /* 列表项数目增加 */
    (pxList->uxNumberOfItems)++;
}

(2)示例代码

ListItem_t item;
item.xItemValue = 10;

vListInsert(&myList, &item);

6.2.5 列表项末尾插入列表

(1)函数原型

void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem );

参数解释:

参数名类型说明
pxListList_t*目标列表
pxNewListItemListItem_t*要插入的列表项

完整函数:

void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem )
{
    /* 获取当前列表的末尾项,即 xListEnd 前的项 */
    ListItem_t *pxIndex = pxList->xListEnd.pxPrevious;

    /* 将新列表项插入到 xListEnd 之前 */
    pxNewListItem->pxNext = ( ListItem_t * ) &( pxList->xListEnd ); // 下一个是xListEnd
    pxNewListItem->pxPrevious = pxIndex;                            // 前一个是插入前的末尾项
    pxIndex->pxNext = pxNewListItem;                      // 更新当前末尾项的下一个项为新项
    pxList->xListEnd.pxPrevious = pxNewListItem;          // 更新 xListEnd 的前一个项为新项

    /* 设置新列表项的容器为目标列表 */
    pxNewListItem->pxContainer = pxList;

    /* 列表项数目增加 */
    (pxList->uxNumberOfItems)++;
}

(2)示例代码

ListItem_t item;
vListInsertEnd(&myList, &item);

6.2.6 列表移出列表项

(1)函数原型

UBaseType_t uxListRemove( ListItem_t *pxItemToRemove );

参数解释:

参数名类型说明
pxItemToRemoveListItem_t*要从列表中移除的列表项

返回值:

  • 类型:UBaseType_t
  • 说明:移除操作后列表中剩余的项数量

完整函数:

UBaseType_t uxListRemove( ListItem_t *pxItemToRemove )
{
    /* 获取当前列表的容器 */
    List_t * const pxList = pxItemToRemove->pxContainer;

    /* 更新前后节点的指针,移除目标项 */
    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; 
    // 后一个节点的前指针指向前一个节点
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;   
    // 前一个节点的下一个指针指向后一个节点

    /* 清除目标项的容器指针,表示已从列表中移除 */
    pxItemToRemove->pxContainer = NULL;

    /* 列表项数目减少 */
    pxList->uxNumberOfItems--;

    /* 返回更新后的列表项数量 */
    return pxList->uxNumberOfItems;
}

(2)示例代码

uxListRemove(&item);

6.3 编写例题代码

正点原子例题

在这里插入图片描述

首先,直接把第二章动态任务的创建和删除代码复制一份,然后把多余任务删掉

... ...
/****** (1) 创建三个任务函数 *******/

void Task_Start(void *argument)
{
	printf("Hello!Task Start!\r\n");
	
	taskENTER_CRITICAL(); // 进入临界区
	
	xTaskCreate(Task1, "Task1", 128, NULL, 26, &Task1_Handle);
	xTaskCreate(Task2, "Task2", 128, NULL, 27, &Task2_Handle);	
	
	printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());

	// 删除自己
    vTaskDelete(NULL); 
	
	taskEXIT_CRITICAL(); // 退出临界区
}

void Task1(void *argument)
{
	while (1)
	{		
		switch(LED1_Flag)
		{
			case 1: LEDx |= 0x01; LED1_Flag = 2; break;
			case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;
			default: break;
		}
		LED_Disp(LEDx);
		
		vTaskDelay(500);
	}
}

void Task2(void *argument)
{
	// 等会在任务2进行列表和列表项操作
	while (1)
	{
		vTaskDelay(1000);		
	}
}
... ...
void MX_FREERTOS_Init(void) {
  ... ...

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
	
  /****** (2) 添加任务 *******/
  xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL)
}

OK!接下来我们需要做的就是

  • 初始化列表和列表项

  • (1) 初始化并添加 列表项1

  • (2) 添加 列表项2

  • (3) 添加 列表项3

  • (4) 删除 列表项2

  • (5) 重新在末尾添加 列表项2

直接在任务2中添加,以下是完整代码:

List_t List1;              // 创建一个列表 List1
ListItem_t ListItem1;      // 创建列表项1 ListItem1
ListItem_t ListItem2;      // 创建列表项2 ListItem2
ListItem_t ListItem3;      // 创建列表项3 ListItem3

void Task2(void *argument)
{
    /**************** 初始化列表、列表项 ****************/
    // 初始化列表
    vListInitialise(&List1);

    // 初始化列表项	
    vListInitialiseItem(&ListItem1);
    vListInitialiseItem(&ListItem2);
    vListInitialiseItem(&ListItem3);

    // 设置列表项的数值
    ListItem1.xItemValue = 40;
    ListItem2.xItemValue = 60;
    ListItem3.xItemValue = 50;

    /**************** 【1】将列表项1插入列表 ****************/
    vListInsert(&List1, &ListItem1);

    /**************** 【2】将列表项2插入列表 ****************/
    vListInsert(&List1, &ListItem2);

    /**************** 【3】将列表项3插入列表 ****************/
    vListInsert(&List1, &ListItem3);

    /**************** 【4】将列表项2移出列表 ****************/
    uxListRemove(&ListItem2);

    /**************** 【5】将列表项2插入列表末尾 ****************/
    List1.pxIndex = List1.pxIndex->pxNext; // pxIndex 后移
    vListInsertEnd(&List1, &ListItem2);
    
	while (1)
	{
		vTaskDelay(1000);		
	}
}

但是过程我们看不到,需要添加打印代码!如下:

void Task2(void *argument)
{
	/**************** 【1】初始化列表、列表项 ****************/
	// 初始化列表
	vListInitialise(&List1);
	
	// 初始化列表项	
	vListInitialiseItem(&ListItem1);
	vListInitialiseItem(&ListItem2);
	vListInitialiseItem(&ListItem3);
	
	// 设置列表项的数值
	ListItem1.xItemValue = 40;
	ListItem2.xItemValue = 60;
	ListItem3.xItemValue = 50;
	
	// 打印列表和其他列表项的地址
	printf("/********* 列表和列表项地址 *********/\r\n");
	printf("项地址:\r\n");
	printf("List1: %#x\r\n", (int)&List1);                      // 打印 TestList 的地址
	printf("List1->pxIndex: %#x\r\n", (int)(List1.pxIndex));    // 打印 pxIndex 的地址
	printf("List1->xListEnd: %#x\r\n", (int)&(List1.xListEnd)); // 打印 xListEnd 的地址
	printf("ListItem1: %#x\r\n", (int)&ListItem1);                 // 打印 ListItem1 的地址
	printf("ListItem2: %#x\r\n", (int)&ListItem2);                 // 打印 ListItem2 的地址
	printf("ListItem3: %#x\r\n", (int)&ListItem3);                 // 打印 ListItem3 的地址
	printf("*************************************/\r\n");

	/**************** 【1】将列表项1插入列表 ****************/
	vListInsert(&List1, &ListItem1);   // 插入 ListItem1 到 List1 中

	// 打印添加后的列表项连接情况
	printf("/******* (1)添加列表项 ListItem1 *******/\r\n");  
	printf("List1->xListEnd->pxNext: %#x\r\n", (int)(List1.xListEnd.pxNext)); 
	printf("ListItem1->pxNext: %#x\r\n", (int)(ListItem1.pxNext));            
	printf("/--------- 前后向连接分割线 ----------/\r\n");  
	printf("List1->xListEnd->pxPrevious: %#x\r\n", (int)(List1.xListEnd.pxPrevious)); 
	printf("ListItem1->pxPrevious: %#x\r\n", (int)(ListItem1.pxPrevious));            
	printf("**************************************/\r\n"); 

	/**************** 【2】将列表项2插入列表 ****************/
	vListInsert(&List1, &ListItem2);   // 插入 ListItem1 到 List1 中

	printf("/****** (2)添加列表项 ListItem2 *******/\r\n");
	printf("List1->xListEnd->pxNext   = %#x\r\n", (int)(List1.xListEnd.pxNext));
	printf("ListItem1->pxNext         = %#x\r\n", (int)(ListItem1.pxNext));
	printf("ListItem2->pxNext         = %#x\r\n", (int)(ListItem2.pxNext));
	printf("/--------- 前后向连接分割线 ----------/\r\n");
	printf("List1->xListEnd->pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));
	printf("ListItem1->pxPrevious       = %#x\r\n", (int)(ListItem1.pxPrevious));
	printf("ListItem2->pxPrevious       = %#x\r\n", (int)(ListItem2.pxPrevious));
	printf("/************************************/\r\n");

	/**************** 【3】将列表项3插入列表 ****************/
	vListInsert(&List1, &ListItem3);
	
	printf("/******** (3)添加列表项 ListItem3 ********/\r\n");
	printf("List1->xListEnd->pxNext   = %#x\r\n", (int)(List1.xListEnd.pxNext));
	printf("ListItem1->pxNext         = %#x\r\n", (int)(ListItem1.pxNext));
	printf("ListItem2->pxNext         = %#x\r\n", (int)(ListItem2.pxNext));
	printf("ListItem3->pxNext         = %#x\r\n", (int)(ListItem3.pxNext));
	printf("/--------- 前后向连接分割线 ----------/\r\n");
	printf("List1->xListEnd->pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));
	printf("ListItem1->pxPrevious       = %#x\r\n", (int)(ListItem1.pxPrevious));
	printf("ListItem2->pxPrevious       = %#x\r\n", (int)(ListItem2.pxPrevious));
	printf("ListItem3->pxPrevious       = %#x\r\n", (int)(ListItem3.pxPrevious));
	printf("/**************************************/\r\n");

	/**************** 【4】将列表项2移出列表 ****************/
	uxListRemove(&ListItem2); 

	printf("/********* (4)删除列表项 ListItem2 *********/\r\n");
	printf("项目地址:\r\n");
	printf("List1->xListEnd.pxNext   = %#x\r\n", (int)(List1.xListEnd.pxNext));
	printf("ListItem1->pxNext        = %#x\r\n", (int)(ListItem1.pxNext));
	printf("ListItem3->pxNext        = %#x\r\n", (int)(ListItem3.pxNext));
	printf("/----------- 前后向连接分割线 ------------/\r\n");
	printf("List1->xListEnd.pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));
	printf("ListItem1->pxPrevious      = %#x\r\n", (int)(ListItem1.pxPrevious));
	printf("ListItem3->pxPrevious      = %#x\r\n", (int)(ListItem3.pxPrevious));
	printf("/****************************************/\r\n");


	/**************** 【5】将列表项2插入列表末尾 ****************/
	vListInsertEnd(&List1, &ListItem2); // 将 ListItem2 添加到链表末尾

	printf("/********* (5)重新在末尾添加列表项 ListItem2 *********/\r\n");
	printf("项目地址:\r\n");
	printf("List1->pxIndex            = %#x\r\n", (int)List1.pxIndex);
	printf("List1->xListEnd.pxNext    = %#x\r\n", (int)(List1.xListEnd.pxNext));
	printf("ListItem2->pxNext         = %#x\r\n", (int)(ListItem2.pxNext));
	printf("ListItem1->pxNext         = %#x\r\n", (int)(ListItem1.pxNext));
	printf("ListItem3->pxNext         = %#x\r\n", (int)(ListItem3.pxNext));
	printf("/----------------- 前后向连接分割线 ----------------/\r\n");
	printf("List1->xListEnd.pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));
	printf("ListItem2->pxPrevious      = %#x\r\n", (int)(ListItem2.pxPrevious));
	printf("ListItem1->pxPrevious      = %#x\r\n", (int)(ListItem1.pxPrevious));
	printf("ListItem3->pxPrevious      = %#x\r\n", (int)(ListItem3.pxPrevious));
	printf("/*************** 链表重连后的结构完成 **************/\r\n");

	while (1)
	{	
		vTaskDelay(1000);		
	}
}

OK!下载,打开串口输出:

Hello!Task Start!
Free Heap: 2616
/********* 列表和列表项地址 *********/
项地址:
List1: 0x200000ac
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
ListItem1: 0x200000c0
ListItem2: 0x200000d4
ListItem3: 0x200000e8
*************************************/
/******* (1)添加列表项 ListItem1 *******/
List1->xListEnd->pxNext: 0x200000c0
ListItem1->pxNext: 0x200000b4
/--------- 前后向连接分割线 ----------/
List1->xListEnd->pxPrevious: 0x200000c0
ListItem1->pxPrevious: 0x200000b4
**************************************/
/****** (2)添加列表项 ListItem2 *******/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000d4
ListItem2->pxNext         = 0x200000b4
/--------- 前后向连接分割线 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000c0
/************************************/
/******** (3)添加列表项 ListItem3 ********/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000e8
ListItem2->pxNext         = 0x200000b4
ListItem3->pxNext         = 0x200000d4
/--------- 前后向连接分割线 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000e8
ListItem3->pxPrevious       = 0x200000c0
/**************************************/
/********* (4)删除列表项 ListItem2 *********/
项目地址:
List1->xListEnd.pxNext   = 0x200000c0
ListItem1->pxNext        = 0x200000e8
ListItem3->pxNext        = 0x200000b4
/----------- 前后向连接分割线 ------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/****************************************/
/********* (5)重新在末尾添加列表项 ListItem2 *********/
项目地址:
List1->pxIndex            = 0x200000b4
List1->xListEnd.pxNext    = 0x200000c0
ListItem2->pxNext         = 0x200000b4
ListItem1->pxNext         = 0x200000e8
ListItem3->pxNext         = 0x200000d4
/----------------- 前后向连接分割线 ----------------/
List1->xListEnd.pxPrevious = 0x200000d4
ListItem2->pxPrevious      = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/*************** 链表重连后的结构完成 **************/

太长啦,我们一个一个分析!!!!!!!

1、初始化列表和列表项

Hello!Task Start!
Free Heap: 2616
/********* 列表和列表项地址 *********/
项地址:
List1: 0x200000ac
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
ListItem1: 0x200000c0
ListItem2: 0x200000d4
ListItem3: 0x200000e8
*************************************/

初始化时,List1->xListEndpxNextpxPrevious 都指向自身。

┌──────────────────────────── List_t ───────────────────────────┐
│                                                               │
│  List1 地址: 0x200000ac                                        │
│  pxIndex → 0x200000b4 (xListEnd)                              │
│                                                               │
│      ┌──────────────── xListEnd ────────────────────┐         │
│      │ 地址: 0x200000b4                              │         │
│      │ pxNext    → 0x200000b4 (xListEnd)             │        │
│      │ pxPrevious→ 0x200000b4 (xListEnd)             │        │
│      └───────────────────────────────────────────────┘        │
└───────────────────────────────────────────────────────────────┘

先注意一个事:

List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4

pxIndex 这个指针用于在链表中记录当前位置。它并不是用来存储链表开始或结束的位置,而是用来标记当前操作或遍历的节点。默认情况下,pxIndex 指向的是 xListEnd(链表的末尾标记),所以地址相同!!

2、列表项1插入到列表

/******* (1)添加列表项 ListItem1 *******/
List1->xListEnd->pxNext: 0x200000c0
ListItem1->pxNext: 0x200000b4
/--------- 前后向连接分割线 ----------/
List1->xListEnd->pxPrevious: 0x200000c0
ListItem1->pxPrevious: 0x200000b4
**************************************/

列表项1和末尾列表项互指。

┌──────────────────────────── List_t ───────────────────────────┐
│                                                               │
│  List1 地址: 0x200000ac                                        │
│  pxIndex → 0x200000b4 (xListEnd)                              │
│                                                               │
│      ┌────────────── ListItem1 ───────────────┐               │
│      │ 地址: 0x200000c0                        │               │
│      │ pxNext    → 0x200000b4 (xListEnd)      │               │
│      │ pxPrevious→ 0x200000b4 (xListEnd)      │               │
│      └────────────────────────────────────────┘               │
│                        ▲                                      │
│                        ▼                                      │
│      ┌──────────────── xListEnd ────────────────┐             │
│      │ 地址: 0x200000b4                          │             │
│      │ pxNext    → 0x200000c0 (ListItem1)       │             │
│      │ pxPrevious→ 0x200000c0 (ListItem1)       │             │
│      └──────────────────────────────────────────┘             │
└───────────────────────────────────────────────────────────────┘

3、列表项2和3插入到列表

/******* 添加列表项 ListItem2 ********/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000d4
ListItem2->pxNext         = 0x200000b4
/--------- 前后向连接分割线 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000c0
/************************************/
/******** 添加列表项 ListItem3 *********/
List1->xListEnd->pxNext   = 0x200000c0
ListItem1->pxNext         = 0x200000e8
ListItem2->pxNext         = 0x200000b4
ListItem3->pxNext         = 0x200000d4
/------------ 前后向连接分割线 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious       = 0x200000b4
ListItem2->pxPrevious       = 0x200000e8
ListItem3->pxPrevious       = 0x200000c0
/**************************************/

可以发现,顺序并1-2-3,而是1-3-2。

┌────────────────────────────── List_t ──────────────────────────────┐
│                                                                    │
│  List1 地址 : 0x200000ac                                           │
│  pxIndex   → 0x200000b4 (xListEnd)                                 │
│                                                                    │
│    ┌──────────────────── ListItem1 ─────────────────────┐         │
│    │ 地址        : 0x200000c0                            │         │
│    │ pxPrevious  → 0x200000b4 (xListEnd)                │         │
│    │ pxNext      → 0x200000e8 (ListItem3)               │◄────┐   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                    │   │
│                          ▼                                    │   │
│    ┌──────────────────── ListItem3 ─────────────────────┐     │   │
│    │ 地址        : 0x200000e8                            │     │   │
│    │ pxPrevious  → 0x200000c0 (ListItem1)               │     │   │
│    │ pxNext      → 0x200000d4 (ListItem2)               │     │   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                    │   │
│                          ▼                                    │   │
│    ┌──────────────────── ListItem2 ─────────────────────┐     │   │
│    │ 地址        : 0x200000d4                            │     │   │
│    │ pxPrevious  → 0x200000e8 (ListItem3)               │      │   │
│    │ pxNext      → 0x200000b4 (xListEnd)                │      │   │
│    └────────────────────────────────────────────────────┘      │   │
│                          ▲                                     │   │
│                          ▼                                     │   │
│    ┌───────────────────── xListEnd ─────────────────────┐      │   │
│    │ 地址        : 0x200000b4                            │      │   │
│    │ pxPrevious  → 0x200000d4 (ListItem2)               │      │   │
│    │ pxNext      → 0x200000c0 (ListItem1)               │◄─────┘   │
│    └────────────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────────────┘

这是因为我们刚刚设置的列表项的数值

 ListItem1.xItemValue = 40; 
 ListItem2.xItemValue = 60; 
 ListItem3.xItemValue = 50;

当我们将这些列表项插入到列表中时,列表会根据 xItemValue 的值进行排序!!!

3、删掉列表项2

/********* (4)删除列表项 ListItem2 *********/
项目地址:
List1->xListEnd.pxNext   = 0x200000c0
ListItem1->pxNext        = 0x200000e8
ListItem3->pxNext        = 0x200000b4
/----------- 前后向连接分割线 ------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/****************************************/

移出列表项2后,列表的结构通过调整 pxNextpxPrevious 指针得以重新连接,使得 ListItem1ListItem3xListEnd又形成了一个连续的双向链表。

┌────────────────────────────── List_t ──────────────────────────────┐
│                                                                    │
│  List1 地址 : 0x200000ac                                            │
│  pxIndex   → 0x200000b4 (xListEnd)                                 │
│                                                                    │
│    ┌──────────────────── ListItem1 ─────────────────────┐          │
│    │ 地址        : 0x200000c0                            │         │
│    │ pxPrevious  → 0x200000b4 (xListEnd)                │         │
│    │ pxNext      → 0x200000e8 (ListItem3)               │◄────┐   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                    │   │
│                          ▼                                    │   │
│    ┌──────────────────── ListItem3 ─────────────────────┐     │   │
│    │ 地址        : 0x200000e8                            │     │   │
│    │ pxPrevious  → 0x200000c0 (ListItem1)               │     │   │
│    │ pxNext      → 0x200000b4 (ListItem2)               │     │   │
│    └────────────────────────────────────────────────────┘     │   │
│                          ▲                                     │   │
│                          ▼                                     │   │
│    ┌───────────────────── xListEnd ─────────────────────┐      │   │
│    │ 地址        : 0x200000b4                            │      │   │
│    │ pxPrevious  → 0x200000e8 (ListItem2)               │      │   │
│    │ pxNext      → 0x200000c0 (ListItem1)               │◄─────┘   │
│    └────────────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────────────┘

4、列表项2插入列表末尾

/********* (5)重新在末尾添加列表项 ListItem2 *********/
项目地址:
List1->pxIndex            = 0x200000b4
List1->xListEnd.pxNext    = 0x200000c0
ListItem2->pxNext         = 0x200000b4
ListItem1->pxNext         = 0x200000e8
ListItem3->pxNext         = 0x200000d4
/----------------- 前后向连接分割线 ----------------/
List1->xListEnd.pxPrevious = 0x200000d4
ListItem2->pxPrevious      = 0x200000e8
ListItem1->pxPrevious      = 0x200000b4
ListItem3->pxPrevious      = 0x200000c0
/*************** 链表重连后的结构完成 **************/

我们是将 pxNewListItem 插入到 xListEnd.pxPrevious 后面,即链表的逻辑尾部(实际是尾前一项),也就是xListEnd 前面(因为 xListEnd 是一个固定项,永远在尾部)。

所以,框图跟删掉列表项2之前一模一样

xListEnd <--> ListItem1 <--> ListItem3 <--> ListItem2 <--> xListEnd

关键点:pxIndex 的作用

  • pxIndex 这个指针用于在链表中记录当前位置。它并不是用来存储链表开始或结束的位置,而是用来标记当前操作或遍历的节点。
  • 如果 pxIndex 没有被改变,默认情况下它指向链表的结尾(xListEnd)。
  • 当调用 vListInsertEnd(&List1, &ListItem2) 时,ListItem2 会被插入到 xListEnd 之前,也就是pxIndex 之前,即链表的末尾。

如果我们改动一下呢??

	List1.pxIndex = &ListItem1;	// 让其指向列表项1
	vListInsertEnd(&List1, &ListItem2); // 将 ListItem2 添加到链表末尾

重新下载输出

/********* (5)重新在末尾添加列表项 ListItem2 *********/
项目地址:
List1->pxIndex            = 0x200000c0
List1->xListEnd.pxNext    = 0x200000d4
ListItem2->pxNext         = 0x200000c0
ListItem1->pxNext         = 0x200000e8
ListItem3->pxNext         = 0x200000b4
/----------------- 前后向连接分割线 ----------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem2->pxPrevious      = 0x200000b4
ListItem1->pxPrevious      = 0x200000d4
ListItem3->pxPrevious      = 0x200000c0
/*************** 链表重连后的结构完成 **************/

我们来对比看看

字段第一次(pxIndex = 0x200000b4)第二次(pxIndex = 0x200000c0)
List1->pxIndex0x200000b40x200000c0
List1->xListEnd.pxNext0x200000c00x200000d4
List1->xListEnd.pxPrevious0x200000d40x200000e8
列表项顺序1 → 3 → 2 → xListEnd2 → 1 → 3 → xListEnd

列表项2被插入到列表项1的前面了!!

所以,在末尾插入列表项,是靠List1->pxIndex决定的!!

OK,一章又完美结束!

七、启动任务调度器【内容太多,先略】

7.1 概念理解

我们点开main.c,可以看见

  /* Init scheduler */
  osKernelInitialize();

  /* Call init function for freertos objects (in cmsis_os2.c) */
  MX_FREERTOS_Init();

  /* Start scheduler */
  osKernelStart();

这个是CubeMX自动生成的

步骤函数名作用
1osKernelInitialize()初始化 RTOS 内核
2MX_FREERTOS_Init()创建任务、信号量、队列等 RTOS 对象
3osKernelStart()启动调度器,开始运行任务

第三句就是启动调度器!

  • osKernelStart() 是 CMSIS-RTOS v2 接口下的 FreeRTOS 调用方式;
  • 如果你使用的是原始 FreeRTOS API,则对应函数是 vTaskStartScheduler();
  • 调用这个函数后,RTOS 会接管 MCU 的控制权,不会再返回主函数

我们点进函数看看

osStatus_t osKernelStart (void) {
  osStatus_t stat;

  // 检查当前是否在中断上下文中执行
  if (IS_IRQ()) {
    stat = osErrorISR;  // 如果是在中断中调用 osKernelStart,返回错误
  }
  else {
    // 检查当前内核状态是否为“就绪”
    if (KernelState == osKernelReady) {
      /* 设置 SVC 的优先级为默认值(在 FreeRTOS 中用于上下文切换) */
      SVC_Setup();

      /* 更改内核状态为“正在运行” */
      KernelState = osKernelRunning;

      /* 启动 FreeRTOS 的调度器,开始任务调度 */
      vTaskStartScheduler();

      // 启动成功,返回 osOK(正常)
      stat = osOK;
    } else {
      // 如果不是“就绪”状态,不允许启动,返回错误
      stat = osError;
    }
  }
    
  // 返回启动结果
  return (stat);
}

有点迷迷糊糊,再看看

代码行说明
IS_IRQ()判断当前代码是否在中断上下文中执行。不能在中断中启动调度器!
KernelState == osKernelReady启动调度器之前,内核状态必须是“就绪”状态。
SVC_Setup()配置 SVC(Supervisor Call)优先级。FreeRTOS 利用 SVC 触发上下文切换。
KernelState = osKernelRunning;状态标志位更新,表示 RTOS 现在正在运行。
vTaskStartScheduler();核心函数,正式启动调度器,执行最高优先级任务。
stat = osOK / osError / osErrorISR返回启动状态,供调用者判断是否成功。

最最最重要的是!vTaskStartScheduler()

它的作用是:

  • 创建空闲任务 (Idle Task),确保系统始终有任务在运行。
  • (可选)创建定时器任务 (Timer Task) ,用于管理软件定时器。
  • 初始化调度器内核相关变量。
  • 关闭中断,调用底层启动调度函数(启动系统时钟节拍中断和任务切换)。
  • 进入任务调度状态,开始多任务运行。

==

八、时间片调度

8.1 概念理解

什么是时间片?

  • 就是每个任务可以占用 CPU 的时间长度
  • 等于系统滴答定时器的中断周期(SysTick,比如 1ms)

假设,我们创建了 3 个优先级相同的任务:Task1, Task2, Task3,系统每 1ms 触发一次 SysTick,即每个任务时间片为 1ms。

时间线 →
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│Task1 │Task2 │Task3 │Task1 │Task2 │Task3 │Task1 │Task2 │Task3 │...
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
 ↑       ↑       ↑
 1ms     2ms     3ms ...

① 时间片不能“累加”

  • 如果一个任务时间片没用完(比如提前进入阻塞态),它不会“攒时间”留着以后用
  • 系统立刻调度下一个就绪态任务。

② 阻塞状态打断调度

  • 比如 Task3 运行到一半用 vTaskDelay(100) 进入阻塞,即使还有时间也不会等待!
  • 立马进入下一个就绪任务(如 Task1),而不是等到 Task3 时间片结束。

② 时间片大小

  • 取决于滴答定时器中断频率

举个小例子:

假设,食堂里有三个学生排队打饭:小明、小红、小刚。他们都同等重要(优先级一样)。

食堂规定:每个人只能打饭 30秒钟,就必须轮到下一个人继续打饭,这 30 秒就是“时间片”。

特别情况:有人提前走了,如果小红打饭打了一半,有急事走了(类比阻塞)。

那食堂阿姨不会等他时间30秒用完,而是立刻让下一个人(小刚)继续打饭。

8.2 函数介绍

8.3 编写例题代码

正点原子例题

在这里插入图片描述


总结

自用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值