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 );