1. 先搞清楚这个项目到底能做什么,以及它适合谁
这个项目标题叫“STM32温控开关”,核心功能是用STM32单片机读取DHT11温湿度传感器的温度值,然后通过继电器去控制一个外部设备(比如风扇、加热棒)的开关,并且还能通过4个按键手动调节温度阈值。听起来很简单,对吧?但很多新手在第一次做这类项目时,最容易卡住的不是代码本身,而是几个关键环节的衔接:传感器数据怎么稳定读取、继电器怎么安全驱动、按键怎么防抖并实时更新阈值、以及整个逻辑循环怎么设计才不冲突。
如果你正在学STM32,想找一个能把GPIO、定时器、中断、外设驱动和状态机逻辑串起来的综合小项目,那么这个温控开关就是一个非常典型的练手案例。它不涉及复杂的通信协议或操作系统,但几乎涵盖了单片机开发中最基础、最常用的那些技能点。最值得关注的价值在于,你能通过它建立起一个完整的“感知-决策-执行”的嵌入式系统概念,并且所有代码和硬件连接都是可以自己从头搭出来的。
我一般会建议先从功能拆解开始:测温部分要解决DHT11的时序读取问题;继电器控制要解决隔离和驱动电流问题;按键调阈值要解决实时响应和参数存储问题。把这些都跑通了,一个可靠的温控开关原型也就出来了。
2. 硬件清单与连接:别在第一步就把电路接错
动手写代码之前,先把硬件理清楚。这个项目的硬件核心就四样:STM32最小系统板、DHT11温湿度传感器、一个继电器模块、四个轻触按键。电源部分根据你的继电器型号来定,如果继电器模块是5V驱动的,那么可能需要一个额外的5V电源,或者从开发板的5V引脚取电(要确认电流够用)。
关键硬件说明:
- STM32开发板:F1系列(如STM32F103C8T6)就足够,资源丰富,社区资料也多。项目对性能要求不高,F1、F4甚至G0系列都可以。
- DHT11传感器:这是一个单总线数字传感器,只需要一根数据线(加上VCC和GND)与单片机通信。它输出的是整数的温度和湿度值,精度对于温控开关来说完全够用。注意:它的时序要求比较严格,必须按照数据手册的时序图来操作。
- 继电器模块:市面上常见的是低电平触发或高电平触发的模块。一定要看清楚你的模块是哪种触发方式,这直接决定了你初始化GPIO时应该设置成推挽输出还是开漏输出,以及输出电平的逻辑。模块上通常会有COM(公共端)、NO(常开端)、NC(常闭端)三个接线端子,控制负载(如风扇)接在COM和NO之间。
- 按键:四个轻触按键,用于增加阈值、减少阈值、切换模式(如手动/自动)、确认等。需要接上拉电阻(开发板内部或外部均可),配置为输入模式,并做好软件防抖。
接线参考表示例(以STM32F103C8T6、低电平触发继电器为例):
| 元件 | 引脚/端子 | 连接到STM32 | 备注 |
|---|---|---|---|
| DHT11 | VCC | 3.3V或5V | 看模块支持,通常3.3V-5.5V |
| GND | GND | ||
| DATA | PA0 | 可自定义,需配置为推挽输出/浮空输入切换 | |
| 继电器模块 | VCC | 5V | 注意电流,可从外部电源取电 |
| GND | GND | ||
| IN | PA1 | 低电平触发:输出0继电器吸合,1断开 | |
| 按键1 (加) | 一端 | PA2 | 另一端接GND,STM32端配置上拉输入 |
| 按键2 (减) | 一端 | PA3 | 同上 |
| 按键3 (模式) | 一端 | PA4 | 同上 |
| 按键4 (确认) | 一端 | PA5 | 同上 |
注意:继电器的负载端(COM/NO)接的是你要控制的220V交流设备(如风扇),这部分是强电!务必在断电情况下操作,做好绝缘,或者直接用低压直流负载(如LED灯、小电机)来模拟测试,安全第一。
3. 软件环境与工程框架搭建
软件部分,你可以用标准库,也可以用HAL库。从学习和项目复杂度来看,两者都可以。HAL库的抽象层次更高,移植性稍好,但代码量可能大一点;标准库更直接,对硬件操作更清晰。我这里以目前更通用的HAL库为例,使用STM32CubeMX进行初始化配置,然后用Keil MDK或STM32CubeIDE进行开发。
第一步:用STM32CubeMX配置引脚和时钟
- 打开CubeMX,选择你的芯片型号。
- 系统核心:在
SYS里,将Debug设为Serial Wire(如果要用ST-Link调试)。 - 时钟:在
RCC里,将HSE(高速外部时钟)设为Crystal/Ceramic Resonator。 - 引脚配置:
- DHT11 DATA (PA0):初始化为
GPIO_Output(推挽输出,上拉),我们先把它当输出引脚给传感器发开始信号。 - 继电器控制 (PA1):初始化为
GPIO_Output(推挽输出,上拉),默认输出高电平(继电器断开)。 - 四个按键 (PA2, PA3, PA4, PA5):全部初始化为
GPIO_Input,并启用内部上拉(Pull-up)。
- DHT11 DATA (PA0):初始化为
- 配置一个定时器(如TIM2)用于微秒级延时和按键扫描。将定时器时钟源设为内部时钟,预分频器和周期值根据你的系统时钟计算,使其产生1微秒的计数周期。
- 配置一个系统定时器(如SysTick)或另一个定时器用于主循环周期调度,比如每100ms执行一次传感器读取和逻辑判断。
- 生成代码,选择你的IDE(MDK-ARM V5)。
第二步:工程结构规划生成的代码已经有了HAL库和基本的引脚初始化。我建议在Src和Inc文件夹下新建几个文件来模块化代码:
dht11.c / dht11.h: DHT11驱动,包含初始化、发送开始信号、读取40位数据、校验和计算、获取温湿度值的函数。relay.c / relay.h: 继电器驱动,包含继电器打开、关闭、状态获取的函数。key.c / key.h: 按键驱动,包含按键扫描、防抖处理、返回键值的函数。controller.c / controller.h: 主控逻辑,包含温度阈值比较、自动/手动模式切换、控制继电器动作的状态机。main.c: 主函数,负责初始化各模块,并在主循环中调度任务。
这样结构清晰,以后要添加显示(如OLED)或通信(如串口上报)功能也方便。
4. DHT11驱动编写:时序是成败的关键
DHT11的通信协议是单总线,主机(STM32)发起通信,然后传感器返回40位数据。时序要求非常严格,微秒级的延时误差都可能导致读取失败。所以,不要用HAL_Delay()这种毫秒级延时,必须用定时器实现精准的微秒延时。
核心步骤:
- 主机发送开始信号:DATA引脚设置为输出模式,拉低至少18ms,然后拉高20-40us,接着切换为输入模式等待传感器响应。
- 传感器响应:传感器会拉低80us,再拉高80us,之后开始传输数据。
- 数据位读取:每一位数据都以一个50us的低电平起始位开始,随后的高电平持续时间决定数据是0(26-28us)还是1(70us)。需要在高电平期间采样DATA引脚的电平。
代码要点(dht11.c片段):
// 微秒延时函数,基于配置好的定时器(如TIM2)实现 void DHT11_Delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim2, 0); while (__HAL_TIM_GET_COUNTER(&htim2) < us); } // 读取一个字节 uint8_t DHT11_ReadByte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { // 等待50us低电平起始位结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); DHT11_Delay_us(40); // 延时40us后采样,避开起始位,落在数据位的高电平区间 data <<= 1; if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { data |= 1; // 等待剩余的高电平结束(如果是‘1’,高电平长约70us) while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); } } return data; } // 读取温湿度 uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i; // 主机拉低18ms HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); DHT11_Delay_us(18000); // 主机拉高20-40us HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); DHT11_Delay_us(30); // 切换为输入模式 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct); // 等待传感器响应 if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) return 1; DHT11_Delay_us(80); if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) return 2; DHT11_Delay_us(80); // 读取40位数据 for (i = 0; i < 5; i++) { buf[i] = DHT11_ReadByte(); } // 校验和 if (buf[0] + buf[1] + buf[2] + buf[3] != buf[4]) return 3; *humi = buf[0]; *temp = buf[2]; return 0; // 成功 }常见问题排查:
- 一直读取失败(返回非0):首先检查硬件连接(VCC, GND, DATA),确保电源稳定。然后用逻辑分析仪或示波器抓取DATA引脚的波形,对照DHT11数据手册的时序图,看开始信号、响应信号和数据位的时序是否符合。最常见的原因是微秒延时不准。
- 数据偶尔出错:可能是电源干扰或接线过长。尝试在DHT11的VCC和GND之间加一个104(0.1uF)的滤波电容,并尽量缩短DATA线的长度。
- 连续读取间隔太短:DHT11两次读取之间需要至少1秒的间隔,否则传感器可能不响应。
5. 继电器驱动与按键处理:稳定控制与交互基础
继电器驱动很简单,就是控制一个GPIO输出高低电平。关键在于理解你的继电器模块是高电平触发还是低电平触发。
// relay.h #define RELAY_ON() HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET) // 假设低电平触发 #define RELAY_OFF() HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET) #define RELAY_TOGGLE() HAL_GPIO_TogglePin(RELAY_GPIO_Port, RELAY_Pin) uint8_t RELAY_GetState(void);按键处理稍微复杂点,需要消抖。我一般用状态机的方式在定时器中断里扫描,既稳定又不阻塞主程序。
// key.h typedef enum { KEY_NONE = 0, KEY_UP, KEY_DOWN, KEY_MODE, KEY_OK } Key_TypeDef; Key_TypeDef KEY_Scan(void);// key.c // 按键扫描状态机 Key_TypeDef KEY_Scan(void) { static uint8_t key_state[4] = {0}; // 每个按键的状态 static uint16_t key_count[4] = {0}; // 计数器 Key_TypeDef key_pressed = KEY_NONE; GPIO_PinState pin_state; // 扫描每个按键 for (int i = 0; i < 4; i++) { pin_state = HAL_GPIO_ReadPin(KEY_GPIO_Port[i], KEY_Pin[i]); switch (key_state[i]) { case 0: // 等待按下 if (pin_state == GPIO_PIN_RESET) { // 按键按下(低电平) key_state[i] = 1; key_count[i] = 0; } break; case 1: // 消抖确认 if (pin_state == GPIO_PIN_RESET) { key_count[i]++; if (key_count[i] > 5) { // 持续5个扫描周期(如5ms)低电平,确认按下 key_state[i] = 2; // 根据i返回对应的按键值 if (i == 0) key_pressed = KEY_UP; else if (i == 1) key_pressed = KEY_DOWN; // ... 其他按键 } } else { key_state[i] = 0; // 抖动,回到初始状态 } break; case 2: // 等待释放 if (pin_state == GPIO_PIN_SET) { // 按键释放 key_state[i] = 0; } break; } } return key_pressed; // 返回本次扫描到的按键(短按) }把这个KEY_Scan()函数放在一个每1ms或5ms执行一次的定时器中断回调函数里,主程序只需要检查返回值即可。
6. 核心控制逻辑与状态机设计
这是项目的“大脑”。我们需要定义几个关键变量和状态:
target_temp:目标温度阈值,通过按键增减。current_temp:当前温度,来自DHT11。hysteresis:回差(迟滞)。比如目标25度,回差2度。那么温度高于27度时打开制冷(继电器吸合),低于23度时才关闭。防止继电器在临界点频繁开关。control_mode:控制模式,比如自动模式(根据温度自动控制)、手动模式(强制开/关)。relay_status:继电器当前状态。
主控逻辑(controller.c)可以这样设计:
void Controller_Update(void) { Key_TypeDef key = KEY_Scan(); // 1. 处理按键,更新阈值和模式 switch (key) { case KEY_UP: target_temp++; break; case KEY_DOWN: target_temp--; break; case KEY_MODE: control_mode = (control_mode == MODE_AUTO) ? MODE_MANUAL : MODE_AUTO; break; case KEY_OK: // 手动模式下,切换继电器状态 if (control_mode == MODE_MANUAL) { relay_status = !relay_status; if (relay_status) RELAY_ON(); else RELAY_OFF(); } break; default: break; } // 2. 自动模式下的温度控制逻辑 if (control_mode == MODE_AUTO) { if (current_temp > (target_temp + hysteresis)) { // 温度过高,需要制冷(假设继电器控制风扇) if (relay_status == 0) { // 如果继电器是关闭状态 RELAY_ON(); relay_status = 1; } } else if (current_temp < (target_temp - hysteresis)) { // 温度过低,关闭制冷 if (relay_status == 1) { // 如果继电器是打开状态 RELAY_OFF(); relay_status = 0; } } // 温度在回差范围内,保持原状态 } // 3. 可以在这里更新显示(如果有OLED/LCD) }然后在主循环里,以固定的周期(比如每100ms或500ms)调用Controller_Update()和DHT11_ReadData()(注意DHT11读取间隔要大于1秒)。
7. 主程序框架与调试要点
把上面所有模块整合到main.c中:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); // 用于微秒延时 MX_TIM3_Init(); // 用于10ms定时,扫描按键 // ... 其他初始化 uint8_t temperature, humidity; uint32_t last_read_time = 0; uint32_t last_control_time = 0; while (1) { uint32_t current_time = HAL_GetTick(); // 每1秒读取一次DHT11 if (current_time - last_read_time > 1000) { if (DHT11_ReadData(&temperature, &humidity) == 0) { current_temp = temperature; // 更新全局当前温度 // 可以打印到串口调试 printf("Temp: %d C, Humi: %d %%\n", temperature, humidity); } else { printf("DHT11 Read Error!\n"); } last_read_time = current_time; } // 每100ms执行一次控制逻辑 if (current_time - last_control_time > 100) { Controller_Update(); last_control_time = current_time; } // 其他任务... } }调试时优先看这几个点:
- DHT11数据是否正常:通过串口打印温度和湿度,看数值是否在合理范围(比如室内10-35度)。如果一直是0或255,检查时序和接线。
- 按键响应是否灵敏:按下按键,看串口打印的键值或阈值变化是否正确。如果按键不灵,检查上拉电阻和消抖逻辑。
- 继电器动作逻辑:用手改变温度(比如用手握住DHT11升温),观察继电器是否在设定的阈值点附近动作。用万用表测量继电器输出端子的通断来确认。
- 模式切换:测试自动模式和手动模式切换是否正常,手动模式下按键能否强制控制继电器。
8. 从原型到实用的优化建议
跑通基本功能后,可以考虑下面这些优化,让项目更接近一个实用的产品:
- 阈值存储:使用STM32的内部Flash(如EEPROM模拟)或外置AT24C02这类EEPROM芯片,把用户设定的目标温度、回差值、控制模式等参数保存下来,掉电不丢失。
- 显示界面:添加一个OLED或LCD屏,实时显示当前温度、目标温度、继电器状态、控制模式等信息,交互体验更好。
- 参数校准:DHT11有±1℃的误差,如果要求高,可以做一个简单的软件校准。比如读取一个已知准确温度计的值,计算偏移量。
- 通信功能:增加串口通信,可以将温度数据上报给电脑上位机,或者接收上位机的控制指令,实现远程监控。
- 更复杂的控制算法:简单的开关控制(Bang-Bang Control)在临界点容易震荡。可以引入更平滑的PWM控制(如果继电器支持PWM或者控制的是固态继电器),或者实现一个简单的PID控制器,让温度控制更平稳。
- 安全性:增加温度超限报警(比如超过50度强制断开并报警)、继电器动作次数统计、看门狗防止程序跑飞等。
这个项目麻雀虽小,五脏俱全。把它做扎实了,你对STM32的GPIO操作、定时器使用、外设驱动、状态机编程和系统调试会有非常直观的理解。下次再做更复杂的项目,比如智能台灯、环境监控、小车控制,你会发现底层逻辑都是相通的。