news 2026/9/1 19:04:55

基于STM32F103的智能晾衣架控制系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103的智能晾衣架控制系统设计与实现

相信不少做过嵌入式课设或实际小项目的朋友都有过这种体会:功能需求其实不复杂,但把传感器、电机、显示、控制逻辑全部整合到一块 STM32 上时,硬件接线、驱动移植、状态切换、调试排错很容易把人绕晕。尤其是“自动收衣服”这种带实时判断的场景,既要处理传感器数据,又要控制电机动作,还要兼顾手动和自动两种模式,网上资料又比较零散。

这篇文章就围绕“基于 STM32F103 的智能晾衣架控制系统”展开,从系统需求、硬件选型、CubeMX 初始化、模块驱动到完整控制逻辑,一步步讲清楚。代码会按模块拆分,方便直接复制到工程里验证。无论你是做课程设计、电子竞赛,还是想自己动手做一个阳台晾衣架控制器,这篇文章都能提供一个完整的参考。

1. 项目背景与功能概述

1.1 为什么需要智能晾衣架

传统的晾衣架核心操作就是“手动伸出、手动收回”。天气突变时,如果家里没人,衣服很容易被雨淋湿;白天出门上班,衣服晒在外面又怕突然刮风、下雨。即使人在家里,也可能因为忙于其他事情而忘记收衣服。

智能晾衣架要解决的核心问题,就是把“晾晒”这件事从人工判断变成自动决策。系统通过传感器感知当前是否下雨、光照是否充足、环境温湿度是否适合晾晒,然后自动控制电机把晾衣杆伸出或收回。同时还保留手动控制能力,方便用户随时干预。

从这个角度看,智能晾衣架并不需要多高深的人工智能,它本质上是“传感器数据采集 + 逻辑判断 + 电机驱动”的典型嵌入式控制系统。

1.2 功能需求分析

一个完整的智能晾衣架控制系统,通常包含以下功能:

  • 自动模式:根据雨滴传感器、光敏传感器、温湿度传感器的数据,自动判断是否伸出晾衣杆或收回晾衣杆。
  • 手动模式:通过按键主动控制晾衣杆伸出、收回、停止。
  • 状态显示:实时显示当前工作模式、传感器数值、电机状态,方便调试和确认。
  • 声光提示:在模式切换、动作执行、异常报警时,通过蜂鸣器或 LED 提示用户。
  • 安全保护:电机运行到极限位置时停止,避免机械结构损坏;预留限位开关接口。

1.3 技术选型

主控选用 STM32F103C8T6,这是意法半导体推出的基于 ARM Cortex-M3 内核的经典芯片,主频最高 72MHz,拥有丰富的 GPIO、定时器、ADC、USART、I2C、SPI 外设。它价格便宜、资料多、上手门槛低,非常适合这类中小型控制系统。

传感器方面:

  • 雨滴传感器模块,输出模拟量和数字量,用于检测是否下雨。
  • 光敏电阻模块,输出模拟量,用于检测光照强度。
  • DHT11 温湿度传感器,单总线协议,用于获取环境温湿度。
  • LCD1602 液晶屏,用于显示数据和状态。
  • 直流减速电机与 L298N 驱动模块,用于带动晾衣杆伸缩。

2. 系统总体设计与硬件选型

2.1 系统整体架构

整个系统可以划分为四层:

  1. 感知层:雨滴传感器、光敏传感器、DHT11 温湿度传感器。
  2. 主控层:STM32F103C8T6,负责数据采集、状态判断、电机控制逻辑。
  3. 执行层:L298N 电机驱动模块、直流减速电机、蜂鸣器、LED 指示灯。
  4. 交互层:LCD1602 显示屏、独立按键。

数据流向如下:

传感器把物理量转换成电信号,STM32 通过 ADC 或 GPIO 读取数据;主控根据预设的阈值和当前工作模式,决定是伸出还是收回晾衣杆;电机驱动模块接收控制信号后驱动直流电机正转或反转;同时 LCD 实时刷新显示内容,蜂鸣器在动作完成或异常时提醒用户。

2.2 硬件器件清单

器件名称型号/规格数量说明
STM32 最小系统板STM32F103C8T61主控核心板
雨滴传感器模块LM393 比较器输出1检测雨水,模拟量+数字量
光敏电阻模块4 针输出1检测环境光照强度
温湿度传感器DHT111单总线数字温湿度
LCD 液晶屏LCD1602 + IIC 转接板1显示模式与状态
电机驱动模块L298N1驱动直流电机正反转
直流减速电机3V-6V 减速电机1带动晾衣杆伸缩
独立按键轻触按键3手动控制
蜂鸣器有源蜂鸣器1声音提示
电源5V/2A 适配器1系统供电

这里说明一下,LCD1602 常见有直接并口和 I2C 转接板两种。为了节省 GPIO,推荐使用 I2C 转接板版本,只占用 SCL 和 SDA 两根线。下面代码也按照 I2C LCD1602 编写。

2.3 引脚分配规划

引脚分配直接影响代码编写和接线,建议先在表格里规划好。

功能引脚外设/说明
雨滴传感器 AOPA0ADC1_IN0,读取模拟量
光敏电阻 AOPA1ADC1_IN1,读取模拟量
DHT11 数据PA4GPIO 输出/输入切换,模拟单总线
L298N IN1PA6GPIO 输出,电机正转
L298N IN2PA7GPIO 输出,电机反转
L298N ENAPB0TIM3_CH3,PWM 调速
I2C LCD SCLPB8I2C1_SCL
I2C LCD SDAPB9I2C1_SDA
蜂鸣器PB5GPIO 输出
手动/自动切换按键PB3GPIO 输入,下拉/外部中断
伸出按键PB4GPIO 输入
收回/停止按键PB6GPIO 输入
板载 LEDPC13状态指示

需要注意,PB3 和 PB4 默认是 JTAG 引脚(JTDO、JNTRST),如果要在普通 GPIO 模式下使用,必须在 STM32CubeMX 中将调试接口配置为 Serial Wire,否则这两个引脚无法作为普通输入使用。这也是新手比较容易踩的坑。

3. 开发环境准备与工程配置

3.1 开发工具说明

本项目使用 ST 官方推出的 STM32CubeMX 进行初始化配置,它可以自动生成底层初始化代码,包括时钟树、GPIO、ADC、定时器、I2C 等外设配置,大大减少手写寄存器或繁琐库函数初始化的时间。

主要开发环境:

  • STM32CubeMX:用于生成工程代码,版本可以根据自己的情况选择,新版界面差别不大。
  • Keil MDK-ARM:用于代码编写、编译和下载,推荐使用 Keil MDK 5 以上版本。
  • ST-Link 下载器:用于给 STM32 下载程序。
  • 串口调试助手:可选,用于打印调试信息。

关于库函数,本文使用 HAL 库。HAL 库的优势是代码分层清晰,外设句柄封装完整,配合 CubeMX 生成的工程可以直接往里添加业务代码,很适合项目开发和学习。

3.2 STM32CubeMX 创建工程的基本步骤

打开 STM32CubeMX,点击 New Project,在 MCU 选择界面输入 STM32F103C8,选择具体芯片 STM32F103C8Tx 即可。

进入配置界面后,重点做以下几项设置:

3.2.1 RCC 时钟配置

在 System Core -> RCC 中,将 HSE 设置为 Crystal/Ceramic Resonator。如果核心板上没有外部晶振,也可以直接使用内部 HSI 时钟,但为了串口波特率准确,建议优先使用外部高速晶振。

3.2.2 SYS 调试接口配置

在 System Core -> SYS 中,将 Debug 设置为 Serial Wire。这一步一定要做,否则 PB3、PB4 不能被正常使用,同时也会影响后续 ST-Link 调试。

3.2.3 GPIO 配置

在 GPIO 界面中,根据引脚分配表,把以下引脚配置为输出模式:

  • PA6、PA7、PB5、PC13

输入模式:

  • PB3、PB4、PB6

这里要注意,按键引脚建议使能内部上拉或下拉电阻,具体取决于按键外部接法。如果按键一端接 GND,按下为低电平,则配置为内部上拉;如果按键一端接 3.3V,按下为高电平,则配置为内部下拉。

3.2.4 ADC 配置

在 Analog -> ADC1 中,使能两个通道:

  • IN0 对应 PA0,采集雨滴传感器
  • IN1 对应 PA1,采集光敏传感器

配置为独立模式,扫描模式可以根据需要开启,采样分辨率选择 12 位,与其他默认参数保持一致即可。

3.2.5 定时器 PWM 配置

在 Timers -> TIM3 中:

  • Channel3 选择 PWM Generation CH3
  • Prescaler 设置为 71
  • Counter Period 设置为 999

这样在 72MHz 主频下,PWM 频率为 72MHz / (71+1) / (999+1) = 1kHz,占空比可以在 0% 到 100% 之间调节,适合直流电机调速。

3.2.6 I2C 配置

在 Connectivity -> I2C1 中:

  • 选择 I2C 模式
  • 默认速度 100kHz 即可

LCD1602 的 I2C 转接板通常采用 PCF8574 芯片,地址默认为 0x27 或 0x3F。写代码时可以用 I2C 扫描方法确认地址。

3.3 生成代码

配置完成后,在 Project Manager 中设置工程名称、保存路径、工具链为 MDK-ARM V5,然后点击 GENERATE CODE。生成的代码会自动包含所有初始化函数,接下来就是在 main.c 和自定义模块文件中添加业务逻辑。

如果你使用的是 Keil MDK,还需要在魔术棒 Options for Target 中选择正确的 Debug 工具(ST-Link Debugger),并在 Settings 中确认能识别到芯片。

4. 核心模块驱动实现

这一节是整个项目的核心。每个模块我都会说明驱动原理,并给出可直接移植到 HAL 库工程中的代码。

4.1 雨滴传感器驱动

雨滴传感器模块通过 PCB 上裸露的叉指电极检测水滴。没有水时,电极之间的电阻很大,输出电压较高;有水时,电极之间被水桥接,电阻变小,输出电压降低。

模块通常有两个输出:

  • AO:模拟电压输出,连接到 STM32 的 ADC 引脚。
  • DO:数字电平输出,可以通过电位器调节灵敏度阈值。

在本项目中,我们读取 AO 的模拟电压,阈值可在代码里灵活调整,这样比固定数字输出更稳定。

ADC 读取代码可以直接使用 HAL 库函数:

// 文件路径:Src/adc_utils.c #include "adc_utils.h" uint16_t ADC_GetValue(ADC_HandleTypeDef *hadc, uint32_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; uint16_t adc_value = 0; sConfig.Channel = channel; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc, &sConfig); HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 10); adc_value = HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); return adc_value; }

在这个函数中,每次读取前都重新配置通道,方便在多通道之间切换。雨滴传感器的电压值经过 STM32 ADC 转换后,是一个 0 到 4095 的数值。数值越低,表示雨水越多。

在主循环中,可以这样获取雨滴值:

uint16_t rain_value = ADC_GetValue(&hadc1, ADC_CHANNEL_0);

注意,HAL_ADC_PollForConversion 的超时时间不能太短,第一次采集时 ADC 可能需要几个时钟周期完成稳定,一般 10ms 足够。

4.2 光敏电阻驱动

光敏电阻模块同样是输出电压随着光照强度变化。光线越强,光敏电阻阻值越低,AO 输出电压越低;光线越弱,电阻越大,输出电压越高。不同厂家的模块电压范围会有差异,所以最好实际测试一下。

光敏电阻 ADC 读取方式与雨滴传感器完全一致,只是通道不同:

uint16_t light_value = ADC_GetValue(&hadc1, ADC_CHANNEL_1);

两种传感器都使用 ADC 后,主循环里读取数据时要注意通道切换顺序。每次调用 ADC_GetValue 函数内部都会重新配置通道,因此不会产生冲突。

4.3 DHT11 温湿度传感器驱动

DHT11 是单总线数字传感器,数据引脚一次传输 40 位数据,包括湿度整数部分、湿度小数部分、温度整数部分、温度小数部分和校验和。它的时序要求比较严格,使用 HAL 库时通常直接用 GPIO 模拟时序,不依赖硬件外设。

下面是一个基于 HAL 库 GPIO 模拟 DHT11 采集的完整驱动。

先看头文件:

// 文件路径:Inc/dht11.h #ifndef __DHT11_H #define __DHT11_H #include "main.h" typedef struct { uint8_t humidity_int; uint8_t humidity_deci; uint8_t temperature_int; uint8_t temperature_deci; uint8_t checksum; uint8_t is_valid; } DHT11_Data_t; void DHT11_Init(void); uint8_t DHT11_ReadData(DHT11_Data_t *data); #endif

再看源文件:

// 文件路径:Src/dht11.c #include "dht11.h" #include "delay.h" #define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_4 static void DHT11_Start(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置为推挽输出 GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); // 主机拉低至少 18ms,然后释放总线 HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(30); } static uint8_t DHT11_ReadByte(void) { uint8_t value = 0; uint8_t i; for (i = 0; i < 8; i++) { while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 等待低电平结束 delay_us(40); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { value |= (0x80 >> i); } while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); // 等待高电平结束 } return value; } static uint8_t DHT11_ReadRaw(DHT11_Data_t *data) { GPIO_InitTypeDef GPIO_InitStruct = {0}; uint8_t buffer[5] = {0}; uint8_t i; // 配置为浮空输入 GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET) { return 0; } // 等待 DHT11 响应低电平 while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 读取 40 位数据 for (i = 0; i < 5; i++) { buffer[i] = DHT11_ReadByte(); } // 配置为输出模式,便于下一次启动 GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); >__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 800);

这里 800 对应 80% 占空比。电机启动时,可以先用比较低的占空比,再逐步升高,避免瞬间冲击电流导致复位。

电机动作控制函数:

// 文件路径:Src/motor.c #include "motor.h" void Motor_Forward(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 800); } void Motor_Backward(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 800); } void Motor_Stop(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 0); }

在实际使用中,电机伸缩到极限位置后必须马上停止,否则机械结构会卡死,堵转电流还可能烧坏驱动模块或电机。最简单的办法是加限位开关。

4.5 按键输入驱动

按键用于手动控制。考虑到实时性,可以使用查询方式扫描,也可以使用外部中断。对于课程设计项目,查询方式已经足够。

按键扫描要考虑“软件消抖”。机械按键在按下和释放的瞬间会产生抖动,通常持续 5ms 到 20ms。最简单的消抖方法是检测到低电平后延时 10ms 到 20ms,再确认一次电平状态。

下面是一个简单的按键扫描示例:

// 文件路径:Src/key.c #include "key.h" #include "delay.h" uint8_t Key_Scan(void) { if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET) { delay_ms(10); if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET) { while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET); return KEY_MODE; // 手动/自动切换 } } if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4) == GPIO_PIN_RESET) { delay_ms(10); if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4) == GPIO_PIN_RESET) { while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4) == GPIO_PIN_RESET); return KEY_OUT; // 伸出 } } if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6) == GPIO_PIN_RESET) { delay_ms(10); if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6) == GPIO_PIN_RESET) { while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6) == GPIO_PIN_RESET); return KEY_IN; // 收回 } } return KEY_NONE; }

上面扫描函数是阻塞式的,按下后要等按键释放才会返回,好处是逻辑清晰,坏处是长按或干扰时主循环会被卡住。实际项目中如果按键数量多,建议改成状态机扫描方式。

4.6 I2C LCD1602 显示驱动

LCD1602 是 16 字符 x 2 行的字符型液晶屏。配合 I2C 转接板后,只需要两根信号线就能控制。常见的 I2C 转接板使用 PCF8574 芯片,寄存器地址默认是 0x27,也有一部分是 0x3F。

以下代码基于 HAL 库 I2C 实现基本的 LCD1602 显示功能。

// 文件路径:Inc/lcd1602_i2c.h #ifndef __LCD1602_I2C_H #define __LCD1602_I2C_H #include "main.h" void LCD1602_Init(I2C_HandleTypeDef *hi2c); void LCD1602_SetCursor(uint8_t row, uint8_t col); void LCD1602_PrintString(char *str); void LCD1602_Clear(void); #endif
// 文件路径:Src/lcd1602_i2c.c #include "lcd1602_i2c.h" static I2C_HandleTypeDef *lcd_i2c; static uint8_t lcd_addr = 0x27 << 1; static void LCD1602_WriteCmd(uint8_t cmd); static void LCD1602_WriteData(uint8_t data); static void LCD1602_PulseEnable(uint8_t data); static void LCD1602_SendByte(uint8_t data, uint8_t mode); static void LCD1602_SendByte(uint8_t data, uint8_t mode) { uint8_t high_nibble = (data & 0xF0) | mode | 0x08; uint8_t low_nibble = ((data << 4) & 0xF0) | mode | 0x08; uint8_t buf[4]; buf[0] = high_nibble | 0x04; // EN 拉高 buf[1] = high_nibble; // EN 拉低 buf[2] = low_nibble | 0x04; // EN 拉高 buf[3] = low_nibble; // EN 拉低 HAL_I2C_Master_Transmit(lcd_i2c, lcd_addr, buf, 4, 100); } static void LCD1602_WriteCmd(uint8_t cmd) { LCD1602_SendByte(cmd, 0x00); } static void LCD1602_WriteData(uint8_t data) { LCD1602_SendByte(data, 0x01); } void LCD1602_Init(I2C_HandleTypeDef *hi2c) { lcd_i2c = hi2c; HAL_Delay(50); // 4 位模式初始化时序 LCD1602_SendByte(0x30, 0); HAL_Delay(5); LCD1602_SendByte(0x30, 0); HAL_Delay(5); LCD1602_SendByte(0x30, 0); HAL_Delay(5); LCD1602_SendByte(0x20, 0); HAL_Delay(5); LCD1602_WriteCmd(0x28); // 4 位模式,2 行,5x7 点阵 LCD1602_WriteCmd(0x0C); // 显示开,光标关 LCD1602_WriteCmd(0x01); // 清屏 HAL_Delay(2); LCD1602_WriteCmd(0x06); // 光标右移,整体不移动 } void LCD1602_SetCursor(uint8_t row, uint8_t col) { uint8_t addr = 0x80 + (row * 0x40) + col; LCD1602_WriteCmd(addr); } void LCD1602_PrintString(char *str) { while (*str) { LCD1602_WriteData((uint8_t)(*str)); str++; } } void LCD1602_Clear(void) { LCD1602_WriteCmd(0x01); HAL_Delay(2); }

I2C LCD 驱动有几个细节要注意:

  • PCF8574 的 P0 到 P3 对应数据线 D4 到 D7,P4 对应 RS,P5 对应 RW,P6 对应 E,P7 对应背光。
  • 发送数据时,RS 位要区分是命令还是数据。
  • E 引脚(使能)需要一高一低的脉冲,LCD 才会采样数据。
  • 初始化可以只用 4 位模式,但必须先按照 HD44780 规定的初始化时序发送 0x30 和 0x20。

如果你拿到的是 0x3F 地址的模块,只需要把lcd_addr改成0x3F << 1即可。如果屏幕没有任何反应,优先确认 I2C 地址和接线。

5. 完整控制逻辑与主程序

5.1 主程序总体框架

主程序负责把各个模块串起来。整体流程如下:

  1. 系统初始化:HAL 初始化、时钟、GPIO、ADC、定时器、I2C、LCD、DHT11。
  2. 进入主循环:
    • 读取传感器数据。
    • 判断当前模式(自动/手动)。
    • 自动模式下,根据传感器状态决定是否伸出或收回。
    • 手动模式下,根据按键控制电机。
    • 刷新 LCD 显示。

来看 main.c 中的主循环代码:

// 文件路径:Src/main.c 主循环片段 #include "main.h" #include "adc_utils.h" #include "dht11.h" #include "motor.h" #include "key.h" #include "lcd1602_i2c.h" I2C_HandleTypeDef hi2c1; TIM_HandleTypeDef htim3; ADC_HandleTypeDef hadc1; typedef enum { MODE_AUTO, MODE_MANUAL } WorkMode_t; static WorkMode_t g_mode = MODE_AUTO; static DHT11_Data_t g_dht11; static uint16_t g_rain_value = 0; static uint16_t g_light_value = 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_TIM3_Init(); MX_I2C1_Init(); LCD1602_Init(&hi2c1); DHT11_Init(); LCD1602_PrintString("Smart Clothes Rack"); HAL_Delay(1000); LCD1602_Clear(); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); while (1) { // 每 2 秒读取一次 DHT11,其他传感器每次循环读取 g_rain_value = ADC_GetValue(&hadc1, ADC_CHANNEL_0); g_light_value = ADC_GetValue(&hadc1, ADC_CHANNEL_1); if (HAL_GetTick() - last_dht_time > 2000) { DHT11_ReadData(&g_dht11); last_dht_time = HAL_GetTick(); } uint8_t key = Key_Scan(); if (key == KEY_MODE) { if (g_mode == MODE_AUTO) { g_mode = MODE_MANUAL; } else { g_mode = MODE_AUTO; } HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); LCD1602_Clear(); } if (g_mode == MODE_AUTO) { AutoRackControl(); } else { ManualRackControl(key); } UpdateDisplay(); HAL_Delay(50); } }

主循环的节奏控制在 50ms 左右一次,既能及时响应按键,又不会因为传感器读取太频繁导致系统卡顿。

5.2 自动收衣/晾衣逻辑

自动模式是整个项目的核心。判断逻辑需要结合雨滴、光照、湿度三个因素,并考虑“防误动作”。

这里有一个工程上很关键的思想:不要因为传感器的一次快速跳变就立刻执行电机动作,而应该加上抖动判断和延时确认。

来看自动控制函数:

// 文件路径:Src/control.c static uint8_t rain_count = 0; static uint8_t no_rain_count = 0; void AutoRackControl(void) { // 雨滴 ADC 值越低表示雨越大,这里定义阈值为 2000 int is_raining = (g_rain_value < 2000); // 光线 ADC 值越低表示光照越强,这里定义阈值为 2500 int is_daylight = (g_light_value < 2500); // DHT11 湿度大于 75% 时认为环境潮湿,可能正在下雨或刚下过雨 int is_humid = (g_dht11.humidity_int > 75); if (is_raining || (is_humid && !is_daylight)) { rain_count++; no_rain_count = 0; if (rain_count >= 5) { // 收回晾衣杆 Motor_Backward(); HAL_Delay(3000); Motor_Stop(); LCD1602_Clear(); LCD1602_SetCursor(0, 0); LCD1602_PrintString("Auto: Raining !"); LCD1602_SetCursor(1, 0); LCD1602_PrintString("Retract..."); rain_count = 0; } } else { no_rain_count++; rain_count = 0; if (no_rain_count >= 5 && is_daylight) { // 伸出晾衣杆 Motor_Forward(); HAL_Delay(3000); Motor_Stop(); LCD1602_Clear(); LCD1602_SetCursor(0, 0); LCD1602_PrintString("Auto: Sunny !"); LCD1602_SetCursor(1, 0); LCD1602_PrintString("Extend..."); no_rain_count = 0; } } }

在这个逻辑中,连续 5 次判断满足条件才会执行动作,大约相当于 250ms 的确认时间。这样即使雨滴传感器误触发一次,或者光敏模块被飞鸟短暂遮挡,也不会导致电机频繁动作。

5.3 手动控制与停止保护

手动模式下,用户通过按键控制电机。每次按键触发一次动作,电机运行固定时间后自动停止,避免持续运行损坏电机。

void ManualRackControl(uint8_t key) { if (key == KEY_OUT) { Motor_Forward(); HAL_Delay(3000); Motor_Stop(); LCD1602_Clear(); LCD1602_SetCursor(0, 0); LCD1602_PrintString("Manual: Extend"); } else if (key == KEY_IN) { Motor_Backward(); HAL_Delay(3000); Motor_Stop(); LCD1602_Clear(); LCD1602_SetCursor(0, 0); LCD1602_PrintString("Manual: Retract"); } }

这种固定时间控制实现起来最简单,但是精度不高。如果晾衣杆已经到位,电机还在转,就容易堵转。更稳妥的做法是在伸缩轨道两端安装限位开关,行程到位后立刻断开电机控制信号。

使用限位开关后,电机动作逻辑会变成:

  1. 执行伸出动作。
  2. 检查限位开关是否触发。
  3. 触发后立刻停止。
  4. 主程序根据开关状态决定后续动作。

5.4 LCD 实时信息显示

显示函数把当前模式、传感器数据和电机状态实时刷新到 LCD1602 上。每行显示的内容需要格式化。

void UpdateDisplay(void) { char line1[16]; char line2[16]; if (g_mode == MODE_AUTO) { snprintf(line1, sizeof(line1), "AUTO R:%d", g_rain_value); snprintf(line2, sizeof(line2), "L:%d %d.%dC", g_light_value, g_dht11.temperature_int, g_dht11.temperature_deci); } else { snprintf(line1, sizeof(line1), "MANUAL R:%d", g_rain_value); snprintf(line2, sizeof(line2), "L:%d H:%d%%", g_light_value, g_dht11.humidity_int); } LCD1602_SetCursor(0, 0); LCD1602_PrintString(line1); LCD1602_SetCursor(1, 0); LCD1602_PrintString(line2); }

由于 LCD1602 只有 16 个字符宽度,字符串要控制在 16 字节以内。使用 snprintf 时要注意目标数组大小,避免越界。

6. 功能调试与实物验证

6.1 编译与下载

代码写完后,在 Keil 中点击 Build 按钮编译。常见错误包括:

  • 头文件路径没有添加,导致找不到 dht11.h、motor.h 等文件。
  • HAL_TIM_PWM_Start 没有被调用,导致 PWM 引脚无输出。
  • ADC 多通道配置不正确,导致读取值始终为 0。

编译通过后,用 ST-Link 连接 STM32 最小系统板,点击 Download 下载程序。下载成功后再按一下复位按键,程序开始运行。

6.2 传感器阈值调试

传感器阈值不建议直接照抄,因为不同模块输出电压范围不同。我的经验是先用串口把 ADC 原始值打印出来,观察无雨、小雨、大雨时的数值范围,再确定阈值。

比如我测试时:

场景雨滴传感器 ADC 值
干燥3900 左右
滴少量水2500 左右
水量增大1200 以下

所以把下雨阈值定在 2000 比较合适。光敏模块也采用同样方式。

6.3 功能验证清单

以下是建议的完整测试项:

测试项操作预期结果
手动伸出手动模式,按伸出键电机正转 3 秒后停止,LCD 显示 Extend
手动收回手动模式,按收回键电机反转 3 秒后停止,LCD 显示 Retract
模式切换按模式切换键LCD 在 AUTO 和 MANUAL 之间切换
雨天自动收回自动模式,在雨滴传感器上滴水电机收回,LCD 显示 Raining
晴天自动伸出自动模式,遮挡光敏后移开电机伸出,LCD 显示 Sunny
断电重启重新上电系统回到默认自动模式

7. 常见问题与排查思路

7.1 LCD1602 不显示

这个问题出现的频率非常高。可以先排查以下几个方面:

  • 确认 I2C 地址:用 I2C 扫描程序打印设备地址,确认是 0x27 还是 0x3F。
  • 确认接线:SCL 接 PB8,SDA 接 PB9,VCC 接 5V,GND 接 GND。
  • 确认对比度电位器:部分 LCD1602 模块上有一个可调电阻,需要旋转它调整对比度,否则即使程序正常也可能看不到字。
  • 确认背光:如果转接板上没有跳线或默认不使能背光,屏幕会非常暗,看起来像没显示。

7.2 DHT11 读取失败或一直返回 0

DHT11 时序比较敏感。如果信号线过长,或者使用了过长的杜邦线,就可能导致波形失真。排查思路:

  • 缩短 DHT11 与 STM32 之间的连线,最好控制在 20cm 以内。
  • 数据引脚和 VCC 之间加上 4.7k 上拉电阻。部分模块自带电阻,但独立传感器可能需要外接。
  • DHT11 两次读取之间必须间隔至少 1 秒,连续快速读取容易失败。
  • 校验失败时可以连续重试,代码里已经做了 5 次重试,如果还失败,检查接线和供电。

7.3 电机不转或只单方向转

电机不转通常有三个原因:

  • 电源功率不够:STM32 和电机共用电源时,电机启动瞬间电流很大,可能拉低电压导致系统复位。建议电机单独使用 5V/2A 适配器,并与 STM32 共地。
  • PWM 没启动:确认调用过 HAL_TIM_PWM_Start。
  • IN1/IN2 电平组合不对:用万用表测量 PA6、PA7 输出电平,确认代码执行到了电机动作分支。

7.4 系统频繁复位

系统复位多数是电源问题。电机堵转电流能达到 1A 以上,如果电源输出能力不足,电压跌落会让 STM32 掉电复位。解决方法是:

  • 使用稳压能力更好的 5V/2A 以上电源。
  • 在电机电源两端并联 100uF 或 470uF 电解电容。
  • 将电机地和单片机地单点共地,减少干扰。

7.5 按键误触发

按键误触发主要原因是没有消抖,或者引脚悬空。处理办法:

  • 软件消抖延时从 10ms 增加到 20ms。
  • 如果按键一端悬空,要检查外部上拉/下拉是否正确。
  • 使用短一点的杜邦线,减少外界电磁干扰。

8. 最佳实践与工程建议

8.1 代码分层设计

虽然在课程设计中主程序可以直接写一大段,但为了项目可维护性和后续扩展,建议按照模块拆分:

  • app层:主循环、模式切换、控制逻辑。
  • driver层:dht11.c、lcd1602_i2c.c、motor.c、key.c。
  • 公共层:delay.c、adc_utils.c。

这样以后换传感器或者增加新功能,只需要修改对应模块,不影响主程序框架。

8.2 阈值做成可配置参数

雨滴阈值、光敏阈值、电机运行时间这些参数,不要硬编码在业务逻辑里。建议统一放到一个头文件中,或者使用结构体保存。

// 文件路径:Inc/config.h #ifndef __CONFIG_H #define __CONFIG_H #define RAIN_THRESHOLD 2000 #define LIGHT_THRESHOLD 2500 #define HUMID_THRESHOLD 75 #define MOTOR_RUN_TIME_MS 3000 #define SENSOR_CONFIRM_COUNT 5 #endif

这样后期调试只需要改配置头文件,不需要在控制逻辑里到处找数字。

8.3 增加看门狗提高稳定性

如果项目需要长时间运行,建议开启独立看门狗 IWDG。主循环正常运行时,周期性喂狗;一旦程序跑飞或卡死,看门狗会自动复位系统。

// 在主循环中调用 HAL_IWDG_Refresh(&hiwdg);

开启看门狗后,调试时要注意 HAL_Delay 或者 while 等待按键释放不能太久,否则喂狗超时会导致系统不断重启。

8.4 安全边界与电气防护

本项目属于低压电子制作,但电机驱动模块和电源部分仍然要注意安全:

  • 所有接线先断开电源再进行,避免带电操作。
  • 直流电机没有堵转保护,必须有行程限位或超时停止机制。
  • 如果以后扩展到 220V 交流电机或烘干加热模块,必须增加隔离电路,并且严格遵循强电安全规范,在产品化设计中需要由专业电气工程师参与。
  • 传感器线缆和电机线缆不要捆扎在一起,避免电机启停时产生的电磁干扰影响传感器读数。

8.5 数据记录与调试接口

建议在开发阶段固定保留一个串口输出。通过串口打印完整的传感器数据和状态切换信息,调试起来会轻松很多。

printf("rain=%d light=%d temp=%d hum=%d mode=%d\r\n", g_rain_value, g_light_value, g_dht11.temperature_int, g_dht11.humidity_int, g_mode);

使用串口打印时,记得在 CubeMX 中把 USART1 打开,并将 printf 重定向到串口。

9. 开源与后续扩展方向

9.1 项目开源的意义

这个项目非常适合作为开源项目发布。一方面,STM32F103 生态非常成熟,大量学生和入门工程师都需要完整的、可运行的示例,而不是零散的代码片段;另一方面,智能晾衣架虽然是课程设计常见题目,但要把传感器融合、电机控制、模式切换、显示交互做好,仍然需要不少工程细节。

开源代码时,建议在仓库中同时提供:

  • STM32CubeMX 工程文件。
  • Keil MDK 工程源码。
  • README 文档,写明硬件接线表、引脚分配、阈值配置方法。
  • 实物演示视频或图片。

许可证可以选择 MIT 或 GPL 等常见协议。如果希望代码被更多项目复用,MIT 更宽松;如果希望后续改进也保持开源,可以选择 GPL。

9.2 可扩展方向

在设计这个系统时,我把所有可扩展的接口都预留出来了,后续可以继续完善这些方向:

9.2.1 增加物联网远程控制

使用 ESP8266 或 ESP32 模块,把 STM32F103 的传感器数据和控制状态通过 Wi-Fi 上传到物联网平台。用户可以在手机 App 上远程查看天气数据、手动控制晾衣架。通信方式可以是串口 AT 指令,也可以是 MQTT 协议。

9.2.2 使用 FreeRTOS 重构

当前主循环是前后台系统。如果功能变多,比如增加网络协议栈、多个传感器任务、屏幕刷新任务,前后台系统的实时性会变差。使用 FreeRTOS 可以把传感器采集、控制逻辑、显示刷新拆成独立任务,系统结构更清晰。

9.2.3 增加步进电机方案

直流减速电机加上限位开关虽然简单,但无法精确控制晾衣杆位置。如果改成步进电机,配合丝杆机构,可以实现位置闭环控制。STM32F103 的定时器可以输出多路 PWM,驱动步进电机也比较方便。

9.2.4 增加太阳能供电和低功耗设计

如果晾衣架放在室外,可以考虑太阳能电池板加锂电池供电方案。平时系统处于低功耗模式,定时唤醒采集传感器数据;检测到下雨再立刻唤醒电机执行收回动作。ST 官方提供了 STM32 的 STOP 模式低功耗方案,可以进一步降低系统平均功耗。

9.2.5 增加烘干和消毒模块

部分高端智能晾衣架在收回晾衣杆之后,会自动开启烘干热风、紫外线消毒灯。这就需要在控制系统中增加继电器模块,并且考虑温控保护,设置温度上限,避免过热风险。

9.3 如何参与和改进

如果你按这篇文章完成了基础版本,建议继续做以下几件事:

  • 把调试过程中遇到的坑和解决方法记录下来,补充到项目 README 中。
  • 测试不同品牌型号传感器模块的输出差异,记录实际阈值。
  • 做 100 次连续伸缩测试,验证机械结构和电机运行可靠性。
  • 尝试把自动判断逻辑优化成更平滑的策略,例如根据湿度变化趋势预测天气,而不是只看瞬时值。

一个开源项目的价值,往往不只是“代码可用”,而是“其他人拿到之后能复现、能理解、能改进”。这也正是我整理这篇教程的初衷:让每个拿到这个项目的开发者,都能快速跑起来,再根据自己的场景改造成更合适的产品原型。

如果你在搭建过程中遇到问题,建议先对照引脚分配表检查接线,再对照本节调试方法排查软件逻辑。抓住“传感器输入 -> 主控判断 -> 电机输出 -> 结果显示”这条主线,问题基本都能定位出来。代码在项目开源仓库中也会持续维护,当然如果你在使用中发现了可以优化的细节,也欢迎继续完善,这也是开源社区最有意思的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 19:03:16

Matlab实现3D直流电法正演:有限单元法全流程解析

简介&#xff1a;本资源是一套面向地球物理勘探初学者与科研人员的Matlab三维有限元直流电阻率正演模拟实践包&#xff0c;聚焦电阻率法正演建模核心能力训练&#xff0c;适用于水文地质调查、矿产勘查及工程地质建模等实际场景。压缩包含99个文件&#xff08;1001KB&#xff0…

作者头像 李华
网站建设 2026/9/1 19:00:02

STM32F407嵌入式AI实战:手写数字识别从PyTorch到C代码部署

简介&#xff1a;本资源是面向嵌入式初学者与STM32进阶开发者的手写数字识别实战项目&#xff0c;聚焦于在资源受限的STM32F407系列MCU上部署轻量级图像识别功能&#xff0c;适用于智能终端、人机交互界面及IoT边缘识别等场景。压缩包共635个文件&#xff0c;含279个C源码&…

作者头像 李华
网站建设 2026/9/1 18:59:33

Java+SpringBoot+Vue+MySQL美妆购物网站毕设全流程解析

简介&#xff1a;这是一套面向计算机专业本科生的高分毕业设计级美妆购物网站实战项目&#xff0c;适用于毕设开题、课程设计与期末大作业&#xff0c;解决电商类系统从需求分析到部署落地的全流程实践难题。资源包共839个文件&#xff0c;涵盖133个Java后端逻辑文件、76个Vue前…

作者头像 李华
网站建设 2026/9/1 18:58:40

SAR ADC行为级建模实战:基于Matlab的模型搭建与性能评估

简介&#xff1a;本资源是一套面向电子工程与集成电路设计初学者的SAR ADC建模实践材料&#xff0c;聚焦MATLAB环境下的逐次逼近型模数转换器原理仿真与性能分析&#xff0c;解决理论理解与算法实现脱节的问题。压缩包共2个文件&#xff08;1个MATLAB源码文件、1份PDF技术文档&…

作者头像 李华
网站建设 2026/9/1 18:58:27

2038危机:32位Unix时间戳溢出原理与迁移实践

时间戳是计算机里最容易被忽略、又最容易引发事故的基础数据之一。你可能每天都在使用毫秒时间戳调接口、存订单、做倒计时&#xff0c;却很少会想&#xff1a;如果存储时间戳的整数类型只有 32 位&#xff0c;会发生什么&#xff1f;答案就是 2038 危机。所谓 2038 危机&#…

作者头像 李华