
简介本资源是一套基于STM32F103ZET6微控制器与DHT11传感器的完整温湿度检测嵌入式项目面向嵌入式初学者及单片机课程实践者解决环境参数采集、协议解析与串口通信等典型物联网感知层开发问题。压缩包含141个文件以33个.h头文件和32个.c源码为主涵盖STM32标准外设库如stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_tim.c等、DHT11驱动实现、UART串口通信配置及Keil工程配置文件uvprojx、uvoptx、sct等辅以.o、.d、.axf、.hex等编译中间与输出文件总大小3.85MB。已有7354人学习下载资源结构清晰包含可直接编译运行的Keil工程、完整的GPIO时序控制逻辑、DHT11单总线协议解析代码、数据校验与错误重试机制以及基础串口调试输出功能是掌握STM32底层驱动开发与传感器集成的实用入门范例。 做单片机开发有一段时间的朋友肯定都有过这种经历手里的开发板换了一块又一块点灯、串口、按键都玩腻了总想做一个能“感知环境”的东西。如果你手里正好有一块STM32F103ZET6开发板又不想一上来就折腾操作系统或者以太网那些复杂外设那基于DHT11的温湿度检测是一个非常合适的起点。这个项目看着不起眼但做完之后你就会发现GPIO操作、单总线协议、微秒级延时、串口打印这些嵌入式基本功全部被串起来了对STM32的HAL库开发也会有更踏实的感觉。DHT11这颗传感器很多人觉得它“太入门了”其实它恰恰适合用来讲清楚单总线时序是怎么回事。STM32F103ZET6作为F1系列的大个子型号144脚、512KB Flash、64KB SRAM用在这种小项目上虽然有点“杀鸡用牛刀”但好处是引脚足够多、外设足够丰富你可以在同一个板子上把DHT11、OLED、串口、按键全接上一次性打通一套完整的小系统。这篇文章会把从选型、硬件连接、CubeMX配置、HAL库驱动到问题排查的整个流程全部过一遍适合刚学完基础知识、想自己独立做一个完整项目的朋友参考。1. 项目整体设计与方案选型思路1.1 为什么这套组合适合作为嵌入式入门项目先说芯片。STM32F103ZET6名字拆开看很直白F103是主流系列Z表示144引脚E是512KB FlashT是LQFP封装6代表工作温度范围是-40到85℃。它的主频72MHz在今天看来不算高但用来做温湿度检测绰绰有余。为什么选它而不是选更便宜的C8T6或者更高级的F407最核心的原因是它够“典型”——F1系列的资料最多、HAL库生态最成熟、淘宝开发板最便宜遇到问题搜一下全是现成的案例。而且ZET6引脚多外设多就算这个项目做完了后面想扩展屏幕显示、舵机控制、CAN通信、USB转串口都不用换板子。再说DHT11。这颗传感器常见的模块几块钱一个三根线VCC、GND、DATA或者四根线多一个NC数字输出走的是单总线协议。精度方面温度±2℃、湿度±5%RH说实话不算优秀但它有两个别的传感器比不了的优势第一单总线协议只占一个GPIO引脚代码写起来也不复杂非常适合用来理解时序类传感器的通信方式第二它直接输出数字信号不需要ADC采样省掉了一大堆模拟电路的知识。对于刚入门的朋友来说能用最少的硬件和代码把一个传感器真实地“跑起来”获得正反馈比什么都重要。如果你之后要做更严谨的项目把DHT11换成DHT22AM2302或SHT30这套代码框架大部分还是可以复用的只是时序参数和数据位长不一样。这也是我推荐先搞懂DHT11的原因——它帮你建立的是“单总线设备如何通信”的思维方式而不是单纯教你调一个库。1.2 系统整体架构与数据流设计整个项目的逻辑流程不复杂DHT11把温度和湿度转换成数字信号通过单总线发给STM32的GPIO引脚STM32解析出40bit数据经过校验后把结果通过串口打印出来或者显示在OLED屏幕上。DHT11发送的数据帧固定是40bit顺序是湿度整数部分8bit、湿度小数部分8bit、温度整数部分8bit、温度小数部分8bit、校验和8bit。这里有个容易忽略的点DHT11的小数部分精度只有0.1所以对大多数型号来说小数位默认输出0。也就是说你从DHT11读到的湿度是整数温度也是整数实际分辨率并不像宣传的“0.1%RH”那样精细。这不影响项目效果但你要心里有数别把数据精度期待得过高。校验和的算法也很简单前四个字节相加取低8位如果和第五个字节相等就认为这次数据有效。比如湿度整数是45小数是0温度整数是26小数是0那么45026071如果校验字节是0x47十进制71数据就是对的。设计整个项目时建议给自己留好调试接口。串口打印几乎是必须的后续所有排查都要靠它。如果手上有多余的OLED屏幕也建议接上把温湿度刷到屏上看起来更完整。我自己的习惯是把DHT11的数据引脚放在一个独立、方便万用表测量的GPIO上这样逻辑分析仪或者示波器没到位的时候至少能用串口打印辅助判断。2. 开发环境搭建与硬件连接要点2.1 CubeMX初始化与引脚分配细节现在的STM32开发基本离不开STM32CubeMX它的好处是把时钟树、GPIO、外设参数全部可视化生成的初始化代码至少能保证“配置没大错”。我建议不管熟不熟练都从CubeMX开始后面改起来也方便。打开CubeMX选芯片STM32F103ZET6RCC的HSE选Crystal/Ceramic ResonatorSYS的Debug选Serial Wire这样能保住SWD下载。时钟树直接把HCLK填72让CubeMX自动算出PLL配置不用手动去调分频倍频系数。DHT11的数据引脚我习惯选PG11或PB7这种“普通GPIO”尽量避免和调试口、晶振脚冲突。比如PA13/PA14是SWD引脚、PC14/PC15是RTC相关引脚虽然做输入输出也能用但没必要给自己埋坑。配置GPIO模式时先按“推挽输出”配一个初始状态后面代码里我会再切换到输入模式这里只要保证引脚能使能就行。速度档位可以选High理论上GPIO翻转速度越快越好虽然DHT11并不吃高速但高一点总没坏处。串口必须配一个。我用USART1引脚PA9/PA10模式Asynchronous波特率1152008位数据、无校验、1位停止位。如果你接了OLED那就再开一个I2C默认I2C1用PB8/PB9速率为400kHz速度太快有些OLED模块反应不过来但400kHz一般没问题。生成代码的时候IDE选MDK-ARM或STM32CubeIDE都行这里不强制。工程生成后HAL基础初始化、GPIO、USART、I2C的初始化代码都已经写好了我们的核心任务就是写DHT11的驱动。2.2 硬件接线与原理图设计注意点如果你买的是DHT11模块接线其实没什么可犹豫的VCC接3.3V或5VGND接地DATA接你的GPIO。但有一点要提醒模块和裸传感器是两回事模块通常已经集成了上拉电阻和滤波电容稳定性好很多裸传感器则需要你自己在数据线上加一个4.7k到10k的上拉电阻到VCC。很多朋友买到的是那种三脚裸传感器结果不加上拉电阻读到一半数据全是0就是这个原因。自己画原理图的话需要注意几个点上拉电阻R1选4.7k到10k不要省这一颗电阻。DHT11的供电引脚旁边加一个0.1uF的陶瓷电容做去耦能吸收电源上的高频噪声对提高读取稳定性帮助很大。数据线尽量走短、走直不要和电机线、PWM线紧贴在一起因为DHT11的时序是微秒级的干扰很容易造成电平毛刺。传感器离MCU距离超过20cm信号完整性就会开始下降这时候优先考虑缩短距离或者换用屏蔽线。供电电压这块我再啰嗦一句。DHT11模块标称支持3.3V到5.5V但和STM32同一个板子上最好都用3.3V供电。如果VCC接5V模块输出的高电平可能是5V对STM32的引脚来说虽然大多数情况下能容忍但长期使用不推荐。反过来如果你用裸传感器接3.3V高电平是3.3V和STM32电平匹配没问题。我的建议是统一用3.3V简单又安全。3. DHT11时序原理与HAL库驱动实现3.1 单总线协议与微秒级延时的关键作用DHT11的通信协议叫做“单总线”。从名字就能看出来它只有一根数据线所有的握手和传输都靠这一根线上的高低电平时序来区分。理解它的时序是写驱动的前提我在这里展开讲一下。主机发送起始信号的过程是先把数据线拉低保持至少18ms再拉高保持20到40us然后释放总线。DHT11收到这个起始信号后会先拉低80us作为应答再拉高80us表示“我准备好了开始发数据”。之后它开始发送40bit数据。每一位数据的发送方式很有意思每个bit都会先拉低50us然后拉高。如果高电平持续的时间是26到28us就代表逻辑0如果高电平持续的时间是70us左右就代表逻辑1。也就是说区分0和1的关键是测量“高电平维持了多久”。接收方在这个过程中需要每几十微秒就去采样一次引脚电平判断最终是高还是低所以对延时的准确性要求很高。这也是DHT11驱动最容易翻车的地方——很多新手一开始直接用HAL库的HAL_Delay()做微秒级延时结果发现读出来的数据全是0xFF。原因很简单HAL_Delay()是基于SysTick实现的最小单位是1ms你调用HAL_Delay(30)它可能延时几毫秒时序完全对不上。DHT11需要的是微秒us级的延时必须用其他方式实现切记。3.2 三种微秒级延时方案对比与推荐既然HAL_Delay不能用那怎么实现us级延时我给大家梳理三种常见方案各有优劣新手建议直接用第三种。方案一for循环空转。写法最简单比如void delay_us(uint32_t us) { uint32_t i; for (i 0; i us * 8; i) { __NOP(); } }但问题很明显这个循环次数和编译器优化等级、MCU主频强相关你在-O0下能跑通开-O2可能就完全乱套。而且如果你换了一块主频不同的芯片系数又得重新调。只适合临时验证不适合正式项目。方案二定时器延时。用一个基本定时器比如TIM4做us级定时配置成1us计数一次在延时函数里清零、等待CNT到达目标值。优点是准确不依赖主频缺点是占用一个定时器外设。如果项目外设比较紧张就有点浪费。方案三DWT延时。DWT是Cortex-M3内核自带的调试硬件其中有一个CYCCNT寄存器会随着内核时钟周期递增。我们只要读取这个寄存器的差值就能精确实现us级延时不占用任何外设资源。F103全系列支持代码量又少。强烈推荐方案三具体使用方式见下一节。它在我后来做的很多项目里都派上了用场连RTOS下的短延时都靠它。3.3 DWT微秒级延时实现DWT延时的原理是通过使能DWT-CYCCNT周期计数器来实现。初始化时要先使能TRCENA然后使能CYCCNT。代码实现如下#include stm32f1xx_hal.h /** * brief 初始化DWT延时功能 */ void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } /** * brief 微秒级延时 * param us 延时微秒数需小于UINT32_MAX/72 */ void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks) { } }注意SystemCoreClock在HAL库中默认就是72MHz所以(SystemCoreClock / 1000000U)就是72表示1us对应72个时钟周期。us乘以72就是目标周期数。这里没有处理溢出问题但72MHz下延时几千微秒完全不会溢出日常使用够了。初始化函数放在main函数最前面调用一次即可。3.4 完整驱动代码与逐段解读有了DWT延时DHT11驱动就顺理成章了。这里给出我实际用过的精简版代码注释写得比较全。先定义引脚和模式切换宏// dht11.h #ifndef __DHT11_H #define __DHT11_H #include main.h #define DHT11_GPIO_PORT GPIOG #define DHT11_GPIO_PIN GPIO_PIN_11 #define DHT11_GPIO_CLK_EN __HAL_RCC_GPIOG_CLK_ENABLE() // 切换为输出模式推挽 #define DHT11_OUT_MODE() do{ \ GPIO_InitTypeDef GPIO_InitStruct {0}; \ GPIO_InitStruct.Pin DHT11_GPIO_PIN; \ GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; \ GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; \ HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); \ }while(0) // 切换为输入模式浮空 #define DHT11_IN_MODE() do{ \ GPIO_InitTypeDef GPIO_InitStruct {0}; \ GPIO_InitStruct.Pin DHT11_GPIO_PIN; \ GPIO_InitStruct.Mode GPIO_MODE_INPUT; \ GPIO_InitStruct.Pull GPIO_NOPULL; \ HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); \ }while(0) uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature); #endif然后是主驱动文件// dht11.c #include dht11.h /** * brief 读取一个字节 */ static uint8_t DHT11_Read_Byte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { // 等待数据位开始低电平开始 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET); // 从低电平结束开始延时40us DWT_Delay_us(40); // 40us后采样如果仍为高则判定为1 if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { data | (0x80 i); } // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET); } return data; } /** * brief 读取DHT11数据 * retval 0:成功 1:应答失败 2:校验失败 */ uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0}; // 主机起始信号拉低至少18ms再拉高20-40us DHT11_OUT_MODE(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); DWT_Delay_us(30); // 切换为输入读取应答 DHT11_IN_MODE(); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { return 1; // 无应答 } while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET); // 等待应答低电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET); // 等待应答高电平结束 // 读取40bit数据 for (int i 0; i 5; i) { buf[i] DHT11_Read_Byte(); } // 校验 uint8_t sum buf[0] buf[1] buf[2] buf[3]; if (sum ! buf[4]) { return 2; // 校验失败 } *humidity buf[0]; *temperature buf[2]; return 0; }这里有几个细节要特别说明。第一切换输入模式后我用的浮空输入而不是下拉输入。因为模块上已经有上拉电阻浮空输入配合外部上拉读取到的空闲电平是确定的不会因为下拉电阻强行拉低而干扰DHT11的数据。裸传感器场景下也一样外部上拉已经存在GPIO内部不需要再配置上拉。第二DHT11_Read_Byte里用while等待电平跳变如果没有超时保护一旦DHT11异常程序会卡死在等待循环里。如果要做健壮性更强的版本建议给每个while加一个超时计数超过一定周期就强制返回错误。第三整个读取过程中不要开中断否则一个中断跳进来DWT延时被打断时序就乱了。主函数里调用代码如下int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); DWT_Delay_Init(); uint8_t hum, temp; while (1) { HAL_Delay(2000); uint8_t ret DHT11_Read_Data(hum, temp); if (ret 0) { printf(Humidity: %d.%d%% RH, Temperature: %d.%d C\r\n, hum, 0, temp, 0); } else { printf(DHT11 read error: %d\r\n, ret); } } }printf重定向这块就不展开细写了在MDK下重写fputc或者在STM32CubeIDE里用Retarget都是常见操作。唯一想提醒的是printf的输出速度远慢于DHT11的读取速度所以主循环里务必加HAL_Delay(2000)一方面适应DHT11的2秒采样周期另一方面避免串口输出堆积。DHT11官方手册明确写了“采样周期大于2秒才能保证数据准确性”你如果1秒读一次短时间内测出来可能数据也正常但长期跑容易偶发出错。4. 常见问题排查与实操经验4.1 故障速查表——读不到数据时先查这些我把这几年帮人调试DHT11遇到的高频问题整理成了一张表照着查基本能解决九成问题现象可能原因排查步骤一直返回应答失败返回1数据线接错、引脚配置错误、起始信号拉低时间不够长用万用表量DATA引脚电压确认空闲时是上拉高电平检查CubeMX中引脚号是否和实际接线一致读到数据全是0xFF输入模式配置成下拉或者读取时未等待高电平结束改成浮空输入GPIO_NOPULL确认模块/电路有上拉电阻校验一直失败返回2信号干扰、线太长、读取过程中被中断打断缩短数据线距离读取前临界区关中断在VCC和GND之间加0.1uF电容温度读数明显偏高传感器靠近发热源、供电电流不稳定、长时间连续读取发热让传感器远离板载稳压芯片连续读取间隔加大到2秒以上湿度读数固定不变传感器老化、数据总线被长期占用更换传感器检查是否有其他外设把该引脚占用程序卡死在读取函数while等待电平变化时没有超时保护给所有while加超时计数超时则返回错误其中“程序卡死”这个问题我自己早期写过一版没有超时的驱动结果DHT11线松了之后程序就卡在while循环里连按键扫描都不响应了排查了很久才反应过来。之后我养成了一个习惯所有传感器和通信协议的等待循环都必须带超时退出机制。单总线的等待超时尤其重要因为从协议设计上讲主机只能被动等从机拉低电平从机一旦挂了主机永远等不到。4.2 中断、实时性以及和RTOS共存的问题如果项目里只有DHT11和串口打印那么读取期间关中断完全没问题。但如果你把系统升级成FreeRTOS或者同时跑着WiFi模块、OLED刷新、按键扫描那“读取期间关闭全局中断”这种野蛮做法就会带来麻烦WLan模块的数据接收可能因此丢包任务调度也会被延迟。这种情况下我有两个建议。方案一是把DHT11的读取放到一个专用任务里这个任务优先级设得比较低读取前用临界区只保护最关键的一段也就是从“主机拉低起始信号”到“读完40bit”这段一共几毫秒的时间。因为读取过程本身很短偶尔关几毫秒中断对系统影响不大。方案二是干脆给DHT11换一个I2C接口的传感器比如SHT30或AHT20I2C通信天然受时钟控制不会出现“等不到从机响应就死等”的问题RTOS环境下稳定得多。我个人的经验是如果只是做个桌面小玩具DHT11怎么折腾都行一旦要做一个7天24小时跑的生产环境监测节点DHT11的时序脆弱性和精度短板就会暴露出来这时候用I2C传感器是更省心的路线。当然这是后话作为入门学习DHT11依然是性价比极高的选择。4.3 实测优化从“能读到数据”到“稳定读到数据”很多朋友把DHT11驱动写通之后测试时发现数据经常跳动比如湿度一会44%一会46%就开始怀疑传感器坏了。其实DHT11本身分辨率有限数据跳动并不是异常但要追求更稳定的读数有几件事值得做。第一上电后不要立刻读取。DHT11上电后需要1到2秒稳定时间主循环一开始就紧接着发起始信号很容易失败。我一般会在初始化后等2秒再进入读取循环实测失败率能降一大截。第二多次读取取中位值或平均值。比如连续读5次把成功的数据存下来计算平均值能平滑掉偶尔的跳变。但要注意DHT11读取间隔是2秒连续读5次就要等10秒千万别循环里不加延时猛读。第三检查供电纹波。DHT11对电源纹波比较敏感特别是湿度数据如果电源是那种便宜的USB转TTL供电纹波大了湿度数据会飘。给板子加一个100uF电解电容和一个0.1uF瓷片电容能明显改善。5. 扩展方向与项目复盘5.1 从温湿度检测到完整的环境监测终端DHT11项目跑通之后下一步可以做很多有意思的扩展。最简单的是加一个0.96寸SSD1306 OLED用I2C接口显示温湿度和时间瞬间就有一个“产品”的样子了。OLED驱动网上资料很多HAL库下用软件模拟I2C或者硬件I2C都行但要注意硬件I2C在F1系列上有一些坑如果读不到ACK可以先把速率降到100kHz试试。再进一步加上ESP8266或ESP32模块把温湿度数据上报到本地MQTT服务器就可以在手机上看家里温湿度。这个扩展会把项目从“单片机学习”带到“物联网入门”的领域涉及串口AT指令解析、MQTT协议、JSON格式难度跳跃比较大但做一次收获会非常扎实。如果想让采集节点的功耗降下来还可以让系统进入低功耗模式。F103ZET6在停止模式下的功耗能做到几十微安每隔几秒醒来一次读一次DHT11和RTC时间然后继续睡。这个思路在做电池供电的野外环境监测节点时很实用。这里要注意低功耗唤醒后DHT11的初始化代码可能需要重新执行因为有些外设和系统时钟会重启。5.2 F103ZET6和F407ZET6怎么选搜索引擎里经常有人问F103ZET6和F407ZET6的区别。简单来说F407ZET6是Cortex-M4内核主频168MHz带FPU和DSP指令还有USB OTG、以太网MAC等更高级的外设。性能差异在裸跑传感器项目上感受不出来但如果你要在板子上跑摄像头图像处理、音频算法、电机FOC控制那F103会明显吃力F407的浮点运算能力就能派上用场。我的建议是入门阶段选F103ZET6完全够用而且它的资料丰富程度、开发板价格、教材数量都远胜于F407。等你把基础玩熟明确知道自己下一步要做什么方向再决定要不要跳到M4内核。如果只是想进阶玩智能小车、做PID调速闭环F103ZET6反而更合适因为它的定时器多、PWM输出通道多控制多个直流电机绰绰有余还不用处理F407的5V容忍性等新问题。5.3 项目复盘这套方案的优势与局限整个项目做下来我对这套组合的评价是学习价值高工程局限性也明显。优势很明显硬件成本极低、代码量小、调试直观一个周末就能搞定。DHT11的“弱”反而成了优点——因为协议不完善你能逼着自己去思考时序、噪声、超时、校验这些问题而这些恰恰是嵌入式开发里最核心的工程意识。局限性同样真实。DHT11精度低、采样周期长、长期稳定性一般湿度长时间使用后容易漂移。如果要做高精度或高可靠性的方案建议直接换SHT30、AHT20或BME280。其中AHT20价格和DHT11差不多但走I2C协议不需要微秒级延时代码写起来更简单数据精度却高了一个级别。这也是很多新项目不再用DHT11的原因——不是它不好而是同价位的替代方案已经把体验拉高了。最后说一点个人体会如果让我只分享一个这个项目里最值得掌握的技巧我会选DWT延时。当时我第一次写DHT11驱动时用的是for循环延时在-O0优化下能跑一开-O2就全部乱套排查了很久才发现是延时的问题。后来换成DWT代码量不多却彻底解决了“微秒级延时不可靠”的通病。这个技巧在之后调试很多传感器、驱动WS2812灯带、模拟I2C时序时都帮了大忙算是投入产出比极高的一个知识点。DHT11这个项目做完之后你会发现单总线、I2C、UART这些通信方式的基本套路都通了再去碰ESP8266、OLED、各种传感器心里会踏实很多。如果你也正在折腾这块板子希望这篇总结能帮你少踩几个坑。本文还有配套的精品资源点击获取