任务介绍
本项目实现了Funpack第3-4期活动的任务二,在MCXN947开发板及扩展板上,实现了基于MQTT的多传感器测控。
硬件平台
首先介绍本次用到的开发板:来自恩智浦的FRDM-MCXN947,这是一块性能很强的板卡,主控是主频高达150MHz的双核ARM Cortex-M33。我把它理解成AI增强版的LPC55S69,它们使用相同的内核,最不一样的地方是MCXN947配备了一个用于AI加速的协处理器,并且在安全、存储资源等各方面都做了增强。这次正好借着Funpack活动上车体验一下。
任务分析与实现
这次主办方出了三种任务
- 任务一是做一个多协议通信HUB,使用到USB协议栈、串口和CAN总线,他们之间互相做输入输出转换,需要准备一个额外的CAN模块。
- 任务二是使用板卡的以太网网口,实现远程读取板卡和外部的的传感器和输入设备,并控制板载的三色LED,以及外部的执行设备。
- 任务三是使用板卡的AI加速,实现音频或图像分类。
这次我选择了任务二。因为题目要求增加额外的传感器和执行器模块,我手上正好有一块带可燃气体传感器、蜂鸣器和OLED显示屏的扩展板。
所以,项目最终涉及板载网口的网络通信、板载温度传感器、外接OLED屏幕的I2C通信、蜂鸣器的PWM输出、三色LED的GPIO输出、板载触摸开关的触摸输入、板载按键的输入、外接可燃气体传感器MQ-2的ADC模拟输入,可以说硬件功能覆盖的相当全面。
功能定好了,接下来就是分模块调通,最后合并联调。
因为这块板卡年初的时候在RT-Thread社区的活动中已经做好了大部分的功能移植和测试,相比于裸机开发,在RT-Thread平台上可以更容易的集成相对复杂的能力。于是我选择在RT-Thread平台上进行开发。
我们首先调最核心最复杂的网络模块,首先使能网口驱动,在RT-Thread Components -> Device Drivers中选择Using ethernet phy device drivers。
随后在RT-Thread Components -> Network中选择网络相关的组件。
这两个基础组件打开后,接上网线,就能通过ifconfig命令看到板卡的ip地址了。
如果只使用LWIP做最基础的Socket通信,有点过于底层。我们选择更高层的MQTT协议来做通信,到IoT列表打开RyanMQTT选项,就可以在RT-Thread的命令行使用基本的命令来测试连接MQTT服务器,以及发布和监听Topic。
基础的网络功能调好了,我们继续调其他模块,对于两个I2C设备,OLED屏幕很好接入,我们在RT-Thread的软件包里引入SSD1306的驱动,同时在硬件列表使能I2C1,简单的配置后就可以正常驱动。
板载温度传感器就要略微复杂一点,因为它对接了主控的I3C引脚,我们需要单独引入fsl_i3c这个库文件来驱动i3c接口,好在本身的读取逻辑很简单,在调试的过程我们需要持续观察逻辑分析仪,来确保引脚正常输出了波形和时序。
随后是触摸模块,我们可以从官网下载一个触摸例程来移植,例程中驱动触摸模块的代码文件比较多,同时还附带了FreeMaster调试工具的代码,因为FreeMaster在我们的程序里不是必须的,这里需要做一下裁剪。
蜂鸣器和ADC的移植比较相似,都是先找一个例程跑通,然后移植初始化以及驱动代码,MCXN947有很多种方式来实现PWM,这里我选择使用FlexIO的PWM输出功能,这一过程中也可以通过示波器来观察引脚有没有正常输出波形。
基础设备都驱动好了,就可以开始做MQTT数据发送和接收了,因为每个设备都是在各自的线程控制工作的,这里我用操作系统的消息队列来实现它们和MQTT线程的信息交互。ADC、温度的数据会循环采集,触摸、按键则只在状态变化时触发发送,MQTT线程收到它们的消息后,转换成json字符串,通过Topic为uplink的上行通道传给上位机。上位机在页面上控制三色LED和蜂鸣器,通过Topic为downlink的下行通道通知开发板。
MQTT线程代码
// 处理消息的通用函数
static void process_message(struct rt_messagequeue *queue, const char *type, void (*add_value)(cJSON *, data_t))
{
data_t msg;
if (rt_mq_recv(queue, &msg, sizeof(data_t), RT_WAITING_NO) > 0)
{
if (client != NULL)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", type);
add_value(root, msg);
char *payload_str = cJSON_PrintUnformatted(root);
RyanMqttPublish(client, "uplink", payload_str, strlen(payload_str), RyanMqttQos0, 0);
cJSON_free(payload_str);
cJSON_Delete(root);
}
}
}
static void my_mqtt_task(void)
{
// 处理adc消息
process_message(&adc_queue, "adc", add_double_value);
// 处理temp消息
process_message(&temp_queue, "temp", add_double_value);
// 处理btn消息
process_message(&btn_queue, "btn", add_uint8_value);
// 处理touch消息
process_message(&touch_queue, "touch", add_uint8_value);
// rt_kprintf("my_mqtt_task.\r\n");
}
static void mqtt_entry(void *paremeter)
{
MY_MQTT_Init();
while (1)
{
my_mqtt_task();
rt_thread_mdelay(100);
}
}
int run_mqtt(void)
{
mqtt_thread = rt_thread_create("mqtt_task", mqtt_entry, RT_NULL, 4096, 15, 20);
if (mqtt_thread != RT_NULL)
{
rt_thread_startup(mqtt_thread);
}
return 0;
}
上位机交互界面使用PyQT开发,用GPT可以快速的生成一个原型程序,界面很简单但功能够用,简单修改debug一下就可以跑起来。
上位机代码
import sys
import json
from PyQt5.QtWidgets import QApplication, QGroupBox, QWidget, QLabel, QPushButton, QSlider, QVBoxLayout, QHBoxLayout
from PyQt5.QtCore import Qt
import paho.mqtt.client as mqtt
# MQTT 相关参数
broker_address = "localhost" # MQTT Broker 地址
port = 1883
topic_subscribe = "uplink"
topic_publish = "downlink"
class MyWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("MQTT 上位机")
self.setGeometry(300, 300, 300, 300)
# 创建 MQTT 客户端
self.client = mqtt.Client()
self.client.on_connect = self.on_connect
self.client.on_message = self.on_message
self.client.connect(broker_address, port=port, keepalive=60)
self.client.loop_start()
# 创建控件
self.label_adc = QLabel("ADC值: ")
self.label_temp = QLabel("温度值: ")
self.label_touch = QLabel("触摸状态: ")
self.label_btn = QLabel("按钮状态: ")
groupBox_sensor = QGroupBox("传感器状态")
# 创建两个水平布局,分别容纳两个QLabel
hlayout_sensor1 = QHBoxLayout()
hlayout_sensor1.addWidget(self.label_adc)
hlayout_sensor1.addWidget(self.label_temp)
hlayout_sensor2 = QHBoxLayout()
hlayout_sensor2.addWidget(self.label_touch)
hlayout_sensor2.addWidget(self.label_btn)
# 创建垂直布局,将两个水平布局放入其中
vlayout_sensor = QVBoxLayout()
vlayout_sensor.addLayout(hlayout_sensor1)
vlayout_sensor.addLayout(hlayout_sensor2)
groupBox_sensor.setLayout(vlayout_sensor)
# 创建 RGB 滑块组
groupBox_rgb = QGroupBox("RGB 控制")
hlayout_rgb = QHBoxLayout()
for color in ['R', 'G', 'B']:
label = QLabel(color)
slider = QSlider(Qt.Horizontal)
slider.setRange(0, 1)
slider.valueChanged.connect(lambda value, color=color: self.on_slider_changed(value, color))
hlayout_rgb.addWidget(label)
hlayout_rgb.addWidget(slider)
setattr(self, f"slider_{color.lower()}", slider) # 动态设置属性
groupBox_rgb.setLayout(hlayout_rgb)
# 创建按钮组
groupBox_control = QGroupBox("蜂鸣器控制")
hbox_control = QHBoxLayout()
self.button_buzzer = QPushButton("蜂鸣器")
self.button_buzzer.clicked.connect(self.on_buzzer_clicked)
hbox_control.addWidget(self.button_buzzer)
groupBox_control.setLayout(hbox_control)
# 创建主布局
vlayout = QVBoxLayout()
vlayout.addWidget(groupBox_sensor)
vlayout.addWidget(groupBox_rgb)
vlayout.addWidget(groupBox_control)
self.setLayout(vlayout)
# 设置样式表
self.setStyleSheet("""
QGroupBox {
border: 1px solid gray;
border-radius: 5px;
padding: 5px;
}
""")
def on_connect(self, client, userdata, flags, rc):
print("Connected with result code "+str(rc))
self.client.subscribe(topic_subscribe)
print("subscribe")
def on_message(self, client, userdata, msg):
print(msg.payload.decode("utf-8"))
data = json.loads(msg.payload.decode("utf-8"))
if data["type"] == "adc":
self.label_adc.setText("ADC值: {:.2f}".format(data["value"]) + ' V')
elif data["type"] == "temp":
self.label_temp.setText("温度值: {:.2f}".format(data["value"]) + ' ℃')
elif data["type"] == "touch":
self.label_touch.setText("触摸状态: " + str(data["value"]))
elif data["type"] == "btn":
self.label_btn.setText("按钮状态: " + str(data["value"]))
def on_buzzer_clicked(self):
data = {"type": "pwm", "value": 1 if self.button_buzzer.text() == "蜂鸣器" else 0}
self.client.publish(topic_publish, json.dumps(data))
self.button_buzzer.setText("停止蜂鸣" if data["value"] == 1 else "蜂鸣器")
def on_slider_changed(self, value, color):
r = self.slider_r.value()
g = self.slider_g.value()
b = self.slider_b.value()
data = {"type": "rgb", "value": (b << 2) | (g << 1) | r}
self.client.publish(topic_publish, json.dumps(data))
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
效果展示
活动感想
在这次活动中,因为之前就开发过LPC55S69、I.MX RT1062等NXP家的微控制器。另外手上有M2K这个电子调试的大杀器,这次开发的过程可以说很顺利。同时在开发过程中也了解到了FreeMaster工具,对于它以及在MCXN947平台上的AI开发,之后可以通过官方文档和其他伙伴的项目,更深入的进行学习。可以说通过一块板卡既锻炼了开发能力,也带出了更多的未知的知识和学习方向。这就是参加电子活动最有意思的地方。
感谢硬禾学堂和得捷电子联合举办的Funpack活动,祝硬禾的活动越办越好!