简介:一套基于STM32CubeMX的DHT11温湿度传感器驱动实例,面向嵌入式初学者与物联网开发者,完整演示从STM32F103ZET6选型、系统时钟配置到GPIO与定时器协同实现单总线通信的开发流程。压缩包共974个文件,以.c源文件、.h头文件、.s启动汇编及.icf链接脚本为主,内含Keil工程、CubeMX的.ioc/.mxproject配置及编译产物,整体12.28MB,目录模块清晰,便于对照学习。已有6779人浏览学习,适合希望动手实践DHT11时序解析与STM32外设配置的读者。资源包含完整可编译工程与全部驱动源码,覆盖DHT11启动信号、应答信号、40位数据读取及校验和解析等关键环节,可直接运行观察温湿度数值,也可作为家庭自动化或环境监测项目的基础代码复用。 STM32CubeMX配上DHT11,算是我做嵌入式这几年里用得最多的入门组合之一。CubeMX帮我们把时钟、GPIO、外设初始化这些重复劳动全干掉了,DHT11又是一个便宜到几块钱的温湿度传感器,两个一搭,不管是做环境监测、智能家居小节点,还是纯粹练手学HAL库,都是非常顺的一条路径。
我见过不少人在这一套上翻车——有人卡在CubeMX生成的工程不知道从哪里下手改代码,有人被DHT11的单总线时序坑得怀疑人生,还有人调了半天读出来的数据全是0xFF。这些问题其实都不难,但确实有一些"文档里不会写明白"的细节。这篇文章我就把整个流程拆开讲清楚,从CubeMX配置到HAL库手写驱动,再到数据解析和常见坑,争取让你照着走一遍就能跑通。
1. 整体思路与硬件准备
1.1 为什么是CubeMX + DHT11的组合
先说句实在话:DHT11这个传感器,精度不高、采样慢、分辨率只有1℃,湿度整数位也是1%RH,跟SHT30、AHT20这些没法比。但它的价值恰恰在于"简单皮实"——单总线协议只需要一根数据线,时序虽然严格但也足够直观,非常适合拿来理解嵌入式里最基础的"GPIO模拟时序"这一套东西。
而STM32CubeMX的价值就更直接了。以前用标准库写STM32,第一步初始化时钟树就能劝退一批新手:RCC、PLL、Flash等待周期、总线分频,哪个配错了都不跑。CubeMX把这些全部可视化,选个晶振、拖一下时钟树、点两下GPIO,代码骨架就出来了,你只需要把精力放在DHT11的时序逻辑上。我自己现在做项目原型也是先用CubeMX搭工程,省下的时间非常可观。
1.2 硬件清单与接线方案
这套方案需要的硬件非常少,基本是手边有什么用什么:
- 主控板:STM32F103C8T6最小系统板,或者你手头任意一款STM32都行,代码逻辑是通用的,只需要改引脚编号。
- DHT11模块:建议直接买蓝色四脚模块,板上自带上拉电阻和LED指示灯,比买裸探头省事很多。裸探头的话需要自己外接一个4.7kΩ~10kΩ的上拉电阻到VCC。
- 杜邦线:三根就够。
- 可选:0.96寸OLED(I2C接口)、USB转TTL模块,用来把数据显示出来或者打印到串口。
接线特别简单,以F103C8T6为例:
| DHT11模块引脚 | STM32引脚 |
|---|---|
| VCC(中间引脚) | 3.3V |
| GND(最外侧) | GND |
| DATA(有字一侧) | PA0(可自定义) |
注意:DHT11虽然标称供电范围是3.3V~5V,但数据引脚的电平逻辑是跟随供电电压的。如果你用5V给模块供电,数据线返回的高电平就是5V,STM32的GPIO不一定扛得住。稳妥的做法是模块用3.3V供电,或者用5V供电但数据线上串一个1kΩ电阻做分压。我一般直接3.3V供电,实测完全没问题。
2. DHT11通信原理与时序细节
2.1 单总线协议与数据格式
DHT11用的是单总线协议,跟DS18B20是同一类路数:一根数据线既做发送又做接收,靠不同时长的高低电平来区分0和1,主机和从机之间通过一套严格的时序完成握手和数据传输。
数据格式是固定的40位,一次完整读取会收到:
- 8位湿度整数部分
- 8位湿度小数部分(DHT11固定为0)
- 8位温度整数部分
- 8位温度小数部分(同样固定为0)
- 8位校验和
校验算法很简单:前四个字节相加,如果低8位等于第五个字节,说明这次数据有效。比如收到0010 1101 0000 0000 0001 1000 0000 0000 0100 0101,那就是湿度45%、温度24℃,校验和0x45=0x2D+0x00+0x18+0x00。
说的直白一点,这个传感器的数据非常"原始",没有I2C那样现成的寄存器地址可以读,全靠你在GPIO上自己掐着时间采样。理解了这一点,后面写代码的思路就清晰了——本质上就是"拉低引脚、延时、拉高引脚、读引脚电平变化"。
2.2 时序拆解:从起始信号到40位数据
DHT11的通信过程可以分为三个阶段,我按实际代码执行顺序给你拆开:
第一阶段:主机发送起始信号
主机先把数据线拉低,至少保持18ms(典型值是20ms),让DHT11检测到起始信号。然后释放总线(拉高),此时数据线被上拉电阻拉回高电平。主机紧接着把GPIO切换成输入模式,准备读取DHT11的回应。
第二阶段:DHT11响应
DHT11检测到起始信号后,会先拉低总线80us左右,再拉高80us左右,作为应答信号。主机此时应该能读到"低-高"各80us的电平变化,如果这一步没读到,说明传感器没在工作,或者接线有问题。
第三阶段:40位数据传输
应答结束后,DHT11开始一位一位地发数据。每一位的传输方式都是:先拉低50us,然后拉高。关键在于拉高的时长——如果高电平持续26us~28us,这一位是0;如果高电平持续70us左右,这一位是1。
所以读数据位的核心逻辑就是:循环40次,每次等待低电平结束,然后测量高电平持续的时间,超过某个阈值就记1,否则记0。阈值我习惯取40us,实测非常稳定。
时序这块是整个DHT11驱动的心脏,也是最容易出bug的地方。后面写代码的时候我会再强调几个坑,这里你先记住一个核心结论:DHT11的时序精度在微秒级,HAL_Delay只支持毫秒级,必须自己写微秒延时,这是整个驱动能跑通的前提。
3. STM32CubeMX工程配置实操
3.1 新建工程与时钟树配置
打开STM32CubeMX,选择芯片型号。我这里用的是STM32F103C8T6,你在搜索框输入后双击就能进入配置界面。如果你用的是其他型号,下面这些配置逻辑完全一致,只是时钟树的上限不一样。
进入界面后,第一步先配RCC:
- System Core -> RCC -> HSE:选择
Crystal/Ceramic Resonator,因为我们板子上有8MHz晶振。 - System Core -> SYS -> Debug:选择
Serial Wire。这一步非常重要,不然你下载一次程序之后,第二次就识别不到芯片了,只能按住复位键强刷。
接下来点开Clock Configuration选项卡,配置时钟树。F103的最高主频是72MHz,配置思路是:HSE 8MHz -> PLL倍频x9 -> SYSCLK 72MHz。CubeMX会自动计算,你只需要在HCLK那一栏输入72,回车,它会自动帮你把PLLM、PLLN、PLLP这些参数填好。
注意:如果
HCLK输入72后变成红色,说明某个分频值不对。F103的关键限制是APB1总线最高36MHz,APB2最高72MHz,你需要在APB1 Prescaler那里设成/2,红色就会消失。这是新手最容易卡住的地方。
3.2 GPIO模式选择与工程生成
DHT11的数据引脚比较特殊,它既要做输出(发送起始信号),又要做输入(读取数据),所以GPIO模式要选成Output Open Drain(开漏输出),然后靠外部上拉电阻把电平拉高。我们的模块自带4.7kΩ上拉,选这个模式最合适。
具体配置路径:Pinout & Configuration -> System Core -> GPIO -> 选中PA0,在GPIO Mode and Configuration里:
- GPIO mode:Output Open Drain
- Maximum output speed:Low 或 Medium 都行,不需要High
- GPIO Pull-up/Pull-down:Pull-up(接上拉,防止空闲时电平飘忽)
如果你用的是推挽输出(Output Push Pull)也不是不行,但切换输入模式时需要额外处理,开漏输出配合上拉电阻更符合单总线的物理特性。这一点是实际调试中总结出来的,文档上通常不会告诉你为什么要这样选。
配置完成后切到Project Manager,填工程名、选存储路径,Toolchain/IDE选择MDK-ARM V5.27(对应Keil),然后点右上角GENERATE CODE生成基础工程。生成的工程里所有初始化代码CubeMX都帮你写好了,你只需要在USER CODE BEGIN和USER CODE END之间补充自己的逻辑就行。
4. HAL库驱动DHT11代码实现
4.1 微秒延时与引脚操作封装
代码的核心是先解决微秒延时问题。HAL_Delay内部是基于SysTick的,默认配置下精度只有1ms,而DHT11需要的是26us、50us、70us这种级别的延时,必须自建微秒延时函数。
在F103这种72MHz主频下,一个简单的空循环就可以实现微秒延时。我用的方案是:
void DHT11_Delay_us(uint16_t us) { // 72MHz主频下,一个空循环周期约为几个时钟周期 // 实测这个系数在F103@72MHz下比较准确 for (uint16_t i = 0; i < us * 8; i++) { __NOP(); } }不同主频系数不同,比如F407跑到168MHz,这个系数就要翻倍。你可以先用逻辑分析仪或者示波器校准,没有仪器的话用下面的"响应超时判断法"间接验证:如果延时严重偏长,DHT11的应答信号会直接错过;如果偏短,读出来的数据位会全是乱码。
引脚操作我封装成两个宏,方便后续复用:
#define DHT11_DATA_H() HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET) #define DHT11_DATA_L() HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET) #define DHT11_DATA_READ() HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin)CubeMX生成的main.h里会自动定义DHT11_GPIO_Port和DHT11_Pin这两个宏,直接用就行。
4.2 起始信号发送与响应检测
这一步是驱动里最关键的一段。主机发送起始信号后,需要把GPIO切回输入模式来读应答信号。因为我们在CubeMX里配置的是开漏输出,切换输入模式有两种方式:一是重新初始化GPIO(麻烦),二是直接修改GPIO的CRL寄存器(更底层)。不过HAL库有个更简单的办法——开漏输出模式下,直接把输出寄存器写1,引脚对外就表现为高阻态,等效于输入模式。
所以发送起始信号的代码可以这样写:
uint8_t DHT11_Start(void) { uint8_t retry = 0; // 主机拉低,发送起始信号 DHT11_DATA_L(); HAL_Delay(20); // 至少18ms,取20ms更稳妥 // 释放总线(写1=高阻态,靠上拉电阻拉高) DHT11_DATA_H(); DHT11_Delay_us(30); // 等待上升到高电平 // 检测DHT11应答:先低80us,再高80us // 先等低电平出现 while (DHT11_DATA_READ() == GPIO_PIN_RESET) { if (++retry > 100) { return 1; // 超时,传感器无响应 } DHT11_Delay_us(1); } // 再等高电平结束 retry = 0; while (DHT11_DATA_READ() == GPIO_PIN_SET) { if (++retry > 100) { return 1; // 超时 } DHT11_Delay_us(1); } return 0; // 响应正常 }这段代码里我加了一个超时计数,防止传感器没接好时主控死循环。这是实际项目中很实用的一个习惯,宁可返回错误,也不要让程序卡死在某一行。
4.3 40位数据读取与校验
读取数据位的核心逻辑就是测量高电平的持续时间。每一位都是"低50us + 高Nus",所以代码流程是:先等低电平结束,然后开始计时高电平,根据时长判断0还是1。
uint8_t DHT11_ReadBit(void) { uint8_t retry = 0; // 等待低电平结束 while (DHT11_DATA_READ() == GPIO_PIN_RESET) { if (++retry > 100) return 0; DHT11_Delay_us(1); } // 高电平开始,延时40us后采样 DHT11_Delay_us(40); if (DHT11_DATA_READ() == GPIO_PIN_SET) { // 高电平持续超过40us,说明是1 // 等待剩余高电平结束 retry = 0; while (DHT11_DATA_READ() == GPIO_PIN_SET) { if (++retry > 100) break; DHT11_Delay_us(1); } return 1; } else { return 0; } }这个"延时40us后采样"的思路很关键:0的高电平只持续26~28us,1的高电平持续70us,所以在第40us处采样,0已经变回低电平了,1还在高电平,阈值选在中间位置,抗干扰最好。
完整读取一轮数据并校验的代码:
uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] = {0, 0, 0, 0, 0}; uint8_t i, j; if (DHT11_Start() != 0) { return 1; // 传感器无响应 } // 读取40位数据 for (j = 0; j < 5; j++) { for (i = 0; i < 8; i++) { buf[j] <<= 1; buf[j] |= DHT11_ReadBit(); } } // 校验 if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { *humi = buf[0]; // 湿度整数 *temp = buf[2]; // 温度整数 return 0; // 读取成功 } else { return 2; // 校验失败 } }注意:DHT11的湿度精度是1%RH,温度精度是1℃,小数部分(buf[1]和buf[3])永远是0,但协议里保留了这两个字节。读取的时候可以忽略,也可以把小数位也拼出来显示,只是实际意义不大。
主函数里的调用方式很简单,加一个2秒间隔的轮询就行,因为DHT11的采样周期本身就只有1Hz:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); uint8_t temperature = 0; uint8_t humidity = 0; while (1) { if (DHT11_ReadData(&temperature, &humidity) == 0) { printf("Temp: %d.%d C, Humi: %d.%d %%\r\n", temperature, 0, humidity, 0); } else { printf("DHT11 read failed!\r\n"); } HAL_Delay(2000); } }如果你不想用串口打印,也可以把数据送到OLED上显示。OLED用I2C接口,CubeMX里把I2C1配好,然后用现成的SSD1306驱动库,把温湿度变量格式化显示出来即可,画面比看串口直观得多。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
把我在调试过程中遇到过的、以及身边朋友踩过的问题整理成一个表格,你看一眼基本就能定位:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 读到的数据全是0xFF | 数据线接错或接触不良 | 检查杜邦线是否松动,确认PA0引脚对应关系 |
| 读到的数据全是0x00 | 上拉电阻缺失或GPIO配置成推挽输出 | 确认模块是否带上拉,GPIO模式改为开漏输出 |
| 卡在DHT11_Start()里出不来 | 传感器没供电、接线错误、延时严重偏长 | 检查VCC/GND,缩小微秒延时系数 |
| 数据偶尔变化但不稳定 | 3.3V供电导致信号边沿不够陡峭 | 换5V供电串1kΩ分压电阻,或加长高电平采样延时 |
| 校验和一直失败 | 微秒延时严重偏差 | 用逻辑分析仪抓波形,对比标准时序逐项校准 |
| 温度恒定,湿度跳变 | DHT11本身精度限制 | 换成DHT22/AM2302,或者SHT30 |
| CubeMX生成的工程编译报错 | 没选对芯片型号或工具链版本 | 确认Project Manager里Toolchain选择MDK-ARM V5 |
5.2 我最想强调的三个坑
第一个坑是GPIO模式选错。很多人照着网上的教程选推挽输出,然后发现怎么都读不到数据,最后在论坛上问了一圈才找到原因。开漏输出配合外部上拉,本质上就是单总线协议要求的"线与"结构——多个设备可以共用一条线,任何一个设备拉低,总线就是低电平。推挽输出不支持这种结构,两个设备同时输出不同电平时会直接短路,虽然STM32内部有保护,但信号会乱掉。
第二个坑是微秒延时的精度。HAL_Delay只到毫秒级别,很多人不知道这一点,直接拿它来凑时序,结果DHT11的50us低电平被延时成了1ms,传感器直接不响应了。我的建议是:用示波器或者逻辑分析仪测一下自己的延时函数是否准确,没仪器就按我上面给的系数先跑,如果失败再微调系数。
第三个坑是初始化时总线电平不稳。CubeMX生成代码后,GPIO的初始状态是配置成开漏输出但默认输出高电平,理论上没问题。但如果你的代码里先执行了拉低操作又没延时够,或者中途复位了传感器,下一次读取就容易失败。DHT11每次上电后需要至少1秒的稳定时间才能接受第一次读取,所以主循环里先HAL_Delay(2000)再开始读数据,成功率会高很多。
5.3 一个实用的波形验证方法
如果你有逻辑分析仪(几十块钱的8通道就够了),强烈建议抓一次DHT11数据线上的波形。你会看到非常清晰的图形:20ms的低电平起始信号、80us低+80us高的应答信号、然后是40个"低50us+高Xus"的数据位。
这个波形图有两个直接的用途:一是验证微秒延时函数是否准,二是判断采样阈值设在哪。我第一次调通DHT11驱动就是靠逻辑分析仪,当时我的延时函数偏大了接近一倍,读出来的数据全是乱的,抓波形后一眼就看出高电平时间整体拉长了,修正系数后一切正常。如果你手头没有仪器,也有一个笨办法:把延时系数从1到20逐个试,看哪个范围内数据能稳定读出来,这个范围就是你的有效窗口,取中间值即可。
6. 从DHT11到实际项目的一点延伸
DHT11这个传感器虽然本身很简单,但把它的驱动调通之后,你可以很快迁移到其他单总线传感器上,比如DHT22、DS18B20,它们的时序逻辑大同小异,只是数据格式和精度不同。DHT22的湿度小数位是真实有效的,温度精度到了0.1℃,读取代码只需要在解析部分做调整。
移植的思路也很清晰:把DHT11的驱动文件抽成一个独立的模块,引脚定义放在头文件里,通过宏来控制。换一个芯片或者换一个引脚,只需要改头文件里的端口和引脚号,驱动代码一个字都不用动。我在实际项目中一直保持这个习惯,上次从F103换到F407,整个传感器模块只花了五分钟就移植完了。
另外一个方向是把数据上传到上位机或者云平台。STM32通过串口把温湿度数据发给ESP8266,ESP8266再走MQTT协议推到服务器,就能做远程监控。这种玩法在智能家居项目里很常见,核心还是你手头已经调通的传感器驱动——底层数据采集的问题解决了,上面想接什么都行。
回到DHT11本身,虽然它精度不高,但在很多不需要精确温湿度控制的场景里完全够用:机房温度告警、温室大棚监测、孵化器控制、仓库环境记录,这些场合看的是一个大概范围,DHT11几块钱的成本和稳定的性能反而成了优势。
有条件的话,我建议你动手之前先备一个逻辑分析仪,几十块钱的投资,能省下好几个晚上的调试时间。没有也没关系,按我上面说的超时判断和阈值窗口法,同样能把驱动调好。做嵌入式就是这样,踩过坑才知道哪里容易出问题,DHT11这个项目作为入门单总线协议和HAL库的练习,性价比真的很高。
本文还有配套的精品资源,点击获取