1.1 作者有话要说
对于这个越来约浮躁的社会,什么都要钱,特别是网上那些垃圾教程,越听越模糊,那行吧,我直接就从 FreeRTOS 的 API函数 学起,管你这么多底层内容的,以后再说吧!!!(对不起!还是讲了不少底层知识。。。)(不晓得写得有没有问题,若是大神看到了可以直接指出来!!)
我写的只是简略描述一下这些 API ,然后直接教你怎么调用,但是具体的 API 函数参考官网的 --> FreeRTOS API 引用
主要参考:FreeRTOS_Leung_ManWah的博客-CSDN博客
1.2 CubeMX 配置
这一节主要参考这个大佬的博客->STM32CubeMX学习笔记(28)——FreeRTOS实时操作系统使用(任务管理)_thread id identifies the thread._Leung_ManWah的博客-CSDN博客
1.2.1 新建工程
- 打开 STM32CubeMX 软件,点击
“新建工程”
- 选择 MCU 和封装
- 配置时钟
RCC
设置,选择HSE
(外部高速时钟) 为Crystal/Ceramic Resonator
(晶振/陶瓷谐振器)
选择 Clock Configuration
,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK
的值为 72
后,输入回车,软件会自动修改所有配置
- 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS
设置,选择Debug
为Serial Wire
1.2.2 SYS Timebase Source
在 System Core
中选择 SYS ,对 Timebase Source
进行设置,选择 TIM1 作为HAL库的时基(除了 SysTick 外都可以)
。
在基于STM32 HAL的项目中,一般需要维护的 “时基” 主要有2个:
- HAL的时基,SYS Timebase Source
- OS的时基(仅在使用OS的情况下才考虑)
而这些 “时基” 该去如何维护,主要分为两种情况考虑:
-
裸机运行:
可以通过SysTick
(滴答定时器)或 (TIMx
)定时器 的方式来维护SYS Timebase Source
,也就是HAL库中的uwTick
,这是HAL库中维护的一个全局变量。在裸机运行的情况下,我们一般选择默认的SysTick
(滴答定时器) 方式即可,也就是直接放在SysTick_Handler()
中断服务函数中来维护。 -
带OS运行:
前面提到的SYS Timebase Source
是 STM32 的 HAL 库中的新增部分,主要用于实现HAL_Delay()
以及作为各种timeout
的时钟基准。
在使用了OS(操作系统)之后,OS 的运行也需要一个时钟基准(简称“时基”),来对任务和时间等进行管理。而 OS 的这个 时基 一般也都是通过 SysTick
(滴答定时器) 来维护的,这时就需要考虑 “HAL的时基” 和 “OS的时基” 是否要共用 SysTick(滴答定时器) 了。
如果共用 SysTick,当我们在 CubeMX 中选择启用 FreeRTOS 之后,在生成代码时,CubeMX 一定会报如下提示:
强烈建议用户在使用FreeRTOS的时候,不要使用 SysTick(滴答定时器)作为 “HAL的时基”,因为FreeRTOS要用,最好是要换一个!!!如果共用,潜在一定风险。
1.2.3 FreeRTOS
1.2.3.1 参数配置
在 Middleware
中选择 FREERTOS
设置,并选择 CMSIS_V1
接口版本
CMSIS是一种接口标准,目的是屏蔽软硬件差异以提高软件的兼容性。RTOS v1使得软件能够在不同的实时操作系统下运行(屏蔽不同RTOS提供的API的差别),而RTOS v2则是拓展了RTOS v1,兼容更多的CPU架构和实时操作系统。因此我们在使用时可以根据实际情况选择,如果学习过程中使用STM32F1、F4等单片机时没必要选择RTOS v2,更高的兼容性背后时更加冗余的代码,理解起来比较困难。
在 Config parameters
进行具体参数配置。
1.2.3.2 Kernel settings:
- USE_PREEMPTION:
Enabled
:RTOS使用抢占式调度器;Disabled:RTOS使用协作式调度器(时间片)。 - TICK_RATE_HZ: 值设置为
1000
,即周期就是1ms。RTOS系统节拍中断的频率,单位为HZ。 - MAX_PRIORITIES: 可使用的最大优先级数量。设置好以后任务就可以使用从0到(MAX_PRIORITIES - 1)的优先级,其中0位最低优先级,(MAX_PRIORITIES - 1)为最高优先级。
- MINIMAL_STACK_SIZE: 设置空闲任务的最小任务堆栈大小,以字为单位,而不是字节。如该值设置为128 Words,那么真正的堆栈大小就是 128*4 = 512 Byte。
- MAX_TASK_NAME_LEN: 设置任务名最大长度。
- IDLE_SHOULD_YIELD:
Enabled
空闲任务放弃CPU使用权给其他同优先级的用户任务。 - USE_MUTEXES: 为1时使用互斥信号量,相关的API函数会被编译。
- USE_RECURSIVE_MUTEXES: 为1时使用递归互斥信号量,相关的API函数会被编译。
- USE_COUNTING_SEMAPHORES: 为1时启用计数型信号量, 相关的API函数会被编译。
- QUEUE_REGISTRY_SIZE: 设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会在内核调试器中看到,如果不使用内核调试器的话次宏设置为0即可。
- USE_APPLICATION_TASK_TAG: 为1时可以使用vTaskSetApplicationTaskTag函数。
- ENABLE_BACKWARD_COMPATIBILITY: 为1时可以使V8.0.0之前的FreeRTOS用户代码直接升级到V8.0.0之后,而不需要做任何修改。
- USE_PORT_OPTIMISED_TASK_SELECTION: FreeRTOS有两种方法来选择下一个要运行的任务,一个是通用的方法,另外一个是特殊的方法,也就是硬件方法,使用MCU自带的硬件指令来实现。STM32有计算前导零指令吗,所以这里强制置1。
- USE_TICKLESS_IDLE: 置1:使能低功耗tickless模式;置0:保持系统节拍(tick)中断一直运行。假设开启低功耗的话可能会导致下载出现问题,因为程序在睡眠中,可用ISP下载办法解决。
- USE_TASK_NOTIFICATIONS: 为1时使用任务通知功能,相关的API函数会被编译。开启了此功能,每个任务会多消耗8个字节。
- RECORD_STACK_HIGH_ADDRESS: 为1时栈开始地址会被保存到每个任务的TCB中(假如栈是向下生长的)。
1.2.3.3 Memory management settings:
- Memory Allocation:
Dynamic/Static
支持动态/静态内存申请 - TOTAL_HEAP_SIZE: 设置堆大小,如果使用了动态内存管理,FreeRTOS在创建 task, queue, mutex, software timer or semaphore的时候就会使用heap_x.c(x为1~5)中的内存申请函数来申请内存。这些内存就是从堆ucHeap[configTOTAL_HEAP_SIZE]中申请的。
- Memory Management scheme: 内存管理策略
heap_4
。
1.2.3.4 Hook function related definitions:
- USE_IDLE_HOOK: 置1:使用空闲钩子(Idle Hook类似于回调函数);置0:忽略空闲钩子。
- USE_TICK_HOOK: 置1:使用时间片钩子(Tick Hook);置0:忽略时间片钩子。
- USE_MALLOC_FAILED_HOOK: 使用内存申请失败钩子函数。
- CHECK_FOR_STACK_OVERFLOW: 大于0时启用堆栈溢出检测功能,如果使用此功能用户必须提供一个栈溢出钩子函数,如果使用的话此值可以为1或者2,因为有两种栈溢出检测方法。
1.2.3.5 Run time and task stats gathering related definitions:
- GENERATE_RUN_TIME_STATS: 启用运行时间统计功能。
- USE_TRACE_FACILITY: 启用可视化跟踪调试。
- USE_STATS_FORMATTING_FUNCTIONS: 与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数prvWriteNameToBuffer()、vTaskList()、vTaskGetRunTimeStats()。
1.2.3.6 Co-routine related definitions:
- USE_CO_ROUTINES: 启用协程。
- MAX_CO_ROUTINE_PRIORITIES: 协程的有效优先级数目。
1.2.3.7 Software timer definitions:
- USE_TIMERS: 启用软件定时器。
1.2.3.8 Interrupt nesting behaviour configuration:
- LIBRARY_LOWEST_INTERRUPT_PRIORITY: 中断最低优先级。
- LIBRARY_LOWEST_INTERRUPT_PRIORITY: 系统可管理的最高中断优先级。
1.2.4 创建任务 Task
在 Tasks and Queues
进行配置。
默认空闲任务是在系统无其它任务执行时执行。
创建任务:
- Task Name: 任务名称
- Priority: 优先级,在 FreeRTOS 中,数值越大优先级越高,0 代表最低优先级
- Stack Size (Words): 堆栈大小,单位为字,在32位处理器(STM32),一个字等于4字节,如果传入512那么任务大小为512*4字节
- Entry Function: 入口函数
- Code Generation Option: 代码生成选项
- Parameter: 任务入口函数形参,不用的时候配置为0或NULL即可
- Allocation: 分配方式:Dynamic 动态内存创建
- Buffer Name: 缓冲区名称
- Conrol Block Name: 控制块名称
然后我们创建3个动态任务,1个静态任务。
若是任务空间不够,则加大堆大小:
1.2.5 创建队列 Queue
在 Tasks and Queues
进行配置。
创建一个消息队列 MyQueue01:
- Queue Name: 队列名称
- Queue Size: 队列能够存储的最大单元数目,即队列深度
- Queue Size: 队列中数据单元的长度,以字节为单位
- Allocation: 分配方式:Dynamic 动态内存创建
- Buffer Name: 缓冲区名称(开启静态创建时可以修改)
- Buffer Size: 缓冲区大小(当开启静态创建时默认为32)
- Conrol Block Name: 控制块名称(开启静态创建时可以修改)
1.3 软件调试技巧
若是掌握点 Keil 的 Debug 调试技巧来学 操作系统就最好不过了,因为使用该技巧不需要用到 单片机,而减少了硬件及减小了复杂性。
当然若是你想结合单片机和操作系统一起研究,也是一个不错的学习方法,可以跳过这一小节。
Keil Debug 串口调试技巧_嵌入一下?的博客-CSDN博客
Keil Debug 逻辑分析仪使用_嵌入一下?的博客-CSDN博客
1.4 初期配置
虽然在 CubeMX 配置的都是这个文件,但是可以打开了解一下,也可以直接跳过。
先找到 FreeRTOS.c
这个文件,在头位置找到 FreeRTOS.h
文件跳转过去,往下翻一翻就可以看到 调用了 FreeRTOSConfig.h
文件,可以过来看看,里面写了啥,至于为什么要看,那当然是,省的老是打开 CubeMX 配置生成来去,又卡又慢的。
对于这部分内容想详细了解的可以看下官网(有中文还是挺香的)。
1.5 注意事项
- 使用 STM32CubeMX 代码生成,在 STM32Cube 固件中,通过 ARM 提供的通用 CMSIS-OS( cmsis_os.h 和 cmsis_os.c 文件,这两个文件把给 FreeRTOS 封装了一层,调用这其中的各类函数,和直接调用 FreeRTOS 的函数没有区别 ) 封装层,将 FreeRTOS 用作实时操作系统。也就是说在一套代码里有着两套标准,在阅读源码时需要注意区分。我主要讲解的是 FreeRTOS 的函数,配合 CubeMX 使用,而尽量少用 CMSIS-OS 封装层。
- 等待补充。。。
2 任务创建
这一章的创建任务可以由 CubeMX 生成,任务删除可以看看。
任务创建由两种方法,一种是动态创建,一种是静态创建。
这一章就介绍上面两个 创建任务函数,加一个 删除任务的函数
2.1 xTaskCreate 动态创建
要动态创建得在 CubeMX 中开启 :
2.1.1 函数介绍
对于 动态创建 的 函数如下:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
参数分别是:
- 指向任务入口函数的指针(pvTaskCode):这里要传入的参数是一个自定义的函数,这个任务应该是要符合规则,就是符合 带有一个参数(这个参数是一个 void * 类型,就是可以传入任意类型参数) 的 无返回值 的函数。
- 任务名字(pcName)。
- 任务堆栈大小(usStackDepth):以 size_t(这里的 size_t 是 4 个字节(32位系统中的 int 的大小)) 为
最小步进长度
的堆栈大小。- 假如传入 128,则堆栈大小是 ( 128 * sizeof( size_t ) ) Byte。
- 任务的传入形参(pvParameters),在任务内部可以获取这个形参的值(貌似用的比较少)。
- 任务优先级(uxPriority),任务优先级数值越小,任务优先级越低。有这几个优先级可以选择:
- 这里虽然写了 负的优先级,但是会在创建任务的时候改到 0 值以上( CMSIS-OS 封装层 改值后传入 FreeRTOS 的创建任务中)。
- 这里定义优先级的范围可以是 4 到 32,必须有至少 4 种优先级
- 不知到为什么,我在 cubemx 调成了32的优先级最大值,而在代码中还是只有这几种优先级可以选择,但是我翻看源码的时候,里面虽然支持 0 到 31的优先级,这里却只会枚举定义这么几个。。(现在知道了,是因为 cmsis_os .c/.h 文件是 ARM 提供的通用 CMSIS-OS 封装层,CubeMX 生成不改这个,改的是 FreeRTOSConfig.h 文件,也就是优先级支持了,可以直接传入数字进去,不用管那几个枚举值 )
- 这里虽然写了 负的优先级,但是会在创建任务的时候改到 0 值以上( CMSIS-OS 封装层 改值后传入 FreeRTOS 的创建任务中)。
- 任务句柄(pxCreatedTask),类型是void * 类型,指针指向的是一个
tskTCB
(任务控制块,详见[2.4.2 TCB结构体是什么?](#2.4.2 TCB结构体是什么?))(保存任务的数据结构体) 结构体,里面存储的是这个任务的
返回值是:用于判断创建是否成功的,当值为 pdPASS 时创建成功,当值为 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 时创建失败
2.1.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
2.2 xTaskCreateStatic 静态创建
一般比较少用这个,不是重点。
要静态创建得在 CubeMX 中开启 :
2.2.1 函数介绍
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 );
参数分别是:
- 任务函数起始地址(pxTaskCode),和动态创建描述一样。
- 任务名字(pcName)
- 任务堆栈大小(ulStackDepth),用于指定传入的
puxStackBuffer
参数的 buf 的大小,最小步进大小是 sizeof( StackType_t ) 。- 假如传入 128,则堆栈大小是 ( 128 * sizeof( StackType_t ) ) Byte,此时
puxStackBuffer
的数组必须有 128 的索引。
- 假如传入 128,则堆栈大小是 ( 128 * sizeof( StackType_t ) ) Byte,此时
- 任务的传入形参(pvParameters),和动态创建描述一样。
- 任务优先级(uxPriority),和动态创建描述一样。
- 必须指向至少具有
ulStackDepth
索引的StackType_t
数组(请参阅上面的ulStackDepth
参数),该数组用作任务的堆栈,因此必须是永久性
的(全局,或静态)。 - 是一个 StaticTask_t * 变量,在函数内部被强转成 TCB_t * 变量,然后获取各种 TCB_t(保存任务的数据结构体) 内容,最后给函数当返回值。
返回值是:返回一个TCB_t
指针(这个tcb结构体保存的内容和 pxTaskBuffer 参数是相同的),若是为空,则创建失败。
2.2.2 函数使用
// 一定要放在全局的位置,使栈空间永久存在
StackType_t myTask02Buff[128];
StaticTask_t myTask02Handle;
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 静态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreateStatic((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Buff, &myTask02Handle) != NULL)
{
printf("Create StartTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
2.3 vTaskDelete 删除任务
被删除的任务将从所有的就绪、阻塞、挂起和事件的列表中移除。
空闲任务的责任是要将分配给已删除任务的内存释放掉。
注意:只有内核为任务分配的内存空间才会在任务被删除后自动回收,任务自己占用的内存或资源需要由应用程序自己显式地释放。
2.3.1 函数介绍
void vTaskDelete( TaskHandle_t xTask );
参数:
- xTask ,待删除的任务的句柄。传递 NULL 将导致调用他的任务被删除。
- 这里注意一下,若是传入的参数是 NULL ,则会删除调用他的任务,这个怎么实现的?
- 因为在 tasks.c 中有一个全局 TCB 指针,会指向当前任务 TCB,
- 在 vTaskDelete 函数中调用了 prvGetTCBFromHandle 宏来判断该传入TCB参数是否为空,若是为空,则获取当前正在执行的任务的 TCB ,否则返回传入的 TCB 指针。
该函数没有返回值。
2.3.2 函数使用
// 一定要放在全局的位置,使栈空间永久存在
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
StackType_t myTask02Buff[128];
StaticTask_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
// 当 i == 20 时,释放当前线程 ( StartTask02 )
if(i++ == 20)
{
vTaskDelete(NULL);
}
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
// 静态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreateStatic((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Buff, &myTask02Handle) != NULL)
{
printf("Create StartTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
2.4 对于任务创建的扩展知识
2.4.1 对于CubeMX生成的任务创建函数解读
任务的创建包含两个部分,第一部分是一个宏函数( 动态时为 osThreadDef )(静态时为 osThreadStaticDef),用于打包各个参数,给到一个新创建的 osThreadDef_t 类型变量,该变量名为,os_thread_def_##name,(在宏中,## 号用于连接两边。假如名字是123,则变量名为,os_thread_def_123)。
对于任务创建的第二部分是一个函数(osThreadCreate),在函数内部直接调用本章的任务创建函数,来创建任务。第一个参数是,第一部分的宏创建的 osThreadDef_t 类型变量;第二个参数是,创建函数的第 4 个参数(pvParameters,任务的传入参数)。
2.4.2 TCB结构体是什么?
这一小节参考: FreeRTOS解析:TCB_t结构体及重要变量说明(Task-1)_stacktype结构体_Nrush的博客-CSDN博客
TCB_t 的全称为 Task Control Block,也就是任务控制块,这个结构体包含了一个任务所有的信息,它的定义以及相关变量的解释如下
/*
* 任务控制块。为每个任务分配一个任务控制块(TCB),
* 并存储任务状态信息,包括指向任务上下文(任务的运行时环境,包括寄存器值)的指针
*/
typedef struct tskTaskControlBlock
{
// 这里栈顶指针必须位于TCB第一项是为了便于上下文切换操作,详见 xPortPendSVHandler 中任务切换的操作。
volatile StackType_t *pxTopOfStack;
#if ( portUSING_MPU_WRAPPERS == 1 )
// MPU设置被定义为端口层的一部分。这必须是TCB结构的第二个成员。
xMPU_SETTINGS xMPUSettings;
#endif
// 表示任务状态,不同的状态会挂接在不同的状态链表下(就绪、阻塞、挂起)。
ListItem_t xStateListItem;
// 事件链表项,会挂接到不同事件链表下
ListItem_t xEventListItem;
// 任务优先级,数值越大优先级越高
UBaseType_t uxPriority;
// 指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出
StackType_t *pxStack;
// 任务名
char pcTaskName[ configMAX_TASK_NAME_LEN ];
// 指向栈尾,可以用来检测堆栈是否溢出
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t *pxEndOfStack;
#endif
// 记录临界段的嵌套层数
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
// 跟踪调试用的变量
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
// 任务优先级被临时提高时,保存任务原本的优先级
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
UBaseType_t uxMutexesHeld;
#endif
// 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做 Hook 函数调用
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
// 任务的线程本地存储指针,可以理解为这个任务私有的存储空间
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
// 运行时间变量
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif
// 支持NEWLIB的一个变量
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
// 任务通知功能需要用到的变量
#if( configUSE_TASK_NOTIFICATIONS == 1 )
// 任务通知的值
volatile uint32_t ulNotifiedValue;
// 任务通知的状态
volatile uint8_t ucNotifyState;
#endif
// 用来标记这个任务的栈是不是静态分配的
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
// 延时是否被打断
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
/* 起一个别名 */
typedef tskTCB TCB_t;
2.4.3 创建时的 TCB 获取
对于 任务相关的 API 由一个在 task. h 中 TaskHandle_t 实际就是一个 void * 类型用于接收所有类型的指针,在创建任务时不管调用 xTaskCreate 还是 xTaskCreateStatic 都会获得一个 TaskHandle_t 类型的值,我在追溯到最后时,看见实际这个 TaskHandle_t 指针获取的是一个 TCB_t 的结构体。
拿动态创建任务函数举例:
开始传入的一个 pxCreatedTask 一般是一个空指针,用于接收在函数内部 TCB指针,里面包含任务的信息。
这里把两个参数传入了 prvInitialiseNewTask 函数中。
在 prvInitialiseNewTask 函数的最末尾把 TCB结构体的值,给到了 pxCreatedTask ,函数返回时,这个参数已经指向了创建的新任务的 TCB 结构体。
2.4.4 任务优先级
这一届主要参考大佬博客:FreeRTOS 之任务调度 - 知乎 (zhihu.com)
怎么做到高优先级先行,同等优先级轮转调度的?
有限这里的先行与等待是基于一个认识,就是这些任务都处于就绪态。
只有就绪态才需要这些任务之间有次序排列的问题,只有最高优先级的任务链表的第一个任务,才能第一个进入到运行态。
这些任务在 FreeRTOS 中是通过一个 链表数组 统一管理的。
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ] = {0}; /*< Prioritised ready tasks. */
pxReadyTasksList[] 是个静态全局变量,元素是链表,大小为 configMAX_PRIORITES
,也就是说任务优先级最高可以为 configMAX_PRIORITES - 1
,0 为 最低的优先级,IDLE task
的优先级就是 0.
2.4.4.1 任务添加进就绪链表函数
如何将任务添加进就绪链表,看下图:
从 prvAddTaskToReadyList()
函数可以看出,pxReadyTasksList
数组是根据创建的任务优先级进行初始化的。
比如创建任务时指定的优先级都是2,那么此时只有 pxReadyTasksList[2] 和 pxReadyTasksList[0] 成员被初始化,0 优先级的链表是系统默认创建的(当用户不创建任务的时候,系统就运行 IDLE 任务)。这样做的好处就是,从高到低遍历的时候,如果该数组值为空,表示没有该优先级的任务,直接进入下一级比较。
2.4.4.2 找到任务链表中的当前进入运行态的任务
pxReadyTasksList 链表数组表示如下图所示:
可以看到每个优先级都对应一个循环链表,同一优先级的任务都在同一级链表上。任务调度实际就是找到最高优先级的任务,然后调度它。如何找到最高优先级的任务呢?概括起来就一句话:
从高到低遍历优先级链表,如果链表不为空,就返回该链表中的下一个元素。
从高到低遍历保证了找到的任务是最高优先级的,而返回该链表中的下一个元素,就实现了任务轮转。如果该链表上有多个任务,那么就会执行下一个同等优先级的任务,如果只有一个任务,那还是执行它本身。这样就保证系统永远在执行最高优先级的任务。对应的函数:
这个函数很简单,
- 第一步:通过宏
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );
获取当前在就绪链表
上的最高优先级的链表数组索引值; - 第二步:断言判断当前优先级链表内是否有任务节点(这一步不重要,对结果没影响);
- 第三步:获取当前就绪链表的第一个任务节点。
2.4.5任务句柄的获取
要获得任务句柄,请使用 xTaskCreate() 创建任务并使用 pxCreatedTask 参数,或使用返回值创建任务 xTaskCreateStatic() 并存储该值,或在 调用 xTaskGetHandle() 中使用任务名称。
3 任务控制
3.1 vTaskDelay 相对延时
- 这个函数用于任务延时的
- 是非阻塞延时,当任务调用他时,只有调用他的任务进入阻塞状态,此时会执行其他任务。
- 当延时结束时,延时事件到来,会将被阻塞的任务切换回就绪状态。
- 进入就绪状态不代表可以马上执行该任务,还要等待轮到她进入
- 将
INCLUDE_vTaskDelay
定义为 1,此函数才可用。
3.1.1 函数介绍
void vTaskDelay( const TickType_t xTicksToDelay );
参数:
- xTicksToDelay ,调用任务应阻塞的
tick
周期数。(tick 详见3.10.1)[3.10.1 tick 时钟原理与设置](#3.10.1 tick 时钟原理与设置)
3.1.2 函数使用
// 一定要放在全局的位置,使栈空间永久存在
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
// 当 i == 20 时,释放当前线程 ( StartTask02 )
if(i++ == 20)
{
vTaskDelete(NULL);
}
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.2 vTaskDelayUntil 绝对延时
-
INCLUDE_vTaskDelayUntil
必须被定义为 1 才能使用此函数。 -
此函数与 vTaskDelay() 在一个重要的方面有所不同: 传入的时间参数是相对于 vTaskDelay() 被调用的时间, 而 vTaskDelayUntil() 会指定任务希望取消阻塞的 绝对 时间。
-
具体来说:
- vTaskDelay 函数是通过将 当前任务挂起一段时间来实现延迟 的,而在这段时间内,任务不会执行任何操作。这意味着,如果在任务挂起期间发生了某些事件,例如中断或其他高优先级任务的执行,那么任务将会 “丢失执行”,即任务的执行时间会相应地延迟。因此,vTaskDelay 不能保证任务的执行时间是精确的。
- 相比之下,vTaskDelayUntil 函数是 通过计算时间差来实现延迟 的,它可以精确地控制任务的执行时间。使用 vTaskDelayUntil 的任务可以在指定的时间内暂停执行,然后在预期的时间内恢复执行。这种方式可以保证任务在预期的时间内执行,从而提高系统的实时性和性能。因此,使用 vTaskDelayUntil 的任务不会 “丢失执行”。
-
使用说明:
- 在使用 vTaskDelayUntil 函数时,我们需要首先初始化 pxPreviousWakeTime 变量,然后在函数中传入该变量的地址,以及需要等待的时间间隔 xTimeIncrement。
- 函数会自动计算任务需要等待多长时间,然后阻塞任务直到该时间到达,然后任务会被自动唤醒。这种方式可以精确控制任务的执行时间,从而提高系统的实时性和性能。
3.2.1 函数介绍
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_t xTimeIncrement );
参数介绍:
- pxPreviousWakeTime,
- 指向一个 TickType_t 类型变量的指针,用于保存任务最后一次解除阻塞的时间。
- 这个变量需要在第一次使用前用当前时间进行初始化,可以通过调用 xTaskGetTickCount 函数获取当前时间。
- 在 vTaskDelayUntil 函数内部,此变量会自动更新,以便在下一次调用时使用。
- xTimeIncrement,周期时间段,即任务需要等待的时间。
- 该任务将在 (*pxPreviousWakeTime + xTimeIncrement) 时间解除阻塞。
- 如果任务需要以固定的间隔期执行,可以使用相同的 xTimeIncrement 参数值调用 vTaskDelayUntil 函数。
相信很多人看到这个函数的参数时都会疑问,为什么会有 pxPreviousWakeTime 的传入参数呢,只使用第二个参数不好吗?
但是实际上这个参数在设计上是必须的,因为这个参数是用来定标的,就是当这个周期任务被暂停时,中间会少几个周期未执行,但是我给他唤醒后,他要重新计算下一个周期的到来,而不是像 vTaskDelay 一样,在唤醒后,还保持上次的计数,然后等待不确定的时间来执行任务。所以这个参数是确保函数的周期性的,不管中间是否中断这个周期,在唤醒后能保持周期的性质。
例如,如果一个任务需要每 100 个 tick 执行一次,而在第 300 个 tick 时被暂停了,当它恢复运行时,它错过了第 3 个和第 4 个周期执行,因为此时 *pxPreviousWakeTime + 2*xTimeIncrement 已经小于当前 tick 计数了。因此,在周期性执行的任务被暂停或遇到其他原因导致错过周期时,必须重新计算其所需的唤醒时间,以确保下一个周期性执行在正确的时间开始。
3.2.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
// 创建一个
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
printf("I'm Task02.\r\n");
// 绝对延时
vTaskDelayUntil( &xLastWakeTime, 50 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
// 相对延时
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask03 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.3 xTaskDelayUntil
官网有,而 CubeMX 生成没有的函数。
3.4 uxTaskPriorityGet 获取任务优先级
- 必须将
INCLUDE_uxTaskPriorityGet
定义为 1,此函数才可用。 - 获得任何任务的优先级。
3.4.1 函数介绍
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask );
参数:
- xTask,待查询的任务句柄。传递 NULL 句柄会导致返回调用任务的优先级。
返回:
- xTask 的优先级。
3.4.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
// 创建一个
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
printf("I'm Task02.\r\n");
// 获取 Task01 的优先级(传入参数 Task01 的 TCB 句柄)
printf("Task01 priority : %ld.\r\n", uxTaskPriorityGet(myTask01Handle));
// 绝对延时
vTaskDelayUntil( &xLastWakeTime, 50 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
// 获取当前任务的优先级(传入参数 NULL)
printf("Task03 priority : %ld.\r\n", uxTaskPriorityGet(NULL));
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask03 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.5 vTaskPrioritySet 设置任务优先级
INCLUDE_vTaskPrioritySet
必须定义为 1 才能使用此函数。- 设置任何任务的优先级。如果正在设置的任务的优先级原本就高于当前执行任务的优先级,则函数返回之前将发生上下文切换。
3.5.1 函数介绍
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
参数:
- xTask,正在设置优先级的任务的句柄。空句柄会设置调用任务的优先级。
- uxNewPriority, 将要设置任务的优先级。断言优先级低于
configMAX_priority
。如果未定义configASSERT
,则优先级会被默认限制为 (configMAX_PRIORITIES
- 1)。
返回:
- 无
3.5.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
// 创建一个
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
if(i++ == 10)
{
// 获取任务1的优先级(传入参数 NULL)
printf("Task01 priority : %ld.\r\n", uxTaskPriorityGet(myTask01Handle));
// 设置任务1的优先级为 3
vTaskPrioritySet(myTask01Handle, 3);
printf("Task01 priority : %ld.\r\n", uxTaskPriorityGet(myTask01Handle));
}
// 在之后打印看看会不会切换任务
printf("I'm Task02.\r\n");
// 绝对延时
vTaskDelayUntil( &xLastWakeTime, 50 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
uint8_t i = 0;
for(;;)
{
if(i++ == 10)
{
// 获取当前任务的优先级(传入参数 NULL)
printf("Task03 before priority : %ld\r\n", uxTaskPriorityGet(NULL));
// 设置当前任务的优先级为 3
vTaskPrioritySet(NULL, 3);
printf("Task03 after priority : %ld\r\n", uxTaskPriorityGet(NULL));
}
// 在之后打印看看会不会切换任务
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
// 注意这里的优先级发生了改变
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityLow, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.6 vTaskSuspend 挂起任务
- 必须将
INCLUDE_vTaskSuspend
定义为 1 才能使用此函数。 - 暂停任意任务。无论任务优先级如何,任务被暂停后将永远无法被获取控制器的执行权力。
- 对
vTaskSuspend
的调用不会累积次数,例如:若在同一任务上调用vTaskSuspend ()
两次,将仍然仅需调用一次vTaskResume ()
,即可使任务脱离暂停的状态。 - 若是挂起的任务是自身(即传入参数为 NULL 时),在挂起后不调用 vTaskDelay 这类函数,使自身变成阻塞态,则会一直在任务中轮询,等待阻塞然后才能挂起。
3.6.1 函数介绍
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数:
- xTaskToSuspend 被挂起的任务句柄。传递 NULL 将导致调用任务被暂停。
返回:
- 无
3.6.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
uint8_t i = 0;
for(;;)
{
printf("I'm Task01.\r\n");
if(i++ == 20)
{
// 挂起任务3
vTaskSuspend(myTask03Handle);
}
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
// 创建一个
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
printf("I'm Task02.\r\n");
if(i++ == 10)
{
// 挂起当前任务
vTaskSuspend(NULL);
}
// 绝对延时
vTaskDelayUntil( &xLastWakeTime, 50 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.7 vTaskResume 解除任务挂起
- 必须将
INCLUDE_vTaskSuspend
定义为 1 才能使用此函数。 - 恢复已挂起的任务。
- 由一次或多次调用
vTaskSuspend ()
而挂起的任务可通过单次调用vTaskResume ()
重新运行。
3.7.1 函数介绍
void vTaskResume( TaskHandle_t xTaskToResume );
参数:
- xTaskToResume,要恢复的任务句柄。
返回:
- 无
3.7.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
uint8_t i = 0;
for(;;)
{
printf("I'm Task01.\r\n");
if(i++ == 30)
{
// 挂起任务3
vTaskSuspend(myTask03Handle);
}
else if(i == 50)
{
// 释放任务2、3
xTaskToResume(myTask02Handle);
xTaskToResume(myTask03Handle);
}
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
uint8_t i = 0;
// 创建一个
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
printf("I'm Task02.\r\n");
if(i++ == 20)
{
// 挂起当前任务
vTaskSuspend(NULL);
}
// 绝对延时
vTaskDelayUntil( &xLastWakeTime, 50 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.8 xTaskResumeFromISR 中断内解除任务挂起
-
必须将
include_vTaskSuspend
和INCLUDE_xTaskResumeFromISR 定义为 1
才能使用此函数。 -
可从 ISR (中断服务程序(interrupt service routine))内调用的恢复挂起任务的函数。
-
由多次调用 vTaskSuspend() 中的一次调用挂起的任务可通过单次调用 xTaskResumeFromISR() 重新运行。
-
xTaskResumeFromISR() 通常被认为是一个危险的函数,因为它的操作没有被锁存(函数没有使用任何锁机制来保护其内部操作的
原子性
[3.10.3 原子性什么意思?](#3.10.3 原子性什么意思?))。 出于这个原因,如果中断有可能在任务挂起之前到达并因此丢失中断(我觉得这里应该是中断白运行了的意思,因为任务还没挂起,运行这个函数就是没运行一样了),则绝对不应该使用它来同步任务与中断。 使用信号量,或者更好的直接任务通知,可以避免这种情况。 提供了一个使用直接任务通知的工作示例。
3.8.1 函数介绍
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume );
参数:
- xTaskToResume,要恢复的任务句柄。
返回:
- 当函数的返回值为 pdTRUE 时:恢复运行的任务的优先级等于或高于正在运行的任务,表明在中断服务函数退出后必 须进行一次上下文切换 , 使用 portYIELD_FROM_ISR() 进行上下文切换。
- 当函数的返回值为 pdFALSE 时:恢复运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需 要进行上下文切换。
3.8.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
// 任务1状态
eTaskState Task01State;
// 中断函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
{
if( htim->Instance == TIM1 )
{
// 如果任务处于挂起状态,则给他取消挂起
if( eTaskGetState(myTask01Handle) == eSuspended )
{
/* 恢复被挂起的任务 */
/* 执行上下文切换,当 xTaskResumeFromISR 返回值为 pdTRUE 时, ISR 返回的时候将运行另外一个任务 */
portYIELD_FROM_ISR( xTaskResumeFromISR( myTask01Handle ) );
}
}
}
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
// 挂起自己
vTaskSuspend(NULL);
vTaskDelay( 100 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 1000 );
}
}
void main(void)
{
// 开启定时器溢出中断
HAL_TIM_Base_Start_IT(&htim1);
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.9 xTaskAbortDelay 强制任务离开阻塞状态
- 必须将 INCLUDE_xTaskAbortDelay 定义为 1,此函数才可用。
- 强制任务离开阻塞状态,并 进入“准备就绪”状态,即使任务在阻塞状态下等待的事件没有发生, 并且任何指定的超时没有过期。
3.9.1 函数介绍
BaseType_t xTaskAbortDelay( TaskHandle_t xTask );
参数:
- xTask,将被强制退出阻塞状态的任务的句柄 。
返回:
- 如果 xTask 引用的任务不在“阻塞”状态, 则返回 pdFAIL。 否则返回 pdPASS。
3.9.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
// 使 任务2 强制退出阻塞状态
TaskState = xTaskAbortDelay(myTask02Handle);
if(TaskState == pdFAIL)
{
printf("Task02 Not in a blocked state...\r\n");
}
else if(TaskState == pdPASS)
{
printf("Task02 in a blocked state.\r\n");
}
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 1000 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
3.10 对于任务控制的扩展知识
3.10.1 tick 时钟原理与设置
这一小节参考:[FreeRTOS 系统时钟节拍和时间管理 - Crystal_Guang - 博客园 (cnblogs.com)](https://www.cnblogs.com/yangguang-it/p/7181420.html#:~:text=FreeRTOS 的系统时钟节拍可以在配置文件 FreeRTOSConfig.h 里面设置: %23define,configTICK_RATE_HZ ((TickType_t) 1000) 如上所示的宏定义配置表示系统时钟节拍是 1KHz,即 1ms。)
-
任何操作系统
都需要提供一个时钟节拍,以供系统处理诸如延时、 超时等与时间相关的事件。 -
时钟节拍是特定的
周期性中断
,这个中断是系统心跳。中断之间的时间间隔取决于不同的应
用,一般是 1ms – 100ms。时钟的节拍中断使得内核可以将任务延迟若干个时钟节拍,以及当任务等待
事件发生时,提供等待超时等依据。 -
时钟节拍率越快,系统的额外开销就越大。
-
对于 Cortex-M3 、 Cortex-M4 内核等,一般采用滴答定时器来实现系统时钟节拍的。
-
SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号: 15), 滴答定时器是一个 24 位
的递减计数器,支持中断。 使用比较简单, 专门用于给操作系统提供时钟节拍。 -
FreeRTOS 的系统时钟节拍可以在配置文件 FreeRTOSConfig.h 里面设置:
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
如上所示的宏定义配置表示系统时钟节拍是 1KHz,即 1ms。
- 也可以是 CubeMX 设置 :
-
FreeRTOS 中的时间延迟函数主要有以下两个作用:
- 为周期性执行的任务提供延迟。
- 对于抢占式调度器,让高优先级任务可以通过时间延迟函数释放 CPU 使用权,从而让低优先级任务可以得到执行。
- 下面我们通过如下的框图来说明一下延迟函数对任务运行状态的影响,让大家有一个形象的认识。
- 运行条件:
- 仅对任务 Task1 的运行状态做说明。
- 调度器支持时间片调度和抢占式调度。
- 运行过程描述如下:
- 起初任务 Task1 处于运行态,调用 vTaskDelay 函数后进入到阻塞状态,也就是 blocked 状态。
- vTaskDelay 函数设置的延迟时间到,由于任务 Task1 不是当前就绪的最高优先级任务,所以不能进
入到运行状态,只能进入到就绪状态,也就是 ready 状态。 - 一段时间后, 调度器发现任务 Task1 是当前就绪的最高优先级任务,且在等待链表中排第一,从而任务从就绪态切换到运行态。
- 由于时间片调度,任务 Task1 由运行态切换到就绪态。
3.10.2 delay 链表
3.10.3 原子性什么意思?
原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行
,不会出现执行部分操作的情况。在多线程或多任务的并发执行环境中,原子性是保证共享资源安全的重要性质。
例如,在一个多线程环境中,如果多个线程同时访问同一个共享变量,如果没有保证原子性,就会出现多个线程同时修改同一个共享变量的情况,导致数据不一致的问题。为了避免这种情况,可以使用锁机制来保证某个操作的原子性,即在访问共享资源时先锁定资源,执行完操作后再释放锁,保证只有一个线程可以访问共享资源。
在实时操作系统中,原子性操作也是非常重要的。例如,在中断服务程序中访问共享资源时,需要保证访问的原子性,以避免多个中断同时访问同一共享资源的情况。这可以通过使用临界区来保证。临界区是指在访问共享资源时需要锁定资源,避免其他中断或线程访问该资源,直到当前中断或线程访问完毕后再释放锁。这样可以保证原子性,避免数据不一致的问题。
4 任务实用程序
4.1 uxTaskGetSystemState
- 必须在 FreeRTOS.h 中将 configUSE_TRACE_FACILITY 中定义为 1,才能使用 uxTaskGetSystemState()。
4.1.1 函数介绍
UBaseType_t uxTaskGetSystemState(
TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
unsigned long * const pulTotalRunTime );
参数:
返回:
4.1.2 函数使用
4.2
4.2.1函数介绍
参数:
返回:
4.2.2函数使用
4.8 eTaskGetState 获取任务状态
- 必须在 FreeRTOSConfig.h 中将 INCLUDE_eTaskGetState 设置为 1,eTaskGetState() 才可用。
- 返回在执行 eTaskGetState() 时任务所在的状态作为枚举类型。
4.8.1 函数介绍
eTaskState eTaskGetState( TaskHandle_t xTask );
参数:
- xTask,需要获取状态的任务的任务句柄(注意这里
不支持传入 NULL
,必须具体指定一个任务的句柄)。
返回:
-
返回传入的 任务句柄 对应任务 的 状态
-
-
/* Task states returned by eTaskGetState. */ typedef enum { eRunning = 0, /* A task is querying the state of itself, so must be running. */ eReady, /* The task being queried is in a read or pending ready list. */ eBlocked, /* The task being queried is in the Blocked state. */ eSuspended, /* The task being queried is in the Suspended state, or is in the Blocked state with an infinite time out. */ eDeleted, /* The task being queried has been deleted, but its TCB has not yet been freed. */ eInvalid /* Used as an 'invalid state' value. */ } eTaskState;
-
4.8.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
eTaskState TaskState;
for(;;)
{
printf("I'm Task01.\r\n");
// 获取任务自身的状态
TaskState = eTaskGetState(myTask01Handle);
printf("Task 1 state val:%d\r\n", TaskState);
// 获取任务2状态
TaskState = eTaskGetState(myTask02Handle);
printf("Task 2 state val:%d\r\n", TaskState);
// 获取任务3状态
TaskState = eTaskGetState(myTask03Handle);
printf("Task 3 state val:%d\r\n", TaskState);
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 1000 );
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
4.23
4.23.1函数介绍
参数:
返回:
4.23.2函数使用
4.24 对于任务实用程序的扩展知识
5 RTOS 内核控制
5.1
5.1.1 函数介绍
参数:
返回:
5.1.2 函数使用
5.8 vTaskStartScheduler 开启调度
- 启动 RTOS 调度器。调用后,RTOS 内核可以控制在何时执行哪些任务。
- 空闲任务 和可选的 定时器守护进程任务会自动创建(当 RTOS 调度器启动时)。
- vTaskStartScheduler() 仅在没有足够的 RTOS 堆 可用来创建空闲或定时器守护进程任务时才会返回。
- 所有 RTOS 演示应用程序项目都包含使用 vTaskStartScheduler() 的示例,通常 位于 main.c 的 main() 函数中。
- 总而言之:
- 在创建完任务的时候,我们需要开启调度器,因为创建仅仅是把任务添加到系统中,还没真正调度,并且空闲任务也没实现,定时器任务也没实现,这些都是在开启调度函数 vTaskStartScheduler() 中实现的。为什么要空闲任务?因为 FreeRTOS 一旦启动,就必须要保证系统中每时每刻都有一个任务处于运行态(Runing),并且空闲任务不可以被挂起与删除,空闲任务的优先级是最低的,以便系统中其他任务能随时抢占空闲任务的 CPU 使用权。FreeRTOS学习笔记(2)——任务管理
5.8.1 函数介绍
void vTaskStartScheduler( void );
参数:
- 无
返回:
- 无
5.8.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
5.9 vTaskEndScheduler 结束调度
- 注意: 这仅适用于 x86 Real Mode PC 移植。
- 停止 RTOS 内核滴答。
所有已创建的任务将自动删除
,多任务处理(抢占式或协作式)将停止。然后从调用 vTaskStartScheduler() 的位置恢复执行
,就像 vTaskStartScheduler() 刚刚返回一样。 - 有关使用 vTaskEndScheduler() 的示例,请参阅 demo/PC 目录中的演示应用程序文件 main.c。
- vTaskEndScheduler() 需要在可移植层中定义一个退出函数(有关 PC 移植,请参阅 port.c 中的 vPortEndScheduler())。这将执行硬件特定的操作,例如停止 RTOS 内核滴答。
- vTaskEndScheduler() 将释放 RTOS 内核分配的所有资源,但不会释放应用程序任务分配的资源。
5.9.1 函数介绍
void vTaskEndScheduler( void );
参数:
- 无
返回:
- 无
5.9.2 函数使用
暂无……
5.10 vTaskSuspendAll 挂起所有任务
- 挂起调度器。 挂起调度器会阻止上下文切换,但会让中断处于启用状态。 如果调度器被挂起时,中断请求切换上下文,那么请求将会被挂起。而且只有在调度器恢复(取消挂起)时才会执行。
- 在 vTaskSuspendAll() 之后调用 xTaskResumeAll() 会转换调度器的状态,取消其阻塞状态。
- vTaskSuspendAll() 可以嵌套调用。 调用
xTaskResumeAll()
的次数必须与先前调用vTaskSuspendAll()
的次数相同
,然后调度器将取消挂起状态并重新进入活动状态。 - xTaskResumeAll() 只能在任务中调用,因此要在调度器开启后才能调用。
- 不得在调度器挂起时调用其他
FreeRTOS API
函数。 - 调度器挂起时,**禁止 **调用可能切换上下文的 API 函数(例如 vTaskDelayUntil()、xQueueSend() 等等) 。
5.10.1 函数介绍
void vTaskSuspendAll( void );
参数:
- 无
返回:
- 无
5.10.2 函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 1000 );
vTaskSuspendAll();
/* 处理 xxx 代码 */
// vTaskDelay(10); // 禁止使上下文切换
xTaskResumeAll();
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
5.11xTaskResumeAll 解除所有任务挂起
恢复通过调用 vTaskSuspendAll() 挂起的调度器。
5.11.1函数介绍
BaseType_t xTaskResumeAll( void );
参数:
- 无
返回:
- 返回一个 BaseType_t 类型的值,如果恢复调度器导致了上下文切换,则返回 pdTRUE,否则返回 pdFALSE。
5.11.2函数使用
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t 起了别名
TaskHandle_t myTask02Handle;
TaskHandle_t myTask03Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 1000 );
vTaskSuspendAll();
/* 处理 xxx 代码 */
// vTaskDelay(10); // 禁止使上下文切换
xTaskResumeAll();
}
}
// 任务调度函数
void StartTask03(void const * argument)
{
for(;;)
{
printf("I'm Task03.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)osPriorityIdle, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask03, "myTask03", 128, NULL, (UBaseType_t)osPriorityIdle, myTask03Handle) == pdPASS)
{
printf("Create myTask03 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
5.14 关于 RTOS 内核控制的拓展知识
5.14.1 简单的讲一下调度函数做了什么
参考:FreeRTOS 启动第一个任务 prvStartFirstTask vPortSVCHandler_jltsun的博客-CSDN博客
开启调度函数内部干了什么事情,他是如何调度的?
进来先调用 vApplicationGetIdleTaskMemory 获取事先创建好的空的 静态任务 buff 空间和任务 TCB,然后调用静态创建函数来创建任务,这里创建的是一个空闲任务。
要想知道如何调度的,得先知道是在函数的哪里发生了调度。
先进入 xPortStartScheduler 函数,
继续深入 xPortStartScheduler 函数,又在里面找到了 prvStartFirstTask 函数,
在 prvStartFirstTask 函数中进入了 SVC 异常(这个异常可以看博客的解释 [SVC和PendSV异常有什么用途? - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/513133828#:~:text=SVC指令需要一个8立即数来作为系统调用的代号,SVC异常服务例程会提取这个立即数,从而知道此次的调用请求。 例如调用编号2的系统系统服务请求的汇编指令: SVC,%230x2 一些编译器会提供SVC调用的内建函数,在C%2FC%2B%2B代码中调用该内建函数就会触发SVC异常。 没有提供SVC内建函数的编译器一般是使用内联汇编的方式,在C%2FC%2B%2B中插入SVC异常触发的汇编指令。) ),然后在异常中,找到当前任务指针指向的任务(这个任务是当前已经创建的任务中,优先级最高且在该优先级链表中排名最前的任务),然后在 SVC 异常中跳转到这了任务,完成了任务的调度。
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* 使用NVIC偏移寄存器定位堆栈。 */
ldr r0, =0xE000ED08 // 0xE000ED08 地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址
ldr r0, [r0] // 启动文件中, 最初地址放置的 __initial_sp
ldr r0, [r0] // 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值
/* 将 __initial_sp 的初始值写入 MSP 中,也就是将 msp 设置回堆栈的起始位置. */
msr msp, r0 //
/* 全局启用中断。 */
cpsie i
cpsie f
dsb
isb
/* 调用SVC中断 */
svc 0
nop
nop
}
调用的 SVCall 在怎么找到的?
首先我们找到 汇编文件的 SVC_Handler 通过 ctrl+f 找,然后找到在 FreeRTOSConfig.h 中给他起了别名,然后我们再 ctrl+f 找 vPortSVCHandler,然后在port.c 文件里面找到了vPortSVCHandler 异常。
__asm void vPortSVCHandler( void )
{
PRESERVE8
/* 获取当前任务 TCB */
ldr r3, =pxCurrentTCB
ldr r1, [r3] // 根据 tskTCB 结构体定义, 首地址存的是 pxTopOfStack
ldr r0, [r1] // r0 得到 TopOfStack 的值
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
mov r0, #0
msr basepri, r0
bx r14
}
pxCurrentTCB
指向的是最高优先级的 Ready
状态的任务指针;根据 pxCurrentTCB
获取到对应 tskTCB
的地址;
然后获取第一个成员变量 pxTopOfStack
,也就是当前任务的栈顶地址
;
在任务创建 xTaskCreate
的时候,需要模拟的 Cortex
的异常入栈顺序,做好数据放置;
使用 LDMIA
指令,以 pxTopOfStack
开始顺序出栈,先出 R4~R11
(在创建任务的时候,最后入栈的就是这些个),同时 R0
递增;
将此刻的 R0
赋值给 PSP
(因为出栈的时候,处理器会按照入栈的顺序去取 R4-R11、R14
,而这些寄存器在我们创建任务的时候已经手动压栈)
将 BASEPRI
寄存器赋值为 0,也就是允许任何中断
最后执行 bx R14
,告诉处理器 ISR 完成,需要返回,此刻处理器便会进行出栈操作,PC 被我们赋值成为了执行任务的函数的入口,也即正式跑起来;
5.14.2 什么是空闲任务?
这一节主要参考:FreeRTOS高级篇11—空闲任务分析_freertos空闲任务_研究是为了理解的博客-CSDN博客
当RTOS调度器开始工作后,为了保证至少有一个任务在运行,空闲任务被自动创建,占用最低优先级(0优先级)。
空闲任务是 FreeRTOS 不可缺少的任务,因为 FreeRTOS 设计要求必须至少有一个任务处于运行状态。我们来看一下空闲任务要做的工作。
-
释放内存
- 从V9.0版本开始,如果一个任务删除另外一个任务,被删除任务的堆栈和TCB立即释放。
- 如果一个任务删除自己,则任务的堆栈和TCB和以前一样,通过空闲任务删除。
- 所以空闲任务开始就会检查是否有任务删除了自己,如果有的话,空闲任务负责删除这个任务的TCB和堆栈空间。
-
处理空闲优先级任务
- 当使用抢占式内核,相同优先级的任务使用时间片方式获得CPU权限。
- 如果有任务与空闲任务共享一个优先级,并且宏 configIDLE_SHOULD_YIELD 设置为 1,那么空闲任务不必等到时间片耗尽再进行任务切换。
- 所以空闲任务检查空闲优先级下的就绪列表中是否有多个任务,有的话则执行任务切换,让用户任务获得 CPU 权限。
- 宏 configIDLE_SHOULD_YIELD 控制任务在空闲优先级中的行为。仅在满足下列条件后,才会起作用:
- 使用抢占式内核调度
- 用户任务使用空闲优先级。
- 通过时间片共享同一个优先级的多个任务,如果共享的优先级大于空闲优先级,并没有更高优先级任务,这些任务应该获得相同的处理器时间。
- 但如果共享空闲优先级时,情况会稍微有些不同。当configIDLE_SHOULD_YIELD为1时,其它共享空闲优先级的用户任务就绪时,空闲任务立刻让出CPU,用户任务运行,这样确保了能最快响应用户任务。处于这种模式下也会有不良效果(取决于你的程序需要),描述如下:
- 图中描述了四个处于空闲优先级的任务,任务A、B和C是用户任务,任务I是空闲任务。上下文切换周期性的发生在T0、T1…T6时刻。当用户任务运行时,空闲任务立刻让出CPU,但是,空闲任务已经消耗了当前时间片中的一定时间。这样的结果就是空闲任务I和用户任务A共享一个时间片。用户任务B和用户任务C因此获得了比用户任务A更多的处理器时间。
- 可以通过下面方法避免:
- 如果合适的话,将处于空闲优先级的各单独的任务放置到空闲钩子函数中;
- 创建的用户任务优先级大于空闲优先级;
- 设置 IDLE_SHOULD_YIELD 为0;
- 设置configIDLE_SHOULD_YIELD为0将阻止空闲任务为用户任务让出CPU,直到空闲任务的时间片结束。这确保所有处在空闲优先级的任务分配到相同多的处理器时间,但是,这是以分配给空闲任务更高比例的处理器时间为代价的。
-
执行空闲任务钩子函数
-
空闲任务钩子是一个函数,这个函数由用户来实现,RTOS 规定了函数的名字和参数,这个函数在每个空闲任务周期都会被调用。要创建一个空闲钩子:
-
设置FreeRTOSConfig.h 文件中的 configUSE_IDLE_HOOK 为1;
-
定义一个函数,函数名和参数如下所示:
void vApplicationIdleHook(void );
-
-
这个钩子函数不可以调用会引起空闲任务阻塞的API函数(例如:vTaskDelay()、带有阻塞时间的队列和信号量函数),在钩子函数内部使用协程是被允许的。
-
使用空闲钩子函数设置CPU进入省电模式是很常见的。
-
-
低功耗 tickless 模式
-
通常情况下,FreeRTOS回调空闲任务钩子函数(需要设计者自己实现),在空闲任务钩子函数中设置微处理器进入低功耗模式来达到省电的目的。因为系统要响应系统节拍中断事件,因此使用这种方法会周期性的退出、再进入低功耗状态。如果系统节拍中断频率过快,则大部分电能和CPU时间会消耗在进入和退出低功耗状态上。
-
FreeRTOS的tickless空闲模式会在空闲周期时停止周期性系统节拍中断。停止周期性系统节拍中断可以使微控制器长时间处于低功耗模式。移植层需要配置外部唤醒中断,当唤醒事件到来时,将微控制器从低功耗模式唤醒。微控制器唤醒后,会重新使能系统节拍中断。由于微控制器在进入低功耗后,系统节拍计数器是停止的,但我们又需要知道这段时间能折算成多少次系统节拍中断周期,这就需要有一个不受低功耗影响的外部时钟源,即微处理器处于低功耗模式时它也在计时的,这样在重启系统节拍中断时就可以根据这个外部计时器计算出一个调整值并写入RTOS 系统节拍计数器变量中。
-
空闲任务的源代码如下所示,其中宏portTASK_FUNCTION翻译出来为:void prvIdleTask(void * pvParameters)。
-
static portTASK_FUNCTION( prvIdleTask,pvParameters ) { /*防止编译器警告 */ (void ) pvParameters; for(;; ) { /*检查是否有任务删除了自己,如果有的话,空闲任务负责删除这个任务的TCB和堆栈空间 */ prvCheckTasksWaitingTermination(); #if( configUSE_PREEMPTION == 0 ) { /*如果我们没有使用抢占式调度,我们会强制任务切换,看看是否有其它任务变得有效. 如果使用抢占式调度,是不需要这样的,因为任务变得有效后会抢占空闲任务.*/ taskYIELD(); } #endif/* configUSE_PREEMPTION */ #if( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) { /* 当使用抢占式内核,相同优先级的任务使用时间片方式获得CPU权限.如果有任务与空闲 任务共享一个优先级,那么空闲任务不必等到时间片耗尽再进行任务切换. 如果空闲优先级下的就绪列表中有多个任务,则执行用户任务*/ if(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) >( UBaseType_t ) 1 ) { taskYIELD(); } } #endif/* ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 )) */ #if( configUSE_IDLE_HOOK == 1 ) { externvoid vApplicationIdleHook( void ); /*调用用户定义函数.这样允许设计者在不增加任务开销的情况下实现后台功能 注意:这个函数中绝对不允许调用任务可能引起阻塞的函数.*/ vApplicationIdleHook(); } #endif/* configUSE_IDLE_HOOK */ #if( configUSE_TICKLESS_IDLE != 0 ) { TickType_txExpectedIdleTime; /*如果每次执行空闲任务都挂起调度器,起然后再解除调度器,这很难让人满意,因此这里 执行两次同样的比较(xExpectedIdleTime和configEXPECTED_IDLE_TIME_BEFORE_SLEEP), 第一次比较是测试一下是否达到预期的空闲时间,并不会挂起调度器.*/ xExpectedIdleTime= prvGetExpectedIdleTime(); if(xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) { vTaskSuspendAll(); { /*现在调度器被挂起,需要再次采样空闲时间,这次空闲时间可以使用了*/ configASSERT(xNextTaskUnblockTime >= xTickCount ); xExpectedIdleTime= prvGetExpectedIdleTime(); if(xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) { portSUPPRESS_TICKS_AND_SLEEP(xExpectedIdleTime ); } } (void ) xTaskResumeAll(); } } #endif/* configUSE_TICKLESS_IDLE */ } }
-
6 直达任务通知
7 队列
7.1 xQueueCreate 动态创建队列
-
创建一个新队列并返回 可引用此队列的句柄。
-
configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中被设置为 1,或保留未定义状态(此时,它默认 为 1) ,才能使用此 RTOS API 函数。
-
要在 CubeMX 中开启 :
-
-
每个队列都需要 RAM, 用于保存队列状态和队列中包含的数据项(队列存储区域)。 如果使用 xQueueCreate() 创建队列, 则会自动从 RAM 堆FreeRTOS中分配 。 如果使用 xQueueCreateStatic() 创建队列, 则 RAM 由应用程序编写者提供,这会产生更多的参数, 但这样能够在编译时静态分配 RAM 。 请参阅静态分配与 动态分配页面了解详情。
7.1.1 函数介绍
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );
参数:
- uxQueueLength,队列能够存储的最大单元数目,即队列深度
- uxItemSize,队列中数据单元的长度,以字节为单位
我们调用的是起了别名的队列创建函数(原名 xQueueGenericCreate)。
返回:
- 如果创建成功则返回一个队列句柄,用于访问创建的队列。如果创建不成功则返回 NULL,可能原因是创建队列需要的 RAM 无法分配成功
7.1.2 函数使用
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
};
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 创建一个能够包含10个指向消息结构的指针的队列。这些将通过指针排列访问,因为它们是相对较大的结构
myQueue01Handle = xQueueCreate(10, sizeof(struct AMessage *));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.2 xQueueCreateStatic 静态创建队列
-
创建一个新队列并返回 可以引用该队列的句柄。 configSUPPORT_STATIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,该 RTOS API 函数才可用。
-
要在 CubeMX 中开启 :
-
-
每个队列都需要 RAM, 用于保存队列状态和队列中包含的数据项(队列存储区域)。 如果使用 xQueueCreate() 创建队列, 则会自动从 RAM 堆FreeRTOS中分配 。 如果使用 xQueueCreateStatic() 创建队列, 则 RAM 由应用程序编写者提供,这会产生更多的参数, 但这样能够在编译时静态分配 RAM 。 请参阅静态分配与 动态分配页面了解详情。
7.2.1 函数介绍
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer );
参数:
- uxQueueLength,队列能够存储的最大单元数目
- uxItemSize,队列中数据单元的长度,以字节为单位
- pucQueueStorageBuffer,存储的数组 buff 大小应该达到 uxQueueLength * uxItemSize 个字节的大小。
- pxQueueBuffer,一个指向 StaticQueue_t 类型的变量, 该变量将用于保存队列的数据结构体。
返回:
- 如果队列创建成功,则返回所创建队列的句柄 则返回已创建队列的句柄。 如果 pxQueueBuffer 为 NULL,则返回 NULL。
7.2.2 函数使用
QueueHandle_t myQueue01Handle;
uint8_t myQueue01Buffer[ 16 * sizeof( uint32_t ) ];
StaticQueue_t myQueue01ControlBlock;
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
// 任务调度函数
void StartTask01(void const * argument)
{
for(;;)
{
printf("I'm Task01.\r\n");
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 创建一个能够包含10个指向消息结构的指针的队列。这些将通过指针排列访问,因为它们是相对较大的结构
myQueue01Handle = xQueueCreateStatic(16, sizeof( uint32_t ), myQueue01Buffer, &myQueue01ControlBlock);
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.3 vQueueDelete 删除队列
- 队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了。
7.3.1 函数介绍
void vQueueDelete( QueueHandle_t xQueue );
参数:
- xQueue,队列的句柄
返回:
- 无
7.3.2 函数使用
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
};
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle; // hal 库生产的是 osThreadId myTask01Handle; 两者没有区别 osThreadId 就是对 TaskHandle_t起了别名
// 任务调度函数
void StartTask01(void const * argument)
{
uint8_t i=0;
for(;;)
{
printf("I'm Task01.\r\n");
if(i++ == 10)
{
vQueueDelete(myQueue01Handle);
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
// 创建一个能够包含10个指向消息结构的指针的队列。这些将通过指针排列访问,因为它们是相对较大的结构
myQueue01Handle = xQueueCreate(10, sizeof(struct AMessage *));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)osPriorityIdle, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.4 xQueueSend 队列发送
-
用于向队列尾部发送一个队列消息。消息以拷贝的形式入队,而不是以引用的形式。该函数绝对不能在中断服务程序里面被调用,中断中必须使用带有中断保护功能的 xQueueSendFromISR()来代替。
-
这是一个调用 xQueueGenericSend() 函数的宏。
7.4.1 函数介绍
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
参数:
- xQueue,目标队列的句柄。这个句柄即是调用 xQueueCreate() 创建该队列时的返回值;
- pvItemToQueue,发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域;
- xTicksToWait,队列满时,等待队列空闲的最大超时时间。如果队列满并且 xTicksToWait 被设置成 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务挂起(没有超时)。
返回:
- 消息发送成功成功返回 pdTRUE,否则返回 errQUEUE_FULL
7.4.2 函数使用
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
for(;;)
{
printf("I'm Task01.\r\n");
// 等待接收队列
xReturn = xQueueReceive( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void StartTask02(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t t_queue; /* 定义一个发送消息的变量 */
uint8_t uartRxA = 0; // 定义一个串口接收变量
for(;;)
{
printf("I'm Task02.\r\n");
// 接受一个数据
if(HAL_UART_Receive(&huart1, &uartRxA, 1, 1) == HAL_OK)
{
// 接收到数据则把数据添加到队列的尾部
t_queue = uartRxA;
xReturn = xQueueSend( myQueue01Handle,
&t_queue,
5 );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
myQueue01Handle = xQueueCreate(2, sizeof( uint32_t ));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 不晓得为啥,只有两个队列优先级不一样的时候,才能接收到数据。。
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.5 xQueueSendFromISR 在中断中发送
- 该宏是 xQueueSend() 的中断保护版本,用于在中断服务程序中向队列尾部发送一个队列消息,等价于 xQueueSendToBackFromISR()。
- 将数据放置到队列尾部。在中断服务程序中使用此函数是安全的。
- 数据项通过复制而非引用入队,因此最好只将较小的项放入队列, 特别是从 ISR 调用时。在大多数情况下,最好存储一个指向正在排队的数据项的指针。
7.1.1 函数介绍
BaseType_t xQueueSendFromISR
(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
参数:
- xQueue,队列的句柄,数据项将发布到此队列;
- pvItemToQueue,发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域;
- pxHigherPriorityTaskWoken, 如果发送到队列导致某个任务解除阻塞,并且解除阻塞的任务的优先级高于当前运行的任务,则 xQueueSendTobackFromISR() 会将 *pxHigherPriorityTaskWoken 设置为 pdTRUE。 如果 xQueueSendTobackFromISR() 将此值设置为 pdTRUE,则应在退出中断之前请求上下文切换。从 FreeRTOSV7.3.0 开始,pxHigherPriorityTaskWoken 是一个可选参数,可设置为 NULL。
返回:
- 若成功发送至队列,则返回 pdPASS,否则返回 errQUEUE_FULL。
7.1.2 函数使用
// 队列句柄
QueueHandle_t myQueue01Handle;
// 任务句柄
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 中断函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
{
char t_queue;
BaseType_t xHigherPriorityTaskWoken;
BaseType_t xReturn = pdTRUE;
if(huart->Instance == USART1)
{
/* 在ISR开始时,我们还没有唤醒任务。 */
xHigherPriorityTaskWoken = pdFALSE;
t_queue = uartRxA;
/* 复制数据到队列尾部 */
xQueueSendFromISR( myQueue01Handle, &t_queue, &xHigherPriorityTaskWoken );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
// 再次开启串口接收中断
HAL_UART_Receive_IT(&huart1, &uartRxA, 1);
// 判断是否有更高优先级任务解除阻塞
/* 执行上下文切换, ISR 返回的时候将运行另外一个任务 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
}
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
for(;;)
{
printf("I'm Task01.\r\n");
xReturn = xQueueReceive( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay( 10 ); // 自动进入阻塞状态,等待
}
}
// 任务调度函数
void StartTask02(void const * argument)
{
for(;;)
{
printf("I'm Task02.\r\n");
vTaskDelay( 10 );
}
}
void main(void)
{
// 串口接收中断
HAL_UART_Receive_IT(&huart1, &uartRxA, 1);
// 动态的创建一个任务啦,参数可以看前面的解释
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
printf("Create myTask02 OK!\r\n");
}
/* 启动任务,开启调度 */
vTaskStartScheduler();
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.8 xQueueSendToFront 队列发送到队首
- 这是一个调用 xQueueGenericSend() 函数的宏。
- 从队列头部入队一个数据项。 数据项通过复制 而非引用入队。 不得从中断服务程序 调用此函数。 请参阅 xQueueSendToFrontFromISR() 了解 可在 ISR 中使用的替代方法。
7.8.1 函数介绍
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
参数:
- xQueue,队列的句柄,数据项将发布到此队列;
- pvItemToQueue,发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域;
- xTicksToWait,队列满时,等待队列空闲的最大超时时间。如果队列满并且 xTicksToWait 被设置成 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务挂起(没有超时)。
返回:
- 如果成功发布项目,返回 pdTRUE,否则返回 errQUEUE_FULL。
7.8.2 函数使用
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
for(;;)
{
printf("I'm Task01.\r\n");
// 等待接收队列
xReturn = xQueueReceive( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void StartTask02(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t t_queue; /* 定义一个发送消息的变量 */
uint8_t uartRxA = 0; // 定义一个串口接收变量
for(;;)
{
printf("I'm Task02.\r\n");
// 接受一个数据
if(HAL_UART_Receive(&huart1, &uartRxA, 1, 1) == HAL_OK)
{
// 接收到数据则把数据添加到队列的尾部
t_queue = uartRxA;
xReturn = xQueueSendToFront( myQueue01Handle,
&t_queue,
5 );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
myQueue01Handle = xQueueCreate(2, sizeof( uint32_t ));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.10 xQueueReceive 队列接收
- 用于从一个队列中接收消息并把消息从队列中删除。接收的消息是以拷贝的形式进行的,所以我们必须提供一个足够大空间的缓冲区。具体能够拷贝多少数据到缓冲区,这个在队列创建的时候已经设定。该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的 xQueueReceiveFromISR () 来代替。
- 这是一个调用 xQueueGenericReceive() 函数的宏。
7.10.1 函数介绍
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait );
参数:
- xQueue,被读队列的句柄。这个句柄即是调用 xQueueCreate() 创建该队列时的返回值
- pvBuffer,接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来的数据。数据单元的长度在创建队列时就已经被设定,所以该指针指向的内存区域大小应当足够保存一个数据单元。
- xTicksToWait,队列空时,阻塞超时的最大时间。如果该参数设置为 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)
返回:
- 队列项接收成功返回 pdTRUE,否则返回 pdFALSE
7.10.2 函数使用
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
for(;;)
{
printf("I'm Task01.\r\n");
// 等待接收队列
xReturn = xQueueReceive( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void StartTask02(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t t_queue; /* 定义一个发送消息的变量 */
uint8_t uartRxA = 0; // 定义一个串口接收变量
for(;;)
{
printf("I'm Task02.\r\n");
// 接受一个数据
if(HAL_UART_Receive(&huart1, &uartRxA, 1, 1) == HAL_OK)
{
// 接收到数据则把数据添加到队列的尾部
t_queue = uartRxA;
xReturn = xQueueSend( myQueue01Handle,
&t_queue,
5 );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
myQueue01Handle = xQueueCreate(2, sizeof( uint32_t ));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 不晓得为啥,只有两个队列优先级不一样的时候,才能接收到数据。。
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.12 uxQueueMessagesWaiting 获取队列消息数量
- 返回队列中存储的消息数。
- 中断中不可用,中断中调用
UBaseType_t uxQueueMessagesWaitingFromISR( QueueHandle_t xQueue );
即可。
7.12.1 函数介绍
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
参数:
- xQueue,被获取数量的队列的句柄。这个句柄即是调用 xQueueCreate() 创建该队列时的返回值
返回:
- 队列中可用的消息数。
7.12.2 函数使用
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
uint32_t queue_len; // 定义一个消息数量的变量
for(;;)
{
printf("I'm Task01.\r\n");
// 查看队列中数据的数量
queue_len = uxQueueMessagesWaiting(myQueue01Handle);
printf("myQueue01Handle len:%d\r\n", queue_len);
// 等待接收队列
xReturn = xQueueReceive( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay(100); // 自动进入阻塞状态,等待
}
}
void StartTask02(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t t_queue; /* 定义一个发送消息的变量 */
uint8_t uartRxA = 0; // 定义一个串口接收变量
for(;;)
{
printf("I'm Task02.\r\n");
// 接受一个数据
if(HAL_UART_Receive(&huart1, &uartRxA, 1, 1) == HAL_OK)
{
// 接收到数据则把数据添加到队列的尾部
t_queue = uartRxA;
xReturn = xQueueSend( myQueue01Handle,
&t_queue,
5 );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
myQueue01Handle = xQueueCreate(2, sizeof( uint32_t ));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 不晓得为啥,只有两个队列优先级不一样的时候,才能接收到数据。。
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.14 uxQueueSpacesAvailable 获取队列中剩下空间的大小
- 返回队列中的可用空间数。
7.14.1 函数介绍
UBaseType_t uxQueueSpacesAvailable( QueueHandle_t xQueue );
参数:
- xQueue,正在查询的队列的句柄。
返回:
- 队列中可用的可用空间数。
7.14.2 函数使用
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
uint32_t queue_len; // 定义一个消息数量的变量
for(;;)
{
printf("I'm Task01.\r\n");
// 查看队列中数据的数量
queue_len = uxQueueMessagesWaiting(myQueue01Handle);
printf("myQueue01Handle len:%d\r\n", queue_len);
// 查看队列中剩下空间的大小
queue_len = uxQueueSpacesAvailable(myQueue01Handle);
printf("myQueue01Handle len:%d\r\n", queue_len);
// 等待接收队列
xReturn = xQueueReceive( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay(100); // 自动进入阻塞状态,等待
}
}
void StartTask02(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t t_queue; /* 定义一个发送消息的变量 */
uint8_t uartRxA = 0; // 定义一个串口接收变量
for(;;)
{
printf("I'm Task02.\r\n");
// 接受一个数据
if(HAL_UART_Receive(&huart1, &uartRxA, 1, 1) == HAL_OK)
{
// 接收到数据则把数据添加到队列的尾部
t_queue = uartRxA;
xReturn = xQueueSend( myQueue01Handle,
&t_queue,
5 );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
myQueue01Handle = xQueueCreate(2, sizeof( uint32_t ));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 不晓得为啥,只有两个队列优先级不一样的时候,才能接收到数据。。
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.15 xQueueReset 复位队列
- 将队列重置为其原始的空状态。
7.15.1 函数介绍
BaseType_t xQueueReset( QueueHandle_t xQueue );
参数:
- xQueue,正在重置的队列的句柄
返回:
- 因为 FreeRTOSV7.2.0 xQueueReset() 总是返回 pdPASS。
7.15.2 函数使用
7.18 xQueuePeek 复制队列中的元素
- xQueuePeek() 也是从从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。
- xQueuePeek() 从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储序顺。
- 该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的 xQueuePeekFromISR () 来代替。
7.18.1 函数介绍
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait);
参数:
- xQueue,被读队列的句柄。这个句柄即是调用 xQueueCreate() 创建该队列时的返回值
- pvBuffer,接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来的数据。数据单元的长度在创建队列时就已经被设定,所以该指针指向的内存区域大小应当足够保存一个数据单元。
- xTicksToWait,队列空时,阻塞超时的最大时间。如果该参数设置为 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)
返回:
- 如果从队列中成功接收(复制)数据,则返回 pdTRUE,否则返回 pdFALSE。
7.18.2 函数使用
QueueHandle_t myQueue01Handle;
TaskHandle_t myTask01Handle;
TaskHandle_t myTask02Handle;
// 任务调度函数
void StartTask01(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t r_queue; // 定义一个接收消息的变量
uint32_t queue_len; // 定义一个消息数量的变量
for(;;)
{
// 查看队列中数据的数量
queue_len = uxQueueMessagesWaiting(myQueue01Handle);
printf("myQueue01Handle len:%d\r\n", queue_len);
// 查看队列中剩下空间的大小
queue_len = uxQueueSpacesAvailable(myQueue01Handle);
printf("myQueue01Handle len:%d\r\n", queue_len);
// 等待接收队列
xReturn = xQueuePeek( myQueue01Handle, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if (pdPASS == xReturn)
{
printf("本次接收到的数据是%d\n\n", r_queue);
}
else
{
printf("数据接收出错,错误代码: 0x%lx\n", xReturn);
}
vTaskDelay(100); // 自动进入阻塞状态,等待
}
}
void StartTask02(void const * argument)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
uint32_t t_queue; /* 定义一个发送消息的变量 */
uint8_t uartRxA = 0; // 定义一个串口接收变量
for(;;)
{
// 接受一个数据
if(HAL_UART_Receive(&huart1, &uartRxA, 1, 1) == HAL_OK)
{
// 接收到数据则把数据添加到队列的尾部
t_queue = uartRxA;
xReturn = xQueueSend( myQueue01Handle,
&t_queue,
5 );
if(pdPASS == xReturn)
{
printf("消息: %d 发送成功!\r\n", t_queue);
}
}
vTaskDelay(10); // 自动进入阻塞状态,等待
}
}
void main(void)
{
myQueue01Handle = xQueueCreate(2, sizeof( uint32_t ));
if(myQueue01Handle != NULL)
{
// 创建成功打印一下啦
printf("Create myQueue01 OK!\r\n");
}
// 不晓得为啥,只有两个队列优先级不一样的时候,才能接收到数据。。
if(xTaskCreate((TaskFunction_t)StartTask01, "myTask01", 128, NULL, (UBaseType_t)1, myTask01Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask01 OK!\r\n");
}
if(xTaskCreate((TaskFunction_t)StartTask02, "myTask02", 128, NULL, (UBaseType_t)2, myTask02Handle) == pdPASS)
{
// 创建成功打印一下啦
printf("Create myTask02 OK!\r\n");
}
vTaskStartScheduler(); /* 启动任务,开启调度 */
/* 永远不应该到达这里,因为现在控制权已经被调度器接管了 */
while(1);
}
7.1
7.1.1 函数介绍
参数:
返回:
7.1.2 函数使用
7.25 对于队列的扩展知识
8 队列集
9 流缓冲区
10 信号量/互斥锁
10.1
10.1.1 函数介绍
参数:
返回:
10.1.2 函数使用