感谢EEPW推出的“换取手持示波器”活动,本次分享的主题是:基于Nordic的无线开发板nrf7002-dk通过MQTT协议远程控制led并获取按键信息。
目前Nordic最新的nrf54系列的开发板已经面向市场,感兴趣的朋友可以采购体验一下BLE 6.0的新特性!
一、
1.1 板卡介绍:
nRF7002-DK是用于nRF7002 Wi-Fi 6协同IC的开发套件,该开发套件采用nRF5340多协议片上系统 (SoC) 作为nRF7002的主处理器,在单一的电路板上包含了开发工作所需的一切,可让开发人员轻松开启基于nRF7002 的物联网项目。该 DK 包括 Arduino 连接器、两个可编程按钮、一个 Wi-Fi 双频段天线和一个低功耗蓝牙天线,以及电流测量引脚。
这款DK支持低功耗 Wi-Fi 应用开发,并实现了多项 Wi-Fi 6 功能,比如 OFDMA、波束成型和 TWT。nRF7002 Wi-Fi 6配套IC为另一个主机添加了低功耗Wi-Fi 6功能,提供无缝连接和基于Wi-Fi的定位(本地Wi-Fi集线器的SSID嗅探)功能。该IC设计用于搭配Nordic现有的nRF52®和nRF53®系列多协议片上系统 (SoC) 和nRF91®系列蜂窝物联网系统级封装 (SiP) 使用。nRF7002 IC 还可与非nordic主机器件搭配使用。通过SPI或QSPI与主机通信,并带有额外的共存功能,可与其他协议如蓝牙、Thread或Zigbee无缝共存。
nRF7002在Nordic的nRF Connect SDK中提供集成和支持。
板卡特性
用于nRF7002双频带Wi-Fi 6配套IC的开发套件
nRF5340 SoC主机器件
Wi-Fi 6 (IEEE 802.11 a/b/g/n/ac/ax)、蓝牙低功耗 (LE)、蓝牙网状网络、802.15.4、Thread、Zigbee®、ANT、2.4GHz专有和NFC无线协议支持
2.4GHz、5GHz芯片和NFC天线
SWF射频连接器
SEGGER J-Link板载编程器/调试器
用户可编程LED (2x) 和按钮 (2x)
用于测量功耗的引脚
来自USB、外部或锂聚合物电池的2.9V至5.0V电源
Arduino连接器
1.2 开发环境介绍:
在vscode中搭建nrf7002dk的开发环境是非常方便的,先安装ncs command line工具,再安装desktop工具,最后在vscode中设置。
细节可以参考官方文档:https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-VS-Code
二、项目思路/流程图
Nordic的开发环境对我来说比较新,从未接触过。因此首先想到的是去官方找一下有没有相关的demo。入手点就是Wi-Fi的sta例程,提供了联网的入门代码。其次是MQTT,Zephyr有相关的MQTT库,熟悉后可以正常调用。Broker,我选择的是安卓手机上可以使用的MQTT DASHBOARD,比较方便的创建Broker以及客户端。
三、连接Wi-Fi
nRF Connect SDK有一个开箱即用的Wi-Fi Station模式的例程:Wi-Fi: Station https://developer.nordicsemi.com/nRF_Connect_SDK/doc/2.3.0/nrf/samples/wifi/sta/README.html
首先要进行相关回调函数的初始化,如下所示是Zephyr里的网络管理函数,官方链接:https://docs.zephyrproject.org/apidoc/latest/group__net__mgmt.html
Network Management event callback structure Used to register a callback into the network management event part, in order to let the owner of this struct to get network event notification based on given event mask.
net_mgmt_init_event_callback(&wifi_shell_mgmt_cb, wifi_mgmt_event_handler, WIFI_SHELL_MGMT_EVENTS); net_mgmt_add_event_callback(&wifi_shell_mgmt_cb); net_mgmt_init_event_callback(&net_shell_mgmt_cb, net_mgmt_event_handler, NET_EVENT_IPV4_DHCP_BOUND); net_mgmt_add_event_callback(&net_shell_mgmt_cb); LOG_INF("Starting %s with CPU frequency: %d MHz", CONFIG_BOARD, SystemCoreClock/MHZ(1)); k_sleep(K_SECONDS(1));接下来在main函数中调用进行Wi-Fi连接: do { wifi_connect(); for (i = 0; i < CONNECTION_TIMEOUT; i++) { k_sleep(K_MSEC(STATUS_POLLING_MS)); cmd_wifi_status(); if (context.connect_result) { break; } } if (context.connected) { } else if (!context.connect_result) { LOG_ERR("Connection Timed Out"); } }while (0);
下面是wifi_connect函数的实现:
static int wifi_connect(void) { struct net_if *iface = net_if_get_default(); static struct wifi_connect_req_params cnx_params; context.connected = false; context.connect_result = false; __wifi_args_to_params(&cnx_params); if (net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, &cnx_params, sizeof(struct wifi_connect_req_params))) { LOG_ERR("Connection request failed"); return -ENOEXEC; } LOG_INF("Connection requested"); return 0; }
其中比较重要的是net_mgmt这个宏,其定义如下,经过一些列展开后,它会调用Wi-Fi相关的底层驱动函数去连接网络。
#define net_mgmt(_mgmt_request, _iface, _data, _len) net_mgmt_##_mgmt_request(_mgmt_request, _iface, _data, _len)
串口输出如下,成功连接到了2.4G Wi-Fi网络。
[00:00:00.307,281] <inf> wifi_nrf: qspi_init: QSPI freq = 24 MHz [00:00:00.307,312] <inf> wifi_nrf: qspi_init: QSPI latency = 1 [00:00:00.314,788] <inf> wifi_nrf: zep_shim_pr_info: wifi_nrf_fmac_fw_load: LMAC patches loaded [00:00:00.325,653] <inf> wifi_nrf: zep_shim_pr_info: wifi_nrf_fmac_fw_load: LMAC boot check passed [00:00:00.328,613] <inf> wifi_nrf: zep_shim_pr_info: wifi_nrf_fmac_fw_load: UMAC patches loaded [00:00:00.339,385] <inf> wifi_nrf: zep_shim_pr_info: wifi_nrf_fmac_fw_load: UMAC boot check passed [00:00:00.360,992] <inf> wifi_nrf: zep_shim_pr_info: RPU LPM type: HW [00:00:00.479,034] <inf> fs_nvs: nvs_mount: 2 Sectors of 4096 bytes [00:00:00.479,064] <inf> fs_nvs: nvs_mount: alloc wra: 0, fe8 [00:00:00.479,064] <inf> fs_nvs: nvs_mount: data wra: 0, 0 *** Booting Zephyr OS build v3.2.99-ncs2 *** [00:00:00.479,125] <inf> net_config: net_config_init_by_iface: Initializing network [00:00:00.479,156] <inf> net_config: check_interface: Waiting interface 1 (0x200013e8) to be up... [00:00:00.479,949] <inf> net_config: setup_ipv4: IPv4 address: 192.168.0.104 [00:00:00.479,980] <inf> net_config: setup_dhcpv4: Running dhcpv4 client... [00:00:00.480,987] <inf> MQTT_OVER_WIFI: main: Starting nrf7002dk_nrf5340_cpuapp with CPU frequency: 64 MHz [00:00:00.481,231] <inf> wpa_supp: wpa_printf_impl: z_wpas_start: 385 Starting wpa_supplicant thread with debug level: 3 [00:00:00.481,475] <inf> wpa_supp: wpa_printf_impl: Successfully initialized wpa_supplicant [00:00:01.481,048] <inf> MQTT_OVER_WIFI: main: QSPI Encryption disabled [00:00:01.481,140] <inf> MQTT_OVER_WIFI: main: Static IP address (overridable): 192.168.0.104/255.255.255.0 -> [00:00:03.083,801] <inf> MQTT_OVER_WIFI: wifi_connect: Connection requested [00:00:03.383,941] <inf> MQTT_OVER_WIFI: cmd_wifi_status: ================== [00:00:03.383,972] <inf> MQTT_OVER_WIFI: cmd_wifi_status: State: SCANNING [00:00:06.997,833] <inf> wpa_supp: wpa_printf_impl: wlan0: SME: Trying to authenticate with 20:6b:e7:bb:8c:3b (SSID='TP-LINK_168' freq=2437 MHz) [00:00:07.001,098] <inf> wifi_nrf: wifi_nrf_wpa_supp_authenticate: wifi_nrf_wpa_supp_authenticate:Authentication request sent successfully [00:00:07.258,483] <inf> wpa_supp: wpa_printf_impl: wlan0: Trying to associate with 20:6b:e7:bb:8c:3b (SSID='TP-LINK_168' freq=2437 MHz) [00:00:07.267,700] <inf> wifi_nrf: wifi_nrf_wpa_supp_associate: wifi_nrf_wpa_supp_associate: Association request sent successfully [00:00:07.283,569] <inf> wpa_supp: wpa_printf_impl: wlan0: Associated with 20:6b:e7:bb:8c:3b [00:00:07.283,721] <inf> wpa_supp: wpa_printf_impl: wlan0: CTRL-EVENT-SUBNET-STATUS-UPDATE status=0 [00:00:07.292,633] <inf> MQTT_OVER_WIFI: cmd_wifi_status: ================== [00:00:07.292,663] <inf> MQTT_OVER_WIFI: cmd_wifi_status: State: 4WAY_HANDSHAKE [00:00:07.292,694] <inf> MQTT_OVER_WIFI: cmd_wifi_status: Interface Mode: STATION [00:00:07.292,724] <inf> MQTT_OVER_WIFI: cmd_wifi_status: Link Mode: WIFI 4 (802.11n/HT) [00:00:07.292,755] <inf> MQTT_OVER_WIFI: cmd_wifi_status: SSID: TP-LINK_168 [00:00:07.292,785] <inf> MQTT_OVER_WIFI: cmd_wifi_status: BSSID: 20:6B:E7:BB:8C:3B [00:00:07.292,785] <inf> MQTT_OVER_WIFI: cmd_wifi_status: Band: 2.4GHz [00:00:07.292,816] <inf> MQTT_OVER_WIFI: cmd_wifi_status: Channel: 6 [00:00:07.292,816] <inf> MQTT_OVER_WIFI: cmd_wifi_status: Security: WPA2-PSK [00:00:07.292,846] <inf> MQTT_OVER_WIFI: cmd_wifi_status: MFP: Optional [00:00:07.292,846] <inf> MQTT_OVER_WIFI: cmd_wifi_status: RSSI: -40
四、添加MQTT功能
MQTT(消息队列遥测传输协议),是一种基于 发布/订阅 (publish/subscribe)模式的"轻量级"通讯协议, 该协议构建于TCP/IP协议上 。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
在项目配置文件中添加如下配置:
# MQTT CONFIG_MQTT_LIB=y CONFIG_MQTT_CLEAN_SESSION=y # Application CONFIG_MQTT_PUB_TOPIC="publish/buttonStatus" CONFIG_MQTT_SUB_TOPIC="subscribe/ledAction" CONFIG_MQTT_BROKER_HOSTNAME="broker.hivemq.com" CONFIG_MQTT_CLIENT_ID="Shanghai-9989" CONFIG_MQTT_BROKER_PORT=1883
连接Wi-Fi后,连接到MQTT Broker,在main函数中调用:static void connect_mqtt(void),连接成功后,会在循环中检查mqtt心跳。
err = poll(&fds, 1, mqtt_keepalive_time_left(&client)); if (err < 0) { LOG_ERR("Error in poll(): %d", errno); break; }
关于MQTT相关代码,自己在测试的时候也发现,有时候手机MQTT客户端发布的主题,nrf7002dk订阅该主题,有时候延迟比较明显。后来发现Nordic论坛有个类似的帖子:
https://devzone.nordicsemi.com/f/nordic-q-a/102064/nrf7002-dk-with-mqtt-os-usage-fault-os-stack-overflow-context-area-not-valid
其中有关于上述代码的相关说明,这里引用一下:
The first line in the while(1)-loop is a polling function. Lets have a look at it:
err = poll(&fds, 1, mqtt_keepalive_time_left(&client));
So, if the fds get a flag (mqtt rx), it will loop through the code. If not, it will continue polling for mqtt_keepalive_time_left(&client). This will be the keepalive-value.
What you could do, is to use this as a "mqtt handle thread", which keeps the connection alive and does something with any data it receives. Then you could use a different thread to do any sending in any frequency you'd like.
主要的发布与订阅都是跟mqtt_evt_handler密切相关:
void mqtt_evt_handler(struct mqtt_client *const c, const struct mqtt_evt *evt) { int err; switch (evt->type) { case MQTT_EVT_CONNACK: if (evt->result != 0) { LOG_ERR("MQTT connect failed: %d", evt->result); break; } LOG_INF("MQTT client connected"); subscribe(c); break; case MQTT_EVT_DISCONNECT: LOG_INF("MQTT client disconnected: %d", evt->result); break; case MQTT_EVT_PUBLISH: { const struct mqtt_publish_param *p = &evt->param.publish; //Print the length of the recived message LOG_INF("MQTT PUBLISH result=%d len=%d", evt->result, p->message.payload.len); //Extract the data of the recived message err = get_received_payload(c, p->message.payload.len); //Send acknowledgment to the broker on receiving QoS1 publish message if (p->message.topic.qos == MQTT_QOS_1_AT_LEAST_ONCE) { const struct mqtt_puback_param ack = { .message_id = p->message_id }; /* Send acknowledgment. */ mqtt_publish_qos1_ack(c, &ack); } if (err >= 0) { data_print("Received: ", payload_buf, p->message.payload.len); // Control LED1 and LED2 if(strncmp(payload_buf,CONFIG_TURN_LED1_ON_CMD,sizeof(CONFIG_TURN_LED1_ON_CMD)-1) == 0){ dk_set_led_on(DK_LED1); } else if(strncmp(payload_buf,CONFIG_TURN_LED1_OFF_CMD,sizeof(CONFIG_TURN_LED1_OFF_CMD)-1) == 0){ dk_set_led_off(DK_LED1); } else if(strncmp(payload_buf,CONFIG_TURN_LED2_ON_CMD,sizeof(CONFIG_TURN_LED2_ON_CMD)-1) == 0){ dk_set_led_on(DK_LED2); } else if(strncmp(payload_buf,CONFIG_TURN_LED2_OFF_CMD,sizeof(CONFIG_TURN_LED2_OFF_CMD)-1) == 0){ dk_set_led_off(DK_LED2); } // Payload buffer is smaller than the received data } else if (err == -EMSGSIZE) { LOG_ERR("Received payload (%d bytes) is larger than the payload buffer size (%d bytes).", p->message.payload.len, sizeof(payload_buf)); // Failed to extract data, disconnect } else { LOG_ERR("get_received_payload failed: %d", err); LOG_INF("Disconnecting MQTT client..."); err = mqtt_disconnect(c); if (err) { LOG_ERR("Could not disconnect: %d", err); } } } break; case MQTT_EVT_PUBACK: if (evt->result != 0) { LOG_ERR("MQTT PUBACK error: %d", evt->result); break; } LOG_INF("PUBACK packet id: %u", evt->param.puback.message_id); break; case MQTT_EVT_SUBACK: if (evt->result != 0) { LOG_ERR("MQTT SUBACK error: %d", evt->result); break; } LOG_INF("SUBACK packet id: %u", evt->param.suback.message_id); break; case MQTT_EVT_PINGRESP: if (evt->result != 0) { LOG_ERR("MQTT PINGRESP error: %d", evt->result); } break; default: LOG_INF("Unhandled MQTT event type: %d", evt->type); break; } }
上述MQTT_EVT_PUBLISH就是当前订阅的主题被别的MQTT Client发布后,Nrf7002dk这个客户端接收了该报文后产生的事件。对应到本项目就是LED1ON或者LEDOFF等当前客户端订阅的主题有了更新!因此可以根据payload的内容进一步判断是需要亮灭哪个小灯。
按键方面:按键按下后,会调用回调函数button_handler,进而判断是BTN1还是BTN2被按下,然后把相应的字符串打包到MQTT报文,这样别的MQTT客户端如果订阅了这个主题,也会收到更新。
static void button_handler(uint32_t button_state, uint32_t has_changed) { switch (has_changed) { case DK_BTN1_MSK: if (button_state & DK_BTN1_MSK){ int err = data_publish(&client, MQTT_QOS_1_AT_LEAST_ONCE, CONFIG_BUTTON1_EVENT_PUBLISH_MSG, sizeof(CONFIG_BUTTON1_EVENT_PUBLISH_MSG)-1); if (err) { LOG_ERR("Failed to send message, %d", err); return; } } break; case DK_BTN2_MSK: if (button_state & DK_BTN2_MSK){ int err = data_publish(&client, MQTT_QOS_1_AT_LEAST_ONCE, CONFIG_BUTTON2_EVENT_PUBLISH_MSG, sizeof(CONFIG_BUTTON2_EVENT_PUBLISH_MSG)-1); if (err) { LOG_ERR("Failed to send message, %d", err); return; } } break; } }
五、配置MQTT Broker
本项目中使用的是MQTT DASHBOARD这个app。
连接wifi后,连接到mqtt broker,串口输出如下。可以看到nrf7002会Publish按键的状态,会订阅LED打开/关闭的命令。
[00:00:13.613,647] <inf> MQTT_OVER_WIFI: main: Connecting to MQTT Broker... [00:00:13.654,052] <inf> MQTT_OVER_WIFI: broker_init: IPv4 Address found 52.28.106.54 [00:00:13.922,821] <inf> net_mqtt: client_connect: Connect completed [00:00:15.043,395] <inf> MQTT_OVER_WIFI: mqtt_evt_handler: MQTT client connected [00:00:15.043,426] <inf> MQTT_OVER_WIFI: subscribe: Subscribing to: subscribe/ledAction len 27 [00:00:15.356,536] <inf> MQTT_OVER_WIFI: mqtt_evt_handler: SUBACK packet id: 1234 [00:00:46.666,534] <inf> MQTT_OVER_WIFI: mqtt_evt_handler: MQTT PUBLISH result=0 len=6 [00:00:46.666,625] <inf> MQTT_OVER_WIFI: data_print: Received: LED2ON [00:00:47.486,114] <inf> MQTT_OVER_WIFI: mqtt_evt_handler: MQTT PUBLISH result=0 len=7 [00:00:47.486,175] <inf> MQTT_OVER_WIFI: data_print: Received: LED2OFF [00:00:52.721,649] <inf> MQTT_OVER_WIFI: data_print: Publishing: Button 1 Pressed [00:00:52.721,679] <inf> MQTT_OVER_WIFI: data_publish: to topic: publish/buttonStatus len: 28 [00:00:53.011,718] <inf> MQTT_OVER_WIFI: mqtt_evt_handler: PUBACK packet id: 51886 [00:00:54.529,937] <inf> MQTT_OVER_WIFI: data_print: Publishing: Button 2 Pressed [00:00:54.529,968] <inf> MQTT_OVER_WIFI: data_publish: to topic: publish/buttonStatus len: 28
实物展示:请看LED2的亮灭,LED1灯用于代表网络连接,请忽略。右图按键按下后,手机上的MQTT客户端会显示:Button1Pressed或者Button2Pressed.
遇到的问题:
手机上使用的MQTT客户端软件是MQTT DashBoard,当时创建了一个client,测试过程中发现两者只有一个能够连接到Broker。手机客户端一连接,开发板这边就断开连接了。
后来在CSDN找到了类似的问题:MQTT client conflict 客户端ID冲突导致重复掉线问题 https://blog.csdn.net/Holy_Q/article/details/129519248?spm=1001.2014.3001.5502
所以问题的原因是当时使用的client id与nrf7002dk使用的是相同的id。
解决的办法就是在手机app创建客户端的时候,客户端id使用随机的一个id,必须保证与nrf7002dk用的是不同的id.
总结:
后续改进空间:
1. 可以使用单独的线程来处理MQTT相关任务,加入小屏幕进行显示等。
2. Zephyr这块Iot领域的专业RTOS,这次了解的比较浅,期待自己能够深入的去了解一下。