项目实战-外卖自提柜 3. FreeRTOS主要API的应用_freertos项目实战-程序员宅基地

技术标签: FreeRTOS  

项目实战-外卖自提柜 1.项目介绍、协议制定
项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门
项目实战-外卖自提柜 3. FreeRTOS主要API的应用
项目实战-外卖自提柜 4. FreeRTOS 堆栈分配、调试技巧
项目实战-外卖自提柜 5. ESP8266 01S配置与掉线处理
项目实战-外卖自提柜 6. 硬件工作与测试(原理图、PCB绘制、测试视频)

一、第一个任务

        初步入门FreeRTOS以后,着重解决通信部分,重新梳理一下与服务器通信部分的需求:

1、设备端和服务器通信,发送方每发送一条指令,接收方都要在收到后返回一个应答帧,发送方收到应答帧后,才判断此次通信正常,若规定时间内未收到应答帧,则重新发送。
2、另外需要注意的是,发送方在等待接收方返回应答帧时,不能阻塞系统运行,也就是说,即便当前有一帧数据在等待应答,也不影响下一帧数据的发送,且理论上应该保证同时在等待应答的帧的数量不受限制

        根据上述需求,显而易见的,应当把每一帧的发送单独作为一个任务,这个任务对这一帧进行监听,并控制重发。只要系统还有足够的剩余栈,就可以不断地创建新的发送任务,这样就可以保证最大限度地使用硬件资源保证每一帧的通信【并行】。
        刚好,FreeRTOS创建任务时是可以传入一个参数的,这个参数就可以传入我们要发送的数据。
        第一个任务诞生了:

数据发送任务:

/**
  * @brief  数据发送任务
  * @note	需要向服务器发送一条指令时,就创建一个发送任务,特点是等待回复和重发时不会阻塞其他任务进行
  * @param  argument:要发送的数据
  * @retval None
  */
void SendData_Task(void const * argument)
{
	//待添加
	for(;;)
	{
		//待添加
	}
}

下面来构思函数体中要写些什么

首先,肯定是要发送数据了,发送数据之前,有一件事要考虑,由于传入的是argument是指针,这个任务在进行过程中,这个指针指向的内容很可能被其他任务更改,所先需要先申请空间来拷贝要发送的数据
再来回顾一下帧格式:
帧头 + Length + CmdId + DevId + Content + FrameId + 校验和

成分 描述
帧头 0x0a 0x0a 0x0a 0x0a
Length 指令字节数总长度,包括其本身和校验和,两个字节的无符号short类型,顺序为 [低字节,高字节]
CmdId 指令的Id , 一个字节的无符号byte类型
DevId 目标设备的Id,两个字节的无符号short类型,顺序为 [低字节,高字节]
Content 该条指令包含的详细信息
FrameId 每一帧的唯一Id,两字节无符号short类型,顺序为[低字节,高字节]
校验和 一字节有符号byte类型

我们通过上述的Length获取数据长度,然后用FreeRTOS提供的API:
pvPortMalloc 申请内存,这个函数与C语言的malloc的区别是,前者从FreeRTOS的TOTAL_HEAP_SIZE中申请空间,而后者是从系统的堆(heap)中申请空间。
详细的分析看这篇博客:
https://www.cnblogs.com/LinTeX9527/p/8007541.html

数据发送任务的前几行代码有着落了:

void SendData_Task(void const * argument)
{
	uint8_t *Data;				//创建指针
	uint16_t Data_Len = 0;		//数据长度
	Data_Len = ((uint16_t*)argument)[0];//获取数据长度
	Data = pvPortMalloc(Data_Len-1);	//申请内存,去掉校验和1字节
	memcpy(Data,(uint8_t*)argument,sizeof(uint8_t)*(Data_Len-1));	//复制数组,去掉校验和
	for(;;)
	{
		//待添加
	}
}

二、互斥量的使用

        当然,如果这里严谨一点的话,你会发现,即便这里进行了数据拷贝,但拷贝也不是一瞬间完成的,所以拷贝的时候,这段数据仍然不是安全的,仍可能被更改,下面就用到FreeRTOS的另一个功能了: 互斥量

        正如其名,一个资源在被一个任务访问时,不能再被另一个任务访问,就叫互斥
        通过下面两个函数实现互斥:

osMutexWait(mutex_CopyData_h, osWaitForever);	//等待互斥量被释放
osMutexRelease(mutex_CopyData_h);	//释放互斥量

        其中mutex_CopyData_h是互斥量的句柄(可以看作是名称),osWaitForever表示一直阻塞等待,直到互斥量被释放。

        如何使用呢?
        按照上述情形举例,我们要在拷贝数据时用互斥量进行保护,数据发送任务就改进为下面这种形式:

/**
  * @brief  数据发送任务
  * @note	需要向服务器发送一条指令时,就创建一个发送任务,特点是等待回复和重发时不会阻塞其他任务进行
  * @param  argument:要发送的数据
  * @retval None
  */
void SendData_Task(void const * argument)
{
	uint8_t *Data;				//申请内存指针
	uint16_t Data_Len = 0;		//数据长度
	Data_Len = ((uint16_t*)argument)[0];//获取数据长度
	Data = pvPortMalloc(Data_Len-1);	//申请内存,去掉校验和1字节
	osMutexWait(mutex_CopyData_h, osWaitForever);	//等待互斥量被释放
	/*被互斥量保护的区域*/
	memcpy(Data,(uint8_t*)argument,sizeof(uint8_t)*(Data_Len-1));	
	/*被互斥量保护的区域*/
	osMutexRelease(mutex_CopyData_h);	//释放互斥量
	for(;;)
	{
		//待添加发送函数
	}
}


        osMutexWaitosMutexRelease之间,就是我们希望保护的位置。当然这只完成了一半,同样的,我们需要在存在数据覆盖风险的位置设置互斥量的保护区。

        例如下面:传入数据发送任务的参数是名为Data_Buf的数组

osThreadDef(DATA_SEND_TASK_H,SendData_Task, osPriorityHigh,0, 128);	//心跳帧重发任务的宏
osThreadCreate(osThread(DATA_SEND_TASK_H),Data_Buf)

        那么我需要在修改Data_Buf的位置设置互斥量保护区:

osMutexWait(mutex_CopyData_h, osWaitForever);	//等待互斥量被释放
Data_Buf[0] = 0;
osMutexRelease(mutex_CopyData_h);	//释放互斥量

        被互斥量保护的区域,同时只能被一个任务访问,直到这个任务释放互斥量,下一个任务才能访问。这样,我们就可以保证拷贝数据的时候,数据不会被误修改。

三、消息队列的使用

        我们继续完善数据发送任务,回到需求分析,数据发送任务除了需要完成数据发送,还需要监听是否收到与此帧数据匹配的应答帧。如果同时有好几个数据发送任务在等待应答帧,这时候收到了一条应答帧,对于某一个数据发送任务来说,如何判断这条应答帧是发给自己的呢?

        上翻查阅数据帧格式的表格,可以看到,每一帧数据有唯一的FrameId,回复帧也有FrameId,它的FrameId与它要回复的数据帧的FrameId相同。对于某一个数据发送任务来说,它只需要与收到的回复帧的FrameId进行匹配,若与自己的Frame相同,则判断这个回复帧是回复给自己的,如果是回复给自己的,这个数据发送任务就完成了自己的使命,可以把自己删除了。所以当有多帧数据同时等待回复帧时,需要开设一个缓存区,存放收到的回复帧的FrameId,供数据发送任务查询。

        这个缓存区,就交给 消息队列来完成。FreeRTOS对消息队列的处理,我用到了下面几个API:

//查询队列中元素的个数
osMessageWaiting(MsgBox_Frame_Id_Handle);
//获取并删除队列中的一个元素
osMessageGet(MsgBox_Frame_Id_Handle,osWaitForever);
//向队列存放一个元素
osMessagePut(MsgBox_Frame_Id_Handle,evt.value.v,osWaitForever);
  • MsgBox_Frame_Id_Handle是这个队列的句柄
  • osWaitForever表示这个函数执行的超时时间,超过了这个值就会自动退出,这里是永久等待
  • evt.value.v是要向队列里存入的元素

        如何实现查询队列中是否有与自己匹配的FrameId呢?

        我的思路是,先通过osMessageWaiting读出当前队列中元素的数量N,进入循环,每个循环中,使用osMessageGet取出一个元素,由于队列是先进先出,所以这个元素是从队列头部取出的,判断是否匹配,如果匹配,皆大欢喜,这个数据发送任务就解脱了;如果不匹配,再将这个元素用osMessagePut重新加入到队列尾部,这样循环N次,就相当于把队列查询了一遍。

        数据发送任务就基本完成了:

/**
  * @brief  数据发送任务
  * @note	需要向服务器发送一条指令时,就创建一个发送任务,特点是等待回复和重发时不会阻塞其他任务进行
  * @param  argument:要发送的数据
  * @retval None
  */
void SendData_Task(void const * argument)
{
	uint8_t *Data;				//申请内存指针
	uint16_t Data_Len = 0;		//数据长度
	Data_Len = ((uint16_t*)argument)[0];//获取数据长度
	uint16_t FrameId = 0;		//帧Id
	uint32_t MsgBox_Data_Num = 0;//队列中有效数据的数量
	osEvent evt;				//存放osMessageGet的返回值
	Data = pvPortMalloc(Data_Len-1);	//申请内存,去掉校验和1字节
	osMutexWait(mutex_CopyData_h, osWaitForever);	//等待互斥量被释放
	/*被互斥量保护的区域*/
	memcpy(Data,(uint8_t*)argument,sizeof(uint8_t)*(Data_Len-1));	
	/*被互斥量保护的区域*/
	osMutexRelease(mutex_CopyData_h);	//释放互斥量
	FrameId = (uint16_t)Data[Data_Len-2]<<8|(uint16_t)Data[Data_Len-3];	//装载这一帧数据的FrameId
	for(;;)
	{
		osMutexWait(mutex_id_Resend, osWaitForever);//获取互斥量,防止其他的数据发送任务打断
		MsgBox_Data_Num = osMessageWaiting(MsgBox_Frame_Id_Handle);	//获取当前队列数量
		if(MsgBox_Data_Num != 0)		//如果队列非空
		{
			for(i=0;i<MsgBox_Data_Num;i++)
			{
				evt = osMessageGet(MsgBox_Frame_Id_Handle,100);	//从队列中取出一个元素
				if(evt.value.v == FrameId)		//如果FrameId匹配
				{
					/****删除任务****/
					osMutexRelease(mutex_id_Resend);	//释放令牌
					vPortFree(Data);					//释放内存
					osThreadTerminate (NULL);			//删除本任务
				}
				else		//如果不匹配
				{
					osMessagePut(MsgBox_Frame_Id_Handle,evt.value.v,500) //存回队列尾
				}
			}
		}
		User_SendData(Data,Data_Len);	//发送数据
		osMutexRelease(mutex_id_Resend);//释放互斥量
		osDelay(5000);	//每5s检测一次
	}
}

        除了上面的思路,我这里还使用了一个互斥量,用以保护整个发送过程,因为当有多个数据发送任务都再执行时,队列的取出和放回动作可能会被打断,出现某种极端情况。

        例如任务A刚刚从队列中取出一个元素,发现跟自己的FrameId不匹配,但还没来得及放回去,CPU控制权就被任务B抢去了,任务B查询的时候,就少了这个任务A取走的元素,造成误判。
        另外,发送数据是通过串口的,执行时间也比较长,如果发送时被打断,可能造成不可预估的后果,所以使用互斥量进行保护是十分有必要的。

        在整个项目中,主要用到的就是上面几个API数据发送任务,也是仅有的稍显复杂的任务,另外还有一些调试用的API,下一节更新。

 

四、任务划分

        根据功能划分了下面几个任务

  1. 人机交互任务:
    包括按键扫描、LCD显示、蜂鸣器扫描,优先级较低

  2. 无线模块管理任务:
    包括检测到服务器离线时,对WIFI模块/GPRS模块进行重新初始化,切换wifi网络或运营商网络模式等,优先级最高

  3. TCP透传发送任务
    当要发送一帧数据时,该任务被创建,发送一帧数据,并对这帧数据进行监听,等待接收方回复,若未收到回复,则重新发送,通信完成则删除本任务。优先级较高

  4. TCP接收数据解析任务
    由接收中断触发,解析数据,执行相应操作,优先级最高

        这里简单列两个
        人机交互任务:

/**
  * @brief  人机交互任务
  * @note	包括按键扫描、LCD显示、蜂鸣器鸣叫
  * @param  argument:任务参数(未用到)
  * @retval None
  */
void Interactive_Task(void const * argument)
{
	for(;;)
	{
		/**矩阵键盘扫描**/
		/**键值处理**/
		/**LCD显示**/
		/**蜂鸣器扫描**/
		osDelay(20);
	}
}

        无线模块管理任务:

/**
  * @brief  无线模块管理任务
  * @note	检测服务器是否离线,若离线则重新初始化无线模块
  * @param  argument:任务参数(未用到)
  * @retval None
  */
void WirelessCTR_Task(void const * argument)
{
	osDelay(1000);		//等待ESP8266上电
	for(;;)
	{
		if(server_sta == SERVER_OFF_LINE)	//服务器离线
		{
//			osThreadSetPriority(NULL,osPriorityHigh);	//调高优先级,防止打断
			esp8266_init();		//初始化esp8266
//			osThreadSetPriority(NULL,osPriorityNormal);	//调低优先级
		}
		osDelay(20);
	}
}

代码和硬件都打包上传至立创开源社区了:
https://oshwhub.com/doee/wai-mai-zi-ti-gui-she-bei-duan-z

项目实战-外卖自提柜 1.项目介绍、协议制定
项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门
项目实战-外卖自提柜 3. FreeRTOS主要API的应用
项目实战-外卖自提柜 4. FreeRTOS 堆栈分配、调试技巧
项目实战-外卖自提柜 5. ESP8266 01S配置与掉线处理
项目实战-外卖自提柜 6. 硬件工作与测试(原理图、PCB绘制、测试视频)

https://blog.csdn.net/weixin_44578655/article/details/105969808

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ba_wang_mao/article/details/113650387

智能推荐

zabbix短信告警oracle,zabbix 实现短信告警-程序员宅基地

文章浏览阅读402次。之前一直调用飞信接口发送告警信息,最近购买了第三方短信接口。所以准备使用接口发送告警。短信接口是基于https的摘要认证。https认证还是自己做的,调用接口的时候还需要load证书。感觉超级难用,不管那么多,先让它跑起来再说。废话不多说,先上代码。#!/usr/bin/envpython#coding:utf-8importrequestsfromrequests.authimport..._zabbix实现短信告警

soapui中文操作手册(四)----MOCK服务_soapui设置成中文-程序员宅基地

文章浏览阅读6.8k次,点赞2次,收藏12次。转载地址:http://www.cnblogs.com/zerotest/p/4670005.htmlWeb Service Mocking是武器库一个非常有用的工具。这是解决“如果没有Web服务如何创建针对性的Web服务测试”问题的办法。Web Service Mocking将在这里派上用场。它允许你实际的Web服务产生之前,创建近似或模拟的Web Service。在本教_soapui设置成中文

Swift 包管理器 (SPM):管理 iOS 中的依赖关系_ios spm-程序员宅基地

文章浏览阅读845次,点赞29次,收藏7次。Swift 包管理器 (SPM):管理 iOS 中的依赖关系_ios spm

SCI论文润色真有必要吗?-程序员宅基地

文章浏览阅读381次,点赞10次,收藏7次。总的来说,sci论文润色虽然不会改变论文的学术内容和贡献,但它能够显著的提升论文的质量和可读性,从而增加论文被接受和引用的机会。在论文投稿前都是需要润色的,特别是英文论文投稿,一定得靠谱。但如果是一些小问题,比如语法语句错误,专业言论不恰当,那么你的文章会在投稿过程中外审评定完以后,也会给你返修意见和修改机会。如果是新作者,或者是对自己的语言能力不那么自信,那么是很有必要的。其他人的视角可能会发现你忽略的错误或不清晰的表达,同时也可以提供有关论文结构和逻辑的反馈意见。关于SCI论文润色的常见方法。

Prometheus监控数据格式的学习-程序员宅基地

文章浏览阅读1.1k次,点赞33次,收藏9次。Prometheus 指标(metrics)的数据形式是一种简单的文本格式(容易通过 HTTP 协议被 Prometheus 服务器拉取)。每一行包含了一个指标的数据,通常包括指标名称、可选的一组标签以及指标的值。Prometheus 的指标数据可以有不同类型,如 Counter、Gauge、Histogram 和 Summary,它们的表示形式会有所不同。

数字图像处理(10): OpenCV 图像阈值化处理_binarization threshold-程序员宅基地

文章浏览阅读5.6k次,点赞26次,收藏43次。目录1 什么是阈值化-threshold()2 二进制阈值化3 反二进制阈值化4 截断阈值化5 反阈值化为06 阈值化为07 小结参考资料1 什么是阈值化-threshold()图像的二值化或阈值化 (Binarization)旨在提取图像中的目标物体,将背景以及噪声区分开来。通常会设定一个阈值,通过阈值将图像的像素划分为两类:大于阈值的..._binarization threshold

随便推点

使用安卓模拟器时提示关闭hyper-v_hyperv影响 模拟器-程序员宅基地

文章浏览阅读1.6w次。本电脑是宏碁传奇X,cpu是r7 5800u,显卡rtx3050;使用了雷电、mumu两款安卓模拟器,雷电启动报错g_bGuestPowerOff fastpipeapi.cpp:1161,使用了网上的所有方案都不行,包括开启VT(amd开启SVM),命令关闭hyper-v服务等;尝试mumu模拟器,安装时支持vt项检测不通过,后来发现mumu模拟器在amd的cpu上只支持32位版,换装32位版检测通过,但是只要打开模拟器就提示需要关闭hyper-v,我已经确认关闭后,启动依旧这样提示,查找了网上很_hyperv影响 模拟器

【大厂秘籍】系列 - Mysql索引详解-程序员宅基地

文章浏览阅读564次。MySQL官方对索引定义:是存储引擎用于快速查找记录的一种数据结构。需要额外开辟空间和数据维护工作。● 索引是物理数据页存储,在数据文件中(InnoDB,ibd文件),利用数据页(page)存储。● 索引可以加快检索速度,但是同时也会降低增删改操作速度,索引维护需要代价。

CSS实现当鼠标停留在一个元素上时,使得两个元素的样式发生改变_css鼠标悬浮修改其他元素样式-程序员宅基地

文章浏览阅读825次。使用兄弟选择器实现同时改变两个元素的样式_css鼠标悬浮修改其他元素样式

文献学习-40-基于可迁移性引导的多源模型自适应医学图像分割-程序员宅基地

文章浏览阅读4.8k次,点赞32次,收藏43次。香港中文大学袁奕萱教授团队提出了一种名为多源模型自适应 (MSMA) 的新型无监督域适应方法。MSMA 旨在仅利用预训练的源模型(而非源数据)将知识迁移到未标记的目标域,从而实现对目标域的有效分割。

(4)FPGA开发工具介绍(第1天)-程序员宅基地

文章浏览阅读8.8k次。(4)FPGA开发工具介绍(第1天)1 文章目录1)文章目录2)FPGA初级课程介绍3)FPGA初级课程架构4)FPGA开发工具介绍(第1天)5)技术交流6)参考资料2 FPGA初级课程介绍1)FPGA初级就业课程共100篇文章,目的是为了让想学FPGA的小伙伴快速入门。2)FPGA初级就业课程包括FPGA简介、Verilog HDL基本语法、Verilog HDL 入门实例、FPGA入门实例、Xilinx FPGA IP core设计、Xilinx FPGA原语与U_fpga开发工具

js中的定时器如何使用_js定时器用法-程序员宅基地

文章浏览阅读1.4k次。JS提供了一些原生方法来实现延时去执行某一段代码,下面来简单介绍一下setTiemout、setInterval、setImmediate、requestAnimationFrame。首先,我们先来了解一下什么是定时器:JS提供了一些原生方法来实现延时去执行某一段代码下面来简单介绍一下setTimeout() :在指定的毫秒数后调用函数或计算表达式。setTimeout(code,millisec,lang)参数 描述code 必需。要调用的函数后要执行的 JavaScript 代码串。_js定时器用法

推荐文章

热门文章

相关标签