一、小型操作系统的由来
最开始我们想要每一秒钟去读取一次温度的数据,所以代码如下
while(1)
{
readTemperature();
delay_ms(1000);
}
但是此时我们会发现在延时的那一秒钟里面CPU没有并没有干任何事情,死延时很占用CPU资源,后面我们开始用一个定时器计时,时间到了则调用读取温度的数据,这样可以大大提高CPU空闲时间。
while(1)
{
if(timer==1000)//每个1ms,定时器中断里面timer值都会++
{
timer=0;
readTemperature();
}
}
随着任务的复杂,代码逻辑的增多,我们在while(1)中写了无数个if语句,这很不利于我们去观看代码,并且逻辑也很复杂,所以出现了任务调度,也就是小型操作系统
二、FreeRTOS任务调度三种方法
在FreeRTOSConfig.h中,可以配置FreeRTOS的任务调度方式
#define configUSE_PREEMPTION 1 //1使用抢占式内核,0使用协程
#define configUSE_TIME_SLICING 1 //1使能时间片调度(默认式使能的)
FreeRTOS支持的任务调度方法有抢占式、协作式、时间片轮转
协作式调度:一旦一个任务开始执行,它将持续运行,直到它自己放弃 CPU 控制权为止。这通常发生在任务主动调用 task delay(等待时间片到期)、task yield(放弃剩余时间片)、或者进入阻塞状态(等待事件或资源)时。协作式调度简化了任务间共享资源的管理,因为开发者可以确保在没有明确放弃 CPU 控制权的情况下,任务不会被中断。但这种模式要求每个任务都必须定期放弃 CPU 控制权,否则可能导致系统响应性能下降。 在FreeRTOS里,是通过taskYIELD()这个函数实现放弃CPU的。一个典型的协作式任务是在while(1){}大循环的最后,调用taskYIELD()去主动放弃CPU;这时其他处于就绪态的最高优先级的任务才可能运行;如果其他任务都不在就绪状态,那么仍然回到taskYIELD()后面继续运行原来的任务。
时间片轮转:让相同优先级的几个任务轮流运行,每个任务运行一个时间片,任务在时间片运行完之后,操作系统自动切换到下一个任务运行;在任务运行的时间片中,也可以提前让出CPU运行权,把它交给下一个任务运行。 FreeRTOS的时间片固定为一个时钟节拍,由configTICK_RATE_HZ这个宏设置,时间片轮转调度功能,由configUSE_TIME_SLICING宏定义(注意:使能时间片轮转调度功能,两个相同优先级的任务哪怕两者都没有释放CPU资源,两个任务都还是会被调用)
抢占式调度:允许操作系统根据优先级来决定哪个任务应当获得 CPU 控制权。当一个高优先级任务变为就绪状态时,系统会中断当前运行的较低优先级任务,把 CPU 控制权交给高优先级任务。这确保了对紧急任务的快速响应。在这种模式下,任务不需要显式放弃 CPU 控制权,因为它们可以随时被更高优先级的任务抢占。
三、任务栈空间大小
FreeRTOS的任务栈空间是在任务创建的时候从FreeRTOSConfig.h文件中的heap空间中申请的
在FreeRTOSConfig.h文件中,有一个变量configTOTAL_HEAP_SIZE,它定义的是所有任务栈空间大小的总和。
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 18 * 1024 ) )
//FreeRTOS堆中可用的RAM总量,单位是Bytes
为什么任务栈空间是在是堆中的?因为我们采用的就是动态创建任务的方式。
此时会存在疑问,在启动文件中定义的栈大小在哪里使用了?
启动文件中的Stack_Size EQU 0x800栈大小,它的名字叫做系统栈空间,任务栈不使用这里的栈空间,哪里使用这里的栈空间呢?答案是中断函数和中断嵌套。 Cortex-M3 和 M4 内核具有双堆栈指针,MSP 主堆栈指针和 PSP 进程堆栈指针。在 FreeRTOS 操作系统中,主堆栈指针 MSP 是给系统栈空间使用的,进程堆栈指针 PSP 是给任务栈使用的。 也就是说,在 FreeRTOS 任务中,所有栈空间的使用都是通过PSP 指针进行指向的。 一旦进入了中断函数以及可能发生的中断嵌套都是用的 MSP 指针。
任务栈:每个任务在创建时,其栈空间是从FreeRTOS的堆(configTOTAL_HEAP_SIZE)中分配的(除非使用静态创建任务方式)。
局部变量:位于各自任务的栈上,因此占用的也是从FreeRTOS堆中分配的任务栈空间。
pvPortMalloc分配的内存:直接占用FreeRTOS堆(configTOTAL_HEAP_SIZE)的空间。
启动文件(如startup_stm32xxxx.s)中定义的Stack_Size(如ARM Cortex-M架构)是为中断栈和主栈(main函数的栈)预留的,这个栈空间在FreeRTOS初始化之前就已经存在,用于系统启动和中断处理。而FreeRTOS运行后,每个任务有自己的栈,这些任务栈是从FreeRTOS的堆中分配的,与启动文件中的Stack_Size无关。
四、空闲任务
当所有任务都被挂起或者阻塞,没有被调用时,此时CPU在执行什么任务了?
当调用函数 vTaskStartScheduler()启动任务调度器的时候此函数就会自动创建空闲任务
configMINIMAL_STACK_SIZE是控制空闲任务(Idle Task)栈空间的核心配置参数,单位是字
空闲任务的核心作用:
CPU资源回收 当所有高优先级任务阻塞时,系统自动运行空闲任务(优先级为0)
钩子函数执行平台 支持用户自定义vApplicationIdleHook(),用于低功耗模式、内存清理等后台操作
内存释放触发点 若删除任务,其占用的栈和TCB内存会在空闲任务中被回收
就绪任务链维护 管理任务状态链表,确保调度器高效运行
五、任务栈空间分配的地址在哪里
我们知道单片机有Flash和Ram两块内存区域,其中Flash掉电保存,而RAM掉电不保存,Flash主要存储代码,只读变量和已经初始化的变量,而RAM就主要存储已经初始化的变量、没有初始化的变量和初始化为0的变量,但讲了上述的这些,我们还是会疑惑,比如,启动文件中栈空间的大小又存放在哪里了?
下图大致讲解RAM的分配情况
六、malloc申请的空间存放在哪里
七、如何确认任务大小
xPortGetFreeHeapSize()
//获取调用堆中空闲内存的大小,以Byte为单位 (heap_3 方案不能使用这个API)
xPortGetMinimumEverFreeHeapSize()
整个FreeRTOS堆的内存池历史最小剩余量,单位字节
uxTaskGetStackHighWaterMark()
printf(" the min free stack size is %d words
",(int32_t)uxTaskGetStackHighWaterMark(NULL));
前提:在FreertosConfig.h中把 INCLUDE_uxTaskGetStackHighWaterMark 配置为1
作用:单个任务从创造至今栈空间的最小剩余量,单位:字(Words)
通过上面函数得到任务所需栈空间的大小后,然后将任务大小设置为正常情况下的1.5倍即可
八、任务间通信方式
在linux里面,我们知道进程的通信方式由管道,命名管道,消息队列,信号,信号量,共享内存
在FreeRTOS里面,任务间的通信方式有队列、信号量、互斥量、事件标志、共享内存、任务通知、软件定时器和全局变量,而他们的使用场景和作用分别是什么了
首先我们要明确,任务是一个线程还是一个进程?
其次,线程间会存在以下问题,他们之间如何进行通信,他们之间同时操作同一个变量可以吗?
虽然上面涉及到任务间的通信方式有很多种,但我们都清楚,实际上我们写代码时可能一种都不混用上或者只用常见的一些,下面来进行介绍
线程间通信:
(1)二值信号量(A任务释放资源,B任务等待A任务释放资源后运行,常用场合:一个任务等待某个标志位的完成)
(2)任务通知(实质是一个32位的无符号整数,作用相当于二值信号量,但是他的优点更多,比如不需要提前创建,相当于创建了32个二值信号量)
常见场景,CAN接收中断接收到数据,此时任务通知任务处理数据
(3)消息队列,常见场景,CAN接收中断接收到数据,放入到消息队列中,此时任务获取消息队列中的数据进行处理
(注意:消息队列比自定义队列的一个好处是,实时性更高,CPU资源利用更加合理)
(4)事件组(可以实现多任务间的同步)
常见场景,一个任务需要等待多个事件发生;多个任务之间需要保证正确的执行顺序;
暂无评论内容