LORA-B1S——Ping通信
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进去一个事件让这个状态机运转起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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 数组描述了状态转移的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* 定义所有状态 */ 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对象:
1 |
APP_FSM_DEF(ping_fsm); //定义app_fsm对象 |
在初始化代码中针对fsm有两句:
1 2 3 4 |
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
这个函数实现的就是针对各个状态去做处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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代码分析
初始化部分:
和前面初始化一样,不再过多描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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循环中)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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中定义
1 2 3 4 5 6 7 |
#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专栏
源码下载地址,最新文档都会更新在专栏内,欢迎大家订阅收藏