说在前面 此篇所有有关函数均为CubeMX 提供的os函数(特殊地方会专门说明),freertos相关用户调用函数也分类似标准库和hal库的区别!但大体用法也与 FreeRTOS的任务相关 | 小董的BLOG (gitee.io) 中介绍的相似,可以参考
2022.8.26: 阅读了os函数内容并验证后,发现os函数与vtask函数兼容,(其实os函数也是调用的vtask函数)所以理论来说两边的函数可以混用,但是为了可读性等方面尽量还是不要混用
2022.8.28:在自己用了很多功能后,越发觉得库函数比os函数好用,大家自己取舍吧,反正二者是可以混用的,先用cubemx配置,再用库函数编写具体内容也是完全🆗的。
1、配置界面
配置项
功能
Tasks and Queues
任务与队列,用于配置任务体以及消息队列
Timers and Semaphores
软件定时器与信号量,用于配置内核对象 (软件定时器和信号量)
Mutexes
互斥量,用于配置内核对象(互斥量)
Events
事件,配置内核对象(事件)
FreeRTOS Heap Usage
查看用户任务和系统任务的堆占用
Config Parameters
系统的参数配置
Include Parameters
系统的功能裁剪
Advanced Settings
CubeMX 生成代码预配置项
User Constants
用户常量定义
版本选择
CMSIS是一种接口标准,目的是屏蔽软硬件差异以提高软件的兼容性。RTOS v1使得软件能够在不同的实时操作系统下运行(屏蔽不同RTOS提供的API的差别),而RTOS v2则是拓展了RTOS v1,兼容更多的CPU架构和实时操作系统。因此我们在使用时可以根据实际情况选择,如果学习过程中使用STM32F1、F4等单片机时没必要选择RTOS v2,更高的兼容性背后时更加冗余的代码,理解起来比较困难。
2、各配置详细设置 一、Config Parameters——系统的参数配置 1.Kernel Setting——FreeRTOS 调度内核设置 由于一些重要配置已在FreeRTOS_基本框架 | 小董的BLOG (gitee.io) 中讲到,因此本文只会记录一些容易搞混的地方
USE_MUTEXES、USE_RECURSIVE_MUTEXES、USE_COUNTING_SEMAPHORES
为 1 则开启系统构建过程中的互斥量、递归互斥量和信号量,默认开启就可。
QUEUE_REGISTRY_SIZE
队列注册表的大小,可以用于管理队列名称和队列实体,方便运行中进行查看与管理,默认为8
ENABLE_BACKWARD_COMPATIBILITY
一个兼容性使能,使能后, FreeRTOS 8.0.0 之后的版本可以通过宏定义使用 8.0.0 版本之前的函数接口,默认使能
USE_TASK_NOTIFICATIONS
任务通知使能,每个RTOS任务都有一个32位的通知值,RTOS任务通知是一个直接发送给任务的事件,它可以解除接收任务的阻塞,并可选地更新接收任务的通知值,为1开启,为0关闭,关闭可以为每个任务节省8个字节的内存空间
RECORD_STACK_HIGH_ADDRESS
记录任务的堆栈入口地址到TCB,为1使能,为0关闭
2.Memory management setting ——内存管理设置视情况调整即可,内存分配方式默认heap_4,方便完善。
3.Hook function relateed difinitions——钩子函数配置 钩子函数是一种回调函数,用于在任务执行一次之后或者某些事件发生后执行的函数 ,该配置项里面有五个选项,控制5种不同功能的钩子函数开启,当然用户也可以在代码中自己定义
USE_IDLE_HOOK
每当空闲任务执行一次,钩子函数都会被执行一次
4.Run time and task stats… ——任务运行追踪配置
GENERATE_RUN_TIME_STATS
开启时间统计功能,在调用 vTaskGetRunTimeStats() 函数时,将任务运行时间信息保存到可读列表中
USE_TRACE_FACILITY
使能后会包含额外的结构成员和函数以帮助执行可视化和跟踪,默认开启,方便 MDK 软件工具调试使用
USE_STATS_FORMATTING_FUNCTIONS
使能后会生成 vTaskList() 和 vTaskGetRunTimeStats() 函数用于获取任务运行状态
5.Co-routine ralated definitions——协程配置 默认不开启,协程现在几乎很少使用且FreeRTOS不再更新和维护协程。
6.Software timer definitions——软件定时器配置 默认即可。
7.Interrupt nesting behavior configuration——优先级配置 具体参数在FreeRTOS_基本框架 | 小董的BLOG (gitee.io) 已有详细描述,不再赘述。
二、Include Parameters ——系统的功能裁剪Include Parameters 下的选项应用于内核裁剪,裁剪不必要的功能,精简系统功能,减少资源占用
选项
功能
vTaskPrioritySet
改变某个任务的任务优先级
uxTaskPriorityGet
查询某个任务的优先级
vTaskDelete
删除任务
vTaskCleanUpResources
回收任务删除后的资源如RAM等等
vTaskSuspend
挂起任务
vTaskDelayUntil
阻塞延时一段绝对时间
vTaskDelay
阻塞延时一段相对时间
xTaskGetSchedulerState
获取任务调度器的状态,开启或未开启
xTaskResumeFromISR
在中断服务函数中恢复一个任务的运行
xQueueGetMutexHolder
获取信号量的队列拥有者,返回拥有此信号量的队列
xSemaphoreGetMutexHolder
查询拥有互斥锁的任务,返回任务控制块
pcTaskGetTaskName
获取任务名称
uxTaskGetStackHighWaterMark
获取任务的堆栈的历史剩余最小值
xTaskGetCurrentTaskHandle
此函数用于获取当前任务的任务句柄
eTaskGetState
此函数用于查询某个任务的运行壮态
xEventGroupSetBitFromISR
在中断服务函数中将指定的事件位清零
xTimerPendFunctionCall
定时器守护任务的回调函数(定时器守护任务使用到一个命令队列,只要向队列发送信号就可以执行相应代码,可以实现“中断推迟处理”功能)
xTaskAbortDelay
中止延时函数,该函数能立即解除任务的阻塞状态,将任务插入就绪列表中
xTaskGetHandle
此函数根据任务名字获取的任务句柄
三、Tasks and Queues ——创建任务与队列1.任务创建
具体内容已在FreeRTOS的任务相关 | 小董的BLOG (gitee.io) 中讲到,需要注意的是:
Task Name为任务名称 ,Entry Function才是任务实体,即运行的任务函数入口 (函数名)
任务的优先级细分有各等级优先级,详见文章最后对于优先级问题的理解
Code Generation为代码生成模式,细分为
As weak: 产生一个用 __weak 修饰的弱定义任务函数,用户可自己再进行定义
As external: 产生一个外部引用的任务函数,用户需要自己定义该函数
Default: 产生一个默认格式的任务函数,用户需要在该函数内实现自己的功能
一般使用默认格式即可
Allocation为定义内存分配方式,分为静态与动态,与c语言中static用法类似
CubeMX 提供的一些用户调用函数
函数
功能
osThreadNew
创建新任务
osThreadGetName
获取任务名称
osThreadGetId
获取当前任务的控制块(TCB)
osThreadGetState
获取当前任务的运行状态
osThreadGetStackSize
获取任务的堆栈大小
osThreadGetStackSpace
获取任务剩余的堆栈大小
osThreadSetPriority
设定任务优先级
osThreadGetPriority
获取任务优先级
osThreadYield
切换控制权给下一个任务
osThreadSuspend
挂起任务
osThreadResume
恢复任务(挂起多少次恢复多少次)
osThreadDetach
分离任务,方便任务结束进行回收
osThreadJoin
等待指定的任务停止
osThreadExit
停止当前任务
osThreadTerminate
停止指定任务
osThreadGetCount
获取激活的任务数量
osThreadEnumerate
列举激活的任务
2.队列创建 FreeRTOS中的队列是一种用于实现【任务与任务】,【任务与中断】以及【中断与任务】之间的通信机制。此外,任务从队列读数据或者写入数据到队列时,都可能被阻塞。这个特性使得任务可以被设计成基于事件驱动的运行模式,大大提高了CPU的执行效率。队列是实现FreeRTOS中其他特性的基础组件,像软件定时器,信号量,互斥锁都是基于队列而实现的。
在操作系统里面,直接使用全局变量传输数据十分危险,看似正常运行,但不知道啥时候就会因为寄存器或者内存等等原因引起崩溃,所以引入消息,队列的概念。
队列的基本特性 队列是一种FIFO操作的数据结构,入队操作就是把一个新的元素放进队尾(tail),出队操作就是从队头(front)取出一个元素。FreeRTOS中也支持把一个元素放到队头的操作,这个操作会覆盖 之前队头的元素。
入队策略 队列在设计的时候,主要有两种元素入队存储策略:Queue by copy 和 Queue by reference。FreeRTOS的队列使用的是Queue by copy存储策略,考虑到这种策略实现起来更加简单,灵活,且安全。
Queue by copy:数据入队的时候,队列中存储的是此数据的一份拷贝
Queue by reference:数据队列的时候,队列中存储的是数据的指针,而非数据本身
入队堵塞 一个任务在尝试写入数据到队列时,可以指定一个阻塞时间,即任务在等待队列有空余空间(非满)可写前最长等待阻塞时间 。当队列中有空间可写入时,任务会自动从阻塞态转换为就绪态。如果在等待时间内队列一直是满的,则等待时间到期后,任务也会自动从阻塞态转换为就绪态,但是它会返回一个写入失败的结果。
队列可能有多个writerTask,所以在等待队列有空闲空间时,可能会有多个任务阻塞。当队列有空闲空间可写时,调度器会从所有阻塞的任务中选取优先级最高的那个任务让其进入到就绪态 。如果最高优先级的不止一个,则让等待最久的那个进入到就绪态。
出队堵塞 一个任务在尝试从队列中读取数据时,可以指定一个阻塞时间,即任务在等待队列有元素可读前最长等待阻塞时间。当队列中有元素可读时,任务会自动从阻塞态转换为就绪态。如果在等待时间内队列一直没有数据可读,则等待时间到期后,任务也会自动从阻塞态转换为就绪态,但是它会返回一个读取失败的结果。
队列可能有多个reader Task,所以在等待队列有数据可读时,可能会有多个任务阻塞。当队列有数据可读时,调度器会从所有阻塞的任务中选取优先级最高的那个任务让其进入到就绪态。如果最高优先级的不止一个,则让等待最久的那个进入到就绪态。
相关API函数(非os函数,原博客中提供的os函数不适用于v1版本)(已测试,可用)
函数
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数
uxQueueLength :指定队列的长度,即最多可以存放的元素个数uxItemSize :队列中存储的元素的大小(占用的字节数)
返回值
返回NULL代表创建失败,没有足够的堆空间来创建当前队列;创建成功则返回队列的句柄
函数
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
参数
xQueue :目标队列的句柄pvItemToQueue :入队元素的指针。队列将存储此指针指向的数据的备份xTicksToWait :指定等待队列有空间可以容纳新元素入队的最长等待(阻塞)时间
返回值
当元素成功入队时返回pdPASS,因为队列满而无法入队时返回errQUEUE_FULL(超时后)
函数
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );
参数
xQueue :目标队列的句柄pvBuffer :用于存放读取到的队列元素的缓冲区,队列将把出队的元素拷贝到此缓冲区中xTicksToWait :指定等待队列有数据可读的最长等待(阻塞)时间
返回值
当成功从队列读取到元素时返回pdPASS,因为队列空而无法出队时返回errQUEUE_EMPTY
函数
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
参数
xQueue :队列的句柄
返回值
队列中的元素个数,返回0代表队列为空
老规矩,来个按键例程(可以正常运行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void task1 (void const * argument) { BaseType_t result; uint8_t dat[]="666\r\n" ; for (;;) { if (HAL_GPIO_ReadPin(key_1_GPIO_Port,key_1_Pin == GPIO_PIN_SET)) { result= xQueueSendToBack(myQueue01Handle,dat,portMAX_DELAY); if (result==pdPASS) { HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET); } } osDelay(5 ); } } void task2 (void const * argument) { BaseType_t result; uint8_t datbuf[10 ]; for (;;) { result=xQueueReceive(myQueue01Handle,datbuf,portMAX_DELAY); if (result==pdPASS) { HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_RESET); } osDelay(10 ); } }
懒得去找os函数了,库函数挺好用的是吧😋
四、事件(组) 写在前面2022.8.27 目前网上对于os函数的使用教程很少,而且大部分都有问题,本小节讲到的os函数使用方法感觉BUG是真的多,搞了整整两天也无法完全实现想要的功能,而且总是一些莫名其妙的问题,感觉是与任务调度有关,但是始终没找到真正原因,人都麻了QAQ。所以本小节(事件)仅提供思路,无法保证能正常运行。(新手建议直接跳过,高手可以找找原因)
在尝试两天发现效果极差后,果断换任务通知的方法,使用起来可以说很丝滑了,第一次用就可以正常实现功能(从未如此美妙的开局!!)也基本能代替事件通知的功能,建议直接使用任务通知(见下文)
没有试过库函数,因为发现任务通知够用了,感兴趣的同学可以试一下
* 分割线**
函数
功能
osEventFlagsNew
创建事件标志组
*osEventFlagsGetName
获取事件标志组名称
osEventFlagsSet
设置事件标志组
osEventFlagsClear
清除事件标志组
osEventFlagsGet
获取当前事件组标志信息
osEventFlagsWait
等待事件标志组触发
osEventFlagsDelete
删除事件标志组
具体配置 对于网络上的解释感觉有点晦涩难懂,自己总结了容易理解的方法,仅供参考
事件的作用主要是实现任务间通信的机制,主要用于实现多任务间的同步,但是事件类型通信只能是事件类型的通信 ,没有数据传输
使用流程:
在cubemx中配置相关事件句柄等
在程序中定义触发事件位
1 2 #define event1 1<<1 #define event2 1<<2
事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量(事件组 )中(例如下文中的result),当 configUSE_16_BIT_TICKS 为 1 的时候事件标志组可以存储 8 个事件位,当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24个事件位,每个事件位其实就是一个0或者1数字
定义事件组(在触发任务和使用到等待事件函数的任务中均需定义,定义成局部变量感觉方便点)
以下是对osStatus_t的定义,方便查阅
1 2 3 4 5 6 7 8 9 10 typedef enum { osOK = 0 , osError = -1 , osErrorTimeout = -2 , osErrorResource = -3 , osErrorParameter = -4 , osErrorNoMemory = -5 , osErrorISR = -6 , osStatusReserved = 0x7FFFFFFF } osStatus_t;
设置事件标志(触发条件)
以按键触发为例:(可以正常运行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void event (void *argument) { osStatus_t result; for (;;) { if (HAL_GPIO_ReadPin(key_1_GPIO_Port,key_1_Pin)!=0 ) { result = osEventFlagsSet(key_pHandle,event1); if (result == osOK) { printf ("事件1触发成功" ); } else { printf ("事件1触发失败" ); } } if (HAL_GPIO_ReadPin(key_2_GPIO_Port,key_2_Pin)!=0 ) { result = osEventFlagsSet(key_pHandle,event2); if (result == osOK) { printf ("事件2触发成功" ); } else { printf ("事件2触发失败" ); } } } }
等待函数参数介绍:
uint32_t osEventFlagsWait (osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout);
参数
功能
ef_id
事件标志组控制句柄
flags
等待的事件位
options
等待事件位的操作 osFlagsWaitAny :等待的事件位有任意一个等到就恢复任务 osFlagsWaitAll:等待的事件位全部等到才恢复任务 osFlagsNoClear:等待成功后不清楚所等待的标志位(默认清除)
timeout
等待事件组的等待时间(等待期间任务挂起在内核对象的挂起队列)
使用例子:同时等待事件1和事件2,且等待到不清除
(该任务由于未知原因不能正常运行,仅提供思路)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void wait_task (void *argument) { osStatus_t result; for (;;) { result = osEventFlagsWait(myEvent01Handle, event1|event2, osFlagsWaitAll, 10 ); if (result == osOK) { } else { } } }
五、任务通知 写在前面 2022.8.27 本文最开始参考的是 CubeMX使用FreeRTOS编程指南_Top嵌入式的博客-CSDN博客_cubemx freertos 但是在实际使用的过程中可以说有很多漏洞甚至错误,本小节在参考博客中所提供的flag函数好像根本不是任务通知的函数(?)并且其所说的使用方法中连函数的入口参数都写错了,(不晓得他自己跑过自己写的程序没有😅)所以关于flag相关函数在这里仅提供函数内容,用法就不探索了,后面会讲到我实际跑过并可以使用的任务通知函数。
flag相关函数介绍 FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue 就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态,CubeMX内没有提供相关的配置项,但在其生成的 FreeRTOS 接口里面有相关函数进行配置。
函数
功能
osThreadFlagsSet
设置任务的通知标志
osThreadFlagsClear
清除任务通知
osThreadFlagsGet
获取任务标志
osThreadFlagsWait
等待特定的任务标志
常用osThreadFlagsSet,osThreadFlagsWait,使用方式与事件触发相同 ,不再细说,仅介绍一下函数参数
uint32_t osThreadFlagsWait (uint32_t flags, uint32_t options, uint32_t timeout);
其中options的参数:
参数
功能
osFlagsWaitAny
等待32位通知值任意一位触发后恢复任务(默认)
osFlagsWaitAll
等待指定的任务通知值全部触发后再恢复任务
osFlagsNoClear
恢复任务后不清除任务标志(默认清除)
任务通知其实个任务事件标志组使用上没有多大的区别,但他们两个的实现原理不同,同时任务通知对资源的占用更少
根据 FreeRTOS 官方的统计,使用任务通知替代二值信号量的时候任务解除阻 塞的时间要快 45%,并且需要的 RAM 也更少
可以实际使用的任务通知 使用前记得在cubemx配置中开启USE_TASK_NOTIFICATIONS和在include parameters里开启eTaskGetState
FreeRTOS 提供以下几种方式发送通知给任务 :
发送通知给任务, 如果有通知未读,不覆盖通知值。
发送通知给任务,直接覆盖通知值。
发送通知给任务,设置通知值的一个或者多个位 ,可以当做事件组来使用。
发送通知给任务,递增通知值,可以当做计数信号量使用。
通过对以上任务通知方式的合理使用,可以在一定场合下替代 FreeRTOS 的信号量,队列、事件组等。
相关api函数:
函数
int32_t osSignalSet (osThreadId thread_id, int32_t signal)
参数
thread_id: 接收通知的任务IDsignal: 任务通知值(按位操作数字)
返回值
错误码
函数
osEvent osSignalWait (int32_t signals, uint32_t millisec)
参数
signals: 接收完成后等待被清零的数据位(0x0001\
0x0002=0x003)millisec: 等待超时时间,单位为系统节拍周期
返回值
错误码
实例(按键触发任务通知,已经过验证) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define event1 0x0001 void task1 (void const * argument) { for (;;) { if (HAL_GPIO_ReadPin(key_1_GPIO_Port,key_1_Pin == GPIO_PIN_SET)) { osSignalSet(task2Handle,event1); } osDelay(10 ); } } void task2 (void const * argument) { osEvent event; for (;;) { event = osSignalWait(event1,osWaitForever); if (event.status == osEventSignal) { HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET); } } }
这里task2可以不用额外在循环中添加堵塞,因为wait函数本身自带堵塞,其堵塞的解除是在osSignalSet函数的作用之后的瞬间,也就是说在执行完osSignalSet(task2Handle,event1)后task2会直接抢占task1
而xTask函数则是在task1执行完后才会进入task2,这点请务必记住,二者通知原理不同,使用方法因此也不同
其他的内容以后再来探索吧(指还没学到)XD 遇见的一些问题 一、FreeRTOS的时钟源配置 当使用了FreeRtos的时候,建议HAL库使用除了Systick以外的时钟源。也就是说当不使用FreeRtos的时候,HAL使用的是systick作为时钟源,现在使用了rtos,不建议hal库和rtos一起使用systick作为时钟源
二、优先级问题
这里我自己也没太搞懂,姑且谈谈自己的理解:
按道理来说cortex内核对于优先级数值是越小其逻辑上的优先度就越高,其在cubemx上的大部分配置也同样符合这一原则,但是在对于任务优先级的分配上cubemx采用了上图这种low、normal等的分配方式,并且在程序中的定义数值则是优先级数值越大,逻辑优先度越高;这可能是cubemx为了方便理解做出的优化(?)
三、阻塞问题 在程序中的阻塞函数≠正常的延时函数 !!!
当程序执行到vTaskDelay或osdelay后会将当前任务添加到延时任务列表里,并强行切换任务。也就是说,在这段阻塞期内,只要有任务处于就绪态,就会进入该任务,若多个任务就绪,则按照优先级进行抢占。当堵塞态的任务延时到期过后会重新进入就绪态等待。 并且在重新进入就绪态后任务会从阻塞之后的位置开始。
任务函数内可以正常调用延时函数
挂起规律与阻塞规律基本一致
四、任务抢占问题 原先一直以为抢占式工作流程为:先运行优先级最高的任务,直至其被阻塞、挂起等;这期间运行优先级第二高的任务,直到其被阻塞、挂起等;若最高优先级此时已处于就绪态,则会在第二优先级任务进入阻塞等非运行态时进入运行态,但其实这是不对的
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void task1 (void *argument) { HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_SET); for (;;) { HAL_GPIO_TogglePin(led_b_GPIO_Port,led_b_Pin); HAL_Delay(5000 ); osDelay(1 ); } } void task2 (void *argument) { osStatus_t result; for (;;) { result=osEventFlagsSet(key1_pHandle,event1); if (result == osOK) { HAL_GPIO_WritePin(led_r_GPIO_Port,led_r_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(led_b_GPIO_Port,led_b_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_SET); } else { HAL_GPIO_WritePin(led_b_GPIO_Port,led_b_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(led_g_GPIO_Port,led_g_Pin,GPIO_PIN_RESET); } osDelay(5 ); }
按照原先的思路,程序开始,task2会优先运行,若事件设定正常,此时红灯会亮起;然后task2进入阻塞态5ms,task1进入运行态,此时按道理红灯会熄灭,蓝灯亮起5秒后,task1进入阻塞态,task2重新运行,此时红灯再亮,蓝灯灭。
但是现实是:红灯亮起后,蓝灯只会极快地闪烁一下后红灯继续亮起,并且蓝灯闪烁的频率大概为5s左右。
所以,真正的抢占流程应该是:程序开始,task2会优先运行,若事件设定正常,此时红灯会亮起;然后task2进入阻塞态5ms,task1进入运行态,当task1运行5ms后,task2的阻塞结束,进入就绪态,此时task1刚刚开始运行HAL_Delay(5000),由于task2优先级更高且已经就绪,task2直接抢占并运行(此时蓝灯就会熄灭,因此蓝灯一次只亮了5ms),而不是等到task1运行osDelay(10)才会抢占 ;当task2再一次进入堵塞时,task1从上次被抢占的位置继续运行5ms然后被task2抢占,这样一直循环,直到HAL_Delay(5000)结束,蓝灯才会再次闪烁一次。
总结:高优先级的任务一旦就绪后会立刻抢占 优先级低的任务,哪怕当前低优先级正在运行,而不是等到优先级低的任务进入非运行态时才抢占。