前言:前阵子需要用Lora做一个移动平台上的数据采集系统,具体应用在野外等对功耗要求较高的场景,相关的要求则是移动平台上的主机能够唤醒处于休眠状态下的地面站,并对地面站进行数据采集。检索了一下网上现有的资料,似乎没有发现有类似的使用场景,绝大部分都是Lora固定站点在特定区域内组网并通过网关设备上传云端,因此开始了独立方案设计......
目录
方案一:单频段寻呼+通话
一开始拿到这个项目需求,我最初的设计就是在一个频段内实现主机呼叫从机、主从连接、主从通话所有的过程,但是由于之前没有使用过Lora通信,因此在实现过程中遇到了不少的问题,下面一一讲解:
1.Lora半双工通信:Lora是半双工通信意味着Lora模块不能同时处于收发状态,而本项目中主机需要不断对可能出现在周围的从机进行唤醒操作,而由于Lora唤醒工作原理的原因(唤醒模式下会自动添加一段唤醒码),发送一次唤醒信号需要大致2s的时间,而在发送唤醒码期间无法收到从机的请求信号。因此需要根据项目实际情况确定唤醒时间间隔,间隔太长会导致从机不能及时唤醒,间隔太短会导致主机难以收到从机请求。
2.433MHz频段干扰:大部分Lora的模块默认通信频段都是433MHz,而日常生活中很多无线遥控器、RFID系统之类的设备都使用433MHz频段,信道干扰较为严重,调试时尽量避开433MHz频段。
3.唤醒效果不佳:在使用主机唤醒模式空中唤醒时效果十分不好,经过排查最终发现是因为唤醒模式下丢包错传严重,因此使用固定数据包唤醒从机的方法唤醒成功率较低,将唤醒机制改为串口中断一旦被触发立即唤醒后,唤醒效果大幅改善。
4.无线信道拥塞-连续发送:连续不断地在相同频段上发送数据可能会造成信道过度拥挤,增加数据包碰撞的概率,从而影响接收方的接收效果。通过在发送过程中引入延时,可以减少这种碰撞和干扰,提高信道利用效率。
5.无线信道拥塞-同时请求:在方案一设计之初,我缺乏多设备无线通信方面的经验,因此也没有考虑到一片区域的从机被唤醒时同时发送请求与建立通话之间数据包碰撞的问题。由于电磁波在空中传播的速率很快,因此主机发送的唤醒信号到达同一片区域内的从机时,会导致同一片区域内的从机同时唤醒并定时发送通信请求,解决方法是根据从机ID号不同设计了一定范围内的随机延时处理,避免了同时发送请求导致的数据包碰撞。
由于是单频段,从机定时发送的通信请求严重干扰主从建立连接与主从通话过程,这个问题的无法规避也让我不得不放弃了这个方案。
方案二:所有从机分配频段
一个从机分配一个频段的方案完全避免了多设备导致的数据包碰撞问题,然而这种方案又带来了其他无法解决的问题:
1.频段有限:这种方案的设备上限受制于Lora模块支持的频段范围,不利于后期增加设备。
2.唤醒耗时:在多频段情况下,主机由原来的单频段广播唤醒命令的方式,变为逐个频段切换并发送唤醒命令,频段切换与发送唤醒命令方式与方案一相比,唤醒从机所耗时间随从机数量呈线性增长,耗时大幅增长,效率严重降低。
方案三:单频段令牌传递
在查阅论文时发现了一篇关于单双工通信下令牌传递通信算法的研究,为我提供了一定思路。方案三核心方法是将单频段无线信道抽象为一条总线,在总线上主机通过轮询、令牌传递的方式保证信道始终只有一个设备处于发送,从而避免了信道中数据包的碰撞。
然而这种方式在有线通信情况下效果很好,而应用在此项目中还是遇到了一些问题:
1.轮询列表更新:由于是轮询并发放令牌的工作机制,主机需要预先知晓所有设备的ID,这就要求每更改设备ID以及添加设备时都需要重新烧录代码修改轮询列表,不利于后期增加设备。
2.轮询效率低:由于主机对每一个从机发出通信邀请后都需要一定的等待时间,而无线通信的时延受工作环境及传输介质影响很大,再加上多径问题、丢包问题,因此难以确定等待时间,导致一轮轮询时间较长,效率较低。若要解决这个问题,则可以主机在轮询时不等待,根据收到的应答先后进行令牌发放,但这样又绕回到数据包碰撞的问题,在效果上与主机广播类似,但效率比主机广播低。
不过这个方案还是给我提供了一定改进思路,为方案四奠定了基础。
方案四:寻呼频段+通话频段
这个方案解决了上述绝大部分的问题,具体工作原理灵感来自于移动通信网络课上老师讲的GSM系统的寻呼和通话建立过程:GSM系统中用户在专用的寻呼信道发起寻呼后,一旦移动台响应了寻呼,就会进入通话建立阶段。移动台通过接入信道(RACH)向网络发送通道请求,请求建立一个专用的通信信道(SDCCH)。网络响应移动台的请求,为其分配一个专用通信信道,并通知移动台。必要的步骤完成后,网络会将通信切换到专用的话音信道(TCH),此时双方可以开始通话。
借鉴这个机制以及充分利用Lora模块可以切换多频段的特点,根据原来的方案进行改进得到了方案四,具体的流程图如下图所示:
工作原理图如下图所示:
Lora主机如下图所示:
Lora主机核心逻辑代码如下:
void LoRa_ReceData(void)
{
usart2_rx(1);//开启串口接收
u16 len = 0;
Lora_mode=1;//1:模块接收模式
// 判断是否接收到了一个新的数据包
if (USART2_RX_STA & 0x8000)
{
len = USART2_RX_STA & 0X7FFF; // 获取数据长度
// USART2_RX_BUF[len] = 0; // 添加字符串结束符
USART2_RX_STA = 0;
if(1)
{
u8 rlcd_buff[20]={0}; //LCD显示字符串缓冲区
// 将接收到的数据转换为16进制的字符串形式
sprintf(rlcd_buff,"%02X %02X %02X %02X %02X %02X",USART2_RX_BUF[0],USART2_RX_BUF[1], USART2_RX_BUF[2], USART2_RX_BUF[3],USART2_RX_BUF[4],USART2_RX_BUF[5]);
// 在LCD上显示接收到的数据
LCD_Fill(10,280,240,310,WHITE);
Show_Str_Mid(10,290,rlcd_buff,16,240);
// // 清零接收缓冲区,为下一个数据包做准备
// memset(USART2_RX_BUF, 0, sizeof(USART2_RX_BUF));
}
//状态机
switch(cur_state)
{
//寻呼信道 唤醒模式
case 0:
//从机发送的请求ID 0xF0请求头
if (USART2_RX_BUF[0] == 0xF0 )
{
RequestID = USART2_RX_BUF[1];
memcpy(rx_buff, USART2_RX_BUF, 2); //将USART2_RX_BUF中的数据复制到rx_buff中
if(1)
{
u8 rlcd_buff[20]={0}; //LCD显示字符串缓冲区
// 将接收到的数据转换为16进制的字符串形式
sprintf(rlcd_buff,"%02X %02X %02X %02X %02X %02X",rx_buff[0],rx_buff[1], rx_buff[2], rx_buff[3],rx_buff[4],rx_buff[5]);
// 在LCD上显示接收到的数据
LCD_Fill(10,250,240,280,WHITE);
// Show_Str_Mid(10,260," ",16,240);
Show_Str_Mid(10,260,rlcd_buff,16,240);
}
//接收到从机通信请求:F0 ID
if (1)
{
//循环查表ID
for (int i = 0; i < 5; i++)
{
if (ID_list[i] == RequestID)
{
return;// 设备ID号已存在于ID列表中,不进行任何操作
}
// 如果设备ID号不存在于列表中,则将其添加到列表中
if (ID_count < 6)
{
ID_list[ID_count++] = RequestID;
// LCD_ShowNum(130+10,80,ID_list[ID_count-1],2,12);
cur_state = 1;//切换到状态1
}
// 在LCD上显示ID列表
char list_buff[20];
sprintf(list_buff, "%02X %02X %02X %02X %02X",ID_list[0], ID_list[1], ID_list[2],ID_list[3],ID_list[4]);
LCD_Fill(10,210,260,235,WHITE);
Show_Str_Mid(10,220,list_buff,16,240);
return;
}
}
}
break;
case 1:
break;
//通话信道 普通模式 监听有无通话
case 2:
if(LoRa_CFG.chn ==20 && USART2_RX_BUF[0]!=0)
{
//若收到信息则关闭定时中断
TIM_Cmd(TIM3, DISABLE);
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// Show_Str(10+120,140,200,12,"通话接通",12,0);
if (USART2_RX_BUF[4] == 0x0F || USART2_RX_BUF[6] == 0x0F)
{
cur_state = 0;//切换到状态0
}
u8 rlcd_buff[20]={0}; //LCD显示字符串缓冲区
// 将接收到的数据转换为16进制的字符串形式
sprintf(rlcd_buff,"%02X %02X %02X %02X %02X %02X",USART2_RX_BUF[0],USART2_RX_BUF[1], USART2_RX_BUF[2], USART2_RX_BUF[3],USART2_RX_BUF[4],USART2_RX_BUF[5]);
// 在LCD上显示接收到的数据
LCD_Fill(10,280,240,310,WHITE);
Show_Str_Mid(10,290,rlcd_buff,16,240);
}
break;
}
}
}
void LoRa_Process(void)
{
u8 key=0;
static u16 t=0;
DATA:
Process_ui();//界面显示
LoRa_Set();//LoRa配置(进入配置需设置串口波特率为115200)
u8 count0=0;
u8 count1=0;
u8 count2=0;
static u8 num=0;
u16 addr;
u8 chn;
u16 i=0;
while(1)
{
Lora_mode=1;//1:模块接收模式
key = KEY_Scan(0);
//状态机
switch(cur_state)
{
//寻呼信道 唤醒模式
case 0:
if(LoRa_CFG.mode!=1 ||LoRa_CFG.chn !=16)
{
LoRa_CFG.mode = 1; //lora切换为唤醒模式
LoRa_CFG.chn =16; //寻呼信道
LoRa_CFG.addr = 0XFFFF ; //设备地址
LoRa_Set();
//LCD显示lora状态
Show_Str(10+60,100,200,12," ",12,0);
Show_Str(10+60,100,200,12,"唤醒模式",12,0);
}
//数据接收
LoRa_ReceData();
//唤醒状态 未寻找到从机
if(!LORA_AUX && t>=500)
{
//发送唤醒命令
u2_printf("%c",0xAB);
LCD_Fill(0,195,240,220,WHITE); //清除显示
Show_Str(10+90,195,200,12,"正在唤醒",12,0);
Lora_mode=1;//1:模块接收模式
t=0;
}
break;
//寻呼信道 发放通信令牌
case 1:
if(LoRa_CFG.mode!=0 ||LoRa_CFG.chn !=16 ||LoRa_CFG.mode_sta != LORA_STA_Dire)
{
//定向传输
LoRa_CFG.mode_sta = LORA_STA_Dire;//LORA_STA_Tran透明传输
//lora切换为普通模式
LoRa_CFG.mode = 0;
//设备地址
LoRa_CFG.addr = 0XFFFE ;
LoRa_Set();
//LCD显示lora状态
Show_Str(10+60,100,200,12," ",12,0);
Show_Str(10+60,100,200,12,"普通模式",12,0);
Show_Str(10+60,120,200,12,"定向传输",12,0);
Show_Str(10+60,140,200,12,"寻呼信道",12,0);
delay_ms(50);
}
if(LoRa_CFG.mode_sta == LORA_STA_Dire && t>200)//定向传输
{
date[0] =0X00;//高位地址
date[1] = RequestID;//低位地址
date[2] = 16; //寻呼信道
date[3] = 0xF0;
date[4] = RequestID;
for(i=0;i<5;i++)
{
while(USART_GetFlagStatus(USART2,USART_FLAG_TC)==RESET);//循环发送,直到发送完毕
USART_SendData(USART2,date[i]);
}
sprintf(tlcd_buff, "正在发放令牌%02X",RequestID);
LCD_Fill(0,195,240,220,WHITE); //清除显示
Show_Str_Mid(10,195,tlcd_buff,16,240);//LED显示
// delay_ms(10);
cur_state = 2;//切换到状态2
t=0;
}
break;
//通话信道 普通模式 等待从机发送
case 2:
if(!LORA_AUX && LoRa_CFG.chn !=20)
{
//普通模式
LoRa_CFG.mode = 0;
//透明传输
LoRa_CFG.mode_sta = LORA_STA_Tran;
//通话信道
LoRa_CFG.chn =20;
//设备地址
LoRa_CFG.addr = 0XFFFF ;
LoRa_Set();
Show_Str(10+60,140,200,12,"通话信道",12,0);
delay_ms(50);
//计时3S
TIM3_SetReload(4000, 0);
Show_Str(10+120,140,200,12,"开始计时",12,0);
// TIM3 使能定时中断
TIM_Cmd(TIM3, ENABLE);//若3S未接收则跳转至状态1
/* 使能TIM3更新中断 */
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
}
// memset(USART2_RX_BUF, 0, sizeof(USART2_RX_BUF));
//数据接收
LoRa_ReceData();
break;
default:break;
}
if(key==KEY0_PRES)
{
//按键0逻辑
}
if(key==KEY1_PRES)
{
//按键1逻辑
}
Show_Str(130+15+55,80,200,12," ",12,0);//清空之前的显示
LCD_ShowNum(130+15+50,80,cur_state ,2,12);
t++;
delay_ms(10);
}
}
Lora地面站测试Demo如下图所示:
Lora地面站核心逻辑代码如下:
int main(void)
{
usart2_init(115200);
Serial_Init();
OLED_Init();
Key_Init();
LED_Init();
Timer_Init();
LED1_OFF();
// LED5_ON();
OLED_ShowString(2,2, "Initializing...");
while(!LoRa_Init()){
Delay_ms(50);
}
OLED_ShowString(2,1, "LoRa_Init:over!");
Delay_ms(200);
OLED_ShowString(2,1, " ");
LoRa_Set(); //lora初始化设置
OLED_ShowString(1,1, "State:");
OLED_ShowString(1,8, (u8*)cnttbl_mode[LoRa_CFG.mode]);//显示lora默认工作模式
OLED_ShowString(2,1, "ID:");
OLED_ShowNum(2,4, ID ,2);//显示lora模块ID号
Init_flag = 1;
Send_request[3] = ID;//通信请求ID
u16 t =0;
static u8 num=0;
u16 addr; //地址
u8 chn; //信道
u16 i=0;
while (1)
{
Lora_mode=1;//1:接收模式
OLED_ShowString(1,7, (u8*)cnttbl_mode[LoRa_CFG.mode]); //显示lora当前工作模式
OLED_ShowNum(2,13, LoRa_CFG.chn ,2); //显示lora当前信道
OLED_ShowNum(2,16, LoRa_CFG.mode_sta ,1); //定向传输或透明传输
OLED_ShowNum(1,14, cur_state ,1); //状态机状态
KeyNum = Key_GetNum(); //按键输入
OLED_ShowNum(1,16, t ,1);
//手动切换模块状态
if(KeyNum==1)
{
LoRa_CFG.mode = 2 -LoRa_CFG.mode;
LoRa_Set();
OLED_ShowNum(2,7, LoRa_CFG.mode ,3);
}
//状态机
switch(cur_state)
{
case 0:
//等待被唤醒
break;
//唤醒模式->普通模式 寻呼信道 定向传输
case 1:
LoRa_ReceData(); //接收
//从机被唤醒
if (LoRa_CFG.mode != 0 || LoRa_CFG.chn !=16 ||LoRa_CFG.mode_sta != LORA_STA_Dire)
{
LoRa_CFG.mode = 0; //普通模式
LoRa_CFG.chn =16; //寻呼信道
LoRa_CFG.mode_sta = LORA_STA_Dire;//定向传输
LoRa_Set();
Delay_ms(50);
memset(USART2_RX_BUF, 0, sizeof(USART2_RX_BUF));// 清零接收缓冲区
OLED_ShowString(1,7, (u8*)cnttbl_mode[LoRa_CFG.mode]); //显示lora当前工作模式
cur_state = 2;
}
break;
case 2:
LoRa_ReceData(); //接收
//普通模式下寻呼信道寻呼
if(!LORA_AUX && t>=100)//定向传输
{
date[0] =0XFF;//高位地址
date[1] =0XFE;//低位地址
date[2] = 16; //寻呼信道
date[3] = 0xF0;
date[4] = ID;
LED5_ON();
for(i=0;i<5;i++)
{
while(USART_GetFlagStatus(USART2,USART_FLAG_TC)==RESET);//循环发送,直到发送完毕
USART_SendData(USART2,date[i]);
}
LED5_OFF();
int random_time = rand() % 200;
// Delay_ms(40+random_time*(tm%ID));
t=0;
}
break;
case 3:
if (LoRa_CFG.mode != 0 || LoRa_CFG.chn !=20 ||LoRa_CFG.mode_sta != LORA_STA_Tran)
{
LoRa_CFG.mode = 0; //普通模式
LoRa_CFG.chn =20; //通话信道
LoRa_CFG.mode_sta = LORA_STA_Tran;//透明传输
LoRa_Set();
Delay_ms(50);
memset(USART2_RX_BUF, 0, sizeof(USART2_RX_BUF));// 清零接收缓冲区
OLED_ShowString(1,7, (u8*)cnttbl_mode[LoRa_CFG.mode]); //显示lora当前工作模式
}
Delay_ms(2000);
for(int num=0;num<=20;num++)
{
LED5_Turn();
Data_Transfer(num);
TX_packet[3] = ID;
u2_send_packet_with_header_footer(TX_packet,4);
// 在OLED上显示发送的数据
char rlcd_buff[16]; // 长度应根据需要进行调整
sprintf(rlcd_buff, "%02X %02X %02X %02X ",TX_packet[0], TX_packet[1],TX_packet[2], TX_packet[3]);
OLED_ShowString(3,1, rlcd_buff);
Delay_ms(100);
LED5_Turn();
}
u2_send_packet_with_header_footer(Send_over,4);
Delay_ms(200);
u2_send_packet_with_header_footer(Send_over,4);
Delay_ms(200);
u2_send_packet_with_header_footer(Send_over,4);
OLED_ShowString(4,1, "Send over! ");
Delay_ms(5000);
OLED_ShowString(4,1, " ");
cur_state = 4;
break;
case 4:
if (LoRa_CFG.mode != 2 || LoRa_CFG.chn !=16 ||LoRa_CFG.mode_sta != LORA_STA_Tran)
{
LoRa_CFG.mode = 2; //休眠模式
LoRa_CFG.chn =16; //寻呼信道
LoRa_CFG.mode_sta = LORA_STA_Tran;//透明传输
LoRa_Set();
Delay_ms(50);
memset(USART2_RX_BUF, 0, sizeof(USART2_RX_BUF));// 清零接收缓冲区
OLED_ShowString(1,7, (u8*)cnttbl_mode[LoRa_CFG.mode]); //显示lora当前工作模式
}
LED5_OFF();
// cur_state = 0;
break;
deafult:break;
}
if(tm>=100)
{
tm=0;
}
tm++;
t++;
// Delay_ms(10);
}
}
void LoRa_ReceData(void)
{
u16 len = 0;
static uint8_t rx_packet[4]={0,0,0,0};
// 判断是否接收到了一个新的数据包
if (USART2_RX_STA & 0x8000)
{
len = USART2_RX_STA & 0X7FFF; // 获取数据长度
USART2_RX_BUF[len] = 0; // 添加字符串结束符
USART2_RX_STA = 0;
// 在OLED上显示接收到的数据
char rlcd_buff[16]; // 长度应根据需要进行调整
sprintf(rlcd_buff, "%02X %02X %02X %02X ",USART2_RX_BUF[0], USART2_RX_BUF[1],USART2_RX_BUF[2], USART2_RX_BUF[3]);
OLED_ShowString(4,1, rlcd_buff);
// 检查数据包长度是否正确
if (USART2_RX_BUF[0] == 0xF0 )
{
//主机应答
if (cur_state == 2 && USART2_RX_BUF[1] == ID)
{
cur_state = 3;
}
}
}
}
方案四的核心工作逻辑是将寻呼过程和通话过程分别放到两个频段进行以避免寻呼信号与通话信号之间发生碰撞,而为了减小寻呼信号碰撞的可能,对定时发送间隔进行适当增加并且将通信请求缩小到两个字节,即包头0XF0加设备ID。
后记:
这个项目在接手时我由于不熟悉Lora无线通信的一些特性以及中间发生了一系列遇到没有预想到的问题,导致时间远远超过初始预期的一星期完成,正巧前一阵子在备赛关键时期,断断续续花费了一个月才基本完成π_π
实际上这个项目还有很多方面可以完善,例如对错码使用CRC校验的方法进行优化、借鉴TCP协议中连续ARQ的工作原理保证数据包传输过程(需要针对半双工方式进行改编)等等。另外这里地面站的休眠模式主要侧重于Lora模块本身的功耗管理,并不侧重于芯片本身的功耗管理,若需要对整个地面站进行严格功耗控制,则需要进一步对芯片本身进行工作模式、睡眠模式、停机模式、待机模式各个状态之间进行切换,逻辑更为复杂,需要在现有的状态机基础上进行更深入的逻辑控制,状态机的编程风格也为后期深入开发降低了开发难度。
具体的一些细节要是有空再继续写吧^_^