目录
第十一章FreeRTOS 其他任务API 函数
前面几章我们花费了大量的精力来学习FreeRTOS 的任务管理,但是真正涉及到的与任务
相关的API 函数只有那么几个。但是FreeRTOS 还有很多与任务相关的API 函数,不过这些API
函数大多都是辅助函数了,本章我们就来看一下这些与任务相关的其他的API 函数。本章分为
如下几部分:
11.1 任务相关API 函数预览
11.2 任务相关API 函数详解
11.3 任务壮态查询API 函数实验
11.4 任务运行时间壮态统计实验
11.1 任务相关API 函数预览
先通过一个表11.1.1 来看一下这些与任务相关的其他API 函数都有哪些:
这些API 函数在FreeRTOS 官网上都有,如图11.1.2 所示:
11.2 任务相关API 函数详解
1、函数uxTaskPriorityGet()
此函数用来获取指定任务的优先级,要使用此函数的话宏INCLUDE_uxTaskPriorityGet 应
该定义为1,函数原型如下:
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )
参数:
xTask:要查找的任务的任务句柄。
返回值:获取到的对应的任务的优先级。
2、函数vTaskPrioritySet()
此函数用于改变某一个任务的任务优先级,要使用此函数的话宏
INCLUDE_vTaskPrioritySet 应该定义为1,函数原型如下:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority )
参数:
xTask:要查找的任务的任务句柄。
uxNewPriority: 任务要使用的新的优先级,可以是0~ configMAX_PRIORITIES – 1。
返回值:无。
3、uxTaskGetSystemState()
此函数用于获取系统中所有任务的任务壮态,每个任务的壮态信息保存在一个TaskStatus_t
类型的结构体里面,这个结构体里面包含了任务的任务句柄、任务名字、堆栈、优先级等信息,
要使用此函数的话宏configUSE_TRACE_FACILITY 应该定义为1,函数原型如下:
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
uint32_t * const pulTotalRunTime )
参数:
pxTaskStatusArray:指向TaskStatus_t 结构体类型的数组首地址,每个任务至少需要一个
TaskStatus_t 结构体,任务的数量可以使用函数
uxTaskGetNumberOfTasks()。结构体TaskStatus_t 在文件task.h 中有如下
定义:
typedef struct xTASK_STATUS
{
TaskHandle_t xHandle; //任务句柄
const char * pcTaskName; //任务名字
UBaseType_t xTaskNumber; //任务编号
eTaskState eCurrentState; //当前任务壮态,eTaskState 是一个枚举类型
UBaseType_t uxCurrentPriority; //任务当前的优先级
UBaseType_t uxBasePriority; //任务基础优先级
uint32_t ulRunTimeCounter;//任务运行的总时间
StackType_t * pxStackBase; //堆栈基地址
uint16_t usStackHighWaterMark; //从任务创建以来任务堆栈剩余的最小大小,此
//值如果太小的话说明堆栈有溢出的风险。
} TaskStatus_t;
uxArraySize: 保存任务壮态数组的数组的大小。
pulTotalRunTime: 如果configGENERATE_RUN_TIME_STATS 为1 的话此参数用来保存系
统总的运行时间。
返回值:统计到的任务壮态的个数,也就是填写到数组pxTaskStatusArray 中的个
数,此值应该等于函数uxTaskGetNumberOfTasks()的返回值。如果参数
uxArraySize 太小的话返回值可能为0。
4、函数vTaskGetInfo()
此函数也是用来获取任务壮态的,但是是获取指定的单个任务的壮态的,任务的壮态信息
填充到参数pxTaskStatus 中,这个参数也是TaskStatus_t 类型的。要使用此函数的话宏
configUSE_TRACE_FACILITY 要定义为1,函数原型如下:
void vTaskGetInfo( TaskHandle_t xTask,
TaskStatus_t * pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState )
参数:
xTask:要查找的任务的任务句柄。
pxTaskStatus: 指向类型为TaskStatus_t 的结构体变量。
xGetFreeStackSpace: 在结构体TaskStatus_t 中有个字段usStackHighWaterMark 来保存自任务
运行以来任务堆栈剩余的历史最小大小,这个值越小说明越接近堆栈溢
出,但是计算这个值需要花费一点时间,所以我们可以通过将
xGetFreeStackSpace 设置为pdFALSE 来跳过这个步骤,当设置为pdTRUE
的时候就会检查堆栈的历史剩余最小值。
eState: 结构体TaskStatus_t 中有个字段eCurrentState 用来保存任务运行壮态,
这个字段是eTaskState 类型的,这是个枚举类型,在task.h 中有如下定
义:
typedef enum
{
eRunning = 0, //运行壮态
eReady, //就绪态
eBlocked, //阻塞态
eSuspended, //挂起态
eDeleted, //任务被删除
eInvalid //无效
} eTaskState;
获取任务运行壮态会耗费不少时间,所以为了加快函数vTaskGetInfo()的执行
速度结构体TaskStatus_t 中的字段eCurrentState 就可以由用户直接赋值,
参数eState 就是要赋的值。如果不在乎这点时间,那么可以将eState 设置为
eInvalid,这样任务的壮态信息就由函数vTaskGetInfo()去想办法获取。
返回值:无。
5、函数xTaskGetApplicationTaskTag()
此函数用于获取任务的Tag(标签)值,任务控制块中有个成员变量pxTaskTag 来保存任务的
标签值。标签的功能由用户自行决定,此函数就是用来获取这个标签值的,FreeRTOS 系统内核
是不会使用到这个标签的。要使用此函数的话宏configUSE_APPLICATION_TASK_TAG 必须为
1,函数原型如下:
TaskHookFunction_t xTaskGetApplicationTaskTag( TaskHandle_t xTask )
参数:
xTask:要获取标签值的任务对应的任务句柄,如果为NULL 的话就获取当前正在运
行的任务标签值。
返回值:任务的标签值。
6、函数xTaskGetCurrentTaskHandle()
此函数用于获取当前任务的任务句柄,其实获取到的就是任务控制块,在前面讲解任务创
建函数的时候说过任务句柄就是任务控制。如果要使用此函数的话宏
INCLUDE_xTaskGetCurrentTaskHandle 应该为1,函数原型如下:
TaskHandle_t xTaskGetCurrentTaskHandle( void )
参数:无
返回值:当前任务的任务句柄。
7、函数xTaskGetHandle()
此函数根据任务名字获取任务的任务句柄,在使用函数xTaskCreate()或xTaskCreateStatic()
创建任务的时候都会给任务分配一个任务名,函数xTaskGetHandle()就是使用这个任务名字来
查询其对应的任务句柄的。要使用此函数的话宏INCLUDE_xTaskGetHandle 应该设置为1,此
函数原型如下:
TaskHandle_t xTaskGetHandle( const char * pcNameToQuery )
参数:
pcNameToQuery:任务名,C 语言字符串。
返回值:
NULL:没有任务名pcNameToQuery 所对应的任务。
其他值:任务名pcNameToQuery 所对应的任务句柄
8、函数xTaskGetIdleTaskHandle()
此函数用于返回空闲任务的任务句柄,要使用此函数的话宏
INCLUDE_xTaskGetIdleTaskHandle 必须为1,函数原型如下:
TaskHandle_t xTaskGetIdleTaskHandle( void )
参数:无
返回值:空闲任务的任务句柄。
9、函数uxTaskGetStackHighWaterMark()
每个任务都有自己的堆栈,堆栈的总大小在创建任务的时候就确定了,此函数用于检查任
务从创建好到现在的历史剩余最小值,这个值越小说明任务堆栈溢出的可能性就越大!
FreeRTOS 把这个历史剩余最小值叫做“高水位线”。此函数相对来说会多耗费一点时间,所以
在代码调试阶段可以使用,产品发布的时候最好不要使用。要使用此函数的话宏
INCLUDE_uxTaskGetStackHighWaterMark 必须为1,此函数原型如下:
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )
参数:
xTask:要查询的任务的任务句柄,当这个参数为NULL 的话说明查询自身任务(即调用
函数uxTaskGetStackHighWaterMark()的任务)的“高水位线”。
返回值:任务堆栈的“高水位线”值,也就是堆栈的历史剩余最小值。
10、函数eTaskGetState()
此函数用于查询某个任务的运行壮态,比如:运行态、阻塞态、挂起态、就绪态等,返回
值是个枚举类型。要使用此函数的话宏INCLUDE_eTaskGetState 必须为1,函数原型如下:
eTaskState eTaskGetState( TaskHandle_t xTask )
参数:
xTask:要查询的任务的任务句柄。
返回值:返回值为eTaskState 类型,这是个枚举类型,在文件task.h 中有定义,前面讲解
函数vTaskGetInfo()的时候已经讲过了。
11、函数pcTaskGetName()
根据某个任务的任务句柄来查询这个任务对应的任务名,函数原型如下:
char *pcTaskGetName( TaskHandle_t xTaskToQuery )
参数:
xTaskToQuery:要查询的任务的任务句柄,此参数为NULL 的话表示查询自身任务(调
用函数pcTaskGetName())的任务名字
返回值:返回任务所对应的任务名。
12、函数xTaskGetTickCount()
此函数用于查询任务调度器从启动到现在时间计数器xTickCount 的值。xTickCount 是系统
的时钟节拍值,并不是真实的时间值。每个滴答定时器中断xTickCount 就会加1,一秒钟滴答
定时器中断多少次取决于宏configTICK_RATE_HZ。理论上xTickCount 存在溢出的问题,但是
这个溢出对于FreeRTOS 的内核没有影响,但是如果用户的应用程序有使用到的话就要考虑溢
出了。什么时候溢出取决于宏configUSE_16_BIT_TICKS,当此宏为1 的时候xTixkCount 就是
个16 位的变量,当为0 的时候就是个32 位的变量。函数原型如下:
TickType_t xTaskGetTickCount( void )
参数:无。
返回值:时间计数器xTickCount 的值。
13、函数xTaskGetTickCountFromISR()
此函数是xTaskGetTickCount()的中断级版本,用于在中断服务函数中获取时间计数器
xTickCount 的值,函数原型如下:
TickType_t xTaskGetTickCountFromISR( void )
参数:无。
返回值:时间计数器xTickCount 的值。
14、函数xTaskGetSchedulerState()
此函数用于获取FreeRTOS 的任务调度器运行情况:运行?关闭?还是挂起!要使用此函
数的话宏INCLUDE_xTaskGetSchedulerState 必须为1,此函数原型如下:
BaseType_t xTaskGetSchedulerState( void )
参数:无。
返回值:
taskSCHEDULER_NOT_STARTED:调度器未启动,调度器的启动是通过函数
vTaskStartScheduler() 来完成,所以在函数
vTaskStartScheduler() 未调用之前调用函数
xTaskGetSchedulerState()的话就会返回此值。
taskSCHEDULER_RUNNING:调度器正在运行。
taskSCHEDULER_SUSPENDED:调度器挂起。
15、函数uxTaskGetNumberOfTasks()
此函数用于查询系统当前存在的任务数量,函数原型如下:
UBaseType_t uxTaskGetNumberOfTasks( void )
参数:无。
返回值:当前系统中存在的任务数量,此值=挂起态的任务+阻塞态的任务+就绪态的任务
+空闲任务+运行态的任务。
16、函数vTaskList()
此函数会创建一个表格来描述每个任务的详细信息,如图11.2.1 所示
表中的信息如下:
Name:创建任务的时候给任务分配的名字。
State:任务的壮态信息,B 是阻塞态,R 是就绪态,S 是挂起态,D 是删除态。
Priority:任务优先级。
Stack:任务堆栈的“高水位线”,就是堆栈历史最小剩余大小。
Num:任务编号,这个编号是唯一的,当多个任务使用同一个任务名的时候可以通过此
编号来做区分。
函数原型如下:
void vTaskList( char * pcWriteBuffer )
参数:
pcWriteBuffer:保存任务壮态信息表的存储区。存储区要足够大来保存任务状态信息表。
返回值:无
17、函数vTaskGetRunTimeStats()
FreeRTOS 可以通过相关的配置来统计任务的运行时间信息,任务的运行时间信息提供了
每个任务获取到CPU 使用权总的时间。函数vTaskGetRunTimeStats()会将统计到的信息填充到
一个表里面,表里面提供了每个任务的运行时间和其所占总时间的百分比,如图11.2.2 所示:
函数vTaskGetRunTimeStats() 是一个很实用的函数,要使用此函数的话宏
configGENERATE_RUN_TIME_STATS 和configUSE_STATS_FORMATTING_FUNCTIONS 必须
都为1。如果宏configGENERATE_RUN_TIME_STATS 为1 的话还需要实现一下几个宏定义:
●portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(),此宏用来初始化一个外设来
提供时间统计功能所需的时基,一般是定时器/计数器。这个时基的分辨率一定要比FreeRTOS
的系统时钟高,一般这个时基的时钟精度比系统时钟的高10~20 倍就可以了。
●portGET_RUN_TIME_COUNTER_VALUE()或者
portALT_GET_RUN_TIME_COUNTER_VALUE(Time),这两个宏实现其中一个就行
了,这两个宏用于提供当前的时基的时间值。
函数原型如下:
void vTaskGetRunTimeStats( char *pcWriteBuffer )
参数:
pcWriteBuffer:保存任务时间信息的存储区。存储区要足够大来保存任务时间信息。
返回值:无
18、函数vTaskSetApplicationTaskTag()
此函数是为高级用户准备的,此函数用于设置某个任务的标签值,这个标签值的具体函数
和用法由用户自行决定,FreeRTOS 内核不会使用这个标签值,如果要使用此函数的话宏
configUSE_APPLICATION_TASK_TAG 必须为1,函数原型如下:
void vTaskSetApplicationTaskTag( TaskHandle_t xTask,
TaskHookFunction_t pxHookFunction )
参数:
xTask:要设置标签值的任务,此值为NULL 的话表示设置自身任务的标签值。
pxHookFunction:要设置的标签值,这是一个TaskHookFunction_t 类型的函数指针,但是
也可以设置为其他值。
返回值:无
19、函数SetThreadLocalStoragePointer()
此函数用于设置线程本地存储指针的值,每个任务都有它自己的指针数组来作为线程本地
存储,使用这些线程本地存储可以用来在任务控制块中存储一些应用信息,这些信息只属于任
务自己的。线程本地存储指针数组的大小由宏
configNUM_THREAD_LOCAL_STORAGE_POINTERS 来决定的。如果要使用此函数的话宏
configNUM_THREAD_LOCAL_STORAGE_POINTERS 不能为0,宏的具体值是本地存储指针
数组的大小,函数原型如下:
void vTaskSetThreadLocalStoragePointer( TaskHandle_t xTaskToSet,
BaseType_t xIndex,
void * pvValue )
参数:
xTaskToSet:要设置线程本地存储指针的任务的任务句柄,如果是NULL 的话表示设置任
务自身的线程本地存储指针。
xIndex:要设置的线程本地存储指针数组的索引。
pvValue: 要存储的值。
返回值:无
20、函数GetThreadLocalStoragePointer()
此函数用于获取线程本地存储指针的值,如果要使用此函数的话宏
configNUM_THREAD_LOCAL_STORAGE_POINTERS 不能为0,函数原型如下:
void *pvTaskGetThreadLocalStoragePointer( TaskHandle_t xTaskToQuery,
BaseType_t xIndex )
参数:
xTaskToSet:要获取的线程本地存储指针的任务句柄,如果是NULL 的话表示获取任务自
身的线程本地存储指针。
xIndex:要获取的线程本地存储指针数组的索引。
返回值:获取到的线程本地存储指针的值。
11.3 任务壮态查询API 函数实验
11.3.1 实验程序设计
FreeRTOS 与任务相关的API 函数中有很多是与任务壮态或者信息查询有关的,比如函数
uxTaskGetSystemState()、vTaskGetInfo()、eTaskGetState()和vTaskList()。本实验我们就来学习这
些函数的使用方法。
1、实验目的
学习使用FreeRTOS 与任务壮态或者信息查询有关的API 函数,包括uxTaskGetSystemState()、
vTaskGetInfo()、eTaskGetState()和vTaskList()。
2、实验设计
本实验设计三个任务:start_task、led0_task 和query_task ,这三个任务的任务功能如下:
start_task:用来创建其他2 个任务。
led0_task :控制LED0 灯闪烁,提示系统正在运行。
query_task :任务壮态和信息查询任务,在此任务中学习使用与任务的壮态和信息查询有
关的API 函数。
实验需要一个按键KEY_UP,这四个按键的功能如下:
KEY_UP: 控制程序的运行步骤。
3、实验工程
FreeRTOS 实验11-1 FreeRTOS 任务壮态或信息查询。
4、实验程序与分析
●任务设置
实验中任务优先级、堆栈大小和任务句柄等的设置如下:
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define LED0_TASK_PRIO 2 //任务优先级
#define LED0_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Led0Task_Handler; //任务句柄
void led0_task(void *pvParameters); //任务函数
#define QUERY_TASK_PRIO 3 //任务优先级
#define QUERY_STK_SIZE 256 //任务堆栈大小
TaskHandle_t QueryTask_Handler; //任务句柄
void query_task(void *pvParameters); //任务函数
char InfoBuffer[1000]; //保存信息的数组
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
LCD_Init(); //初始化LCD
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 11-1");
LCD_ShowString(30,50,200,16,16,"Task Info Query");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2016/11/25");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
在main 函数中我们主要完成硬件的初始化,在硬件初始化完成以后创建了任务start_task()
并且开启了FreeRTOS 的任务调度。
●任务函数
//led0 任务函数
void led0_task(void *pvParameters)
{
while(1)
{
LED0=~LED0;
vTaskDelay(500); //延时500ms,也就是500 个时钟节拍
}
}
//query 任务函数
void query_task(void *pvParameters)
{
u32 TotalRunTime;
UBaseType_t ArraySize,x;
TaskStatus_t *StatusArray;
//第一步:函数uxTaskGetSystemState()的使用
printf("/********第一步:函数uxTaskGetSystemState()的使用**********/\r\n");
ArraySize=uxTaskGetNumberOfTasks(); //获取系统任务数量(1)
StatusArray=pvPortMalloc(ArraySize*sizeof(TaskStatus_t)); //申请内存(2)
if(StatusArray!=NULL) //内存申请成功
{
ArraySize=uxTaskGetSystemState((TaskStatus_t* )StatusArray, (3)
(UBaseType_t )ArraySize,
(uint32_t* )&TotalRunTime);
printf("TaskName\t\tPriority\t\tTaskNumber\t\t\r\n");
for(x=0;x<ArraySize;x++)
{
//通过串口打印出获取到的系统任务的有关信息,比如任务名称、
//任务优先级和任务编号。
printf("%s\t\t%d\t\t\t%d\t\t\t\r\n", (4)
StatusArray[x].pcTaskName,
(int)StatusArray[x].uxCurrentPriority,
(int)StatusArray[x].xTaskNumber);
}
}
vPortFree(StatusArray); //释放内存(5)
printf("/**************************结束***************************/\r\n");
printf("按下KEY_UP 键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下
//第二步:函数vTaskGetInfo()的使用
TaskHandle_t TaskHandle;
TaskStatus_t TaskStatus;
printf("/************第二步:函数vTaskGetInfo()的使用**************/\r\n");
TaskHandle=xTaskGetHandle("led0_task"); //根据任务名获取任务句柄。(6)
//获取LED0_Task 的任务信息
vTaskGetInfo((TaskHandle_t )TaskHandle, //任务句柄(7)
(TaskStatus_t* )&TaskStatus, //任务信息结构体
(BaseType_t )pdTRUE, //允许统计任务堆栈历史最小剩余大小
(eTaskState )eInvalid); //函数自己获取任务运行壮态
//通过串口打印出指定任务的有关信息。
printf("任务名: %s\r\n",TaskStatus.pcTaskName); (8)
printf("任务编号: %d\r\n",(int)TaskStatus.xTaskNumber);
printf("任务壮态: %d\r\n",TaskStatus.eCurrentState);
printf("任务当前优先级: %d\r\n",(int)TaskStatus.uxCurrentPriority);
printf("任务基优先级: %d\r\n",(int)TaskStatus.uxBasePriority);
printf("任务堆栈基地址: %#x\r\n",(int)TaskStatus.pxStackBase);
printf("任务堆栈历史剩余最小值:%d\r\n",TaskStatus.usStackHighWaterMark);
printf("/**************************结束***************************/\r\n");
printf("按下KEY_UP 键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下
//第三步:函数eTaskGetState()的使用
eTaskState TaskState;
char TaskInfo[10];
printf("/***********第三步:函数eTaskGetState()的使用*************/\r\n");
TaskHandle=xTaskGetHandle("query_task"); //根据任务名获取任务句柄。(9)
TaskState=eTaskGetState(TaskHandle); //获取query_task 任务的任务壮态(10)
memset(TaskInfo,0,10); //数组清零
switch((int)TaskState) (11)
{
case 0:sprintf(TaskInfo,"Running");break;
case 1:sprintf(TaskInfo,"Ready");break;
case 2:sprintf(TaskInfo,"Suspend");break;
case 3:sprintf(TaskInfo,"Delete");break;
case 4:sprintf(TaskInfo,"Invalid");break;
}
printf("任务壮态值:%d,对应的壮态为:%s\r\n",TaskState,TaskInfo); (12)
printf("/**************************结束**************************/\r\n");
printf("按下KEY_UP 键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下
//第四步:函数vTaskList()的使用
printf("/*************第三步:函数vTaskList()的使用*************/\r\n");
vTaskList(InfoBuffer); //获取所有任务的信息(13)
printf("%s\r\n",InfoBuffer); //通过串口打印所有任务的信息(14)
while(1)
{
LED1=~LED1;
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
(1)、使用函数uxTaskGetNumberOfTasks()获取当前系统中的任务数量,因为要根据任务数
量给任务信息数组StatusArray 分配内存。注意,这里StatusArray 是个指向TaskStatus_t 类型的
指针,但是在使用的时候会把他当作一个数组来用。
(2)、调用函数pvPortMalloc()给任务信息数组StatusArray 分配内存,数组是TaskStatus_t 类
型。
(3)、调用函数uxTaskGetSystemState()获取系统中所有任务的信息,并将获取到的信息保存
在StatusArray 中。
(4)、通过串口将获取到的所有任务的部分信息打印出来,这里并没有把所获取到的信息都
输出,只是将任务的任务名、任务优先级和任务编号做了输出。
(5)、任务信息数组StatusArray 使用完毕,释放其内存。
(6)、调用函数xTaskGetHandle()根据任务名来获取任务句柄,这里获取任务名为“led0_task”的任务句柄。我们在创建任务的时候一般都会保存任务的句柄,如果保存了任务句柄的话就可
以直接使用。
(7)、调用函数vTaskGetInfo()获取任务名为“led0_task”的任务信息,任务信息保存在
TaskStatus 中。获取任务信息的时候允许统计任务堆栈历史最小剩余大小,任务的运行壮态也是
由函数vTaskGetInfo()来统计。
(8)、通过串口输出获取到的任务led0_task 的任务信息。
(9)、通过函数xTaskGetHandle()获取任务名为“query_task”的任务句柄。
(10)、调用函数eTaskGetState()获取任务的运行壮态。
(11)、通过函数eTaskGetState()获取到的任务运行壮态是个枚举类型:eTaskState,枚举类型
不同的值表示不同的含义,这里用字符串来描述这些枚举值的含义。
(12)、通过串口输出任务query_task 的运行壮态信息。
(13)、调用函数vTaskList()统计所有任务的信息,统计出来的任务信息存储在缓冲区
InfoBuffer 中,这些任务信息以表格的形式呈现,
(14)、通过串口输出保存在缓冲区InfoBuffer 中的任务信息。
11.3.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,然后按照任务函数query_task()中的
步骤一步步的测试分析。
●第一步
第一步是学习uxTaskGetSystemState()函数的使用,通过此函数获取到系统中当前所有任务
的信息,并且通过串口输出其中的一部分信息,这里我们输出了任务名、任务的优先级和任务
的编号,如图11.3.2.1 所示:
从图11.3.2.1 可以看出空闲任务的任务优先级最低,为0,定时器服务任务优先级最高,为
31。而且任务的优先级和任务的编号是不同的,优先级是用户自行设定的,而编号是根据任务
创建的先后顺序来自动分配的。
●第二步
第二步是通过函数vTaskGetInfo()来获取任务名为“led0_task”的任务信息,并通过串口输
出,如图11.3.2.2 所示:
图11.3.2.2 中可以看出有关任务led0_task 的详细信息,包括任务名、任务编号、任务当前
优先级、任务基优先级,任务堆栈基地址和堆栈历史剩余最小值。
●第三步
第三步通过函数eTaskGetStat()来任务query_task 的运行壮态,串口调试助手显示如图
11.3.2.3 所示:
●第四步
第四步是通过函数vTaskList()获取系统中当前所有任务的详细信息,并且将这些信息按照
表格的形式组织在一起存放在用户提供的缓冲区中,例程中将这些信息放到了缓冲区InfoBuffer
中。最后通过串口输出缓冲区InfoBuffer 中的信息,如图11.3.2.4 所示:
图11.3.2.4 中各列的含义我们在讲解函数vTaskList()的时候已经详细的讲解过了。
11.4 任务运行时间信息统计实验
FreeRTOS 可以通过函数vTaskGetRunTimeStats()来统计每个任务使用CPU 的时间,以及所
使用的时间占总时间的比例。在调试代码的时候我们可以根据这个时间使用值来分析哪个任务
的CPU 占用率高,然后合理的分配或优化任务。本实验我们就来学习如何使用FreeRTOS 的这
个运行时间壮态统计功能。
11.4.1 相关宏的设置
要使用此功能的话宏configGENERATE_RUN_TIME_STATS 必须为1,还需要在定义其他
两个宏:
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS():配置一个高精度定时器/计数器提供时基。
portGET_RUN_TIME_COUNTER_VALUE():读取时基的时间值。
这三个宏在FreeRTOSConfig.h 中定义,如下:
#define configGENERATE_RUN_TIME_STATS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks
其中函数ConfigureTimeForRunTimeStats()和变量FreeRTOSRunTimeTicks 在timer.c 里面定义,如下:
//FreeRTOS 时间统计所用的节拍计数器
volatile unsigned long long FreeRTOSRunTimeTicks;
//初始化TIM3 使其为FreeRTOS 的时间统计提供时基
void ConfigureTimeForRunTimeStats(void)
{
//定时器3 初始化,定时器时钟为72M,分频系数为72-1,所以定时器3 的频率
//为72M/72=1M,自动重装载为50-1,那么定时器周期就是50us
FreeRTOSRunTimeTicks=0;
TIM3_Int_Init(50-1,72-1); //初始化TIM3
}
函数ConfigureTimeForRunTimeStats()其实就是初始化定时器,因为时间统计功能需要用户
提供一个高精度的时钟,这里使用定时器3。前面在讲函数vTaskGetRunTimeStats()的时候说过,
这个时钟的精度要比FreeRTOS 的系统时钟高,大约10~20 倍即可。FreeRTOS 系统时钟我们配
置的是1000HZ,周期1ms,这里我们将定时器3 的中断频率配置为20KHZ,周期50us,刚好
是系统时钟频率的20 倍。
定时器3 初始化函数如下:
//通用定时器3 中断初始化
//这里时钟选择为APB1 的2 倍,而APB1 为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
//定时器TIM3 初始化
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler =psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3 中断,允许更新中断
//中断优先级NVIC 设置
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级1 级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0 级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC 寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
//定时器3 中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
FreeRTOSRunTimeTicks++; //运行时间统计时基计数器加一
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
FreeRTOSRunTimeTicks 是个全局变量,用来为时间统计功能提供时间,在定时器3 的中断
服务函数中进行更新。
11.4.2 实验程序设计
1、实验目的
学习使用FreeRTOS 运行时间壮态统计函数vTaskGetRunTimeStats()的使用。
2、实验设计
本实验设计四个任务:start_task、task1_task 、task2_task 和RunTimeStats_task,这四个任
务的任务功能如下:
start_task:用来创建其他3 个任务。
task1_task :应用任务1,控制LED0 灯闪烁,并且刷新LCD 屏幕上指定区域的颜色。
task2_task :应用任务2,控制LED1 灯闪烁,并且刷新LCD 屏幕上指定区域的颜色。
RunTimeStats_task:获取按键值,当KEY_UP 键按下以后就调用函数vTaskGetRunTimeStats()
获取任务的运行时间信息,并且将其通过串口输出到串口调试助手上。
实验需要一个按键KEY_UP,用来获取系统中任务运行时间信息。
3、实验工程
FreeRTOS 实验11-2 FreeRTOS 任务运行时间统计。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define TASK2_TASK_PRIO 3 //任务优先级
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数
#define RUNTIMESTATS_TASK_PRIO 4 //任务优先级
#define RUNTIMESTATS_STK_SIZE 128 //任务堆栈大小
TaskHandle_t RunTimeStats_Handler; //任务句柄
void RunTimeStats_task(void *pvParameters); //任务函数
char RunTimeInfo[400]; //保存任务运行时间信息
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
LCD_Init(); //初始化LCD
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 11-2");
LCD_ShowString(30,50,200,16,16,"Get Run Time Stats");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2016/11/25");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
在main 函数中我们主要完成硬件的初始化,在硬件初始化完成以后创建了任务start_task()
并且开启了FreeRTOS 的任务调度。
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建TASK1 任务
xTaskCreate((TaskFunction_t )task1_task,
(const char* )"task1_task",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_TASK_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
//创建TASK2 任务
xTaskCreate((TaskFunction_t )task2_task,
(const char* )"task2_task",
(uint16_t )TASK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_TASK_PRIO,
(TaskHandle_t* )&Task2Task_Handler);
//创建RunTimeStats 任务
xTaskCreate((TaskFunction_t )RunTimeStats_task,
(const char* )"RunTimeStats_task",
(uint16_t )RUNTIMESTATS_STK_SIZE,
(void* )NULL,
(UBaseType_t )RUNTIMESTATS_TASK_PRIO,
(TaskHandle_t* )&RunTimeStats_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{
u8 task1_num=0;
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,115,314); //画一个矩形
LCD_DrawLine(5,130,115,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(6,111,110,16,16,"Task1 Run:000");
while(1)
{
task1_num++; //任务执1 行次数加1 注意task1_num1 加到255 的时候会清零!!
LED0=!LED0;
LCD_Fill(6,131,114,313,lcd_discolor[task1_num%14]); //填充区域
LCD_ShowxNum(86,111,task1_num,3,16,0x80); //显示任务执行次数
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
//task2 任务函数
void task2_task(void *pvParameters)
{
u8 task2_num=0;
POINT_COLOR = BLACK;
LCD_DrawRectangle(125,110,234,314); //画一个矩形
LCD_DrawLine(125,130,234,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(126,111,110,16,16,"Task2 Run:000");
while(1)
{
task2_num++; //任务2 执行次数加1 注意task1_num2 加到255 的时候会清零!!
LED1=!LED1;
LCD_ShowxNum(206,111,task2_num,3,16,0x80); //显示任务执行次数
LCD_Fill(126,131,233,313,lcd_discolor[13-task2_num%14]); //填充区域
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
//RunTimeStats 任务
void RunTimeStats_task(void *pvParameters)
{
u8 key=0;
while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)
{
memset(RunTimeInfo,0,400); //信息缓冲区清零
vTaskGetRunTimeStats(RunTimeInfo); //获取任务运行时间信息(1)
printf("任务名\t\t\t 运行时间\t 运行所占百分比\r\n");
printf("%s\r\n",RunTimeInfo); (2)
}
vTaskDelay(10); //延时10ms,也就是1000 个时钟节拍
}
}
(1)、调用函数vTaskGetRunTimeStats()获取任务运行时间信息,此函数会统计任务的运行时
间,并且将统计到的运行时间信息按照表格的形式组织在一起并存放在用户设置的缓冲区里面,
缓冲区的首地址通过参数传递给函数vTaskGetRunTimeStats()。
(2)、通过串口输出统计到的任务运行时间信息。
11.4.3 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,按下KEY_UP 按键输出任务的运行
时间信息,如图11.4.3.1 所示:
图11.4.3.1 任务运行时间信息
要注意,函数vTaskGetRunTimeStats()相对来说会很耗时间,所以不要太过于频繁的调用此
函数,测试阶段可以使用此函数来分析任务的运行情况。还有就是运行时间不是真正的运行时
间,真正的时间值要乘以50us。
第十二章FreeRTOS 时间管理
在使用FreeRTOS 的过程中我们通常会在一个任务函数中使用延时函数对这个任务延时,
当执行延时函数的时候就会进行任务切换,并且此任务就会进入阻塞态,直到延时完成,任务
重新进入就绪态。延时函数属于FreeRTOS 的时间管理,本章我们就来学习一些FreeRTOS 的这
个时间管理过程,看看在调用延时函数以后究竟发生了什么?任务是如何进入阻塞态的,在延
时完成以后任务又是如何从阻塞态恢复到就绪态的,本章分为如下几部分:
12.1 FreeRTOS 延时函数
12.3 FreeRTOS 系统时钟节拍
12.1 FreeRTOS 延时函数
12.1 函数vTaskDelay()
学习过UCOSIII 的朋友应该知道,在UCOSIII 中延时函数OSTimeDly()可以设置为三种模
式:相对模式、周期模式和绝对模式。在FreeRTOS 中延时函数也有相对模式和绝对模式,不
过在FreeRTOS 中不同的模式用的函数不同,其中函数vTaskDelay()是相对模式(相对延时函数),
函数vTaskDelayUntil()是绝对模式(绝对延时函数)。函数vTaskDelay()在文件tasks.c 中有定义,
要使用此函数的话宏INCLUDE_vTaskDelay 必须为1,函数代码如下:
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
//延时时间要大于0。
if( xTicksToDelay > ( TickType_t ) 0U ) (1)
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); (2)
{
traceTASK_DELAY();
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); (3)
}
xAlreadyYielded = xTaskResumeAll(); (4)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xAlreadyYielded == pdFALSE ) (5)
{
portYIELD_WITHIN_API(); (6)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
(1)、延时时间由参数xTicksToDelay 来确定,为要延时的时间节拍数,延时时间肯定要大
于0。否则的话相当于直接调用函数portYIELD()进行任务切换。
(2)、调用函数vTaskSuspendAll()挂起任务调度器。
(3) 、调用函数prvAddCurrentTaskToDelayedList() 将要延时的任务添加到延时列表
pxDelayedTaskList 或者pxOverflowDelayedTaskList() 中。后面会具体分析函数
prvAddCurrentTaskToDelayedList()。
(4)、调用函数xTaskResumeAll()恢复任务调度器。
(5)、如果函数xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度。
(6)、调用函数portYIELD_WITHIN_API()进行一次任务调度。
12.2 函数prvAddCurrentTaskToDelayedList()
函数prvAddCurrentTaskToDelayedList()用于将当前任务添加到等待列表中,函数在文件
tasks.c 中有定义,缩减后的函数如下:
static void prvAddCurrentTaskToDelayedList( TickType_t x TicksToWait,
const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount; (1)
#if( INCLUDE_xTaskAbortDelay == 1 )
{
//如果使能函数xTaskAbortDelay()的话复位任务控制块的ucDelayAborted 字段为
//pdFALSE。
pxCurrentTCB->ucDelayAborted = pdFALSE;
}
#endif
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) (2)
{
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority ); (3)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
#if ( INCLUDE_vTaskSuspend == 1 )
{
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )(4)
{
vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) ); (5)
}
else
{
xTimeToWake = xConstTickCount + xTicksToWait; (6)
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), \ (7)
xTimeToWake );
if( xTimeToWake < xConstTickCount ) (8)
{
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->\ (9)
xStateListItem ) );
}
else
{
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) ); (10)
if( xTimeToWake < xNextTaskUnblockTime ) (11)
{
xNextTaskUnblockTime = xTimeToWake; (12)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
/***************************************************************************/
/****************************其他条件编译语句*******************************/
/***************************************************************************/
}
(1)、读取进入函数prvAddCurrentTaskToDelayedList()的时间点并保存在xConstTickCount 中,
后面计算任务唤醒时间点的时候要用到。xTickCount 是时钟节拍计数器,每个滴答定时器中断
xTickCount 都会加一。
(2)、要将当前正在运行的任务添加到延时列表中,肯定要先将当前任务从就绪列表中移除。
(3)、将当前任务从就绪列表中移除以后还要取消任务在uxTopReadyPriority 中的就绪标记。
也就是将uxTopReadyPriority 中对应的bit 清零。
(4) 、延时时间为最大值portMAX_DELAY ,并且xCanBlockIndefinitely 不为
pdFALSE(xCanBlockIndefinitely 不为pdFALSE 的话表示允许阻塞任务)的话直接将当前任务添
加到挂起列表中,任务就不用添加到延时列表中。
(5)、将当前任务添加到挂起列表xSuspendedTaskList 的末尾。
(6)、计算任务唤醒时间点,也就是(1)中获取到的进入函数prvAddCurrentTaskToDelayedList()
的时间值xConstTickCount 加上延时时间值xTicksToWait。
(7)、将计算到的任务唤醒时间点值xTimeToWake 写入到任务列表中壮态列表项的相应字
段中。
(8)、计算得到的任务唤醒时间点小于xConstTickCount,说明发生了溢出。全局变量
xTickCount 是TickType_t 类型的,这是个32 位的数据类型,因此在用xTickCount 计算任务唤
醒时间点xTimeToWake 的时候的肯定会出现溢出的现象。FreeRTOS 针对此现象专门做了处理,
在FreeROTS 中定义了两个延时列表xDelayedTaskList1 和xDelayedTaskList2,并且也定义了两
个指针pxDelayedTaskList 和pxOverflowDelayedTaskList 来访问这两个列表,在初始化列表函数
prvInitialiseTaskLists() 中指针pxDelayedTaskList 指向了列表xDelayedTaskList1 ,指针
pxOverflowDelayedTaskList 指向了列表xDelayedTaskList2。这样发生溢出的话就将任务添加到
pxOverflowDelayedTaskList 所指向的列表中,如果没有溢出的话就添加到pxDelayedTaskList 所
指向的列表中。
(9)、如果发生了溢出的话就将当前任务添加到pxOverflowDelayedTaskList 所指向的列表中。
(10)、如果没有发生溢出的话就将当前任务添加到pxDelayedTaskList 所指向的列表中。
(11)、xNextTaskUnblockTime 是个全局变量,保存着距离下一个要取消阻塞的任务最小时
间点值。当xTimeToWake 小于xNextTaskUnblockTime 的话说明有个更小的时间点来了。
(12)、更新xNextTaskUnblockTime 为xTimeToWake。
12.3 函数vTaskDelayUntil()
函数vTaskDelayUntil()会阻塞任务,阻塞时间是一个绝对时间,那些需要按照一定的频率
运行的任务可以使用函数vTaskDelayUntil()。此函数再文件tasks.c 中有如下定义:
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); (1)
{
const TickType_t xConstTickCount = xTickCount; (2)
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement; (3)
if( xConstTickCount < *pxPreviousWakeTime ) (4)
{
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake >\ (5)
xConstTickCount ) )
{
xShouldDelay = pdTRUE; (6)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > \ (7)
xConstTickCount ) )
{
xShouldDelay = pdTRUE; (8)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
*pxPreviousWakeTime = xTimeToWake; (9)
if( xShouldDelay != pdFALSE ) (10)
{
traceTASK_DELAY_UNTIL( xTimeToWake );
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );(11)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll(); (12)
if( xAlreadyYielded == pdFALSE )
{
ortYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
参数:
pxPreviousWakeTime:上一次任务延时结束被唤醒的时间点,任务中第一次调用函数
vTaskDelayUntil 的话需要将pxPreviousWakeTime 初始化进入任务的while()循环体的时间点值。
在以后的运行中函数vTaskDelayUntil()会自动更新pxPreviousWakeTime。
xTimeIncrement:任务需要延时的时间节拍数(相对于pxPreviousWakeTime 本次延时的节
拍数)。
(1)、挂起任务调度器。
(2)、记录进入函数vTaskDelayUntil()的时间点值,并保存在xConstTickCount 中。
(3)、根据延时时间xTimeIncrement 来计算任务下一次要唤醒的时间点,并保存在
xTimeToWake 中。可以看出这个延时时间是相对于pxPreviousWakeTime 的,也就是上一次任务
被唤醒的时间点。pxPreviousWakeTime、xTimeToWake、xTimeIncrement 和xConstTickCount 的
关系如图12.3.1 所示。
如12.3.1 中(1)为任务主体,也就是任务真正要做的工作,(2)是任务函数中调用
vTaskDelayUntil()对任务进行延时,(3)为其他任务在运行。任务的延时时间是xTimeIncrement,
这个延时时间是相对于pxPreviousWakeTime 的,可以看出任务总的执行时间一定要小于任务的
延时时间xTimeIncrement!也就是说如果使用vTaskDelayUntil()的话任务相当于任务的执行周
期永远都是xTimeIncrement,而任务一定要在这个时间内执行完成。这样就保证了任务永远按
照一定的频率运行了,这个延时值就是绝对延时时间,因此函数vTaskDelayUntil()也叫做绝对
延时函数。
(4)、根据图12.3.1 可以看出,理论上xConstTickCount 要大于pxPreviousWakeTime 的,但
是也有一种情况会导致xConstTickCount 小于pxPreviousWakeTime,那就是xConstTickCount 溢
出了!
(5)、既然xConstTickCount 都溢出了,那么计算得到的任务唤醒时间点肯定也是要溢出的,
并且xTimeToWake 肯定也是要大于xConstTickCount 的。这种情况如图12.3.2 所示:
(6)、如果满足(5)条件的话就将pdTRUE 赋值给xShouldDelay,标记允许延时。
(7)、还有其他两种情况,一:只有xTimeToWake 溢出,二:都没有溢出。只有xTimeToWake
溢出的话如图12.3.3 所示:
都不溢出的话就如图12.3.1 所示,这两种情况都允许进行延时。
(8)、将pdTRUE 赋值给xShouldDelay,标记允许延时。
(9)、更新pxPreviousWakeTime 的值,更新为xTimeToWake,为本函数的下一次执行做准
备。
(10)、经过前面的判断,允许进行任务延时。
(11)、调用函数prvAddCurrentTaskToDelayedList()进行延时。函数的第一个参数是设置任务
的阻塞时间,前面我们已经计算出了任务下一次唤醒时间点了,那么任务还需要阻塞的时间就
是下一次唤醒时间点xTimeToWake 减去当前的时间xConstTickCount。而在函数vTaskDelay()中
只是简单的将这参数设置为xTicksToDelay。
(12)、调用函数xTaskResumeAll()恢复任务调度器。
函数vTaskDelayUntil()的使用方法如下:
void TestTask( void * pvParameters )
{
TickType_t PreviousWakeTime;
//延时50ms,但是函数vTaskDelayUntil()的参数需要设置的是延时的节拍数,不能直接
//设置延时时间,因此使用函数pdMS_TO_TICKS 将时间转换为节拍数。
const TickType_t TimeIncrement = pdMS_TO_TICKS( 50 );
PreviousWakeTime = xTaskGetTickCount(); //获取当前的系统节拍值
for( ;; )
{
/******************************************************************/
/*************************任务主体*********************************/
/******************************************************************/
//调用函数vTaskDelayUntil 进行延时
vTaskDelayUntil( &PreviousWakeTime, TimeIncrement);
}
}
其实使用函数vTaskDelayUntil()延时的任务也不一定就能周期性的运行,使用函数
vTaskDelayUntil()只能保证你按照一定的周期取消阻塞,进入就绪态。如果有更高优先级或者中断的话你还是得等待其他的高优先级任务或者中断服务函数运行完成才能轮到你。这个绝对延时只是相对于vTaskDelay()这个简单的延时函数而言的。
12.2 FreeRTOS 系统时钟节拍
不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是
FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中xTickCount 就会加一,xTickCount 的
具体操作过程是在函数xTaskIncrementTick()中进行的,此函数在文件tasks.c 中有定义,如下:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
//每个时钟节拍中断(滴答定时器中断)调用一次本函数,增加时钟节拍计数器xTickCount 的
//值,并且检查是否有任务需要取消阻塞。
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) (1)
{
const TickType_t xConstTickCount = xTickCount + 1; (2)
//增加系统节拍计数器xTickCount 的值,当为0,也就是溢出的话就交换延时和溢出列
//表指针值。
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U ) (3)
{
taskSWITCH_DELAYED_LISTS(); (4)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//判断是否有任务延时时间到了,任务都会根据唤醒时间点值按照顺序(由小到大的升
//序排列)添加到延时列表中,这就意味这如果延时列表中第一个列表项对应的任务的
//延时时间都没有到的话后面的任务就不用看了,肯定也没有到。
if( xConstTickCount >= xNextTaskUnblockTime ) (5)
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ) (6)
{
//延时列表为空,设置xNextTaskUnblockTime 为最大值。
xNextTaskUnblockTime = portMAX_DELAY; (7)
break;
}
else
{
//延时列表不为空,获取延时列表的第一个列表项的值,根据判断这个值
//判断任务延时时间是否到了,如果到了的话就将任务移除延时列表。
pxTCB = ( TCB_t * )\ (8)
listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue =\ (9)
listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue ) (10)
{
//任务延时时间还没到,但是xItemValue 保存着下一个即将解除
//阻塞态的任务对应的解除时间点,所以需要用xItemValue 来更新
//变量xNextTaskUnblockTime
xNextTaskUnblockTime = xItemValue; (11)
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//将任务从延时列表中移除
( void ) uxListRemove( &( pxTCB->xStateListItem ) ); (12)
//任务是否还在等待其他事件?如信号量、队列等,如果是的话就将这些
//任务从相应的事件列表中移除。相当于等待事件超时退出!
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) !\ (13)
= NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) ); (14)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//将任务添加到就绪列表中
prvAddTaskToReadyList( pxTCB ); (15)
#if ( configUSE_PREEMPTION == 1 )
{
//使用抢占式内核,判断解除阻塞的任务优先级是否高于当前正在
//运行的任务优先级,如果是的话就需要进行一次任务切换!
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) (16)
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
//如果使能了时间片的话还需要处理同优先级下任务之间的调度
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )(17)
{
if( listCURRENT_LIST_LENGTH( &( \
pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
//使用时钟节拍钩子函数
#if ( configUSE_TICK_HOOK == 1 )
{
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook(); (18)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
}
else //任务调度器挂起(19)
{
++uxPendedTicks; (20)
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE ) (21)
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
return xSwitchRequired; (22)
}
(1)、判断任务调度器是否被挂起。
(2)、将时钟节拍计数器xTickCount 加一,并将结果保存在xConstTickCount 中,下一行程
序会将xConstTickCount 赋值给xTickCount,相当于给xTickCount 加一。
(3)、xConstTickCount 为0,说明发生了溢出!
(4)、如果发生了溢出的话使用函数taskSWITCH_DELAYED_LISTS 将延时列表指针
pxDelayedTaskList 和溢出列表指针pxOverflowDelayedTaskList 所指向的列表进行交换,函数
taskSWITCH_DELAYED_LISTS()本质上是个宏,在文件tasks.c 中有定义,将这两个指针所指向
的列表交换以后还需要更新xNextTaskUnblockTime 的值。
(5)、变量xNextTaskUnblockTime 保存着下一个要解除阻塞的任务的时间点值,如果
xConstTickCount 大于xNextTaskUnblockTime 的话就说明有任务需要解除阻塞了。
(6)、判断延时列表是否为空。
(7)、如果延时列表为空的话就将xNextTaskUnblockTime 设置为portMAX_DELAY。
(8)、延时列表不为空,获取延时列表第一个列表项对应的任务控制块。
(9)、获取(8)中获取到的任务控制块中的壮态列表项值。
(10)、任务控制块中的壮态列表项值保存了任务的唤醒时间点,如果这个唤醒时间点值大于
当前的系统时钟(时钟节拍计数器值),说明任务的延时时间还未到。
(11)、任务延时时间还未到,而且xItemValue 已经保存了下一个要唤醒的任务的唤醒时间
点,所以需要用xItemValue 来更新xNextTaskUnblockTime。
(12)、任务延时时间到了,所以将任务先从延时列表中移除。
(13)、检查任务是否还等待某个事件,比如等待信号量、队列等。如果还在等待的话就任务
从相应的事件列表中移除。因为超时时间到了!
(14)、将任务从相应的事件列表中移除。
(15)、任务延时时间到了,并且任务已经从延时列表或者事件列表中已经移除。所以这里需
要将任务添加到就绪列表中。
(16)、延时时间到的任务优先级高于正在运行的任务优先级,所以需要进行任务切换了,标
记xSwitchRequired 为pdTRUE,表示需要进行任务切换。
(17)、如果使能了时间片调度的话,还要处理跟时间片调度有关的工作,具体过程参考9.6
小节。
(18)、如果使能了时间片钩子函数的话就执行时间片钩子函数vApplicationTickHook(),函
数的具体内容由用户自行编写。
(19)、如果调用函数vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不
不会更新xTickCount 了。取而代之的是用uxPendedTicks 来记录调度器挂起过程中的时钟节拍
数。这样在调用函数xTaskResumeAll()恢复任务调度器的时候就会调用uxPendedTicks 次函数
xTaskIncrementTick(),这样xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞。
函数xTaskResumeAll()中相应的处理代码如下:
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
configASSERT( uxSchedulerSuspended );
taskENTER_CRITICAL();
/************************************************************************/
/****************************省略部分代码********************************/
/************************************************************************/
UBaseType_t uxPendedCounts = uxPendedTicks;
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
//do-while()循环体,循环次数为uxPendedTicks
do
{
if( xTaskIncrementTick() != pdFALSE ) //调用函数xTaskIncrementTick
{
xYieldPending = pdTRUE; //标记需要进行任务调度。
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts; //变量减一
} while( uxPendedCounts > ( UBaseType_t ) 0U );
uxPendedTicks = 0; //循环执行完毕,uxPendedTicks 清零
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/************************************************************************/
/****************************省略部分代码********************************/
/************************************************************************/
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
(20)、uxPendedTicks 是个全局变量,在文件tasks.c 中有定义,任务调度器挂起以后此变量
用来记录时钟节拍数。
(21)、有时候调用其他的API 函数会使用变量xYieldPending 来标记是否需要进行上下文切
换,后面具体遇到具体分析。
(22)、返回xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果
为pdTRUE 的话就需要进行任务切换,pdFALSE 的话就不需要进行任务切换。函数
xPortSysTickHandler()中调用xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定
是否进行任务切换。
第十三章FreeRTOS 队列
在实际的应用中,常常会遇到一个任务或者中断服务需要和另外一个任务进行“沟通交流”,
这个“沟通交流”的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行
消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变量来传递消息
就会涉及到“资源管理”的问题。FreeRTOS 对此提供了一个叫做“队列”的机制来完成任务与
任务、任务与中断之间的消息传递。本章我们就来学习FreeRTOS 队列,本章分为如下几部分:
13.1 队列简介
13.2 队列结构体
13.3 队列创建
13.4 向队列发送消息
13.5 队列上锁和解锁
13.6 从队列读取消息
13.7 队列操作实验
13.1 队列简介
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中
断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之
间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的
长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以
也称为消息队列。FreeRTOS 中的信号量的也是依据队列实现的!所以有必要深入的了解
FreeRTOS 的队列。
1、数据存储
通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永
远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是
也可以使用LIFO 的存储缓冲,也就是后进先出,FreeRTOS 中的队列也提供了LIFO 的存储缓
冲机制。
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在
队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传
递。学过UCOS 的同学应该知道,UCOS 的消息队列采用的是引用传递,传递的是消息指针。
采用引用传递的话消息内容就必须一直保持可见性,也就是消息内容必须有效,那么局部变量
这种可能会随时被删掉的东西就不能用来传递消息,但是采用引用传递会节省时间啊!因为不
用进行数据拷贝。
采用值传递的话虽然会导致数据拷贝,会浪费一点时间,但是一旦将消息发送到队列中原
始的数据缓冲区就可以删除掉或者覆写,这样的话这些缓冲区就可以被重复的使用。FreeRTOS
中使用队列传递消息的话虽然使用的是数据拷贝,但是也可以使用引用来传递消息啊,我直接
往队列中发送指向这个消息的地址指针不就可以了!这样当我要发送的消息数据太大的时候就
可以直接发送消息缓冲区的地址指针,比如在网络应用环境中,网络的数据量往往都很大的,
采用数据拷贝的话就不现实。
1、多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中
提取消息。
2、出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任
务从队列中读取消息无效的时候任务阻塞的时间。出队就是就从队列中读取消息,出队阻塞是
针对从队列中读取消息的任务而言的。比如任务A 用于处理串口接收到的数据,串口接收到数
据以后就会放到队列Q 中,任务A 从队列Q 中读取数据。但是如果此时队列Q 是空的,说明
还没有数据,任务A 这时候来读取的话肯定是获取不到任何东西,那该怎么办呢?任务A 现在
有三种选择,一:二话不说扭头就走,二:要不我在等等吧,等一会看看,说不定一会就有数
据了,三:死等,死也要等到你有数据!选哪一个就是由这个阻塞时间决定的,这个阻塞时间
单位是时钟节拍数。阻塞时间为0 的话就是不阻塞,没有数据的话就马上返回任务继续执行接
下来的代码,对应第一种选择。如果阻塞时间为0~ portMAX_DELAY,当任务没有从队列中获
取到消息的话就进入阻塞态,阻塞时间指定了任务进入阻塞态的时间,当阻塞时间到了以后还
没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码,如果在阻塞时间内接收到
了数据就立即返回,执行任务中下面的代码,这种情况对应第二种选择。当阻塞时间设置为
portMAX_DELAY 的话,任务就会一直进入阻塞态等待,直到接收到数据为止!这个就是第三种选择。
3、入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队
列发送消息的话也可以设置阻塞时间。比如任务B 向消息队列Q 发送消息,但是此时队列Q 是
满的,那肯定是发送失败的。此时任务B 就会遇到和上面任务A 一样的问题,这两种情况的处
理过程是类似的,只不过一个是向队列Q 发送消息,一个是从队列Q 读取消息而已。
4、队列操作过程图示
下面几幅图简单的演示了一下队列的入队和出队过程。
●创建队列
图13.1.1 中任务A 要向任务B 发送消息,这个消息是x 变量的值。首先创建一个队列,并
且指定队列的长度和每条消息的长度。这里我们创建了一个长度为4 的队列,因为要传递的是
x 值,而x 是个int 类型的变量,所以每条消息的长度就是int 类型的长度,在STM32 中就是4
字节,即每条消息是4 个字节的。
●向队列发送第一个消息
图13.1.2 中任务A 的变量x 值为10,将这个值发送到消息队列中。此时队列剩余长度就是
3 了。前面说了向队列中发送消息是采用拷贝的方式,所以一旦消息发送完成变量x 就可以再
次被使用,赋其他的值。
●向队列发送第二个消息
图13.1.3 中任务A 又向队列发送了一个消息,即新的x 的值,这里是20。此时队列剩余长
度为2。
●从队列中读取消息
图13.1.4 中任务B 从队列中读取消息,并将读取到的消息值赋值给y,这样y 就等于10
了。任务B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个
消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变成3。如果
不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是2。
13.2 队列结构体
有一个结构体用于描述队列,叫做Queue_t,这个结构体在文件queue.c 中定义如下:
typedef struct QueueDefinition
{
int8_t *pcHead; //指向队列存储区开始地址。
int8_t *pcTail; //指向队列存储区最后一个字节。
int8_t *pcWriteTo; //指向存储区中下一个空闲区域。
union
{
int8_t *pcReadFrom; //当用作队列的时候指向最后一个出队的队列项首地址
UBaseType_t uxRecursiveCallCount;//当用作递归互斥量的时候用来记录递归互斥量被
//调用的次数。
} u;
List_t xTasksWaitingToSend; //等待发送任务列表,那些因为队列满导致入队失败而进
//入阻塞态的任务就会挂到此列表上。
List_t xTasksWaitingToReceive; //等待接收任务列表,那些因为队列空导致出队失败而进
//入阻塞态的任务就会挂到此列表上。
volatile UBaseType_t uxMessagesWaiting; //队列中当前队列项数量,也就是消息数
UBaseType_t uxLength; //创建队列时指定的队列长度,也就是队列中最大允许的
//队列项(消息)数量
UBaseType_t uxItemSize; //创建队列时指定的每个队列项(消息)最大长度,单位字节
volatile int8_t cRxLock; //当队列上锁以后用来统计从队列中接收到的队列项数
//量,也就是出队的队列项数量,当队列没有上锁的话此字
//段为queueUNLOCKED
volatile int8_t cTxLock; //当队列上锁以后用来统计发送到队列中的队列项数量,
//也就是入队的队列项数量,当队列没有上锁的话此字
//段为queueUNLOCKED
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) &&\
( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; //如果使用静态存储的话此字段设置为pdTURE。
#endif
#if ( configUSE_QUEUE_SETS == 1 ) //队列集相关宏
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关宏
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
老版本的FreeRTOS 中队列可能会使用xQUEUE 这个名字,新版本FreeRTOS 中队列的名
字都使用Queue_t。
13.3 队列创建
13.3.1 函数原型
在使用队列之前必须先创建队列,有两种创建队列的方法,一种是静态的,使用函数
xQueueCreateStatic();另一个是动态的,使用函数xQueueCreate()。这两个函数本质上都是宏,
真正完成队列创建的函数是xQueueGenericCreate()和xQueueGenericCreateStatic(),这两个函数
在文件queue.c 中有定义,这四个函数的原型如下。
1、函数xQueueCreate()
此函数本质上是一个宏,用来动态创建队列,此宏最终调用的是函数xQueueGenericCreate(),
函数原型如下:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize)
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位为字节
返回值:
其他值:队列创捷成功以后返回的队列句柄!
NULL: 队列创建失败。
2、函数xQueueCreateStatic()
此函数也是用于创建队列的,但是使用的静态方法创建队列,队列所需要的内存由用户自
行分配,此函数本质上也是一个宏,此宏最终调用的是函数xQueueGenericCreateStatic(),函数
原型如下:
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t * pucQueueStorageBuffer,
StaticQueue_t * pxQueueBuffer)
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自
行分配。此参数必须指向一个uint8_t 类型的数组。这个存储区要大于等
于(uxQueueLength * uxItemsSize)字节。
pxQueueBuffer: 此参数指向一个StaticQueue_t 类型的变量,用来保存队列结构体。
返回值:
其他值:队列创捷成功以后的队列句柄!
NULL: 队列创建失败。
3、函数xQueueGenericCreate()
函数xQueueGenericCreate()用于动态创建队列,创建队列过程中需要的内存均通过
FreeRTOS 中的动态内存管理函数pvPortMalloc()分配,函数原型如下:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位为字节。
ucQueueType:队列类型,由于FreeRTOS 中的信号量等也是通过队列来实现的,创建信号
量的函数最终也是使用此函数的,因此在创建的时候需要指定此队列的用途,
也就是队列类型,一共有六种类型:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
函数xQueueCreate() 创建队列的时候此参数默认选择的就是
queueQUEUE_TYPE_BASE。
返回值:
其他值:队列创捷成功以后的队列句柄!
NULL: 队列创建失败。
4、函数xQueueGenericCreateStatic()
此函数用于动态创建队列,创建队列过程中需要的内存需要由用户自行分配好,函数原型
如下:
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t * pucQueueStorage,
StaticQueue_t * pxStaticQueue,
const uint8_t ucQueueType )
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自
行分配。此参数必须指向一个uint8_t 类型的数组。这个存储区要大于等
于(uxQueueLength * uxItemsSize)字节。
pxStaticQueue: 此参数指向一个StaticQueue_t 类型的变量,用来保存队列结构体。
ucQueueType:队列类型。
返回值:
其他值:队列创捷成功以后队列句柄!
NULL: 队列创建失败。
13.3.2 队列创建函数详解
最终完成队列创建的函数有两个,一个是静态方法的xQueueGenericCreateStatic(),另外一
个就是动态方法的xQueueGenericCreate() 。我们来详细的分析一下动态创建函数
xQueueGenericCreate(),静态方法大同小异,大家可以自行分析一下。函数xQueueGenericCreate()
在文件queue.c 中有如下定义:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
configASSERT( uxQueueLength > ( UBaseType_t ) 0 );
if( uxItemSize == ( UBaseType_t ) 0 )
{
//队列项大小为0,那么就不需要存储区。
xQueueSizeInBytes = ( size_t ) 0;
}
else
{
//分配足够的存储区,确保随时随地都可以保存所有的项目(消息),
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); (1)
}
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes ); (2)
//内存申请成功
if( pxNewQueue != NULL )
{
pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t ); (3)
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
//队列是使用动态方法创建的,所以队列字段ucStaticallyAllocated 标
//记为pdFALSE。
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, \ (4)
ucQueueType, pxNewQueue );
}
return pxNewQueue;
}
(1)、队列是要存储消息的,所以必须要有消息的存储区,函数的参数uxQueueLength 和
uxItemSize 指定了队列中最大队列项目(消息)数量和每个消息的长度,两者相乘就是消息存储
区的大小。
(2)、调用函数pvPortMalloc()给队列分配内存,注意这里申请的内存大小是队列结构体和队
列中消息存储区的总大小。
(3)、计算出消息存储区的首地址,(2)中申请到的内存是队列结构体和队列中消存储区的总
大小,队列结构体内存在前,紧跟在后面的就是消息存储区内存。
(4)、调用函数prvInitialiseNewQueue()初始化队列。
可以看出函数xQueueGenericCreate()重要的工作就是给队列分配内存,当内存分配成功以
后调用函数prvInitialiseNewQueue()来初始化队列。
13.3.3 队列初始化函数
队列初始化函数prvInitialiseNewQueue()用于队列的初始化,此函数在文件queue.c 中有定
义,函数代码如下:
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度
const UBaseType_t uxItemSize, //队列项目长度
uint8_t * pucQueueStorage, //队列项目存储区
const uint8_t ucQueueType, //队列类型
Queue_t * pxNewQueue ) //队列结构体
{
//防止编译器报错
( void ) ucQueueType;
if( uxItemSize == ( UBaseType_t ) 0 )
{
//队列项(消息)长度为0,说明没有队列存储区,这里将pcHead 指向队列开始地址
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
//设置pcHead 指向队列项存储区首地址
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; (1)
}
//初始化队列结构体相关成员变量
pxNewQueue->uxLength = uxQueueLength; (2)
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); (3)
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关字段初始化
{
pxNewQueue->ucQueueType = ucQueueType;
}
#endif /* configUSE_TRACE_FACILITY */
#if( configUSE_QUEUE_SETS == 1 ) //队列集相关字段初始化
{
pxNewQueue->pxQueueSetContainer = NULL;
}
#endif /* configUSE_QUEUE_SETS */
traceQUEUE_CREATE( pxNewQueue );
}
(1)、队列结构体中的成员变量pcHead 指向队列存储区中首地址。
(2)、初始化队列结构体中的成员变量uxQueueLength 和uxItemSize,这两个成员变量保存
队列的最大队列项目和每个队列项大小。
(3)、调用函数xQueueGenericReset()复位队列。PS:发一句牢骚,绕来绕去的,函数调了一
个又一个的。
13.3.4 队列复位函数
队列初始化函数prvInitialiseNewQueue()中调用了函数xQueueGenericReset()来复位队列,
函数xQueueGenericReset()代码如下:
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
configASSERT( pxQueue );
taskENTER_CRITICAL();
{
//初始化队列相关成员变量
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->\ (1)
uxItemSize );
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
pxQueue->pcWriteTo = pxQueue->pcHead;
pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - \
( UBaseType_t ) 1U ) * pxQueue->uxItemSize );
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;
if( xNewQueue == pdFALSE ) (2)
{
//由于复位队列以后队列依旧是空的,所以对于那些由于出队(从队列中读取消
//息)而阻塞的任务就依旧保持阻塞壮态。但是对于那些由于入队(向队列中发送
//消息)而阻塞的任务就不同了,这些任务要解除阻塞壮态,从队列的相应列表中
//移除。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\
xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
//初始化队列中的列表
vListInitialise( &( pxQueue->xTasksWaitingToSend ) ); (3)
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
taskEXIT_CRITICAL();
return pdPASS;
}
(1)、初始化队列中的相关成员变量。
(2)、根据参数xNewQueue 确定要复位的队列是否是新创建的队列,如果不是的话还需要
做其他的处理
(3)、初始化队列中的列表xTasksWaitingToSend 和xTasksWaitingToReceive。
至此,队列创建成功,比如我们创建一个有4 个队列项,每个队列项长度为32 个字节的队
列TestQueue,创建成功的队列如图13.3.4.1 所示:
13.4 向队列发送消息
13.4.1 函数原型
创建好队列以后就可以向队列发送消息了,FreeRTOS 提供了8 个向队列发送消息的API 函数,如表13.4.1 所示:
1、函数xQueueSend()、xQueueSendToBack()和xQueueSendToFront()
这三个函数都是用于向队列中发送消息的,这三个函数本质都是宏,其中函数xQueueSend()
和xQueueSendToBack()是一样的,都是后向入队,即将新的消息插入到队列的后面。函数
xQueueSendToToFront()是前向入队,即将新消息插入到队列的前面。然而!这三个函数最后都
是调用的同一个函数:xQueueGenericSend()。这三个函数只能用于任务函数中,不能用于中断
服务函数,中断服务函数有专用的函数,它们以“FromISR”结尾,这三个函数的原型如下:
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void* pvItemToQueue,
TickType_t xTicksToWait);
BaseType_t xQueueSendToToFront(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait);
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait:阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大
时间。如果为0 的话当队列满的时候就立即返回;当为portMAX_DELAY 的
话就会一直等待,直到队列有空闲的队列项,也就是死等,但是宏
INCLUDE_vTaskSuspend 必须为1。
返回值:
pdPASS:向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
2、函数xQueueOverwrite()
此函数也是用于向队列发送数据的,当队列满了以后会覆写掉旧的数据,不管这个旧数据
有没有被其他任务或中断取走。这个函数常用于向那些长度为1 的队列发送消息,此函数也是
一个宏,最终调用的也是函数xQueueGenericSend(),函数原型如下:
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,
const void * pvItemToQueue);
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
返回值:
pdPASS:向队列发送消息成功,此函数也只会返回pdPASS!因为此函数执行过程中不
在乎队列满不满,满了的话我就覆写掉旧的数据,总之肯定能成功。
3、函数xQueueGenericSend()
此函数才是真正干活的,上面讲的所有的任务级入队函数最终都是调用的此函数,此函数
也是我们后面重点要讲解的,先来看一下函数原型:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
xTicksToWait:阻塞时间。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK:后向入队
queueSEND_TO_FRONT:前向入队
queueOVERWRITE:覆写入队。
上面讲解的入队API 函数就是通过此参数来决定采用哪种入队方式的。
返回值:
pdTRUE:向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
4、函数xQueueSendFromISR()、
xQueueSendToBackFromISR()、
xQueueSendToFrontFromISR()
这三个函数也是向队列中发送消息的,这三个函数用于中断服务函数中。这三个函数本质
也宏,其中函数xQueueSendFromISR ()和xQueueSendToBackFromISR ()是一样的,都是后向入
队,即将新的消息插入到队列的后面。函数xQueueSendToFrontFromISR ()是前向入队,即将新
消息插入到队列的前面。这三个函数同样调用同一个函数xQueueGenericSendFromISR ()。这三
个函数的原型如下:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一
个变量来保存这个值就行了。当此值为pdTRUE 的时候在退
出中断服务函数之前一定要进行一次任务切换。
返回值:
pdTRUE:向队列中发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
我们注意观察,可以看出这些函数都没有设置阻塞时间值。原因很简单,这些函数都是在
中断服务函数中调用的,并不是在任务中,所以也就没有阻塞这一说了!
5、函数xQueueOverwriteFromISR()
此函数是xQueueOverwrite()的中断级版本,用在中断服务函数中,在队列满的时候自动覆
写掉旧的数据,此函数也是一个宏,实际调用的也是函数xQueueGenericSendFromISR(),此函
数原型如下:
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
此函数的参数和返回值同上面三个函数相同。
6、函数xQueueGenericSendFromISR()
上面说了4 个中断级入队函数最终都是调用的函数xQueueGenericSendFromISR(),这是真
正干活的主啊,也是我们下面会详细讲解的函数,先来看一下这个函数的原型,如下:
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue,
const void* pvItemToQueue,
BaseType_t* pxHigherPriorityTaskWoken,
BaseType_t xCopyPosition);
参数:
xQueue:队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一
个变量来保存这个值就行了。当此值为pdTRUE 的时候在退
出中断服务函数之前一定要进行一次任务切换。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK:后向入队
queueSEND_TO_FRONT:前向入队
queueOVERWRITE:覆写入队。
返回值:
pdTRUE:向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
13.4.2 任务级通用入队函数
不管是后向入队、前向入队还是覆写入队,最终调用的都是通用入队函数
xQueueGenericSend(),这个函数在文件queue.c 文件中由定义,缩减后的函数代码如下:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
for( ;; )
{
taskENTER_CRITICAL(); //进入临界区
{
//查询队列现在是否还有剩余存储空间,如果采用覆写方式入队的话那就不用在
//乎队列是不是满的啦。
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||\ (1)
( xCopyPosition == queueOVERWRITE ) )
{
traceQUEUE_SEND( pxQueue );
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue,\ (2)
xCopyPosition );
/**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/
{
//检查是否有任务由于等待消息而进入阻塞态
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) ==\(3)
pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\ (4)
xTasksWaitingToReceive ) ) != pdFALSE )
{
//解除阻塞态的任务优先级最高,因此要进行一次任务切换
queueYIELD_IF_USING_PREEMPTION(); (5)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else if( xYieldRequired != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return pdPASS; (6)
}
else
{
if( xTicksToWait == ( TickType_t ) 0 ) (7)
{
//队列是满的,并且没有设置阻塞时间的话就直接返回
taskEXIT_CRITICAL();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; (8)
}
else if( xEntryTimeSet == pdFALSE ) (9)
{
//队列是满的并且指定了任务阻塞时间的话就初始化时间结构体
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
//时间结构体已经初始化过了,
mtCOVERAGE_TEST_MARKER();
}
}
}
taskEXIT_CRITICAL(); //退出临界区
vTaskSuspendAll(); (10)
prvLockQueue( pxQueue ); (11)
//更新时间壮态,检查是否有超时产生
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) (12)
{
if( prvIsQueueFull( pxQueue ) != pdFALSE ) (13)
{
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), \ (14)
xTicksToWait );
prvUnlockQueue( pxQueue ); (15)
if( xTaskResumeAll() == pdFALSE ) (16)
{
portYIELD_WITHIN_API();
}
}
else
{
//重试一次
prvUnlockQueue( pxQueue ); (17)
( void ) xTaskResumeAll();
}
}
else
{
//超时产生
prvUnlockQueue( pxQueue ); (18)
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; (19)
}
}
}
(1)、要向队列发送数据,肯定要先检查一下队列是不是满的,如果是满的话肯定不能发送
的。当队列未满或者是覆写入队的话就可以将消息入队了。
(2)、调用函数prvCopyDataToQueue()将消息拷贝到队列中。前面说了入队分为后向入队、
前向入队和覆写入队,他们的具体实现就是在函数prvCopyDataToQueue()中完成的。如果选择
后向入队queueSEND_TO_BACK 的话就将消息拷贝到队列结构体成员pcWriteTo 所指向的队
列项,拷贝成功以后pcWriteTo 增加uxItemSize 个字节,指向下一个队列项目。当选择前向入
队queueSEND_TO_FRONT 或者queueOVERWRITE 的话就将消息拷贝到u.pcReadFrom 所指向
的队列项目,同样的需要调整u.pcReadFrom 的位置。当向队列写入一个消息以后队列中统计当
前消息数量的成员uxMessagesWaiting 就会加一,但是选择覆写入队queueOVERWRITE 的话还
会将uxMessagesWaiting 减一,这样一减一加相当于队列当前消息数量没有变。
(3) 、检查是否有任务由于请求队列消息而阻塞,阻塞的任务会挂在队列的
xTasksWaitingToReceive 列表上。
(4)、有任务由于请求消息而阻塞,因为在(2)中已将向队列中发送了一条消息了,所以调用
函数xTaskRemoveFromEventList()将阻塞的任务从列表xTasksWaitingToReceive 上移除,并且把
这个任务添加到就绪列表中,如果调度器上锁的话这些任务就会挂到列表xPendingReadyList 上。
如果取消阻塞的任务优先级比当前正在运行的任务优先级高还要标记需要进行任务切换。当函
数xTaskRemoveFromEventList()返回值为pdTRUE 的话就需要进行任务切换。
(5)、进行任务切换。
(6)、返回pdPASS,标记入队成功。
(7)、(2)到(6)都是非常理想的效果,即消息队列未满,入队没有任何障碍。但是队列满了以
后呢?首先判断设置的阻塞时间是否为0,如果为0 的话就说明没有阻塞时间。
(8)、由(7)得知阻塞时间为0,那就直接返回errQUEUE_FULL,标记队列已满就可以了。
(9)、如果阻塞时间不为0 并且时间结构体还没有初始化的话就初始化一次超时结构体变量,
调用函数vTaskSetTimeOutState()完成超时结构体变量xTimeOut 的初始化。其实就是记录当前
的系统时钟节拍计数器的值xTickCount 和溢出次数xNumOfOverflows。
(10)、任务调度器上锁,代码执行到这里说明当前的状况是队列已满了,而且设置了不为0
的阻塞时间。那么接下来就要对任务采取相应的措施了,比如将任务加入到队列的
xTasksWaitingToSend 列表中。
(11)、调用函数prvLockQueue()给队列上锁,其实就是将队列中的成员变量cRxLock 和
cTxLock 设置为queueLOCKED_UNMODIFIED。
(12)、调用函数xTaskCheckForTimeOut()更新超时结构体变量xTimeOut,并且检查阻塞时
间是否到了。
(13)、阻塞时间还没到,那就检查队列是否还是满的。
(14) 、经过(12) 和(13) 得出阻塞时间没到,而且队列依旧是满的,那就调用函数
vTaskPlaceOnEventList()将任务添加到队列的xTasksWaitingToSend 列表中和延时列表中,并且
将任务从就绪列表中移除。注意!如果阻塞时间是portMAX_DELAY 并且宏
INCLUDE_vTaskSuspend 为1 的话,函数vTaskPlaceOnEventList() 会将任务添加到列表
xSuspendedTaskList 上。
(15)、操作完成,调用函数prvUnlockQueue()解锁队列。
(16)、调用函数xTaskResumeAll()恢复任务调度器
(17)、阻塞时间还没到,但是队列现在有空闲的队列项,那么就在重试一次。
(18)、相比于第(12)步,阻塞时间到了!那么任务就不用添加到那些列表中了,那就解锁队
列,恢复任务调度器。
(19)、返回errQUEUE_FULL,表示队列满了。
13.4.3 中断级通用入队函数
讲完任务级入队函数再来看一下中断级入队函数xQueueGenericSendFromISR(),其他的中
断级入队函数都是靠此函数来实现的。中断级入队函数和任务级入队函数大同小异,函数代码
如下:
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,
const void * const pvItemToQueue,
BaseType_t * const pxHigherPriorityTaskWoken,
const BaseType_t xCopyPosition )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||\ (1)
( xCopyPosition == queueOVERWRITE ) )
{
const int8_t cTxLock = pxQueue->cTxLock; (2)
traceQUEUE_SEND_FROM_ISR( pxQueue );
( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); (3)
//队列上锁的时候就不能操作事件列表,队列解锁的时候会补上这些操作的。
if( cTxLock == queueUNLOCKED ) (4)
{
/**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == \ (5)
pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\ (6)
xTasksWaitingToReceive ) ) != pdFALSE )
{
//刚刚从事件列表中移除的任务对应的任务优先级更高,所以
标记要进行任务切换
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE; (7)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
//cTxLock 加一,这样就知道在队列上锁期间向队列中发送了数据
pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 ); (8)
}
xReturn = pdPASS; (9)
}
else
{
traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
xReturn = errQUEUE_FULL; (10)
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}
(1)、队列未满或者采用的覆写的入队方式,这是最理想的壮态。
(2)、读取队列的成员变量xTxLock,用于判断队列是否上锁。
(3)、将数据拷贝到队列中。
(4)、队列上锁了,比如任务级入队函数在操作队列中的列表的时候就会对队列上锁。
(5)、判断队列列表xTasksWaitingToReceive 是否为空,如果不为空的话说明有任务在请求
消息的时候被阻塞了。
(6)、将相应的任务从列表xTasksWaitingToReceive 上移除。跟任务级入队函数处理过程一
样。
(7)、如果刚刚从列表xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,
那么标记pxHigherPriorityTaskWoken 为pdTRUE,表示要进行任务切换。如果要进行任务切换
的话就需要在退出此函数以后,退出中断服务函数之前进行一次任务切换。
(8)、如果队列上锁的话那就将队列成员变量cTxLock 加一,表示进行了一次入队操作,在
队列解锁(prvUnlockQueue())的时候会对其做相应的处理。
(9)、返回pdPASS,表示入队完成。
(10)、如果队列满的话就直接返回errQUEUE_FULL,表示队列满。
13.5 队列上锁和解锁
在上面讲解任务级通用入队函数和中断级通用入队函数的时候都提到了队列的上锁和解锁,
队列的上锁和解锁是两个API 函数:prvLockQueue()和prvUnlockQueue()。首先来看一下队列上
锁函数prvLockQueue(),此函数本质上就是一个宏,定义如下:
#define prvLockQueue( pxQueue ) \
taskENTER_CRITICAL(); \
{ \
if( ( pxQueue )->cRxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \
} \
if( ( pxQueue )->cTxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \
} \
} \
taskEXIT_CRITICAL()
prvLockQueue()函数很简单,就是将队列中的成员变量cRxLock 和cTxLock 设置为
queueLOCKED_UNMODIFIED 就行了。
在来看一下队列的解锁函数prvUnlockQueue(),函数如下:
static void prvUnlockQueue( Queue_t * const pxQueue )
{
//上锁计数器(cTxLock 和cRxLock)记录了在队列上锁期间,入队或出队的数量,当队列
//上锁以后队列项是可以加入或者移除队列的,但是相应的列表不会更新。
taskENTER_CRITICAL();
{
//处理cTxLock。
int8_t cTxLock = pxQueue->cTxLock;
while( cTxLock > queueLOCKED_UNMODIFIED ) (1)
{
/**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/
{
//将任务从事件列表中移除
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == \ (2)
pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\ (3)
xTasksWaitingToReceive ) ) != pdFALSE )
{
//从列表中移除的任务优先级比当前任务的优先级高,因此要
//进行任务切换。
vTaskMissedYield(); (4)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
break;
}
}
--cTxLock; (5)
}
pxQueue->cTxLock = queueUNLOCKED; (6)
}
taskEXIT_CRITICAL();
//处理cRxLock。
taskENTER_CRITICAL();
{
int8_t cRxLock = pxQueue->cRxLock;
while( cRxLock > queueLOCKED_UNMODIFIED ) (7)
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) !=\
pdFALSE )
{
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--cRxLock;
}
else
{
break;
}
}
pxQueue->cRxLock = queueUNLOCKED;
}
taskEXIT_CRITICAL();
}
(1)、判断是否有中断向队列发送了消息,在13.2.3 小节讲解中断级通用入队函数的时候说
了,如果当队列上锁的话那么向队列发送消息成功以后会将入队计数器cTxLock 加一。
(2)、判断列表xTasksWaitingToReceive 是否为空,如果不为空的话就要将相应的任务从列
表中移除。
(3)、将任务从列表xTasksWaitingToReceive 中移除。
(4)、如果刚刚从列表xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,
那么就要标记需要进行任务切换。这里调用函数vTaskMissedYield()来完成此任务,函数
vTaskMissedYield()只是简单的将全局变量xYieldPending 设置为pdTRUE。那么真正的任务切换
是在哪里完成的呢?在时钟节拍处理函数xTaskIncrementTick()中,此函数会判断xYieldPending
的值,从而决定是否进行任务切换,具体内容可以参考12.2 小节。
(5)、每处理完一条就将cTxLock 减一,直到处理完所有的。
(6)、当处理完以后标记cTxLock 为queueUNLOCKED,也就说cTxLock 是没有上锁的了。
(7)、处理完cTxLock 以后接下来就要处理xRxLock 了,处理过程和xTxLock 很类似,大
家自行分析一下。
13.6 从队列读取消息
有入队就有出队,出队就是从队列中获取队列项(消息),FreeRTOS 中出队函数如表13.6.1.1所示:
1、函数xQueueReceive()
此函数用于在任务中从队列中读取一条(请求)消息,读取成功以后就会将队列中的这条数
据删除,此函数的本质是一个宏,真正执行的函数是xQueueGenericReceive()。此函数在读取消
息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读
取的数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
xTicksToWait:阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最
大时间。如果为0 的话当队列空的时候就立即返回;当为portMAX_DELAY
的话就会一直等待,直到队列有数据,也就是死等,但是宏
INCLUDE_vTaskSuspend 必须为1。
返回值:
pdTRUE:从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
2、函数xQueuePeek()
此函数用于从队列读取一条(请求)消息,只能用在任务中!此函数在读取成功以后不会将
消息删除,此函数是一个宏,真正执行的函数是xQueueGenericReceive()。此函数在读取消息的
时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的
数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueuePeek(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
xTicksToWait:阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最
大时间。如果为0 的话当队列空的时候就立即返回;当为portMAX_DELAY
的话就会一直等待,直到队列有数据,也就是死等,但是宏
INCLUDE_vTaskSuspend 必须为1。
返回值:
pdTRUE:从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
3、函数xQueueGenericReceive()
不管是函数xQueueReceive() 还是xQueuePeek() ,最终都是调用的函数
xQueueGenericReceive(),此函数是真正干事的,函数原型如下:
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue,
void* pvBuffer,
TickType_t xTicksToWait
BaseType_t xJustPeek)
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
xTicksToWait:阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最
大时间。如果为0 的话当队列空的时候就立即返回;当为portMAX_DELAY
的话就会一直等待,直到队列有数据,也就是死等,但是宏
INCLUDE_vTaskSuspend 必须为1。
xJustPeek:标记当读取成功以后是否删除掉队列项,当为pdTRUE 的时候就不用删除,
也就是说你后面再调用函数xQueueReceive()获取到的队列项是一样的。当为
pdFALSE 的时候就会删除掉这个队列项。
返回值:
pdTRUE:从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
4、函数xQueueReceiveFromISR()
此函数是xQueueReceive()的中断版本,用于在中断服务函数中从队列中读取(请求)一条消
息,读取成功以后就会将队列中的这条数据删除。此函数在读取消息的时候是采用拷贝方式的,
所以需要用户提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时
候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
void* pvBuffer,
BaseType_t * pxTaskWoken);
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
pxTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值是由函数来设置的,
用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值
为pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdTRUE:从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
5、函数xQueuePeekFromISR()
此函数是xQueuePeek()的中断版本,此函数在读取成功以后不会将消息删除,此函数原型
如下:
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,
void * pvBuffer)
参数:
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
返回值:
pdTRUE:从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
出队函数的具体过程和入队函数类似,具体的过程就不在详细的分析了,有兴趣的,大家
自行对照着源码看一下就可以了。
13.7 队列操作实验
13.7.1 实验程序设计
1、实验目的
学习使用FreeRTOS 的队列相关API 函数,学会如何在任务或中断中向队列发送消息或者
从队列中接收消息。
2、实验设计
本实验设计三个任务:start_task、task1_task 、Keyprocess_task 这三个任务的任务功能如下:
start_task:用来创建其他2 个任务。
task1_task :读取按键的键值,然后将键值发送到队列Key_Queue 中,并且检查队列的剩
余容量等信息。
Keyprocess_task :按键处理任务,读取队列Key_Queue 中的消息,根据不同的消息值做相
应的处理。
实验需要三个按键KEY_UP、KEY2 和KEY0,不同的按键对应不同的按键值,任务
task1_task 会将这些值发送到队列Key_Queue 中。
实验中创建了两个队列Key_Queue 和Message_Queue,队列Key_Queue 用于传递按键值,
队列Message_Queue 用于传递串口发送过来的消息。
实验还需要两个中断,一个是串口1 接收中断,一个是定时器2 中断,他们的作用如下:
串口1 接收中断:接收串口发送过来的数据,并将接收到的数据发送到队列Message_Queue 中。
定时器2 中断:定时周期设置为500ms,在定时中断中读取队列Message_Queue 中的消息,并
将其显示在LCD 上。
3、实验工程
FreeRTOS 实验13-1 FreeRTOS 队列操作实验。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define KEYPROCESS_TASK_PRIO 3 //任务优先级
#define KEYPROCESS_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Keyprocess_Handler; //任务句柄
void Keyprocess_task(void *pvParameters); //任务函数
//按键消息队列的数量
#define KEYMSG_Q_NUM 1 //按键消息队列的数量(1)
#define MESSAGE_Q_NUM 4 //发送数据的消息队列的数量(2)
QueueHandle_t Key_Queue; //按键值消息队列句柄
QueueHandle_t Message_Queue; //信息队列句柄
(1)、队列Key_Queue 用来传递按键值的,也就是一个u8 变量,所以队列长度为1 就行了。
并且消息长度为1 个字节。
(2)、队列Message_Queue 用来传递串口接收到的数据,队列长度设置为4,每个消息的长
度为USART_REC_LEN(在usart.h 中有定义)。
●其他应用函数
在main.c 中还有一些其他的函数,如下:
//用于在LCD 上显示接收到的队列的消息
//str: 要显示的字符串(接收到的消息)
void disp_str(u8* str)
{
LCD_Fill(5,230,110,245,WHITE); //先清除显示区域
LCD_ShowString(5,230,100,16,16,str);
}
//加载主界面
void freertos_load_main_ui(void)
{
POINT_COLOR = RED;
LCD_ShowString(10,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(10,30,200,16,16,"FreeRTOS Examp 13-1");
LCD_ShowString(10,50,200,16,16,"Message Queue");
LCD_ShowString(10,70,220,16,16,"KEY_UP:LED1 KEY0:Refresh LCD");
LCD_ShowString(10,90,200,16,16,"KEY1:SendMsg KEY2:BEEP");
POINT_COLOR = BLACK;
LCD_DrawLine(0,107,239,107); //画线
LCD_DrawLine(119,107,119,319); //画线
LCD_DrawRectangle(125,110,234,314); //画矩形
POINT_COLOR = RED;
LCD_ShowString(0,130,120,16,16,"DATA_Msg Size:");
LCD_ShowString(0,170,120,16,16,"DATA_Msg rema:");
LCD_ShowString(0,210,100,16,16,"DATA_Msg:");
POINT_COLOR = BLUE;
}
//查询Message_Queue 队列中的总队列数量和剩余队列数量
void check_msg_queue(void)
{
u8 *p;
u8 msgq_remain_size; //消息队列剩余大小
u8 msgq_total_size; //消息队列总大小
taskENTER_CRITICAL(); //进入临界区
msgq_remain_size=uxQueueSpacesAvailable(Message_Queue);//得到队列剩余大小(1)
msgq_total_size=uxQueueMessagesWaiting(Message_Queue)+\ (2)
uxQueueSpacesAvailable(Message_Queue);//得到队列总大小,总大小=使用+剩余的。
p=mymalloc(SRAMIN,20); //申请内存
sprintf((char*)p,"Total Size:%d",msgq_total_size); //显示DATA_Msg 消息队列总的大小
LCD_ShowString(10,150,100,16,16,p);
sprintf((char*)p,"Remain Size:%d",msgq_remain_size); //显示DATA_Msg 剩余大小
LCD_ShowString(10,190,100,16,16,p);
myfree(SRAMIN,p); //释放内存
taskEXIT_CRITICAL(); //退出临界区
}
定时器9 的中断服务函数会调用函数disp_str()在LCD 上显示从队列Message_Queue 接收
到的消息。函数freertos_load_main_ui() 就是在屏幕上画出实验的初始UI 界面。函数
check_msg_queue()用于查询队列Message_Queue 的相关信息,比如队列总大小,队列当前剩余
大小。
(1)、调用函数uxQueueSpacesAvailable()获取队列Message_Queue 的剩余大小。
(2)、调用函数uxQueueMessagesWaiting()获取队列当前消息数量,也就是队列的使用量,将
其与函数uxQueueSpacesAvailable()获取到的队列剩余大小相加就是队列的总大小。
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化LCD
TIM2_Int_Init(5000,7200-1); //初始化定时器2,周期500ms
my_mem_init(SRAMIN); //初始化内部内存池
freertos_load_main_ui(); //加载主UI
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建消息Key_Queue
Key_Queue=xQueueCreate(KEYMSG_Q_NUM,sizeof(u8)); (1)
//创建消息Message_Queue,队列项长度是串口接收缓冲区长度
Message_Queue=xQueueCreate(MESSAGE_Q_NUM,USART_REC_LEN); (2)
//创建TASK1 任务
xTaskCreate((TaskFunction_t )task1_task,
(const char* )"task1_task",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_TASK_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
//创建TASK2 任务
xTaskCreate((TaskFunction_t )Keyprocess_task,
(const char* )"keyprocess_task",
(uint16_t )KEYPROCESS_STK_SIZE,
(void* )NULL,
(UBaseType_t )KEYPROCESS_TASK_PRIO,
(TaskHandle_t* )&Keyprocess_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{
u8 key,i=0;
BaseType_t err;
while(1)
{
key=KEY_Scan(0); //扫描按键
if((Key_Queue!=0)&&(key)) //消息队列Key_Queue 创建成功,并且按键被按下
{
err=xQueueSend(Key_Queue,&key,10); (3)
if(err==errQUEUE_FULL) //发送按键值
{
printf("队列Key_Queue 已满,数据发送失败!\r\n");
}
}
i++;
if(i%10==0) check_msg_queue();//检Message_Queue 队列的容量(4)
if(i==50)
{
i=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10 个时钟节拍
}
}
//Keyprocess_task 函数
void Keyprocess_task(void *pvParameters)
{
u8 num,key;
while(1)
{
if(Key_Queue!=0)
{
//请求消息Key_Queue
if(xQueueReceive(Key_Queue,&key,portMAX_DELAY)) (5)
{
switch(key) (6)
{
case WKUP_PRES: //KEY_UP 控制LED1
LED1=!LED1;
break;
case KEY2_PRES: //KEY2 控制蜂鸣器
BEEP=!BEEP;
break;
case KEY0_PRES: //KEY0 刷新LCD 背景
num++;
LCD_Fill(126,111,233,313,lcd_discolor[num%14]);
break;
}
}
}
vTaskDelay(10); //延时10ms,也就是10 个时钟节拍
}
}
(1)、在使用队列之前要先创建队列,调用函数xQueueCreate()创建队列Key_Queue,队列
长度为1,每个队列项(消息)长度为1 个字节。
(2) 、同样的创建队列Message_Queue ,队列长度为4 ,每个队列项( 消息) 长度为
USART_REC_LEN,USART_REC_LEN 为50。
(3)、获取到按键键值以后就调用函数xQueueSend()发送到队列Key_Queue 中,由于只有一
个队列Key_Queue 只有一个队列项,所以此处也可以用覆写入队函数xQueueOverwrite()。
(4)、调用函数check_msg_queue()检查队列信息,并将相关的信息显示在LCD 上,如队列
总大小,队列剩余大小等。
(5)、调用函数xQueueReceive()获取队列Key_Queue 中的消息
(6)、变量key 保存着获取到的消息,也就是按键值,这里根据不同的按键值做不同的处理。
●中断初始化及处理过程
本实验用到了两个中断,串口1 的接收中断和定时器9 的定时中断,串口1 的具体配置看
基础例程中的串口实验就可以了,这里要将串口中断接收缓冲区大小改为50,如下:
#define USART_REC_LEN 50 //定义最大接收字节数50
#define EN_USART1_RX 1 //使能(1)/禁止(0)串口1 接收
还要注意!由于要在中断服务函数中使用FreeRTOS 中的API 函数,所以一定要注意中断
优先级的设置,这里设置如下:
void uart_init(u32 bound)
{
//GPIO 端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA,
ENABLE); //使能USART1,GPIOA 时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10 初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=7 ;//抢占优先级7
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound; //串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8 位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1
}
注意红色代码的串口中断优先级设置,这里设置抢占优先级为7,子优先级为0。这个优先
级中可以调用FreeRTOS 中的API 函数。串口1 的中断服务函数如下:
void USART1_IRQHandler(void) //串口1 中断服务程序
{
u8 Res;
BaseType_t xHigherPriorityTaskWoken;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
if((USART_RX_STA&0x8000)==0) //接收未完成
{
if(USART_RX_STA&0x4000) //接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0; //接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
}
}
}
}
//就向队列发送接收到的数据
if((USART_RX_STA&0x8000)&&(Message_Queue!=NULL)) (1)
{
//向队列中发送数据
xQueueSendFromISR(Message_Queue,USART_RX_BUF,&xHigherPriorityTaskWoken);(2)
USART_RX_STA=0;
//清除数据接收缓冲区USART_RX_BUF,用于下一次数据接收
memset(USART_RX_BUF,0,USART_REC_LEN); (3)
//如果需要的话进行一次任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); (4)
}
}
(1)、判断是否接收到数据,如果接收到数据的话就要将数据发送到队列Message_Queue 中,
(2)、调用函数xQueueSendFromISR()将串口接收缓冲区USART_RX_BUF[]中接收到的数据
发送到队列Message_Queue 中。
(3)、发送完成以后要将串口接收缓冲区USART_RX_BUF[]清零。
(4) 、如果需要进行任务调度的话在退出串口中断服务函数之前调用函数
portYIELD_FROM_ISR()进行一次任务调度。
在定时2 的中断服务函数中请求队列Message_Queue 中的数据并将请求到的消息显示在
LCD 上,定时器2 的定时周期设置为500ms,定时器初始化很简单,唯一要注意的就是中断优
先级的设置,如下:
//中断优先级NVIC 设置
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 8; //先占优先级4 级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0 级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC 寄存器
定时器2 的中断服务函数是重点,代码如下:
extern QueueHandle_t Message_Queue; //信息队列句柄
extern void disp_str(u8* str);
//定时器2 中断服务函数
void TIM2_IRQHandler(void)
{
u8 *buffer;
BaseType_t xTaskWokenByReceive=pdFALSE;
BaseType_t err;
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET) //溢出中断
{
buffer=mymalloc(SRAMIN,USART_REC_LEN); (1)
if(Message_Queue!=NULL)
{
memset(buffer,0,USART_REC_LEN); //清除缓冲区(2)
//请求消息Message_Queue
err=xQueueReceiveFromISR(Message_Queue,buffer,&xTaskWokenByReceive); (3)
if(err==pdTRUE) //接收到消息
{
disp_str(buffer); //在LCD 上显示接收到的消息(4)
}
}
myfree(SRAMIN,buffer); //释放内存(5)
//如果需要的话进行一次任务切换
portYIELD_FROM_ISR(xTaskWokenByReceive); (6)
}
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除中断标志位
}
(1)、从队列中获取消息也是采用数据拷贝的方式,所以我们要先准备一个数据缓冲区用来
保存从队列中获取到的消息。这里通过动态内存管理的方式分配一个数据缓冲区,这个动态内
存管理方法是ALIENTEK 编写的,具体原理和使用方法请参考基础例程中的内存管理实验。当
然了,也可以使用FreeRTOS 提供的动态内存管理函数。直接提供一个数组也行,这个数据缓
冲区的大小一定要和队列中队列项大小相同,比如本例程就是USART_REC_LEN。
(2)、清除缓冲区。
(3)、调用函数xQueueReceiveFromISR()从队列Message_Queue 中获取消息。
(4)、如果获取消息成功的话就调用函数disp_str()将获取到的消息显示在LCD 上。
(5)、使用完成以后就释放(3)中申请到的数据缓冲区内存。
(6) 、如果需要进行任务调度的话在退出定时器的中断服务函数之前调用函数
portYIELD_FROM_ISR()进行一次任务调度。
13.7.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,LCD 默认显示如图13.7.2.1 所示,
通过串口调试助手给开发板发送一串字符串,比如“ALIENTEK”,由于定时器9 会周期
性的读取队列Message_Queue 中的数据,当读取成功以后就会将相应的数据显示在LCD 上,
所以LCD 上会显示字符串“ALENTEK”,如图13.7.2.3 所示:
通过串口向开发板发送数据的时候注意观察队列Message_Queue 剩余大小的变化,最后按
不同的按键看看有什么反应,是否和我们的代码中设置的相同。
第十四章FreeRTOS 信号量
信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步,FreeRTOS
中信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其
应用场景不同,但有些应用场景是可以互换着使用的,本章我们就来学习一下FreeRTOS 的信
号量,本章分为如下几部分:
14.1 信号量简介
14.2 二值信号量
14.3 二值信号量操作实验
14.4 计数型信号量
14.5 计数型信号量操作实验
14.6 优先级翻转
14.7 优先级翻转实验
14.8 互斥信号量
14.9 互斥信号量操作实验
14.10 递归互斥信号量
14.1 信号量简介
信号量常常用于控制对共享资源的访问和任务同步。举一个很常见的例子,某个停车场有
100 个停车位,这100 个停车位大家都可以用,对于大家来说这100 个停车位就是共享资源。
假设现在这个停车场正常运行,你要把车停到这个这个停车场肯定要先看一下现在停了多少车
了?还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是这个信号量值,当
这个值到100 的时候说明停车场满了。停车场满的时你可以等一会看看有没有其他的车开出停
车场,当有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时你就可以把车
停进去了,你把车停进去以后停车数量就会加一,也就是信号量加一。这就是一个典型的使用
信号量进行共享资源管理的案例,在这个案例中使用的就是计数型信号量。再看另外一个案例:
使用公共电话,我们知道一次只能一个人使用电话,这个时候公共电话就只可能有两个状态:
使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。
信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得了这个锁的钥匙
才能够执行。
上面我们讲了信号量在共享资源访问中的使用,信号量的另一个重要的应用场合就是任务
同步,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务
发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度
下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函
数里面不能放太多的代码,否则的话会影响的中断的实时性。裸机编写中断服务函数的时候一
般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在使
用RTOS 系统的时候我们就可以借助信号量完成此功能,当中断发生的时候就释放信号量,中
断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获
取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间
非常短。这个例子就是中断与任务之间使用信号量来完成同步,当然了,任务与任务之间也可
以使用信号量来完成同步。
FreeRTOS 中还有一些其他特殊类型的信号量,比如互斥信号量和递归互斥信号量,这些具
体遇到的时候在讲解。有关信号量的知识在FreeRTOS 的官网上都有详细的讲解,包括二值信
号量、计数型信号量、互斥信号量和递归互斥信号量,我们下面要讲解的这些涉及到理论性的
知识都是翻译自FreeRTOS 官方资料,感兴趣的可以去官网看原版的英文资料。
14.2 二值信号量
14.2.1 二值信号量简介
二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一
些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号
另更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问,
有关互斥信号量的内容后面会专门讲解,本节只讲解二值信号量在同步中的应用。
和队列一样,信号量API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时
候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一
一个信号量上的话那么优先级最高的哪个任务优先获得信号量,这样当信号量有效的时候高优
先级的任务就会解除阻塞状态。
二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空
的,这不正好就是二值的吗?任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。
在实际应用中通常会使用一个任务来处理MCU 的某个外设,比如网络应用中,一般最简
单的方法就是使用一个任务去轮询的查询MCU 的ETH(网络相关外设,如STM32 的以太网
MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费
CPU 资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任
务就进入阻塞态,把CPU 让给其他的任务,当有数据的时候网络任务才去执行。现在使用二值
信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入
阻塞态,而网络中断服务函数(大多数的网络外设都有中断功能,比如STM32 的MAC 专用DMA
中断,通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络
数据,网络任务可以去提取处理了。网络任务只是在一直的获取二值信号量,它不会释放信号
量,而中断服务函数是一直在释放信号量,它不会获取信号量。在中断服务函数中发送信号量
可以使用函数xSemaphoreGiveFromISR(),也可以使用任务通知功能来替代二值信号量,而且使
用任务通知的话速度更快,代码量更少,有关任务通知的内容后面会有专门的章节介绍。
使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及
时的处理,这样做相当于推迟了中断处理过程。也可以使用队列来替代二值信号量,在外设事
件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话
任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。下面几
个步骤演示了二值信号量的工作过程。
1、二值信号量无效
在图14.2.1.1 中任务Task 通过函数xSemaphoreTake()获取信号量,但是此时二值信号量无
效,所以任务Task 进入阻塞态。
2、中断释放信号量
此时中断发生了,在中断服务函数中通过函数xSemaphoreGiveFromISR()释放信号量,因此信号量变为有效。
3、任务获取信号量成功
由于信号量已经有效了,所以任务Task 获取信号量成功,任务从阻塞态解除,开始执行相
关的处理过程。
4、任务再次进入阻塞态
由于任务函数一般都是一个大循环,所以在任务做完相关的处理以后就会再次调用函数
xSemaphoreTake()获取信号量。在执行完第三步以后二值信号量就已经变为无效的了,所以任务
将再次进入阻塞态,和第一步一样,直至中断再次发生并且调用函数xSemaphoreGiveFromISR()
释放信号量。
14.2.2 创建二值信号量
同队列一样,要想使用二值信号量就必须先创建二值信号量,二值信号量创建函数如表14.2.2
所示:
1、函数vSemaphoreCreateBinary ()
此函数是老版本FreeRTOS 中的创建二值信号量函数,新版本已经不再使用了,新版本的
FreeRTOS 使用xSemaphoreCreateBinary()来替代此函数,这里还保留这个函数是为了兼容那些
基于老版本FreeRTOS 而做的应用层代码。此函数是个宏,具体创建过程是由函数
xQueueGenericCreate()来完成的,在文件semphr.h 中有如下定义:
void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
参数:
xSemaphore:保存创建成功的二值信号量句柄。
返回值:
NULL: 二值信号量创建失败。
其他值: 二值信号量创建成功。
2、函数xSemaphoreCreateBinary()
此函数是vSemaphoreCreateBinary()的新版本,新版本的FreeRTOS 中统一用此函数来创建
二值信号量。使用此函数创建二值信号量的话信号量所需要的RAM 是由FreeRTOS 的内存管
理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号
量使用函数xSemaphoreTake()是获取不到的,此函数也是个宏,具体创建过程是由函数
xQueueGenericCreate()来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinary( void )
参数:
无。
返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量的句柄。
3、函数xSemaphoreCreateBinaryStatic()
此函数也是创建二值信号量的,只不过使用此函数创建二值信号量的话信号量所需要的
RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数xQueueGenericCreateStatic()
来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
参数:
pxSemaphoreBuffer:此参数指向一个StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量句柄。
14.2.3 二值信号量创建过程分析
上一小节讲了三个用于二值信号量创建的函数,两个动态的创建函数和一个静态的创建函
数。本节就来分析一下这两个动态的创建函数,静态创建函数和动态的类似,就不做分析了。
首先来看一下老版本的二值信号量动态创建函数vSemaphoreCreateBinary(),函数代码如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define vSemaphoreCreateBinary( xSemaphore ) \
{ \
( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, \ (1)
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE ); \
if( ( xSemaphore ) != NULL ) \
{ \
( void ) xSemaphoreGive( ( xSemaphore ) ); \ (2)
} \
}
#endif
(1)、上面说了二值信号量是在队列的基础上实现的,所以创建二值信号量就是创建队列的
过程。这里使用函数xQueueGenericCreate()创建了一个队列,队列长度为1,队列项长度为0,
队列类型为queueQUEUE_TYPE_BINARY_SEMAPHORE,也就是二值信号量。
(2)、当二值信号量创建成功以后立即调用函数xSemaphoreGive()释放二值信号量,此时新
创建的二值信号量有效。
在来看一下新版本的二值信号量创建函数xSemaphoreCreateBinary(),函数代码如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() \
xQueueGenericCreate( ( UBaseType_t ) 1, \
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE ) \
#endif
可以看出新版本的二值信号量创建函数也是使用函数xQueueGenericCreate()来创建一个类
型为queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为1、队列项长度为0 的队列。这一步
和老版本的二值信号量创建函数一样,唯一不同的就是新版本的函数在成功创建二值信号量以
后不会立即调用函数xSemaphoreGive()释放二值信号量。也就是说新版函数创建的二值信号量
默认是无效的,而老版本是有效的。
大家注意看,创建的队列是个没有存储区的队列,前面说了使用队列是否为空来表示二值
信号量,而队列是否为空可以通过队列结构体的成员变量uxMessagesWaiting 来判断。
14.2.4 释放信号量
释放信号量的函数有两个,如表14.2.4.1 所示:
同队列一样,释放信号量也分为任务级和中断级。还有!不管是二值信号量、计数型信号
量还是互斥信号量,它们都使用表14.2.4.1 中的函数释放信号量,递归互斥信号量有专用的释
放函数。
1、函数xSemaphoreGive()
此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信
号量的过程是由函数xQueueGenericSend()来完成的,函数原型如下:
BaseType_t xSemaphoreGive( xSemaphore )
参数:
xSemaphore:要释放的信号量句柄。
返回值:
pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
我们再来看一下函数xSemaphoreGive()的具体内容,此函数在文件semphr.h 中有如下定义:
#define xSemaphoreGive( xSemaphore ) \
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
NULL, \
semGIVE_BLOCK_TIME, \
queueSEND_TO_BACK ) \
可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息,
阻塞时间为0(宏semGIVE_BLOCK_TIME 为0),入队方式采用的后向入队。具体入队过程第十
三章已经做了详细的讲解,入队的时候队列结构体成员变量uxMessagesWaiting 会加一,对于二值信号量通过判断uxMessagesWaiting 就可以知道信号量是否有效了,当uxMessagesWaiting 为
1 的话说明二值信号量有效,为0 就无效。如果队列满的话就返回错误值errQUEUE_FULL,提
示队列满,入队失败。
2、函数xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量,此函数只能用来释放二值信号量和计数型信号量,绝对
不能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数
xQueueGiveFromISR(),此函数原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)
参数:
xSemaphore:要释放的信号量句柄。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一
个变量来保存这个值就行了。当此值为pdTRUE 的时候在退
出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。
在中断中释放信号量真正使用的是函数xQueueGiveFromISR(),此函数和中断级通用入队
函数xQueueGenericSendFromISR() 极其类似!只是针对信号量做了微小的改动。函数
xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量,因为互斥信号量涉及到优先级继
承的问题,而中断不属于任务,没法处理中断优先级继承。大家可以参考第十三章分析函数
xQueueGenericSendFromISR()的过程来分析xQueueGiveFromISR()。
14.2.5 获取信号量
获取信号量也有两个函数,如表14.2.5.1 所示:
同释放信号量的API 函数一样,不管是二值信号量、计数型信号量还是互斥信号量,它们
都使用表14.2.5.1 中的函数获取信号量
1、函数xSemaphoreTake()
此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信
号量的过程是由函数xQueueGenericReceive ()来完成的,函数原型如下:
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
TickType_t xBlockTime)
参数:
xSemaphore:要获取的信号量句柄。
xBlockTime: 阻塞时间。
返回值:
pdTRUE: 获取信号量成功。
pdFALSE: 超时,获取信号量失败。
再来看一下函数xSemaphoreTake ()的具体内容,此函数在文件semphr.h 中有如下定义:
#define xSemaphoreTake( xSemaphore, xBlockTime ) \
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \
NULL, \
( xBlockTime ), \
pdFALSE ) \
获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。在
第十三章讲解函数xQueueGenericReceive()的时候说过如果队列为空并且阻塞时间为0 的话就
立即返回errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为0 的话就将任务
添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数
据读取完成以后还需要将队列结构体成员变量uxMessagesWaiting 减一,然后解除某些因为入
队而阻塞的任务,最后返回pdPASS 表示出对成功。互斥信号量涉及到优先级继承,处理方式
不同,后面讲解互斥信号量的时候在详细的讲解。
2、函数xSemaphoreTakeFromISR ()
此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量,
绝对不能使用此函数来获取互斥信号量!此函数是一个宏,真正执行的是函数xQueueReceiveFromISR (),此函数原型如下:
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)
参数:
xSemaphore:要获取的信号量句柄。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一
个变量来保存这个值就行了。当此值为pdTRUE 的时候在退
出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS: 获取信号量成功。
pdFALSE: 获取信号量失败。
在中断中获取信号量真正使用的是函数xQueueReceiveFromISR (),这个函数就是中断级
出队函数!当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后
将队列结构体中的成员变量uxMessagesWaiting 减一,如果有任务因为入队而阻塞的话就解除
阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数pxHigherPriorityTaskWoken 设置为
pdTRUE,最后返回pdPASS 表示出队成功。如果队列为空的话就直接返回pdFAIL 表示出队失
败!这个函数还是很简单的。
14.3 二值信号量操作实验
14.3.1 实验程序设计
1、实验目的
二值信号量的使命就是同步,完成任务与任务或中断与任务之间的同步。大多数情况下都
是中断与任务之间的同步。本节就学习一下如何使用二值信号量来完成中断与任务之间的同步。
2、实验设计
本节我们设计一个通过串口发送指定的指令来控制开发板上的LED1 和BEEP 开关的实验,
指令如下(不区分大小写):
LED1ON:打开LED1。
LED1OFF:关闭LED1。
BEEPON:打开蜂鸣器。
BEEPOFF:关闭蜂鸣器。
这些指令通过串口发送给开发板,指令是不分大小写的!开发板使用中断接收,当接收到
数据以后就释放二值信号量。任务DataProcess_task()用于处理这些指令,任务会一直尝试获取
二值信号量,当获取到信号量就会从串口接收缓冲区中提取这些指令,然后根据指令控制相应
的外设。
本实验设计三个任务:start_task、task1_task 、DataProcess_task 这三个任务的任务功能如
下:
start_task:用来创建其他2 个任务。
task1_task :控制LED0 闪烁,提示系统正在运行。
DataProcess_task :指令处理任务,根据接收到的指令来控制不同的外设。
实验中还创建了一个二值信号量BinarySemaphore 用于完成串口中断和任务
DataProcess_task 之间的同步。
3、实验工程
FreeRTOS 实验14-1 FreeRTOS 二值信号量操作实验。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define DATAPROCESS_TASK_PRIO 3 //任务优先级
#define DATAPROCESS_STK_SIZE 256 //任务堆栈大小
TaskHandle_t DataProcess_Handler; //任务句柄
void DataProcess_task(void *pvParameters); //任务函数
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
//用于命令解析用的命令值
#define LED1ON 1
#define LED1OFF 2
#define BEEPON 3
#define BEEPOFF 4
#define COMMANDERR 0XFF
●其他应用函数
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{
u8 i;
for(i=0;i<len;i++)
{
if((96<str[i])&&(str[i]<123)) //小写字母
str[i]=str[i]-32; //转换为大写
}
}
//命令处理函数,将字符串命令转换成命令值
//str:命令
//返回值: 0XFF,命令错误;其他值,命令值
u8 CommandProcess(u8 *str)
{
u8 CommandValue=COMMANDERR;
if(strcmp((char*)str,"LED1ON")==0) CommandValue=LED1ON;
else if(strcmp((char*)str,"LED1OFF")==0) CommandValue=LED1OFF;
else if(strcmp((char*)str,"BEEPON")==0) CommandValue=BEEPON;
else if(strcmp((char*)str,"BEEPOFF")==0) CommandValue=BEEPOFF;
return CommandValue;
}
函数LowerToCap()用于将串口发送过来的命令中的小写字母统一转换成大写字母,这样就
可以在发送命令的时候不用区分大小写,因为开发板会统一转换成大写。函数CommandProcess()
用于将接收到的命令字符串转换成命令值,比如命令“LED1ON”转换成命令值就是0(宏
LED1ON 为0)。
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR=RED;
LCD_ShowString(10,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(10,30,200,16,16,"FreeRTOS Examp 14-1");
LCD_ShowString(10,50,200,16,16,"Binary Semap");
LCD_ShowString(10,70,200,16,16,"Command data:");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建二值信号量
BinarySemaphore=xSemaphoreCreateBinary();
//创建TASK1 任务
xTaskCreate((TaskFunction_t )task1_task,
(const char* )"task1_task",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_TASK_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
//创建TASK2 任务
xTaskCreate((TaskFunction_t )DataProcess_task,
(const char* )"keyprocess_task",
(uint16_t )DATAPROCESS_STK_SIZE,
(void* )NULL,
(UBaseType_t )DATAPROCESS_TASK_PRIO,
(TaskHandle_t* )&DataProcess_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{
while(1)
{
LED0=!LED0;
vTaskDelay(500); //延时500ms,也就是500 个时钟节拍
}
}
//DataProcess_task 函数
void DataProcess_task(void *pvParameters)
{
u8 len=0;
u8 CommandValue=COMMANDERR;
BaseType_t err=pdFALSE;
u8 *CommandStr;
POINT_COLOR=BLUE;
while(1)
{
if(BinarySemaphore!=NULL)
{
err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY);//获取信号量(1)
if(err==pdTRUE) //获取信号量成功
{
len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度
CommandStr=mymalloc(SRAMIN,len+1); //申请内存
sprintf((char*)CommandStr,"%s",USART_RX_BUF);
CommandStr[len]='\0'; //加上字符串结尾符号
LowerToCap(CommandStr,len); //将字符串转换为大写(2)
CommandValue=CommandProcess(CommandStr); //命令解析(3)
if(CommandValue!=COMMANDERR) //接收到正确的命令
{
LCD_Fill(10,90,210,110,WHITE); //清除显示区域
LCD_ShowString(10,90,200,16,16,CommandStr);//在LCD 上显示命令
printf("命令为:%s\r\n",CommandStr);
switch(CommandValue) //处理命令(4)
{
case LED1ON:
LED1=0;
break;
case LED1OFF:
LED1=1;
break;
case BEEPON:
BEEP=1;
break;
case BEEPOFF:
BEEP=0;
break;
}
}
else
{
printf("无效的命令,请重新输入!!\r\n");
}
USART_RX_STA=0;
memset(USART_RX_BUF,0,USART_REC_LEN);//串口接收缓冲区清零
myfree(SRAMIN,CommandStr); //释放内存
}
}
else if(err==pdFALSE)
{
vTaskDelay(10); //延时10ms,也就是10 个时钟节拍
}
}
}
(1) 、使用函数xSemaphoreTake() 获取二值信号量BinarySemaphore ,延时时间为
portMAX_DELAY。
(2)、调用函数LowerToCap()将命令字符串中的小写字母转换成大写的。
(3)、调用函数CommandProcess()处理命令字符串,其实就是将命令字符串转换为命令值。
(4)、根据不同的命令值执行不同的操作,如开关LED1,开关BEEP。
●中断初始化及处理过程
本实验中串口1 是通过中断方式来接收数据的,所以需要初始化串口1,串口的初始化很
简单,前面已经讲了很多次了。不过要注意串口1 的中断优先级!因为我们要在串口1 的中断
服务函数中使用FeeRTOS 的API 函数,本实验设置串口1 的抢占优先级为7,子优先级为0,
如下:
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=7 ; //抢占优先级7
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC 寄存器
串口1 的中断服务函数如下:
extern SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
void USART1_IRQHandler(void) //串口1 中断服务程序
{
u8 Res;
BaseType_t xHigherPriorityTaskWoken;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
}
}
}
}
//释放二值信号量
if((USART_RX_STA&0x8000)&&(BinarySemaphore!=NULL))
{
//释放二值信号量
xSemaphoreGiveFromISR(BinarySemaphore,&xHigherPriorityTaskWoken); (1)
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//如果需要的话进行一次任务切换
}
}
(1) 、当串口接收到数据以后就调用函数xSemaphoreGiveFromISR() 释放信号量
BinarySemaphore。
14.3.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,通过串口调试助手发送命令,比如
命令“led1ON”,开发板接收到命令以后就会将命令中的小写字母转换为大写,并且显示在LCD上,如图14.3.2.1 所示:
当命令正确的时候LED1 就会亮,同时开发板向串口调试助手发送经过大小写转换后的命
令字符串,如图14.3.2.2 所示:
当命令错误的时候开发板就会向串口调试助手发送命令错误的提示信息,比如我们发送
“led1_off”这个命令,串口调试助手显示如图14.3.2.3 所示:
14.4 计数型信号量
14.4.1 计数型信号量简介
有些资料中也将计数型信号量叫做数值信号量,二值信号量相当于长度为1 的队列,那么
计数型信号量就是长度大于1 的队列。同二值信号量一样,用户不需要关心队列中存储了什么
数据,只需要关心队列是否为空即可。计数型信号量通常用于如下两个场合:
1、事件计数
在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数
值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量
uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为0。
2、资源管理
在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。
一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减
一。当信号量值为0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,
释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,
比如停车场一共有100 个停车位,那么创建信号量的时候信号量值就应该初始化为100。
14.4.2 创建计数型信号量
FreeRTOS 提供了两个计数型信号量创建函数,如表14.4.2.1 所示:
1、函数xSemaphoreCreateCounting()
此函数用于创建一个计数型信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数xQueueCreateCountingSemaphore(),此函数原型如
下:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount )
参数:
uxMaxCount:计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount:计数信号量初始值。
返回值:
NULL: 计数型信号量创建失败。
其他值: 计数型信号量创建成功,返回计数型信号量句柄。
2、函数xSemaphoreCreateCountingStatic()
此函数也是用来创建计数型信号量的,使用此函数创建计数型信号量的时候所需要的内存
需要由用户分配。此函数也是一个宏,真正执行的是函数xQueueCreateCountingSemaphoreStatic(),
函数原型如下:
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t * pxSemaphoreBuffer )
参数:
uxMaxCount:计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount:计数信号量初始值。
pxSemaphoreBuffer:指向一个StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 计数型信号量创建失败。
其他值: 计数型号量创建成功,返回计数型信号量句柄。
14.4.3 计数型信号量创建过程分析
这里只分析动态创建计数型信号量函数xSemaphoreCreateCounting(),此函数是个宏,定义
如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) ) \
#endif
可以看出,真正干事的是函数xQueueCreateCountingSemaphore(),此函数在文件queue.c 中
有如下定义:
QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,
const UBaseType_t uxInitialCount )
{
QueueHandle_t xHandle;
configASSERT( uxMaxCount != 0 );
configASSERT( uxInitialCount <= uxMaxCount );
xHandle = xQueueGenericCreate( uxMaxCount,\ (1)
queueSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_COUNTING_SEMAPHORE );
if( xHandle != NULL )
{
( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount; (2)
traceCREATE_COUNTING_SEMAPHORE();
}
else
{
traceCREATE_COUNTING_SEMAPHORE_FAILED();
}
return xHandle;
}
(1)、计数型信号量也是在队列的基础上实现的,所以需要调用函数xQueueGenericCreate()
创建一个队列,队列长度为uxMaxCount ,对列项长度为
queueSEMAPHORE_QUEUE_ITEM_LENGTH( 此宏为0) ,队列的类型为
queueQUEUE_TYPE_COUNTING_SEMAPHORE,表示是个计数型信号量。
(2)、队列结构体的成员变量uxMessagesWaiting 用于计数型信号量的计数,根据计数型信
号量的初始值来设置uxMessagesWaiting。
14.4.4 释放和获取计数信号量
计数型信号量的释放和获取与二值信号量相同,具体请参考14.2.3 和14.2.4 小节。
14.5 计数型信号量操作实验
14.5.1 实验程序设计
1、实验目的
计数型信号量一般用于事件计数和资源管理,计数型信号量在这个场景中的使用方法基本
一样,本实验就来学习一下计数型信号量在事件计数中的使用方法。
2、实验设计
本实验中用KEY_UP 按键来模拟事件,当KEY_UP 按下以后就表示事件发生,当检测到
KEY_UP 按下以后就释放计数型信号量,按键的检测和信号量的释放做成一个任务。另外一个
任务获取信号量,当信号量获取成功以后就刷新LCD 上指定区域的背景颜色,并且显示计数型
信号量的值。
本实验设计三个任务:start_task、SemapGive_task 、SemapTake_task 这三个任务的任务功
能如下:
start_task:用来创建其他2 个任务。
SemapGive_task :获取按键状态,当KEY_UP 键按下去以后就释放信号量CountSemaphore,
此任务还用来控制LED0 的亮灭来提示程序正在运行中。
SemapTake_task :获取信号量CountSemaphore,当获取信号量成功以后就刷新LCD 指定
区域的背景色。
实验中创建了一个计数型信号量CountSemaphore,此信号量用于记录KEY_UP 按下的次
数。硬件部分需要用到KEY_UP 按键,用于模拟事件发生。
3、实验工程
FreeRTOS 实验14-2 FreeRTOS 计数型信号量操作实验。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define SEMAPGIVE_TASK_PRIO 2 //任务优先级
#define SEMAPGIVE_STK_SIZE 256 //任务堆栈大小
TaskHandle_t SemapGiveTask_Handler; //任务句柄
void SemapGive_task(void *pvParameters); //任务函数
#define SEMAPTAKE_TASK_PRIO 3 //任务优先级
#define SEMAPTAKE_STK_SIZE 256 //任务堆栈大小
TaskHandle_t SemapTakeTask_Handler; //任务句柄
void SemapTake_task(void *pvParameters); //任务函数
//计数型信号量句柄
SemaphoreHandle_t CountSemaphore;//计数型信号量
//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW, BROWN,
BRRED, GRAY };
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 14-2");
LCD_ShowString(30,50,200,16,16,"Count Semaphore");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2016/11/25");
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,234,314);
LCD_DrawLine(5,130,234,130);
POINT_COLOR = RED;
LCD_ShowString(30,111,200,16,16,"COUNT_SEM Value: 0");
POINT_COLOR = BLUE;
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建计数型信号量
CountSemaphore=xSemaphoreCreateCounting(255,0); (1)
//创建释放信号量任务
xTaskCreate((TaskFunction_t )SemapGive_task,
(const char* )"semapgive_task",
(uint16_t )SEMAPGIVE_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEMAPGIVE_TASK_PRIO,
(TaskHandle_t* )&SemapGiveTask_Handler);
//创建获取信号量任务
xTaskCreate((TaskFunction_t )SemapTake_task,
(const char* )"semaptake_task",
(uint16_t )SEMAPTAKE_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEMAPTAKE_TASK_PRIO,
(TaskHandle_t* )&SemapTakeTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//释放计数型信号量任务函数
void SemapGive_task(void *pvParameters)
{
u8 key,i=0;
u8 semavalue;
BaseType_t err;
while(1)
{
key=KEY_Scan(0); //扫描按键
if(CountSemaphore!=NULL) //计数型信号量创建成功
{
switch(key)
{
case WKUP_PRES:
err=xSemaphoreGive(CountSemaphore);//释放计数型信号量(2)
if(err==pdFALSE)
{
printf("信号量释放失败!!!\r\n");
}
//获取计数型信号量值
semavalue=uxSemaphoreGetCount(CountSemaphore); (3) LCD_ShowxNum(155,111,semavalue,3,16,0);//显示信号量值(4)
break;
}
}
i++;
if(i==50)
{
i=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10 个时钟节拍
}
}
//获取计数型信号量任务函数
void SemapTake_task(void *pvParameters)
{
u8 num;
u8 semavalue;
while(1)
{
xSemaphoreTake(CountSemaphore,portMAX_DELAY); //等待数值信号量(5)
num++;
semavalue=uxSemaphoreGetCount(CountSemaphore); //获取数值信号量值(6)
LCD_ShowxNum(155,111,semavalue,3,16,0); //显示信号量值
LCD_Fill(6,131,233,313,lcd_discolor[num%14]); //刷屏
LED1=!LED1;
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
(1)、要使用计数型信号量,首先肯定是要先创建,调用函数xSemaphoreCreateCounting()创
建一个计数型信号量CountSemaphore。计数型信号量计数最大值设置为255,由于本实验中计
数型信号量是用于事件计数的,所以计数型信号量的初始值设置为0。如果计数型信号量用于
资源管理的话那么事件计数型信号量的初始值就应该根据资源的实际数量来设置了。
(2) 、如果KEY_UP 键按下的话就表示事件发生了,事件发生以后就调用函数
xSemaphoreGive()释放信号量CountSemaphore。
(3)、调用函数uxSemaphoreGetCount()获取信号量CountSemaphore 的信号量值,释放信号
量的话信号量值就会加一。函数uxSemaphoreGetCount()是用来获取信号量值的,这个函数是个
宏,是对函数uxQueueMessagesWaiting()的一个简单封装,其实就是返回队列结构体成员变量
uxMessagesWaiting 的值。
(4)、在LCD 上显示信号量CountSemaphore 的信号量值,可以直观的观察信号量的变化过
程。
(5)、调用函数xSemaphoreTake()获取信号量CountSemaphore。
(6)、同样的在获取信号量以后调用函数uxSemaphoreGetCount()获取信号量值,并且在LCD
上显示出来,因为获取信号量成功以后信号量值会减一。
14.5.2 程序运行结果分析
编译并下载实验代码到开发板中,默认情况下LCD 显示如图14.5.2.1 所示。
按下KEY_UP 键释放信号量CountSemaphore,注意观察LCD 上的信号量值的变化。信号
量有效以后任务SemapTake_task()就可以获取到信号量,获取到信号量的话就会刷新LCD 指定
区域的背景色。释放信号量的话信号量值就会增加,获取信号量的话信号量值就会减少,如图
14.5.2.2 所示:
当信号量值减为0 的时候就表示信号量无效,任务SemapTake_task()获取信号量失败,任
务因此进入阻塞态,LCD 指定区域背景色刷新停止,看起来任务就好像是“停止”运行了。
14.6 优先级翻转
在使用二值信号量的时候会遇到很常见的一个问题——优先级翻转,优先级翻转在可剥夺
内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能
会导致严重的后果,图14.6.1 就是一个优先级翻转的例子。
(1) 任务H 和任务M 处于挂起状态,等待某一事件的发生,任务L 正在运行。
(2) 某一时刻任务L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。
(3) 任务L 获得信号量并开始使用该共享资源。
(4) 由于任务H 优先级高,它等待的事件发生后便剥夺了任务L 的CPU 使用权。
(5) 任务H 开始运行。
(6) 任务H 运行过程中也要使用任务L 正在使用着的资源,由于该资源的信号量还被任务
L 占用着,任务H 只能进入挂起状态,等待任务L 释放该信号量。
(7) 任务L 继续运行。
(8) 由于任务M 的优先级高于任务L,当任务M 等待的事件发生后,任务M 剥夺了任务
L 的CPU 使用权。
(9) 任务M 处理该处理的事。
(10) 任务M 执行完毕后,将CPU 使用权归还给任务L。
(11) 任务L 继续运行。
(12) 最终任务L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高
优先级的任务在等待这个信号量,故内核做任务切换。
(13) 任务H 得到该信号量并接着运行。
在这种情况下,任务H 的优先级实际上降到了任务L 的优先级水平。因为任务H 要一直
等待直到任务L 释放其占用的那个共享资源。由于任务M 剥夺了任务L 的CPU 使用权,使
得任务H 的情况更加恶化,这样就相当于任务M 的优先级高于任务H,导致优先级翻转。
14.7 优先级翻转实验
14.7.1 实验程序设计
1、实验目的
在使用二值信号量的时候会存在优先级翻转的问题,本实验通过模拟的方式实现优先级翻
转,观察优先级翻转对抢占式内核的影响。
2、实验设计
本实验设计四个任务:start_task、high_task 、middle_task ,low_task,这四个任务的任务
功能如下:
start_task:用来创建其他3 个任务。
high_task :高优先级任务,会获取二值信号量,获取成功以后会进行相应的处理,处理完
成以后就会释放二值信号量。
middle_task :中等优先级的任务,一个简单的应用任务。
low_task:低优先级任务,和高优先级任务一样,会获取二值信号量,获取成功以后会进行
相应的处理,不过不同之处在于低优先级的任务占用二值信号量的时间要久一点(软件模拟占
用)。
实验中创建了一个二值信号量BinarySemaphore,高优先级和低优先级这两个任务会使用这
个二值信号量。
3、实验工程
FreeRTOS 实验14-3 FreeRTOS 优先级翻转实验。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define LOW_TASK_PRIO 2 //任务优先级
#define LOW_STK_SIZE 256 //任务堆栈大小
TaskHandle_t LowTask_Handler; //任务句柄
void low_task(void *pvParameters); //任务函数
#define MIDDLE_TASK_PRIO 3 //任务优先级
#define MIDDLE_STK_SIZE 256 //任务堆栈大小
TaskHandle_t MiddleTask_Handler; //任务句柄
void middle_task(void *pvParameters); //任务函数
#define HIGH_TASK_PRIO 4 //任务优先级
#define HIGH_STK_SIZE 256 //任务堆栈大小
TaskHandle_t HighTask_Handler; //任务句柄
void high_task(void *pvParameters); //任务函数
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量
//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW, BROWN,
BRRED, GRAY };
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 14-3");
LCD_ShowString(30,50,200,16,16,"Priority Overturn");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2016/11/25");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建二值信号量
BinarySemaphore=xSemaphoreCreateBinary(); (1)
//二值信号量创建成功以后要先释放一下
if(BinarySemaphore!=NULL)xSemaphoreGive(BinarySemaphore); (2)
//创建高优先级任务
xTaskCreate((TaskFunction_t )high_task,
(const char* )"high_task",
(uint16_t )HIGH_STK_SIZE,
(void* )NULL,
(UBaseType_t )HIGH_TASK_PRIO,
(TaskHandle_t* )&HighTask_Handler);
//创建中等优先级任务
xTaskCreate((TaskFunction_t )middle_task,
(const char* )"middle_task",
(uint16_t )MIDDLE_STK_SIZE,
(void* )NULL,
(UBaseType_t )MIDDLE_TASK_PRIO,
(TaskHandle_t* )&MiddleTask_Handler);
//创建低优先级任务
xTaskCreate((TaskFunction_t )low_task,
(const char* )"low_task",
(uint16_t )LOW_STK_SIZE,
(void* )NULL,
(UBaseType_t )LOW_TASK_PRIO,
(TaskHandle_t* )&LowTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//高优先级任务的任务函数
void high_task(void *pvParameters)
{
u8 num;
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,115,314); //画一个矩形
LCD_DrawLine(5,130,115,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(6,111,110,16,16,"High Task");
while(1)
{
vTaskDelay(500); //延时500ms,也就是500 个时钟节拍
num++;
printf("high task Pend Sem\r\n");
xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取二值信号量(3)
printf("high task Running!\r\n");
LCD_Fill(6,131,114,313,lcd_discolor[num%14]); //填充区域
LED1=!LED1;
xSemaphoreGive(BinarySemaphore); //释放信号量(4)
vTaskDelay(500); //延时500ms,也就是500 个时钟节拍
}
}
//中等优先级任务的任务函数
void middle_task(void *pvParameters)
{
u8 num;
POINT_COLOR = BLACK;
LCD_DrawRectangle(125,110,234,314); //画一个矩形
LCD_DrawLine(125,130,234,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(126,111,110,16,16,"Middle Task");
while(1)
{
num++;
printf("middle task Running!\r\n");
LCD_Fill(126,131,233,313,lcd_discolor[13-num%14]); //填充区域
LED0=!LED0;
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
//低优先级任务的任务函数
void low_task(void *pvParameters)
{
static u32 times;
while(1)
{
xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取二值信号量(5)
printf("low task Running!\r\n");
for(times=0;times<20000000;times++) //模拟低优先级任务占用二值信号量(6)
{
taskYIELD(); //发起任务调度
}
xSemaphoreGive(BinarySemaphore); //释放二值信号量(7)
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
(1)、调用函数xSemaphoreCreateBinary()创建二值信号量。
(2)、默认创建的二值信号量是无效的,这里需要先调用函数xSemaphoreGive()释放一次二
值信号量。否则任务high_task()和low_task()都会获取不到信号量。
(3)、高优先级任务调用函数xSemaphoreTake()获取二值信号量。
(4)、使用完以后需要调用函数xSemaphoreGive()释放二值信号量。
(5)、低优先级任务获取二值信号量BinarySemaphore。
(6)、低优先级任务模拟长时间占用二值信号量。
(7)、低优先级任务释放二值信号量。
14.7.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,默认情况下LCD 显示如图14.7.2.1
所示。
图14.7.2.1 LCD 默认画面
从LCD 上不容易看出优先级翻转的现象,我们可以通过串口很方便的观察优先级翻转,
串口输出如图14.7.2.2 所示。
为了方便分析,我们将串口输出复制出来,如下:
LCD ID:5510
middle task Running!
low task Running! (1)
high task Pend Sem (2)
middle task Running! (3)
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
high task Running! (4)
middle task Running!
high task Pend Sem
(1)、low_task 任务获取到二值信号量BinarySemaphore 开始运行。
(2)、high_task 获取信号量BinarySemaphore,但是此时信号量BinarySemaphore 被任务
low_task 占用着,因此high_task 就要一直等待,直到low_task 任务释放信号量BinarySemaphore。
(3)、由于high_task 没有获取到信号量BinarySemaphore,只能一直等待,红色部分代码中
high_task 没有运行,而middle_task 一直在运行,给人的感觉就是middle_task 的任务优先级高
于high_task。但是事实上high_task 任务的任务优先级是高于middle_task 的,这个就是优先级
反转!
(4)、high_task 任务因为获取到了信号量BinarySemaphore 而运行
从本例程中可以看出,当一个低优先级任务和一个高优先级任务同时使用同一个信号量,
而系统中还有其他中等优先级任务时。如果低优先级任务获得了信号量,那么高优先级的任务
就会处于等待状态,但是,中等优先级的任务可以打断低优先级任务而先于高优先级任务运行
(此时高优先级的任务在等待信号量,所以不能运行),这是就出现了优先级翻转的现象。
既然优先级翻转是个很严重的问题,那么有没有解决方法呢?有!这就要引出另外一种信
号量——互斥信号量!
14.8 互斥信号量
14.8.1 互斥信号量简介
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中
断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在
互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,
当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。
互斥信号量使用和二值信号量相同的API 操作函数,所以互斥信号量也可以设置阻塞时间,
不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优
先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不
过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是
优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的
“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。
硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,
原因如下:
●互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
●中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
14.8.2 创建互斥信号量
FreeRTOS 提供了两个互斥信号量创建函数,如表14.8.2.1 所示:
1、函数xSemaphoreCreateMutex()
此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数xQueueCreateMutex(),此函数原型如下:
SemaphoreHandle_t xSemaphoreCreateMutex( void )
参数:
无。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
2、函数xSemaphoreCreateMutexStatic()
此函数也是创建互斥信号量的,只不过使用此函数创建互斥信号量的话信号量所需要的
RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数xQueueCreateMutexStatic ()
来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )
参数:
pxMutexBuffer:此参数指向一个StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
14.8.3 互斥信号量创建过程分析
这里只分析动态创建互斥信号量函数xSemaphoreCreateMutex (),此函数是个宏,定义如下:
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
可以看出,真正干事的是函数xQueueCreateMutex(),此函数在文件queue.c 中有如下定义,
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;
pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize,\ (1)
ucQueueType );
prvInitialiseMutex( pxNewQueue ); (2)
return pxNewQueue;
}
(1)、调用函数xQueueGenericCreate()创建一个队列,队列长度为1,队列项长度为0,队列
类型为参数ucQueueType。由于本函数是创建互斥信号量的,所以参数ucQueueType 为
queueQUEUE_TYPE_MUTEX。
(2)、调用函数prvInitialiseMutex()初始化互斥信号量。
函数prvInitialiseMutex()代码如下:
static void prvInitialiseMutex( Queue_t *pxNewQueue )
{
if( pxNewQueue != NULL )
{
//虽然创建队列的时候会初始化队列结构体的成员变量,但是此时创建的是互斥
//信号量,因此有些成员变量需要重新赋值,尤其是那些用于优先级继承的。
pxNewQueue->pxMutexHolder = NULL; (1)
pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX; (2)
//如果是递归互斥信号量的话。
pxNewQueue->u.uxRecursiveCallCount = 0; (3)
traceCREATE_MUTEX( pxNewQueue );
//释放互斥信号量
( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U,\
queueSEND_TO_BACK );
}
else
{
traceCREATE_MUTEX_FAILED();
}
}
(1)和(2)、这里大家可能会疑惑,队列结构体Queue_t 中没有pxMutexHolder 和uxQueueType
这两个成员变量吖?这两个东西是哪里来的妖孽?这两个其实是宏,专门为互斥信号量准备的,
在文件queue.c 中有如下定义:
#define pxMutexHolder pcTail
#define uxQueueType pcHead
#define queueQUEUE_IS_MUTEX NULL
当Queue_t 用于表示队列的时候pcHead 和pcTail 指向队列的存储区域,当Queue_t 用于表
示互斥信号量的时候就不需要pcHead 和pcTail 了。当用于互斥信号量的时候将pcHead 指向
NULL 来表示pcTail 保存着互斥队列的所有者,pxMutexHolder 指向拥有互斥信号量的那个任
务的任务控制块。重命名pcTail 和pcHead 就是为了增强代码的可读性。
(3)、如果创建的互斥信号量是互斥信号量的话,还需要初始化队列结构体中的成员变量
u.uxRecursiveCallCount。
互斥信号量创建成功以后会调用函数xQueueGenericSend()释放一次信号量,说明互斥信号
量默认就是有效的!互斥信号量创建完成以后如图14.8.3.1 所示:
14.8.4 释放互斥信号量
释放互斥信号量的时候和二值信号量、计数型信号量一样,都是用的函数
xSemaphoreGive()(实际上完成信号量释放的是函数xQueueGenericSend())。不过由于互斥信号量
涉及到优先级继承的问题,所以具体处理过程会有点区别。使用函数xSemaphoreGive()释放信
号量最重要的一步就是将uxMessagesWaiting 加一,而这一步就是通过函数
prvCopyDataToQueue() 来完成的,释放信号量的函数xQueueGenericSend() 会调用
prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数prvCopyDataToQueue()中完成的,
此函数中有如下一段代码:
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,
const void * pvItemToQueue,
const BaseType_t xPosition )
{
BaseType_t xReturn = pdFALSE;
UBaseType_t uxMessagesWaiting;
uxMessagesWaiting = pxQueue->uxMessagesWaiting;
if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )
{
#if ( configUSE_MUTEXES == 1 ) //互斥信号量
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) (1)
{
xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );(2)
pxQueue->pxMutexHolder = NULL; (3)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
}
/*********************************************************************/
/*************************省略掉其他处理代码**************************/
/*********************************************************************/
pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;
return xReturn;
}
(1)、当前操作的是互斥信号量。
(2)、调用函数xTaskPriorityDisinherit()处理互斥信号量的优先级继承问题。
(3)、互斥信号量释放以后,互斥信号量就不属于任何任务了,所以pxMutexHolder 要指向
NULL。
在来看一下函数xTaskPriorityDisinherit() 是怎么具体的处理优先级继承的,函数
xTaskPriorityDisinherit()代码如下:
BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{
TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
BaseType_t xReturn = pdFALSE;
if( pxMutexHolder != NULL ) (1)
{
//当一个任务获取到互斥信号量以后就会涉及到优先级继承的问题,正在释放互斥
//信号量的任务肯定是当前正在运行的任务pxCurrentTCB。
configASSERT( pxTCB == pxCurrentTCB );
configASSERT( pxTCB->uxMutexesHeld );
( pxTCB->uxMutexesHeld )--; (2)
//是否存在优先级继承?如果存在的话任务当前优先级肯定和任务基优先级不同。
if( pxTCB->uxPriority != pxTCB->uxBasePriority ) (3)
{
//当前任务只获取到了一个互斥信号量
if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 ) (4)
{
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) (5)
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority ); (6)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//使用新的优先级将任务重新添加到就绪列表中
traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
pxTCB->uxPriority = pxTCB->uxBasePriority; (7)
/* Reset the event list item value. It cannot be in use for
any other purpose if this task is running, and it must be
running to give back the mutex. */
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), \ (8)
( TickType_t ) configMAX_PRIORITIES - \
( TickType_t ) pxTCB->uxPriority );
prvAddTaskToReadyList( pxTCB ); (9)
xReturn = pdTRUE; (10)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xReturn;
}
(1)、函数的参数pxMutexHolder 表示拥有此互斥信号量任务控制块,所以要先判断此互斥
信号量是否已经被其他任务获取。
(2)、有的任务可能会获取多个互斥信号量,所以就需要标记任务当前获取到的互斥信号量
个数,任务控制块结构体的成员变量uxMutexesHeld 用来保存当前任务获取到的互斥信号量个
数。任务每释放一次互斥信号量,变量uxMutexesHeld 肯定就要减一。
(3)、判断是否存在优先级继承,如果存在的话任务的当前优先级肯定不等于任务的基优先
级。
(4)、判断当前释放的是不是任务所获取到的最后一个互斥信号量,因为如果任务还获取了
其他互斥信号量的话就不能处理优先级继承。优先级继承的处理必须是在释放最后一个互斥信
号量的时候。
(5)、优先级继承的处理说白了就是将任务的当前优先级降低到任务的基优先级,所以要把
当前任务先从任务就绪表中移除。当任务优先级恢复为原来的优先级以后再重新加入到就绪表
中。
(6)、如果任务继承来的这个优先级对应的就绪表中没有其他任务的话就将取消这个优先级
的就绪态。
(7)、重新设置任务的优先级为任务的基优先级uxBasePriority。
(8)、复位任务的事件列表项。
(9)、将优先级恢复后的任务重新添加到任务就绪表中。
(10)、返回pdTRUE,表示需要进行任务调度。
14.8.5 获取互斥信号量
获取互斥信号量的函数同获取二值信号量和计数型信号量的函数相同,都是
xSemaphoreTake()(实际执行信号量获取的函数是xQueueGenericReceive()),获取互斥信号量的
过程也需要处理优先级继承的问题,函数xQueueGenericReceive()在文件queue.c 中有定义,在
第十三章讲解队列的时候我们没有分析这个函数,本节就来简单的分析一下这个函数,缩减后
的函数代码如下:
BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
int8_t *pcOriginalReadPosition;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
for( ;; )
{
taskENTER_CRITICAL();
{
const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;
//判断队列是否有消息
if( uxMessagesWaiting > ( UBaseType_t ) 0 ) (1)
{
pcOriginalReadPosition = pxQueue->u.pcReadFrom;
prvCopyDataFromQueue( pxQueue, pvBuffer ); (2)
if( xJustPeeking == pdFALSE ) (3)
{
traceQUEUE_RECEIVE( pxQueue );
//移除消息
pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1; (4)
#if ( configUSE_MUTEXES == 1 ) (5)
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
pxQueue->pxMutexHolder = (6)
( int8_t * ) pvTaskIncrementMutexHeldCount();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
//查看是否有任务因为入队而阻塞,如果有的话就需要解除阻塞态。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == (7)
pdFALSE )
{
if( xTaskRemoveFromEventList( &
( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
//如果解除阻塞的任务优先级比当前任务优先级高的话就需要
//进行一次任务切换
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else (8)
{
traceQUEUE_PEEK( pxQueue );
//读取队列中的消息以后需要删除消息
pxQueue->u.pcReadFrom = pcOriginalReadPosition;
//如果有任务因为出队而阻塞的话就解除任务的阻塞态。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == (9)
pdFALSE )
{
if( xTaskRemoveFromEventList( &
( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
//如果解除阻塞的任务优先级比当前任务优先级高的话就需要
//进行一次任务切换
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return pdPASS;
}
else //队列为空(10)
{
if( xTicksToWait == ( TickType_t ) 0 )
{
//队列为空,如果阻塞时间为0 的话就直接返回errQUEUE_EMPTY
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else if( xEntryTimeSet == pdFALSE )
{
//队列为空并且设置了阻塞时间,需要初始化时间状态结构体。
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
taskEXIT_CRITICAL();
vTaskSuspendAll();
prvLockQueue( pxQueue );
//更新时间状态结构体,并且检查超时是否发生
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) (11)
{
if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) (12)
{
traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) (13)
{
taskENTER_CRITICAL();
{
vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );(14)
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), (15)
xTicksToWait );
prvUnlockQueue( pxQueue );
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
//重试一次
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
(1)、队列不为空,可以从队列中提取数据。
(2)、调用函数prvCopyDataFromQueue()使用数据拷贝的方式从队列中提取数据。
(3)、数据读取以后需要将数据删除掉。
(4)、队列的消息数量计数器uxMessagesWaiting 减一,通过这一步就将数据删除掉了。
(5)、表示此函数是用于获取互斥信号量的。
(6)、获取互斥信号量成功,需要标记互斥信号量的所有者,也就是给pxMutexHolder 赋值,
pxMutexHolder 应该是当前任务的任务控制块。但是这里是通过函数
pvTaskIncrementMutexHeldCount()来赋值的,此函数很简单,只是将任务控制块中的成员变量
uxMutexesHeld 加一,表示任务获取到了一个互斥信号量,最后此函数返回当前任务的任务控
制块。
(7)、出队成功以后判断是否有任务因为入队而阻塞的,如果有的话就需要解除任务的阻塞
态,如果解除阻塞的任务优先级比当前任务的优先级高还需要进行一次任务切换。
(8)、出队的时候不需要删除消息。
(9)、如果出队的时候不需要删除消息的话那就相当于刚刚出队的那条消息接着有效!既然
还有有效的消息存在队列中,那么就判断是否有任务因为出队而阻塞,如果有的话就解除任务
的阻塞态。同样的,如果解除阻塞的任务优先级比当前任务的优先级高的话还需要进行一次任
务切换。
(10)、上面分析的都是队列不为空的时候,那当队列为空的时候该如何处理呢?处理过程和
队列的任务级通用入队函数xQueueGenericSend()类似。如果阻塞时间为0 的话就就直接返回
errQUEUE_EMPTY,表示队列空,如果设置了阻塞时间的话就进行相关的处理。
(11)、检查超时是否发生,如果没有的话就需要将任务添加到队列的xTasksWaitingToReceive
列表中。
(12)、检查队列是否继续为空?如果不为空的话就会在重试一次出队。
(13)、表示此函数是用于获取互斥信号量的。
(14)、调用函数vTaskPriorityInherit()处理互斥信号量中的优先级继承问题,如果函数
xQueueGenericReceive()用于获取互斥信号量的话,此函数执行到这里说明互斥信号量正在被其
他的任务占用。此函数和14.8.4 小节中的函数xTaskPriorityDisinherit()过程相反。此函数会判断
当前任务的任务优先级是否比正在拥有互斥信号量的那个任务的任务优先级高,如果是的话就
会把拥有互斥信号量的那个低优先级任务的优先级调整为与当前任务相同的优先级!
(15)、经过(12)步判断,队列依旧为空,那么就将任务添加到列表xTasksWaitingToReceive
中。
在上面的分析中,红色部分就是当函数xQueueGenericReceive()用于互斥信号量的时候的处
理过程,其中(13)和(14)条详细的分析了互斥信号量优先级继承的过程。我们举个例子来简单的
演示一下这个过程,假设现在有两个任务HighTask 和LowTask,HighTask 的任务优先级为4,
LowTask 的任务优先级为2。这两个任务都会操同一个互斥信号量Mutex,LowTask 先获取到互
斥信号量Mutex。此时任务HighTask 也要获取互斥信号量Mutex,任务HighTask 调用函数
xSemaphoreTake()尝试获取互斥信号量Mutex,发现此互斥信号量正在被任务LowTask 使用,
并且LowTask 的任务优先级为2,比自己的任务优先级小,因为任务HighTask 就会将LowTask
的任务优先级调整为与自己相同的优先级,即4,然后任务HighTask 进入阻塞态等待互斥信号
量有效。
14.9 互斥信号量操作实验
14.9.1 实验程序设计
1、实验目的
学习使用互斥信号量,并且观察互斥信号量是否可以解决或者缓解优先级翻转。
2、实验设计
本实验在“FreeRTOS 实验14-3 FreeRTOS 优先级翻转实验”的基础上完成,只是将其中的
二值信号量更换为互斥信号量,其他部分完全相同。
3、实验工程
FreeRTOS 实验14-4 FreeRTOS 互斥信号量操作实验。
4、实验程序与分析
本实验是在实验“FreeRTOS 实验14-3 FreeRTOS 优先级翻转实验”的基础上修改的,除了
任务函数以外其他的部分都相同。
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建互斥信号量
MutexSemaphore=xSemaphoreCreateMutex(); (1)
//创建高优先级任务
xTaskCreate((TaskFunction_t )high_task,
(const char* )"high_task",
(uint16_t )HIGH_STK_SIZE,
(void* )NULL,
(UBaseType_t )HIGH_TASK_PRIO,
(TaskHandle_t* )&HighTask_Handler);
//创建中等优先级任务
xTaskCreate((TaskFunction_t )middle_task,
(const char* )"middle_task",
(uint16_t )MIDDLE_STK_SIZE,
(void* )NULL,
(UBaseType_t )MIDDLE_TASK_PRIO,
(TaskHandle_t* )&MiddleTask_Handler);
//创建低优先级任务
xTaskCreate((TaskFunction_t )low_task,
(const char* )"low_task",
(uint16_t )LOW_STK_SIZE,
(void* )NULL,
(UBaseType_t )LOW_TASK_PRIO,
(TaskHandle_t* )&LowTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//高优先级任务的任务函数
void high_task(void *pvParameters)
{
u8 num;
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,115,314); //画一个矩形
LCD_DrawLine(5,130,115,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(6,111,110,16,16,"High Task");
while(1)
{
vTaskDelay(500); //延时500ms,也就是500 个时钟节拍
num++;
printf("high task Pend Sem\r\n");
xSemaphoreTake(MutexSemaphore,portMAX_DELAY); //获取互斥信号量(2)
printf("high task Running!\r\n");
LCD_Fill(6,131,114,313,lcd_discolor[num%14]); //填充区域
LED1=!LED1;
xSemaphoreGive(MutexSemaphore); //释放信号量(3)
vTaskDelay(500); //延时500ms,也就是500 个时钟节拍
}
}
//中等优先级任务的任务函数
void middle_task(void *pvParameters)
{
u8 num;
POINT_COLOR = BLACK;
LCD_DrawRectangle(125,110,234,314); //画一个矩形
LCD_DrawLine(125,130,234,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(126,111,110,16,16,"Middle Task");
while(1)
{
num++;
printf("middle task Running!\r\n");
LCD_Fill(126,131,233,313,lcd_discolor[13-num%14]); //填充区域
LED0=!LED0;
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
//低优先级任务的任务函数
void low_task(void *pvParameters)
{
static u32 times;
while(1)
{
xSemaphoreTake(MutexSemaphore,portMAX_DELAY); //获取互斥信号量(4)
printf("low task Running!\r\n");
for(times=0;times<20000000;times++) //模拟低优先级任务占用互斥信号量(5)
{
taskYIELD(); //发起任务调度
}
xSemaphoreGive(MutexSemaphore); //释放互斥信号量(6)
vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍
}
}
(1)、调用函数xSemaphoreCreateMutex()创建互斥信号量MutexSemaphore。
(2)、任务high_task 获取互斥信号量。
(3)、互斥信号量使用完成以后一定要释放!
(4)、任务low_task 获取互斥信号量MutexSemaphore,阻塞时间为portMAX_DELAY。
(5)、模拟任务low_task 长时间占用互斥信号量portMAX_DELAY。
(6)、任务low_task 使用完互斥信号量,释放!
14.9.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,串口调试助手如图14.9.2.1 所示:
图14.9.2.1 串口调试助手
为了方便分析,我们将串口调试助手中的数据复制下来,如下:
LCD ID:5510
middle task Running! (1)
low task Running! (2)
high task Pend Sem (3)
high task Running! (4)
middle task Running!
high task Pend Sem
high task Running!
middle task Running!
low task Running!
(1)、middle_task 任务运行。
(2)、low_task 获得互斥信号量运行。
(3)、high_task 请求信号量,在这里会等待一段时间,等待low_task 任务释放互斥信号量。
但是middle_task 不会运行。因为由于low_task 正在使用互斥信号量,所以low_task 任务优先
级暂时提升到了与high_task 相同的优先级,这个优先级比任务middle_task 高,所以middle_task
任务不能再打断low_task 任务的运行了!
(4)、high_task 任务获得互斥信号量而运行。
从上面的分析可以看出互斥信号量有效的抑制了优先级翻转现象的发生。
14.10 递归互斥信号量
14.10.1 递归互斥信号量简介
递归互斥信号量可以看作是一个特殊的互斥信号量,已经获取了互斥信号量的任务就不能
再次获取这个互斥信号量,但是递归互斥信号量不同,已经获取了递归互斥信号量的任务可以
再次获取这个递归互斥信号量,而且次数不限!一个任务使用函数xSemaphoreTakeRecursive()
成功的获取了多少次递归互斥信号量就得使用函数xSemaphoreGiveRecursive()释放多少次!比
如某个任务成功的获取了5 次递归信号量,那么这个任务也得同样的释放5 次递归信号量。
递归互斥信号量也有优先级继承的机制,所以当任务使用完递归互斥信号量以后一定要记
得释放。同互斥信号量一样,递归互斥信号量不能用在中断服务函数中。
●由于优先级继承的存在,就限定了递归互斥信号量只能用在任务中,不能用在中断服务
函数中!
●中断服务函数不能设置阻塞时间。
要使用递归互斥信号量的话宏configUSE_RECURSIVE_MUTEXES 必须为1!
14.10.2 创建互斥信号量
FreeRTOS 提供了两个互斥信号量创建函数,如表14.10.2.1 所示:
1、函数xSemaphoreCreateRecursiveMutex()
此函数用于创建一个递归互斥信号量,所需要的内存通过动态内存管理方法分配。此函数
本质是一个宏,真正完成信号量创建的是函数xQueueCreateMutex (),此函数原型如下:
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )
参数:
无。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
2、函数xSemaphoreCreateRecursiveMutexStatic()
此函数也是创建递归互斥信号量的,只不过使用此函数创建递归互斥信号量的话信号量所
需要的RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数
xQueueCreateMutexStatic ()来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateRecursiveMutexStatic( StaticSemaphore_t *pxMutexBuffer )
参数:
pxMutexBuffer:此参数指向一个StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
NULL: 互斥信号量创建失败。
其他值: 创建成功的互斥信号量的句柄。
14.10.3 递归信号量创建过程分析
这里只分析动态创建互斥信号量函数xSemaphoreCreateRecursiveMutex (),此函数是个宏,
定义如下:
#define xSemaphoreCreateRecursiveMutex()
xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )
可以看出,真正干事的是函数xQueueCreateMutex(),互斥信号量的创建也是用的这个函数,
只是在创建递归互斥信号量的时候类型选择为queueQUEUE_TYPE_RECURSIVE_MUTEX。具
体的创建过程参考14.8.3 小节。
14.10.4 释放递归互斥信号量
递归互斥信号量有专用的释放函数:xSemaphoreGiveRecursive(),此函数为宏,如下:
#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )
函数的参数就是就是要释放的递归互斥信号量,真正的释放是由函数
xQueueGiveMutexRecursive()来完成的,此函数代码如下:
BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex )
{
BaseType_t xReturn;
Queue_t * const pxMutex = ( Queue_t * ) xMutex;
configASSERT( pxMutex );
//检查递归互斥信号量是不是被当前任务获取的,要释放递归互斥信号量的任务肯定是当
//前正在运行的任务。因为同互斥信号量一样,递归互斥信号量的获取和释放要在同一个
//任务中完成!如果当前正在运行的任务不是递归互斥信号量的拥有者就不能释放!
if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ) (1)
{
traceGIVE_MUTEX_RECURSIVE( pxMutex );
( pxMutex->u.uxRecursiveCallCount )--; (2)
if( pxMutex->u.uxRecursiveCallCount == ( UBaseType_t ) 0 ) (3)
{
( void ) xQueueGenericSend( pxMutex, NULL, \ (4)
queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xReturn = pdPASS; (5)
}
else
{
xReturn = pdFAIL; (6)
traceGIVE_MUTEX_RECURSIVE_FAILED( pxMutex );
}
return xReturn;
}
(1)、哪个任务获取到的递归互斥信号量,哪个任务就释放!要释放递归互斥信号量的任务
肯定是当前正在运行的任务。检查这个任务是不是递归互斥信号量的拥有者,如果不是的话就
不能完成释放。
(2)、uxRecursiveCallCount 减一,uxRecursiveCallCount 用来记录递归信号量被获取的次数。
由于递归互斥信号量可以被一个任务多次获取,因此在释放的时候也要多次释放,但是只有在
最后一次释放的时候才会调用函数xQueueGenericSend()完成释放过程,其他的时候只是简单的
将uxRecursiveCallCount 减一即可。
(3)、当uxRecursiveCallCount 为0 的时候说明是最后一次释放了。
(4)、如果是最后一次释放的话就调用函数xQueueGenericSend()完成真正的释放过程。阻塞
时间是queueMUTEX_GIVE_BLOCK_TIME,宏queueMUTEX_GIVE_BLOCK_TIME 为0。
(5)、递归互斥信号量释放成功,返回pdPASS。
(6)、递归互斥信号量释放未成功,返回pdFAIL。
由于递归互斥信号量可以被一个任务重复的获取,因此在释放的时候也要释放多次,但是
只有在最后一次释放的时候才会调用函数xQueueGenericSend()完成真正的释放。其他释放的话
只是简单的将uxRecursiveCallCount 减一。
14.10.5 获取递归互斥信号量
递归互斥信号量的获取使用函数xSemaphoreTakeRecursive(),此函数是个宏,定义如下:
#define xSemaphoreTakeRecursive( xMutex, xBlockTime )
xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
函数第一个参数是要获取的递归互斥信号量句柄,第二个参数是阻塞时间。真正的获取过
程是由函数xQueueTakeMutexRecursive()来完成的,此函数如下:
BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, //要获取的信号量
TickType_t xTicksToWait )//阻塞时间
{
BaseType_t xReturn;
Queue_t * const pxMutex = ( Queue_t * ) xMutex;
configASSERT( pxMutex );
traceTAKE_MUTEX_RECURSIVE( pxMutex );
if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ) (1)
{
( pxMutex->u.uxRecursiveCallCount )++; (2)
xReturn = pdPASS;
}
else
{
xReturn = xQueueGenericReceive( pxMutex, NULL, xTicksToWait, pdFALSE ); (3)
if( xReturn != pdFAIL )
{
( pxMutex->u.uxRecursiveCallCount )++; (4)
}
else
{
raceTAKE_MUTEX_RECURSIVE_FAILED( pxMutex );
}
}
return xReturn;
}
(1)、判断当前要获取递归互斥信号量的任务是不是已经是递归互斥信号量的拥有者。通过
这一步就可以判断出当前任务是第一次获取递归互斥信号量还是重复获取。
(2)、如果当前任务已经是递归互斥信号量的拥有者,那就说明任务已经获取了递归互斥信
号量,本次是重复获取递归互斥信号量,那么就简单的将uxRecursiveCallCount 加一,然后返回
pdPASS 表示获取成功。
(3)、如果任务是第一次获取递归互斥信号量的话就需要调用函数xQueueGenericReceive()
完成真正的获取过程。
(4)、第一次获取递归互斥信号量成功以后将uxRecursiveCallCount 加一。
14.10.6 递归互斥信号量使用示例
互斥信号量使用很简单,就不做专门的实验了,FreeRTOS 官方提供了一个简单的示例,大
家可以参考一下,示例如下:
SemaphoreHandle_t RecursiveMutex; //递归互斥信号量句柄
//某个任务中创建一个递归互斥信号量
void vATask( void * pvParameters )
{
//没有创建创建递归互斥信号量之前不要使用!
RecursiveMutex = xSemaphoreCreateRecursiveMutex(); //创建递归互斥信号量
for( ;; )
{
/************任务代码**************/
}
}
//任务调用的使用递归互斥信号量的功能函数。
void vAFunction( void )
{
/**********其他处理代码*****************/
if( xMutex != NULL )
{
//获取递归互斥信号量,阻塞时间为10 个节拍
if( xSemaphoreTakeRecursive( RecursiveMutex, 10 ) == pdTRUE )
{
/***********其他处理过程*************/
//这里为了演示,所以是顺序的获取递归互斥信号量,但是在实际的代码中肯定
//不是这么顺序的获取的,真正的代码中是混合着其他程序调用的。
xSemaphoreTakeRecursive( RecursiveMutex, ( TickType_t ) 10 );
xSemaphoreTakeRecursive( RecursiveMutex, ( TickType_t ) 10 );
//任务获取了三次递归互斥信号量,所以就得释放三次!
xSemaphoreGiveRecursive( RecursiveMutex);
xSemaphoreGiveRecursive( RecursiveMutex);
xSemaphoreGiveRecursive( RecursiveMutex);
//递归互斥信号量释放完成,可以被其他任务获取了
}
else
{
/**********递归互斥信号量获取失败***********/
}
}
}
第十五章FreeRTOS 软件定时器
定时器可以说是每个MCU 都有的外设,有的MCU 其定时器功能异常强大,比如提供PWM、
输入捕获等功能。但是最常用的还是定时器最基础的功能——定时,通过定时器来完成需要周
期性处理的事务。MCU 自带的定时器属于硬件定时器,不同的MCU 其硬件定时器数量不同,
因为要考虑成本的问题。FreeRTOS 也提供了定时器功能,不过是软件定时器,软件定时器的精
度肯定没有硬件定时器那么高,但是对于普通的精度要求不高的周期性处理的任务来说够了。
当MCU 的硬件定时器不够的时候就可以考虑使用FreeRTOS 的软件定时器,本章就来学习一
下FreeRTOS 的软件定时器,本章分为如下几部分:
15.1 软件定时器简介
15.2 定时器服务/Daemon 任务
15.3 单次定时器和周期定时器
15.4 复位软件定时器
15.5 创建软件定时器
15.6 开启软件定时器
15.7 停止软件定时器
15.8 软件定时器实验
15.1 软件定时器简介
1、软件定时器概述
软件定时器允许设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器
调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期,
简而言之,当定时器的定时周期到了以后就会执行回调函数。
2、编写回调函数的注意事项
软件定时器的回调函数是在定时器服务任务中执行的,所以一定不能在回调函数中调用任
何会阻塞任务的API 函数!比如,定时器回调函数中千万不能调用vTaskDelay() 、
vTaskDelayUnti(),还有一些访问队列或者信号量的非零阻塞时间的API 函数也不能调用。
15.2 定时器服务/Daemon 任务
15.2.1 定时器服务任务与队列
定时器是一个可选的、不属于FreeRTOS 内核的功能,它是由定时器服务(或Daemon)任务
来提供的。FreeRTOS 提供了很多定时器有关的API 函数,这些API 函数大多都使用FreeRTOS
的队列发送命令给定时器服务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给
FreeRTOS 的软件定时器使用的,用户不能直接访问!图15.2.1 描述了这个过程:
图15.2.1 左侧部分属于用户应用程序的一部分,并且会在某个用户创建的用户任务中调用。
图中右侧部分是定时器服务任务的任务函数,定时器命令队列将用户应用任务和定时器服务任
务连接在一起。在这个例子中,应用程序调用了函数xTimerReset(),结果就是复位命令会被发
送到定时器命令队列中,定时器服务任务会处理这个命令。应用程序是通过函数xTimerReset()
间接的向定时器命令队列发送了复位命令,并不是直接调用类似xQueueSend()这样的队列操作
函数发送的。
15.2.2 定时器相关配置
上一小节我们知道了软件定时器有一个定时器服务任务和定时器命令队列,这两个东西肯
定是要配置的,配置方法和我们前面讲解的FreeRTOSCofig.h 一样,而且相关的配置也是放到
文件FreeRTOSConfig.h 中的,涉及到的配置如下:
1、configUSE_TIMERS
如果要使用软件定时器的话宏configUSE_TIMERS 一定要设置为1,当设置为1 的话定时
器服务任务就会在启动FreeRTOS 调度器的时候自动创建。
2、configTIMER_TASK_PRIORITY
设置软件定时器服务任务的任务优先级,可以为0~( configMAX_PRIORITIES-1)。优先级
一定要根据实际的应用要求来设置。如果定时器服务任务的优先级设置的高的话,定时器命令
队列中的命令和定时器回调函数就会及时的得到处理。
3、configTIMER_QUEUE_LENGTH
此宏用来设置定时器命令队列的队列长度。
4、configTIMER_TASK_STACK_DEPTH
此宏用来设置定时器服务任务的任务堆栈大小,单位为字,不是字节!,对于STM32 来说
一个字是4 字节。由于定时器服务任务中会执行定时器的回调函数,因此任务堆栈的大小一定
要根据定时器的回调函数来设置。
15.3 单次定时器和周期定时器
软件定时器分两种:单次定时器和周期定时器,单次定时器的话定时器回调函数就执行一
次,比如定时1s,当定时时间到了以后就会执行一次回调函数,然后定时器就会停止运行。对
于单次定时器我们可以再次手动重新启动(调用相应的API 函数即可),但是单次定时器不能自
动重启。相反的,周期定时器一旦启动以后就会在执行完回调函数以后自动的重新启动,这样
回调函数就会周期性的执行。图15.3.1 描述了单次定时器和周期定时器的不同:
图中Timer1 为单次定时器,定时器周期为100,Timer2 为周期定时器,定时器周期为200。
15.4 复位软件定时器
有时候我们可能会在定时器正在运行的时候需要复位软件定时器,复位软件定时器的话会重新计算定时周期到达的时间点,这个新的时间点是相对于复位定时器的那个时刻计算的,并
不是第一次启动软件定时器的那个时间点。图15.4.1 演示了这个过程,Timer1 是单次定时器,
定时周期是5s:
在图15.4.1 中我们展示了定时器复位过程,这是一个通过按键打开LCD 背光的例子,我们
假定当唤醒键被按下的时候应用程序打开LCD 背光,当LCD 背光点亮以后如果5s 之内唤醒键
没有再次按下就自动熄灭。如果在这5s 之内唤醒键被按下了,LCD 背光就从按下的这个时刻
起再亮5s。
FreeRTOS 提供了两个API 函数来完成软件定时器的复位,如表15.4.1 所示:
1、函数xTimerReset()
复位一个软件定时器,此函数只能用在任务中,不能用于中断服务函数!此函数是一个宏,
真正执行的是函数xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerReset( TimerHandle_t xTimer,
TickType_t xTicksToWait )
参数:
xTimer:要复位的软件定时器的句柄。
xTicksToWait:设置阻塞时间,调用函数xTimerReset ()开启软件定时器其实就是向定时器命
令队列发送一条tmrCOMMAND_RESET 命令,既然是向队列发送消息,那
肯定会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
2、函数xTimerResetFromISR()
此函数是xTimerReset()的中断版本,此函数用于中断服务函数中!此函数是一个宏,真正
执行的是函数xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTimer:要复位的软件定时器的句柄。
pxHigherPriorityTaskWoken:记退出此函数以后是否进行任务切换,这个变量的值函数会自
动设置的,用户不用进行设置,用户只需要提供一个变量来保
存这个值就行了。当此值为pdTRUE 的时候在退出中断服务函
数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器复位成功,其实就是命令发送成功。
pdFAIL: 软件定时器复位失败,命令发送失败。
15.5 创建软件定时器
使用软件定时器之前要先创建软件定时器,软件定时器创建函数如表15.5.1 所示:
1、函数xTiemrCreate()
此函数用于创建一个软件定时器,所需要的内存通过动态内存管理方法分配。新创建的软
件定时器处于休眠状态,也就是未运行的。函数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 :软件定时器的定时器周期,单位是时钟节拍数。可以借助portTICK_PERIOD_MS 将ms 单位转换为时钟节拍数。举个例子,定
时器的周期为100 个时钟节拍的话,那么xTimerPeriodInTicks 就为
100,当定时器周期为500ms 的时候xTimerPeriodInTicks 就可以设置
为(500/ portTICK_PERIOD_MS)。
uxAutoReload:设置定时器模式,单次定时器还是周期定时器?当此参数为pdTRUE
的时候表示创建的是周期定时器。如果为pdFALSE 的话表示创建的
是单次定时器。
pvTimerID:定时器ID 号,一般情况下每个定时器都有一个回调函数,当定时器定
时周期到了以后就会执行这个回调函数。但是FreeRTOS 也支持多个
定时器共用同一个回调函数,在回调函数中根据定时器的ID 号来处
理不同的定时器。
pxCallbackFunction:定时器回调函数,当定时器定时周期到了以后就会调用这个函数。
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
2、函数xTimerCreateStatic()
此函数用于创建一个软件定时器,所需要的内存需要用户自行分配。新创建的软件定时器
处于休眠状态,也就是未运行的。函数xTimerStart()、xTimerReset()、xTimerStartFromISR()、
xTimerResetFromISR()、xTimerChangePeriod()和xTimerChangePeriodFromISR()可以使新创建的
定时器进入活动状态,此函数的原型如下:
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t * pxTimerBuffer )
参数:
pcTimerName:软件定时器名字,名字是一串字符串,用于调试使用。
xTimerPeriodInTicks :软件定时器的定时器周期,单位是时钟节拍数。可以借助
portTICK_PERIOD_MS 将ms 单位转换为时钟节拍数。举个例子,定
时器的周期为100 个时钟节拍的话,那么xTimerPeriodInTicks 就为
100,当定时器周期为500ms 的时候xTimerPeriodInTicks 就可以设置
为(500/ portTICK_PERIOD_MS)。
uxAutoReload:设置定时器模式,单次定时器还是周期定时器?当此参数为pdTRUE
的时候表示创建的是周期定时器。如果为pdFALSE 的话表示创建的
是单次定时器。
pvTimerID:定时器ID 号,一般情况下每个定时器都有一个回调函数,当定时器定
时周期到了以后就会执行这个回调函数。当时FreeRTOS 也支持多个
定时器共用同一个回调函数,在回调函数中根据定时器的ID 号来处
理不同的定时器。
pxCallbackFunction:定时器回调函数,当定时器定时周期到了以后就会调用这个函数。
pxTimerBuffer:参数指向一个StaticTimer_t 类型的变量,用来保存定时器结构体。
返回值:
NULL: 软件定时器创建失败。
其他值: 创建成功的软件定时器句柄。
15.6 开启软件定时器
如果软件定时器停止运行的话可以使用FreeRTOS 提供的两个开启函数来重新启动软件定
时器,这两个函数表15.6.1 所示:
1、函数xTimerStart()
启动软件定时器,函数xTimerStartFromISR()是这个函数的中断版本,可以用在中断服务函
数中。如果软件定时器没有运行的话调用函数xTimerStart()就会计算定时器到期时间,如果软
件定时器正在运行的话调用函数xTimerStart()的结果和xTimerReset()一样。此函数是个宏,真
正执行的是函数xTimerGenericCommand,函数原型如下:
BaseType_t xTimerStart( TimerHandle_t xTimer,
TickType_t xTicksToWait )
参数:
xTimer:要开启的软件定时器的句柄。
xTicksToWait:设置阻塞时间,调用函数xTimerStart()开启软件定时器其实就是向定时器命令
队列发送一条tmrCOMMAND_START 命令,既然是向队列发送消息,那肯
定会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
2、函数xTimerStartFromISR()
此函数是函数xTimerStart()的中断版本,用在中断服务函数中,此函数是一个宏,真正执行
的是函数xTimerGenericCommand(),此函数原型如下:
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTimer:要开启的软件定时器的句柄。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值函数会自动设置的,用户不用进行设置,用户只需要提供一个变量来
保存这个值就行了。当此值为pdTRUE 的时候在退出中断服务
函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器开启成功,其实就是命令发送成功。
pdFAIL: 软件定时器开启失败,命令发送失败。
15.7 停止软件定时器
既然有开启软件定时器的API 函数,那么肯定也有停止软件定时器的函数,FreeRTOS 也
提供了两个用于停止软件定时器的API 函数,如表15.7.1 所示:
1、函数xTimerStop()
此函数用于停止一个软件定时器,此函数用于任务中,不能用在中断服务函数中!此函数
是一个宏,真正调用的是函数xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerStop ( TimerHandle_t xTimer,
TickType_t xTicksToWait )
参数:
xTimer:要停止的软件定时器的句柄。
xTicksToWait:设置阻塞时间,调用函数xTimerStop()停止软件定时器其实就是向定时器命令
队列发送一条tmrCOMMAND_STOP 命令,既然是向队列发送消息,那肯定
会涉及到入队阻塞时间的设置。
返回值:
pdPASS: 软件定时器停止成功,其实就是命令发送成功。
pdFAIL: 软件定时器停止失败,命令发送失败。
1、函数xTimerStopFromISR()
此函数是xTimerStop()的中断版本,此函数用于中断服务函数中!此函数是一个宏,真正执
行的是函数xTimerGenericCommand(),函数原型如下:
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTimer:要停止的软件定时器句柄。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值函数会
自动设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为pdTRUE 的时候在退出中断服务
函数之前一定要进行一次任务切换。
返回值:
pdPASS: 软件定时器停止成功,其实就是命令发送成功。
pdFAIL: 软件定时器停止失败,命令发送失败。
15.8 软件定时器实验
15.8.1 实验程序设计
1、实验目的
学习FreeROTS 软件定时器的使用,包括软件定时器的创建、开启和停止。
2、实验设计
本实验设计两个任务:start_task 和timercontrol_task 这两个任务的任务功能如下:
start_task:用来创建任务timercontrol_task 和两个软件定时器。
timercontrol_task:控制两个软件定时器的开启和停止。
实验中还创建了两个软件定时器:AutoReloadTimer_Handle 和OneShotTimer_Handle,这两
个定时器分别为周期定时器和单次定时器。定时器AutoReloadTimer_Handle 的定时器周期为
1000 个时钟节拍(1s),定时器OneShotTimer_Handle 的定时器周期为2000 个时钟节拍(2s)。
3、实验工程
FreeRTOS 实验15-1 FreeRTOS 软件定时器实验。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TIMERCONTROL_TASK_PRIO 2 //任务优先级
#define TIMERCONTROL_STK_SIZE 256 //任务堆栈大小
TaskHandle_t TimerControlTask_Handler; //任务句柄
void timercontrol_task(void *pvParameters); //任务函数
TimerHandle_t AutoReloadTimer_Handle; //周期定时器句柄
TimerHandle_t OneShotTimer_Handle; //单次定时器句柄
void AutoReloadCallback(TimerHandle_t xTimer); //周期定时器回调函数
void OneShotCallback(TimerHandle_t xTimer); //单次定时器回调函数
//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,
GRED, GBLUE, RED, MAGENTA,
GREEN, CYAN, YELLOW, BROWN,
BRRED, GRAY };
●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 15-1");
LCD_ShowString(30,50,200,16,16,"KEY_UP:Start Tmr1");
LCD_ShowString(30,70,200,16,16,"KEY0:Start Tmr2");
LCD_ShowString(30,90,200,16,16,"KEY1:Stop Tmr1 and Tmr2");
LCD_DrawLine(0,108,239,108); //画线
LCD_DrawLine(119,108,119,319) ; //画线
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,115,314); //画一个矩形
LCD_DrawLine(5,130,115,130); //画线
LCD_DrawRectangle(125,110,234,314); //画一个矩形
LCD_DrawLine(125,130,234,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(6,111,110,16,16,"AutoTim:000");
LCD_ShowString(126,111,110,16,16,"OneTim: 000");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建软件周期定时器, 周期定时器,周期1s(1000 个时钟节拍),周期模式
AutoReloadTimer_Handle=xTimerCreate((const char* )"AutoReloadTimer", (1)
(TickType_t )1000,
(UBaseType_t )pdTRUE,
(void* )1,
(TimerCallbackFunction_t)AutoReloadCallback);
//创建单次定时器, 单次定时器,周期2s(2000 个时钟节拍),单次模式
OneShotTimer_Handle=xTimerCreate((const char* )"OneShotTimer", (2)
(TickType_t )2000,
(UBaseType_t )pdFALSE,
(void* )2,
(TimerCallbackFunction_t)OneShotCallback);
//创建TASK1 任务
xTaskCreate((TaskFunction_t )timercontrol_task,
(const char* )"timercontrol_task",
(uint16_t )TIMERCONTROL_STK_SIZE,
(void* )NULL,
(UBaseType_t )TIMERCONTROL_TASK_PRIO,
(TaskHandle_t* )&TimerControlTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//TimerControl 的任务函数
void timercontrol_task(void *p_arg)
{
u8 key,num;
while(1)
{
//只有两个定时器都创建成功了才能对其进行操作
if((AutoReloadTimer_Handle!=NULL)&&(OneShotTimer_Handle!=NULL))
{
key = KEY_Scan(0);
switch(key)
{
case WKUP_PRES: //当key_up 按下的话打开周期定时器
xTimerStart(AutoReloadTimer_Handle,0); //开启周期定时器(3)
printf("开启定时器1\r\n");
break;
case KEY0_PRES: //当key0 按下的话打开单次定时器
xTimerStart(OneShotTimer_Handle,0); //开启单次定时器(4)
printf("开启定时器2\r\n");
break;
case KEY1_PRES: //当key1 按下话就关闭定时器
xTimerStop(AutoReloadTimer_Handle,0); //关闭周期定时器(5)
xTimerStop(OneShotTimer_Handle,0); //关闭单次定时器
printf("关闭定时器1 和2\r\n");
break;
}
}
num++;
if(num==50) //每500msLED0 闪烁一次
{
num=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10 个时钟节拍
}
}
(1)、调用函数xTimerCreate()创建定时器AutoReloadTimer_Handle,这是一个周期定时器,
定时周期是1000 个时钟节拍,在本例程中就是1s。
(2)、调用函数xTimerCreate()创建定时器OneShotTimer_Handle,这是一个单次定时器,定
时器周期为2000 个时钟节拍,在本例程中就是2s。
(3)、当KEY_UP 按下以后调用函数xTimerStart()开启周期定时器AutoReloadTimer_Handle。
(4)、当KEY0 按下以后调用函数xTimerStart()开启单次定时器OneShotTimer_Handle。
(5)、当KEY1 按下以后就调用函数xTimerStop()同时关闭定时器AutoReloadTimer_Handle
和OneShotTimer_Handle。
●定时器回调函数
//周期定时器的回调函数
void AutoReloadCallback(TimerHandle_t xTimer)
{
static u8 tmr1_num=0;
tmr1_num++; //周期定时器执行次数加1
LCD_ShowxNum(70,111,tmr1_num,3,16,0x80); //显示周期定时器的执行次数
LCD_Fill(6,131,114,313,lcd_discolor[tmr1_num%14]); //填充区域
}
//单次定时器的回调函数
void OneShotCallback(TimerHandle_t xTimer)
{
static u8 tmr2_num = 0;
tmr2_num++; //周期定时器执行次数加1
LCD_ShowxNum(190,111,tmr2_num,3,16,0x80); //显示单次定时器执行次数
LCD_Fill(126,131,233,313,lcd_discolor[tmr2_num%14]); //填充区域
LED1=!LED1;
printf("定时器2 运行结束\r\n");
}
15.8.2 程序运行结果分析
编译并下载实验代码到开发板中,默认情况下LCD 显示如图15.8.2.1 所示。
当按下KEY0 键以后定时器OneShotTimer_Handle 开始运行,当定时器计时时间到了以后
就会调用函数OneShotCallback(),屏幕右侧区域的背景色会被刷新为其他颜色,背景颜色刷新
完成以后定时器OneShotTimer_Handle 就会停止运行。当按下KEY_UP 键的话定时器
AutoReloadTimer_Handle 就会开始运行,定时器周期到了以后屏幕左侧区域的背景色会被刷新
为其他颜色。由于定时器AutoReloadTimer_Handle 是周期定时器,所以不会停止运行,除非按
下KEY1 键同时关闭定时器AutoReloadTimer_Handle 和OneShotTimer_Handle。