ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

基于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 系统整体架构整个系统可以划分为四层感知层雨滴传感器、光敏传感器、DHT11 温湿度传感器。主控层STM32F103C8T6负责数据采集、状态判断、电机控制逻辑。执行层L298N 电机驱动模块、直流减速电机、蜂鸣器、LED 指示灯。交互层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_CH3PWM 调速I2C LCD SCLPB8I2C1_SCLI2C 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 CH3Prescaler 设置为 71Counter Period 设置为 999这样在 72MHz 主频下PWM 频率为 72MHz / (711) / (9991) 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 到 D7P4 对应 RSP5 对应 RWP6 对应 EP7 对应背光。发送数据时RS 位要区分是命令还是数据。E 引脚使能需要一高一低的脉冲LCD 才会采样数据。初始化可以只用 4 位模式但必须先按照 HD44780 规定的初始化时序发送 0x30 和 0x20。如果你拿到的是 0x3F 地址的模块只需要把lcd_addr改成0x3F 1即可。如果屏幕没有任何反应优先确认 I2C 地址和接线。5. 完整控制逻辑与主程序5.1 主程序总体框架主程序负责把各个模块串起来。整体流程如下系统初始化HAL 初始化、时钟、GPIO、ADC、定时器、I2C、LCD、DHT11。进入主循环读取传感器数据。判断当前模式自动/手动。自动模式下根据传感器状态决定是否伸出或收回。手动模式下根据按键控制电机。刷新 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); } }这种固定时间控制实现起来最简单但是精度不高。如果晾衣杆已经到位电机还在转就容易堵转。更稳妥的做法是在伸缩轨道两端安装限位开关行程到位后立刻断开电机控制信号。使用限位开关后电机动作逻辑会变成执行伸出动作。检查限位开关是否触发。触发后立刻停止。主程序根据开关状态决定后续动作。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 接 PB8SDA 接 PB9VCC 接 5VGND 接 GND。确认对比度电位器部分 LCD1602 模块上有一个可调电阻需要旋转它调整对比度否则即使程序正常也可能看不到字。确认背光如果转接板上没有跳线或默认不使能背光屏幕会非常暗看起来像没显示。7.2 DHT11 读取失败或一直返回 0DHT11 时序比较敏感。如果信号线过长或者使用了过长的杜邦线就可能导致波形失真。排查思路缩短 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 次连续伸缩测试验证机械结构和电机运行可靠性。尝试把自动判断逻辑优化成更平滑的策略例如根据湿度变化趋势预测天气而不是只看瞬时值。一个开源项目的价值往往不只是“代码可用”而是“其他人拿到之后能复现、能理解、能改进”。这也正是我整理这篇教程的初衷让每个拿到这个项目的开发者都能快速跑起来再根据自己的场景改造成更合适的产品原型。如果你在搭建过程中遇到问题建议先对照引脚分配表检查接线再对照本节调试方法排查软件逻辑。抓住“传感器输入 - 主控判断 - 电机输出 - 结果显示”这条主线问题基本都能定位出来。代码在项目开源仓库中也会持续维护当然如果你在使用中发现了可以优化的细节也欢迎继续完善这也是开源社区最有意思的地方。
返回列表