
简介这是一份面向嵌入式与物联网初学者的STM32实战工程基于HAL库实现DHT11温湿度传感器数据采集并通过OLED屏实时显示温湿度信息适合正在学习单总线时序控制、I2C/SPI显示驱动与HAL库移植的开发者参考。压缩包共193个文件约6.9MB以h头文件、c源文件、o目标文件、crf编译中间文件为主另含ioc配置、uvprojx与uvoptx工程文件、hex烧录文件及map、lst等编译产物完整保留了从驱动到工程的目录结构。资源已有772人学习下载可帮助读者理清DHT11的40位数据解析、OLED显示驱动配置以及GPIO、I2C等外设初始化流程并借鉴异常处理与低功耗设计思路快速搭建可运行的温湿度监测原型。1. 从一根数据线到屏幕数字这个项目能让你少走三天弯路如果你手头正捏着一块 STM32 开发板、一个 DHT11 温湿度模块和一块 OLED 屏却卡在“时序对不上、屏幕不亮、数据全是 0”的阶段这个项目就是冲着你来的。它做的事情很具体用 HAL 库驱动 DHT11 采集环境温湿度再把结果实时刷到 OLED 上形成一个能独立运行的物联网感知节点 Demo。别小看这个组合它几乎是每个嵌入式新手从“点灯”迈向“做产品”的必经关卡——单总线协议、微秒级延时、I2C 通信、字库取模四个坑一个不落全踩一遍。适合谁刚学完 GPIO 和定时器、想找个综合小项目练手的嵌入式初学者也适合需要快速验证传感器方案、不想从寄存器手册翻起的在职开发者。代码结构清晰改起来不费劲拿来当毕业设计的基础框架或者课程设计的底稿都撑得住场面。2. 硬件连线与 CubeMX 配置先把地基打牢2.1 硬件清单与接线逻辑这个项目对硬件要求不高但接线错了后面全是玄学问题。核心三件套一块 STM32F103C8T6 最小系统板或者任何带 HAL 库支持的 STM32 型号、一个 DHT11 温湿度模块三针或四针都行四针的忽略 NC 脚、一块 0.96 寸 OLED 屏SSD1306 驱动I2C 接口四针VCC、GND、SCL、SDA。接线方案我一般这样安排DHT11 的数据脚接 PA0OLED 的 SCL 接 PB6、SDA 接 PB7这是 STM32 的硬件 I2C1 默认引脚省得软件模拟。DHT11 的 VCC 和 GND 分别接 3.3V 和 GNDOLED 同理。注意 DHT11 的供电范围是 3.3V 到 5.5V但为了和 STM32 电平匹配统一用 3.3V 最稳妥。数据线上拉电阻的问题DHT11 模块本身通常自带 4.7k 到 10k 的上拉如果你买的是裸传感器必须在数据线和 VCC 之间手动加一个 4.7k 电阻否则读出来的数据会随机跳变这个坑后面避坑章节还会细说。2.2 CubeMX 里的关键配置项打开 CubeMX 新建工程选好芯片型号后按下面几步走。第一步配时钟在 RCC 里把 HSE 设为 Crystal/Ceramic Resonator然后到 Clock Configuration 把 HCLK 拉到 72MHz这是 F103 的标配。第二步配 GPIOPA0 设为 GPIO_Input模式选 Pull-up内部上拉因为 DHT11 空闲时数据线是高电平开漏输出需要外部上拉配合。第三步配 I2C1模式选 I2C速度设 400kHz 快速模式其他保持默认。第四步配一个微秒级延时用的定时器TIM1 或者 TIM2 都行预分频器设 71自动重装载值设 65535这样计数器每加一就是 1 微秒用来做 DHT11 的时序控制。配置完生成代码MDK 或者 STM32CubeIDE 都行。生成的时候注意勾选“Generate peripheral initialization as a pair of .c/.h files”这样每个外设的初始化代码会单独成文件后面改起来清爽。// tim.c 中已经由 CubeMX 生成好初始化代码 // 这里补充一个微秒级延时函数放在 tim.c 或者单独的工具文件里 void delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(htim1, 0); // 计数器清零 HAL_TIM_Base_Start(htim1); // 启动定时器 while (__HAL_TIM_GET_COUNTER(htim1) us); // 等待计数到目标值 HAL_TIM_Base_Stop(htim1); // 关闭定时器 }这段代码的逻辑很直白利用 TIM1 的 1MHz 计数频率每计数一次就是 1 微秒。__HAL_TIM_SET_COUNTER把计数器归零然后启动定时器开始累加while循环里不断读取当前计数值一旦达到设定的微秒数就跳出循环并停止定时器。参数us就是你要延时的微秒数DHT11 的时序里最长需要 30 微秒左右的延时这个函数完全够用。注意不要用 HAL_Delay 来做微秒延时它的最小分辨率是毫秒根本满足不了 DHT11 的时序要求。3. DHT11 驱动层拆解单总线协议的时序博弈3.1 单总线通信的起始与响应DHT11 用的是单根数据线完成双向通信靠的是严格的时序配合。主机先拉低数据线至少 18 毫秒然后拉高 20 到 40 微秒接着释放总线转为输入模式。DHT11 检测到这个起始信号后会拉低总线 80 微秒作为响应再拉高 80 微秒之后才开始传输 40 位数据。这 40 位数据里前 16 位是湿度整数和小数中间 16 位是温度整数和小数最后 8 位是校验和。// dht11.c 中的起始信号函数 uint8_t DHT11_Start(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 切换 PA0 为推挽输出 GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 拉低总线 HAL_Delay(20); // 保持至少 18ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 拉高总线 delay_us(30); // 保持 20-40us // 切换 PA0 为输入模式准备接收响应 GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 等待 DHT11 拉低响应 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 等待响应低电平结束 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); // 等待响应高电平结束 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET); return 1; // 响应成功 } return 0; // 无响应 }这段代码的关键在于 GPIO 模式的动态切换。DHT11 是单总线设备主机发送起始信号时数据线是输出模式发送完毕后必须立刻切成输入模式否则 DHT11 的响应信号会和主机的输出打架轻则读不到数据重则烧掉引脚。HAL_Delay(20)保证拉低时间超过 18 毫秒delay_us(30)保证拉高时间落在 20 到 40 微秒之间。后面的两个while循环用来等待 DHT11 的响应低电平和高电平依次结束如果第一个判断就没检测到低电平说明传感器没接好或者坏了直接返回 0。3.2 40 位数据的读取与校验响应信号结束后DHT11 开始逐位传输数据。每一位都以 50 微秒的低电平开始然后是 26 到 28 微秒的高电平表示“0”70 微秒的高电平表示“1”。所以读取每一位的逻辑就是先等低电平过去然后延时 40 微秒后采样如果还是高电平就是“1”否则是“0”。// 读取一个字节8 位 uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { // 等待 50us 低电平结束 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); delay_us(40); // 延时 40us 后采样 data 1; if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { data | 1; } // 等待剩余的高电平结束 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET); } return data; } // 读取完整的温湿度数据 uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; if (DHT11_Start() 0) return 0; for (int i 0; i 5; i) { buf[i] DHT11_ReadByte(); } // 校验前四字节之和等于第五字节 if (buf[0] buf[1] buf[2] buf[3] buf[4]) { *humi buf[0]; // 湿度整数部分 *temp buf[2]; // 温度整数部分 return 1; } return 0; }DHT11_ReadByte里的delay_us(40)是采样时机的核心。DHT11 的“0”和“1”靠高电平持续时间区分40 微秒正好卡在两者之间如果是“0”高电平只有 26 到 28 微秒40 微秒后早就变低了如果是“1”高电平有 70 微秒40 微秒时还是高的。这个采样点不能偏偏了就会把“1”读成“0”或者反过来。DHT11_ReadData里读 5 个字节前四个是湿度和温度数据第五个是校验和校验通过才把值赋给输出参数。注意 DHT11 的小数部分通常都是 0所以这里只取了整数部分够用且省事。4. OLED 显示层从显存到屏幕的最后一公里4.1 SSD1306 初始化与显存映射OLED 屏用的是 SSD1306 驱动芯片I2C 地址通常是 0x788 位地址或者 0x3C7 位地址。初始化流程比较长但 CubeMX 已经帮我们配好了 I2C 外设剩下的就是发命令和发数据。SSD1306 的显存是 128x64 位分成 8 页每页 8 行每行 128 个像素。写入数据时按页地址和列地址定位然后连续写入字节每个字节的 8 个位对应垂直方向的 8 个像素。// oled.c 中的写命令和写数据函数 void OLED_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, OLED_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_WriteData(uint8_t data) { HAL_I2C_Mem_Write(hi2c1, OLED_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, data, 1, 100); } // 初始化序列只列关键几条 void OLED_Init(void) { HAL_Delay(100); // 上电等待稳定 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x10); // 页寻址模式 OLED_WriteCmd(0xB0); // 页地址从 0 开始 OLED_WriteCmd(0xC8); // 扫描方向 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0xAF); // 开启显示 }HAL_I2C_Mem_Write的第二个参数是设备地址第三个参数是寄存器地址——对 SSD1306 来说0x00 表示后面跟的是命令0x40 表示后面跟的是数据。这个区分很关键写反了屏幕要么不亮要么花屏。初始化序列里0x8D和0x14是开启电荷泵没有这两条屏幕根本亮不起来很多人卡在这里以为是接线问题其实是初始化漏了命令。4.2 温湿度数据的格式化与刷新显示部分我一般用一个 8x16 的 ASCII 字库每个字符占 16 个字节刚好填满两页。显示字符串的时候按页写入先写上半页再写下半页。温湿度数据是整数用sprintf格式化成字符串再送显示。// 显示温湿度到 OLED void OLED_ShowTempHumi(uint8_t temp, uint8_t humi) { char buf[20]; // 格式化字符串 sprintf(buf, Temp: %d C, temp); OLED_ShowString(0, 0, (uint8_t *)buf); // 第 0 页开始显示 sprintf(buf, Humi: %d %%, humi); OLED_ShowString(0, 2, (uint8_t *)buf); // 第 2 页开始显示 OLED_Refresh(); // 统一刷新显存到屏幕 }OLED_ShowString内部会查字库表把每个字符的 16 个字节依次写入显存缓冲区。这里用了一个显存缓冲区uint8_t OLED_GRAM[8][128]所有绘制操作先改缓冲区最后调OLED_Refresh一次性刷到屏幕。这样做的好处是避免频繁 I2C 通信导致闪烁也方便做局部刷新。sprintf里的%%是为了输出一个百分号因为%在格式化字符串里是转义字符。温度值后面加个C表示摄氏度湿度加%表示百分比显示效果直观。5. 避坑与常见问题排查那些让你怀疑人生的瞬间5.1 数据全是 0 或者 255现象OLED 能亮但温湿度读数始终是 0 或者 255怎么复位都不变。原因通常是 DHT11 的起始信号没被响应或者数据线接触不良。先检查 PA0 的接线是否牢固杜邦线松了是高频事故。然后用万用表量一下数据线在空闲时是不是 3.3V如果不是说明上拉电阻没起作用。解决方法是换一根线或者在数据线和 VCC 之间并一个 4.7k 电阻。还有一种可能是HAL_Delay(20)被中断打断导致拉低时间不够可以在起始信号前后关一下全局中断。5.2 读数偶尔跳变或者校验失败现象大部分时候读数正常但每隔几次就出现一次校验失败或者数值突变。原因是 DHT11 的采样周期不能低于 1 秒如果你在循环里连续调用读取函数传感器还没完成上一次转换返回的就是旧数据或者错误数据。解决方法是在DHT11_ReadData调用之后加一个HAL_Delay(1500)保证每次读取间隔超过 1.5 秒。另外电源纹波也会影响 DHT11如果和电机或者继电器共用电源最好给 DHT11 的 VCC 加一个 100nF 的去耦电容。5.3 OLED 不亮或者显示乱码现象OLED 通电后完全不亮或者显示一堆乱码像素。先确认 I2C 地址对不对0x78 和 0x3C 是两个常见地址用 I2C 扫描程序扫一下就能确定。如果地址对但还不亮检查初始化序列里有没有漏掉电荷泵开启命令0x8D和0x14。乱码通常是字库取模方式不对比如阴码阳码搞反了或者页地址计算错了。解决方法是换一个标准的 8x16 字库文件确认取模方式为“列行式、逆向、阴码”。5.4 编译报错找不到 HAL_I2C_Mem_Write现象编译时提示HAL_I2C_Mem_Write未定义。原因是 CubeMX 生成代码时没有使能 I2C 外设或者stm32f1xx_hal_i2c.h没有被包含进来。解决方法是回到 CubeMX 把 I2C1 设为 I2C 模式重新生成或者在main.h里手动包含对应的头文件。还有一种可能是你用的是 LL 库而不是 HAL 库函数名不一样需要换成 LL_I2C_ 开头的函数。5.5 定时器延时不准导致时序错乱现象DHT11 偶尔能读到数据但成功率很低大部分时候超时。原因是delay_us函数用的定时器被其他中断打断导致实际延时比设定值长。解决方法是把 TIM1 的中断优先级设到最高或者在delay_us执行期间关中断。另外检查定时器的预分频器是不是 71如果系统时钟不是 72MHz预分频值要相应调整否则 1 微秒的基准就错了。6. 二次开发与改造思路从 Demo 到可用节点这个项目跑通之后最自然的改造方向是把它变成一个物联网节点。我一般会从三个层面动手通信层、数据层、应用层。通信层加一个 ESP8266 或者 ESP32 模块通过串口 AT 指令连上 WiFi把温湿度数据推到 MQTT 服务器。HAL 库的 UART 收发很简单HAL_UART_Transmit和HAL_UART_Receive_IT配合就能搞定。数据层加一个环形缓冲区把最近几十条温湿度记录存下来OLED 上除了显示当前值还能翻页看历史曲线。应用层加一个按键短按切换显示页面长按进入设置模式调整报警阈值。// 一个简单的 MQTT 上报数据拼接示例 void MQTT_PublishTempHumi(uint8_t temp, uint8_t humi) { char payload[64]; sprintf(payload, {\temp\:%d,\humi\:%d}, temp, humi); // 通过串口发送 AT 指令给 ESP8266 HAL_UART_Transmit(huart1, (uint8_t *)ATMQTTPUB0,\topic\,\, 26, 100); HAL_UART_Transmit(huart1, (uint8_t *)payload, strlen(payload), 100); HAL_UART_Transmit(huart1, (uint8_t *)\,0,0\r\n, 7, 100); }这段代码把温湿度拼成 JSON 格式然后分三次通过串口发给 ESP8266。实际用的时候要把topic换成你自己的 MQTT 主题AT 指令的格式也要根据模块固件版本调整。注意HAL_UART_Transmit是阻塞发送数据量大的时候会卡住主循环建议改成 DMA 或者中断发送。另一个改造方向是低功耗。DHT11 和 OLED 都是耗电大户如果做电池供电的节点可以在两次采集之间让 STM32 进入 STOP 模式用 RTC 定时唤醒。OLED 也可以设置成 30 秒无操作自动熄屏按键唤醒。这些改动不需要动核心驱动只是在main循环里加状态机就行。验证改造是否成功的方法很简单先确保原始项目能稳定读数然后每加一个功能就用串口打印调试信息确认数据流没断。我习惯在main循环里加一个 LED 闪烁频率和采集周期一致这样一眼就能看出程序有没有跑飞。从那以后我每次改驱动层代码都强制走一遍“先读固定值、再读传感器、最后上屏幕”的流程避免一上来就调显示导致问题定位不清。希望帮到你。本文还有配套的精品资源点击获取