任务间通信
目录
任务间通信
回顾
-- WiFi模块:1、所有和服务器相关的操作,2、可以实现局域网通信
-- 操作系统(Freertos):
FreeRTOS之任务间通信
消息队列
信号量
更改接收数据方式
互斥量
回顾
-- 我们要学的是什么?
-- 首先要了解模块的基本作用,第二知道模块更进一步的应用,或者说知道这个模块的应用场景
-- 要学会理代码思路
-- 上一周讲的是wifi和操作系统(Freertos)
-- WiFi模块:1、所有和服务器相关的操作,2、可以实现局域网通信
-- wifi是咱们的联网模块(esp8266,4G,NBIOT模块等都可以实现联网)
-- 大家要记忆的是首先要知道服务器怎么操作?
-- 怎么连接阿里云?(阿里云是支持mqtt协议的服务器)
- 1、连接阿里云服务器
- 2、发送mqtt连接报文,连接物联网平台
- 3、订阅或者发布数据
-- 阿里云也可以获取实时时间,阿里云上有一个NTP的主题,订阅之后,发布请求,就可以获得实时时间
-- 其他的服务器支持的协议可能也会不同(mqtt、http、tcp/ip。。。)
-- 目前手机APP是咱们借助阿里云平台自己开发的,到公司的时候有专门人做APP,那么咱们所作的工作就是将数据上传云平台
-- 如何保证数据每次都能上传成功?
-
mqtt协议上有一个发布确认报文,阿里云平台上有一个主题叫做发布响应主题
-
当数据上传成功之后,阿里云平台会向这个主题发布一个响应报文,咱们只要订阅这个主题,就可以知道数据是否上传成功
-- 操作系统(Freertos):
-- 主要做的是操作系统的移植和简单应用
-- 加入操作系统之后,逻辑变复杂了,但是出去工作99%都要加操作系统
-- 操作系统有很多,比如FREERTOS,UCOS,RTTHREAD、LINUX等。。。
-- 每一种操作系统,使用起来都会有所不同,但是操作系统的核心思想都是一致的
-- 那什么是操作系统的核心思想?是任务管理
FreeRTOS之任务间通信
-- 参考文档
-- 这个文档从17章开始之后就是任务间通信
消息队列
-- 核心用途:消息传递
-- 裸机中传递消息的是数组
-- 消息队列就是来传递消息的
-- 那么数组就可以传递消息,为什么要引入消息队列呢?
-- 消息队列的特性
-- 数组中的消息会被覆盖
-- 消息队列的三种阻塞方式
-- 消息队列的核心控制有四个函数,基本上每一个任务间通信都有这四个函数
-- 可以说任务间通信就是学这四个函数,一般情况下,创建和删除是固定的,实际上是学不同通信的读写
-- 下面依旧是找到事例复制到工程中
-- 发送和接收函数
-- 在工程中编写代码
-- 这里将发送写在按键任务中
#include "freertos.h"
#include "task.h"
#include "queue.h"
QueueHandle_t Test_Queue =NULL;
//例如我们创建一个按键的任务
//第一步
TaskHandle_t keyTaskCreat_Handle = NULL;//任务句柄
//2
void KETTaskCreat(void *pvParameters)//函数名称不固定,参数格式是固定的(任务一定是一个while循环,每个任务一定要有自己的任务周期)//设置任务周期的目的就是可以让任务可以主动释放cpu(指的是任务能够自动释放cpu)
{
uint8_t keyflag = 0;
uint8_t buff[10]={0};
BaseType_t xReturn = pdPASS;
while(1)
{
keyflag = get_key();
switch(keyflag)
{
case 1:
// vTaskDelete( NULL );//删除自身
//vTaskDelete(Led1TaskCreat_Handle);//删除led的任务
printf("发送消息 send_data1!\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
"123456qwe",0);/* 发送的消息内容 */
/* 等待时间 0 */
if (pdPASS == xReturn)
printf("消息 send_data1 发送成功!\n\n");
break;
case 2:
xReturn = xQueueReceive( Test_Queue,
/* 消息队列的句柄 */
buff,/* 接收的消息内容 */
10000); //超时时间(等待10s)/* 等待时间 一直等 */
if (pdTRUE== xReturn)
printf("本次接收到的数据是:%s\r\n",buff);
else
printf("数据接收出错,错误代码: 0x%lx\r\n",xReturn);
break;
}
vTaskDelay(50); //在任务周期时可以释放cpu
}
}
int main()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
usart_init();
key_init();
/* 创建 Test_Queue */
Test_Queue = xQueueCreate(3,/* 消息队列的长度 */
10);/* 消息的大小 */
if (NULL != Test_Queue)
printf("创建 Test_Queue 消息队列成功!\r\n");
//Delay_nms(2000);
//KEY*******************************************************************************************************
BaseType_t xReturn1 = pdPASS;
xReturn1 = xTaskCreate(KETTaskCreat, //任务函数接口 函数名称 //任务主体函数名称(也叫做任务函数接口)
"key", //任务别名不能一样 //人为起的一个任务别名(不能超过16个字节)(不能太长)(相当于给任务起绰号)(每个任务不能相同)
200, //任务栈空间(字节) //单位是字节(一般在创建任务的时候空间尽量给大一些)
NULL, //任务传参 //任务传参,有参数的时候写,没参数写NULL
1, //任务优先级 那个任务重要,优先级高 //任务优先级,
&keyTaskCreat_Handle); //任务句柄
if(xReturn == pdPASS)
vTaskStartScheduler();//任务调度
}
-- 效果图
-- 注意,消息队列读完数据后,数据就没有了。
-- 执行的结果,如果先点发送数据,再点接收数据,就会比较快的接收到,如果先按按键2(也就是先接收数据的时候),消息队列里面没有数据,就会等待设置的超时时间后,再结束。
信号量
-- 信号量用于任务同步,资源保护。。。
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。 抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。 通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:
- 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。
- 正值,表示有一个或多个释放信号量操作。
-- 信号量本质就是一个数字(非负整数)
-- 在操作系统中,信号量是有分类的,
-- 信号量中有两种信号量是比较关键的,一个叫做二值信号量,一个叫做互斥信号量
-- 二值信号量更偏向于任务同步,互斥信号量更偏向于资源保护
-- 任务同步:
-- 什么叫做任务同步?
- 例如屏幕显示与数据获取,数据获取之后,立马就要在屏幕上显示
- 任务同步用于两个有关联的任务
-- 如果一个信号量只有0和1两个值,那么这个信号量就是二值信号量
-- 为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。
-- 二值信号量的应用场景?
-- 二值信号量在任务与中断同步的应用场景:我们在串口接收中,我们不知道啥 时候有数据发送过来,有一个任务是做接收这些数据处理,总不能在任务中每时每刻都在任务查询有没有数据到来,那样会浪费 CPU 资源,所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,任务就进入阻塞态,不参与任务的调度,等到数据到来了,释放一个二值信号量,任务就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。
-- 编写代码
-- 这里由于消息队列对于项目的用处没有那么大,可以尝试将消息队列裁剪掉,操作系统的空间就会更大些.将消息队列的宏定义由1变为0.这就叫做操作系统裁剪。
-- 这样操作系统就可以不需要那么多空间,所以分给操作系统的空间也可以适量降低
-- 编写二值信号量代码
-- 依旧是复制事例
-- 创建二值信号量(文档P261页)
-- 释放信号量应该放在采集DHt11的任务里,为什么呢?
-- 因为咱们希望的是采集完数据后,再在LCD中显示
-- 获取写在LCD显示任务里,因为咱们希望的是先采集数据,再显示数据
-- 如果不用二值信号量,获取一次DHT11数据的时间是1s,在LCD上刷新数据的时间是0.5s,所以至少有两次LCD上显示的数据是一样的,那是不是就浪费了cpu的资源,所以我们设置一个同步机制。采取到时间后,再去显示LCD上。
#include "freertos.h"
#include "semphr.h"
SemaphoreHandle_t xSemaphore = NULL;
TaskHandle_t DHTTaskCreat_Handle = NULL;
TaskHandle_t LCDTaskCreat_Handle = NULL;
void DHTTaskCreat(void *pvParameters)//函数名称不固定,参数格式是固定的(任务一定是一个while循环,每个任务一定要有自己的任务周期)//设置任务周期的目的就是可以让任务可以主动释放cpu(指的是任务能够自动释放cpu)
{
while(1)
{
get_dht11_val();
xSemaphoreGive( xSemaphore );//释放信号量
vTaskDelay(1000);
}
}
void LCDTaskCreat(void *pvParameters)//函数名称不固定,参数格式是固定的(任务一定是一个while循环,每个任务一定要有自己的任务周期)//设置任务周期的目的就是可以让任务可以主动释放cpu(指的是任务能够自动释放cpu)
{
BaseType_t xReturn = pdPASS;//有没有获取成功,再显示LCD
while(1)
{
xReturn = xSemaphoreTake(xSemaphore,/* 二值信号量句柄 */
500); /* 等待时间 */
if (pdTRUE == xReturn)//获取成功
{
printf("BinarySem_Handle 二值信号量获取成功!\n\n");
LCD_ShowPhoto(0,80,98,100,(uint8_t *)gImage[i++]);
if(i>=2)i=0;
sprintf(D_wen, "tem: %.2f℃",dht.tem);
sprintf(D_shi, "hum: %.2f%RH", dht.hum);
//printf("tem:%.2f℃\r\n",dht.tem);
LCD_ShowString(0,16,strlen(D_wen)*8,16,16,D_wen);
LCD_ShowString(0,32,120,16,16,D_shi);
sprintf(D_time, "%04d/%02d/%02d %02d:%02d:%02d",a.tm_year+1900,a.tm_mon+1,a.tm_mday,a.tm_hour+8,a.tm_min,a.tm_sec);
LCD_ShowString(0,48,180,16,16,D_time);
//vTaskDelay(500);
}
}
}
int main()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
dht11_init();
usart_init();;
LCD_Init();//屏幕初始化一定要写到串口初始化后面
//创建信号量**********************************************
/* 尝试创建一个信号量 */
LCD_DrawPoint(10,10);
LCD_ShowString(0,0,80,16,16,"123456qwer"); //宽度字符是汉字的一半,是8,汉字是16,8*10
Delay_nms(2000);
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore == NULL ) {
/* 内存不足,创建失败 */
} else {
printf("二值信号量创建成功\r\n");
}
BaseType_t xReturn = pdPASS;
//DHT11******************************************************************************************************
xReturn = xTaskCreate(DHTTaskCreat, //任务函数接口 函数名称 //任务主体函数名称(也叫做任务函数接口)
"dht", //任务别名不能一样 //人为起的一个任务别名(不能超过16个字节)(不能太长)(相当于给任务起绰号)(每个任务不能相同)
200, //任务栈空间(字节) //单位是字节(一般在创建任务的时候空间尽量给大一些)
NULL, //任务传参 //任务传参,有参数的时候写,没参数写NULL
2, //任务优先级 那个任务重要,优先级高 //任务优先级,
&DHTTaskCreat_Handle); //任务句柄
//LCD******************************************************************************************************
xReturn = xTaskCreate(LCDTaskCreat, //任务函数接口 函数名称 //任务主体函数名称(也叫做任务函数接口)
"lcd", //任务别名不能一样 //人为起的一个任务别名(不能超过16个字节)(不能太长)(相当于给任务起绰号)(每个任务不能相同)
512, //任务栈空间(字节) //单位是字节(一般在创建任务的时候空间尽量给大一些)
NULL, //任务传参 //任务传参,有参数的时候写,没参数写NULL
3, //任务优先级 那个任务重要,优先级高 //任务优先级,
&LCDTaskCreat_Handle); //任务句柄
if(xReturn == pdPASS)
vTaskStartScheduler();//任务调度器 //当程序执行完这句话之后,正常情况下下面的代码不会再运行,只会运行任务主题函数
}
更改接收数据方式
-- 将之前裸机的写法改为操作系统
-- 这里是中断接收数据完成后,再去判断数据,所以先要判断数据是否接收完成,再判断数据是否正确。
-- 因此将释放信号量写在接收数据完成之后。而判断接收数据完成是在中断里,而中断中的释放函数跟普通的释放函数不一样。
-- 在判断接收的数据前获取信号量
-- 代码
void USART3_IRQHandler(void)//串口收到1字节数据,中断就会被触发一次
{
uint8_t data = 0;
//判断接收中断是否发生
if(USART_GetITStatus(USART3, USART_IT_RXNE) == SET)
{
//处理中断:保存数据
data = USART_ReceiveData(USART3);
USART_SendData(USART1, data);
esp.rxbuff[esp.rxlen++] = data;
esp.rxlen%=1024; //计数值最大是1024(前面一堆乱码也会存在buff里面,要控制数据)
//清理终端
USART_ClearITPendingBit(USART3,USART_IT_RXNE);
}
if(USART_GetITStatus(USART3, USART_IT_IDLE) == SET)
{
//esp.rxflag=1;
BaseType_t pxHigherPriorityTaskWoken = 0;
//释放二值信号量,发送接收到新数据标志,供前台程序查询
xSemaphoreGiveFromISR(xSemaphore,&pxHigherPriorityTaskWoken);
//如果需要的话进行一次任务切换,系统会判断是否需要进行切换
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
//清理空闲中断标志(比较特殊)
data = USART_ReceiveData(USART3);
}
}
//发送不定长数组
void uart3_txstr(uint8_t *buff)
{
while( *buff )
{
uart3_tx(*buff);
buff++;
}
}
//判断指令是否发送成功
uint8_t SendCmdAndJudgeSuccess(uint8_t *cmd,uint8_t * ack,uint16_t outtime)//指令和回复的,超时时间(每条指令都有最大的超时时间)
{
//清空缓冲区
memset(esp.rxbuff,0,1024);
esp.rxlen = 0;
esp.rxflag = 0;
//1、发送指令
uart3_txstr(cmd);
//2、判断接收数据中是否有对应的ack
// while(outtime--)
// {
// if(esp.rxflag==1)
// {
// esp.rxflag=0; //接受两次数据,一次是(前三句话)disconnect会触发,没有OK,然后退出,后面输出ok会再次触发
// if(strstr((char *)esp.rxbuff,(char *)ack)!=NULL)//strstr找到返回地址,没找到返回空
// return 1;
// }
// Delay_nms(1);
// }
//
//改成操作系统
BaseType_t xReturn = pdPASS;
AAA:
xReturn = xSemaphoreTake(xSemaphore,outtime);
if(pdTRUE == xReturn)
{
if(strstr((char *)esp.rxbuff,(char *)ack)!=NULL)//strstr找到返回地址,没找到返回空
{
return 1;
} else
goto AAA;
}
return 0;
}
-- 执行效果图,数据都在获取,不会相互影响
-- 注意:信号量无论释放多少次,都只能获取一次,因为他只有两个值
互斥量
-- 他有一个很重要的特性:防止优先级翻转
-- 本来我们写的代码,LCD屏幕的优先级比DHT11高,但是我们写了同步之后,我们要等到DHT11任务执行完,才会执行LCD任务,这样优先级翻转就发生了,低优先级的比高优先级的先执行。这就叫做优先级翻转
-- 互斥量怎么防止优先级翻转
-- 互斥量有个特点叫做优先级继承P277
--