基于STM32F103C8T6与ESP8266的WiFi智能插座开发实战
简介一套基于STM32F103C8T6的WiFi智能插座完整设计方案包含原理图、PCB、MCU固件源码与Android APP源码适合物联网嵌入式开发者及电子设计爱好者参考学习。方案实现远程/本地APP遥控开关、定时循环开关、预约倒计时、万能学习遥控、插座温度检测过载断电、防盗报警联动等实用功能覆盖智能家居常用场景。资源包共925个文件压缩后47.06MB其中png为原理图与界面预览pcbdoc/schdoc为PCB与原理图工程c/h为STM32底层驱动及应用逻辑java/xml为Android客户端源码hex为可直接烧录的固件。已有1543人学习下载资料结构清晰从硬件设计到软件实现均有呈现适合需要完整项目参考或进行二次开发的学习者。1. 基于 STM32F103C8T6 的 WiFi 智能插座项目到底在做什么画过 STM32F103C8T6 最小系统板的人很多看得见的是 LED 在闪看不见的是这块板子能不能扛住 WiFi 模块启动时几百毫安级电流的冲击、继电器在 220V 侧断开时会不会拉弧、手机在客厅通过 APP 操作卧室插座时消息链路是否真的通。这个项目标题把四件事压在一起原理图决定电气逻辑PCB 决定长期通电的可靠性MCU 源码决定控制与协议APP 源码决定用户侧体验。本文就从这四条线展开从原理图一路讲到 APP 联调适合能调通串口、画过两层板、但第一次把联网和设备控制串起来的工程师。2. 原理图与 PCB 设计把 STM32F103C8T6 最小系统做成可以长期通电的插座板为什么用 STM32 做主控而不是让 ESP8266 裸跑这个选择背后是可靠性。ESP8266 的优势是自带 WiFi 协议栈劣势是 GPIO 驱动能力和代码可维护性普通。智能插座控制继电器、保存状态、处理本地逻辑这些活更适合一个外设丰富、芯片资料密集的 MCU。STM32F103C8T6 作为 48 引脚的中等容量芯片价格透明国产替代型号也多后期换芯片不用改板子。WiFi 部分用 ESP-01S 这类模组做通信协处理器MCU 只通过串口操作它协议栈跑在模组里两边职责清晰调试时也容易定位问题。2.1 最小系统与引脚功能分配从最小系统板到自主布线很多人的第一块板就是 STM32F103C8T6 最小系统板但它把晶振、供电、调试口都提前画好了。自己设计原理图时要补齐这几样8MHz 外部晶振加两颗 20pF 负载电容NRST 引脚上拉 10kΩ 电阻BOOT0 下拉到地VDDA 与 VSSA 之间加 10uF 和 100nF 去耦SWD 调试口引出 PA13、PA14。如果不需要外部电池备份VBAT 直接接 3.3V 即可这是最省事也最不容易出错的接法。引脚分配要遵循一个原则先看 IO 上电默认电平再决定哪个引脚控制继电器而不是把需求反过来强塞给 GPIO。表 1 是这套设计里常用的一组分配。引脚复用功能方向说明PA1通用输出OUT继电器驱动复位期间默认浮空外部加下拉PA2USART2_TXOUT连接 ESP-01S 的 RXDPA3USART2_RXIN连接 ESP-01S 的 TXD3.3V 电平直连PA9USART1_TXOUT调试日志输出接 USB 转 TTLPA10USART1_RXIN调试命令输入PB12通用输出OUT状态 LEDPA0 不建议给继电器它是 WKUP 引脚复位期间如果电路噪声耦合到驱动管容易引起上电瞬间误动作。如果必须用就要在驱动管基极加一个 10kΩ 下拉电阻分压。继电器驱动 IO 的初始化代码建议放在所有外设初始化之前先写低电平再开外设时钟。// 主流程中最早执行的 GPIO 初始化 __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gi {0}; gi.Pin GPIO_PIN_1; gi.Mode GPIO_MODE_OUTPUT_PP; gi.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, gi); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);代码说明先把 PA1 配置成推挽输出并输出低电平之后才允许其他外设初始化继续执行。GPIO_SPEED_FREQ_LOW不是让继电器变慢而是降低上升沿的 dV/dt减少对旁边天线和晶振的干扰继电器的开关速度由三极管和继电器机械结构决定MCU 侧不需要高速翻转。GPIO_PIN_RESET就是低电平这是保证上电不误吸合的关键。2.2 继电器驱动与电源设计为什么你的插座板容易复位继电器驱动电路用经典的低边 NPN 方案。STM32F103C8T6 的 GPIO 输出高电平约 3.3VSRD-05VDC-SL-C 这种 5V 继电器线圈不能用 IO 直接推要经过一颗 1kΩ 基极电阻接到 S8050 的基极。S8050 集电极接继电器线圈负极线圈正极接 5V发射极接地。线圈两端并联一颗 1N4148 二极管阴极接 5V反向电动势靠这个二极管泄放。这三行逻辑看着简单却是整个原理图里最容易被抄错的点二极管方向反了三极管关断瞬间直接击穿。电源侧要分两条路走。220V 交流经过 AC-DC 模块变成 5V5V 直接给继电器线圈供电3.3V 由 AMS1117 从 5V 线性稳压出来。这里有一个常见坑ESP-01S 在启动瞬间会抽取几百毫安级电流AMS1117 在这种瞬态下压降会接近极限3.3V 掉到 2.6V 左右模块表现为持续复位或者 TCP 反复断开。我一般会在 3.3V 主干上加 470uF 电解电容同时让 ESP8266 模组供电走独立的 RC 滤波。提示ESP-01S 与 STM32 都是 3.3V 电平可以直接相连。如果用的是 5V 供电的 ESP8266 开发板TX/RX 就必须加电阻分压或电平转换否则长期使用可能损坏 MCU 的 IO。更彻底一些的做法是用一颗 PMOS 或负载开关芯片做 ESP8266 电源开关让 ESP 在 MCU 自检完成后再上电。下面的代码片段表达的是这个时序。// 上电时序继电器先闭锁再给 ESP8266 供电 HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(ESP_PWR_GPIO_Port, ESP_PWR_Pin, GPIO_PIN_SET); HAL_Delay(500); // 等待 ESP8266 AT 固件启动完成 // 500ms 后串口应能正常回 AT esp_send_at_wait(AT\r\n, 1000);逻辑说明ESP_PWR_GPIO_Pin控制负载开关高电平导通。500ms 是经验值AT 固件冷启动后需要一段时间准备隔 100ms 就发 AT 大概率收到空响应。这里把继电器先置低即使用户按物理按键关机再重启板子也不会在复位瞬间把继电器误吸合对 220V 负载来说这是底线要求。2.3 PCB 布线规则与 DRC 检查AD20 下单前必过的细节两层板足够应付这个设计。布局顺序按电流路径走AC 输入端子放在板边AC-DC 模块靠近端子继电器次之MCU 和 ESP8266 模组放在远离强电的一侧强弱电之间画一条清晰的分界线。这条线上开 1mm 宽阻焊桥或物理开槽继电器引脚之间也开槽目的是拉长爬电路径避免 220V 在潮湿环境下沿板面跳火。开槽在大多数 PCB 软件里用线切割层画板厂能识别。走线宽度方面IPC-2221 的经验值可以参考1oz 铜厚、10℃ 温升情况下0.3mm 外层走线能承受的电流大约在 0.5A 到 0.8A 之间。控制信号线用 0.3mm 完全够5V 电源主干建议 0.5mm 以上220V 侧走线至少 1.5mm两线间距保持 2mm 以上。ESP8266 天线区域正下方不铺铜天线向板边伸出且周围净空 5mm 以上。晶振靠近 STM32F103C8T6 的 OSC 引脚放置走线不要与继电器控制线平行长距离并排。AD20 里 DRC 不是默认就好用下单前建议过一遍这五项线宽规则、间距规则、电源网络铺铜方式、过孔外径与孔径、是否有未连接引脚。将 Board Rules 里的最小间距设为 0.2mm控制区最小线宽 8mil电源与强电网络单独加宽敷铜方式选 Solid网络连接到地。导出文件时不要发 AD 工程给板厂而是用 File - Fabrication Outputs - Gerber Files 导出 Gerber避免版本兼容性导致钻孔或焊盘错位。下表是打样前评审单上逐项打勾的内容。检查项本设计设置常见失败现象最小线宽与间距控制区 8mil电源 0.5mm板厂报断裂DRC 报间距错误强弱电开槽分界线及继电器引脚间 1mm高温高湿下打火、阻焊烧焦天线净空净空区无铜且边缘伸出WiFi 信号差连接速率不稳定晶振走线靠近 MCU包地启动失败时钟偏差导致串口乱码敷铜网络地网络实心敷铜浮铜充电导致 ESD 损伤3. MCU 固件STM32F103C8T6 在 Keil 下跑通 ESP8266 AT 指令状态机原理图解决的是“能不能通电”固件解决的是“能不能听话”。开发环境用 Keil MDK也可以用 IARCubeMX 生成的工程两种工具链都能导出。可能是受 STM32 生态影响“stm32f103c8t6 use_hal_driver”已经是很多人的搜索方式下面的代码统一基于 HAL 库不用老的标准外设库。3.1 用 CubeMX 生成的最小固件框架CubeMX 配置里RCC 选择 Crystal/Ceramic ResonatorSYS 选择 Serial Wire 保留 SWD 调试时钟树将 8MHz 外部晶振倍频到 72MHz。串口开两个USART1 用于调试日志115200-8-N-1USART2 接 ESP8266波特率同样用 115200。GPIO 页把 PA1 设成输出初始电平设为 Low。生成代码时 Toolchain 选 Keil MDK-ARM V5如果电脑装的是 AC6 编译器也可以注意确认优化等级不会把延时函数优化掉。中断回调里不要直接做字符串解析。AT 指令的返回是异步的发一条ATCWMODE可能几毫秒就回 OKATMQTTCONN则可能等几秒才回 CONNACK中间还会混入路由器掉线事件。所以串口这一层只负责收字节主循环负责解析。工程目录建议拆成三个文件esp_at.c放 AT 命令的拼接与发送app_protocol.c放 JSON 解析与设备状态机flash_store.c放状态保存。主循环是一个 while(1) 轮询事件标志优先级是接收事件、超时事件、心跳事件。这里没有上 FreeRTOS是因为这套逻辑的状态很少。如果要在 STM32F103C8T6 上继续加定时任务和 OTA再移植 FreeRTOS 也不晚但裸机状态机的调试信息更直观适合先把链路跑通。3.2 串口异步接收与 AT 应答解析ESP8266 的 AT 固件返回值有两种格式单行的 OK/ERROR 和多行的 事件。最常见可靠的接收方式是 DMA 加空闲中断一帧数据到达后自动进回调不用每收一个字节进一次中断。#define ESP_RX_MAX 512 static uint8_t esp_rx[ESP_RX_MAX]; static volatile uint16_t esp_len; void esp_uart_start(void) { // 打开空闲中断接收数据装入 esp_rx HAL_UARTEx_ReceiveToIdle_DMA(huart2, esp_rx, ESP_RX_MAX); } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART2) { esp_len Size; // 记录本次一帧的长度 // 立即重新挂载接收缓冲避免漏掉下一帧 HAL_UARTEx_ReceiveToIdle_DMA(huart2, esp_rx, ESP_RX_MAX); } }逻辑说明HAL_UARTEx_ReceiveToIdle_DMA在收到空闲条件时把已接收长度通过RxEventCallback抛出来这里只做长度记录和重新挂载。512 字节对单条 MQTT 报文足够实际 AT 响应通常不超过 200 字节。如果缓冲区被完全填满则没有空闲中断此时回调也会被触发处理逻辑一样不会丢帧。主循环里按关键词分流解析前先把缓冲区末尾补上字符串结束符。void esp_poll(void) { if (!esp_len) return; if (esp_len ESP_RX_MAX) esp_len ESP_RX_MAX - 1; esp_rx[esp_len] \0; // 保证 strstr 不会越界 if (strstr((char *)esp_rx, OK)) { esp_wait_ok 0; } else if (strstr((char *)esp_rx, ERROR)) { esp_wait_ok 1; } else if (strstr((char *)esp_rx, WIFI DISCONNECT)) { app_on_wifi_lost(); } else if (strstr((char *)esp_rx, MQTTDISCONN)) { app_on_mqtt_lost(); } esp_len 0; }代码说明strstr匹配的是裸关键字注意 OK 与 ERROR 要分开判断因为 AT 固件在收到错误指令时可能只回 ERROR。MQTTDISCONN是较新 AT 固件里的事件旧固件没有需要先确认自己刷的固件支持哪些事件。发送指令的封装要带超时和重试。这里给出一个常用的“发指令等结果”函数uint8_t esp_send_at_wait(const char *cmd, uint32_t timeout_ms) { esp_wait_ok 0xFF; // 0xFF 表示还没有收到任何结果 HAL_UART_Transmit(huart2, (uint8_t *)cmd, strlen(cmd), 100); uint32_t t0 HAL_GetTick(); while ((HAL_GetTick() - t0) timeout_ms) { esp_poll(); if (esp_wait_ok 0) return 1; // 收到 OK if (esp_wait_ok 1) return 0; // 收到 ERROR } return 0; // 超时未应答 }参数说明cmd 必须带\r\nAT 固件不识别裸字符串。timeout_ms 要按指令类型区分查询类给 1000 到 2000ms连接类给 5000ms因为路由器弱网下ATCWJAP可能在 3 到 4 秒才返回。把esp_poll()放进等待循环里是为了让 DMA 中断只标记长度、解析和状态更新都在这里完成这本身就是一个小型状态机。3.3 继电器控制、Flash 掉电保存与上电安全时序继电器控制本身很简单麻烦的是状态怎么保存、上电怎么恢复。void relay_set(uint8_t on) { HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, on ? GPIO_PIN_SET : GPIO_PIN_RESET); }STM32F103C8T6 没有 EEPROM要用内部 Flash 模拟。Flash 一页 1KB如果每次状态变化都擦一页寿命很快耗尽。实际做法是把状态按 4 字节对齐写进页内的顺序位置先写满一页再擦除。简化版实现如下。#define ADDR_FLASH_PAGE_63 0x0800FC00 // 最后一页的起始地址 void state_save(uint32_t marker) { HAL_FLASH_Unlock(); FLASH_ErasePage(ADDR_FLASH_PAGE_63); // 整页擦除 HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, ADDR_FLASH_PAGE_63, marker); HAL_FLASH_Lock(); }逻辑说明Flash 最小擦除单位是页FLASH_ErasePage会先把整页清成 0xFF。HAL_FLASH_Program以字为单位写入页地址必须 4 字节对齐。写前调用HAL_FLASH_Unlock写完必须HAL_FLASH_Lock这是 HAL 库的固定流程。产品级做法会把这个操作封装成带磨损均衡的模块但项目调试期先跑通数据回读即可。上电安全时序是这套代码的收尾重点。复位后先写死继电器断开等待 500ms 让 DC-DC 输出稳定再读 Flash。如果状态位有效再按原状态恢复。relay_set(0); HAL_Delay(500); uint32_t saved state_read(); if (saved STATE_ON_MARKER) { relay_set(1); }这个设计符合“断电记忆”的用户预期拔电时继电器是开的重新上电后自动恢复为开。如果产品要求“上电必须关断”这里就不要恢复改成等 APP 下发指令后再动作。两种策略由产品定义代码结构是同一套只差一行判断。4. APP 控制链路MQTT 远程与局域网 TCP 双通道APP 与插座之间“说话”的协议我一般直接选 MQTT。为什么不是 HTTPHTTP 是请求-响应服务器不知道设备在线状态设备要上报状态必须靠轮询轮询太短费流量费电太长又不实时。MQTT 是发布订阅模型设备端保持长连接APP 发一条命令服务端立刻推给设备设备的回执也能推回来智能插座选它很自然。4.1 通信协议先行用 JSON 设计一版可扩展的消息格式写代码前先定义消息格式。下面是一套很简洁的 JSON 协议。{type:set,on:true,seq:12} {type:state,on:true,seq:12} {type:ack,seq:12,on:true,err:0}type 字段区分消息方向set 是 APP 下发的控制指令state 是设备主动上报的状态ack 是设备对 set 的应答。on 字段控制继电器开合。seq 是自增序号用于去重因为 MQTT QoS1 会重复投递消息设备侧记录最近收到的一个 seq相同 seq 直接丢弃。消息类型方向用途关键字段setAPP 到设备下发控制onstate设备到 APP状态上报on, errack设备到 APP命令应答seq, err这个协议可以继续加字段而不破坏兼容性比如 power、temp 这些遥测字段旧版 APP 忽略未知字段即可。消息主题建议把命令和状态分开home/{device_id}/cmd用于控制home/{device_id}/state用于状态通知。如果不分开设备发一条 state 会被自己订阅到形成消息回环。4.2 Android 端 MQTT 客户端最小实现APP 源码部分Android 端用 Eclipse Paho Android Client 库就够不需要引入额外框架。初始化连接的核心代码如下。String broker tcp://192.168.1.100:1883; MqttClient client new MqttClient(broker, MqttClient.generateClientId(), new MemoryPersistence()); MqttConnectOptions options new MqttConnectOptions(); options.setCleanSession(true); options.setConnectionTimeout(10); // 连接超时 10 秒 options.setKeepAliveInterval(30); // 心跳间隔 30 秒 options.setAutomaticReconnect(true); client.setCallback(new MqttCallback() { Override public void connectionLost(Throwable cause) { } Override public void messageArrived(String topic, MqttMessage message) throws Exception { // 在 UI 线程解析 message 并更新开关状态 } Override public void deliveryComplete(IMqttDeliveryToken token) { } }); client.connect(options); client.subscribe(home/switch/1/state, 1);参数说明broker 地址在局域网调试时填开发板的 IP远程控制时填云主机或公网 broker 的 IP 或域名不能写 localhost手机端连不上。clientId 必须全局唯一Paho 自带的generateClientId()测试够用但两台手机如果用同一个 clientId后连接的会把前者踢下线。keepAliveInterval 设 30 秒太短会产生大量心跳空包太长 NAT 超时后服务端可能收不到心跳导致消息堆积。下发命令的代码也很直接。JSONObject obj new JSONObject(); obj.put(type, set); obj.put(on, isChecked); obj.put(seq, seqCounter); MqttMessage msg new MqttMessage( obj.toString().getBytes(StandardCharsets.UTF_8)); msg.setQos(1); client.publish(home/switch/1/cmd, msg);代码说明QoS 1 保证消息至少到达一次配合协议里的 seq 去重不会因为重复投递导致继电器反复动作。消息体按 UTF-8 编码避免特殊字符在传输途中出现乱码。MqttClient的构造和connect都会抛MqttException完整工程里需要 try-catch 或统一抛出这里不展开。4.3 配网与断线重连智能插座能不能在日常环境里用起来这一层最容易忽略。家用插座往往放在不方便插 USB 转 TTL 的位置配 WiFi 凭据不能靠每次烧固件。常见做法是 SoftAP 配网设备上电后进入 AP 模式手机先连上这个暂时没有外网的 WiFi然后用 APP 访问 192.168.4.1提交路由器名和密码。手机状态栏显示“无 internet 连接”属于正常现象因为 AP 本身只用来配网不是用来上外网的配网完成后设备切回 STA 模式。AT 指令下这一整套同样由串口指令完成。断线重连的策略要定义在 MCU 侧不能全指望 AT 固件。ESP8266 掉线后可能长时间保持在未连接状态我一般在固件里监听WIFI DISCONNECTED事件如果 30 秒内没有收到任何推送消息主动执行ATRST。重连不能疯狂发ATCWJAP要加指数退避比如第一次 1 秒后重试第二次 3 秒第三次 10 秒避免路由器被多个设备同时扫描打满。事件处理策略参数建议WIFI DISCONNECTED启动重连计数器指数退避 1s / 3s / 10sMQTT DISCONNECTED等待重连超过 3 次复位模块最多重试 3 次后 ATRST收到陌生 topic 消息直接丢弃按 topic 前缀过滤seq 重复直接丢弃记录最近 50 条 seq这套策略看着规则多代码里就是一个事件计数器加一个定时器。APP 端也要处理“设备离线”的状态UI 上显示为灰色而不是停留在上一次开关状态避免用户以为已经发出去了其实设备根本没收到。5. 联调三板斧串口日志、adb 日志和继电器声PCB、固件、APP 都齐了之后联调阶段拼的不是原理而是定位手段。下面三个方法能解决大部分“为什么控制不了”的问题。5.1 两路串口并联观察一路发 AT一路看日志调试串口接 PA9/PA10波特率 115200。MCU 固件里把 USART2 收到的所有原始 AT 响应转发到 USART1。void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART2) { HAL_UART_Transmit(huart1, esp_rx, Size, 100); esp_len Size; HAL_UARTEx_ReceiveToIdle_DMA(huart2, esp_rx, ESP_RX_MAX); } }这条代码的价值在于调试串口不但能看到 MCU 自己打印的日志还能看到 ESP8266 的原始回显。用一个串口助手就能判断是“MCU 没发出发 AT”还是“固件不回 OK”问题定位速度快很多。我在第一次联调时遇到的现象是 APP 显示已连接但继电器不动串口里看到MQTTDISCONN一瞬间就锁定是 MQTT 断线重连逻辑没写好而不是继电器驱动坏了。5.2 adb 过滤日志代替抓包工具APP 联调时不必一开始就上抓包工具。自研 APP 完全可以直接用 adb 看 Logcat很多“app 抓包失败”的情况是因为混淆包和 TLS 加密而不是网络问题。先确认手机与开发板在同一个网段再执行。adb logcat -v time | grep -iE mqtt|socket|switch参数说明-v time在每行前加时间戳grep 同时过滤 mqtt、socket、switch 三个关键词。Paho 库会把 connect、subscribe、publish 的关键节点打到 Log 级别能看到 “Connecting to...” 却看不到 “Connected”说明 TCP 握手失败问题在网络和端口而不是协议本身。这一步配合 5.1 的串口日志基本能把“APP 没发出”和“设备没收到”区分开。5.3 一个本地测试模式不看 APP 也能验证继电器最后分享一个非常实用的调试手法在固件里加一个本地测试模式上电时长按 PA0 按键 3 秒跳过 WiFi 连接直接读取 Flash 状态并翻转继电器。这个模式适合生产测试或返修场景不需要 WiFi 环境就能验证继电器驱动、Flash 读写和电源时序是否正常。平时测试听到继电器“咔嗒”一声基本就可以排除硬件侧问题。如果继电器声音正常而 APP 没有状态变化优先检查 MQTT 主题是否匹配不要先去怀疑继电器驱动电路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →