ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于STM32与ESP8266的定时智能插座设计:本地与远程控制实现

基于STM32与ESP8266的定时智能插座设计:本地与远程控制实现

1. 项目缘起与整体设计思路

1.1 为什么选择STM32做智能插座

智能插座这个品类,市面上成品已经很多了,几十块钱就能买到带WiFi的。但如果你是想把它当作一个完整的嵌入式项目来做,或者作为毕业设计、课程设计,那自己从头搭一遍的价值就完全不一样了。我选STM32作为主控,核心原因有三个:第一,STM32的GPIO驱动能力、定时器资源、通信外设(USART、SPI、I2C)足够丰富,一颗芯片就能把继电器控制、按键输入、显示屏、无线通信模块全部带起来,不需要额外扩展;第二,ST官方提供的HAL库和CubeMX工具链成熟,配置外设的时候不用像51单片机那样一个寄存器一个寄存器地查手册,开发效率高很多;第三,STM32的社区资料量大,遇到问题搜索一下基本都能找到方向,这对个人开发者来说非常重要。

这个项目的核心目标很明确:做一个既能本地按键控制、又能通过无线模块远程控制的定时智能插座。所谓“定时”,是指用户可以设定具体的开关时间点或者倒计时,到时间自动执行;所谓“远程控制”,是指不依赖插座面板上的物理按键,通过手机或者上位机发送指令来控制通断。本地控制和远程控制这两条路径必须同时存在,而且互不冲突,这是整个系统设计的核心难点。

1.2 系统整体架构拆解

整个系统我把它分成四个层次来理解,这样在画原理图和写代码的时候思路会清晰很多。

第一层是电源层。智能插座本身要插在220V市电上,所以需要把220V交流转成5V直流,再降到3.3V给STM32和无线模块供电。这一层通常用成品的小型AC-DC降压模块来实现,比如HLK-PM01这类模块,输出5V,再经过AMS1117-3.3稳压给MCU。这里要特别注意强弱电隔离的问题,后面我会详细讲。

第二层是控制层。核心就是STM32最小系统板,包括晶振电路、复位电路、BOOT启动配置、SWD调试接口。我用的型号是STM32F103C8T6,48引脚封装,72MHz主频,64KB Flash,20KB SRAM,对于这个项目来说绰绰有余。如果你手头有STM32F401或者F411也可以,性能更强但价格略高,看预算决定。

第三层是执行与交互层。执行部分就是继电器模块,用来控制220V回路的通断。交互部分包括本地按键(用来手动开关和设置定时)、OLED或LCD显示屏(用来显示当前状态和时间)、以及LED指示灯(用来指示当前插座是开还是关)。

第四层是通信层。远程控制需要一个无线通信模块。常见的选择有ESP8266(WiFi)、HC-05(蓝牙)、或者NB-IoT模块。我选的是ESP8266,因为它可以通过AT指令和STM32的串口通信,成本低,而且家里有路由器就能用,不需要额外的网关设备。ESP8266负责连接网络,STM32通过串口发送AT指令让它把数据传到云端或者局域网内的服务器。

1.3 本地控制与远程控制的优先级设计

这是整个项目里最容易被忽略但最影响使用体验的部分。本地按键和远程指令可能同时到达,比如你正在按面板上的开关,手机上也发了一条开灯指令,这时候以谁为准?我的设计原则是:本地操作优先级最高,远程指令次之,定时任务最低。

为什么这么设计?因为本地按键是用户站在插座面前直接操作的,意图最明确,不应该被远程指令覆盖。定时任务是预设的自动化行为,如果用户手动干预了,那定时任务就应该让位。具体实现上,我在代码里用一个状态变量来记录当前的控制来源,每次执行动作前先判断优先级。

另外还有一个细节:本地按键操作后,状态变化需要同步到远程端,让手机App或者上位机知道当前插座的实际状态。这个同步机制如果不做,就会出现手机显示“开”但实际插座是“关”的情况,用户体验很差。

2. 硬件选型与核心电路细节

2.1 主控芯片与最小系统

STM32F103C8T6是这个项目里性价比最高的选择。72MHz的主频足够处理按键扫描、定时器中断、串口通信和显示刷新这些任务。64KB的Flash对于裸机程序来说完全够用,即使加上FreeRTOS也还有余量。20KB的SRAM在跑裸机的时候很充裕,但如果要跑RTOS并且开多个任务栈,就需要留意一下内存分配。

最小系统板上必须有的是:8MHz外部晶振(提供系统时钟源)、32.768kHz晶振(给RTC提供时钟,做定时功能必须要有)、复位按键、BOOT0和BOOT1跳线(用来选择启动模式)、SWD接口(用来下载和调试)。这些在市面上卖的BluePill开发板上都已经集成了,直接拿来用就行。

有一个坑我踩过:有些便宜的BluePill板子上的32.768kHz晶振负载电容不匹配,导致RTC走时不准,一天能差好几分钟。如果你要做精确的定时功能,建议单独买一个精度高一点的32.768kHz晶振换上,或者在代码里做软件校准。

2.2 继电器驱动电路

继电器是控制220V通断的关键元件。我选的是5V驱动的SRD-05VDC-SL-C,触点容量10A 250VAC,带一个普通的电饭煲或者台灯完全没问题。但STM32的GPIO输出电流最大只有20mA左右,驱动不了继电器线圈(线圈电阻约70欧姆,5V下需要70mA左右),所以必须加驱动电路。

最简单的方案是用一个NPN三极管(比如S8050)做开关,基极通过1k电阻接STM32的GPIO,集电极接继电器线圈的一端,线圈另一端接5V,发射极接地。继电器线圈两端要反向并联一个续流二极管(1N4148或1N4007),用来吸收继电器断电时产生的反向电动势,保护三极管不被击穿。

如果你想要更省事,可以直接买成品的光耦隔离继电器模块,几块钱一个,输入端直接接STM32的GPIO,输出端接220V负载。这种模块内部已经做好了光耦隔离和驱动电路,安全性和便利性都更好。但要注意,有些廉价模块的光耦驱动电流要求比较大,STM32的GPIO可能推不动,需要确认模块的输入电流要求。

注意:继电器模块的输入端和输出端必须保持足够的爬电距离,PCB布线时强弱电区域要分开,否则220V可能会击穿到低压侧,烧毁STM32甚至造成人身危险。

2.3 无线通信模块选型对比

远程控制的核心是无线通信模块。我对比了几种常见方案:

模块通信方式优点缺点适用场景
ESP8266WiFi成本低,家里有路由器就能用功耗较高,配网稍麻烦家庭固定安装
HC-05蓝牙配对简单,不需要网络距离短(10米左右),不能远程近距离控制
ESP32WiFi+蓝牙功能强,可独立运行价格略高,开发门槛稍高需要复杂功能
NB-IoT蜂窝网络覆盖广,不需要路由器需要SIM卡,有月租无WiFi环境

我最终选ESP8266-01S,因为它最便宜(十几块钱),而且AT指令集成熟,STM32通过串口发指令就能控制。ESP8266负责连接WiFi路由器,然后通过MQTT协议或者TCP协议和服务器通信。服务器可以是一台局域网内的树莓派,也可以是云端的MQTT Broker。

这里有一个关键点:ESP8266的默认固件是AT固件,波特率通常是115200。STM32的串口要配置成对应的波特率,并且要处理好AT指令的响应解析。AT指令的响应是不定长的,不能用固定的接收缓冲区大小来处理,建议用空闲中断加DMA的方式接收,或者用一个环形缓冲区来缓存数据。

2.4 电源电路与强弱电隔离

电源部分是这个项目里最需要谨慎对待的。220V交流输入经过保险丝(1A)后,进入AC-DC降压模块。我用的HLK-PM01输出5V/0.6A,功率3W,足够给STM32、ESP8266和继电器供电。5V再经过AMS1117-3.3降到3.3V给STM32和ESP8266。

强弱电隔离方面,如果用的是成品光耦继电器模块,隔离已经由模块内部的光耦完成了。但如果是自己搭继电器驱动电路,那STM32的地和220V的地是共地的,这就非常危险。正确的做法是用光耦(比如PC817)把STM32的GPIO和继电器驱动电路隔离开,两边不共地。光耦的输入端接STM32的GPIO和地,输出端接继电器驱动电路和5V地,这样220V侧的任何异常都不会影响到STM32。

提示:调试的时候千万不要把220V接到面包板上。面包板的爬电距离不够,220V很容易拉弧短路。强电部分必须焊接在洞洞板或者PCB上,并且做好绝缘处理。

3. 软件架构与核心功能实现

3.1 裸机还是RTOS

这个问题我纠结了很久。裸机方案用一个大循环加中断就能跑起来,代码简单,调试方便。但问题是,当你在处理显示屏刷新的时候,串口来了数据需要及时响应,按键也需要及时扫描,裸机方案下这些任务会互相阻塞,导致响应变慢。

最终我选了FreeRTOS。STM32F103C8T6的20KB SRAM跑FreeRTOS完全没问题,我创建了四个任务:按键扫描任务(优先级中)、串口通信任务(优先级高)、显示刷新任务(优先级低)、定时器管理任务(优先级中)。任务之间通过队列和信号量通信,互不阻塞。

如果你不想用RTOS,裸机方案也不是不行,但需要用状态机来组织代码。比如串口接收用空闲中断加DMA,按键用定时器中断扫描,显示刷新放在主循环里但每次只刷一部分。这样也能做到不阻塞,但代码结构会复杂一些。

3.2 定时功能的实现逻辑

定时功能分两种:一种是绝对时间定时,比如“每天18:00打开”;另一种是倒计时,比如“30分钟后关闭”。绝对时间定时依赖RTC,倒计时依赖系统滴答定时器。

RTC的配置需要注意:STM32F103的RTC是一个独立的BCD计数器,需要配置LSE(外部32.768kHz晶振)作为时钟源。配置步骤是:使能PWR和BKP时钟,使能LSE,等待LSE稳定,选择RTC时钟源为LSE,使能RTC时钟,然后设置时间和日期。这些步骤在HAL库里都有对应的函数,但顺序不能错,否则RTC不会工作。

倒计时的实现比较简单,用一个软件定时器或者直接在FreeRTOS里用vTaskDelay来实现。但要注意,vTaskDelay是阻塞式的,会挂起当前任务,所以倒计时管理最好单独放在一个任务里,不要和其他逻辑混在一起。

定时任务的存储我用的是STM32内部的Flash模拟EEPROM。STM32F103C8T6没有真正的EEPROM,但可以用Flash的最后几页来模拟。每次用户设置定时任务后,把任务数据写入Flash,断电后重新上电时从Flash读取。这里要注意Flash的擦写寿命是1万次左右,不要频繁写入,建议只在用户确认设置后才写一次。

3.3 远程通信协议设计

STM32和ESP8266之间的通信协议我设计得很简单:STM32发送AT指令给ESP8266,ESP8266返回响应。但ESP8266和服务器之间的通信协议需要自己定义。

我用的是MQTT协议。ESP8266烧录MQTT固件后,可以通过AT指令连接MQTT Broker,订阅和发布主题。STM32只需要通过串口发送对应的AT指令即可。比如:

# 连接WiFi AT+CWJAP="SSID","PASSWORD" # 连接MQTT Broker AT+MQTTCONN=0,"broker地址",1883,1 # 订阅主题 AT+MQTTSUB=0,"topic/sub",1 # 发布消息 AT+MQTTPUB=0,"topic/pub","{\"state\":\"on\"}",1,0

STM32端需要解析ESP8266返回的MQTT消息。当收到+MQTTSUBRECV开头的消息时,提取出主题和负载,然后根据负载内容执行对应的动作。

这里有一个坑:ESP8266的AT固件版本不同,AT指令的格式可能略有差异。建议先用USB转TTL模块把ESP8266接到电脑上,用串口助手手动发指令测试一遍,确认固件版本和指令格式后再写STM32的代码。

3.4 本地按键与状态机设计

本地按键我用了三个:一个用于手动开关,一个用于进入设置模式,一个用于在设置模式下调整数值。按键扫描用定时器中断,每10ms扫描一次,做20ms的软件消抖。

按键的处理逻辑我用了一个简单的状态机:

  • 空闲状态:等待按键按下
  • 消抖状态:检测到按键按下后,等待20ms再次确认
  • 短按处理:按键释放后,如果按下时间小于1秒,执行短按功能
  • 长按处理:如果按下时间超过1秒,执行长按功能(比如进入设置模式)

状态机的代码结构比一堆if-else清晰得多,而且容易扩展。比如后面如果要加双击功能,只需要在状态机里加一个状态就行。

4. 实操过程与关键环节记录

4.1 开发环境搭建

我用的开发环境是STM32CubeMX + Keil MDK。CubeMX用来配置引脚和外设,生成初始化代码;Keil用来写业务逻辑和编译下载。

CubeMX的配置步骤:

  1. 选择芯片型号STM32F103C8T6
  2. 配置RCC:HSE选择Crystal/Ceramic Resonator,LSE也选择Crystal/Ceramic Resonator
  3. 配置SYS:Debug选择Serial Wire
  4. 配置GPIO:继电器控制引脚设为Output Push Pull,按键引脚设为Input Pull-up
  5. 配置USART1:波特率115200,用于和ESP8266通信
  6. 配置USART2:波特率115200,用于调试打印
  7. 配置I2C1:用于OLED显示屏
  8. 配置RTC:时钟源选LSE
  9. 配置FreeRTOS:在Middleware里选FREERTOS,创建四个任务
  10. 生成代码

生成代码后,在Keil里打开工程,先编译一遍确认没有错误,然后再开始写业务逻辑。

4.2 ESP8266配网与MQTT连接

ESP8266的配网我做了两种方式:一种是硬编码WiFi名称和密码在代码里,适合自己用;另一种是通过按键进入配网模式,用手机连接ESP8266的热点,发送WiFi信息。第二种方式更灵活,但实现起来复杂一些。

我一开始用的是硬编码方式,后来发现换一个环境就要重新烧录代码,太麻烦了。后来改成了SmartConfig配网,用手机App(比如ESP8266的官方配网App)发送WiFi信息,ESP8266自动连接。SmartConfig的AT指令是AT+CWSTARTSMART=3,启动后等待手机App发送信息,连接成功后ESP8266会返回+CWSTARTSMART:smartconfig connected。

MQTT连接我用的是免费的公共Broker做测试,比如broker.emqx.io。正式使用的话建议自己搭一个Mosquitto Broker,放在树莓派或者云服务器上,安全性更好。

4.3 继电器控制与安全测试

继电器控制代码很简单,就是拉高或拉低GPIO。但安全测试不能马虎。我第一次测试的时候,先用万用表确认了继电器模块的输入输出是隔离的,然后把一个台灯接到继电器输出端,用STM32控制通断,观察了半个小时,确认没有异常发热后才敢接更大的负载。

测试步骤:

  1. 不接220V,只给STM32和继电器模块供5V,用万用表测量继电器输出端的通断状态
  2. 接一个低功率负载(比如5W的LED灯),测试通断是否正常
  3. 接一个中等功率负载(比如60W的白炽灯),测试长时间工作是否稳定
  4. 接一个高功率负载(比如1000W的电暖器),测试继电器触点是否发热

注意:测试高功率负载时,继电器触点可能会产生电弧,这是正常现象,但如果电弧很大或者触点粘连,说明继电器容量不够,需要换更大容量的继电器。

4.4 定时任务的设置与存储

定时任务的设置流程是:用户长按设置键进入设置模式,通过按键选择要设置的任务编号,然后依次设置小时、分钟、动作(开或关),最后确认保存。

保存到Flash的时候,我用了一个简单的数据结构:

typedef struct { uint8_t hour; uint8_t minute; uint8_t action; // 0=关,1=开 uint8_t enabled; // 0=禁用,1=启用 } TimerTask; typedef struct { TimerTask tasks[8]; // 最多8个定时任务 uint8_t magic; // 用于判断Flash是否已初始化 } TimerConfig;

写入Flash之前要先擦除对应的页,然后按半字(16位)写入。STM32F103的Flash写入要求每次写半字,不能直接写字节。HAL库提供了HAL_FLASH_Program函数,可以按半字、字或双字写入。

读取的时候直接按结构体读取即可,但要注意Flash的起始地址要对齐。我用的起始地址是0x0801FC00,这是STM32F103C8T6的最后一页(第63页)的起始地址。

5. 常见问题与排查技巧实录

5.1 STM32无法下载程序

这是新手最常遇到的问题。可能的原因和排查步骤:

现象可能原因排查方法
Keil提示“No Cortex-M Device found”SWD线接错或接触不良检查SWDIO和SWCLK是否接对,换一根杜邦线
能识别芯片但下载失败Flash被读保护用STM32CubeProgrammer解除读保护
下载后程序不运行BOOT0跳线不对BOOT0接GND,重新上电
下载速度很慢或失败SWD时钟太快在Keil的Debug设置里把SWD时钟降到1MHz

我遇到过一次,SWD线接在面包板上,因为面包板接触不良,时好时坏。后来直接焊在洞洞板上就再也没出过问题。所以如果你用的是面包板,建议检查一下接触是否可靠。

5.2 ESP8266 AT指令无响应

ESP8266上电后,如果串口助手发送AT没有返回OK,可能是以下原因:

  • 波特率不对。ESP8266-01S的默认波特率是115200,但有些模块是9600或74880。可以尝试这几个波特率。
  • 供电不足。ESP8266在发送数据时瞬间电流可能达到300mA,如果5V电源的电流不够,模块会不断重启。建议单独给ESP8266供3.3V,电流至少500mA。
  • 固件损坏。如果模块之前被烧录过其他固件,AT固件可能被覆盖了。需要用USB转TTL重新烧录AT固件。

5.3 继电器误动作

继电器误动作通常是因为GPIO在上电瞬间有毛刺,导致继电器短暂吸合。解决方法是在GPIO初始化的时候,先把引脚设为低电平,然后再配置为输出模式。或者在继电器驱动电路里加一个RC延时电路,让继电器在上电后延迟几十毫秒再吸合。

还有一个原因是电源干扰。当继电器吸合或断开时,线圈会产生反向电动势,如果电源滤波不好,这个干扰会串到STM32的电源上,导致MCU复位。解决方法是在继电器线圈两端加续流二极管,在电源输入端加一个100uF的电解电容和0.1uF的陶瓷电容。

5.4 RTC走时不准

前面提到过,32.768kHz晶振的负载电容不匹配会导致RTC走时不准。STM32F103的RTC晶振负载电容推荐值是6pF,但很多BluePill板子上焊的是12.5pF的晶振,导致频率偏移。

解决方法有两个:一是换一个6pF负载电容的晶振,二是在代码里做软件校准。STM32的RTC有一个校准寄存器(BKP_RTCCR),可以调整RTC的计数频率。具体校准值需要根据实际走时误差来计算,比如一天快了10秒,那校准值大约是10/86400*2^20,约等于121。

5.5 远程控制延迟大

如果手机发送指令后,插座要好几秒才响应,可能是以下原因:

  • MQTT Broker的QoS等级设置太高。QoS 2需要三次握手,延迟比QoS 0大很多。如果对可靠性要求不是极高,用QoS 0就够了。
  • ESP8266的串口波特率太低。115200的波特率下,一条AT指令的传输时间大约几毫秒,但如果波特率降到9600,传输时间会增加到几十毫秒。
  • STM32的任务优先级设置不合理。如果串口通信任务的优先级太低,MQTT消息可能会在队列里等待很久才被处理。建议把串口通信任务的优先级设为最高。

6. 项目扩展与个人经验分享

6.1 可以继续扩展的方向

这个项目的基础框架搭好之后,有很多可以扩展的方向。比如加一个电流传感器(比如ACS712),实时监测插座的功率,超过阈值自动断电;加一个温湿度传感器(比如DHT11),根据环境温度自动控制风扇或加热器;加一个红外发射模块,把普通空调或电视变成智能设备。

如果你想把数据上传到云端做长期记录,可以把MQTT Broker换成云端的服务,然后在服务器端用数据库存储历史数据,再用Grafana或者自己写一个Web页面做可视化。这样就是一个完整的物联网项目了。

6.2 我在这个项目里踩过的坑

第一个坑是电源。我一开始用了一个便宜的5V手机充电器给整个系统供电,结果ESP8266一发送数据就重启。后来换了一个2A的充电器就好了。所以电源的电流余量一定要留够,至少是峰值电流的两倍。

第二个坑是Flash存储。我一开始把定时任务数据存在Flash的最后一页,但后来发现程序代码也占用了那一页,导致写入数据时把代码擦掉了,程序直接跑飞。后来查了链接脚本(.ld文件)才知道,代码段和数据段是分开的,Flash的最后一页确实没有被代码占用,但前提是你的程序大小没有超过倒数第二页的末尾。所以用Flash模拟EEPROM的时候,一定要确认程序大小,留出足够的空间。

第三个坑是MQTT主题命名。我一开始用了中文主题名,结果ESP8266的AT固件不支持中文,发送失败。后来改成英文和数字的组合就好了。所以MQTT主题名建议只用字母、数字和斜杠。

6.3 给后来者的几点建议

如果你打算做这个项目,我的建议是:先把硬件调通,再写软件。硬件部分先不接220V,只用5V和3.3V测试STM32、继电器、ESP8266和显示屏是否都能正常工作。确认硬件没问题后,再一个一个模块地写软件,每写一个模块就测试一个模块,不要一次性把所有代码写完再调试,那样出了问题很难定位。

另外,调试信息一定要打印出来。我在USART2上接了一个USB转TTL模块,专门用来打印调试信息。每个关键步骤都打印一条日志,比如“按键按下”“MQTT消息收到”“继电器状态切换”。这样出问题的时候,看一眼串口输出就知道程序跑到哪里了。

最后,安全永远是第一位的。220V不是闹着玩的,调试的时候一定要断电操作,接负载的时候一定要确认接线牢固,不要有裸露的铜线。如果你对强电不熟悉,建议先用低压直流负载(比如12V的LED灯带)做测试,确认逻辑没问题后再换成220V负载。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进