STM32智能晾衣架实战:传感器采集与步进电机状态机控制

📅 发布时间:2026/9/10 11:39:28
STM32智能晾衣架实战:传感器采集与步进电机状态机控制
简介基于STM32单片机的智能晾衣架项目源码与配套资料定位于毕业设计、课程设计及嵌入式入门进阶场景。项目经完整测试运行成功可直接作为演示或二次开发基础适合电子信息、物联网、自动化等专业学生参考。资源包共53个文件容量约130KB。核心为C语言工程与头文件配合Keil的uvproj/uvopt工程配置和hex烧录文件可快速复现功能。另有编译过程生成的obj、lst文件便于调试定位教程文档和README则梳理了整体设计与使用要点目录按CORE、SYSTEM、USER、HARDWARE模块组织结构与常见STM32工程一致容易上手。目前已有245人学习下载。通过解压可获得完整工程源码、编译产物、教程文档及模块划分说明既能直接用于课程报告或答辩演示也便于结合实际需求修改晾衣架控制逻辑是兼顾完成度与学习价值的参考资源。1. 梅雨天里跑通的一套 STM32 智能晾衣架做这套东西的起因特别朴素阳台晾着衣服下午突然变天下雨等到下班回家已经白晾一场。后来就把一直在吃灰的 STM32F103C8T6 最小系统板翻出来加了个雨滴传感器、光敏电阻模块、一个步进电机和手头能找到的型材支架做成了一款能自动收晾衣服的智能晾衣架。这份资料就是把完整源码、教程文档和项目文件打包在一起的东西拿到手不是只有代码还包括从硬件接线到主循环逻辑的完整说明GitHub 上三个人就敢叫开源项目的现在这种资料算是比较难得的闭环。这套东西适合正在做毕业设计、课程设计的电子信息与物联网方向学生也适合想把手头 STM32 板子玩出实际项目的工程师。开发环境是 Keil MDK5 ST 标准外设库没有依赖 CubeMX 生成的代码模块之间的调用关系非常透明改起来不费劲。和那些只能点灯的教程项目最大区别在于它有完整的传感器采集、电机执行、状态机切换和手动干预逻辑是一个真正能放在阳台上跑的嵌入式小系统。2. 从硬件选型到源码结构为什么是 F103C8T6 加三段式布线2.1 主控与外设选型先看主控。STM32F103C8T6 在同类毕业设计里出现的频率高是有道理的72MHz 主频、20KB RAM、64KB Flash跑这套晾衣架逻辑绰绰有余而且 LQFP48 封装手工贴片不困难。关键是整个 STM32F1 生态太成熟了哪怕后续要加 ESP8266 做远程控制F103 也能腾出两个串口和一个定时器资源。传感器方面雨滴检测用的是 LM393 比较器输出的雨滴传感器模块这种模块正面是叉指电极背面是 LM393 比较器输出引脚有两种信号DO 是数字电平阈值可调AO 是模拟电压。我一般取模拟量因为数字量的阈值由模块上的电位器决定每次换环境都要重新拧不方便取模拟量到 ADC阈值改代码就行。光敏检测用带光敏电阻的 LM393 模块同样输出 AO/DO我这里是两个引脚都接了AO 进 ADC 算光照强度DO 留作备用。电机驱动这块要单独强调。市面上智能晾衣架资料里最常见的两个方案一是 28BYJ-48 步进电机加 ULN2003 驱动板二是直流减速电机加 L298N/DRV8833 驱动板。我手上这份资源用的是步进电机方案原因有两点步进电机可以精确控制晾衣杆伸出收回的位置不需要额外加限位开关就能做到固定行程ULN2003 驱动板便宜且不容易烧。代价是 28BYJ-48 转速慢、扭矩一般晾晒较重衣物时表现吃力。2.2 整套目录结构的拆解拿到资源包解压后第一件事不是打开 main.c而是先把目录结构过一遍。首层有 stm32-master、智能晾衣架CORE、CORE、SYSTEM、USER、HARDWARE 这几个目录其中 stm32-master 是资料的根目录智能晾衣架CORE 和顶层的 CORE 是同一层级工程的两种归档方式。实际开发关注 CORE、SYSTEM、USER、HARDWARE 这四个├── CORE │ ├── startup_stm32f10x_hd.s │ ├── core_cm3.c │ └── core_cm3.h ├── SYSTEM │ ├── delay │ ├── sys │ └── usart ├── HARDWARE │ ├── rain │ ├── light │ ├── motor │ ├── key │ └── oled ├── USER │ ├── main.c │ ├── stm32f10x_it.c │ └── system_stm32f10x.c └── OBJ └── 智能晾衣架.hex各自的职责分成三层来说第一层是启动与内核相关。CORE 下的 startup_stm32f10x_hd.s 是启动文件负责初始化堆栈、中断向量表最后调用 main 函数core_cm3.c 提供 NVIC、SysTick 等 Cortex-M3 内核接口的封装。这两部分基本不用动工程新建时拷贝进去即可。第二层是 SYSTEM 基础驱动。delay、sys、usart 这三个文件是每个模块的公共依赖delay 提供毫秒和微秒级延时usart 重定向 printf 到串口 1。调试时 printf 输出传感器原始值非常关键调阈值全靠它。第三层才是业务代码所在。HARDWARE 下的每个子目录对应一个硬件模块rain 是雨滴传感器、light 是光敏、motor 是步进电机、key 是按键、oled 是显示。USER 下的 main.c 把所有这些模块串起来完成状态机的流转。从这个目录结构能看出这套工程的模块化程度是到位的。每块外设的初始化函数、数据读取函数和操作函数都被收拢在对应的 .c/.h 文件里main.c 里不会有 GPIO 寄存器操作散落一地。如果要做二次开发加一个温湿度传感器只需要新建一个 dht11 目录在 main 里调用初始化函数然后到状态机里加一条分支即可对原有代码的影响面极小。2.3 资料完整度和 Keil 工程配置教程文档在资源包里是独立的一份其中有一章节专门讲 Keil 工程的配置重点就是三点芯片型号要选 STM32F103C8 而不是 CB宏定义要写 STM32F10X_HD因为 F103C8T6 虽然是中等容量但其 FLASH 是 64KB 起步很多资料按其引脚兼容性沿用了 HD 宏实际编译链接没问题下载器选 ST-Link 的话不要勾选 Reset and Run 以外的多余选项。一个常见的坑是新建工程时把启动文件选成 stm32f10x_md.s导致烧录后程序跑飞。原因是 C8T6 属于中等容量产品按理说该用 md 启动文件但这份资源沿用了 hd 文件配合 STM32F10X_HD 宏的配置组合。实测这个组合可以正常工作所以不要看到 C8T6 就自己改成 md动这两处之前先把教程文档的 2.3 节看完再决定。如果按资料里生成的 hex 文件直接下载能观察到上电默认状态是雨滴传感器未检测到水时晾衣杆保持停止用手沾水碰触传感器叉指电极电机开始反转收回晾衣杆。这个现象说明主程序、电机驱动和雨滴采集链路是通的可以进入下一步的代码走读。3. 核心驱动实现ADC 采集与步进电机八拍控制3.1 雨滴和光敏的 ADC 采集链路晾衣架的决策依据来自两个模拟量传感器。雨滴传感器的 AO 输出接到 STM32 的 PA1光敏模块的 AO 输出接 PA2。这两路都是模拟输入用 ADC1 的规则通道 1 和通道 2 交替采样。初始化代码在硬件模块初始化里统一做void Rain_Light_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这段是配置 ADC1 以独立模式、单次转换方式工作时钟由 APB2 提供分频系数取 672MHz/612MHz符合 ADC 最高 14MHz 的要求。每次读取一个通道前切换通道序号读取后返回 12 位数据。写法上没有开启扫描模式因为两个传感器不需要同步采样分时读取足够。雨滴数值的判读逻辑是这样的传感器模块没有水时AO 输出电压接近 VCCADC 采样值偏大通常在 2500 以上含水量越大AO 电压越低ADC 值越小。代码里把雨量判定阈值设在 800即 ADC 读数低于 800 认为雨水已覆盖电极。为什么不是更低因为雨滴传感器电极的表面张力会导致水膜残留晾干过程是渐变的阈值太低容易在下雨初期漏判。光敏的判定就反过来光照强时光敏电阻阻值小AO 电压高ADC 值大用 0~4095 映射 0~100% 光照强度低于 20% 判定为光线不足。uint16_t Get_ADC_Value(uint8_t channel) { ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); } uint8_t Rain_GetLevel(void) { uint16_t adc Get_ADC_Value(ADC_Channel_1); if (adc RAIN_THRESHOLD) return RAIN_ON; return RAIN_OFF; }采样时间取 239.5 个周期为了抑制传感器信号上的高频抖动。雨滴传感器直接暴露在空气中轻微风扰就会让电极上的水膜产生阻值波动短采样时间会把这种波动当成真实信号。实际测试下来一个采样周期约 20 微秒对整机响应没影响。3.2 步进电机八拍驱动与转向控制电机是 28BYJ-48 步进电机四相五线制减速比约 1/64步距角 5.625 度经过减速后输出轴每步约 0.088 度。配合 ULN2003 驱动板时IN1~IN4 连接到 STM32 的 PB0~PB3。它的控制核心是四相励磁信号的时序这里有单四拍和八拍两种驱动方式代码里用的是八拍因为八拍方式每一步的角度减半电机运行更平稳启动抖动更小。const uint8_t motor_steps[8] { 0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09 }; void Motor_Step(uint8_t direction, uint16_t speed) { static uint8_t step_index 0; for (uint8_t i 0; i 8; i) { if (direction MOTOR_FORWARD) step_index (step_index 1) % 8; else step_index (step_index 7) % 8; GPIO_Write(GPIOB, motor_steps[step_index] 4); Delay_Ms(speed); } }这段代码里最重要的是 motor_steps 数组和 direction 的加减索引。motor_steps 数组的每一位对应 PB0~PB3 的高低电平组合比如 0x08 二进制是 0000 1000即 PB3 高电平其余三个低电平此时 A 相线圈通电。数组从头到尾遍历一轮就是完整的四相八拍时序。direction 控制步进方向索引加一还是减一决定了电机正转还是反转从机械结构上看一个方向对应晾衣杆伸出另一个方向对应收回。速度参数 speed 是每一步之间的延时毫秒数数值越大转速越慢。本项目的默认值是 3ms对应输出轴转速约每秒 25 步左右也就是晾衣杆从完全收回到完全伸出大约需要 4 秒。这个速度比较保守暴晒或下雨时电机启停冲击小。如果嫌慢可以改成 1ms但要注意28BYJ-48 在 3ms 以下的高速时失步率会明显上升表现为电机会咔咔响但不转或者转到中途停住。3.3 电机行程控制与堵转问题晾衣杆要停在正确的位置就需要对行程做控制。资料里采用的是开环步数控制即代码记录一次完整伸出或收回过程中执行的总步数。具体实现是 Motor_Extend() 和 Motor_Retract() 两个函数内部用 for 循环调用 Motor_Step循环次数默认是 200参数默认值说明单次伸缩步数200 步对应晾衣杆约 300mm 行程步间延时3 ms决定电机转速雨滴判定阈值ADC 800小于该值认为是下雨光照判定阈值光照 20%低于该值不晾出200 步这个数怎么确定的测量所需行程换算成输出轴转的圈数乘以每圈步数即可。28BYJ-48 输出轴每圈约 4076 步64 细分 × 63.68395 步200 步大约就是 1/20 圈机械上移动约 300mm。真正拿回家装好后务必用直尺量一遍实际位移然后回代码里改这一个宏。堵转问题值得多说一句。开环步进方案没有编码器反馈如果晾衣杆被卡住电机仍在持续发脉冲时间稍长就容易烧驱动板。常见的保护方式是加电流检测或限位开关但这套资料没有这两样硬件。折中做法是每次电机动作前先把步数清零动作完成后延迟 200ms 再判断雨滴状态是否已变化如果动作后状态和预期不符该收回却仍检测到雨水或者该伸出却仍检测到下雨就认为是机械故障进入停止状态并蜂鸣器报警防止系统在故障状态下反复动作烧毁电机。三个参数在联调时都要根据现场环境重新标定下面第 4 章会详细说标定步骤。现在先看主逻辑也就是状态机是怎么把这些函数整合起来的。4. 状态机主循环自动收晾逻辑与手动干预4.1 状态定义与转换条件智能晾衣架的程序主体不是顺序执行的一次性流程而是一个循环扫描的状态机。系统一共有 4 个状态状态 IDLE 表示待机不执行任何电机动作状态 EXTENDING 表示正在伸出晾衣杆状态 RETRACTING 表示正在收回晾衣杆状态 FAULT 表示机械故障报警。状态迁移由雨滴状态、光敏状态和按键输入三者共同决定typedef enum { ST_IDLE 0, ST_EXTENDING, ST_RETRACTING, ST_FAULT } SysState; SysState current_state ST_IDLE; uint8_t clothes_out 0; // 晾衣杆当前位置1伸出 uint8_t auto_mode 1; // 自动/手动模式clothes_out 变量记录晾衣杆是伸出还是收回它比从电机状态推断更可靠因为电机可以在中途停止。auto_mode 为 1 时全自动运行为 0 时只响应按键手动控制。为什么要单独标一个当前模式位因为在手动模式下状态机必须完全忽略传感器输入否则按键收回的晾衣杆会被下雨与否的漫反射干扰造成误动作。自动模式下的转换逻辑如下表当前状态触发条件目标状态IDLE下雨RETRACTINGIDLE光线充足 且 未晾出EXTENDINGEXTENDING下雨 或 光线不足RETRACTINGRETRACTING晾晒条件恢复 且 未晾出IDLEFAULT手动复位按键IDLE表里这条「RETRACTING 恢复后回 IDLE 而不是直接 EXTENDING」是调出来的经验。收回过程中如果突然放晴就立刻伸出遇到阵雨天气电机会在几分钟内反复启停不仅整机晃动电机发热也快。所以在收回动作完成后必须回到 IDLE 等待至少 30 秒代码里用计时器计数才会重新评估晾晒条件。4.2 主循环中的状态机实现主循环代码长度不长但每个分支配对都很清楚。以自动模式为例int main(void) { Delay_Init(); USART1_Init(115200); Rain_Light_ADC_Init(); Motor_Init(); Key_Init(); OLED_Init(); while (1) { if (auto_mode) { uint8_t rain Rain_GetLevel(); uint8_t light Light_GetLevel(); uint8_t retract_cond (rain RAIN_ON) || (light LIGHT_DARK); switch (current_state) { case ST_IDLE: if (retract_cond clothes_out) { current_state ST_RETRACTING; } else if (!retract_cond !clothes_out) { current_state ST_EXTENDING; } break; case ST_EXTENDING: Motor_Extend(); clothes_out 1; current_state ST_IDLE; break; case ST_RETRACTING: Motor_Retract(); clothes_out 0; current_state ST_IDLE; break; case ST_FAULT: break; } } else { Key_Scan_Manual(); } OLED_ShowState(current_state, clothes_out, auto_mode); Delay_Ms(5); } }注意几个细节。首先是 retract_cond 这个变量的含义雨滴为 ON 表示有雨光照为 DARK 表示光线弱两者任一成立就要收回晾衣杆。这比单纯的「下雨才收」合理因为阴天或者傍晚光线不足时晾衣服干得慢还容易返潮不如直接收回。其次是 ST_EXTENDING 和 ST_RETRACTING 两个状态内部并不做条件判断只执行完整个行程再回到 IDLE这是为了避免半路被传感器状态反复横跳打断——比如收回过程中雨停了按条件应该继续收回还是立即伸出经验是执行完当前动作再说这样系统行为可预期。按键扫描在手动模式下走另一条路径。KEY1 切换 auto_mode 模式KEY2 手动伸出KEY3 手动收回。因为是 GPIO 外部中断加防抖所以 Key_Scan_Manual 里只轮询标志位void Key_Scan_Manual(void) { if (key2_pressed) { key2_pressed 0; if (!clothes_out) Motor_Extend(); clothes_out 1; } if (key3_pressed) { key3_pressed 0; if (clothes_out) Motor_Retract(); clothes_out 0; } }手动模式的一个细节是电机动作完成后不校验传感器状态而是直接把 clothes_out 置位。因为手动操作代表用户明确的意图即使这时正在下雨也应该以用户的操作为准。这个设计在知乎上看到过不少争议有人说应该加一个手动收回后禁止自动伸出的锁存实际挂在阳台上用才会明白加锁存之后偶尔会出现用户睡觉前手动收回、明早忘了解锁结果整个上午晾衣杆都不自动伸出的情况。我后来把锁存改成了 30 分钟自动恢复自动模式折中之后体验好很多。4.3 联调必看的串口输出与阈值调整程序在初始化时已经配置了串口 1 的 printf 重定向所以联调阶段直接打开串口助手看输出。正常情况下系统启动后每秒打印一行传感器状态和当前状态机状态。printf(RAIN_ADC:%d LIGHT_ADC:%d STATE:%d OUT:%d MODE:%d\r\n, rain_adc, light_adc, current_state, clothes_out, auto_mode);这行输出是调阈值的关键工具。用手触碰雨滴传感器观察 RAIN_ADC 数值变化范围用手机闪光灯贴近光敏模块观察 LIGHT_ADC 的变化范围两者的边界值记录下来取中间值写回宏定义。实测下来雨滴阈值取干燥值和湿润值的平均值附近比较可靠而光敏阈值受朝向影响极大面朝西的阳台下午阳光斜射和正午直射差异很大建议在一天内分三个时段采样再定。一个容易忽略的点是接线极性。雨滴传感器模块的 DO 输出是集电极开路结构没有上拉电阻的情况下默认电平不确定所以很多新手下第一次板子会读到随机的雨滴状态。资料里的接线图如果标注清楚了就没问题如果自己手焊务必在 DO 和 VCC 之间接一个 10k 上拉电阻或者干脆只接 AO 不接 DO项目代码只用了模拟量通道DO 不是必需的。5. 移植到自己板子上的关键技巧5.1 换引脚时怎么改最不容易出错手上的这块板子未必和资料里的引脚分配一致。资料默认接线是雨滴 AO 接 PA1、光敏 AO 接 PA2、电机 IN1~IN4 接 PB0~PB3按键接 PA0、PA3、PA4。如果板子引脚冲突需要换核心原则是传感器两个引脚必须在同一 ADC 外设的规则通道里。STM32F103C8T6 只有一个 ADC1它的可用通道是 PA0~PA7 和 PB0~PB1也就是说雨滴和光敏的模拟输入只能在这 8 个引脚里面选不能挪到 PB3 之类的地方。Motor_Init 里的 GPIO 配置也要跟着改。如果新板子用 PB5~PB8 控制 ULN2003除了改 GPIO_Pin_x 之外还要注意 GPIO_Write(GPIOB, ...) 这个操作是把整个 B 口低 16 位一起写用了其他引脚会互相覆盖。稳妥做法是把 Motor_Step 中的整口写入改成按位操作GPIO_WriteBit(GPIOB, GPIO_Pin_0, (motor_steps[step_index] 0x01) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_1, (motor_steps[step_index] 0x02) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_2, (motor_steps[step_index] 0x04) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_3, (motor_steps[step_index] 0x08) ? Bit_SET : Bit_RESET);每个 WriteBit 只影响对应引脚其他引脚电平不受干扰。代价是执行时间从一次总线写变成四次位写对 3ms 一步来说完全无感。5.2 用 OLED 把状态机搬到眼前源码里 OLED 显示不难移植关键是把信息组织好。我用 4 行显示第一行当前模式第二行系统状态第三行传感器原始值第四行晾衣杆位置。这样整机在阳台运行时不需要连着电脑就能观察各部分状态。OLED 的 I2C 引脚用的是 PB6/PB7这是 STM32F103 硬件 I2C 的官方引脚位软件模拟 I2C 实现的话也可以随意挪。注意资料里如果用的是软件 I2C那 OLED 引脚就可以随便换如果用了硬件 I2C 外设就只能在 PB6/PB7 上。调试时有几个典型的时序问题可以重点验证电机动作瞬间 OLED 是否闪烁或停更。这是步进电机占满 CPU 的常见病症。Motor_Step 里的 Delay_Ms(3) 是阻塞延时执行 200 步意味着 CPU 有 600ms 完全卡在电机驱动里OLED 自然刷新不了。解决方式是把电机步进分成多个批次每批次执行半步或者一步就出让 CPU 给 OLED 刷新一次或者干脆把 OLED 刷新放到电机执行完再刷效果上差别不大。5.3 验收清单与改进空间一条一条对着跑完这个项目才算真正到自己手里。上电按下复位键OLED 显示 IDLE 状态串口打印的传感器值和实际环境对应用手触碰雨滴电极电机收回把雨滴吹干且用强光照射光敏电机伸出按下 KEY1 切到手动模式雨滴无论怎么碰电机都不动作再按 KEY1 回到自动模式系统在 30 秒后恢复自动判断。再往后做有两个低成本的改进方向值得先试。一是用 EEPROM比如 AT24C02把三个阈值参数存下来换环境不用重新编译烧录长按按键进入校准模式直接调参二是给电机加一个霍尔限位把开环步数控制升级成闭环位置控制彻底解决失步导致的晾衣杆位置漂移问题。这两个方向在现有代码框架上都只是增加模块不动主流程。改动之前先把本章提到的四层结构理清每一步动作弄清楚驱动层在做什么后面怎么改都不会翻车。本文还有配套的精品资源点击获取