02-MQTT Demo
Funpack2-6:nRF7002-DK
开发环境的搭建请参考上篇文章:
https://www.bilibili.com/read/cv25137087?spm_id_from=333.999.0.0书签:Nordic+VSCode 开发环境搭建
硬件介绍
nRF5340
主要特性
- 高性能应用处理器
- 具有FPU和DSP指令的128/64 MHz Arm Cortex-M33
- 1 MB閃存+ 512 KB RAM
- 8 KB双路緩存
- 完全可编程的网络处理器
- 具有2 KB指令緩存的64 MHz Arm Cortex-M33
- 256 KB闪存+ 64 KB RAM
- 更高级别安全性
- 蓝牙低功耗
- 蓝牙5.2
- LE Audio
- 2Mbps数据吞吐量
- 蓝牙mesh
- Thread Zigbee
- NFC
- 带有EasyDMA的全系列数字接口
- 全速USB
- 用于外部存储器的96MHz加密QSPI
nRF7002
特征
- Wi-Fi 6基站(STA)
- 2.4 GHz和5 GHz双频段
- 符合802.11a/b/g/n/ac/ax标准
- 用于物联网的低功耗安全Wi-Fi
- 与低功耗蓝牙的理想共存
- 目标唤醒时间(TWT)
- SPI / QSPI
- ...
开发板
特征
- 作为主处理器的nRF5340 SoC
- nRF7002 Wi-Fi 协同IC
- Arduino连接器
- 两个可编程按钮
- 2.4GHz和5GHz天线
- 电流测量引脚
实验所用外设:
- GPIO(LED)
- SPI(nRF5340 与nRF7002)通讯
设计思路
实现通过网络远程控制LED灯
- 电脑和nRF7002都通过路由器连接到MQTT Broker上
- PC软件订阅LED状态的Topic 开发板订阅LED控制的Topic
- PC软件根据LED状态消息更新页面上LED的状态
- 开发板收到LED控制的消息开关LED灯
背景知识
ZephryOS
Zephyr OS 基于小占用内核,设计用于资源受限的嵌入式系统:从简单的嵌入式环境传感器和 LED 可穿戴设备到复杂的嵌入式控制器、智能手表和物联网无线应用。
特性
Zephyr 提供了大量且不断增长的功能,包括:
丰富的内核服务套件
- 用于协作、基于优先级、非抢占式和抢占式线程的多线程服务,_具有可选的循环时间切片。包括 POSIX pthreads 兼容 API 支持。
- 中断服务_用于中断处理程序的编译时注册。
- 用于动态分配和释放固定大小或可变大小内存块的_内存分配服务。
- 用于二进制信号量、计数信号量和互斥信号量的_线程间同步服务。
- 用于基本消息队列、增强消息队列和字节流的_线程间数据传递服务。
- 电源管理服务,例如总体、应用程序或策略定义的系统电源管理和细粒度、驱动程序定义的设备电源管理。
多种调度算法
- 协作和抢占式调度
- 最早截止日期优先 (EDF)
- 元 IRQ 调度实现“中断下半部分”或“tasklet”行为
- 时间切片:在同等优先级的可抢占线程之间启用时间切片
- 多种排队策略:
- 简单链表就绪队列
- 红/黑树就绪队列
- 传统多队列就绪队列
高度可配置/模块化
允许应用程序仅根据需要合并其所需的功能,并指定其数量和大小。
内存保护
在 x86、ARC 和 ARM 架构、用户空间和内存域上实现可配置的特定于架构的堆栈溢出保护、内核对象和设备驱动程序权限跟踪以及具有线程级内存保护的线程隔离。
对于没有 MMU/MPU 和内存受限设备的平台,支持将特定于应用程序的代码与自定义内核相结合,以创建在系统硬件上加载和执行的整体映像。应用程序代码和内核代码都在单个共享地址空间中执行。
优化的设备驱动模型
提供用于配置属于平台/系统的驱动程序的一致设备模型,以及用于初始化配置到系统中的所有驱动程序的一致模型,并允许跨具有公共设备/IP 块的平台重用驱动程序。
设备树支持
使用devicetree来描述硬件。来自 devicetree 的信息用于创建应用程序映像。
主持多种协议的网络协议栈
网络支持功能齐全且经过优化,包括 LwM2M 和 BSD 套接字兼容支持。还提供 OpenThread 支持(在 Nordic 芯片组上) - 一个网状网络,旨在安全可靠地连接家庭周围的数百个产品。
低功耗蓝牙
符合蓝牙 5.0 标准 (ESR10) 和蓝牙低功耗控制器支持(LE 链路层)。包括蓝牙网状网络和蓝牙资格就绪的蓝牙控制器。
-
- 具有所有可能的 LE 角色的通用访问配置文件 (GAP)
- 通用属性配置文件 (GATT)
- 配对支持,包括蓝牙 4.2 的安全连接功能
- 清晰的 HCI 驱动程序抽象
- 原始 HCI 接口将 Zephyr 作为控制器运行,而不是完整的主机堆栈
- 经多个流行控制器验证
- 高度可配置
...
MQTT Moudle
MQTT(消息队列遥测传输)是一种运行在 TCP/IP 堆栈之上的应用层协议。它是一种用于机器对机器通信的轻量级发布/订阅消息传输。有关协议本身的更多信息,请参阅http://mqtt.org/。
Zephyr 提供了一个基于 BSD 套接字 API 构建的 MQTT 客户端库。该库可针对每个客户端进行配置,支持 MQTT 版本 3.1.0 和 3.1.1。Zephyr MQTT 实现可与通过 TCP 进行通信的普通套接字一起使用,也可与通过 TLS 进行通信的安全套接字一起使用。有关 Zephyr 套接字的更多信息,请参阅BSD 套接字。
MQTT 客户端需要连接到 MQTT 服务器。这样的服务器称为 MQTT Broker,负责管理客户端订阅并分发客户端发布的消息。MQTT 代理有许多实现,其中之一是 Eclipse Mosquitto。有关 Eclipse Mosquitto 项目的更多信息,请参阅https://mosquitto.org/
Example
要创建 MQTT 客户端,需要定义客户端上下文结构和缓冲区:
/* Buffers for MQTT client. */
static uint8_t rx_buffer[256];
static uint8_t tx_buffer[256];
/* MQTT client context */
static struct mqtt_client client_ctx;
可以在应用程序中创建多个 MQTT 客户端实例并独立管理。此外,还需要一个 MQTT Broker 地址信息的结构。此结构必须在 MQTT 客户端的整个生命周期内可访问,并且可以在 MQTT 客户端之间共享:
/* MQTT Broker address information. */
static struct sockaddr_storage broker;
客户端上下文结构需要在使用之前进行初始化和设置。TCP 传输的示例配置如下所示:
mqtt_client_init(&client_ctx);
/* MQTT client configuration */
client_ctx.broker = &broker;
client_ctx.evt_cb = mqtt_evt_handler;
client_ctx.client_id.utf8 = (uint8_t *)"zephyr_mqtt_client";
client_ctx.client_id.size = sizeof("zephyr_mqtt_client") - 1;
client_ctx.password = NULL;
client_ctx.user_name = NULL;
client_ctx.protocol_version = MQTT_VERSION_3_1_1;
client_ctx.transport.type = MQTT_TRANSPORT_NON_SECURE;
/* MQTT buffers configuration */
client_ctx.rx_buf = rx_buffer;
client_ctx.rx_buf_size = sizeof(rx_buffer);
client_ctx.tx_buf = tx_buffer;
client_ctx.tx_buf_size = sizeof(tx_buffer);
ZBUS
Zephyr 消息总线_- Zbus_是一种轻量级且灵活的消息总线,为线程之间的通信提供了一种简单的方式。
概念
线程可以使用zbus向所有感兴趣的观察者广播消息。可以进行多对多通信。总线实现消息传递和发布/订阅通信范例,使线程能够通过共享内存进行同步或异步通信。通过 zbus 的通信是基于通道的,其中线程使用消息发布和读取消息。此外,线程可以观察通道并在通道被修改时从总线接收通知。下图显示了使用 zbus 的典型应用程序示例,其中应用程序逻辑(独立于硬件)通过消息总线与其他线程对话。请注意,线程之间是解耦的,因为它们只使用 zbus 的通道,不需要彼此了解即可进行通信。
该总线包括:
- 由唯一标识符、其控制元数据信息和消息本身组成的通道集;
- 虚拟分布式事件调度程序(VDED),负责向观察者发送通知的总线逻辑。VDED 逻辑在同一线程上下文中的发布操作内部运行,为总线提供了分布式执行的概念。当线程发布到通道时,它还会将通知传播给观察者;
- 线程(订阅者)和回调(侦听器)从总线发布、读取和接收通知。
DT Device
Zephyr 内核支持多种设备驱动程序。驱动程序是否可用取决于板卡和驱动程序。
Zephyr 设备模型提供了一致的设备模型,用于配置系统中的驱动程序。设备模型负责初始化配置到系统中的所有驱动程序。
每种类型的驱动程序(例如 UART、SPI、I2C)均由通用类型 API 支持。
在此模型中,驱动程序在驱动程序初始化期间填充指向结构的指针,该结构包含指向其 API 函数的函数指针。这些结构按初始化级别顺序放入 RAM 部分。
网络图片:https://docs.zephyrproject.org/latest/_images/device_driver_model.svg
GPIO Control
LED API 提供对单个和条状发光二极管的访问。
int led_blink ( const struct device * dev , uint32_t led , uint32_tdelay_on , uint32_t delay_off ) 使 LED 闪烁。
此可选例程开始在给定时间段内使 LED 永远闪烁。
参数:
- dev - LED 设备
- led - LED 数量
- delay_on - LED 应亮起的时间段(以毫秒为单位)
- delay_off - LED 应关闭的时间段(以毫秒为单位)
退货:
0 表示成功,负值表示错误
int led_on ( const struct device * dev , uint32_t led ) 打开 LED
该例程打开 LED
参数:
- dev - LED 设备
- led - LED 数量
退货:
0 表示成功,负值表示错误
开发流程
工程创建
Application template选择nrf的mqtt demo
Build ConfigKconfig fragments需要添加7002的overlay和config |
Kconfig
配置MQTT Broker的hostname port 发布和定于的topic
硬件原理
根据原理图可以得知,LED1和LED2分别接到了P1.06和P1.07,IO口拉低LED亮,同时连接的网络是LEDS 这个也与设备树的描述相对应
设备树描述
代码修改
1.根据上文可知 可以使用ZBUS在各个模块(线程)间通讯,project\src\common\message_channel中已经创建了TRIGGER_CHAN、PAYLOAD_CHAN、NETWORK_CHAN等通道 用于触发,更新网络状态等工作,我们可以增加一个通道用于同步LED Control信号
//(project\src\common\message_channel.c)
ZBUS_CHAN_DEFINE(NET_CONTROL_CHAN,
int,
NULL,
NULL,
ZBUS_OBSERVERS(led),
ZBUS_MSG_INIT(0)
);
2.参考上述GPIO LED相关背景知识,在project\src\modules\led\led.c中添加LED1控制的相关代码
void led_callback(const struct zbus_channel *chan)
{
if (&NETWORK_CHAN == chan)
{
...
}
else if (&NET_CONTROL_CHAN == chan)
{
const bool *status = zbus_chan_const_msg(chan);
LOG_INF("Recv LED Control CMD! :%d",*status);
switch (*status) {
case true:
err = led_on(led_device, LED_0_GREEN);
if (err) {
LOG_ERR("led_on, error: %d", err);
}
break;
case false:
err = led_off(led_device, LED_0_GREEN);
if (err) {
LOG_ERR("led_off, error: %d", err);
}
break;
}
}
}
3.在project\src\modules\transport\transport.c 中添加部分收到mqtt消息后的处理措施
static void on_mqtt_publish(struct mqtt_helper_buf topic, struct mqtt_helper_buf payload)
{
int err;
bool status = false;
LOG_INF("Received payload: %.*s on topic: %.*s", payload.size,
payload.ptr,
topic.size,
topic.ptr);
if(payload.size==6)//这里跟上位机约定好 直接通过字符串长度判断是否开灯
{
status = true;
LOG_INF("Net Control LED0 ON");
}
else
{
status = false;
LOG_INF("Net Control LED0 OFF");
}
err = zbus_chan_pub(&NET_CONTROL_CHAN, &status, K_MSEC(100));//通过ZBUS发送消息给LED
if (err) {
LOG_ERR("zbus_chan_pub, error: %d", err);
SEND_FATAL_ERROR();
}
}
4.在src\modules\trigger\trigger.c里添加btn状态处理的代码
static void button_handler(uint32_t button_states, uint32_t has_changed)
{
uint32_t buttons = button_states & has_changed;
LOG_INF("User Key Status:%d Changed:%d",button_states,has_changed);
if (has_changed & USER_KEY_1)
{
if(button_states & USER_KEY_1)
{
//LOG_INF("User Key1 Pressed!");
message_send(1,true);
}
else
{
message_send(1,false);
//LOG_INF("User Key1 Released!");
}
}
if (has_changed & USER_KEY_2)
{
if(button_states & USER_KEY_2)
{
message_send(2,true);
//LOG_INF("User Key2 Pressed!");
}
else
{
message_send(2,false);
//LOG_INF("User Key2 Released!");
}
}
}
上位机
上位机使用QT+MQTT库进行开发,分为界面和逻辑2部分
界面
界面部分比较简单,主要是有Broker的地址和端口号设置,上位机与MQTT Broker的连接状态有指示灯表示,绿色为正常连接。LED Switch为控制开发板的开关,Switch 的状态改变后上位机将向MQTT Broker发送LED控制的Payload,从而实现远程控制LED,同时 板子上的按键按下后,会向MQTT Broker发送Btn状态的Payload,上位机在接收到消息后,会调整BTN的状态指示灯
代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->btn_disconnect->setEnabled(false);
pub_topic = "arilink/subscribe/topic";
sub_topic = "arilink/publish/topic";
client = new QMqttClient(this);
client->setHostname(ui->address->text());
client->setPort(ui->port->value());
client->setAutoKeepAlive(true);
connect(client,&QMqttClient::stateChanged,this,&MainWindow::connectStatus);
connect(client,SIGNAL(messageReceived(QByteArray,QMqttTopicName)),this,SLOT(platformDataParsing(QByteArray,QMqttTopicName)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_switchButton_checkedChanged(bool checked)
{
QString payload="";
if(checked)
{
payload = "led_on";
qDebug()<<"LED ON";
//ui->led_status->setBgColor(QColor(0,200,0));
}
else
{
payload = "led_off";
qDebug()<<"LED OFF";
//ui->led_status->setBgColor(QColor(166,166,166));
}
if(client->publish(pub_topic,payload.toUtf8(),0)==-1)
{
qDebug() << "public error";
}
}
void MainWindow::on_btn_connect_clicked()
{
client->connectToHost();
ui->btn_connect->setEnabled(false);
ui->btn_disconnect->setEnabled(true);
}
void MainWindow::on_btn_disconnect_clicked()
{
client->disconnectFromHost();
ui->btn_connect->setEnabled(true);
ui->btn_disconnect->setEnabled(false);
}
void MainWindow::connectStatus()
{
QString content;
if (client->state()== QMqttClient::Disconnected)
{
content = QDateTime::currentDateTime().toString()
+ QLatin1String(": Disconnected")
+ QLatin1Char('\n');
ui->net_status->setBgColor(QColor(200,0,0));
}
else if (client->state()== QMqttClient::Connecting) {
content = QDateTime::currentDateTime().toString()
+ QLatin1String(": Connecting")
+ QLatin1Char('\n');
ui->net_status->setBgColor(QColor(255,211,91));
}
else if (client->state()== QMqttClient::Connected) {
content = QDateTime::currentDateTime().toString()
+ QLatin1String(": Connected")
+ QLatin1Char('\n');
client->subscribe(sub_topic);
ui->net_status->setBgColor(QColor(0,200,0));
}
qDebug()<<content;
}
void MainWindow::platformDataParsing(const QByteArray &message, const QMqttTopicName &topic)
{
QString content;
content = QDateTime::currentDateTime().toString();
content += QLatin1String(" Received Topic:")+topic.name();
content += QLatin1String(" Message:")+message+QLatin1Char('\n');
qDebug()<<content;
if(message=="BTN1 Status:1")
{
ui->btn1_status->setBgColor(QColor(0,200,0));
}
else if (message=="BTN1 Status:0")
{
ui->btn1_status->setBgColor(QColor(166,166,166));
}
else if (message=="BTN2 Status:1")
{
ui->btn2_status->setBgColor(QColor(0,200,0));
}
else if (message=="BTN2 Status:0")
{
ui->btn2_status->setBgColor(QColor(166,166,166));
}
else
{
}
}
总结
本次项目是本人第一次实际操作Nordic的芯片和Zephyr OS,也是非常巧合能有这样一个项目可以将这两者结合起来学习,总的来说,收获还是挺多的,但是在开发过程中还是遇到了一些问题,但是经过反复不断的尝试终于把问题解决了,很珍惜这次学习的机会,也感谢硬禾科技提供这次项目。