FreeRTOS概述
FreeRTOS目录结构
主要涉及2个目录:
-
Demo
-
Demo目录下是工程文件,以"芯片和编译器"组合成一个名字
-
比如:CORTEX_STM32F103_Keil
-
-
Source
-
根目录下是核心文件,这些文件是通用的
-
portable目录下是移植时需要实现的文件
-
目录名为:[compiler]/[architecture]
-
比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS工具上的移植文件
-
-
核心文件
FreeRTOS的最核心文件只有2个:
-
FreeRTOS/Source/tasks.c
-
FreeRTOS/Source/list.c
移植时涉及的文件
移植FreeRTOS时涉及的文件放在FreeRTOS/Source/portable/[compiler]/[architecture]
目录下,
比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。
里面有2个文件:
-
port.c
-
portmacro.h
头文件相关
头文件目录
FreeRTOS需要3个头文件目录:
-
FreeRTOS本身的头文件:FreeRTOS/Source/include
-
移植时用到的头文件:FreeRTOS/Source/portable/[compiler]/[architecture]
-
含有配置文件FreeRTOSConfig.h的目录
头文件
列表如下:
头文件 | 作用 |
---|---|
FreeRTOSConfig.h | FreeRTOS的配置文件,比如选择调度算法:configUSE_PREEMPTION 每个demo都必定含有FreeRTOSConfig.h 。建议去修改demo中的FreeRTOSConfig.h,而不是从头写一个 |
FreeRTOS.h | 使用FreeRTOS API函数时,必须包含此文件。 在FreeRTOS.h之后,再去包含其他头文件,比如: task.h、queue.h、semphr.h、event_group.h |
内存管理
文件在FreeRTOS/Source/portable/MemMang
下,它也是放在portable
目录下,表示你可以提供自己的函数。
文件 | 优点 | 缺点 |
---|---|---|
heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
heap_4.c | 相邻空闲内存可合并 | 可解决碎片问题、时间不定 |
heap_5.c | 在heap_4基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
Demo
Demo目录下是预先配置好的、没有编译错误的工程。目的是让你可以基于它进行修改,以适配你的单板。
这些Demo还可以继续精简:
-
Demo/Common
中的文件可以完全删除 -
main函数中只需要保留2个函数:
-
prvSetupHardware()
-
vTaskStartScheduler()
-
数据类型和编程规范
数据类型
每个移植的版本都含有自己的portmacro.h
头文件,里面定义了2个数据类型:
-
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
-
变量名
变量名有前缀:
变量名前缀 | 含义 |
---|---|
c | char |
s | int16_t,short |
l | int32_t,long |
x | BaseType_t, 其他非标准的类型:结构体、task handle、queue handle等 |
u | unsigned |
p | 指针 |
uc | uint8_t,unsigned char |
pc | char指针 |
函数名
函数名的前缀有2部分:返回值类型、在哪个文件定义。
函数名前缀 | 含义 |
---|---|
vTaskPrioritySet | 返回值类型:void 在task.c中定义 |
xQueueReceive | 返回值类型:BaseType_t 在queue.c中定义 |
pvTimerGetTimerID | 返回值类型:pointer to void 在tmer.c中定义 |
宏的名
宏的名字是大小写,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。
宏的前缀 | 含义:在哪个文件里定义 |
---|---|
port (比如portMAX_DELAY) | portable.h或portmacro.h |
task (比如taskENTER_CRITICAL()) | task.h |
pd (比如pdTRUE) | projdefs.h |
config (比如configUSE_PREEMPTION) | FreeRTOSConfig.h |
err (比如errQUEUE_FULL) | projdefs.h |
通用的宏定义如下:
宏 | 值 |
---|---|
pdTRUE | 1 |
pdFALSE | 0 |
pdPASS | 1 |
pdFAIL | 0 |
内存管理
内存的动态管理
task、queue、semaphores和event group等。为了让FreeRTOS更容 易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。
堆,heap,就是一块空闲的内存,需要提供管理函数 malloc:从堆里划出一块空间给程序使用free:用完后,再把它标记为"空闲"的,可以再次使用
栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中, 可以从堆中分配一块空间用作栈
FreeRTOS的五种内存管理方法
FreeRTOS中内存管理的接口函数为:pvPortMalloc 、vPortFree,对应于C库的malloc、free。文件在FreeRTOS/Source/portable/MemMang。
Heap_1
它只实现了pvPortMalloc,没有实现vPortFree。如果程序不需要删除内核对象,那么可以使用heap_1:实现最简单、没有碎片问题、一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1。
Heap_2
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于Heap_2使用最佳匹配算法(best fit)来分配内存,它支持vPortFree。 最佳匹配算法:假设heap有3块空闲内存:5字节、25字节、100字节pvPortMalloc想申请20字节,找出最小的、能满足pvPortMalloc的内存,25字节把它划分为20字节、5字节返回这20字节的地址,剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用。
Heap_3
Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用。C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
Heap_4
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。 首次适应算法:假设堆中有3块空闲内存:5字节、200字节、100字节,pvPortMalloc想申请20字节,找出第1个能满足pvPortMalloc的内存:200字节 把它划分为20字节、180字节,返回这20字节的地址 ,剩下的180字节仍然是空闲状态,留给后续的pvPortMalloc使用,Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Heap_5
Heap_5分配内存、释放内存的算法跟Heap_4是一样的。 相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。 在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。 既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大: 在使用pvPortMalloc之前,必须先指定内存块的信息 使用vPortDefineHeapRegions来指定这些信息 怎么指定一块内存?使用如下结构体:
typedef struct HeapRegion { uint8_t * pucStartAddress; // 起始地址 size_t xSizeInBytes; // 大小 } HeapRegion_t;
怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。 比如:
HeapRegion_t xHeapRegions[] = { { ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000 { ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000 { NULL, 0 } // 表示数组结束 };
vPortDefineHeapRegions函数原型如下:
void vPortDefineHeapRegions( const HeapRegion_t * const xHeapRegions );
把xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。
Heap相关的函数
pvPortMalloc/vPortFree
函数原型:
void * pvPortMalloc( size_t xWantedSize ); void vPortFree( void * pv );
作用:分配内存、释放内存。 如果分配内存不成功,则返回值为NULL。
xPortGetFreeHeapSize
函数原型:
size_t xPortGetFreeHeapSize( void );
作用:返回当前还有多少空闲内存,可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。 注意:在heap_3中无法使用。
xPortGetMinimumEverFreeHeapSize
函数原型:
size_t xPortGetMinimumEverFreeHeapSize( void );
返回:程序运行过程中,空闲内存容量的最小值。 注意:只有heap_4、heap_5支持此函数。
malloc失败的钩子函数
...
任务管理
基本概念
对于整个单片机程序,我们称之为application,应用程序。使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
任务状态(State): 分为running状态 与not running状态,not running状态还可以细分: ready:就绪,随时可以运行 blocked:阻塞,卡住了,母亲在等待同事回信息 suspended:挂起,同事废话太多,不管他了
优先级(Priority):
栈(Stack): 做不同的任务,这些细节不一样,对于人来说,当然是记在脑子里,对于程序,是记在栈里,每个任务有自己的栈。
事件驱动:
协助式调度(Co-operative Scheduling):
任务创建与删除
在FreeRTOS中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是: 任务函数不能返回,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数,函数内部尽量使用局部变量:每个任务都有自己的栈,每个任务运行这个函数时,任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里。 函数使用全局变量、静态变量的话只有一个副本,多个任务使用的是同一个副本。
创建任务
创建任务时使用的函数如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 const char * const pcName, // 任务的名字 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位 为word,1表示4字节 void * const pvParameters, // 调用任务函数时传入的参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用 它来操作这个任务
参数说明:
pvTaskCode:函数指针,可以简单地认为任务就是一个C函数。它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)"
pcName:任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。长度为:configMAX_TASK_NAME_LEN
usStackDepth:每个任务都有自己的栈,这里指定栈大小。单位是word,比如传入100,表示栈大小为100 word,也就是400字节。最大值为uint16_t的最大值。怎么确定栈的大小,并不容易,很多时候是估计。精确的办法是看反汇编码。
pvParameters:调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters)
uxPriority:优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低,如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1)
pxCreatedTask:用来保存xTaskCreate的输出结果:task handle。以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。如果不想使用该handle,可以传入NULL。
返回值:成功:pdPASS;失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存 不足)
创建2个任务,分别使用2个函数示例:
任务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( ;; ) { /* 打印任务2的信息 */ 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; }
创建2个任务,使用同一个函数示例:
任务函数:
void vTaskFunction( void *pvParameters ) { const char *pcTaskText = pvParameters; volatile uint32_t ul; /* volatile用来避免被优化掉 */ /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务的信息 */ printf(pcTaskText); /* 延迟一会(比较简单粗暴) */ for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ ) { } } }
main函数:
static const char *pcTextForTask1 = "T1 run\r\n"; static const char *pcTextForTask2 = "T2 run\r\n"; int main( void ) { prvSetupHardware(); xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL); xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
删除任务
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:
pvTaskCode:任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。
自杀: 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"); // 如果不休眠的话, Idle任务无法得到执行 // Idle任务会清理任务2使用的内存 // 如果不休眠则Idle任务无法执行, 最后内存耗尽 vTaskDelay( xDelay100ms ); } }
任务2函数:
void vTask2( void *pvParameters ) { /* 打印任务的信息 */ printf("Task2 is running and about to delete itself\r\n"); // 可以直接传入参数NULL, 这里只是为了演示函数用法 vTaskDelete(xTask2Handle); }
main函数:
int main( void ) { prvSetupHardware(); xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
任务优先级和Tick
任务优先级
高优先级的任务先运行。优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行。对于相同优先级的、可运行的任务,轮流执行。
Tick
FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。两次中断之间的时间被称为时间片(time slice、tick period).时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就是10ms。,基于Tick实现的延时并不精确。
有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如:
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms // 还可以使用pdMS_TO_TICKS宏把ms转换为tick vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
优先级示例
本程序会创建3个任务: 任务1、任务2:优先级相同,都是1 任务3:优先级最高,是2
任务1、任务2函数:
void vTask1( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务的信息 */ printf("T1\r\n"); } } void vTask2( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务的信息 */ printf("T2\r\n"); } }
任务3函数:
void vTask3( void *pvParameters ) { const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL ); /* 任务函数的主体一般都是无限循环 */ for( ;; ) { /* 打印任务的信息 */ printf("T3\r\n"); // 如果不休眠的话, 其他任务无法得到执行 vTaskDelay( xDelay3000ms ); } }
main函数:
{ prvSetupHardware(); xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
修改优先级示例:
使用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 );
任务1函数:
void vTask1( void *pvParameters ) { UBaseType_t uxPriority; /* Task1,Task2都不会进入阻塞或者暂停状态 *根据优先级决定谁能运行 */ /* 得到Task1自己的优先级 */ uxPriority = uxTaskPriorityGet( NULL ); for( ;; ) { printf( "Task 1 is running\r\n" ); printf("About to raise the Task 2 priority\r\n" ); /* 提升Task2的优先级高于Task1 *Task2会即刻执行 */ vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 )); /* 如果Task1能运行到这里,表示它的优先级比Task2高 *那就表示Task2肯定把自己的优先级降低了 */ } }
任务2函数:
void vTask2( void *pvParameters ) { UBaseType_t uxPriority; /* Task1,Task2都不会进入阻塞或者暂停状态 *根据优先级决定谁能运行 */ /* 得到Task2自己的优先级 */ uxPriority = uxTaskPriorityGet( NULL ); for( ;; ) { /* 能运行到这里表示Task2的优先级高于Task1 *Task1提高了Task2的优先级 */ printf( "Task 2 is running\r\n" ); printf( "About to lower the Task 2 priority\r\n" ); /* 降低Task2自己的优先级,让它小于Task1 *Task1得以运行 */ vTaskPrioritySet( NULL, ( uxPriority - 2 ) ); } }
main函数:
int main( void ) { prvSetupHardware(); /* Task1的优先级更高, Task1先执行 */ xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL ); xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle ); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
任务状态
任务的状态分为两种:运行(Runing)、非运行(Not Running)。
对于非运行的状态,还可以继续细分三种:阻塞状态(blocked)、暂停状态(Suspended)、就绪状态(Ready)。
Delay函数
有两个Delay函数: vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态 vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
函数原型如下:
/* xTicksToDelay: 等待多少给Tick */ void vTaskDelay( const TickType_t xTicksToDelay ); /* *pxPreviousWakeTime: 上一次被唤醒的时间 *xTimeIncrement: 要阻塞到(pxPreviousWakeTime + *xTimeIncrement) */ BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );
空闲任务及其钩子函数
空闲任务(Idle任务)的作用:释放被删除的任务的内存。空闲任务优先级为0,它不能阻碍用户任务运行。空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞。
一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务。
可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些: 执行一些低优先级的、后台的、需要连续执行的函数。 测量系统的空闲时间,空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。 让系统进入省电模式,空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
空闲任务的钩子函数的限制: 不能导致空闲任务进入阻塞状态、暂停状态。 如果使用vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
钩子函数实现方法:
在 FreeRTOS\Source\tasks.c 中,可以看到如下代码,所以前提就是:
1、把这个宏定义为1:confifigUSE_IDLE_HOOK
2、实现 vApplicationIdleHook 函数
调度算法
基本概念
正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理系统中,任何时间里只能有一个任务处于运行状态。非运行状态的任务,它处于这3中状态之一:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。
事件分为两类:时间相关的事件、同步事件。所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"?方法很多,有:任务通知(task notifification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。这些方法用来发送同步信息,比如表示某个外设得到了数据。
配置调度算法
调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态的算法。
高优先级的任务能否优先执行:
confifigUSE_PREEMPTION=1,可以:可抢占调度(Pre-emptive),高优先级的就绪任务马上执行。
confifigUSE_PREEMPTION=0,不可以:合作调度模式(Co-operative Scheduling),只能协商。
可抢占的前提下,同优先级的任务是否轮流执行:
confifigUSE_TIME_SLICING=1,轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行。
confifigUSE_TIME_SLICING=0,不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占。
可抢占的前提下,空闲任务是否让步于用户任务:
confifigIDLE_SHOULD_YIELD=1,每执行一次循环,就看看是否主动让位给用户任务。
confifigIDLE_SHOULD_YIELD=0,空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊。
调度示例
代码里创建了3个任务:Task1、Task2的优先级都是0,跟空闲任务一样,Task3优先级最高为2。程序里定义了4个全局变量,当某个的任务执行时,对应的变量就被设为1。
任务1、任务2函数:
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"); } }
任务3函数:
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 ); } }
main函数:
int main( void ) { prvSetupHardware(); xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL); xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
在 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 /**********************************************************/ // 实验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 /**********************************************************/ // 实验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
同步、互斥与通信
同步与互斥的概念
在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。
实现方法
任务通知(task notification)
队列(queue)
事件组(event group)
信号量(semaphoe)
互斥量((mutex)
队列
队列的特性
队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)。每个数据大小固定创建队列时就要指定长度、数据大小。数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读,也可以强制写队列头部:覆盖头部数据,使用队列传输数据时有两种方法:
拷贝:把数据、把变量的值复制进队列里
引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法,这更简单。
队列的阻塞访问
只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可以多个任务读写队列。
某个任务读/写队列时,如果队列没有数据/数据已满,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了/有空间了,则该阻塞的任务会变为就绪态。如果一直都没有数据/空间,则时间到之后它也会进入就绪态。
当队列可读/写时,优先级最高的任务进入就绪态,如果大家的优先级相同,那等待时间最久的任务会进入就绪态。
队列函数
使用队列的流程:创建队列、写队列、读队列、删除队列。
创建
队列的创建有两种方法,动态分配内存与静态分配内存。
动态分配内存:xQueueCreate,队列的内存在函数内部动态分配,函数原型如下:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数说明 uxQueueLength: 队列长度,最多能存放多少个数据(item)。 uxItemSize: 每个数据(item)的大小,以字节为单位。 返回值:非0成功,返回句柄,以后使用句柄来操作队列,NULL失败,因为内存不足。
静态分配内存:xQueueCreateStatic,队列的内存要事先分配好,函数原型如下:
QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength, UBaseType_t uxItemSize, uint8_t *pucQueueStorageBuffer, StaticQueue_t *pxQueueBuffer );
参数说明
uxQueueLength: 队列长度,最多能存放多少个数据(item)。 uxItemSize: 每个数据(item)的大小:以字节为单位。 pucQueueStorageBuffer:如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,此数组大小至少为"uxQueueLength * uxItemSize"。 pxQueueBuffer: 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 返回值: 非0成功,返回句柄,以后使用句柄来操作队列,NULL失败,因为pxQueueBuffer为NULL。
// 静态内存分配示例代码 #define QUEUE_LENGTH 10 #define ITEM_SIZE sizeof( uint32_t ) // xQueueBuffer用来保存队列结构体 StaticQueue_t xQueueBuffer; // ucQueueStorage 用来保存队列的数据 // 大小为:队列长度 * 数据大小 uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ]; void vATask( void *pvParameters ) { QueueHandle_t xQueue1; // 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小ITEM_SIZE xQueue1 = xQueueCreateStatic( QUEUE_LENGTH, ITEM_SIZE, ucQueueStorage, &xQueueBuffer ); }
复位
队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态,此函数原型为:
/* pxQueue : 复位哪个队列;返回值: pdPASS(必定成功)*/ BaseType_t xQueueReset( QueueHandle_t pxQueue);
删除
删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:
void vQueueDelete( QueueHandle_t xQueue );
写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于xQueueSendToBack * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue,TickType_t xTicksToWait ); /* * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ); /* * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞 */ BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken ); /* * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ); /* * 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞 */ BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
参数说明 xQueue: 队列句柄,要写哪个队列。
pvItemToQueue:数据指针,这个数据的值会被复制进队列,复制多大的数据?在创建队列时已经指定了数据大小。
xTicksToWait:如果队列满则无法写入新数据,可以让任务进入阻塞状态,
xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法写入数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写。 返回值: pdPASS,数据成功写入了队列。errQUEUE_FULL,写入失败,因为队列满了。
读队列
使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ); BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxTaskWoken );
参数说明
xQueue :队列句柄,要读哪个队列。
pvBuffffer :bufer指针,队列的数据会被复制到这个buffffer,复制多大的数据?在创建队列时已经指定了数据大小。
xTicksToWait:果队列空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写。
返回值 :pdPASS,从队列读出数据入;errQUEUE_EMPTY,读取失败,因为队列空了。
查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
/* * 返回队列中可用数据的个数 */ UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue ); /* * 返回队列中可用空间的个数 */ UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
覆盖/偷看
当队列长度为1时,可以使用 xQueueOverwrite() 或 xQueueOverwriteFromISR() 来覆盖数据。注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也意味着这些函数不会被阻塞。函数原型如下:
/* 覆盖队列 * xQueue: 写哪个队列 * pvItemToQueue: 数据地址 * 返回值: pdTRUE表示成功, pdFALSE表示失败 */ BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue ); BaseType_t xQueueOverwriteFromISR( QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t*pxHigherPriorityTaskWoken );
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是 xQueuePeek() 或 xQueuePeekFromISR() 。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。函数原型如下:
/* 偷看队列 * xQueue: 偷看哪个队列 * pvItemToQueue: 数据地址, 用来保存复制出来的数据 * xTicksToWait: 没有数据的话阻塞一会 * 返回值: pdTRUE表示成功, pdFALSE表示失败 */ BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ); BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer, );
分辨数据源
当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信息,比如写入队列的数据是一个结构体,结构体中lDataSouceID用来表示数据来源:
typedef struct { ID_t eDataID; int32_t lDataValue; }Data_t;
例如:
CAN任务发送的数据:eDataID=eMotorSpeed,lDataValue=10
HMI任务发送的数据:eDataID=eSpeedSetPoint,lDataValue=5
传输大块数据
这时候,我们要传输的是这个巨大结构体的地址:把它的地址写入队列,对方从队列得到这个地址,使用地址去访问那1000字节的数据。使用地址来间接传输数据时,这些数据放在RAM里。
邮箱
FreeRTOS的邮箱概念跟别的RTOS不一样,这里的邮箱称为"橱窗"也许更恰当。
它是一个队列,队列长度只有1,写邮箱:新数据覆盖旧数据,在任务中使用 xQueueOverwrite() ,在中断中使用xQueueOverwriteFromISR() 。既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。读邮箱:读数据时,数据不会被移除;在任务中使用 xQueuePeek() ,在中断中使xQueuePeekFromISR() 。这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。
信号量
当只需要传递状态,并不需要传递具体的信息时使用信号量。只有计数值,无法容纳其他数据。创建信号量时,只需要分配信号量结构体。
信号量的特性
信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。
计数型信号量:计数:事件产生时"give"信号量,让计数值加1;处理事件时要先"take"信号量,就是获得信号量,让计数值减1。资源管理:要想访问资源需要先"take"信号量,让计数值减1;用完资源后"give"信号量,让计数值加1。
信号量函数
创建
/* 创建一个二进制信号量,返回它的句柄。此函数内部会分配信号量结构体,返回值: 返回句柄,非NULL表示成功*/ SemaphoreHandle_t xSemaphoreCreateBinary( void ); /* 创建一个二进制信号量,返回它的句柄。此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针.返回值: 返回句柄,非NULL表示成功*/ SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer ); /**********************************************************/ /* 创建一个计数型信号量,返回它的句柄。此函数内部会分配信号量结构。uxMaxCount: 最大计数值。uxInitialCount: 初始计数值。返回值: 返回句柄,非NULL表示成功*/ SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount ); /* 创建一个计数型信号量,返回它的句柄。此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针,uxMaxCount: 最大计数值,uxInitialCount: 初始计数值*/ SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t *pxSemaphoreBuffer );
删除
/*xSemaphore: 要删除的信号量句柄*/ void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
give/take
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken ); BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait ); BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
防止数据丢失
需要使用其他方法来防止数据丢失,比如:
在串口中断中把数据放入缓冲区或在任务中,一次性把缓冲区中的数据都读出。简单地说,就是:你提醒了我多次,我太忙只响应你一次,但是我一次性拿走所有数据。
互斥量
在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。
上述问题的解决方法是互斥量也被称为互斥锁:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。
互斥量函数
创建
/* 创建一个互斥量,返回它的句柄。此函数内部会分配互斥量结构体。返回值: 返回句柄,非NULL表示成功*/ SemaphoreHandle_t xSemaphoreCreateMutex( void ); /* 创建一个互斥量,返回它的句柄。此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针。返回值: 返回句柄,非NULL表示成功*/ SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );
要想使用互斥量,需要在配置文件FreeRTOSConfifig.h中定义:
##define configUSE_MUTEXES 1
其它
要注意的是,互斥量不能在ISR中使用。各类操作函数,比如删除、give/take,跟一般是信号量是一样的。
/* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量*/ void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); /* 释放 */ BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 释放(ISR版本) */ BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken ); /* 获得 */ BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait ); /* 获得(ISR版本) */ xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
优先级继承
假设任务A、B都想使用串口,A优先级比较低:任务A获得了串口的互斥量,任务B也想使用串口,它将会阻塞、等待A释放互斥量高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority inversion)
优先级继承:假设持有互斥锁的是任务A,如果更高优先级的任务B也尝试获得这个锁,任务B说:你既然持有宝剑,又不给我,那就继承我的愿望吧,于是任务A就继承了任务B的优先级,这就叫优先级继承。等任务A释放互斥锁时,它就恢复为原来的优先级,互斥锁内部就实现了优先级的提升、恢复。
递归锁
互锁
假设有2个互斥量M1、M2,2个任务A、B,A获得了互斥量M1,B获得了互斥量M2,A还要获得互斥量M2才能运行,结果A阻塞,B还要获得互斥量M1才能运行,结果B阻塞,A、B都阻塞,再无法释放它们持有的互斥量,死锁发生!
自锁
任务A获得了互斥锁M,它调用一个库函数,库函数要去获取同一个互斥锁M,于是它阻塞,任务A休眠,等待任务A来释放互斥锁,死锁发生!
递归锁
使用递归锁解决互锁和自锁问题,任务A获得递归锁M后,它还可以多次去获得这个锁,"take"了N次,要"give"N次,这个锁才会被释放。递归锁实现了:谁上锁就由谁解锁。函数原型如下:
/* 创建一个递归锁,返回它的句柄。此函数内部会分配互斥量结构体,返回值: 返回句柄,非NULL表示成功*/ SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void ); /* 释放 */ BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore ); /* 获得 */ BaseType_t xSemaphoreTakeRecursive( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
事件组
事件组概念
事件组可以简单认为是一个整数,用一个整数来表示,高8位留给内核使用,其它的每一位表示一个事件,值为1表示事件发生,值为0表示事件没有发生。它可以被任务和ISR读写。
如果confifigUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件
如果confifigUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件
事件组操作
事件发生时,会唤醒所有符合条件的任务,具有广播作用,被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件。
设置事件
有一个或多个任务在等待事件,如果这些事件符合这些任务的期望,那么任务还会被唤醒。函数原型如下:
/* 设置事件组中的位,xEventGroup: 哪个事件组,uxBitsToSet: 设置哪些位?如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1,可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0,返回值: 返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)*/ EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet ); /* 设置事件组中的位,xEventGroup: 哪个事件组,uxBitsToSet: 设置哪些位?,如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1,可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0,pxHigherPriorityTaskWoken: 有没有导致更高优先级的任务进入就绪态? pdTRUE-有,pdFALSE-没有,返回值: pdPASS-成功, pdFALSE-失败*/ BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t * pxHigherPriorityTaskWoken );
等待事件
可以等待某一位、某些位中的任意一个,也可以等待多位;等到期望的事件后,还可以清除某些位。函数原型如下:
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait );
参数说明 xEventGroup::等待哪个事件组?
uxBitsToWaitFor:等待哪些位?哪些位要被测试?
xWaitForAllBits:怎么测试?是"AND"还是"OR"?
pdTRUE::等待的位,全部为1;
pdFALSE::等待的位,某一个为1即可
xClearOnExit:函数提出前是否要清除事件?
pdTRUE:清除uxBitsToWaitFor指定的位
pdFALSE: 不清除
xTicksToWait:如果期待的事件未发生,阻塞多久。可以设置为0:判断后即刻返回;可设置为portMAX_DELAY:一定等到成功才返回;可以设置为期望的Tick Count,一般用 pdMS_TO_TICKS() 把ms转换为Tick Count。
返回值:返回的是事件值,如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值;如果是超时退出,返回的是超时时刻的事件值。
同步点
使用 xEventGroupSync() 函数可以同步多个任务:
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait );
参数说明
xEventGroup: 哪个事件组?
uxBitsToSet:要设置哪些事件?我完成了哪些事件?比如0x05(二进制为0101)会导致事件组的bit0,bit2被设置为1
uxBitsToWaitFor:等待那个位、哪些位?比如0x15(二级制10101),表示要等待bit0,bit2,bit4都为1
xTicksToWait:如果期待的事件未发生,阻塞多久。可以设置为0:判断后即刻返回;可设置为portMAX_DELAY:一定等到成功才返回;可以设置为期望的Tick Count,一般用 pdMS_TO_TICKS() 把ms转换为Tick Count
返回值:返回的是事件值,如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值;如果是超时退出,返回的是超时时刻的事件值。
任务通知
任务通知特性
效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。但是不能发送数据给ISR,数据只能给该任务独享,无法缓冲数据,无法广播给多个任务,如果发送受阻,发送方无法进入阻塞状态等待。
通知状态和通知值
每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:
一个是uint8_t类型,用来表示通知状态
一个是uint32_t类型,用来表示通知值
typedef struct tskTaskControlBlock { ...... /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */ volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; ...... } tskTCB;
任务通知函数
函数原型如下:
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken ); uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
软件定时器
软件定时器就是"闹钟",软件定时器可以完成两类事情,在"未来"某个时间点,运行函数和周期性地运行函数。
守护任务
当FreeRTOS的配置项 configUSE_TIMERS 被设置为1时,在启动调度器时,会自动创建RTOSDamemon Task。
我们自己编写的任务函数要使用定时器时,是通过"定时器命令队列"(timer command queue)和守护任务交互。
回调函数
定时器的回调函数的原型如下:
void ATimerCallback( TimerHandle_t xTimer );
软件定时器的函数
创建
要使用定时器,需要先创建它,得到它的句柄。有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:
/* 使用动态分配内存的方法创建定时器,pcTimerName:定时器名字, 用处不大, 尽在调试时用到xTimerPeriodInTicks: 周期, 以Tick为单位 \* uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性 \* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器 \* pxCallbackFunction: 回调函数 \* 返回值: 成功则返回TimerHandle_t, 否则返回NULL */ TimerHandle_t xTimerCreate( const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction ); /* 使用静态分配内存的方法创建定时器 * pcTimerName:定时器名字, 用处不大, 尽在调试时用到 * xTimerPeriodInTicks: 周期, 以Tick为单位 * uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性 * pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器 * pxCallbackFunction: 回调函数 * pxTimerBuffer: 传入一个StaticTimer_t结构体, 将在上面构造定时器 * 返回值: 成功则返回TimerHandle_t, 否则返回NULL */ TimerHandle_t xTimerCreateStatic( const char * const pcTimerName, TickType_t xTimerPeriodInTicks, UBaseType_t uxAutoReload, void * pvTimerID, TimerCallbackFunction_t pxCallbackFunction, StaticTimer_t *pxTimerBuffer );
删除
/* 删除定时器 \* xTimer: 要删除哪个定时器 \* xTicksToWait: 超时时间 \* 返回值: pdFAIL表示"删除命令"在xTicksToWait个Tick内无法写入队列 \* pdPASS表示成功 */ BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。涉及的函数原型如下:
/* 启动定时器 \* xTimer: 哪个定时器 \* xTicksToWait: 超时时间 \* 返回值: pdFAIL表示"启动命令"在xTicksToWait个Tick内无法写入队列 \* pdPASS表示成功 */ BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait ); /* 启动定时器(ISR版本) * xTimer: 哪个定时器 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, * 如果守护任务的优先级比当前任务的高, * 则"*pxHigherPriorityTaskWoken = pdTRUE", * 表示需要进行任务调度 * 返回值: pdFAIL表示"启动命令"无法写入队列 * pdPASS表示成功 */ BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken ); /* 停止定时器 * xTimer: 哪个定时器 * xTicksToWait: 超时时间 * 返回值: pdFAIL表示"停止命令"在xTicksToWait个Tick内无法写入队列 * pdPASS表示成功 */ BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait ); /* 停止定时器(ISR版本) * xTimer: 哪个定时器 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, * 如果守护任务的优先级比当前任务的高, * 则"*pxHigherPriorityTaskWoken = pdTRUE", * 表示需要进行任务调度 * 返回值: pdFAIL表示"停止命令"无法写入队列 * pdPASS表示成功 */ BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
复位
从定时器的状态转换图可以知道,使用 xTimerReset() 函数可以让定时器的状态从冬眠态转换为运行态,相当于使用 xTimerStart() 函数。如果定时器已经处于运行态,使用 xTimerReset() 函数就相当于重新确定超时时间。假设调用xTimerReset() 的时刻是tX,定时器的周期是n,那么 tX+n 就是重新确定的超时时间。
复位函数的原型如下:
/* 复位定时器 * xTimer: 哪个定时器 * xTicksToWait: 超时时间 * 返回值: pdFAIL表示"复位命令"在xTicksToWait个Tick内无法写入队列 * pdPASS表示成功 */ BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait ); /* 复位定时器(ISR版本) * xTimer: 哪个定时器 * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, * 如果守护任务的优先级比当前任务的高, * 则"*pxHigherPriorityTaskWoken = pdTRUE", * 表示需要进行任务调度 * 返回值: pdFAIL表示"停止命令"无法写入队列 * pdPASS表示成功 */ BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
修改周期
从定时器的状态转换图可以知道,使用 xTimerChangePeriod() 函数,处理能修改它的周期外,还可以让定时器的状态从冬眠态转换为运行态。修改定时器的周期时,会使用新的周期重新计算它的超时时间。假设调用 xTimerChangePeriod() 函数的时间tX,新的周期是n,则 tX+n 就是新的超时时间。相关函数的原型如下:
/* 修改定时器的周期 \* xTimer: 哪个定时器 \* xNewPeriod: 新周期 \* xTicksToWait: 超时时间, 命令写入队列的超时时间 \* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列 \* pdPASS表示成功 */ BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, TickType_t xNewPeriod, TickType_t xTicksToWait ); /* 修改定时器的周期 \* xTimer: 哪个定时器 \* xNewPeriod: 新周期 \* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, \* 如果守护任务的优先级比当前任务的高, \* 则"*pxHigherPriorityTaskWoken = pdTRUE", \* 表示需要进行任务调度 \* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列 \* pdPASS表示成功 */ BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, TickType_t xNewPeriod, BaseType_t *pxHigherPriorityTaskWoken )
定时器ID
函数原型如下:
/* 获得定时器的ID \* xTimer: 哪个定时器 \* 返回值: 定时器的ID */ void *pvTimerGetTimerID( TimerHandle_t xTimer ); /* 设置定时器的ID \* xTimer: 哪个定时器 \* pvNewID: 新ID \* 返回值: 无 */ void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );
中断管理
中断处理概念
在RTOS中,需要应对各类事件。这些事件很多时候是通过硬件中断产生,怎么处理中断呢?假设当前系统正在运行Task1时,用户按下了按键,触发了按键中断。这个中断的处理流程如下:CPU跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转时硬件实现的。执行代码做什么?
保存现场:Task1被打断,需要先保存Task1的运行环境,比如各类寄存器的值
分辨中断、调用处理函数(这个函数就被称为ISR,interrupt service routine):ISR的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。
恢复现场:继续运行Task1,或者运行其他优先级更高的任务
中断的延迟处理
t1:任务1运行,任务2阻塞
t2:发生中断,该中断的ISR函数被执行,任务1被打断,ISR函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤醒任务2
t3:在创建任务时设置任务2的优先级比任务1高(这取决于设计者),所以ISR返回后,运行的是任务2,它要完成中断的处理。任务2就被称为"deferred processing task",中断的延迟处理任务。
t4:任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行
中断与任务间的通信
前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。要注意的是,在ISR中使用的函数要有"FromISR"后缀。
资源管理
屏蔽中断
/*在任务中屏蔽中断的示例代码 在任务中,当前时刻中断是使能的 执行这句代码后,屏蔽中断 */ taskENTER_CRITICAL(); /* 访问临界资源 */ /* 重新使能中断 */ taskEXIT_CRITICAL(); /*在ISR中屏蔽中断*/ void vAnInterruptServiceRoutine( void ) { /* 用来记录当前中断是否使能 */ UBaseType_t uxSavedInterruptStatus; /* 在ISR中,当前时刻中断可能是使能的,也可能是禁止的 * 所以要记录当前状态, 后面要恢复为原先的状态 * 执行这句代码后,屏蔽中断 */ uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR(); /* 访问临界资源 */ /* 恢复中断状态 */ taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus ); /* 现在,当前ISR可以被更高优先级的中断打断了 */ }
暂停调度器
/* 暂停调度器 */ void vTaskSuspendAll( void ); /* 恢复调度器 \* 返回值: pdTRUE表示在暂定期间有更高优先级的任务就绪了 \* 可以不理会这个返回值 */ BaseType_t xTaskResumeAll( void );
评论(1)
您还未登录,请登录后发表或查看评论