ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32F1驱动DHT11温湿度传感器:从选型到时序调试全攻略

STM32F1驱动DHT11温湿度传感器:从选型到时序调试全攻略 STM32F1系列是很多人入坑嵌入式摸到的第一颗芯片DHT11温湿度传感器又是新手必玩的第一颗数字传感器。这两个东西凑在一起正好能把GPIO操作、时序控制、数据校验和调试手法全部过一遍是性价比极高的一节实践课。这篇文章不扯空话直接讲STM32F1怎么选型、最小系统怎么搭、DHT11驱动代码怎么写、实际调试会踩哪些坑给刚开始玩或者正在被时序折磨的朋友一个可以直接照抄的参考。我自己用F103配合DHT11做过好几版温湿度采集项目从裸板飞线到带RTOS的小设备都跑过今天把这些经验一次性说透。1. STM32F1系列的核心定位与选型思路1.1 一颗2007年的芯片为什么还在大量出货STM32F1系列基于ARM Cortex-M3内核主频最高72MHzFlash从16KB到512KBSRAM从4KB到64KB。论性能指标它被后来的F4、F7、H7甩开好几条街但在大量中小型项目里这些性能根本用不满。真正让它持续出货的核心原因有三个价格低、资料全、外设够用。一颗STM32F103C8T6在正常渠道也就几块钱比很多8位单片机都便宜。而且这芯片的资料密度高到夸张无论你用寄存器、标准外设库还是HAL库遇到问题搜一下基本都有前人的答案。对产品开发来说可维护性和供应链稳定性比纸面性能重要得多。外设方面USART、I2C、SPI、CAN、USB、ADC、PWM这些常用模块都齐了工业控制、仪器仪表、智能家居终端这些场景基本覆盖到位。DHT11这类数字传感器正好能用它最普通的GPIO口来驱动既不需要额外硬件也能把底层逻辑吃透。1.2 常见型号对照与选型参考STM32F1系列里最常见的是F103子系列下面这张表整理了几颗市场上流通量最大的型号型号引脚数FlashSRAM常见用途STM32F103C8T64864KB20KB小体积控制板、传感器采集、飞控STM32F103RBT664128KB20KB带简单界面或RTOS的中型项目STM32F103VET6100512KB64KB多外设、多串口、带GUI的项目STM32F103ZET6144512KB64KB资源需求更高的复杂设备选型的时候不要只看Flash引脚数往往才是最先卡住你的因素。比如C8T6只有48个引脚PA/PB/PC三组IO分别还被打折过如果你的设计里同时要用到两路串口、一路CAN、一组ADC采集、一个OLED屏幕和几个按键引脚可能刚刚好但几乎没有余量。这时候换RBT6或者VET6会更舒服。另外要注意封装。C8T6和RBT6都是LQFP封装手工焊接有一定难度不过配合拖锡和助焊剂也能搞定。个人建议新手先买现成的核心板等电路设计熟悉了再自己画板打样能省掉很多焊接和调试的烦恼。1.3 做一个DHT11温湿度项目选哪颗合适如果你只是采集温湿度、显示在OLED上或者通过串口上报给上位机那么STM32F103C8T6完全够用选它准没错。DHT11占用一个GPIO、一个定时器延时、几个字节的RAM这点资源对C8T6来说连皮毛都算不上。但要是你的项目计划里还有WiFi模块、LCD屏幕、多路传感器、FreeRTOS这些我建议直接上RBT6甚至VET6。不是因为C8T6跑不动而是内存和引脚余量会影响你后期的迭代效率。我见过不少人一开始用C8T6开发功能做到一半发现引脚不够最后又重新画板换芯片浪费了两三周时间。硬件选型这种事宁可稍微富余一点也不要抠到刚刚好。2. 开发环境与最小系统搭建2.1 工具链怎么选Keil、CubeIDE还是GCCSTM32F1的开发工具链目前主流是三个方向Keil MDK、STM32CubeIDE、GCC加Makefile。Keil MDK是老牌工具调试界面顺手配合ST-Link体验很好很多公司的量产项目都在用。缺点是License收费新版本对中文用户不太友好不过网上资源多教程也多。STM32CubeIDE是ST官方免费IDE内置了CubeMX图形化初始化工具能帮你生成时钟树、GPIO、外设的初始化代码。对新手来说这个工具能把很多低级错误挡在门外。我自己现在很多项目都是CubeMX先搭框架再进去填业务逻辑。GCC加Makefile适合喜欢命令行和持续集成的人但如果你只是学DHT11驱动不建议一上来就折腾这套容易劝退。我的建议是新手用STM32CubeIDE或者Keil加标准库也行。重点不是工具而是你能否理解生成的代码在干什么。DHT11这个例子我会用CubeMX加HAL库来讲代码逻辑也能平移到标准库。2.2 最小系统板原理与接线要点STM32F103的最小系统主要包含以下几部分供电3.3V供给VDD每个VDD引脚旁边放100nF陶瓷电容再放一颗4.7uF到10uF的钽电容做整体滤波。外部8MHz晶振连接OSC_IN和OSC_OUT两个引脚各接一个20pF负载电容到地。STM32F1内部也有HSI振荡器但HSI精度一般而且USB模块需要外部晶振所以只要不是极端成本敏感的设计强烈建议上8MHz晶振。复位电路NRST引脚接10kΩ上拉到3.3V再接0.1uF电容到地这样上电时能产生可靠复位。BOOT引脚BOOT0接地从用户Flash启动这是正常模式。BOOT1可以随意一般也接地。下载调试接口STM32F1支持SWD只要SWDIO、SWCLK、GND、3.3V四根线就能下载和调试比JTAG省引脚。接线时最容易犯的错是晶振电容选得太大或太小。20pF是常见值但实际和PCB寄生电容有关如果系统能稳定运行一般不用纠结。还有一个坑是VDD和VDDA必须分开供电模拟电源引脚VDDA如果悬空ADC会工作不正常很多新手在这里栽过跟头。2.3 让时钟树跑在72MHz初始化细节STM32F1的时钟树比后来的系列要繁琐一些核心在于理解HSE、PLL、AHB、APB1、APB2这几级的关系。下面这串代码是用CubeMX生成的时钟配置外部8MHz晶振经过PLL倍频到72MHzvoid SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2) ! HAL_OK) { Error_Handler(); } }这里的关键是8MHz HSE经过PLL倍频9倍得到72MHz系统时钟AHB不分频APB1分频2得到36MHzAPB2不分频得到72MHz。STM32F103规定APB1最大36MHzAPB2最大72MHz超频会导致外设工作异常。Flash等待周期也要设为2否则CPU从Flash取指跟不上72MHz速度程序会随机跑飞。对于DHT11驱动来说时钟配置直接决定了延时的准确性。如果你用SysTick做微秒延时SysTick的时钟源、分频系数都会影响结果如果按72MHz主频手工计算循环周期主频一变全部作废。所以不管用什么方案第一步都要确保SystemClock_Config执行正确并且SystemCoreClock变量确实是72000000。3. DHT11温湿度传感器驱动实现3.1 DHT11的单总线协议和40位数据格式DHT11使用单总线协议通信只需要一根数据线。一次完整的数据传输是40个bit分为5个字节湿度整数湿度小数温度整数温度小数校验和校验规则是前四个字节相加取低8位等于第五个字节时认为数据有效。DHT11的实际精度是1%所以小数位通常读出来是0但协议里确实有这几位驱动代码里最好还是把它们读出来不要直接忽略。通信过程分两个阶段主机发起起始信号主机把数据线拉低至少18ms然后释放。DHT11检测到这个低电平后会拉低总线约80us作为响应再拉高约80us通知主机准备发送数据。DHT11发送40个bit每一位数据都由低电平和高电平组成。先是一个约50us的低电平然后是一个高电平高电平持续26到28us代表0持续约70us代表1。判断0和1的差异是关键。硬件上DHT11的数据线通常是开漏输出外部必须有上拉电阻。总线空闲时是高电平主机和传感器都释放总线时由上拉电阻把电平拉高。3.2 硬件连接与上拉电阻的选择DHT11最常见的是4脚封装实际只用三根线VCC接3.3VGND接地DATA接STM32的一个GPIO。注意DHT11虽然标称支持3.3V到5.5V供电但为了和STM32直连我强烈建议统一用3.3V省去电平匹配和引脚耐压的问题。DATA引脚必须接一个上拉电阻到VCC阻值通常选4.7kΩ到10kΩ。如果只是短距离杜邦线连接10kΩ也能工作如果飞线较长或者环境干扰大换4.7kΩ会更稳定。STM32内部也有上拉电阻但阻值大约40kΩ太弱不适合直接替代外部上拉。我实测过不加外部上拉的方案偶尔能读到数据但错误率很高不建议这么省。推荐的连接方式DHT11 VCC - 3.3V DHT11 GND - GND DHT11 DATA - PA0 PA0外接4.7kΩ上拉到3.3V选PA0只是举例你可以换成任意普通GPIO。只要注意别选到被调试器或板上外设占用的引脚比如PA13、PA14默认是SWD功能PB3、PB4也有特殊功能走普通IO时要先禁用对应复用功能。3.3 微秒级延时的正确姿势DWT方案DHT11时序最小的单位是几十微秒HAL_Delay只能精确到毫秒不够用。所以必须要有us级延时函数。新手最常见的做法是用for循环空转void delay_us(uint32_t us) { uint32_t i; for (i 0; i us * 8; i); }这种代码在O0优化下可能能跑一旦开O2优化或者换一颗主频不同的芯片整个时序全部乱掉。调试时最难查的就是这种隐性依赖。我更推荐用DWT的CYCCNT寄存器做微秒延时。DWT是Cortex-M3内核自带的调试组件有一个32位的周期计数器每过一个内核时钟周期自动加1不会受定时器配置影响。初始化代码很短static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }注意SystemCoreClock必须在时钟初始化之后才是72000000所以要确保SystemClock_Config先执行。DWT方案还有一个好处CPU在调试暂停时计数器也停单步调试不会让时序悄悄流逝这对排查DHT11问题特别有用。3.4 完整的GPIO模拟时序驱动代码驱动DHT11需要频繁在输入和输出模式之间切换GPIO因为起始信号需要GPIO输出低电平之后又要切换成输入去读总线电平。HAL库的初始化函数可以直接改模式#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_0 #define DHT11_CLK_ENABLE __HAL_RCC_GPIOA_CLK_ENABLE() static void DHT11_Pin_Output(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); } static void DHT11_Pin_Input(void) { 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); }读取一个字节的逻辑是等待低电平结束延时40us后采样电平如果还是高电平就记为1否则记为0。完整代码static uint8_t DHT11_ReadByte(void) { uint8_t value 0; for (int i 7; i 0; i--) { while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET); delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { value | (1 i); } } return value; }读取整个DHT11数据的函数uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; uint32_t timeout; DHT11_Pin_Output(); 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); delay_us(30); DHT11_Pin_Input(); 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; } timeout 10000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; } for (int i 0; i 5; i) { data[i] DHT11_ReadByte(); } if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) { return 2; } *humidity data[0]; *temperature data[2]; return 0; }这里有几个细节值得说。起始信号的20ms用HAL_Delay因为18ms以上是DHT11的唤醒时间20ms留了余量毫秒级延时足够。释放总线后延时30us再切换输入是为了避免GPIO输出模式刚关闭时电平还没有被上拉电阻完全拉高就进入读取状态容易误判。三组等待循环都必须加超时保护。单总线是半双工机制万一DHT11没响应或者信号异常主控会一直卡在while循环里整个程序就像死机了一样。加上超时然后返回错误码是驱动这类单总线器件的基本素养。读bit时用40us作为判断点是因为0的高电平持续26到28us1的高电平持续约70us。延时40us之后0已经变成低电平1仍然是高电平。这个40us不用特别精确DHT11的时序容忍度还可以但前提是延时要稳定不能忽快忽慢。3.5 校验与数据解析前四个字节相加取低8位如果和校验字节不一致数据大概率是某一位被干扰了直接丢弃不要用。我在代码里把所有数据都接收完毕后再校验而不是边读边用这样可以避免读到一半时数据异常还拿去做显示。主程序里调用示例uint8_t humi, temp; while (1) { if (DHT11_ReadData(humi, temp) 0) { printf(Temp%d C, Humi%d %%\n, temp, humi); } else { printf(DHT11 read error\n); } HAL_Delay(2000); }注意DHT11的采样周期是1到2秒连续读取时两次读取之间至少要间隔1秒否则传感器还没准备好很容易出现超时或校验失败。所以我每次读完都会延时2秒再发起下一次请求。4. 常见问题与排查技巧实录4.1 读不到数据或超时从哪里查起如果你把代码烧进去串口一直打印read error先别急着改代码按下面这个顺序排查最快量电压。DHT11的VCC是不是3.3VGND接好没有很多新手把DHT11方向插反上电就能闻到糊味。看空闲电平。不发起读取时DATA引脚应该被上拉电阻拉到3.3V用万用表量一下。如果是0V要么上拉电阻没接要么DHT11坏了要么引脚模式不对。查代码里的GPIO配置。时钟有没有使能用的引脚和代码里写的是不是同一个CubeMX生成代码后时钟使能一般在MX_GPIO_Init里手动写代码时最容易漏掉__HAL_RCC_GPIOA_CLK_ENABLE()这一句。漏掉时钟使能的结果是GPIO操作完全无效电平一直不对。确认起始信号真的发出去了。可以用示波器或者逻辑分析仪抓DATA引脚的波形。正常时序能明显看到一段20ms左右的低电平接着是DHT11回应的两个约80us的脉冲然后是40个bit的数据脉冲。如果波形里根本没有低电平说明GPIO输出没配好或者代码逻辑有问题。没有示波器的话可以写个最简单的灯测试在DHT11_Pin_Output之后让GPIO输出低电平看LED是否点亮。如果灯不亮先查引脚配置和时钟。灯亮了再继续调时序。4.2 数据跳变与校验失败的根源校验失败是最折磨人的问题因为一次可能是坏数据反复出现就是系统性问题。我总结下来最常见的原因是延时函数不准。用for循环空转做us延时的代码在不同优化级别下表现完全不同。编译器可能把你的循环优化掉或者因为指令流水线导致实际时间不是预期值。解决办法就是换DWT或者SysTick方案。我用了DWT之后DHT11的一次读取成功率从大概70%提升到接近100%。另一个原因是中断干扰。读取40个bit的过程中如果某个中断处理函数占用了较长时间或者嵌套中断打断了40us的采样点某一位就会判断错误。简单的排查方法是先关闭所有中断测试如果关闭中断后数据稳定说明确实是中断导致。这时可以选择在读DHT11时进入临界区或者调整中断优先级让DHT11读取不能被轻易打断。还有一个容易被忽略的点是GPIO速度配置。输出模式下Speed字段如果设置成LOWGPIO翻转速度会变慢可能拉低起始信号时边沿不够陡影响DHT11的响应。我一般统一用GPIO_SPEED_FREQ_HIGH对时序类外设越稳越好。4.3 延长线与多传感器场景的坑如果你的DHT11不是直接插在开发板上而是用了一两米长的导线这时候波形会受到线间电容和电磁干扰的影响。导线长了之后上拉电阻和线电容会形成一个RC低通滤波器原本几十纳秒的高电平边沿可能被拖成几百纳秒甚至微秒级。如果上拉电阻太大波形上升得更慢DHT11发送的1信号可能还没被主控识别到高电平就结束了。解决办法有两个把上拉电阻从10kΩ降到4.7kΩ或者1kΩ或者加一个74HC125之类的缓冲器。我实测过一米左右的线用4.7kΩ能稳定工作两米以上就不建议硬扛了还是换I2C接口的传感器靠谱。如果一条总线上接了多个DHT11注意DHT11的地址是固定的单总线协议并不支持多个器件挂在同一条线上寻址每个DHT11必须独立占用一个GPIO。不要尝试把多个DHT11的DATA脚接在一起那样只会相互干扰。4.4 排查速查表下面这张表是我实际调试中整理出来的可以直接对照排查现象可能原因处理办法一直超时上拉电阻缺失、GPIO时钟没开、接线错误量空闲电平检查GPIO配置补上拉电阻数据全0GPIO输入配置错误、DHT11供电异常确保输入模式无下拉测量VCC数据全1上拉电阻太大、引脚悬空、未正确切换输入模式降上拉阻值检查模式切换代码偶发校验失败延时不准、中断干扰换DWT延时读数据时关中断或加临界区温度正常湿度乱跳DHT11传感器脏污、靠近热源或风口清洁传感器远离热源延长读取间隔连续读取失败读取周期小于1秒两次读取之间延时2秒还有一个小技巧调试时可以在串口打印每次读取的原始5字节数据而不仅仅是解析后的温湿度。如果五个字节里永远只有某个bit错误对比正常波形很快就能定位比盯着温湿度数字瞎猜有效得多。我在调试DHT11时习惯把这个打印功能常开确认数据稳定后再关掉能省不少时间。最后再分享一个个人习惯DHT11这类单总线传感器的读取代码我会单独放到一个dht11.c文件里并且把GPIO引脚宏定义放在头文件顶部。以后换板子、换引脚只需要改两个宏不需要动驱动逻辑。手头这块板子当时就是吃了没做封装的亏换个引脚得全局搜索修改麻烦得很。你如果刚开始写驱动建议一开始就把引脚配置隔离出来后面会感谢这个习惯。
返回列表