STM32F103智能晾衣架控制系统设计:硬件、状态机与代码详解 📅 发布时间:2026/9/1 19:05:02 👁 浏览次数: 刚做完一个基于 STM32F103 的智能晾衣架控制系统想把这套完整的设计思路、硬件选型、软件状态机和核心代码一次性整理出来分享给做单片机项目的同学。先给结论STM32F103 做这种中小型智能家居控制器完全够用而且生态成熟、资料多、成本低。真正决定项目上限的不是主控性能而是你的系统设计思路——晾衣架控制看起来简单无非是电机正反转、传感器判断、按键输入但一旦把“自动收晾”“防误触发”“保护停机”“低功耗待机”这些真实需求叠加进去代码就不再是点个灯、转个电机那么简单。这篇文章会从项目规划、硬件架构、软件状态机、关键代码实现、调试排错到开源发布规范完整过一遍。无论你是准备做课设、做毕设还是想给家里阳台做个实用的小改造这篇文章都能提供一个可以落地的参考方案。1. 为什么选 STM32F103 做智能晾衣架先聊聊主控选型。有人会问晾衣架控制逻辑这么简单用 51 单片机或者 Arduino 不就行了为什么非要 STM32F103这个问题的答案其实也是很多嵌入式项目选型时的通用判断逻辑。51 单片机成本低、上手快但片上外设资源有限。一个稍完整的自动晾衣架系统大概率要同时用到 GPIO 输入输出、外部中断、ADC 采样、PWM 输出、定时器、串口调试甚至低功耗模式。51 虽然能凑出来但外设资源非常紧张代码写到后期会非常别扭。Arduino 开发效率高但 Arduino 的抽象层在真实项目中容易让人忽略底层寄存器行为而且用 Arduino 做课设或毕设答辩时往往“不好讲”。STM32F103 处在中间位置它有丰富的外设、够用的性能和内存同时市面上有标准外设库、HAL 库、LL 库多种开发方式学习资料极其丰富。无论是做最小系统板还是直接用市面上的开发板成本都能控制在几十元级别。更重要的是STM32F103 在 GitHub、Gitee 上的开源项目非常多遇到问题几乎都能搜到现成的排查思路。再回到智能晾衣架本身。这个项目典型的功能需求包括手动升、降控制下雨自动收衣光线变暗或到设定时间自动收衣晾干后自动停止或提醒电机限位保护防止冲顶或拉断按键、遥控器或手机端控制待机低功耗这些功能落在硬件上对应的是 GPIO、ADC、PWM、外部中断、定时器、低功耗模式落在软件上则是多输入事件管理和状态切换。用 STM32F103 恰好能把嵌入式开发里最核心的几个知识点全部串起来——这也是很多课程设计和毕业设计选它的原因。2. 系统功能拆解与整体架构设计在写代码之前先把系统的功能模块拆清楚。很多初学者拿到项目就急着点灯、转电机结果做到一半发现引脚不够、逻辑冲突、传感器干扰只能推翻重来。2.1 功能需求列表一个可用的智能晾衣架控制系统至少要包含以下功能功能模块说明对应硬件/软件资源手动控制按键或遥控控制电机正反转实现升降GPIO 输入、继电器/电机驱动自动收衣雨滴传感器检测到雨水自动控制电机收回ADC 采样雨滴传感器光线检测光线较弱时自动收回避免衣物夜间受潮ADC 采样光敏传感器限位保护到达顶部或底部时自动断电防止电机堵转限位开关、GPIO 外部中断状态指示通过 LED/OLED 显示当前工作状态GPIO 输出、PWM/呼吸灯低压保护/低功耗电池供电时进入 STOP 模式降低待机功耗RTC/外部中断、低功耗模式调试输出通过串口打印状态信息方便调试USART2.2 系统工作流程系统上电后进入待机状态此时电机不转传感器持续采样指示灯以低频率闪烁或呼吸效果提示设备在线。当用户按下“上升”或“下降”按键时系统进入手动运行状态。在自动模式下雨滴传感器和光线传感器会周期性采样当雨滴值超过阈值或光线低于阈值时系统判断需要收衣电机收回衣架。到达限位开关位置后电机停止系统回到待机状态。核心思路是把系统划分成几个确定的状态所有输入事件只负责触发状态切换不直接操作电机。这样无论用户怎么乱按、传感器怎么抖动系统都不会进入“电机一边正转一边反转”这类危险状态。3. 硬件架构与核心器件选型3.1 主控芯片与最小系统主控推荐 STM32F103C8T6也就是最常见的“蓝丸”核心板使用的芯片。它有 64KB Flash、20KB RAM、37 个 GPIO内部集成了 2 个 12 位 ADC、4 个 16 位定时器、3 个 USART足够支撑整个晾衣架控制系统。需要注意STM32F103C8T6 原厂型号已经进入成熟生命期市面上货源充足兼容型号也多比如一些国产 pin-to-pin 替代型号。项目中使用时建议以实际采购型号为准代码层面保持标准库/HAL 库兼容即可。3.2 电机驱动方案智能晾衣架的升降机构通常使用直流减速电机因为扭矩大、控制简单。直流电机本身不能直接接在 STM32 的 GPIO 上原因有两点一是 GPIO 驱动能力不足二是电机启停瞬间的电流冲击会干扰单片机工作。因此必须使用电机驱动模块。常用方案有两种方案器件优点缺点驱动芯片方案TB6612 / DRV8833支持 PWM 调速、正反转、制动控制精细成本略高接线稍多继电器方案5V 继电器模块接线简单成本低只能开关不能调速继电器寿命有限推荐使用 TB6612 驱动芯片方案因为自动晾衣过程中电机启动和停止的缓冲对机械结构更友好。尤其是衣架上有湿衣物时突然启停对晾杆的冲击很大通过 PWM 设置一个较软的启动曲线能在一定程度上减少机械损耗。3.3 传感器选型雨滴传感器是自动晾衣架的核心传感器。典型模块是 LM393 比较器 裸露叉形电极的雨滴传感器模块输出有模拟量和数字量两种。项目里建议使用模拟量输出通过 ADC 采样判断雨量大小而不是直接用数字量阈值因为不同天气下雨水溅落程度不同模拟量能提供更灵活的判断空间。光线传感器可以用光敏电阻配合分压电路也可以用集成的 BH1750 数字光照传感器。前者成本极低适合模拟采样后者精度高、直接输出 lux 值但协议和接线都更复杂。做基础版用光敏电阻即可。限位开关建议使用微动开关或霍尔传感器安装在晾衣架的顶部和底部位置。电机运行时触碰限位开关后立即停止防止衣架冲过机械边界导致损坏。3.4 LCD/OLED 显示与按键状态显示可以选 0.96 寸 SSD1306 OLED 屏I2C 接口只要两根线显示系统状态、传感器数值和电机方向。如果控制逻辑简单也可以只用一颗三色 LED 做状态指示比如绿色呼吸表示待机红色快闪表示故障。按键建议使用独立按键并接 RC 滤波或直接在软件里做消抖。STM32F103 的 GPIO 内部可以配置上拉输入接线时按键一端接 GPIO、另一端接 GND 即可省去外部上拉电阻。3.5 电源设计系统电源是整个项目最容易忽略的部分。直流电机启动瞬间电流可能达到 1A 甚至更高如果和 STM32 共用一条电源线会导致单片机供电电压跌落出现莫名其妙的复位。推荐方案是分开供电电源适配器 12V 或 24V ├── DC-DC 降压模块 - 5V - AMS1117-3.3 - STM32F103 └── 电机驱动模块 VCC 直接接 12V/24V如果做电池供电版本则需要考虑系统低功耗设计。STM32F103 的 STOP 模式可以显著降低待机功耗但电机驱动模块在待机时仍会有静态电流建议用 MOS 管或 P-MOS 开关在待机时切断传感器和电机驱动的电源。3.6 引脚分配建议下面是一个参考引脚分配实际项目可以根据自己手头的板子调整功能引脚说明电机 PWMPA6 (TIM3_CH1)控制电机转速电机方向 1PB0IN1电机方向 2PB1IN2雨滴传感器 ADCPA0 (ADC1_IN0)模拟输入光线传感器 ADCPA1 (ADC1_IN1)模拟输入上升限位开关PB3外部中断下降限位开关PB4外部中断上升按键PB5GPIO 输入下降按键PB6GPIO 输入自动模式按键PB7GPIO 输入LED 指示灯PC13GPIO 输出/呼吸灯OLED I2CPB8 / PB9I2C1串口调试PA9 / PA10USART14. 软件架构状态机比嵌套逻辑更可靠接下来是软件部分也是文章的重点。智能晾衣架控制系统的软件不建议用“顺序执行 大量 if 嵌套”的方式写。因为系统有多个输入源按键、雨滴传感器、光线传感器、限位开关而且这些输入可能同时发生。如果每个输入都写一个 if 分支去控制电机代码一旦进入互斥场景比如“下雨了要收衣但用户又按了下降”就会很难处理。更合理的方案是使用有限状态机Finite State Machine, FSM。系统划分出以下状态SYS_IDLE 待机状态 SYS_RAISING 正在上升 SYS_LOWERING 正在下降 SYS_FAULT 故障保护状态状态迁移规则如下当前状态触发事件下一状态动作IDLE按下上升键RAISING电机正转IDLE按下下降键LOWERING电机反转IDLE雨滴检测RAISING电机正转自动收衣RAISING上升限位触发IDLE电机停止RAISING雨滴信号消失且未到上限可选IDLE电机停止LOWERING下降限位触发IDLE电机停止LOWERING任何保护条件触发FAULT电机急停FAULT故障清除后按复位键IDLE恢复待机状态机的好处是程序在任何时刻只关心“当前状态 当前事件”不需要去考虑所有历史状态组合。调试时只要打印状态切换日志就能清晰看到系统经历了什么流程问题出在哪个环节。对于这种小型单任务控制系统裸机主循环 状态机就足够了不一定要上 FreeRTOS。只有当系统需要同时处理网络协议栈、多传感器任务、屏幕刷新且对实时性要求很高时才考虑引入 RTOS。晾衣架控制这种“低频输入 慢速机械执行”的场景裸机状态机不仅代码更简洁也更容易让别人阅读。5. 核心代码实现下面给出一个基于 STM32 HAL 库的可运行示例框架。使用 Keil MDK 或 STM32CubeIDE 都可以编译重点演示 GPIO、ADC、PWM、外部中断和状态机的组织方式。5.1 GPIO 初始化// 文件路径src/bsp/bsp_gpio.c #include bsp_gpio.h void BSP_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 使能 GPIO 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); // 按键输入PB5 上升键、PB6 下降键、PB7 自动模式键 GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 限位开关输入PB3 上升限位、PB4 下降限位 // 使用外部中断模式触发方式为下降沿 GPIO_InitStruct.Pin GPIO_PIN_3 | GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 电机方向引脚PB0、PB1 推挽输出 GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 状态 LEDPC13 推挽输出 GPIO_InitStruct.Pin GPIO_PIN_13; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); // 配置 EXTI 中断优先级 HAL_NVIC_SetPriority(EXTI3_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI3_IRQn); HAL_NVIC_SetPriority(EXTI4_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI4_IRQn); }注意PA6、PA7 等 PWM 输出引脚在 CubeMX 中配置为复用推挽模式不需要在 GPIO 初始化里重复配置。如果使用标准外设库GPIO 配置方式类似只是 API 名称不同。5.2 电机 PWM 初始化与调速函数电机 PWM 使用定时器的 PWM 输出模式这里以 TIM3_CH1PA6为例。// 文件路径src/bsp/bsp_motor.c #include bsp_motor.h TIM_HandleTypeDef htim3; void BSP_Motor_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_OC_InitTypeDef sConfigOC {0}; // 使能 TIM3 时钟和 GPIOA 时钟 __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // PA6 复用为 TIM3_CH1 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 72MHz / 72 1MHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 1000 - 1; // 1MHz / 1000 1kHz htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; // 初始占空比 0% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); } void BSP_Motor_SetSpeed(uint16_t speed) { if (speed 1000) speed 1000; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, speed); } void BSP_Motor_Stop(void) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0); }这里把 PWM 周期做成 1000这样占空比可以直接用 0~1000 表示换算成百分比非常直观。电机启动时可以在主循环里逐步增加占空比形成软启动效果避免晾杆瞬间受力。5.3 雨滴传感器与光线传感器 ADC 采样雨滴传感器输出模拟电压随雨量变化使用 ADC1 的通道 0 和通道 1 分别采样雨滴和光线。// 文件路径src/bsp/bsp_sensor.c #include bsp_sensor.h ADC_HandleTypeDef hadc1; void BSP_Sensor_ADC_Init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); hadc1.Instance ADC1; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); } // 读取指定通道的 ADC 值channel 可取 ADC_CHANNEL_0 或 ADC_CHANNEL_1 uint16_t BSP_Sensor_Read(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t value HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return value; }在使用 ADC 时要注意雨滴传感器模块的 AO 输出电压在干燥时接近 3.3V在遇水后电压下降。所以判断逻辑是“ADC 值低于阈值表示检测到雨水”。不同模块的输出极性可能不同建议先把传感器放到水龙头下测试一遍确定阈值范围再把阈值写进程序。5.4 按键扫描与软件消抖按键扫描可以放到主循环里以 10ms 周期执行。这里用非阻塞方式避免 delay 阻塞状态机。// 文件路径src/app/app_key.c #include app_key.h #define KEY_PRESS_LEVEL 0 // 按下为低电平 static uint32_t key_scan_tick 0; typedef struct { uint8_t last_state; uint8_t curr_state; uint8_t pressed; } KEY_STATE; static KEY_STATE key_raise; static KEY_STATE key_lower; static KEY_STATE key_auto; // 每 10ms 调用一次 void APP_Key_Scan(void) { if (HAL_GetTick() - key_scan_tick 10) return; key_scan_tick HAL_GetTick(); uint8_t value; value HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_5); key_raise.curr_state value; if (key_raise.last_state KEY_PRESS_LEVEL key_raise.curr_state ! KEY_PRESS_LEVEL) key_raise.pressed 1; key_raise.last_state key_raise.curr_state; value HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6); key_lower.curr_state value; if (key_lower.last_state KEY_PRESS_LEVEL key_lower.curr_state ! KEY_PRESS_LEVEL) key_lower.pressed 1; key_lower.last_state key_lower.curr_state; value HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7); key_auto.curr_state value; if (key_auto.last_state KEY_PRESS_LEVEL key_auto.curr_state ! KEY_PRESS_LEVEL) key_auto.pressed 1; key_auto.last_state key_auto.curr_state; } uint8_t APP_Key_GetPressed(KEY_ID id) { uint8_t ret 0; switch (id) { case KEY_RAISE: ret key_raise.pressed; key_raise.pressed 0; break; case KEY_LOWER: ret key_lower.pressed; key_lower.pressed 0; break; case KEY_AUTO: ret key_auto.pressed; key_auto.pressed 0; break; } return ret; }5.5 主循环与状态机实现这是整个程序的灵魂部分。主循环按照固定周期完成按键扫描、传感器采样、状态机执行和串口日志输出。// 文件路径src/app/app_main.c #include app_main.h #include bsp_gpio.h #include bsp_motor.h #include bsp_sensor.h #include app_key.h typedef enum { SYS_IDLE 0, SYS_RAISING, SYS_LOWERING, SYS_FAULT } SYS_STATE; static SYS_STATE sys_state SYS_IDLE; // 雨滴阈值和光线阈值需要根据实际传感器测试后调整 #define RAIN_THRESHOLD 1800 #define LIGHT_THRESHOLD 1200 #define SENSOR_SAMPLE_PERIOD 200 uint16_t g_rain_value 0; uint16_t g_light_value 0; static uint32_t sensor_tick 0; static uint32_t motor_soft_start_tick 0; static uint16_t motor_speed 0; static uint8_t auto_mode_enabled 1; static void Motor_Run(uint8_t direction); static void Motor_SlowStop(void); void APP_Init(void) { BSP_GPIO_Init(); BSP_Motor_PWM_Init(); BSP_Sensor_ADC_Init(); APP_Key_Scan(); } void APP_Loop(void) { // 1. 周期采样传感器 if (HAL_GetTick() - sensor_tick SENSOR_SAMPLE_PERIOD) { sensor_tick HAL_GetTick(); g_rain_value BSP_Sensor_Read(ADC_CHANNEL_0); g_light_value BSP_Sensor_Read(ADC_CHANNEL_1); } // 2. 扫描按键 APP_Key_Scan(); // 3. 状态机执行 switch (sys_state) { case SYS_IDLE: if (APP_Key_GetPressed(KEY_RAISE)) { Motor_Run(1); sys_state SYS_RAISING; } else if (APP_Key_GetPressed(KEY_LOWER)) { Motor_Run(0); sys_state SYS_LOWERING; } else if (auto_mode_enabled (g_rain_value RAIN_THRESHOLD)) { // 检测到雨水自动收衣 Motor_Run(1); sys_state SYS_RAISING; } break; case SYS_RAISING: // 上升限位开关由 EXTI 中断处理中断里将状态置为 IDLE // 在主循环里做软启动的占空比递增 if (motor_speed 800) { motor_speed 20; BSP_Motor_SetSpeed(motor_speed); } if (APP_Key_GetPressed(KEY_RAISE) || APP_Key_GetPressed(KEY_LOWER)) { // 用户再次按键立即停止 Motor_SlowStop(); sys_state SYS_IDLE; } break; case SYS_LOWERING: if (motor_speed 800) { motor_speed 20; BSP_Motor_SetSpeed(motor_speed); } if (APP_Key_GetPressed(KEY_RAISE) || APP_Key_GetPressed(KEY_LOWER)) { Motor_SlowStop(); sys_state SYS_IDLE; } break; case SYS_FAULT: // 故障状态只允许手动复位 // 这里简化处理等待 5 秒后自动恢复到 IDLE // 实际项目建议通过按键复位或断电重启 HAL_Delay(5000); sys_state SYS_IDLE; break; } // 4. 日志输出 printf(state%d rain%d light%d speed%d\r\n, sys_state, g_rain_value, g_light_value, motor_speed); } static void Motor_Run(uint8_t direction) { motor_speed 0; BSP_Motor_SetSpeed(0); if (direction 1) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); } } static void Motor_SlowStop(void) { // 快速停止直接清零占空比 // 对机械冲击更友好的方式是占空比递减这里按简单处理 BSP_Motor_SetSpeed(0); motor_speed 0; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); }限位开关的外部中断处理函数// 文件路径src/bsp/stm32f1xx_it.c void EXTI3_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_3) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_3); // 上升限位到达 Motor_SlowStop(); sys_state SYS_IDLE; } } void EXTI4_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_4) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_4); // 下降限位到达 Motor_SlowStop(); sys_state SYS_IDLE; } }这里有一个实际工程中非常关键的细节外部中断服务函数里只做“置标志 停电机”的快速处理不要在中断服务函数里调用 HAL_Delay 或者 printf 等耗时函数。这是因为晾衣架的限位触发意味着机械结构已经到了边界电机必须立即停止中断响应越快机械冲击越小。5.6 低功耗 STOP 模式设计如果项目做电池供电版本待机功耗就是必须考虑的问题。STM32F103 支持 SLEEP、STOP、STANDBY 三种低功耗模式。STOP 模式在保留 SRAM 内容的前提下功耗可以降到很低适合这种“大部分时间待机偶尔动作”的晾衣架应用。进入 STOP 模式的典型代码如下void APP_Enter_StopMode(void) { // 停止所有外设和电机 BSP_Motor_Stop(); // 关闭不需要在 STOP 模式下工作的外设时钟 __HAL_RCC_ADC1_CLK_DISABLE(); __HAL_RCC_TIM3_CLK_DISABLE(); // 设置一个 GPIO 作为唤醒引脚比如按键 // 这里用 PB5 作为唤醒源配置为 EXTI 下降沿中断 // 注意在 STOP 模式下外部中断可以唤醒芯片 HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 系统被唤醒后重新配置系统时钟 HAL_ResumeTick(); SystemClock_Config(); }这里最容易踩坑的地方是从 STOP 模式唤醒后系统时钟会回到上电默认状态。如果代码里没有重新调用 SystemClock_Config()外设时钟频率会异常串口波特率、ADC 采样时间、PWM 频率全部不对。所以唤醒后必须重新配置系统时钟再重新初始化被关闭的外设。另外STOP 模式只适合“纯待机等待唤醒”的场景。如果系统需要保持传感器周期性采样比如每 1 分钟检查一次雨滴就不能长时间停在 STOP 模式而要用 RTC 唤醒 STOP 模式交替的方式。6. 运行结果与效果验证代码写完之后按下面顺序验证系统是否正常。6.1 编译与烧录在 Keil 或 STM32CubeIDE 中编译确认无错误。通过 ST-Link 或 USB 转串口烧录固件。烧录成功后复位开发板打开串口调试助手波特率建议设置为 115200。6.2 串口日志判断状态切换预期输出类似state0 rain2050 light2400 speed0 state0 rain1980 light2380 speed0 state1 rain1600 light2350 speed400 state1 rain1500 light2300 speed800 state0 rain1500 light2290 speed0第一列 state 对应状态机状态0 是 IDLE1 是 RAISING2 是 LOWERING3 是 FAULT。手动按下上升键后state 从 0 变为 1说明按键触发和状态切换正常。雨滴传感器数值 rain 从 2000 以上降到 1600 以下说明雨滴检测生效。6.3 功能验证清单测试项操作预期结果手动上升按下上升键电机正转到达上限位后停止手动下降按下下降键电机反转到达下限位后停止遇雨自动收衣向雨滴传感器滴水电机正转收衣到达上限位后停止按键中断上升过程中按下降键电机立即停止回到待机状态限位保护模拟顶到上限位电机立即停止不再继续运行低功耗唤醒进入 STOP 模式后按唤醒键系统复位运行串口日志重新输出如果某一步结果不符合预期先看串口日志里 state 是否发生了对应切换。如果 state 没变问题大概率在输入检测如果 state 变了但电机不动问题在电机驱动或 PWM。7. 常见问题与排查思路问题现象可能原因排查方式解决方案电机不转PWM 未初始化或占空比为 0用示波器/万用表测量 PA6 是否有 PWM 波形确认 HAL_TIM_PWM_Start 已调用BSP_Motor_SetSpeed 传入非 0 值电机不转驱动板电源不足测量电机驱动板 VCC 电压是否稳定电机和主控分开供电电机方向反了IN1/IN2 接线反了观察衣架实际运动方向交换电机驱动板的电机输出线或交换代码中的 IN1/IN2 逻辑按键按下没反应按键抖动导致漏检测串口打印按键状态值延长消抖时间或改为检测低电平持续 20ms雨滴误触发阈值设置不准确串口打印干燥/淋水两种情况的 ADC 值根据实测值重新设定阈值并留出 10%~15% 的余量ADC 读数跳变明显采样引脚悬空或受电机干扰测量引脚电压是否稳定加 0.1uF 滤波电容避免传感器走线与电机电源线平行进入 STOP 模式后无法唤醒唤醒引脚中断未配置检查 EXTI 是否使能NVIC 优先级是否正确重新配置 EXTIN 和 NVIC确保使用下降沿唤醒从 STOP 唤醒后串口乱码唤醒后未重新配置系统时钟检查系统时钟配置函数是否被调用唤醒后先 SystemClock_Config()再初始化外设限位开关触发后电机不断电外部中断里只清了标志没停电机检查中断服务函数是否有停止逻辑在 EXTI 服务函数中调用电机停止函数另外一个常见问题是电机驱动的 PWM 频率选择不当导致电机啸叫。直流减速电机 PWM 频率一般选 10kHz~20kHz低于 1kHz 时人耳能听到明显的电磁噪声。如果出现啸叫可以提高 TIM 分频值把频率调整到 15kHz 左右。8. 工程化与开源发布建议既然项目标题带了“开源”发布时就要有一个完整的工程结构。很多新手把整个 Keil 工程一打包就传到 Gitee 或 GitHub这样别人下载后完全不知道怎么编译、怎么接线也就失去开源的意义。8.1 仓库目录结构参考smart-clothes-drying-system/ ├── docs/ │ ├── README.md │ ├── hardware_design.md │ └── images/ ├── firmware/ │ ├── Core/ │ ├── Drivers/ │ ├── Middlewares/ │ └── src/ │ ├── bsp/ │ ├── app/ │ └── main.c ├── hardware/ │ ├── schematic.pdf │ └── pcb/ ├── tools/ │ └── debug_script.py └── LICENSE8.2 README 必写内容一个合格的开源硬件项目 README 至少包含项目简介和功能列表硬件清单和接线图软件环境Keil 版本、HAL 库版本、ST-Link 驱动等编译烧录步骤传感器阈值标定方法常见问题开源许可证声明8.3 开源许可证选择在 Gitee 上创建仓库时系统会提供许可证选择。常见选项包括 MIT、Apache 2.0、GPL 3.0 等。选型原则很简单如果你希望别人自由使用、修改甚至商用选 MIT 或 Apache 2.0如果你希望后续修改后的代码也必须开源选 GPL 3.0。课程设计和毕业设计项目通常选 MIT 即可声明简单、限制最少。需要特别提醒如果项目中使用了别人的代码尤其是来自其他仓库的代码一定要保留原作者的版权声明并且按照原项目许可证的要求发布。开源不是“免费拿”而是“在约定规则下协作”。8.4 代码规范STM32 项目代码建议分层bsp 层放板级驱动比如 GPIO、UART、ADC、PWM 初始化这一层尽量和业务逻辑解耦app 层放状态机、业务逻辑这一层不要直接操作寄存器只调用 bsp 层提供的接口外设初始化参数用宏定义或者配置文件管理避免散落在代码里这样做的直接好处是当你想把 STM32F103 换成其他型号或者把 HAL 库换成标准库时只需要重写 bsp 层app 层的状态机代码几乎不用动。9. 总结与后续扩展方向这套基于 STM32F103 的智能晾衣架控制系统核心价值不在于“电机正反转”这个动作而在于把传感器采集、状态机管理、PWM 控制和保护逻辑组合成一个可靠可维护的嵌入式系统。哪怕不用这块板子、不照抄这些代码状态机设计思路、限位保护思想、低功耗处理方式也可以平移到其他智能家居控制项目中。如果后续想继续深入有几个很实际的扩展方向第一增加联网能力。给 STM32F103 接一个 ESP8266 或 ESP32-C3 模块通过串口对接 MQTT 协议把晾衣架接入 Home Assistant 或者微信小程序实现远程手动控制、雨天远程提醒、定时收晾。这时建议引入 RTOS因为网络协议栈和主控逻辑需要并行调度。第二优化低功耗方案。把系统改成电池供电使用 RTC 定时唤醒 STOP 模式交替工作优化传感器供电策略待机电流做到微安级别。第三增加故障诊断。通过电流检测判断电机是否堵转添加过流保护异常时自动停机并通过 OLED 显示故障码。第四完善上位机调试工具。可以写一个简单的 Python 串口工具实时绘制传感器曲线辅助阈值标定这个工具本身也可以作为一个独立开源模块放出来。暂时没有更多内容建议把文章收藏起来做项目的时候对照着接线和写代码能省不少事。如果你在实际调试中遇到某个具体问题欢迎在评论区带上串口日志和接线方式交流。