lora ping通信例程名为:lora-Ping
,基于LADK进行创建。
ping通信和p2p通信类似,也是实现两块LORA-B1S之间进行通信的。不过p2p是实现单向通信,而Ping则实现了双向通信,可以用来测试lora的传输距离或者丢包情况。
如下图所示,客户端发送一包数据给服务端,服务端收到以后会再返回给客户端。客户端按照顺序发送数据包1,数据包2……数据包10,共连续发送10包数据为一个完整过程,之后再次循环。通过串口输出就可以查看传输的数据丢包率。

例程代码共用一个MDK工程,但分为两个不同的target
- ping-server服务端
- ping-client客户端
通过MDK这个地方可以选择编译为client还是server

分别编译烧录到两块Lora-B1s开发板中,按下复位按键,正常如果通信成功两块板子的LED都会闪烁,串口tx可以接到电脑,串口助手可以观察到通信数据包的信息以及信号强度。
需要开发环境
硬件:
- 两块Lora-B1S开发板
- 调试器
软件:
- MDK5
- stm32Cubemx
基于LADK工程创建实现步骤
- 拷贝lora-ADK并重新命名为lora-Ping
- 在
./user/app
文件夹中新建三个文件ping-client.c
ping-server.c
ping.h
- 把
ping-client.c
ping-server.c
添加到MDK工程分组App中 - 在
ping-client.c
ping-server.c
中添加应用代码,分别实现客户端和服务端功能 - 在MDK中添加两个target:client和server
- client 排除ping-server.c参与编译
- server 排除ping-client.c参与编译
- 修改main.c 调用ping.h 接口
- 编译烧录到开发板中
Ping-client代码分析
client的逻辑会稍微多一些,所以client我们会使用app_fsm状态机组件来简化逻辑部分。
初始化部分
初始化部分的代码和p2p例程中的几乎一样,区别在于这里多初始化了一个app_fsm,并且在最后给fsm状态机put进去一个事件让这个状态机运转起来。
APP_FSM_DEF(ping_fsm); //定义app_fsm对象
APP_TIMER_DEF(ping_timer); //定义app_timer对象
/* lora 回调函数结构体 */
static RadioEvents_t events = {
.TxDone = lora_tx_done_callback,
.TxTimeout = lora_tx_timeout_callback,
.RxDone = lora_rx_done_callback,
.RxTimeout = lora_rx_timeout_callback,
};
void ping_init()
{
/* 初始化app_scheduler, app_timer 和 app_fsm 都要依赖于他运行 */
APP_SCHED_INIT(12,20);
/* 创建一个app_fsm*/
app_fsm_create( &ping_fsm,ping_fsm_list,APP_FSM_LIST_LEN(ping_fsm_list),
PING_STATE_IDLE,ping_fsm_handler);
/* 初始化app_timer*/
app_timer_init();
app_timer_create(&ping_timer,1000,APP_TIMER_ONESHOT,ping_timer_handler,NULL);
/* lora 参数配置 */
Radio.Init(&events);
Radio.SetChannel(LORA_FREQUENCY);
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 2000);
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,
0, true, 0, 0, LORA_IQ_INVERSION_ON, false);
Radio.SetPublicNetwork(false);
Radio.Rx(5000);
/* fsm状态机发送一个事件 */
app_fsm_event_put(&ping_fsm,PING_EVENT_NEXT);
}
FSM状态机组件使用
该例子的核心是通过app_fsm状态机组件来实现的,所以要明白搞懂这个例子的代码,关键就是要看明白app_fsm 的使用。
有限状态机在单片机开发中是非常常用的一个工具,用的好能大大简化代码的开发逻辑。有限状态机可以通过状态和事件组成,事件可以触发从一个状态调转到另外一个状态。
先来看下client客户端的核心逻辑状态图:

图中的圆形就是一个个状态,圆圈与圆圈之间的线代表的就是事件触发转移。client初始化以后进行IDLE状态,当产生NEXT时间的时候,状态机切换到开始状态。这样通过上图的5个状态和4种时间就把client的逻辑描述清楚了。
如下代码就用ping_state_e 定义出来所有的状态,ping_event_e定义出来所有的事件,ping_fsm_list 数组描述了状态转移的逻辑。
/* 定义所有状态 */
typedef enum
{
PING_STATE_IDLE,
PING_STATE_START,
PING_STATE_SEND,
PING_STATE_RECV,
PING_STATE_END,
} ping_state_e;
/* 定义所有事件 */
typedef enum
{
PING_EVENT_NEXT,
PING_EVENT_TIMEOUT,
PING_EVENT_LESS_10,
PING_EVENT_MORE_10,
}ping_event_e;
/* 定义一个状态机转移的列表,控制着状态转移的逻辑 */
fsm_list_t ping_fsm_list[]=
{
{PING_STATE_IDLE, PING_EVENT_NEXT, PING_STATE_START},
{PING_STATE_START, PING_EVENT_NEXT, PING_STATE_SEND},
{PING_STATE_SEND, PING_EVENT_TIMEOUT, PING_STATE_RECV},
{PING_STATE_RECV, PING_EVENT_LESS_10, PING_STATE_SEND},
{PING_STATE_RECV, PING_EVENT_MORE_10, PING_STATE_END},
{PING_STATE_END, PING_EVENT_NEXT, PING_STATE_START},
};
在文件开头定义一个fsm对象:
APP_FSM_DEF(ping_fsm); //定义app_fsm对象
在初始化代码中针对fsm有两句:
app_fsm_create( &ping_fsm,ping_fsm_list,APP_FSM_LIST_LEN(ping_fsm_list),
PING_STATE_IDLE,ping_fsm_handler);
app_fsm_event_put(&ping_fsm,PING_EVENT_NEXT);
- app_fsm_create 创建了ping_fsm这个状态机,
ping_fsm
是创建的fsm对象;ping_fsm_list
定义的状态转移列表;APP_FSM_LIST_LEN(ping_fsm_list)
求列表的长度,PING_STATE_IDLE
初始状态,ping_fsm_handler
状态机处理函数。 - app_fsm_event_put 触发一个事件,初始化完以后触发一个PING_EVENT_NEXT事件,根据状态转移列表描述就会自动跳转到下一个状态,并交给ping_fsm_handler去做处理。其他地方需要触发事件的就调用这个接口函数。
所以ping_fsm_handler
这个函数实现的就是针对各个状态去做处理:
static void ping_fsm_handler(uint8_t state)
{
static uint8_t send_counter = 0;
static uint8_t success_counter = 0;
uint8_t buf[4] = {0};
switch (state)
{
case PING_STATE_START:
printf("\r\n**********ping start********\r\n");
send_counter = 0;
success_counter = 0;
app_fsm_event_put(&ping_fsm,PING_EVENT_NEXT);
break;
case PING_STATE_SEND:
send_counter++;
now_num = send_counter;
memset(buf, send_counter, 4);
Radio.Send(buf, 4);
app_timer_start(&ping_timer);
break;
case PING_STATE_RECV:
if (recv_flag)
{
success_counter++;
printf("client ping pack [%d],result:1, rssi:%d, snr:%d\r\n", send_counter, recv_rssi, recv_snr);
}
else
{
printf("client ping pack [%d],result:0\r\n", send_counter);
}
if (send_counter >= 10)
app_fsm_event_put(&ping_fsm,PING_EVENT_MORE_10);
else
app_fsm_event_put(&ping_fsm,PING_EVENT_LESS_10);
break;
case PING_STATE_END:
printf("ping all pack=10,success=%d\r\n", success_counter);
app_fsm_event_put(&ping_fsm,PING_EVENT_NEXT);
break;
default:
break;
}
}
Lora回调函数
static void lora_tx_done_callback()
{
Radio.Rx(5000);
}
static void lora_tx_timeout_callback()
{
}
static void lora_rx_done_callback(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr)
{
recv_rssi = rssi;
recv_snr = snr;
if ((size == 4) &&
(payload[0] == now_num) &&
(payload[1] == now_num) &&
(payload[2] == now_num) &&
(payload[3] == now_num))
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
recv_flag=true;
}
Radio.Sleep();
}
static void lora_rx_timeout_callback()
{
}
重点的就是lora_rx_done_callback
,当client发送一包数据以后就会进入接收状态,等待server端收到数据以后重新返回,这时候client端接收到就会和发出去的数据做对比,如果对比一样就认为通信一包数据成功了!
Ping-server代码分析
初始化部分:
和前面初始化一样,不再过多描述。
APP_SCHED_INIT(MAX_SCHED_EVENT_SIZE,10);
Radio.Init(&events);
Radio.SetChannel(LORA_FREQUENCY);
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 2000);
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,
0, true, 0, 0, LORA_IQ_INVERSION_ON, false);
Radio.SetPublicNetwork(false);
Radio.Rx(5000);
Lora回调函数
server的逻辑比较简单,当接收到数据以后还把这个数据再发送出去就可以了。那重点我们就来看接收完成中断函数lora_rx_done_callback
。收到数据以后就调用了app_sched_event_put
函数,这个是app_scheduler的常用接口:把需要执行的函数压入调度器去运行,这样ping_shceduler_handler处理最终会在调度器中执行,而不会在lora_rx_done_callback
里面运行。
这个也是app_scheduler的关键作用,在一些中断中比较耗时的函数就把他压入调度器中运行(main的while循环中)。
static void ping_shceduler_handler(void * p_event_data, uint16_t event_size)
{
printf("server receive pack:%d, rssi:%d, snr:%d\r\n", ((uint8_t *)p_event_data)[0], recv_rssi, recv_snr);
Radio.Send((uint8_t *)p_event_data, event_size);
}
static void lora_rx_done_callback(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr)
{
recv_rssi = rssi; //更新rssi
recv_snr = snr; //更新snr
if(size>MAX_SCHED_EVENT_SIZE)return ;
app_sched_event_put(payload,size,ping_shceduler_handler);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
main.c修改
client和server 留出来接口函数是一样的,在ping.h中定义
#ifndef __PING_H
#define __PING_H
void ping_init(void);
void ping_process(void);
#endif
在main 的while之前调用ping_init
,while 里面调用ping_process
MDK中如何创建不同的target
在ping例程中,通过targe来选择不同的代码,在MDK中可以通过这里添加targe:

添加完成以后,选择文件右键->Options 设置,就可以选择这个文件是否参与编译:

LORA-B1S支持
淘宝购买地址:
https://item.taobao.com/item.htm?&id=657480900713
Lora技术支持群:
QQ群:603253865
LORA-B1S专栏
源码下载地址,最新文档都会更新在专栏内,欢迎大家订阅收藏