1. 项目背景与核心需求
最近在整理工作室设备时,发现传统温湿度计只能显示数据却无法自动调节环境。作为一个常年与电子设备打交道的工程师,我决定动手打造一个能自动控制环境的智能温湿度控制器。这个项目特别适合需要稳定环境的手工工作室、小型温室或是注重居家舒适度的技术爱好者。
市面上的智能家居设备往往价格昂贵且功能冗余,而基于单片机的自制方案不仅成本可控(整套材料费不超过100元),还能完全自定义控制逻辑。我选择的STM32F103C8T6开发板(俗称"蓝板")性能足够应对常规需求,配合DHT22温湿度传感器和继电器模块,构成了整个系统的硬件基础。
关键提示:DHT22虽然比DHT11贵约10元,但测量精度更高(湿度±2%RH,温度±0.5℃),响应速度也更快,是性价比最优选。
2. 硬件系统设计详解
2.1 核心器件选型对比
经过实测对比多种方案,最终确定的硬件配置如下表所示:
| 部件名称 | 选型型号 | 关键参数 | 成本 | 替代方案 |
|---|---|---|---|---|
| 主控芯片 | STM32F103C8T6 | 72MHz Cortex-M3, 64KB Flash | 12元 | ESP8266(需WiFi功能时) |
| 温湿度传感器 | DHT22(AM2302) | 0-100%RH, -40~80℃ | 25元 | SHT30(精度更高但更贵) |
| 显示模块 | 0.96寸OLED(I2C) | 128x64分辨率 | 15元 | LCD1602(更便宜但耗电) |
| 执行机构 | 5V继电器模块 | 10A/250VAC负载能力 | 8元/个 | 固态继电器(无触点寿命长) |
| 电源模块 | LM2596降压模块 | 输入6-40V,输出5V/3A | 6元 | AMS1117(仅限小电流场景) |
2.2 电路连接实拍图解
实际组装时最容易出错的传感器接线部分,这里分享我的接线心得:
-
DHT22连接要点:
- 数据线必须接10K上拉电阻
- 导线长度不宜超过20cm
- 避免与继电器等高干扰源并行走线
-
继电器安全使用:
c复制// 典型驱动电路
void Relay_Control(uint8_t state) {
GPIO_WriteBit(GPIOB, GPIO_Pin_12, (state) ? Bit_SET : Bit_RESET);
// 添加500ms延时防止频繁切换
if(state != lastState) Delay_ms(500);
}
- 供电系统设计:
- 主控与传感器共用5V电源
- 继电器线圈电源单独从降压模块引出
- 大功率负载(如加热器)必须另接市电
血泪教训:曾因继电器反电动势烧毁IO口,后来都在驱动端并联1N4148续流二极管。
3. 软件系统实现解析
3.1 传感器数据采集优化
DHT22的单总线协议看似简单,但实际使用中会遇到各种问题。经过反复测试,总结出最稳定的读取流程:
c复制#define DHT_TIMEOUT 10000 // 10ms超时
uint8_t DHT_ReadData(float *temp, float *humi) {
uint8_t buf[5] = {0};
// 主机拉低18ms后拉高20-40us
GPIO_ResetBits(DHT_PORT, DHT_PIN);
Delay_ms(18);
GPIO_SetBits(DHT_PORT, DHT_PIN);
Delay_us(30);
// 检测从机响应(83us低电平+87us高电平)
if(!Wait_Level(0, DHT_TIMEOUT)) return 0;
if(!Wait_Level(1, DHT_TIMEOUT)) return 0;
// 接收40bit数据(50us低电平+26-28us表示0,70us表示1)
for(int i=0; i<40; i++) {
if(!Wait_Level(0, DHT_TIMEOUT)) return 0;
uint32_t t = Get_Level_Time(1);
buf[i/8] <<= 1;
if(t > 50) buf[i/8] |= 1;
}
// 校验和验证
if(buf[4] != (buf[0]+buf[1]+buf[2]+buf[3])) return 0;
*humi = (buf[0]<<8 | buf[1]) * 0.1;
*temp = ((buf[2]&0x7F)<<8 | buf[3]) * 0.1;
if(buf[2]&0x80) *temp = -*temp;
return 1;
}
实测发现两个关键点:
- 时序精度必须控制在us级,普通延时函数需用定时器实现
- 每次读取间隔建议≥2s,否则传感器可能不响应
3.2 控制算法实现
采用增量式PID算法实现精确控制,参数整定过程值得详细记录:
c复制typedef struct {
float Kp, Ki, Kd;
float Err[3]; // 当前、前次、前前次误差
float Output;
} PID_TypeDef;
float PID_Calculate(PID_TypeDef *pid, float target, float actual) {
pid->Err[2] = pid->Err[1];
pid->Err[1] = pid->Err[0];
pid->Err[0] = target - actual;
float dErr = pid->Err[0] - pid->Err[1];
float iErr = (pid->Err[0] + pid->Err[1] + pid->Err[2]) / 3;
float output = pid->Kp * pid->Err[0]
+ pid->Ki * iErr
+ pid->Kd * dErr;
// 输出限幅(0~100%)
if(output > 100) output = 100;
if(output < 0) output = 0;
return output;
}
参数整定经验:
- 先设Ki=Kd=0,增大Kp至系统开始振荡
- 取振荡时Kp的60%作为最终Kp
- Ki取0.5*Kp/T (T为采样周期)
- Kd取Kp*T/8
4. 系统集成与调试
4.1 人机交互设计
使用OLED显示配合旋转编码器实现参数设置,界面布局经过三次迭代优化:
code复制[温湿度实时显示区]
当前温度:25.6℃ ▲
目标温度:26.0℃ ▼
当前湿度:45%RH ▲
目标湿度:50%RH ▼
[控制状态指示区]
加热器:OFF 加湿器:ON
编码器中断处理代码要点:
c复制void EXTI9_5_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line9) != RESET) {
// 检测A相B相电平判断方向
uint8_t a = GPIO_ReadInputDataBit(ENC_A_PORT, ENC_A_PIN);
uint8_t b = GPIO_ReadInputDataBit(ENC_B_PORT, ENC_B_PIN);
if(a == b) {
// 顺时针旋转
param[current_select] += step_size;
} else {
// 逆时针旋转
param[current_select] -= step_size;
}
EXTI_ClearITPendingBit(EXTI_Line9);
}
}
4.2 典型问题排查表
在三个月实际使用中遇到的典型问题及解决方案:
| 故障现象 | 可能原因 | 解决方案 |
|---|---|---|
| 显示"传感器错误" | 1. 接线接触不良 | 检查DHT22三根线连接 |
| 2. 供电不足 | 测量5V电源实际输出 | |
| 3. 读取间隔过短 | 确保两次读取间隔≥2s | |
| 继电器频繁开关 | 1. PID参数过于激进 | 适当减小Kp增大Ki |
| 2. 传感器数据跳变 | 增加软件滤波(滑动平均) | |
| OLED显示残影 | 1. 刷新速率过高 | 将刷新率从60Hz降至30Hz |
| 2. 未清屏直接重绘 | 每次刷新前先发送清屏指令 | |
| 系统偶尔死机 | 1. 看门狗未启用 | 配置IWDG(独立看门狗) |
| 2. 堆栈溢出 | 增大启动文件中的堆栈大小 |
5. 进阶优化方向
经过半年使用后,我又做了以下改进使系统更可靠:
-
电源管理优化:
- 增加超级电容作为备用电源
- 实现异常断电时的参数保存
c复制void Backup_Save(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); BKP_WriteBackupRegister(BKP_DR1, *(uint16_t*)&target_temp); BKP_WriteBackupRegister(BKP_DR2, *(uint16_t*)&target_humi); } -
网络功能扩展:
- 通过ESP-01S模块添加WiFi接入
- 使用MQTT协议上传数据到HomeAssistant
c复制void ESP_SendData(void) { UART_SendString("AT+CIPSTART=\"TCP\",\"192.168.1.100\",1883\r\n"); Delay_ms(1000); UART_SendString("AT+CIPSEND=48\r\n"); Delay_ms(500); sprintf(mqttMsg, "{\"temp\":%.1f,\"humi\":%.1f}", current_temp, current_humi); UART_SendString(mqttMsg); } -
能耗统计功能:
- 通过电流传感器监测设备功耗
- 计算每日/每月用电量
c复制float Calc_Energy(uint16_t current, uint8_t hours) { // 假设220V电压 return current * 220 * hours / 1000.0; // kWh }
这个项目最让我满意的不是技术实现,而是它实实在在地改善了工作环境。现在工作室常年保持25±1℃、50±5%RH的理想状态,电子元件再也没出现过受潮问题。整个系统至今稳定运行11个月,唯一更换过的部件是继电器的触点(机械寿命约10万次)。