一、为啥要用实时多任务操作系统
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 );
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 );
}
创建任务API
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
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;
for( ;; )
{
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;
for( ;; )
{
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;
}
运行结果: 注:
- task 2先运行!
- 要分析xTaskCreate的代码才能知道原因:更高优先级的、或者后面创建的任务先运行。
删除任务API
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明 举个栗子
- 自杀: vTaskDelete(NULL)
- 被杀:别的任务执行 vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄
- 杀人:执行 vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄
上示例: 大体的意思是:
- 创建任务1:任务1的大循环里,创建任务2,然后休眠一段时间
- 任务2:打印一句话,然后就删除自己
任务1代码:
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
BaseType_t ret;
for( ;; )
{
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)
printf("Create Task2 Failed\r\n");
}
}
任务2代码
void vTask2( void *pvParameters )
{
printf("Task2 is running and about to delete itself\r\n");
vTaskDelete(xTask2Handle);
}
main函数
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 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);
vTaskDelay(pdMS_TO_TICKS(100));
七、任务的几种状态
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
任务转换图
七、两个Delay函数
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态
void vTaskDelay( const TickType_t xTicksToDelay );
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();
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;
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 中,定义这样的宏,对比逻辑分析仪的效果:
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
#define configUSE_PREEMPTION 0
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
- 抢占时:高优先级任务就绪时,就可以马上执行
- 不抢占时:优先级失去意义了,既然不能抢占就只能协商了,图中任务1一直在运行(一点都没有协商精神),其他任务都无法执行。即使任务3的 vTaskDelay 已经超时、即使它的优先级更高,都没办法执行。
是否时间片轮转对比 在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 0
#define configIDLE_SHOULD_YIELD 1
- 时间片轮转:在Tick中断中会引起任务切换
- 时间片不轮转:高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换, 可以看到任务1、任务2都运行了很长时间。
空闲任务是否让步对比 在 FreeRTOSConfig.h 中,定义这样的宏,对比逻辑分析仪的效果:
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 0
- 让步时:在空闲任务的每个循环中,会主动让出处理器,从图中可以看到flagIdelTaskrun的波形很小
- 不让步时:空闲任务跟任务1、任务2同等待遇,它们的波形宽度是差不多的
十、同步互斥与通信
关于RTOS的其他内容后续更新,已是凌晨1点,先睡会
|