基于STM32与机智云的智能窗帘控制系统实现 做智能窗帘之前我一直觉得窗帘是家里最难“自动化”的东西。直到我拿一套 STM32 做主控再把设备接到机智云平台上才发现远程开关、定时闭合、光线联动这些功能其实都能稳定落地。这篇内容就是我当时从硬件选型到平台配置、从固件逻辑到联调排错的全过程复盘涉及 28BYJ-48 步进电机、ESP8266 模块、机智云数据点和 App 控制。如果你正在做物联网课设、电子设计竞赛或者纯粹想给家里装一套低成本智能窗帘这篇内容值得对照着看。这套系统的核心思路并不复杂STM32 负责电机、按键、传感器这些底层逻辑ESP8266 模块走 Wi-Fi 链路数据透传到机智云平台手机 App 通过云端下发开关和开合百分比指令。断网时手动按键和本机光感逻辑依然能工作不依赖云端这是我认为这套方案最舒服的地方。1. 项目概述与方案设计1.1 这套系统解决了什么传统电动窗帘一般就带个遥控器电源一通按一下转一下。时间久了你会发现三个痛点一是人不在家时没法远程把窗帘拉开夏天家里闷热还不通风二是定时开关很难做要么换成品窗帘电机要么加个可编程插座但对行程控制无能为力三是窗帘开一半这种粒度控制普通成品方案里属于“高级功能”价格直接翻倍。用 STM32 自己做一套控制系统刚好能把这三个问题全部覆盖。我在原型机上实现了手机 App 远程控制电机正转、反转、停止支持 0% 到 100% 的开合百分比调节两个限位开关做硬到位保护软件行程计数做软限位光照传感器联动模拟“天亮自动打开窗帘”支持机智云 App 的云端定时功能意外断电后重新上电窗帘位置可以恢复。整个硬件成本控制在 80 元以内不含外壳和导轨配件比较适合作为智能家居入门项目来学习。1.2 系统组成与数据流先看数据流方向你就能理解后面每一个模块为什么存在。控制链路是手机 App → 机智云云端 → 路由器 → ESP8266 模块 → STM32 串口 → 电机驱动 → 步进电机 → 窗帘导轨。反馈链路是限位开关 / 光照传感器 / 电机转速计数 → STM32 状态计算 → 串口 → ESP8266 → 机智云 → App 界面。这里要注意两点。第一ESP8266 并不是直接通过 AT 指令控制电机它只负责把云端的命令原样打包成机智云定义的帧格式再通过串口转发给 STM32。STM32 解析数据帧后才去操作电机。这种“云模块只做透明传输、MCU 做决策”的架构优点是逻辑集中以后换一个 WiFi 模块最多只需动串口驱动机械动作和传感策略不需要重写。第二机智云平台上有两种典型接入方式SoC 方案传感器和逻辑直接写在 ESP8266 里和 MCU 方案ESP8266 使用官方 GAgent 固件配合外部 MCU。我选的是 MCU 方案因为 STM32 的资源、外设、调试手段都比 ESP8266 更丰富而且 MCU 端可以保留手动按键完全不依赖网络。1.3 为什么是 STM32 而不是其它方案我画过一张对比表列了三种常见实现方式做选型时你可以直接参考方案开发难度稳定性扩展性成本ESP8266 直接接继电器/电机低较差网络波动时逻辑混乱弱IO 紧张最低Arduino/ESP32 做主控 云平台中中等中等低STM32 做主控 独立 WiFi 模块中高高可保留本地手动逻辑强可扩展传感器和总线中实际做下来我的感受是ESP8266 直驱方案不是不行而是 GPIO 中断、PWM、I2C、EEPROM 这些资源全挤在一颗 ESP8266 上固件稍微复杂一点就很容易出问题。ESP32 虽然资源多但用在这种小项目上有点浪费而且如果我后续要接 485 总线电机、加语音模块STM32F103 的外设接口明显更从容。所以最后定为 STM32F103C8T6 板卡搭配 ESP8266-12F 模块接机智云。1.4 功能需求拆解红点在手工开清单时经常会把“智能”两个字想得太复杂。我整理项目时只把需求拆成下面四类执行类开窗帘、关窗帘、停止、开合百分比传感类光照值检测、左右限位检测状态类当前位置百分比、当前电机状态、在线状态策略类光感联动阈值、云端定时任务。每一个需求最终都会映射到机智云数据点里也映射到 STM32 的某个变量或函数上。需求清单越清晰后面写固件和定义数据点就越顺不用反复改协议。2. 硬件器件与电路搭建2.1 主控、WiFi 模块与电平匹配主控我用的是一块常见的 STM32F103C8T6 核心板也称蓝丸。它自带 USB 转串口下载调试方便片上资源足够。资源分配如下USART1PA9/PA10给 ESP8266 模块使用波特率 9600USART2PA2/PA3留作调试串口接 USB 转 TTL打印日志PB0、PB1接步进电机驱动模块的 IN1、IN2实际上我用的是四个 IO下面再说PB4、PB5接两个限位开关PB6、PB7接 BH1750 光照传感器的 I2CPA8接蜂鸣器或者 LED 指示。这里有一个初学者最容易踩的坑不要把 WiFi 模块 TX/RX 直接接到调试串口也不要让两个串口输出混在同一个软件串口调试工具里。我刚开始把 ESP8266 和 STM32 的连接暂时拆掉单独用 USB 转 TTL 测试 ESP8266 的固件版本后来发现如果 PA9/PA10 和板载 USB 串口同时使用会出现电平冲突导致 WiFi 模块乱发数据。ESP8266-12F 模块的逻辑电平是 3.3VSTM32F103 的 IO 也是 3.3V所以二者直接相连没有电平问题。但电机驱动模块如果是 5V 逻辑就不能直接把 STM32 引脚接到 5V 驱动的输入端需要用光耦或在中间串电阻做电平变换。我选 ULN2003 驱动模块输入可以兼容 3.3V所以免去了电平转换。2.2 电机驱动与减速机构选择这里要根据你的应用场景来选。如果是演示原型用 28BYJ-48 步进电机 ULN2003 最大众化成本十几块力矩在低速时表现还可以如果是实际挂窗帘建议直接用 24V 或 12V 直流减速电机配一颗 HQ 桥驱动芯片或成品驱动模块。我后来在测试中又碰到一个问题28BYJ-48 是四相五线步进电机驱动方式需要对四个 IO 顺序送脉冲。如果用 HAL 库的 GPIO 翻转来做频率一高系统就很容易被中断淹没因为每走一步要翻转好几次引脚。于是我把它改成“固定半步序列查表 定时器中断更新”的方式主循环只负责设置方向和目标位置中断里每 2ms 输出一组相序走起来才稳定。如果你选的是直流减速电机则更简单两根 PWM 信号控制正反转再用两路限位开关或霍尔编码器检测位置。对于真实窗帘直流减速电机在力矩和噪音方面比 28BYJ-48 要好用得多而且不会出现步进电机失步累计误差的问题。我这边因为课程演示需要最终保留了 28BYJ-48 版本但代码架构上把电机驱动封装成了独立模块换电机时只需改写motor_ctrl.c上层逻辑基本不动。2.3 电源与抗干扰设计电源设计是整个系统最容易影响稳定性的环节。步进电机启动瞬间电流可能到 200mA 甚至更高如果和 STM32 共用一个 5V 稳压输出很容易造成主控复位。我实际测试中电机一动屏幕就重启排查后确认是电源跌落导致。后面我改用两路供电主板用 USB 或单独 5V 适配器供电电机部分用另一路 5V 或 12V 电源两组电源的地线在单点汇合。电机驱动控制信号属于弱电控制强电如果控制信号线比较长还应该在 ULN2003 输入端对地并联 10kΩ 下拉电阻防止 STM32 上电瞬间 IO 悬空导致电机误动。光照传感器 BH1750 需要 3.3V 供电I2C 上拉电阻我用了 4.7kΩ正常读取环境光照没问题。实际安装在窗边时发现阳光直射下读数很容易饱和我就在传感器外面加了半透明塑料罩并把采集时间间隔设成 3 秒一次再取滑动平均值数值稳定很多。2.4 接线清单与电路设计要点我把布线总结成下面几张表方便照着搭模块引脚STM32备注ESP8266 RXPA9模块 TX 接 MCU RX交叉连接ESP8266 TXPA10模块 RX 接 MCU TX不要接反ULN2003 IN1PB0电机相序 1公共端接电源ULN2003 IN2PB1电机相序 2ULN2003 IN3PB10电机相序 3ULN2003 IN4PB11电机相序 4左限位PB4触发接地开启内部上拉右限位PB5触发接地开启内部上拉BH1750 SDAPB7I2C 数据上拉 4.7kBH1750 SCLPB6I2C 时钟上拉 4.7k硬件搭完之后一定要先测裸板给 STM32 烧一个简单的点灯程序确认 GPIO 能正常拉高拉低再用万用表量电机驱动输入端的电平变化。不要一上来就把所有模块接齐否则出了问题不好定位。3. 机智云平台配置与设备接入3.1 创建产品与数据点定义机智云的开发者后台是我用过比较顺手上手的。在“产品开发”里创建一个新产品设备接入方式选择“Wi-Fi”方案选“MCU 对接方案”系统会自动帮你把 GAgent 固件相关文档列出来。创建完成后第一个工作是定义数据点。数据点是云端与设备通信的最小单元类似数据库字段。我的项目定义了六个数据点数据点名称标识名读写类型数据类型取值范围备注窗帘开关curtain_switch可写布尔0/10 关1 开开合百分比curtain_percent可写数值0-100目标位置运行模式auto_mode可写枚举0/1/2手动/光感/定时当前位置cur_position只读数值0-100上报给 App当前状态run_state只读枚举0/1/2停止/运行中/到位光照强度light_lux只读数值0-2000只读数据定义数据点时有几个经验布尔类型不要设太多因为 App 端开关样式不如枚举直观。状态类数据点尽量用只读属性否则 App 端一旦下发很容易让 MCU 收到意外指令。百分比尽量在 MCU 端计算好再上报不要把电机步数直接上报这样 App 展示更友好。3.2 虚拟设备调试与协议自查平台里有一个“虚拟设备”功能在没有真实硬件时也可以调试。你可以给虚拟设备下发命令它会返回设备应该上报的数据格式。我把虚拟设备当成“云端假设备”来用先把每个数据点的下行命令格式记录下来再去比对 STM32 串口应该解析到的帧格式。这一步帮我提前定位了一个问题初始时我把curtain_switch定义为“可写”的枚举但枚举值在 App 端显示的是中文列表发送给设备时实际传输值和显示值可能不一致。后来我把所有枚举的起始值从 0 开始并且在固件里用宏定义做好映射才避免“App 显示开启、实际设备收到关闭指令”这种坑。虚拟设备调试通过后再去烧真实设备固件成功率会大幅提升。调试时注意看机智云后台的“设备日志”它会记录每条设备上行、下行消息很多通信问题在这里就能看出来。3.3 烧录 GAgent 固件到 ESP8266这是“机智云版”方案里比较关键的一环。裸的 ESP8266 模块如果没有机智云固件是不能直接和云端通信的。你需要在机智云官网下载对应型号的 GAgent 固件然后用烧录工具把它烧进 ESP8266。这个过程和普通 AT 固件刷机类似但需要注意固件版本必须与你的模块 flash 容量匹配我用的 ESP-12F 是 4MB选 4MB 固件烧录时 GPIO0 要拉低进入编程模式如果你买的模块带了 USB 转串口直接按 Boot 键下载即可烧录成功后串口波特率默认是 9600此时模块会输出特定格式的日志或等待 MCU 下发查询命令。我一开始忽略了波特率用 115200 去连 ESP8266串口全是乱码后来查了机智云文档才发现 GAgent 串口通信带宽是 9600。ST 端主循环配置成 9600、8N1 后就能正常对包。这里提醒你如果你购买的是已经刷好机智云固件的模块商家可能会用不同版本新拿到的模块应先用一个测试程序让它上电打印日志确认波特率。3.4 配网流程设备上电后需要让 ESP8266 知道要连接哪个 Wi-Fi。机智云支持两种配网方式AirLink 和 SoftAP。AirLink 模式比较方便App 会把 Wi-Fi 名和密码编码成声音或广播包模块听到后自动连接。SoftAP 模式则让模块自己开热点手机连上去填密码。我在 STM32 里做了一个按键短按是手动控制电机长按 5 秒进入配网模式。长按后 MCU 会通过串口向 ESP8266 发送进入配网的命令LED 转为闪烁这时打开机智云 App 选择“添加设备”配网完成后 LED 常亮App 上就能看到设备在线。配网失败率比较高通常和路由器有关。我实测可以发现大部分家用路由器双频合一模式下手机如果连的是 5G 频段配网经常超时因为 GAgent 收到的广播包在 2.4G 上。建议调试时先关掉路由器双频合一或者把手机切换到 2.4G SSID 再配网。4. STM32 固件开发与核心逻辑实现4.1 工程结构与串口协议解析固件我基于标准外设库写的虽然 HAL 库也是主流但标准外设库上手快、代码量更少。工程文件按模块划分main.c系统初始化、主循环wifi_ctrl.c负责与 ESP8266 串口收发、缓存帧数据gizwits_protocol.c解析机智云数据帧motor_ctrl.c步进电机动作目标位置管理bh1750.c光强读取flash_store.c掉电状态保存。与 ESP8266 通信的串口使用中断接收中断里把每个字节塞进一个环形缓冲区主循环再从缓冲区中查找帧头。机智云协议帧一般以0xFF 0x55开头后面是命令字、事件号、数据长度、数据区和校验。我不建议在中断回调里做复杂解析因为串口数据量不大完全可以放到主循环慢慢来。下面是一段简化的解析框架void wifi_uart_irq_handler(uint8_t byte) { ring_buffer_push(rx_buf, byte); } void gizwits_task(void) { uint8_t data[256]; int len; len ring_buffer_pop_frame(rx_buf, data); if (len 0) return; if (data[0] 0xFF data[1] 0x55) { uint8_t cmd data[2]; uint8_t event data[3]; uint16_t dlen (data[4] 8) | data[5]; parse_payload(cmd, event, data[6], dlen); } }实际处理中还要做校验和判断确保帧没有在传输中被破坏。如果校验失败简单丢弃这个帧即可不要尝试恢复因为 GAgent 有自己的超时重发机制。4.2 数据点下发处理当 App 下发一条命令STM32 解析出数据点后应该调用对应的动作函数。举个例子void parse_data_point(uint8_t action, uint8_t *payload, int len) { switch (action) { case ACTION_CURTAIN_SWITCH: if (payload_len 1) { if (payload[0] 1) { curtain_target_percent 100; } else { curtain_target_percent 0; } motor_run(); } break; case ACTION_CURTAIN_PERCENT: if (payload_len 2) { curtain_target_percent (payload[0] 8) | payload[1]; if (curtain_target_percent 100) curtain_target_percent 100; motor_run(); } break; default: break; } }这里有个容易忽略的问题机智云数据点的数值类型在串口协议里可能是小端字节序也可能是大端字节序。一定要先对照上位机调试工具打印的数据确认高字节在前还是低字节在前。我在这上面栽过一次App 设定 50%设备端却解析成 12800电机直接朝一个方向跑到底并触发限位停止。4.3 步进电机行程控制与百分比换算我们用的 28BYJ-48 步进电机在 4 相驱动和 1/64 减速比下内部转子转一圈需要 2048 个脉冲输出轴转一圈则需要 32 圈电机转子大概 65536 步。但这只是电机本身的参数实际窗帘行程还需要结合导轨和卷绳轮直径换算。我在原型系统里的计算方式是先用定时器输出固定频率的 PWM 脉冲给 ULN2003然后数脉冲数。假设电机输出轴每转一圈可以带动窗帘移动 10cm导轨全行程是 1.2 米那么总脉冲数就是总脉冲数 全行程距离 / 每转行程 × 电机每转脉冲数 1200 / 100 × 65536 ≈ 786432 步这个数字看着大但实际操作中 28BYJ-48 速度很慢全行程跑下来要好几分钟。如果按 400Hz 脉冲频率走完约 2000 秒完全不符合实际使用体验。所以我在演示项目里把导轨行程缩短到 30cm速度调快到 1kHz跑完约 15 秒可以接受。如果你做的是真实门窗我更推荐用 12V 或 24V 的直流减速电机速度可以做到几十转每分钟位置检测用霍尔编码器或者限位开关做归零不要纯靠开环步数。固件里的位置换算逻辑#define TOTAL_STEPS 10000 // 标定后的实际总行程步数 int32_t current_steps 0; // 当前位置 // 百分比目标转换为步数 int32_t target_steps(int percent) { if (percent 0) percent 0; if (percent 100) percent 100; return (int32_t)TOTAL_STEPS * percent / 100; } // 上报当前位置百分比 int current_percent(void) { return (int)((int64_t)current_steps * 100 / TOTAL_STEPS); }如果只写 OPEN 和 CLOSE 两种状态就不需要标定行程但做了百分比调节后标定 TOTAL_STEPS 是必须的。我的标定流程是开机后先让电机反转到左限位停止后将当前步数记为 0再正转到右限位记下总步数把总步数写入内部 Flash。这样下次开机就知道整个行程的长度。4.4 限位保护与异常处理限位开关是硬件保护的最后一道防线。我将两个限位开关接在 PB4、PB5开启内部上拉开关另一头接地当触发导通时引脚被拉低。电机运行逻辑里每次移动前和移动中都会检查对应方向的限位void motor_task(void) { if (motor_dir DIR_OPEN is_right_limit_triggered()) { motor_soft_stop(); current_steps TOTAL_STEPS; } if (motor_dir DIR_CLOSE is_left_limit_triggered()) { motor_soft_stop(); current_steps 0; } }除了限位我还加了一个超时保护如果电机持续运行超过 40 秒还没有触发限位直接停止并上报故障状态。这个超时时间比理论行程时间多 1.5 倍既能覆盖正常行程又能防止因传感器损坏电机一直转。4.5 手动按键与本地策略按键控制不能完全依赖网络我当时设计了三套本地逻辑短按按键 1开窗帘短按按键 2关窗帘短按按键 3停止长按按键 1进入配网模式。按键扫描放在主循环采用 20ms 消抖。每次按键动作除了执行电机控制还要主动向机智云上报当前状态这样 App 端不会因为本地操作后状态不同步而显示错误。如果启用了光感模式主循环每 3 秒读取一次 BH1750。当光照值大于设定阈值时向开合方向移动 10%小于阈值时向关闭方向移动 10%。这种步进式调节比一刀切全开全关体验好很多而且能避免光照临界点时反复开关。4.6 状态上报与定时任务机智云 App 里的定时任务实际上是在云端执行的。你设定“每天早上 7 点打开窗帘”云端到了时间会向设备下发一条打开命令。所以 MCU 端不需要存储定时规则也不需要实时时钟。这个设计非常省心。需要做的是MCU 每次收到命令后把执行结果以数据点形式上报App 端才能显示正确。上报逻辑可以设计成一个统一的函数void report_status(void) { uint8_t buffer[64]; int len 0; buffer[len] 0xFF; buffer[len] 0x55; buffer[len] CMD_ACTION_REPORT; buffer[len] 0x00; // 填充当前数据点cur_position, run_state, light_lux... // ... // 发送到串口 HAL_UART_Transmit(huart1, buffer, len, 100); }上报频率要控制我一般只在状态变化时上报光照值如果变化不明显则不报。机智云后台对设备消息频率有限流过于频繁上报会造成丢包甚至封设备。5. 整机联调、常见问题与避坑指南5.1 联调顺序硬件和固件都完成后不要直接全部接上去联调按照下面的顺序来问题定位会快不少。第一步先把 ESP8266 单独接到 USB 转 TTL上电观察打印日志确认 GAgent 固件正常。第二步STM32 先不接 ESP8266使用串口助手模拟机智云协议直接向 STM32 发送手动控制帧。这样可以验证电机方向、行程和限位逻辑。第三步再接上 ESP8266让 STM32 和模块通信使用机智云 App 配网。如果 App 控制不了优先看 STM32 调试串口打印的日志确认收到的数据是否被解析。调试日志里我会打印三类信息串口原始帧、解析后的数据点值、电机状态变化。打印原始帧能看出字节序和校验问题打印数据点值能判断是不是解析错误打印电机状态能判断执行逻辑是否有 bug。打印尽量用简洁格式不要一次打印太多否则会打断主循环的实时控制。5.2 常见问题速查表下面这张表我用的频率非常高几乎涵盖了做这套系统时可能遇到的大部分问题问题现象可能原因处理思路App 设备列表看不到设备配网未成功 / 固件不对改用 SoftAP检查路由器 2.4G 频段设备在线但控制无响应数据点定义不相符 / 串口波特率不对查看 STM32 调试串口对比协议格式光照值一直是 0 或饱和I2C 地址或上拉电阻问题检查 SDA/SCL实测上拉电阻电机一直往一个方向跑限位开关未触发 / 软限位判断逻辑错误检查限位电平单步调试运动任务开合百分比不准确总行程未标定重新进行行程标定保存到 Flash电机一启动 MCU 复位电源跌落改为独立供电增加滤波电容App 操作后实际电机不动数据点类型错误 / 字节序错误用虚拟设备对比命令格式调整解析5.3 避坑经验电源、地线、IO 状态关于电源我再强调一次。我一个同学按同样的方案做电机一动STM32 就重启最初他还以为是软件死循环。把两块板子都串上万用表才发现电机启动瞬间电流把 5V 电压拉低到 3.9VMCU 供电自然不稳定。改完独立供电后现象立刻消失。关于地线STM32 和电机驱动的控制信号必须共地否则电机端参考电位和主控端不一致ULN2003 的输入端会呈现随机电平非常容易误动作。共地方法很简单就是电源模块的负极和 STM32 的 GND 连在一起但连接点尽量选在电源输出端避开电机电流回路。关于 IO 初始状态STM32 上电后到初始化这段时间GPIO 默认一般是高阻态。如果电机驱动输入没有下拉电阻IO 悬空可能让电机短暂抖动。处理方法是初始化 GPIO 时先拉低所有电机控制引脚再初始化时钟和定时器。5.4 掉电保存与恢复位置真正装在窗帘上之后最容易被用户吐槽的功能就是断电后再开机窗帘不知道自己在哪里需要重新开关一次才能恢复。我在固件里做了内部 Flash 保存每 1% 位置变化就同步写入一个备份变量。保存时不能频繁擦除 Flash否则寿命很快耗尽我采用的策略是只在断电检测到或每累计 10% 变化时才写入。STM32F103 内部 Flash 写入必须按字4 字节操作且写入前需要擦除扇区。我留了最后一个扇区专门存放配置信息内容包含魔数、总步数、当前位置等。上电时先读魔数如果非法就认为没有标定过强制进入一次找限位的初始化流程。#define CONFIG_FLASH_ADDR 0x0800FC00 typedef struct { uint32_t magic; int32_t total_steps; int32_t current_steps; } sys_config_t; void config_load(sys_config_t *cfg) { sys_config_t *p (sys_config_t *)CONFIG_FLASH_ADDR; if (p-magic CONFIG_MAGIC) { *cfg *p; } else { cfg-magic 0; cfg-total_steps 0; cfg-current_steps 0; } }如果你的系统里有相机或需要频繁记忆状态建议外接一个 I2C EEPROM比如 AT24C02写入次数更多代码也更简单。6. 一个值得继续扩展的方向这个项目做到能稳定远程开关后我把它挂在客厅窗户上用了两周。整体控不如期间遇到过两次配网后设备掉线查到原因是路由器自动切换频段改成固定 2.4G 后就很稳定了。我体会到真正拖进度的不是电机控制也不是云端配置而是工程化的细节限位开关的选择、电源抗干扰、行程标定的重复性。如果你想在上面继续玩我建议优先做两件事。第一把电机驱动换成带霍尔编码器的直流减速电机做闭环位置控制这样窗帘开合精度能精确到毫米级并且不怕偶尔用手拉一下的扰动。第二接入语音助手把设备绑定到智能音箱通过语音命令控制开合百分比这比手机 App 更符合日常使用习惯。还有一个小改进我后来把光照传感器从单点检测改成两个传感器分别放在窗帘两侧一个测室内光一个测室外光。这样阴天和晴天判断更准联动策略可以写成“室外光照高于室内且高于阈值时让窗帘打开反之关闭”。这个改动不需要改硬件太多只是 I2C 要加一个地址切换或者用两路 I2C固件里多一个数据点。做这套系统最关键的一点是先把控制链路打通再慢慢折腾精度。如果你现在手里也攒了一套类似硬件建议先接上电机和限位开关把行程跑通再去接云平台。如果一开始就把所有模块接齐出问题的时候你会分不清到底是网络、协议还是硬件的问题。