在之前的示例程序中,经常要通过串口发送信息,当多个任务同时访问串口时,就会发生资源冲突,造成数据混乱。之前的做法,要么限制只有一个任务能够运行,要么在访问串口时用临界段代码保护或挂起调度器的方式进行代码保护。这种解决多个任务同时访问某个资源的方法叫作互斥访问,相关内容将在后面的章节中详细介绍。
1 守护任务
守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的资源,其他任务要访问该资源只能间接地通过守护任务提供的服务实现。守护任务提供了一种干净利落的方法用来实现互斥功能,不用担心会发生优先级反转和死锁问题。
2 串口守护任务示例
本示例对延时函数使用示例进行改写,将原来两个任务通过临界段代码保护实现串口输出改写成通过串口守护任务输出。
本示例通过appStartTask()函数创建3个具有相同优先级的FreeRTOS任务,均运行于优先级3,抢占式调度和时间片调度开启。
任务1的任务函数为Led0Task(),其功能是使LED0闪烁,统计任务1的运行次数,并将任务1的运行总节拍数通过队列传给串口守护任务打印。
任务2的任务函数为Led1Task(),其功能是使LED1闪烁,统计任务2的运行次数,并将任务2的运行总节拍数通过队列传给串口守护任务打印。任务3是串口守护任务,任务函数为printfTask(),其功能是通过队列传送过来的字符信息在串口上输出,任何时候只有该守护任务能访问串口。
2.1 修改appTask.h
添加队列实现头文件,声明串口守护任务函数
#ifndef __APPTASK_H
#define __APPTASK_H
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
static void Led0Task(void *pvParameters);
static void Led1Task(void *pvParameters);
static void printTask(void *pvParameters);
void appStartTask(void);
#endif
2.2 任务函数
static char pcToPrint[80]; //待打印内容缓冲区
xQueueHandle xQueuePrint; //消息队列句柄
/**********************************************************************
函 数 名:Led0Task
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
static void Led0Task(void *pvParameters)
{
uint16_t cnt=0;
TickType_t xFirstTime; //保存延时前的系统节拍数值
while(1)
{
xFirstTime = xTaskGetTickCount(); //获取任务进入点的系统时钟节拍
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_4))); //LED0闪烁
my_delay_ms(200);
vTaskDelay(pdMS_TO_TICKS(500));//延时500ms
cnt = xTaskGetTickCount() - xFirstTime ;
//生成待打印出信息
sprintf(pcToPrint,"任务1:LED0闪烁,任务1运行的节拍数为: %3d 次 \r\n",cnt);
//打印信息,所有发送串口的信息不能直接输出,通过队列发送给串口守护任务
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
}
/**********************************************************************
函 数 名:Led1Task
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
static void Led1Task(void *pvParameters)
{
uint16_t cnt=0;
TickType_t xFirstTime; //保存延时前的系统节拍数值
TickType_t xNextTime; //保存解除阻塞系统时钟的节拍值
while(1)
{
xFirstTime = xTaskGetTickCount(); //获取任务进入点的系统时钟节拍
xNextTime = xFirstTime ; //获取任务进入点的系统时钟节拍
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_5))); //LED1闪烁
my_delay_ms(200);
vTaskDelayUntil(&xNextTime,pdMS_TO_TICKS(500));//绝对延时500
cnt = xTaskGetTickCount() - xFirstTime;
//生成待打印出信息
sprintf(pcToPrint,"任务2:LED1闪烁,任务2运行的节拍数为: %3d 次 \r\n",cnt);
//打印信息,所有发送串口的信息不能直接输出,通过队列发送给串口守护任务
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
}
/**********************************************************************
函 数 名:printTask
功能说明:串口守护任务使用了一个FreeRTOS队列来对串口实现串行化访问,该守护任务是唯一能够直接访问串口的任务。
串口守护任务大部分时间都在阻塞态等待队列中有消息到来,当一个消息到达时,
串口守护任务仅简单地将接收到的消息发送到串口上,然后又返回阻塞态,继续等待下一条消息的到来。
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
void printTask(void *pvParameters)
{
char pcTowrite[80]; //缓存从队列接受到的数据
while(1)
{
/*当队列为空,即没有字符需要输出时间,阻塞超时时间为portMAX_DELAY,任务将进入无期限等待
状态,可以不检测队列读取函数的返回值*/
xQueueReceive(xQueuePrint,pcTowrite,portMAX_DELAY);
printf("%s",pcTowrite);
}
}
2.3 任务创建
/**********************************************************************
函 数 名:appStartTask
功能说明:任务开始函数,用于创建其他函数并且开启调度器
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
**********************************************************************/
void appStartTask(void)
{
/*创建一个长度为2,队列项大小足够容纳待输出字符的队列*/
xQueuePrint = xQueueCreate(2,sizeof(pcToPrint));
if(xQueuePrint != NULL)//如果队列创建成功
{
taskENTER_CRITICAL(); /*进入临界段,关中断*/
xTaskCreate(Led0Task,"Led0Task",128,NULL,3,&Led0TaskHandle);
xTaskCreate(Led1Task,"Led1Task",128,NULL,3,&Led1TaskHandle);
xTaskCreate(printTask,"printTask",128,NULL,3,&printTaskHandle);
taskEXIT_CRITICAL(); /*退出临界段,关中断*/
vTaskStartScheduler();/*开启调度器*/
}
}
2.4 下载测试
串口输出信息准确无误。
3 总结
队列是一种特殊的数据结构,可以保存有限个具有长度的数据单元,一般采用先进先出的存取方式。FreeRTOS利用队列实现任务间的通信,消息传递,后面将要介绍的信号量也是队列来实现的。