
大家好我是专注于嵌入式物联网开发的技术博主。在智能家居项目中实现窗帘的远程控制是一个经典且实用的需求。本文将手把手带你完成一个完整的“智能窗帘”项目它集成了STM32单片机、WiFi模块、手机APP和云平台实现从硬件选型、固件开发、APP设计到云端数据联动的全流程实战。无论你是刚接触STM32的新手还是想深入了解物联网系统架构的开发者都能从本文中获得一套可直接复用的解决方案。1. 项目背景与核心概念智能窗帘系统是现代智能家居的重要组成部分它旨在通过自动化或远程控制的方式替代传统的手动拉合窗帘提升生活便利性和舒适度。一个完整的智能窗帘方案通常包含感知、决策、执行和交互四个层面。核心组成部分感知层由STM32微控制器作为核心大脑负责采集环境光强度、接收控制指令、监测电机状态等。网络层采用ESP8266或ESP32等WiFi模块负责将STM32与互联网连接实现数据的无线传输。平台层即云平台如阿里云物联网平台、OneNET作为设备与APP之间的数据中转站和规则引擎负责设备管理、数据存储和命令下发。应用层用户手机上的APP如基于Android开发的应用程序为用户提供直观的控制界面和状态查看功能。为什么选择STM32WiFi云平台的架构STM32资源丰富、性价比高、生态完善非常适合作为智能设备的控制核心。WiFi几乎家家户户都有无线网络接入方便无需额外布线通信速率也能满足控制需求。云平台解决了设备与APP直连带来的IP动态变化、端口映射等复杂网络问题。同时云平台提供了设备管理、数据可视化、规则引擎等高级功能为后续扩展如语音控制、场景联动奠定了基础。本项目将实现以下核心功能本地手动控制通过物理按键控制窗帘开、关、停。APP远程控制通过手机APP在任何有网络的地方控制窗帘。状态同步窗帘的实时开合状态在APP上同步显示。定时任务通过APP设置定时开启或关闭窗帘。环境光控制根据光照传感器数据自动控制窗帘可选进阶功能。2. 硬件设计与环境准备2.1 硬件清单与选型建议一个最小系统的智能窗帘硬件主要包括以下部分模块名称型号推荐主要作用主控MCUSTM32F103C8T6 (核心板)系统控制核心处理逻辑驱动外设WiFi模块ESP-01S (ESP8266)连接路由器与云平台进行MQTT/HTTP通信电机驱动L298N或TB6612FNG驱动直流电机正反转实现窗帘开合执行电机12V直流减速电机提供动力拉动窗帘电源模块LM2596降压模块将外部12V电源降压为5V/3.3V给各模块供电限位开关微动开关 (2个)检测窗帘完全打开和完全关闭的位置防止电机堵转光照传感器BH1750 (可选)检测环境光强度用于自动模式按键轻触开关 (3个)本地手动控制开、关、停连接示意图核心部分12V电源 - 降压模块 - 【5V/3.3V系统供电】 STM32 (PA2/PA3) --UART-- ESP8266 (TX/RX) STM32 (PB6/PB7) - 电机驱动 (IN1/IN2) - 直流电机 STM32 (PC13, PC14, PC15) - 按键 (开、关、停) STM32 (PA0, PA1) - 限位开关 (开限位、关限位)2.2 软件开发环境准备IDE/编译器Keil uVision 5 (MDK-ARM) 或 STM32CubeIDE。本文以Keil为例。STM32固件库使用标准外设库Standard Peripheral Library或HAL库。HAL库兼容性更好推荐初学者使用。可通过STM32CubeMX工具初始化生成代码。串口调试助手如XCOM、SSCOM用于调试STM32与ESP8266的AT指令通信。网络调试助手用于模拟测试MQTT服务器连接。云平台账号注册一个阿里云物联网平台或OneNET开发者账号用于创建设备和获取连接密钥。APP开发环境如需自己开发APP需要Android StudioJava/Kotlin或uni-app框架。本文会提供与云平台通信的核心逻辑代码。3. 核心原理与通信协议拆解3.1 系统工作流程整个系统的数据流如下图所示文字描述用户通过APP发送“打开窗帘”指令。指令通过互联网到达云平台。云平台通过MQTT协议将指令下发给在线的设备ESP8266。ESP8266通过串口UART将指令传递给STM32。STM32解析指令控制电机驱动模块驱动电机正转。电机带动窗帘移动直到触发“开限位”开关STM32停止电机并将“已完全打开”的状态通过ESP8266上报给云平台。云平台将状态推送给APPAPP界面更新。3.2 关键通信协议MQTTMQTT是一种轻量级的发布/订阅消息传输协议非常适合物联网设备在低带宽、不稳定网络环境下通信。在本项目中的角色主题Topic云平台为每个设备定义了两个核心主题属性上报主题/sys/{productKey}/{deviceName}/thing/event/property/post。设备STM32ESP8266通过此主题向云端上报当前状态如{curtain_state: 1}(1代表打开中2代表关闭中3代表停止4代表全开5代表全关)。属性设置主题/sys/{productKey}/{deviceName}/thing/service/property/set。云端或APP通过此主题向设备下发控制指令如{curtain_control: 1}(1代表开2代表关3代表停)。通信过程ESP8266作为MQTT客户端连接云平台的MQTT服务器Broker。订阅Subscribe属性设置主题以接收指令。当需要上报状态时向Publish属性上报主题发布消息。APP同样作为另一个MQTT客户端订阅设备的上报主题并向设置主题发布指令。3.3 STM32与ESP8266的串口通信STM32通过AT指令集控制ESP8266。通信流程如下初始化STM32发送AT、ATCWMODE1STA模式、ATCWJAPSSID,password连接WiFi等指令配置ESP8266。连接云平台发送ATMQTTUSERCFG和ATMQTTCONN等指令使ESP8266连接到指定的MQTT服务器。订阅主题发送ATMQTTSUB指令订阅控制主题。数据透传初始化完成后STM32只需将需要发送的JSON数据通过串口发给ESP8266ESP8266会自动将其作为MQTT消息发布。同样ESP8266收到的MQTT消息也会通过串口原样传给STM32。4. 完整实战案例从零搭建系统4.1 STM32端固件开发我们使用STM32CubeMX生成基础工程再添加业务逻辑。步骤1使用STM32CubeMX配置引脚和时钟配置USART2用于与ESP8266通信PA2-TX PA3-RX。配置3个GPIO引脚为输入模式上拉连接3个按键。配置2个GPIO引脚为输入模式上拉连接2个限位开关。配置2个GPIO引脚为输出模式连接电机驱动的IN1和IN2。配置系统时钟为72MHz。生成Keil MDK-ARM工程。步骤2编写核心控制逻辑main.c关键部分// 定义窗帘状态 typedef enum { CURTAIN_STOP 0, CURTAIN_OPENING, CURTAIN_CLOSING, CURTAIN_FULLY_OPEN, CURTAIN_FULLY_CLOSED } CurtainState_t; CurtainState_t g_curtain_state CURTAIN_STOP; uint8_t g_mqtt_rx_buffer[256]; // 存储从ESP8266收到的数据 uint8_t g_mqtt_rx_index 0; // 电机控制函数 void Motor_Control(uint8_t cmd) { // cmd: 0-停止 1-正转开 2-反转关 switch(cmd) { case 0: // 停 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); g_curtain_state CURTAIN_STOP; break; case 1: // 正转-开 if(HAL_GPIO_ReadPin(LIMIT_OPEN_GPIO_Port, LIMIT_OPEN_Pin) GPIO_PIN_SET) { // 已经到全开位置不再动作 g_curtain_state CURTAIN_FULLY_OPEN; Motor_Control(0); Report_Status(); // 上报状态 return; } 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); g_curtain_state CURTAIN_OPENING; break; case 2: // 反转-关 if(HAL_GPIO_ReadPin(LIMIT_CLOSE_GPIO_Port, LIMIT_CLOSE_Pin) GPIO_PIN_SET) { // 已经到全关位置 g_curtain_state CURTAIN_FULLY_CLOSED; Motor_Control(0); Report_Status(); return; } 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); g_curtain_state CURTAIN_CLOSING; break; } Report_Status(); } // 上报状态到云平台 void Report_Status(void) { char json_buffer[128]; // 根据阿里云物联网平台物模型规范格式上报 sprintf(json_buffer, {\id\:\%d\,\version\:\1.0\,\params\:{\curtain_state\:%d},\method\:\thing.event.property.post\}, (int)HAL_GetTick(), // 用时间戳作为消息ID g_curtain_state); // 通过串口发送给ESP8266 ESP8266会将其作为MQTT消息发布 printf(ATMQTTPUB0,\/sys/a1b2c3d4e5/MyCurtain/thing/event/property/post\,\%s\,1,0\r\n, json_buffer); // 注意实际需要等待ESP8266返回“OK”或通过更严谨的AT指令流程 } // 解析从云平台收到的JSON指令 void Parse_MQTT_Command(char* json_str) { // 这是一个简化的解析示例实际项目建议使用cJSON等库 if(strstr(json_str, \curtain_control\:1) ! NULL) { Motor_Control(1); // 开 } else if(strstr(json_str, \curtain_control\:2) ! NULL) { Motor_Control(2); // 关 } else if(strstr(json_str, \curtain_control\:3) ! NULL) { Motor_Control(0); // 停 } } // 串口接收中断回调函数接收ESP8266的数据 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART2) { uint8_t rx_byte g_mqtt_rx_buffer[g_mqtt_rx_index]; if(rx_byte \n) { // 假设以换行符结束一条消息 g_mqtt_rx_buffer[g_mqtt_rx_index] \0; // 添加字符串结束符 // 判断是否是MQTT消息包含主题信息 if(strstr((char*)g_mqtt_rx_buffer, MQTTSUBRECV) ! NULL) { // 提取消息中的JSON部分进行解析 char* json_start strchr((char*)g_mqtt_rx_buffer, {); if(json_start ! NULL) { Parse_MQTT_Command(json_start); } } g_mqtt_rx_index 0; // 清空缓冲区 } else { g_mqtt_rx_buffer[g_mqtt_rx_index] rx_byte; if(g_mqtt_rx_index 255) g_mqtt_rx_index 0; // 防止溢出 } HAL_UART_Receive_IT(huart2, g_mqtt_rx_buffer[g_mqtt_rx_index], 1); // 重新开启接收中断 } } // 主循环 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); // 初始化ESP8266 ESP8266_Init(); // 此函数内包含发送AT指令连接WiFi和MQTT的流程 HAL_UART_Receive_IT(huart2, g_mqtt_rx_buffer[0], 1); // 启动串口接收中断 while (1) { // 1. 扫描按键本地控制优先级最高 if(HAL_GPIO_ReadPin(KEY_OPEN_GPIO_Port, KEY_OPEN_Pin) GPIO_PIN_RESET) { HAL_Delay(50); // 消抖 if(HAL_GPIO_ReadPin(KEY_OPEN_GPIO_Port, KEY_OPEN_Pin) GPIO_PIN_RESET) { Motor_Control(1); while(HAL_GPIO_ReadPin(KEY_OPEN_GPIO_Port, KEY_OPEN_Pin) GPIO_PIN_RESET); // 等待松开 } } // ... 扫描其他按键关、停 // 2. 扫描限位开关 if(g_curtain_state CURTAIN_OPENING HAL_GPIO_ReadPin(LIMIT_OPEN_GPIO_Port, LIMIT_OPEN_Pin) GPIO_PIN_SET) { Motor_Control(0); g_curtain_state CURTAIN_FULLY_OPEN; Report_Status(); } // ... 扫描关闭限位 HAL_Delay(10); } }4.2 ESP8266 AT指令配置流程ESP8266需要正确初始化才能工作。下面是在ESP8266_Init()函数中需要执行的AT指令序列需根据实际WiFi和云平台信息修改void ESP8266_Init(void) { HAL_Delay(2000); // 等待模块启动 ESP8266_SendCmd(AT\r\n, OK, 2000); // 测试AT指令 ESP8266_SendCmd(ATCWMODE1\r\n, OK, 2000); // 设置为STA模式 ESP8266_SendCmd(ATCWJAP\Your_WiFi_SSID\,\Your_WiFi_Password\\r\n, OK, 10000); // 连接WiFi ESP8266_SendCmd(ATCIPSNTPCFG1,8,\ntp1.aliyun.com\\r\n, OK, 2000); // 配置SNTP获取时间可选 // 配置MQTT用户信息以阿里云为例 // productKey, deviceName, deviceSecret 需从物联网平台获取 char mqtt_cfg_cmd[256]; sprintf(mqtt_cfg_cmd, ATMQTTUSERCFG0,1,\NULL\,\%s%s\,\%s\,0,0,\\\r\n, deviceName, productKey, deviceSecret); ESP8266_SendCmd(mqtt_cfg_cmd, OK, 2000); // 配置MQTT客户端ID sprintf(mqtt_cfg_cmd, ATMQTTCLIENTID0,\%s|securemode3,signmethodhmacsha1|\\r\n, clientId); // clientId需要根据平台规则生成 ESP8266_SendCmd(mqtt_cfg_cmd, OK, 2000); // 连接MQTT服务器 ESP8266_SendCmd(ATMQTTCONN0,\iot-as-mqtt.cn-shanghai.aliyuncs.com\,1883,1\r\n, OK, 10000); // 订阅设备属性设置主题 sprintf(mqtt_cfg_cmd, ATMQTTSUB0,\/sys/%s/%s/thing/service/property/set\,1\r\n, productKey, deviceName); ESP8266_SendCmd(mqtt_cfg_cmd, OK, 2000); }注ESP8266_SendCmd是一个需要自己实现的函数用于发送AT指令并检查响应。4.3 云平台配置以阿里云物联网平台为例登录平台进入阿里云物联网平台控制台。创建产品产品名称“智能窗帘”节点类型“直连设备”联网方式“Wi-Fi”数据格式“ICA标准数据格式Alink JSON”。定义物模型在产品下添加一个“窗帘”功能。添加一个属性curtain_state窗帘状态数据类型为int32枚举值1开2关3停4全开5全关。设置为“可读”。添加一个服务curtain_control窗帘控制输入参数为control_cmdint32类型枚举值1开2关3停。设置为“可写”。创建设备在产品下添加一个设备记录下生成的ProductKey、DeviceName和DeviceSecret用于ESP8266连接。查看设备设备上线后可以在控制台看到在线状态并能进行调试下发测试指令。4.4 Android APP开发核心逻辑APP的核心是作为一个MQTT客户端连接云平台订阅设备状态并发布控制指令。这里使用Eclipse PahoAndroid 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. 核心MQTT连接与控制代码 (MainActivity.java片段):public class MainActivity extends AppCompatActivity { private MqttAndroidClient mqttClient; private final String serverUri tcp://iot-as-mqtt.cn-shanghai.aliyuncs.com:1883; private final String clientId AndroidApp_ System.currentTimeMillis(); // 客户端ID private final String productKey 您的ProductKey; private final String deviceName 您的DeviceName; private String subscribeTopic; // 订阅主题接收设备状态 private String publishTopic; // 发布主题发送控制指令 Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化主题 subscribeTopic /sys/ productKey / deviceName /thing/event/property/post; publishTopic /sys/ productKey / deviceName /thing/service/property/set; connectToMQTT(); findViewById(R.id.btn_open).setOnClickListener(v - sendControlCommand(1)); findViewById(R.id.btn_close).setOnClickListener(v - sendControlCommand(2)); findViewById(R.id.btn_stop).setOnClickListener(v - sendControlCommand(3)); } private void connectToMQTT() { try { mqttClient new MqttAndroidClient(this, serverUri, clientId); MqttConnectOptions options new MqttConnectOptions(); options.setCleanSession(true); options.setUserName(deviceName productKey); // 密码需要根据DeviceSecret计算此处简化实际需按阿里云规则生成 options.setPassword(calculatePassword().toCharArray()); mqttClient.connect(options, null, new IMqttActionListener() { Override public void onSuccess(IMqttToken asyncActionToken) { runOnUiThread(() - Toast.makeText(MainActivity.this, 连接成功, Toast.LENGTH_SHORT).show()); subscribeToTopic(); } Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { runOnUiThread(() - Toast.makeText(MainActivity.this, 连接失败, Toast.LENGTH_SHORT).show()); } }); mqttClient.setCallback(new MqttCallback() { Override public void connectionLost(Throwable cause) { } Override public void messageArrived(String topic, MqttMessage message) throws Exception { // 收到设备状态更新 String payload new String(message.getPayload()); updateUIWithStatus(payload); // 解析JSON更新界面显示 } Override public void deliveryComplete(IMqttDeliveryToken token) { } }); } catch (MqttException e) { e.printStackTrace(); } } private void subscribeToTopic() { try { mqttClient.subscribe(subscribeTopic, 1, null, new IMqttActionListener() { Override public void onSuccess(IMqttToken asyncActionToken) { Log.d(MQTT, 订阅成功); } Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { Log.e(MQTT, 订阅失败); } }); } catch (MqttException e) { e.printStackTrace(); } } private void sendControlCommand(int cmd) { try { // 构造Alink JSON格式指令 String message String.format( {\id\:\%d\,\version\:\1.0\,\params\:{\curtain_control\:%d},\method\:\thing.service.property.set\}, System.currentTimeMillis(), cmd); MqttMessage mqttMessage new MqttMessage(message.getBytes()); mqttMessage.setQos(1); mqttClient.publish(publishTopic, mqttMessage, null, new IMqttActionListener() { Override public void onSuccess(IMqttToken asyncActionToken) { runOnUiThread(() - Toast.makeText(MainActivity.this, 指令发送成功, Toast.LENGTH_SHORT).show()); } Override public void onFailure(IMqttToken asyncActionToken, Throwable exception) { runOnUiThread(() - Toast.makeText(MainActivity.this, 指令发送失败, Toast.LENGTH_SHORT).show()); } }); } catch (MqttException e) { e.printStackTrace(); } } // ... calculatePassword() 和 updateUIWithStatus() 方法实现略 }5. 常见问题与排查思路在开发调试过程中你可能会遇到以下问题问题现象可能原因排查步骤与解决方案ESP8266无法连接WiFi1. SSID或密码错误。2. 路由器设置了MAC过滤或隐藏SSID。3. 模块供电不足。1. 用串口调试助手发送ATCWJAP?查看已连接网络或用ATCWLAP扫描周围WiFi确认。2. 检查路由器设置确保是2.4GHz网络。3. 确保ESP8266的VCC接3.3V稳定电源电流足够300mA。ESP8266连接云平台失败1. MQTT连接参数ClientId、Username、Password错误。2. 设备三元组ProductKey等填写错误。3. 网络问题无法解析云平台域名。1. 逐一核对AT指令中的参数特别是DeviceSecret生成的密码。2. 登录云平台控制台确认设备状态是否为“未激活”。3. 尝试用ATPING\iot-as-mqtt.cn-shanghai.aliyuncs.com\测试网络连通性。APP能发指令但设备无反应1. STM32未正确解析MQTT消息。2. ESP8266未将消息通过串口转发给STM32。3. 电机驱动电路或电机本身故障。1. 在STM32端将ESP8266串口收到的所有数据打印出来查看是否收到正确的JSON指令。2. 检查STM32的串口接收中断和缓冲区处理逻辑。3. 用万用表测量电机驱动模块输出端电压或直接给电机驱动输入信号测试电机。设备状态无法同步到APP1. STM32未上报状态。2. ESP8266上报的MQTT主题或格式错误。3. APP未正确订阅主题。1. 检查STM32中Report_Status函数是否被调用并通过串口查看其发送的AT指令。2. 在云平台控制台的“设备日志”或“实时日志”中查看是否有属性上报记录。3. 检查APP中订阅的主题字符串是否与设备上报的主题完全一致。电机堵转或行程不准1. 限位开关安装位置不当或接触不良。2. 电机扭矩不足或窗帘轨道阻力过大。3. 程序中没有加入软件去抖或防重复触发逻辑。1. 手动触发限位开关用STM32 GPIO读取函数检查电平变化是否正常。2. 更换更大扭矩的电机或润滑轨道。3. 在限位开关检测中断或循环扫描中加入延时去抖。6. 最佳实践与工程建议电源设计电机12V与单片机3.3V/5V务必使用独立的电源或通过大功率DCDC隔离防止电机启停对单片机造成干扰复位。ESP8266模块对电源纹波敏感建议在其VCC引脚附近并联一个100uF的电解电容和一个0.1uF的瓷片电容。通信可靠性心跳包与重连机制在STM32程序中定时如每30秒通过ESP8266向云平台发送一个简单的心跳包如空属性上报并在检测到网络断开时自动尝试重连。AT指令应答超时与重发实现ESP8266_SendCmd函数时必须加入超时判断。如果未收到“OK”或期望的应答应进行有限次数的重试。JSON解析在STM32端建议使用轻量级的JSON解析库如cJSON避免使用strstr进行简单的字符串匹配以提高解析的准确性和健壮性。固件升级与维护预留调试接口除了程序本身的调试信息可以预留一个通过串口接收简单命令如设置WiFi的接口方便现场维护。考虑OTA对于正式产品可以考虑通过云平台进行固件无线升级OTA。ESP8266本身支持OTA需要规划好固件分区和升级流程。安全性三元组保密设备的三元组ProductKey, DeviceName, DeviceSecret是设备身份凭证严禁硬编码在明文中。对于量产产品应使用芯片唯一ID进行加密存储或通过安全烧录方式写入。通信加密MQTT over TLSSSL是更安全的选择但会占用更多资源。对于家庭环境使用平台提供的基于密码的鉴权通常已足够。用户体验状态反馈任何控制指令发出后APP都应有一个明确的等待或加载状态直到收到设备的状态上报确认后再更新界面。本地优先确保本地按键控制的响应优先级高于网络指令即使在断网情况下设备的基本功能依然可用。异常处理在APP和设备端都要考虑网络异常、指令超时等情况给出友好的提示而不是让用户无休止地等待。7. 总结与扩展方向通过本文我们完成了一个从硬件到软件、从设备端到云平台和手机APP的完整智能窗帘系统。你掌握了STM32与ESP8266的串口通信、MQTT协议在物联网中的应用、阿里云物联网平台的基本操作以及Android APP与云端的交互。核心掌握点STM32作为主控处理外设按键、限位、电机和业务逻辑。ESP8266作为网络透传模块通过AT指令连接WiFi和MQTT。云平台作为消息中转和规则引擎解耦了设备与APP。MQTT的发布/订阅模式是实现设备与云端双向通信的有效手段。下一步可以探索的扩展方向增加传感器集成温湿度、光照传感器实现“光线过强自动关帘”、“室内过热自动开帘通风”等自动化场景。语音控制将设备接入天猫精灵、小爱同学等智能音箱平台。场景联动在云平台设置规则引擎实现“离家模式”自动关闭所有窗帘。多设备组网如果家里有多个智能窗帘可以通过一个APP统一管理。低功耗设计如果使用电池供电需要深入研究STM32的低功耗模式并选用支持低功耗的WiFi模块如ESP32的Deep-sleep模式。智能家居的开发是一个软硬件结合的综合性工程这个项目为你提供了一个绝佳的起点。建议你亲手焊接一块电路板将代码烧录进去体验从零到一创造的乐趣。遇到问题时善用串口调试工具打印日志分层排查一定能找到解决方案。