我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

STM32F1驱动DHT11温湿度传感器实战:从环境搭建到数据滤波

STM32F1驱动DHT11温湿度传感器实战:从环境搭建到数据滤波 1. 为什么STM32F1至今仍是嵌入式入门的首选平台如果你在电子论坛或者技术群里问一句新手该从哪颗单片机开始学十个人里至少有六个会告诉你STM32F1。这个答案从十年前延续到今天依然没有被动摇。STM32F1系列是意法半导体基于ARM Cortex-M3内核推出的32位微控制器主频最高72MHz片上资源覆盖从16KB到1MB的Flash、多种封装从LQFP48到LQFP144外设包括多路USART、SPI、I2C、ADC、定时器、CAN等。它之所以能成为国民级入门芯片核心原因有三个资料存量极大、价格足够低、生态工具链成熟。我接触STM32F1是从一块最小系统板开始的当时手头只有一根USB转串口线和几个LED。后来逐步扩展到DHT11温湿度采集、OLED显示、串口屏交互这些实际项目。回过头看STM32F1的学习曲线其实非常友好——它的参考手册和技术文档写得足够详细社区里能搜到的例程覆盖了几乎所有常见外设你遇到的坑大概率已经有人踩过并留下了解决方案。这篇文章面向的是刚拿到STM32F1开发板、或者准备用F1做一个小型数据采集项目的朋友。我会围绕STM32F1 DHT11温湿度传感器这个典型组合展开把从环境搭建、工程配置、驱动编写到数据校验的完整链路讲清楚。中间会穿插一些我在实际调试中踩过的坑和总结出来的经验这些内容在官方文档里通常不会写但对实际项目来说非常关键。注意本文所有操作基于STM32F103C8T6最小系统板Blue Pill和标准外设库如果你用的是HAL库或者别的F1型号思路完全一致只是函数名和配置方式略有差异。2. 开发环境搭建中最容易被忽略的三个细节2.1 工具链选择Keil、IAR还是STM32CubeIDE选工具链这件事很多人一上来就纠结。我的建议很直接如果你目标是快速跑通第一个工程用Keil MDK如果你希望跨平台且不想折腾License用STM32CubeIDE如果你在公司环境里已经有一套IAR的工程模板那就跟着用IAR。对于STM32F1来说这三种工具链都能很好地支持不存在哪个不能用的问题。Keil MDK的优势在于资料多、编译速度快、调试器兼容性好。你随便搜一个STM32F1的例程大概率就是Keil工程。缺点是License问题不过对于F1这种小容量芯片Keil的免费版本32KB代码限制基本够用。STM32CubeIDE的优势是免费、跨平台、集成了CubeMX配置工具缺点是界面基于Eclipse初次使用会觉得有些笨重编译速度也比Keil慢一些。我个人的习惯是快速验证用Keil正式项目用CubeIDE Makefile。这样既能享受CubeMX的图形化配置又能在命令行下灵活构建。2.2 时钟配置别让默认值坑了你STM32F1的时钟树是新手最容易出错的地方之一。默认情况下芯片启动后使用的是内部8MHz RC振荡器HSI经过PLL倍频后系统时钟可以跑到72MHz。但如果你外接了8MHz晶振大部分最小系统板都带了就需要在SystemInit或者CubeMX里正确配置HSE作为PLL源。我见过太多人代码跑起来串口乱码、延时不准最后发现是时钟配置错了。比如你以为系统跑在72MHz实际上跑在8MHz那所有基于SysTick的延时函数都会差9倍。验证方法很简单写一个1秒的延时用示波器或者逻辑分析仪测一下GPIO翻转周期如果不对第一时间查时钟配置。在标准外设库里时钟配置通常在system_stm32f10x.c文件的SystemInit函数里。你需要确认几个关键宏定义#define SYSCLK_FREQ_72MHz 72000000然后在main函数开头调用SystemInit()。如果你用的是CubeMX生成的代码时钟配置会在SystemClock_Config()函数里直接看图形界面里HSE和PLL的设置是否和你的硬件一致就行。2.3 下载器与调试接口SWD比JTAG更省引脚STM32F1支持JTAG和SWD两种调试接口。JTAG需要5根线TCK、TMS、TDI、TDO、nTRSTSWD只需要2根线SWCLK、SWDIO。对于引脚紧张的项目SWD是更好的选择。而且SWD的速度在大多数场景下完全够用下载和调试体验没有明显差异。我用的是ST-Link V2便宜且稳定。接线的时候注意SWCLK接PA14SWDIO接PA13GND和3.3V对应接好。如果你用的是串口下载ISP需要把BOOT0拉高、BOOT1拉低复位后进入Bootloader然后用FlyMcu之类的工具通过USART1下载。这种方式不需要额外的下载器但调试起来没有SWD方便。提示SWD接口在PCB布局时尽量远离高频信号线走线长度控制在10cm以内否则容易出现连接不稳定或者下载失败的情况。3. DHT11的通信时序与STM32F1上的实现逻辑3.1 DHT11为什么用单总线成本与复杂度的权衡DHT11是一款温湿度复合传感器内部集成了电阻式湿度传感元件和NTC温度传感元件输出的是经过校准的数字信号。它最大的特点是采用单总线通信——一根数据线同时完成命令发送和数据接收。这种设计的好处是引脚占用极少接线简单成本低廉代价是通信时序要求严格对主控的延时精度有一定要求。DHT11的测量范围是温度0~50摄氏度精度±2摄氏度湿度20%~90%RH精度±5%RH。对于一般室内环境监测来说完全够用。如果你需要更宽的测量范围或者更高的精度可以考虑DHT22AM2302或者SHT30但DHT11的价格优势非常明显批量采购单价可以低到几块钱。单总线协议的核心在于主机先拉低总线至少18ms作为起始信号然后释放总线等待DHT11响应。DHT11会拉低总线80微秒再拉高80微秒作为响应信号。之后DHT11依次发送40位数据8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。每一位数据以50微秒的低电平开始随后高电平的持续时间决定数据是0还是1——高电平持续26~28微秒表示0持续70微秒表示1。3.2 微秒级延时的实现SysTick还是DWT在STM32F1上实现微秒级延时常见的方法有两种用SysTick定时器或者用DWTData Watchpoint and Trace周期计数器。SysTick是Cortex-M3内核自带的24位递减计数器配置起来简单但如果你已经在用SysTick做毫秒级延时或者RTOS的时基就需要小心处理冲突。DWT的CYCCNT寄存器可以直接读取CPU周期数精度更高但需要先使能DWT。我通常用SysTick来做微秒延时因为配置简单且不依赖调试器。具体做法是将SysTick的时钟源设为系统时钟的8分频即9MHz这样每个计数周期约0.111微秒加载值设为9就可以得到约1微秒的延时。不过更常见的做法是直接用72MHz不分频加载值72得到1微秒。void delay_us(uint32_t us) { uint32_t temp; SysTick-LOAD 9 * us; // 假设系统时钟72MHz8分频后9MHz SysTick-VAL 0x00; SysTick-CTRL 0x01; // 使能不分频中断 do { temp SysTick-CTRL; } while ((temp 0x01) !(temp (1 16))); SysTick-CTRL 0x00; SysTick-VAL 0x00; }这段代码的关键在于LOAD值根据分频系数和系统频率计算CTRL寄存器的bit0使能计数器bit16是计数到0的标志位。轮询等待标志位置位后关闭计数器并清空。注意如果你在中断服务函数里调用delay_us要确保SysTick的中断优先级不会导致死循环。更安全的做法是在延时期间关闭全局中断但这样会影响系统实时性需要根据具体场景权衡。3.3 GPIO模式切换开漏输出与浮空输入的配合DHT11的数据线需要双向通信所以GPIO的模式需要在输出和输入之间切换。在STM32F1的标准外设库里配置GPIO的模式通过GPIO_Init函数完成。输出时通常配置为通用推挽输出GPIO_Mode_Out_PP输入时配置为浮空输入GPIO_Mode_IN_FLOATING或者上拉输入GPIO_Mode_IPU。这里有一个细节DHT11的数据线需要外接一个4.7k到10k的上拉电阻。如果你用的是模块比如常见的三针DHT11模块上拉电阻已经集成在模块上了如果你用的是裸传感器必须自己加。没有上拉电阻的话总线在释放后无法回到高电平通信会直接失败。切换GPIO模式的代码大概长这样// 输出模式 void DHT11_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, GPIO_InitStructure); } // 输入模式 void DHT11_SetInput(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(DHT11_PORT, GPIO_InitStructure); }每次通信前切换为输出模式发送起始信号发送完毕后切换为输入模式等待响应。这个切换过程需要一定的时间但STM32F1的GPIO配置速度很快对DHT11的时序影响可以忽略。4. 从零编写DHT11驱动逐段拆解与实测验证4.1 起始信号与响应检测的代码实现起始信号的流程是主机拉低总线至少18ms然后拉高20~40微秒再切换为输入模式等待DHT11响应。DHT11检测到起始信号后会拉低总线80微秒作为响应然后拉高80微秒准备发送数据。uint8_t DHT11_Start(void) { DHT11_SetOutput(); DHT11_LOW(); delay_ms(20); // 拉低至少18ms DHT11_HIGH(); delay_us(30); // 拉高20-40us DHT11_SetInput(); if (DHT11_ReadPin() 0) { // 检测DHT11响应低电平 while (DHT11_ReadPin() 0); // 等待80us低电平结束 while (DHT11_ReadPin() 1); // 等待80us高电平结束 return 1; // 响应成功 } return 0; // 响应失败 }这段代码里有两个while循环用于等待电平变化。实际调试时如果DHT11没有接好或者上拉电阻缺失程序会卡在while循环里出不来。所以我在实际项目中会加一个超时计数避免死循环uint32_t timeout 0; while (DHT11_ReadPin() 0) { if (timeout 10000) return 0; }这个超时值需要根据系统频率调整10000次循环在72MHz下大约几毫秒足够覆盖DHT11的正常响应时间。4.2 40位数据的逐位读取与校验响应成功后DHT11开始发送40位数据。每一位的格式是50微秒低电平 26~28微秒高电平表示0或70微秒高电平表示1。读取逻辑是等待低电平结束然后延时30微秒后采样总线电平——如果还是高电平说明这一位是1如果已经变低说明是0。uint8_t DHT11_ReadByte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { while (DHT11_ReadPin() 0); // 等待50us低电平结束 delay_us(30); // 延时30us后采样 byte 1; if (DHT11_ReadPin() 1) { byte | 1; while (DHT11_ReadPin() 1); // 等待剩余高电平结束 } } return byte; }读取完5个字节后进行校验前四个字节相加取低8位与第五个字节比较。如果相等说明数据有效否则丢弃本次数据重新读取。uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i; if (DHT11_Start() 0) return 0; for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } if (buf[0] buf[1] buf[2] buf[3] buf[4]) { *humi buf[0]; *temp buf[2]; return 1; } return 0; }4.3 实测中遇到的三个典型问题与解决过程第一个问题是数据偶尔校验失败。排查后发现是延时精度不够——delay_us在中断干扰下会产生偏差导致采样点偏移。解决办法是在读取期间关闭全局中断或者提高delay_us的精度。我最终选择了在DHT11读取函数前后加__disable_irq()和__enable_irq()虽然会影响其他中断的响应但DHT11读取一次只需要几毫秒对系统整体影响很小。第二个问题是上电后第一次读取总是失败。原因是DHT11上电后需要1秒左右的稳定时间如果上电后立即读取传感器还没准备好。解决办法是在初始化后加一个1~2秒的延时或者第一次读取失败后自动重试。第三个问题是长导线导致的通信不稳定。我用了一根约50cm的杜邦线连接DHT11发现数据错误率明显上升。缩短到10cm以内后问题消失。如果项目确实需要长导线建议降低上拉电阻阻值比如从10k降到4.7k或者在数据线附近加一个小电容滤波。5. 数据滤波与异常处理让温湿度读数更可靠5.1 滑动平均滤波在温湿度采集中的应用DHT11的精度本身有限温度±2摄氏度湿度±5%RH加上单总线通信容易受到干扰原始读数偶尔会出现跳变。比如温度突然从25跳到30下一帧又回到25。这种跳变如果直接显示或者上传用户体验会很差。滑动平均滤波是最简单的解决方案维护一个长度为N的数组每次新数据到来时替换最旧的数据然后计算平均值。N的选择需要权衡——N太大响应慢N太小滤波效果不明显。对于DHT11这种每秒最多采集一次实际上DHT11的采样周期建议不低于1秒的传感器N取5~10比较合适。#define FILTER_N 5 uint8_t temp_buf[FILTER_N] {0}; uint8_t humi_buf[FILTER_N] {0}; uint8_t buf_index 0; void Filter_Update(uint8_t temp, uint8_t humi) { temp_buf[buf_index] temp; humi_buf[buf_index] humi; buf_index (buf_index 1) % FILTER_N; } uint8_t Filter_GetTemp(void) { uint16_t sum 0; uint8_t i; for (i 0; i FILTER_N; i) { sum temp_buf[i]; } return sum / FILTER_N; }这种滤波方式对周期性噪声有很好的抑制效果但对突发性的大幅跳变比如传感器故障导致的异常值处理不够理想。更 robust 的做法是结合中值滤波先去掉一个最大值和一个最小值再对剩余数据求平均。5.2 校验失败后的重试策略与超时保护DHT11的校验失败率在良好环境下通常低于5%但在电磁干扰较强的环境中可能达到20%以上。如果每次失败都直接放弃数据更新率会明显下降。合理的做法是加入重试机制连续读取3次只要有一次成功就采用该数据如果3次都失败则保留上一次的有效数据并标记传感器状态异常。uint8_t DHT11_ReadWithRetry(uint8_t *temp, uint8_t *humi, uint8_t retry) { uint8_t i; for (i 0; i retry; i) { if (DHT11_ReadData(temp, humi)) { return 1; } delay_ms(100); // 重试间隔 } return 0; }超时保护同样重要。前面提到的while循环等待电平变化如果没有超时机制一旦传感器断开或者损坏程序就会卡死。我在每个while循环里都加了计数器超过阈值就返回错误码。这个阈值需要根据实际系统频率调试确定太小会误判太大会延长卡死时间。5.3 温湿度数据的显示与串口输出格式采集到的数据最终要呈现给用户。最常见的两种方式是串口打印和OLED显示。串口输出适合调试和上位机记录OLED适合独立设备。串口输出的格式建议包含时间戳、温度、湿度、校验状态方便后续分析printf(Time:%lu Temp:%d Humi:%d Status:%s\r\n, tick, temp, humi, status ? OK : FAIL);OLED显示的话通常用SSD1306驱动I2C接口。显示内容可以设计成两行第一行温度第二行湿度。刷新频率不需要太高1秒一次足够因为DHT11本身的采样周期就限制在1秒以上。提示DHT11的采样周期建议不低于1秒如果读取频率过高传感器内部可能来不及完成转换导致数据错误率上升。我在实际项目中用的是2秒采集一次稳定性和实时性平衡得比较好。6. 从最小系统到完整项目扩展思路与实战建议6.1 加入OLED显示与按键交互的硬件连接当你把DHT11的基本读取跑通之后下一步通常是加一个显示屏和几个按键做成一个独立的温湿度监测仪。OLED屏推荐0.96寸SSD1306I2C接口只需要两根线SCL、SDA和DHT11加起来一共占用3个GPIO。按键可以用两个轻触开关一个用于切换显示页面一个用于手动刷新数据。接线方案如下DHT11数据线接PA0OLED的SCL接PB6SDA接PB7按键1接PA1按键2接PA2。所有未使用的引脚保持默认状态即可。电源方面最小系统板上的3.3V输出足够驱动DHT11和OLED不需要额外供电。软件层面你需要把DHT11驱动、OLED驱动、按键扫描整合到一个主循环里。主循环的结构大概是每2秒读取一次DHT11每100毫秒扫描一次按键每500毫秒刷新一次OLED。这种非阻塞的轮询结构比delay大循环要可靠得多后续加入低功耗模式也更容易。6.2 低功耗场景下的采集策略如果你的项目是电池供电的低功耗就是必须考虑的问题。STM32F1支持睡眠、停止、待机三种低功耗模式。对于温湿度采集这种低频场景停止模式Stop Mode是最合适的选择——功耗可以降到几十微安同时保留RAM内容和寄存器状态唤醒后可以快速恢复运行。具体策略是采集完一次数据后关闭DHT11和OLED的电源如果硬件支持配置一个RTC闹钟或者外部中断作为唤醒源然后进入停止模式。唤醒后重新初始化外设读取数据上传或者显示然后再次进入停止模式。这样平均功耗可以控制在毫安级别用两节AA电池可以运行数月。需要注意的是DHT11本身在上电后需要1秒稳定时间所以每次唤醒后不能立即读取要等1秒以上。这个等待时间会增加平均功耗需要在采集频率和功耗之间做权衡。6.3 常见问题速查表与调试心得下面这张表整理了我在STM32F1驱动DHT11过程中遇到的高频问题方便你快速定位现象可能原因排查方法串口无输出时钟配置错误检查SystemInit和HSE配置数据全为0上拉电阻缺失测量数据线空闲电平是否为高校验频繁失败延时精度不足用逻辑分析仪抓时序调整delay_us读取卡死缺少超时机制在while循环中加入计数器温度跳变电源噪声在VCC和GND之间加100nF电容OLED不亮I2C地址错误用I2C扫描程序确认地址调试DHT11最有效的工具是逻辑分析仪。几十块钱的USB逻辑分析仪配合开源软件可以清晰地看到起始信号、响应信号和每一位数据的时序。当你怀疑延时不准或者时序不对时抓一段波形比反复改代码高效得多。我在实际项目中还发现一个细节DHT11对电源质量比较敏感。如果和电机、继电器等大功率设备共用电源读数会明显不稳定。解决办法是给DHT11单独加一个LDO稳压或者在电源入口加LC滤波。这个经验在数据手册里不会写但实际项目中非常实用。最后说一个关于代码组织的心得把DHT11驱动写成独立的.c和.h文件对外只暴露初始化、读取、滤波三个接口。这样后续换传感器比如换成SHT30时只需要替换驱动文件上层逻辑完全不用改。这种分层设计在项目规模变大之后会省很多事。
返回列表