卡卷网
当前位置:卡卷网 / 每日看点 / 正文

物联网到底是什么?

作者:卡卷网发布时间:2024-12-09 14:13浏览数量:93次评论数量:0次

一、代码规范

物联网到底是什么?  第1张

上图的工程代码部分截图,用Keil作为开发工具,根据截图框起来部分不难看出项目的代码风格,这里做些总结:

1.函数名、文件名用蛇形命名法,即小写字母加下划线“_”;

2.结构体、枚举变量用帕斯卡命名法,即大小写穿插;

3.驱动文件统一用“drv_”开头,例如drv_sx1278.c、drv_encrypt.c等;

4.应用层文件统一用“app_”开头,例如app_node.c、app_oled96.c;

5.协议栈相关内容都是"Nwk"或“nwk”开头,即network网络的缩写;

6.协议栈相关内容“nwk”后紧跟角色名称,这里的角色名称有三种,分别是“node”节点,“master”网关主机和“slave”网关天线从机;

物联网到底是什么?  第2张

7.全局结构体变量名称用“g_s”开头,g是global全局的意思,s是Struct的意思,例如NwkNodeWorkStruct g_sNwkNodeWork={0};

8.局部指针引用以*p开头+大写字母,例如

NwkNodeSearchStruct *pSearch=&g_sNwkNodeWork.node_search;

9.函数命名尽量用 有效关键字,函数名尽量短、参数尽量少,要让人一眼就知道函数的大概作用,例如 void nwk_node_cad_init(void) 一看就知道是节点的CAD初始化函数;

10.尽量用unsigned char,即u8,不要用char,因为有些编译器char范围是-127~128,有些编译器范围是0~255,如果对数值正负敏感的话要注意处理;

11.对数值范围敏感的话也不要用int,int在不同的单片机宽度不一样,最好根据芯片厂家的代码库来定义,

物联网到底是什么?  第3张

12.宏定义一般都用大写+下划线的方式,同时如果允许用户进行配置的话,要用#ifndef的形式,这样用户就不用在公用文件上改来改去,影响其他项目了;

物联网到底是什么?  第4张

13.结构体要注意四字节对齐,这样可以节省空间,特别是需要创建结构体数组的情况,不对齐会浪费很多空间。

代码命名就像人的形象气质一样,好的规范自然就是帅哥美女了,让人自然而然就想去阅读。写代码写多了后会发现,命名也是个让人很头疼的事情,特别是项目时间紧迫的情况下,没有过多时间考虑,随便命名,甚至用拼音,等项目结束了、回头看跟屎一样。项目里有用了之前的一些旧代码,没有按照规范来,望见

二、基本框架

2.1 目录结构

LoRaSun协议栈可以类比于mqtt开源库,它的作用就是组合、分解数据包,使得数据按照特定的方式进行收发,而用户无需多多关心内在的过程,拿来即用。只不过MQTT针对的物理层是以太网、4G、WiFi等类似网络,LoRaSun针对的是LoRa无线网。

整个协议栈角色有三个,分别是前文提到的终端节点、网关主机、网关天线从机,对应的就有三个Keil工程了,为了便于管理,统一使用STM32F103C8T6作为主控芯片。

物联网到底是什么?  第5张

2.2 公共配置

项目中,有一个三者共用的基础文件nwk_bsp.c,里面包含了协议栈的一些基本定义,比如频率范围、广播参数、协议版本等,还包含了crc16、延时、加密等基本函数;其中有个很重要的是通道表,即SF和BW的组合,一个网络要互通,这个通道表是要保证一致的,在这里为了兼容LLCC68,我选用了最大的SF是11,当然,这个通道表是可以自定义的,要自己权衡速度跟距离之间的关系,一般也不建议搞太多通道,这样网关天线的整体CAD检测效率会降低,降低监听成功率。

物联网到底是什么?  第6张

三、通讯协议

3.1 基础协议

下图是数据包组合函数,里面包含的就是基本的通讯协议:

1.首先是A5开头,相当于帧头,用nwk_bsp文件里的公用函数nwk_find_head查找匹配。

2.后面跟着选项字节opt,opt的每个bit定义在框起来部分中间行,包含了网络角色、加密方式、密码类型以及扩展位,以备不时之需。

3.接下来是发送发的SN、负载长度和负载,其中负载是加密的,前面部分是明文,接收方根据明文部分的信息选择合适的密码和算法进行解密。

4.解密出来后的数据包含了数据长度、目标SN、命令字、包序号和校验码,接收方进行CRC校验,确保数据的正确性,然后对目标SN、命令字和包序号进行二次匹配,以保证数据的正确用途。

物联网到底是什么?  第7张

5.网络角色分为两种,节点和网关,这样接收方就知道数据是哪一方发送来的,需要采取相应的策略,因为协议栈是支持设备间(D2D)发送的,所以这里要进行区分。

3.2 命令字

命令字是很重要的一个字段,他告诉了接收方这个数据包的作用,以下是协议栈支持的命令字,并不复杂:

物联网到底是什么?  第8张

1. 心跳命令属于保留功能,暂时没什么用;

2.回复命令用于协议栈交互结束前的一个确认信号,以保证接收方确实收到数据了;

3. 入网命令这个很容易理解了,由节点主动发起的、申请加入网关,网关会回复入网结果,当前默认都是允许入网的,方便测试,后续还要加入一些更完善的机制,比如网关未配置的节点SN不允许入网、网关管理容量满了不允许入网等等。

4.单包数据命令是核心指令了,用于数据上下行,单包数据长度由nwk_bsp.h中的宏定义NWK_TRANSMIT_MAX_SIZE决定,默认200字节,一般都够用了。

5.连续数据命令用于发送大数据包,其实协议层的交互本质上是一种握手机制,一般应用握手后只发送一次数据就可以了;在一些特殊场合,例如旧表计远程抄表,可能需要发送一些低分辨率图片,如果在握手后进行一次性发送,那么效率会增加,相应的信道也会被占用。这个功能暂未实现。

3.3 加密方式

协议栈支持明文、TEA加密和AES三种加密模式,明文为了方便学习调试,查看发送内容;TEA加密单元是8字节,数据没对齐的时候冗余数据比较小,同时加密算法比较简单高效,占用空间小;AES加密采用AES-CBC模式,加密单元16字节,安全性高,对应的空间开销也更大,ROM:5KB左右。

物联网到底是什么?  第9张

3.4 密码类型

密码类型分为根密码和应用密码,根密码属于初始密码,系统入网时使用,因为这时候只有根密码;入网时,网关返回的数据包内包含一个随机数组,节点会根据事先约定好的算法对这个数组进行运算,形成应用密码,随后节点与网关通讯都会使用这个应用密码,增加安全性。

应用密码的主动性在于节点,为了进一步增加安全性,节点可以定时入网更新密码,比如每天更新。

至于约定的算法,也可以自定义,这里简单的就用TEA算法和指定密码加密一下。

物联网到底是什么?  第10张

四、广播搜网

4.1 广播逻辑

有了基本的通讯协议后,就可以进行协议层间的交互通讯,整体的交互流程是网关广播--节点搜网--入网--数据收发;所以,第一步就是广播部分的代码。

物联网到底是什么?  第11张

两个关键内容是广播偏移和广播周期,广播偏移之前讲过了,是为了避免同一区域内的网关干扰,让网关按顺序广播;广播周期暂定为2分钟,这是一个比较合适的周期。

4.2 核心参数

下图是具体的广播内容,框起来部分是网关的核心信息,包含起始频段、运行模式、广播天线ID和天线数量;起始频段可以保证在区域内的网关信道不会冲突,最大化利用国内的免费频段。

物联网到底是什么?  第12张

4.3 运行模式

运行模式分为静态和动态模式,类似于路由器的动态IP和静态IP,动态模式就是用上一篇文章所述的CAD嗅探原理,单根天线就能满足自适应速率的需求,对应的弊端是CAD会导致距离缩水。静态模式本质上就是有自组网能力的点对点通讯,距离跟点对点一样,需要多跟天线配合才能实现自适应速率的需求。在部署时,可以使用 “静态为主、动态为辅” 的策略,在几个中心点部署多天线的静态模式网关,边缘盲区部署单天线的动态网关,实现低成本、快速地无盲区覆盖。

4.4 时间同步

时间同步是件麻烦事,即便是秒级同步。如果直接用LoRa把当前时间直接广播出去会有两个问题,一个是LoRa发送时间比较长,按照默认参数SF11/BW6 需要1.3秒左右,这个误差就很大了;另一个是读取当前时刻的秒时间戳不一定是对齐的,可能是0.5秒,如果这个不消除,那么就有初始的0.5秒偏差了,留给晶振的容差空间就更小了。

为了解决这个问题,首先要尽量保证发送前读取时间戳的时刻要秒对齐,这个比较好操作,就是增加读取频率,一旦时间戳满足周期的倍数(比如默认的120),即可认为是对齐的,该任务大概是10ms运行一次,所以初始对齐误差也是在10ms以内,完全可以接受。

第二步是如何消除发送时间偏差,这里就有点小技巧了,我选用延时策略来保证发送秒对齐,具体看下图指示部分。首先计算空中时间,这个是比较准确的,误差就几十ms;然后判断这个空中时间是否为整秒数,一般都不是的,目前大概是1.3秒(16字节数据);接下来就是算延时的时间了,先求空中时间的余数(这里>50意思是多余50ms以上才需要对齐调整),1000减去这个余数就是要延时的毫秒数了;最后就是强制延时,硬凑对齐。那么,最终广播出去的时间就把发送时间和这个对齐的时间差补上就行了,下图箭头所示。这样同步的时间误差可以控制在100ms以内,再详细测试应该还能进一步精确。

物联网到底是什么?  第13张

4.5 天线

广播选用的天线 理论上是谁有空就用谁,目前简单处理都是用第一根天线;在这种多天线网关系统里,天线的数量决定了网关的容量,是一个很重要的参数,这样节点才能综合评估上行数据的策略——大家尽量往信号好的、天线多的网关发送。至于节点处的策略目前还不完善,完全是随机化选择的。

4.6 节点搜网

节点搜网触发条件主要有以下三个:一是刚开机,二是时间未同步,三是搜网周期到达。其中搜网周期可以应用层配置,包含周期和搜网时间,目前为了测试效率这两个参数都填得很小,周期是120秒,搜网时间是10秒;理论上设置为至少每天搜网2分钟比较合适,这样可以定时更新网关信息,当然,这个根据具体应用来确定。

物联网到底是什么?  第14张

搜网任务

物联网到底是什么?  第15张

搜网参数配置

五、数据上行

5.1 发送任务

数据上行是指节点发送数据到网关,即下图所示函数,他作为一个模块按需运行,有发送任务了才会运行。

物联网到底是什么?  第16张

发送接口由下面函数提供,给应用层调用。

物联网到底是什么?  第17张

5.2 静态发送

静态发送较为简单,就是选择合适的网关、合适的天线,直接先发送一个抢占包,这个抢占包格式固定、内含SN信息、数据比较短,目的是先占用目标天线;如果该天线回应了,说明抢占成功,如果等待回应超时,那就是抢占失败,随机延时再度抢占。

网关天线回应后就可以发送应用数据了,收到确认信号后就说明发送成功了,通知应用层发送成功,结束回合,整体逻辑比较简洁清晰。下面贴一些相关代码,具体看工程比较好理解。

if(pGateWay->run_mode==NwkRunModeStatic)//静态模式 { pNodeTxGw->wireless_ptr=0; if(pGateWay->wireless_num>0) { pNodeTxGw->wireless_ptr=nwk_get_rand()%pGateWay->wireless_num;//随机选择天线 // printf("wireless_ptr=%d\n", pNodeTxGw->wireless_ptr); } printf("wireless_ptr=%d\n", pNodeTxGw->wireless_ptr); printf_oled("wireless_ptr=%d\n", pNodeTxGw->wireless_ptr); pNodeTxGw->tx_state=NwkNodeTxStaticInit;//下一步,静态参数初始化 }


case NwkNodeTxStaticInit://静态参数初始化 { u8 sf=0, bw=0; nwk_get_static_channel4(pNodeTxGw->wireless_ptr, &sf, &bw); u8 freq_ptr=pNodeTxGw->pGateWay->base_freq_ptr + pNodeTxGw->wireless_ptr*NWK_GW_SPACE_FREQ;//计算频率序号,每根天线间隔2MHz pNodeTxGw->freq=NWK_GW_BASE_FREQ+freq_ptr*1000000;//目标天线监听频率 pNodeTxGw->sf=sf; pNodeTxGw->bw=bw; nwk_node_set_lora_param(pNodeTxGw->freq, sf, bw); printf("NwkNodeTxStaticInit P(%.2f, %d, %d)\n", pNodeTxGw->freq/1000000.f, pNodeTxGw->sf, pNodeTxGw->bw); u8 first_buff[20]={0}; u8 first_len=0; first_buff[first_len++]=0xA7; first_buff[first_len++]=g_sNwkNodeWork.node_sn>>24; first_buff[first_len++]=g_sNwkNodeWork.node_sn>>16; first_buff[first_len++]=g_sNwkNodeWork.node_sn>>8; first_buff[first_len++]=g_sNwkNodeWork.node_sn; first_buff[first_len++]=pNodeTxGw->tx_len;//发送长度 first_buff[first_len++]=nwk_get_rand();//随机数1 first_buff[first_len++]=nwk_get_rand();//随机数2 这里暂且明文发送 nwk_node_send_buff(first_buff, first_len);//发送抢占包 u32 tx_time=nwk_calcu_air_time(pNodeTxGw->sf, pNodeTxGw->bw, first_len);//发送时间,冗余 pNodeTxGw->start_rtc_time=nwk_get_rtc_counter();//记录当前时间,防止超时 pNodeTxGw->wait_cnts=tx_time/1000+1;//等待秒数 pNodeTxGw->tx_state=NwkNodeTxStaticFirstCheck; printf("send first buff! tx_time=%ums\n", tx_time); break; } case NwkNodeTxStaticFirstCheck://发送抢占包检测 { u32 now_time=nwk_get_rtc_counter(); u8 result=nwk_node_send_check();//发送完成检测 if(result)//发送完成 { printf("tx_first ok!\n"); pNodeTxGw->freq=nwk_get_sn_freq(g_sNwkNodeWork.node_sn);//根据序列号计算频段 nwk_node_set_lora_param(pNodeTxGw->freq, pNodeTxGw->sf, pNodeTxGw->bw); printf("into recv, wait ack, P(%.2f, %d, %d)\n", pNodeTxGw->freq/1000000.f, pNodeTxGw->sf, pNodeTxGw->bw); nwk_node_recv_init();//进入接收,等待回复 u32 tx_time=nwk_calcu_air_time(pNodeTxGw->sf, pNodeTxGw->bw, 20);//接收回复包等待时间 pNodeTxGw->start_rtc_time=nwk_get_rtc_counter();//记录当前时间,防止超时 pNodeTxGw->wait_cnts=tx_time/1000+2; pNodeTxGw->tx_state=NwkNodeTxStaticFirstAck; } else if(now_time-pNodeTxGw->start_rtc_time>pNodeTxGw->wait_cnts)//发送超时 { printf("tx first time out! wait time=%ds\n", pNodeTxGw->wait_cnts); pNodeTxGw->tx_state=NwkNodeTxStaticExit; } break; } case NwkNodeTxStaticFirstAck://抢占包回复检测 { u32 now_time=nwk_get_rtc_counter(); u8 recv_len=nwk_node_recv_check(g_sNwkNodeWork.node_rx.recv_buff, &g_sNwkNodeWork.rf_param); if(recv_len>0) { //数据解析 u8 *pBuff=g_sNwkNodeWork.node_rx.recv_buff; printf("first ack rssi=%ddbm, snr=%ddbm\n", g_sNwkNodeWork.rf_param.rssi, g_sNwkNodeWork.rf_param.snr); printf_hex("ack=", pBuff, recv_len); u8 head[1]={0xA7}; u8 *pData=nwk_find_head(pBuff, recv_len, head, 1); if(pData) { pData+=1; u32 dst_sn=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3]; pData+=4; printf("dst_sn=0x%08X\n", dst_sn); if(dst_sn==g_sNwkNodeWork.node_sn) { nwk_node_send_buff(make_buff, make_len);//发送数据包 u32 tx_time=nwk_calcu_air_time(pNodeTxGw->sf, pNodeTxGw->bw, make_len);//发送时间,冗余 pNodeTxGw->start_rtc_time=nwk_get_rtc_counter();//记录当前时间,防止超时 pNodeTxGw->wait_cnts=tx_time/1000+2;//等待秒数 pNodeTxGw->tx_state=NwkNodeTxStaticAppCheck; printf("send app buff\n"); printf_oled("tx app wire=%d\n", pNodeTxGw->wireless_ptr); } else { printf("sn error!\n"); pNodeTxGw->tx_state=NwkNodeTxStaticExit; } } } else if(now_time-pNodeTxGw->start_rtc_time>pNodeTxGw->wait_cnts)//超时,可能没有抢占到 { printf("wait first ack time out!\n"); pNodeTxGw->tx_state=NwkNodeTxStaticExit; } break; } case NwkNodeTxStaticAppCheck://发送数据包检测 { u32 now_time=nwk_get_rtc_counter(); u8 result=nwk_node_send_check();//发送完成检测 if(result)//发送完成 { printf("tx app ok, recv ack!\n"); nwk_node_recv_init();//进入接收,等待回复 u32 tx_time=nwk_calcu_air_time(pNodeTxGw->sf, pNodeTxGw->bw, 20);//接收回复包等待时间 pNodeTxGw->start_rtc_time=nwk_get_rtc_counter();//记录当前时间,防止超时 pNodeTxGw->wait_cnts=tx_time/1000+2; pNodeTxGw->tx_state=NwkNodeTxStaticAppAck; } else if(now_time-pNodeTxGw->start_rtc_time>pNodeTxGw->wait_cnts)//发送超时 { printf("tx app time out! wait time=%ds\n", pNodeTxGw->wait_cnts); pNodeTxGw->tx_state=NwkNodeTxStaticExit; } break; } case NwkNodeTxStaticAppAck://等待网关回复确认 { u32 now_time=nwk_get_rtc_counter(); u8 recv_len=nwk_node_recv_check(g_sNwkNodeWork.node_rx.recv_buff, &g_sNwkNodeWork.rf_param); if(recv_len>0) { //数据解析 printf("tx ack rssi=%ddbm, snr=%ddbm\n", g_sNwkNodeWork.rf_param.rssi, g_sNwkNodeWork.rf_param.snr); srand(g_sNwkNodeWork.rf_param.rssi); printf_hex("ack=", g_sNwkNodeWork.node_rx.recv_buff, recv_len); nwk_node_recv_parse(g_sNwkNodeWork.node_rx.recv_buff, recv_len); printf_oled("*tx ok! wire=%d", pNodeTxGw->wireless_ptr); } else if(now_time-pNodeTxGw->start_rtc_time>pNodeTxGw->wait_cnts)//超时 { printf("wait ack time out!\n"); pNodeTxGw->tx_state=NwkNodeTxGwExit; } break; } case NwkNodeTxStaticExit://静态退出 { pNodeTxGw->try_cnts++; printf("static try_cnts=%d\n", pNodeTxGw->try_cnts); if(pNodeTxGw->try_cnts>=3)//结束发送 { nwk_node_clear_tx(); pNodeTxGw->alarm_rtc_time=0xFFFFFFFF;//可以进入休眠 } else { u32 now_time=nwk_get_rtc_counter(); pNodeTxGw->wait_cnts=nwk_get_rand()%10;//随机延时,再次尝试 pNodeTxGw->start_rtc_time=now_time; pNodeTxGw->alarm_rtc_time=now_time+pNodeTxGw->wait_cnts;//闹钟时间 if(pNodeTxGw->wireless_ptr>=pNodeTxGw->pGateWay->wireless_num)//结束发送 { nwk_node_clear_tx(); pNodeTxGw->alarm_rtc_time=0xFFFFFFFF;//可以进入休眠 printf_oled("static tx failed!"); } else { printf("tx wait time=%ds\n", pNodeTxGw->wait_cnts); printf_oled("tx wait time=%ds\n", pNodeTxGw->wait_cnts); } printf("static alarm time=%us\n", pNodeTxGw->alarm_rtc_time); } nwk_node_set_led(false);//指示灯灭 pNodeTxGw->tx_state=NwkNodeTxGwIdel;//回合结束 break; }

5.3 动态发送

动态发送麻烦一些,要进行速率自适应操作。选定合适的参数后,首先发送嗅探帧,所谓嗅探帧如下图所示,发送一字节,5ms后强制停止。

物联网到底是什么?  第18张

嗅探10次,增加成功率,嗅探结束后立马以增加1M频率的参数进入CAD检测回复信号,例如嗅探是(470.25, 11, 8),那么监听参数就是(471.25, 11, 8),跳频减少干扰。回复监听次数是10次,也是为了增加成功率,因为两者在节奏上没那么合拍,就需要增加次数的方式提高成功率,这也是跟单纯专利方法的区别,理论和实践上的差距。

物联网到底是什么?  第19张

如果CAD有回复的话,说明与目标天线握手成功了,就可以进行数据发送了,如果这次不成功,那么就再尝试握手一次,即多次嗅探,增加成功率,整个过程其实很快,一轮几百ms时间,随着通道ID增加,时间也会增加,所以后面的轮次也减少了。

物联网到底是什么?  第20张

无论静态模式还是动态模式,网关天线从机都有对应的任务配合节点传输,从天线从机角度看,那就是接收函数了,如下图所示:

物联网到底是什么?  第21张

如果搜索到CAD信号就会返回嗅探帧并进入接收模式,如下图所示。

物联网到底是什么?  第22张

握手成功后,就进入应用数据发送了,节点首先会发送一个前导包,里面包含将要发送数据的长度,这样网关天线这边就可以明确自己要等待的时间了,如果直接发送应用数据,因为干扰等原因又没收到,那么网关天线就要按最长数据发送时间等待,很浪费时间。

节点发送完前导包后会立马发送应用数据包,顺利的话,等待网关回复确认信号就可以完成本回合的发送任务了,通知应用层发送成功。

物联网到底是什么?  第23张

5.4 下行尾随

对于唤醒周期为0xFFFF的节点,本质上是不进行唤醒监听的,对于这类设备要发送下行数据的话只能是主动上报的时候顺带下发,为了实现这一功能,网关在回复上行数据包的时候会检查该设备是否有缓存的下行数据,如果有的话会把数据包长度写入,这样节点就知道自己要等多久了,同时将下行包打包好,一同传给天线从机,从机发送完回复包后会继续发送应用数据包,完成下行数据的发送。这一功能适用于所有类型的节点,不一定是休眠设备。

物联网到底是什么?  第24张

六、数据下行

6.1 网关下行管理

下行数据根据节点休眠周期分为三种:不休眠、周期唤醒和长休眠。对于长休眠设备刚才5.4节已经描述过了,周期唤醒和不休眠设备有点类似,区别在于周期唤醒设备要多个唤醒步骤,发送唤醒嗅探帧。这一步在天线从机的发送函数里完成。

物联网到底是什么?  第25张

对于网关主机来讲,它要根据节点的唤醒周期来判断能否下发,首先在间隔上不能太频繁,影响系统稳定,这里是要间隔10秒的;其次是时间戳是否为唤醒周期的整数倍,箭头指示的时间+1目的是提前1秒发送,确保不会错过。

物联网到底是什么?  第26张

物联网到底是什么?  第27张

6.2 从机发送

从机接收到下行数据指令后,根据唤醒标志决定是否发送唤醒包,针对不休眠设备可以省去这一步,节约时间。发送完唤醒帧后就直接发送匹配包,告诉被唤醒的节点:网关这次是要跟谁通讯,无关的节点继续休眠。因为如果唤醒周期一样的话是有可能会被同时唤醒的,为了减少这种情况,唤醒周期尽量在某个范围内用随机值代替,比如阀门的唤醒周期可以5~10秒之间,那就节点自行产生5~10的随机数作为唤醒周期,入网后就不需要改变了。

另外,下行唤醒的频率是根据节点的SN计算的,会有冲突的可能性,但是加上随机的唤醒周期,同频又同周期的可能性就大大减少了。

物联网到底是什么?  第28张

发送完匹配包后,就进入速率自适应阶段了,这个流程跟上行数据时是一样的,只不过角色互换了,现在是天线从机发送嗅探包,节点监听扫描,整个流程是一致的。匹配成功后会直接发送应用数据包,最后就是等待回复确认信号了,天线从机方面下行任务完成。

物联网到底是什么?  第29张

在网关主机方面,这里配合端点物联APP,还会把下发结果上报给APP端,主要有下图几种状态,如果成功发送就是返回下发成功。

物联网到底是什么?  第30张

6.3 节点接收

在节点端,如果SN匹配的话就会进入速率匹配阶段,同时也获取到了将要发送的应用数据长度,这在后续中会用到,可以明确等待时间。

物联网到底是什么?  第31张

速率匹配成功后就进入接收模式了,根据之前收到的待发送数据长度确定等待时间。如果后续收到数据解析正确,就会返回确认包,完成下行接收任务。

物联网到底是什么?  第32张

七、设备间发送(D2D)

设备间发送本质就是让发送方充当临时网关,在软件和硬件上都很好兼容,而传统的LoRaWAN协议是没有D2D能力的,后面好像阿里有个补充协议,有点曲线救国了。D2D在设备联动的场景比较有用,实时性和可靠性都提升了。

这部分代码发送和接收都是在节点端,接收部分是复用的,就是增加个D2D发送程序。发送前首先应用层要添加数据包,同时携带目标SN和唤醒周期参数,如下图所示:

物联网到底是什么?  第33张

确保无误后,在主任务中检查周期是否到达,到达后进入发送流程。

物联网到底是什么?  第34张

随后流程跟下行数据一样,唤醒--SN匹配--速率自适应--发送应用数据--回复确认,具体可以看项目里的代码,这里就不贴了。

物联网到底是什么?  第35张

八、OLED屏幕

8.1 OLED驱动

为了方便节点调试,除了增加电池供电的以外,还增加了一个OLED屏幕,可以展示节点的状态信息,具体内容在第二篇中已经有详细解释了,这里主要讲解下代码。

代码包含驱动和应用两部分,这个屏幕通讯接口采用IIC,为了增加速度,采用了硬件IIC,下图是两个相关文件。

物联网到底是什么?  第36张

驱动层主要是对屏幕的寄存器进行设置,比如下面的初始化函数,这个根据厂家的代码来就行了。

物联网到底是什么?  第37张

为了增加可移植性,IIC相关的代码都是在应用层写的,驱动层只是提供注册接口,把相关函数注册即可。

物联网到底是什么?  第38张

其他就是功能性函数了,根据厂家提供的代码改下命名风格就行了。

物联网到底是什么?  第39张

8.2 OLED应用

下图是OLED的IIC初始化和写函数,网上有相关代码,或者看ST的demo库。

物联网到底是什么?  第40张

剩下的就是根据应用需求,在指定位置显示指定内容,像这里就是显示节点信息、信号强度、时间等信息了。

物联网到底是什么?  第41张

这里有个函数需要提下,就是下图所示函数,他是用来显示打印信息的,类似于串口的printf。但是因为效率原因,没有直接在函数内显示,而是将待显示的字符串先缓存,然后在自己的显示线程里打印。因为这个函数主要是要显示协议栈运行过程的关键信息,比如接收到的数据,发送状态等等,这是在协议栈内调用的,如果直接显示,由于IIC速度较慢,会造成协议栈运行阻塞,导致CAD检测成功率下降。

物联网到底是什么?  第42张

为了避免这个问题,我在OLED应用程序里定义了一个结构体,用于缓存节点状态信息,在协议栈线程中将这些信息缓存过来,由于显示任务的优先级比较低,这样就不会影响协议栈运行了。

物联网到底是什么?  第43张

缓存结构体

物联网到底是什么?  第44张

缓存状态信息

物联网到底是什么?  第45张

显示任务

九、应用配置

9.1 节点配置

节点配置主要在这个函数里,自己根据注释和需求修改,SN和唤醒周期可以通过串口修改并保存,搜索周期和时长只能代码修改了。根密码要跟网关保持一致,这里只是举例,比较随意。

物联网到底是什么?  第46张

9.2 网关配置

网关配置在这里修改,内容比较少,包括起始频段、运行模式和广播偏移,其中起始频段和运行模式可以利用端点物联APP修改。

物联网到底是什么?  第47张

物联网到底是什么?  第48张

9.3 从机配置

从机没什么配置内容,就一个从机地址,可以通过调试串口更改,出厂时已经设置好,必须严格按照PCB上的序号顺序设置,所以一般也不需要动。

所以,LoRaSun协议栈的使用是极其简单的,应用层基本也无需怎么配置。

十、节点运行逻辑

10.1 任务模块

有了以上那些程序模块以后,剩下的问题就是如何把它们串起来,流畅运行了。具体看下面这个函数:

/* ================================================================================ 描述 : 工作状态检测 输入 : 输出 : ================================================================================ */ void nwk_node_work_check(void) { static NwkNodeTxGwStruct *pNodeTxGw=&g_sNwkNodeWork.node_tx_gw; static NwkNodeTxD2dStruct *pNodeD2d=&g_sNwkNodeWork.node_tx_d2d; static NwkNodeRxStruct *pNodeRx=&g_sNwkNodeWork.node_rx; static NwkNodeSearchStruct *pNodeSearch=&g_sNwkNodeWork.node_search; switch(g_sNwkNodeWork.work_state) { case NwkNodeWorkIdel://空闲 { //网关数据检查 if(pNodeTxGw->tx_len>0)//是否有网关数据需要发送 { u32 now_time=nwk_get_rtc_counter(); if(now_time-pNodeTxGw->start_rtc_time>=pNodeTxGw->wait_cnts)//随机延时结束 { printf("tx gw wait ok! time=%us\n", now_time); pNodeTxGw->alarm_rtc_time=0; pNodeTxGw->tx_state=NwkNodeTxGwInit;//重新开始 g_sNwkNodeWork.work_state=NwkNodeWorkTXGw; } } //D2D数据检查 if(g_sNwkNodeWork.work_state==NwkNodeWorkIdel)//仍旧空闲--D2D { if(pNodeD2d->tx_len>0)//是否有D2D数据需要发送 { u32 now_time=nwk_get_rtc_counter(); u16 wake_period=pNodeD2d->wake_period; if(wake_period==0) wake_period=1; if((now_time+1)%wake_period==0)//唤醒周期到 提前1秒 { printf("tx d2d wait ok! time=%us\n", now_time); pNodeD2d->alarm_rtc_time=0; pNodeD2d->d2d_state=NwkNodeTxD2dInit;//开始 g_sNwkNodeWork.work_state=NwkNodeWorkTXD2d; } } } if(g_sNwkNodeWork.work_state==NwkNodeWorkIdel)//仍旧空闲--入网检查 { u32 now_time=nwk_get_rtc_counter(); for(u8 i=0; i<NWK_GW_NUM; i++) { NwkParentWorkStrcut *pGateWay=&g_sNwkNodeWork.parent_list[i]; if(pGateWay->gw_sn>0) { if(pGateWay->wait_join_time==0) { pGateWay->wait_join_time=nwk_get_rand()%10+3;//首次入网等待时间 pGateWay->last_join_time=now_time; } if(pGateWay->join_state==JoinStateNone && now_time-pGateWay->last_join_time>pGateWay->wait_join_time) { printf("pGateWay=0x%08X, join_state=%d\n", pGateWay->gw_sn, pGateWay->join_state); pGateWay->wait_join_time=nwk_get_rand()%60+60; pGateWay->last_join_time=now_time; nwk_node_req_join(pGateWay->gw_sn);//请求入网 return; } } } } //搜索检查 if(g_sNwkNodeWork.work_state==NwkNodeWorkIdel)//仍旧空闲--搜索 { u32 now_time=nwk_get_rtc_counter(); if(pNodeSearch->search_start_time==0 || //起始 now_time<TIME_STAMP_THRESH || //时间未同步 (pNodeSearch->period>0 && now_time%pNodeSearch->period==0) )//周期到达 { pNodeSearch->search_start_time=now_time; pNodeSearch->search_state=NwkNodeSearchInit; g_sNwkNodeWork.work_state=NwkNodeWorkSearch;//进入搜索状态 } } //接收检查 if(g_sNwkNodeWork.work_state==NwkNodeWorkIdel)//仍旧空闲--接收 { if(g_sNwkNodeWork.wake_period==0)//无需休眠 { pNodeRx->alarm_rtc_time=0;//不休眠 pNodeRx->rx_state=NwkNodeRxInit; g_sNwkNodeWork.work_state=NwkNodeWorkRX; //进入接收监听 } else if(g_sNwkNodeWork.wake_period==0xFFFF)//无需监听 { pNodeRx->alarm_rtc_time=0xFFFFFFFF;//不唤醒 } else { static u32 last_wake_time=0;//上次唤醒时间,避免单次周期内重复唤醒 u32 now_time=nwk_get_rtc_counter(); if(now_time%g_sNwkNodeWork.wake_period==0 && now_time!=last_wake_time) { printf("wake!!! time=%us\n", now_time); // printf_oled("wake!!! time=%us\n", now_time); pNodeRx->rx_state=NwkNodeRxInit; pNodeRx->alarm_rtc_time=now_time+g_sNwkNodeWork.wake_period;//下一次唤醒周期 g_sNwkNodeWork.work_state=NwkNodeWorkRX;//进入接收监听 last_wake_time=now_time; } } } break; } /******************************/ case NwkNodeWorkSearch://搜索 1 { nwk_node_search_process(); if(pNodeSearch->search_state==NwkNodeSearchIdel)//单回合结束 { g_sNwkNodeWork.work_state=NwkNodeWorkIdel; } break; } case NwkNodeWorkRX://接收 2 { nwk_node_rx_process(); if(pNodeRx->rx_state==NwkNodeRxIdel)//单回合结束 { g_sNwkNodeWork.work_state=NwkNodeWorkIdel; } break; } case NwkNodeWorkTXGw://发送到网关 3 { nwk_node_tx_gw_process(); if(pNodeTxGw->tx_state==NwkNodeTxGwIdel)//单回合结束 { g_sNwkNodeWork.work_state=NwkNodeWorkIdel; } break; } case NwkNodeWorkTXD2d://发送到设备 4 { nwk_node_tx_d2d_process(); if(pNodeD2d->d2d_state==NwkNodeTxD2dIdel)//单回合结束 { g_sNwkNodeWork.work_state=NwkNodeWorkIdel; } break; } } }

简单讲就是根据任务的优先级,配合状态机运行各个子模块。根据代码,在空闲时会检查各个模块的状态,其中上行发送是第一优先级,然后是D2D、入网、搜网,最后才是接收任务。各个子模块结束后都会切换到空闲状态,主任务检测到子模块空闲后就会进入整体空闲,检查各个模块的任务状态。

如下图所示,主任务有自己的状态,各个模块也有自己的状态。

物联网到底是什么?  第49张

物联网到底是什么?  第50张

10.2 闹钟计算

对于需要休眠的节点,协议栈要告诉应用层什么时候要休眠、什么时候要唤醒,因为一个系统还有其他外设模块和功能需求,应用层要综合所有外设的休眠需求决定休眠、唤醒时刻。这里核心来讲就是一个唤醒闹钟,只要告诉应用层需要在什么时刻叫醒我就行了,我协议栈先去睡觉了,其他的你们自己看着办吧。协议栈的唤醒闹钟需要综合子模块的闹钟才能计算出来,所以在每个功能模块的结构体里都有一个闹钟时间,主任务把他们中最小那个记录下来报告给应用层就行了。

物联网到底是什么?  第51张

结构体里的闹钟

物联网到底是什么?  第52张

节点唤醒时间计算

在应用层,进行了简单的唤醒休眠操作,由于这个测试板不适合低功耗测试,就没有进行整体的低功耗配置了。低功耗开发需要软硬件配合,有点难度,后续会出相关项目教程。

物联网到底是什么?  第53张

10.3 事件通知

节点入网、数据发送结果都会通过事件的方式通知应用层,让应用层进行相应的处理,其实原理很简单,就是定义一个结构体,包含事件类型和参数,不同的事件事先定义好,有什么参数也都知道,这样在协议栈内部对结构体赋值,应用层通过检查这个结构体变量就能知道协议栈内部通知的事件了。下面是事件结构体和事件类型,以及应用层的检查处理函数。

物联网到底是什么?  第54张

事件结构体和类型

物联网到底是什么?  第55张

应用层事件处理函数

对于网关,道理其实也是一样的。

十一、总结

这篇文章比较长,基本上把协议栈的逻辑和内容解释一遍了,细节的东西实在没法写,可能后续会出个视频讲解下,会通过公众号通知。

在理解了上一篇专利的基础上,代码理解起来应该也很简单的,主要是内容比较多,单纯节点协议栈的代码也有两千多行了,细节的东西只能代码慢慢看了。

后面如果有时间就继续讲解下LoRa硬件的移植和驱动过程,同时了解下什么是扩频原理,毕竟这一优势大家只是口口相传,很少有人深入数学和通讯原理去讲解的。

最后需要声明一点的是,本协议栈完全是个人兴趣爱好驱动完成的,并没有要去横向对比LoRaWAN等成熟的协议栈,这里仅提供一个自组网思路,以供学习研究使用。



本专栏地址:物联网实战--LoRa自组网(LoRaSun)_端点物联的博客-CSDN博客

本人的其它文章导航地址:端点物联网学习资源合集-CSDN博客

关注VX公众号 端点物联,以便即时接收文章更新信息。

学习交流QQ群: 701889554

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码