ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 HAL库驱动DHT11+OLED完整教程:从时序到调试的实战总结

STM32 HAL库驱动DHT11+OLED完整教程:从时序到调试的实战总结 简介一套基于STM32 HAL库的物联网入门项目面向嵌入式开发者演示DHT11温湿度传感器数据采集与OLED屏实时显示。工程涵盖传感器时序解析、I2C/GPIO配置、SSD1306驱动调用等关键环节适合学习HAL库外设操作与小型显示方案集成。压缩包共193个文件以C源文件29个c、头文件59个h及工程配置文件uvprojx、ioc、uvoptx为主体附带编译生成的axf、hex等文件整体6.9MB目录结构清晰便于对照学习。项目最大的价值在于打通从外设初始化、数据采集到界面渲染的完整链路覆盖单总线时序、数据校验、OLED驱动移植等实战细节可直接迁移至智能家居或环境监测等物联网场景。资源已有772人学习对初学者快速上手HAL库开发很有帮助。 搞嵌入式这一年多最大的感受就是看似简单的传感器真正拿到工程里用起来坑往往不在传感器本身而在你选的库、走的通信方式、以及时序的细节处理上。DHT11温湿度传感器加上OLED显示算是最经典的入门组合了网上教程一抓一大把但大多数都是标准库的老代码或者直接抄来的延时凑时序。这次我直接用HAL库从零开始写了一遍把DHT11的单总线时序、OLED的I2C驱动、以及两者配合时的调度问题全部趟了一遍把过程和踩过的坑都记下来给后面走这条路的朋友一个参考。这篇东西适合谁看如果你已经会用STM32CubeMX建工程、知道怎么点灯、用过串口但还没系统搞过单总线传感器和OLED屏那这篇文章基本就是为你写的。如果你是大佬可以直接跳到第4节看我在时序校准和调度上踩的坑那部分应该是常规教程里不会写的东西。1. 为什么选HAL库来做DHT11OLED而不是继续用标准库先说一个争论了很久的问题DHT11这种对时序要求极端的传感器到底适不适合用HAL库很多老工程师会说HAL库函数调用开销大、延时不准、中断优先级乱七八糟根本不该拿来驱动DHT11这种单总线设备。这话放在五六年前有一定道理但HAL库发展到现在配合CubeMX生成的代码框架只要你会正确配置定时器或者用好systick时序精度完全是够用的。我自己选HAL库的核心原因是工程可维护性。标准库写DHT11的驱动代码量确实少但一旦你要在这个项目里加上OLED、加上按键、加上串口调试、甚至后面想上RTOS标准库的代码组织方式就会变得非常痛苦。HAL库虽然函数调用层级深但它把硬件抽象做得很好CubeMX生成的初始化代码几乎不用改你只需要专注在业务逻辑上。另外一个很现实的原因是学习成本。现在新出的STM32芯片、开发板资料几乎全是HAL库的CubeMX生成的工程结构已经成了事实标准。你花一周时间适应HAL库的写法换来的是一劳永逸的工程框架能力。而我实际测试下来在72MHz主频的STM32F103C8T6上HAL库驱动DHT11的数据读取误差完全在可接受范围内温度精度±1℃湿度精度±1%RH这是DHT11本身的精度极限跟库的关系不大。1.1 整体方案选型用GPIO模拟单总线还是硬件外设DHT11用的是单总线协议STM32没有专用的单总线外设。市面上常见做法有三种纯GPIO模拟直接用普通GPIO口拉高拉低靠延时函数控制时序。最简单但对延时精度要求高。GPIO定时器GPIO控制电平定时器精确计时适合对时序要求苛刻的场景。GPIO外部中断监听DHT11的应答信号靠中断记录高低电平持续时间。代码最复杂但最稳。我这次选的是纯GPIO模拟HAL库的HAL_Delay和DWT时钟计数器配合的方式。为什么不用纯HAL_Delay因为HAL_Delay基于Systick最小精度是1ms而DHT11的时序要求是微秒级的差一个毫秒整个通信就废了。所以我的方案是毫秒级的延时用HAL_Delay微秒级的延时用DWTData Watchpoint and Trace硬件计数器这个后面详细说。OLED方面我用的是I2C接口的SSD13060.96寸128x64分辨率。选I2C而不是SPI主要是省引脚I2C只需要SCL和SDA两根线加上电源和地总共四根线就搞定了。STM32F103C8T6的I2C1在PB6和PB7上正好空出来用。1.2 硬件连接与引脚分配先给一张我实际使用的引脚分配表照着接就不会出错器件引脚STM32引脚说明DHT11 DATA数据脚PA0配置为开漏输出上拉兼容输入输出DHT11 VCC电源3.3VDHT11供电范围3.3V-5V3.3V可用DHT11 GND地GND共地OLED SCL时钟PB6I2C1_SCL复用开漏OLED SDA数据PB7I2C1_SDA复用开漏OLED VCC电源3.3VSSD1306支持3.3VOLED GND地GND共地有一点要特别注意DHT11的数据脚建议接一个4.7kΩ上拉电阻到VCC。虽然DHT11模块上通常已经自带上拉电阻了但如果你用的是裸传感器而不是模块这个上拉必须自己加。没有上拉电阻DHT11的数据线无法被正确拉高通信时序直接崩。我一开始用的是裸传感器忘了加上拉折腾了半个小时后来仔细看了数据手册才想起来这回事。2. DHT11单总线协议解析时序才是真正的核心考点DHT11能成为入门神器一方面是因为便宜好用另一方面是因为它的通信协议足够简单——单根数据线40bit数据一次性传完。但简单不等于容易恰恰是因为协议简单它对时序的要求反而非常苛刻。DHT11的数据手册里所有时间参数都是微秒级的最严格的地方是数据位0和数据位1的区分两者的高电平持续时间差异只有大概26微秒。DHT11的一次完整通信过程可以拆成四个阶段主机发起起始信号主机将数据线拉低至少18ms然后释放并拉高20-40us等待DHT11响应。DHT11应答信号DHT11检测到起始信号后拉低数据线80us再拉高80us表示我准备好了。数据输出DHT11依次输出40bit数据高位先出格式是湿度整数部分8bit 湿度小数部分8bit 温度整数部分8bit 温度小数部分8bit 校验和8bit。通信结束DHT11释放数据线回到空闲状态。其中最关键的判断逻辑在数据位读取每一位数据都是以50us的低电平开始然后是一个高电平。如果高电平持续约26-28us表示这一位是0如果高电平持续约70us表示这一位是1。你只需要测量每个bit的高电平持续时间然后跟一个阈值比较通常取50us就能判断出0和1。这里我要提醒一个很容易被忽略的点DHT11唤醒之后从你发出起始信号到DHT11真正开始应答中间有20-40us的释放总线时间。很多初学者在这个阶段没加延时直接就开始读数据线电平导致读到的永远是高电平然后一脸懵。正确的做法是在起始信号结束后先把GPIO切成输入模式然后耐心等DHT11把数据线拉低这个等待是有时间窗口的——如果超过100us数据线还没被拉低说明DHT11没被唤醒应该重新发起通信。2.1 40bit数据的校验逻辑DHT11的数据帧里前四个字节是真实的温湿度数据第五个字节是校验和。校验规则极其简单前四个字节相加取低8位如果等于校验字节说明数据传输正确。比如实测数据湿度整数45湿度小数0温度整数25温度小数0那么前四个字节相加就是450250700x46校验字节也应该是0x46。如果校验失败直接丢弃这帧数据等下一次采样。校验这个步骤一定不能省哪怕你是在实验室环境测试。我真实遇到过一次数据线没插紧导致某个bit读取错误温度直接跳到了-38℃。如果没有校验逻辑这个错误数据就直接显示在OLED上了你还会以为是传感器坏了。2.2 时序参数表与实测值DHT11数据手册给出的典型时序参数以及我实际用逻辑分析仪抓到的时间值对比如下参数手册标称值实测值说明起始信号低电平时间至少18ms20ms用HAL_Delay(20)实现起始信号后高电平时间20-40us30usDWT延时DHT11应答低电平80us78us实测接近DHT11应答高电平80us82us实测接近数据位0高电平26-28us27us阈值判断用50us数据位1高电平70us68us实测接近数据位低电平前缀50us48us每位数据开始逻辑分析仪是这个项目里最值得投资的调试工具淘宝几十块钱的24MHz 8通道逻辑分析仪就够用了。没有它之前我写时序全靠猜出了问题只能瞎试有了它之后一眼就能看出是高电平时间不对还是低电平时间不对排查效率提升十倍不止。3. HAL库工程搭建CubeMX配置和代码框架开始写代码之前先把CubeMX的工程配置说清楚。我用的是STM32F103C8T6也就是最常见的蓝色Pill板主频设到72MHz。CubeMX里需要配置的内容如下RCCHSE选择Crystal/Ceramic Resonator外部8MHz晶振。Clock ConfigurationHCLK设为72MHzAPB1分频器设为/236MHzAPB2设为/1。GPIOPA0配置为GPIO_MODE_OUTPUT_OD开漏输出上拉GPIO_PULLUP初始电平高。这个引脚用于DHT11数据通信开漏输出可以方便地在输入输出之间切换。PB6和PB7由I2C1自动配置不用手动设置。I2C1选择I2C模式速度设为100KHzStandard Mode其他参数默认。SSD1306完全兼容100KHz不需要上400KHz。USART1用于调试信息输出可选但强烈建议开。我配置的是115200-8-N-1PA9/PA10。生成的代码框架之后你需要在main.c里做的核心事情是实现微秒级延时函数用DWT。实现DHT11的GPIO输入输出切换。实现DHT11的时序读取函数。移植SSD1306驱动。在主循环里做数据采集和显示刷新。3.1 DWT微秒延时HAL库下最可靠的延时方案前面说过HAL_Delay只能做到毫秒级而DHT11需要微秒级延时。在HAL库环境下实现微秒延时有三个常见方案空的for循环简单但不可靠编译优化级别变了延时就不准了。弃用。定时器延时准确但浪费一个定时器外设而且调用HAL_GetTick()的封装有开销。DWT延时ARM Cortex-M3内核自带的调试硬件计数器精度高不占用额外外设强烈推荐。DWTData Watchpoint and Trace是Cortex-M3内核里的一个调试组件它内部有一个32位的自由运行计数器CYCCNT每个CPU时钟周期加1。72MHz主频下它的分辨率就是1/72MHz≈13.9ns用来做微秒级延时绰绰有余。启用DWT的代码非常简洁先通过CoreDebug-DEMCR使能DWT然后通过DWT-CTRL使能CYCCNT计数器最后等待计数器清零。实测下来整个初始化过程不到10行代码static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; } static void DWT_Delay_us(uint32_t us) { uint32_t startTick DWT-CYCCNT; uint32_t delayTicks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - startTick) delayTicks); }核心逻辑很简单读取当前计数器值计算目标延迟对应的时钟周期数然后死等。后台while循环里没有函数调用没有任何中断开销精度非常高。我在逻辑分析仪上实测过DWT_Delay_us(50)的实际延时误差在1us以内完全满足DHT11的时序要求。3.2 DHT11驱动代码实现与逐段解释下面是DHT11驱动最核心的代码我把关键逻辑注释进去了。这个驱动思路参考了ST官方的应用笔记但在时序细节上做了修正和实测校准。// 定义GPIO切换宏PA0作为数据脚 #define DHT11_GPIO_Port GPIOA #define DHT11_GPIO_Pin GPIO_PIN_0 // 将PA0切换为输入模式 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_PULLUP; HAL_GPIO_Init(DHT11_GPIO_Port, GPIO_InitStruct); } // 将PA0切换为输出模式 static void DHT11_Pin_Output(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_Pin; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_Port, GPIO_InitStruct); } // 读取一位数据返回0或1 static uint8_t DHT11_ReadBit(void) { uint16_t highTime 0; // 等待低电平结束50us低电平前缀 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) { highTime; DWT_Delay_us(1); if (highTime 100) break; // 超时保护 } // 高电平时间超过50us判定为1否则为0 return (highTime 50) ? 1 : 0; } // 读取DHT11的40bit数据 uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; // 1. 主机发送起始信号拉低至少18ms DHT11_Pin_Output(); HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_GPIO_Pin, GPIO_PIN_RESET); HAL_Delay(20); // 拉低20ms超过手册要求的18ms HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_GPIO_Pin, GPIO_PIN_SET); DWT_Delay_us(30); // 释放总线20-40us // 2. 切换为输入等待DHT11应答 DHT11_Pin_Input(); // 等待DHT11将总线拉低应答开始超时100us uint16_t waitCnt 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_GPIO_Pin) GPIO_PIN_SET) { DWT_Delay_us(1); if (waitCnt 100) return 1; // 无应答返回错误 } // 应答低电平80us waitCnt 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_GPIO_Pin) GPIO_PIN_RESET) { DWT_Delay_us(1); if (waitCnt 100) return 1; } // 应答高电平80us waitCnt 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_GPIO_Pin) GPIO_PIN_SET) { DWT_Delay_us(1); if (waitCnt 100) return 1; } // 3. 连续读取40bit数据 for (int i 0; i 40; i) { data[i / 8] 1; data[i / 8] | DHT11_ReadBit(); } // 4. 校验 uint8_t checksum (data[0] data[1] data[2] data[3]) 0xFF; if (checksum ! data[4]) return 2; // 校验失败 *humidity data[0]; *temperature data[2]; return 0; // 成功 }这段代码有几个关键细节值得说明开漏输出输入切换是DHT11通信的正确姿势。开漏输出模式下GPIO输出低电平可以主动拉低总线输出高电平时实际上是把总线释放给上拉电阻。这样切换输入模式时不需要额外的方向寄存器操作也不会出现推挽输出高电平时跟DHT11驱动的低电平打架的情况。超时保护是必须的。如果DHT11没有接入、线断了、或者芯片损坏总线会一直保持高电平。没有超时保护的话你的程序会卡死在等待应答的死循环里整个系统看起来就跟死机了一样。我在读取bit的高电平等待里也加了100us的上限防止异常数据导致死循环。读取bit时的高电平时间判断用了一个简单的循环计数。每计数1次延时1us然后判断是否超过50。这个阈值是有讲究的DHT11的0高电平是26-28us1的高电平是70us50us正好处于两者中间分类错误率最低。如果你用的主频不是72MHz这个阈值也需要相应调整——具体做法是用逻辑分析仪抓一下实际波形看你的1高电平是多少us然后取0和1的中间值。3.3 SSD1306 OLED驱动的移植要点OLED显示部分我用的是SSD1306驱动芯片的0.96寸屏I2C接口。网上SSD1306的驱动代码很多但大多是Arduino或者标准库的版本。在HAL库下移植核心工作是重写底层I2C发送函数。SSD1306的驱动逻辑分为三层底层I2C发送通过HAL库的HAL_I2C_Mem_Write向SSD1306发送命令或数据。中间层画点/画线在显存数组上操作像素点。上层显示函数显示字符串、汉字、数字等。这里重点说底层。SSD1306的I2C地址一般是0x787位地址0x3C左移一位。发送数据时需要先发送控制字节0x00表示后续是命令0x40表示后续是显存数据。// 向SSD1306发送命令 void SSD1306_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, 2, 100); } // 向SSD1306写显存数据 void SSD1306_WriteData(uint8_t *data, uint16_t size) { uint8_t buf[size 1]; buf[0] 0x40; memcpy(buf[1], data, size); HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, size 1, 100); }显存缓存方案SSD1306有128x648192个像素点1bit对应一个像素所以整屏显存是8192/81024字节。我直接在MCU内存里开一个1024字节的数组当显存所有画点操作都在这块数组上进行需要刷新时才一次性把整块显存推给SSD1306。uint8_t SSD1306_Buffer[1024]; // 画点函数 void SSD1306_DrawPixel(uint8_t x, uint8_t y, uint8_t color) { if (x 127 || y 63) return; if (color 1) { SSD1306_Buffer[x (y / 8) * 128] | (1 (y % 8)); } else { SSD1306_Buffer[x (y / 8) * 128] ~(1 (y % 8)); } } // 整屏刷新 void SSD1306_Refresh(void) { SSD1306_WriteCmd(0x21); // 设置列地址范围 SSD1306_WriteCmd(0x00); SSD1306_WriteCmd(0x7F); SSD1306_WriteCmd(0x22); // 设置页地址范围 SSD1306_WriteCmd(0x00); SSD1306_WriteCmd(0x07); for (int i 0; i 8; i) { SSD1306_WriteData(SSD1306_Buffer[i * 128], 128); } }需要注意的坑I2C传输1024字节的显存在100KHz速率下大约需要80-100ms左右。如果你在每次读完DHT11之后立刻刷新OLED整个刷新过程会阻塞CPU导致你无法及时响应其他任务。我的做法是降低刷新频率DHT11每2秒采样一次OLED每1秒刷新一次并且通过HAL_I2C_Master_Transmit的超时参数做了保护就算I2C异常也不会把系统卡死。3.4 中文字库和显示排版OLED显示汉字是很多新手卡壳的地方。SSD1306内部没有字库只能用点阵方式显示。我用的方案是16x16的汉字点阵每个汉字需要32字节数据。可以先用取模软件比如PCtoLCD2002把要显示的汉字生成C语言数组然后按行列扫描方式写入显存。我项目里显示的内容布局如下第1行温度: 25 C (16x16汉字2倍大小容易出界所以用1倍) 第2行湿度: 45 %RH 第3行状态: OK/ERROR 第4行时间戳: 12345sOLED的128x64分辨率虽然不大但合理排版还是能显示很多信息。温度和小数这里我做了取舍DHT11的湿度精度是1%RH、温度精度是1℃整数部分完全够用小数部分意义不大。如果需要更高精度应该直接换DHT22或者SHT30DHT11本身就是个入门级传感器不要指望它测出0.1℃级别的变化。4. 实测调参HAL库与DHT11时序的磨合过程代码写完之后进入最磨人的阶段调时序。这部分我把完整过程写出来因为这才是DHT11项目真正花时间的环节。第一次上电测试现象是OLED正常点亮温度显示-1℃湿度显示0%RH数据明显不对。用逻辑分析仪抓引脚波形发现问题出在起始信号后的总线释放时间上。我原来写的是HAL_Delay(1)也就是1ms但DHT11手册要求的是20-40us。1ms是什么概念比要求的25倍还长DHT11早就从唤醒状态跳过了应答窗口主机开始读的时候总线已经被DHT11释放回高电平了自然读到的是全1解析出来就是明显的错误数据。把HAL_Delay(1)换成DWT_Delay_us(30)之后波形正常了数据也出来了。但紧跟着第二个问题出现数据偶发读取失败大概每十次采样会有一次返回校验错误。排查过程比较曲折。我一开始怀疑是电源噪声给DHT11单独加了100nF去耦电容没用又怀疑是杜邦线太长导致信号反射换短线改善但不彻底。最后用逻辑分析仪逐位对比发现问题出在DHT11_ReadBit函数里对高电平的判断逻辑——原来的写法是在高电平期间不断DWT_Delay_us(1)并计数但当高电平长达70us时计数达到70这个时间长度本身没问题问题在于DWT延时循环里头的读取操作和计数操作也有执行时间叠加起来导致实测判断阈值偏大某些数据位为027us的情况下也会被判成1从而误码。调整方案是把判断逻辑改为直接读取当前计数值而不是用循环计数static uint8_t DHT11_ReadBit(void) { uint32_t t1, t2; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_GPIO_Pin) GPIO_PIN_RESET); t1 DWT-CYCCNT; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_GPIO_Pin) GPIO_PIN_SET); t2 DWT-CYCCNT; uint32_t highUs (t2 - t1) / (SystemCoreClock / 1000000); return (highUs 50) ? 1 : 0; }这个版本用DWT硬件计数器直接测量高电平的精确时钟周期数while循环里不做任何额外操作测量精度极高。改完后再测连续读取200次零错误。这个经历也验证了一件事HAL库并不妨碍你写出高精度的时序代码关键是你得知道在关键路径上用对工具。HAL_Delay负责毫秒级的粗延时DWT负责微秒级的精延时各司其职完全不冲突。4.1 数据刷新频率与OLED显示冲突的调度思路DHT11的数据手册里写着采样周期不得低于1秒——不是说不能每秒采样而是DHT11内部的上电测量周期就是1-2秒你如果高频连续读取DHT11根本来不及完成新的测量返回的是上次的旧数据。所以DHT11的读取频率建议2秒一次留足余量。但OLED的刷新和DHT11的采样是互相影响的。如果你在DHT11时序读取的关键阶段微秒级被OLED的I2C中断打断会导致时序超时读取失败。I2C传输1024字节需要不少时间如果DHT11读取和OLED刷新同时进行系统调度就会出现优先级问题。我的解决思路是把任务拆成两个状态机在主循环里按时间片轮转DHT11采样任务每2秒执行一次执行期间关闭I2C中断或者保证OLED不发起传输。OLED刷新任务每1秒执行一次只刷新显存不做其他事。实际代码里我用了最简单的调度方式主循环里用HAL_GetTick()获取当前毫秒时间戳判断距离上次执行是否超过设定周期。DHT11读取和OLED刷新之间用标志位互斥确保同一时刻只有一个任务在占用I2C总线。uint32_t lastDHT11Tick 0, lastOLEDTick 0; uint8_t dht11Busy 0; while (1) { uint32_t now HAL_GetTick(); // DHT11采样任务每2秒执行一次 if ((now - lastDHT11Tick 2000) !dht11Busy) { dht11Busy 1; uint8_t hum, temp, err; err DHT11_ReadData(hum, temp); if (err) { snprintf(displayBuf, sizeof(displayBuf), DHT11 ERR %d, err); } else { snprintf(displayBuf, sizeof(displayBuf), T:%dC H:%d%%, temp, hum); } dht11Busy 0; lastDHT11Tick now; } // OLED刷新任务每1秒执行一次 if (now - lastOLEDTick 1000) { SSD1306_Clear(); SSD1306_ShowString(0, 0, displayBuf, 16); SSD1306_Refresh(); lastOLEDTick now; } }这个调度方式不能说多优雅但非常实用。它避免了DHT11时序关键段被I2C传输打断的问题也不可能出现两个任务同时访问I2C总线的竞态条件。如果你后面要做更复杂的系统建议整体迁移到RTOS用信号量来管理I2C总线的互斥访问。4.2 常见硬件问题排查清单代码都调通之后如果还是出问题大概率是硬件层面。我把这个项目里遇到的所有硬件坑列成一张排查表现象可能原因排查方法一直读到0xFF或0x00数据线未加上拉用万用表测数据线电压正常空闲时应为3.3V温度湿度固定不变采样频率过高DHT11来不及更新把采样周期拉长到2秒以上数据偶发校验错误杜邦线过长或接触不良缩短连线或改用杜邦线面包板之外的焊接方式OLED白屏I2C地址错误SSD1306的7位地址一般是0x3C或0x3D对应8位地址0x78或0x7AOLED显示一半正常一半花屏显存刷新数据错位检查SSD1306的页地址设置确认每页128字节DHT11读数偏高传感器靠近发热元件如MCU远离热源必要时用延长线引出传感器特别是最后一条我实际测试时发现DHT11贴在STM32旁边读出来的温度比室温高2-3℃因为MCU本身发热。如果你要做的是环境温度监测传感器一定要远离主控板放。5. 进阶扩展从DHT11到更精准的测量方案做完DHT11OLED显示这个基础项目之后大概率你会有一个想法DHT11的精度实在太低了温度整数1℃的粒度让人很不爽。这时候有两条进阶路线路线一换DHT22AM2302。DHT22同样是单总线协议通信流程和DHT11完全一样区别在于DHT22返回的是16bit温湿度数据温度分辨率0.1℃湿度分辨率0.1%RH。你只需要把驱动里读取数据的位数从8bit改成16bit解析逻辑稍微调整就能复用绝大部分代码。路线二换I2C接口的SHT30/SHT40。SHT30的精度更高温度±0.2℃湿度±2%RH而且走I2C接口不需要自己写时序。代码更简洁、更可靠缺点是价格比DHT11贵一些。我个人建议的顺序是先用DHT11把单总线协议的时序、GPIO切换、校验逻辑吃透然后迁移到DHT22几乎无痛之后再换SHT30你会发现HAL库的I2C驱动已经玩得门儿清了。这条路走下来嵌入式常用的传感器通信方式基本就都覆盖了。除了换传感器显示端也可以升级。如果觉得0.96寸的OLED显示信息太少可以换1.3寸的OLED同样是SSD1306/SSH1106驱动分辨率128x64不变或者换成LCD屏幕。核心驱动逻辑都不用动只是初始化配置的屏幕尺寸参数要改。6. 写在最后的工程建议项目到这里已经完整跑通了STM32F103C8T6 HAL库 DHT11 SSD1306 OLED温湿度实时显示在屏幕上数据稳定校验可靠。最后分享几个我在这个项目里沉淀下来的工程经验都是拿时间换来的第一调试工具一定要舍得买。一个逻辑分析仪、一个质量好点的万用表总共不到一百块但能帮你省下的调试时间是以天为单位的。没有逻辑分析仪之前我调DHT11时序全靠加串口打印然后在串口助手里猜效率极低。第二写代码的时候就要考虑数据校验和超时保护。DHT11这种单总线设备任何一根线接触不良、任何一个环境干扰都可能导致数据错误。如果你的代码不做校验、不加超时系统会偶尔卡死或者显示错误数据而且极难复现和排查。我在上面驱动里加的waitCnt超时保护就是吃够了卡死的亏之后才加上的。第三代码结构要为一个更大的项目做准备。如果你只是想点个灯随便写写没问题。但如果你后面要做产品原型建议从第一天就把驱动模块和数据解析模块分开。DHT11的驱动就放在dht11.c里OLED驱动放在ssd1306.c里主循环只负责调度和业务逻辑。这样后期加蓝牙上报、加WIFI、加按键就是在主循环里加状态不会把代码改成一团乱麻。HAL库驱动的路入门的时候总觉得啰嗦等你真正习惯了这套抽象回头看标准库那份什么都得自己管的酸爽你会庆幸自己当初选了HAL库。这个DHT11OLED的项目就是一套很好的练手组合硬件成本十几块钱代码量也不大但里面涉及的单总线时序、GPIO切换、I2C驱动、状态机调度全是嵌入式开发的看家本事。花一个周末做完收获绝对超出你的预期。本文还有配套的精品资源点击获取
返回列表