物联网到底是什么?
作者:卡卷网发布时间:2024-12-09 14:13浏览数量:93次评论数量:0次
一、代码规范
上图的工程代码部分截图,用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”网关天线从机;
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在不同的单片机宽度不一样,最好根据芯片厂家的代码库来定义,
12.宏定义一般都用大写+下划线的方式,同时如果允许用户进行配置的话,要用#ifndef的形式,这样用户就不用在公用文件上改来改去,影响其他项目了;
13.结构体要注意四字节对齐,这样可以节省空间,特别是需要创建结构体数组的情况,不对齐会浪费很多空间。
代码命名就像人的形象气质一样,好的规范自然就是帅哥美女了,让人自然而然就想去阅读。写代码写多了后会发现,命名也是个让人很头疼的事情,特别是项目时间紧迫的情况下,没有过多时间考虑,随便命名,甚至用拼音,等项目结束了、回头看跟屎一样。项目里有用了之前的一些旧代码,没有按照规范来,望见
二、基本框架
2.1 目录结构
LoRaSun协议栈可以类比于mqtt开源库,它的作用就是组合、分解数据包,使得数据按照特定的方式进行收发,而用户无需多多关心内在的过程,拿来即用。只不过MQTT针对的物理层是以太网、4G、WiFi等类似网络,LoRaSun针对的是LoRa无线网。
整个协议栈角色有三个,分别是前文提到的终端节点、网关主机、网关天线从机,对应的就有三个Keil工程了,为了便于管理,统一使用STM32F103C8T6作为主控芯片。
2.2 公共配置
项目中,有一个三者共用的基础文件nwk_bsp.c,里面包含了协议栈的一些基本定义,比如频率范围、广播参数、协议版本等,还包含了crc16、延时、加密等基本函数;其中有个很重要的是通道表,即SF和BW的组合,一个网络要互通,这个通道表是要保证一致的,在这里为了兼容LLCC68,我选用了最大的SF是11,当然,这个通道表是可以自定义的,要自己权衡速度跟距离之间的关系,一般也不建议搞太多通道,这样网关天线的整体CAD检测效率会降低,降低监听成功率。
三、通讯协议
3.1 基础协议
下图是数据包组合函数,里面包含的就是基本的通讯协议:
1.首先是A5开头,相当于帧头,用nwk_bsp文件里的公用函数nwk_find_head查找匹配。
2.后面跟着选项字节opt,opt的每个bit定义在框起来部分中间行,包含了网络角色、加密方式、密码类型以及扩展位,以备不时之需。
3.接下来是发送发的SN、负载长度和负载,其中负载是加密的,前面部分是明文,接收方根据明文部分的信息选择合适的密码和算法进行解密。
4.解密出来后的数据包含了数据长度、目标SN、命令字、包序号和校验码,接收方进行CRC校验,确保数据的正确性,然后对目标SN、命令字和包序号进行二次匹配,以保证数据的正确用途。
5.网络角色分为两种,节点和网关,这样接收方就知道数据是哪一方发送来的,需要采取相应的策略,因为协议栈是支持设备间(D2D)发送的,所以这里要进行区分。
3.2 命令字
命令字是很重要的一个字段,他告诉了接收方这个数据包的作用,以下是协议栈支持的命令字,并不复杂:
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左右。
3.4 密码类型
密码类型分为根密码和应用密码,根密码属于初始密码,系统入网时使用,因为这时候只有根密码;入网时,网关返回的数据包内包含一个随机数组,节点会根据事先约定好的算法对这个数组进行运算,形成应用密码,随后节点与网关通讯都会使用这个应用密码,增加安全性。
应用密码的主动性在于节点,为了进一步增加安全性,节点可以定时入网更新密码,比如每天更新。
至于约定的算法,也可以自定义,这里简单的就用TEA算法和指定密码加密一下。
四、广播搜网
4.1 广播逻辑
有了基本的通讯协议后,就可以进行协议层间的交互通讯,整体的交互流程是网关广播--节点搜网--入网--数据收发;所以,第一步就是广播部分的代码。
两个关键内容是广播偏移和广播周期,广播偏移之前讲过了,是为了避免同一区域内的网关干扰,让网关按顺序广播;广播周期暂定为2分钟,这是一个比较合适的周期。
4.2 核心参数
下图是具体的广播内容,框起来部分是网关的核心信息,包含起始频段、运行模式、广播天线ID和天线数量;起始频段可以保证在区域内的网关信道不会冲突,最大化利用国内的免费频段。
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以内,再详细测试应该还能进一步精确。
4.5 天线
广播选用的天线 理论上是谁有空就用谁,目前简单处理都是用第一根天线;在这种多天线网关系统里,天线的数量决定了网关的容量,是一个很重要的参数,这样节点才能综合评估上行数据的策略——大家尽量往信号好的、天线多的网关发送。至于节点处的策略目前还不完善,完全是随机化选择的。
4.6 节点搜网
节点搜网触发条件主要有以下三个:一是刚开机,二是时间未同步,三是搜网周期到达。其中搜网周期可以应用层配置,包含周期和搜网时间,目前为了测试效率这两个参数都填得很小,周期是120秒,搜网时间是10秒;理论上设置为至少每天搜网2分钟比较合适,这样可以定时更新网关信息,当然,这个根据具体应用来确定。
五、数据上行
5.1 发送任务
数据上行是指节点发送数据到网关,即下图所示函数,他作为一个模块按需运行,有发送任务了才会运行。
发送接口由下面函数提供,给应用层调用。
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后强制停止。
嗅探10次,增加成功率,嗅探结束后立马以增加1M频率的参数进入CAD检测回复信号,例如嗅探是(470.25, 11, 8),那么监听参数就是(471.25, 11, 8),跳频减少干扰。回复监听次数是10次,也是为了增加成功率,因为两者在节奏上没那么合拍,就需要增加次数的方式提高成功率,这也是跟单纯专利方法的区别,理论和实践上的差距。
如果CAD有回复的话,说明与目标天线握手成功了,就可以进行数据发送了,如果这次不成功,那么就再尝试握手一次,即多次嗅探,增加成功率,整个过程其实很快,一轮几百ms时间,随着通道ID增加,时间也会增加,所以后面的轮次也减少了。
无论静态模式还是动态模式,网关天线从机都有对应的任务配合节点传输,从天线从机角度看,那就是接收函数了,如下图所示:
如果搜索到CAD信号就会返回嗅探帧并进入接收模式,如下图所示。
握手成功后,就进入应用数据发送了,节点首先会发送一个前导包,里面包含将要发送数据的长度,这样网关天线这边就可以明确自己要等待的时间了,如果直接发送应用数据,因为干扰等原因又没收到,那么网关天线就要按最长数据发送时间等待,很浪费时间。
节点发送完前导包后会立马发送应用数据包,顺利的话,等待网关回复确认信号就可以完成本回合的发送任务了,通知应用层发送成功。
5.4 下行尾随
对于唤醒周期为0xFFFF的节点,本质上是不进行唤醒监听的,对于这类设备要发送下行数据的话只能是主动上报的时候顺带下发,为了实现这一功能,网关在回复上行数据包的时候会检查该设备是否有缓存的下行数据,如果有的话会把数据包长度写入,这样节点就知道自己要等多久了,同时将下行包打包好,一同传给天线从机,从机发送完回复包后会继续发送应用数据包,完成下行数据的发送。这一功能适用于所有类型的节点,不一定是休眠设备。
六、数据下行
6.1 网关下行管理
下行数据根据节点休眠周期分为三种:不休眠、周期唤醒和长休眠。对于长休眠设备刚才5.4节已经描述过了,周期唤醒和不休眠设备有点类似,区别在于周期唤醒设备要多个唤醒步骤,发送唤醒嗅探帧。这一步在天线从机的发送函数里完成。
对于网关主机来讲,它要根据节点的唤醒周期来判断能否下发,首先在间隔上不能太频繁,影响系统稳定,这里是要间隔10秒的;其次是时间戳是否为唤醒周期的整数倍,箭头指示的时间+1目的是提前1秒发送,确保不会错过。
6.2 从机发送
从机接收到下行数据指令后,根据唤醒标志决定是否发送唤醒包,针对不休眠设备可以省去这一步,节约时间。发送完唤醒帧后就直接发送匹配包,告诉被唤醒的节点:网关这次是要跟谁通讯,无关的节点继续休眠。因为如果唤醒周期一样的话是有可能会被同时唤醒的,为了减少这种情况,唤醒周期尽量在某个范围内用随机值代替,比如阀门的唤醒周期可以5~10秒之间,那就节点自行产生5~10的随机数作为唤醒周期,入网后就不需要改变了。
另外,下行唤醒的频率是根据节点的SN计算的,会有冲突的可能性,但是加上随机的唤醒周期,同频又同周期的可能性就大大减少了。
发送完匹配包后,就进入速率自适应阶段了,这个流程跟上行数据时是一样的,只不过角色互换了,现在是天线从机发送嗅探包,节点监听扫描,整个流程是一致的。匹配成功后会直接发送应用数据包,最后就是等待回复确认信号了,天线从机方面下行任务完成。
在网关主机方面,这里配合端点物联APP,还会把下发结果上报给APP端,主要有下图几种状态,如果成功发送就是返回下发成功。
6.3 节点接收
在节点端,如果SN匹配的话就会进入速率匹配阶段,同时也获取到了将要发送的应用数据长度,这在后续中会用到,可以明确等待时间。
速率匹配成功后就进入接收模式了,根据之前收到的待发送数据长度确定等待时间。如果后续收到数据解析正确,就会返回确认包,完成下行接收任务。
七、设备间发送(D2D)
设备间发送本质就是让发送方充当临时网关,在软件和硬件上都很好兼容,而传统的LoRaWAN协议是没有D2D能力的,后面好像阿里有个补充协议,有点曲线救国了。D2D在设备联动的场景比较有用,实时性和可靠性都提升了。
这部分代码发送和接收都是在节点端,接收部分是复用的,就是增加个D2D发送程序。发送前首先应用层要添加数据包,同时携带目标SN和唤醒周期参数,如下图所示:
确保无误后,在主任务中检查周期是否到达,到达后进入发送流程。
随后流程跟下行数据一样,唤醒--SN匹配--速率自适应--发送应用数据--回复确认,具体可以看项目里的代码,这里就不贴了。
八、OLED屏幕
8.1 OLED驱动
为了方便节点调试,除了增加电池供电的以外,还增加了一个OLED屏幕,可以展示节点的状态信息,具体内容在第二篇中已经有详细解释了,这里主要讲解下代码。
代码包含驱动和应用两部分,这个屏幕通讯接口采用IIC,为了增加速度,采用了硬件IIC,下图是两个相关文件。
驱动层主要是对屏幕的寄存器进行设置,比如下面的初始化函数,这个根据厂家的代码来就行了。
为了增加可移植性,IIC相关的代码都是在应用层写的,驱动层只是提供注册接口,把相关函数注册即可。
其他就是功能性函数了,根据厂家提供的代码改下命名风格就行了。
8.2 OLED应用
下图是OLED的IIC初始化和写函数,网上有相关代码,或者看ST的demo库。
剩下的就是根据应用需求,在指定位置显示指定内容,像这里就是显示节点信息、信号强度、时间等信息了。
这里有个函数需要提下,就是下图所示函数,他是用来显示打印信息的,类似于串口的printf。但是因为效率原因,没有直接在函数内显示,而是将待显示的字符串先缓存,然后在自己的显示线程里打印。因为这个函数主要是要显示协议栈运行过程的关键信息,比如接收到的数据,发送状态等等,这是在协议栈内调用的,如果直接显示,由于IIC速度较慢,会造成协议栈运行阻塞,导致CAD检测成功率下降。
为了避免这个问题,我在OLED应用程序里定义了一个结构体,用于缓存节点状态信息,在协议栈线程中将这些信息缓存过来,由于显示任务的优先级比较低,这样就不会影响协议栈运行了。
九、应用配置
9.1 节点配置
节点配置主要在这个函数里,自己根据注释和需求修改,SN和唤醒周期可以通过串口修改并保存,搜索周期和时长只能代码修改了。根密码要跟网关保持一致,这里只是举例,比较随意。
9.2 网关配置
网关配置在这里修改,内容比较少,包括起始频段、运行模式和广播偏移,其中起始频段和运行模式可以利用端点物联APP修改。
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、入网、搜网,最后才是接收任务。各个子模块结束后都会切换到空闲状态,主任务检测到子模块空闲后就会进入整体空闲,检查各个模块的任务状态。
如下图所示,主任务有自己的状态,各个模块也有自己的状态。
10.2 闹钟计算
对于需要休眠的节点,协议栈要告诉应用层什么时候要休眠、什么时候要唤醒,因为一个系统还有其他外设模块和功能需求,应用层要综合所有外设的休眠需求决定休眠、唤醒时刻。这里核心来讲就是一个唤醒闹钟,只要告诉应用层需要在什么时刻叫醒我就行了,我协议栈先去睡觉了,其他的你们自己看着办吧。协议栈的唤醒闹钟需要综合子模块的闹钟才能计算出来,所以在每个功能模块的结构体里都有一个闹钟时间,主任务把他们中最小那个记录下来报告给应用层就行了。
在应用层,进行了简单的唤醒休眠操作,由于这个测试板不适合低功耗测试,就没有进行整体的低功耗配置了。低功耗开发需要软硬件配合,有点难度,后续会出相关项目教程。
10.3 事件通知
节点入网、数据发送结果都会通过事件的方式通知应用层,让应用层进行相应的处理,其实原理很简单,就是定义一个结构体,包含事件类型和参数,不同的事件事先定义好,有什么参数也都知道,这样在协议栈内部对结构体赋值,应用层通过检查这个结构体变量就能知道协议栈内部通知的事件了。下面是事件结构体和事件类型,以及应用层的检查处理函数。
对于网关,道理其实也是一样的。
十一、总结
这篇文章比较长,基本上把协议栈的逻辑和内容解释一遍了,细节的东西实在没法写,可能后续会出个视频讲解下,会通过公众号通知。
在理解了上一篇专利的基础上,代码理解起来应该也很简单的,主要是内容比较多,单纯节点协议栈的代码也有两千多行了,细节的东西只能代码慢慢看了。
后面如果有时间就继续讲解下LoRa硬件的移植和驱动过程,同时了解下什么是扩频原理,毕竟这一优势大家只是口口相传,很少有人深入数学和通讯原理去讲解的。
最后需要声明一点的是,本协议栈完全是个人兴趣爱好驱动完成的,并没有要去横向对比LoRaWAN等成熟的协议栈,这里仅提供一个自组网思路,以供学习研究使用。
本专栏地址:物联网实战--LoRa自组网(LoRaSun)_端点物联的博客-CSDN博客
本人的其它文章导航地址:端点物联网学习资源合集-CSDN博客
关注VX公众号 端点物联,以便即时接收文章更新信息。
学习交流QQ群: 701889554
免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。
相关推荐

你 发表评论:
欢迎