系列文章目录
留空
文章目录
- 系列文章目录
- 前言
- 一、从头开始创建一个FreeRTOS工程
- 1.1 在 "Timebase Source" 中,选择其他TIM
- 1.2 配置FreeRTOS的参数
- 1. 3 添加任务
- 二、动态任务的创建/删除
- 2.1 函数介绍
- 2.1.1 创建动态任务`xTaskCreate()`
- 2.1.2 创建静态任务`xTaskCreateStatic()`
- 2.1.3 删除任务 `vTaskDelete()`
- 2.2 编写例题代码
- 2.2.1 添加任务
- 2.2.2 编写任务
- 2.2.3 完整代码
- 三、任务挂起与恢复
- 3.1 函数介绍
- 3.1.1 任务挂起`vTaskSuspend()`
- 3.1.2 任务恢复 `vTaskResume()`
- 3.1.3 从中断任务恢复 `xTaskResumeFromISR()`
- 3.1.4 获取任务状态 `eTaskGetState()`
- 3.2 编写例题代码
- 3.2.1 任务挂起/恢复
- 3.2.2 从中断恢复任务
- 四、FreeRTOS中断管理
- 4.1 概念理解
- 4.1.1 中断管理
- 4.1.2 中断优先级推荐设置
- 4.1.3 FreeRTOS相关宏
- 4.2 函数介绍
- 4.2.1 禁用中断 `portDISABLE_INTERRUPTS()`
- 4.2.2 启用中断 `portENABLE_INTERRUPTS()`
- 4.3 编写例题代码
- 五、临界段代码保护及任务调度器的挂起和恢复
- 5.1 概念理解
- 5.1.1 临界段代码保护
- 5.1.2 任务调度器
- 5.2 函数介绍
- 5.2.1 临界段保护函数(任务级)
- 5.2.2 临界段保护函数(中断级)
- 5.2.3 任务调度器的挂起和恢复函数
- 六、列表和列表项
- 6.1 概念理解
- 6.2 函数介绍
- 6.2.1 列表/列表项结构体
- 6.2.2 初始化列表
- 6.2.3 初始化列表项
- 6.2.4 列表项插入列表
- 6.2.5 列表项末尾插入列表
- 6.2.6 列表移出列表项
- 6.3 编写例题代码
- 七、启动任务调度器【内容太多,先略】
- 7.1 概念理解
- 八、时间片调度
- 8.1 概念理解
- 8.2 函数介绍
- 8.3 编写例题代码
- 总结
前言
自用
猪猪猪:还在更新中
因为参加完蓝桥杯后,想学RTOS,所以直接无缝衔接,此笔记是基于蓝桥杯板子G431RBT6学习的!
一、从头开始创建一个FreeRTOS工程
基本的配置跳过,只记录有关FreeRTOS的创建!
1.1 在 “Timebase Source” 中,选择其他TIM
在 STM32 + FreeRTOS 项目中,FreeRTOS 默认使用 SysTick
作为时基,而 STM32CubeMX 默认的 HAL 库也是使用 SysTick
,这两个会冲突,导致系统运行不正常,尤其是出现任务调度异常、延时失效等问题。
所以把Timebase Source
改成了TIM17
!
1.2 配置FreeRTOS的参数
关键参数(全部默认即可)
参数名称 | 设置值 | 描述 |
---|---|---|
USE_PREEMPTION | Enabled | 启用抢占式调度,允许高优先级任务抢占低优先级任务的CPU时间。 |
CPU_CLOCK_HZ | SystemCoreClock | CPU的时钟频率,通常由系统定义,表示处理器的时钟速度。 |
TICK_RATE_HZ | 1000 | 系统的时基(tick)频率为1000Hz,即每1毫秒产生一个tick。 |
MAX_PRIORITIES | 56 | 系统中任务的最大优先级数,FreeRTOS使用优先级来调度任务。 |
MINIMAL_STACK_SIZE | 128 Words | 任务的最小堆栈大小为128个词(word)。 |
MAX_TASK_NAME_LEN | 16 | 任务名称的最大长度为16个字符。 |
TOTAL_HEAP_SIZE | 3072 Bytes | 为FreeRTOS堆分配的总内存大小为3072字节。 |
Memory Management scheme | heap_4 | 使用的内存管理方案,不同的方案可能有不同的内存分配和释放策略。 |
以下是 FreeRTOS Mode and Configuration 界面中全部参数,按功能模块分类(可跳过)
(1)Kernel Settings(内核设置)
参数名称 | 当前配置值 | 含义说明 |
---|---|---|
USE_PREEMPTION | Enabled | 启用抢占式调度(高优先级任务可立即抢占低优先级任务) |
CPU_CLOCK_HZ | SystemCoreClock | CPU时钟频率(通常由MCU定义,如SystemCoreClock=16MHz ) |
TICK_RATE_HZ | 1000 | 系统Tick频率(1kHz=1ms一个Tick) |
MAX_PRIORITIES | 56 | 最大任务优先级数(0为最低,55为最高) |
MINIMAL_STACK_SIZE | 128 Words | 空闲任务(Idle Task)的堆栈大小(单位:字,具体字节数需乘以字长) |
MAX_TASK_NAME_LEN | 16 | 任务名称的最大字符长度 |
USE_16_BIT_TICKS | Disabled | 禁用16位Tick计数器(使用32位计数器,支持更长运行时间) |
IDLE_SHOULD_YIELD | Enabled | 空闲任务主动让出CPU给同等优先级的用户任务(节能场景可能需要禁用) |
USE_PORT_OPTIMISED _TASK_SELECTION | Disabled | 禁用硬件优化任务选择(通用软件实现,兼容性更好) |
USE_TICKLESS_IDLE | Disabled | 禁用Tickless低功耗模式(始终维持Tick中断) |
(2)Mutexes & Semaphores(互斥量与信号量)
参数名称 | 当前配置值 | 含义说明 |
---|---|---|
USE_MUTEXES | Enabled | 启用互斥量(Mutex)支持。 |
USE_RECURSIVE_MUTEXES | Enabled | 启用递归互斥量(同一任务可重复加锁)。 |
USE_COUNTING_SEMAPHORES | Enabled | 启用计数信号量。 |
QUEUE_REGISTRY_SIZE | 8 | 队列注册表大小(用于调试工具跟踪队列/信号量)。 |
(3)Memory Management(内存管理)
参数名称 | 当前配置值 | 含义说明 |
---|---|---|
TOTAL_HEAP_SIZE | 3072 Bytes | 动态内存堆总大小(根据任务和队列数量调整)。 |
Memory Management scheme | heap_4 | 使用动态内存分配方案4(合并空闲块,避免碎片化)。 |
Memory Allocation | Dynamic / Static | 支持动态和静态内存分配(需用户提供静态内存时需配置configSUPPORT_STATIC_ALLOCATION )。 |
1. 3 添加任务
下图是STM32CubeMX 的默认任务,可以修改它的名称和函数类型,但不能删除它。这是 CubeMX 提供的一个固定设置,用于初始化FreeRTOS和提供一个最基本的任务框架。
参数说明
配置项 | 当前值 | 解释说明 |
---|---|---|
Task Name | defaultTask | 任务的名称,这里是 defaultTask 。任务名称用于标识该任务 |
Priority | osPriorityNormal | 任务的优先级,osPriorityNormal 表示任务的优先级为正常(即中等优先级) |
Stack Size (Words) | 128 | 任务堆栈的大小,单位是字(Words),这里的 128 表示任务栈有128个字的空间。每个字的大小通常是4字节(32位系统) |
Entry Function | StartDefaultTask | 任务的入口函数,任务开始执行时会调用该函数。这里的 StartDefaultTask 是该任务的函数名称 |
Code Generation Option | Default | 代码生成选项,设置为 Default 表示使用默认的代码生成设置 |
Parameter | NULL | 传递给任务的参数,这里设置为 NULL ,表示任务不需要传入任何参数 |
Allocation | Dynamic | 任务栈内存分配方式,设置为 Dynamic 表示任务栈的内存是在运行时动态分配的 |
Buffer Name | NULL | 缓冲区名称,设置为 NULL 表示没有指定缓冲区。通常用于处理一些任务的输入输出缓冲区 |
Control Block Name | NULL | 任务控制块名称,设置为 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); // 翻转LEDvTaskDelay(500); // 延时500ms}
}/***** (2)创建任务函数 ******/
// 创建的任务,系统会把它加入调度器,由 FreeRTOS 自动进行任务切换调度
xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);
这个任务的功能是:每隔 500ms 翻转一次 GPIOB 的 PIN_0 引脚,从而实现 LED 的闪烁效果
位 | 参数名 | 类型 | 示例值 | 含义 |
---|---|---|---|---|
1 | pxTaskCode | TaskFunction_t | LED_Task | 任务函数指针,告诉 FreeRTOS 这个任务要做什么。这里是一个控制 LED 闪烁的函数。 |
2 | pcName | const char * | "LED" | 任务名称,用于调试和查看任务状态时显示的名字。 |
3 | usStackDepth | uint16_t | 128 | 栈大小,单位是“字”(word),不是字节。STM32 中 1 字 = 4 字节,因此此任务分配了 512 字节栈空间。 |
4 | pvParameters | void * | NULL | 传递给任务的参数。如果不需要传递参数,写 NULL 。 |
5 | uxPriority | UBaseType_t | 2 | 任务优先级。值越大,优先级越高。 |
6 | pxCreatedTask | TaskHandle_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,堆栈大小 128xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);// 创建 LED2 任务,优先级 27,堆栈大小 128xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle); // 创建 KEY 按键任务,优先级 28,堆栈大小 128xTaskCreate(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_Start | 25 | 启动时创建其他任务后自删除 |
LED1任务 | Task1_LED | 26 | 控制LED1,每500ms打印一次 |
LED2任务 | Task2_LED | 27 | 控制LED2,每1000ms打印一次 |
按键处理任务 | Task3_KEY | 28 | 处理按键输入,优先级最高 |
我们在第一章可以看到,优先级设置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 + 6osPriorityBelowNormal7 = 16+7, ///< Priority: below normal + 7osPriorityNormal = 24, ///< Priority: normalosPriorityNormal1 = 24+1, ///< Priority: normal + 1osPriorityNormal2 = 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.c
的while
函数中
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 挂起任务1if(KEY_Down == 1){vTaskSuspend(TaskLED1_Handle);printf("-----挂起任务1-----\r\n");}// 按下按键2 恢复任务1else 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,否则返回 pdFALSExResume = 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 中断优先级对接的重要部分,可以总结为以下几信息:
configPRIO_BITS
表示中断优先级一共用了几位,我们是NVIC_PRIORITYGROUP_4。configLIBRARY_LOWEST_INTERRUPT_PRIORITY
和configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
是 FreeRTOS允许参与调度(或调用 API)的中断优先级范围,它只能管5~15
这部分!!- 数值 < 5 的高优中断:FreeRTOS不控制,也不允许在这些中断里用任何 FreeRTOS API。这些高优先级的中断可以写,比如紧急故障中断、DMA完成中断等;
- 数值 ≥ 5 且 ≤ 15 的中断:可以在中断里调用 FreeRTOS 的函数(比如发消息、信号量);
- FreeRTOS 自己的调度器用的是优先级 15(也就是最慢的调度中断);
- 后面的
configKERNEL_INTERRUPT_PRIORITY
和configMAX_SYSCALL_INTERRUPT_PRIORITY
是为了把上面这些优先级转换成芯片实际使用的格式,Cortex-M 的优先级是左对齐的,所以需要<< (8 - configPRIO_BITS)
来位移。 vPortSVCHandler
和xPortPendSVHandler
这些名字是把 FreeRTOS 的关键中断函数(系统调用和任务切换)映射到 CMSIS 的标准函数名,确保启动文件能识别。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
,增加两个定时器中断。
TIM6
和TIM7
,配置相同!
然后在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); // 阻塞5sportENABLE_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 的排序规则)。pxNext
、pxPrevious
: 指向列表中前/后一个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 );
参数解释:
参数名 | 类型 | 说明 |
---|---|---|
pxList | List_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 );
参数解释:
参数名 | 类型 | 说明 |
---|---|---|
pxItem | ListItem_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 );
参数解释:
参数名 | 类型 | 说明 |
---|---|---|
pxList | List_t* | 目标列表 |
pxNewListItem | ListItem_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 );
参数解释:
参数名 | 类型 | 说明 |
---|---|---|
pxList | List_t* | 目标列表 |
pxNewListItem | ListItem_t* | 要插入的列表项 |
完整函数:
void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem )
{/* 获取当前列表的末尾项,即 xListEnd 前的项 */ListItem_t *pxIndex = pxList->xListEnd.pxPrevious;/* 将新列表项插入到 xListEnd 之前 */pxNewListItem->pxNext = ( ListItem_t * ) &( pxList->xListEnd ); // 下一个是xListEndpxNewListItem->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 );
参数解释:
参数名 | 类型 | 说明 |
---|---|---|
pxItemToRemove | ListItem_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 ListItem3void 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->xListEnd
的 pxNext
和 pxPrevious
都指向自身。
┌──────────────────────────── 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后,列表的结构通过调整 pxNext
和 pxPrevious
指针得以重新连接,使得 ListItem1
和 ListItem3
和xListEnd
又形成了一个连续的双向链表。
┌────────────────────────────── 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; // 让其指向列表项1vListInsertEnd(&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->pxIndex | 0x200000b4 | 0x200000c0 |
List1->xListEnd.pxNext | 0x200000c0 | 0x200000d4 |
List1->xListEnd.pxPrevious | 0x200000d4 | 0x200000e8 |
列表项顺序 | 1 → 3 → 2 → xListEnd | 2 → 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自动生成的
步骤 | 函数名 | 作用 |
---|---|---|
1 | osKernelInitialize() | 初始化 RTOS 内核 |
2 | MX_FREERTOS_Init() | 创建任务、信号量、队列等 RTOS 对象 |
3 | osKernelStart() | 启动调度器,开始运行任务 |
第三句就是启动调度器!
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 编写例题代码
正点原子例题
总结
自用