文章目录
- 摘要
- 一、浅识FreeRTOS快速入门课程
-
- [1.2、FerrRTOS 与裸机区别](#1.2、FerrRTOS 与裸机区别)
-
- [1.2.7 任务状态](#1.2.7 任务状态)
- [1.2.8 空闲任务和钩子函数](#1.2.8 空闲任务和钩子函数)
- [1.2.9 任务调度算法](#1.2.9 任务调度算法)
摘要
持续更新中
一、浅识FreeRTOS快速入门课程
1.2、FerrRTOS 与裸机区别
1.2.7 任务状态
时间片
c
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xHandleTask3 = xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
在实际运行中,是以滴答定时器为该系统的时间基准,并且利用调度器进行相关执行。
在上述例子子,最先执行的是任务3,然后是任务1、任务2、任务3,切换的原因是按照调度器的思路执行,什么时候切换是按照时钟中断进行的。并且还需要判断是否切换任务,并且需要记住的是FreeRTOS每个任务运行的最基本时间是1ms。关于如何使用滴答定时器,在前面文章也有详细分析如何配置。
FreeRTOS的时间片长度通常是由系统时钟节拍决定的,也就是我们常说的Tick,在默认情况下,一个时间片就是1个Tick,也就是1ms。这个地方我觉得应该是规定,这是因为在FreeRTOS中这个时间好像是约定俗成的,就是在我们配置的时候要配置为1ms。(configTICK_RATE_HZ=1000
时,时间片为 1ms)
时间片调度机制(同优先级任务),
当多个任务优先级相同,并且启用了时间片调度,那么当一个任务运行满一个时间片的时候(1ms),即时该任务没有执行完,也会被强制切换,至于这是为什么?这是FreeRTOS的本身机制,就是要这样,就是要和裸机的执行逻辑要求区别才有意义,以一种新的架构去执行不同任务。
例如:两个优先级相同的任务 Task1 和 Task2 会轮流执行各 1ms,无论任务是否完成逻辑。
触发任务切换的其他条件:
即使时间片未耗尽,任务也可能被切换:
-
主动放弃 CPU :
任务调用
taskYIELD()
、vTaskDelay()
等函数时,会立即触发切换(即使时间片未用完)。 -
高优先级任务就绪 :
若有更高优先级任务进入就绪态,当前任务会立即被抢占,无论是否用完时间片 。
-
任务进入阻塞状态 :
若任务因等待信号量、队列或延时操作(如
vTaskDelay()
)进入阻塞态,会立刻切换至其他任务。
时间片未耗尽时不被切换的场景:
在该批次优先级下仅有一个任务,也就是说没有任务可以切换,那么不就是只能就继续执行。
没有使用时间片调度。
在FreeRTOS中
认时间片是固定的1个系统节拍,仅支持同优先级任务轮转,并且无法自定义单个任务的时间片,也就说一个时间片只能是1个系统节拍。
ThreadX
支持在创建任务时显式指定时间片长度。例如,可为关键任务分配更长的时间片(如5ms),提升其单次执行时长。
注意这两个的区别:
FreeRTOS只能统一调整所有同优先级任务的时间片长度 。FreeRTOS 不允许为不同任务分配独立的时间片长度。
但是ThreadX可单独设置单个任务时间片。
任务的状态分析
运行就是runing状态,一个时刻只能运行一个任务, 那么其他任务就需要给一个状态,这个非运行状态还需要进行一点细分,这是因为不运行状态,有可能是一直不能运行,还有可能是准备好了,所以还需要进行细分:
ready:已经准备好了,随时运行,属于是准备好了就绪状态。随时运行,但是还没有轮到我。
blocked:阻塞,就是卡住了。以例子为讲解,就是任务等到某些事情发生才能继续执行,这个等待某些事情可以是一些标志位等,有点像任务暂停?但是又跟暂停不一样,这个是只要条件到了,就可以再次触发回到就绪状态,就是随时可以执行。阻塞态(Blocked):被动等待事件 任务因等待外部事件 或时间条件 而主动进入阻塞态,等待信号量、消息队列、事件标志(同步事件),调用延时函数(如 vTaskDelay()
)等待时间到期(定时事件)。在等待事件的过程中,这个任务就处于阻塞状态(Blocked)。
阻塞状态是事件驱动型:任务因等待外部条件暂停,内核自动监控并恢复,适用于需要同步或定时的场景。
suspend:挂起,就是先不执行。这个是属于暂停状态,主动休息状态,或者是被命令休息。就是我不干了。挂起态(Suspended):主动暂停任务 , 任务被移入挂起列表,完全脱离调度器管理,内核不在检查其状态。只能通过vTaskResume()
或 xTaskResumeFromISR()
手动恢复,且恢复后直接进入就绪状态。场景:日志记录、调试时临时冻结任务。
挂起态是人工干预 :任务被显式暂停且不受内核管理,需手动恢复,适合长期暂停或调试。 是主动休息或者是被动休息。

通过这个状态跳转图其实可以看到很多东西,能理解很多FreeRTOS的开发思路。能明白为什么会调用API,以及如何调用API。
在任务处于Runing状态的时候,我们可以操作将部分任务给挂起,从这个状态图中,只要能连接到挂起状态的状态都可以被挂起,甚至我可以让我自己暂停。
同理处于挂起状态的任务,必须要主动进行回复,也就是我可以在某些任务中嵌套恢复的API vTaskResume(),这是因为要想让挂起的任务动作,只能通过这个API恢复,只有这一条路。
并且想要让任务执行Runing,必须要先处于Ready状态,否则都是无稽之谈。
而处于阻塞状态的 任务必须依靠事件进行跳转 到Ready,还有就是每一个状态,都是通过链表进行管理的,至于什么事链表,在之前文章已经详细分析。
数据结构---链表结构体、指针深入理解(三)_void inserhead(node *node,elemtype value){ node *p-CSDN博客
换句话说,我们在开发过程中,可以随便的在不同任务中调用相关控制函数,从而实现了复杂性。复杂的原因就是这里,并且任务之间可以控制任务就是因为使用了每一个任务的句柄。
c
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask3;
如果没有这个句柄,外部的任务根本无法控制。
那这个地方其实就又引入一个新的问题,句柄是全局变量,那么频繁的使用这些全局句柄是否会带来影响?或者说应该怎么优化?
这个地方跟裸机开发的思维还是一样的, 1、使用static关键字,模块化编程思维。此外还有一些FreeRTOS中特有的一些思路如:
1、替代全局句柄的通信机制,任务通知(Task Notifications) 使用任务通知代替句柄操作,无需全局句柄即可唤醒或传递数据。
2、句柄存储优化,将句柄存入专用结构体,结合互斥锁保护。
3、动态查询替代静态句柄,按任务名查询句柄。
-
避免滥用全局句柄:优先使用任务通知、事件组等内核机制替代直接句柄操作。
-
必要时的保护 :若必须全局使用,需通过互斥锁(
xSemaphoreCreateMutex()
)或临界区(taskENTER_CRITICAL()
)保护。
关于这些内容,后续会详细分析。一定要深入的理解FreeRTOS,才能在开发过程中解决很多问题,遇到Bug才不会慌张,不然你无法定位到Bug,只能在应用层解决一些问题,并不能解决深入的问题。 都是以结果为导向的解决问题。
任务嵌套理解

c
void Task1Function(void * param)
{
TickType_t tStart = xTaskGetTickCount();
TickType_t t;
int flag = 0;
while (1)
{
t = xTaskGetTickCount();
task1flagrun = 1;
task2flagrun = 0;
task3flagrun = 0;
printf("1");
if(!flag && (t > tStart + 10)){
vTaskSuspend(xHandleTask3);
flag = 1; /* 标志位 */
}
if(t > tStart + 20){
vTaskResume(xHandleTask3);
}
}
}
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
task3flagrun = 0;
printf("2");
vTaskDelay(10); /* 阻塞状态。 */
}
}
void Task3Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 0;
task3flagrun = 1;
printf("3");
}
}
通过此次实验可以明显看出,Task1可以灵活处理Task3,可以让他挂起也可以让他恢复。同时自己可以控制自己让自己处于阻塞,例如使用延时函数,但是延时函数结束,阻塞状态就结束。那么是否可以让别人控制是否阻塞?
肯定可以,但是目前还没有掌握这种思路,因为:任务 A 控制任务 B 的阻塞状态主要通过任务间通信与同步机制实现,接下里理解了这种机制,自然也就明白了如何实现控制。理论上来说,任何任务都可以控制任何任务的任何状态。
通过这个实验,只是对任务的相互控制有一个简单的认识。
真正复杂的是在业务逻辑之间的嵌套,以及如何设计实现任务之间的循环嵌套。
FreeRTOS中延时函数分析(用于阻塞?不拘泥于阻塞)
c
void vTask1( void *pvParameters )
{
const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
TickType_t xLastWakeTime;
int i;
/* 获得当前的Tick Count */
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
flag = 1;
/* 故意加入多个循环,让程序运行时间长一点 */
for (i = 0; i <5; i++)
printf( "Task 1 is running\r\n" );
#if 1
vTaskDelay(xDelay50ms);
#else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
}
}
事件驱动型和中断驱动型在裸机中也是存在的,但是在RTOS中,只不过多了几个枷锁,或者说执行的更加规范,
任务的执行一定是按照优先级和调度策略的。这是逻辑中不一样的,裸机中事件来了就能直接驱动。在FreeRTOS中,中断和事件的到来确实是唤醒任务的契机,并且唤醒只是让他处于Ready状态。但它们不能直接决定任务是否立即执行 。任务的实际执行由调度器根据优先级和调度策略综合决策。
以两个任务再次理解FreeRTOS内核。
c
void vTask1( void *pvParameters )
{
const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
TickType_t xLastWakeTime;
int i;
/* 获得当前的Tick Count */
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
flag = 1;
/* 故意加入多个循环,让程序运行时间长一点 */
for (i = 0; i <5; i++)
printf( "Task 1 is running\r\n" );
#if 1
vTaskDelay(xDelay50ms);
#else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
}
}
void vTask2( void *pvParameters )
{
for( ;; )
{
flag = 0;
printf( "Task 2 is running\r\n" );
}
}
int main( void )
{
prvSetupHardware();
/* Task1的优先级更高, Task1先执行 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
在上述两个任务创建函数中,我们可以明确的看出,这两个任务函数都是死循环,这也是FreeRTOS中的核心,区别于裸机开发。并且FreeRTOS调度器是无法处理任务"正常返回"的情况,因为任务的上下文(栈和TCB)会处于未定义状态。那怎么实现任务轮询呐?依靠的是调度器,这不正是我们使用FreeRTOS的原因,正是因为这个原因,我们才借助于系统开发。
如果我们必须终止任务,那么也可以就需要显示删除自身,例如初始化任务,避免死循环,一直初始化。
删除自身:
c
vTaskDelete(NULL); // 删除当前任务,传递 NULL 表示删除自身
但是还需要明白vTask1中延时函数:
vTaskDelay(xDelay50ms);
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
c
void vTask1( void *pvParameters )
{
const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
TickType_t xLastWakeTime;
int i;
/* 获得当前的Tick Count */
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
flag = 1;
/* 故意加入多个循环,让程序运行时间长一点 */
for (i = 0; i <5; i++)
printf( "Task 1 is running\r\n" );
#if 1
vTaskDelay(xDelay50ms);
#else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
}
}
在任务1中,执行到延时函数vTaskDelay(xDelay50ms);
任务右执行状态,被调整为挂起,从就绪列表给移除,避免被调度器选中执行,并且加入延时列表,这是因为内核需要计算唤醒时间,并将任务按唤醒时间排序插入延时列表(xDelayedTaskList
或 pxOverflowDelayedTaskList
)。恢复调度与切换,调用 xTaskResumeAll()
恢复调度器,并触发任务切换(portYIELD_WITHIN_API()
),让出 CPU 给其他就绪任务。并且该转换状态是已经实现了,不需要开发者在关注。
阻塞态 → 就绪态
延时到期后,内核将任务从延时列表移除,并插入就绪列表,状态自动变为就绪(Ready)。
就绪态 → 运行态
调度器根据优先级自动分配 CPU:若任务优先级最高,则立即抢占当前任务;否则等待调度点(如时间片结束)。
任务是不会退出,但是会在调度器中切换,该栈空间还是会存在的。函数调用栈被完整保留在任务私有堆栈中,等待唤醒后恢复。相当于任务只是暂停。
并且恢复以后唤醒后任务从 vTaskDelay
的下一行代码继续执行 (即 for(;;)
循环的末尾 }
),而非重新开始函数或退出循环。
并且vTaskDelay(xDelay20ms);
内部会对该任务函数进行上述描述的处理。
vTaskDelay(xDelay20ms);
延时函数的作用是:
从进入该延时函数到退出该延时函数是固定的,

也就是说只要在本次任务执行完成,进入该延时函数,那么就必须要固定的绝对的时间才能退出来,也就是固定时间阻塞。
vTaskDelayUntil(&xLastWakeTime, xDelay20ms);
延时函数的作用是:
如果我想让该任务周期性执行,固定时间执行。那么就需要使用上述的延时函数。

并且只要在任务1执行范围内,我们调用该函数vTaskDelayUntil(&xLastWakeTime, xDelay20ms);
就可以,因为这个延时函数只是固定了终点的时间和上一次的终点时间,也就是说只要在这两个时间点之间执行就行,相当于是告诉调度器,我上一次是什么时候,而我下一次应该是什么时候。固定间隔执行。

通过上述的逻辑分析仪也能看出来时间。
并且这个函数vTaskDelayUntil(&xLastWakeTime, xDelay20ms);
记录的是每次的唤醒时间,因此每次都是这一次唤醒时间和下一次的唤醒时间都是20ms。

从这三个图片中可以明显看出,不管Task1的执行时间是多少,任务1就是每隔20ms时间执行一次。并且一定是这一次的唤醒时间和下一次的唤醒时间。
vTaskDelayUntil(&xLastWakeTime, xDelay20ms);
xLastWakeTime会自动更新的。
并且还有一些隐藏的细节:

就是在任务1执行完成以后,关于任务2和任务3的启动顺序是不一样的。
目前尝试分析并没有分析出来什么原因。暂时先放在这里。
**==优先级任务相同的情况下,是交替执行。
1.2.8 空闲任务和钩子函数
在创建任务是有返回值的,如果创建成功就会返回dpPASS
,
失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)
当我们直接创建两个任务的时候,一个是高优先级,一个是低优先级,那么如果没有其他附加条件,高优先级会一直执行,而低优先级不会执行。这是因为FreeRTOS中,任务调度式基于优先级的抢占式调度,高优先的任务总是优先获取CPU使用权,低优先级任务的执行依赖于高优先级任务主动释放CPU资源,所以如果高优先级的不主动释放CPU资源,那么低优先级的任务是无法执行的。
低优先级任务的三种核心执行场景:
1、高优先级任务进入阻塞态(Blocked State)
当高优先级任务调用阻塞式函数(如 vTaskDelay()
、xQueueReceive()
等)时,会主动让出 CPU 使用权,进入阻塞态等待事件(如延时结束、信号量释放)。此时调度器会从就绪态任务中选择最高优先级的任务执行。若当前无更高优先级任务就绪,低优先级任务即可获得 CPU 时间
存在一种情况,低优先级任务内部创建了一个高优先级任务,然后高优先级任务内部又没有阻塞,那么从此以后就再也不会执行低优先级任务了。
2、高优先级任务主动挂起(Suspended)或删除自身
若高优先级任务调用 vTaskSuspend()
挂起自身或 vTaskDelete()
删除自身,其不再参与调度。调度器会从剩余就绪任务中选择最高优先级任务(可能是低优先级任务)运行。相当于内核不在追踪该高优先级任务,内核直接放弃这个任务了,所以低优先级任务就有机会了。
3、所有高优先级任务均未就绪时(空闲窗口)
当所有用户任务均阻塞或挂起时,FreeRTOS 会运行优先级为 0 的空闲任务(Idle Task)。此时若低优先级任务处于就绪态(如延时结束),调度器会将其从就绪态切换至运行态。
-
1.无阻塞则无执行
- 若高优先级任务始终未阻塞(如死循环中无延时或同步操作),低优先级任务永远无法执行。这是抢占式调度的核心特性。
-
2.状态转换与调度触发
- 低优先级任务需处于就绪态(Ready)才能被调度。若其因等待事件而阻塞,需等待条件满足(如延时结束)后重回就绪链表,才可能被选中。
-
3.中断的影响
- 中断服务程序(ISR)可抢占任何任务(包括高优先级任务)。若中断中释放了信号量或消息,可能唤醒阻塞的低优先级任务,但该任务仍需等待高优先级任务释放 CPU 后才能运行。
场景 | 触发条件 | 调度行为 |
---|---|---|
高优先级任务阻塞 | 调用 vTaskDelay() 、等待队列/信号量等 |
调度器选择就绪链表中最高优先级任务(可能是低优先级)运行 |
高优先级任务挂起或删除 | 调用 vTaskSuspend() 或 vTaskDelete() |
低优先级任务作为当前最高优先级就绪任务被选中 |
所有高优先级任务未就绪 | 系统进入空闲状态 | 空闲任务运行,低优先级任务若就绪则可能被调度 |
c
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
taskidleflagrun = 0;
printf("2");
vTaskDelay(2);
vTaskDelete(NULL);
}
}
对于该任务自杀以后,需要空闲任务去释放内存,不然是没有任务会进行清理的,就会导致内存用完,报错。
但是在这种情况下:
c
void Task1Function(void * param)
{
TaskHandle_t xHandleTask2;
BaseType_t xReturn;
while (1)
{
task1flagrun = 1;
task2flagrun = 0;
taskidleflagrun = 0;
printf("1");
xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);
if(xReturn != pdPASS)
printf("xTaskCreate err\r\n");
vTaskDelete(xHandleTask2);
}
}
删除以后,直接就会清除内存。为什么呐?
我们知道如果任务2没有阻塞,那么任务1再也不会被执行。但是我们在创建任务2的时候设置了阻塞。
那这就不得不带来一个疑问,空闲任务到底是什么时候有机会执行内存清空的?
FreeRTOS的SysTick中断(通常1ms一次)会强制触发任务调度器检查任务状态。即使Task1未主动阻塞(如无vTaskDelay
),SysTick中断仍会暂停Task1的执行,使调度器有机会切换到空闲任务(Idle Task)。 存疑?
在创建空闲任务以后,注意他的优先级是0,要给他运行的机会,不然永远就不会被运行。

里面不能包含死循环,不然就不能干其他事情了,就不会清空内存了。
所以说这一点还是要注意的。
1.2.9 任务调度算法
configUSE_PREEMPTION 实现是否是抢占式调度
关于是否抢占式调度式可以配置的,一种是抢占式调度,另外一种就是非抢占式调度。抢占式很简单,就是高优先级的执行,然后自己主动阻塞释放CPU资源,给低优先级的任务执行空间。而非抢占式就是很简单,只要我不释放资源,谁都抢不走我。这种也被称为合作调度模式,这样的写法就是每个任务都要配置延时函数,主动的释放相关资源。理解起来还是简单的。在实际应用中,一般都是使用抢占式调度。
configUSE_TIME_SLICING 可抢占的前提下,同优先级的任务是否轮流执行
轮流执行,就是很简单,你执行一次,我执行一次,大家轮流执行。
不轮流执行,就是我要一直执行,除非我主动释放。

可以看出,当最高优先级的任务到来以后,肯定是执行最高优先级的任务,但是高优先级任务执行完成以后,这些同优先级的任务除了空闲任务会主动释放资源,其他的任务都是不会释放任务,那么就会一直霸占CPU资源。
configIDLE_SHOULD_YIELD 空闲任务是否让步于用户任务
空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务,上面就是只执行一次。
空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊
空闲任务如果礼让别人就是相当于自己主动触发一次调度任务函数。
不礼让的波形:

礼让的波形:

可以明显看出两者的区别。
如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。
专栏介绍
《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。