
简介一套基于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寸OLEDI2C接口、USB转TTL模块用来把数据显示出来或者打印到串口。接线特别简单以F103C8T6为例DHT11模块引脚STM32引脚VCC中间引脚3.3VGND最外侧GNDDATA有字一侧PA0可自定义注意DHT11虽然标称供电范围是3.3V~5V但数据引脚的电平逻辑是跟随供电电压的。如果你用5V给模块供电数据线返回的高电平就是5VSTM32的GPIO不一定扛得住。稳妥的做法是模块用3.3V供电或者用5V供电但数据线上串一个1kΩ电阻做分压。我一般直接3.3V供电实测完全没问题。2. DHT11通信原理与时序细节2.1 单总线协议与数据格式DHT11用的是单总线协议跟DS18B20是同一类路数一根数据线既做发送又做接收靠不同时长的高低电平来区分0和1主机和从机之间通过一套严格的时序完成握手和数据传输。数据格式是固定的40位一次完整读取会收到8位湿度整数部分8位湿度小数部分DHT11固定为08位温度整数部分8位温度小数部分同样固定为08位校验和校验算法很简单前四个字节相加如果低8位等于第五个字节说明这次数据有效。比如收到0010 1101 0000 0000 0001 1000 0000 0000 0100 0101那就是湿度45%、温度24℃校验和0x450x2D0x000x180x00。说的直白一点这个传感器的数据非常原始没有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你在搜索框输入后双击就能进入配置界面。如果你用的是其他型号下面这些配置逻辑完全一致只是时钟树的上限不一样。进入界面后第一步先配RCCSystem 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总线最高36MHzAPB2最高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 modeOutput Open DrainMaximum output speedLow 或 Medium 都行不需要HighGPIO Pull-up/Pull-downPull-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主频下一个空循环周期约为几个时钟周期 // 实测这个系数在F10372MHz下比较准确 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~28us1的高电平持续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的采样周期本身就只有1Hzint 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或者SHT30CubeMX生成的工程编译报错没选对芯片型号或工具链版本确认Project Manager里Toolchain选择MDK-ARM V55.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通过串口把温湿度数据发给ESP8266ESP8266再走MQTT协议推到服务器就能做远程监控。这种玩法在智能家居项目里很常见核心还是你手头已经调通的传感器驱动——底层数据采集的问题解决了上面想接什么都行。回到DHT11本身虽然它精度不高但在很多不需要精确温湿度控制的场景里完全够用机房温度告警、温室大棚监测、孵化器控制、仓库环境记录这些场合看的是一个大概范围DHT11几块钱的成本和稳定的性能反而成了优势。有条件的话我建议你动手之前先备一个逻辑分析仪几十块钱的投资能省下好几个晚上的调试时间。没有也没关系按我上面说的超时判断和阈值窗口法同样能把驱动调好。做嵌入式就是这样踩过坑才知道哪里容易出问题DHT11这个项目作为入门单总线协议和HAL库的练习性价比真的很高。本文还有配套的精品资源点击获取