介绍
这篇文章是我学习FreeRTOS的笔记
学的是哔哩哔哩韦东山老师的课程
在学习FreeRTOS之前已经学习过江协的标准库和一丢丢的超子说物联网的HAL了。他们讲的都很不错
正在更新,
大家可以在我的Gitee仓库中下载笔记源文件、项目资料等
笔记源文件可以在Notion中导入
01.创建项目模板
一、项目基础配置
-
选用stm32F103C8T6芯片
-
设置Rcc 高速时钟为 陶瓷晶振
-
设置SYS Debug为 Serial Wire、基准时钟为TIM4
-
设置HCLK为72M
-
找到Middleware and Software Packs(中间件和软件包)、选择FreeRTOS、选择CMSIS V2、配置参数保持默认
-
设置工程名、配置IDE为MDK-ARM
-
在Code Generator中设置每个外设的初始代码的.c .h 分开(Generate peripheral initialization as a pair of'c/.h' files per periphera) 、其他默认
-
固件包版本设置为1.85 (为了防止 在编译时出现这个错误 #include CMSIS_device_header )
-
生成代码
-
选择下载器
-
设置复位后运行
-
关闭调试说明
二、添加基础程序
-
配置pc13 板载led测试程序
-
在CubeMX设置PC13为推挽输出。
-
添加led测试程序.c .h文件之后 在freertos.c中的
void StartDefaultTask(void *argument)
函数中添加测试程序
-
-
配置屏幕
-
在Connectivity中设置I2C1、使能为I2C、参数默认
-
添加lcd、oled的.c .h文件 添加ascii_font.c字库文件
-
02.创建第一个多任务程序
cmsis.os2.c是一个统一的接口。
因为有很多的操作系统。他们的函数又不同。
但有一个统一的接口,就会根据底层的操作系统的不同调用不同的函数来执行操作。
而作为用户的我们,只需要知道cmsis.os2.c中的函数就可以了。
并且我们写出的函数就能即运行在这个操作系统中,又可以运行在别的操作系统中
xTaskCreate
是 FreeRTOS 中用于动态创建任务的函数
各参数的含义如下:
-
pvTaskCode
:指向任务入口函数的指针。 -
pcName
:任务的描述性名称。 -
uxStackDepth
:任务堆栈的大小(单位是字,而不是字节)。 -
pvParameters
:传递给创建任务的参数。 -
uxPriority
:创建的任务将运行的优先级。数字越大,优先级越高。 -
pxCreatedTask
:用于返回已创建任务的句柄。
在指定的地方写好函数原型之后,把函数原型丢到xTaskCreate
中,填写好相应参数即可,比如
xTaskCreate(My_Task, "MyFirstTask", 128, NULL, osPriorityNormal, NULL);
这里两个NULL为空指针,暂时不做了解
03.硬件架构和汇编指令
一、硬件架构
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),
它所用的指令比较简单,有如下特点: ① 对内存只有读、写指令 ② 对于数据的运算是在CPU内部实现 ③ 使用RISC指令的CPU复杂度小一点,易于设计
CPU内部有寄存器 R0-R15。以及计算单元.
后三个为程序状态寄存器:
-
R13:别名SP(Stack Pointer),栈指针。用来保存栈的地址 R14:别名LR(Link Register),用来保存返回地址。(A函数到B函数,B函数完成后返回A) R15:别名PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转
二、一些简单的汇编指令
-
读内存:Load
LDR R0, [R1, #4] ;
读地址 ”R1+4 “ ,得到的四个字节数据存入R0 -
写内存:Stroe
STR R0, [R1, #4] ;
把R0的四个字节写入到”R1+4“中去
可以在LDR或STR后加H 、 B 来表示读写 半字(Half) 字节(Byte)
-
加
ADD R0, R1, R2 ;
R0 = R1 + R2ADD R0, R1, #1 ;
R0 = R1 + 1 -
减
SUB R0, R1, R2 ;
R0 = R1 - R2SUM R0, R1, #1 ;
R0 = R1 - 1 -
比较
CMP R0, R1 ;比较R0和R1,比较的结果保存在程序状态寄存器中 PSR
-
跳转
-
B :main ;
Branch, 直接跳转执行这条指令,会导致程序状态寄存器R15(PC)寄存器中被已写入一个数值。数值为main函数的地址。CPU会从这个地址开始执行程序
-
BL:main ; Branch and Link, 在函数之间的跳转时,先把返回值保存在R14(LR)寄存器里再跳转
-
04. 堆与栈
一、堆
堆是一块动态分配的内存空间。
-
特点:
-
分配和释放由程序员手动控制,相对灵活。可以在运行时根据实际需求从中分配出不同大小的内存块,例如可以使用编程语言提供的内存分配函数(如 C 语言中的
malloc
、C++ 中的new
等)来分配堆内存。当使用完这块内存后,需要显式地调用相应的释放函数(如 C 语言中的free
、C++ 中的delete
)把它放回去,否则会导致内存泄漏。 -
内存分配和释放的时间开销相对较大,因为堆的管理通常涉及复杂的算法来寻找合适大小的空闲内存块。
-
堆中的内存空间大小通常只受限于系统的可用物理内存和虚拟内存大小。
-
-
用途:
-
适用于需要动态分配较大内存块且生命周期不确定的情况。比如存储大量数据结构(如链表、树等)、动态创建对象等。
-
二、栈
栈是一块由系统自动管理的内存空间。
-
特点:
-
先进后出(FILO)的数据结构。CPU 的栈指针寄存器(SP)始终指向栈顶。当函数被调用时,函数的参数、局部变量和返回地址等信息被压入栈中;当函数返回时,这些信息被弹出栈。
-
内存的分配和释放由系统自动完成,速度快。在函数调用结束后,栈上的局部变量会自动被释放,无需程序员手动管理。
-
栈的大小通常是有限的,不同的操作系统和编译器可能会设置不同的栈大小限制。如果栈空间被耗尽,可能会导致栈溢出错误。
-
-
用途:
-
主要用于函数调用和局部变量的存储。在多任务系统中,栈还可以用于保存任务的现场,例如当一个任务被中断时,当前的寄存器值、程序计数器等信息会被压入栈中,以便在任务恢复执行时能够恢复到中断前的状态。
-
三、问题
-
在函数调用时,LR被覆盖了怎么办?
-
会 在C入口保存LR进栈。
-
-
局部变量在栈中是如何分配的?
-
使用volatile 修饰的局部变量会保存在栈中。默认使用CPU寄存器来进行保存,如果寄存器不够用了才用栈
-
-
为什么每个ROTS任务都有自己的栈
因为每个人物都有自己的调用关系和局部变量、所以在每个任务在切换出的时候要保存自己的现场
-
保存现场
-
在FreeRTOS运行时,内核的定时器中断函数会自动切换任务。
-
并为需要为每个任务 保存现场,为恢复现场奠定基础
-
保存现场当然要保存在RAM中的 这个任务分配的栈中。
-
保存现场是保存所有的寄存器
-
在任务A中的结构体里记录SP
-
-
恢复现场
-
找到A的结构体,得到A的栈。
-
把栈中的值恢复到CPU中(把所有的寄存器的值恢复到硬件)
-
-
R13:别名SP(Stack Pointer),栈指针。用来保存栈的地址 R14:别名LR(Link Register),用来保存返回地址。(A函数到B函数,B函数完成后返回A) R15:别名PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转
05.FreeRTOS源码描述
7.1 FreeRTOS目录结构
使用STM32CubeMX创建的FreeRTOS工程中,FreeRTOS相关的源码如下:
主要涉及2个目录:
-
Core
-
Inc目录下的FreeRTOSConfig.h是配置文件
-
Src目录下的freertos.c是STM32CubeMX创建的默认任务
-
-
Middlewares\Third_Party\FreeRTOS\Source
-
根目录下是核心文件,这些文件是通用的
-
portable目录下是移植时需要实现的文件
-
目录名为:[compiler]/[architecture]
-
比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS工具上的移植文件
-
-
7.2核心文件 FreeRTOS的最核心文件只有2个:
-
FreeRTOS/Source/tasks.c
-
FreeRTOS/Source/list.c
其他文件的作用也一起列表如下:
**#7.3 移植时涉及的文件**
移植FreeRTOS时涉及的文件放在 FreeRTOS/Source/portable/[compiler]/[architecture] 目录下,比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。 里面有2个文件:
-
port.c
-
portmacro.h
**#7.4 头文件相关**
**#7.4.1 头文件目录**
FreeRTOS需要3个头文件目录:
-
FreeRTOS本身的头文件:
Middlewares\Third_Party\FreeRTOS\Source\include
-
移植时用到的头文件:
Middlewares\Third_Party\FreeRTOS\Source\portablecompiler
-
含有配置文件FreeRTOSConfig.h的目录:Core\Inc
**#7.4.2 头文件**
列表如下:
**#7.5 内存管理**
文件在Middlewares\Third_Party\FreeRTOS\Source\portable\MemMang下,它也是放在“portable”目录下,表示你可以提供自己的函数。
源码中默认提供了5个文件,对应内存管理的5种方法。
后续章节会详细讲解。
**#7.6 入口函数**
在Core\Src\main.c的main函数里,初始化了FreeRTOS环境、创建了任务,然后启动调度器。源码如下:
/* Init scheduler */osKernelInitialize(); /* 初始化FreeRTOS运行环境 */MX_FREERTOS_Init(); /* 创建任务 */ /* Start scheduler */osKernelStart(); /* 启动调度器 */
**#7.7 数据类型和编程规范**
**#7.7.1 数据类型**
每个移植的版本都含有自己的portmacro.h头文件,里面定义了2个数据类型:
-
TickType_t:
-
FreeRTOS配置了一个周期性的时钟中断:Tick Interrupt
-
每发生一次中断,中断次数累加,这被称为tick count
-
tick count这个变量的类型就是TickType_t
-
TickType_t可以是16位的,也可以是32位的
-
FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t就是uint16_t
-
否则TickType_t就是uint32_t
-
对于32位架构,建议把TickType_t配置为uint32_t
-
-
BaseType_t:
-
这是该架构最高效的数据类型
-
32位架构中,它就是uint32_t
-
16位架构中,它就是uint16_t
-
8位架构中,它就是uint8_t
-
BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE
-
**#7.7.2 变量名**
变量名有前缀:
**#7.7.3 函数名**
函数名的前缀有2部分:返回值类型、在哪个文件定义。
**#7.7.4 宏的名**
宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。
通用的宏定义如下:
06.内存管理
8.1 为什么要自己实现内存管理
后续的章节涉及这些内核对象:task、queue、semaphores和event group等。为了让FreeRTOS更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化API函数的涉及,甚至可以减少内存的使用。
内存的动态管理是C程序的知识范畴,并不属于FreeRTOS的知识范畴,但是它跟FreeRTOS关系是如此紧密,所以我们先讲解它。
在C语言的库函数中,有mallc、free等函数,但是在FreeRTOS中,它们不适用:
-
不适合用在资源紧缺的嵌入式系统中
-
这些函数的实现过于复杂、占据的代码空间太大
-
并非线程安全的(thread- safe)
-
运行有不确定性:每次调用这些函数时花费的时间可能都不相同
-
内存碎片化
-
使用不同的编译器时,需要进行复杂的配置
-
有时候难以调试
注意:我们经常"堆栈"混合着说,其实它们不是同一个东西:
-
堆,heap,就是一块空闲的内存,需要提供管理函数
-
malloc:从堆里划出一块空间给程序使用
-
free:用完后,再把它标记为"空闲"的,可以再次使用
-
-
栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
-
可以从堆中分配一块空间用作栈
-
8.2 FreeRTOS的5中内存管理方法
FreeRTOS中内存管理的接口函数为:pvPortMalloc 、vPortFree,对应于C库的malloc、free。 文件在FreeRTOS/Source/portable/MemMang下,它也是放在portable目录下,表示你可以提供自己的函数。
源码中默认提供了5个文件,对应内存管理的5种方法。
参考文章:FreeRTOS说明书吐血整理【适合新手+入门】在新窗口打开
8.2.1 Heap_1
它只实现了pvPortMalloc,没有实现vPortFree。
如果你的程序不需要删除内核对象,那么可以使用heap_1:
-
实现最简单
-
没有碎片问题
-
一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1
它的实现原理很简单,首先定义一个大数组:
/* Allocate the memory for the heap. */ ##if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) /* The application writer has already defined the array used for the RTOS * heap - probably so it can be placed in a special segment or address. */extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; ##elsestatic uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; ##endif /* configAPPLICATION_ALLOCATED_HEAP */
然后,对于pvPortMalloc调用时,从这个数组中分配空间。
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。 使用heap_1时,内存分配过程如下图所示:
-
A:创建任务之前整个数组都是空闲的
-
B:创建第1个任务之后,蓝色区域被分配出去了
-
C:创建3个任务之后的数组使用情况
8.2.2 Heap_2
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
-
Heap_2使用最佳匹配算法(best fit)来分配内存
-
它支持vPortFree
最佳匹配算法:
-
假设heap有3块空闲内存:5字节、25字节、100字节
-
pvPortMalloc想申请20字节
-
找出最小的、能满足pvPortMalloc的内存:25字节
-
把它划分为20字节、5字节
-
返回这20字节的地址
-
剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用
-
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。
但是,如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时,需要分配TCB和栈,TCB总是一样的)。
虽然不再推荐使用heap_2,但是它的效率还是远高于malloc、free。
使用heap_2时,内存分配过程如下图所示:
-
A:创建了3个任务
-
B:删除了一个任务,空闲内存有3部分:顶层的、被删除任务的TCB空间、被删除任务的Stack空间
-
C:创建了一个新任务,因为TCB、栈大小跟前面被删除任务的TCB、栈大小一致,所以刚好分配到原来的内存
8.2.3 Heap_3
Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用。
C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
8.2.4 Heap_4
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。
Heap_4使用 首次适应算法(first fit)来分配内存 。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
首次适应算法:
-
假设堆中有3块空闲内存:5字节、200字节、100字节
-
pvPortMalloc想申请20字节
-
找出第1个能满足pvPortMalloc的内存:200字节
-
把它划分为20字节、180字节
-
返回这20字节的地址
-
剩下的180字节仍然是空闲状态,留给后续的pvPortMalloc使用
Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Heap_4的使用过程举例如下:
-
A:创建了3个任务
-
B:删除了一个任务,空闲内存有2部分:
-
顶层的
-
被删除任务的TCB空间、被删除任务的Stack空间合并起来的
-
C:分配了一个Queue,从第1个空闲块中分配空间
-
D:分配了一个User数据,从Queue之后的空闲块中分配
-
E:释放的Queue,User前后都有一块空闲内存
-
F:释放了User数据,User前后的内存、User本身占据的内存,合并为一个大的空闲内存
Heap_4执行的时间是不确定的,但是它的效率高于标准库的malloc、free。
8.2.5 Heap_5
Heap_5分配内存、释放内存的算法跟Heap_4是一样的。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。
既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
-
在使用pvPortMalloc之前,必须先指定内存块的信息
-
使用vPortDefineHeapRegions来指定这些信息
怎么指定一块内存?使用如下结构体:
typedef struct HeapRegion {uint8_t * pucStartAddress; // 起始地址size_t xSizeInBytes; // 大小 } HeapRegion_t;
怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。 比如:
HeapRegion_t xHeapRegions[] = {{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000{ NULL, 0 } // 表示数组结束};
vPortDefineHeapRegions函数原型如下:
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions ); ```c 把xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。 ## 8.3 Heap相关的函数 ### 8.3.1 pvPortMalloc/vPortFree 函数原型: ```c void * pvPortMalloc( size_t xWantedSize ); void vPortFree( void * pv );
8.3 Heap相关的函数
8.3.1 pvPortMalloc/vPortFree
函数原型:
void * pvPortMalloc( size_t xWantedSize ); void vPortFree( void * pv );
作用:分配内存、释放内存。
如果分配内存不成功,则返回值为NULL。
8.3.2 xPortGetFreeHeapSize
函数原型:
size_t xPortGetFreeHeapSize( void );
当前还有多少空闲内存,这函数可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。
注意:在heap_3中无法使用。
8.3.3 xPortGetMinimumEverFreeHeapSize
函数原型:
size_t xPortGetMinimumEverFreeHeapSize( void );
返回:程序运行过程中,空闲内存容量的最小值。
注意:只有heap_4、heap_5支持此函数。
8.3.4 malloc失败的钩子函数
在pvPortMalloc函数内部:
void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions {......#if ( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn; }
所以,如果想使用这个钩子函数:
-
在FreeRTOSConfig.h中,把configUSE_MALLOC_FAILED_HOOK定义为1
-
提供vApplicationMallocFailedHook函数
-
pvPortMalloc失败时,才会调用此函数
07.动态和静态创建任务
任务:
-
做什么事:函数
-
栈和 TCB
-
可以动态分配也可以事先静态分配
-
-
优先级
一个任务备切换出来之后, 如何才能再次找到它:
-
在一个链表中找到任务A、B、C
-
在链表中存放任务控制块(TCB )(task contol block)
在动态分配内存的时候 根据你输入的栈的大小自动分配栈和TCB
在静态分配内存的时候,需要事先准备好栈和TCB结构体
动态:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 const char * const pcName, // 任务的名字 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节 void * const pvParameters, // 调用任务函数时传入的参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
静态:
TaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个bufferStaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务 );
测试:
使用静态分配的时候,需要为任务提供buff 和TCB结构体
static StackType_t g_pucStackOfLightTask[128]; //提供Buff g 为全局的缩写 static StaticTask_t g_TCBofLightTask; //提供TCB任务结构体 static TaskHandle_t xLightTaskHandle; //光任务 句柄结构体 以FreeRTOS 的规范, x表示某些结构体 static StackType_t g_pucStackOfColorTask[128]; static StaticTask_t g_TCBofColorTask; static TaskHandle_t xColorTaskHandle;
使用时是这样
/* 创建任务:光 */xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);/* 创建任务:色 */xColorTaskHandle = xTaskCreateStatic(ColorLED_Test, "ColorTask", 128, NULL, osPriorityNormal, g_pucStackOfColorTask, &g_TCBofColorTask);
任务句柄还没学到。 AI说 主要有以下作用:一是用于识别和管理任务,可进行挂起、恢复、删除等操作;二是在任务间通信与同步中,可指定消息的接收者或发送者,以及用于同步操作;三是能查询任务状态和获取任务属性信息。
TCB的简要介绍:
一、存储任务状态信息
-
记录任务当前的运行状态,如就绪态、运行态、阻塞态等。
-
保存任务的优先级,决定任务在可运行状态下获取 CPU 时间片的优先级顺序。
二、管理任务资源
-
可能包含任务所使用的栈信息,包括栈的起始地址和大小。
-
可以存储与任务相关的同步对象指针,如信号量、互斥量等,以便任务进行同步和通信操作。
三、支持任务调度
-
操作系统在进行任务调度时,通过检查 TCB 中的信息来决定下一个要执行的任务。
-
TCB 使得操作系统可以快速地切换任务,保存和恢复任务的上下文。
总之,任务控制块是 FreeRTOS 管理任务的核心数据结构,它为任务的创建、运行、暂停、恢复和删除等操作提供了必要的信息和控制机制。
动态任务在创建时会自动分配内存和初始化任务块等等。但有可能因为内存不够等原因失败,所以返回值为创建任务的成功或失败。
静态任务在创建时需要用户提前准备buff和 TCB控制块 。 所以他一定能创建成功。他的参数中没有任务句柄。他的返回值是任务句柄
08.估算栈的大小
栈的作用是
-
返回地址 比如LR寄存器、其他寄存器
-
取决于函数的调用深度
-
-
局部变量
-
取决于代码
-
-
保存现场(在任务被切换时)
-
现场时个寄存器 16 * 4 = 64 字节
-
如何评估栈的大小
-
选取最复杂的调用关系
-
n级调用 * (被调用者寄存器R4 - R11共8个 + LR寄存器)也就是一次调用最多使用36 个字节
-
所以 调用深度越深 所需要的栈的空间就越大
-
-
选取创建局部变量最大的函数。
09.一个函数创建多个任务
定义结构体,通过在函数内定义 void* 类型的指针变量 来接收我们定义的结构体。
通过创建值不同的结构体,来作为创建任务创建时传入函数的参数
通过多次创建任务,但任务在调用时传入函数的 参数不同。 可以做到同一个函数,创建多个不同的任务。
注意使用struct 而不能使用Typedef struct 否则任务在传参时会报错~
10.删除任务
删除任务 使用 vTaskDelete 函数
它的参数为 任务在创建时 返回 或 传入的 任务句柄
使用这个函数可以直接删除函数,但是多次的删除和创建 ,每次创建都会去分配内存。频繁地动态分配内存很容易导致内存的碎片化,直到分配不到内存。
并且在删除任务之后。这个任务就终止了。没法做一些清除的工作。
所以,在删除任务时,我们并不会经常使用vTaskDelete函数。
一般来说是让任务自己来根据(比如遥控器的按键)自己停止。自己做一些清除的工作
11.优先级与阻塞
这里会通过提升任务的优先级来达到改善播放的效果
可以通过修改优先级 来达到优化音乐播放,
学习使用vTaskdelay的使用。使用自己写的会阻塞任务。如果阻塞的delay在高优先级的任务,会导致只会运行高优先级的任务。