这一次的代码基础是"FreeRTOS_任务创建与删除"这篇文章。博客链接如下:
多任务交替执行的原理
原理
任务在FreeRTOS中的存在形式是结构体,这些结构体以链表的形式进行管理。
RTOS中有一个tick中断,这是一个周期性的定时器中断。当中断发生后,会在就绪链表中去搜索下一个需要执行的任务是什么,从而实现交替执行。
具体的简化模型如下:
假设就绪任务链表中有三个任务,当前point指向的是任务1,那么此刻执行的就是任务1的任务函数。过了一段时间,产生了一个tick中断,指针指向了下一个任务,开始指向任务2。又过一段时间,执行任务3。又过一段时间,重新指向了任务1。从而实现了交替执行。
任务切换时间
这个交替的时间tick默认为1ms,这并不是一个固定值,可以通过宏来设置。
具体设置如下:
在上图中,设置的为1000Hz,就是1ms。通过修改这个宏的值,即可修改任务切换的时间。
任务的状态
理论知识
任务的状态有四种:运行态、就绪态、挂起态(暂停态)、阻塞态
- 运行态:正在运行的任务的状态
- 就绪态:可以运行,但是还没轮到它的任务的状态
- 挂起态:该任务已经停止,不会再运行的状态。但可以解挂,解挂之后就可以变成就绪态
- 阻塞态:正在等待一些事件,不能继续运行的任务的状态
它们之间的转换关系如下图:
- 1:当任务创建时,进入就绪态
- 2:当轮到该任务执行时,任务进行运行态开始运行
- 3:当tick中断产生后,该任务重新变回运行态,这时另一个任务进入运行态
- 4:当任务运行时遇到了需要等待的事件,就会进入阻塞态
- 5:当任务运行时,调用了挂起的函数,就进入了挂起态
- 6:当阻塞的任务接收到了等待的事件,就会再次就绪
- 7:当阻塞态的任务被其他任务调用挂起函数后,就进入了挂起态
- 8:当就绪态的任务被其他任务调用挂起函数后,就进入了挂起态
- 9:当挂起态的任务被其他任务调用解挂函数后,就进入了就绪态
挂起与解挂函数
挂起与解挂函数非常简单,传入相应的任务句柄,即可实现挂起与解挂。声明如下:
/* 挂起 */
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
/* 解挂 */
void vTaskResume( TaskHandle_t xTaskToResume );
挂起函数vTaskSuspend传入NULL时,代表挂起自己
解挂函数不可能由自己调用,必须由其他任务来解挂被挂起的任务。
延时函数
等待函数非常简单,传入相应的值即可实现以阻塞形式进行延时。声明如下:
/* 延时,类似 HAL_Delay() */
void vTaskDelay( const TickType_t xTicksToDelay );
/* 延时,该延时是个宏*/
vTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement );
/* 获取计数值,类似HAL_GetTick() */
TickType_t xTaskGetTickCount( void );
vTaskDelay传入的参数的单位为tick,这与前面“ 任务切换时间 ”这一节叙述的宏有关。默认为1ms
vTaskDelayUntil的第一个参数是基准时刻,第二个参数为延时的时间,单位为tick
这两个函数的区别为:vTaskDelay可以确定延时过程为多少tick,vTaskDelayUntil可以确定结束时刻为多少tick。模型如下:
使用vTaskDelayUntil可实现任务周期性的执行,具体代码实现如下:
void TaskFunction(void* param){
TickType_t tStart = xTaskGetTickCount();/* 获取当前的tick值 */
while(1){
/* 功能函数 */
/* ........... */
vTaskDelayUntil(&tStart,20);/* 以进入该任务为基准时间,阻塞20tick后,进入就绪态 */
}
}
具体的模型如下:
这样,就实现了每次任务执行的时间都是tStart到tStart+20这段时间。
空闲任务
空闲任务的创建并不需要手动创建,FreeRTOS在启动调度器函数中已经自动创建好了空闲函数。
空闲任务的作用如下:
- 回收自杀任务空间
- 执行一些低优先级、低功耗的功能(这需要使用钩子函数来实现)
回收自杀任务空间
当任务进行自杀时,即:vTaskDelete传入的参数为NULL。空闲任务会进行清理工作。
需要注意的是,空闲任务的优先级为0,就是最低优先级。这意味着当任务自杀后,如果有其他任务的优先级高于0时,就不会进行任务的释放。
下面进行一个实验,实验内容如下:
- 在main中创建一个任务1,这个任务的优先级是1。
- 在任务1中,创建任务2,这个任务2的优先级为2,如果创建失败了,就打印一个err。
- 在任务2中,删除自己。
实验一:空闲任务内存回收失败
具体代码实现如下:
/* Task1任务处理函数 */
void Task1Function(void *param){
TaskHandle_t xHandleTask2;
int i='a';
BaseType_t xReturn;
while(1){
/* 创建任务2,传入参数a~z */
xReturn = xTaskCreate(Task2Function,"Task2",100,(void*)&i,2,&xHandleTask2);
/* 创建失败,打印err */
if(xReturn != pdPASS){
printf("err");
}
printf("1");
i++;
}
}
/* Task2任务处理函数 */
void Task2Function(void *param){
while(1){
/* 打印创建时传入的参数 */
printf("%c",*((int*)param));
/* 删除自己 */
vTaskDelete(NULL);
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
TaskHandle_t xHandleTask1;
prvSetupHardware();
SerialPortInit();
printf("UART TEST\r\n");
/* 创建任务1 */
xTaskCreate(Task1Function,"Task1",100,NULL,1,&xHandleTask1);
vTaskStartScheduler();
return 0;
}
代码分析如下:
因为任务2的优先级为2,高于任务1的优先级,所以任务1创建完成任务2后,会直接执行任务2。这时,任务2打印传入的参数并删除自己。之后重复这些操作。
运行结果如下:
结果分析如下:
可以看到,任务2打印了a~z还有一些其他字符,这说明任务2都在执行。但是打印到?之后,任务1就开始打印了err,说明任务2创建失败。
任务2的创建失败的原因是没有足够的内存,这说明任务2在调用vTaskDelete删除自己的时候并未释放内存。释放内存的工作是空闲任务来进行的,这说明空闲任务并未执行。
空闲任务的优先级是0,在任务2删除自己后,依旧有一个任务1在执行,它的优先级是1。因此导致空闲任务无法执行,因此删除后的任务的空间无法释放。
实验2:修改代码使得回收成功
经过上述分析,我们需要让空闲任务可以执行。改变任务1的优先级为0,这样就可以在任务2删除之后,任务1和空闲任务交替执行。
基于上述代码,修改部分为main中的创建任务1函数:
/* 修改前的创建任务1函数 */
xTaskCreate(Task1Function,"Task1",100,NULL,1,&xHandleTask1);
/* 修改后的创建任务1函数 */
xTaskCreate(Task1Function,"Task1",100,NULL,0,&xHandleTask1);
运行结果如下:
结果分析:
打印乱码是因为任务2打印的是字符,字符超过ASCII范围就是乱码。但这不是重点。
可以看到的是任务1已经不再打印err,这说明任务2再不断的创建成功,因此也说明了空闲任务回收了任务2 自杀时的任务空间。
钩子函数
钩子函数是空闲任务处理函数中调用到的一个函数。空闲任务可以执行一些功能,这些功能并不会直接让你直接在空闲任务处理函数中修改,而是钩子函数中修改。
钩子函数相关限制:
- 禁止编写阻塞、暂停相关的函数,因此钩子函数只能有就绪态和运行态
- 建议钩子函数中的功能执行越快越好,这样可以不影响空闲任务的执行
使用钩子函数需要进行以下操作:
- 打开宏开关
- 修改钩子函数,在钩子函数中实现自己的功能
打开宏开关
在FreeRTOSConfig.h中,置1宏开关。具体操作如下:
修改钩子函数
修改如下:
重点是钩子函数名,功能是任意的,这里只是简单打印0
注意:钩子函数中不能写个while(1)
/* 钩子函数 */
void vApplicationIdleHook( void ){
printf("0");
}
运行结果:
可以看到,相比与实验2,这里多打印了0,证明了钩子函数已经运行。