你是不是也遇到过这样的场景:想给家里的鱼缸、温室大棚或者一个简单的恒温箱做个自动温控,但一查方案,要么是买现成的温控器(功能固定、价格不透明),要么就得从零开始学复杂的单片机编程,感觉无从下手?
其实,一个核心功能完备的温控系统,其硬件核心无非是“感知温度”和“执行开关”,软件核心则是“判断逻辑”。今天,我们就用最经典的STM32单片机、最常见的DHT11温湿度传感器和最通用的继电器模块,手把手带你实现一个功能完整、可手动调节阈值、代码清晰易懂的温控开关项目。
这个项目远不止是简单的“温度高了关,低了开”。我们将实现:
- 实时监测:通过DHT11持续获取环境温度。
- 动态控制:当温度超过设定上限时,自动打开继电器(模拟加热设备);当温度低于设定下限时,自动关闭继电器。
- 灵活交互:通过4个独立按键,可以随时查看、增加或减少上下限阈值,并将设置保存到STM32的Flash中,断电不丢失。
- 状态可视:通过串口打印实时温度、阈值及继电器状态,方便调试。
你会发现,搞懂了这个项目,你不仅学会了DHT11和继电器的驱动,更掌握了嵌入式开发中状态机编程、非阻塞式按键处理、数据持久化存储这几个至关重要的工程化思维。这比单纯点个灯、读个传感器值,要实用得多。
1. 项目核心要解决什么问题?
很多STM32初学者在学习了GPIO、定时器、串口等基础外设后,会陷入一个迷茫期:这些知识怎么串联成一个有用的实际产品?网上很多教程是零散的,比如单独讲如何驱动DHT11,或者单独讲如何控制继电器,但很少告诉你如何将它们有机结合起来,并加入关键的人机交互和逻辑控制。
本项目的目标,就是填补这个“从模块到系统”的鸿沟。我们将一个温控开关的需求,拆解为具体的、可实现的软件模块和硬件连接,并着重解决以下几个工程实践中的典型问题:
- 传感器数据读取的稳定性:DHT11采用单总线协议,时序要求严格,如何写出稳定、抗干扰的读取代码?
- 继电器控制的逻辑与安全:如何设计控制逻辑,避免在阈值临界点附近频繁开关(继电器抖动)?如何确保系统上电或异常时的安全状态?
- 友好的人机交互:如何用有限的按键(4个)实现查看、增加、减少阈值等多个功能?如何设计交互逻辑让用户感觉自然?
- 系统参数的持久化:用户设定的温度阈值,如何在单片机断电后依然保存?如何安全地读写STM32的内部Flash?
通过完成这个项目,你将获得一个可以直接用于小型恒温、散热报警等场景的原型核心,并且其代码框架可以轻松移植到其他需要“感知-判断-执行”的物联网或自动化控制项目中。
2. 核心硬件与原理简介
在动手写代码之前,我们必须先理解手中“武器”的特性与限制。
2.1 STM32:我们的大脑
我们以最常见的STM32F103C8T6(俗称“蓝桥杯”或“最小系统板”)为例。它基于ARM Cortex-M3内核,拥有足够的GPIO、定时器和Flash资源来完成本项目。我们将使用STM32CubeMX工具配合HAL库进行开发,这能极大简化初始化过程,让我们专注于应用逻辑。
在本项目中的角色:负责协调所有外设——读取传感器数据、处理按键输入、执行控制逻辑、保存用户设置、通过串口输出信息。
2.2 DHT11:我们的感官
DHT11是一款廉价的数字温湿度复合传感器。它输出的是已校准的数字信号,通过单总线(Single-Bus)与MCU通信。
- 关键特性:
- 测量范围:温度0-50°C(精度±2°C),湿度20-90%RH(精度±5%RH)。
- 单总线协议:仅需一根数据线即可通信,但需要严格的时序。
- 供电:3.3V-5.5V。
- 通信原理:
- MCU发起起始信号:拉低数据线至少18ms,然后拉高20-40µs,等待DHT11响应。
- DHT11响应:拉低80µs,再拉高80µs,表示准备发送数据。
- 数据传输:每位数据都以一个50µs的低电平起始位开始,随后高电平的持续时间决定数据位是0(26-28µs)还是1(70µs)。
- 数据格式:一次传输40位数据(8位湿度整数+8位湿度小数+8位温度整数+8位温度小数+8位校验和)。对于DHT11,小数部分通常为0。
重要提示:DHT11的时序要求非常严格,在编写驱动时,必须使用微秒级延时,并且要处理好中断的影响。HAL库的HAL_Delay()是毫秒级,不适用,我们需要用定时器或系统滴答定时器(SysTick)来实现微秒延时。
2.3 继电器模块:我们的手脚
继电器是一种电控开关,用小电流(来自STM32的GPIO)控制大电流电路的通断。
- 关键特性:
- 控制端:通常有VCC(接3.3V/5V)、GND、IN(信号输入)三个引脚。IN脚接STM32的GPIO。
- 被控端:通常有COM(公共端)、NO(常开端)、NC(常闭端)。我们一般使用COM和NO,当IN为低电平时,COM与NO断开;IN为高电平时,COM与NO吸合。
- 电平逻辑:常见模块有低电平触发和高电平触发两种。务必根据你的模块说明书确定!本文假设为高电平触发(IN给高电平,继电器吸合)。
- 安全警告:
- 继电器模块的控制端(VCC, GND, IN)和被控端(COM, NO, NC)是电气隔离的。严禁将控制端的GND与被控端电路的任何部分直接相连!
- 被控端连接的是强电(如220V交流)时,必须做好绝缘防护,操作时务必断电,并由具备资质的人员进行。本文为演示,被控端仅连接一个LED灯模拟负载。
2.4 4键按键模块:我们的交互界面
使用四个独立的轻触按键,分别定义为:
- KEY_UP:用于增加设定值。
- KEY_DOWN:用于减少设定值。
- KEY_LEFT:用于切换设置对象(如切换设置上限温度或下限温度)。
- KEY_RIGHT:用于确认/进入设置模式或退出。
我们将实现一个简单的状态机来管理按键功能,例如:短按KEY_RIGHT进入阈值设置模式,此时KEY_LEFT切换设置上限/下限,KEY_UP/KEY_DOWN调整数值,再次短按KEY_RIGHT保存并退出。
3. 开发环境与工程创建
硬件清单:
- STM32F103C8T6核心板 或 最小系统板 x1
- DHT11温湿度传感器模块 x1
- 继电器模块(高/低电平触发,需确认) x1
- 4键独立按键模块 或 4个独立轻触按键 x1
- USB转TTL串口模块(用于程序下载和调试输出) x1
- LED灯及220Ω电阻(模拟被控负载) x1
- 杜邦线 若干
- 面包板 或 焊接工具
软件环境:
- STM32CubeMX (v6.0+)
- Keil MDK-ARM (v5.30+) 或 STM32CubeIDE (v1.10+)
- 串口调试助手(如XCOM, Putty)
工程创建步骤:
- 启动STM32CubeMX,新建工程,选择你的MCU型号(如STM32F103C8)。
- 配置系统核心:
- SYS: Debug 选择
Serial Wire(如果要用ST-Link调试)。 - RCC: HSE 选择
Crystal/Ceramic Resonator(如果板载外部高速晶振)。
- SYS: Debug 选择
- 配置时钟树:根据你的晶振频率(通常8MHz),将HCLK配置到最大72MHz(对于F103)。
- 配置外设:
- USART1: 模式选择
Asynchronous,用于串口打印。波特率可设为115200。 - GPIO:
- 为DHT11的数据线(假设接PA0)配置一个推挽输出模式的引脚,初始输出高电平。同时,在代码中我们需要将其切换为输入模式来读取数据,这将在驱动函数里动态操作。
- 为继电器控制线(假设接PA1)配置为推挽输出模式,初始输出低电平(确保继电器初始为断开状态)。
- 为四个按键(假设接PA2-PA5)配置为上拉输入模式(如果外部有上拉电阻,也可配置为浮空输入)。
- 定时器:选择一个基本定时器(如TIM2)用于产生微秒级延时。配置预分频器和周期,使其每计数一次对应1微秒。例如,当系统时钟72MHz,预分频器设为72-1,则计数器每增加1,时间为1µs。
- USART1: 模式选择
- 生成工程:
- 在
Project Manager选项卡设置项目名称、路径、IDE(MDK-ARM或STM32CubeIDE)。 - 在
Code Generator中,选择“Copy only necessary library files”和“Generate peripheral initialization as a pair of ‘.c/.h’ files”。 - 点击
GENERATE CODE生成工程。
- 在
4. 核心驱动与逻辑代码实现
工程生成后,打开你的IDE。我们将在Core/Src和Core/Inc目录下添加和修改代码。
4.1 微秒延时函数
首先,我们需要一个精准的微秒延时函数,这是驱动DHT11的基础。我们利用在CubeMX中配置的定时器(如TIM2)来实现。
在main.c的/* USER CODE BEGIN 0 */部分添加以下代码:
// 微秒延时函数(基于TIM2) void delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim2, 0); // 清零计数器 HAL_TIM_Base_Start(&htim2); // 启动定时器 while (__HAL_TIM_GET_COUNTER(&htim2) < us); // 等待计数值达到设定值 HAL_TIM_Base_Stop(&htim2); // 停止定时器 }别忘了在/* USER CODE BEGIN Includes */部分包含#include "tim.h"。
4.2 DHT11驱动代码
创建两个文件:dht11.c和dht11.h,放在Core/Src和Core/Inc目录下。
dht11.h:
#ifndef __DHT11_H #define __DHT11_H #include "main.h" // 定义DHT11数据线连接的GPIO端口和引脚 #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_0 // 函数声明 void DHT11_Init(void); uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity); uint8_t DHT11_Read_Byte(void); uint8_t DHT11_Read_Bit(void); void DHT11_Reset(void); #endifdht11.c:
#include "dht11.h" #include "tim.h" // 为了使用delay_us // 设置数据线为输出模式 static void DHT11_IO_Out(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); } // 设置数据线为输入模式(上拉) static void DHT11_IO_In(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); } // 读取一个位 uint8_t DHT11_Read_Bit(void) { uint8_t data = 0; DHT11_IO_In(); // 等待低电平开始信号结束 (约50us) while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_RESET); // 测量高电平持续时间 delay_us(40); // 等待40us后采样 if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET) { data = 1; } // 等待该位的高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); return data; } // 读取一个字节 uint8_t DHT11_Read_Byte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { data <<= 1; data |= DHT11_Read_Bit(); } return data; } // 启动DHT11 void DHT11_Reset(void) { DHT11_IO_Out(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); HAL_Delay(20); // 拉低至少18ms HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); delay_us(30); // 拉高20-40us DHT11_IO_In(); // 等待DHT11响应低电平 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); // 等待DHT11响应高电平 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); } // 读取温湿度数据 uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity) { uint8_t buf[5]; uint8_t i; uint8_t checksum; DHT11_Reset(); // 如果等待响应超时,返回错误 if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET) { return 0; // 无响应 } // 读取5个字节(40位) for (i = 0; i < 5; i++) { buf[i] = DHT11_Read_Byte(); } // 校验和 checksum = buf[0] + buf[1] + buf[2] + buf[3]; if (checksum != buf[4]) { return 0; // 校验失败 } *humidity = buf[0]; *temperature = buf[2]; // DHT11温度小数部分通常为0,这里只取整数部分 return 1; // 读取成功 }4.3 按键扫描与状态机
我们采用非阻塞式、支持短按的按键扫描方式。在main.c中定义相关变量和函数。
/* USER CODE BEGIN PV */ // 定义按键引脚 (根据你的实际连接修改) #define KEY_UP_PIN GPIO_PIN_2 #define KEY_DOWN_PIN GPIO_PIN_3 #define KEY_LEFT_PIN GPIO_PIN_4 #define KEY_RIGHT_PIN GPIO_PIN_5 #define KEY_GPIO_PORT GPIOA // 按键状态枚举 typedef enum { SYS_NORMAL = 0, // 正常监控模式 SYS_SET_HIGH, // 设置高温阈值 SYS_SET_LOW // 设置低温阈值 } SysMode_t; // 全局变量 uint8_t current_temp = 0; uint8_t temp_high_threshold = 30; // 默认高温阈值 uint8_t temp_low_threshold = 20; // 默认低温阈值 SysMode_t sys_mode = SYS_NORMAL; uint8_t relay_state = 0; // 0: off, 1: on // 按键扫描函数(非阻塞) void Key_Scan(void) { static uint8_t key_up_last = 1, key_down_last = 1, key_left_last = 1, key_right_last = 1; uint8_t key_up_now = HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_UP_PIN); uint8_t key_down_now = HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_DOWN_PIN); uint8_t key_left_now = HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_LEFT_PIN); uint8_t key_right_now = HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_RIGHT_PIN); // 检测KEY_RIGHT短按(模式切换/确认) if (key_right_last == 1 && key_right_now == 0) { // 下降沿,按键按下 HAL_Delay(10); // 简单消抖 if (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_RIGHT_PIN) == 0) { if (sys_mode == SYS_NORMAL) { sys_mode = SYS_SET_HIGH; printf("Enter High Threshold Setting Mode.\r\n"); } else if (sys_mode == SYS_SET_HIGH) { sys_mode = SYS_SET_LOW; printf("Enter Low Threshold Setting Mode.\r\n"); } else if (sys_mode == SYS_SET_LOW) { sys_mode = SYS_NORMAL; // 保存阈值到Flash(此处调用Flash写入函数,后文实现) printf("Settings Saved. Back to Normal Mode.\r\n"); } } } key_right_last = key_right_now; // 在设置模式下,处理KEY_UP/KEY_DOWN调整数值,KEY_LEFT切换设置对象 if (sys_mode != SYS_NORMAL) { // KEY_LEFT短按:切换设置高温/低温阈值(在SYS_SET_HIGH和SYS_SET_LOW间切换) if (key_left_last == 1 && key_left_now == 0) { HAL_Delay(10); if (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_LEFT_PIN) == 0) { sys_mode = (sys_mode == SYS_SET_HIGH) ? SYS_SET_LOW : SYS_SET_HIGH; printf("Switch to %s Threshold Setting.\r\n", (sys_mode == SYS_SET_HIGH) ? "High" : "Low"); } } key_left_last = key_left_now; // KEY_UP短按:增加当前设置的阈值 if (key_up_last == 1 && key_up_now == 0) { HAL_Delay(10); if (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_UP_PIN) == 0) { if (sys_mode == SYS_SET_HIGH && temp_high_threshold < 50) { temp_high_threshold++; } else if (sys_mode == SYS_SET_LOW && temp_low_threshold < temp_high_threshold - 1) { // 低温阈值必须低于高温阈值至少1度 temp_low_threshold++; } printf("High:%dC, Low:%dC\r\n", temp_high_threshold, temp_low_threshold); } } key_up_last = key_up_now; // KEY_DOWN短按:减少当前设置的阈值 if (key_down_last == 1 && key_down_now == 0) { HAL_Delay(10); if (HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_DOWN_PIN) == 0) { if (sys_mode == SYS_SET_HIGH && temp_high_threshold > temp_low_threshold + 1) { // 高温阈值必须高于低温阈值至少1度 temp_high_threshold--; } else if (sys_mode == SYS_SET_LOW && temp_low_threshold > 0) { temp_low_threshold--; } printf("High:%dC, Low:%dC\r\n", temp_high_threshold, temp_low_threshold); } } key_down_last = key_down_now; } } /* USER CODE END PV */4.4 温度控制逻辑与继电器驱动
在main.c的主循环while (1)中,我们需要周期性地执行以下任务:
- 读取DHT11温度。
- 根据当前模式和温度,控制继电器。
- 扫描按键。
- 通过串口打印状态信息。
/* USER CODE BEGIN WHILE */ while (1) { uint8_t humidity = 0; uint8_t temperature = 0; // 1. 读取DHT11数据(例如每2秒读一次) static uint32_t dht11_tick = 0; if (HAL_GetTick() - dht11_tick > 2000) { dht11_tick = HAL_GetTick(); if (DHT11_Read_Data(&temperature, &humidity)) { current_temp = temperature; printf("Temp:%dC, Humi:%d%%\r\n", current_temp, humidity); } else { printf("DHT11 Read Error!\r\n"); } } // 2. 温度控制逻辑(仅在正常模式下自动控制) if (sys_mode == SYS_NORMAL) { if (current_temp > temp_high_threshold) { // 温度过高,打开继电器(假设继电器高电平触发,控制散热设备如风扇) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); relay_state = 1; } else if (current_temp < temp_low_threshold) { // 温度过低,关闭继电器(或打开加热设备,逻辑相反,根据实际设备调整) // 这里假设继电器控制的是散热设备,所以低温时关闭 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); relay_state = 0; } // 温度在阈值之间时,保持原状态(引入迟滞,避免临界点抖动) } // 3. 按键扫描 Key_Scan(); // 4. 打印系统状态(频率可低于温度读取) static uint32_t status_tick = 0; if (HAL_GetTick() - status_tick > 5000) { status_tick = HAL_GetTick(); printf("[Status] Mode:%s, Temp:%dC, High:%dC, Low:%dC, Relay:%s\r\n", (sys_mode == SYS_NORMAL) ? "Normal" : ((sys_mode == SYS_SET_HIGH) ? "SetHigh" : "SetLow"), current_temp, temp_high_threshold, temp_low_threshold, relay_state ? "ON" : "OFF"); } HAL_Delay(10); // 主循环延时,降低CPU占用 } /* USER CODE END WHILE */4.5 阈值参数存储(内部Flash)
为了保存用户设置的阈值,我们需要将数据写入STM32的内部Flash。注意:Flash写入前必须先擦除,且以扇区为单位操作,频繁擦写会损耗Flash寿命。
我们选择STM32F103C8T6的最后一个扇区(Page 127,地址0x0801F800 - 0x0801FFFF)来存储数据。首先,在main.c中添加Flash操作函数。
/* USER CODE BEGIN 4 */ #include "stm32f1xx_hal_flash.h" // 定义参数存储的Flash地址(最后一页) #define PARAM_FLASH_ADDR 0x0801F800 // 保存参数结构体 typedef struct { uint8_t high_threshold; uint8_t low_threshold; uint8_t checksum; // 简单的校验和 } SystemParams_t; // 计算校验和 static uint8_t calc_checksum(SystemParams_t *params) { return (params->high_threshold + params->low_threshold) ^ 0xAA; } // 从Flash读取参数 void params_load(SystemParams_t *params) { uint32_t *src = (uint32_t*)PARAM_FLASH_ADDR; uint32_t *dst = (uint32_t*)params; for (int i = 0; i < sizeof(SystemParams_t)/4; i++) { *dst++ = *src++; } // 验证校验和 if (params->checksum != calc_checksum(params)) { // 校验失败,使用默认值 params->high_threshold = 30; params->low_threshold = 20; params->checksum = calc_checksum(params); printf("Load default params.\r\n"); } else { printf("Load params from Flash: High=%d, Low=%d\r\n", params->high_threshold, params->low_threshold); } } // 保存参数到Flash void params_save(SystemParams_t *params) { HAL_FLASH_Unlock(); // 解锁Flash FLASH_EraseInitTypeDef EraseInitStruct; uint32_t PageError = 0; // 配置擦除操作:擦除最后一页 EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES; EraseInitStruct.PageAddress = PARAM_FLASH_ADDR; EraseInitStruct.NbPages = 1; // 执行擦除 if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK) { printf("Flash Erase Error!\r\n"); HAL_FLASH_Lock(); return; } // 写入数据 params->checksum = calc_checksum(params); // 更新校验和 uint32_t *src = (uint32_t*)params; uint32_t addr = PARAM_FLASH_ADDR; for (int i = 0; i < sizeof(SystemParams_t)/4; i++) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, *src) != HAL_OK) { printf("Flash Write Error at 0x%08lx!\r\n", addr); break; } addr += 4; src++; } HAL_FLASH_Lock(); // 锁定Flash printf("Params saved to Flash.\r\n"); } /* USER CODE END 4 */然后,在main函数的初始化部分(/* USER CODE BEGIN 2 */)加载参数,并在按键保存时调用保存函数。
/* USER CODE BEGIN 2 */ SystemParams_t sys_params; params_load(&sys_params); temp_high_threshold = sys_params.high_threshold; temp_low_threshold = sys_params.low_threshold; printf("System Start. High:%dC, Low:%dC\r\n", temp_high_threshold, temp_low_threshold); /* USER CODE END 2 */修改按键处理中保存配置的部分:
// ... 在KEY_RIGHT按键处理中,退出设置模式时 ... } else if (sys_mode == SYS_SET_LOW) { sys_mode = SYS_NORMAL; // 保存阈值到Flash sys_params.high_threshold = temp_high_threshold; sys_params.low_threshold = temp_low_threshold; params_save(&sys_params); printf("Settings Saved. Back to Normal Mode.\r\n"); }5. 硬件连接与测试
连接电路:
- STM32<->DHT11: VCC -> 3.3V/5V, GND -> GND, DATA -> PA0。
- STM32<->继电器模块: VCC -> 3.3V/5V, GND -> GND, IN -> PA1。
- STM32<->4个按键: 一端分别接PA2, PA3, PA4, PA5;另一端接GND(配置为上拉输入时,按键按下即为低电平)。
- 继电器模块<->负载: COM接电源正极,NO接负载(如LED+电阻)正极,负载负极接电源负极。若控制220V,务必断电操作,注意安全!
- STM32<->USB转TTL: PA9(TX) -> RXD, PA10(RX) -> TXD, GND -> GND。
编译与下载:使用Keil或CubeIDE编译工程,无错误后通过ST-Link或串口下载到STM32。
上电测试:
- 打开串口调试助手,设置波特率115200。
- 复位STM32,观察串口输出启动信息和初始温度。
- 用手触摸或吹气改变DHT11周围温度,观察串口温度变化及继电器状态变化(可通过继电器模块上的指示灯或连接的LED判断)。
- 按下
KEY_RIGHT键,进入设置模式,按照串口提示,使用KEY_LEFT切换设置项,KEY_UP/KEY_DOWN调整阈值,再次按下KEY_RIGHT保存。 - 断电重启,检查阈值是否从Flash中成功加载。
6. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 串口无任何输出 | 1. 串口线接反(TX/RX) 2. 波特率设置错误 3. 单片机未正常运行 | 1. 检查TX/RX交叉连接。 2. 确认代码和助手波特率一致(115200)。 3. 检查供电,测量晶振,看程序是否下载成功。 | 1. 交换TX/RX线序。 2. 统一波特率。 3. 确保Boot0下拉,重新下载程序。 |
| DHT11读取一直失败 | 1. 时序不准确 2. 接线错误或接触不良 3. 电源不稳 4. 未接上拉电阻 | 1. 用逻辑分析仪或示波器抓取DATA线波形,对比DHT11时序图。 2. 检查VCC、GND、DATA连接。 3. 确保供电电压在3.3V-5V。 4. DATA线需要接一个4.7K-10K上拉电阻到VCC。 | 1. 调整delay_us的延时值,特别是40µs采样点。2. 重新接线,确保牢固。 3. 使用稳定的电源,或给DHT11的VCC并联一个100nF电容。 4. 在DATA线上添加一个4.7K上拉电阻。 |
| 继电器不动作 | 1. 电平逻辑搞反 2. 驱动电流不足 3. 继电器模块损坏 | 1. 确认模块是高电平触发还是低电平触发。 2. STM32的GPIO驱动能力可能不足,尝试用三极管或MOS管驱动。 3. 直接用导线短接模块的VCC和IN,看继电器是否吸合。 | 1. 根据模块规格修改代码中HAL_GPIO_WritePin的输出电平。2. 增加驱动电路,或选择驱动电流要求更低的模块。 3. 更换继电器模块。 |
| 按键无反应 | 1. GPIO模式配置错误 2. 按键消抖处理不当 3. 按键扫描函数未被调用 | 1. 检查CubeMX中按键GPIO是否配置为上拉输入。 2. 增加消抖延时或改进消抖算法(如状态机)。 3. 确保 Key_Scan()函数在主循环中被调用。 | 1. 重新检查GPIO配置。 2. 优化消抖逻辑,或使用定时器中断进行按键扫描。 3. 将 Key_Scan()调用放在主循环中。 |
| Flash保存后数据丢失 | 1. 擦除或写入地址错误 2. 擦写期间发生中断 3. Flash寿命耗尽(极端情况) | 1. 检查PARAM_FLASH_ADDR是否在有效的Flash地址范围内且是页起始地址。2. 在擦写操作前关闭全局中断 __disable_irq(),操作后开启__enable_irq()。3. 避免在程序运行中频繁保存。 | 1. 参考芯片手册,使用正确的扇区地址。 2. 在 params_save函数的开始和结束处加上中断开关。3. 增加保存条件,例如阈值变化超过一定范围或手动触发时才保存。 |
| 温度控制频繁开关(抖动) | 控制逻辑没有迟滞区间 | 观察当温度在阈值附近微小波动时,继电器状态是否频繁变化。 | 引入迟滞(Hysteresis)。例如,修改控制逻辑:if (current_temp > temp_high_threshold + 1) relay_on;else if (current_temp < temp_low_threshold - 1) relay_off;这样在阈值上下1度的区间内,状态保持不变。 |
7. 最佳实践与项目扩展建议
一个稳定的温控系统,远不止让继电器“动起来”那么简单。以下是提升项目鲁棒性和实用性的关键点:
传感器数据滤波:DHT11的读数可能存在偶发跳动。采用滑动平均滤波或中值滤波可以平滑数据,避免因单次误读导致误动作。
#define FILTER_SIZE 5 uint8_t temp_buffer[FILTER_SIZE] = {0}; uint8_t buffer_index = 0; // 每次读取到新温度后 temp_buffer[buffer_index] = current_temp; buffer_index = (buffer_index + 1) % FILTER_SIZE; // 计算平均值 uint16_t sum = 0; for(int i=0; i<FILTER_SIZE; i++) sum += temp_buffer[i]; uint8_t filtered_temp = sum / FILTER_SIZE; // 使用 filtered_temp 进行控制判断控制算法升级:简单的开关控制(Bang-Bang Control)在阈值点附近容易振荡。对于要求更高的场合,可以尝试比例积分微分(PID)控制,它能根据温差的大小和变化趋势,更平滑地调节执行机构(如通过PWM控制加热功率)。
更健壮的按键处理:当前的消抖简单延时会影响系统响应。推荐使用状态机或定时器中断扫描的方式来实现非阻塞、支持长按、连按等高级功能的按键驱动。
增加显示模块:使用OLED或LCD屏幕实时显示温度、阈值、继电器状态和系统模式,用户体验会远好于串口调试。
通信与联网:添加Wi-Fi模块(如ESP-01S)或蓝牙模块,将温度数据和控制接口暴露给手机App或云平台,实现远程监控,项目就升级为了一个简单的物联网设备。
低功耗设计:如果用于电池供电场景,可以让STM32进入休眠模式(Stop或Standby),定时唤醒读取传感器和检查按键,从而大幅降低系统功耗。
工程结构优化:将DHT11驱动、按键驱动、Flash操作、控制逻辑分别模块化,放在独立的
.c/.h文件里,通过清晰的接口调用。这会让代码更易维护和移植。
通过这个项目,你搭建的不仅仅是一个温控开关,而是一个经典的嵌入式系统闭环控制框架。这个框架包含了传感器输入、人机交互、逻辑处理、执行输出、数据存储等核心要素。掌握了它,你就能快速地将想法转化为各种实实在在的自动化小设备。