欢迎光临
我们一直在努力

【STM32+ESP01S+微信小程序】零基础实现远程点灯:从硬件到云端全流程教学

一、项目简介与系统架构

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

工作流程:

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

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

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

  • 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 下载与配置

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

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

  • 打开安装目录下的 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.c、esp01s.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 地址


    七、调试与运行流程

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

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

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

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

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

  • 常见问题排查

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

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

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

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

    赞(0)
    未经允许不得转载:171主机测评 » 【STM32+ESP01S+微信小程序】零基础实现远程点灯:从硬件到云端全流程教学
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址