一、项目简介与系统架构

本项目使用 STM32F407VET6 作为主控,通过 ESP01S 连接 WiFi,用 MQTT 协议 与本地 Mosquitto 服务器通信,最终用微信小程序远程控制一盏 LED 的亮灭。
看似简单,却完整覆盖了嵌入式开发、网络通信、消息协议、服务器搭建和移动端开发,适合物联网初学者打通全栈链路(本次的项目我想的是先把简单的项目跑通,后期自己添加自己想要的模块)。

工作流程:

  1. STM32 通过串口(USART2)发送 AT 指令给 ESP01S(这个wifi模块需要烧录程序才能使用,烧录过程可以上哔哩哔哩搜一下{后期我也会出一个详细教程}),使其连接路由器并作为 TCP 客户端连接 Mosquitto(需要下载)。

  2. STM32 手动构建 MQTT 报文,通过 ESP01S 完成 Broker 的 CONNECT 与 SUBSCRIBE。

  3. 微信小程序同样连接 Mosquitto(WebSocket),向指定主题 stm32/led/control 发布 on 或 off

  4. STM32 接收到来自 Broker 的 PUBLISH 消息后,解析指令,控制 PA0 引脚电平,点亮或熄灭 LED。


二、硬件清单与接线

2.1 所需材料

物料 规格/说明
STM32F407VET6 开发板 核心主控
ESP01S 模块 WiFi模块,出厂 AT 固件(115200bps)
LED ×1 + 220Ω 电阻 ×1 指示灯
面包板、杜邦线 接插连接
USB 转 TTL 模块(可选) 用于串口调试 USART1
独立 3.3V 电源模块(这个不需要我就在主控板上供电的,但是有一点不稳定) ESP01S 对供电要求较高,建议使用 AMS1117 单独供电
ST-Link 下载器 烧录程序

2.2 接线图(文字描述)

STM32 ↔ ESP01S(USART2)
STM32 引脚 ESP01S 引脚 说明
3.3V VCC, EN(CH_PD) 必须共地,EN 直接拉高
GND GND
PA2 (TX) RX 交叉连接
PA3 (RX) TX 交叉连接
悬空 RST, GPIO0, GPIO2 AT 模式下可不接
LED 电路
  • LED 正极(长脚) → 220Ω 电阻 → STM32 PA0

  • LED 负极(短脚) → GND

调试串口 USART1(可选)【加上这个串口调试,如果项目不成功很容易知道是哪一个部分出现了问题】
  • PA9 (TX) → USB转TTL的 RXD

  • PA10 (RX) → USB转TTL的 TXD

  • GND → GND

  • 3V3 → 3V3
下载器

下载器这个就是st link的引脚对应开发板上的四个引脚(一一对应即可)

警告:ESP01S 对电源纹波敏感,务必保证 3.3V 输出电流 ≥ 500mA,且所有模块 GND 连通。


三、Mosquitto 服务器的安装与使用

3.1 为什么需要本地 MQTT Broker?

微信小程序只能通过 WebSocket(ws/wss)连接 MQTT 服务器,而 STM32 的 ESP01S 使用普通 TCP 连接。
Mosquitto 是一个轻量级 MQTT Broker,可同时监听 TCP 1883 和 WebSocket 8083 端口,完美打通二者通信。

3.2 下载与配置

  1. 官网下载 Windows 安装包:Download | Eclipse Mosquitto

  1. 选择 mosquitto-2.x.x-install-windows-x64.exe,安装至 F:\Mosquitto 服务器\Mosquitto

  2. 打开安装目录下的 mosquitto.conf(用记事本),在文件末尾添加(我的建议是把里面的全部删掉添加下面的内容):

text

listener 1883
protocol mqtt

listener 8083
protocol websockets

保存退出。

3.3 启动与停止

  • 启动:以管理员身份打开 cmd,进入安装目录,运行:

cmd

mosquitto -v -c mosquitto.conf

出现 mosquitto version 2.1.2 running 即为成功。

  • 停止:在当前窗口按 Ctrl + C,等待 terminating 输出,切勿直接点 ❌ 关闭。

  • 验证:另开 cmd 执行 netstat -ano | findstr "1883",应有 LISTENING

3.4 服务器工作原理

  • Broker 接收到来自客户端(小程序、STM32)的 PUBLISH 消息后,检查该消息的主题(Topic)。

  • 如果有其他客户端订阅了此主题,Broker 会将消息转发给他们。

  • 通过 Keep Alive 心跳机制维持长连接。


四、CubeMX 工程配置

4.1 新建工程

打开 STM32CubeIDE 或 CubeMX,芯片选择 STM32F407VETx

引脚的配置过程

4.2 时钟配置

  • RCC:HSE 选择 “Crystal/Ceramic Resonator”

  • Clock Configuration:HCLK 设为 168 MHz(PLLM=25, PLLN=336, PLLP=2)

4.3 引脚与串口设置

  • PA0:GPIO_Output(推挽,无上下拉,标签 LED

  • PA2:USART2_TX

  • PA3:USART2_RX

  • USART2:波特率 115200,8N1,无流控

  • USART1(可选调试):PA9(TX), PA10(RX),参数同上

  • SYS:Debug 选择 Serial Wire

4.4 生成代码

Project Manager 中填写工程名、路径,选择 MDK-ARM(Keil),生成代码。


五、Keil 工程代码详解

所有自定义代码必须放在 /* USER CODE BEGIN ... */ 之间,防止重新生成时被覆盖。

5.1 添加源文件

整体代码布局

在 Core/Src 和 Core/Inc 下新建 esp01s.cesp01s.h
右键工程组 Application/User → Add Existing Files → 添加 esp01s.c

5.2 esp01s.h (头文件声明)

c

#ifndef INC_ESP01S_H_
#define INC_ESP01S_H_

#include "stm32f4xx_hal.h"
#include <string.h>
#include <stdio.h>

void ESP_Init(UART_HandleTypeDef *huart);
void ESP_SendString(const char *str);
int  ESP_ReadResponse(char *buf, int maxlen, uint32_t timeout);
int  ESP_SendCmdWait(const char *cmd, const char *wait, uint32_t timeout);

void mqtt_connect(uint8_t *packet, int *packetLen);
void mqtt_subscribe(uint8_t *packet, int *packetLen, const char* topic);
void mqtt_send_packet(UART_HandleTypeDef *huart, uint8_t *packet, int len);

#endif

5.3 esp01s.c (ESP01S 驱动与 MQTT 报文构建)

c

#include "esp01s.h"

static UART_HandleTypeDef *esp_huart;

void ESP_Init(UART_HandleTypeDef *huart) {
    esp_huart = huart;
}

void ESP_SendString(const char *str) {
    HAL_UART_Transmit(esp_huart, (uint8_t*)str, strlen(str), 1000);
}

int ESP_ReadResponse(char *buf, int maxlen, uint32_t timeout) {
    int len = 0;
    uint32_t tickstart = HAL_GetTick();
    memset(buf, 0, maxlen);
    while (1) {
        if (HAL_UART_Receive(esp_huart, (uint8_t*)&buf[len], 1, 10) == HAL_OK) {
            len++;
            if (len >= maxlen - 1) break;
        }
        if ((HAL_GetTick() - tickstart) > timeout) break;
    }
    buf[len] = '\0';
    return len;
}

int ESP_SendCmdWait(const char* cmd, const char* wait, uint32_t timeout) {
    char buf[256] = {0};
    ESP_SendString(cmd);
    ESP_SendString("\r\n");
    ESP_ReadResponse(buf, sizeof(buf), timeout);
    return (strstr(buf, wait) != NULL);
}

/* MQTT CONNECT 报文 */
void mqtt_connect(uint8_t *packet, int *packetLen) {
    int pos = 0;
    packet[pos++] = 0x10;           // 固定头部
    int remLenPos = pos;
    pos++;

    const char PROTOCOL[] = "MQTT";
    packet[pos++] = 0x00;
    packet[pos++] = 0x04;
    memcpy(&packet[pos], PROTOCOL, 4);
    pos += 4;

    packet[pos++] = 0x04;   // 协议级别
    packet[pos++] = 0x02;   // 连接标志:清除会话
    packet[pos++] = 0x00;   // 保活时间高字节
    packet[pos++] = 0x3C;   // 60秒

    const char *clientID = "stm32f4_board";
    int idLen = strlen(clientID);
    packet[pos++] = (idLen >> 8) & 0xFF;
    packet[pos++] = idLen & 0xFF;
    memcpy(&packet[pos], clientID, idLen);
    pos += idLen;

    int remLen = pos - (remLenPos + 1);
    packet[remLenPos] = remLen;
    *packetLen = pos;
}

/* MQTT SUBSCRIBE 报文 */
void mqtt_subscribe(uint8_t *packet, int *packetLen, const char* topic) {
    int pos = 0;
    packet[pos++] = 0x82;
    int remLenPos = pos;
    pos++;

    packet[pos++] = 0x00;
    packet[pos++] = 0x01;   // 报文标识符

    int topicLen = strlen(topic);
    packet[pos++] = (topicLen >> 8) & 0xFF;
    packet[pos++] = topicLen & 0xFF;
    memcpy(&packet[pos], topic, topicLen);
    pos += topicLen;
    packet[pos++] = 0x00;   // QoS 0

    int remLen = pos - (remLenPos + 1);
    packet[remLenPos] = remLen;
    *packetLen = pos;
}

/* 通过 AT+CIPSEND 发送 MQTT 报文 */
void mqtt_send_packet(UART_HandleTypeDef *huart, uint8_t *packet, int len) {
    char cmd[32];
    sprintf(cmd, "AT+CIPSEND=%d", len);
    ESP_SendString(cmd);
    ESP_SendString("\r\n");
    char buf[8];
    ESP_ReadResponse(buf, sizeof(buf), 2000); // 等待 >
    HAL_UART_Transmit(huart, packet, len, 1000);
    HAL_Delay(100);
}

5.4 main.c (主程序,含调试打印)

c

/* 文件头部 include 略,注意添加 #include <stdio.h> */
/* 在 USER CODE BEGIN 4 中重定义 printf 到 USART1 */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}
/* 数据解析函数 */
void process_esp_data(void) {
    static char esp_buf[512];
    static int pos = 0;
    char ch;
    while (HAL_UART_Receive(&huart2, (uint8_t*)&ch, 1, 5) == HAL_OK) {
        esp_buf[pos++] = ch;
        if (ch == '\n' || pos >= 512-1) {
            esp_buf[pos] = '\0';
            printf("RAW:[%s]\r\n", esp_buf);  // 打印原始接收数据,方便调试
            char *ipd = strstr(esp_buf, "+IPD,");
            if (ipd) {
                printf("Got +IPD\r\n");
                // 暴力关键字搜索(绕过主题解析,快速验证)
                char *colon = strchr(ipd, ':');
                if (colon) {
                    uint8_t *data = (uint8_t*)(colon + 1);
                    if (strstr((char*)data, "on")) {
                        printf("LED ON\r\n");
                        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
                    } else if (strstr((char*)data, "off")) {
                        printf("LED OFF\r\n");
                        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
                    }
                }
            }
            pos = 0;
        }
    }
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_USART2_UART_Init();

  printf("STM32 Starting...\r\n");
  ESP_Init(&huart2);
  HAL_Delay(2000);

  printf("Sending AT...\r\n");
  ESP_SendCmdWait("AT", "OK", 2000);
  printf("AT OK\r\n");

  ESP_SendCmdWait("AT+CWMODE=1", "OK", 2000);
  printf("Setting CWMODE...\r\n");

  printf("Connecting WiFi...\r\n");
  ESP_SendCmdWait("AT+CWJAP=\"你的WiFi名\",\"你的密码\"", "OK", 10000);
  printf("WiFi Connected\r\n");

  printf("Connecting TCP...\r\n");
  ESP_SendCmdWait("AT+CIPSTART=\"TCP\",\"192.168.211.2\",1883", "CONNECT", 8000);
  printf("TCP Connected\r\n");

  uint8_t packet[128];
  int len;
  mqtt_connect(packet, &len);
  mqtt_send_packet(&huart2, packet, len);
  printf("MQTT connect sent\r\n");
  HAL_Delay(500);

  mqtt_subscribe(packet, &len, "stm32/led/control");
  mqtt_send_packet(&huart2, packet, len);
  printf("MQTT subscribed\r\n");

  uint32_t lastPing = HAL_GetTick();
  while (1) {
    process_esp_data();
    if (HAL_GetTick() - lastPing > 30000) {
        uint8_t ping[] = {0xC0, 0x00};
        mqtt_send_packet(&huart2, ping, 2);
        lastPing = HAL_GetTick();
    }
    HAL_Delay(10);
  }
}

5.5 其他外设配置文件

  • gpio.c:初始化 PA0 推挽输出,gpio.h 中宏定义 LED_Pin 为 GPIO_PIN_0,LED_GPIO_Port 为 GPIOA。

  • gpio.c的代码:

#include "gpio.h"

void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /*Configure GPIO pin : PA0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
  • gpio.h的代码:

#ifndef __GPIO_H__
#define __GPIO_H__

#include "main.h"

void MX_GPIO_Init(void);

#define LED_Pin GPIO_PIN_0
#define LED_GPIO_Port GPIOA

#endif
  • usart.c:包含 USART1 和 USART2 的初始化代码,由 CubeMX 自动生成,无需额外修改。


六、微信小程序开发

6.1 环境准备

  • 下载微信开发者工具,登录后新建小程序项目(可使用测试号)。

  • 在项目根目录创建 utils/mqtt.js(小程序专用 MQTT over WebSocket 库,使用 wx.connectSocket 实现)。

6.2 自定义 MQTT 库 utils/mqtt.js

javascript

class MQTTClient {
  constructor(url, options) {
    this.url = url;
    this.clientId = options.clientId || 'mqttjs_' + Math.random().toString(16).substr(2, 8);
    this.keepalive = options.keepalive || 60;
    this._connectHandlers = [];
    this._messageHandlers = [];
    this._connected = false;
  }

  connect() {
    let that = this;
    this._socketTask = wx.connectSocket({
      url: this.url,
      header: { 'content-type': 'application/json' }
    });
    this._socketTask.onOpen(() => {
      that._connected = true;
      that._sendConnect();
      that._connectHandlers.forEach(cb => cb());
    });
    this._socketTask.onMessage((res) => {
      let data = new Uint8Array(res.data);
      that._handleMessage(data);
    });
    this._socketTask.onError((err) => {
      console.error('WebSocket error', err);
    });
    this._pingInterval = setInterval(() => {
      if (that._connected) that._sendPing();
    }, this.keepalive * 500);
  }

  _sendConnect() {
    // 构建 CONNECT 报文(协议名 MQTT,级别4,清除会话,60秒心跳)
    let protocol = 'MQTT', clientId = this.clientId;
    let payload = new Uint8Array(2 + 4 + 1 + 1 + 2 + 2 + clientId.length);
    let pos = 0;
    payload[pos++] = 0; payload[pos++] = 4;
    for (let i = 0; i < 4; i++) payload[pos++] = protocol.charCodeAt(i);
    payload[pos++] = 4; payload[pos++] = 2; // 协议级别和连接标志
    payload[pos++] = 0; payload[pos++] = 60; // 保活
    payload[pos++] = (clientId.length >> 8) & 0xFF; payload[pos++] = clientId.length & 0xFF;
    for (let i = 0; i < clientId.length; i++) payload[pos++] = clientId.charCodeAt(i);
    let header = new Uint8Array([0x10, payload.length]);
    let packet = new Uint8Array(header.length + payload.length);
    packet.set(header); packet.set(payload, header.length);
    this._socketTask.send({ data: packet.buffer });
  }

  _sendPing() {
    this._socketTask.send({ data: new Uint8Array([0xC0, 0x00]).buffer });
  }

  publish(topic, message) {
    let payload = new Uint8Array(2 + topic.length + message.length);
    payload[0] = (topic.length >> 8) & 0xFF; payload[1] = topic.length & 0xFF;
    for (let i = 0; i < topic.length; i++) payload[2 + i] = topic.charCodeAt(i);
    for (let i = 0; i < message.length; i++) payload[2 + topic.length + i] = message.charCodeAt(i);
    let header = new Uint8Array([0x30, payload.length]);
    let packet = new Uint8Array(header.length + payload.length);
    packet.set(header); packet.set(payload, header.length);
    this._socketTask.send({ data: packet.buffer });
  }

  subscribe(topic) {
    let payload = new Uint8Array(2 + 2 + topic.length + 1);
    payload[0] = 0; payload[1] = 1; // packetId
    payload[2] = (topic.length >> 8) & 0xFF; payload[3] = topic.length & 0xFF;
    for (let i = 0; i < topic.length; i++) payload[4 + i] = topic.charCodeAt(i);
    payload[4 + topic.length] = 0; // qos
    let header = new Uint8Array([0x82, payload.length]);
    let packet = new Uint8Array(header.length + payload.length);
    packet.set(header); packet.set(payload, header.length);
    this._socketTask.send({ data: packet.buffer });
  }

  _handleMessage(data) {
    if (data[0] === 0x30) { // PUBLISH
      let topicLen = (data[2] << 8) | data[3];
      let topic = '';
      for (let i = 0; i < topicLen; i++) topic += String.fromCharCode(data[4 + i]);
      let msg = '';
      for (let i = 4 + topicLen; i < data.length; i++) msg += String.fromCharCode(data[i]);
      this._messageHandlers.forEach(h => h.callback(topic, msg));
    }
  }

  on(event, callback) {
    if (event === 'connect') {
      if (this._connected) callback();
      else this._connectHandlers.push(callback);
    } else if (event === 'message') {
      this._messageHandlers.push({ callback: (t, p) => callback(t, p) });
    } else if (event === 'error') {
      this.onerror = callback;
    }
  }
}

function connect(url, options) {
  let client = new MQTTClient(url, options);
  client.connect();
  return client;
}

module.exports = { connect: connect };

6.3 页面代码

pages/index/index.wxml

html

<view class="container">
  <text>LED 控制面板</text>
  <button bindtap="turnOn">开灯</button>
  <button bindtap="turnOff">关灯</button>
  <text>状态:{{status}}</text>
</view>
pages/index/index.js
javascript

const mqtt = require('../../utils/mqtt.js')

Page({
  data: {
    status: '未连接'
  },

  onLoad: function () {
    // 延迟连接确保页面生命周期就绪
    setTimeout(() => {
      this.connectMqtt()
    }, 200)
  },

  connectMqtt: function () {
    const client = mqtt.connect('ws://192.168.211.2:8083', {
      clientId: 'wx_mini_' + Math.random().toString(16).substr(2, 8),
      keepalive: 60,
      clean: true
    })
    this.client = client

    client.on('connect', () => {
      console.log('MQTT连接成功')
      this.setData({ status: '已连接' })
    })

    client.on('error', (err) => {
      console.error('连接错误', err)
      this.setData({ status: '连接失败' })
    })
  },

  turnOn: function () {
    if (this.client) {
      this.client.publish('stm32/led/control', 'on')
      wx.showToast({ title: '已发送开灯' })
    }
  },

  turnOff: function () {
    if (this.client) {
      this.client.publish('stm32/led/control', 'off')
      wx.showToast({ title: '已发送关灯' })
    }
  }
})

其他的代码你们自己补一下(这个小程序特别简单)

点灯的命令传输依赖 MQTT 协议(讲解mqtt的配置):

这个 192.168.211.2 是你电脑的局域网 IP 地址


七、调试与运行流程

  1. 启动 Mosquitto:确保服务器运行在 192.168.211.2:1883 (TCP) 和 8083 (WebSocket)。

  2. STM32 上电:烧录程序后,打开串口助手(115200)连接 USART1,观察打印。

  3. 打开微信开发者工具:编译小程序,检查是否出现 MQTT连接成功,状态变为“已连接”。

  4. 点击按钮:串口应显示 RAW:[+IPD,...] 和 LED ON/OFF,同时 LED 动作。

  5. 真机测试:点击 预览 生成二维码,手机微信扫描即可控制。

常见问题排查

  • 小程序连接失败:检查是否勾选“不校验合法域名”;Mosquitto 是否运行;手机与电脑同一局域网。

  • STM32 无打印:确认 USART1 接线正确,且代码中添加了 printf 重定向。

  • 收到 +IPD 但灯不亮:检查主题 stm32/led/control 是否一致;消息内容是否精确为 on/off(无空格)。

  • Mosquitto 启动报端口占用:任务管理器结束残留 mosquitto 进程,或执行 taskkill /IM mosquitto.exe /F

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐