在智能家居的单项设备里,智能晾衣架看起来功能不多,真正动手做控制系统时才会发现,它把传感器采集、电机驱动、限位保护、按键交互、状态显示和低功耗管理全部串在了一条链路上。基于 STM32F103 设计一套智能晾衣架控制系统,是一个适合用来打通 GPIO、ADC、定时器 PWM、DMA、状态机这些基础外设的完整案例。本文从方案选型开始,依次给出硬件连接、引脚分配、CubeMX 工程配置、核心模块代码、运行验证方法和常见问题排查路径,最终目标是让读者从最小系统板出发,把整套控制系统跑起来,并理解每一步为什么这样设计。
1. 系统功能与方案选型
1.1 智能晾衣架要解决哪些问题
传统晾衣架需要人工判断天气,下雨时来不及收,阳光好时又常常忘记晾出去。智能晾衣架控制系统要完成的动作可以归纳为四类:
- 自动收起:检测到雨滴后,先收回撑杆,再上升归位。
- 自动晾晒:天气晴朗且无雨时,下降并展开撑杆。
- 手动控制:用户通过按键随时执行上升、下降、展开、收起。
- 限位与保护:电机运行到顶部或底部限位开关时立即停止,防止机械损坏。
在此基础上还可以扩展干燥、紫外消毒、远程控制等功能,但核心控制逻辑始终围绕“传感器阈值判断 + 电机动作 + 限位保护”这条主线展开。真正影响可靠性的不是某个外设有多高级,而是传感器误判时能不能兜住,电机堵转时限位有没有生效,按钮按下后状态机是否按预期迁移。
1.2 为什么选择 STM32F103 作为主控
STM32F103 是意法半导体基于 Cortex-M3 内核的 72MHz 主流 MCU,在智能晾衣架这种控制场景里优势非常具体:
- 定时器资源充足。通用定时器 TIM2、TIM3、TIM4 各带 4 个 PWM 通道,电机调速、LED 呼吸灯、干燥风机调速可以复用。
- ADC 外设完整。ADC1 支持多通道扫描,雨滴传感器和光敏传感器可以直接接模拟输出脚,不需要外挂专用 ADC 芯片。
- 低功耗模式可用。待机时进入 STOP 模式,通过按键或雨滴传感器的 EXTI 中断唤醒,适合晾衣架长期待机的使用场景。
- 生态成熟。HAL 库、标准外设库、寄存器开发三种方式都有大量资料,问题排查成本低。
对比 51 单片机,F103 的定时器、ADC 精度和 DMA 能力明显更强;对比 ESP32,F103 没有无线功能,但控制实时性、功耗表现和成本更适合纯本地控制场景。如果后续想加 App 远程控制,可以保留 F103 做执行控制,再外接 WiFi 模块走串口通信,这是比较稳妥的分层方案。
1.3 系统总体架构
整个系统可以分成三层:
- 感知层:雨滴传感器、光敏传感器、限位开关、按键。
- 控制层:STM32F103 负责信号采集、阈值判断、状态机迁移、PWM 输出。
- 执行层:直流减速电机、电机驱动模块、OLED 显示屏、干燥继电器。
数据流向是:雨滴传感器和光敏传感器输出模拟电压,由 ADC 采集后滤波,再与阈值比较产生“下雨 / 晴天 / 阴天 / 夜晚”等天气结论;状态机根据当前状态和天气结论决定是否启动电机;电机驱动模块接收方向 GPIO 和 PWM 占空比信号,驱动直流电机;限位开关反馈行程是否到顶或到底,状态机收到限位信号后停机关闭 PWM。
这套架构的关键是状态机。晾衣架不是一直转电机,而是“收到触发 -> 执行动作 -> 到达限位 -> 回到待机”的短时工作过程。如果不用状态机而用阻塞延时,雨滴检测和按键扫描都会被电机转动时的大延时卡死,系统看起来就会像“按了没反应”。
2. 硬件设计与引脚分配
2.1 硬件物料清单
下面是一套适合学习验证的物料清单。原始设计没有限定具体型号,落地前要根据自己手上的板子核对引脚和电压。
| 模块 | 推荐型号 | 用途 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103C8T6 最小系统板 | 控制核心 | 常见 Blue Pill 板载 LED 在 PC13 |
| 电机驱动 | L298N 或 TB6612FNG | 驱动直流电机 | TB6612 功耗更低,L298N 更容易买到 |
| 直流减速电机 | 6V 或 12V 带减速箱电机 | 升降、展开 | 需要两路,或先验证一路升降 |
| 雨滴传感器 | LM393 雨滴模块 | 检测下雨 | 同时有 DO 数字输出和 AO 模拟输出 |
| 光敏传感器 | 光敏电阻模块 | 检测光照强度 | 用 AO 模拟输出 |
| 限位开关 | 直动式微动开关 | 顶部 / 底部限位 | 常闭或常开均可,软件按逻辑调整 |
| 按键 | 轻触按键 x4 | 手动控制 | 上升、下降、自动、停止 |
| 显示屏 | SSD1306 OLED 0.96 寸 I2C | 显示状态 | I2C 地址默认 0x78 |
| 继电器 | 5V 单路继电器模块 | 干燥 / 紫外线设备 | 控制交流设备时注意隔离 |
| 供电 | 5V 2A 电源 + 12V 电源 | 主控和电机分区供电 | 电机驱动单独供电更稳定 |
2.2 引脚分配表
这里以 STM32F103C8T6 为例,给出完整引脚分配,实际焊接和接线时对照此表。选择 PA0-PA5 作为 ADC 和 PWM 通道,是因为 ADC1 的输入通道与定时器 PWM 通道天然分布在 PA 口附近,布局方便。
| 功能 | 引脚 | 说明 |
|---|---|---|
| 升降电机 PWM | PA0 | TIM2_CH1,PWM 输出 |
| 展开电机 PWM | PA1 | TIM2_CH2,PWM 输出 |
| 升降电机方向 1 | PB0 | 控制 IN1 电平 |
| 升降电机方向 2 | PB1 | 控制 IN2 电平 |
| 展开电机方向 1 | PB2 | 控制 IN3 电平 |
| 展开电机方向 2 | PB10 | 控制 IN4 电平 |
| 雨滴传感器 AO | PA4 | ADC1_IN4 |
| 光敏传感器 AO | PA5 | ADC1_IN5 |
| 顶部限位开关 | PB12 | 输入,内部上拉,按下接地 |
| 底部限位开关 | PB13 | 输入,内部上拉,按下接地 |
| 上升按键 | PB14 | 输入,内部上拉 |
| 下降按键 | PB15 | 输入,内部上拉 |
| 自动模式按键 | PA8 | 输入,内部上拉 |
| 停止按键 | PA11 | 输入,内部上拉 |
| OLED SCL | PB6 | I2C1 时钟 |
| OLED SDA | PB7 | I2C1 数据 |
| 串口 TX | PA9 | USART1 调试输出 |
| 串口 RX | PA10 | USART1 调试输入 |
| 干燥继电器 | PC14 | 输出,高电平闭合 |
| 板载 LED | PC13 | 调试指示灯 |
注意,PC14 和 PC15 在最小系统板上默认连接外部低速晶振引脚,如果时钟配置选择不使用 LSE,这两个引脚可以作为普通 GPIO 使用。CubeMX 里不要把 PC13-PC15 之外的晶振相关配置改乱。
2.3 电机驱动与限位开关电路
直流电机的正反转通过电机驱动模块的 H 桥完成。以 L298N 为例,ENA 接 PWM 控制速度,IN1 和 IN2 组合控制方向:
| IN1 | IN2 | ENA | 电机状态 |
|---|---|---|---|
| 高 | 低 | PWM | 正转 |
| 低 | 高 | PWM | 反转 |
| 高 | 高 | - | 刹车 |
| 低 | 低 | - | 停止 |
STM32 的 GPIO 输出 3.3V 电平,L298N 逻辑输入兼容 5V 和 3.3V 的型号可以直接接,但如果驱动模块要求 5V 逻辑电平,需要加电平转换或选择逻辑供电 3.3V 的模式。TB6612 的 VM 接电机电源,VCC 接 3.3V 或 5V,控制逻辑更友好。
限位开关的接法建议使用“一端接 GND,另一端接 MCU 引脚,内部上拉”的方式。开关未按下时读到高电平,按下时读到低电平。这样即使开关线脱落,也只会读到持续高电平,不会误触发限位动作。
实际接线时最容易踩的坑是驱动模块的 GND 与 MCU 的 GND 没有共地。MCU 的 PWM 信号和驱动模块之间如果没有共同参考地,逻辑电平会悬空,电机要么不转,要么随机抖动。所有模块的 GND 必须接到同一个网络。
2.4 传感器接入与 ADC 设计
雨滴模块和光敏模块都有 AO 模拟输出,输出电压范围约 0 到 3.3V(或 0 到 5V,取决于模块供电)。如果模块供电 5V 输出可能超过 3.3V,需要分压后送入 STM32 ADC 引脚。这里在物料选型时直接选择 3.3V 供电的模块,把输出限制在 MCU ADC 输入范围内。
STM32F103 的 ADC 是 12 位分辨率,参考电压 VREF 通常接 3.3V,因此采样值换算电压的公式是:
float voltage = (float)adc_value * 3.3f / 4095.0f;雨滴传感器模块板上有一个蓝色电位器,用来调节比较器跳变阈值。这个电位器直接影响 DO 数字输出,但对 AO 模拟输出没有影响。使用模拟量时,阈值应该在程序里根据实测数据确定,而不是靠模块上的电位器拍脑袋。
3. 开发环境与工程准备
3.1 HAL 库还是标准外设库
STM32F103 的开发方式主要有三种:寄存器、标准外设库、HAL 库。本文代码使用 HAL 库,原因是 CubeMX 配置外设更直观,ADC、PWM、I2C 的初始化代码由工具生成,可以把精力放在业务逻辑上。标准外设库运行效率更高,库更轻量,但遇到外设组合配置时需要手动处理更多细节。
实际项目里更重要的是工程可维护性。HAL 库虽然代码量稍大,但通过 CubeMX 重新生成不会覆盖用户代码段,适合团队协作。如果对 HAL 库的某些实现不满意,可以针对关键路径改写寄存器,这也是常见优化手段。
3.2 CubeMX 工程配置
用 CubeMX 新建工程选择 STM32F103C8Tx,按以下步骤配置外设:
- RCC:HSE 选择 Crystal/Ceramic Resonator,SYS 选择 Serial Wire(保留 SWD 调试)。
- Clock Configuration:HSE 8MHz,PLL 倍频到 72MHz,APB1 分频 2 得到 36MHz,APB2 不分频得到 72MHz。
- ADC1:使能通道 4(PA4)和通道 5(PA5),采样时间选择 239.5 周期,连续转换或扫描模式按需配置。
- TIM2:选择 Channel1 和 Channel2 为 PWM Generation CH1/CH2,预分频器 71,自动重载值根据 PWM 频率计算。
- I2C1:使能标准模式或快速模式,速度 400kHz。
- USART1:异步模式,波特率 115200,8 位数据,无校验。
- GPIO:PB0、PB1、PB2、PB10 设置为输出推挽,初始电平低;PB12、PB13、PB14、PB15、PA8、PA11 设置为输入上拉。
PWM 频率的计算公式是:
PWM频率 = 定时器时钟 / ((预分频器 + 1) * (自动重载值 + 1))定时器时钟为 72MHz 时,预分频器设为 71,自动重载值设为 999,PWM 频率约 1kHz。直流电机通常用 1kHz 到 20kHz 之间的 PWM,频率太低电机噪声大,频率太高会加重驱动模块开关损耗。1kHz 用于验证足够。
3.3 最小验证:点亮板载 LED
工程生成后,先不要急着写业务代码,第一步确认编译下载链路正常。在main函数中写一个 LED 翻转逻辑:
while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(200); }编译下载后板载 LED 以 200ms 周期闪烁,说明芯片、下载器、工程配置都正常。如果这一步都没通过,先检查 SWD 连接、复位电路和 BOOT0 跳线,不要继续往下写传感器代码。
3.4 工程目录结构
一个适合长期维护的工程结构建议如下:
project/ ├── Core/ │ ├── Inc/ │ └── Src/ │ ├── main.c │ ├── stm32f1xx_it.c │ └── system_stm32f1xx.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── App/ │ ├── app_state_machine.c │ ├── app_state_machine.h │ ├── app_sensor.c │ ├── app_sensor.h │ ├── app_motor.c │ ├── app_motor.h │ ├── app_ui.c │ └── app_ui.h ├── Middlewares/ └── MDK-ARM/把用户代码放到独立的App目录,而不是全部塞进main.c,是为了避免 CubeMX 重新生成时被覆盖,也方便后续移植到其他型号的 MCU。
4. 核心功能模块实现
4.1 顶层状态机设计
晾衣架的工作状态可以归纳为六种:
| 状态 | 枚举 | 动作说明 |
|---|---|---|
| 待机 | STATE_IDLE | 电机停止,等待触发 |
| 上升 | STATE_LIFTING | 升降电机正转,晾衣架上升 |
| 下降 | STATE_LOWERING | 升降电机反转,晾衣架下降 |
| 展开 | STATE_EXTENDING | 展开电机正转,撑杆伸出 |
| 收起 | STATE_RETRACTING | 展开电机反转,撑杆收回 |
| 手动暂停 | STATE_STOPPED | 用户主动停止,解除自动锁定 |
为什么用状态机而不是直接在主循环里顺序执行?因为晾衣架的动作不是瞬时完成的,电机会运行几百毫秒甚至几秒,期间必须持续检测限位开关、按键和雨滴传感器。状态机把“当前在做什么”和“下一步做什么”分离,每个循环只处理当前状态对应的事件,逻辑清晰且不会阻塞传感器采样。
状态机主循环的核心框架如下:
typedef enum { STATE_IDLE = 0, STATE_LIFTING, STATE_LOWERING, STATE_EXTENDING, STATE_RETRACTING, STATE_STOPPED } RackState; RackState rack_state = STATE_IDLE; void Rack_StateMachine(void) { switch (rack_state) { case STATE_IDLE: Rack_HandleIdle(); break; case STATE_LIFTING: Rack_HandleLifting(); break; case STATE_LOWERING: Rack_HandleLowering(); break; case STATE_EXTENDING: Rack_HandleExtending(); break; case STATE_RETRACTING: Rack_HandleRetracting(); break; case STATE_STOPPED: Rack_HandleStopped(); break; default: rack_state = STATE_IDLE; break; } }每个状态处理函数只做三件事:检查限位是否到达,检查停止按键是否按下,决定是继续当前动作还是切换状态。例如下降状态下,一旦底部限位开关变成有效电平,立即停止电机,切回待机。
4.2 传感器采集与滤波
ADC 采集不能拿单次采样值直接做判断。传感器受电源纹波、线缆干扰影响,单次值可能有较大波动,需要软件滤波。这里采用“中位值平均滤波法”:连续采集 N 次,去掉最大值和最小值,剩余值求平均。
#define SENSOR_SAMPLE_N 5 uint16_t ADC_ReadFiltered(ADC_HandleTypeDef *hadc, uint32_t channel) { uint16_t samples[SENSOR_SAMPLE_N]; uint16_t max_val = 0, min_val = 4095; uint32_t sum = 0; for (int i = 0; i < SENSOR_SAMPLE_N; i++) { ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = channel; sConfig.Rank = 1; sConfig.SamplingPeriod = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc, &sConfig); HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); samples[i] = HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); if (samples[i] > max_val) max_val = samples[i]; if (samples[i] < min_val) min_val = samples[i]; sum += samples[i]; } return (uint16_t)((sum - max_val - min_val) / (SENSOR_SAMPLE_N - 2)); }这段代码每次循环重新配置通道,适合采样通道少、采样频率要求不高的场景。更高效的做法是开启 ADC 扫描模式加 DMA,一次转换多通道,CPU 只在 DMA 传输完成后取数据。晾衣架的采样频率毫秒级即可满足,轮询方式更直观,也更容易排查问题。
阈值判断必须包含回差(迟滞)。以上为例:如果雨滴采样值从干燥的 3800 降到下雨的 1200,不能只设一个 2500 的阈值,否则雨停时采样值在阈值附近抖动,继电器和电机会频繁启停。正确做法是设置两个阈值:
#define RAIN_WET_THRESHOLD 2000 // 采样值低于该值判定为下雨 #define RAIN_DRY_THRESHOLD 2800 // 采样值高于该值判定为雨停天气状态从“无雨”变成“有雨”必须低于 2000,从“有雨”变成“无雨”必须高于 2800。中间 2000 到 2800 的区间保持不变,这就是回差。回差不仅能抑制抖动,还能避免传感器表面残留水珠造成的反复触发。实际阈值要按传感器供电和模块型号实测调整,不同批次的传感器输出范围不完全一样。
4.3 电机控制与限位检测
电机的运动封装成一个函数,方向由两个 GPIO 控制,速度由 PWM 占空比控制。占空比范围 0 到 999,对应 0 到 100%。
void Motor1_Set(int8_t direction, uint16_t speed) { if (direction > 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); } else if (direction < 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0); return; } __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed); }限位开关检测在状态机中每一轮都会执行。因为在上升或下降过程中,限位开关按下可能导致电机堵转,堵转电流会烧毁驱动模块或电机,检测必须在主循环中高频执行。
int8_t Limit_TopPressed(void) { return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_RESET) ? 1 : 0; } int8_t Limit_BottomPressed(void) { return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) == GPIO_PIN_RESET) ? 1 : 0; }限位开关也有抖动问题。机械开关按下和释放的瞬间会产生毛刺,可能导致状态机误判。软件层面可以做一个连续确认机制:连续 5 次读到有效电平才认为限位到达。这样做的另一个好处是避免开关老化后接触不良导致误动作。
4.4 自动控制逻辑
自动模式是系统的核心业务。进入自动模式后,主循环每次运行都调用Rack_AutoControl,输入是滤波后的传感器值和当前状态,输出是状态迁移信号。
void Rack_AutoControl(void) { uint16_t rain_adc = ADC_ReadFiltered(&hadc1, ADC_CHANNEL_4); uint16_t light_adc = ADC_ReadFiltered(&hadc1, ADC_CHANNEL_5); if (rack_state != STATE_IDLE) { return; } if (Light_IsRain(rain_adc)) { if (clothes_extended_flag) { rack_state = STATE_RETRACTING; } else if (clothes_lowered_flag) { rack_state = STATE_LIFTING; } } else if (Light_IsSunny(light_adc) && !clothes_lowered_flag) { rack_state = STATE_LOWERING; } }这里补充两个标志位clothes_extended_flag和clothes_lowered_flag,它们由状态机在到达限位时更新,用来记录当前衣架在哪个位置,避免每次循环都重新执行已完成的动作。
自动逻辑必须考虑动作完成后的状态持久化。系统断电再上电后,MCU 不知道衣架是在顶部还是底部,如果直接执行自动逻辑可能造成错误动作。生产环境可以在 Flash 或者外挂 EEPROM 中保存状态,也可以要求上电后先回到初始位置,这属于工程化必须处理的边界条件。
4.5 PWM + DMA 实现电机平滑启停
直接给电机 100% 占空比会产生很大的启动电流,同时机械结构会剧烈抖动;直接切断 PWM 会因为惯性造成冲击。用 PWM + DMA 实现占空比斜坡是一个实用的优化方案。
思路是预先准备一段占空比数组,启动时 DMA 按照定时器更新事件的节奏把数组中的值逐个写入比较寄存器,实现占空比从 0 到目标值的平滑递增。以 TIM2_CH1 为例:
#define RAMP_STEPS 200 uint16_t ramp_up_table[RAMP_STEPS]; TIM_HandleTypeDef htim2_dma; DMA_HandleTypeDef hdma_tim2_ch1; void Motor_PwmDmaInit(void) { for (int i = 0; i < RAMP_STEPS; i++) { ramp_up_table[i] = (uint16_t)((uint32_t)i * 999 / RAMP_STEPS); } hdma_tim2_ch1.Instance = DMA1_Channel5; hdma_tim2_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tim2_ch1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tim2_ch1.Init.MemInc = DMA_MINC_ENABLE; hdma_tim2_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_tim2_ch1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_tim2_ch1.Init.Mode = DMA_NORMAL; hdma_tim2_ch1.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_tim2_ch1); __HAL_LINKDMA(&htim2, hdma[TIM_DMA_ID_CC1], hdma_tim2_ch1); HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_1, ramp_up_table, RAMP_STEPS); }使用 DMA 模式后,CPU 不需要在中断里逐次修改比较寄存器,只需要在发送完成后确认动作是否结束。这个例子对晾衣架来说属于优化项,不是必需项。先跑通普通 PWM 控制,再考虑启动平滑。启动时从 30% 占空比开始、逐步加到目标值,已经能明显减少机械冲击,不一定要引入 DMA。
4.6 OLED 状态显示
0.96 寸 SSD1306 OLED 通过 I2C1 连接,显示内容建议包含:当前模式(自动 / 手动)、雨滴采样值、光照采样值、当前状态(待机 / 上升 / 下降 / 展开 / 收起)。显示刷新不宜太频繁,否则会占用 I2C 总线影响其他逻辑,建议 500ms 刷新一次。
void UI_Update(RackState state, uint8_t mode, uint16_t rain, uint16_t light) { static uint32_t last_refresh = 0; uint32_t now = HAL_GetTick(); if (now - last_refresh < 500) { return; } last_refresh = now; char buf[16]; OLED_Clear(); OLED_ShowString(0, 0, (mode == MODE_AUTO) ? "MODE:AUTO" : "MODE:MANUAL"); snprintf(buf, sizeof(buf), "RAIN:%4d", rain); OLED_ShowString(0, 2, buf); snprintf(buf, sizeof(buf), "LIGHT:%4d", light); OLED_ShowString(0, 4, buf); snprintf(buf, sizeof(buf), "STATE:%d", (int)state); OLED_ShowString(0, 6, buf); }snprintf 在单片机上会占用较多 Flash 和栈空间,如果芯片容量紧张,可以换成自写的整数转字符串函数。日常调试阶段打印字符串方便移植,生产版本再优化成直接拼接 ASCII 码。
4.7 按键处理与手动优先级
晾衣架必须支持手动模式覆盖自动模式。用户正在手动操作时,如果自动逻辑强行切入,会给用户带来困惑,甚至可能夹到手。因此手动操作的优先级设计成:手动按键按下时,自动逻辑本周期不执行。
按键检测使用轮询加软件消抖。最简单可靠的消抖方法是:检测到电平变化后,延时 20ms 再确认一次。晾衣架的按键操作频率很低,20ms 延时对系统无影响,但如果主循环中还有其他高频任务,建议用定时器中断消抖。
int8_t Key_Scan(uint16_t pin, uint32_t timeout_ms) { if (HAL_GPIO_ReadPin(GPIOB, pin) == GPIO_PIN_RESET) { uint32_t start = HAL_GetTick(); while (HAL_GetTick() - start < timeout_ms) { if (HAL_GPIO_ReadPin(GPIOB, pin) == GPIO_PIN_SET) { return 0; } } return 1; } return 0; }使用这个函数检测 PB14 上升按键和 PB15 下降按键。停止按键 PA11 在任何状态按下,都直接执行Motor1_Set(0, 0)和Motor2_Set(0, 0),然后回到STATE_STOPPED,直到用户再次按自动或手动方向键。
5. 联调验证与低功耗改进
5.1 分模块验收清单
不要等到全部代码写完再上电,每个模块独立验证,问题范围会小很多。
| 模块 | 验证方法 | 预期结果 |
|---|---|---|
| LED | 下载闪烁程序 | PC13 闪烁 |
| 串口 | printf 输出测试字符串 | 串口助手收到文本 |
| ADC 雨滴 | 手指沾水触碰传感器 | 采样值明显下降 |
| ADC 光照 | 遮挡光敏电阻 | 采样值明显变化 |
| PWM | 用逻辑分析仪测 PA0 | 看到 1kHz 方波 |
| 电机方向 | 短暂给定方向信号 | 电机以预期方向转动 |
| 限位开关 | 按下限位开关观察状态变量 | 状态切换 |
| OLED | 刷新显示 | 显示内容和变量一致 |
| 按键 | 按一次观察打印日志 | 单次触发,无重复 |
5.2 系统联调步骤
分模块通过后,按照下列顺序做系统联调:
- 先挂空载,不要连接实际衣架机械结构,防止机械故障损坏设备。
- 串口打印雨滴和光照采样值,用手模拟下雨、遮挡光照,确认阈值和回差合理。
- 低速测试升降电机,逐步提高占空比,观察运行电流和机械抖动。
- 手动模式按键测试:上升、下降、展开、收起各动作都要独立合格。
- 自动模式验证:把雨滴传感器喷水,观察系统是否先收起再上升;把光敏电阻置于强光下模拟晴天,观察是否下降并展开。
- 测试中断保护:在电机运行中直接按下停止按键,电机应立即停止。
5.3 用串口日志定位状态跳变
系统联调阶段,每个状态迁移都要打印日志。一条理想日志要包含时间、当前状态、触发源。
[3000ms] RAIN=1250 LIGHT=3800 -> RAIN_EVENT [3000ms] STATE_IDLE -> STATE_RETRACTING [3400ms] LIMIT_TOP OFF -> MOTOR2 STOP [3400ms] STATE_RETRACTING -> STATE_LIFTING [3900ms] LIMIT_TOP ON -> MOTOR1 STOP [3900ms] STATE_LIFTING -> STATE_IDLE打印位置放在状态迁移函数里,而不是放在每个 case 分支口,避免同一状态被重复打印。日志格式统一用[时间] 事件,后续用脚本就能做自动化分析。
5.4 引入 STOP 低功耗模式
晾衣架大量时间处于待机状态,功耗优化有实际意义。STM32F103 的 STOP 模式可以停止内部时钟,但保留 SRAM 内容,电流可以降到几十微安级别,适合用按键或雨滴传感器边沿唤醒。
进入 STOP 模式前必须满足前提条件:
- 电机已停止,PWM 已关闭。
- 所有 I2C、ADC 外设已退出工作状态。
- 唤醒源已配置为 EXTI 中断。
按键和雨滴传感器的 DO 信号都可以作为 EXTI 唤醒源。雨滴模块的 DO 输出在有雨时跳变,可以作为唤醒信号;按键使用上升沿或下降沿唤醒,取决于按键接法。
void System_EnterStop(void) { HAL_UART_DeInit(&huart1); HAL_ADC_Stop(&hadc1); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9 | GPIO_PIN_10); HAL_GPIO_DeInit(GPIOB, GPIO_PIN_6 | GPIO_PIN_7); HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); HAL_ResumeTick(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_ADC1_Init(); }STOP 模式唤醒后,时钟系统需要重新初始化,串口和 ADC 也要重新初始化,否则打印和采集都会异常。注意HAL_Delay使用 SysTick,STOP 模式下 SysTick 停止,唤醒后必须调用HAL_ResumeTick恢复,否则后续延时全乱。
低功耗模式只建议在系统稳定后再引入。前期调试阶段反复进入 STOP 会让问题定位变难,建议功能全部验收后再做功耗优化。
6. 常见问题排查
实际项目中最常见的几类问题,按现象、原因、处理方式整理如下。
6.1 电机不转或抖动
现象:程序正常编译下载,手动按下按键后电机没有反应,或者听到嗡嗡声但转不动。
排查顺序:
- 检查电机驱动模块 GND 是否与 MCU GND 共地。没有共地时 PWM 信号没有参考基准,这是最容易忽略的问题。
- 检查电机供电。L298N 的供电如果是 5V 或 12V,电源功率至少要达到电机启动电流的 2 倍以上,电源电压跌落会导致电机转不动。
- 检查 PWM 是否真的输出。用逻辑分析仪或示波器测 PA0、PA1,如果占空比一直为 0,排查
__HAL_TIM_SET_COMPARE是否被正确调用。 - 检查方向引脚。IN1/IN2 如果都是低电平,电机不会转;如果都是高电平,电机处于刹车状态。
- 检查限位开关状态。如果在下降状态但底部限位一直有效,状态机会立刻停止电机。用串口打印限位引脚电平确认。
6.2 雨滴传感器误触发
现象:没有下雨,但系统频繁执行收起动作。
原因有两类。一类是雨滴传感器安装位置容易积水或者被风吹动的水滴溅到;另一类是模块的 AO 输出本身随湿度缓慢变化,阈值设置太靠近边界。
处理方式:
- 将雨滴传感器安装在晾衣架顶部靠内侧,避免被风吹到;
- 软件中增加连续触发确认,连续多次采样值低于阈值才判断为下雨;
- 重新标定阈值,将“干燥”、“微湿”、“下雨”三档采样值分别记录下来,阈值取中间值并保留回差。
6.3 ADC 读数不稳定
现象:OLED 上显示的同一传感器数值在几百甚至上千的范围内跳动。
原因通常是引脚悬空、采样时间过短、或者传感器的 GND 与 MCU 之间有电位差。
处理方式:
- PA4、PA5 在 PCB 上尽量走短线,远离电机线缆;
- ADC 采样时间改为 239.5 周期,给内部采样电容充分充电时间;
- 软件做中位值平均滤波;
- 电机运行时 ADC 跳动严重,可以考虑在电机运行时暂停 ADC 采样,电机停止后再恢复。
6.4 按键和限位开关抖动
现象:按一次按键,日志却打印两次;或者限位开关按下后状态反复切换。
原因:机械触点通断瞬间产生毛刺,MCU 在几十微秒内反复读到高低电平。
处理方式:软件消抖至少 20ms,限位开关建议 50ms 连续确认,必要时在按键两端并联 0.1uF 电容做硬件消抖。
6.5 PWM + DMA 无输出
现象:启用HAL_TIM_PWM_Start_DMA后 PA0 没有波形,或者只输出一次。
排查方式:
- 确认 DMA 请求映射。TIM2_CH1 对应 DMA1 的 Channel5,映射错误时事件不会触发传输;
- NORMAL 模式下 DMA 只能传输一次,需要循环启动或使用 CIRCULAR 模式;
- 检查数组是否定义为全局变量并保持生命周期,局部数组在函数返回后内容不可用;
- 使用 HAL_DMA_Start_IT 等接口时确认中断回调函数已实现。
7. 工程化改造与最佳实践
7.1 从实验板到量产板的差异
实验板用杜邦线连接,验证逻辑没问题,但距离可靠产品还有明显差距。量产方向需要关注以下改造点:
- 电机驱动从 L298N 换成专用电机驱动芯片,体积更小,保护更完善;
- 电源增加防反接、过流保护、TVS 和电解电容,电机启停会产生浪涌;
- PCB 布局上把功率地和信号地分开,单点接地,避免电机回路干扰传感器采样;
- 增加硬件看门狗,防止程序跑飞后电机无法停止;
- 限位开关建议使用常闭型,断线时状态更安全;
- 传感器阈值不要写死在代码里,建议存储在 Flash 模拟 EEPROM 中,产品出厂时通过串口指令标定。
开源发布时,仓库里至少要有原理图、PCB 文件、固件源码、README 使用说明和物料清单。许可证建议在 MIT、Apache-2.0、GPL-3.0 之间按意图选择:希望别人随意使用和闭源,选 MIT 或 Apache-2.0;希望衍生代码也必须开源,选 GPL-3.0。README 中要写明引脚定义、烧录方法和机械结构照片,否则别人很难复现。
7.2 可复用代码检查清单
以下清单适用于 STM32 类小型控制项目,发布前逐项检查:
- 状态枚举没有魔法数字,状态迁移统一走函数;
- 所有按键、限位开关都做了软件消抖;
- 传感器判断都有回差,且阈值可配置;
- 电机运行前检查限位状态,方向改变前先停止;
- 串口日志统一格式,状态迁移只打印一次;
- 所有阻塞延时都来自 HAL_Delay 或自定义非阻塞延时,没有裸空循环;
- 全局变量用
volatile修饰,中断和主循环共享的数据有保护; - 进入 STOP 前确认外设已关闭,唤醒后重新初始化时钟和串口;
- 看门狗在电机运行时不会被错误喂狗导致保护失效。
7.3 引入 FreeRTOS 的任务划分
如果系统要扩展网络通信、传感器采样、UI 刷新等多个并发任务,可以引入 FreeRTOS。任务划分建议:
| 任务 | 周期 / 触发 | 优先级 | 说明 |
|---|---|---|---|
| 传感器任务 | 50ms | 中 | ADC 采样、滤波、天气判断 |
| 状态机任务 | 20ms | 高 | 状态迁移、电机控制、限位检测 |
| UI 任务 | 500ms | 低 | OLED 刷新 |
| 通信任务 | 事件触发 | 中 | 串口指令解析、WiFi 数据上报 |
引入 RTOS 后,原先主循环里的非阻塞逻辑可以拆成多个任务,但共享变量必须用互斥量或消息队列保护,尤其是状态枚举和传感器阈值。FreeRTOS 不是银弹,任务多了以后,优先级反转、栈溢出和延时抖动会变成新的排查难点。对于核心功能只有晾衣架的场景,裸机状态机足够,RTOS 属于锦上添花。
7.4 扩展方向
当前系统完成本地控制后,可以沿几个方向扩展:
- 无线通信:外接 WiFi 模块或蓝牙模块,通过 AT 指令或 MQTT 协议接入智能家居平台,实现手机远程控制。
- 多传感器:加入风速传感器、温湿度传感器,在强风天气自动收起,湿度高时自动展开晾晒。
- 衣物状态识别:加入称重或热电偶检测,判断衣物是否晾干,达到干燥目标后自动收起。
- 安全防护:在下降路径上加红外避障或防夹检测,遇到障碍物立即停止并回退。
扩展时同步评估 MCU 资源。STM32F103C8T6 的 64KB Flash 和 20KB SRAM 在加入网络协议栈后可能紧张,可以换用更高容量型号或把通信任务放在 WiFi 模块内部处理,F103 只负责执行控制。
智能晾衣架这套系统的价值在于,它把一个看似简单的家电设备拆成了完整的嵌入式控制链路。从传感器标定到状态机设计,从限位保护到低功耗处理,每个环节都是一类通用工程问题。建议先按本文的模块顺序完成一次裸机版本,再逐步加入 DMA、低功耗、RTOS 和无线通信,每一步都确保可运行、可验证,再进入下一步。