FreeRTOS随记
任务函数原型:
void ATaskFunction(void * pvParameters);
任务不允许从实现函数中返回.如果一个任务不再需要,可以用vTaskDelete()删除;
一个任务函数可以用来创建多个任务,各任务均是独立的执行实例,拥有属于自己的栈空间.
典型的任务函数结构:
void ATaskFunction( void *pvParameters ) { /* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变 量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务实 例将会共享这个变量。 */ int iVariableExample = 0; /* 任务通常实现在一个死循环中。 */ for( ;; ) { /* 完成任务功能的代码将放在这里。 */ } /* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除 的是当前任务 */ vTaskDelete( NULL ); }
在最简单的情况下,一个任务可以有两个状态:1.运行状态 2.非运行状态.
当某个任务处于运行状态时,处理器就正在执行它的代码;当一个任务处于非运行状态时,该任务就进行休眠,它的所有状态都被妥善保存,以便在它再次进入运行状态时可以继续执行之前的代码.
当任务恢复执行时,其将精确地从离开运行状态时正在准备执行的那一条指令开始执行.
任务从非运行状态转移到运行状态被称为”切入或切换入”,反之则称为”切出或切换出”
创建任务:vTaskCreate
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode, const signed portCHAR * const pcName, unsigned portSHORT usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask );
参数:
pvTaskCode:指向一个实现函数的指针.
pcName:描述性的任务名,这个参数不会被FreeRTOS使用,只是单纯用于辅助调试.
usStackDepth:分配的栈空间大小,注意不是Byte,而是word.比如32位宽的栈空间,usStackDepth为100则会分配400字节的栈空间(4bytes*100).
pvParameters:传入的参数
uxPriority:优先级(0为最低优先级,configMAX_PRIORITIES – 1)为最大优先级)
pxCreatedTask :用于传出任务的句柄。这个句柄将在 API 调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。
返回值:
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函数代替这个原始空循环。 */ } } }
int main( void ) { /* 创建第一个任务。需要说明的是一个实用的应用程序中应当检测函数xTaskCreate()的返回值,以确保任 务创建成功。 */ xTaskCreate( vTask1, /* 指向任务函数的指针 */ "Task 1", /* 任务的文本名字,只会在调试中用到 */ 1000, /* 栈深度 – 大多数小型微控制器会使用的值会比此值小得多 */ NULL, /* 没有任务参数 */ 1, /* 此任务运行在优先级1上. */ NULL ); /* 不会用到任务句柄 */ /* 启动调度器,任务开始执行 */ vTaskStartScheduler(); /* 如果一切正常,main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲 任务无法创建。第五章有讲述更多关于内存管理方面的信息 */ for( ;; ); }
在tick的周期性中断里,运行调度器.
configTICK_RATE_HZ:tick的中断频率,比如设为1000(即1KHZ),这是每1ms中断一次.
vTaskPrioritySet():在调度器启动后设置任务的优先级.
portTICK_RATE_MS:用于将以心跳为单位的时间值转化为以毫秒为单位的时间值.比如我要延时500毫秒,于是就可以这样 vTaskDelay( 500/ portTICK_RATE_MS )
一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务。一个高优先级但不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级.
如果一个任务正在等待某个事件,则称这个任务处于”阻塞态(blocked)”。阻塞态是非运行态的一个子状态。
任务可以进入阻塞态以等待以下两种不同类型的事件:
1. 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点
2. 同步事件——源于其它任务或中断的事件。
FreeRTOS 的队列,二值信号量,计数信号量,互斥信号量(recursive semaphore,递归信号量,本文一律称为互斥信号量,因为其主要用于实现互斥访问)和互斥量都可以用来实现同步事件
vTaskSuspend() :让一个任务进入挂起状态
vTaskResume() 或vTaskResumeFromISR(): 把 一 个 挂 起 状 态 的 任 务 唤 醒
大多数应用程序中都不会用到挂起状态。
空闲任务是在调度器启动时自动创建的,以保证至少有一个任务可运行
void vTaskDelay( portTickType xTicksToDelay );
参数:
xTicksToDelay :延迟多少个心跳周期.
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
vTaskDelayUntil() :类似vTaskDelay. 可以用于实现一个固定执行周期的需求.由于调用此函数的任务解除阻塞的时间是绝对时刻,比起相对于调用时刻的相对时间更精确(即比调用 vTaskDelay()可以实现更精确的周期性)。
参数:
pxPreviousWakeTime:此参数命名时假定 vTaskDelayUntil()用于实现某个任务以固定频率周期性执行。这种情况下 pxPreviousWakeTime保存了任务上一次离开阻塞态(被唤醒)的时刻。这个时刻被用作一个参考点来计算该任务下一次离开阻塞态的时刻。
xTimeIncrement:此参数命名时同样是假定 vTaskDelayUntil()用于实现某个任 务 以 固 定 频 率 周 期 性 执 行 —— 这 个 频 率 就 是 由xTimeIncrement 指定的。 xTimeIncrement 的 单 位 是 心 跳 周 期 , 可 以 使 用 常 量portTICK_RATE_MS 将毫秒转换为心跳周期。
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 ) ); } }
vTaskStartScheduler():调度器会自动创建一个空闲任务。空闲任务拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务进入运行态.
空闲任务钩子函数 :通过空闲任务钩子函数(或称回调,hook, or call-back),可以直接在空闲任务中添加应用程序相关的功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。
通常空闲任务钩子函数被用于:
1.执行低优先级,后台或需要不停处理的功能代码。
2. 测试处系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
3.将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式。
空闲任务钩子函数必须遵从以下规则
1. 绝不能阻或挂起。空闲任务只会在其它任务都不运行时才会被执行(除非有应用任务共享空闲任务优先级)。以任何方式阻塞空闲任务都可能导致没有任务能够进入运行态!
2. 如果应用程序用到了 vTaskDelete() AP 函数,则空闲钩子函数必须能够尽快返回。因为在任务被删除后,空闲任务负责回收内核资源。如果空闲任务一直运行在钩子函数中,则无法进行回收工作。
空闲任务钩子函数必须具有如下所示的函数名和函数原型:
void vApplicationIdleHook( void );
如果要使用空闲任务钩子, configUSE_IDLE_HOOK 必须定义为 1.
void vTaskDelete( xTaskHandle pxTaskToDelete );
参数:
pxTaskToDelete:
被删除任务的句柄(目标任务) —— 参考 xTaskCreate() API 函数的参数 pxCreatedTask 以了解如何得到任务句柄方面的信息。 任务可以通过传入 NULL 值来删除自己。
任务可以使用 API 函数 vTaskDelete()删除自己或其它任务。
任务被删除后就不复存在,也不会再进入运行态。
空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就是使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。
需要说明一点,只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显式地释放。
作为一种通用规则,完成硬实时功能的任务优先级会高于完成软件时功能任务的优先级。但其它一些因素,比如执行时间和处理器利用率,都必须纳入考虑范围,以保证应用程序不会超过硬实时的需求限制。
单调速率调度(Rate Monotonic Scheduling, RMS)是一种常用的优先级分配技术。其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。这种优先级分配方式被证明了可以最大化整个应用程序的可调度性(schedulability),但是运行时间不定以及并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。
队列Queue:
往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。而写队列也类似.
同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。
创建一个队列:
xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength,
unsigned portBASE_TYPE uxItemSize );
参数:
uxQueueLength:队列能够存储的最大单元数目,即队列深度.
uxItemSize:队列中数据单元的长度,以字节为单位。
返回值:
NULL 表示没有足够的堆空间分配给队列而导致创建失败。
非 NULL 值表示队列创建成功。此返回值应当保存下来,以作为操作此队列的句柄。
portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );
portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );
功能:如同函数名字字面意思,xQueueSendToBack()用于将数据发送到队列尾,xQueueSendToFront()用于将数据发送到队列首.
xQueueSend()等同xQueueSendToBack().
但 切 记 不 要 在 中 断 服 务 例 程 中 调 用 xQueueSendToFront() 或xQueueSendToBack()。系统提供中断安全版本的 xQueueSendToFrontFromISR()与xQueueSendToBackFromISR()用于在中断服务中实现相同的功能。
参数:
xQueue:目标队列的句柄。这个句柄即是调用 xQueueCreate()创建该队列时的返回值。
pvItemToQueue:发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域。
xTicksToWait:阻塞超时时间。如果在发送时队列已满,这个时间即是任务处于阻塞态等待队列空间有效的最长等待时间。
xQueueReceive()与xQueuePeek():相同点:用于从队列中接收数据.不同点:xQueuePeek不会从队列中改变(删除,存储顺序)数据,而xQueueReceive则会从队列中删除接收到的数据.
uxQueueMessagesWaiting():用于查询队列中当前有效数据单元个数。
taskYIELD():通知调度器现在就切换到其它任务,而不必等到本任务的时间片耗尽.
调用 taskYIELD()的任务转移到就绪态,
只有以”FromISR”或”FROM_ISR”结束的 API 函数或宏才可以在中断服务例程中。
在中断服务例程中使用队列:
信号量用于事件通信。而队列不仅可以用于事件通信,还可以用来传递数据。
xQueueSendToFrontFromISR(),xQueueSendToBackFromISR()与 xQueueReceiveFromISR()分别是
xQueueSendToFront(),xQueueSendToBack()与 xQueueReceive()的中断安全版本,专门用于中断服务例程中。
xQueueSendFromISR()完全等同于 xQueueSendToBackFromISR().
ISR越短越好.
ISR:interrupt service routine
创建一个二值信号量:
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );
参数:
xSemaphore :
创建的信号量
获取一个信号量.除互斥信号量外,所有类型的信号量都可以调用函数 xSemaphoreTake()来获取:
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );
参数:
xSemaphore:信号量
xTicksToWait:阻塞超时时间
返回值:
pdPASS:成功获得信号量
pdFALSE:未能获得信号量
给出一个信号量,除互斥信号量外,所有类型的信号量都可以调用函数 xSemaphoreTake()来获取:
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken );
参数:
xSemaphore:信号量
pxHigherPriorityTaskWoken:
如果调用 xSemaphoreGiveFromISR()使得一个任务解除阻塞,并且这个任务的优先级高于当前任务(也就是被中断的任务),那么 xSemaphoreGiveFromISR()会在 函 数 内 部 将 pxHigherPriorityTaskWoken 设 为pdTRUE。 如 果 xSemaphoreGiveFromISR() 将 此 值 设 为pdTRUE,则在中断退出前应当进行一次上下文切换。这样才能保证中断直接返回到就绪态任务中优先级最高的任务中。
计数信号量:相对于二值信号量,可防止信号丢失.
延迟处理任务正在在处理二值信号量时,如果此时又有中断发生,并在中断里给出信号,那么这个信号将会丢失,得不到处理.计数信号量可解决这个问题.
计数信号量的两种典型用法:
1.事件计数
中断服务例程给出信号量(计数值加1).延迟处理任务获取信号量(计数值减1).创建时计数值初始化为0
2.资源管理
一个任务要获取资源的控制权,其必须先获得(take)信号量(计数值减1),当计数值减至0,则表示没有可用资源.当任务利用资源完成后,将给出(giving)信号量(计数值加1).创建时计数值初始化为可用总和.
xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount, unsigned portBASE_TYPE uxInitialCount );
参数:
uxMaxCount:最大计数值.
uxInitialCount :信号量的初始值.
返回:非NULL表示成功;NULL表示堆上空间不足.
中断优先级是硬件控制的优先级,中断服务例程的执行会与之关联。任务并非运行在中断服务中,所以赋予任务的软件优先级与赋予中断源的硬件优先级之间没有任何关系。
互斥:
1.基本临界区
基本临界区是指宏 taskENTER_CRITICAL()与 taskEXIT_CRITICAL()之间的代码区间.
临界区的工作仅仅是简单地把中断全部关掉,或是关掉优先级在 configMAX_SYSCAL_INTERRUPT_PRIORITY 及以下的中断.
临界区必须只具有很短的时间,否则会反过来影响中断响应时间.
2.挂起(锁定)调度器
通过调用 vTaskSuspendAll()来挂起调度器.
通过调用 xTaskResumeAll()来唤醒调度器.
由挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的.
但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作.
挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行。
在调度器处于挂起状态时,不能调用 FreeRTOS API 函数.
3.互斥量(即二值信号量)
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源.
互斥量的缺点:优先级反转
创建一个互斥量:
xSemaphoreHandle xSemaphoreCreateMutex( void );
互斥量使用(take)后必须归还(giving)
例:
xSemaphoreTake( xMutex, portMAX_DELAY ); { /* 程序执行到这里表示已经成功持有互斥量。现在可以自由访问标准输出,因为任意时刻只会有一个任 务能持有互斥量。 */ printf( "%s", pcString ); fflush( stdout ); /* 互斥量必须归还! */ } xSemaphoreGive( xMutex );
FreeRTOS 将内存分配作为可移植层面.
当内核请求内存时,其调用 pvPortMalloc()而不是直接调用 malloc();当释放内存时,调用 vPortFree()而不是直接调用 free()。pvPortMalloc()具有与 malloc()相同的函数原型;vPortFree()也具有与 free()相同的函数原型。
Heap_1.c 实现了一个非常基本的 pvPortMalloc()版本,而且没有实现 vPortFree()。如果应用程序不需要删除任务,队列或者信号量,则具有使用 heap_1 的潜质。Heap_1总是具有确定性。
Heap_2.c 也是使用了一个由 configTOTAL_HEAP_SIZE 定义大小的简单数组。不同于 heap_1 的是,heap_2 采用了一个最佳匹配算法来分配内存,并且支持内存释放。
Heap_3.c 简单地调用了标准库函数 malloc()和 free(),但是通过暂时挂起调度器使得函数调用备线程安全特性。
unsigned portBASE_TYPE uxTaskGetStackHighWaterMark( xTaskHandle xTask );
uxTaskGetStackHighWaterMark()主要用来查询指定任务的运行历史中,其栈空间还差多少就要溢出.
返回值:
任务栈空间的实际使用量会随着任务执行和中断处理过程上下浮动。uxTaskGetStackHighWaterMark()返回从任务启动执行开始的运行历史中,栈空间具有的最小剩余量。这个值即是栈空间使用达到最深
时的剩下的未使用的栈空间。这个值越是接近 0,则这个任务就越是离栈溢出不远了。
把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量 后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。