由于正在学习韦东山老师的RTOS课程,结合了网上的一些资料,整理记录了下自己的感悟,用于以后自己的回顾。如有不对的地方请各位大佬纠正。
文章目录
- 一、RTOS的优势
- 二、RTOS的核心功能
- 2.1 任务管理
- 2.1.1 任务的创建
- 2.1.2 任务的删除*
- 2.1.3 任务优先级和Tick
- 一、优先级
- 二、Tick(滴答)
- 三、优先级的实验
- 三、优先级设定的实验
- 2.1.4 任务状态
- 一、阻塞状态(Blocked)
- 二、就绪状态(Ready)
- 三、暂停状态(Suspended)
- 四、完整的状态转移图
- 2.1.5 相对延时和绝对延时
- 一、相对延时函数
- 二、绝对延时函数
- 三、延时实验
- 2.1.6 空闲任务及钩子函数
- 一、钩子函数
- 2.1.7 调度算法
- 一、调度算法的配置
一、RTOS的优势
①:确定性和实时性:
RTOS的最大特点是能够在严格的时间约束内完成任务。这种确定性对于时间敏感的应用(如工业控制、医疗设备等)至关重要。
②:优先级调度:
RTOS通常支持优先级调度机制,确保高优先级的任务可以抢占低优先级的任务执行。这种机制保证了关键任务能够在最短时间内得到处理。
③:低延迟和高响应性:
RTOS设计的目标是最小化任务切换时间和中断延迟,从而实现高响应性。这在需要快速反应的嵌入式系统中非常重要。
④:资源管理和内存控制:
RTOS通常提供精细的资源管理工具,允许开发者更好地控制内存和CPU资源的使用。这种控制对于嵌入式系统中的资源有限环境尤其重要。
⑤:模块化和灵活性:
RTOS通常具有模块化设计,允许开发者根据具体需求启用或禁用特定的功能模块。这种灵活性有助于优化系统性能和减少系统开销。
⑥:可靠性和稳定性:
RTOS被广泛应用于需要高可靠性和稳定性的系统中,例如自动驾驶、军事系统等。RTOS通过严格的测试和验证,确保其在各种边界情况下都能稳定运行。
⑦:较小的内存占用:
RTOS通常占用的内存和资源较少,这使得它非常适合嵌入式系统或其他资源受限的环境。
二、RTOS的核心功能
RTOS的核心功能块主要分为任务管理、内核管理、时间管理以及通信管理4部分,框架图如下所示:
(1)任务管理:负责管理和调度任务的执行,确保系统中的任务能够按照预期运行。
(2)内核管理:负责系统核心功能的管理,包括内存、中断、异常处理和系统启动等。
(3)时间管理:负责所有与时间相关的操作,包括系统时钟、定时器、任务延迟和周期性任务的执行。
(4)通信管理:提供任务之间的通信机制,确保任务能够有效地协作和共享资源。
2.1 任务管理
2.1.1 任务的创建
任务就是一个无返回的函数(Void)。由于函数传参的不同,一个函数可以创建多个任务,然后每个任务都有对应自身的栈,也就是说一个函数可以有多个栈(当然一个函数对应一个栈也是可以的)。使用下面的函数用于创建任务:
void TaskAFunction(void *param)
{
int* tmp = (int*) param;//首先将void *指针类型的param转为int *类型的指针
int value = *tmp; //然后解引用来获取指针指向的值
while(1)
{
printf("%d",value);
}
}
尽管是同一个函数,但是创建的多个任务主要不同还是在于传参而不是名字,下面的代码使用了相同的名字(“TaskA”)创建了三个参数不同的任务。
int x1=1;int x2=2;int x3=3;
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(TaskAFunction,"TaskA",100,&x1,1,NULL);
xTaskCreate(TaskAFunction,"TaskA",100,&x2,1,NULL);
xTaskCreate(TaskAFunction,"TaskA",100,&x3,1,NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
2.1.1 xTaskCreate
上面使用的xTaskCreate是动态创建任务的,当然还有静态创建任务的函数xTaskCreateStatic,后面再提静态创建。下图为xTaskCreate函数的参数及介绍:
下图摘自韦东山的FreeRTOS完全开发手册3.2.2节
2.1.2 任务的删除*
任务的删除使用如下函数,其中填入的参数如果是NULL表示自杀,如果是自己的句柄则是被杀,别人的句柄就是杀人。
void vTaskDelete( TaskHandle_t xTaskToDelete );
实验是在vTask1任务中嵌套vTask2任务的创建,而vTask2任务中执行删除自身任务的操作,而相对延时函数vTaskDelay( xDelay100ms );
在Task1中的存在与否会有影响么呢?
代码如下所示,肯定先创建并运行Task1,执行完自身的printf后,创建并优先调用Task2,Task2也会printf自身信息并删除自己的任务(内存还未释放)。此时就要注意了vTaskDelay
函数会起了一个很重要的作用。因为vTaskDelay
的存在使得Task1进入了阻塞状态,此时没有其他任务(Task2也被删除啦)需要执行,导致系统会执行优先级最低的IDLE任务,这个任务会把Task2所占用栈的内存给释放。代码如下
TaskHandle_t xTask2Handle = NULL;
int main( void )
{
...
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
...
}
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );//100ms的延时
BaseType_t ret;
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务1的信息 */
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)//判断vTask2是否创建成功,一般pdPASS默认为1
printf("Create Task2 Failed\r\n");
vTaskDelay( xDelay100ms );
}
}
void vTask2( void *pvParameters )
{
/* 打印任务的信息 */
printf("Task2 is running and about to delete itself\r\n");
// 可以直接传入参数NULL,进行“自杀”
vTaskDelete(xTask2Handle);
}
实验结果如下,Task1带有相对延时函数后,能够正常释放被删除的Task2所占用的内存空间,所以能够如下打印:
通过上文我们知道vTaskDelay
函数会起一个很重要的作用。此刻若是删除这个函数的话,Task1自然不会进入阻塞状态而系统更没机会调用IDLE任务,多次被删除的Task2任务所占用的内存一直无法释放而导致最后内存的耗尽,结果如下。
2.1.3 任务优先级和Tick
一、优先级
优先级在上文中提过,优先级的值大的优先执行,相同优先级的则交替执行,这个函数xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
的第5个参数则是表示优先级。
如何找到优先级最高的任务,RTOS的调度器会根据configMAX_PRIORITIES
的值来判断采用C函数还是汇编指令的方法来实现调度。
二、Tick(滴答)
函数vTaskDelay
可以用于指定任务休眠的时间,一般有以下两种表示方式:
方式一:vTaskDelay(5)
【存在延时不准的问题】
该方式直接设置5个Tick,根据下面公式可以算出时间T为:
T=(1/configTICK_RATE_HZ)*5=0.05s=50ms
方式二:vTaskDelay(pdMS_TO_TICKS(50UL))
【存在延时不准的问题】
该方式采用pdMS_TO_TICKS宏直接将ms转换为tick,上式表示为等待50ms。
三、优先级的实验
参考韦东山FreeRTOS手册,创建了3个任务,其中Task1和Task2的优先级为1,Task3的优先级为2。我们知道Task3任务优先级明显高于Task1和Task2的,但是如果不对Task3进行进行vTaskDelay的话,高优先级的会一直占用CPU,那么Task1和Task2的则不会有机会执行(就像备胎一样,一直在当女神的备胎,但是在女神眼里就是没正主优先级高,备胎就算等着舔不到女神,说明不要当舔狗,不过这也对应了任务的阻塞状态)。Task1~3的代码和main代码如下:
xTaskCreate(vTask1,"Task1",1000,NULL,1,NULL);
xTaskCreate(vTask2,"Task2",1000,NULL,1,NULL);
xTaskCreate(vTask3,"Task3",1000,NULL,2,NULL);
void vTask1( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务1的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务2的信息 */
printf("T2\r\n");
}
}
void vTask3( void *pvParameters )
{
const TickType_t xDelay3000ms=pdMS_TO_TICKS(1000UL);
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务3的信息 */
printf("T3\r\n");
// vTaskDelay(xDelay3000ms);
}
结果如下,只执行了Task3
当解开vTask3
函数中vTaskDelay(xDelay3000ms);代码的注释后,结果如下。Task3只执行1次后就不执行了,后面是Task1和Task2两个优先级为1的相互执行。那是因为Task3执行到vTaskDelay这个函数后会进入休眠状态,尽管优先级高于Task1和2,但是休眠状态不占用CPU资源,于是让给了两个优先级相同的Task1和Task2,而Task3休眠结束后,Task1和Task2没有休眠机制于是疯狂不断运行从而导致Task3的打印只出现了一次。
三、优先级设定的实验
本实验主要是通过vTaskPrioritySet
函数实现对任务优先级的设定。该函数具体如下:
void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);
其中第一个参数是也就是对应Task的handle,即每个任务在xTaskCreate
创建任务时所传入的第6个参数xTask2Handle。而第二个参数uxNewPriority是通过函数uxTaskPriorityGet
进行获取。
完整的实验如下,创建Task1和Task2。在Task1中print,并提高Task2的任务优先级来保证高于Task1。在Task2中同样print自己内容,并降低Task2的任务优先级来保证低于Task1。这样很明显两者通过调整任务优先级来实现一个来回执行的效果,代码如下:
void vTask1( void *pvParameters )
{
UBaseType_t uxPriority;
//获取Task1的优先级,其中NULL表示获取自身的优先级
uxPriority = uxTaskPriorityGet(NULL);
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务1的信息 */
printf("Task1 is runing\r\n");
printf("About to raise the Task 2 priority\r\n");
/*通过使用vTask1的优先级再+1,来保证vTask2具有更高的优先级,*/
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
}
}
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
//获取Task2的优先级,其中NULL表示获取自身的优先级
uxPriority = uxTaskPriorityGet(NULL);
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务2的信息 */
printf("Task2 is runing\r\n");
printf("About to lower the Task 2 priority\r\n");
/*通过使用vTask2的优先级再-2,来保证vTask1具有更低的优先级,*/
vTaskPrioritySet(NULL,(uxPriority - 2));
}
}
经代码验证,Task1与Task2的效果如下:
2.1.4 任务状态
任务一般可以分为运行(Runing)和非运行(不 Runing)两类。但是非运行的状态还能分成:①阻塞状态;②暂停状态;③就绪状态。
一、阻塞状态(Blocked)
阻塞状态,指的是任务因为等待某个事件或条件发生而无法继续执行的状态。如(1)相对/绝对延时函数这类时间等待;(2)队列或信号量等待;(3)事件标志等待。等等。这个状态下任务不会占用CPU资源,一旦满足某个事件的条件,就能转为就绪状态了。
二、就绪状态(Ready)
就绪状态,即随时准备响应调度器的号召,可以由阻塞状态转换而成。就像女神会择优选择好的备胎来处一样,调度器也会选择优先级最高且就绪(Ready)的任务来运行。
优先级最高好理解,就绪状态是怎么由阻塞状态转过来的呢?这个就涉及到了事件的概念,时间一般包含两类:(1)时间相关事件;(2)同步事件。/* 同步事件的具体概念后面学习内容会涉及 */
(1)时间相关事件:即设定一定的时间,这个时间内会处于阻塞状态,时间满足了就会转成就绪状态,就像延时函数vTaskDelay
一样,能够用来实现周期性/超时功能。
(2)同步事件:某个任务在等待别的任务或者中断服务程序发来的信息来唤醒它。这些同步方式包括:①任务通知;②队列;③事件组;④信号量(semaphoe);⑤互斥量(mutex);等
三、暂停状态(Suspended)
&emsp暂停状态一般很少用,唯一使用的方法就是通过void vTaskSuspend( TaskHandle_t xTaskToSuspend );
来使用。
四、完整的状态转移图
2.1.5 相对延时和绝对延时
FreeRTOS中两个延时函数分别是相对延时函数vTaskDelay()
和绝对延时函数vTaskDelayUntil()
,尽管两个函数都能使任务进入堵塞状态,但是由于延时方式的差异也会导致应用也有所不同。
一、相对延时函数
相对延时函数vTaskDelay()
的开始时间是从任务中执行到这个函数开始计算的,上面提到过这个函数的时间并不准确,是因为容易受到其他任务和中断活动的影响导致的。以当前任务遇到更高优先级的任务为例,当前任务执行到这个相对延时函数后会进入阻塞状态,系统会调度其他任务运行。如果有更高的优先级任务处于就绪状态,那么调度器会优先运行高优先级任务。当高优先级任务占用了CPU资源后,当前这个调用了vTaskDelay
函数的低优先级任务则需等待高优先级任务结束或者进入阻塞状态,才能再次运行,这也会导致延迟时间的不准确。此外遇到中断处理时间较长或者频繁发生导致占用过多的CPU时间,也会导致原计划中任务被推迟,在中断结束后调度器才会重新调度任务,因此vTaskDelay
延迟时间可能会比预期要长。
void vTaskDelay(TickType_t xTicksToDelay);
二、绝对延时函数
绝对延时函数vTaskDelayUntil()
的开始时间。如下所示,参数pxPreviousWakeTime用于存储上次任务唤醒的时刻;而参数xTimeIncrement用于表示每次任务被唤醒后所要延时的时间。正是由于有存储上轮任务唤醒时刻的机制,这个绝对延时函数更适合用于实现周期性的延时操作。
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
三、延时实验
相对延时的实验结果如下图所示,flag1为1表示Task1任务运行中,flag1为0表示Task2运行中(Task1处于堵塞状态)。相对延时函数的开始时间是从调用vTaskDelay
这个函数开始(即flag1从1跳变到0时)计算的50ms。
绝对延时的实验结果如下图所示,flag1为1表示Task1任务运行中,flag1为0表示Task2运行中(Task1处于堵塞状态)。绝对延时函数的开始时间是从Task1记录的上轮任务调用时间开始计算的的50ms。
具体实验代码如下所示:
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");
}
}
2.1.6 空闲任务及钩子函数
空闲任务也就是IDLE任务,在本文的 “ 2.1.2 任务的删除 ”这个实验例子中有体现。在任务的删除中一般离不开IDLE任务,可以回看下,我个人感觉还是写的比较清晰的。
IDLE任务的比较特殊,永远不会堵塞,优先级为0。一般在系统没有任务或任务处于堵塞状态下,IDLE任务会被调出来。
一、钩子函数
空闲任务的钩子函数是FreeRTOS提供的一种机制,允许用户在系统进入空闲任务时执行一些特定的操作。可以通过定义一个空闲任务钩子函数(vApplicationIdleHook())来扩展 IDLE 任务的功能,比如在系统空闲时进入低功耗模式、执行后台任务等,具体作用如下:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
2.1.7 调度算法
一、调度算法的配置
调度算法不仅要保证高优先级的任务先运行,还要确保同优先级的就绪态任务以“轮转调度”的策略来轮流执行。当然轮流调度存在的不保证任务运行时间的公平分配,因此可以细化运行时间的分配。
从3个角度理解多种调度算法:
(1)可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
√: 可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
×: 不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)
①:当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。
②:其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
(2)可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)
√: 轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片
×: 不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占
(3)在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:configIDLE_SHOULD_YIELD)
√: 空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
×: 空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊
下表用于配置调度算法,一共包含三个配置项,分别是(1)用于可抢占调度的配置项configUSE_PREEMPTION;(2)用于时间片轮转的配置项configUSE_TIME_SLICING;(3)用于关闭Tick中断来实现省电的配置项onfigUSE_TICKLESS_IDLE。
(1)配置项configUSE_PREEMPTION的影响
实验共有两个优先级为0的Task1和Task2,一个优先级为2的Task3。每个任务都有自己对应的flag(1表示该任务运行中),若系统所有任务都未执行则将IDLE任务的标志位置1。改变FreeRTOSConfig.c中配置项configUSE_PREEMPTION的值来判断影响:
此时FreeRTOSConfig.c里面配置项USE_PREEMPTION为1表示高优先级抢占
int main(void)
{
......
xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
......
}
void vTask1( void *pvParameters )
{
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 1;
flagTask2run = 0;
flagTask3run = 0;
/* 打印任务的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 1;
flagTask3run = 0;
/* 打印任务的信息 */
printf("T2\r\n");
}
}
void vTask3( void *pvParameters )
{
const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 1;
/* 打印任务的信息 */
printf("T3\r\n");
// 如果不休眠的话, 其他任务无法得到执行
vTaskDelay( xDelay5ms );
}
}
void vApplicationIdleHook(void)//空闲状态下的钩子函数,在task.c里面掉用
{
flagIdleTaskrun = 1;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 0;
/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
printf("Id\r\n");
}
#if ( configUSE_IDLE_HOOK == 1 )//钩子函数的调用
{
extern void vApplicationIdleHook( void );
/* Call the user defined function from within the idle task. This
* allows the application designer to add background functionality
* without the overhead of a separate task.
* NOTE: vApplicationIdleHook() MUST NOT, UNDER ANY CIRCUMSTANCES,
* CALL A FUNCTION THAT MIGHT BLOCK. */
vApplicationIdleHook();
}
实验结果如下所示,Task3优先级最高,优先执行了该任务,并在红线1的时刻调用延时函数进入了阻塞状态。此时Task1和Task2两个同优先级的任务开始交叉执行。等到Task3延时结束后由于优先级最高则会立马抢占重新开始Task3任务的执行。而在这三个任务都不执行的时候,系统则会执行IDLE状态(对应红线)。
相应的如果FreeRTOSConfig.c中配置项configUSE_PREEMPTION的值为0表示不抢占的话,结果如下所示。可以看到在红线前半部分正常,当Task3因为延时进入阻塞状态后,开始就混乱了。没有抢占更没有协商好,即使Task3延时超时后,优先级更高的它也没机会执行了。
(2)配置项configUSE_TIME_SLICING的影响
实验代码如上,只不过这里是对时间片是否轮转来判断影响的。因此这里只改变configUSE_TIME_SLICING的值,另外两个配置项都为1。
下图为时间片轮转,即配置项configUSE_TIME_SLICING值为1。
下图为时间片不轮转,即配置项configUSE_TIME_SLICING值为0。不同于时间片轮转会在高优先级任务Task3阻塞的时候(flag3为0的时候)轮流执行相同优先级的Task1和Task2。时间片不轮转的情况下,在高优先级任务阻塞时只引起了一个任务的执行(Task1/Task2)。而只有高优先级任务就绪或者不再运行时才会引起任务的切换。
(3)配置项configIDLE_SHOULD_YIELD的影响
实验代码如上,只不过这里是对空闲任务是否让步来进行。因此这里只改变configIDLE_SHOULD_YIELD的值,另外两个配置项都为1。
下图为空闲任务让步,即配置项configIDLE_SHOULD_YIELD值为1。
下图为空闲任务不让步,即配置项configIDLE_SHOULD_YIELD值为0。可以看到配置为空闲任务为不让步后,三者的优先级是相同的。在高优先级任务阻塞的时候,Task1、Task2以及IDLE任务都是相同优先级,因此他们会采用轮流执行。