简介:这份资源面向嵌入式与单片机初学者及课程设计、毕业设计开发者,围绕DHT11数字温湿度传感器与OLED显示模块,提供一套可直接编译运行的完整工程,解决温湿度采集与屏幕实时显示的实现问题。压缩包共190个文件,约6.82MB,以C源码与头文件为核心,包含34个c、36个h,并配有Keil工程文件、启动汇编、链接脚本及编译中间产物,覆盖STM32标准外设库的定时器、串口、I2C、ADC等驱动模块,便于按模块阅读与二次修改。资源已有3710人学习下载,说明其在同类教程中具备一定参考价值。读者可从中获得传感器单线制串行通信的驱动思路、OLED显示刷新逻辑以及完整的工程目录组织方式,适合作为入门练手或项目移植的基础模板。1. 从一次“数据跳变”说起:DHT11温湿度OLED显示方案要解决什么做过嵌入式的人都遇到过这种场景:DHT11明明接好了,串口打印也正常,可一接到OLED上,数值就开始抽风——要么显示85.0这种经典错误值,要么刷新几次之后整块屏直接黑掉。问题往往不在传感器本身,而在于时序、刷新节奏和供电被搅在了一起。这套DHT11温湿度OLED显示方案,就是围绕“单总线传感器采集 + OLED本地显示”这条最小闭环来设计的,适合正在做嵌入式课设、单片机毕设,或者需要给某个环境监测节点加一块本地屏的开发者。它不追求联网、不上云,核心目标只有一个:让温湿度数据稳定、可读地出现在屏幕上,并且这套代码你能直接改、直接烧、直接验收。2. 需求拆解与技术选型:为什么是DHT11加OLED2.1 功能需求与非功能需求先把需求写清楚,不然后面选型全是拍脑袋。这个项目的功能需求其实就四条:第一,周期性采集环境温度和相对湿度;第二,把两个数值实时显示在本地屏幕上;第三,数值刷新时不能闪烁、不能残留旧字符;第四,系统上电后能自恢复,不需要人工复位。非功能需求往往被忽略,但恰恰是翻车重灾区。采集周期通常定在1到2秒,因为DHT11本身响应就慢,手册里写得很清楚,采样间隔低于1秒容易读到上一次的残留数据。显示刷新率反而可以高一些,但没必要,跟着采集节奏走就行。供电方面,DHT11对电压波动比较敏感,3.3V和5V都能跑,但如果你用的是5V单片机系统,数据引脚上拉电阻别省。还有一个隐性需求:代码要能移植,换一块同尺寸OLED或者换一个引脚,改几行宏定义就能跑,而不是重写整个驱动。2.2 主控、传感器与显示器的选型逻辑主控这块,STM32F103C8T6和GD32这类Cortex-M3内核的芯片是这个场景里最稳的选择。原因不复杂:GPIO翻转速度足够跑单总线时序,硬件I2C或者软件模拟I2C都吃得开,而且资料多、踩坑记录全。如果你手头只有51单片机,也能做,但DHT11的微秒级时序对51的晶振和中断响应要求更苛刻,调试时间会明显拉长。传感器选DHT11而不是DHT22或者SHT30,核心原因是成本和教学场景的匹配度。DHT11精度是湿度±5%RH、温度±2℃,分辨率1℃,对于“看个大概”的环境监测完全够用。它的单总线协议虽然时序要求死板,但正好是嵌入式课设里练GPIO和延时函数的经典素材。你要是做毕设,答辩时老师问“为什么不用I2C的SHT30”,你可以从成本、协议教学价值、项目定位三个角度回答,这比单纯说“便宜”要有说服力。显示器选0.96寸OLED,SSD1306驱动,I2C接口,128×64分辨率。选它的理由很实际:第一,I2C只要两根线,不占太多IO;第二,SSD1306的显存结构清晰,页地址模式写起来不绕;第三,0.96寸这个尺寸在课设实物里看起来足够“像个成品”,比LCD1602的观感好,而且不需要背光调节。唯一要注意的是I2C地址,市面上常见的是0x78和0x7A两种,买回来先用扫描程序确认一下,别硬编码。2.3 通信协议与引脚分配DHT11用的是单总线,一根数据线完成双向通信。主机拉低至少18ms作为起始信号,然后释放,DHT11响应拉低80微秒再拉高80微秒,接着连续输出40位数据。前16位是湿度整数和小数,中间16位是温度整数和小数,最后8位是校验和。校验规则是前四个字节相加取低8位,和校验字节比对。这个协议的特点是没有时钟线,全靠延时卡时间窗口,所以中断干扰是最大的敌人。OLED这边走I2C,SCL和SDA两根线,加上VCC和GND,四根线搞定。引脚分配我一般这样安排:DHT11数据脚接PA0,OLED的SCL接PB6、SDA接PB7,用硬件I2C。如果你用软件模拟I2C,那就随便挑两个普通GPIO,但延时函数要调准。下面这张表是典型接线参考,实际项目里按你的板子改。模块引脚主控引脚说明DHT11DATAPA0单总线,需4.7k上拉DHT11VCC3.3V供电DHT11GNDGND共地OLEDSCLPB6I2C时钟OLEDSDAPB7I2C数据OLEDVCC3.3V供电OLEDGNDGND共地注意:DHT11的数据引脚上拉电阻不能省,尤其是杜邦线较长的时候。我见过太多“读出来全是85”的案例,最后查出来就是上拉没接或者接触不良。3. 总体架构设计:三层结构与两个核心模块3.1 分层结构:驱动层、服务层、应用层这套方案我习惯按三层来切。最底下是驱动层,包含DHT11的时序驱动和SSD1306的I2C驱动,这一层只负责“把电平变成字节”和“把字节写到屏幕”,不关心业务。中间是服务层,负责把原始字节转成带单位的温湿度值,做校验、做异常过滤,比如连续读到85就标记为无效。最上面是应用层,管调度:什么时候采、什么时候刷、刷新时先清哪块区域。这样分的好处是调试的时候能快速定位。屏幕不亮,查驱动层;数值不对,查服务层;刷新节奏乱,查应用层。很多课设代码把这三层揉在一个main函数里,出了问题只能一行行看,效率极低。3.2 数据流与刷新策略数据流很直白:应用层触发采集,服务层调用DHT11驱动拿到40位原始数据,校验通过后转成温度和湿度两个浮点数或者整型,然后交给OLED驱动显示。这里有个细节:DHT11的温度小数位在DHT11上其实恒为0,湿度小数位也是0,所以显示的时候直接按整数处理就行,别被某些库里的浮点转换绕进去。刷新策略上,我一般用“局部刷新”而不是“整屏清空重画”。整屏清空会导致肉眼可见的闪烁,尤其是刷新周期短的时候。具体做法是:温湿度数值固定显示在屏幕的某两行,每次刷新只覆盖数值区域,前面的标签“Temp:”和“Humi:”只写一次。SSD1306的页地址模式支持按页写,你只要算好坐标,每次只更新那几页的显存就行。3.3 关键数据结构与状态机DHT11的读取过程适合用一个简单的状态机来描述,而不是一路delay到底。状态机大概分四个状态:空闲、起始信号、等待响应、接收数据。每个状态里做有限的事,超时就跳回空闲并标记错误。这样即使某次读取失败,系统也不会卡死。数据结构上,定义一个结构体存温湿度值和有效性标志:typedef struct { uint8_t temperature; // 温度整数部分,单位℃ uint8_t humidity; // 湿度整数部分,单位%RH uint8_t valid; // 1=本次数据有效,0=无效 uint32_t last_read_ms; // 上次成功读取的时间戳 } dht11_data_t;这个结构体在服务层和应用层之间传递,应用层只认valid标志,不关心底层时序。valid为0的时候,屏幕上保持上一次的有效值,或者显示“--”,而不是把85.0直接怼上去。4. 模块实现:DHT11时序驱动与OLED显示驱动4.1 DHT11单总线时序的代码实现DHT11的时序核心就三件事:拉低、释放、按窗口读位。下面这段是基于STM32 HAL库的微秒级延时实现,关键点在于读位的时候要在高电平持续时间内判断是0还是1。// 微秒级延时,基于SysTick或者DWT,这里用简单的循环延时示意 static void delay_us(uint32_t us) { uint32_t count = us * (SystemCoreClock / 1000000) / 5; while (count--) { __NOP(); } } // 读取一个字节,MSB先出 static uint8_t dht11_read_byte(void) { uint8_t byte = 0; for (int i = 0; i < 8; i++) { // 等待50us低电平结束 while (DHT11_PIN_READ() == 0); delay_us(40); // 40us后采样,高电平仍为1则是数据1 byte <<= 1; if (DHT11_PIN_READ() == 1) { byte |= 1; // 等待剩余的高电平结束 while (DHT11_PIN_READ() == 1); } } return byte; } // 完整读取一次,返回0成功,-1失败 int dht11_read(dht11_data_t *out) { uint8_t buf[5] = {0}; // 主机拉低至少18ms DHT11_PIN_OUT(); DHT11_PIN_LOW(); HAL_Delay(20); DHT11_PIN_HIGH(); delay_us(30); DHT11_PIN_IN(); // 等待DHT11响应 if (DHT11_PIN_READ() == 1) return -1; while (DHT11_PIN_READ() == 0); while (DHT11_PIN_READ() == 1); // 读40位 for (int i = 0; i < 5; i++) { buf[i] = dht11_read_byte(); } // 校验 if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) { out->valid = 0; return -1; } out->humidity = buf[0]; out->temperature = buf[2]; out->valid = 1; return 0; }这段代码里,delay_us(40)那个采样点是关键。DHT11的位数据里,0的高电平持续26到28微秒,1的高电平持续70微秒。40微秒的时候采样,0已经变低,1还是高,刚好能区分。这个值不能随便改,改了就容易读错。另外while等待循环里没有加超时,实际项目里建议加一个计数器,防止传感器掉线时死循环。4.2 SSD1306的I2C写命令与写数据SSD1306的I2C协议里,每次传输的第一个字节是控制字节,0x00表示后面跟的是命令,0x40表示后面跟的是数据。写命令和写数据分开封装,后面初始化的时候会反复用到。// 写一个命令字节 void oled_write_cmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100); } // 写一个数据字节 void oled_write_data(uint8_t data) { uint8_t buf[2] = {0x40, data}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100); } // 设置显示坐标,页地址模式 void oled_set_pos(uint8_t page, uint8_t col) { oled_write_cmd(0xB0 + page); // 设置页地址 oled_write_cmd(col & 0x0F); // 列低4位 oled_write_cmd(0x10 | (col >> 4)); // 列高4位 }OLED_ADDR一般是0x78,但如果你买到的模块背面电阻跳到了0x7A,就得改。写命令和写数据之间不需要额外延时,SSD1306的I2C速率支持到400kHz,HAL库默认100kHz完全够用。初始化序列里,0xAE关显示、0xD5设时钟、0xA8设复用率、0x8D开电荷泵,这几条顺序不能乱,尤其是电荷泵,不开的话屏幕永远不亮。4.3 温湿度数值的格式化与局部刷新显示部分,我一般把温度和湿度分两行放,字体用6x8或者8x16。局部刷新的逻辑是:先算出数值字符串,比如“25C”和“60%”,然后只更新这两行对应的显存区域。// 在指定页和列显示一个字符串 void oled_show_str(uint8_t page, uint8_t col, char *str) { oled_set_pos(page, col); while (*str) { // 这里调用字库取模函数,把字符点阵逐字节写入 for (int i = 0; i < 6; i++) { oled_write_data(font6x8[*str - ' '][i]); } str++; } } // 应用层刷新 void refresh_display(dht11_data_t *data) { char temp_str[8], humi_str[8]; if (data->valid) { sprintf(temp_str, "%dC", style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
简介:这份资源面向嵌入式与单片机初学者及课程设计、毕业设计开发者,围绕DHT11数字温湿度传感器与OLED显示模块,提供一套可直接编译运行的完整工程,解决温湿度采集与屏幕实时显示的实现问题。压缩包共190个文件,约6.82MB,以C源码与头文件为核心,包含34个c、36个h,并配有Keil工程文件、启动汇编、链接脚本及编译中间产物,覆盖STM32标准外设库的定时器、串口、I2C、ADC等驱动模块,便于按模块阅读与二次修改。资源已有3710人学习下载,说明其在同类教程中具备一定参考价值。读者可从中获得传感器单线制串行通信的驱动思路、OLED显示刷新逻辑以及完整的工程目录组织方式,适合作为入门练手或项目移植的基础模板。1. 从一次“数据跳变”说起:DHT11温湿度OLED显示方案要解决什么做过嵌入式的人都遇到过这种场景:DHT11明明接好了,串口打印也正常,可一接到OLED上,数值就开始抽风——要么显示85.0这种经典错误值,要么刷新几次之后整块屏直接黑掉。问题往往不在传感器本身,而在于时序、刷新节奏和供电被搅在了一起。这套DHT11温湿度OLED显示方案,就是围绕“单总线传感器采集 + OLED本地显示”这条最小闭环来设计的,适合正在做嵌入式课设、单片机毕设,或者需要给某个环境监测节点加一块本地屏的开发者。它不追求联网、不上云,核心目标只有一个:让温湿度数据稳定、可读地出现在屏幕上,并且这套代码你能直接改、直接烧、直接验收。2. 需求拆解与技术选型:为什么是DHT11加OLED2.1 功能需求与非功能需求先把需求写清楚,不然后面选型全是拍脑袋。这个项目的功能需求其实就四条:第一,周期性采集环境温度和相对湿度;第二,把两个数值实时显示在本地屏幕上;第三,数值刷新时不能闪烁、不能残留旧字符;第四,系统上电后能自恢复,不需要人工复位。非功能需求往往被忽略,但恰恰是翻车重灾区。采集周期通常定在1到2秒,因为DHT11本身响应就慢,手册里写得很清楚,采样间隔低于1秒容易读到上一次的残留数据。显示刷新率反而可以高一些,但没必要,跟着采集节奏走就行。供电方面,DHT11对电压波动比较敏感,3.3V和5V都能跑,但如果你用的是5V单片机系统,数据引脚上拉电阻别省。还有一个隐性需求:代码要能移植,换一块同尺寸OLED或者换一个引脚,改几行宏定义就能跑,而不是重写整个驱动。2.2 主控、传感器与显示器的选型逻辑主控这块,STM32F103C8T6和GD32这类Cortex-M3内核的芯片是这个场景里最稳的选择。原因不复杂:GPIO翻转速度足够跑单总线时序,硬件I2C或者软件模拟I2C都吃得开,而且资料多、踩坑记录全。如果你手头只有51单片机,也能做,但DHT11的微秒级时序对51的晶振和中断响应要求更苛刻,调试时间会明显拉长。传感器选DHT11而不是DHT22或者SHT30,核心原因是成本和教学场景的匹配度。DHT11精度是湿度±5%RH、温度±2℃,分辨率1℃,对于“看个大概”的环境监测完全够用。它的单总线协议虽然时序要求死板,但正好是嵌入式课设里练GPIO和延时函数的经典素材。你要是做毕设,答辩时老师问“为什么不用I2C的SHT30”,你可以从成本、协议教学价值、项目定位三个角度回答,这比单纯说“便宜”要有说服力。显示器选0.96寸OLED,SSD1306驱动,I2C接口,128×64分辨率。选它的理由很实际:第一,I2C只要两根线,不占太多IO;第二,SSD1306的显存结构清晰,页地址模式写起来不绕;第三,0.96寸这个尺寸在课设实物里看起来足够“像个成品”,比LCD1602的观感好,而且不需要背光调节。唯一要注意的是I2C地址,市面上常见的是0x78和0x7A两种,买回来先用扫描程序确认一下,别硬编码。2.3 通信协议与引脚分配DHT11用的是单总线,一根数据线完成双向通信。主机拉低至少18ms作为起始信号,然后释放,DHT11响应拉低80微秒再拉高80微秒,接着连续输出40位数据。前16位是湿度整数和小数,中间16位是温度整数和小数,最后8位是校验和。校验规则是前四个字节相加取低8位,和校验字节比对。这个协议的特点是没有时钟线,全靠延时卡时间窗口,所以中断干扰是最大的敌人。OLED这边走I2C,SCL和SDA两根线,加上VCC和GND,四根线搞定。引脚分配我一般这样安排:DHT11数据脚接PA0,OLED的SCL接PB6、SDA接PB7,用硬件I2C。如果你用软件模拟I2C,那就随便挑两个普通GPIO,但延时函数要调准。下面这张表是典型接线参考,实际项目里按你的板子改。模块引脚主控引脚说明DHT11DATAPA0单总线,需4.7k上拉DHT11VCC3.3V供电DHT11GNDGND共地OLEDSCLPB6I2C时钟OLEDSDAPB7I2C数据OLEDVCC3.3V供电OLEDGNDGND共地注意:DHT11的数据引脚上拉电阻不能省,尤其是杜邦线较长的时候。我见过太多“读出来全是85”的案例,最后查出来就是上拉没接或者接触不良。3. 总体架构设计:三层结构与两个核心模块3.1 分层结构:驱动层、服务层、应用层这套方案我习惯按三层来切。最底下是驱动层,包含DHT11的时序驱动和SSD1306的I2C驱动,这一层只负责“把电平变成字节”和“把字节写到屏幕”,不关心业务。中间是服务层,负责把原始字节转成带单位的温湿度值,做校验、做异常过滤,比如连续读到85就标记为无效。最上面是应用层,管调度:什么时候采、什么时候刷、刷新时先清哪块区域。这样分的好处是调试的时候能快速定位。屏幕不亮,查驱动层;数值不对,查服务层;刷新节奏乱,查应用层。很多课设代码把这三层揉在一个main函数里,出了问题只能一行行看,效率极低。3.2 数据流与刷新策略数据流很直白:应用层触发采集,服务层调用DHT11驱动拿到40位原始数据,校验通过后转成温度和湿度两个浮点数或者整型,然后交给OLED驱动显示。这里有个细节:DHT11的温度小数位在DHT11上其实恒为0,湿度小数位也是0,所以显示的时候直接按整数处理就行,别被某些库里的浮点转换绕进去。刷新策略上,我一般用“局部刷新”而不是“整屏清空重画”。整屏清空会导致肉眼可见的闪烁,尤其是刷新周期短的时候。具体做法是:温湿度数值固定显示在屏幕的某两行,每次刷新只覆盖数值区域,前面的标签“Temp:”和“Humi:”只写一次。SSD1306的页地址模式支持按页写,你只要算好坐标,每次只更新那几页的显存就行。3.3 关键数据结构与状态机DHT11的读取过程适合用一个简单的状态机来描述,而不是一路delay到底。状态机大概分四个状态:空闲、起始信号、等待响应、接收数据。每个状态里做有限的事,超时就跳回空闲并标记错误。这样即使某次读取失败,系统也不会卡死。数据结构上,定义一个结构体存温湿度值和有效性标志:typedef struct { uint8_t temperature; // 温度整数部分,单位℃ uint8_t humidity; // 湿度整数部分,单位%RH uint8_t valid; // 1=本次数据有效,0=无效 uint32_t last_read_ms; // 上次成功读取的时间戳 } dht11_data_t;这个结构体在服务层和应用层之间传递,应用层只认valid标志,不关心底层时序。valid为0的时候,屏幕上保持上一次的有效值,或者显示“--”,而不是把85.0直接怼上去。4. 模块实现:DHT11时序驱动与OLED显示驱动4.1 DHT11单总线时序的代码实现DHT11的时序核心就三件事:拉低、释放、按窗口读位。下面这段是基于STM32 HAL库的微秒级延时实现,关键点在于读位的时候要在高电平持续时间内判断是0还是1。// 微秒级延时,基于SysTick或者DWT,这里用简单的循环延时示意 static void delay_us(uint32_t us) { uint32_t count = us * (SystemCoreClock / 1000000) / 5; while (count--) { __NOP(); } } // 读取一个字节,MSB先出 static uint8_t dht11_read_byte(void) { uint8_t byte = 0; for (int i = 0; i < 8; i++) { // 等待50us低电平结束 while (DHT11_PIN_READ() == 0); delay_us(40); // 40us后采样,高电平仍为1则是数据1 byte <<= 1; if (DHT11_PIN_READ() == 1) { byte |= 1; // 等待剩余的高电平结束 while (DHT11_PIN_READ() == 1); } } return byte; } // 完整读取一次,返回0成功,-1失败 int dht11_read(dht11_data_t *out) { uint8_t buf[5] = {0}; // 主机拉低至少18ms DHT11_PIN_OUT(); DHT11_PIN_LOW(); HAL_Delay(20); DHT11_PIN_HIGH(); delay_us(30); DHT11_PIN_IN(); // 等待DHT11响应 if (DHT11_PIN_READ() == 1) return -1; while (DHT11_PIN_READ() == 0); while (DHT11_PIN_READ() == 1); // 读40位 for (int i = 0; i < 5; i++) { buf[i] = dht11_read_byte(); } // 校验 if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) { out->valid = 0; return -1; } out->humidity = buf[0]; out->temperature = buf[2]; out->valid = 1; return 0; }这段代码里,delay_us(40)那个采样点是关键。DHT11的位数据里,0的高电平持续26到28微秒,1的高电平持续70微秒。40微秒的时候采样,0已经变低,1还是高,刚好能区分。这个值不能随便改,改了就容易读错。另外while等待循环里没有加超时,实际项目里建议加一个计数器,防止传感器掉线时死循环。4.2 SSD1306的I2C写命令与写数据SSD1306的I2C协议里,每次传输的第一个字节是控制字节,0x00表示后面跟的是命令,0x40表示后面跟的是数据。写命令和写数据分开封装,后面初始化的时候会反复用到。// 写一个命令字节 void oled_write_cmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100); } // 写一个数据字节 void oled_write_data(uint8_t data) { uint8_t buf[2] = {0x40, data}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100); } // 设置显示坐标,页地址模式 void oled_set_pos(uint8_t page, uint8_t col) { oled_write_cmd(0xB0 + page); // 设置页地址 oled_write_cmd(col & 0x0F); // 列低4位 oled_write_cmd(0x10 | (col >> 4)); // 列高4位 }OLED_ADDR一般是0x78,但如果你买到的模块背面电阻跳到了0x7A,就得改。写命令和写数据之间不需要额外延时,SSD1306的I2C速率支持到400kHz,HAL库默认100kHz完全够用。初始化序列里,0xAE关显示、0xD5设时钟、0xA8设复用率、0x8D开电荷泵,这几条顺序不能乱,尤其是电荷泵,不开的话屏幕永远不亮。4.3 温湿度数值的格式化与局部刷新显示部分,我一般把温度和湿度分两行放,字体用6x8或者8x16。局部刷新的逻辑是:先算出数值字符串,比如“25C”和“60%”,然后只更新这两行对应的显存区域。// 在指定页和列显示一个字符串 void oled_show_str(uint8_t page, uint8_t col, char *str) { oled_set_pos(page, col); while (*str) { // 这里调用字库取模函数,把字符点阵逐字节写入 for (int i = 0; i < 6; i++) { oled_write_data(font6x8[*str - ' '][i]); } str++; } } // 应用层刷新 void refresh_display(dht11_data_t *data) { char temp_str[8], humi_str[8]; if (data->valid) { sprintf(temp_str, "%dC", style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />