用 STM32F407VET6 做一个温湿度监测系统(附代码)

AI助手已提取文章相关产品:

用 STM32F407VET6 搭建一个温湿度监测系统:从时序踩坑到稳定输出

你有没有遇到过这样的情况?明明电路接得没问题,代码也照着例程写的,可 DHT11 就是“间歇性罢工”——一会儿能读出数据,一会儿又超时失败。更离谱的是,换块板子、换个传感器,问题居然还跟着变?

这其实不是玄学,而是嵌入式开发里最真实的战场: 硬件时序的毫厘之争

今天我们就以 STM32F407VET6 为核心,手把手实现一个真正可靠的温湿度采集系统。不玩虚的,不堆术语,只讲你在调试过程中会真真切切踩到的坑,以及怎么一步步把它填平。


为什么选 STM32F407VET6?它真的比 F1 强那么多吗?

说实话,如果你只是做个简单的温湿度显示,STM32F103C8T6(蓝丸)完全够用。但一旦你开始考虑扩展功能——比如加上 OLED 显示、Wi-Fi 上云、本地存储、甚至做点简单的滤波算法,F1 的性能瓶颈就来了。

而 STM32F407VET6,这块 LQFP100 封装的“小钢炮”,主频直接飙到 168MHz ,带 FPU 浮点单元,SRAM 高达 192KB,外设资源更是全面开花:3 个 USART、3 个 SPI、2 个 I2C……关键是价格也不贵,批量采购也就十几块钱。

更重要的是,它的 HAL 库生态成熟,CubeMX 配置顺滑,适合快速原型开发。我们这次项目虽然看起来简单,但它是一个绝佳的练手机会——你可以借此掌握 GPIO 精确控制、定时器延时、UART 调试输出等核心技能。

🧠 小贴士:别小看这个芯片,很多工业 HMI、电机控制器、PLC 模块的主控就是它。


DHT11:便宜好用,但“脾气”不小

DHT11 是初学者最爱的传感器之一,单总线、5V 兼容、价格不到五块钱。但它有个致命弱点: 对时序极其敏感

你以为它只是发个数据?错。整个通信过程就像一场精密的“电平舞蹈”,每一步都必须卡在点上:

  1. 主机先拉低至少 18ms ,告诉 DHT11:“我要开始啦!”
  2. DHT11 回应:拉低 80μs ,再拉高 80μs
  3. 然后才开始传 40 位数据
  4. 每一位通过高电平持续时间区分是 0 还是 1
    - 0 :高电平约 26~28μs
    - 1 :高电平约 70μs

听起来不难?那你一定没在实际项目中被它折磨过 😅

常见翻车现场

  • ❌ 使用 HAL_Delay(1) 实现微秒级延时 → 不行!最小单位是 ms
  • ❌ 忽略响应阶段的超时检测 → 程序卡死在 while 循环里
  • ❌ 没加外部上拉电阻 → 信号上升沿拖尾严重
  • ❌ 连续读取间隔小于 1 秒 → DHT11 直接拒绝服务

这些问题都会导致一个结果: 偶尔成功,频繁失败

所以我们的目标不是“让代码跑起来”,而是让它 在各种环境下都能稳定运行


如何搞定微秒级延时?别再用 for 循环了!

这是新手最容易掉进去的坑。很多人为了省事,写个空循环来延迟几微秒:

void delay_us(uint32_t us) {
    while (us--) {
        __NOP(); __NOP(); __NOP(); // ……数不清几个 NOP
    }
}

这种方法不仅不可靠,还会因为编译器优化直接被干掉(O2 优化下可能变成一条指令都不剩)。而且不同主频下表现完全不同,移植性极差。

正确做法:基于 SysTick 或 TIM 定时器

我们选择使用 SysTick 来实现高精度延时,因为它独立于 HAL_Delay(),不会被打断。

// delay.h
#ifndef __DELAY_H
#define __DELAY_H

#include "stm32f4xx_hal.h"

void delay_init(void);
void delay_us(uint32_t us);
void delay_ms(uint32_t ms);

#endif
// delay.c
#include "delay.h"

static uint32_t ticks_per_us = 0;

void delay_init(void) {
    // 基于系统时钟设置每微秒对应的 SysTick 计数值
    ticks_per_us = SystemCoreClock / 1000000;  // 例如 168MHz -> 168
}

void delay_us(uint32_t us) {
    uint32_t start = SysTick->VAL;
    uint32_t wait_ticks = us * ticks_per_us;

    while ((start - SysTick->VAL) < wait_ticks) {
        // 空转等待
    }
}

void delay_ms(uint32_t ms) {
    HAL_Delay(ms);  // 毫秒级可以直接用 HAL
}

⚠️ 注意:SysTick->VAL 是向下计数器,值从 LOAD 到 0 反复循环。所以我们比较的是“经过的时间”。

这个 delay_us() 函数精度可以达到 ±1μs,在大多数场景下足够用了。当然,如果你追求更高精度,可以用 DWT Cycle Counter(Data Watchpoint and Trace),但需要额外使能。


DHT11 驱动重构:从“能用”到“可靠”

现在进入重头戏。我们要重新设计 DHT11 的驱动逻辑,确保每一个步骤都有 超时保护 状态反馈

先来看结构体定义:

// dht11.h
typedef struct {
    uint8_t humidity_int;   // 湿度整数部分
    uint8_t humidity_dec;   // 湿度小数部分(DHT11 固定为 0)
    uint8_t temp_int;       // 温度整数部分
    uint8_t temp_dec;       // 温度小数部分(同上)
    uint8_t checksum;       // 校验和
} DHT11_DataTypedef;

接下来是关键函数 DHT11_Read() ,我们将它拆解成几个阶段处理:

第一阶段:发送启动信号

// 设置为推挽输出模式
GPIO_InitTypeDef gpio = {0};
gpio.Pin = DHT11_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(DHT11_PORT, &gpio);

// 拉低至少 18ms
HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET);
delay_ms(18);

// 拉高并延时 20~40μs 给予上升沿时间
HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET);
delay_us(30);

注意这里不能立刻切换输入模式,要留一点时间让总线上升到位。

第二阶段:等待 DHT11 响应

这才是最容易出问题的地方。我们必须检测两个跳变沿:

// 切换为输入模式
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULLUP;  // 启用内部上拉
HAL_GPIO_Init(DHT11_PORT, &gpio);

uint32_t timeout;

// 等待 DHT11 拉低(应答开始)
timeout = 10000;  // 约 10ms 超时
while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) && --timeout);
if (!timeout) return HAL_ERROR;  // 超时说明没收到响应

// 等待 DHT11 拉高(80μs)
timeout = 10000;
while (!HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) && --timeout);
if (!timeout) return HAL_ERROR;

// 等待 DHT11 再次拉低(80μs)
timeout = 10000;
while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) && --timeout);
if (!timeout) return HAL_ERROR;

这三个 while 循环缺一不可。任何一个失败,都意味着通信未建立。

第三阶段:接收 40 位数据

这里是真正的“逐位采样”。由于每位的起始是低→高跳变,我们先等待上升沿,然后在中间位置判断电平长短:

uint8_t R[5] = {0};

for (int i = 0; i < 5; i++) {
    for (int j = 0; j < 8; j++) {
        // 等待每一位的起始高电平
        timeout = 10000;
        while (!HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) && --timeout);
        if (!timeout) return HAL_ERROR;

        // 延迟 40μs 后读取电平
        delay_us(40);
        if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)) {
            R[i] |= (1 << (7 - j));  // 是 '1'
        }

        // 等待当前位结束(下一个低电平到来)
        timeout = 10000;
        while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) && --timeout);
        if (!timeout) return HAL_ERROR;
    }
}

为什么是 delay_us(40) ?因为:
- 如果是 0 ,高电平只有 ~27μs,40μs 后已经回落;
- 如果是 1 ,高电平有 ~70μs,40μs 后仍是高电平。

这就是典型的“中值判别法”。

第四阶段:校验与返回

最后一步别忘了验证数据完整性:

if ((R[0] + R[1] + R[2] + R[3]) == R[4]) {
    data->humidity_int = R[0];
    data->humidity_dec = R[1];  // 实际为 0
    data->temp_int = R[2];
    data->temp_dec = R[3];      // 实际为 0
    data->checksum = R[4];
    return HAL_OK;
} else {
    return HAL_ERROR;
}

DHT11 的校验和是前四字节之和,非常简单但也有效。


串口调试:让 printf 成为你的眼睛

在嵌入式开发中,没有日志等于闭眼开车。幸运的是,我们可以把 printf 重定向到 UART,实时查看运行状态。

配置 USART2

使用 CubeMX 配置 USART2:
- 波特率:115200
- 数据位:8
- 停止位:1
- 无校验
- TX 引脚映射到 PA2(或你实际使用的引脚)

生成代码后,在 main.c 中添加如下重定向函数:

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE {
    HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

这样你就可以直接用:

printf("Temp: %d°C, Humidity: %d%%\r\n", temp, humi);

再也不用手动拼字符串了 ✅

💡 提示:如果发现打印乱码,优先检查波特率是否匹配、晶振频率配置是否正确。


主程序逻辑:不只是无限循环

很多教程到这里就结束了,但实际上还有很多细节值得打磨。

int main(void) {
    HAL_Init();
    SystemClock_Config();           // 168MHz
    MX_GPIO_Init();
    MX_USART2_UART_Init();
    delay_init();

    printf("🌡️  DHT11 Monitoring Started...\r\n");

    DHT11_DataTypedef sensor_data;

    while (1) {
        HAL_StatusTypeDef status = DHT11_Read(&sensor_data);

        if (status == HAL_OK) {
            printf("✅ Temp: %d°C | Humidity: %d%%\r\n",
                   sensor_data.temp_int,
                   sensor_data.humidity_int);
        } else {
            printf("❌ DHT11 Read Failed!\r\n");
        }

        HAL_Delay(2000);  // 2秒采集一次
    }
}

看起来很简单,但我们还可以做得更好:

✅ 加入重试机制

int retry = 0;
while (retry < 3) {
    if (DHT11_Read(&sensor_data) == HAL_OK) break;
    retry++;
    HAL_Delay(100);
}

if (retry < 3) {
    printf("✅ Success after %d retries\r\n", retry);
} else {
    printf("💀 All retries failed.\r\n");
}

避免单次异常导致整个系统误判。

✅ 数据滤波建议

对于长期监测应用,可以加入滑动平均滤波:

#define FILTER_SIZE 5
float temp_buf[FILTER_SIZE] = {0};
int idx = 0;

// 更新缓冲区
temp_buf[idx] = (float)sensor_data.temp_int;
idx = (idx + 1) % FILTER_SIZE;

// 计算均值
float avg_temp = 0;
for (int i = 0; i < FILTER_SIZE; i++) {
    avg_temp += temp_buf[i];
}
avg_temp /= FILTER_SIZE;

能有效消除偶然误差。


硬件设计那些事:别让“小问题”毁了整个项目

软件写得再好,硬件不过关照样白搭。以下是几个实战经验总结:

🔌 上拉电阻一定要加!

DHT11 的数据引脚是开漏输出,必须外接上拉电阻。推荐阻值 4.7kΩ ~ 10kΩ ,接在 VCC 和 DATA 之间。

虽然 STM32 有内部上拉,但其阻值较大(通常 >40kΩ),可能导致上升沿缓慢,影响时序判断。

⚡ 电源去耦不可少

在 DHT11 的 VCC 和 GND 之间并联一个 0.1μF 陶瓷电容 ,靠近焊盘放置。这能吸收瞬态干扰,防止因电压波动导致初始化失败。

📏 走线不要太长

传感器引线尽量短, 不超过 20cm 。太长容易引入电磁干扰,特别是在电机、继电器附近使用时。

若必须远距离传输,建议改用数字接口传感器(如 SHT30 via I2C)或增加信号调理电路。

🧊 工作环境限制

DHT11 并不适合所有场合:
- 温度范围仅 0~50°C,超出可能损坏
- 湿度测量在 20%~90%RH 较准,极端干燥或潮湿偏差大
- 长期处于高温高湿环境会加速老化

如果是粮仓、温室、地下室等场景,建议升级为 SHT30 SHT40 ,I2C 接口、精度高、稳定性强。


扩展思路:不止于串口打印

你现在有了一个能稳定采集温湿度的 STM32 节点,下一步想做什么?

🖥️ 添加 OLED 显示屏(I2C)

用 SSD1306 驱动的 0.96 寸 OLED,本地显示温度和湿度曲线,无需电脑也能监控。

ssd1306_Init();
ssd1306_Fill(Black);
ssd1306_SetCursor(10, 10);
ssd1306_WriteString("Temp:", Font_11x18, White);
ssd1306_WriteString(temp_str, Font_11x18, White);
ssd1306_UpdateScreen();

视觉效果立马提升一个档次 👌

🌐 接入 ESP8266 上云

通过 UART 连接 ESP-01S 模块,将数据上传至阿里云 IoT、ThingsBoard 或自建 MQTT 服务器。

AT+CIPSTART="TCP","iot.example.com",1883
AT+CIPSEND=...
{"temp":23,"humi":56,"time":1712345678}

实现远程监控和历史数据分析。

📂 记录到 SD 卡

配合 FATFS 文件系统,每隔一段时间记录一条数据到 CSV 文件,可用于后期分析趋势。

f_open(&fil, "data.csv", FA_OPEN_ALWAYS | FA_WRITE);
f_lseek(&fil, f_size(&fil));
f_printf(&fil, "%lu,%d,%d\r\n", timestamp, temp, humi);
f_close(&fil);

变身简易数据记录仪。

🔋 低功耗优化(STOP 模式)

如果用于野外或电池供电场景,可以在两次采集之间进入 STOP 模式:

__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWREx_EnableLowPowerRunMode();  // 进入低功耗运行模式
HAL_SuspendTick();                  // 暂停 SysTick
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);

// 唤醒后恢复
SystemClock_Config();
HAL_ResumeTick();

电流可以从几十 mA 降到几 μA,续航能力大幅提升。


写到最后:做一个“靠谱”的工程师

这个项目看似简单,但它涵盖了嵌入式开发的几乎所有基础模块:时钟配置、GPIO 控制、精确延时、协议解析、串口通信、错误处理……

更重要的是,它教会我们一件事: 可靠性比功能性更重要

你能写出一百个花哨的功能,但如果系统三天两头死机、数据不准、通信中断,用户根本不会买单。

而真正优秀的嵌入式工程师,不是看他写了多少行炫酷代码,而是看他能不能把一个“看起来很简单的任务”,做到 在各种条件下都稳定运行

就像我们现在做的这个温湿度系统——它可能永远都不会拿去参展,也不会成为爆款产品,但它会让你明白:原来那 18ms 的拉低、那 80μs 的响应、那一个小小的上拉电阻,都在默默决定着系统的成败。

而这,才是硬核技术的魅力所在 💪

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

内容概要:本文设计了一种基于PLC的全自动洗衣机控制系统内容概要:本文设计了一种,采用三菱FX基于PLC的全自动洗衣机控制系统,采用3U-32MT型PLC作为三菱FX3U核心控制器,替代传统继-32MT电器控制方式,提升了型PLC作为系统的稳定性与自动化核心控制器,替代水平。系统具备传统继电器控制方式高/低水,实现洗衣机工作位选择、柔和过程的自动化控制/标准洗衣模式切换。系统具备高、暂停加衣、低水位选择、手动脱水及和柔和、标准两种蜂鸣提示等功能洗衣模式,支持,通过GX Works2软件编写梯形图程序,实现进洗衣过程中暂停添加水、洗涤、排水衣物,并增加了手动脱水功能和、脱水等工序蜂鸣器提示的自动循环控制功能,提升了使用的,并引入MCGS组便捷性与灵活性态软件实现人机交互界面监控。控制系统通过GX。硬件设计包括 Works2软件进行主电路、PLC接梯形图编程线与关键元,完成了启动、进水器件选型,软件、正反转洗涤部分完成I/O分配、排水、脱、逻辑流程规划水等工序的逻辑及各功能模块梯设计,并实现了大形图编程。循环与小循环的嵌; 适合人群:自动化套控制流程。此外、电气工程及相关,还利用MCGS组态软件构建专业本科学生,具备PL了人机交互C基础知识和梯界面,实现对洗衣机形图编程能力的运行状态的监控与操作。整体设计涵盖了初级工程技术人员。硬件选型、; 使用场景及目标:I/O分配、电路接线、程序逻辑设计及组①掌握PLC在态监控等多个方面家电自动化控制中的应用方法;②学习,体现了PLC在工业自动化控制中的高效全自动洗衣机控制系统的性与可靠性。;软硬件设计流程 适合人群:电气;③实践工程、自动化及相关MCGS组态软件与PLC的专业的本科生、初级通信与联调工程技术人员以及从事;④完成PLC控制系统开发毕业设计或工业的学习者;具备控制类项目开发参考一定PLC基础知识。; 阅读和梯形图建议:建议结合三菱编程能力的人员GX Works2仿真更为适宜。; 使用场景及目标:①应用于环境与MCGS组态平台进行程序高校毕业设计或调试与运行验证课程项目,帮助学生掌握PLC控制系统的设计,重点关注I/O分配逻辑、梯形图与实现方法;②为工业自动化领域互锁机制及循环控制结构的设计中类似家电控制系统的开发提供参考方案;③思路,深入理解PL通过实际案例理解C在实际工程项目PLC在电机中的应用全过程。控制、时间循环、互锁保护、手动干预等方面的应用逻辑。; 阅读建议:建议结合三菱GX Works2编程软件和MCGS组态软件同步实践,重点理解梯形图程序中各环节的时序逻辑与互锁机制,关注I/O分配与硬件接线的对应关系,并尝试在仿真环境中调试程序以加深对全自动洗衣机控制流程的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值