本文主要是介绍简单物联网终端设备的设计思路总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
简单物联网终端设备的设计思路总结
个人总结,物联网终端设备的研发一般有以下步骤:
- 公司领导经过“极为慎重严谨的调研和评估”之后决定立项;
- 产品经理根据领导层决定的产品定位,“参考”竞品,输出“十分确定的”需求;
- 系统工程师根据“十分确定的”需求,输出“提纲挚领而又细致入微的”系统方案;
- 软硬件开发人员根据“十分确定的”需求和“提纲挚领而又细致入微的”系统方案,输出“稳定性极其棒,可扩展性十分强,可维护性特别好的”软硬件设计方案以及版本;
- 测试人员同样根据”十分确定的“需求和”提纲挚领而又细致入微的”系统方案,输出“非常自动化,可24小时无人值守而且100%覆盖所有功能的”软硬件测试方案并完成测试;
- 生产产线“无需试产的直接一次批量成功”;
- 领导请大家在公司楼下小餐馆聚餐庆功。
哈哈,当然以上带有夸张,不过大体步骤是没问题的。这里主要想讨论的就是步骤3和步骤4。以下将以智能水表为例进行讨论。
一 从逻辑上分解设备
个人认为,一般的物联网终端设备,都可以分为三个部分:属性,数据,状态。而系统方案就可以根据这三个部分进行完善和输出。
1. 属性
属性指的是这个设备一般不会发生变动(多次断电开机都会保持一致)的参数。可以分为三类,基础属性,联网属性,业务属性。其中业务属性是跟需求紧密相关的。例如:
- 水表的基础属性包括:表号,表类型,软件版本,硬件版本等;
- 水表的联网属性包括:IMEI,IMSI,CCID,APN,远端IP和端口等;
- 水表的业务属性包括:最高流速阈值,最大累计流量阈值,最高水温阈值,最高水压阈值,最低水温阈值,最低水压阈值,数据上报周期,数据采集周期等;
2. 数据
数据就是这个设备在工作过程中所需要获取的某些数值。可以分为两种,一种是从硬件中可以直接获取的数值,称为直接数据;另一种是因为业务需求所需要计算或记录的数值,称为间接数据。例如:
- 水表的直接数据包括:累计流量,瞬时流量,水温,水压等;
- 水表的间接数据包括:最大瞬时流量,最高水温,最低水温,最高水压,最低水压等;
3.状态
状态指的是这个设备在工作中的运行情况。分为物理状态,逻辑状态两种。物理状态又可以分为网络状态,能源状态,控件状态等;逻辑状态则主要是指业务相关的一些状态。例如:
- 水表的网络状态包括:信号强度,信噪比,小区id,频点,覆盖等级等
- 水表的能源状态包括:电池电压,电量等
- 水表的控件状态包括:阀门开关状态等
- 水表的逻辑状态包括:各种报警标志,定时上报标志,数据采集标志,LCD显示标志等
二 确定与云端的通信协议
这里的协议指的是业务协议,这种通信协议原则就一个,精炼。具体来说,就是能云端做的事都在云端做,因为本身物联网终端设备的能源和各种代码空间都受限,而且由于一个系统中,设备数量一般都很多,对稳定性的要求很高,因此协议越简单,通信过程就越可控,功耗也越少。
我们结合上面所说的设备的三个部分,协议也就有了最基本(但不是必须的)的三个内容:
- 设置设备属性;
- 上报设备数据;
- 改变设备状态;
在这三个内容的基础上,也可以根据业务需求,再进行扩展,例如水表的协议可以是这样的:
- 设置设备属性:包括,设置联网属性帧、设置业务属性帧,基础属性一般不会修改(软件版本也是通过fota的方式进行变更,而非远程设置)。
- 上报设备数据:包括,上报直接数据帧和报警信息帧,间接数据不需要上报,因为可以通过直接数据进行处理得到。
- 改变设备状态:包括,改变控件状态帧及其响应帧,网络状态和能源状态一般是不能修改的,逻辑状态一般不建议修改,属于设备运行过程的标识。
其中,只有改变控件状态帧一定需要应用层的响应,因为它在执行层面涉及改变外界,存在由于控件故障而失败的风险,这是不可控风险,而其他的帧,均属于程序逻辑操作,风险是属于软件内部的问题,是可控风险。
关于是否需要响应帧,如果它是用来规避通信失败的,应当根据系统所使用的具体传输协议来决定,如果使用的本身就是类似于TCP这种有保障机制的协议,那就不需要在业务协议中再额外规定响应帧。现在比较成熟的物联网协议,例如coap,mqtt等一般都包含用于确认某条数据重要程度的机制。
简而言之,用于确保通信稳定的响应帧,应该交给底层(非应用层)协议去做,应用层中的响应帧,是用于确认某种存在不可控风险的操作的执行结果的。
另外,关于应用层协议中的帧头帧尾帧类型帧长等帧结构相关的部分,应当根据具体情况确认是否需要,只有确定payload中不会有跟帧头帧尾相同的数值时(或者使用转义的方法从payload中排除与帧头帧尾相同的数据),那帧头帧尾才可以对帧的数据处理起到比较大的作用,否则作用很小甚至是反作用。
而帧长和帧类型,则是在不适合使用帧头帧尾的情况下的最好办法,在不考虑传输错误的情况下,完全可以以帧类型开头,后面跟帧长和数据然后结束。而说到传输错误,一般的物联网协议都没有纠错机制(当然有可能是我了解的不够深),因此我们就需要有校验机制,这里我们一般采用CRC校验,简单的可以采用累加和或者异或校验。
所以,个人认为物联网应用层最精简的帧结构,完全可以是:
|帧类型|帧长|数据|CRC|
或
|帧头|(|帧类型|数据|CRC)(转义)|帧尾
业务协议到这里基本上就结束了,但是一般物联网设备都有一个最终兜底的措施——FOTA,所以一般协议里也会涉及FOTA相关的内容,这里简单说一下。
由于它是软件层面的兜底措施,因此不容失败,所以个人认为应当采用成熟的文件传输协议,HTTP,FTP等。而触发方式一般就是在业务协议中,额外多一个帧用于触发FOTA,之后一般都通过另一个FOTA协议完成文件下载,这个过程一般是这样的:
- 对旧版本号与新版本号进行比对与确认;
- 然后进行文件下载;
- 下载完成后进行文件完整性确认;
- 然后进行flash烧录;
- 然后运行新程序;
- 还要再对比该程序的版本号是不是目标版本号,不一致还要再回退到之前的版本;
- 然后再使用业务层协议上报升级结果。
所以这个帧也是跟改变控件状态帧一样,需要有响应帧,因为FOTA这个行为太重要了,一点风险也不能承受,同时一般FOTA过程是不考虑功耗的(都需要FOTA了,事情已经很严重了)。
三 确定系统框架
1.硬件框架
PS:笔者主要是做软件的,硬件工作只在大学期间和刚工作的一年内做过,因此只能做简单描述。
个人认为,硬件框架最重要的是关键器件选型,什么是关键器件一般是根据产品需求来确定,而选型时的功能和性能标准又是根据产品定位来确定。还是以智能水表为例,智能水表的产品需求中必要的有三点:
- 能获取水流相关的物理信息,流速,水温,水压等等;这意味着传感器是关键器件;
- 能将这些数据上报;这意味着微控制器(MCU)和无线通信模块也是关键器件;
- 易安装,易维护;这意味着低功耗长续航,也就是说电源模块也是关键器件,一般是电池和电源芯片。
产品定位可能有两种,工业水表和民用水表。
工业水表,对水表本身的稳定性要求更高,对于水流信息的精度,时效性要求更高,工作环境也更加恶劣,续航时间也要求更高,但是对成本相对不敏感。因此需要选择精度更高的传感器,抗干扰能力更强,速度更快的微控制器和无线通信模块,容量更大的电池和更稳定的电源芯片。民用水表则相反。选型时需要更多向成本倾斜。
除此之外,选型还有一个原则就是尽量考虑使用同样的器件和可替代性强的器件,以此来确保供应链的稳定性。
这其中的取舍把握,就是对需求的理解和硬件经验的体现。
另外一般来说,第一版的选型都会稍微偏向性能,也更加考虑兼容性一些,更加便于开发,而后根据第一版产品的市场反馈以及实际开发结果,再将额外冗余的部分去掉来降成本。
个人认为,选型完成之后,对于水表这种比较简单的物联网终端来说,硬件设计就没有太大的难点了,但还需要注意一下射频的设计,低功耗的设计,PCB的布局结构与外壳的匹配等细节问题,避免无谓的功耗和信号损失。
2.软件框架
PS:由于嵌入式编程主要是使用C,因此以下的讨论也主要用C来说明。
软件框架的设计总体来说是根据需求和协议来的,但也需要考虑硬件的选型——主要是MCU的选型。比如MCU是否允许跑RTOS甚至linux,还有休眠时MCU外设的状态以及如何唤醒等等。一般情况下,个人建议,只要MCU允许,能上操作系统就上,它可以帮助你更好的控制软件的逻辑和时序(在使用熟练的情况下),而且主流RTOS都支持了很多组件,可以方便各种功能的实现(非常推荐RT-Thread)。
另外,主流的MCU都有成熟的HAL库,这样我们主要就是基于硬件驱动层和RTOS之上,考虑应用层软件的框架。
个人习惯在设计应用层软件框架时,从数据结构入手,先根据需求建立合适的数据结构及其处理接口,然后再根据需求和数据结构设计程序流程。
那首先来看数据结构,结合我们在上面分解的三部分,数据结构就很明显可以分为三种了,属性,数据,状态。接下来我们根据上面对设备的逻辑分解,逐一分析这三个数据结构:
- 属性,属性是云端主动配置或者缺省默认的,是要能掉电保存的,设备的联网参数,数据采集间隔,上报间隔,报警阈值等信息都来源于它。所以它需要可以进行读取,修改,保存操作。同时,它的各个成员参数在真正的业务流程启动之前就应该是有效的。
- 数据,数据是从传感器采集到的,不需要掉电保存,它需要被上报,因此它要能被读取和修改。
- 状态,状态是设备在运行过程中产生的一些逻辑参数,表明它的网络状况,运行情况等等,也不需要掉电保存,它同样也是需要能被读取和修改。
根据以上的分析可知,应用层软件大致可以分为四个模块:
- 首先要有一个采集数据的模块A,它负责在需要的时候从传感器中获取信息,并填充到数据中,同时要读取属性中的各种报警阈值,进行报警判断,然后再将报警状态更新到状态中;
- 然后,需要有一个与云端通信的模块B,它负责在需要的时候将数据中的数据取出并发送至云端,并接收云端下发的命令等,同时也要更新状态。
- 由于水表可能还需要控制阀门,检测按键等,因此还需要有一个模块C处理这些工作。它也需要更新状态。
- 上面三个模块都是要在需要的时候才工作,因此就需要有个告诉它们什么时候该工作的总服务模块Z。
也就是说这四个模块的关系应该如下:
Z给ABC下发命令并接收结果
这样的模式下,业务逻辑被抽离出来放在了Z中,真正干活的是ABC,代码的可维护性和可扩展性就会很好,有新的需求或者工作流程需要修改时,只需要修改Z即可。但是一定要确保Z除了处理业务逻辑和分发指令之外,不会做其他实际的工作,否则Z和ABC的耦合性就会越来越强。
那么到目前为止,我们的应用层程序里共有七个部分,三个数据结构,四个逻辑模块,接下来我们依次分析一下它们应该怎么实现。
四 详细设计
PS:这里只对软件的详细设计进行讨论
1.数据结构
三个数据结构由于都需要被多个模块访问,因此,它们可以实现为全局变量,那么同样由于它们会被多个模块访问,因此需要被加以控制,防止同时被不同模块访问造成混乱。控制方式可以采用互斥锁,也就是在这三个数据结构中,除了它们本身的成员,再加上一个互斥锁成员。如此一来,可以专门为它们编写相应的操作函数,其中由于属性需要掉电保存,因此它还需要一个存储函数,且它的读取函数也会不同。
三个数据结构的操作函数伪代码如下:
属性
static attr_t s_attr = {0};
uint8 init_attr(void)
{if(NULL == s_attr.attr_mutex){mutex_t temp_mutex = NULL;s_attr.attr_mutex = osMutexNew(NULL);osMutexAcquire(s_attr.attr_mutex, osWaitForever); temp_mutex = s_attr.attr_mutex;尝试从flash中读取属性到s_attr;if(尝试从flash中读取属性失败){ 给s_attr赋默认值;}s_attr.attr_mutex = temp_mutex;osMutexRelease(s_attr.attr_mutex);return 0; }elsereturn -1;
}uint8 read_attr(attr_t* attr)
{if((NULL != attr) && (NULL != s_attr.attr_mutex)){osMutexAcquire(s_attr.attr_mutex, osWaitForever); memcpy(attr, &s_attr, sizeof(attr_t));attr.attr_mutex = NULL;osMutexRelease(s_attr.attr_mutex); return 0;}elsereturn -1;
}uint8 write_attr(attr_t* attr)
{if((NULL != attr) && (NULL != s_attr.attr_mutex)){osMutexAcquire(s_attr.attr_mutex, osWaitForever); attr->attr_mutex = s_attr.attr_mutex;memcpy(&s_attr, attr, sizeof(attr_t));osMutexRelease(s_attr.attr_mutex); }elsereturn -1;
}uint8 store_attr(void)
{uint8 ret = 0;if(NULL != s_attr.attr_mutex){osMutexAcquire(s_attr.attr_mutex, osWaitForever); ret = 尝试将attr存储至flash中;osMutexRelease(s_attr.attr_mutex); return ret;}elsereturn -1;
}
状态
static state_t s_state = {0};
uint8 init_state(void)
{if(NULL == s_state.state_mutex){s_state.state_mutex = osMutexNew(NULL);osMutexAcquire(s_state.state_mutex, osWaitForever); 给s_state赋默认值;osMutexRelease(s_state.state_mutex);return 0; }elsereturn -1;
}uint8 read_state(state_t* state)
{if((NULL != state) && (NULL != s_state.state_mutex)){osMutexAcquire(s_state.state_mutex, osWaitForever); memcpy(state, &s_state, sizeof(state_t));state.state_mutex = NULL;osMutexRelease(s_state.state_mutex); return 0;}elsereturn -1;
}uint8 write_state(state_t* state)
{if((NULL != state) && (NULL != s_state.state_mutex)){osMutexAcquire(s_state.state_mutex, osWaitForever); state->state_mutex = s_state.state_mutex;memcpy(&s_state, state, sizeof(state_t));osMutexRelease(s_state.state_mutex); }elsereturn -1;
}
数据
static data_t s_data = {0};
uint8 init_data(void)
{if(NULL == s_data.data_mutex){s_data.data_mutex = osMutexNew(NULL);osMutexAcquire(s_data.data_mutex, osWaitForever); 给s_data赋默认值;osMutexRelease(s_data.data_mutex);return 0; }elsereturn -1;
}uint8 read_data(data_t* data)
{if((NULL != data) && (NULL != s_data.data_mutex)){osMutexAcquire(s_data.data_mutex, osWaitForever); memcpy(data, &s_data, sizeof(data_t));data.data_mutex = NULL;osMutexRelease(s_data.data_mutex); return 0;}elsereturn -1;
}uint8 write_data(data_t* data)
{if((NULL != data) && (NULL != s_data.data_mutex)){osMutexAcquire(s_data.data_mutex, osWaitForever); data->data_mutex = s_data.data_mutex;memcpy(&s_data, data, sizeof(data_t));osMutexRelease(s_data.data_mutex);return 0;}elsereturn -1;
}
这种方法是加锁和解锁在操作函数内部,更加安全,缺点是效率略低,且消耗的内存空间会大一些。
还可以使用如下方法:
属性
static attr_t s_attr = {0};
uint8 init_attr(void)
{if(NULL == s_attr.attr_mutex){mutex_t temp_mutex = NULL;s_attr.attr_mutex = osMutexNew(NULL);osMutexAcquire(s_attr.attr_mutex, osWaitForever); temp_mutex = s_attr.attr_mutex;尝试从flash中读取属性到s_attr;if(尝试从flash中读取属性失败){ 给s_attr赋默认值;}s_attr.attr_mutex = temp_mutex;osMutexRelease(s_attr.attr_mutex);return 0; }elsereturn -1;
}attr_t* handle_attr(void)
{if(NULL == s_attr.attr_mutex)return NULL;osMutexAcquire(s_attr.attr_mutex, osWaitForever); return &s_attr;
}void release_attr(void)
{if(NULL == s_attr.attr_mutex)return;osMutexRelease(s_attr.attr_mutex);
}uint8 store_attr(void)//该函数需要保证使用在handle_attr()和release_attr()函数之间
{uint8 ret = 0;if(NULL == s_attr.attr_mutex)return -1;ret = 尝试将s_attr存储至flash中;return ret;
}
状态
static state_t s_state = {0};
uint8 init_state(void)
{if(NULL == s_state.state_mutex){s_state.state_mutex = osMutexNew(NULL);osMutexAcquire(s_state.state_mutex, osWaitForever); 给s_state赋默认值;osMutexRelease(s_attr.attr_mutex);return 0; }elsereturn -1;
}state_t* handle_state(void)
{if(NULL == s_state.state_mutex)return NULL;osMutexAcquire(s_state.state_mutex, osWaitForever); return &s_state;
}void release_state(void)
{if(NULL == s_state.state_mutex)return;osMutexRelease(s_state.state_mutex);
}
数据
static data_t s_data = {0};
uint8 init_data(void)
{if(NULL == s_data.data_mutex){s_data.data_mutex = osMutexNew(NULL);osMutexAcquire(s_data.data_mutex, osWaitForever); 给s_data赋默认值;osMutexRelease(s_data.data_mutex);return 0; }elsereturn -1;
}state_t* handle_data(void)
{if(NULL == s_data.data_mutex)return NULL;osMutexAcquire(s_data.data_mutex, osWaitForever); return &s_data;
}void release_data(void)
{if(NULL == s_data.data_mutex)return;osMutexRelease(s_data.data_mutex);
}
这种方法是把加锁解锁拆成了两部分,优点是使用方便,效率比较高,缺点是程序员必须要保证加锁解锁是成对使用,且需要注意死锁问题。
相对来说,个人更推荐第一种。
2.逻辑模块
接下来分析四个逻辑模块的实现:
总体分析
首先来看总服务模块Z,它需要告诉其他模块什么时候该做什么,那有四点:
- 它需要知道真正的工作目标是什么;
- 它能根据业务逻辑关系将目标分解为命令;
- 它需要与其他模块通信的手段,用于向其他模块分发命令,以及收集它们的命令处理结果;
- 它还需要在所有工作目标都完成之后判断是否应该休眠。
关于第一点,一般像智能水表这类比较简单的物联网终端,都会选择主要用于低功耗场景的低成本MCU,这类MCU都有睡眠深度的选择机制,也会对唤醒源做出标记,我们一般通过判断唤醒源(掉电重启,外部电平唤醒,外部串口唤醒,内部定时器唤醒等)还有睡眠深度来决定本次程序运行是需要做什么工作(采集数据,上报数据,控制控件等)。而除了从休眠态醒来,还可能是设备已经醒来的情况下,收到从云平台发来的命令,或者某个另外的唤醒源触发,这种情况下也会产生新的工作目标;
关于第二点,也就说明我们的主要业务逻辑,都是在这个模块中实现;业务目标我们称为work,分解的命令,我们称为cmd。在程序中定义这两个结构时,建议使用枚举类型,同时,定义work一定是从需求出发,可以参考应用层通信协议定义,一般协议中定义了什么条目,就会需要一个work。定义cmd时,要从模块行为出发,某个模块的读,写或者其他行为,都要定义为一个cmd,同时,还要有对应的行为结束cmd。业务逻辑的分解,就是把需求变为单个或多个模块协同执行一些行为。
关于第三点,手段很多,可以将它实现为一个线程,通过消息队列与其他模块通信,这同时也要求其他模块也至少要有一个线程和消息队列;
关于第四点,也就是说休眠控制也要由该模块完成。这里建议休眠控制采用投票的方式,也就是说,只要产生了work,那么就要投一票不能休眠,完成了一个work,就投一票可以休眠,每次完成work时,都检测一次票数是否为0,是则进入休眠。
下来看采集模块A,它需要在收到Z发送的命令之后,执行命令,并向Z反馈执行结果;因此它也需要一个线程和一个消息队列;
然后是通信模块B,它会稍微复杂一些,除了在收到Z的命令之后要执行命令,还需要能收到云平台下发的命令,同时由于这些命令会涉及到各个模块,因此为了统一,我们最好将命令发回给Z进行处理。因此我这里将B分为三个线程,一个专门用于接收从Z发来的命令,一个用于向云平台发送数据,一个用于接收从云平台发来的命令;
最后是控制模块C,它也是只需要在收到Z的命令之后,执行命令,并反馈执行结果即可;所以它也是一个线程和一个消息队列。
根据以上分析,由于消息队列是用于Z和A、B、C之间通信的,那么就需要有个统一的消息结构。主要用途是传递工作目标,命令,命令执行结果,部分命令可能需要的参数,另外由于目前的主流云平台下发的命令都带有MID,设备的响应也需要有同样的MID,因此消息中还需要有MID。
据此,消息结构应该如下:
typedef enum
{WORK_NONE,WORK_POWERON,WORK_COLLECT_DATA,WORK_UPLOAD_PR_DATA,//period dataWORK_UPLOAD_AL_DATA,//alarm dataWORK_UPLOAD_OLD_DATA,//old dataWORK_UPLOAD_RESPONSE,//responseWORK_CONTROL_VAVLE,······
}
work_t;typedef enum
{CMD_NONE,CMD_COLLECT_GET_DATA,CMD_COLLECT_GET_DATA_OVER,CMD_COLLECT_SET_METER,CMD_COLLECT_SET_METER_OVER,CMD_COLLECT_CHANGE_MODE,CMD_COLLECT_CHANGE_MODE_OVER,CMD_CONNECT_SEND_DATA,CMD_CONNECT_SEND_DATA_OVER,CMD_CONNECT_SET_PARA,CMD_CONNECT_SET_PARA_OVER,CMD_CONNECT_FOTA,CMD_CONNECT_FOTA_OVER,CMD_CONTROL_VALVE,CMD_CONTROL_VALVE_OVER,······
}
command_t;typedef struct
{work_t work;command_t cmd;uint16 cmd_id;bool cmd_result;void * arg;
}
msg_t;
其中,work_t和cmd_t都应该是枚举量,cmd_result仅仅代表成功和失败。
接下来我们来看一下四个逻辑模块的伪代码实现:
总服务模块Z:
//所有模块的消息队列句柄
osMessageQueueId_t g_service_msg_queue = NULL;
osMessageQueueId_t g_collect_msg_queue = NULL;
osMessageQueueId_t g_connect_msg_queue = NULL;
osMessageQueueId_t g_control_msg_queue = NULL;static uint8 s_sleep_control = 0;
uint8 enable_sleep(void)
{if(s_sleep_control > 0)s_sleep_control--;if(!s_sleep_control){开始准备休眠(启动timer,关闭外设或者其他的休眠前动作);进入休眠;}return s_sleep_control;
}
uint8 disable_sleep(void)
{return ++s_sleep_control;
}void service_thread(void * arg)
{osThreadAttr_t thread_attr = {0}; msg_t get_msg = {0};msg_t put_msg = {0};g_service_msg_queue = osMessageQueueNew(MSG_QUEUE_COUNT_MAX, MSG_QUEUE_MSG_SIZE, NULL);init_attr();init_state();init_data();获取唤醒源;根据唤醒源确定工作目标work;put_msg.work = work;put_msg.cmd = CMD_NONE;osMessageQueuePut(g_service_msg_queue, &put_msg, NULL, osWaitForever);while(1){osMessageQueueGet(g_service_msg_queue, &get_msg, NULL, osWaitForever);switch(get_msg.cmd){case CMD_NONE:{//根据work确定第一个cmd;switch(get_msg.work){case WORK_XXXXXXX:disable_sleep();if(NULL == g_xxxxxxx_msg_queue){创建对应的消息队列g_xxxxxxx_msg_queue;创建对应的线程xxxxxxx_thread; } put_msg.work = get_msg.work;put_msg.cmd = CMD_XXXXXXX;osMessageQueuePut(g_xxxxxxx_msg_queue, &put_msg, NULL, osWaitForever);break;case WORK_XXXXXXX:······break;······default:break;}}break;case CMD_COLLECT_DATA_OVER:{if(!get_msg.result){获取state中的报警标志位;if(需要报警){disable_sleep();if(NULL == g_connect_msg_queue){创建对应的消息队列g_connect_msg_queue;创建对应的线程connect_thread; } put_msg.work = WORK_UPLOAD_AL_DATA;put_msg.cmd = CMD_SEND_DATA;osMessageQueuePut(g_connect_msg_queue, &put_msg, NULL, osWaitForever);}}else{执行采集数据失败的操作;//这里个人建议不要再重复下发采集数据的cmd,//重试应该是在对应的模块中执行的,//这里的失败应该是采集模块已经尽了最大努力仍然失败的意思}//根据work确定下一个cmd;switch(get_msg.work){case WORK_XXXXXXX:if(NULL == g_xxxxxxx_msg_queue){创建对应的消息队列g_xxxxxxx_msg_queue;创建对应的线程xxxxxxx_thread; } put_msg.work = get_msg.work;put_msg.cmd = CMD_XXXXXXX;osMessageQueuePut(g_xxxxxxx_msg_queue, &put_msg, NULL, osWaitForever);break;case WORK_XXXXXXXX:······break;······case WORK_COLLECT_DATA:enable_sleep();break;default:break;}}break;case CMD_SEND_DATA_OVER:{if(!get_msg.result){ if(有旧数据(过去发送失败的数据)){disable_sleep();put_msg.work = WORK_UPLOAD_OLD_DATA;put_msg.cmd = CMD_SEND_DATA;osMessageQueuePut(g_connect_msg_queue, &put_msg, NULL, osWaitForever); }}else{执行发送数据失败的动作;//个人建议,存储发送失败数据的动作不要放在这里,//放在通信模块里,降低耦合}//根据work确定下一个cmd;switch(get_msg.work){case WORK_XXXXXXX:if(NULL == g_xxxxxxx_msg_queue){创建对应的消息队列g_xxxxxxx_msg_queue;创建对应的线程xxxxxxx_thread;} put_msg.work = get_msg.work;put_msg.cmd = CMD_XXXXXXX;osMessageQueuePut(g_xxxxxxx_msg_queue, &put_msg, NULL, osWaitForever);break;case WORK_XXXXXXXX:······break;······case WORK_UPLOAD_PR_DATA:case WORK_UPLOAD_AL_DATA:case WORK_UPLOAD_OLD_DATA:case WORK_UPLOAD_RESPONSE:enable_sleep();break;default:break;}}break;case CMD_CONTROL_VAVLE_OVER:{if(!get_msg.result){执行控制成功的操作;}else{执行控制失败的操作;}//根据work确定下一个cmd;switch(get_msg.work){case WORK_XXXXXXX:if(NULL == g_xxxxxxx_msg_queue){创建对应的消息队列g_xxxxxxx_msg_queue;创建对应的线程xxxxxxx_thread;} put_msg.work = get_msg.work;put_msg.cmd = CMD_XXXXXXX;osMessageQueuePut(g_xxxxxxx_msg_queue, &put_msg, NULL, osWaitForever);break;case WORK_XXXXXXXX:······break;······case WORK_CONTROL_VAVLE:enable_sleep();break;default:break;}}break;default:break;}}
}
上面代码中的work和cmd只是举例,实际是需要根据业务逻辑确定每个work需要由哪些cmd组成及其顺序。
采集模块A:
void collect_thread(void * arg)
{msg_t get_msg = {0};msg_t put_msg = {0};data_t data = {0};state_t state = {0};attr_t attr = {0};收集数据前的准备工作;//启动相应外设,准备数据结构,缓冲区等while(1){osMessageQueueGet(g_collect_msg_queue, &get_msg, NULL, osWaitForever);switch(get_msg.cmd){case CMD_COLLECT_DATA:{ 从相应外设中采集数据至data中;//这里可以加入超时或者重试机制if(采集数据成功){write_data(&data);read_attr(&attr);将数据data与属性attr中的各门限值相比较;if(有超出门限值的情况){更新state数据结构;write_state(&state);} put_msg.cmd_result = 0; }else{put_msg.cmd_result = 1;}put_msg.work = get_msg.work;put_msg.cmd = CMD_COLLECT_DATA_OVER;osMessageQueuePut(g_service_msg_queue, &put_msg, NULL, osWaitForever);}break;default:break;} }
}
通信模块B:
static osMessageQueueId_t s_recv_svr_msg_queue = NULL;
static osMessageQueueId_t s_send_svr_msg_queue = NULL;void recv_svr_thread(void * arg)
{msg_t get_msg = {0};msg_t put_msg = {0};while(1){等待驱动层接收到数据并上报上来;调用协议层接口解析数据;switch(解析结果){根据解析结果,产生对应的work;put_msg.work = WORK_XXXXXXX;put_msg.cmd = CMD_NONE;osMessageQueuePut(g_service_msg_queue, &put_msg, NULL, osWaitForever);}}
}void send_svr_thread(void * arg)
{msg_t get_msg = {0};msg_t put_msg = {0};data_t data = {0};state_t state = {0};attr_t attr = {0};while(1){osMessageQueueGet(s_send_svr_msg_queue, &get_msg, NULL, osWaitForever);switch(get_msg.cmd){case CMD_SEND_DATA:{ switch(get_msg.work){case WORK_UPLOAD_PR_DATA:{read_data(&data);从data中获取数据并按照协议组包;}break;case WORK_UPLOAD_AL_DATA:{read_state(&state);从state中获取报警状态并按照协议组包;}break;case WORK_UPLOAD_OLD_DATA:{从flash中(或其他存储设备中)获取旧数据包;}break;case WORK_UPLOAD_RESPONSE:{根据get_msg.cmd_result,按照协议组包;}break;default:break;}发送数据包,并确定发送结果(由驱动层反馈);//驱动层应有超时机制if(发送成功){put_msg.cmd_result = 0;}else{将组好的数据包存储至flash(或其他掉电保存的设备中);put_msg.cmd_result = 1; }put_msg.work = get_msg.work;put_msg.cmd = CMD_SEND_DATA_OVER;osMessageQueuePut(g_service_msg_queue, &put_msg, NULL, osWaitForever);}break;default:break;}}
}void connect_thread(void * arg)
{osThreadAttr_t thread_attr = {0}; msg_t get_msg = {0};msg_t put_msg = {0};s_recv_svr_msg_queue = osMessageQueueNew(MSG_QUEUE_COUNT_MAX, MSG_QUEUE_MSG_SIZE, NULL);memset(&thread_attr , 0, sizeof(thread_attr));thread_attr.stack_mem = NULL;thread_attr.stack_size = RECV_SRV_THREAD_STACK_SIZE;thread_attr.cb_mem = NULL;osThreadNew(recv_svr_thread, NULL, &thread_attr);s_send_svr_ms_queue = osMessageQueueNew(MSG_QUEUE_COUNT_MAX, MSG_QUEUE_MSG_SIZE, NULL);memset(&thread_attr, 0, sizeof(thread_attr));thread_attr.stack_mem = NULL;thread_attr.stack_size = SEND_SRV_THREAD_STACK_SIZE;thread_attr.cb_mem = NULL;osThreadNew(send_svr_thread, NULL, &thread_attr);进行数据通信前的准备工作,使相关外设上电,联网等;while(1){osMessageQueueGet(g_connect_msg_queue, &get_msg, NULL, osWaitForever);switch(get_msg.cmd){case CMD_SEND_DATA:{ put_msg.work = get_msg.work;put_msg.cmd = get_msg.cmd;osMessageQueuePut(s_send_svr_msg_queue, &put_msg, NULL, osWaitForever);}break;default:break;} }
}
另外,关于发送失败数据的存储方式,是跟读取以及覆盖方式相关的。个人推荐可以在数据包的前后都添加上数据包的长度,这样无论是存储空间满了,需要覆盖最老的数据,还是发送最新的旧数据成功了,需要擦除,都很好操作。
控制模块C:
void control_thread(void * arg)
{msg_t get_msg = {0};msg_t put_msg = {0};state_t state = {0};控制控件前的准备工作;//启动相应外设,准备数据结构,缓冲区等while(1){osMessageQueueGet(g_control_msg_queue, &get_msg, NULL, osWaitForever);switch(get_msg.cmd){case CMD_CONTROL_VAVLE:{ 根据get_msg.arg来控制阀门的状态;根据驱动层的反馈(或调用驱动层的接口)确定是否控制成功;//这里可以加入超时或者重试机制if(控制成功){write_state(&state); put_msg.cmd_result = 0; }else{put_msg.cmd_result = 1;}put_msg.work = get_msg.work;put_msg.cmd = CMD_CONTROL_VAVLE_OVER;osMessageQueuePut(g_service_msg_queue, &put_msg, NULL, osWaitForever);}break;case CMD_XXXXXXX:······break;default:break;} }
}
到这里呢,我们应用层代码的框架应该就设计的差不多了。
本文是对自己做项目思路的一个总结,抛砖引个玉,各位大佬给看看哪里有问题,欢迎斧正,有啥建议也非常欢迎评论区留言,谢谢。
这篇关于简单物联网终端设备的设计思路总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!