【freeRTOS】学习记录

freeRTOS总结

	因工作需要,自学实时系统,此文章用于记录学习嵌入式实时系统过程中的相关知识点,才疏学浅,难免会有差错,请指正。

一、术语介绍

1、软实时和硬实时
软实时:桌面电脑的输入处理可以归类为”软实时”。为了保证用户的最佳体验,计算机对每个输入的响应应当限定在一个恰当的时间范围——但是如果响应时间超出了限定范围,并不会让人觉得这台电脑无法使用。比如说,键盘操作必须在键按下后的某个时间内作出明显的提示。但如果按键提示超出了这个时间,会使得这个系统看起来响应太慢,而不致于说这台电脑不能使用。
硬实时:硬实时功能必须在给定的时间限制之内完成——如果无法做到即意味着整个系统的绝对失败。汽车的安全气囊触发机制就是一个硬实时功能的例子。安全气囊在撞击发生后给定时间限制内必须弹出。如果响应时间超出了这个时间限制,会使得驾驶员受到伤害,而这原本是可以避免的。
2、任务:嵌入式系统中每个执行的线程被称为任务。
3、任务切入/切出:任务从非运行态切换为运行态称为任务切入。任务从运行态切换到非运行态称为切出。

二、任务管理

任务函数

任务是由C语言函数实现,返回类型为void,形参为void类型的指针,原型如下所示

void ATaskFunction( void *pvParameters );

每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一
个死循环中,也不会退出。如果一个任务不再需要,可以显式地将其删除。

void ATaskFunction( void *pvParameters )
{
/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变
量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务实
例将会共享这个变量。 */
int iVariableExample = 0;
/* 任务通常实现在一个死循环中。 */
for( ;; )
{
/* 完成任务功能的代码将放在这里。 */
}
/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除
的是当前任务 */
vTaskDelete( NULL );
}

顶层任务状态

任意给定时间,实际上只会有一个任务被执行。这就意味着一个任务可以有一个或两个状态,即运行状态和非运行状态。任务处于非运行态时,该任务进行休眠,它的所有状态都被妥善保存,以便在下一次调试器决定让它进入运行态时可以恢复执行。当任务恢复执行时,其将精确地从离开运行态时正准备执行的那一条指令开始执行。
顶层任务状态示意图

任务创建

任务创建函数由C语言实现,函数原型、返回类型及形参列表如下所示:

//任务创建函数原型
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask );

形参描述:
pvTaskCode 任务是永不退出的 C 函数,实现通常是一个死循环。参数pvTaskCode 只一个指向任务的实现函数的指针。
pcName 具有描述性的任务名。这个参数不会被 FreeRTOS 使用。其只是单纯地用于辅助调试。识别一个具有可读性的名字总是比通过句柄来识别容易得多。应用程序可以通过定义常量 config_MAX_TASK_NAME_LEN 来定义任务名的最大长度——包括’\0’结束符。如果传入的字符串长度超过了这个最大值,字符串将会自动被截断。
usStackDepth 当任务创建时,内核会分为每个任务分配属于任务自己的唯一状态。usStackDepth 值用于告诉内核为它分配多大的栈空间。这个值指定的是栈空间可以保存多少个字(word),而不是多少个字节(byte)。比如说,如果是 32 位宽的栈空间,传入的 usStackDepth值为 100,则将会分配 400 字节的栈空间(100 * 4bytes)。栈深度乘以栈宽度的结果千万不能超过一个 size_t 类型变量所能表达的最大值。应用程序通过定义常量 configMINIMAL_STACK_SIZE 来决定空闲任务任用的栈空间大小。
pvParameters 任务函数接受一个指向 void 的指针(void*)。 pvParameters 的值即是传递到任务中的值。
uxPriority 指定任务执行的优先级。优先级的取值范围可以从最低优先级 0 到最高优先级(configMAX_PRIORITIES – 1)。configMAX_PRIORITIES 是一个由用户定义的常量。优先级号并没有上限(除了受限于采用的数据类型和系统的有效内存空间),但最好使用实际需要的最小数值以避免内存浪费。
pxCreatedTask pxCreatedTask 用于传出任务的句柄。这个句柄将在 API 调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。如果应用程序中不会用到这个任务的句柄,则 pxCreatedTask 可以被设为 NULL。
返回值 有两个可能的返回值:

  1. pdTRUE
    表明任务创建成功。
  2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
    由于内存堆空间不足, FreeRTOS 无法分配足够的空间来保存任务
    结构数据和任务栈,因此无法创建任务。

创建任务示例

//任务一定义
void vTask1( void *pvParameters )
{
	const char *pcTaskName = "Task 1 is running\r\n";
	volatile unsigned long ul;
	/* 和大多数任务一样,该任务处于一个死循环中。 */
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( pcTaskName );
		/* 延迟,以产生一个周期 */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		/* 这个空循环是最原始的延迟实现方式。在循环中不做任何事情。后面的示例程序将采用
		delay/sleep函数代替这个原始空循环。 */
		}
	}
}
//任务二定义
void vTask2( void *pvParameters )
{
	const char *pcTaskName = "Task 2 is running\r\n";
	volatile unsigned long ul;
	/* 和大多数任务一样,该任务处于一个死循环中。 */
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( pcTaskName );
		/* 延迟,以产生一个周期 */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		/* 这个空循环是最原始的延迟实现方式。在循环中不做任何事情。后面的示例程序将采用
		delay/sleep函数代替这个原始空循环。 */
		}
	}
}
//主函数创建任务,任务调度器调度任务
int main( void )
{
	/* 创建第一个任务。 需要说明的是一个实用的应用程序中应当检测函数xTaskCreate()的返回值,以确保任
	务创建成功。 */
	xTaskCreate( vTask1, /* 指向任务函数的指针 */
	"Task 1", /* 任务的文本名字,只会在调试中用到 */
	1000, /* 栈深度 – 大多数小型微控制器会使用的值会比此值小得多 */
	NULL, /* 没有任务参数 */
	1, /* 此任务运行在优先级1上. */
	NULL ); /* 不会用到任务句柄 */
	/* Create the other task in exactly the same way and at the same priority. */
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
	/* 启动调度器,任务开始执行 */
	vTaskStartScheduler();
	/* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲
	任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
	for( ;; );
}
//也可以在一个任务中创建另一个任务
void vTask1( void *pvParameters )
{
	const char *pcTaskName = "Task 1 is running\r\n";
	volatile unsigned long ul;
	/* 如果已经执行到本任务的代码,表明调度器已经启动。在进入死循环之前创建另一个任务。 */
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( pcTaskName );
		/* Delay for a period. */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		/* This loop is just a very crude delay implementation. There is
		nothing to do in here. Later examples will replace this crude
		loop with a proper delay/sleep function. */
		}
	}
}

这两个任务都迅速地进入与退出运行态。由于这两个任务运行在同一个处理器上,所以会平等共享处理器时间。
上述示例中创建的两个任务几乎完全相同,唯一的区别就是打印输出的字符串。这种重复性可以通过创建同一个任务代码的两个实例来去除。这时任务参数就可以用来传递各自打印输出的字符串。

//函数的任务参数被强制转化为 char*以得到任务需要打印输出的字符串
void vTaskFunction( void *pvParameters )
{
	char *pcTaskName;
	volatile unsigned long ul;
	/* 需要打印输出的字符串从入口参数传入。强制转换为字符指针。 */
	pcTaskName = ( char * ) pvParameters;
	/* As per most tasks, this task is implemented in an infinite loop. */
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( pcTaskName );
		/* Delay for a period. */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		/* This loop is just a very crude delay implementation. There is
		nothing to do in here. Later exercises will replace this crude
		loop with a proper delay/sleep function. */
		}
	}
}

任务函数只有一个,但可以创建多个任务实例,如下所示

/* 定义将要通过任务参数传递的字符串。定义为const,且不是在栈空间上,以保证任务执行时也有效。 */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main( void )
{
	/* Create one of the two tasks. */
	xTaskCreate( vTaskFunction, /* 指向任务函数的指针. */
	"Task 1", /* 任务名. */
	1000, /* 栈深度. */
	(void*)pcTextForTask1, /* 通过任务参数传入需要打印输出的文本. */
	1, /* 此任务运行在优先级1上. */
	NULL ); /* 不会用到此任务的句柄. */
	/* 同样的方法创建另一个任务。至此,由相同的任务代码(vTaskFunction)创建了多个任务,仅仅是传入
	的参数不同。同一个任务创建了两个实例。 */
	xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
	/* Start the scheduler so our tasks start executing. */
	vTaskStartScheduler();
	/* If all is well then main() will never reach here as the scheduler will
	now be running the tasks. If main() does reach here then it is likely that
	there was insufficient heap memory available for the idle task to be created.
	CHAPTER 5 provides more information on memory management. */
	for( ;; );
}

任务优先级

xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这个侁先级可以在调度器启动后调用 vTaskPrioritySet() API 函数进行修改。优先级的个数通过configMAX_PRIORITIES设置,个数没有规定,但个数越多消耗的内存就越大,建议设置为够用就行,0表示最低优先级,configMAX_PRIORITIES-1表示最高优先级。任意数量的任务可
以共享同一个优先级,也可以为每一个任务创建唯一的优先级。调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运行态。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。
两个测试任务被创建在同一个优先级上,并且一直是可运行的。所以每个任务都执行一个”时间片”,任务在时间片起始时刻进入运行态,在时间片结束时刻又退出运行态。
时间片示意图
时间片的长度和滴答时钟有关,滴答时钟在FreeRTOSConfig.h 中的编译时配置常量 configTICK_RATE_HZ 进行配置,比如说 configTICK_RATE_HZ 设为 100(HZ),则时间片长度为 10ms。

/* 定义将要通过任务参数传递的字符串。定义为const,且不是在栈空间上,以保证任务执行时也有效。 */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main( void )
{
	/* 第一个任务创建在优先级1上。优先级是倒数第二个参数。 */
	xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );
	/* 第二个任务创建在优先级2上。 */
	xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
	/* Start the scheduler so the tasks start executing. */
	vTaskStartScheduler();
	return 0;
}

任务调度器总是选择最高优先级的任务执行,任务2的优先级比任务1高,因此总是执行任务二,这种情况称之为任务2饿死任务1

非运行态扩充

以上例子中创建的每个任务都只顾不停地处理自己的事情而没有其它任何事情需要等待——由于它们不需要等待所以总是能够进入运行态。这种”不停处理”类型的任务限制了其有用性,因为它们只可能被创建在最低优先级上。如果它们运行在其它任何优先级上,那么比它们优先级更低的任务将被饿死。
为了使我们的任务切实有用,我们需要通过某种方式来进行事件驱动。一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务。一个高优先级但不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级上,并且最高优先级任务不会把所有的低优先级任务饿死。
阻塞状态:如果一个任务正在等待某个事件,则称这个任务处于”阻塞态(blocked)”。
任务可以进入阻塞态以等待以下两种不同类型的事件:

  1. 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如
    说某个任务可以进入阻塞态以延迟 10ms。
  2. 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞
    态以等待队列中有数据到来。同步事件囊括了所有板级范围内的事件类型。
    任务可以在进入阻塞态以等待同步事件时指定一个等待超时时间,这样可以有效地实现阻塞状态下同时等待两种类型的事件。比如说,某个任务可以等待队列中有数据到来,但最多只等 10ms。如果 10ms 内有数据到来,或是 10ms 过去了还没有数据到来,这两种情况下该任务都将退出阻塞态。
    挂起状态:处于挂起状态的任务对调度器而言是不可见的。让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() API 函数。而 把 一 个 挂 起 状 态 的 任 务 唤 醒 的 唯 一 途 径 就 是 调 用 vTaskResume() 或vTaskResumeFromISR() API 函数。
    就绪状态:任务处于非运行状态,既没有阻塞也没有挂起,则此任务处于就绪态,处于就绪态的任务能够被运行,但当前并未运行
    通过调用延时函数来替换之前的空循环,原型如下所示:
//返回类型void,形参为延时的滴答周期数
void vTaskDelay( portTickType xTicksToDelay );

调用该延迟函数的任务将进入阻塞态,经延迟指定的心跳周期数后,再转移到就绪态。
举个例子,当某个任务调用 vTaskDelay( 100 )时,心跳计数值为 10,000,则该任务将保持在阻塞态,直到心跳计数计到
10100。常数 portTICK_RATE_MS 可以用来将以毫秒为单位的时间值转换为以心跳周期为单位的时间值。

//使用延时函数来代替空循环示例
void vTaskFunction( void *pvParameters )
{
	char *pcTaskName;
	/* The string to print out is passed in via the parameter. Cast this to a
	character pointer. */
	pcTaskName = ( char * ) pvParameters;
	/* As per most tasks, this task is implemented in an infinite loop. */
	for( ;; )
	{
	/* Print out the name of this task. */
	vPrintString( pcTaskName );
	/* 延迟一个循环周期。 调用vTaskDelay()以让任务在延迟期间保持在阻塞态。延迟时间以心跳周期为
	单位,常量portTICK_RATE_MS可以用来在毫秒和心跳周期之间相换转换。本例设定250毫秒的循环周
	期。 */
	vTaskDelay( 250 / portTICK_RATE_MS );
	}
}

这样做尽管两个任务的优先级不一样,但通过延时函数可以保证两个任务都能被执行。对比采用空循环延时的优点,采用延时函数不会导致低优先级任务饿死,在延时阶段任务处于阻塞状态,不会占用CPU时间。
采用延时函数代替空循环任务执行示例
空闲任务是在调度器启动时自动创建的,以保证至少有一个任务可运行(至少有一个任务处于就绪态)。上图示例中,大多数时间都没有一个应用任务可运行(即没有应用任务处于就绪态),因此没有应用任务可以被选择进入运行态。这种情况下,空闲任务得以执行。空间任务可以获得的执行时间量,是系统处理能力裕量的一个度量指标。
vTaskDelayUntil() API 函数:任务从调用延时函数开始则进入阻塞态,任务保持阻塞态的时间长度由延时函数的形参决定,任务离开阻塞态的的时间是相对于调用延时函数而言的,vTaskDelayUntil()的参数就是用来指定任务离开阻塞态进入就绪态那一刻的精确心跳计数值。API 函数 vTaskDelayUntil()可以用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性执行的时候)。由于调用此函数的任务解除阻塞的时间是绝对时刻,比起相对于调用时刻的相对时间更精确(即比调用 vTaskDelay()可以实现更精确的周期性)。
vTaskDelayUntil() API 函数原型如下所示:

void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );

pxPreviousWakeTime 此参数命名时假定 vTaskDelayUntil()用于实现某个任务以固定频率周期性执行。这种情况下 pxPreviousWakeTime保存了任务上一次离开阻塞态(被唤醒)的时刻。这个时刻被用作一个参考点来计算该任务下一次离开阻塞态的时刻。
pxPreviousWakeTime 指 向 的 变 量 值 会 在 API 函 数vTaskDelayUntil()调用过程中自动更新,应用程序除了该变量第一次初始化外,通常都不要修改它的值。
xTimeIncrement 此参数命名时同样是假定 vTaskDelayUntil()用于实现某个任 务 以 固 定 频 率 周 期 性 执 行 —— 这 个 频 率 就 是 由xTimeIncrement 指定的。xTimeIncrement 的 单 位 是 心 跳 周 期 , 可 以 使 用 常 量portTICK_RATE_MS 将毫秒转换为心跳周期。
使用 vTaskDelay()无法保证它们具有固定的执行频率,因为这两个任务退出阻塞态的时刻相对于调用 vTaskDelay()的时刻。通过调用 vTaskDelayUntil()代替 vTaskDelay(),可以解决此问题

//采用vTaskDelayUntil()函数代替vTaskDelay()函数
void vTaskFunction( void *pvParameters )
{
	char *pcTaskName;
	portTickType xLastWakeTime;
	/* The string to print out is passed in via the parameter. Cast this to a
	character pointer. */
	pcTaskName = ( char * ) pvParameters;
	/* 变量xLastWakeTime需要被初始化为当前心跳计数值。说明一下,这是该变量唯一一次被显式赋值。之后,
	xLastWakeTime将在函数vTaskDelayUntil()中自动更新。 */
	xLastWakeTime = xTaskGetTickCount();
	/* As per most tasks, this task is implemented in an infinite loop. */
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( pcTaskName );
		/* 本任务将精确的以250毫秒为周期执行。同vTaskDelay()函数一样,时间值是以心跳周期为单位的,
		可以使用常量portTICK_RATE_MS将毫秒转换为心跳周期。变量xLastWakeTime会在
		vTaskDelayUntil()中自动更新,因此不需要应用程序进行显示更新。 */
		vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
	}
}

任务没有调用任何可能导致它们进入阻塞态的 API 函数,任务要么处于就绪态,要么处于运行态。具有这种性质的任务被称为”不停处理(或持续处理, continuous processing)”任务
示例如下:

void vContinuousProcessingTask( void *pvParameters )
{
	char *pcTaskName;
	/* 打印输出的字符串由任务参数传入,强制转换为char* */
	pcTaskName = ( char * ) pvParameters;
	/* As per most tasks, this task is implemented in an infinite loop. */
	for( ;; )
	{
		/* 打印输出任务名,无阻塞,也无延迟。 */
		vPrintString( pcTaskName );
	}
}

周期调用任务示例:

void vPeriodicTask( void *pvParameters )
{
	portTickType xLastWakeTime;
	/* 初始化xLastWakeTime,之后会在vTaskDelayUntil()中自动更新。 */
	xLastWakeTime = xTaskGetTickCount();
	/* As per most tasks, this task is implemented in an infinite loop. */
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( "Periodic task is running\r\n" );
		/* The task should execute every 10 milliseconds exactly. */
		vTaskDelayUntil( &xLastWakeTime, ( 10 / portTICK_RATE_MS ) );
	}
}

不停处理任务和周期调用任务执行示意图
不停处理任务和周期调用任务执行示意图

空闲任务和空闲任务钩子函数

空闲任务

处理器需要不停的有任务执行,空闲任务优先级为0,确保没有其他任务执行的时候可以执行空闲任务,空闲任务不会阻碍其他任务的执行,因为他的优先级最低。

空闲任务钩子函数

通过空闲任务钩子函数(或称回调, hook, or call-back),可以直接在空闲任务中添加应用程序相关的功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。

空闲任务钩子函数用法:

执行低优先级,后台或需要不停处理的功能代码。
测试系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式。
FreeRTOSConfig.h 中的配置常量 configUSE_IDLE_HOOK 必须定义为 1,空闲钩子函数使能

空闲任务钩子函数规则

  1. 绝不能阻塞或挂起。空闲任务只会在其它任务都不运行时才会被执行(除非有应用任务共享空闲任务优先级)。以任何方式阻塞空闲任务都可能导致没有任务能够进入运行态!
  2. 如果应用程序用到了 vTaskDelete() AP 函数,则空闲钩子函数必须能够尽快返回。因为在任务被删除后,空闲任务负责回收内核资源。如果空闲任务一直运行在钩子函数中,则无法进行回收工作。
    空闲钩子函数原型如下所示:
void vApplicationIdleHook( void );

设置和查询任务优先级

设置任务优先级

函数原型如下所示:

void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );

pxTask 被修改优先级的任务句柄,创建任务时可以通过传入 NULL 值来修改自己的优先级。
uxNewPriority 目标任务将被设置到哪个优先级上。如果设置的值超过了最大可用优先级(configMAX_PRIORITIES – 1),则会被自动封顶为最大值。

查询任务优先级

函数原型如下所示:

unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );

形参pxTask :被查询任务句柄
返回值:任务对应的优先级
创建任务时,可以通过传入 NULL 值来查询自己的优先级。

代码示例

//任务一执行完成之后,将任务二优先级改成高于任务一,此时立即执行任务二,任务查询和设置自身优先级使用的句柄都是NULL
void vTask1( void *pvParameters )
{
	unsigned portBASE_TYPE uxPriority;
	/* 本任务将会比任务2更先运行,因为本任务创建在更高的优先级上。任务1和任务2都不会阻塞,所以两者要
	么处于就绪态,要么处于运行态。
	查询本任务当前运行的优先级 – 传递一个NULL值表示说“返回我自己的优先级”。 */
	uxPriority = uxTaskPriorityGet( NULL );
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( "Task1 is running\r\n" );
		/* 把任务2的优先级设置到高于任务1的优先级,会使得任务2立即得到执行(因为任务2现在是所有任务
		中具有最高优先级的任务)。注意调用vTaskPrioritySet()时用到的任务2的句柄。程序清单24将展示
		如何得到这个句柄。 */
		vPrintString( "About to raise the Task2 priority\r\n" );
		vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
		/* 本任务只会在其优先级高于任务2时才会得到执行。因此,当此任务运行到这里时,任务2必然已经执
		行过了,并且将其自身的优先级设置回比任务1更低的优先级。 */
	}
}
//任务二执行完成之后,将优先级改成低于任务一
void vTask2( void *pvParameters )
{
	unsigned portBASE_TYPE uxPriority;
	/* 任务1比本任务更先启动,因为任务1创建在更高的优先级。任务1和任务2都不会阻塞,所以两者要么处于
	就绪态,要么处于运行态。
	查询本任务当前运行的优先级 – 传递一个NULL值表示说“返回我自己的优先级”。 */
	uxPriority = uxTaskPriorityGet( NULL );
	for( ;; )
	{
		/* 当任务运行到这里,任务1必然已经运行过了,并将本身务的优先级设置到高于任务1本身。 */
		vPrintString( "Task2 is running\r\n" );
		/* 将自己的优先级设置回原来的值。传递NULL句柄值意味“改变我己自的优先级”。把优先级设置到低
		于任务1使得任务1立即得到执行 – 任务1抢占本任务。 */
		vPrintString( "About to lower the Task2 priority\r\n" );
		vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
	}
}
//主函数中创建任务
/* 声明变量用于保存任务2的句柄。 */
xTaskHandle xTask2Handle;
int main( void )
{
	/* 任务1创建在优先级2上。任务参数没有用到,设为NULL。任务句柄也不会用到,也设为NULL */
	xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
	/* The task is created at priority 2 ______^. */
	/* 任务2创建在优先级1上 – 此优先级低于任务1。任务参数没有用到,设为NULL。但任务2的任务句柄会被
	用到,故将xTask2Handle的地址传入。 */
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
	/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
	/* Start the scheduler so the tasks start executing. */
	vTaskStartScheduler();
	/* If all is well then main() will never reach here as the scheduler will
	now be running the tasks. If main() does reach here then it is likely that
	there was insufficient heap memory available for the idle task to be created.
	CHAPTER 5 provides more information on memory management. */
	for( ;; );
}

任务删除

可以使用 API 函数 vTaskDelete()删除自己或其它任务。
任务被删除后就不复存在,也不会再进入运行态。空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就是使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。需要说明一点,只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显式地释放。函数原型如下:

void vTaskDelete( xTaskHandle pxTaskToDelete );

形参pxTaskToDelete:被删除任务的句柄,如果是删除自己,则传入NULL。

代码示例

int main( void )
{
	/* 任务1创建在优先级1上 */
	xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
	/* The task is created at priority 1 ______^. */
	/* Start the scheduler so the tasks start executing. */
	vTaskStartScheduler();
	/* main() should never reach here as the scheduler has been started. */
	for( ;; );
}
//任务一中创建任务二
void vTask1( void *pvParameters )
{
	const portTickType xDelay100ms = 100 / portTICK_RATE_MS;
	for( ;; )
	{
		/* Print out the name of this task. */
		vPrintString( "Task1 is running\r\n" );
		/* 创建任务2为最高优先级。 */
		xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
		/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
		/* 因为任务2具有最高优先级,所以任务1运行到这里时,任务2已经完成执行,删除了自己。任务1得以
		执行,延迟100ms */
		vTaskDelay( xDelay100ms );
	}
}
//任务二删除自身
void vTask2( void *pvParameters )
{
	vPrintString( "Task2 is running and about to delete itself\r\n" );
	vTaskDelete( NULL);			//传参可传任务二句柄也可使用NULL
}

任务调度算法

固定优先级抢占式调度:
1、每个任务都赋予了一个优先级。
2、每个任务都可以存在于一个或多个状态。
3、在任何时候都只有一个任务可以处于运行状态。
4、调度器总是在所有处于就绪态的任务中选择具有最高优先级的任务来执行。
”固定优先级”是指每个任务都被赋予了一个优先级,这个优先级不能被内核本身改变(只能被任务修改)。 ”抢占式”是指当任务进入就绪态或是优先级被改变时,如果处于运行态的任务优先级更低,则该任务总是抢占当前运行的任务。
阻塞态的任务需要等待一个事件才能进入就绪态,通常时延时事件或者时同步事件,延时事件是指:通常用于周期性或超时行为,如延时多少毫秒,任务进入就绪态。同步事件是指:任务或中断服务例程往队列发送消息或发送任务一种信号量,如某个外围信号到达了。
处于阻塞态的任务,每当事件发生时其就从阻塞态转移到就绪态。 FreeRTOS 中所有的任务间通信机制(队列,信号量等)都可以通过这种方式用于发送事件以及让任务解除阻塞。
任务调度示意图:
任务调度示意图
任务三:事件任务,当其关心的事件产生时,从阻塞态进入就绪态。事件在 t3, t5 以及 t9 至 t12 之间的某个时刻发生。发生在 t3 和 t5 时刻的事件可以立即被处理,因为这些时刻任务 3 在所有可运行任务中优先级最高。发生在 t9 至 t12 之间某个时刻的事件不会得到立即处理,需要一直等到 t12 时刻。因为具有更高优先级的任务 1 和任务 2 尚在运行中,只有到了 t12 时刻,这两个任务进入阻塞态,使得任务 3 成为具有最高优先级的就绪态任务。
任务二:周期性任务,其优先级高于任务 3 并低于任务 1。根据周期间隔,任务 2 期望在 t1, t6 和 t9 时刻执行。在 t6 时刻任务 3 处于运行态,但是任务 2 相对具有更高的优先级,所以会抢占任务 3,并立即得到执行。任务 2 完成处理后,在 t7 时刻返回阻塞态。同时,任务 3 得以重新进入运行态,继续完成处理。任务 3 在 t8 时刻进入阻塞状态。
任务一:任务 1 也是一个事件驱动任务。任务 1 在所有任务中具有最高优先级,因此可以抢占系统中的任何其它任务。在图中看到,任务 1 的事件只是发生在在 t10时刻,此时任务 1 抢占了任务 2。只有当任务 1 在 t11 时刻再次进入阻塞态之后,任务 2 才得以机会继续完成处理。

任务优先级选择

通常完成硬实时功能的任务优先级会高于完成软件时功能任务的优先级。
单调速率调度:是一种常用的优先级分配技术。其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务赋予最高优先级;具有最低周期执行频率的任务赋予最低优先级。这种优先级分配方式被证明了可以最大化整个应用程序的可调度性(schedulability),但是运行时间不定以及并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。

协作式任务调度

采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显式调用 taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可能会导致系统响应不够快。

混合调度方案

实现混合调度方案也是可行的,这需要在中断服务例程中显式地进行上下文切换,从而允许同步事件产生抢占行为,但时间事件却不行。混合调度方案本质上是一个没有时间片的抢占式调度机制,这样做的好处是提升了效率,这也是一种常用的任务调度方式。

(二) 队列管理

队列

队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。
往队列写入数据是通过字节拷贝把数据复制存储到队列中。从队列读出数据使得把队列中的数据拷贝删除。
队列是一个具有独立权限的内核对象,不属于任何任务,所有任务都可以向同一队列写入和读出,一个队列由多方写入是经常的事,但由多方读出倒是很少遇到。

读队列阻塞

当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。

写队列阻塞

同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。
由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。

队列读写示意图

队列读写示意图

使用队列

创建队列

队列由声明为 xQueueHandle 的变量进行引用。 xQueueCreate()用于创建一个队列,并返回一个 xQueueHandle 句柄以便于对其创建的队列进行引用。
当创建队列时, FreeRTOS 从堆空间中分配内存空间。分配的空间用于存储队列数据结构本身以及队列中包含的数据单元。如果内存堆中没有足够的空间来创建队列,xQueueCreate()将返回 NULL。
队列创建函数原型:

xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength,unsigned portBASE_TYPE uxItemSize );

uxQueueLength 队列能够存储的最大单元数目,即队列深度。
uxItemSize 队列中数据单元的长度,以字节为单位。
返回值:句柄(NULL 表示没有足够的堆空间分配给队列而导致创建失败,非 NULL 值表示队列创建成功)

数据发送到队列

xQueueSendToBack() 与 xQueueSendToFront() API 函数,xQueueSendToBack()用于将数据发送到队列尾;而 xQueueSendToFront()用于将数据发送到队列首。xQueueSend()完全等同于 xQueueSendToBack()。在中断中将数据发送到队列有专门的函数,xQueueSendToFrontFromISR()和xQueueSendToBackFromISR(),作用和前两个函数一致,区别是这两个函数只能在中断中使用

//发送数据到队列头函数原型
portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue,const void * pvItemToQueue,portTickType xTicksToWait );
//发送数据到队列尾函数原型
portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue,const void * pvItemToQueue,portTickType xTicksToWait );

xQueue 目标队列的句柄,这个句柄即是调用 xQueueCreate()创建该队列时的返回值。
pvItemToQueue 发送数据的指针,其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域。
xTicksToWait 阻塞超时时间。如果在发送时队列已满,这个时间即是任务处于阻塞态等待队列空间有效的最长等待时间。如 果 xTicksToWait 设 为 0 , 并 且 队 列 已 满 , 则xQueueSendToFront()与 xQueueSendToBack()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位
转换为毫秒时间单位。如 果 把 xTicksToWait 设 置 为 portMAX_DELAY , 并 且 在FreeRTOSConig.h 中设定INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。
返回值:有两个可能的返回值

  1. pdPASS
    返回 pdPASS 只会有一种情况,那就是数据被成功发送到队列中。
    如果设定了阻塞超时时间(xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效—在超时到来前能
    够将数据成功写入到队列,函数则会返回 pdPASS。
  2. errQUEUE_FULL
    如 果 由 于 队 列 已 满 而 无 法 将 数 据 写 入 , 则 将 返 回errQUEUE_FULL。
    如果设定了阻塞超时时间( xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效。但直到超时也没有其它任务或是中断服务例程读取队列而腾出空间,函数则会返回 errQUEUE_FULL。

从队列中接收数据

xQueueReceive()与 xQueuePeek() API 函数,xQueueReceive()函数从队列中接收数据,并删除队列中的数据,xQueuePeek() 函数从队列中接收数据,不删除队列中的数据,也不改变队列存储顺序。这两个函数不能在中断中使用,中断结收数据函数为 xQueueReceiveFromISR()。

//队列结收数据函数原型(删除队列中数据)
portBASE_TYPE xQueueReceive( xQueueHandle xQueue,const void * pvBuffer,portTickType xTicksToWait );
//队列结收数据函数原型(不改变队列中数据)
portBASE_TYPE xQueuePeek( xQueueHandle xQueue,const void * pvBuffer,portTickType xTicksToWait );

xQueue 被读队列的句柄。这个句柄即是调用 xQueueCreate()创建该队列时的返回值。
pvBuffer 接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来的数据。
数据单元的长度在创建队列时就已经被设定,所以该指针指向的内存区域大小应当足够保存一个数据单元。
xTicksToWait 阻塞超时时间。如果在接收时队列为空,则这个时间是任务处于阻塞状态以等待队列数据有效的最长等待时间。如果 xTicksToWait 设为 0,并且队列为空,则 xQueueRecieve()与 xQueuePeek()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。如 果 把 xTicksToWait 设 置 为 portMAX_DELAY , 并 且 在FreeRTOSConig.h 中设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。
返回值 有两个可能的返回值:

  1. pdPASS
    只有一种情况会返回 pdPASS,那就是成功地从队列中读到数据。
    如果设定了阻塞超时时间(xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列数据有效—在超时到来前能够从队列中成功读取数据,函数则会返回 pdPASS。
  2. errQUEUE_FULL
    如果在读取时由于队列已空而没有读到任何数据,则将返回errQUEUE_FULL。
    如果设定了阻塞超时时间( xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列数据有效。但直到超时也没有其它任务或是中断服务例程往队列中写入数据,函数则会返回errQUEUE_FULL。

查询队列中有效单元个数

uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数。切记不要在中断服务例程中调用 uxQueueMessagesWaiting()。应当在中断服务中使用其中断安全版本 uxQueueMessagesWaitingFromISR()。

//函数原型
unsigned portBASE_TYPE uxQueueMessagesWaiting( xQueueHandle xQueue );

xQueue 被查询队列的句柄。这个句柄即是调用 xQueueCreate()创建该队列时的返回值。
返回值 当前队列中保存的数据单元个数。返回 0 表明队列为空。

队列使用代码示例

//往队列中写入数据
static void vSenderTask( void *pvParameters )
{
	long lValueToSend;
	portBASE_TYPE xStatus;
	/* 该任务会被创建两个实例,所以写入队列的值通过任务入口参数传递 – 这种方式使得每个实例使用不同的
	值。队列创建时指定其数据单元为long型,所以把入口参数强制转换为数据单元要求的类型 */
	lValueToSend = ( long ) pvParameters;
	/* 和大多数任务一样,本任务也处于一个死循环中 */
	for( ;; )
	{
		/* 往队列发送数据
		第一个参数是要写入的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
		第二个参数是被发送数据的地址,本例中即变量lValueToSend的地址。
		第三个参数是阻塞超时时间 – 当队列满时,任务转入阻塞状态以等待队列空间有效。本例中没有设定超
		时时间,因为此队列决不会保持有超过一个数据单元的机会,所以也决不会满。
		*/
		xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
		if( xStatus != pdPASS )
		{
			/* 发送操作由于队列满而无法完成 – 这必然存在错误,因为本例中的队列不可能满。 */
			vPrintString( "Could not send to the queue.\r\n" );
		}
		/* 允许其它发送任务执行。 taskYIELD()通知调度器现在就切换到其它任务,而不必等到本任务的时
		间片耗尽 */
		taskYIELD();
	}
}
//从队列中读取数据
static void vReceiverTask( void *pvParameters )
{
	/* 声明变量,用于保存从队列中接收到的数据。 */
	long lReceivedValue;
	portBASE_TYPE xStatus;
	const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
	/* 本任务依然处于死循环中。 */
	for( ;; )
	{
		/* 此调用会发现队列一直为空,因为本任务将立即删除刚写入队列的数据单元。 */
		if( uxQueueMessagesWaiting( xQueue ) != 0 )
		{
		vPrintString( "Queue should have been empty!\r\n" );
		}
		/* 从队列中接收数据
		第一个参数是被读取的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
		第二个参数是保存接收到的数据的缓冲区地址,本例中即变量lReceivedValue的地址。此变量类型与
		队列数据单元类型相同,所以有足够的大小来存储接收到的数据。
		第三个参数是阻塞超时时间 – 当队列空时,任务转入阻塞状态以等待队列数据有效。本例中常量
		portTICK_RATE_MS用来将100毫秒绝对时间转换为以系统心跳为单位的时间值。
		*/
		xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
		if( xStatus == pdPASS )
		{
		/* 成功读出数据,打印出来。 */
		vPrintStringAndNumber( "Received = ", lReceivedValue );
		}
		else
		{
		/* 等待100ms也没有收到任何数据。
		必然存在错误,因为发送任务在不停地往队列中写入数据 */
		vPrintString( "Could not receive from the queue.\r\n" );
		}
	}
}
//main函数中创建任务
/* 声明一个类型为 xQueueHandle 的变量. 其用于保存队列句柄,以便三个任务都可以引用此队列 */
xQueueHandle xQueue;
int main( void )
{
	/* 创建的队列用于保存最多5个值,每个数据单元都有足够的空间来存储一个long型变量 */
	xQueue = xQueueCreate( 5, sizeof( long ) );
	if( xQueue != NULL )
	{
		/* 创建两个写队列任务实例,任务入口参数用于传递发送到队列的值。所以一个实例不停地往队列发送
		100,而另一个任务实例不停地往队列发送200。两个任务的优先级都设为1。 */
		xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
		xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
		/* 创建一个读队列任务实例。其优先级设为2,高于写任务优先级 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
		/* 启动调度器,任务开始执行 */
		vTaskStartScheduler();
	}
	else
	{
		/* 队列创建失败*/
	}
	/* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲
	任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
	for( ;; );
}

写队列任务在每次循环中都调用 taskYIELD()。 taskYIELD()通知调度器立即进行任务切换,而不必等到当前任务的时间片耗尽。调用taskYIELD()函数,任务将切换到就绪态,因此写队列任务当中一个执行完之后就会立即执行另一个任务。
写队列也可以写入一个结构体或结构体数组。

传递大型数据单元

如果队列存储的数据单元尺寸较大,那最好是利用队列来传递数据的指针而不是对数据本身在队列上一字节一字节地拷贝进或拷贝出。传递指针无论是在处理速度上还是内存空间利用上都更有效。
使用指针传递时,需注意两点:

  1. 指针指向的内存空间的所有权必须明确
    当任务间通过指针共享内存时,应该从根本上保证不会有任意两个任务同时修改共享内存中的数据,或是以其它行为方式使得共享内存数据无效或产生一致性问题。原则上,共享内存在其指针发送到队列之前,其内容只允许被发送任务访问。共享内存指针从队列中被读出之后,其内容亦只允许被接收任务访问。
  2. 指针指向的内存空间必须有效
    如果指针指向的内存空间是动态分配的,只应该有一个任务负责对其进行内存释放。当这段内存空间被释放之后,就不应该有任何一个任务再访问这段空间。切忌用指针访问任务栈上分配的空间。因为当栈帧发生改变后,栈上的数据将不再有效。

(三)中断管理

二值信号量

二值信号量可以在某个特殊的中断发生时,让任务解除阻塞,相当于让任务与中断同步。这样就可以让中断事件处理量大的工作在同步任务中完成,中断服务例程(ISR)中只是快速处理少部份工作。 如此,中断处理可以说是被”推迟(deferred)”到一个”处理(handler)”任务。
如果某个中断处理要求特别紧急,其延迟处理任务的优先级可以设为最高,以保证延迟处理任务随时都抢占系统中的其它任务。这样,延迟处理任务就成为其对应的 ISR退出后第一个执行的任务。
延迟处理任务对一个信号量进行带阻塞性质的”take”调用,意思是进入阻塞态以等待事件发生。当事件发生后, ISR 对同一个信号量进行”give”操作,使得延迟处理任务解除阻塞,从而事件在延迟处理任务中得到相应的处理。
“获取(Taking,带走,按通常的说法译为获取)”和”给出(Giving)”信号量从概念上讲,在不同的应用场合有不同的含义。在经典的信号量术语中,获取信号量等同于一个 P()操作,而给出信号量等同于一个 V()操作。
在这里插入图片描述
二值信号量应用:
二值信号量可以看作是一个深度为1的队列,这个队列由于
最多只能保存一个数据单元,所以其不为空则为满(所谓”二值”)。中断服务函数中调用xSemaphoreGiveFromISR()放置一个令牌(信号量)到队列中,使得队列成为满状态。延迟处理任务调用xSemaphoreTake()时,等效于带阻塞时间地读取队列,如果队列为空的话任务则进入阻塞态。
二值信号量使用示意图
二值信号量通信
二值信号量代码示例:

//函数原型
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );

xSemaphore 创建的信号量。
需要说明的是 vSemaphoreCreateBinary()在实现上是一个宏,所以信号量变量应当直接传入,而不是传址。
除互斥信号量外,所有类型的信号量都可以调用函数 xSemaphoreTake()来获取。xSemaphoreTake()不能再中断中使用

//take信号量,再任务函数中调用
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );

xSemaphore 获取得到的信号量信号量由定义为 xSemaphoreHandle 类型的变量引用。信号量在使
用前必须先创建。
xTicksToWait 阻塞超时时间。任务进入阻塞态以等待信号量有效的最长时间。如果 xTicksToWait 为 0,则 xSemaphoreTake()在信号量无效时会立即返回。
阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。如 果 把 xTicksToWait 设 置 为 portMAX_DELAY , 并 且 在FreeRTOSConig.h 中设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。
返回值 有两个可能的返回值:

  1. pdPASS
    只有一种情况会返回 pdPASS,那就是成功获得信号量。如果设定了阻塞超时时间(xTicksToWait 非 0),在函数返回之前任务
    将被转移到阻塞态以等待信号量有效。如果在超时到来前信号量变为有效,亦可被成功获取,返回 pdPASS。
  2. pdFALSE
    未能获得信号量。如果设定了阻塞超时时间( xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待信号量有效。但直到超时信号量也没有变为有效,所以不会获得信号量,返回 pdFALSE。
    除了互斥信号量之外,FreeRTOS 支 持 的 其 它 类 型 的 信 号 量 都 可 以 通 过 调 用xSemaphoreGiveFromISR()给出。
    函数原型如下:
//专门用于中断服务函数中
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore,portBASE_TYPE *pxHigherPriorityTaskWoken );

xSemaphore 给出的信号量,信号量由定义为 xSemaphoreHandle 类型的变量引用,信号量在使用前必须先创建。
pxHigherPriorityTaskWoken 对某个信号量而言,可能有不止一个任务处于阻塞态在等待其有效。调用 xSemaphoreGiveFromISR()会让信号量变为有效,所以会让其中一个等待任务切出阻塞态。如果调用 xSemaphoreGiveFromISR()使得一个任务解除阻塞,并且这个任务的优先级高于当前任务(也就是被中断的任务),那么 xSemaphoreGiveFromISR()会在 函 数 内 部 将 *pxHigherPriorityTaskWoken 设 为pdTRUE。如 果 xSemaphoreGiveFromISR() 将 此 值 设 为pdTRUE,则在中断退出前应当进行一次上下文切换。这样才能保证中断直接返回到就绪态任务中优先级最高的任务中。
返回值 有两个可能的返回值:

  1. pdPASS
    xSemaphoreGiveFromISR()调用成功。
  2. pdFAIL
    如果信号量已经有效,无法给出,则返回 pdFAIL。
    二值信号量使用例程:
//周期性任务,产生中断
static void vPeriodicTask( void *pvParameters )
{
	for( ;; )
	{
	/* 此任务通过每500毫秒产生一个软件中断来”模拟”中断事件 */
	vTaskDelay( 500 / portTICK_RATE_MS );
	/* 产生中断,并在产生之前和之后输出信息,以便在执行结果中直观直出执行流程 */
	vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
	__asm{ int 0x82 } /* 这条语句产生中断 */
	vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
	}
}
//延时处理任务函数,产生二值信号量之后,进入此任务函数
static void vHandlerTask( void *pvParameters )
{
	/* As per most tasks, this task is implemented within an infinite loop. */
	for( ;; )
	{
		/* 使用信号量等待一个事件。信号量在调度器启动之前,也即此任务执行之前就已被创建。任务被无超
		时阻塞,所以此函数调用也只会在成功获取信号量之后才会返回。此处也没有必要检测返回值 */
		xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
		/* 程序运行到这里时,事件必然已经发生。本例的事件处理只是简单地打印输出一个信息 */
		vPrintString( "Handler task - Processing event.\r\n" );
	}
}
//中断服务函数
static void __interrupt __far vExampleInterruptHandler( void )
{
	static portBASE_TYPE xHigherPriorityTaskWoken;
	xHigherPriorityTaskWoken = pdFALSE;
	/* 'Give' the semaphore to unblock the task. */
	xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
	if( xHigherPriorityTaskWoken == pdTRUE )
	{
		/* 给出信号量以使得等待此信号量的任务解除阻塞。如果解出阻塞的任务的优先级高于当前任务的优先
		级 – 强制进行一次任务切换,以确保中断直接返回到解出阻塞的任务(优选级更高)。
		说明:在实际使用中, ISR中强制上下文切换的宏依赖于具体移植。此处调用的是基于Open Watcom DOS
		移植的宏。其它平台下的移植可能有不同的语法要求。对于实际使用的平台,请参如数对应移植自带的示
		例程序,以决定正确的语法和符号。
		*/
		portSWITCH_CONTEXT();
	}
}
//main函数
int main( void )
{
	/* 信号量在使用前都必须先创建。本例中创建了一个二值信号量 */
	vSemaphoreCreateBinary( xBinarySemaphore );
	/* 安装中断服务例程 */
	_dos_setvect( 0x82, vExampleInterruptHandler );
	/* 检查信号量是否成功创建 */
	if( xBinarySemaphore != NULL )
	{
		/* 创建延迟处理任务。此任务将与中断同步。延迟处理任务在创建时使用了一个较高的优先级,以保证
		中断退出后会被立即执行。在本例中,为延迟处理任务赋予优先级3 */
		xTaskCreate( vHandlerTask, "Handler", 1000, NULL, 3, NULL );
		/* 创建一个任务用于周期性产生软件中断。此任务的优先级低于延迟处理任务。每当延迟处理任务切出
		阻塞态,就会抢占周期任务*/
		xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, 1, NULL );
		/* Start the scheduler so the created tasks start executing. */
		vTaskStartScheduler();
	}
	/* 如果一切正常, main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
	很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
	for( ;; );
}

计数信号量

在中断以相对较慢的频率发生的情况下,上面描述的流程是足够而完美的。如果在延迟处理任务完成上一个中断事件的处理之前,新的中断事件又发生了,等效于将新的事件锁存在二值信号量中,使得延迟处理任务在处理完上一个事件之后,立即就可以处理新的事件。也就是说,延迟处理任务在两次事件处理之间,不会有进入阻塞态的机会,因为信号量中锁存有一个事件,所以当 xSempaphoreTake()调用时,信号量立即有效。
一个二值信号量最多只可以锁存一个中断事件。在锁存的事件还未被处理之前,如果还有中断事件发生,那么后续发生的中断事件将会丢失。如果用计数信号量代替二值信号量,那么,这种丢中断的情形将可以避免。
就如同我们可以把二值信号量看作是只有一个数据单元的队列一样,计数信号量可以看作是深度大于 1 的队列。任务其实对队列中存储的具体数据并不感兴趣——其只关心队列是空还是非空。计数信号量每次被给出(Given),其队列中的另一个空间将会被使用。队列中的有效数据单元个数就是信号量的”计数(Count)”值。
流程如下图所示:
当正在执行第一个中断的延迟任务函数期间,又产生了一个新的中断
一个二值信号量只能锁存一个中断事件
计数信号量工作示意图
计数信号量工作示意图
1.事件计数
在这种用法中,每次事件发生时,中断服务例程都会“给出(Give)”信号量——信号量在每次被给出时其计数值加 1。延迟处理任务每处理一个任务都会”获取(Take)”一次信号量——信号量在每次被获取时其计数值减 1。信号量的计数值其实就是已发生事件的数目与已处理事件的数目之间的差值。
2.资源管理
在这种用法中,信号量的计数值用于表示可用资源的数目。一个任务要获取资源的控制权,其必须先获得信号量——使信号量的计数值减 1。当计数值减至 0,则表示没有可用资源。当任务利用资源完成工作后,将给出(归还)信号量——使信号量的计数值加 1。用于资源管理的信号量,在创建时其计数值被初始化为可用资源总数。
信号量在使用前需要被创建,创建接口如下:

xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount,unsigned portBASE_TYPE uxInitialCount);

uxMaxCount 最大计数值。如果把计数信号量类比于队列的话, uxMaxCount 值就是队列的最大深度。当此信号量用于对事件计数或锁存事件的话, uxMaxCount 就是可锁存事件的最大数目。当此信号量用于对一组资源的访问进行管理的话, uxMaxCount 应当设置为所有可用资源的总数。
uxInitialCount 信号量的初始计数值。当此信号量用于事件计数的话, uxInitialCount 应当设置为 0——因为当信号量被创建时,还没有事件发生。
返回值 如果返回 NULL 值,表示堆上内存空间不足,所以 FreeRTOS 无法为信号量结构分配内存导致信号量创建失败。第五章有提供更多的内存管理方面的信息。如果返回非 NULL 值,则表示信号量创建成功。此值应当被保存起来作为这个的信号量的句柄。

中断服务函数中使用队列

xQueueSendToFrontFromISR(), xQueueSendToBackFromISR()与 xQueueReceiveFromISR()分别是 xQueueSendToFront(), xQueueSendToBack()与 xQueueReceive()的中断安全版本,专门用于中断服务例程中。信号量用于事件通信。而队列不仅可以用于事件通信,还可以用来传递数据。xQueueSendFromISR()完全等同于 xQueueSendToBackFromISR()。

//发送到队列前函数原型
portBASE_TYPE xQueueSendToFrontFromISR( xQueueHandle xQueue,void *pvItemToQueue,portBASE_TYPE *pxHigherPriorityTaskWoken );
//发送到队列后函数原型
portBASE_TYPE xQueueSendToBackFromISR( xQueueHandle xQueue,void *pvItemToQueue,portBASE_TYPE *pxHigherPriorityTaskWoken);

形参列表及返回值
xQueue 目标队列的句柄。这个句柄即是调用 xQueueCreate()创建该队列时的返回值。
pvItemToQueue 发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域。
pxHigherPriorityTaskWoken 对某个队列而言,可能有不止一个任务处于阻塞态在等待其数据有效。调用 xQueueSendToFrontFromISR()
或 xQueueSendToBackFromISR()会使得队列数据变为有效,所以会让其中一个等待任务切出阻塞态。如果调用这两个 API 函数使得一个任务解除阻塞,并且这个任务的优先级高于当前任务(也就是被中断的任务),那么 API 会在函数内部将*pxHigherPriorityTaskWoken 设为 pdTRUE。如果这两个 API 函数将此值设为 pdTRUE,则在中断退出前应当进行一次上下文切换。这样才能保证中断直接返回到就绪态任务中优先级最高的任务中。
返回值 有两个可能的返回值:

  1. pdPASS
    返回 pdPASS 只会有一种情况, 那就是数据被成功发送到队列中。
  2. errQUEUE_FULL
    如 果 由 于 队 列 已 满 而 无 法 将 数 据 写 入 , 则 将 返 回errQUEUE_FULL。
    队列在中断服务中发送或接收数据示例
//产生整数任务
static void vIntegerGenerator( void *pvParameters )
{
	portTickType xLastExecutionTime;
	unsigned portLONG ulValueToSend = 0;
	int i;
	/* 初始化变量,用于调用 vTaskDelayUntil(). */
	xLastExecutionTime = xTaskGetTickCount();
	for( ;; )
	{
		/* 这是个周期性任务。进入阻塞态,直到该再次运行的时刻。此任务每200毫秒执行一次 */
		vTaskDelayUntil( &xLastExecutionTime, 200 / portTICK_RATE_MS );
		/* 连续五次发送递增数值到队列。这此数值将在中断服务例程中读出。中断服务例程会将队列读空,所
		以此任务可以确保将所有的数值都发送到队列。因此不需要指定阻塞超时时间 */
		for( i = 0; i < 5; i++ )
		{
		xQueueSendToBack( xIntegerQueue, &ulValueToSend, 0 );
		ulValueToSend++;
	}
	/* 产生中断,以让中断服务例程读取队列 */
	vPrintString( "Generator task - About to generate an interrupt.\r\n" );
	__asm{ int 0x82 } /* This line generates the interrupt. */
	vPrintString( "Generator task - Interrupt generated.\r\n\r\n\r\n" );
	}
}
//中断服务函数,将
static void __interrupt __far vExampleInterruptHandler( void )
{
	static portBASE_TYPE xHigherPriorityTaskWoken;
	static unsigned long ulReceivedNumber;
	/* 这些字符串被声明为static const,以保证它们不会被定位到ISR的栈空间中,即使ISR没有运行它们也是存
	在的 */
	static const char *pcStrings[] =
	{
		"String 0\r\n",
		"String 1\r\n",
		"String 2\r\n",
		"String 3\r\n"
	};
	xHigherPriorityTaskWoken = pdFALSE;
	/* 重复执行,直到队列为空 */
	while( xQueueReceiveFromISR( xIntegerQueue,&ulReceivedNumber,&xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
	{
		/* 截断收到的数据,保留低两位(数值范围0到3).然后将索引到的字符串指针发送到另一个队列 */
		ulReceivedNumber &= 0x03;
		xQueueSendToBackFromISR( xStringQueue,
		&pcStrings[ ulReceivedNumber ],
		&xHigherPriorityTaskWoken );
	}
	/* 被队列读写操作解除阻塞的任务,其优先级是否高于当前任务?如果是,则进行任务上下文切换 */
	if( xHigherPriorityTaskWoken == pdTRUE )
	{
		/* 说明:在实际使用中, ISR中强制上下文切换的宏依赖于具体移植。此处调用的是基于Open Watcom
		DOS移植的宏。其它平台下的移植可能有不同的语法要求。对于实际使用的平台,请参如数对应移植自带
		的示例程序,以决定正确的语法和符号。 */
		portSWITCH_CONTEXT();
	}
}
//接收中断服务函数中的字符串
static void vStringPrinter( void *pvParameters )
{
	char *pcString;
	for( ;; )
	{
		/* Block on the queue to wait for data to arrive. */
		xQueueReceive( xStringQueue, &pcString, portMAX_DELAY );
		/* Print out the string received. */
		vPrintString( pcString );
	}
}
//main函数内容
int main( void )
{
	/* 队列使用前必须先创建。本例中创建了两个队列。一个队列用于保存类型为unsigned long的变量,另一
	个队列用于保存类型为char*的变量。两个队列的深度都为10。在实际应用中应当检测返回值以确保队列创建
	成功 */
	xIntegerQueue = xQueueCreate( 10, sizeof( unsigned long ) );
	xStringQueue = xQueueCreate( 10, sizeof( char * ) );
	/* 安装中断服务例程。 */
	_dos_setvect( 0x82, vExampleInterruptHandler );
	/* 创建任务用于往中断服务例程中发送数值。此任务优先级为1 */
	xTaskCreate( vIntegerGenerator, "IntGen", 1000, NULL, 1, NULL );
	/* 创建任务用于从中断服务例程中接收字符串,并打印输出。此任务优先级为2 */
	xTaskCreate( vStringPrinter, "String", 1000, NULL, 2, NULL );
	/* Start the scheduler so the created tasks start executing. */
	vTaskStartScheduler();
	/* 如果一切正常, main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
	很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
	for( ;; );
}

中断嵌套

configKERNEL_INTERRUPT_PRIORITY 设置系统心跳时钟的中断优先级。如 果 在 移 植 中 没 有 使 用 常 量configMAX_SYSCALL_INTERRUPT_PRIORITY,那么需要调用中断安全版本 FreeRTOS API的中断都必须运行在此优先级上。
configMAX_SYSCALL_INTERRUPT_PRIORITY 设置中断安全版本 FreeRTOS API 可以运行的最高中断优先级。
建立一个全面的中断嵌套模型需要设置 configMAX_SYSCALL_INTERRUPT_PRIRITY为比 configKERNEL_INTERRUPT_PRIORITY更高的优先级。
任务优先级和中断优先级没有任何关联。
在这里插入图片描述
处于中断优先级 1 到 3(含)的中断会被内核或处于临界区的应用程序阻塞执行, 但是它们可以调用中断安全版本的 FreeRTOS API 函数。 处于中断优先级 4 及以上的中断不受临界区影响,所以其不会被内核的任何行为阻塞,可以立即得到执行——这是由微控制器本身对中断优先级的限定所决定的。通常 需 要 严 格 时 间 精 度 的 功 能 ( 如 电 机 控 制 ) 会 使 用 高 于configMAX_SYSCALL_INTERRUPT_PRIRITY 的优先级,以保证调度器不会对其中断响应时间造成抖动。不需要调用任何 FreeRTOS API 函数的中断,可以自由地使用任意优先级。
对 ARM Cortex M3 用户的一点提示
Cortex M3 使用低优先级号数值表示逻辑上的高优先级中断。这显得不是那么直观,所以很容易被忘记。如果你想对某个中断赋予低优先级,则必须使用一个高优先级号数值。
千万不要给它指定优先级号 0(或是其它低优先级号数值),因为这将会使得这个 中 断 在 系 统 中 拥 有 最 高 优 先 级 — — 如 果 这 个 优 先 级 高 于configMAX_SYSCALL_INTERRUPT_PRIRITY,将很可能导致系统崩溃。
Cortex M3 内核的最低优先级为 255,但是不同的 Cortex M3 处理器厂商实现的优先级位数不尽相同,而各自的配套库函数也使用了不同的方式来支持中断优先级。比如STM32, ST 的驱动库中将最低优先级指定为 15,而最高优先级指定为 0。

资源管理

读改写操作

/* The C code being compiled. */
155: PORTA |= 0x01;
/* The assembly code produced. */
0x00000264 481C LDR R0,[PC,#0x0070] ; Obtain the address of PORTA
0x00000266 6801 LDR R1,[R0,#0x00] ; Read the value of PORTA into R1
0x00000268 2201 MOV R2,#0x01 ; Move the absolute constant 1 into R2
0x0000026A 4311 ORR R1,R2 ; OR R1 (PORTA) with R2 (constant 1)
0x0000026C 6001 STR R1,[R0,#0x00] ; Store the new value back to PORTA

上述是一个或运算对应的汇编代码
这是一个”非原子”操作,因为完成整个操作需要不止一条指令,所以操作过程可能
被中断。考虑如下情形,两个任务都试图更新一个名为 PORTA 的内存映射寄存器:
1、任务 A 把 PORTA 的值加载到寄存器中——整个流程的读操作。
2、在任务 A 完成整个流程的改和写操作之前,被任务 B 抢占。
3、任务 B 完整的执行了对 PORTA 的更新流程,然后进入阻塞态。
4、任务 A 从被抢占处继续执行。其修改了一个 PORTA 的拷贝,这其实只是寄存器在任务 A 回写到 PORTA 之前曾经保存过的值。任务 A 更新并回写了一个过期的 PORTA 寄存器值。在任务 A 获得拷贝与更新回写之间,任务 B 又修改了 PORTA 的值。而之后任务 A 对 PORTA 的回写操作,覆盖了任务 B 对 PORTA 进行的修改结果,效果上等同于破坏了 PORTA 寄存器的值。

访问外设

访问外设
当一个任务在使用某个资源的过程中,即还没有完全结束对资源的访问时,便被切出运行态,使得资源处于非一致,不完整的状态。
考虑如下情形,有两个任务都试图往一个 LCD 中写数据:
1、任务 A 运行,并往 LCD 写字符串”Hello world”。
2、 任务 A 被任务 B 抢占,但此时字符串才输出到”Hello w”。
3、 任务 B 往 LCD 写”Abort, Retry, Fail?”,然后进入阻塞态。
4、 任务 A 从被抢占处继续执行,完成剩余的字符输出——“orld”。现在 LCD 显示的是被破坏了的字符串”Hello wAbort, Retry, Fail?orld”。

变量非原子访问

更新结构体的多个成员变量,或是更新的变量其长度超过了架构体系的自然长度(比如,更新一个 16 位机上的 32 位变量)均是非原子操作的例子。如果这样的操作被中断,将可能导致数据损坏或丢失。

函数重入

如果一个函数可以安全地被多个任务调用,或是在任务与中断中均可调用,则这个函数是可重入的。每个任务都单独维护自己的栈空间及其自身在的内存寄存器组中的值。 如果一个函数除了访问自己栈空间上分配的数据或是内核寄存器中的数据外,不会访问其它任何数据,则这个函数就是不可重入的。
可重入的函数必须满足以下三个条件:
(1)可以在执行的过程中可以被打断。
(2)被打断之后,在该函数一次调用执行完之前,可以再次被调用(或进入,reentered)。
(3)再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。

互斥

访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用”互斥”技术以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直至这个资源又恢复到完整状态。FreeRTOS 提供了多种特性用以实现互斥,但是最好的互斥方法(如果可能的话,任何时候都当如此)还是通过精心设计应用程序,尽量不要共享资源,或者是每个资源都通过单任务访问。

临界区与挂起调度器

基本临界区

基本临界区是指宏 taskENTER_CRITICAL()与 taskEXIT_CRITICAL()之间的代码区间

//临界区代码示例1
/* 为了保证对PORTA寄存器的访问不被中断,将访问操作放入临界区。
进入临界区 */
taskENTER_CRITICAL();
/* 在taskENTER_CRITICAL() 与 taskEXIT_CRITICAL()之间不会切换到其它任务。 中断可以执行,也允许
嵌套,但只是针对优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断 – 而且这些中断不允许访问
FreeRTOS API 函数. */
PORTA |= 0x01;
/* 我们已经完成了对PORTA的访问,因此可以安全地离开临界区了。 */
taskEXIT_CRITICAL();
//临界区代码示例2
void vPrintString( const portCHAR *pcString )
{
	/* 往stdout中写字符串,使用临界区这种原始的方法实现互斥。 */
	taskENTER_CRITICAL();
	{
		printf( "%s", pcString );
		fflush( stdout );
	}
	taskEXIT_CRITICAL();
	/* 允许按任意键停止应用程序运行。实际的应用程序如果有使用到键值,还需要对键盘输入进行保护。 */
	if( kbhit() )
	{
		vTaskEndScheduler();
	}
}

临界区是提供互斥功能的一种非常原始的实现方法。临界区的工作仅仅是简单地把中断全部关掉,或是关掉优先级在 configMAX_SYSCAL_INTERRUPT_PRIORITY 及以下的中断——依赖于具体使用的 FreeRTOS 移植。抢占式上下文切换只可能在某个中断中完成,所以调用 taskENTER_CRITICAL()的任务可以在中断关闭的时段一直保持运行态,直到退出临界区。临界区必须只具有很短的时间,否则会反过来影响中断响应时间。在每次调用taskENTER_CRITICAL()之后,必须尽快地配套调用一个 taskEXIT_CRITICAL()。从这个角度来看,对标准输出的保护不应当采用临界区(如程序清单 61 所示),因为写终端在时间上会是一个相对较长的操作。对于这种情况,可以采用挂起调度器的方式实现。

挂起(锁定)调度器

也可以通过挂起调度器来创建临界区。挂起调度器有些时候也被称为锁定调度器。基本临界区保护一段代码区间不被其它任务或中断打断。由挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的。如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式。但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作。
通过调用 vTaskSuspendAll()来挂起调度器。挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行。在调度器处于挂起状态时,不能调用 FreeRTOS API 函数。
任务调度器挂起函数原型

void vTaskSuspendAll( void );

任务调度器唤醒函数原型

portBASE_TYPE xTaskResumeAll( void );

返回值 在调度器挂起过程中,上下文切换请求也会被挂起,直到调度器被唤醒后才会得到执行。如果一个挂起的上下文切换请求在xTaskResumeAll()返回前得到执行,则函数返回 pdTRUE。在其它情况下, xTaskResumeAll()返回 pdFALSE。
任务调度器挂起示例

void vPrintString( const portCHAR *pcString )
{
	/* Write the string to stdout, suspending the scheduler as a method
	of mutual exclusion. */
	vTaskSuspendScheduler();
	{
		printf( "%s", pcString );
		fflush( stdout );
	}
	xTaskResumeScheduler();
}

互斥量

互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。单词MUTEX(互斥量)源于”MUTual EXclusion”。在用于互斥的场合,互斥量从概念上可看作是与共享资源关联的令牌。一个任务想要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。当令牌持有者完成资源使用,其必须马上归还(Give)令牌。只有归还了令牌,其它任务才可能成功持有,也才可能安全地访问该共享资源。一个任务除非持有了令牌,否则不允许访问共享资源。
互斥量示意图如下所示
互斥量工作示意图
互斥量和二值信号量的区别
1、用于互斥的信号量必须归还。
2、用于同步的信号量通常是完成同步之后便丢弃,不再归还。
创建互斥信号量函数原型:

xSemaphoreHandle xSemaphoreCreateMutex( void );

返回值 如果返回 NULL 表示互斥量创建失败。原因是内存堆空间不足导致FreeRTOS 无法为互斥量分配结构数据空间。返回值非NULL表示互斥量创建成功,返回值应保存起来作为该互斥量的句柄。
使用互斥量来代替基本临界区代码示例:

static void prvNewPrintString( const portCHAR *pcString )
{
	/* 互斥量在调度器启动之前就已创建,所以在此任务运行时信号量就已经存在了。
	试图获得互斥量。如果互斥量无效,则将阻塞,进入无超时等待。 xSemaphoreTake()只可能在成功获得互
	斥量后返回,所以无需检测返回值。如果指定了等待超时时间,则代码必须检测到xSemaphoreTake()返回
	pdTRUE后,才能访问共享资源(此处是指标准输出)。 */
	xSemaphoreTake( xMutex, portMAX_DELAY );
	{
	/* 程序执行到这里表示已经成功持有互斥量。现在可以自由访问标准输出,因为任意时刻只会有一个任
	务能持有互斥量。 */
	printf( "%s", pcString );
	fflush( stdout );
	/* 互斥量必须归还! */
	}
	xSemaphoreGive( xMutex );
}

使用互斥量示意图
使用互斥量示意图

优先级反转

上图展现出了采用互斥量提供互斥功能的潜在缺陷之一。在这种可能的执行流程描述中,高优先级的任务 2 竟然必须等待低优先级的任务 1 放弃对互斥量的持有权。高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。这是一种不合理的行为方式,如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行!
优先级反转的一种最坏情况
优先级反转带来的最坏情况
优先级继承最小化优先级反转带来的影响
优先级继承最小化优先级反转带来的影响

在设计程序时最好优先考虑避免优先级反转的情况。

死锁

死锁是利用互斥量提供互斥功能的另一个潜在缺陷。 Deadlock 有时候会被更戏剧性地称为”deadly embrace(抱死)”。当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁。考虑如下情形,任务 A 与任务 B 都需要获得互斥量 X 与互斥量 Y 以完成各自的工作:

  1. 任务 A 执行,并成功获得了互斥量 X。
  2. 任务 A 被任务 B 抢占。
  3. 任务 B 成功获得了互斥量 Y,之后又试图获取互斥量 X——但互斥量 X 已经被任务 A 持有,所以对任务 B 无效。任务 B 选择进入阻塞态以等待互斥量 X 被释放。
  4. 任务 A 得以继续执行。其试图获取互斥量 Y——但互斥量 Y 已经被任务 B持有而对任务 A 无效。任务 A 也选择进入阻塞态以等待互斥量 Y 被释放。
    这种情形的最终结局是,任务 A 在等待一个被任务 B 持有的互斥量,而任务 B 也在等待一个被任务 A 持有的互斥量。死锁于是发生,因为两个任务都不可能再执行下去了。避免死锁的方法就是在设计阶段避免这种潜在风险。

守护任务

采用守护任务可以避免优先级反转和死锁的情况,守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的资源——其它任务要访问该资源只能间接地通过守护任务提供的服务。
下面代码演示使用守护任务来实现终端输出字符串,守护任务使用了一个 FreeRTOS 队列来对终端实现串行化访问。该任务内部实现不必考虑互斥,因为它是唯一能够直接访问终端的任务。守护任务大部份时间都在阻塞态等待队列中有信息到来。当一个信息到达时,守护任务仅仅简单地将收到的信息写到标准输出上,然后又返回阻塞态,继续等待下一条信息地到来。心跳钩子函数(回调函数),由心跳中断调用,要挂接心跳钩子函数需做以下配置:
1、设置 FreeRTOSConfig.h 中的常量 configUSE_TICK_HOOK 为 1。
2、提供钩子函数的具体实现(要求使用以下函数原型)。

void vApplicationTickHook( void );

心跳钩子函数在系统心跳中断的上下文上执行,所以必须保证非常短小,适度占用栈空间,并且不要调用任何名字不带后缀”FromISR”的 FreeRTOS API 函数。

//守护任务函数,将打印队列中的数据打印出来
static void prvStdioGatekeeperTask( void *pvParameters )
{
	char *pcMessageToPrint;
	/* 这是唯一允许直接访问终端输出的任务。任何其它任务想要输出字符串,都不能直接访问终端,而是将要
	输出的字符串发送到此任务。并且因为只有本任务才可以访问标准输出,所以本任务在实现上不需要考虑互斥
	和串行化等问题。 */
	for( ;; )
	{
		/* 等待信息到达。指定了一个无限长阻塞超时时间,所以不需要检查返回值 – 此函数只会在成功收到
		消息时才会返回。 */
		xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
		/* 输出收到的字符串。 */
		printf( "%s", pcMessageToPrint );
		fflush( stdout );
		/* Now simply go back to wait for the next message. */
	}
}
//将数据放到打印队列中去
static void prvStdioGatekeeperTask( void *pvParameters )
{
	char *pcMessageToPrint;
	/* 这是唯一允许直接访问终端输出的任务。任何其它任务想要输出字符串,都不能直接访问终端,而是将要
	输出的字符串发送到此任务。并且因为只有本任务才可以访问标准输出,所以本任务在实现上不需要考虑互斥
	和串行化等问题。 */
	for( ;; )
	{
		/* 等待信息到达。指定了一个无限长阻塞超时时间,所以不需要检查返回值 – 此函数只会在成功收到
		消息时才会返回。 */
		xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
		/* 输出收到的字符串。 */
		printf( "%s", pcMessageToPrint );
		fflush( stdout );
		/* Now simply go back to wait for the next message. */
	}
}
//心跳钩子函数仅仅是简单地对其被调用次数进行计数,当计数至 200 时就向守护任务发送信息
void vApplicationTickHook( void )
{
	static int iCount = 0;
	portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
	/* Print out a message every 200 ticks. The message is not written out
	directly, but sent to the gatekeeper task. */
	iCount++;
	if( iCount >= 200 )
	{
		/* In this case the last parameter (xHigherPriorityTaskWoken) is not
		actually used but must still be supplied. */
		xQueueSendToFrontFromISR( xPrintQueue,&( pcStringsToPrint[ 2 ] ),&xHigherPriorityTaskWoken );
		/* Reset the count ready to print out the string again in 200 ticks
		time. */
		iCount = 0;
	}
}
//main()函数创建队列和所有任务,然后启动调度器
/* 定义任务和中断将会通过守护任务输出的字符串。 */
static char *pcStringsToPrint[] =
{
"Task 1 ****************************************************\r\n",
"Task 2 ----------------------------------------------------\r\n",
"Message printed from the tick hook interrupt ##############\r\n"
};
/*-----------------------------------------------------------*/
/* 声明xQueueHandle变量。这个变量将会用于打印任务和中断往守护任务发送消息。 */
xQueueHandle xPrintQueue;
/*-----------------------------------------------------------*/
int main( void )
{
	/* 创建队列,深度为5,数据单元类型为字符指针。 */
	xPrintQueue = xQueueCreate( 5, sizeof( char * ) );
	/* 为伪随机数发生器产生种子。 */
	srand( 567 );
	/* Check the queue was created successfully. */
	if( xPrintQueue != NULL )
	{
		/* 创建任务的两个实例,用于向守护任务发送信息。任务入口参数传入需要输出的字符串索引号。这两
		个任务具有不同的优先级,所以高优先级任务有时会抢占低优先级任务。 */
		xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );
		xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );
		/* 创建守护任务。这是唯一一个允许直接访问标准输出的任务。 */
		xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );
		/* Start the scheduler so the created tasks start executing. */
		vTaskStartScheduler();
	}
	/* 如果一切正常, main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
	很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
	for( ;; );
}

守护任务的优先级低于打印任务——所以发送到守护任务的消息会一直保持在队列中,直到两个打印任务都进入阻塞态。在一些情况下,需要给守护任务赋予一个较高的优先级,消息就可以得到更快的处理——但这样做会由于守护任务的开销使得低优先级任务被推迟,直到守护任务完成对受其保护的资源的访问。

内存分配

概述

每当任务,队列或是信号量被创建时,内核需要进行动态内存分配。虽然可以调用标准的 malloc()与 free()库函数,但必须承担以下若干问题:

  1. 这两个函数在小型嵌入式系统中可能不可用。
  2. 这两个函数的具体实现可能会相对较大,会占用较多宝贵的代码空间。
  3. 这两个函数通常不具备线程安全特性。
  4. 这两个函数具有不确定性。每次调用时的时间开销都可能不同。
  5. 这两个函数会产生内存碎片。
  6. 这两个函数会使得链接器配置得复杂。
    freeRTOS提供了相应的内存分配和释放函数,其调用 pvPortMalloc()而不是直接调用 malloc();当释放内存时,调用 vPortFree()而不是直接调用 free()。 pvPortMalloc()具有与 malloc()相同的函数原型; vPortFree()也具有与 free()相同的函数原型。
    堆大小由FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE决定

heap1方式

能实现内存分配,但没有实现内存释放,如果程序不需要删除任务、队列或者信号,就可以采用这种方式,这种方式容易产生内存碎片。

heap2方式

支持内存释放,增加最佳匹配算法
最佳匹配算法保证 pvPortMalloc()会使用最接近请求大小的空闲内存块。比如,考虑以下情形:
7. 堆空间中包含了三个空闲内存块,分别为 5 字节, 25 字节和 100 字节大小。
8. pvPortMalloc()被调用以请求分配 20 字节大小的内存空间。匹配请求字节数的最小空闲内存块是具有25字节大小的内存块——所以pvPortMalloc()会将这个 25 字节块再分为一个 20 字节块和一个 5 字节块 3,然后返回一个指向 20 字节块的指针。剩下的 5 字节块则保留下来,留待以后调用 pvPortMalloc()时使用。Heap_2.c 并不会把相邻的空闲块合并成一个更大的内存块,所以会产生内存碎片——如果分配和释放的总是相同大小的内存块,则内存碎片就不会成为一个问题。Heap_2.c 适合用于那些重复创建与删除具有相同栈空间任务的应用程序。

heap3方式

调用标准库的malloc和free,但是通过短暂的挂起任务调度器使得函数调用线程安全特性。此时的内存堆空间大小不受 configTOTAL_HEAP_SIZE 影响,而是由链接器配置决定
代码示例如下:

void *pvPortMalloc( size_t xWantedSize )
{
	void *pvReturn;
	vTaskSuspendAll();
	{
		pvReturn = malloc( xWantedSize );
	}
	xTaskResumeAll();
	return pvReturn;
	}
	void vPortFree( void *pv )
	{
		if( pv != NULL )
		{
		vTaskSuspendAll();
		{
			free( pv );
		}
		xTaskResumeAll();
	}
}

错误排查

栈溢出

栈溢出问题是常见的错误,当调用标准 C 库函数时,栈空间使用量可能会急剧上升,特别是 IO 与字符串处理函数,比如 sprintf()。在 FreeRTOS 下载包中有一个名为 printf-stdarg.c 的文件。这个文件实现了一个栈效率优化版的小型 sprintf(),可以用来代替标准 C 库函数版本。在大多数情况下,这样做可以使得调用 sprintf()及相关函数的任务对栈空间的需求量小很多。
uxTaskGetStackHighWaterMark()主要用来查询指定任务的运行历史中, 其栈空间还差多少就要溢出。这个值被称为栈空间的”高水线(High Water Mark)”。
函数原型如下:

unsigned portBASE_TYPE uxTaskGetStackHighWaterMark( xTaskHandle xTask );

xTask : 被查询的任务句柄,如果传入的值为NULL,则任务查询的是自身栈空间的高水线。
返回值:任务栈空间的实际使用量会随着任务执行和中断处理过程上下浮动。uxTaskGetStackHighWaterMark()返回从任务启动执行开始的运行历史中,栈空间具有的最小剩余量。这个值即是栈空间使用达到最深时的剩下的未使用的栈空间。这个值越是接近 0,则这个任务就越是离栈溢出不远了。

运行时栈溢出检测

FreeRTOS 包含两种运行时栈侦测机制,由 FreeRTOSConfig.h 中的配置常量configCHECK_FOR_STACK_OVERFLOW 进行控制。这两种方式都会增加上下切换开销。
在 FreeRTOSConfig.h 中把 configCHECK_FOR_STACK_OVERFLOW 设为 1 或 2
提供钩子函数的具体实现
钩子函数原型如下所示:

void vApplicationStackOverflowHook( xTaskHandle *pxTask, signed portCHAR *pcTaskName );

形参为任务句柄和任务名。

运行时栈检测——方法1

当 configCHECK_FOR_STACK_OVERFLOW 设置为 1 时选用方法 1。
任务被交换出去的时候,该任务的整个上下文被保存到它自己的栈空间中。这时任务栈的使用应当达到了一个峰值。当 configCHECK_FOR_STACK_OVERFLOW 设为1 时,内核会在任务上下文保存后检查栈指针是否还指向有效栈空间。一旦检测到栈指针的指向已经超出任务栈的有效范围,栈溢出钩子函数就会被调用。
方法1执行速度比较快,但上下文切换中的栈溢出无法检测到。

运行时栈检测——方法2

将 configCHECK_FOR_STACK_OVERFLOW 设为 2 就可以选用方法 2。方法 2
在方法 1 的基础上进行了一些补充。当创建任务时,任务栈空间中就预置了一个标记。方法 2 会检查任务栈的最后 20个字节,查看预置在这里的标记数据是否被覆盖。如果最后 20 个字节的标记数据与预设值不同,则栈溢出钩子函数就会被调用。方法 2 没有方法 1 的执行速度快,但测试仅仅 20 个字节相对来说也是很快的。这种方法应该可以侦测到任何时候发生的栈溢出,虽然理论上还是有可能漏掉一些情况,
但这些情况几乎是不可能发生的

其他错误检测

在一个 Demo 应用程序中增加了一个简单的任务,导致应用程序崩溃

任务创建时需要在内存堆中分配空间。许多 Demo 应用程序定义的堆空间大小只够用于创建 Demo 任务——所以当任务创建完成后,就没有足够的剩余空间来增加其它的任务,队列或信号量。
空闲任务是在 vTaskStartScheduler()调用中自动创建的。如果由于内存不足而无法创建空闲任务, vTaskStartScheduler()会直接返回。在调用 vTaskStartScheduler()后加上一条空循环[for(;😉]可以使这种错误更加容易调试。如果要添加更多的任务,可以增加内存堆空间大小,或是删掉一些已存在的 Demo任务。

在中断中调用一个 API 函数,导致应用程序崩溃

除了具有后缀为”FromISR”函数名的 API 函数,千万不要在中断服务函数中调用其它 API 函数。

有时候应用程序会在中断服务例程中崩溃

需要做的第一件事是检查中断是否导致了栈溢出。在不同的移植平台和不同的编译器上,中断的定义和使用方法是不尽相同的——所
以,需要做的第二件事是检查在中断服务例程中使用的语法,宏和调用约定是否符合Demo 程序的文档描述,以及是否和 Demp 程序中提供的中断服务例程范例相同。如果应用程序工作在 Cotex M3 上,需要确定给中断指派优先级时,使用低优先级号数值表示逻辑上的高优先级中断,因为这种方式不太直观,所以很容易被忘记。一个比较常见的错误就是,在优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY的中断中调用了 FreeRTOS API 函数。

在启动第一个任务时,调度器就崩溃了

如果使用的是 ARM7,那么请确定调用 vTaskStartScheduler()时处理器处于管理模式(Supervisor mode)。最简单的方式就是在 main()之前的 C 启动态码中将处理器设置为管理模式。 ARM7 的 Demo 应用程序就是这么做的。如果处理器不在管理模式下,调度器是无法启动的。

临界区无法正确嵌套

除了 taskENTER_CRITICA()和 taskEXIT_CRITICAL(),千万不要在其它地方修改控制器的中断使能位或优先级标志。这两个宏维护了一个嵌套深度计数,所以只有当所有的嵌套调用都退出后计数值才会为 0,也才会使能中断。

在调度器启动前应用程序就崩溃了

如果一个中断会产生上下文切换,则这个中断不能在调度器启动之前使能。这同样适用于那些需要读写队列或信号量的中断。在调度器启动之前,不能进行上下文切换。还有一些 API 函数不能在调度器启动之前调用。在调用 vTaskStartScheduler()之
前,最好是限定只使用创建任务,队列和信号量的 API 函数。

在调度器挂起时调用 API 函数,导致应用程序崩溃

调用 vTaskSuspendAll()使得调度器挂起,而唤醒调度器调用 xTaskResumeAll()。千万不要在调度器挂起时调用其它 API 函数。

函数原型 pxPortInitialiseStack()导致编译失败

每种移植都需要定义一个对应的宏,以把正确的内核头文件加入到工程中。如果编译函数原型 pxPortInitialiseStack()时出错,这种现象基本上可以确定是因为没有正确定义相应的宏。请参见附录 4 以获得更多信息。可以基本相应平台的 Demo 工程建立新的应用程序。这种方式就不用担心没有包含正确的文件,也不必担心没有正确地配置编译器选项。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小小许啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值