简介:本资源是一套基于STM32F103ZET6微控制器与DHT11数字温湿度传感器的嵌入式检测项目完整工程,面向嵌入式初学者、单片机课程设计学生及物联网入门开发者,解决环境温湿度实时采集、协议解析与串口通信等典型实践问题。压缩包共141个文件,含33个头文件(.h)定义外设与传感器接口、32个C源文件(.c)实现GPIO时序驱动、DHT11单总线协议解析、UART数据发送及系统初始化逻辑,另有.o、.d、.axf、.hex等编译中间与输出文件,以及Keil MDK工程配置(.uvprojx、.uvoptx)、启动脚本(.bat)和内存映射文件(.sct),整体大小为3.85MB。已有7354人学习下载,资源结构规范,工程可直接编译烧录,配套完整底层驱动与校验重试机制,涵盖DHT11脉宽识别、数据校验、温度湿度阈值判断等关键实现细节,是掌握STM32基础外设编程与传感器应用的高实用性学习范例。 开头先聊点实在的。DHT11这块传感器,但凡玩过STM32的,几乎人手一块,便宜、资料多、接线简单,看起来随便搞搞就能出数。但真正自己从零写驱动、用HAL库调时序、把数据稳定跑出来,中间那点门道比想象中多。尤其是搭配STM32F103ZET6这种经典大容量芯片,很多人第一步就卡在“GPIO模拟单总线时序”上——不是读回全0,就是偶尔跳变,要么就是湿度永远不变。这篇就把我从原理图到代码、从时序到实测踩过的坑一次讲清楚。
这个项目适合刚学完STM32基础外设、想上手真实传感器的同学,也适合准备做课程设计或小产品原型的朋友。全文围绕STM32F103ZET6和DHT11展开,讲透单总线通信协议、HAL库驱动写法、数据校验与实测排错,最后给几个可以直接抄的扩展方案。
1. 主控与传感器选型:为什么是F103ZET6配DHT11
先说说这套组合的逻辑。STM32F103ZET6是STM32F1系列里的高配型号,144脚封装,512KB Flash,64KB SRAM,片上资源非常丰富:5个USART、3个SPI、2个I2C、1个FSMC、2个高级定时器、3个普通定时器,还有SDIO、CAN、USB等等。对绝大多数入门到中级的嵌入式项目来说,这个配置属于“性能严重过剩但用着踏实”的类型。选它做温湿度检测,不是因为DHT11需要这么强的算力,而是因为这个板子能让你把后续的扩展(屏幕显示、WiFi上传、按键菜单、多传感器融合)全部跑起来不用换平台。
DHT11这边,很多人一上来就嫌它精度低、响应慢,这其实是不公平的。温湿度检测场景分很多种:实验室精密测量那是SHT30、SHT35的活;但大棚环境监测、机房温湿度告警、智能家居的舒适度判断,DHT11的±2℃温度和±5%RH湿度精度完全够用。而且它的优势非常明确——数字信号输出,直接进GPIO,不需要ADC,不需要模拟信号调理电路,成本几块钱,坏了随便换。对于刚接触传感器通信协议的人来说,DHT11的单总线协议比I2C、SPI更原始、更直观,能逼着你把时序这件事彻底搞明白。
选型上还有一个关键点:DHT11的供电范围是3.3V到5.5V,STM32F103ZET6的GPIO是3.3V电平,两者可以直接对接。如果用的是5V单片机的板子,反而要在数据线上加一个电阻分压或者电平转换。这一点很多教程没强调,导致有人拿着51开发板的经验往STM32上套,结果逻辑电平不匹配,读数飘得没法看。而在STM32上,直接把DHT11的VCC接3.3V、GND接GND、DATA接任意一个GPIO(我习惯用推挽输出模式),就能开始干活了。
再补一句关于“为什么不选DHT22”的坑。DHT22精度确实高不少,但时序比DHT11复杂,且价格翻好几倍。如果你是第一次调单总线,建议先用DHT11把协议跑通,代码结构设计好,之后想换DHT22只需要改时序参数和数据处理部分,驱动框架不用推倒重来。
2. DHT11单总线协议的核心时序:看懂这张时间图才算入门
DHT11走的是单总线协议,一根数据线既做发送又做接收,主机和传感器之间靠严格的时间长短来区分0和1。很多人的代码读不出数据,根源就是没把时序图吃透,照抄的代码只知其然不知其所以然,一旦环境变了(比如换了引脚、换了主频)就直接翻车。
正常一次完整通信的流程是这样的:
- 主机把数据线拉低,持续至少18ms,这叫起始信号。此时DHT11被唤醒。
- 主机释放数据线,由于线上有上拉电阻(STM32的GPIO内部可以开上拉),电平回到高。
- 等待20~40us,DHT11开始响应,先把数据线拉低80us,再拉高80us,表示“我准备好了”。
- 然后DHT11连续发送40位数据:8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。
- 发完后DHT11释放总线,之后如果需要再次读取,得间隔1~2秒以上。
这里每一个”位“的传输方式是这样的:数据线先被DHT11拉低50us,然后拉高。拉高的时间长短决定这一位是0还是1——高电平持续26~28us判为0,持续70us左右判为1。整个读位过程,主机需要在高电平区间内快速采样,采早了采晚了都会误判。
收到时序图先说这么多,具体到STM32代码上,最大的挑战不是协议本身,而是”微秒级的延时“怎么实现。
STM32F103ZET6最高主频72MHz,一个CPU周期大约是13.9ns。HAL库自带的HAL_Delay只能做到毫秒级,用来看LED闪烁没问题,但DHT11的时序好多地方是几十微秒级别,毫秒延时根本没法用。你当然可以用定时器做微秒延时,不过最直接、最省事的办法是用一个空的for循环来凑时间。关键是要知道你的空循环大概多少遍能凑出1us,这个后面在代码部分详细说。
DHT11另外有一个硬性要求:两次读取间隔至少要1秒,最好1.5秒以上。因为传感器内部采样周期就是1秒左右,你读得太频繁,它上一次的数据还没更新,读到的永远是旧值,而且有可能让传感器进入异常状态。
单总线协议本身不复杂,但“没上拉电阻”这个细节害了不少人。DHT11的数据线是开漏输出的,必须要外部上拉或内部上拉才能保证高电平正确传输。STM32的GPIO可以配置成内部上拉,省一个电阻,但是内部上拉电阻值通常在30~50k欧姆,偏大,在长线传输时波形边缘会变差。如果是模块化DHT11,板子上一般已经带了上拉电阻;如果是裸传感器自己接,建议在数据线上加一颗4.7k~10k欧姆的上拉电阻到VCC,这样波形干净得多。
3. 用HAL库写DHT11驱动的完整思路:避开的坑和直接能用的代码
网上关于DHT11的代码大部分是标准库版本,用HAL库反而不多见。不是说标准库不行,而是现在ST官方主推HAL,CubeMX生成的工程模板更通用,换芯片型号也方便迁移。下面这套驱动是我基于HAL库从零写的,已经跑过F103ZET6,稳定运行无压力。
3.1 CubeMX基础配置:引脚、时钟、串口
打开CubeMX,选择芯片STM32F103ZET6,做这几步配置:
- RCC:HSE设为Crystal/Ceramic Resonator,这是外部晶振,板子上一般有8MHz晶振。
- Clock Configuration:把系统时钟配到72MHz,HCLK设为72MHz。APB1和APB2的分频影响外设时钟,但不影响GPIO翻转速度测试,按默认来就行。
- GPIO:选一个引脚做DHT11的数据脚,比如PC13(注意和板上LED是否冲突),模式设为GPIO_OUTPUT,初始电平设为High,GPIO模式选Open Drain或者Push-Pull都行——推荐Push-Pull加内部上拉,代码里自己切换输入输出方向。
- USART1:如果你需要把温湿度打印到串口调试助手,开一个串口,异步模式,115200-8-N-1。
生成工程后,在main.c的while循环之前加串口打印初始化,然后就可以开始写DHT11驱动了。
3.2 微秒延时的两种实现方式:定时器法和空循环法
强烈建议用定时器法,准确且不依赖编译器优化等级。用TIM4做一个1MHz的计数定时器,也就是每个tick是1us:
void DHT11_Delay_Init(void) { __HAL_RCC_TIM4_CLK_ENABLE(); TIM_HandleTypeDef htim4; htim4.Instance = TIM4; htim4.Init.Period = 0xFFFF; htim4.Init.Prescaler = 72 - 1; // 72MHz/72 = 1MHz, 即1us计一次 htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim4); HAL_TIM_Base_Start(&htim4); } void DHT11_Delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim4, 0); while(__HAL_TIM_GET_COUNTER(&htim4) < us); }注意htim4这个句柄必须定义成全局变量,不然在另一个函数里没法操作。另外Timer的时钟源是APB1,APB1在72MHz主频下通常是36MHz,定时器时钟还要再乘2,所以Prescaler设72-1正好得到1MHz。
空循环法简单但危险。我实测在-O2优化下,一个空的for(i=0;i<8;i++);大概不是1us,具体要看编译器和优化等级。不同优化等级下循环耗时差异很大,经常出现“Debug模式能读、Release模式全0”的情况。所以如果你图省事空循环,请务必用逻辑分析仪或示波器校准,别拍脑袋定循环次数。这也是为什么我后来全部改用定时器延时,一劳永逸。
3.3 GPIO方向切换:HAL库和寄存器二选一
DHT11是单总线,同一根线既要输出起始信号,又要读取传感器的电平,所以GPIO必须在输出和输入之间来回切换。HAL库的实现方式是修改GPIO MODER寄存器,或者用HAL_GPIO_Init重新初始化,但后者太重,每次调用要重新配置一大串结构体,时序会乱。
推荐用寄存器直接改:
#define DHT11_DATA_PIN GPIO_PIN_13 #define DHT11_DATA_PORT GPIOC #define DHT11_OUT_1() HAL_GPIO_WritePin(DHT11_DATA_PORT, DHT11_DATA_PIN, GPIO_PIN_SET) #define DHT11_OUT_0() HAL_GPIO_WritePin(DHT11_DATA_PORT, DHT11_DATA_PIN, GPIO_PIN_RESET) void DHT11_Set_Output_Mode(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_DATA_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_DATA_PORT, &GPIO_InitStruct); } void DHT11_Set_Input_Mode(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_DATA_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT11_DATA_PORT, &GPIO_InitStruct); }每次切换方向都调用HAL_GPIO_Init,会重新设置整个GPIO的配置,实际测试下来大约耗时几个微秒,对协议来说可以接受,但严格追求时序的话,直接操作寄存器更快:
#define DHT11_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE() // 输出模式 #define DHT11_MODE_OUT() do{ \ GPIO_InitTypeDef GPIO_InitStruct = {0}; \ GPIO_InitStruct.Pin = DHT11_DATA_PIN; \ GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; \ GPIO_InitStruct.Pull = GPIO_PULLUP; \ GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; \ HAL_GPIO_Init(DHT11_DATA_PORT, &GPIO_InitStruct); \ }while(0) // 输入模式 #define DHT11_MODE_IN() do{ \ GPIO_InitTypeDef GPIO_InitStruct = {0}; \ GPIO_InitStruct.Pin = DHT11_DATA_PIN; \ GPIO_InitStruct.Mode = GPIO_MODE_INPUT; \ GPIO_InitStruct.Pull = GPIO_PULLUP; \ HAL_GPIO_Init(DHT11_DATA_PORT, &GPIO_InitStruct); \ }while(0)这两种写法效果相同,区别只在代码风格。我自己的工程里用的是HAL_GPIO_Init方式,因为CubeMX生成的初始化结构体都在,读起来清晰。如果你是做产品、对时序有极致要求,可以考虑用寄存器。
3.4 核心函数:复位、读取响应、逐位采样
先看完整流程代码。主函数里只需要调用一次DHT11_Read_Temperature_Humidity就能得到温湿度。
uint8_t DHT11_Read_Byte(void) { uint8_t i, data = 0; for(i = 0; i < 8; i++) { while(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_RESET); // 等待50us低电平结束 DHT11_Delay_us(40); // 高电平中段采样 if(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_SET) { data = (data << 1) | 0x01; // 高电平超过40us,判为1 } else { data = (data << 1); // 高电平很短,判为0 } while(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_SET); // 等待剩余的级别结束 } return data; } uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] = {0}; uint8_t i; // 主机发送起始信号 DHT11_MODE_OUT(); DHT11_OUT_0(); DHT11_Delay_us(20000); // 拉低至少18ms,这里用20ms更保险 DHT11_OUT_1(); DHT11_Delay_us(30); // 释放总线,等待传感器响应 DHT11_MODE_IN(); // 检查传感器响应信号:先低80us,再高80us if(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_RESET) { while(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_RESET); // 等待低电平结束 DHT11_Delay_us(40); // 避开高电平前沿,在中间段采样更准 if(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_SET) { // 确认有高电平响应,开始读取40位数据 for(i = 0; i < 5; i++) { data[i] = DHT11_Read_Byte(); } // 校验和 if((data[0] + data[1] + data[2] + data[3]) == data[4]) { *humidity = data[0]; *temperature = data[2]; return 0; // 成功 } } } return 1; // 失败 }这段代码有几个细节值得说:
第一个是起始信号拉低时间。DHT11规格书说至少18ms,我直接用20ms,留一点余量。有些代码写成18ms也能用,但如果你的延时函数有误差,还是20ms稳妥。
第二个是读位时的采样点。DHT11的一位数据是:低电平50us,高电平26~28us或70us。我在高电平开始后延时40us再采样,这样如果这一位是1(高电平70us),采的时候还在高电平区间;如果是0(高电平28us),高电平已经结束了,采到的是低电平。这个40us的采样点很关键,放大了容易把1误判成0,放小了容易把0误判成1。
第三个是while死循环的风险。如果传感器没接好、线断了或者时序不对,while(HAL_GPIO_ReadPin(...) == GPIO_PIN_RESET)这个循环会永远卡住,程序直接死在这里。实际产品中绝对要加超时保护。简单做法是给while加个计数上限,比如:
uint16_t timeout = 0; while(HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) == GPIO_PIN_RESET) { DHT11_Delay_us(1); if(++timeout > 200) return 1; // 200us超时 }这样传感器异常时程序不会卡死,主循环还能继续跑,只是返回一次读取失败。
3.5 主函数调用与打印
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); DHT11_Delay_Init(); uint8_t hum = 0, temp = 0; char msg[64]; while(1) { if(DHT11_Read_Data(&hum, &temp) == 0) { sprintf(msg, "Humidity: %d.%d%% RH, Temperature: %d.%d C\r\n", hum / 10, hum % 10, temp / 10, temp % 10); HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 1000); } else { printf("DHT11 read failed\r\n"); } HAL_Delay(2000); // 两次读取间隔至少2秒 } }这里有个常见的误区:DHT11返回的温湿度是“整数+小数”分开的字节。比如湿度整数是52、小数是0,那实际湿度就是52.0%;温度整数是24、小数是0,实际温度就是24.0℃。注意DHT11的温湿度小数部分大部分时候是0,只有在特定精度下才有非零值,所以很多驱动直接忽略小数部分,只用整数。但我建议保留,万一你买到的DHT11精度稍好,数据就能直接用。
4. 实测数据与问题排查:为什么你的DHT11读数总是异常
代码能跑通只是第一步,实际调试中遇到的问题比预想的多得多。我把这几个月里见到的、自己踩过的坑汇总一下,基本覆盖了绝大多数DHT11异常现象。
4.1 现象一:读到的温度和湿度一直为0
出现这个现象,第一个查的不是代码,而是接线。DHT11模块一般有3个引脚:VCC、GND、DATA(有些模块是4个引脚,多一个NC空脚,别接错)。很多人在面包板上插反了VCC和GND,感官上“传感器很烫”,然后数据全是0。把VCC接3.3V或5V,GND接GND,DATA接GPIO,先确认供电。
第二个原因是最低时序要求的GPIO速度。CubeMX里GPIO Speed如果设置成LOW,GPIO翻转速率跟不上微秒级时序,也会导致数据全0。建议把DHT11数据脚的GPIO Speed设为HIGH。
第三个原因是微秒延时不准。如果你用的空循环法,在编译优化等级不同的时候,延时差异巨大。建议按前面说的定时器法重写,或者用逻辑分析仪抓一下起始信号的实际低电平时间。
4.2 现象二:数据能读到,但偶尔跳变,比如湿度突然从50%跳到99%
最典型的原因就是采样点太靠边缘。DHT11的高电平脉冲宽度本身就有一个范围(1的脉宽是70us左右,0的脉宽是28us左右),如果你的采样延时设置在边界值附近,一点点噪声就会导致误判。解决办法是把采样点尽量放在高电平的中间位置,即延时40~50us处采样。
另外一个因素是电源纹波。DHT11对电源稳定度有一定要求,如果用开发板的3.3V供电,而板子上的稳压芯片质量一般,在传感器启动时电流波动可能导致数据异常。可以尝试在DHT11的VCC和GND之间加一个10uF或100nF的滤波电容,实测对读数稳定性有帮助。
4.3 现象三:串口每隔一次才能读到一次数据,交替成功和失败
这种问题通常出在读时序上,特别是响应信号的判断。DHT11的响应信号是拉低80us再拉高80us,很多代码在拉低结束后直接进入数据读取,但这时高电平还没有到来,或者刚好在高电平上升沿附近,稳定度不够。
我的做法是在检测到低电平后,等它拉高,再延时40us才开始读数据。这样确保进入读位循环时,数据线已经稳定在高电平的中间段,后续逐位采样也相对从容。
4.4 现象四:长时间运行后,传感器读数完全不更新
DHT11长期跑容易“卡死”,尤其是电源质量差、或者主机发送了不规范的起始信号时。解决办法有两个:
- 在复位起始信号前,先给DHT11一个低电平“复位脉冲”,拉低1ms再释放,让传感器恢复。
- 如果多次读取失败,可以用GPIO输出一个短暂的拉低脉冲(大约50us)再释放,强制传感器重置状态机。
实际产品中更合理的方案是做到“三次读取失败才重新初始化”,避免单次偶发故障直接导致系统上报错误数据。
4.5 关于数据合法性,必须加一个过滤
校验和只能保证数据字节之和正确,但无法防止传感器在异常状态下传回“合理但错误”的数据。比如湿度整数字节可能返回一个明显不合理的值,比如200%。这种做法在教程里很少见到,但实际做项目非常必要。我的做法很简单:
if(hum <= 100 && temp <= 80) { // 数据在合理范围,采信 } else { // 数据非法,丢弃本次读取 }加上这个判断后,线上运行的稳定性会明显提升,不会出现某次异常读数直接触发告警或者控制逻辑的误动作。
5. 画原理图的细节:嘉立创EDA里DHT11符号的坑
在看热词的时候发现不少人在问“DHT11原理图嘉立创怎么画”,实际画过的都知道,嘉立创EDA里DHT11的符号库有好几个版本,选错了会让PCB打样回来对不上。
先说明一个版本问题:嘉立创EDA中DHT11一般有“传感器模块”和“裸元件”两种封装。如果你是买那种板载模块(带电阻、带LED、带排针的),原理图里接4根线(VCC、GND、DATA、NC)或者3根线(VCC、GND、DATA)都可以。但如果你是买裸传感器自己贴片,要注意它的封装是4脚的L811或者类似的插件封装,引脚定义是:1脚VCC,2脚DATA,3脚NC,4脚GND。这个排列和常见的3脚模块不一样,千万别按模块的引脚顺序画进原理图,不然打样回来焊上就冒烟。
另外新手容易忽略的是DHT11的数据线上拉电阻。模块上一般已经集成了,裸传感器推荐在原理图上加一个4.7k~10k欧姆的上拉电阻到VCC。如果画原理图时用的是嘉立创EDA的“DHT11模块”符号,它可能自带了一个电阻的示意,但如果你不用模块只用裸传感器,一定要自己补上拉。
画PCB时还有一个注意点:DHT11尽量放在板边或者远离发热元件的地方。虽然DHT11本身测的是环境温湿度,但旁边如果有大功率电阻、稳压芯片、电机驱动,温度会被局部拉高,湿度也会受影响,读出来的数据就不是真实环境值了。这是我见过很多课程设计翻车的重灾区。
原理图层面能说的就是这些,重点不是“怎么画这个符号”,而是“DHT11的电气连接和引脚定义一定要搞清楚”,剩下的画图操作本身在嘉立创EDA里非常直观,拖一个元件、连三根线、加上拉电阻、放置排针,十分钟就能搞定。
6. 进阶扩展:从“能读温湿度”到“能用温湿度”
读到了温湿度,项目其实才走完一半。怎么把数据变成可用的东西,才是这个项目的价值所在。
6.1 显示方案:OLED和LCD的选择
最常用的搭配是0.96寸OLED(I2C接口)。F103ZET6的I2C1可以直接驱动,代码量不大。OLED的优点是功耗低、显示内容丰富,可以同时显示温度、湿度、时间、状态。LCD1602则是很多课程设计的老选择,但需要占用更多GPIO,且屏幕本身不带字库,要自己维护字符表,开发效率低一些。手上资源充足的话,建议直接上OLED。
6.2 联网方案:让温湿度“上云”
如果想把数据传到手机或服务器,F103ZET6可以通过USART接ESP8266,或者用板载的以太网接口(如果有)。协议方面,最简单的做法是MQTT,用EMQX或者公共broker作为中转,手机端用MQTT客户端订阅主题。这样大棚、机房、仓库都能远程看实时温湿度。
在这个环节有一个很容易踩的坑:DHT11的数据更新速率太慢(1Hz),如果以这个频率往云平台推数据,对服务器来说没什么压力,但很多免费MQTT broker对单个客户端的消息速率有限制,短时间连续上报会被限流。建议本地做一下均值缓存,每10秒或者每30秒上报一次,这样既减少带宽,也让数据曲线更平滑。
6.3 控制联动:超过阈值就报警
温湿度检测的上层逻辑是控制。我的项目里做了这样一个简单联动:温度超过30℃时,打开继电器驱动风扇;湿度低于40%时,打开加湿器;超过阈值同时蜂鸣器报警。F103ZET6的GPIO直接驱动继电器模块(注意要光耦隔离,别用GPIO直接驱动大电流继电器),再用一个无源蜂鸣器接在另一个GPIO上,代码就是在现有温湿度读取逻辑上加了几个if判断。
这块我用的是极简的状态机思路,避免在主循环里堆一堆嵌套if:
typedef enum { STATE_IDLE, STATE_ALERT, STATE_FAN_ON, STATE_HUMIDIFIER_ON, } ControlState;每个状态里根据温湿度区间做切换,同时带一个“状态保持时间”,防止温湿度在阈值附近抖动导致继电器频繁开关。这个细节很关键,继电器频繁动作不仅费电,还会明显缩短寿命。
6.4 低功耗优化:做电池供电的温湿度节点
如果你打算做便携式或无线节点,F103ZET6的功耗其实偏大,正常工作在50mA级别。但如果只是做实验室演示,功耗问题可以忽略。真要考虑低功耗,可以切换到STM32L0系列或者用F103ZET6的STOP模式加定时唤醒。不过这就是另一个话题了,等把DHT11和主控玩熟之后再去做低功耗优化会容易很多。
7. 一些让我印象深刻的实测细节
最后写几个这次调试中印象比较深的点,希望对你有用。
第一,DHT11的数据线在STM32上要用推挽输出,不要用开漏输出。开漏输出需要外部上拉,而STM32内部上拉的阻值偏大,信号上升沿变缓,读时序时容易出错。推挽输出直接输出高低电平,配上内部上拉,信号干净利落。
第二,读取DHT11时要把中断关掉。如果在读时序的过程中来了一个定时器中断或者串口中断,哪怕只有十几微秒,也会让采样点偏移,导致数据错判。简单做法是在DHT11_Read_Data开头关闭全局中断,读完再打开:
__disable_irq(); uint8_t ret = DHT11_Read_Data(&hum, &temp); __enable_irq();注意这个操作会让整个读取过程(大约5ms)阻塞所有中断,如果系统里有实时性要求很高的任务,需要考虑用RTOS的信号量或者更复杂的驱动设计。
第三,DHT11的测量值在传感器刚上电的1秒内是不准的,老驱动力强的传感器需要“预热”。所以程序上电后不要立刻读,先等个2秒再进主循环,或者第一次读取的结果直接丢弃,第二次开始才显示。
第四,如果你用的开发板上有多个传感器共用同一个GPIO中断或者其他外设,建议把DHT11单独放在一个空闲引脚上,别跟按键、LED、调试口混用,否则线上干扰多了,DHT11的时序更容易崩。
第五,关于数据打印。用HAL_UART_Transmit打印字符串时,如果不加超时参数,在DHT11阻塞读时序的过程中串口可能会被堵住。所以建议把printf重定向到串口,并用DMA或者中断方式发送,而不是阻塞式发送。当然,调试阶段用阻塞发送完全没有问题,直到你开始做正式产品时再优化。
DHT11这个传感器太常见了,常见到很多人觉得它“没技术含量”。但真把它调稳、调出可以长期稳定运行的效果,需要的是对时序、电平、电源、代码结构的综合理解。这些经验不会因为DHT11便宜而变得廉价,后面换SHT30、换AHT21、甚至换I2C接口的传感器,核心的调试思路都是相通的:先搞懂时序,再写驱动,再实测排错,最后加保护机制。希望这篇基于STM32F103ZET6的实际项目记录能让你少走几步弯路,一次就把DHT11跑稳。
本文还有配套的精品资源,点击获取