
在智能家居项目中实现窗帘的远程和语音控制是一个高频需求。很多开发者尝试将STM32、WiFi模块、手机APP和云平台整合时常常遇到硬件选型、网络协议对接、数据同步和稳定性等一系列难题网上资料又往往只讲单一环节缺乏一个从零到一的完整闭环方案。本文将以“智能窗帘”为实战项目系统拆解基于STM32单片机、ESP8266 WiFi模块、手机APP和OneNET云平台的全栈物联网开发流程。内容涵盖硬件电路设计、STM32固件开发、MQTT协议通信、APP界面编写以及云平台设备管理并提供完整的可运行代码和配置。无论你是嵌入式新手想入门物联网还是有一定基础的开发者希望整合软硬件都能从本文获得可直接复用的解决方案。1. 项目背景与核心概念智能窗帘系统是现代智能家居的重要组成部分它通过物联网技术将传统的窗帘控制方式从手动拉合升级为远程无线控制、定时任务甚至语音交互。这不仅提升了生活便利性也是学习嵌入式系统、无线通信和云平台技术的绝佳实践项目。一个完整的智能窗帘物联网系统通常包含以下几个核心层感知与控制层设备端以STM32单片机为核心负责采集窗帘状态如通过限位开关或编码器判断位置并驱动电机正反转来实现窗帘的打开与关闭。它是系统的“手”和“脚”。网络传输层通常采用WiFi模块如ESP8266/ESP32负责将设备端的数据如状态、指令通过无线网络上传至互联网并接收来自云平台或APP的控制指令。它是系统的“神经”。云平台层如中国移动OneNET、阿里云物联网平台等。它作为数据中转站和设备管理后台负责接收设备上报的数据并转发给APP同时接收APP下发的指令并转发给设备。它还提供设备管理、数据可视化、规则引擎等高级功能。应用层用户端即手机APP或微信小程序。为用户提供直观的操作界面可以实时查看窗帘状态开/关/百分比并发送控制指令。部分高级应用还支持语音助手如天猫精灵、小爱同学集成实现语音控制。本项目的技术路线为STM32 (MCU) ESP8266 (WiFi) MQTT (协议) OneNET (云平台) 安卓APP (应用)。我们将逐一攻克每个环节。2. 环境准备与版本说明在开始编码和硬件连接前请确保你的开发环境已就绪。以下版本为本文示例所使用实际操作时请根据你的硬件和软件情况进行适配。硬件清单主控芯片STM32F103C8T6 最小系统板核心板WiFi模块ESP-01S (基于ESP8266)电机驱动L298N 电机驱动模块用于驱动直流电机或 ULN2003 驱动板用于驱动步进电机执行机构12V直流减速电机 或 28BYJ-48步进电机电源模块12V电源适配器为电机供电AMS1117-3.3V模块为STM32和ESP8266提供3.3V其他杜邦线、限位开关2个、USB转TTL串口模块、万用表、电烙铁等。软件与工具清单集成开发环境 (IDE)Keil uVision5 (MDK-ARM) 或 STM32CubeIDE单片机程序框架标准外设库 (Standard Peripheral Library) 或 HAL库。本文示例基于HAL库因其可移植性更好。串口调试助手如XCOM、SSCOM用于调试STM32与ESP8266的AT指令通信。APP开发环境Android Studio (用于开发原生安卓APP)云平台账号中国移动OneNET物联网平台 (https://open.iot.10086.cn/) 注册账号。网络协议调试工具MQTT.fx 或 MQTTBox用于测试设备与云端的MQTT连接。关键软件版本STM32CubeMX: 6.5.0HAL库版本: 1.8.0ESP8266 AT固件版本: 支持MQTT的AT指令集如安信可提供的AT固件OneNET MQTT旧版协议较稳定本文采用此协议Android Studio: 2021.3.1 Patch 13. 核心原理与技术选型拆解3.1 系统通信架构与协议选择智能窗帘系统的核心是数据流。我们采用MQTT协议作为设备、云平台、APP之间的通信标准。MQTT是一种基于发布/订阅模式的轻量级消息协议非常适合物联网设备在低带宽、不稳定网络环境下的通信。工作流程如下STM32通过串口AT指令控制ESP8266连接到家庭路由器并作为MQTT客户端连接到OneNET云平台。STM32将窗帘的当前状态如{curtain_state: 50}表示打开50%通过ESP8266发布(Publish)到云平台指定的主题(Topic)。手机APP也作为另一个MQTT客户端连接到同一个OneNET云平台。当用户在APP上点击“打开”按钮时APP会向云平台的一个特定主题发布一条控制命令如{cmd: open}。云平台将该命令消息转发给订阅了该主题的设备即我们的STM32设备。ESP8266通过串口将收到的命令数据传递给STM32。STM32解析命令控制电机转动直到达到目标状态然后上报新的状态完成一个控制闭环。3.2 硬件电路设计要点1. STM32与ESP8266的连接这是最关键的一环。ESP-01S通常通过串口与STM32通信。ESP8266_TX- 连接 STM32的USART_RX引脚 (如PA3)ESP8266_RX- 连接 STM32的USART_TX引脚 (如PA2)ESP8266_VCC- 连接3.3V(必须稳定电流足够建议单独供电)ESP8266_GND- 连接GNDESP8266_CH_PD/EN- 连接3.3V(使能)ESP8266_GPIO0- 悬空或接高电平运行模式ESP8266_RST- 可通过STM32 GPIO控制用于硬件复位。2. STM32与电机驱动的连接以L298N驱动直流电机为例STM32 GPIO1(如PA0) - L298NIN1STM32 GPIO2(如PA1) - L298NIN2L298N OUT1, OUT2- 连接直流电机两极。L298N供电12V和GND接外部12V电源5V输出可为STM32供电需确认电压匹配。使能端ENA可接PWM引脚实现调速或直接接高电平使能。3. 限位开关连接用于检测窗帘完全打开和完全关闭的位置防止电机堵转。两个限位开关常开型一端分别接STM32的GPIO如PC13 PC14另一端接GND。STM32 GPIO配置为上拉输入模式。当窗帘碰到限位开关时开关闭合GPIO读到低电平。3.3 OneNET云平台关键概念产品一类设备的集合。你需要先创建一个“智能窗帘”产品。设备归属于某个产品下的一个实体对应你的一套STM32硬件。创建设备后会获得唯一的设备ID (DEVICE_ID)和鉴权信息 (AUTH_INFO)这是设备登录云平台的凭证。数据流设备的一种属性如“curtain_state”。设备可以上报数据到数据流。API Key用于从平台侧如APP访问该产品下设备数据的密钥。TopicMQTT通信的主题。OneNET有预定义的主题格式例如用于设备上报的$sys/{pid}/{device-name}/dp/post/json用于命令下发的$sys/{pid}/{device-name}/cmd/request/{cmd-id}。4. 完整实战案例从零搭建智能窗帘系统4.1 STM32端固件开发我们使用STM32CubeMX进行初始化配置然后使用Keil编写业务逻辑。步骤1使用STM32CubeMX创建工程选择MCU型号STM32F103C8Tx。配置时钟使用外部高速时钟(HSE)系统时钟设置为72MHz。配置串口USART1用于与ESP8266通信。模式为异步通信(Asynchronous)波特率115200。配置GPIO两个GPIO输出用于控制L298N的IN1和IN2。两个GPIO输入上拉模式连接限位开关。一个GPIO输出连接ESP8266的RST引脚可选用于复位。配置一个定时器如TIM2用于产生PWM控制电机速度可选。生成代码选择IDE为MDK-ARM。步骤2编写ESP8266 MQTT驱动代码核心是通过串口发送AT指令控制ESP8266。我们需要编写一个esp8266.c/.h文件。// 文件esp8266.h #ifndef __ESP8266_H #define __ESP8266_H #include “main.h” #include “stdio.h” #include “string.h” // 定义ESP8266连接的串口句柄 extern UART_HandleTypeDef huart1; #define ESP_UART huart1 // 函数声明 void ESP8266_Init(void); uint8_t ESP8266_SendCmd(char *cmd, char *ack, uint16_t timeout); uint8_t ESP8266_ConnectWiFi(char *ssid, char *pwd); uint8_t ESP8266_ConnectMQTTServer(char *server, char *port, char *client_id, char *username, char *password); uint8_t ESP8266_MQTTPublish(char *topic, char *msg); void ESP8266_UART_RxCpltCallback(UART_HandleTypeDef *huart); // 串口接收中断回调 #endif// 文件esp8266.c #include “esp8266.h” char esp_rx_buffer[512]; // 接收缓冲区 uint16_t esp_rx_index 0; uint8_t esp_rx_flag 0; // 发送AT指令并等待应答 uint8_t ESP8266_SendCmd(char *cmd, char *ack, uint16_t timeout) { char rx_buffer[256]; uint16_t time 0; esp_rx_index 0; esp_rx_flag 0; // 发送指令 HAL_UART_Transmit(ESP_UART, (uint8_t*)cmd, strlen(cmd), 1000); HAL_UART_Transmit(ESP_UART, (uint8_t*)\r\n, 2, 1000); // AT指令以回车换行结束 // 等待应答 while(time timeout) { if(esp_rx_flag 1) { // 在串口中断中置位此标志 esp_rx_flag 0; if(strstr((char*)esp_rx_buffer, ack) ! NULL) { return 1; // 成功 } } HAL_Delay(10); time 10; } return 0; // 超时失败 } // 连接WiFi uint8_t ESP8266_ConnectWiFi(char *ssid, char *pwd) { char cmd[128]; sprintf(cmd, “ATCWJAP\”%s\”,\”%s\””, ssid, pwd); return ESP8266_SendCmd(cmd, “OK”, 10000); // 连接WiFi可能较慢超时设长 } // 连接OneNET MQTT服务器 (旧版协议) uint8_t ESP8266_ConnectMQTTServer(char *server, char *port, char *client_id, char *username, char *password) { char cmd[256]; // 设置MQTT客户端ID、用户名、密码 sprintf(cmd, “ATMQTTCFG\”%s\”,\”%s\”,\”%s\”,%s,0”, server, port, client_id, username); if(!ESP8266_SendCmd(cmd, “OK”, 3000)) return 0; // 发起连接 sprintf(cmd, “ATMQTTCONN1,\”%s\”,\”%s\””, username, password); return ESP8266_SendCmd(cmd, “MQTTCONN:0”, 5000); // 连接成功返回 MQTTCONN:0 } // 发布消息到主题 uint8_t ESP8266_MQTTPublish(char *topic, char *msg) { char cmd[512]; sprintf(cmd, “ATMQTTPUB\”%s\”,\”%s\”,1,0”, topic, msg); return ESP8266_SendCmd(cmd, “OK”, 3000); } // 串口接收中断回调函数在stm32f1xx_it.c中调用 void ESP8266_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance ESP_UART-Instance) { uint8_t ch; HAL_UART_Receive_IT(ESP_UART, ch, 1); // 重新开启中断 if(esp_rx_index sizeof(esp_rx_buffer)-1) { esp_rx_buffer[esp_rx_index] ch; if(ch ‘\n’) { // 简单以换行符作为一帧结束 esp_rx_buffer[esp_rx_index] ‘\0’; esp_rx_flag 1; esp_rx_index 0; } } } }步骤3编写主业务逻辑在main.c中整合初始化、网络连接、电机控制和状态上报。// 文件main.c (部分关键代码) #include “esp8266.h” // 定义WiFi和OneNET信息 #define WIFI_SSID “Your_WiFi_SSID” #define WIFI_PWD “Your_WiFi_Password” #define ONENET_DEVICE_ID “123456789” // 你的设备ID #define ONENET_AUTH_INFO “your_auth_info” // 你的鉴权信息 #define ONENET_PRODUCT_ID “123456” // 你的产品ID // 窗帘状态枚举 typedef enum { CURTAIN_STOP 0, CURTAIN_OPENING, CURTAIN_CLOSING, CURTAIN_OPEN, CURTAIN_CLOSE } CurtainState_t; CurtainState_t curtain_state CURTAIN_STOP; int curtain_position 0; // 0-100 0为全关100为全开 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // ... 其他初始化 // 初始化ESP8266 ESP8266_Init(); HAL_Delay(2000); // 等待模块启动 // 连接WiFi while(!ESP8266_ConnectWiFi(WIFI_SSID, WIFI_PWD)) { HAL_Delay(2000); // 可添加LED闪烁提示连接失败 } // 连接OneNET MQTT char server[] “183.230.40.39”; // OneNET旧版MQTT地址 char port[] “6002”; char client_id[50]; sprintf(client_id, “%s”, ONENET_DEVICE_ID); while(!ESP8266_ConnectMQTTServer(server, port, client_id, ONENET_DEVICE_ID, ONENET_AUTH_INFO)) { HAL_Delay(2000); } // 上报初始状态 report_curtain_status(); while (1) { // 1. 检查限位开关 check_limit_switches(); // 2. 解析来自云平台的命令在串口中断中接收并设置命令标志 parse_mqtt_message(); // 3. 根据命令和限位状态控制电机 control_motor(); // 4. 定时上报状态例如每5秒或状态变化时 static uint32_t last_report 0; if(HAL_GetTick() - last_report 5000) { report_curtain_status(); last_report HAL_GetTick(); } HAL_Delay(50); } } // 控制电机函数示例 void control_motor(void) { switch(curtain_state) { case CURTAIN_OPENING: HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET); break; case CURTAIN_CLOSING: HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_SET); break; default: // STOP, OPEN, CLOSE HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET); break; } } // 上报状态到OneNET void report_curtain_status(void) { char topic[100]; char msg[100]; // 构造OneNET数据点上报Topic sprintf(topic, “$sys/%s/%s/dp/post/json”, ONENET_PRODUCT_ID, ONENET_DEVICE_ID); // 构造JSON格式数据 sprintf(msg, “{\”id\”:123,\”dp\”:{\”curtain_state\”:[{\”v\”:%d}]}}”, curtain_position); ESP8266_MQTTPublish(topic, msg); }4.2 OneNET云平台配置登录OneNET平台进入“开发者中心”。创建产品点击“产品创建”选择“设备接入方式”为“公开协议”“接入协议”选择“MQTT(旧版)”。填写产品名称“智能窗帘”其他信息按需填写。添加设备进入刚创建的产品点击“添加设备”。填写设备名称如Curtain_01、鉴权信息自己设定需与代码中AUTH_INFO一致。记录下生成的设备ID (DEVICE_ID)和产品ID (PRODUCT_ID)。创建数据流进入设备详情页在“数据流”标签页下点击“添加数据流”创建名为curtain_state的数据流。获取API Key在产品详情页点击“API-key”管理创建一个具有“设备数据点查询”和“设备命令下发”权限的API Key并记录下来用于APP开发。4.3 安卓APP开发核心代码示例使用Android Studio集成OneNET提供的MQTT SDK或使用开源的Paho MQTT库。1. 添加依赖 (build.gradle):dependencies { implementation ‘org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5’ implementation ‘org.eclipse.paho:org.eclipse.paho.android.service:1.1.1’ }2. 连接OneNET并订阅命令下发主题:// 文件MqttManager.java public class MqttManager { private MqttAndroidClient mqttClient; private final String serverUri “tcp://183.230.40.39:6002”; private final String clientId “APP_” UUID.randomUUID().toString(); // APP客户端ID需唯一 private final String userName “YOUR_PRODUCT_ID”; // 产品ID private final String password “YOUR_API_KEY”; // 产品的API Key public void connect() { try { mqttClient new MqttAndroidClient(context, serverUri, clientId); MqttConnectOptions options new MqttConnectOptions(); options.setUserName(userName); options.setPassword(password.toCharArray()); options.setCleanSession(true); options.setConnectionTimeout(10); options.setKeepAliveInterval(20); mqttClient.connect(options, null, new IMqttActionListener() { Override public void onSuccess(IMqttToken asyncActionToken) { Log.i(“MQTT”, “Connected to OneNET”); // 连接成功后订阅命令响应主题 subscribeToCmdResponse(); } Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { Log.e(“MQTT”, “Connection failed”, exception); } }); // 设置消息到达回调 mqttClient.setCallback(new MqttCallback() { Override public void connectionLost(Throwable cause) { } Override public void messageArrived(String topic, MqttMessage message) throws Exception { // 处理设备上报的状态消息更新UI String payload new String(message.getPayload()); parseDeviceStatus(payload); } Override public void deliveryComplete(IMqttDeliveryToken token) { } }); } catch (MqttException e) { e.printStackTrace(); } } private void subscribeToCmdResponse() { String topic String.format(“$sys/%s/%s/dp/post/json”, productId, deviceId); try { mqttClient.subscribe(topic, 0); } catch (MqttException e) { e.printStackTrace(); } } }3. 发送控制命令:// 在Activity中发送打开窗帘命令 public void sendOpenCommand() { String topic String.format(“$sys/%s/%s/cmd/request/%s”, productId, deviceId, UUID.randomUUID().toString()); String cmdMsg “{\”cmd\”: \”open\”}”; // 命令内容需与设备端解析格式一致 try { MqttMessage message new MqttMessage(cmdMsg.getBytes()); message.setQos(0); mqttClient.publish(topic, message); } catch (MqttException e) { e.printStackTrace(); } }4. 解析设备状态更新UI:private void parseDeviceStatus(String jsonStr) { try { JSONObject jsonObject new JSONObject(jsonStr); JSONObject dp jsonObject.getJSONObject(“dp”); JSONArray curtainArray dp.getJSONArray(“curtain_state”); int position curtainArray.getJSONObject(0).getInt(“v”); runOnUiThread(() - { progressBar.setProgress(position); textViewStatus.setText(“当前位置” position “%”); }); } catch (JSONException e) { e.printStackTrace(); } }4.4 系统联调与测试硬件连接按照原理图连接STM32、ESP8266、电机驱动、限位开关和电源。务必确认电压匹配特别是ESP8266必须使用稳定的3.3V供电。烧录程序将编译好的STM32固件通过ST-Link或USB转TTL烧录到单片机。串口调试先不接WiFi模块用USB转TTL连接ESP8266到电脑使用串口助手发送AT指令测试模块是否正常。然后测试ATCWJAP连接WiFiATMQTTCONN连接OneNET。整体测试将ESP8266接回STM32上电。观察串口日志确认设备成功连接WiFi和OneNET。APP测试在手机上运行APP输入正确的产品ID、设备ID和API Key点击连接。尝试发送“打开”、“关闭”、“停止”命令观察窗帘电机是否动作同时APP界面是否收到设备上报的最新位置状态。5. 常见问题与排查思路在开发过程中你可能会遇到以下典型问题问题现象可能原因排查思路与解决方案ESP8266无法连接WiFi1. SSID或密码错误。2. WiFi信号太弱。3. 模块供电不足电流小于300mA。4. AT指令格式错误。1. 用串口助手单独测试ATCWJAP指令。2. 确保模块靠近路由器。3. 使用独立3.3V电源给ESP8266供电并并联一个100uF电容。4. 检查指令结尾是否加了\r\n。ESP8266连接OneNET失败1. 设备ID、鉴权信息错误。2. 产品接入协议选错应为MQTT旧版。3. 网络问题无法访问云平台地址。1. 核对OneNET平台上的设备信息与代码是否完全一致。2. 检查产品详情中的接入协议。3. 尝试用电脑Ping183.230.40.39。APP能发命令但设备无反应1. 设备未成功订阅命令主题。2. MQTT命令Topic格式错误。3. 设备端串口未正确解析到命令数据。1. 在OneNET平台“消息追踪”功能中查看命令是否成功下发。2. 严格对照OneNET文档检查Topic格式。3. 在STM32代码中增加串口打印查看是否收到原始MQTT数据。电机只震动不转动1. 电源功率不足。2. L298N使能端(ENA)未接高电平或PWM。3. 电机线序接错。1. 使用额定电流足够的12V电源适配器。2. 将L298N的ENA跳线帽接上或接STM32的5V/3.3V。3. 交换连接电机的两根线。限位开关失灵1. GPIO未配置为上拉输入。2. 开关机械故障或接触不良。3. 程序消抖处理不当。1. 用万用表测量开关按下时GPIO电压是否为0。2. 在代码中为限位开关检测增加软件消抖如连续读取10ms均为低电平才确认触发。设备频繁掉线1. 网络不稳定。2. MQTT KeepAlive时间设置过短。3. OneNET旧版协议连接数限制。1. 优化WiFi信号。2. 将ESP8266的KeepAlive时间设置为60-120秒。3. 确保设备ID唯一避免冲突。6. 最佳实践与工程建议将项目原型转化为稳定可靠的产品还需要考虑以下工程化细节固件健壮性设计看门狗启用STM32的独立看门狗(IWDG)和窗口看门狗(WWDG)防止程序跑飞。异常重启在网络连接失败、解析数据异常等关键错误后进行延时并自动重试超过一定次数后执行软件复位。参数存储将WiFi的SSID/密码、设备ID等配置信息存储在STM32的Flash或EEPROM中方便通过串口指令或按键进行配置无需修改代码重新烧录。通信协议与数据格式优化心跳包设备定时如每60秒向云平台发送一个简单的心跳包用于维持长连接和检测设备在线状态。数据压缩上报的JSON数据可以精简键名如用c:50代替curtain_state:50减少网络流量。命令确认机制APP下发命令后设备执行成功应回复一条确认消息。APP可设置超时重发提高控制可靠性。电机控制与安全软启动/软停止使用PWM逐渐增加/减少电机占空比避免电流冲击使窗帘启停更平缓。堵转检测监测电机电流可通过采样电阻和ADC当电流持续过高时判断为堵转立即停止电机并上报故障。位置记忆如果使用步进电机可以通过记录步进数来精确计算窗帘位置。即使断电位置信息也应保存在非易失存储器中。APP用户体验本地缓存APP应将设备最后上报的状态缓存到本地每次启动时先显示缓存数据再尝试连接云平台获取实时数据避免启动时白屏。离线队列在网络不佳时用户的操作命令可先存入本地队列待网络恢复后自动发送。多设备管理APP应支持添加和管理多个窗帘设备并可能支持场景联动如“离家模式”自动关闭所有窗帘。生产环境部署考虑OTA升级预留通过WiFi进行固件在线升级(OTA)的接口便于后期修复漏洞和增加功能。日志上报设备可将运行日志错误码、网络状态加密后上报到云平台便于远程诊断问题。安全加固使用TLS加密MQTT连接OneNET支持防止通信被窃听。鉴权信息避免硬编码采用安全芯片或加密存储。通过以上步骤你不仅完成了一个可运行的智能窗帘项目更掌握了物联网系统从硬件到云到应用的全栈开发思维。接下来你可以在此基础上扩展更多功能如语音助手接入通过云平台规则引擎转发到设备、光照传感器自动控制、定时任务、多用户权限分享等将其打磨成一个真正实用的智能家居产品。