一、基本描述
首先介绍下nRF7002-DK开发板,这款包含了Nordic公司最新推出的基于低功耗双频WIFI 6协议的nRF7002芯片,以及作为主处理器的nRF5340芯片,两者通过QSPI总线通信连接。板上还支持蓝牙、NFC、Thread等无线协议。官方提供的产品特性如下:
- 用于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连接器
再介绍一下本项目采用的软件开发平台,由Nordic官方推出的nRF Connect SDK,搭配安装在Visual Studio Code的官方插件组合构成。Visual Studio Code插件集工具链管理、代码维护、固件烧录、Debug调试为一体,很是方便。通过下载nRF Connect for Desktop软件,打开后下载工具链管理工具,在工具链管理工具里下载对应的nRF Connect SDK,这里采用的是2.4.1版本。详细安装方法见Installation — nRF Connect SDK 2.4.99 documentation (nordicsemi.com)
nRF Connect SDK的软件代码是基于开源的Zephyr实时操作系统和Nordic官方提供的代码,并采用Zyphyr的west工具进行代码管理。这样很多Zyphyr的代码案例,同样也适用于Nordic的开发板。可以理解成nRF Connect SDK是在Zyphyr软件代码的基础上,集成了Nordic自身维护开发的模块。配置好的SDK代码根目录以及nrf目录信息如下:
本项目通过WIFI连接开发板,通过MQTT协议,实现远程控制LED灯,和获取按键信息。MQTT本身是开源协议,也有很多开源服务器软件、客户端软件。本次使用的是Mosquitto软件Eclipse Mosquitto,消息服务器使用的是官方提供的版本,也可自行搭建Mosquitto服务器。
nRF7002-DK开发板分别提供可编程的两个LED和两个按键,远程操作可选择点亮或者关闭LED1或LED2;在板卡上按下按键1或按键2时,在远程能收到消息,说明当面按下的是哪个按键。具体分解下本次任务:远程控制LED是远程将控制信令从MQTT服务端发出,到达开发板所在的MQTT客户端,这里开发板使用的是MQTT的订阅模式;而远程获取按键信息,是开发板作为MQTT服务端,通过按下对应按键,触发MQTT的发布模式,在远端做好对应的订阅设置,从而实现在远程接收传递的按键信息,这里开发板使用的是MQTT的发布模式。
Mosquitto提供的服务器地址是公开可用的,别的设备也会连接使用,也可能会使用相同的发送或订阅主题名称,这时就要确定唯一的设备标识来区分不同设备发出或者接收的消息。设置MQTT代理服务器地址为test.mosquitto.org,设备标识也可以不填,默认是开发板的MAC地址。另外可以定义发布和订阅的主题,这里用默认的就行。
CONFIG_MQTT_SAMPLE_TRANSPORT_BROKER_HOSTNAME="test.mosquitto.org"
CONFIG_MQTT_SAMPLE_TRANSPORT_CLIENT_ID="There_is_another_nrf7002dk"
CONFIG_MQTT_SAMPLE_TRANSPORT_PUBLISH_TOPIC="my/publish/topic"
CONFIG_MQTT_SAMPLE_TRANSPORT_SUBSCRIBE_TOPIC="my/subscribe/topic"
开发板连接的WIFI信息,同样也是通过配置文件设置,需要后续写入到编译生成的固件之中。如下第一个配置表示是否启用静态WIFI,后面两个配置是设置静态WIFI的名称和密码。
CONFIG_WIFI_CREDENTIALS_STATIC=y
CONFIG_WIFI_CREDENTIALS_STATIC_SSID="xxx"
CONFIG_WIFI_CREDENTIALS_STATIC_PASSWORD="password"
二、软件流程
这里主要参考MQTT — nRF Connect SDK 2.4.99 documentation (nordicsemi.com)的案例实现。代码上将功能分成多个模块来实现,Trigger模块用于定时器触发或者按键触发信号发送;Sampler模块用于订阅Trigger发出的消息,进行过滤转发给Payload通道;Transport用于管理MQTT网络,接收MQTT订阅消息,或者发送Payload通道提供的MQTT发布消息;Network模块用于根据配置维护开机后的联网状态。
Trigger模块的主要代码为trigger_task函数里面开机初始化LED以及按键信息,为按键设置回调触发函数button_hander,在检测到按键状态信息发生改变时,通过message_send函数给Trigger通道发送消息。
static void message_send(void)
{
int not_used = -1;
int err;
err = zbus_chan_pub(&TRIGGER_CHAN, ¬_used, K_SECONDS(1));
if (err) {
LOG_ERR("zbus_chan_pub, error: %d", err);
SEND_FATAL_ERROR();
}
}
static void button_handler(uint32_t button_states, uint32_t has_changed)
{
if (has_changed & button_states) {
message_send();
}
}
static void trigger_task(void)
{
if (dk_leds_init() != 0) {
LOG_ERR("Failed to initialize the LED library");
}
int err = dk_buttons_init(button_handler);
if (err) {
LOG_ERR("dk_buttons_init, error: %d", err);
SEND_FATAL_ERROR();
return;
}
}
Sampler模块订阅Trigger通道上发出的消息,对收到的进行过滤、组装,再将组装后的消息发送给Payload通道。
#define FORMAT_STRING "Current pressed button is: %d!"
static void sample(void)
{
struct payload payload = { 0 };
uint32_t uptime = k_uptime_get_32();
uint32_t buttonpin = dk_get_buttons();
int err, len;
len = snprintk(payload.string, sizeof(payload.string), FORMAT_STRING, buttonpin);
if ((len < 0) || (len >= sizeof(payload))) {
LOG_ERR("Failed to construct message, error: %d", len);
SEND_FATAL_ERROR();
return;
}
err = zbus_chan_pub(&PAYLOAD_CHAN, &payload, K_SECONDS(1));
if (err) {
LOG_ERR("zbus_chan_pub, error:%d", err);
SEND_FATAL_ERROR();
}
}
Transport模块主要有两个功能,一个是订阅Payload通道的消息,作为MQTT服务端,将组装后的消息发布出去,具体是将消息发送给MQTT代理服务器;另外是作为MQTT的消息订阅端,订阅从MQTT代理服务器发送的消息,接收后进行处理。on_mqtt_publish函数是接收MQTT订阅消息处理的地方,在这里根据字符串匹配控制LED的状态。on_mqtt_publish函数是作为回调函数在transport_task函数中使用,在transport_task函数内还订阅接收来自Payload通道的消息,组装后发送给MQTT代理服务器。
#define CONFIG_TURN_LED1_ON "Turn LED1 on"
#define CONFIG_TURN_LED1_OFF "Turn LED1 off"
#define CONFIG_TURN_LED2_ON "Turn LED2 on"
#define CONFIG_TURN_LED2_OFF "Turn LED2 off"
static void on_mqtt_publish(struct mqtt_helper_buf topic, struct mqtt_helper_buf payload)
{
LOG_INF("Received payload: %.*s on topic: %.*s", payload.size, payload.ptr, topic.size, topic.ptr);
if(strncmp(payload.ptr, CONFIG_TURN_LED1_ON, payload.size) == 0){
dk_set_led_on(DK_LED1);
}
else if(strncmp(payload.ptr, CONFIG_TURN_LED1_OFF, payload.size) == 0){
dk_set_led_off(DK_LED1);
}
else if(strncmp(payload.ptr, CONFIG_TURN_LED2_ON, payload.size) == 0){
dk_set_led_on(DK_LED2);
}
else if(strncmp(payload.ptr, CONFIG_TURN_LED2_OFF, payload.size) == 0){
dk_set_led_off(DK_LED2);
}
}
static void transport_task(void)
{
int err;
const struct zbus_channel *chan;
enum network_status status;
struct payload payload;
struct mqtt_helper_cfg cfg = {
.cb = {
.on_connack = on_mqtt_connack,
.on_disconnect = on_mqtt_disconnect,
.on_publish = on_mqtt_publish,
.on_suback = on_mqtt_suback,
},
};
/* Initialize and start application workqueue.
* This workqueue can be used to offload tasks and/or as a timer when wanting to
* schedule functionality using the 'k_work' API.
*/
k_work_queue_init(&transport_queue);
k_work_queue_start(&transport_queue, stack_area,
K_THREAD_STACK_SIZEOF(stack_area),
K_HIGHEST_APPLICATION_THREAD_PRIO,
NULL);
err = mqtt_helper_init(&cfg);
if (err) {
LOG_ERR("mqtt_helper_init, error: %d", err);
SEND_FATAL_ERROR();
return;
}
/* Set initial state */
smf_set_initial(SMF_CTX(&s_obj), &state[MQTT_DISCONNECTED]);
while (!zbus_sub_wait(&transport, &chan, K_FOREVER)) {
s_obj.chan = chan;
if (&PAYLOAD_CHAN == chan) {
err = zbus_chan_read(&PAYLOAD_CHAN, &payload, K_SECONDS(1));
if (err) {
LOG_ERR("zbus_chan_read, error: %d", err);
SEND_FATAL_ERROR();
return;
}
s_obj.payload = payload;
LOG_INF("Current payload: %.*s", sizeof(payload.string), payload.string);
err = smf_run_state(SMF_CTX(&s_obj));
if (err) {
LOG_ERR("smf_run_state, error: %d", err);
SEND_FATAL_ERROR();
return;
}
}
}
}
三、功能展示
1、分别按下开发板的按键1和按键2,能通过mosquitto_sub.exe接收到订阅信息,显示当前按下的是哪个按钮。
2、使用mosquitto_pub.exe向MQTT代理服务器发送点亮LED1消息,LED1随后被点亮。
3、使用mosquitto_pub.exe向MQTT代理服务器发送点亮LED2消息,LED2随后被点亮。
4、使用mosquitto_pub.exe向MQTT代理服务器发送关闭LED1消息,LED1随后被关闭。
四、心得体会
近期也在折腾其他arm板卡,深度感受到了设备树概念的方便之处,只需要有原本的设计原理图,新增的外设也仅需要编辑设备树,实现硬件到软件上的支持。这一点也在nRF7002-DK开发板上有体现,开发板是基于Zephyr RTOS软件系统上运行,结构功能上是类似linux内核。平台自身提供了丰富的案例,方便对功能进行裁剪、整合。