目录
- 任务管理
- 任务函数
- 任务控制块
- 顶层任务状态
- 创建任务
- xTaskCreate
- xTaskCreateStatic
- xTaskCreateRestricted
- 任务优先级和心跳设置
- 心跳设置
- 优先级概述
- vTaskPrioritySet
- uxTaskPriorityGet
- 非运行态扩充
- 阻塞态vTaskDelay
- 挂起状态vTaskSuspend
- 就绪状态
- 完整的状态转换图
- 延迟函数vTaskDelay
- 空闲任务的产生
- vTaskDelayUntil
- 空闲任务与空闲任务钩子
- 空闲任务
- 空闲任务钩子函数vApplicationIdleHook
- 删除任务
- vTaskDelete
- 任务切换
- PendSV异常
- 任务切换
- 调度算法-简述
- 优先级抢占式调度
- 选择任务优先级
- 协作式调度
- 列表和列表项
- 列表
- 列表项
- 列表项的插入:
- 列表项末尾插入:
- 列表项的删除
- 列表的遍历
- 迷你列表项
- 就绪列表和延迟列表
- 队列管理
- 队列特性
- 使用队列
- xQueueCreate
- xQueueSendToBack与 xQueueSendToFront
- xQueueReceive与xQueuePeek
- uxQueueMessagesWaiting
- 工作于大型数据单元
- 中断管理
- 开关中断
- 用于中断屏蔽的特殊寄存器
- FreeRTOS开关中断
- 延迟中断处理
- 采用二值信号量同步
- xSemaphoreCreateBinary
- xSemaphoreTake
- xSemaphoreGiveFromISR
- 计数信号量
- 计数信号量两种用法
- xSemaphoreCreateCounting
- uxSemaphoreGetCount
- 在中断服务例程中使用队列
- xQueueSendToFrontFromISR与xQueueSendToBackFromISR
- xQueueReceiveFromISR与xQueuePeekFromISR
- 中断嵌套
- 资源管理
- 概述
- 临界区与挂起调度器
- 基本临界区
- 挂起(锁定)调度器
- 互斥信号量
- xSemaphoreCreateMutex
- 优先级反转
- 优先级继承
- 死锁
- 递归互斥量
- 创建递归互斥信号量
- xSemaphoreGiveRecursive
- xSemaphoreTakeRecursive
- 守护任务
- 心跳钩子函数vApplicationTickHook
- 软件定时器
- 定时器服务任务与队列
- 单次定时器和周期定时器
- 复位软件定时器
- 创建软件定时器
- 开启软件定时器
- 停止软件定时器
- 事件标志组
- 简介
- 创建事件标志组
- 设置事件位
- 获取事件标志组值
- 等待指定的事件位
- 任务通知
- 简介
- 发送任务通知
- 获取任务通知
- 用作二值信号量
- 用作计数信号量
- 用作消息邮箱
- 用作事件组
- 低功耗 Tickless 模式
- STM32F1 低功耗模式
- Tickless模式
- 内存管理
- 概述
- heap_1.c
- pvPortMalloc申请内存
- vPortFree释放内存
- heap_2.c
- 内存块详解
- prvHeapInit内存初始化
- prvInsertBlockIntoFreeList内存块插入
- pvPortMalloc申请内存
- vPortFree内存释放
- heap_3.c
- heap_4.c
- prvHeapInit内存初始化
- prvInsertBlockIntoFreeList内存块插入
- pvPortMalloc内存申请
- vPortFree内存释放
- heap_5.c
- 错误排查
- printf-stdarg.c
- 栈溢出
- uxTaskGetStackHighWaterMark
- configCHECK_FOR_STACK_OVERFLOW
任务管理
任务函数
一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量。
void ATaskFunction( void *pvParameters );
void ATaskFunction( void *pvParameters )
{
/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变
量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务
实例将会共享这个变量。 */
int iVariableExample = 0;
/* 任务通常实现在一个死循环中。 */
for( ;; )
{
/* 完成任务功能的代码将放在这里。 */
}
/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除
的是当前任务 */
vTaskDelete( NULL );
}
任务控制块
FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。
typedef tskTCB TCB_t;
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /*任务堆栈栈顶*/
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /*MPU 相关设置*/
#endif
ListItem_t xStateListItem; /*状态列表项*/
ListItem_t xEventListItem; /*状态列表项*/
UBaseType_t uxPriority; /*任务优先级*/
StackType_t *pxStack; /*任务堆栈起始地址*/
char pcTaskName[ configMAX_TASK_NAME_LEN ];/*任务名字*/
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; /*任务堆栈栈底*/
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*临界区嵌套深度*/
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) /* trace 或到 debug 的时候用到*/
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*任务基础优先级,优先级反转的时候用到*/
UBaseType_t uxMutexesHeld; /*任务获取到的互斥信号量个数*/
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) /*与本地存储有关*/
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; /*用来记录任务运行总时间*/
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent; /*定义一个 newlib 结构体变量 一般不使用*/
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 ) /*任务通知相关变量*/
volatile uint32_t ulNotifiedValue; /*任务通知值*/
volatile uint8_t ucNotifyState; /*任务通知状态*/
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
/** 用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为 pdTURE,
如果是动态创建的就为 pdFALSE */
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
根据任务句柄获取TCB
#define prvGetTCBFromHandle( pxHandle )
( ( ( pxHandle ) == NULL ) ? ( TCB_t * ) pxCurrentTCB : ( TCB_t * ) ( pxHandle ) )
顶层任务状态
当某个任务处于运行态时,处理器就正在执行它的代码。当一个任务处于非运行态时,该任务进行休眠,它的所有状态都被妥善保存,以便在下一次调试器决定让它进入运行态时可以恢复执行。当任务恢复执行时,其将精确地从离开运行态时正准备执行的那一条指令开始执行。
任务从非运行态转移到运行态被称为”切换入或切入(switched in)”或”交换入(swapped in)”。相反,任务从运行态转移到非运行态被称为”切换出或切出(switched out)”或”交换出(swapped out)”。FreeRTOS 的调度器是能让任务切入切出的唯一实体。
创建任务
xTaskCreate
宏 configSUPPORT_DYNAMIC_ALLOCATION 必须为 1。
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask );
pvTaskCode:指向任务的实现函数的指针(效果上仅仅是函数名)
typedef void (*TaskFunction_t)( void * );
pcName: 具有描述性的任务名。
应用程序可以通过定义常量 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)。如果 uxPriority 的值超过了(configMAX_PRIORITIES – 1),
将会导致实际赋给任务的优先级被自动封顶到最大合法值。
pxCreatedTask: 用于传出任务的句柄。这个句柄将在 API 调用中对该创建出来的任务进行引用,
比如改变任务优先级,或者删除任务。如果应用程序中不会用到这个任务的句柄,则
pxCreatedTask 可以被设为 NULL。
返回值:pdTRUE 任务创建成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
由于内存堆空间不足,FreeRTOS 无法分配足够的空间来保存任务结构数据和任务栈,因此无法创建任务。
//创建 LED0 任务
xTaskCreate(
(TaskFunction_t )led0_task, /*任务函数*/
(const char* )"led0_task", /*任务名字标签*/
(uint16_t )LED0_STK_SIZE, /*堆栈大小*/
(void* )NULL, /*传递函数的参数*/
(UBaseType_t )LED0_TASK_PRIO, /*优先级*/
(TaskHandle_t* )&LED0Task_Handler /*任务句柄,用于改变优先级、删除任务等*/
);
//LED0 任务函数
void led0_task(void *pvParameters)
{
while(1)
{
LED0=~LED0;
vTaskDelay(500);
}
vTaskDelete(NULL);
}
- 创建两个任务,并启动
void vTask1(void* pvParameters)
{
const char *pcTaskName = "Task 1 is runing\r\n";
for(;;)
{
printf("%s", pcTaskName);
vTaskDelay(500);
}
vTaskDelete(NULL);
}
void vTask2(void* pvParameters)
{
const char *pcTaskName = "Task 2 is runing\r\n";
for(;;)
{
printf("%s", pcTaskName);
vTaskDelay(500);
}
vTaskDelete(NULL);
}
int main(void)
{
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
vTaskStartScheduler(); /*启动调度器*/
for( ;; ); /*main函数应该不会执行到这里,如果执行到这里,
很可能是内存堆空间不足导致空闲任务无法创建*/
}
- 任务中创建任务
void vTask1(void* pvParameters)
{
const char *pcTaskName = "Task 1 is runing\r\n";
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL ); /*在任务中创建任务*/
for(;;)
{
printf("%s", pcTaskName);
vTaskDelay(500);
}
}
- 使用任务参数
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
pcTaskName = ( char * ) pvParameters;
for(;;)
{
printf("%s", pcTaskName);
vTaskDelay(500);
}
}
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main(void)
{
xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
vTaskStartScheduler();/*启动调度器*/
for(;;);
}
xTaskCreateStatic
将宏configSUPPORT_STATIC_ALLOCATION
定义为 1。如果使用静态方法的话需要用户实现两个函数 vApplicationGetIdleTaskMemory()
和vApplicationGetTimerTaskMemory()
。通过这两个函数来给空闲任务和定时器服务任务的任务堆栈和任务控制块分配内存。
static StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];//空闲任务任务堆栈
static StaticTask_t IdleTaskTCB;//空闲任务控制块
static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];//定时器服务任务堆栈
static StaticTask_t TimerTaskTCB;//定时器服务任务控制块
// 获取空闲任务地任务堆栈和任务控制块内存
//ppxIdleTaskTCBBuffer:任务控制块内存
//ppxIdleTaskStackBuffer:任务堆栈内存
//pulIdleTaskStackSize:任务堆栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
//获取定时器服务任务的任务堆栈和任务控制块内存
//ppxTimerTaskTCBBuffer:任务控制块内存
//ppxTimerTaskStackBuffer:任务堆栈内存
//pulTimerTaskStackSize:任务堆栈大小
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&TimerTaskTCB;
*ppxTimerTaskStackBuffer=TimerTaskStack;
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;
}
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer );
pxTaskCode: 任务函数。
pcName: 任务名字,一般用于追踪和调试,任务名字长度不能超过 configMAX_TASK_NAME_LEN。
usStackDepth: 任务堆栈大小,由于本函数是静态方法创建任务,所以任务堆栈由用户给出,
一般是个数组,此参数就是这个数组的大小。
pvParameters: 传递给任务函数的参数。
uxPriotiry: 任务优先级,范围 0~ configMAX_PRIORITIES-1。
puxStackBuffer: 任务堆栈,一般为数组,数组类型要为 StackType_t 类型。
pxTaskBuffer: 任务控制块。
返回值:
NULL: 任务创建失败,puxStackBuffer 或 pxTaskBuffer 为 NULL 的时候会导致这个
错误的发生。
其他值: 任务创建成功,返回任务的任务句柄。
xTaskCreateRestricted
此函数要求所使用的 MCU 有 MPU(内存保护单元),用此函数创建的任务会受到 MPU 的保护
BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition,
TaskHandle_t * pxCreatedTask );
pxTaskDefinition: 指向一个结构体 TaskParameters_t,这个结构体描述了任务的任务函数、
堆栈大小、优先级等。此结构体在文件 task.h 中有定义。
pxCreatedTask: 任务句柄。
返回值:
pdPASS: 任务创建成功。
其他值: 任务未创建成功,很有可能是因为 FreeRTOS 的堆太小了。
任务优先级和心跳设置
心跳设置
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。一个称为心跳(tick)中断的周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由FreeRTOSConfig.h
中的编译时配置常量 configTICK_RATE_HZ
进行配置。比如说,如果 configTICK_RATE_HZ
设为 100(HZ),则时间片长度为 10ms。常量 portTICK_RATE_MS
用于将以心跳为单位的时间值转化为以毫秒为单位的时间值。有效精度依赖于系统心跳频率。
优先级概述
xTaskCreate()
API 函数的参数uxPriority
为创建的任务赋予了一个初始优先级。这个侁先级可以在调度器启动后调用vTaskPrioritySet()
API 函数进行修改。文件
FreeRTOSConfig.h
中设定的编译时配置常量configMAX_PRIORITIES
的值,即是最多可具有的优先级数目。FreeRTOS
本身并没有限定这个常量的最大值,但这个值越大,则内核花销的内存空间就越多。所以总是建议将此常量设为能够用到的最小值。低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范围从 0 到
(configMAX_PRIORITES – 1)
。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。
vTaskPrioritySet
API 函数 vTaskPriofitySet()可以用于在调度器启动后改变任何任务的优先级。
void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );
pxTask: 被修改优先级的任务句柄(即目标任务),任务本身可以通过传入 NULL 值来修改自己的优先级。
uxNewPriority:目标任务将被设置到哪个优先级上。
如果设置的值超过了最大可用优先级(configMAX_PRIORITIES – 1),则会被自动封顶为最大值。
vTaskPrioritySet(NULL, 12); /*在自己任务函数中更该*/
vTaskPrioritySet(LED0Task_Handler, 12); /*任意地方更改*/
uxTaskPriorityGet
uxTaskPriorityGet() API 函数用于查询一个任务的优先级。
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );
pxTask: 被修改优先级的任务句柄(即目标任务),任务本身可以通过传入 NULL 值来修改自己的优先级。
返回值:被查询任务的当前优先级。
非运行态扩充
阻塞态vTaskDelay
任务可以进入阻塞态以等待以下两种不同类型的事件:
- 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如说某个任务可以进入阻塞态以延迟 10ms。
- 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞态以等待队列中有数据到来。
任务可以在进入阻塞态以等待同步事件时指定一个等待超时时间,这样可以有效地实现阻塞状态下同时等待两种类型的事件。比如说,某个任务可以等待队列中有数据到来,但最多只等 10ms。如果 10ms 内有数据到来,或是 10ms 过去了还没有数据到来,这两种情况下该任务都将退出阻塞态。
挂起状态vTaskSuspend
处于挂起状态的任务对调度器而言是不可见的。让一个任务进入挂起状态的唯一办法就是调用
vTaskSuspend()
API 函数;而把一个挂起状态的任务唤醒的唯一途径就是调用vTaskResume()
或vTaskResumeFromISR()
API 函数。
void vTaskSuspend( TaskHandle_t xTaskToSuspend);
void vTaskResume( TaskHandle_t xTaskToResume);
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume);
返回值:
pdTRUE: 恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),
这意味着在退出中断服务函数以后必须进行一次上下文切换。
pdFALSE: 恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的任务),
这意味着在退出中断服务函数的以后不需要进行上下文切换。
判断任务是否挂起
static BaseType_t prvTaskIsTaskSuspended( const TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
就绪状态
如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready,准备或就绪)状态。处于就绪态的任务能够被运行,但只是”准备(ready)”运行,而当前尚未运行
完整的状态转换图
延迟函数vTaskDelay
void vTaskDelay( portTickType xTicksToDelay );
xTicksToDelay:延迟多少个心跳周期。调用该延迟函数的任务将进入阻塞态,经
延迟指定的心跳周期数后,再转移到就绪态。
当某个任务调用 vTaskDelay( 100 )时,心跳计数值为 10,000,则该任务将保持在阻塞态,
直到心跳计数计到10,100。
vTaskDelay( 250 / portTICK_RATE_MS ); /*延迟250ms*/
空闲任务的产生
空闲任务是在调度器启动时自动创建的,以保证至少有一个任务可运行(至少有一个任务处于就绪态)
vTaskDelayUntil
vTaskDelayUntil()的参数就是用来指定任务离开阻塞态进入就绪态那一刻的精确心跳计数值。API 函数 vTaskDelayUntil()可以用于实现一个固定执行周期的需求。
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
pxPreviousWakeTime: 保存了任务上一次离开阻塞态(被唤醒)的时刻。这个时刻
被用作一个参考点来计算该任务下一次离开阻塞态的时刻。pxPreviousWakeTime 指向的变量值会在 API 函 数 vTaskDelayUntil() 调用过程中自动更新,应用程序除了该变量第一次初始化外,
通常都不要修改它的值。
xTimeIncrement: 指定周期性执行的固定频率,单位是心跳周期,可以使用常量
portTICK_RATE_MS 将毫秒转换为心跳周期。
// 实现任务以250毫秒为周期执行
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
portTickType xLastWakeTime;
pcTaskName = ( char * ) pvParameters;
/* 变量xLastWakeTime需要被初始化为当前心跳计数值。说明一下,这是该变量唯一一次被显式赋值。之后,
xLastWakeTime将在函数vTaskDelayUntil()中自动更新。 */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
printf("%s", pcTaskName);
/* 本任务将精确的以250毫秒为周期执行。同vTaskDelay()函数一样,时间值是以心跳周期为单位的,
可以使用常量portTICK_RATE_MS将毫秒转换为心跳周期。变量xLastWakeTime会在
vTaskDelayUntil()中自动更新,因此不需要应用程序进行显示更新。 */
vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
}
}
空闲任务与空闲任务钩子
空闲任务
处理器总是需要代码来执行——所以至少要有一个任务处于运行态。为了保证这一点,当调用
vTaskStartScheduler()
时,调度器会自动创建一个空闲任务。空闲任务拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务进入运行态。运行在最低优先级可以保证一旦有更高优先级的任务进入就绪态,空闲任务就会立即切出运行态。
空闲任务钩子函数vApplicationIdleHook
通过空闲任务钩子函数(或称回调,hook, or call-back),可以直接在空闲任务中添加应用程序相关的功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。
通常空闲任务钩子函数被用于:
- 执行低优先级,后台或需要不停处理的功能代码。
- 测试系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
- 将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式。
- 实现限制
- 绝不能阻塞或挂起。空闲任务只会在其它任务都不运行时才会被执行(除非有应用任务共享空闲任务优先级)。以任何方式阻塞空闲任务都可能导致没有任务能够进入运行态!
- 如果应用程序用到了
vTaskDelete()
API 函数,则空闲钩子函数必须能够尽快返回。因为在任务被删除后,空闲任务负责回收内核资源。如果空闲任务一直运行在钩子函数中,则无法进行回收工作。FreeRTOSConfig.h
中的配置常量configUSE_IDLE_HOOK
必须定义为 1,这样空闲任务钩子函数才会被调用。
空闲钩子函数必须命名为 vApplicationIdleHook
,无参数也无返回值。
void vApplicationIdleHook(void)
{
/*实现功能*/
}
删除任务
vTaskDelete
任务可以使用 API 函数 vTaskDelete()删除自己或其它任务。任务被删除后就不复存在,也不会再进入运行态。空闲任务的责任是要将分配给已删除任务的内存释放掉。使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显式地释放。
void vTaskDelete( xTaskHandle pxTaskToDelete );
pxTaskToDelete: 被删除任务的句柄(目标任务), 任务可以通过传入 NULL 值来删除自己。
任务切换
PendSV异常
PendSV异常是可挂起的系统调用,通过将中断控制和状态寄存器ICSR的bit28置为1来触发PendSV中断。他的挂起状态可以在更高优先级异常处理内处置,且会在高优先级处理完成之后执行。若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,这就很有利于上下文切换。
若上下文切换是在滴答定时器SysTick中完成的,SysTick异常可能会抢占IRQ的处理,如果执行了上下文切换,否则中断请求IRQ处理就会被延迟。
PendSV 异常将上下文切换请求延迟到所有其他 IRQ 处理都已经完成后,此时需要将 PendSV 设置为最低优先级。若 OS 需要执行上下文切换,他会设置 PendSV 的挂起状态,并在 PendSV 异常内执行上下文切换。FreeRTOS 系统的任务切换最终都是在 PendSV中断服务函数中完成的,UCOS 也是在 PendSV 中断中完成任务切换的。
不过在FreeRTOS中滴答定时器也会进行上下文切换,只不过是在滴答定时器产生PendSV中断来进行中断切换的。
任务切换
- SysTick中断函数
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI();
{
/*表明可以进行任务切换,存在同等优先级任务*/
if( xTaskIncrementTick() != pdFALSE )
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; /*产生PendSV中断*/
}
}
vPortClearBASEPRIFromISR();
}
- PendSV中断函数
通过保存寄存器的值和管理任务控制块 (TCB),它负责切换任务的上下文。调用 vTaskSwitchContext
函数进行实际的任务切换操作。
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp ; 从进程堆栈指针 (PSP) 读取值到寄存器 r0 中
isb ; 执行指令序列的内存同步
ldr r3, =pxCurrentTCB ; 获取当前任务控制块 (TCB) 的地址
ldr r2, [r3] ; 加载 r3 所指向的地址中的值到 r2 中,即获取当前任务的 TCB
stmdb r0!, {r4-r11} ; 保存剩余寄存器 r4-r11 的值,将值压入 r0 指向的内存地址(栈降序)
str r0, [r2] ; 将 r0 的值存储到 r2 所指向的内存地址中,保存当前任务的栈顶指针
stmdb sp!, {r3, r14} ; 将 r3 和 r14 的值保存在栈上(栈降序)
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY ; 获取 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值到 r0
msr basepri, r0 ; 将 r0 的值存储到 basepri 寄存器中,设置任务切换的最高优先级
dsb ; 执行数据同步
isb ; 执行指令序列的内存同步
bl vTaskSwitchContext ; 调用内部函数 vTaskSwitchContext,用于任务切换的实现
mov r0, #0 ; 将 0 存入 r0
msr basepri, r0 ; 将 r0 的值存储到 basepri 寄存器中,取消任务切换的最高优先级
ldmia sp!, {r3, r14} ; 恢复之前保存在栈上的 r3 和 r14 的值(栈升序)
ldr r1, [r3] ; 将 r3 所指向的地址中的值加载到 r1 中,
;即获取 pxCurrentTCB 的首个成员,即任务的栈顶指针
ldr r0, [r1] ; 将 r1 所指向的地址中的值加载到 r0 中,即获取任务栈顶指针
ldmia r0!, {r4-r11} ; 弹出栈顶指针中的寄存器 r4-r11 的值
msr psp, r0 ; 将 r0 的值存储到进程堆栈指针 (PSP) 中
isb ; 执行指令序列的内存同步
bx r14 ; 跳转到 r14 所指向的地址(返回指令)
nop ; 无操作
}
- taskYIELD
通知调度器现在就切换到其它任务,而不必等到本任务的时间片耗尽。
但是, 除非存在其他任务,其优先级等于或高于调用 taskYIELD() 的任务的优先级, 否则 RTOS 调度器将选择调用了 taskYIELD() 的任务并使其再次运行。
如果 configUSE_PREEMPTION 设置 为 1,则 RTOS 调度器将始终运行能够运行的优先级最高的任务,因此调用 taskYIELD() 将永远无法切换到一个优先级更高的任务。
- portYIELD_FROM_ISR
中断级的任务切换函数。
#define portEND_SWITCHING_ISR( xSwitchRequired ) \
if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
上面的两个任务切换函数,只能往更高或者同等的优先级切换,那么在判断存在更高或者同等优先级的任务的时候,那么就需要使用任务切换函数来切换任务。
调度算法-简述
优先级抢占式调度
- 每个任务都赋予了一个优先级。
- 每个任务都可以存在于一个或多个状态。
- 在任何时候都只有一个任务可以处于运行状态。
- 调度器总是在所有处于就绪态的任务中选择具有最高优先级的任务来执行。
这种类型的调度被称为固定优先级抢占式调度,每个任务被赋予优先级,不能被内核改变,只能通过任务修改。任务进入就绪态或者优先级被改变时,如果处于运行态的任务优先级更低,则该任务总是抢占当前运行的任务。任务可以在阻塞状态等待一个事件,当事件发生时其将自动回到就绪态。
选择任务优先级
作为一种通用规则,完成硬实时功能的任务优先级会高于完成软件时功能任务的优先级。但其它一些因素,比如执行时间和处理器利用率,都必须纳入考虑范围,以保证应用程序不会超过硬实时的需求限制。
单调速率调度(Rate Monotonic Scheduling, RMS)是一种常用的优先级分配技术。其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。这种优先级分配方式被证明了可以最大化整个应用程序的可调度性(schedulability),但是运行时间不定以及并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。
协作式调度
采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显式调用 taskYIELD()
时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可能会导致系统响应不够快。
实现混合调度方案也是可行的,这需要在中断服务例程中显式地进行上下文切换,从而允许同步事件产生抢占行为,但时间事件却不行。这样做的结果是得到了一个没有时间片机制的抢占式系统。
列表和列表项
列表
列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务。与列表相关的全部东西都在文件 list.c 和 list.h 中。
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE /*用于检查列表完整性,如果
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES = 1,会被设置为一个值,默认不开启*/
configLIST_VOLATILE UBaseType_t uxNumberOfItems; /*记录列表中列表项的数量*/
ListItem_t * configLIST_VOLATILE pxIndex; /*记录当前列表项索引号,用于遍历列表*/
MiniListItem_t xListEnd; /*列表中最后一个列表项,用来表示列表结束,为迷你列表项*/
listSECOND_LIST_INTEGRITY_CHECK_VALUE /*用于检查列表完整性,如果
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES = 1,会被设置为一个值,默认不开启*/
} List_t;
初始化:
void vListInitialise( List_t * const pxList );
列表项
列表项就是存放在列表中的项目,FreeRTOS 提供了两种列表项:列表项和迷你列表项。
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
configLIST_VOLATILE TickType_t xItemValue; /*列表项值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*指向下一个列表项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*指向前一个列表项*/
void * pvOwner; /*记录此链表项归谁拥有,通常是任务控制块*/
void * configLIST_VOLATILE pvContainer; /*记录此列表项归哪个列表*/
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE
};
typedef struct xLIST_ITEM ListItem_t;
TCB_t 中有两个变量 xStateListItem 和 xEventListItem,这两个变量的类型就是 ListItem_t。当创建一个任务以后 xStateListItem 的 pvOwner 变量就指向这个任务的任务控制块,表示 xSateListItem属于此任务。当任务就绪态以后 xStateListItem 的变量 pvContainer 就指向就绪列表,表明此列表项在就绪列表中。
列表项初始化:
void vListInitialiseItem( ListItem_t * const pxItem );
列表项的插入:
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem );
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
要插入的位置由列表项中成员变量 xItemValue 来决定。列表项的插入根据 xItemValue 的值按照升序的方式排列!依次插入ListItem1、ListItem2、ListItem3,最终的结果依旧是安装升序排列。
列表项末尾插入:
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem );
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
vListInsertEnd()是往列表的末尾添加列表项的。列表中的 pxIndex 成员变量是用来遍历列表的,pxIndex 所指向的列表项就是要遍历的开始列表项,也就是说 pxIndex 所指向的列表项就代表列表头!由于是个环形列表,所以新的列表项就应该插入到 pxIndex 所指向的列表项的前面。
默认列表:
末尾插入值为50的列表项:
列表项的删除
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove );
pxItemToRemove: 要删除的列表项。
返回值: 返回删除列表项以后的列表剩余列表项数目。
列表项的删除只是将指定的列表项从列表中删除掉,如果这个列表项是动态分配内存,并不会将这个列表项的内存给释放掉!
列表的遍历
listGET_OWNER_OF_NEXT_ENTRY();
每调用一次这个函数列表的 pxIndex 变量就会指向下一个列表项,
并且返回这个列表项的 pxOwner变量值。
此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。
迷你列表项
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检验完整性*/
configLIST_VOLATILE TickType_t xItemValue; /*记录列表列表项值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*指向下一个列表项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*指向上一个列表项*/
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
出迷你列表项只是比列表项少了几个成员变量,有时候不需要那么全的功能,使用迷你列表项即可,避免造成内存的浪费。列表结构体 List_t 中表示最后一个列表项的成员变量 xListEnd 就是 MiniListItem_t 类型的。
就绪列表和延迟列表
任务创建完成以后就会被添加到就绪列表中,FreeRTOS 使用不同的列表表示任务的不同状态
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*按优先级排列的就绪任务列表,相同优先级使用同一个列表 configMAX_PRIORITIES 表示最大优先级*/
static List_t xDelayedTaskList1;/*延时任务列表*/
static List_t xDelayedTaskList2;/*延时任务列表,用于连接已溢出当前计数的任务*/
static List_t * volatile pxDelayedTaskList; /*向当前活动的延时任务列表的指针*/
static List_t * volatile pxOverflowDelayedTaskList; /*指向溢出延时任务列表的指针*/
PRIVILEGED_DATA static List_t xPendingReadyList;/*用于存储在中断中被唤醒但尚未被调度执行的任务*/
添加任务到就绪列表
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB );
队列管理
基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。FreeRTOS 中所有的通信与同步机制都是基于队列实现的,一般来说,我们在队列中的数据使用复合结构体,里面包含数据的来源、作用、数值等等。
队列特性
- 数据存储
队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。
队列一般被作为 FIFO(先进先出)使用,即数据由队列尾写入,从队列首读出,也可以队首写入。往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。
- 可被多任务存取
队列是具有自己独立权限的内核对象,并不属于或赋予任何任务。所有任务都可以向同一队列写入和读出。
- 读队列时阻塞
当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
- 写队列时阻塞
同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。
由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
使用队列
xQueueCreate
xQueueCreate()用于创建一个队列,并返回一个 xQueueHandle 句柄以便于对其创建的队列进行引用。
当创建队列时,FreeRTOS 从堆空间中分配内存空间。分配的空间用于存储队列数据结构本身以及队列中包含的数据单元。
xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength,
unsigned portBASE_TYPE uxItemSize );
uxQueueLength:队列能够存储的最大单元数目,即队列深度。
uxItemSize:队列中数据单元的长度,以字节为单位。
返回值:NULL 表示没有足够的堆空间分配给队列而导致创建失败。
非 NULL 值表示队列创建成功。此返回值应当保存下来,以作为操作此队列的句柄。
xQueueSendToBack与 xQueueSendToFront
xQueueSendToBack()
用于将数据发送到队列尾,也就是从队尾写入。xQueueSend()
完全等同于xQueueSendToBack()
。而
xQueueSendToFront()
用于将数据发送到队列首,也就是从队首写入。切记不要在中断服务例程中调用
xQueueSendToFront()
或xQueueSendToBack()
。系统提供中断安全版本的xQueueSendToFrontFromISR()
与xQueueSendToBackFromISR()
用于在中断服务中实现相同的功能。
portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue,
const void * pvItemToQueue,
portTickType xTicksToWait );
portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue,
const void * pvItemToQueue,
portTickType xTicksToWait );
xQueue: 目标队列的句柄
pvItemToQueue:发送数据的指针,其指向将要复制到目标队列中的数据单元。
会从该指针指向的空间复制对应长度的数据到队列的存储区域。
xTicksToWait:阻塞超时时间
①:如 果 xTicksToWait 设 为 0 ,并且队列已满,
则xQueueSendToFront()与 xQueueSendToBack()均会立即返回。
②:如果把 xTicksToWait 设置为 portMAX_DELAY ,并且在FreeRTOSConig.h 中
设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。
③:常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。
可以传入参数 250/portTICK_RATE_MS 那么就是超时时间就是250ms
返回值:①:pdPASS 数据成功被发送到队列中
②:errQUEUE_FULL 队列已满无法写入,或者阻塞等待超时。
xQueueReceive与xQueuePeek
xQueueReceive()
用于从队列首部中接收(读取)数据单元。接收到的单元同时会从队列中删除。
xQueuePeek()
也是从从队列中接收数据单元,不同的是xQueuePeek()
从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储序顺。在中断中需要调用
xQueueReceiveFromISR
的安全函数。
portBASE_TYPE xQueueReceive( xQueueHandle xQueue,
const void * pvBuffer,
portTickType xTicksToWait );
portBASE_TYPE xQueuePeek( xQueueHandle xQueue,
const void * pvBuffer,
portTickType xTicksToWait );
xQueue:目标队列的句柄
pvBuffer:接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来的数据
xTicksToWait:阻塞超时时间。
①:如 果 xTicksToWait 设 为 0 ,并且队列为空,
则 xQueueReceive()与 xQueuePeek() 均会立即返回。
②:如果把 xTicksToWait 设置为 portMAX_DELAY ,并且在FreeRTOSConig.h 中
设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。
③:常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。
可以传入参数 250/portTICK_RATE_MS 那么就是超时时间就是250ms
返回值:①:pdPASS 数据成功从队列中读取出来
②:errQUEUE_FULL 队列为空,无法读取数据,或者阻塞等待超时。
uxQueueMessagesWaiting
用于查询队列中当前有效数据单元个数。
中断安全版本:uxQueueMessagesWaitingFromISR
unsigned portBASE_TYPE uxQueueMessagesWaiting( xQueueHandle xQueue );
xQueue:被查询队列的句柄。
返回值:当前队列中保存的数据单元个数。返回 0 表明队列为空。
工作于大型数据单元
如果队列存储的数据单元尺寸较大,那最好是利用队列来传递数据的指针而不是对数据本身在队列上一字节一字节地拷贝进或拷贝出。保存数据的指针最好是使用非任务函数分配的内存。
- 指针指向的内存空间的所有权必须明确
当任务间通过指针共享内存时,应该从根本上保证所不会有任意两个任务同时修改共享内存中的数据,或是以其它行为方式使得共享内存数据无效或产生一致性问题。原则上,共享内存在其指针发送到队列之前,其内容只允许被发送任务访问;共享内存指针从队列中被读出之后,其内容亦只允许被接收任务访问。
- 指针指向的内存空间必须有效
如果指针指向的内存空间是动态分配的,只应该有一个任务负责对其进行内存释放。当这段内存空间被释放之后,就不应该有任何一个任务再访问这段空间。
切忌用指针访问任务栈上分配的空间。因为当栈帧发生改变后,栈上的数据将不再有效。
中断管理
对于事件的检测通常采用中断方式,但是事件输入也可以通过查询获得。ISR 应当越短越好。只有以
FromISR
或FROM_ISR
结束的 API 函数或宏才可以在中断服务例程中。
开关中断
用于中断屏蔽的特殊寄存器
- PRIMASK和FAULTMASK寄存器
PRIMASK 用于禁止除 NMI 和 HardFalut 外的所有异常和中断。FAULTMASK 比 PRIMASK 更狠,它可以连 HardFault 都屏蔽掉,使用方法和 PRIMASK 类似,FAULTMASK 会在退出时自动清零。
static __INLINE void __enable_irq() { __ASM volatile ("cpsie i"); }
static __INLINE void __disable_irq() { __ASM volatile ("cpsid i"); }
static __INLINE void __enable_fault_irq() { __ASM volatile ("cpsie f"); }
static __INLINE void __disable_fault_irq() { __ASM volatile ("cpsid f"); }
MOVS R0, #1
MSR PRIMASK, R0 //将 1 写入 PRIMASK 禁止所有中断 类似 __disable_irq
MOVS R0, #0
MSR PRIMASK, R0 //将 0 写入 PRIMASK 以使能中断 类似 __enable_irq
MOVS R0, #1
MSR FAULTMASK, R0 // 将 1 写入 FAULTMASK 禁止所有中断 类似 __disable_fault_irq
MOVS R0, #0
MSR FAULTMASK, R0 // 将 0 写入 FAULTMASK 使能中断 类似 __enable_fault_irq
- BASEPRI寄存器
该寄存器用于屏蔽不高于某个优先级的中断,不会将所有的都屏蔽掉。
static __INLINE uint32_t __get_BASEPRI(void) // 获取屏蔽的最高优先级
{
/*将 __regBasePri 寄存器和basepri寄存器链接起来,相当于映射,
修改__regBasePri相当于修改该寄存器*/
register uint32_t __regBasePri __ASM("basepri");
return(__regBasePri);
}
static __INLINE void __set_BASEPRI(uint32_t basePri) // 设置屏蔽的最高优先级
{
register uint32_t __regBasePri __ASM("basepri");
__regBasePri = (basePri & 0xff);
}
MOV R0, #0X60
MSR BASEPRI, R0 // 屏蔽优先级不高于 0X60 的中断 __set_BASEPRI(0x60);
MOV R0, #0
MSR BASEPRI, R0 // 取消 BASEPRI 对中断的屏蔽 __set_BASEPRI(0x0);
FreeRTOS开关中断
FreeRTOS 开关中断函数为 portENABLE_INTERRUPTS ()和 portDISABLE_INTERRUPTS(),这两个函数其实是宏定义,在 portmacro.h 中有定义。
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() /*关闭中断*/
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0) /*开启中断*/
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
// 默认开关优先级为 configMAX_SYSCALL_INTERRUPT_PRIORITY = 5
// 低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的优先级中断不会被屏蔽
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
延迟中断处理
采用二值信号量同步
二值信号量可以在某个特殊的中断发生时,让任务解除阻塞,相当于让任务与中断同步。这样就可以让中断事件处理量大的工作在同步任务中完成,中断服务例程(ISR)中只是快速处理少部份工作。如此,中断处理可以说是被”推迟(deferred)”到一个”处理(handler)”任务。
如果某个中断处理要求特别紧急,其延迟处理任务的优先级可以设为最高,以保证延迟处理任务随时都抢占系统中的其它任务。这样,延迟处理任务就成为其对应的 ISR退出后第一个执行的任务,在时间上紧接着 ISR 执行,相当于所有的处理都在 ISR 中完成一样。
延迟处理任务对一个信号量进行带阻塞性质的”take”
调用,意思是进入阻塞态以等待事件发生。当事件发生后,ISR 对同一个信号量进行”give”
操作,使得延迟处理任务解除阻塞,从而事件在延迟处理任务中得到相应的处理。
二值信号量可以看作是一个深度为 1 的队列。这个队列由于最多只能保存一个数据单元,所以其不为空则为满(所谓”二值”)。延迟处理任务调用xSemaphoreTake()
时,等效于带阻塞时间地读取队列,如果队列为空的话任务则进入阻塞态。当事件发生后,ISR 简单地通过调用 xSemaphoreGiveFromISR()
放置一个令牌(信号量)到队列中,使得队列成为满状态。这也使得延迟处理任务切出阻塞态,并移除令牌,使得队列再次成为空。当任务完成处理后,再次读取队列,发现队列为空,又进入阻塞态。这里和我们Linux下的信号量不同,这里任务获取信号量后,不同给回来,而Linux下必须要给回来。
xSemaphoreCreateBinary
创建二值信号量,FreeRTOS 中各种信号量的句柄都存储在 SemaphoreHandle_t/xSemaphoreHandle 类型的变量中。
#define xSemaphoreHandle SemaphoreHandle_t
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );
xSemaphore: 创建的信号量
vSemaphoreCreateBinary()在实现上是一个宏,所以信号量变量应当直接传入,而不是传址。
定义在文件 semphr.h 中
上面的函数时老版本的,新版本使用 SemaphoreHandle_t xSemaphoreCreateBinary( void )函数
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1,
semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif
xSemaphoreTake
“带走(Taking)”
一个信号量意为”获取(Obtain)”
或”接收(Receive)”
信号量。只有当信号量有效的时候才可以被获取。除互斥信号量外,所有类型的信号量都可以调用函数
xSemaphoreTake()
来获取。但xSemaphoreTake()
不能在中断服务例程中调用。xSemaphoreTakeFromISR才可以。
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );
xSemaphore:获取得到的信号量
xTicksToWait:阻塞超时时间。任务进入阻塞态以等待信号量有效的最长时间。
如果 xTicksToWait 为 0,则 xSemaphoreTake()在信号量无效时会立即返回。
阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。
常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。
如果把 xTicksToWait 设置为 portMAX_DELAY ,并且在 FreeRTOSConig.h 中
设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。
返回值:
pdPASS: 释放信号量成功。pdFALSE: 释放信号量失败。
xSemaphoreGiveFromISR
除互斥信号量外, FreeRTOS 支持的其它类型的信号量都可以通过调用
xSemaphoreGiveFromISR()
给出。xSemaphoreGiveFromISR()
是xSemaphoreGive()
的特殊形式,专门用于中断服务
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore,
portBASE_TYPE *pxHigherPriorityTaskWoken );
xSemaphore:给出的信号量
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,
这个变量的值由这三个函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。
如果调用 xSemaphoreGiveFromISR()使得一个任务解除阻塞,并且这个任务的优先级高于当前任务
(也就是被中断的任务),那么 xSemaphoreGiveFromISR()会在函数内部将
*pxHigherPriorityTaskWoken 设为pdTRUE。当此值为 pdTRUE 的时候在退出中断服务函数
之前一定要进行一次任务切换。 portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
返回值:pdPASS,调用成功
pdFAILSE:获取信号量失败。
计数信号量
在中断以相对较慢的频率发生的情况下,上面描述的流程是足够而完美的。如果在延迟处理任务完成上一个中断事件的处理之前,新的中断事件又发生了,等效于将新的事件锁存在二值信号量中,使得延迟处理任务在处理完上一个事件之后,立即就可以处理新的事件。如果还有中断事件发生,那么后续发生的中断事件将会丢失。一个二值信号量最多只能锁存一个中断事件。如果用计数信号量代替二值信号量,那么,这种丢中断的情形将可以避免。
计数信号量两种用法
- 事件计数
每次事件发生时,中断服务例程都会给出(Give)
信号量——信号量在每次被给出时其计数值加 1。延迟处理任务每处理一个任务都会获取(Take)
一次信号量——信号量在每次被获取时其计数值减 1。信号量的计数值其实就是已发生事件的数目与已处理事件的数目之间的差值。用于事件计数的计数信号量,在被创建时其计数值被初始化为 0。
- 资源管理
信号量的计数值用于表示可用资源的数目。一个任务要获取资源的控制权,其必须先获得信号量——使信号量的计数值减 1。当计数值减至 0,则表示没有可用资源。当任务利用资源完成工作后,将给出(归还)信号量——使信号量的计数值加 1。 用于资源管理的信号量,在创建时其计数值被初始化为可用资源总数。和Linux的sem类似情况。
xSemaphoreCreateCounting
FreeRTOS 中所有种类的信号量句柄都由声明为
SemaphoreHandle_t
类型的变量保存。信号量在使用前必须先被创建。使用xSemaphoreCreateCounting()
API 函数来创建一个计数信号量。
SemaphoreHandle_t xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount,
unsigned portBASE_TYPE uxInitialCount );
uxMaxCount:最大计数值
当此信号量用于对事件计数或锁存事件的话,uxMaxCount 就是可锁存事件的最大数目。
当此信号量用于对一组资源的访问进行管理的话,uxMaxCount 应当设置为所有可用资源的总数。
uxInitialCount:信号量的初始计数值。
当此信号量用于事件计数的话,uxInitialCount 应当设置为 0
当此信号量用于资源管理的话, uxInitialCount 应当等于 uxMaxCount
返回值:如果返回 NULL 值,表示堆上内存空间不足
如果返回非 NULL 值,则表示信号量创建成功,该值作为这个的信号量的句柄。
uxSemaphoreGetCount
获取信号量计数个数。如果是二值信号量,信号量有效返回1,否则返回零,计数信号量返回计数个数。
UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore );
本质上就是使用的队列
#define uxSemaphoreGetCount( xSemaphore )
uxQueueMessagesWaiting( ( QueueHandle_t ) ( xSemaphore ) )
在中断服务例程中使用队列
xQueueSendToFrontFromISR(),xQueueSendToBackFromISR()与 xQueueReceiveFromISR()分别是 xQueueSendToFront(),xQueueSendToBack()与 xQueueReceive()的中断安全版本,专门用于中断服务例程中。
中断经常产生数据的一般来说不在中断服务中使用队列传递数据,常使用延迟中断处理。
xQueueSendToFrontFromISR与xQueueSendToBackFromISR
portBASE_TYPE xQueueSendToFrontFromISR( xQueueHandle xQueue,
void *pvItemToQueue
portBASE_TYPE *pxHigherPriorityTaskWoken );
portBASE_TYPE xQueueSendToBackFromISR( xQueueHandle xQueue,
void *pvItemToQueue
portBASE_TYPE *pxHigherPriorityTaskWoken
);
xQueue:目标队列的句柄。
pvItemToQueue:发送数据的指针。其指向将要复制到目标队列中的数据单元。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,
这个变量的值由这三个函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。
如果调用 API 使得一个任务解除阻塞,并且这个任务的优先级高于当前任务
(也就是被中断的任务),那么 xSemaphoreGiveFromISR()会在函数内部将
*pxHigherPriorityTaskWoken 设为pdTRUE。当此值为 pdTRUE 的时候在退出中断服务函数
之前一定要进行一次任务切换。 portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
返回值:pdPASS,调用成功
pdFAILSE:获取信号量失败。
xQueueReceiveFromISR与xQueuePeekFromISR
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
xQueue:目标队列的句柄。
pvItemToQueue:获取到的队列数据
上面二者的区别是,xQueuePeekFromISR取出队列数据后删除队列中的该数据,
而xQueuePeekFromISR只是取出其中的数据。
中断嵌套
中断嵌套需要在 FreeRTOSConfig.h 中定义configKERNEL_INTERRUPT_PRIORITY 和 configMAX_SYSCALL_INTERRUPT_PRIORITY
常量 | 描述 |
---|---|
configKERNEL_INTERRUPT_PRIORITY | 设置系统心跳时钟的中断优先级。如果没有这个常量,那么需要调用中断安全版本 FreeRTOS API的中断都必须运行在此优先级上。一般是最低优先级。 |
configMAX_SYSCALL_INTERRUPT_PRIORITY | 设置中断使用中断安全版本 FreeRTOS API 可以运行的最高中断优先级。 |
建立一个全面的中断嵌套模型,我们需要设置configMAX_SYSCALL_INTERRUPT_PRIRITY
为比configKERNEL_INTERRUPT_PRIORITY
更高的优先级。
一、假设高优先级数值高,那么**configMAX_SYSCALL_INTERRUPT_PRIORITY > configKERNEL_INTERRUPT_PRIORITY
**,假定常量 configMAX_SYSCALL_INTERRUPT_PRIRITY
设置为 3,configKERNEL_INTERRUPT_PRIORITY
设置为 1。同时也假定这种情形基于一个具有七个不同中断优先及的微控制器。则有:
- 处于中断优先级 1 到 3(含)的中断会被内核或处于临界区的应用程序阻塞执行,但是它们可以调用中断安全版本的 FreeRTOS API 函数。
- 处于中断优先级 4 及以上的中断不受临界区影响,所以其不会被内核的任何行为阻塞,可以立即得到执行。通常需要严格时间精度的功能(如电机控制)会使用高于
configMAX_SYSCALL_INTERRUPT_PRIRITY
的优先级,以保证调度器不会对其中断响应时间造成抖动。 - 不需要调用任何 FreeRTOS API 函数的中断,可以自由地使用任意优先级。
- 在这二者之间的中断优先级可以使用中断安全版本FreeRTOS API。
二、假设高优先级数值低,那么**configMAX_SYSCALL_INTERRUPT_PRIORITY < configKERNEL_INTERRUPT_PRIORITY
**,假定常量 configMAX_SYSCALL_INTERRUPT_PRIRITY
设置为 5,configKERNEL_INTERRUPT_PRIORITY
设置为 15。同时也假定这种情形基于一个具有十六个不同中断优先及的微控制器。
中断优先级和任务优先级区别:
(1)中断优先级是由微控制器架构体系所定义的。
(2)中断优先级是硬件控制的优先级,中断服务例程的执行会与之关联。
(3)任务并非运行在中断服务中,所以赋予任务的软件优先级与赋予中断源的硬件优先级之间没有任何关系
总结:
以上是根据高优先级数值高来说的,那么现在总结在任何情况都适用的。无论高优先级数值高还是低优先级数值高都适用。
- 如果系统的优先级比configMAX_SYSCALL_INTERRUPT_PRIORITY高,则这些中断可以直接触发,不会被RTOS延时,如果优先级比其低,则有可能被RTOS延时。
- 在configMAX_SYSCALL_INTERRUPT_PRIORITY和configKERNEL_INTERRUPT_PRIORITY之间的优先级的中断均会被系统内核延迟。使用使用中断安全版本FreeRTOS API的中断需要在其中。
资源管理
概述
- 变量的非原子访问:更新结构体的多个成员变量,或是更新的变量其长度超过了架构体系的自然长度(比如,更新一个 16 位机上的 32 位变量)均是非原子操作的例子。如果这样的操作被中断,将可能导致数据损坏或丢失。
- 函数重入:如果一个函数可以安全地被多个任务调用,或是在任务与中断中均可调用,则这个函数是可重入的。每个任务都单独维护自己的栈空间及其自身在的内存寄存器组中的值。如果一个函数除了访问自己栈空间上分配的数据或是内核寄存器中的数据外,不会访问其它任何数据,则这个函数就是可重入的。
long lAddOneHundered(long lVar1) // 可重入的
{
long lVar2;
lVar2 = lVar1 + 100;
return lVar2;
}
long lVar1;
long lNonsenseFunction( void ) // 不可重入的
{
static long lState = 0;
long lReturn;
switch( lState )
{
case 0 : lReturn = lVar1 + 10;
lState = 1;
break;
case 1 : lReturn = lVar1 + 20;
lState = 0;
break;
}
}
- 互斥:访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用”互斥”技术以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直至这个资源又恢复到完整状态。
临界区与挂起调度器
基本临界区
基本临界区是指宏 askENTER_CRITICAL()
与 taskEXIT_CRITICAL()
之间的代码区间。中断中使用taskENTER_CRITICAL_FROM_ISR()
和 taskEXIT_CRITICAL_FROM_ISR(x)
。
在taskENTER_CRITICAL()
与 taskEXIT_CRITICAL()
之间不会切换到其它任务。 中断可以执行,也允许嵌套,但只是针对优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断 – 而且这些中断不允许访问FreeRTOS API
函数
/*为了保证对PORTA寄存器的访问不被中断,将访问操作放入临界区。进入临界区*/
taskENTER_CRITICAL();
PORTA |= 0x01;
/*完成了对PORTA的访问,因此可以安全地离开临界区*/
taskEXIT_CRITICAL();
临界区是提供互斥功能的一种非常原始的实现方法。临界区的工作仅仅是简单地把中断全部关掉,或是关掉优先级在 configMAX_SYSCAL_INTERRUPT_PRIORITY
及以下的中断。抢占式上下文切换只可能在某个中断中完成,所以调用 taskENTER_CRITICAL()的任务可以在中断关闭的时段一直保持运行态,直到退出临界区。临界区必须只具有很短的时间,否则会反过来影响中断响应时间。在每次调用 taskENTER_CRITICAL()
之后,必须尽快地配套调用一个 taskEXIT_CRITICAL()
。
临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为 0 时才会真正退出。
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
status_value=taskENTER_CRITICAL_FROM_ISR();
total_num+=1;
printf("float_num 的值为: %d\r\n",total_num);
taskEXIT_CRITICAL_FROM_ISR(status_value);
}
}
挂起(锁定)调度器
基本临界区保护一段代码区间不被其它任务或中断打断。由挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的。
如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式。但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作。
- vTaskStartScheduler
void vTaskStartScheduler( void ); // 开启任务调度器
- vTaskSuspendAll
void vTaskSuspendAll( void ); // 挂起调度器
挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行。在调度器处于挂起状态时,不能调用 FreeRTOS API 函数。
- xTaskResumeAll
portBASE_TYPE xTaskResumeAll( void );
如果一个挂起的上下文切换请求在 xTaskResumeAll() 返回前得到执行,则函数返回 pdTRUE。
在其它情况下,xTaskResumeAll()返回 pdFALSE。
嵌套调用 vTaskSuspendAll()和 xTaskResumeAll()是安全的,因为内核有维护一个嵌套深度计数。调度器只会在嵌套深度计数为 0 时才会被唤醒。
互斥信号量
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。
一个任务想要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。当令牌持有者完成资源使用,其必须马上归还(Give)令牌。只有归还了令牌,其它任务才可能成功持有,也才可能安全地访问该共享资源。一个任务除非持有了令牌,否则不允许访问共享资源。
互斥信号量不能用于中断服务函数中,原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
- 互斥量:用于互斥的信号量在take后必须要give
- 二值信号量:用于同步的信号量,在take后,即完成了同步,便将他丢弃,不在归还。
xSemaphoreCreateMutex
互斥量是一种信号量。FreeRTOS 中所有种类的信号量句柄都保存在类型为 SemaphoreHandle_t 的变量中。
SemaphoreHandle_t xSemaphoreCreateMutex( void ); // 创建互斥量
如果返回 NULL 表示互斥量创建失败,
返回非 NULL 值表示互斥量创建成功,
返回值应当保存起来作为该互斥量的句柄。
- 例子
static void prvPrintString(const portChAR *pcString)
{
xSemaphoreTake( xMutex, portMAX_DELAY ); // 持有互斥量
printf("%s", pcString);
fflush(stdout);
xSemaphoreGive( xMutex ); // 释放互斥量
}
void prvPrintTask(void *pvParameters)
{
const portChAR *pvString = (char*)pvParameters;
prvPrintString(pvString);
vTaskDelay(250 / portTICK_RATE_MS);
}
int main(void)
{
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); // 创建互斥量
if( xMutex != NULL )
{
xTaskCreate( prvPrintTask, "Print1", 1000, "Task 1 running \r\n", 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000, "Task 2 running \r\n", 2, NULL );
vTaskStartScheduler();
}
for(;;);
}
优先级反转
在使用互斥量中的潜在的缺陷之一,高优先级的任务必须等待低优先级的任务放弃对互斥量的持有权。高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行!
优先级继承
FreeRTOS 中互斥量与二值信号量十分相似——唯一的区别就是互斥量自动提供了一个基本的**”优先级继承”机制**。优先级继承是最小化优先级反转负面影响的一种方案——其并不能修正优先级反转带来的问题,仅仅是减小优先级反转的影响。所以如果可以避免的话,并不建议系统实现对优先级继承有所依赖。优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具有的最高优先级。持有互斥量的低优先级任务”继承”了等待互斥量的任务的优先级。互斥量持有者在归还互斥量时,优先级会自动设置为其原来的优先级。
由于最好是优先考虑避免优先级反转,并且因为 FreeRTOS 本身是面向内存有限的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务在任意时刻只会持有一个互斥量。
死锁
死锁是利用互斥量提供互斥功能的另一个潜在缺陷。当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁。任务 A 在等待一个被任务 B 持有的互斥量,而任务 B 也在等待一个被任务 A 持有的互斥量。死锁于是发生,因为两个任务都不可能再执行下去。和优先级反转一样,避免死锁的最好方法就是在设计阶段就考虑到这种潜在风险。
递归互斥量
递归互斥信号量可以看作是一个特殊的互斥信号量,已经获取了互斥信号量的任务就不能再次获取这个互斥信号量,但是递归互斥信号量不同,已经获取了递归互斥信号量的任务可以再次获取这个递归互斥信号量,而且次数不限!一个任务使用函数 xSemaphoreTakeRecursive()成功的获取了多少次递归互斥信号量就得使用函数 xSemaphoreGiveRecursive()释放多少次!
递归互斥信号量也有优先级继承的机制,所以当任务使用完递归互斥信号量以后一定要记得释放。同互斥信号量一样,递归互斥信号量不能用在中断服务函数中。
创建递归互斥信号量
将configUSE_RECURSIVE_mutexes 配置为1
xSemaphoreCreateRecursiveMutex() 使用动态方法创建递归互斥信号量。
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
xSemaphoreCreateRecursiveMutexStatic() 使用静态方法创建递归互斥信号量。
SemaphoreHandle_t xSemaphoreCreateRecursiveMutexStatic( StaticSemaphore_t *pxMutexBuffer );
pxMutexBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
xSemaphoreGiveRecursive
必须在 FreeRTOSConfig.h 中将 configUSE_RECURSIVE_MUTEXES 设置为 1, 此宏才可用。不得在使用 xSemaphoreCreateMutex() 创建的互斥锁上使用此宏。
xSemaphoreGiveRecursive( SemaphoreHandle_t xMutex );
xMutex 正在释放或“给出”的互斥锁的句柄
如果成功给出信号量,则返回 pdTRUE
xSemaphoreTakeRecursive
xSemaphoreTakeRecursive( SemaphoreHandle_t xMutex, TickType_t xTicksToWait );
xMutex 正在获得的互斥锁的句柄。
xTicksToWait 等待信号量变为可用的时间
如果获得信号量,则返回 pdTRUE。 如果 xTicksToWait 过期,信号量不可用,则返回 pdFALSE
守护任务
守护任务提供了一种干净利落的方法来实现互斥功能,而不用担心会发生优先级反转和死锁。守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的资源——其它任务要访问该资源只能间接地通过守护任务提供的服务。
本质上其实就是使用一个队列,只有该守护任务可以xQueueReceive,拿出其中的消息,其他任务需要直接访问其守护的资源,往队列中发送消息,获取服务。
中断中可以写队列,所以中断服务例程也可以安全地使用守护任务提供的服务。
static void prvStdioGatekeeperTask(void* pvParameters) // 守护任务
{
char* pcMessageToPrint;
/* 这是唯一允许直接访问终端输出的任务。任何其它任务想要输出字符串,都不能直接访问终端,而是将要
输出的字符串发送到此任务。并且因为只有本任务才可以访问标准输出,所以本任务在实现上不需要考虑互斥
和串行化等问题。 */
for(;;)
{
/* 等待信息到达。指定了一个无限长阻塞超时时间,所以不需要检查返回值 – 此函数只会在成功收到
消息时才会返回。 */
xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
printf( "%s", pcMessageToPrint );
fflush( stdout );
}
}
// 输出字符到终端任务
static void prvPrintTask( void *pvParameters )
{
char* iIndexToString;
iIndexToString = ( char* ) pvParameters;
for( ;; )
{
/* 打印输出字符串,不能直接输出,通过队列将字符串指针发送到守护任务。队列在调度器启动之前就
创建了,所以任务执行时队列就已经存在了。并有指定超时等待时间,因为队列空间总是有效。 */
xQueueSendToBack( xPrintQueue, iIndexToString, 0 );
vTaskDelay( ( rand() & 0x1FF ) );
}
}
心跳钩子函数vApplicationTickHook
心跳钩子函数(或称回调函数)由内核在每次心跳中断时调用。要挂接一个心跳钩子函数,需要做以下配置:
- 设置 FreeRTOSConfig.h 中的常量 configUSE_TICK_HOOK 为 1。
- 提供钩子函数的具体实现,void vApplicationTickHook( void );
心跳钩子函数在系统心跳中断的上下文上执行,所以必须保证非常短小,适度占用栈空间,并且不要调用任何名字不带后缀”FromISR”的 FreeRTOS API 函数。
char* pcStringsToPrint = "this is vApplicationTickHook ...\n"
void vApplicationTickHook( void )
{
static int iCount = 0;
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
iCount++;
if( iCount >= 200 ) /*每心跳200次,进行打印*/
{
xQueueSendToFrontFromISR( xPrintQueue, /*放到队列的首位*/
pcStringsToPrint,
&xHigherPriorityTaskWoken );
iCount = 0;
}
}
软件定时器
软件定时器允许设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期,简而言之,当定时器的定时周期到了以后就会执行回调函数。
软件定时器的回调函数是在定时器服务任务中执行的,所以一定不能在回调函数中调用任何会阻塞任务的 API 函数!比如,定时器回调函数中千万不能调用 vTaskDelay()、vTaskDelayUnti(),还有一些访问队列或者信号量的非零阻塞时间的 API 函数也不能调用。
定时器服务任务与队列
软件定时器由定时器服务任务来提供的,FreeRTOS中的大多数关于定时器的API使用队列发送命令给定时器任务服务,这个队列叫做定时器命令队列。该队列提供给软件定时器使用,用户不能直接访问。
单次定时器和周期定时器
软件定时器可以分为单次定时器和周期定时器。
单次定时器:定时时间一到,就会执行一次回调函数,然后定时器就会停止运行。对于单次定时器可以再次启动,但是就是不能自动重启。
周期定时器:一旦启动以后,就会在执行完回调函数以后自动的重新启动。于是实现了周期性的执行。
复位软件定时器
xTimerReset() :复位软件定时器,用在任务中。调用函数 xTimerReset ()开启软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_RESET 命令。
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer: 要复位的软件定时器的句柄。
xTicksToWait: 设置阻塞时间
返回值
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
xTimerResetFromISR() :复位软件定时器,用在中断服务函数中
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, BaseType_t * pxHigherPriorityTaskWoken );
xTimer: 要复位的软件定时器的句柄。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
创建软件定时器
xTimerCreate() :使用动态方法创建软件定时器。新创建的软件定时器处于休眠状态,函 数 xTimerStart()
、 xTimerReset()
、xTimerStartFromISR()
、xTimerResetFromISR()
、 xTimerChangePeriod()
和xTimerChangePeriodFromISR()
可以使新创建的定时器进入活动状态。
TimerHandle_t xTimerCreate( const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
pcTimerName: 软件定时器名字;
xTimerPeriodInTicks : 软件定时器的定时器周期, 单位是时钟节拍数。
uxAutoReload: 设置定时器模式;
当此参数为 pdTRUE表示创建的是周期定时器。pdFALSE表示创建的是单次定时器。
pvTimerID: 定时器 ID 号;
pxCallbackFunction: 定时器回调函数;
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
xTimerCreateStatic() :使用静态方法创建软件定时器。
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t * pxTimerBuffer );
pcTimerName: 软件定时器名字;
xTimerPeriodInTicks : 软件定时器的定时器周期, 单位是时钟节拍数。
uxAutoReload: 设置定时器模式;
当此参数为 pdTRUE表示创建的是周期定时器。pdFALSE表示创建的是单次定时器。
pvTimerID: 定时器 ID 号;
pxCallbackFunction: 定时器回调函数;
pxTimerBuffer:参数指向一个 StaticTimer_t 类型的变量,用来保存定时器结构体;
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
开启软件定时器
xTimerStart() :开启软件定时器,用于任务中。如果软件定时器没有运行的话调用函数 xTimerStart()就会计算定时器到期时间,如果软件定时器正在运行的话调用函数 xTimerStart()的结果和 xTimerReset()一样。
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer: 要开启的软件定时器的句柄。
xTicksToWait: 设置阻塞时间;
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
xTimerStartFromISR() :开启软件定时器,用于中断中。
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
xTimer: 要开启的软件定时器的句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,
为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
停止软件定时器
xTimerStop() :停止软件定时器,用于任务中。
BaseType_t xTimerStop ( TimerHandle_t xTimer, TickType_t xTicksToWait );
xTimer: 要停止的软件定时器的句柄。
xTicksToWait: 设置阻塞时间;
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
xTimerStopFromISR() :停止软件定时器,用于中断服务函数中。
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
xTimer: 要停止的软件定时器句柄。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,
为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
事件标志组
简介
1、事件位:事件位用来表明某个事件是否发生,事件位通常用作事件标志。三个例子:
- 当收到一条消息并且把这条消息处理掉以后就可以将某个位(标志)置 1,当队列中没有消息需要处理的时候就可以将这个位(标志)置 0。
- 当把队列中的消息通过网络发送输出以后就可以将某个位(标志)置 1,当没有数据需要从网络发送出去的话就将这个位(标志)置 0。
- 现在需要向网络中发送一个心跳信息,将某个位(标志)置 1。现在不需要向网络中发送心跳信息,这个位(标志)置 0。
2、事件组:一个事件组就是一组的事件位,事件组中的事件位通过位编号来访问。三个例子:
- 事件标志组的 bit0 表示队列中的消息是否处理掉。
- 事件标志组的 bit1 表示是否有消息需要从网络中发送出去。
- 事件标志组的 bit2 表示现在是否需要向网络发送心跳信息。
3、事件标志组和事件位的数据类型
事件标志组的数据类型为 EventGroupHandle_t,当 configUSE_16_BIT_TICKS 为 1 的时候事件标志组可以存储 8 个事件位,当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24个事件位。
事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量中。
typedef TickType_t EventBits_t;
#if( configUSE_16_BIT_TICKS == 1 )
typedef uint16_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffff
#else
typedef uint32_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
#define portTICK_TYPE_IS_ATOMIC 1
#endif
当 configUSE_16_BIT_TICKS 为 0 的时候 TickType_t 是个 32 位的数据类型,因此 EventBits_t 也是个 32 位的数据类型。EventBits_t 类型的变量可以存储 24 个事件位,另外的那高 8 位有其他用。事件位 0 存放在这个变量的 bit0 上,变量的 bit1 就是事件位 1,以此类推。
创建事件标志组
xEventGroupCreate() :使用动态方法创建事件标志组。
EventGroupHandle_t xEventGroupCreate( void );
返回值:
NULL: 事件标志组创建失败。
其他值: 创建成功的事件标志组句柄。
xEventGroupCreateStatic() :使用静态方法创建事件标志组
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *pxEventGroupBuffer );
pxEventGroupBuffer: 参数指向一个 StaticEventGroup_t 类型的变量,用来保存事件标志组结构体。
返回值:
NULL: 事件标志组创建失败。
其他值: 创建成功的事件标志组句柄。
设置事件位
xEventGroupClearBits() :将指定的事件位清零,用在任务中。
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );
xEventGroup: 要操作的事件标志组的句柄。
uxBitsToClear: 要清零的事件位,比如要清除 bit3 的话就设置为 0X08。可以同时清除多个
bit,如设置为 0X09 的话就是同时清除 bit3 和 bit0。
返回值:
任何值: 将指定事件位清零之前的事件组值。
xEventGroupClearBitsFromISR() A:将指定的事件位清零,用在中断服务函数中
BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
xEventGroup: 要操作的事件标志组的句柄。
uxBitsToClear: 要清零的事件位,比如要清除 bit3 的话就设置为 0X08。可以同时清除多个
bit,如设置为 0X09 的话就是同时清除 bit3 和 bit0。
返回值:
pdPASS: 事件位清零成功。
pdFALSE: 事件位清零失败。
xEventGroupSetBits() :将指定的事件位置 1,用在任务中。
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
xEventGroup: 要操作的事件标志组的句柄。
uxBitsToClear: 要置 1的事件位,比如要将 bit3 置1 的话就设置为 0X08。可以同时多个
bit置1,如设置为 0X09 的话就是同时将 bit3 和 bit0 置1。
返回值:
任何值: 将指定事件位清零之前的事件组值。
xEventGroupSetBitsFromISR() :将指定的事件位置 1,用在中断服务函数中。
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );
xEventGroup: 要操作的事件标志组的句柄。
uxBitsToClear: 要置 1的事件位,比如要将 bit3 置1 的话就设置为 0X08。可以同时多个
bit置1,如设置为 0X09 的话就是同时将 bit3 和 bit0 置1;
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 事件位清零成功。
pdFALSE: 事件位清零失败。
获取事件标志组值
xEventGroupGetBits() :获取当前事件标志组的值(各个事件位的值),用在任务中。
EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup );
xEventGroup: 要获取的事件标志组的句柄;
返回值:
任何值:当前事件标志组的值。
xEventGroupGetBitsFromISR() :获取当前事件标志组的值,用在中断服务函数中。
EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup );
xEventGroup: 要获取的事件标志组的句柄;
返回值:
任何值: 当前事件标志组的值。
等待指定的事件位
某个任务可能需要与多个事件进行同步,那么这个任务就需要等待并判断多个事件位(标志),使用函数 xEventGroupWaitBits()可以完成这个功能。调用函数以后如果任务要等待的事件位还没有准备好(置 1 或清零)的话任务就会进入阻塞态,直到阻塞时间到达或者所等待的事件位准备好。
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
const TickType_t xTicksToWait );
xEventGroup: 指定要等待的事件标志组。
uxBitsToWaitFord:指定要等待的事件位,比如要等待 bit0 和(或) bit2 的时候此参数就是0X05,
如果要等待 bit0 和(或) bit1 和(或) bit2 的时候此参数就是 0X07
xClearOnExit: 此参数要是为pdTRUE的话,那么在退出此函数之前由参数uxBitsToWaitFor
所设置的这些事件位就会清零。如果设置位 pdFALSE 的话这些事件位就不会改变。
xWaitForAllBits: 此参数如果设置为 pdTRUE 的话,当 uxBitsToWaitFor 所设置的这些事件
位都置 1,或者指定的阻塞时间到的时候函数 xEventGroupWaitBits() 才会返回。
当此函数为 pdFALSE 的话,只要 uxBitsToWaitFor 所设置的这些事件位其中的任意一个置 1 ,
或者指定的阻塞时间到的话函数 xEventGroupWaitBits() 就会返回。
xTicksToWait: 设置阻塞时间,单位为节拍数
返回值:
任何值: 返回当所等待的事件位置 1 以后的事件标志组的值,或者阻塞时间到。
任务通知
简介
任务通知在 FreeRTOS 中是一个可选的功能,要使用任务通知的话就需要将宏 configUSE_TASK_NOTIFICATIONS 定义为 1。
FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态。也可以更新接收任务的任务通知值,任务通知可以通过如下方法更新接收任务的通知值:
● 不覆盖接收任务的通知值(如果上次发送给接收任务的通知还没被处理)。
● 覆盖接收任务的通知值。
● 更新接收任务通知值的一个或多个 bit。
● 增加接收任务的通知值。
任务通知在一定程度上可以代替信号量、消息队列、事件标志组的功能;而且任务通知所需要的资源更少、执行速度更快。任务通知虽然可以提高速度,并且减少 RAM 的使用,但是任务通知也是有使用限制的:
● FreeRTOS 的任务通知只能有一个接收任务,其实大多数的应用都是这种情况。
● 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。
发送任务通知
xTaskNotify():发送通知,带有通知值并且不保留接收任务原通知值,用在任务中。
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法,eNotifyAction 是个枚举类型。
typedef enum
{
eNoAction = 0,
eSetBits, //更新指定的 bit
eIncrement, //通知值加一
eSetValueWithOverwrite, //覆写的方式更新通知值
eSetValueWithoutOverwrite //不覆写通知值
} eNotifyAction;
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
xTaskNotifyFromISR() :发送通知,函数 xTaskNotify()的中断版本。
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t * pxHigherPriorityTaskWoken );
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换;
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
xTaskNotifyGive():发送通知,不带通知值并且不保留接收任务的通知值,此函数会将接收任务的通知值加一,用于任务中。
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
返回值: pdPASS: 此函数只会返回 pdPASS。
vTaskNotifyGiveFromISR() :发送通知,函数 xTaskNotifyGive()的中断版本。
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle,
BaseType_t * pxHigherPriorityTaskWoken );
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换;
xTaskNotifyAndQuery():发送通知,带有通知值并且保留接收任务的原通知值。此函数比 xTaskNotify()
多一个参数,此参数用来保存更新前的通知值。
BaseType_t xTaskNotifyAndQuery ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction
uint32_t * pulPreviousNotificationValue);
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
xTaskNotiryAndQueryFromISR():发送通知,函数 xTaskNotifyAndQuery()的中断版本,用在中断服务函数中。
BaseType_t xTaskNotifyAndQueryFromISR ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue
BaseType_t * pxHigherPriorityTaskWoken );
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换
当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换
返回值:
pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回 pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。
获取任务通知
ulTaskNotifyTake():获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。当任务通知用作二值信号量或者计数信号量的时候使用此函数来获取信号量。
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
xClearCountOnExit: 参数为 pdFALSE 的话在退出函数 ulTaskNotifyTake()的时候任务通知值
减一,类似计数型信号量。当此参数为 pdTRUE 的话在退出函数的时候
任务任务通知值清零,类似二值信号量。
xTickToWait: 阻塞时间。
返回值:任何值,任务通知值减少或者清零之前的值。
xTaskNotifyWait():等待任务通知,比 ulTaskNotifyTak()更为强大,全功能版任务通知获取函数。不管任务通知用作二值信号量、计数型信号量、队列和事件标志组中的哪一种,都可以使用此函数来获取任务通知。但是当任务通知用作二值信号量和计数型信号量的时候推荐使用函数ulTaskNotifyTake()。
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t * pulNotificationValue,
TickType_t xTicksToWait );
ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按
位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务
通知值清零。
ulBitsToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值
与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者
ULONG_MAX 的时候就会将任务通知值清零。
pulNotificationValue:此参数用来保存任务通知值。
xTickToWait: 阻塞时间。
返回值:
pdTRUE: 获取到了任务通知。
pdFALSE: 任务通知获取失败。
用作二值信号量
当使用任务通知代替 二进制信号量时,接收任务的通知值用于代替 二进制信号量的计数值,ulTaskNotifyTake()(或 ulTaskNotifyTakeIndexed()) API 函数用于代替信号量的 xSemaphoreTake() API 函数。 ulTaskNotifyTake() 函数的 xClearOnExit 参数设置为 pdTRUE, 因此每次获取通知时计数值返回为零 — 仿真二进制信号量。
同样,xTaskNotifyGive() (或xTaskNotifyGiveIndexed())或 [vTaskNotifyGiveFromISR()(或 vTaskNotifyGiveIndexedFromISR() )函数用于代替信号量的 xSemaphoreGive() 和 xSemaphoreGiveFromISR()h 函数。
NotifyValue=ulTaskNotifyTake(pdTRUE,portMAX_DELAY); //获取任务通知
if(NotifyValue == 1) //清零之前的任务通知值为 1,说明任务通知有效
{
/*执行程序*/
}
vTaskNotifyGiveFromISR(DataProcess_Handler,&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
xTaskNotifyGive(DataProcess_Handler);
用作计数信号量
使用任务通知代替 计数信号量时,接收任务的通知值用于代替 计数信号量的计数值,以及 ulTaskNotifyTake()(或 ulTaskNotifyTakeIndexed())。 API 函数用于代替信号量的 xSemaphoreTake() API 函数。 ulTaskNotifyTake() 函数的 xClearOnExit 参数设置为 pdFALSE,因此 每次接收通知时,计数值只会递减(而不是清除), 模拟计数信号量
同样,xTaskNotifyGive() (或xTaskNotifyGiveIndexed())或 [vTaskNotifyGiveFromISR()(或 vTaskNotifyGiveIndexedFromISR() )函数用于代替信号量的 xSemaphoreGive() 和 xSemaphoreGiveFromISR()h 函数。
NotifyValue=ulTaskNotifyTake(pdFALSE,portMAX_DELAY);//获取任务通知
xTaskNotifyGive(SemapTakeTask_Handler);//发送任务通知
用作消息邮箱
任务通知也可用来向任务发送数据,但是相对于用队列发送消息,任务通知向任务发送消息会受到很多限制!
1、只能发送 32 位的数据值。
2、消息被保存为任务的任务通知值,而且一次只能保存一个任务通知值,相当于队列长度为 1。
发送数据可以使用函数 xTaskNotify()或者 xTaskNotifyFromISR(),函数的参数 eAction 设置eSetValueWithOverwrite 或 者 eSetValueWithoutOverwrite 。 如 果 参 数 eAction 为eSetValueWithOverwrite 的话不管接收任务的通知值是否已经被处理,这个通知值都会被更新。参数 eAction 为 eSetValueWithoutOverwrite 的话如果上一个任务通知值话还没有被处理,那么新的任务通知值就不会更新。如果要读取任务通知值的话就使用函数 xTaskNotifyWait()。下面通过一个实验来演示一下任务通知是如何用作消息邮箱。
err=xTaskNotify((TaskHandle_t )Keyprocess_Handler, //任务句柄
(uint32_t )key, //任务通知值
(eNotifyAction )eSetValueWithOverwrite); //覆写的方式
err=xTaskNotifyWait((uint32_t )0x00, //进入函数的时候不清除任务 bit
(uint32_t )ULONG_MAX,//退出函数的时候清除所有的 bit
(uint32_t* )&NotifyValue, //保存任务通知值
(TickType_t )portMAX_DELAY); //阻塞时间
用作事件组
事件标志组其实就是一组二进制事件标志(位),每个事件标志位的具体意义由应用程序编写者来决定。当一个任务等待事件标志组中的某几个标志(位)的时候可以进入阻塞态,当任务因为等待事件标志(位)而进入阻塞态以后这个任务就不会消耗 CPU。
当任务通知用作事件标志组的话任务通知值就相当于事件组,这个时候任务通知值的每个bit 用作事件标志 ( 位 ) 。函数 xTaskNotifyWait() 替代事件标志组中的 API 函 数xEventGroupWaitBits()。函数 xTaskNotify()和 xTaskNotifyFromISR()(函数的参数 eAction 为eSetBits)替代事件标志组中的 API 函数 xEventGroupSetBits()和 xEventGroupSetBitsFromISR()。
xTaskNotifyFromISR( xHandlingTask,
TX_BIT,
eSetBits,
&xHigherPriorityTaskWoken );
xTaskNotifyFromISR( xHandlingTask,
RX_BIT,
eSetBits,
&xHigherPriorityTaskWoken );
xResult = xTaskNotifyWait( pdFALSE, /* Don't clear bits on entry. */
ULONG_MAX, /* Clear all bits on exit. */
&ulNotifiedValue, /* Stores the notified value. */
xMaxBlockTime );
if( xResult == pdPASS )
{
/* A notification was received. See which bits were set. */
if( ( ulNotifiedValue & TX_BIT ) != 0 )
{
/* The TX ISR has set a bit. */
prvProcessTx();
}
if( ( ulNotifiedValue & RX_BIT ) != 0 )
{
/* The RX ISR has set a bit. */
prvProcessRx();
}
}
else
{
/* Did not receive a notification within the expected time. */
prvCheckForErrors();
}
低功耗 Tickless 模式
STM32F1 低功耗模式
STM32支持三种低功耗模式:
- 睡眠模式
进入睡眠模式有两种指令:WFI(等待中断)和WFE(等待事件)。根据Cortex-M 内核的SCR(系统控制)寄存器可以选择使用立即休眠还是退出时休眠,当 SCR 寄存器的 SLEEPONEXIT(bit1)位为 0 的时候使用立即休眠,当为 1 的时候使用退出时休眠。
提供了两个函数来操作指令 WFI 和 WFE:
static __INLINE void __WFI() { __ASM ("wfi"); }
static __INLINE void __WFE() { __ASM ("wfe"); }
如果使用 WFI 指令进入休眠模式的话那么任意一个中断都会将 MCU 从休眠模式中唤醒,如果使用 WFE 指令进入休眠模式的话那么当有事件发生的话就会退出休眠模式,比如配置一个 EXIT 线作为事件。
当 STM32F103 处于休眠模式的时候 Cortex-M3 内核停止运行,但是其他外设运行正常,比如 NVIC、SRAM 等。休眠模式的功耗比其他两个高,但是休眠模式没有唤醒延时,应用程序可以立即运行。
- 停止模式
停止模式基于 Cortex-M3 的深度休眠模式与外设时钟门控,在此模式下 1.2V 域的所有时钟都会停止,PLL、HSI 和 HSE RC 振荡器会被禁止,但是内部 SRAM 的数据会被保留。调压器可以工作在正常模式,也可配置为低功耗模式。如果有必要的话可以通过将 PWR_CR 寄存器的FPDS 位置 1 来使 Flash 在停止模式的时候进入掉电状态,当 Flash 处于掉电状态的时候 MCU从停止模式唤醒以后需要更多的启动延时。
- 待机模式
相比于前面两种低功耗模式,待机模式的功耗最低。待机模式是基于 Cortex-M3 的深度睡眠模式的,其中调压器被禁止。1.2V 域断电,PLL、HSI 振荡器和 HSE 振荡器也被关闭。除了备份区域和待机电路相关的寄存器外,SRAM 和其他寄存器的内容都将丢失。
退出待机模式的话会导致 STM32F1 重启,所以待机模式的唤醒延时也是最大的。
Tickless模式
考虑当处理器处理空闲任务的时候就进入低功耗模式,当需要处理应用层代码的时候就将处理器从低功耗模式唤醒。FreeRTOS 就是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式、关闭其他外设时钟、降低系统主频等等。
FreeRTOS 的系统时钟是由滴答定时器中断来提供的,系统时钟频率越高,那么滴答定时器中断频率也就越高。中断是可以将 STM32F03 从睡眠模式中唤醒,周期性的滴答定时器中断就会导致周期性的进入和退出睡眠模式。因此,如果滴答定时器中断频率太高的话会导致大量的能量和时间消耗在进出睡眠模式中,这样导致的结果就是低功耗模式的作用被大大的削弱。为此,FreeRTOS 特地提供了一个解决方法——Tickless 模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时候处理器才会被从低功耗模式中唤醒。
问题一:关闭系统节拍中断会导致系统节拍计数器停止,系统时钟就会停止。
解决:使用一个定时器来记录下系统节拍中断的关闭时间,当系统节拍中断再次开启运行的时候补上这段时间。
问题二:如何保证下一个要运行的任务能被准确的唤醒?
解决:应用层任务无法将处理器从低功耗模式唤醒,无法唤醒就无法运行!
处理器在进入低功耗模式之前获取到还有多长时间运行下一个任务,开启一个定时器,将他的周期设置为该时间值即可。
- 宏 configUSE_TICKLESS_IDLE
要想使用 Tickless 模式,首先必须将 FreeRTOSConfig.h 中的宏 configUSE_TICKLESS_IDLE设置为 1
#define configUSE_TICKLESS_IDLE 1 //1 启用低功耗 tickless 模式
- 宏 portSUPPRESS_TICKS_AND_SLEEP()
当下面两种情况都出现的时候 FreeRTOS 内核就会调用宏portSUPPRESS_TICKS_AND_SLEEP()来处理低功耗相关的工作。
①:空闲任务是唯一可运行的任务,因为其他所有的任务都处于阻塞态或者挂起态。
②:系统处于低功耗模式的时间至少大于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP个时钟节拍,宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认在文件 FreeRTOS.h 中定义为 2,我们可以在 FreeRTOSConfig.h 中重新定义,此宏必须大于等于 2!
- 宏 configPRE_SLEEP_PROCESSING ()和configPOST_SLEEP_PROCESSING()
在真正的低功耗设计中不仅仅是将处理器设置到低功耗模式,还需要做一些其他的处理。
①:将处理器降低到合适的频率,因为频率越低功耗越小,甚至可以在进入低功耗模式以后关闭系统时钟。
②:修改时钟源,晶振的功耗肯定比处理器内部的时钟源高,进入低功耗模式以后可以切换到内部时钟源,比如 STM32 的内部 RC 振荡器。
③:关闭其他外设时钟,比如 IO 口的时钟。
④:关闭板子上其他功能模块电源。
FreeRTOS 为我们提供了一个宏来完成这些操作,它就是 configPRE_SLEEP_PROCESSING(),这个宏的具体实现内容需要用户去编写。如果在进入低功耗模式之前我们降低了处理器频率、关闭了某些外设时钟等的话,那在退出低功耗模式以后就 需 要 恢 复 处理器频率、重新打开外设时钟等,这个操作在宏configPOST_SLEEP_PROCESSING()中完成,同样需要用户去编写。
/********************************************************************************/
/* FreeRTOS 与低功耗管理相关配置 */
/********************************************************************************/
extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
//进入低功耗模式前要做的处理
#define configPRE_SLEEP_PROCESSING PreSleepProcessing
//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING PostSleepProcessing
//进入低功耗模式前需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
//关闭某些低功耗模式下不使用的外设时钟,此处只是演示性代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,DISABLE);
}
//退出低功耗模式以后需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
//退出低功耗模式以后打开那些被关闭的外设时钟,此处只是演示性代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); (2)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,ENABLE);
}
函数 PreSleepProcessing()和 PostSleepProcessing()可以在任意一个 C 文件中编写。
- 宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
用于限制进入低功耗的最少系统时间。默认情况下 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 为 2 个时钟节拍,并且最小不能小于 2 个时钟节拍。如果要修改这个值的话可以在文件 FreeRTOSConfi.h 中对其重新定义。此宏会在空闲任务函数 prvIdleTask()中使用!
#ifndef configEXPECTED_IDLE_TIME_BEFORE_SLEEP
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
#endif
#if configEXPECTED_IDLE_TIME_BEFORE_SLEEP < 2
#error configEXPECTED_IDLE_TIME_BEFORE_SLEEP must not be less than 2
#endif
内存管理
概述
每当任务,队列或是信号量被创建时,内核需要进行动态内存分配。虽然可以调用标准的 malloc()与 free()库函数,但必须承担以下若干问题:
-
这两个函数在小型嵌入式系统中可能不可用。
-
这两个函数的具体实现可能会相对较大,会占用较多宝贵的代码空间。
-
这两个函数通常不具备线程安全特性。
-
这两个函数具有不确定性。每次调用时的时间开销都可能不同。
-
这两个函数会产生内存碎片。
-
这两个函数会使得链接器配置得复杂。
不同的嵌入式系统具有不同的内存配置和时间要求。所以单一的内存分配算法只可能适合部分应用程序。因此,FreeRTOS 将内存分配作为可移植层面。这使得不同的应用程序可以提供适合自身的具体实现。
当内核请求内存时,其调用 pvPortMalloc()
而不是直接调用 malloc();当释放内存时,调用 vPortFree()
而不是直接调用 free()。pvPortMalloc()
具有与 malloc()相同的函数原型;vPortFree()
也具有与 free()相同的函数原型。
FreeRTOS
提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和 heap_5.c。
动 态 内 存 分 配 需 要 一 个 内 存 堆 , FreeRTOS
中 的 内 存 堆 为 ucHeap[]
, 大 小 为 configTOTAL_HEAP_SIZE
。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[],而且大小都是 configTOTAL_HEAP_SIZE
。内存堆在文件heap_x.c(x 为 1~5)
中定义的。
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; /*用户自定义内存堆*/
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; /*编译器决定*/
#endif
当宏 configAPPLICATION_ALLOCATED_HEAP 为 1 的时候需要用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部 SRAM 或者 SDRAM 中。使用函数 xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。
heap_1.c
- Heap_1.c 实现了一个非常基本的 pvPortMalloc()版本,而且没有实现 vPortFree()。如果应用程序不需要删除任务,队列或者信号量,则具有使用 heap_1 的潜质。
- 具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
- 代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。
pvPortMalloc申请内存
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
/*确保字节对齐 默认portBYTE_ALIGNMENT = 8 portBYTE_ALIGNMENT_MASK = 0x0007*/
#if( portBYTE_ALIGNMENT != 1 )
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
/*需要进行字节对齐 x = x + 8 - (x & 0x0007)*/
xWantedSize += ( portBYTE_ALIGNMENT -(xWantedSize & portBYTE_ALIGNMENT_MASK ));
}
}
#endif
vTaskSuspendAll(); /*关闭调度器 保护申请内存过程*/
{
if( pucAlignedHeap == NULL )
{
/*确保内存堆的开始是字节对齐的*/
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )
&ucHeap[ portBYTE_ALIGNMENT ] )
& ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
/* 检查是否有足够的内存供分配,有的话就分配内存 */
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )/* 检查是否溢出 */
{
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize;
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();/*恢复调度器*/
#if( configUSE_MALLOC_FAILED_HOOK == 1 ) /*内存malloc失败钩子函数*/
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
vPortFree释放内存
void vPortFree( void *pv )
{
( void ) pv;
configASSERT( pv == NULL ); /*未释放申请的内存*/
}
heap_2.c
- heap_2 采用了一个最佳匹配算法来分配内存,并且支持内存释放,最佳匹配算法保证 pvPortMalloc()会使用最接近请求大小的空闲内存块。
- 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!
- 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了
内存块详解
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*指向链表中下一个空闲内存块 */
size_t xBlockSize; /*当前空闲内存块大小*/
} BlockLink_t;
每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块。用来将各个内存块连接起来
申请内存16字节,但是还要加上另外8字节保存BlockLink_t变量,描述内存块,使得内存形成列表。
static BlockLink_t xStart, xEnd; // 分别代表链表的头部和尾部
prvHeapInit内存初始化
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
/*确保内存堆的开始地址是字节对齐的 portBYTE_ALIGNMENT = 8 */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )
&ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE )
portBYTE_ALIGNMENT_MASK ) ) );
/*使得xStart指向空闲内存块链表首 */
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
/*xEnd 指向空闲内存块链表尾*/
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
xEnd.pxNextFreeBlock = NULL;
/* 刚开始只有一个空闲内存块,空闲内存块的总大小就是可用的内存堆大小 */
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}
prvInsertBlockIntoFreeList内存块插入
本质上是一个单列表,将各个内存块连接起来,用来将某个内存块插入到空闲内存块链表中
#define prvInsertBlockIntoFreeList( pxBlockToInsert )
{
BlockLink_t *pxIterator;
size_t xBlockSize;
xBlockSize = pxBlockToInsert->xBlockSize;
/* 遍历链表,查找插入点 */
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize;
pxIterator = pxIterator->pxNextFreeBlock )
{
/* 不做任何事情 */
}
/* 将内存块插入到插入点 */
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
pvPortMalloc申请内存
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* 如果是第一次申请内存的话需要初始化内存堆 */
if( xHeapHasBeenInitialised == pdFALSE )
{
prvHeapInit();
xHeapHasBeenInitialised = pdTRUE;
}
/* /内存大小字节对齐,实际申请的内存大小还要加上结构体 */
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE;
/* xWantedSize 做字节对齐处理*/
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &
portBYTE_ALIGNMENT_MASK ) );
}
}
/*所申请的内存大小合理,进行内存分配。*/
if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
/* 从 xStart(最小内存块)开始,查找大小满足所需要内存的内存块 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while((pxBlock->xBlockSize<xWantedSize)&&(pxBlock->pxNextFreeBlock != NULL))
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
if( pxBlock != &xEnd )
{
/* 返回申请到的内存首地址 */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock )
+ heapSTRUCT_SIZE );
/*将这个内存块从空闲内存块链表中移除*/
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/*确保该内存块的大小比要申请的内存块大小不大于某个临界值*/
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
pxNewBlockLink = (void *)(((uint8_t *)pxBlock)+xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
}
xFreeBytesRemaining -= pxBlock->xBlockSize;
}
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
vPortFree内存释放
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
puc -= heapSTRUCT_SIZE;/*获取要释放的内存首地址*/
pxLink = ( void * ) puc;
vTaskSuspendAll();
{
/*将内存块添加到空闲内存块链表中*/
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
xFreeBytesRemaining += pxLink->xBlockSize; /*更新系统可用内存大小*/
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
heap_3.c
Heap_3.c 简单地调用了标准库函数 malloc()和 free(),但是通过暂时挂起调度器使得函数调用备线程安全特性。此时的内存堆空间大小不受 configTOTAL_HEAP_SIZE 影响,而是由链接器配置决定。
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll(); // 挂起调度器,为malloc free 提供线程保护
{
pvReturn = malloc( xWantedSize ); // 申请内存
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll(); // 恢复任务调度器
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
void vPortFree( void *pv )
{
if( pv )
{
vTaskSuspendAll();
{
free( pv );
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll();
}
}
STM32通过修改启动文件startup_stm32f10x_hd.s中的 Heap_Size 来修改内存堆的大小。内存堆空间大小不受 configTOTAL_HEAP_SIZE 影响。
heap_4.c
heap_4 提供了一个最优的匹配算法,heap_4 会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。内存堆为 ucHeap[]
,大小同样为 configTOTAL_HEAP_SIZE
。可以通过函数 xPortGetFreeHeapSize()
来获取剩余的内存大小。
- 可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
- 不会像 heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。
- 具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。
- 非常适合于那些需要直接调用函数 pvPortMalloc()和 vPortFree()来申请和释放内存的应用
- heap_4 也使用链表结构来管理空闲内存块,链表结构体与 heap_2 一样。heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾,其中 pxEnd 是指向 BlockLink_t 的指针。
prvHeapInit内存初始化
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
/* 起始地址做字节对齐处理 */
uxAddress = ( size_t ) ucHeap;
if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
{
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
}
/*pucAlignedHeap 为内存堆字节对齐以后的可用起始地址。*/
pucAlignedHeap = ( uint8_t * ) uxAddress;
/* xStart 为空闲链表头。*/
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
/* pxEnd 为空闲内存块列表尾,并且将其放到到内存堆的末尾. */
uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( void * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;
/* 开始的时候将内存堆整个可用空间看成一个空闲内存块 */
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
/* 只有一个内存块,而且这个内存块拥有内存堆的整个可用空间 */
/*xMinimumEverFreeBytesRemaining 记录最小的那个空闲内存块大小*/
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
/*xFreeBytesRemaining 表示内存堆剩余大小。*/
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
/*初始化xBlockAllocatedBit 0X80000000,标记某块内存是否使用*/
xBlockAllocatedBit = ((size_t) 1) << ((sizeof( size_t )*heapBITS_PER_BYTE ) - 1);
}
BlockLink_t 中的成员变量 xBlockSize 是用来描述内存块大小的,在 heap_4 中其最高位表示此内存块是否被使用,如果为 1 的话就表示被使用了,所以在 heap_4 中一个内存块最大只能为 0x7FFFFFFF。
prvInsertBlockIntoFreeList内存块插入
内存块插入函数 prvInsertBlockIntoFreeList()用来将某个内存块插入到空闲内存块链表中。对于内存块的起始地址和结尾地址是连接在一起的,那么就会进行合并。
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;
/*遍历空闲内存块链表,找出内存块插入点,内存块按照地址从低到高连接在一起*/
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{
/*不做任何处理*/
}
/* 插入内存块,如果要插入的内存块可以和前一个内存块合并的话就合并两个内存块 */
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/*检查是否可以和后面的内存块合并,可以的话就合并*/
puc = ( uint8_t * ) pxBlockToInsert;
if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
{
if( pxIterator->pxNextFreeBlock != pxEnd )
{
/*将两个内存块组合成一个大的内存块*/
pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
}
else
{
pxBlockToInsert->pxNextFreeBlock = pxEnd;
}
}
else
{
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}
/* pxIterator 不等于 pxBlockToInsert 就意味着在内存块插入的过程中没有进行过一次内
存合并,这样的话就使用最普通的处理方法*/
if( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pvPortMalloc内存申请
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* 第一次调用,初始化内存堆*/
if( pxEnd == NULL )
{
prvHeapInit();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 需要申请的内存块大小的最高位不能为 1,因为最高位用来表示内存块有没有被使用 */
if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
{
if( xWantedSize > 0 )
{
xWantedSize += xHeapStructSize;
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &
portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
/* 从 xStart(内存块最小)开始,查找大小满足所需要内存的内存块 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) &&
( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 如果找到的内存块是 pxEnd 的话就表示没有内存可以分配 */
if( pxBlock != pxEnd )
{
pvReturn = ( void * ) ( ( ( uint8_t * )
pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
/* 将申请到的内存块从空闲内存链表中移除 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果申请到的内存块大于所需的内存,就将其分成两块 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) +
xWantedSize );
configASSERT( ( ( ( size_t ) pxNewBlockLink ) &
portBYTE_ALIGNMENT_MASK ) == 0 );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
prvInsertBlockIntoFreeList( pxNewBlockLink );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xFreeBytesRemaining -= pxBlock->xBlockSize;
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 内存块申请成功,标记此内存块已经被时候*/
pxBlock->xBlockSize |= xBlockAllocatedBit;
pxBlock->pxNextFreeBlock = NULL;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook(); // 调用钩子函数
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
vPortFree内存释放
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
puc -= xHeapStructSize; // 获取内存块的 BlockLink_t 类型结构体
pxLink = ( void * ) puc;
configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )// 确定是被使用的内存
{
if( pxLink->pxNextFreeBlock == NULL )
{
pxLink->xBlockSize &= ~xBlockAllocatedBit; // 最高位清零,标记此内存块未使用
vTaskSuspendAll();
{
/*将内存块插到空闲内存链表中*/
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
heap_5.c
heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。比如 STM32 的内部 RAM 可以作为内存堆,但是 STM32 内部 RAM 比较小,遇到那些需要大容量 RAM 的应用就不行了,如音视频处理。不过 STM32 可以外接 SRAM 甚至大容量的 SDRAM,如果使用 heap_4 的话你就只能在内部 RAM 和外部SRAM 或 SDRAM 之间二选一了,使用 heap_5 的话就不存在这个问题,两个都可以一起作为内存堆来用。
如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数 vPortDefineHeapRegions ()
来对内存堆做初始化处理,在 vPortDefineHeapRegions()
未执行完之前禁止调用任何可能会调用pvPortMalloc()
的 API 函数!比如创建任务、信号量、队列等函数。
typedef struct HeapRegion // 定义在portable.h文件
{
uint8_t *pucStartAddress; // 内存块的起始地址
size_t xSizeInBytes; // 内存段大小
} HeapRegion_t;
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
heap_5 允许内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。比如以 STM32F103 开发板为例,现在有连个内存段:内部 SRAM、外部 SRAM,起始分别为:0X20000000、0x68000000,大小分别为:64KB、1MB,那么数组就如下:
HeapRegion_t xHeapRegions[] =
{
{( uint8_t *) 0X20000000UL, 0x10000 },//内部 SRAM 内存,起始地址 0X20000000,大小为 64KB
{( uint8_t *) 0X68000000UL, 0x100000},//外部 SRAM 内存,起始地址 0x68000000,大小为 1MB
{ NULL, 0 } //数组结尾
};
注意数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL。
错误排查
printf-stdarg.c
当调用标准 C 库函数时,栈空间使用量可能会急剧上升,特别是 IO 与字符串处理函数,比如 sprintf()。 FreeRTOS 下载包中有一个名为 printf-stdarg.c 的文件。这个文件实现了一个栈效率优化版的小型 sprintf(),可以用来代替标准 C 库函数版本。在大多数情况下,这样做可以使得调用 sprintf()及相关函数的任务对栈空间的需求量小很多。
栈溢出
uxTaskGetStackHighWaterMark
每个任务都独立维护自己的栈空间,栈空间总量在任务创建时进行设定。uxTaskGetStackHighWaterMark()主要用来查询指定任务的运行历史中,其栈空间还差多少就要溢出。这个值被称为栈空间的”高水线(High Water Mark)”。
unsigned portBASE_TYPE uxTaskGetStackHighWaterMark( xTaskHandle xTask );
xTask:被查询任务的句柄
返回值:返回从任务启动执行开始的运行历史中,栈空间具有的最小剩余量。
这个值即是栈空间使用达到最深时的剩下的未使用的栈空间。
这个值越是接近 0,则这个任务就越是离栈溢出不远了。
configCHECK_FOR_STACK_OVERFLOW
FreeRTOSConfig.h 中的配置常量configCHECK_FOR_STACK_OVERFLOW 控制两种运行时栈侦测机制。这两种方式都会增加上下切换开销。
栈溢出钩子函数(或称回调函数)由内核在侦测到栈溢出时调用。要使用栈溢出钩子函数,需配置configCHECK_FOR_STACK_OVERFLOW 设为 1 或 2。
void vApplicationStackOverflowHook( xTaskHandle *pxTask, signed portCHAR *pcTaskName );
该回调函数只是为了调试追踪使用,无法对栈溢出进行恢复,不过还可以在中断的上下文中进行调用。
- configCHECK_FOR_STACK_OVERFLOW 设置为 1
任务被交换出去的时候,该任务的整个上下文被保存到它自己的栈空间中。内核会在任务上下文保存后检查栈指针是否还指向有效栈空间。一旦检测到栈指针的指向已经超出任务栈的有效范围,栈溢出钩子函数就会被调用。
该方法具有较快的执行速度,但栈溢出有可能发生在两次上下文保存之间,这种情况不会被侦测到。
- configCHECK_FOR_STACK_OVERFLOW 设置为 2
在上一个方法进行了补充,当创建任务时,任务栈空间中就预置了一个标记。会检查任务栈的最后 20个字节,查看预置在这里的标记数据是否被覆盖。如果最后 20 个字节的标记数据与预设值不同,则栈溢出钩子函数就会被调用。这种方法可以侦测到任何时候发生的栈溢出。