一、为啥要用实时多任务操作系统
real-time Operate System 简称有:RTOS,有如下的好处:
用户无需关心时间信息
内核负责计时,并由相关的API完成,从而使得用户的应用程序代码结构更简单。
模块化、可拓展性强
也正是由于第一点的原因,程序性能不易受底层硬件更改的影响。姐,各个任务是独立的模块,每个模块都有明确的目
的,降低了代码的耦合性。
效率高
内核可以让软件完全由事件驱动,因次,轮询未发生的事件是不浪费时间的。相当于用中断来进行任务切换。
中断进程更短
通过把中断的处理推迟到用户创建的任务中,可以使得中断处理程序非常短。
二、核心的C文件和头文件
C文件
头文件
三、两个数据类型和变量的定义方法
TickType_t
FreeRTOS配置了一个周期性的时钟中断:Tick Interrupt
每发生一次中断,中断次数累加,这被称为tick count
tick count这个变量的类型就是TickType_t
TickType_t可以是16位的,也可以是32位的
FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t就是uint16_t
否则TickType_t就是uint32_t
对于32位架构,建议把TickType_t配置为uint32_t
BaseType_t
这是该架构最高效的数据类型
32位架构中,它就是uint32_t
16位架构中,它就是uint16_t
8位架构中,它就是uint8_t
BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE
变量名
每个变量的前缀表示的含义
函数名
函数名前缀有有2部分:返回值类型、在哪个文件定义。
宏的名
宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义
通用的宏定义如下:
四、内存管理中的几个API
提及内存管理就必须说一下堆和栈
堆,heap,就是一块空闲的内存,需要提供管理函数
- malloc:从堆里划出一块空间给程序使用
- free:用完后,再把它标记为"空闲"的,可以再次使用
- 栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
- 可以从堆中分配一块空间用作栈
- 在FreeRTOS中内存管理的接口函数(API)为:
1、pvPortMalloc 、vPortFree,对应于C库的malloc、free。
void * pvPortMalloc( size_t xWantedSize ); //分配内存,如果分配内存不成功,则返回值为NULL。 void vPortFree( void * pv );//释放内存
2、当前还有多少空闲内存,这函数可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。
size_t xPortGetFreeHeapSize( void );
3、空闲内存的最小值
size_t xPortGetMinimumEverFreeHeapSize( void );//程序运行过程中,空闲内存容量的最小值。
五、创建任务和删除任务
啥叫任务?
任务就一个函数,但要注意的是
示例
void ATaskFunction( void *pvParameters ) { /* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */ int32_t lVariableExample = 0; /* 任务函数通常实现为一个无限循环 */ for( ;; ) { /* 任务的代码 */ } /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己 * NULL表示删除的是自己 */ vTaskDelete( NULL ); /* 程序不会执行到这里, 如果执行到这里就出错了 */ }
创建任务API
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 const char * const pcName, // 任务的名字 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位 为word,10表示40字节 void * const pvParameters, // 调用任务函数时传入的参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用 它来操作这个任务
里面的参数说明如下:
示例:
任务1代码:
void vTask1( void *pvParameters ) { const char *pcTaskName = "T1 run\r\n"; volatile uint32_t ul; /* volatile用来避免被优化掉 */ /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务1的信息 */ printf( pcTaskName ); /* 延迟一会(比较简单粗暴) */ for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ ) { } } }
任务2代码:
void vTask2( void *pvParameters ) { const char *pcTaskName = "T2 run\r\n"; volatile uint32_t ul; /* volatile用来避免被优化掉 */ /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务1的信息 */ printf( pcTaskName ); /* 延迟一会(比较简单粗暴) */ for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ ) { } } }
main函数:
int main( void ) { prvSetupHardware(); xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
运行结果:
解释:
main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。
任务2的优先级最高,它马上执行。
任务2打印一句话后,就删除了自己。
任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用
vTaskDelay() 进入Block状态
任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)
时间到后,任务1变为最高优先级的任务继续执行。
如此循环。
六、任务优先级和Tick
任务优先级
高优先级的任务先运行。
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
使用uxTaskPriorityGet来获得任务的优先级,
使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用vTaskPrioritySet 来设置任务的优先级,
使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;
参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority );
Tick
对于相同优先级的任务的话,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。那么这个一会就是使用Tick定义的
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms // 还可以使用pdMS_TO_TICKS宏把ms转换为tick vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
七、任务的几种状态
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
任务转换图
七、两个Delay函数
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给 Tick */ /* pxPreviousWakeTime: 上一次被唤醒的时间 * xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement) * 单位都是Tick Count */ BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );
详细介绍:
使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断
1、退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会
2、所以可以使用xTaskDelayUntil来让任务周期性地运行
示例:
本程序会创建2个任务:
Task1:
1、高优先级
2、设置变量flag为1,然后调用 vTaskDelay(xDelay50ms); 或vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
Task2:
1、低优先级
2、设置变量flag为0
main函数
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; }
Task1的代码中使用条件开关来选择Delay函数,把 #if 1 改为 #if 0 就可以使用 vTaskDelayUntil
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 } }
Task2的代码
void vTask2( void *pvParameters ) { for( ;; ) { flag = 0; printf( "Task 2 is running\r\n" ); } }
使用Keil的逻辑分析观察flag变量的bit波形,如下:
- flag为1时表示Task1在运行,flag为0时表示Task2在运行,也就是Task1处于阻塞状态
- vTaskDelay:指定的是阻塞的时间
- vTaskDelayUntil:指定的是任务执行的间隔、周期
- 八、空闲任务和钩子函数
空闲任务
为什么要有空闲函数呢?
因为一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。
在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:
空闲任务优先级为0:它不能阻碍用户任务运行
空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这意为着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用 vTaskDelete() 来删除任务,就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
钩子函数
钩子函数在空闲任务中添加,空闲任务每执行一次,钩子函数就会被调用一次,那么钩子函数能干些什么事呢?
执行一些低优先级的、后台的、需要连续执行的函数
测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式 了。
钩子函数使用过程中应该注意:
不能导致空闲任务进入阻塞状态、暂停状态
如果你会使用 vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植 卡在钩子函数里的话,它就无法释放内存。
如果想使用钩子函数
在FreeRTOSConfig.h中,把configUSE_MALLOC_FAILED_HOOK定义为1
提供vApplicationMallocFailedHook函数
pvPortMalloc失败时,才会调用此函数
因为只能一个任务处在运行状态中,所以需要调度来实现不同任务进入运行状态。
调度算法的行为主要体现为:
- 高优先级的任务先运行
- 同优先级的就绪态任务如何被选中
从3个角度统一理解多种调度算法:
上表解释:
A:可抢占+时间片轮转+空闲任务让步
B:可抢占+时间片轮转+空闲任务不让步
C:可抢占+非时间片轮转+空闲任务让步
D:可抢占+非时间片轮转+空闲任务不让步
E:合作调度
示例:
注:
任务1优先级0
任务2优先级0
任务3优先级2(最高优先级)
是否抢占对比
在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
// 实验1:抢占 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configIDLE_SHOULD_YIELD 1 // 实验2:不抢占 #define configUSE_PREEMPTION 0 #define configUSE_TIME_SLICING 1 #define configIDLE_SHOULD_YIELD 1 ———————————————— 版权声明:本文为CSDN博主「小浩编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_48216397/article/details/125024729
- 抢占时:高优先级任务就绪时,就可以马上执行
- 不抢占时:优先级失去意义了,既然不能抢占就只能协商了,图中任务1一直在运行(一点都没有协商精神),其他任务都无法执行。即使任务3的 vTaskDelay 已经超时、即使它的优先级更高,都没办法执行。
- 是否时间片轮转对比
在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
// 实验1:时间片轮转 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configIDLE_SHOULD_YIELD 1 // 实验2:时间片不轮转 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 0 #define configIDLE_SHOULD_YIELD 1
- 时间片轮转:在Tick中断中会引起任务切换
- 时间片不轮转:高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会
- 引起任务切换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换, 可以看到任务1、任务2都运行了很长时间。
空闲任务是否让步对比
在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
// 实验1:空闲任务让步 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configIDLE_SHOULD_YIELD 1 // 实验2:空闲任务不让步 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configIDLE_SHOULD_YIELD 0
- 让步时:在空闲任务的每个循环中,会主动让出处理器,从图中可以看到flagIdelTaskrun的波形很小
- 不让步时:空闲任务跟任务1、任务2同等待遇,它们的波形宽度是差不多的
十、同步互斥与通信
关于RTOS的其他内容后续更新,已是凌晨1点,先睡会