STM32 HAL库驱动DHT11与OLED:从时序避坑到联合调试 简介这份资源面向嵌入式与物联网方向的开发者及学生提供基于STM32 HAL库驱动DHT11温湿度传感器并在OLED屏上实时显示的完整工程实例可用于智能家居、环境监测等场景的入门实践与课程设计参考。压缩包共193个文件约6.9MB以c与h源码、o与crf编译中间文件、d依赖文件为主另含ioc配置、uvprojx与uvoptx工程文件、hex烧录文件及map、lst等构建产物覆盖从源码到可执行固件的完整链路。目前已有772人学习下载。工程围绕单总线时序读取40位温湿度数据、I2C或SPI驱动SSD1306显示、HAL库接口配置与异常处理等知识点展开读者可据此理解传感器采集与图形显示的协同流程掌握跨STM32系列的代码移植思路并借助现成工程结构快速搭建自己的物联网显示方案。1. 从一次上电只亮不读数的翻车说起DHT11 接 HAL 库到底难在哪很多人第一次把 DHT11 接到 STM32 上代码烧进去OLED 亮是亮了温湿度那一栏却死死卡在0.0或者255串口打印出来全是超时。这不是玄学是时序没对上。DHT11 用的是单总线协议一根数据线既要发命令又要收数据靠的是微秒级的电平翻转来区分 0 和 1。HAL 库默认的HAL_Delay()最小分辨率是毫秒拿它去读 DHT11 等于用秒表量百米冲刺必然翻车。这套环境在物联网课程设计和毕业设计里出现频率极高STM32F103C8T6 加 DHT11 加 0.96 寸 OLED 几乎是标配三件套。装不好你连传感器是好是坏都判断不了装好了后面接 ESP8266 上云、做本地温湿度记录都只是加模块的事。这篇笔记面向的是刚拿到开发板、手里有 HAL 库工程模板但不知道怎么把 DHT11 和 OLED 揉进去的开发者也适合已经能点灯但卡在传感器读数上的熟手对照排查。2. 环境清单与版本选择STM32CubeMX 6.x HAL 库 DHT11 驱动文件2.1 硬件与软件版本对照先把东西列清楚别装到一半发现少文件。我一般会按下面这张表核对一遍再动手。项目推荐版本/型号说明主控STM32F103C8T6最常见资料多价格低传感器DHT11 三针/四针模块四针模块自带上拉电阻省事显示屏0.96 寸 OLEDSSD1306 驱动I2C 接口四针VCC/GND/SCL/SDA开发工具STM32CubeMX 6.5 及以上生成 HAL 库初始化代码IDEKeil MDK 5.30 及以上也可以用 STM32CubeIDEHAL 库STM32Cube FW_F1 V1.8.xCubeMX 里自动下载DHT11 驱动自己写或找现成的dht11.c/h核心是微秒延时和 GPIO 方向切换版本上有个坑CubeMX 6.0 之前生成的 HAL 库HAL_GPIO_ReadPin的宏定义和后面版本略有差异但影响不大。真正要留意的是 Keil 的编译器版本AC5 和 AC6 对__NOP()内联汇编的处理不同微秒延时函数在 AC6 下可能被优化掉后面避坑章节会细说。2.2 CubeMX 里必须配对的三个地方打开 CubeMX选好芯片后下面三处配置直接决定后面能不能跑通。第一系统时钟。在 RCC 里把 HSE 设为 Crystal/Ceramic Resonator然后到 Clock Configuration 里把 HCLK 拉到 72MHz。DHT11 的微秒延时靠 SysTick 或者定时器主频不对延时全歪。第二调试接口。SYS 里 Debug 选 Serial Wire不然第一次烧录后可能锁芯片只能靠复位键续命。第三GPIO 和 I2C。DHT11 数据脚随便选一个普通 GPIO比如 PA5先设成 Output Push Pull速度 Low。OLED 的 I2C 用硬件 I2C1PB6 和 PB7速度设 400kHz 快模式。生成代码时记得勾选“Generate peripheral initialization as a pair of .c/.h files”。# CubeMX 生成后的工程目录结构确认这几个文件在 Core/Src/main.c Core/Src/stm32f1xx_hal_msp.c Core/Inc/main.h Drivers/STM32F1xx_HAL_Driver/生成代码后先编译一次确认空工程能过。这一步不过后面加驱动全是白搭。2.3 DHT11 驱动文件的移植位置把dht11.c和dht11.h放到Core/Src和Core/Inc下然后在 Keil 的工程树里右键添加现有文件。头文件里主要声明两个函数DHT11_Init()和DHT11_Read_Data(temp, humi)。源文件里需要包含main.h来用HAL_GPIO函数同时自己实现一个微秒级延时。// dht11.c 里微秒延时的常见做法基于 SysTick 或 TIM void delay_us(uint32_t us) { uint32_t start HAL_GetTick() * 1000; // 粗略转换实际要用定时器 // 更稳妥的是用 TIM 做 1MHz 计数 }上面这段只是示意真正能用的是用 TIM2 或者 TIM4 配成 1MHz 计数然后读计数器差值。我一般会单独开一个定时器专供微秒延时不跟 HAL 的毫秒 tick 搅在一起。3. 接线与 CubeMX 配置落地从 GPIO 到 I2C 的完整参数3.1 接线表与上拉电阻的处理DHT11 三针模块和四针模块接线不同。三针模块通常已经集成了上拉电阻四针裸传感器需要自己在数据线和 VCC 之间加一个 4.7k 到 10k 的电阻。我见过有人直接接四针裸片不加电阻读数时好时坏查了半天以为是代码问题。DHT11 引脚接到 STM32备注VCC3.3V不要接 5V虽然模块标 3.3-5V但 STM32 的 IO 是 3.3VGNDGND共地DATAPA5加 4.7k 上拉到 3.3VNC悬空四针模块的第四脚OLED 这边简单VCC 接 3.3VGND 共地SCL 接 PB6SDA 接 PB7。I2C 总线上本来就需要上拉很多 OLED 模块背面已经焊了 4.7k不用额外加。3.2 GPIO 模式切换的代码实现DHT11 的单总线要求主机先拉低至少 18ms 作为起始信号然后释放切换成输入模式等待传感器响应。这个切换在 HAL 库里的写法很关键。// 起始信号拉低 18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(18); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); // 切换为输入模式等待 DHT11 响应 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_PORT, GPIO_InitStruct);这里HAL_Delay(18)是毫秒级没问题。但后面等待 DHT11 拉低 80us、再拉高 80us 的响应信号就必须用微秒延时。很多现成驱动里用while循环空转来等电平变化在 72MHz 下勉强能用但换个主频就废了。我一般会写成带超时退出的循环避免传感器没接好时死等。3.3 I2C 初始化与 OLED 地址确认CubeMX 里 I2C1 配好后生成的代码会自动初始化。OLED 的从机地址通常是0x78或0x7A取决于模块上的电阻选择。用HAL_I2C_IsDeviceReady可以扫描确认。// 扫描 I2C 地址确认 OLED 在位 if (HAL_I2C_IsDeviceReady(hi2c1, 0x78 1, 3, 100) HAL_OK) { // OLED 在位地址 0x78 }注意 HAL 库的地址要左移一位因为函数内部会自己补读写位。这个坑几乎每个人都会踩一次现象就是HAL_I2C_Master_Transmit一直返回 HAL_ERROR。4. 避坑与常见问题排查读数失败、OLED 不亮、延时不准4.1 现象温湿度一直是 0 或 255原因DHT11 没有响应或者响应了但数据位解析全错。先查硬件数据线有没有上拉VCC 是不是 3.3V接线有没有松动。再查软件起始信号拉低时间够不够 18ms切换输入模式后有没有及时读电平。我遇到过用HAL_GPIO_ReadPin读电平但 GPIO 模式没切回来一直输出高自然读不到。解决用示波器或者逻辑分析仪抓一下数据线波形看有没有 DHT11 的 80us 低电平响应。没有的话把起始信号延长到 20ms 试试。有响应但数据错检查微秒延时是否准确可以在延时函数里翻转一个空闲 IO用示波器量实际延时。4.2 现象OLED 亮但只显示一行或花屏原因I2C 地址不对或者初始化命令序列不匹配。SSD1306 有 128x64 和 128x32 两种初始化命令不同。另外有些模块需要上电后延时一段时间再发命令。解决先用扫描程序确认地址再对照模块手册改初始化数组。花屏通常是显存写入方式设错把水平寻址和页寻址搞混了。我一般会在初始化后清屏两次确保显存干净。4.3 现象Keil AC6 编译后延时函数失效原因AC6 优化等级高时空的for循环会被直接删掉微秒延时变成瞬间返回。DHT11 时序全乱。解决把延时函数里的循环变量加volatile或者直接用定时器硬件计数。更彻底的做法是用__NOP()配合固定循环次数但主频一变就得重算。我习惯用 TIM 做微秒延时一次配好后面不管主频怎么调都不受影响。4.4 现象程序烧进去第一次能跑复位后就不行原因DHT11 上电后需要 1 秒左右的稳定时间如果初始化太快就去读传感器还没准备好。另外OLED 的 I2C 总线如果被之前的中断打断可能处于忙状态。解决在main函数开头加HAL_Delay(1000)等传感器和 OLED 都稳定。I2C 忙的话在初始化前调用HAL_I2C_DeInit再HAL_I2C_Init复位一下总线。4.5 现象读数偶尔跳变比如温度突然 80 度原因单总线时序对中断敏感。如果读数据过程中被 SysTick 中断打断电平采样就错位了。解决读 DHT11 的那几十毫秒里关掉全局中断用__disable_irq()和__enable_irq()包起来。但注意关中断时间不能太长DHT11 一次完整读取大概 4ms 到 5ms可以接受。关中断前确保其他关键中断不会因此丢事件。5. 装完怎么验证跑通从串口打印到 OLED 刷新的完整检查链验证分三层先确认传感器能读到数再确认 OLED 能显示最后确认两者同时工作不打架。第一层串口验证。在main循环里每 2 秒读一次 DHT11把温度和湿度通过printf打到串口。串口助手设 1152008 数据位无校验。如果这里读数稳定说明 DHT11 驱动和延时都没问题。我一般会连续观察 10 次读数看有没有跳变或者超时。// 串口验证代码片段 DHT11_Read_Data(temp, humi); printf(Temp: %d C, Humi: %d %%\r\n, temp, humi); HAL_Delay(2000);第二层OLED 单独验证。写一个简单的测试函数在屏幕上固定位置显示字符串确认 I2C 通信和显存写入正常。如果串口有数但 OLED 不亮问题就在 I2C 或 OLED 初始化跟 DHT11 无关。第三层联合调试。把 DHT11 读数格式化后送到 OLED 显示。这里要注意 OLED 刷新频率不要太高DHT11 本身采样间隔建议不低于 1 秒否则传感器内部会来不及转换。我一般设 2 秒刷新一次屏幕看起来不闪数据也稳。// 联合调试读 DHT11 并刷新 OLED char buf[32]; DHT11_Read_Data(temp, humi); sprintf(buf, Temp: %d C, temp); OLED_ShowString(0, 0, (uint8_t *)buf); sprintf(buf, Humi: %d %%, humi); OLED_ShowString(0, 2, (uint8_t *)buf); HAL_Delay(2000);最后做一个压力测试让设备连续跑 30 分钟每隔 5 分钟记录一次串口输出看有没有累积误差或者死机。我遇到过跑十几分钟后读数卡死的情况原因是微秒延时函数里用了HAL_GetTick而HAL_GetTick在中断里被调用导致计数器回绕。换成纯定时器计数后就没再出现。从那以后我每次装完 DHT11 加 OLED 这套环境都强制走一遍“串口先通、屏幕后亮、联合跑半小时”的流程不跳过任何一步。希望帮到你。本文还有配套的精品资源点击获取