在前后台架构中,我们使用了全局变量,作为前台ISR和后台main之间的共享数据。
freertos中,可以使用更复杂的data share,那就是list。
对于一个需要数据看板和数据处理的应用场景,多任务之间,可以通过共享list,实现数据处理。
freertos提供了list相关的增删改查的API。
最常见的,就是data board类的应用。
多个任务,均可以在数据报表中增删改查,完成各个任务自己的业务。
++++++++++++++++++++++++++++++++++++
list,是最基础的共享数据的方式。
更常用的IPC,是队列和信号量。
IPC的一个基本特征,就是blocking。
任何尝试对内核通信对象进行读写的任务,都可能被OS阻塞。
+++++++++++++++++++++++++++++++++++
先来看看队列。
队列,是list的更高级形式。
队列是全局的,任何任务都可以向队列中填充消息,任何任务也都可以从队列中取走消息。
不同的是,队列中填充消息和取走消息,是有顺序的,可以是FIFO,也可以是LIFO,但是不能从任意位置填充或者取走消息。
对于FIFO,就是双端口的,一个端口IN用来填充,一个端口OUT用来取走。
对于LIFO,就是单端口的,端口TOP用来写或者用来读。
freertos为队列,提供了三种阻塞方式。通过blocking time来设置。
1)non-blocking,设置time为0,不阻塞,如果数据无效,则直接返回pdFalse。
2)blocking before timeout,设置为大于0,但是小于port_MAX_DELAY的值,可以被阻塞n个时间片,然后返回,如果在超时时间内,数据有效,则立即解除阻塞,并返回,如果在超时时,仍然没有数据有效,则直接返回pdFalse。
3)blocking until valid,设置为port_MAX_DELAY,如果数据无效,则一直阻塞,直到数据有效。
队列是一个全局变量,采用的是拷贝方式,而不是引用方式,所以,当任务向队列中填充消息时,会分配内存,用来存放这个消息。即使消息的源数据不再存在,也不影响其他任务对消息的读取。
freertos提供了两种队列消息的读取操作。
receive和peek,取出和探取。
区别在于,
如果使用取出方式,那么,消息被拷贝到用户指定的buffer中之后,队列中将不再保存这条消息,消息所占的内存,被释放。
如果使用探取方式,那么,消息被拷贝到用户指定的buffer中之后,队列中仍然保存有这条消息,就好像没有任务取走这个消息一样。
++++++++++++++++++++++++++++++++++
来看看task1_task。
void task1_task(void *pvParameters)
{
u8 num,key;
for(;;)
{
if(xQueueReceive(Key_Queue,&key,portMAX_DELAY))//请求消息Key_Queue
{
switch(key)
{
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个时钟节拍
}
}
从队列中取出的消息,被拷贝到指定的buffer中。这里,我们的buffer就是本任务的栈变量Key。
这个任务,是一个后台任务,更细化的角色划分,它是一个产出任务。
后台任务,是消息消费者。
它从队列中,获取前台任务发送的消息,根据消息,安排本任务需要完成的操作,并产生成果。
++++++++++++++++++++++++++++++++++++++
来看看key_task。
void key_task(void *pvParameters)
{
u8 key,i=0;
BaseType_t err;
for(;;)
{
key=KEY_Scan(0); //扫描按键
if(key) //消息队列Key_Queue创建成功,并且按键被按下
{
err=xQueueSend(Key_Queue,&key,10);
if(err==errQUEUE_FULL) //发送按键值
{
printf("队列Key_Queue已满,数据发送失败!\r\n");
}
}
i++;
if(i==50)
{
i=0;
LED0=!LED0;
}
if(i%10==0) check_msg_queue();//检Message_Queue队列的容量
vTaskDelay(10); //延时10ms,也就是10个时钟节拍
}
}
向队列发送消息,OS为新拷贝的消息分配内存,并从指定的buffer中,拷贝到队列中。
这里,我们的buffer,就是本任务的栈变量key。
这个任务,是一个前台任务,更细化的角色划分,它是一个输入任务。
前台任务,是消息生产者。
它扫描按键,并根据按键的状态,生成消息,并发送到队列中。
++++++++++++++++++++++++++++++++++++++++++
二值信号量,是基于队列实现的,可以看成是队列的一种精简版。是队列长度为1的队列。
其操作思想,就是前后台架构,打标清标的使用方式。
give函数,就是填充消息到队列中,take函数,就是从队列中取走消息。
+++++++++++++++++++++++++++++++++++++++
来看看task1_task。
void task1_task(void *pvParameters)
{
for(;;)
{
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;
for(;;)
{
err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取信号量
len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度
CommandStr=mymalloc(SRAMIN,len+1); //申请内存
sprintf((char*)CommandStr,"%s",USART_RX_BUF);
CommandStr[len]='\0'; //加上字符串结尾符号
LowerToCap(CommandStr,len); //将字符串转换为大写
CommandValue=CommandProcess(CommandStr); //命令解析
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) //处理命令
{
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); //释放内存
}
从队列中取出的消息,不需要拷贝到buffer中,直接丢弃,所以,take函数,不需要指定buffer,
这个任务,是一个后台任务,更细化的角色划分,它是一个产出任务。
后台任务,是消息消费者。
它从队列中,获取前台任务发送的消息,根据消息,安排本任务需要完成的操作,并产生成果。
+++++++++++++++++++++++++++++++++++++++++
来看看usart1的IRQ_Handler。
extern SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
BaseType_t xHigherPriorityTaskWoken;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
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)//接收到数据,并且二值信号量有效
{
xSemaphoreGiveFromISR(BinarySemaphore,&xHigherPriorityTaskWoken); //释放二值信号量
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//如果需要的话进行一次任务切换
}
}
在ISR中,完成IPC。
使用fromisr版本的API,发出二值信号量。
由于是在ISR中调用的API,所以API返回后,需要配套调用一次portYIELD_FROM_ISR,进行断点更新。
这里需要注意的是,当我们使用RPC相关的API时,需要提供一个buffer,让API写入抢占码。这里,这个buffer就是xHigherPriorityTaskWoken。
然后,portYIELD_FROM_ISR这个API,会根据抢占码完成合适的断点更新。
+++++++++++++++++++++++++++++++++++++++
计数信号量,也是基于队列实现的,是队列长度大于1的队列。
其操作思想,就是前后台架构,对标志变量加一减一的使用方式。
give函数,就是填充消息到队列中,take函数,就是从队列中取走消息。
计数信号量,通常用于两种场合。
1)事件计数。
每次事件发生时,就发出一次计数信号量。
这种场合下,计数信号量的初始计数值是0。
2)资源管理。
这种场合下,计数信号量代表当前可用的有效的资源数量。
此时,计数信号量的初始计数值是资源总量。
+++++++++++++++++++++++++++++++++++++
来看看semgive_task。
void SemGive_task(void *pvParameters)
{
u8 key,i=0;
u8 semavalue;
BaseType_t err;
for(;;)
{
key=KEY_Scan(0); //扫描按键
switch(key)
{
case WKUP_PRES:
err=xSemaphoreGive(CountSemaphore);//释放计数型信号量
if(err==pdFALSE)
{
printf("信号量释放失败!!!\r\n");
}
semavalue=uxSemaphoreGetCount(CountSemaphore); //获取计数型信号量值
LCD_ShowxNum(155,111,semavalue,3,16,0); //显示信号量值
break;
default:
break;
}
i++;
if(i==50)
{
i=0;
LED0=!LED0;
}
vTaskDelay(10); //延时10ms,也就是10个时钟节拍
}
}
这是一个前台任务,输入任务,当扫描到有效按键的输入时,发出一个计数信号量。
前台任务,是消息生产者。
++++++++++++++++++++++++++++++++++++++++++
来看看semtake_task。
void SemTake_task(void *pvParameters)
{
u8 num;
u8 semavalue;
for(;;)
{
xSemaphoreTake(CountSemaphore,portMAX_DELAY); //等待数值信号量
num++;
semavalue=uxSemaphoreGetCount(CountSemaphore); //获取数值信号量值
LCD_ShowxNum(155,111,semavalue,3,16,0); //显示信号量值
LCD_Fill(6,131,233,313,lcd_discolor[num%14]); //刷屏
LED1=!LED1;
vTaskDelay(1000); //延时1s,也就是1000个时钟节拍
}
}
这个任务,是一个后台任务,更细化的角色划分,它是一个产出任务。
后台任务,是消息消费者。
它从队列中,获取前台任务发送的消息,根据消息,安排本任务需要完成的操作,并产生成果。
从队列中取出的消息,不需要拷贝到buffer中,直接丢弃,所以,take函数,不需要指定buffer,
++++++++++++++++++++++++++++++++++++
二值信号量,适合使用于进程同步的应用场合。但是并不适合使用在进程互斥的应用场合。
使用二值信号量,在RTOS中,可能会出现三国杀现象。
老三占用了二值信号量,
老大被阻塞在信号量上,
此时,如果老二抢占了老三的CPU,导致老三被阻塞,
这样,老大就很憋屈了,
没有卡在优先级上,却卡在了全局资源上。
为了解决这个问题,在RTOS中,推荐使用互斥量,而不是二值信号量。
互斥信号量,是基于计数信号量实现的,可以理解为一个计数初值为1的计数信号量,但是,具备了优先级继承特性。
当一个互斥信号量被一个低优先级任务占用时,
此时如果一个高优先级任务申请互斥量,发现被占用,那么,OS会暂时将低优先级的任务的优先级提升到被阻塞的高优先级一样的级别。这样,可以防止中等优先级的任务钻空子。
优先级继承,只能尽量消除影响,不能完全杜绝,
最佳做法是,在系统设计之初,任务划分时,就设计好角色和分工,以及IPC方式。
由于互斥量具有优先级翻转的特性,所以,互斥量是不能用于ISR中的。
++++++++++++++++++++++++++++++++++++
来看看high_task。
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");
for(;;)
{
num++;
printf("high task Pend Sem\r\n");
xSemaphoreTake(MutexSemaphore,portMAX_DELAY); //获取互斥信号量
printf("high task Running!\r\n");
LCD_Fill(6,131,114,313,lcd_discolor[num%14]); //填充区域
LED1=!LED1;
xSemaphoreGive(MutexSemaphore); //释放信号量
vTaskDelay(500); //延时500ms,也就是500个时钟节拍
}
}
在进入互斥区时,成对使用xSemaphoreTake和xSemaphoreGive。
先take,用完之后,再give。
++++++++++++++++++++++++++++++++++++++
来看看middle_task。
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");
for(;;)
{
num++;
printf("middle task Running!\r\n");
LCD_Fill(126,131,233,313,lcd_discolor[13-num%14]); //填充区域
LED0=!LED0;
vTaskDelay(1000); //延时1s,也就是1000个时钟节拍
}
}
这是一个普通的后台任务,不和其他的进程发生通信。
++++++++++++++++++++++++++++++++++++++++
来看看low_task。
void low_task(void *pvParameters)
{
static u32 times;
for(;;)
{
xSemaphoreTake(MutexSemaphore,portMAX_DELAY); //获取互斥信号量
printf("low task Running!\r\n");
for(times=0;times<20000000;times++) //模拟低优先级任务占用互斥信号量
{
taskYIELD(); //发起任务调度
}
xSemaphoreGive(MutexSemaphore); //释放互斥信号量
vTaskDelay(1000); //延时1s,也就是1000个时钟节拍
}
}
在进入互斥区时,成对使用xSemaphoreTake和xSemaphoreGive。
先take,用完之后,再give。
在这个任务中,在占用了互斥量的前提下,调用API,发起了任务调度,让出CPU,
由于优先级继承,老三此时和老大一样的优先级,老二无法抢CPU,不会出现三国杀现象。
++++++++++++++++++++++++++++++++++++++++++
递归互斥量,是为了解决任务自杀问题。
如果一个任务,在互斥区,再次take同一个互斥量,此时,任务就会被阻塞,这样,任务不能释放互斥量,其他任务也不能获取互斥量。
递归互斥量可以避免这个问题。
在互斥区,如果再次take同一个互斥量,任务不会被阻塞,OS会认为获取信号量成功。
OS首先会检查队列,如果发现队列为空,然后,OS会检查互斥量是否是被任务自己占用,如果是,则递归互斥量的计数加一,如果不是,则OS认为是不同的任务在申请互斥量,则会阻塞任务。
+++++++++++++++++++++++++++++++++++++++++