资讯动态

STM32F1驱动DHT11温湿度传感器开发实战:从入门到排坑

发布时间:2026/10/28 1:05:34 来源:尧图企业网站定制
如果你搞嵌入式开发不管做产品还是玩DIY大概率绕不开STM32F1系列。这颗芯片最早上市已经十多年了Cortex-M3内核跑到72MHz听起来参数平平无奇但直到今天它依旧是很多入门者的第一块开发板也是不少量产产品的稳定之选。尤其是搭配DHT11温湿度传感器这类经典外设一个最小系统加一个传感器就能跑通“采集、处理、输出”的完整链路对理解嵌入式系统非常有帮助。这篇文章我打算从STM32F1本身讲起聊一聊它为什么到现在还有生命力再带你完整过一遍开发环境搭建、DHT11温湿度传感器的驱动原理、代码实现以及我这些年踩过的坑。如果你正要开始学STM32F1或者想在项目里快速接入DHT11这篇内容可以直接照着操作。1. STM32F1系列到底“强”在哪里1.1 从核心资源看选型逻辑STM32F1系列基于ARM Cortex-M3内核主频最高72MHzFlash从32KB到512KBSRAM从6KB到64KB。放到今天看这个性能连很多Cortex-M4都打不过但它的定位从来不是跑复杂算法而是做“稳定可靠的控制中枢”。常见的型号里STM32F103C8T6算是“网红”级的20KB RAM、64KB Flash价格低、资料多淘宝上一块核心板十几块就能买到。再往上走STM32F103ZET6有512KB Flash和64KB RAM引脚拉到144个可以接LCD屏、外部存储器、多路传感器工业控制里也很常见。选择哪一颗主要看引脚数量、Flash大小以及是否需要CAN、USB这类外设。F1系列的外设规模和灵活度在当时非常激进一步。通用定时器和高级定时器加起来有好几个支持PWM输出、输入捕获、编码器接口USART、SPI、I2C都有ADC是12位有些型号还有DAC部分型号集成了USB Device和CAN控制器。这意味着一个芯片能同时搞定传感器采集、电机控制、通信协议非常适合做小型设备的主控。1.2 生态和资料是它最大的护城河STM32F1系列能火这么久技术规格只是基础真正重要的是围绕它建立的庞大生态。网上搜“STM32F1”能找到开发板教程、开源项目、竞赛方案、量产产品例程几乎任何一种应用场景都有人做过。你遇到问题去论坛一搜基本都能找到答案。开发工具有Keil MDK、IAR、STM32CubeIDE还有开源的GCC工具链。库函数的选择也丰富早期是标准外设库后来ST主推HAL库现在CubeMX生成代码已经成了主流开发方式。再加上ST-Link调试器便宜好用JTAG和SWD调试都非常方便。这些因素叠加在一起让STM32F1的上手门槛极低这也是它常年在入门教程里占据C位的原因。2. 开发环境搭建与工程初始化2.1 工具链怎么选如果你刚接触STM32F1我建议直接用Keil MDK加STM32CubeMX的组合。Keil是老牌IDE编译、下载、调试一体化很多老工程师和教程都基于它进行教学。CubeMX负责图形化配置引脚、时钟、外设自动生成初始化代码这样你就不用去手动翻寄存器手册也不用管HAL库那几百个初始化函数怎么填参数。也有朋友喜欢用STM32CubeIDE这是一个基于Eclipse的免费IDE内部集成了CubeMX直接把配置和编译调试融合在一起。它的缺点是在部分低配置电脑上启动和编译稍慢但胜在免费没有Keil的License限制。我个人在Windows上习惯是CubeMX生成工程Keil编译调试。如果你在Linux或Mac上开发也可以用STM32CubeIDE或者直接装arm-none-eabi-gcc配合Makefile不过这样就要自己管理链接脚本和启动文件对新手不太友好。2.2 HAL库还是标准库这个问题经常被问到。简单说标准库在F1时代非常流行代码直观直接操作寄存器封装好的结构体性能好、代码体积小但ST已经不再维护遇到新芯片就用不了。HAL库是ST当前主推的抽象层API统一支持全系列芯片配合CubeMX生成代码非常便利但会增加一点代码量和时延开销对一些极端性能场景会有影响。对大多数入门和产品开发来说我推荐HAL库。原因很简单开发效率高可复用性强ST也一直对HAL库进行bug修复。如果你追求极致实时性比如硬实时控制里对延时、中断响应都精确到微秒那可以部分用寄存器操作或直接写底层驱动。DHT11这类慢速传感器完全用HAL库就够了。2.3 用CubeMX生成一个最小工程打开CubeMX选择芯片型号比如STM32F103C8T6。首先配置时钟源外部高速时钟HSE选择Crystal/Ceramic Resonator这样可以使用板子上的8MHz晶振。在Clock Configuration页面把PLL倍频到72MHz注意系统时钟源要选PLL总线分频按默认配置即可AHB 72MHzAPB1 36MHzAPB2 72MHz。接下来配置调试接口。很多开发板的SWD引脚是PB3、PA15、PB4等重映射引脚如果不小心把它们占用了会导致下载器连不上芯片。最安全的做法是在System Core - SYS页面把Debug设为Serial Wire同时保留PA13和PA14给SWD使用。GPIO方面如果只是跑DHT11可以把一个普通引脚设为推挽输出比如PA1。后续我们驱动DHT11时要在这根线上做输出和输入切换所以CubeMX里先不设置得非常复杂直接在代码里动态配置即可工程骨架先搭起来。生成代码后打开Keil工程编译下载一颗LED就能跑起来说明环境已经通了。3. STM32F1驱动DHT11温湿度传感器3.1 DHT11的工作原理与单总线协议DHT11是一款很常见的数字温湿度传感器测量范围湿度20%-90%RH、温度0-50℃精度不算高湿度误差±5%RH、温度误差±2℃但对环境监测、室内监控这类场景已经够用了。它和数据线之间走的是单总线协议这意味着同一根线上既传输数据也承担部分时序控制。整个通信过程由主机发起传感器被动应答。DHT11上电后先稳定1到2秒然后等待主机发送起始信号起始信号是一个至少18ms的低电平紧接着释放总线主机把IO设置为输入模式读取从机的响应和数据。DHT11返回的数据一共40位格式如下8位湿度整数部分、8位湿度小数部分、8位温度整数部分、8位温度小数部分、8位校验和。校验和的算法是前四个字节相加取低八位如果和第五个字节相等就说明这次数据有效。在这需要注意DHT11的小数部分在很多型号上恒为0所以实际温度和湿度就是整数部分但代码里还是要按协议解析。单总线对时序要求比较严格主机拉低起始信号的时间不够或者过长都会导致传感器不响应。另外每次读取之间必须间隔1秒以上DHT11的采样本身就是1Hz读得太频繁会读到上一次的缓存数据或直接失败。3.2 DHT11读取时序详解先把整个读取过程拆开看。完整时序由几部分组成主机起始信号主机将数据线拉低保持至少18毫秒再释放总线。释放后微控制器将IO配置为输入模式此时外部上拉电阻会把电平拉高。传感器响应信号DHT11检测到起始信号结束后会把总线拉低约80微秒再释放并拉高约80微秒表示准备好发送数据。这一段响应时间很微妙代码里一般用延时函数轮询等待。数据位传输每一位数据的开始都是一个约50微秒的低电平信号此后高低电平的持续时间决定了该位是0还是1。如果高电平持续约26到28微秒代表“0”如果高电平持续约70微秒则代表“1”。这里有一个初学者容易犯的错读数据时不是在低电平阶段判断而是要等待低电平结束之后开始测量高电平的宽度。很多代码里先等待引脚拉低再等待引脚拉高然后用循环测量高电平持续了多少微秒超过40微秒就判定为1。结束信号40位数据全部传完后传感器会释放总线恢复到高电平空闲状态。所以整个驱动的核心就是三个定时能力能精确拉低18ms、能检测80us级别的低电平响应、能测量几十微秒级别的高电平宽度。STM32F1的HAL库自带的HAL_Delay最小单位是毫秒用来做起始信号没问题后面微秒级的时序就得自己写延时或者用定时器。我在实际项目里习惯用SysTick实现一个微秒级延时函数。简单方法是对系统时钟计数循环操作72MHz下每条空指令大约几十纳秒用DWTData Watchpoint and Trace模块也可以更精确地计数但在DHT11这种宽容度还算可以的传感器上普通的循环延时加调整就能稳定工作。3.3 STM32F1驱动DHT11的代码实现直接讲代码。这里我先说思路把PA1配置成输出发送起始信号然后切换成输入模式等待响应信号再逐位读取40位数据最后做校验和判断。HAL库的标准写法是这样。#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_1 static void DHT11_Set_Pin_Output(GPIO_TypeDef *port, uint16_t pin) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin pin; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(port, GPIO_InitStruct); } static void DHT11_Set_Pin_Input(GPIO_TypeDef *port, uint16_t pin) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin pin; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(port, GPIO_InitStruct); } static void DHT11_Delay_us(uint32_t us) { // 在72MHz主频下以下循环大约可以做到us级别延时 // 实际需要根据编译器优化级别做微调 uint32_t count us * 36; // 近似值 while (count--) { __NOP(); } } static uint8_t DHT11_ReadByte(void) { uint8_t value 0; for (int i 0; i 8; i) { // 等待50us低电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET); // 高电平开始计时 DHT11_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { value | (0x80 i); // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET); } } return value; } uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; uint8_t checksum 0; uint16_t timeout 0; // 设置IO为输出模式 DHT11_Set_Pin_Output(DHT11_GPIO_PORT, DHT11_GPIO_PIN); // 起始信号拉低至少18ms 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); // 切换为输入模式等待传感器响应 DHT11_Set_Pin_Input(DHT11_GPIO_PORT, DHT11_GPIO_PIN); // 等待低电平响应信号 timeout 10000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; } // 等待高电平响应信号结束 timeout 10000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 1; } // 读取40位数据 for (int i 0; i 5; i) { data[i] DHT11_ReadByte(); } // 恢复总线空闲状态 DHT11_Set_Pin_Output(DHT11_GPIO_PORT, DHT11_GPIO_PIN); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); // 校验和 checksum data[0] data[1] data[2] data[3]; if (checksum ! data[4]) return 2; *humidity data[0]; *temperature data[2]; return 0; }代码里有两处需要你自己实测微调。第一处是微秒延时函数中那个36的系数我在Keil默认优化级别下测试是稳定的但开了-O2优化之后循环执行时间会明显缩短你可能需要把系数调高一些比如改成48或60。第二处是DHT11_ReadByte里延时40微秒后去读引脚这个时间点是判断0和1的关键早了会把0的高电平还当低电平晚了又会错过1的高电平尾巴。3.4 数据怎么转换成温度和湿度DHT11返回的原始数据是这样用的humidity data[0]也就是湿度整数部分temperature data[2]温度整数部分如果想要小数部分可以按 data[1] / 10 和 data[3] / 10 换算但很多DHT11模块的小数部分固定为0如果硬件上焊接了DHT12或更新的传感器数据格式会有区别但DHT11这套代码基本兼容。这里需要提醒一下DHT11的温湿度精度本身就有限数据如果出现了明显跳变比如湿度从50%一下跳到90%首先要怀疑传感器探头附近是否有水汽干扰、接线是否松动、供电电压是否稳定而不是先怀疑代码。读到的数值可以直接通过串口打印或者驱动OLED显示。最常见的做法是把串口1接USB转TTL模块然后使用printf重定向到串口这样在电脑上就能直接看数据。4. 串口调试和DHT11数据输出4.1 配置UART并重定向printf在CubeMX里把USART1使能异步模式波特率设115200然后在代码里重定向printf。这里有个经典坑Keil环境下需要包含stdio.h并且实现fputc函数。#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }然后调用方式就很简单printf(Humidity: %d%% RH, Temperature: %d C\r\n, humidity, temperature);要注意HAL_UART_Transmit是阻塞式发送在低波特率、数据量较大时会占用CPU时间比如每秒钟打印一次日志就会阻塞几毫秒到几十毫秒。对DHT11这个场景来说问题不大但在做电机控制或通信协议时建议改成中断方式或DMA方式避免长时间卡住主循环。4.2 用逻辑分析仪看DHT11时序如果你把DHT11接到ST-Link或者任意一款逻辑分析仪上抓到的波形会非常直观。图里你会看到一段很长的低电平这是起始信号随后是传感器返回的响应再往后是一连串窄脉冲每一个脉冲代表一个数据位。看波形是排查DHT11时序问题最有效的手段。比如我一直读不到数据用逻辑分析仪一看就明白了起始信号被中断函数打断了低电平只维持了几百微秒根本没有达到18毫秒传感器自然不响应。所以我在代码里读取DHT11时会临时关闭可能会打断延时的中断或者把读取过程放在临界区里执行。4.3 串口常见问题串口打印出现乱码时先确认两边的波特率、数据位、停止位是否一致再看共地是否做好。USB转TTL模块和开发板之间如果参考地不一致通信线上的电压差就会导致乱码甚至损坏引脚。还有一个容易忽略的是电源质量。如果你的开发板用USB供电同时板子上还接了舵机、电机、继电器启动瞬间电流会拉低VDDMCU的ADC和串口电平都会受影响。我曾经遇到DHT11偶尔读到温度85℃的异常值排查到最后发现是电源纹波干扰了传感器供电加了电容滤波之后问题消失。5. 常见问题与排查技巧实录5.1 程序下载不进去怎么办STM32F1最常见的问题之一就是“芯片连接不上”。出现这个情况先别急着怀疑芯片坏了按照下面顺序排查。第一步检查BOOT0引脚。大部分开发板BOOT0默认接地这没问题但如果BOOT0被拉高芯片会进入系统存储器Bootloader模式不再执行Flash里的用户程序SWD和JTAG下载会被跳过或切入不同状态。把BOOT0跳线恢复为0重新上电再试。第二步检查SWD引脚是否被复用。前面说过PA13、PA14默认是SWDIO和SWCLK但如果你在代码里把它们配置成GPIO而且在引脚初始化之前就把下载口占用了那么连接工具会失败。解决办法是用“connect under reset”模式或者按住复位键再点击下载让芯片还没开始跑用户程序时就建立连接。第三步检查供电和复位电路。STM32F1的NRST引脚如果被电容拉低或者外部干扰导致复位不断也会出现连接不稳定。按一下复位键看芯片是否异常复位必要时在复位引脚上加一个10nF左右的电容滤除干扰。5.2 DHT11一直读不到数据这类问题出现频率最高原因也很集中。先检查硬件电路。DHT11模块一般是三根线VCC、DATA、GND。DATA引脚需要一个上拉电阻780欧姆到4.7K欧姆都常见模块板载一般都有。但如果你自己用光板传感器焊接一定要在数据线上外接一个4.7K欧姆左右的上拉电阻否则高电平上不去时序判断全部乱套。再看供电。DHT11工作电压范围标称3.3V到5.5V直接用3.3V供电是可行的但要注意有些劣质模块内部稳压和电平转换电路不完善3.3V下输出高电平只有2.8V左右STM32F1引脚还能识别但稳定性会差一些走线较长时尤为明显。我自己的经验是如果条件允许直接用5V给DHT11模块供电数据线再串一个1K欧姆电阻接到STM32F1引脚这样兼容性和稳定性都更好。代码方面检查延时是否准确。如果你的微秒延时函数慢了十倍或者快了三倍读取高电平宽度的判断就会出错误。最典型的表现是读出来的数据全是0xFF或0x00偶尔能有几次正确数据。这种情况直接调整延时系数做到“延时后读引脚”时恰好落在数据位的高电平中间位置。另外DHT11相邻两次读取之间的间隔必须大于1秒。如果你在主循环里不停调用读取函数会发现第二次读取大概率失败。所以代码里要加一个简单的状态机或者时间锁存例如记录上次读取的系统节拍间隔达到1000毫秒以上才允许下一次读取。5.3 数据校验失败如何处理校验失败会返回一个错误码代码里一般会丢弃这次数据等待下一轮重新读取。不要试图在数据校验失败后强行使用已读到的部分数据因为极有可能是时序错位导致每一位的0和1都判断反了此时数据完全是垃圾值。想要减少校验失败可以尝试以下几件事。第一把IO翻转和延时函数封装成不会被中断打乱的临界区可以用__disable_irq()和__enable_irq()保护整个读取过程。第二给DHT11数据引脚加一个10nF到100nF的去耦电容放在传感器引脚附近能明显改善噪声环境下的信号质量问题。第三检查地线连接是否可靠特别是传感器和STM32F1之间距离偏长时建议使用屏蔽线或绞合线地线和数据线走在一起。5.4 主循环跑飞和死机问题STM32F1本身稳定性很好程序死机大多来自自己的代码比如数组越界、野指针、硬件卡死或者看门狗没有喂狗。典型场景是系统里接入DHT11后同一个主循环里还跑着其他任务。如果其他任务里有阻塞延时或者等待事件HAL_Delay内部用的SysTick会被占用导致DHT11读取过程中的延时不准。解决办法是独立定时器或者使用非阻塞的软件延时或者干脆把传感器读取放到定时器中断里但是要注意中断服务程序不能执行过长的阻塞操作。另一个常见问题是看门狗配置。如果开启了独立看门狗IWDG或窗口看门狗WWDG主循环必须周期性喂狗。而DHT11读取一次往往耗时几十毫秒如果喂狗时机不合理很可能在传感器读取期间触发看门狗复位。我的做法是看门狗喂狗间隔放到50毫秒以上并且把DHT11读取放到一个独立函数中尽量压缩耗时到10毫秒以内。5.5 提高系统稳定性的几条经验这里分享几条我在实际产品里用到的经验。第一STM32F1的ADC引脚如果悬空会采样到一个不定值干扰程序判断。不需要使用的ADC引脚建议配置为模拟输入模式如果引脚悬空又配置成浮空输入芯片内部的寄生电压可能会导致误触发外部中断。第二所有未使用的GPIO引脚不要写成浮空输入建议配置为模拟输入或者带上拉输入这样可以避免引脚悬空引入多余功耗和噪声。第三DHT11这类单总线传感器的引线越短越好超过20厘米后信号完整性会下降极端情况下需要加缓冲器或者换用I2C接口的温湿度传感器比如SHT30、AHT20。我之前一次项目里DHT11引线布了40厘米结果无论怎么调延时和上拉读到的湿度值总是每隔几分钟跳一次最后换成AHT20问题彻底消失。6. 从DHT11扩展到完整项目6.1 把温湿度数据发布到云端或本地显示当DHT11能稳定读取你就已经跑通了传感器采集的完整流程。下一步可以做的很多接一块0.96寸OLED显示温湿度通过ESP8266或ESP32把数据发到MQTT服务器在本地LCD屏上绘制温湿度趋势曲线甚至加一个继电器根据湿度值自动控制加湿器或排风扇。这些扩展的核心逻辑都不复杂无非是把DHT11_Read_Data读取到的变量赋值给通信模块的数据缓冲区然后按协议发送。很多项目卡住的点不是通信协议而是传感器数据本身不稳定导致发出去的数据忽高忽低。所以在做上位机或云平台之前我建议先把采集端做成“连续采样5次取中间值或平均值”的滤波逻辑。6.2 STM32F1系列还能做什么聊回STM32F1本身。这颗芯片虽然性能不拔尖但外设组合非常实用。CAN总线让它成为汽车电子和工业设备的老常客USB Device功能可以做键鼠、HID设备多个定时器可以输出多路PWM驱动机械臂或云台ADC加DMA又能承担高速数据采集任务。很多入门者会问学STM32F1会不会过时。我的看法是如果你把F1彻底搞懂了F4、H7甚至其他厂商的Cortex-M芯片上手都不会太难。MCU开发的核心是理解外设、中断、时钟、DMA、通信协议这套逻辑在F1上可以学得非常扎实。换个芯片无非是寄存器地址变了、库函数封装变了基本原理完全一致。6.3 建议的学习路径如果你是以学习为目的我建议这样走先用CubeMX生成工程点灯和按键实验练熟然后做串口收发把printf重定向搞清楚接下来就是本文的DHT11实战学会单总线协议和时序驱动再往上是I2C和SPI接口的传感器以及PWM电机控制、ADC采集、定时器中断。等这些都会了再回头看看STM32F1的参考手册你会发现之前很多“背下来”的配置自己已经能推导出来。如果是做产品选型时要把Flash和RAM余量留足尤其是要跑RTOS或通信协议栈时STM32F103C8T6的64KB Flash偶尔会显得紧张。这时候可以换STM32F103RCT6256KB Flash和48KB RAM引脚数量和资源都宽裕很多但价格差异很小。不要为了省几块钱选容量正好的芯片后期加功能时你会发现痛苦加倍。我在实际项目里做产品迭代时最深刻的感受是STM32F1的设计虽然老但设计文档极其完善勘误手册、应用笔记、参考例程都是公开的这比某些芯片厂商“资料缺失只能自己猜”的体验好太多了。就算遇到问题按照手册排查基本都能定位到原因。最后再分享一个实用小技巧DHT11读取成功后如果连续出现校验失败先别急着改代码看看传感器旁边的电源是不是有继电器或电机启停。电磁干扰对单总线协议的影响非常大稍微处理一下供电和布线问题往往就不治自愈了。如果你也想在板子上预留温湿度功能建议优先考虑把DHT11换成I2C接口的AHT20但如果你想彻底理解时序协议、学会单总线调试DHT11依然是绝佳的训练场。

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

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

免费获取报价 →
↑