我的FreeRTOS学习,是从朱工的FreeRTOS系列博客开始的,感谢朱工的悉心整理,文章很不错,适合学习。
根据朱工的博客,按照我自己的习惯和思路,把最关键的信息抓取下来,以备快速理清思路。
具体展开和丰富,需要移步朱工的文章,或者参考官方网站的资料。
1. 什么是FreeRTOS
开源,免费,用户量大,内核简练的一个实时操作系统。
因为开源免费,所以用的人多,所有bug少。
核心是任务调度器,比较短小,3个C文件,学习压力相对小。
官网 www.freertos.org
2. 简单入门
文档
- 指南
- 手册
- demo文档, 官网左侧Supported Devices & Demos
源码
- 内核源码
- 移植包
结构
- 顶层
- FreeRTOS-Plus
- FreeRTOS 主要关注这个
FreeRTOS
FreeRTOS
|+-- Demo 包含演示例程工程;
|+-- Source 包含实时内核源文件。
tasks.c
queue.c
list.c 以上3个是主要的文件,下面3个是可选的
timers.c
event_group.c
croutine.c
FreeRTOS/Source
FreeRTOS
| +-- Source FreeRTOS内核代码文件
| |+-- include FreeRTOS内核代码头文件
| |+-- Portable 处理器特定代码
| | |+--Compiler x 支持编译器x的所有移植包
| | |+--Compiler y 支持编译器y的所有移植包
| | |+--MemMang 内存堆实现范例
- 处理器架构相关代码
- FreeRTOS/Source/Portable/[相应编译器]/[相应CPU架构]子目录
- 堆栈设计
- FreeRTOS/Source/portable/MemMang目录下heap_x.c
演示例程
FreeRTOS
|+-- Demo
| |+-- Common 所有例程都可以使用的演示例程文件
| |+-- Dir x 用于x平台的演示例程工程文件
| |+-- Dir y 用于y平台的演示例程工程文件
如果有演示例程中相同的设备,就可以开始编译和查看结果。
3. 移植
演示例程都是针对特定的对象
- 特定的微控制器
- 开发工具(编译器,调试器)
- 特定的平台(开发板,样机)
官方例程没有的,就需要移植。
最简单的上前两条都相同,只是不同的开发板。直接开始编译就可以,target都是一样的。
但是GPIO管脚什么的肯定不一样,所以即使能编译过,一些功能测试函数肯定也是功能错误的。
partest.c里边的vParTestInitialise()用于IO端口配置,vParTestSetLED和vParTestToggleLED用来测试GPIO,这些需要根据当前的target改改,然后在main里边调用测试
int main( void )
{
volatile unsigned long ul; /* 禁止编译器优化此变量 */
/* 初始化LED IO为输出-注:prvSetupHardware()也可能会被调用*/
vParTestInitialise();
/*不断开启LED */
for( ;; )
{
/* 我们暂时不使用RTOS,这里只是使用一个非常粗糙的延时*/
for( ul = 0; ul < 0xfffff; ul++ )
{
}
/* 开启4个LED */
vParTestToggleLED( 0 );
vParTestToggleLED( 1 );
vParTestToggleLED( 2 );
vParTestToggleLED( 3 );
}
return 0;
}
LED点灯测试没有问题了,就可以开始测试任务调度了
使用vStartLEDFlashTasks() 测试任务调度,如果例程不包含,就添加FreeRTOS/Demo/Common/Minimal/Flash.c到工程中,功能是让几个LED等以不公的频率闪。
int main( void )
{
/* 设置用于演示的微控制器硬件 */
prvSetupHardware();
/* 留下这个函数 */
vCreateFlashTasks();
/* 所有的其它创建任务的函数统统注释掉
vCreatePollQTasks();
vCreateComTestTasks();
//等等…
xTaskCreate( vCheckTask,"check", STACK_SIZE, NULL, TASK_PRIORITY, NULL );
*/
/*启动RTOS调度器. */
vTaskStartScheduler();
/* 永远不会执行到这里! */
return 0;
}
能看到主要的函数就3个
- prvSetupHardware() 设置硬件
- vCreateFlashTasks() 创建闪灯任务
- vTaskStartScheduler() 启动调度
特定target的内核文件
特定平台,大多数文件位于
- FreeRTOS/source/portable/[编译器]/[微控制器/port.c
- FreeRTOS/source/portable/[编译器]/[微控制器]/portmacro.h
- 可能还有portasm.s或者portasm.asm
4. 编码标准
FreeRTOS的核心源代码遵从MISRA编码标准指南。
不展开了。
5. CM3的移植
准备
- 开发板
- FreeRTOS程序包
- CMSIS-M3,core_cm3.h
过程
- 核心代码加入工程
- taks.c, queue.c, list.c
- port.c
- heap_1.c
- 头文件路径
- …\FreeRTOS\Source\portable\RVDS\ARM_CM3
- …\FreeRTOS\Source\include
- 编写FreeRTOSConfig.h文件,下一节介绍
- 编写钩子函数
- 如果配置了configUSE_TICK_HOOK=1,需要编写voidvApplicationTickHook( void )
- 如果配置了configCHECK_FOR_STACK_OVERFLOW=1或=2,需要编写voidvApplicationStackOverflowHook( xTaskHandle pxTask, signed char *pcTaskName )
- 检查硬件,亮一下LED或者UART串口发个字符
#include"task.h"
#include"queue.h"
#include"list.h"
#include"portable.h"
#include"debug.h"
int main(void)
{
init_rtos_debug(); //初始化调试串口
MAP_UARTCharPut('A'); //发送一个字符
while(1);
}
5. 挂接中断,在startup.s中,使用IMPORT关键字声明要挂接的异常中断服务函数名
DCD SVC_Handler 换成: DCD vPortSVCHandler
DCD PendSV_Handler 换成: DCD xPortPendSVHandler
DCD SysTick_Handler 换成: DCD xPortSysTickHandler
6. 建立第一个任务,每隔一秒钟发送一个字符
voidvTask(void *pvParameters)
{
while(1)
{
MAP_UARTCharPut(0x31);
vTaskDelay(1000/portTICK_RATE_MS);
}
}
其中vTaskDelay()是API函数。
7. 设置节拍时钟,每10ms产生一个中断,只需要在FreeRTOSConfig.h里边做下配置
- configCPU_CLOCK_HZ (/你的硬件平台CPU系统时钟,Fcclk/)
- configTICK_RATE_HZ ((portTickType)100)
在prot.c中,函数vPortSetupTimerInterrupt()设置节拍时钟,由函数vTaskStartScheduler()调用,这个函数用于启动调度器。
8. 设置中断优先级宏,在FreeRTOSConfig.h中
#ifdef __NVIC_PRIO_BITS
#defineconfigPRIO_BITS __NVIC_PRIO_BITS
#else
#defineconfigPRIO_BITS 5 /*lpc177x_8x微处理器使用优先级寄存器的5位*/
#endif
/*设置内核使用的中断优先级*/
#define configKERNEL_INTERRUPT_PRIORITY ( 31 << (8 - configPRIO_BITS) )
/*定义RTOS可以屏蔽的最大中断优先级,大于这个优先级的中断,不受RTOS控制*/
#defineconfigMAX_SYSCALL_INTERRUPT_PRIORITY ( 5<< (8 - configPRIO_BITS) )
9. 设置其他宏,在FreeRTOSConfig.h里边
#define configUSE_PREEMPTION 1 //配置为1使用抢占式内核,配置为0使用时间片
#define configUSE_IDLE_HOOK 0 //设置为1使用空闲钩子;设置为0不使用空闲钩子
#define configMAX_PRIORITIES ( 5 ) //应用程序任务中可用优先级数目
#define configUSE_TICK_HOOK 0 //就设置为1使用时间片钩子,设置为0不使用
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 80 ) //最小空闲堆栈
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 5 * 1024 ) ) //内核总共可用RAM
9. 创建任务
xTaskCreate(vTask,"Task1",50,NULL,1,NULL);
10. 开启调度器
vTaskStartScheduler();
此时main函数的代码如下
int main(void)
{
init_rtos_debug(); //初始化调试串口
xTaskCreate(vTask,"Task1",50,NULL,1,NULL);
vTaskStartScheduler();
while(1);
}
6. 内核配置
FreeRTOS的定制体现在FreeRTOSConfig.h的定制上。每一个FreeRTOS应用都必须包含这个文件,这个文件针对用户,不针对内核,放在应用程序目录下。
- configUSE_PREEMPTION, 1:抢占式调度,0:协作式调度
- configUSE_PORT_OPTIMISED_TASK_SELECTION, 选择下一个要执行的任务,1:特殊方法,0:通用方法
- configUSE_TICKLESS_IDLE, 1:使能低功耗tickless模式,0:tick中断一直运行
- configUSE_IDLE_HOOK, 1:使用空闲钩子,0:忽略空闲钩子
- configUSE_MALLOC_FAILED_HOOK, 1:必须定义一个malloc()失败钩子函数,0:malloc()失败钩子函数不会被调用
- configUSE_TICK_HOOK, 1:使用时间片钩子函数,0:忽略时间片钩子函数
- configCPU_CLOCK_HZ, CPU内核时钟频率 FCLK
- configTICK_RATE_HZ, 系统街拍中断频率,每秒中断次数
- configMAX_PRIORITIES, 有效优先级数目
- configMINIMAL_STACK_SIZE, 空闲任务使用的堆栈大小
- configTOTAL_HEAP_SIZE, FreeRTOS内核使用的有效RAM大小
- configMAX_TASK_NAME_LEN, 任务描述信息字符串最大长度
- configUSE_TRACE_FACILITY, 1:启动可视化跟踪调试
- configUSE_STATS_FORMATTING_FUNCTIONS, 编译vTaskList()和vTaskGetRunTimeStats()函数,需要configUSE_TRACE_FACILITY也设置成1
- configUSE_16_BIT_TICKS, 定义portTickType是表示16位变量还是32位变量
- configIDLE_SHOULD_YIELD, 如果使用抢占式调度,用户任务使用空闲优先级,1:共享空闲任务优先级的用户任务就绪时,空闲任务立即让出CPU,0:空闲任务结束时才会让出CPU
- configUSE_TASK_NOTIFICATIONS, 1:开启任务通知,0:关闭通知功能
- configUSE_MUTEXES, 1:使用互斥量,0:忽略互斥量
- configUSE_RECURSIVE_MUTEXES, 1:使用递归互斥量,0:不使用
- configUSE_COUNTING_SEMAPHORES, 1:使用计数信号量,0:不使用
- configUSE_ALTERNATIVE_API, 1:使用“替代”队列函数,0:不使用
- configCHECK_FOR_STACK_OVERFLOW, 0:用户提供栈溢出钩子函数,1:任务切换时内核检查栈指针是否指向堆栈,2:任务切换时内核检查堆栈结尾是否被覆盖
- configQUEUE_REGISTRY_SIZE, 记录队列和信号量的最大数目
- configUSE_QUEUE_SETS, 1:使能队列集功能,0:取消队列集功能
- configUSE_TIME_SLICING, 1:基于时间片的优先级抢占调度,节拍器中断时相同优先级任务切换,0:节拍器中断时相同优先级任务不切换
- configUSE_NEWLIB_REENTRANT, 1:每一个创建的任务分配一个neblib reent结构
- configENABLE_BACKWARD_COMPATIBILITY, 1:通过宏定义确保v8.0.0之前版本的代码兼容性,0:去掉这些宏定义
- configNUM_THREAD_LOCAL_STORAGE_POINTERS, 每个任务的线程本地存储指针大小
- configGENERATE_RUN_TIME_STATS, 1:使能时间统计功能
- configUSE_CO_ROUTINES, 1:使用协程,0:不使用(协程主要用于资源非常受限的嵌入式系统,RAM非常少,通常不会用于32位微处理器,FreeRTOS开发者停止开发协程了)
- configMAX_CO_ROUTINE_PRIORITIES, 协程的有效优先级数目
- configUSE_TIMERS, 1:使用软甲定时器,0:不使用
- configTIMER_TASK_PRIORITY, 软件定时服务/守护进程的优先级
- configTIMER_QUEUE_LENGTH, 软件定时器命令队列长度
- configTIMER_TASK_STACK_DEPTH, 软件定时器服务/守护进程的堆栈深度
- configKERNEL_INTERRUPT_PRIORITY、configMAX_SYSCALL_INTERRUPT_PRIORITY和configMAX_API_CALL_INTERRUPT_PRIORITY
- configMAX_SYSCALL_INTERRUPT_PRIORITY 使用FreeRTOS API函数的中断例程,其优先级数值需要大于此定义,即逻辑优先级低于它(CM3的情况),不能设置成0
- configASSERT, 定义断言的宏
- INCLUDE Parameters, 允许用户不编译那些应用程序不需要的内核组件(函数)
/*需要使用vTaskDelete函数*/
#define INCLUDE_vTaskDelete 1
/*不需要使用vTaskDelete函数*/
#define INCLUDE_vTaskDelete 0
7. 内存管理
创建任务、队列、互斥量、软件定时器、信号量或事件组时,RTOS内核会为它们分配RAM。标准函数库中的malloc()和free()函数有些时候能够用于完成这个任务,但是:
- 在嵌入式系统中,它们并不总是可以使用的;
- 它们会占用更多宝贵的代码空间;
- 它们没有线程保护;
- 它们不具有确定性(每次调用执行的时间可能会不同)
FreeRTOS把内存分配放到了移植层,可以有不同的实现,通过API来完成。当RTOS内核需要RAM时,调用pvPortMallo()函数来代替malloc()函数。当RAM要被释放时,调用vPortFree()函数来代替free()函数。
FreeRTOS提供了不同的方案,位于\FreeRTOS\Source\portable\MemMang
- heap_1.c
- heap_2.c
- heap_3.c
- heap_4.c
- heap_5.c
heap_1
- 用于从不会删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS的应用程序都符合这个条件)
- 执行时间是确定的并且不会产生内存碎片
- 实现和分配过程非常简单,需要的内存是从一个静态数组中分配的,意味着这种内存分配通常只是适用于那些不进行动态内存分配的应用
将一个大数组细分出一个子集来,大数组的容量大小通过FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来设置
API函数xPortGetFreeHeapSize()返回未分配的堆栈空间总大小
heap_2
heap_2.c适用于需要动态创建任务的大多数小型实时系统(smallreal time)
- 可以用于重复的分配和删除具有相同堆栈空间的任务、队列、信号量、互斥量等等,并且不考虑内存碎片的应用程序
- 不能用在分配和释放随机字节堆栈空间的应用程序(因为会产生碎片,碎片过多会倒置分配失败)
- 不具有确定性,但是它比标准库中的malloc函数具有高得多的效率
和方案1不同,这个方案使用一个最佳匹配算法,它允许释放之前分配的内存块。它不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)
有效的堆栈空间大小由位于FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来定义
API函数xPortGetFreeHeapSize()返回剩下的未分配堆栈空间的大小
heap_3
heap_3.c简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护
- 需要链接器设置一个堆栈,并且编译器库提供malloc()和free()函数
- 不具有确定性
- 可能明显的增大RTOS内核的代码大小
用heap_3时,FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏定义没有作用
heap_4
使用一个最佳匹配算法,但不像方案2那样。它会将相邻的空闲内存块合并成一个更大的块(包含一个合并算法)
- 可用于重复分配、删除任务、队列、信号量、互斥量等等的应用程序
- 可以用于分配和释放随机字节内存的情况,并不像heap_2.c那样产生严重碎片
- 不具有确定性,但是它比标准库中的malloc函数具有高得多的效率
特别适用于移植层代码,可以直接使用pvPortMalloc()和 vPortFree()函数来分配和释放内存
heap_5
同样实现了heap_4.c中的合并算法,并且允许堆栈跨越多个非连续的内存区
- 通过调用vPortDefineHeapRegions()函数实现初始化,在该函数执行完成前不允许使用内存分配和释放
- 创建RTOS对象(任务、队列、信号量等等)会隐含的调用pvPortMalloc(),因此必须注意:使用heap_5创建任何对象前,要先执行vPortDefineHeapRegions()函数
typedef struct HeapRegion
{
/* 用于内存堆的内存块起始地址*/
uint8_t *pucStartAddress;
/* 内存块大小 */
size_t xSizeInBytes;
} HeapRegion_t;
/* 在内存中为内存堆分配两个内存块.第一个内存块0x10000字节,起始地址为0x80000000,
第二个内存块0xa0000字节,起始地址为0x90000000.起始地址为0x80000000的内存块的
起始地址更低,因此放到了数组的第一个位置.*/
const HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0x80000000UL, 0x10000 },
{ ( uint8_t * ) 0x90000000UL, 0xa0000 },
{ NULL, 0 } /* 数组结尾. */
};
/* 向函数vPortDefineHeapRegions()传递数组参数. */
vPortDefineHeapRegions( xHeapRegions );
8. 任务
8.1 概述
任务和协程
使用不同的API函数,两者不能使用同一个队列或者信号量传递数据。
协程不讨论。
任务的概念
- 每个RTOS应用程序,都是若干独立任务的集合
- 每个任务有自己的环境
- RTOS调度器负责停止,启动任务,任务的上下文切换也由调度器完成,看起来所有的任务都在推进
- 任务的上下文保存在自己的堆栈里
- 任务应该简单
- 任务没有使用限制
- 任务有优先级
- 任务堆栈独立,RAM有消耗
- 使用抢占的话,重入问题需要考虑
任务状态
- 运行
- 就绪
- 阻塞
- 挂起
运行
正在执行,占用CPU。
就绪
具备执行能力,没有阻塞和挂起,但是同优先级或者更高优先级的任务在运行,所以还没有真正执行
阻塞
等待某个时序或者外部中断。通常有“超时”周期,超时后解除阻塞。
挂起
调度器不再管理。通过API进入和退出挂起状态:vTaskSuspend() 和xTaskResume()
优先级
0~configMAX_PRIORITIES,每个任务都有
- 数值低表示优先级低,空闲任务优先级0
- 调度器确保处于最高优先级的就绪或者运行任务获得CPU
- 可以共享优先级,无数量限制
实现
void vATaskFunction( voidvoid *pvParameters )
{
for( ;; )
{
/*-- 应用程序代码放在这里. --*/
}
/* 任务不可以从这个函数返回或退出。在较新的FreeRTOS移植包中,如果
试图从一个任务中返回,将会调用configASSERT()(如果定义的话)。
如果一个任务确实要退出函数,那么这个任务应调用vTaskDelete(NULL)
函数,以便处理一些清理工作。*/
vTaskDelete( NULL );
}
- 返回void
- 入参是void *
- 任务函数绝不应该返回,通常就是死循环
- 任务由xTaskCreate()函数创建,由vTaskDelete()函数删除
空闲任务
- 启动调度器时内核自动创建的任务,确保至少有一个任务在运行
- 是由空闲任务来释放被删除的任务的资源,因此vTaskDelete()函数后确保空闲任务能获得处理器时间就很重要了
- 用户任务可以共享空闲任务的优先级
空闲任务钩子
- 钩子函数每一个空闲任务周期调用一次
- 可以用来实现任务程序运行在空闲任务优先级上,在钩子函数中实现即可
- 限制:钩子函数不可以调用能够阴气阻塞的API函数,例如vTaskDelay()或者带有超时事件的队列或信号量函数
- 创建步骤
- 设置configUSE_IDLE_HOOK为1
- 定义一个函数,名字和参数原型如下所示
void vApplicationIdleHook( void );
通常,使用这个空闲钩子函数设置CPU进入低功耗模式
8.2 创建和删除
1. 创建
1.1 创建函数
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const charchar * const pcName,
unsigned short usStackDepth,
voidvoid *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * pvCreatedTask
);
果使用FreeRTOS-MPU(在官方下载包中,为Cortex-M3内核写了两个移植方案,一个是普通的FreeRTOS移植层,还有一个是FreeRTOS-MPU移植层。后者包含完整的内存保护),那么推荐使用函数xTaskCreateRestricted()来代替xTaskCreate()
1.2 参数
pvTaskCode
指针,指向任务函数的入口。
TaskFunction_t定义在文件projdefs.h中,定义为:typedefvoid (TaskFunction_t)( void )
pcName
任务描述。主要用于调试。
usStackDepth
指定任务堆栈大小,能够支持的堆栈变量数量,而不是字节数。
pvParameters
指针,当任务创建时,作为一个参数传递给任务。
uxPriority
任务的优先级。
比如,创建一个优先级为2的特权任务,参数uxPriority可以设置为( 2 | portPRIVILEGE_BIT )
pvCreatedTask
用于回传一个句柄(ID),创建任务后可以使用这个句柄引用任务。
1.3 返回值
如果任务成功创建并加入就绪列表函数返回pdPASS,否则函数返回错误码,具体参见projdefs.h
1.4 举例
/* 创建任务. */
void vTaskCode( voidvoid * pvParameters )
{
for( ;; )
{
/* 任务代码放在这里 */
}
}
/* 创建任务函数 */
void vOtherFunction( void )
{
static unsigned char ucParameterToPass;
xTaskHandlexHandle;
/* 创建任务,存储句柄。注:传递的参数ucParameterToPass必须和任务具有相同的生存周期,
因此这里定义为静态变量。如果它只是一个自动变量,可能不会有太长的生存周期,因为
中断和高优先级任务可能会用到它。 */
xTaskCreate( vTaskCode, "NAME", STACK_SIZE,&ucParameterToPass, tskIDLE_PRIORITY, &xHandle );
/* 使用句柄删除任务. */
if( xHandle !=NULL )
{
vTaskDelete( xHandle );
}
}
2. 删除
2.1 任务描述
voidvTaskDelete( TaskHandle_t xTask );
任务删除后将会从就绪、阻塞、暂停和事件列表中移除。
FreeRTOSConfig.h中,必须定义宏INCLUDE_vTaskDelete 为1,本函数才有效
2.2 参数
- xTask:被删除任务的句柄。为NULL表示删除当前任务
8.3 任务控制
1. 相对延时
- 调用vTaskDelay()函数后,任务会进入阻塞状态
- 持续时间由vTaskDelay()函数的参数xTicksToDelay指定,单位是系统节拍时钟周期
- 延时时间是从调用vTaskDelay()后开始计算的相对时间
- 其它任务和中断活动,会影响到vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务)
- API函数vTaskDelayUntil()可用于固定频率的延时
void vTaskDelay( portTickTypexTicksToDelay )
/*xTicksToDelay:延时时间总数,单位是系统时钟节拍周期*/
/*举例*/
voidvTaskFunction( voidvoid * pvParameters )
{
/* 阻塞500ms. */
constportTickType xDelay = 500 / portTICK_RATE_MS;
for( ;; )
{
/* 每隔500ms触发一次LED, 触发后进入阻塞状态 */
vToggleLED();
vTaskDelay( xDelay );
}
}
2. 绝对延时
- 周期性任务可以使用此函数,以确保一个恒定的频率执行
- INCLUDE_vTaskDelayUntil 必须设置成1,此函数才有效
- vTaskDelayUntil()指定一个绝对时间,每当时间到达,则解除任务阻塞
- 调用vTaskSuspendAll()函数挂起RTOS调度器时,不可以使用此函数
/*原型*/
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_txTimeIncrement );
/*参数说明*/
/*pxPreviousWakeTime:指针,指向一个变量,该变量保存任务最后一次解除阻塞的时间。第一次使用前,该变量必须初始化为当前时间。之后这个变量会在vTaskDelayUntil()函数内自动更新*/
/*xTimeIncrement:周期循环时间。当时间等于(*pxPreviousWakeTime + xTimeIncrement)时,任务解除阻塞。如果不改变参数xTimeIncrement的值,调用该函数的任务会按照固定频率执行*/
/*举例*/
//每10次系统节拍执行一次
void vTaskFunction( voidvoid * pvParameters )
{
static portTickType xLastWakeTime;
const portTickType xFrequency = 10;
// 使用当前时间初始化变量xLastWakeTime
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
//等待下一个周期
vTaskDelayUntil( &xLastWakeTime,xFrequency );
// 需要周期性执行代码放在这里
}
}
3. 获取任务优先级
- 获取指定任务的优先级
- INCLUDE_vTaskPriorityGet必须设置成1,此函数才有效
/*原型*/
UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask );
/*参数说明*/
/*xTask:任务句柄。NULL表示获取当前任务的优先级*/
/*举例*/
voidvAFunction( void )
{
xTaskHandlexHandle;
// 创建任务,保存任务句柄
xTaskCreate( vTaskCode, "NAME",STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
// ...
// 使用句柄获取创建的任务的优先级
if( uxTaskPriorityGet( xHandle ) !=tskIDLE_PRIORITY )
{
// 任务可以改变自己的优先级
}
// ...
// 当前任务优先级比创建的任务优先级高?
if( uxTaskPriorityGet( xHandle ) <uxTaskPriorityGet( NULL ) )
{
// 当前优先级较高
}
}
4. 设置任务优先级
- 设置指定任务的优先级。如果设置的优先级高于当前运行的任务,在函数返回前会进行一次上下文切换
- INCLUDE_vTaskPrioritySet 必须设置成1,此函数才有效
/*原型*/
void vTaskPrioritySet( TaskHandle_txTask, UBaseType_tuxNewPriority );
/*参数*/
/*xTask:要设置优先级任务的句柄,为NULL表示设置当前运行的任务*/
/*uxNewPriority:要设置的新优先级*/
/*举例*/
voidvAFunction( void )
{
xTaskHandlexHandle;
// 创建任务,保存任务句柄。
xTaskCreate( vTaskCode, "NAME",STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
// ...
// 使用句柄来提高创建任务的优先级
vTaskPrioritySet( xHandle,tskIDLE_PRIORITY + 1 );
// ...
// 使用NULL参数来提高当前任务的优先级,设置成和创建的任务相同。
vTaskPrioritySet( NULL, tskIDLE_PRIORITY +1 );
}
5. 任务挂起
- 挂起指定任务
- 被挂起的任务绝不会得到处理器时间,不管该任务具有什么优先级
- 多次调用vTaskSuspend ()函数将一个任务挂起,也只需调用一次vTaskResume ()函数就能使挂起的任务解除挂起状态
- INCLUDE_vTaskSuspend必须设置成1,此函数才有效
/*原型*/
void vTaskSuspend( TaskHandle_txTaskToSuspend );
/*参数*/
/*xTaskToSuspend:要挂起的任务句柄。为NULL表示挂起当前任务*/
/*举例*/
void vAFunction( void )
{
xTaskHandlexHandle;
// 创建任务,保存任务句柄.
xTaskCreate( vTaskCode, "NAME",STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
// ...
// 使用句柄挂起创建的任务.
vTaskSuspend( xHandle );
// ...
// 任务不再运行,除非其它任务调用了vTaskResume(xHandle )
//...
// 挂起本任务.
vTaskSuspend( NULL );
// 除非另一个任务使用handle调用了vTaskResume,否则永远不会执行到这里
}
6. 恢复挂起任务
- INCLUDE_vTaskSuspend必须置1,此函数才有效
/*原型*/
void vTaskResume( TaskHandle_txTaskToResume );
/*参数*/
/*xTaskToResume:要恢复运行的任务句柄*/
/*举例*/
void vAFunction( void )
{
xTaskHandle xHandle;
// 创建任务,保存任务句柄
xTaskCreate( vTaskCode, "NAME",STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
// ...
```
// 使用句柄挂起创建的任务
vTaskSuspend( xHandle );
// ...
//任务不再运行,除非其它任务调用了vTaskResume(xHandle )
//...
// 恢复挂起的任务.
vTaskResume( xHandle );
// 任务再一次得到处理器时间
// 任务优先级与之前相同
}
7. 恢复挂起的任务(在中断服务函数中使用)
- 用于恢复一个挂起的任务,用在ISR中
- 不可用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(用信号量同步)
- INCLUDE_xTaskResumeFromISR 必须设置成1
/*原型*/
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume );
/*参数*/
/*xTaskToResume:要恢复运行的任务句柄*/
/*举例*/
xTaskHandlexHandle; //注意这是一个全局变量
void vAFunction( void )
{
// 创建任务并保存任务句柄
xTaskCreate( vTaskCode, "NAME",STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
// 剩余代码.
}
void vTaskCode( voidvoid *pvParameters )
{
for( ;; )
{
// ... 在这里执行一些其它功能
// 挂起自己
vTaskSuspend( NULL );
//直到ISR恢复它之前,任务会一直挂起
}
}
void vAnExampleISR( void )
{
portBASE_TYPExYieldRequired;
// 恢复被挂起的任务
xYieldRequired = xTaskResumeFromISR(xHandle );
if( xYieldRequired == pdTRUE )
{
// 我们应该进行一次上下文切换
// 注: 如何做取决于你具体使用,可查看说明文档和例程
portYIELD_FROM_ISR();
}
}
8.4 任务应用函数
1. 获取任务系统状态
UBaseType_t uxTaskGetSystemState(
TaskStatus_t * constpxTaskStatusArray,
const UBaseType_tuxArraySize,
unsigned longlong * constpulTotalRunTime );
2. 获取当前任务句柄
TaskHandle_t xTaskGetCurrentTaskHandle(void );
3. 获取空闲任务句柄
TaskHandle_t xTaskGetIdleTaskHandle(void );
4. 获取任务堆栈最大使用深度
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
5. 获取任务状态
eTaskState eTaskGetState( TaskHandle_txTask );
6. 获取任务描述内容
char * pcTaskGetTaskName( TaskHandle_txTaskToQuery );
7. 获取系统节拍次数
volatile TickType_t xTaskGetTickCount(void );
8. 获取调度器状态
BaseType_t xTaskGetSchedulerState( void);
9. 获取任务总数
UBaseType_t uxTaskGetNumberOfTasks(void );
10. 获取所有任务详情
void vTaskList( char *pcWriteBuffer );
11. 获取任务运行时间
void vTaskGetRunTimeStats( char*pcWriteBuffer );
12. 设置任务标签值
voidvTaskSetApplicationTaskTag( TaskHandle_t xTask, TaskHookFunction_tpxTagValue);
13. 获取任务标签值
TaskHookFunction_txTaskGetApplicationTaskTag( TaskHandle_t xTask );
14. 执行任务的应用钩子函数
BaseType_txTaskCallApplicationTaskHook(TaskHandle_txTask,
void*pvParameter);
15. 设置线程本地存储指针
void vTaskSetThreadLocalStoragePointer(TaskHandle_t xTaskToSet,
BaseType_t xIndex,
void*pvValue )
16. 读取线程本地存储指针
void*pvTaskGetThreadLocalStoragePointer(TaskHandle_txTaskToQuery,
BaseType_txIndex );
17. 设置超时状态
void vTaskSetTimeOutState( TimeOut_t *const pxTimeOut );
18. 超时检测
BaseType_t xTaskCheckForTimeOut(TimeOut_t * const pxTimeOut,
TickType_t* const pxTicksToWait );
8.5 内核控制
内核控制的一些功能需要移植层提供,为了方便移植,这些API函数用宏来实现
强制上下文切换
- taskYIELD
在中断服务程序中的等价版本为portYIELD_FROM_ISR,这也是个宏,其实现取决于移植层
进入临界区宏
- taskENTER_CRITICAL
在临界区中不会发生上下文切换,对于Cortex-M3硬件,先禁止所有RTOS可屏蔽中断,这可以通过向basepri 寄存器写入configMAX_SYSCALL_INTERRUPT_PRIORITY来实现
退出临界区宏
- taskEXIT_CRITICAL
对于Cortex-M3硬件,先将临界区嵌套计数器减1,如果临界区计数器为零,则使能所有RTOS可屏蔽中断,这可以通过向basepri 寄存器写入0来实现
禁止可屏蔽中断宏
- taskDISABLE_INTERRUPTS
在调用宏taskENTER_CRITICAL进入临界区时,也会间接调用该宏禁止所有RTOS可屏蔽中断
使能可屏蔽中断宏
- taskENABLE_INTERRUPTS
在调用宏taskEXIT_CRITICAL退出临界区时,也会间接调用该宏使能所有RTOS可屏蔽中断
启动调度器
void vTaskStartScheduler( void );
如果vTaskStartScheduler()成功执行,则该函数不会返回,直到有任务调用了vTaskEndScheduler()。如果因为RAM不足而无法创建空闲任务,该函数也可能执行失败,并会立刻返回调用处
停止调度器
仅用于x86硬件架构中
void vTaskEndScheduler( void );
挂起调度器
- 挂起调度器,但不禁止中断
- 正在执行的任务会一直继续执行,内核不再调度
- 内核调度器挂起期间,那些可以引起上下文切换的API函数(如vTaskDelayUntil()、xQueueSend()等)决不可使用
void vTaskSuspendAll( void );
恢复被挂起的调度器
- 不会恢复那些被vTaskSuspend()函数挂起的任务
BaseType_t xTaskResumeAll( void );
调整系统节拍
/* 首先定义宏portSUPPRESS_TICKS_AND_SLEEP()。宏参数指定要进入低功耗(睡眠)的时间,单位是系统节拍周期。*/
#defineportSUPPRESS_TICKS_AND_SLEEP( xIdleTime ) vApplicationSleep( xIdleTime )
/* 定义被宏portSUPPRESS_TICKS_AND_SLEEP()调用的函数 */
void vApplicationSleep(TickType_t xExpectedIdleTime )
{
unsigned long ulLowPowerTimeBeforeSleep,ulLowPowerTimeAfterSleep;
/* 从时钟源获取当前时间,当微控制器进入低功耗的时候,这个时钟源必须在运行 */
ulLowPowerTimeBeforeSleep =ulGetExternalTime();
/*停止系统节拍时钟中断。*/
prvStopTickInterruptTimer();
/* 配置一个中断,当指定的睡眠时间达到后,将处理器从低功耗中唤醒。这个中断源必须在微控制器进入低功耗时也可以工作。*/
vSetWakeTimeInterrupt( xExpectedIdleTime );
/*进入低功耗 */
prvSleep();
/* 确定微控制器进入低功耗模式持续的真正时间。因为其它中断也可能使得微处理器退出低功耗模式。注意:在调用宏portSUPPRESS_TICKS_AND_SLEEP()之前,调度器应该被挂起,portSUPPRESS_TICKS_AND_SLEEP()返回后,再将调度器恢复。因此,这个函数未完成前,不会执行其它任务。*/
ulLowPowerTimeAfterSleep =ulGetExternalTime();
/*调整内核系统节拍计数器。*/
vTaskStepTick( ulLowPowerTimeAfterSleep –ulLowPowerTimeBeforeSleep );
/*重新启动系统节拍时钟中断。*/
prvStartTickInterruptTimer();
}
8.6 任务通知
- 每个RTOS任务都有一个32位的通知值,任务创建时,这个值被初始化为0
- RTOS任务通知相当于直接向任务发送一个事件,接收到通知的任务可以解除阻塞状态,前提是这个阻塞事件是因等待通知而引起的
- 发送通知的同时,也可以可选的改变接收任务的通知值
- 相比于使用信号量解除任务阻塞,使用任务通知可以快45%、使用更少的RAM
API函数xTaskNotify()和xTaskNotifyGive()发送通知
接收RTOS任务调用API函数xTaskNotifyWait()或ulTaskNotifyTake()之前,这个通知都被保持着
限制
- 只能有一个任务接收通知事件
- 接收通知的任务可以因为等待通知而进入阻塞状态,但是发送通知的任务即便不能立即完成通知发送也不能进入阻塞状态
发送通知
方法1
BaseType_t xTaskNotify( TaskHandle_txTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction);
方法2
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify );
获取通知
uint32_t ulTaskNotifyTake( BaseType_txClearCountOnExit,
TickType_txTicksToWait );
等待通知
BaseType_t xTaskNotifyWait( uint32_tulBitsToClearOnEntry,
uint32_tulBitsToClearOnExit,
uint32_t*pulNotificationValue,
TickType_txTicksToWait );
任务通知并查询
BaseType_t xTaskNotifyAndQuery(TaskHandle_t xTaskToNotify,
uint32_tulValue,
eNotifyActioneAction,
uint32_t*pulPreviousNotifyValue );
9. 队列
9.1 队列
任务间主要的通讯方式,任务和任务,中断和任务之间传送信息。
- 图中能保存5个项目
- 任务A使用API函数xQueueSendToBack()向队列发送数据
- 任务B使用API函数xQueueReceive()将数据从队列取出
- FIFO,LIFO都可以
FreeRTOS队列特性
- C变量(整形、简单结构体等等)中的简单信息可以直接传送到队列
- 队列是通过拷贝传递数据的,但这并不妨碍队列通过引用来传递数据
- 队列内存区域分配由内核完成
- 变长消息可以通过定义保存一个结构体变量的队列实现,结构体一个成员指向要入队的缓存,另一个成员保存缓存数据的大小
- 单个队列可以接收不同类型信息,并且信息可以来自不同的位置
- 在中断函数中使用独立的API
- API函数很简单
队列阻塞
- 每当任务企图从一个空的队列读取数据时,任务会进入阻塞状态,直到队列中出现有效数据或者阻塞时间到期
- 每当任务企图向一个满的队列写数据时,任务会进入阻塞状态,直到队列中出现有效空间或者阻塞时间到期
- 如果多个任务阻塞在一个队列上,那么最高优先级别的任务会第一个解除阻塞
- 中断程序中绝不可以使用不带“FromISR”结尾的API函数
队列用法总结
- 定义一个队列句柄变量,用于保存创建的队列:xQueueHandle xQueue1
- 使用API函数xQueueCreate()创建一个队列
- FIFO API函数xQueueSend()或xQueueSendToBack()向队列投递队列项
- LIFO API函数xQueueSendToFront()向队列投递队列项
- API函数xQueueReceive()从队列读取队列项
9.2 队列API
1.获取队列入队信息数目
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
2.获取队列的空闲数目
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
3.删除队列
void vQueueDelete( QueueHandle_t xQueue );
4.复位队列
BaseType_t xQueueReset( QueueHandle_t xQueue );
5.创建队列
QueueHandle_t xQueueCreate (UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
6.向队列投递队列项
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
7.向队列投递队列项(带中断保护)
BaseType_t xQueueSendFromISR (QueueHandle_t xQueue,
const voidvoid *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
8.向队列尾部投递队列项
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const voidvoid * pvItemToQueue, TickType_t xTicksToWait );
9. 向队列尾部投递队列项(带中断保护)
BaseType_t xQueueSendToBackFromISR (QueueHandle_t xQueue,
const voidvoid *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
10. 向队列首部投递队列项
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,
const voidvoid * pvItemToQueue,TickType_t xTicksToWait);
11. 向队列首部投递队列项(带中断保护)
BaseType_t xQueueSendToFrontFromISR (QueueHandle_t xQueue,
const voidvoid *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);
12.读取并移除队列项
BaseType_t xQueueReceive(QueueHandle_t xQueue,
voidvoid *pvBuffer,TickType_t xTicksToWait);
13读取并移除队列项(带中断保护)
BaseType_t xQueueReceiveFromISR (QueueHandle_t xQueue,
voidvoid *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken);
14.读取但不移除队列项
BaseType_t xQueuePeek(QueueHandle_t xQueue,
voidvoid *pvBuffer, TickType_t xTicksToWait);
15.读取但不移除队列项(带中断保护)
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer,);
16.队列注册
void vQueueAddToRegistry(QueueHandle_t xQueue, char *pcQueueName,);
17.解除注册
void vQueueUnregisterQueue(QueueHandle_t xQueue);
18.查询队列是否为空(仅用于中断服务程序)
BaseType_t xQueueIsQueueEmptyFromISR( const QueueHandle_t xQueue );
19.查询队列是否满(仅用于中断服务程序)
BaseType_t xQueueIsQueueFullFromISR( const QueueHandle_t xQueue );
20.向队列尾部覆盖式投递队列项
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void * pvItemToQueue);
21向队列尾部覆盖式投递队列项(带中断保护)
BaseType_t xQueueOverwriteFromISR (QueueHandle_t xQueue, const voidvoid * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
10. 信号量
10.1 信号量
FreeRTOS的信号量有4种,后两种虽然叫互斥,也还是信号量,是特殊的信号量
- 二进制信号量
- 计数信号量
- 互斥信号量
- 递归互斥信号量
信号量和互斥信号量的区别
- 信号量用于同步,任务间或者任务和中断;互斥量用于互锁,保护同时只能有一个任务使用的资源
- 信号量一般是一个任务/中断给出信号,另一个任务获取信号;互斥量必须同一个任务发出和获取
- 互斥量有优先级继承,信号量没有
- 互斥量不能用于中断中
- 信号量,互斥量创建API不同,发出和获取API相同
1. 二进制信号量
这里只分析用于同步的二进制信号量(也可以用于互斥,但是没有优先级继承)。
二进制信号量可以看成是长度为1的队列,非空即满,所以叫二进制
- 程序开始运行时,信号量无效,任务阻塞在这个信号下
- 某个时刻,中断发生,使用API xSemaphoreGiveFromISR()给出了一个信号,信号量变得有效
- 退出中断,上下文切换,任务阻塞解除,使用API函数xSemaphoreTake()取走信号量并执行任务
- 之后信号量变得无效,任务再次进入阻塞
2. 计数器信号量
二进制信号量是长度为1的队列,计数器信号量可以认为是长度大于1的队列。信号量使用者不关心存储在队列中的数据,只关心队列是否为空
通常用于以下两种事件
- 计数事件
- 事件发生,事件处理程序给出一个信号量(信号量计数加1),处理事件时,处理程序取走信号量(减1)
- 计数信号量创建时为0
- 资源管理
- 计数值表示有效资源数目
- 任务必须首先取得信号量才能获取资源控制权
- 计数值为0表示没有资源
- 任务完成后需要返还信号量(信号量计数值增加)
- 计数信号量创建时为资源数目
3. 互斥量
资源保护的令牌
- 任务A想获取资源,首先使用API xSemaphoreTake()获取信号量
- 成功取得,A得到了互斥量,可以访问资源
- 任务B开始执行,也想访问资源,首先获取互斥量
- 互斥量此时无效,B进入阻塞
- A执行完成,通过API xSemaphoreGive()释放互斥量
- B解除阻塞,使用API函数xSemaphoreTake()获取并得到互斥量,开始运行
互斥量优先级继承用于优化优先级翻转问题,优先级翻转问题的意思是,系统资源只有一个,低优先级任务先占用了,高优先级任务只能等着
- 低优先级任务取得互斥量
- 高优先级任务想要取得同样的互斥量,得不到,阻塞
- 低优先级任务临时调高优先级到高优先级同级(优先级继承)
- 低优先级任务完成后,高优先级任务接管,优先级继承防止其他低优先级的任务在高优先级阻塞的时候接管CPU
4. 递归互斥量
递归互斥量可以由同一个任务递归的获取(同时也必须递归的返还),完全返还之前,别的任务不能获取
递归获取和返还通过API
- xSemaphoreTakeRecursive()
- xSemaphoreGiveRecursive()
互斥量不能用于中断服务程序
- 互斥量具有优先级继承机制,只有在任务中获取或给出互斥才有意义
- 中断不能因为等待互斥量而阻塞
10.2 信号量API
1创建二进制信号量
SemaphoreHandle_t xSemaphoreCreateBinary( void );
2创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting ( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount )
3创建互斥量
SemaphoreHandle_t xSemaphoreCreateMutex( void )
4创建递归互斥量
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )
5删除信号量
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
6获取信号量
xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
7获取信号量(带中断保护)
xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
signedBaseType_t *pxHigherPriorityTaskWoken);
8获取递归互斥量
xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait );
9释放信号量
xSemaphoreGive(SemaphoreHandle_t xSemaphore );
10释放信号量(带中断保护)
xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
signed BaseType_t *pxHigherPriorityTaskWoken );
11释放递归互斥量
xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex );
12获取互斥量持有任务的句柄
TaskHandle_t xSemaphoreGetMutexHolder( SemaphoreHandle_t xMutex );