资讯动态

STM32F1与DHT11实战:从零搭建温湿度监测系统

发布时间:2026/10/28 1:11:39 来源:尧图企业网站定制
1. 为什么STM32F1到今天还值得花时间学1.1 一颗老芯片的生存逻辑如果你最近在电子论坛或者开源硬件社区里逛会发现一个很有意思的现象一边是各种高性能MCU层出不穷一边是STM32F1系列的开发板依然卖得火热尤其是搭配DHT11温湿度传感器的入门套件几乎成了很多电子爱好者的“第一课”。我从2013年前后开始用STM32F103C8T6做项目到现在零零散散也经手过几十个基于F1的案子说实话这颗芯片能活这么久靠的不是性能而是生态惯性和学习曲线的平滑度。STM32F1系列是ST公司基于ARM Cortex-M3内核推出的32位微控制器最经典的型号就是F103系列比如C8T6、RCT6、ZET6这些。它的主频通常跑在72MHzFlash从16KB到1MB不等SRAM从6KB到96KB外设覆盖了GPIO、USART、SPI、I2C、ADC、定时器、CAN、USB等常用接口。这些参数放到今天看确实不算亮眼但它的价值在于资料多到离谱踩坑的人多到离谱解决方案也多到离谱。你遇到任何问题几乎都能找到前人留下的答案。那为什么DHT11温湿度传感器会和STM32F1绑得这么紧因为DHT11是典型的单总线低速传感器它不需要复杂的协议栈只需要一个GPIO就能读取数据非常适合用来练习GPIO输入输出控制、微秒级延时、时序分析这些嵌入式基本功。而且温湿度数据直观做出来的东西能立刻看到效果对初学者来说正反馈很强。我见过不少人的第一个嵌入式项目就是“STM32F1DHT11OLED显示温湿度”虽然简单但把GPIO、定时器、串口打印、I2C显示屏这些知识点全串起来了。1.2 这套组合到底能做什么基于STM32F1和DHT11你可以做的东西其实比想象中多。最基础的是本地温湿度监测用OLED或者LCD1602把数据显示出来进阶一点可以加个ESP8266或者蓝牙模块把数据传到手机或者云平台再复杂一点可以做成闭环控制系统比如温度超过阈值就启动风扇湿度低于阈值就打开加湿器。我甚至还见过有人用它做孵化箱控制器和菌菇房环境监测成本不到五十块钱但解决的是实实在在的生产问题。这套组合适合谁呢如果你是电子、自动化、物联网相关专业的学生它几乎是必修课如果你是转行做嵌入式开发的工程师用它来练手能快速建立对MCU的直觉如果你只是业余爱好者想做个智能家居的小玩意儿它也完全够用。关键是它的学习成本极低一块核心板十几块钱DHT11几块钱加上杜邦线和USB转串口模块总投入不超过三十块就能跑起来。1.3 我为什么还在用它有人可能会问现在都有ESP32、RP2040这些更便宜、性能更强的芯片了为什么还要折腾F1我的理由很实际当你需要一颗芯片稳定跑十年而且不想在软件上花太多心思的时候F1是经过验证的选择。它的参考手册、标准外设库、HAL库、LL库都很成熟社区里关于它的讨论足够深遇到问题不会卡住。而且很多工业设备、医疗仪器、电力监控终端里F1还在批量出货你学会了它找工作或者接项目都用得上。DHT11也是同样的道理。虽然它的精度不高温度±2℃湿度±5%RH响应速度也慢采样周期至少1秒但它便宜、简单、可靠。在不需要高精度数据的场景里比如农业大棚、仓库监控、室内环境记录它完全够用。而且它的单总线协议非常适合用来学习时序敏感型通信这个技能在嵌入式开发里是绕不开的。2. 核心细节解析DHT11与STM32F1的配合要点2.1 DHT11的单总线协议到底怎么读DHT11的通信协议看起来简单但里面有几个坑不踩一次很难记住。它的数据格式是40位8位湿度整数8位湿度小数8位温度整数8位温度小数8位校验和。注意DHT11的小数部分通常都是0因为它的分辨率就是1%RH和1℃所以实际上你只需要关心整数部分。校验和是前四个字节相加取低8位用来判断数据是否有效。通信过程是这样的主机先拉低总线至少18毫秒然后拉高20-40微秒接着释放总线。DHT11检测到这个起始信号后会拉低总线80微秒再拉高80微秒表示响应。之后就是连续的40位数据每一位都以50微秒的低电平开始然后高电平的持续时间决定数据是0还是126-28微秒表示070微秒左右表示1。最后DHT11会拉低总线50微秒结束一次通信。这里的关键是微秒级延时。STM32F1的SysTick定时器可以配置成1微秒中断一次但中断本身有开销实际延时可能不准。我通常的做法是用DWTData Watchpoint and Trace或者定时器直接计数来做微秒延时精度能控制在±1微秒以内。如果你用HAL库的HAL_Delay()那只能做到毫秒级读DHT11会直接失败。2.2 GPIO模式切换的时机与陷阱DHT11是单总线设备一根线既要输出又要输入所以GPIO的模式必须动态切换。STM32F1的GPIO有四种模式输入浮空、输入上拉/下拉、推挽输出、开漏输出。读DHT11的时候我一般这样配置主机发送起始信号时配置为推挽输出速度50MHz先拉低再拉高再释放。释放总线后立刻切换为输入浮空或者输入上拉。注意DHT11内部有上拉电阻但阻值较大通常4.7kΩ到10kΩ如果线太长或者干扰大最好在外部再并一个4.7kΩ的上拉电阻。读取数据时保持输入模式通过读取IDR寄存器判断电平。这里有个容易忽略的细节切换模式后需要短暂延时让GPIO稳定下来。我一般会加2-3微秒的延时否则第一次读到的电平可能是错的。另外如果你用的是开漏输出模式必须确保外部有上拉电阻否则总线永远拉不高。2.3 时序容错与超时处理DHT11的时序要求虽然不算苛刻但也不是随便写写就能跑通的。我遇到过几种典型情况第一种是延时不准导致起始信号宽度不对DHT11根本不响应第二种是读取速度太慢在等待高电平结束的时候错过了下一位的起始第三种是干扰导致校验失败数据偶尔出错。解决这些问题的核心是超时机制。在等待DHT11响应的时候不能死等要设置一个最大等待时间比如100微秒超过就认为通信失败直接返回错误码。在读取每一位数据的时候也要设置超时防止因为干扰导致程序卡死。我通常会在代码里加一个while循环配合计数器比如uint32_t timeout 0; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET) { timeout; if (timeout 1000) return ERROR_TIMEOUT; delay_us(1); }这样即使DHT11没接好或者坏了程序也不会卡住而是返回一个错误让你知道问题出在哪里。2.4 电源与布线的实际影响DHT11的工作电压是3.3V到5.5VSTM32F1的GPIO是3.3V电平所以直接连没问题。但如果你用5V给DHT11供电而STM32F1的GPIO是3.3V那数据线最好加一个电平转换或者至少串一个1kΩ电阻保护IO口。我实测过短距离小于20厘米直接连3.3VDHT11也能正常工作但数据偶尔会飘。布线方面数据线尽量短最好不超过20厘米。如果必须延长建议用屏蔽线并且在DHT11端加一个0.1μF的去耦电容。电源线也要干净如果和电机、继电器共用电源最好加LC滤波。我做过一个项目DHT11和继电器在同一块板上继电器一吸合温湿度数据就乱跳后来在DHT11的电源脚并了一个100μF的电解电容和0.1μF的陶瓷电容问题就解决了。3. 实操过程从零搭建温湿度监测系统3.1 硬件准备与连接先列一下我常用的物料清单物料型号数量备注核心板STM32F103C8T6最小系统板1蓝色药丸板即可温湿度传感器DHT111三针或四针模块都行显示屏0.96寸OLEDI2C接口1SSD1306驱动下载器ST-Link V21用于烧录和调试杜邦线母对母若干建议用不同颜色区分USB转串口CH340模块1用于串口打印调试连接方式如下DHT11的VCC接3.3VGND接GNDDATA接PA0任意GPIO都可以我习惯用PA0。OLED的VCC接3.3VGND接GNDSCL接PB6SDA接PB7这是I2C1的默认引脚。ST-Link的SWDIO接PA13SWCLK接PA14GND接GND3.3V接3.3V。USB转串口的TX接PA10USART1_RXRX接PA9USART1_TXGND接GND。注意DHT11的DATA引脚建议外接一个4.7kΩ上拉电阻到3.3V虽然很多模块自带上拉但外接一个更稳。3.2 工程搭建与时钟配置我用的是Keil MDK或者STM32CubeIDE这里以CubeIDE为例。新建工程选择STM32F103C8T6然后配置时钟RCCHSE选择Crystal/Ceramic ResonatorLSE可以不选。Clock ConfigurationHSE 8MHzPLL倍频到72MHzAHB不分频APB1分频系数236MHzAPB2不分频72MHz。GPIOPA0配置为GPIO_Input初始状态为Pull-upPB6和PB7配置为I2C1的SCL和SDAPA9和PA10配置为USART1的TX和RX。I2C1模式选择I2C速度100kHz或者400kHz都可以。USART1波特率1152008位数据无校验1位停止位。SYSDebug选择Serial Wire。生成代码后先写一个微秒延时函数。我用的是DWTvoid DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }这个延时函数的精度取决于SystemCoreClock72MHz下每个计数是1/72微秒误差很小。3.3 DHT11驱动代码实现先定义GPIO操作宏#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_0 #define DHT11_OUT_H() HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET) #define DHT11_OUT_L() HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET) #define DHT11_IN() HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) void DHT11_SetOutput(void) { 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); } void DHT11_SetInput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); }读取流程uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] {0}; uint8_t i, j; // 主机发送起始信号 DHT11_SetOutput(); DHT11_OUT_L(); HAL_Delay(20); // 至少18ms DHT11_OUT_H(); delay_us(30); DHT11_SetInput(); // 等待DHT11响应 if (DHT11_WaitForLevel(0, 100) ! OK) return ERROR; if (DHT11_WaitForLevel(1, 100) ! OK) return ERROR; if (DHT11_WaitForLevel(0, 100) ! OK) return ERROR; // 读取40位数据 for (i 0; i 5; i) { for (j 0; j 8; j) { if (DHT11_WaitForLevel(1, 100) ! OK) return ERROR; delay_us(40); buf[i] 1; if (DHT11_IN() GPIO_PIN_SET) { buf[i] | 1; if (DHT11_WaitForLevel(0, 100) ! OK) return ERROR; } } } // 校验 if (buf[4] ! (buf[0] buf[1] buf[2] buf[3])) return ERROR_CHECKSUM; *humi buf[0]; *temp buf[2]; return OK; }其中DHT11_WaitForLevel是一个带超时的等待函数uint8_t DHT11_WaitForLevel(uint8_t level, uint32_t timeout_us) { uint32_t count 0; while (DHT11_IN() ! level) { if (count timeout_us) return ERROR_TIMEOUT; delay_us(1); } return OK; }这段代码我调过很多次关键点是延时精度和超时保护。如果你发现读出来的数据一直是0或者校验失败先检查延时函数准不准可以用示波器或者逻辑分析仪抓一下波形看看起始信号是不是20毫秒响应信号是不是80微秒。3.4 OLED显示与串口打印OLED我用的是SSD1306驱动I2C地址通常是0x78或者0x7A。初始化后可以用ssd1306_DrawString之类的函数显示温湿度。我一般会显示三行第一行“Temp: 25 C”第二行“Humi: 60 %”第三行“Status: OK”。如果读取失败第三行显示错误码方便排查。串口打印更简单重定向printf到USART1int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }然后在主循环里每2秒打印一次printf(Temp: %d C, Humi: %d %%\r\n, temp, humi);提示DHT11的采样周期至少1秒所以主循环里不要频繁读取否则数据会不准。我一般用HAL_Delay(2000)或者定时器中断来触发读取。3.5 完整主循环逻辑主循环里我通常这样写while (1) { if (DHT11_Read(temp, humi) OK) { OLED_ShowString(0, 0, Temp: ); OLED_ShowNum(48, 0, temp, 2); OLED_ShowString(0, 2, Humi: ); OLED_ShowNum(48, 2, humi, 2); printf(Temp: %d C, Humi: %d %%\r\n, temp, humi); } else { OLED_ShowString(0, 4, Read Error); printf(DHT11 Read Error\r\n); } HAL_Delay(2000); }如果你想让系统更实时可以用定时器中断每2秒置一个标志位主循环检测标志位再读取这样不会阻塞其他任务。4. 常见问题与排查技巧实录4.1 数据一直为0或者校验失败这是最常见的问题原因通常有三个延时不准、GPIO模式切换不对、上拉电阻缺失。我的排查顺序是先用示波器看起始信号是不是20毫秒再看DHT11有没有拉低总线80微秒响应。如果没有响应检查DATA线是不是接触不良或者DHT11是不是坏了换一个试试。如果有响应但数据不对检查延时函数的精度特别是delay_us(40)那一段40微秒是判断0和1的分界点如果延时偏短1会被读成0。还有一个隐藏坑编译器优化。如果你用-O2或者-O3优化delay_us里的循环可能被优化掉导致延时完全不准。解决办法是把延时函数用volatile修饰或者直接嵌入汇编NOP指令。4.2 读取间隔太短导致数据不变DHT11的内部采样周期是1秒如果你每100毫秒读一次它会返回上一次的数据看起来就像“数据不变”。我见过有人以为是传感器坏了其实是读得太快。解决办法很简单两次读取之间至少间隔1秒最好2秒。4.3 长线传输导致数据跳变如果你把DHT11放在离STM32F1比较远的地方比如1米以上数据可能会跳变。这是因为单总线对分布电容很敏感线越长上升沿越缓DHT11可能识别不到。解决办法是降低上拉电阻比如从10kΩ降到4.7kΩ或者用双绞线加屏蔽层。如果还不行就只能把DHT11靠近MCU或者改用I2C接口的温湿度传感器比如SHT30。4.4 电源干扰导致复位DHT11在发送数据时电流会突然增大如果电源内阻大或者去耦不足电压会瞬间跌落导致DHT11复位或者STM32F1复位。我遇到过一次DHT11和STM32F1共用USB供电读数据时STM32F1偶尔重启。后来在DHT11的VCC和GND之间并了一个100μF的电解电容问题消失。所以电源去耦不能省尤其是电机、继电器、无线模块共存的时候。4.5 常见问题速查表现象可能原因排查方法解决方案数据全0延时不准示波器看起始信号用DWT或定时器做微秒延时校验失败干扰或时序偏差多次读取看错误率加超时重试检查上拉电阻数据不变读取间隔太短看代码延时间隔至少1秒偶尔复位电源跌落示波器看VCC波形加100μF0.1μF去耦电容长线跳变分布电容大缩短线长测试降低上拉电阻用屏蔽线编译后失效编译器优化对比-O0和-O2延时函数加volatile4.6 我的独家避坑经验第一个经验不要用HAL_Delay做微秒延时。HAL_Delay的最小分辨率是1毫秒而且它依赖SysTick中断在中断里调用会死锁。我早期用HAL_Delay(1)想延时1毫秒结果实际延时可能1.5毫秒DHT11直接不响应。第二个经验DHT11的DATA线不要接在有复用功能的引脚上。比如PA0默认是WKUPPA13和PA14是SWD如果你不小心把DHT11接在这些引脚上可能会和调试功能冲突。我一般选PA0到PA7这些纯GPIO。第三个经验读取失败不要立刻报错连续读三次取成功的那次。DHT11偶尔会因为干扰返回错误数据如果每次失败都报警系统会很吵。我的做法是连续读三次只要有一次成功就更新数据三次都失败才报错。第四个经验OLED和DHT11不要共用电源线。OLED在刷新时电流波动较大如果和DHT11共用一根细杜邦线DHT11可能会被拉低。我一般给OLED单独走一根电源线或者在OLED的VCC脚并一个10μF电容。5. 从温湿度监测延伸到实际项目5.1 加一个继电器做闭环控制温湿度监测只是第一步真正有用的是根据数据做控制。比如温度超过30℃就启动风扇湿度低于40%就打开加湿器。继电器模块通常有光耦隔离输入端接STM32F1的GPIO输出端接风扇或加湿器。代码里加一个判断if (temp 30) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 开风扇 } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 关风扇 }注意继电器吸合时会产生反向电动势最好在继电器线圈两端并一个续流二极管比如1N4148否则可能干扰STM32F1。5.2 通过串口屏做本地显示如果你觉得OLED太小可以换串口屏比如陶晶驰的TJC系列。串口屏通过USART和STM32F1通信你只需要发送特定格式的指令就能更新显示。比如t0.txt25就是把t0控件的文本改成25。这种方式比OLED灵活可以做按钮、曲线、报警提示。5.3 数据上云与远程监控如果你想远程看温湿度可以加一个ESP8266模块通过AT指令连上WiFi然后走MQTT协议把数据发到服务器。STM32F1和ESP8266之间用USART通信波特率通常115200。代码里需要解析AT指令的响应处理连接、订阅、发布这些流程。这个部分稍微复杂但网上有很多现成的例程可以参考。5.4 低功耗改造如果项目是电池供电比如放在大棚里那低功耗就很重要。STM32F1支持睡眠、停止、待机三种低功耗模式。我的做法是平时让STM32F1进入停止模式用RTC定时唤醒醒来后读DHT11通过无线模块发送数据然后继续睡。这样平均电流可以降到几十微安一节18650电池能撑几个月。注意进入停止模式前要把GPIO配置成模拟输入或者下拉避免漏电流。5.5 从F1迁移到其他芯片的思路如果你学会了F1DHT11这套组合以后换到STM32F4、G0、或者国产GD32、APM32基本只需要改时钟配置和GPIO初始化DHT11的驱动逻辑几乎不用动。因为单总线协议是纯软件时序和芯片无关。这也是我建议新手从F1入手的原因你学到的是通用技能不是某颗芯片的专有知识。6. 一些关于工具和调试的实在话调试DHT11这种时序敏感的设备逻辑分析仪比示波器更实用。示波器看模拟波形好但逻辑分析仪能直接解码单总线协议把40位数据直接显示出来省得你手动数脉冲。我用的是一款几十块钱的8通道逻辑分析仪配合开源软件抓DHT11的波形一目了然。如果你没有逻辑分析仪也可以用串口打印调试法在代码里把每一位的持续时间打印出来看看0和1的宽度是不是符合预期。虽然麻烦但也能定位问题。还有一点不要迷信模块。市面上很多DHT11模块自带上拉电阻和LED但质量参差不齐。我买过一批模块上拉电阻用的是10kΩ读数据经常失败后来自己换成4.7kΩ就稳了。所以如果你遇到奇怪的问题先检查模块上的元件别急着怀疑代码。最后关于代码风格我建议把DHT11的驱动单独放在一个.c和.h文件里对外只暴露DHT11_Read和DHT11_Init两个函数。这样以后换引脚或者换芯片只需要改驱动文件主程序不用动。这个习惯在项目变大之后会救你很多时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑