准备工作
在开始前,需要前往 GD32官网 下载如下资源:
- GD32VW55x Wi-Fi&BLE SDK:Wi-Fi和蓝牙开发工具包
- GD32 Embedded Builder:GD32 IDE
- GD32 All In One Programmer:下载器
- Nuclei RISC-V Embedded Toolchain:工具链,在此处下载
另外,还需要准备如 GDLink、JLink 等的下载器,也可以使用 UART 烧录,需要准备 USB to TTL 工具(本文使用 CH341A 烧录器 UART 烧录)
搭建 MQTT 服务器
如果你需要一台云服务器,欢迎选择雨云,雨云是一家国产云计算服务提供商,以下教程均使用雨云服务器(注意这不是必须的,你也可以在本地安装服务端)
这里使用 Eclipse Mosquitto 作为 MQTT 服务端,Mosquitto 是 MQTT 5.0、3.1.1 及 3.1 协议的开源实现
推荐使用容器技术进行快速部署,同时可以保证环境的一致性;下文是使用安装了宝塔面板的 Linux 服务器部署 Mosquitto 的步骤,你可以直接在你的本地设备安装 Docker 来部署 Mosquitto
使用宝塔面板创建 Docker 容器
可以使用宝塔面板便捷管理 Docker 镜像与容器
1. 进入宝塔面板的 Docker 镜像管理界面拉取 Eclipse Mosquitto 镜像
2. 拉取完成后,点击右侧的“创建容器”开始创建容器
3. 前往服务器防火墙控制台,放行 TCP 端口 1883
(其他云服务商操作基本相同)
使用 MQTT 客户端测试
此处使用 MQTTBox 进行测试
1. 创建 MQTT 客户端
2. 测试
在终端中输入 mosquitto_sub -t 'test/topic' -v
订阅主题 test/topic
,在 MQTTBox 点击“Publish”,即可在终端看到收到了这条消息
/ # mosquitto_sub -t 'test/topic' -v
test/topic {"msg": "Hello, World"}
在终端中按 Ctrl+C 结束订阅,
在 MQTTBox 点击“Subscribe”订阅主题 test/topic
,在终端输入 mosquitto_pub -t 'test/topic' -m '{"msg": "ni hao!"}'
即可在 MQTTBox 看到这条消息
自此,MQTT 服务器搭建完成
利用 GD32 SDK 进行硬件开发
搭建 GD32 Embedded Builder 开发环境
将下载到的 Nuclei RISC-V Embedded Toolchain 解压到 GD32 Embedded Builder 的 Tools
目录
打开并配置项目
创建项目/配置项目可以参考其他大佬的评测文章/视频,此处直接使用官方例程作为脚手架进行开发:
- 启动
Embedded Builder.exe
,选择目录MSDK\examples\wifi
作为 workspace
- 点击菜单栏 File > Open Projects from file System 导入项目,选择
MSDK\examples\wifi\mqtt_client\Eclipse_project
- 右键导入的项目,选择 Properties,进入 C/C++ Build > Settings 中的 Toolchain Settings 设置 ToolChain Path 为刚刚解压那个文件中的 bin 目录
- 打开 /main/mqtt_client_main.c,在这里编写代码(可以直接删除内容)
开发
引入头文件和定义常量
#include <stdint.h>
#include <stdio.h>
#include "app_cfg.h"
#include "gd32vw55x_platform.h"
#include "wifi_management.h"
#include "wifi_init.h"
#include "lwip/apps/mqtt.h"
#include "lwip/apps/mqtt5.h"
#include "lwip/apps/mqtt_priv.h"
#include "mqtt5_client_config.c"
定义 Wi-Fi 的 SSID 和密码、MQTT 服务器的 IP 地址和端口
#define SSID "Wi-Fi"
#define PASSWORD "PASSWORD"
#define SERVER_PORT 1883
ip_addr_t sever_ip_addr = IPADDR4_INIT_BYTES(192,168,0,10);
定义 MQTT 客户端信息
static char client_id[] = {'G', 'i', 'g', 'a', 'D', 'e', 'v', 'i', 'c', 'e', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
struct mqtt_connect_client_info_t client_info = {
.client_id = client_id,
.client_user = NULL,
.client_pass = NULL,
.keep_alive = 120,
.will_topic = NULL,
.will_topic = NULL,
.will_qos = 0,
.will_retain = 0
};
char* topic_sub = "test/topic";
static mqtt_client_t *mqtt_client = NULL;
int16_t connect_fail_reason = -1;
主函数
初始化,创建任务 MQTT Client
int main(void)
{
platform_init();
if (wifi_init()) {
printf("wifi init failed.\r\n");
}
// 初始化 GPIO
rcu_periph_clock_enable(RCU_GPIOA);
gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_5);
sys_task_create_dynamic((const uint8_t *)"MQTT Client", 4096, OS_TASK_PRIORITY(0), mqtt_client_task, NULL);
sys_os_start();
for ( ; ; );
}
任务函数
完成 Wi-Fi 连接和启动 MQTT 客户端
static void mqtt_client_task(void *param)
{
int status = 0;
char *ssid = SSID;
char *password = PASSWORD;
struct mac_scan_result candidate;
if (ssid == NULL) {
printf("ssid can not be NULL!\r\n");
goto exit;
}
/* * 1. Start Wi-Fi scan */
printf("Start Wi-Fi scan.\r\n");
status = wifi_management_scan(1, ssid);
if (status != 0) {
printf("Wi-Fi scan failed.\r\n");
goto exit;
}
sys_memset(&candidate, 0, sizeof(struct mac_scan_result));
status = wifi_netlink_candidate_ap_find(WIFI_VIF_INDEX_DEFAULT, NULL, ssid, &candidate);
if (status != 0) {
goto exit;
}
/* * 2. Start Wi-Fi connection */
printf("Start Wi-Fi connection.\r\n");
if (wifi_management_connect(ssid, password, 1) != 0) {
printf("Wi-Fi connection failed\r\n");
goto exit;
}
/* * 3. Start MQTT client */
printf("Start MQTT client.\r\n");
mqtt_client_demo();
/* * 4. Stop Wi-Fi connection */
printf("Stop Wi-Fi connection.\r\n");
wifi_management_disconnect();
exit:
printf("The test has ended.\r\n");
sys_task_delete(NULL);
}
MQTT 客户端主函数
连接 MQTT 服务器及订阅主题
static void mqtt_client_demo(void)
{
if (client_connect() != 0) {
printf("MQTT connect server failed.\r\n");
goto exit;
}
if (client_subscribe() != 0) {
printf("MQTT subscribe failed.\r\n");
goto exit;
}
while(1) {
sys_msleep(1000);
}
exit:
printf("MQTT: close mqtt connection.\r\n");
mqtt_connect_free();
return;
}
创建 MQTT 连接
创建 MQTT 客户端实例
static int client_connect(void)
{
err_t ret = ERR_OK;
uint32_t connect_time = 0;
mqtt_client = mqtt_client_new();
printf("MQTT: start link server...\r\n");
mqtt_set_inpub_callback(mqtt_client, mqtt_receive_topic_print, mqtt_receive_msg_print, &client_info);
if (mqtt5_param_cfg(mqtt_client)) {
printf("MQTT: Configuration MQTT parameters failed, stop connection.\r\n");
return -2;
}
connect_time = sys_current_time_get();
ret = mqtt5_client_connect(mqtt_client, &sever_ip_addr, SERVER_PORT, mqtt_connect_callback, NULL, &client_info,
&(mqtt_client->mqtt5_config->connect_property_info),
&(mqtt_client->mqtt5_config->will_property_info));
if (ret != ERR_OK) {
printf("MQTT mqtt_client: connect to server failed.\r\n");
return ret;
}
// 等待连接成功
while (mqtt_client_is_connected(mqtt_client) == false) {
// 超时
if ((sys_current_time_get() - connect_time) > 5000) {
printf("MQTT mqtt_client: connect to server timeout.\r\n");
return -3;
}
// 异常
if (connect_fail_reason == MQTT_CONNECTION_REFUSE_PROTOCOL) {
mqtt5_disconnect(mqtt_client);
mqtt5_param_delete(mqtt_client);
printf("MQTT: The server does not support version 5.0, now switch to version 3.1.1.\r\n");
connect_fail_reason = -1;
break;
} else if (connect_fail_reason > 0) {
mqtt5_fail_reason_display((mqtt5_connect_return_res_t)connect_fail_reason);
return connect_fail_reason;
}
sys_yield();
}
printf("MQTT: Successfully connected to server.\r\n");
return 0;
}
连接状态回调
处理 MQTT 连接状态的变化
void mqtt_connect_callback(mqtt_client_t *client, void *arg, mqtt_connection_status_t status)
{
char *prefix = NULL;
char *reason = NULL;
if ((status == MQTT_CONNECT_ACCEPTED) ||
(status == MQTT_CONNECT_REFUSED_PROTOCOL_VERSION)) {
return;
}
prefix = "MQTT: client will be closed, reason is ";
switch (status) {
case MQTT_CONNECT_DISCONNECTED:
reason = "remote has closed connection";
break;
case MQTT_CONNECT_TIMEOUT:
reason = "connect attempt to server timed out";
break;
default:
reason = "others";
break;
}
printf("%s%s, id is %d.\r\n", prefix, reason, status);
}
错误码解析
void mqtt_fail_reason_display(mqtt_connect_return_res_t fail_reason)
{
char *prefix = "MQTT mqtt_client: connection refused reason is ";
char *reason = NULL;
switch(fail_reason) {
case MQTT_CONNECTION_REFUSE_PROTOCOL:
reason = "Bad protocol";
break;
case MQTT_CONNECTION_REFUSE_ID_REJECTED:
reason = "ID rejected";
break;
case MQTT_CONNECTION_REFUSE_SERVER_UNAVAILABLE:
reason = "Server unavailable";
break;
case MQTT_CONNECTION_REFUSE_BAD_USERNAME:
reason = "Bad username or password";
break;
case MQTT_CONNECTION_REFUSE_NOT_AUTHORIZED:
reason = "Not authorized";
break;
default:
reason = "Unknown reason";
break;
}
printf("%s%s, id is %d.\r\n", prefix, reason, fail_reason);
}
释放 MQTT 连接
断开并释放 MQTT 客户端资源
void mqtt_connect_free(void)
{
connect_fail_reason = -1;
if (mqtt_client == NULL)
return;
mqtt5_disconnect(mqtt_client);
mqtt5_param_delete(mqtt_client);
mqtt_client_free(mqtt_client);
mqtt_client = NULL;
}
订阅函数
配置订阅主题和 QoS,调用 mqtt5_msg_subscribe
发送订阅请求
static int client_subscribe(void)
{
err_t ret = ERR_OK;
mqtt5_topic_t topic_info;
topic_info.filter = topic_sub;
topic_info.qos = 0;
ret = mqtt5_msg_subscribe(mqtt_client, mqtt_sub_cb, &client_info, &topic_info,
1, mqtt_client->mqtt5_config->subscribe_property_info);
return ret;
}
订阅回调函数
用于处理订阅结果
void mqtt_sub_cb(void *arg, err_t status)
{
if (status == ERR_OK) {
printf("topic subscribe success.\r\n");
printf("Waiting for the message of topic \"%s\" ...\r\n", topic_sub);
} else if (status == ERR_TIMEOUT) {
printf("topic subscribe time out.\r\n");
}
}
消息负载回调
用于处理接收到的 MQTT 消息负载,如果消息 JSON 中 "cmd":1
,则设置 GPIOA 的第 5 引脚为高电平;否则,将其设置为低电平
void mqtt_receive_msg_print(void *inpub_arg, const uint8_t *data, uint16_t payload_length, uint8_t flags, uint8_t retain)
{
if (retain > 0 ) {
printf("retain: ");
}
printf("payload: ");
for (uint16_t idx = 0; idx < payload_length; idx++) {
printf("%c", *(data + idx));
}
printf("\r\n");
// 将 data 转换为字符串
char json[payload_length + 1]; // +1 用于存储字符串结束符 '\0'
memcpy(json, data, payload_length);
json[payload_length] = '\0'; // 添加字符串结束符
if(get_json_num(json, "cmd") == 1) {
gpio_bit_set(GPIOA, GPIO_PIN_5);
} else {
gpio_bit_reset(GPIOA, GPIO_PIN_5);
}
}
消息主题回调
将打印收到的 MQTT 消息主题
void mqtt_receive_topic_print(void *inpub_arg, const char *data, uint16_t payload_length)
{
printf("received topic: ");
for (uint16_t idx = 0; idx < payload_length; idx++) {
printf("%c", *(data + idx));
}
printf("\r\n");
}
解析 JSON 数据
int get_json_num(char *json, char *key) {
// 构造查找的键字符串,例如 "cmd":"
char search_key[256];
snprintf(search_key, sizeof(search_key), "\"%s\":", key);
// 在 JSON 字符串中查找键
char *key_start = strstr(json, search_key);
if (key_start == NULL) {
printf("JSON data does not contain '%s' field.\n", key);
return -1; // 返回错误码
}
// 跳过键名和冒号,例如跳过 "cmd":"
key_start += strlen(search_key);
// 跳过可能存在的空格
while (*key_start == ' ') {
key_start++;
}
// 找到键值的结束位置(假设键值是数字,且后面紧跟逗号或右大括号)
char *key_end = strchr(key_start, ',');
if (key_end == NULL) {
key_end = strchr(key_start, '}');
if (key_end == NULL) {
printf("Invalid JSON format.\n");
return -1; // 返回错误码
}
}
// 提取键值
int value = 0;
if (sscanf(key_start, "%d", &value) != 1) {
printf("Failed to parse '%s' value.\n", key);
return -1; // 返回错误码
}
return value; // 返回解析的整数值
}
构建项目
右键点击项目,选择“Build Project”即可开始构建,首次构建耗时较久,显示 Build Finished. 0 errors, 0 warnings.
即完成
使用 GD32 All In One Programmer 烧录
在烧录前,需要进入 BOOT0,开发板并未焊接 R4,所以调整跳线帽后需要手动短接 R4(或焊接电阻)的同时按下复位按钮
按照接线图连接电路(此处使用的是 CH341A 下载器,其他同理),连接完成后进入 BOOT0,使用 GD32 All In One Programmer 软件烧录
测试
烧录完成后点击 Disconnect 断开连接,打开串口助手(可以使用在线串口)连接设备
如果不先断开连接,串口助手将无法连接设备
再次按下复位按钮退出 BOOT0,若无误,串口助手将输出下面的内容
...... 省略 Wi-Fi 配网日志......
« Start MQTT client.
MQTT: start link server...
« MQTT: Successfully connected to server.
« topic subscribe success.
Waiting for the message of topic "test/topic" ...
这时,使用 MQTTBox 发送消息 {"cmd": 1}
可以看到串口助手输出如下的同时小灯点亮
« received topic: test/topic
payload: {"cmd": 1}
发送消息 {"cmd": 0}
可以看到串口助手输出如下的同时小灯熄灭
« received topic: test/topic
payload: {"cmd": 0}