【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)