任务间通信(1)

时间:2024-10-25 19:13:13

任务间通信

目录

任务间通信

回顾

-- 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之任务间通信

-- 参考文档 

alt text

-- 这个文档从17章开始之后就是任务间通信

消息队列

alt text

-- 核心用途:消息传递

-- 裸机中传递消息的是数组

-- 消息队列就是来传递消息的

-- 那么数组就可以传递消息,为什么要引入消息队列呢?

-- 消息队列的特性 

alt text

-- 数组中的消息会被覆盖

alt text

alt text

alt text

alt text

-- 消息队列的三种阻塞方式

alt text

alt text

alt text

-- 消息队列的核心控制有四个函数,基本上每一个任务间通信都有这四个函数

alt text

-- 可以说任务间通信就是学这四个函数,一般情况下,创建和删除是固定的,实际上是学不同通信的读写

alt text

alt text

-- 下面依旧是找到事例复制到工程中

alt text

 

alt text

alt text

-- 发送和接收函数

alt text

alt text

alt text

-- 在工程中编写代码

-- 这里将发送写在按键任务中

#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();//任务调度
}

alt text

 -- 效果图

alt text

-- 注意,消息队列读完数据后,数据就没有了。

-- 执行的结果,如果先点发送数据,再点接收数据,就会比较快的接收到,如果先按按键2(也就是先接收数据的时候),消息队列里面没有数据,就会等待设置的超时时间后,再结束。

信号量

alt text

-- 信号量用于任务同步,资源保护。。。

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。 抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。 通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

  • 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。
  • 正值,表示有一个或多个释放信号量操作。

-- 信号量本质就是一个数字(非负整数)

-- 在操作系统中,信号量是有分类的,

-- 信号量中有两种信号量是比较关键的,一个叫做二值信号量,一个叫做互斥信号量

-- 二值信号量更偏向于任务同步,互斥信号量更偏向于资源保护 

alt text

-- 任务同步: 

alt text

-- 什么叫做任务同步?

  • 例如屏幕显示与数据获取,数据获取之后,立马就要在屏幕上显示
  • 任务同步用于两个有关联的任务

-- 如果一个信号量只有0和1两个值,那么这个信号量就是二值信号量

-- 为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。

-- 二值信号量的应用场景?

-- 二值信号量在任务与中断同步的应用场景:我们在串口接收中,我们不知道啥 时候有数据发送过来,有一个任务是做接收这些数据处理,总不能在任务中每时每刻都在任务查询有没有数据到来,那样会浪费 CPU 资源,所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,任务就进入阻塞态,不参与任务的调度,等到数据到来了,释放一个二值信号量,任务就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。

-- 编写代码

-- 这里由于消息队列对于项目的用处没有那么大,可以尝试将消息队列裁剪掉,操作系统的空间就会更大些.将消息队列的宏定义由1变为0.这就叫做操作系统裁剪。

alt text

-- 这样操作系统就可以不需要那么多空间,所以分给操作系统的空间也可以适量降低

alt text

-- 编写二值信号量代码

-- 依旧是复制事例

-- 创建二值信号量(文档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();//任务调度器			//当程序执行完这句话之后,正常情况下下面的代码不会再运行,只会运行任务主题函数
}
更改接收数据方式

-- 将之前裸机的写法改为操作系统

alt text

-- 这里是中断接收数据完成后,再去判断数据,所以先要判断数据是否接收完成,再判断数据是否正确。 

alt text

-- 因此将释放信号量写在接收数据完成之后。而判断接收数据完成是在中断里,而中断中的释放函数跟普通的释放函数不一样。 

alt text

alt text

-- 在判断接收的数据前获取信号量

alt text

-- 代码

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;
}

-- 执行效果图,数据都在获取,不会相互影响 

alt text

-- 注意:信号量无论释放多少次,都只能获取一次,因为他只有两个值

互斥量

-- 他有一个很重要的特性:防止优先级翻转

-- 本来我们写的代码,LCD屏幕的优先级比DHT11高,但是我们写了同步之后,我们要等到DHT11任务执行完,才会执行LCD任务,这样优先级翻转就发生了,低优先级的比高优先级的先执行。这就叫做优先级翻转

alt text

-- 互斥量怎么防止优先级翻转

-- 互斥量有个特点叫做优先级继承P277

-- 

alt text