STM32F1驱动DHT11传感器:OLED实时显示温湿度数据实战
1. 项目概述与环境搭建
大家好,今天我来分享一个非常实用的STM32实战项目——用STM32F1驱动DHT11温湿度传感器,并在OLED屏幕上实时显示数据。这个项目特别适合刚入门嵌入式开发的朋友,既能学习单总线通信协议,又能掌握I2C显示设备的驱动方法。
我选择STM32F103C8T6(Blue Pill)作为主控芯片,因为它性价比高,资源足够,而且社区支持完善。DHT11是一款常见的温湿度传感器,采用单总线通信,接线简单,价格便宜。OLED显示屏我选用0.96英寸的I2C接口版本,只需要4根线就能驱动,显示效果清晰。
硬件准备清单:
- STM32F103C8T6核心板
- DHT11温湿度传感器模块(建议选择带板上拉电阻的版本)
- 0.96英寸I2C接口OLED显示屏
- ST-Link V2下载器
- 杜邦线若干(建议使用公对母和公对公两种)
- 可选:10kΩ电阻(如果DHT11模块没有内置上拉电阻)
软件环境:
- Keil MDK-ARM开发环境(v5或更高版本)
- STM32F1标准外设库或HAL库
- ST-Link驱动程序
- 串口调试助手(可选,用于调试)
接线时特别注意:所有设备的GND一定要共地,这是很多初学者容易忽略的点。OLED屏幕只能接3.3V,千万不要误接5V,否则很容易烧毁屏幕。DHT11的DATA线如果没有上拉电阻,一定要外接一个10kΩ电阻到3.3V,否则通信会不稳定。
2. 硬件连接详解
正确的硬件连接是项目成功的基础。让我详细说明各模块的接线方法,这些都是我实际项目中总结的经验。
DHT11传感器接线:
- VCC → 3.3V(虽然DHT11支持5V,但为了电平匹配,建议统一用3.3V)
- GND → GND(必须与其他模块共地)
- DATA → PA1(你可以选择其他GPIO,但代码要相应修改)
OLED显示屏接线:
- VCC → 3.3V(切记不要接5V!)
- GND → GND
- SDA → PB7(对应I2C1的SDA线)
- SCL → PB6(对应I2C1的SCL线)
ST-Link下载器接线:
- SWDIO → PA13
- SWCLK → PA14
- GND → GND
- 3.3V → 3.3V(如果外接电源,这个可以不接)
在实际焊接或插接杜邦线时,一定要确保接触良好。我遇到过很多次因为杜邦线虚接导致的问题:有时能读取数据,有时又不能,排查了很久才发现是线缆接触不良。建议插好后轻轻拉一下确认是否插稳。
如果发现DHT11读数不稳定,可以在DATA线和3.3V之间加一个10kΩ的上拉电阻。虽然很多模块已经内置了这个电阻,但加上会更稳定。OLED屏幕如果完全不亮,首先检查电源是否是3.3V,然后检查I2C地址是否正确(通常是0x3C或0x78)。
3. DHT11驱动原理与代码实现
DHT11采用单总线通信协议,对时序要求非常严格。这也是很多初学者觉得DHT11难驱动的原因。让我来详细解析其工作原理和代码实现。
单总线通信原理: DHT11只有一根数据线,既要作为输入又要作为输出,所以需要频繁切换GPIO模式。通信开始时,主机(STM32)先拉低总线至少18ms,然后拉高20-40us,这是起始信号。接着DHT11会响应一个80us的低电平,然后拉高80us,之后开始传输数据。
数据格式:每次传输40位数据,包括:
- 8位湿度整数
- 8位湿度小数(DHT11总是0)
- 8位温度整数
- 8位温度小数(DHT11总是0)
- 8位校验和
校验和是前4个字节的和,用于验证数据是否正确。
GPIO模式配置: 我们需要在代码中动态切换GPIO的输入输出模式。输出模式用于发送起始信号,输入模式用于接收数据。
// 设置DHT11数据引脚为输出模式
void DHT11_Output_Mode(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
// 设置DHT11数据引脚为输入模式
void DHT11_Input_Mode(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
复位和检测函数: 发送起始信号后,需要检测DHT11是否正常响应。这个函数很关键,它决定了能否正常进入数据读取流程。
// DHT11复位信号
void DHT11_Reset(void)
{
DHT11_Output_Mode();
DHT11_DQ_OUT(0); // 拉低DQ,复位信号的开始
Delay_ms(20); // 拉低至少18ms
DHT11_DQ_OUT(1); // 将DHT11的DQ引脚拉高,结束复位信号
Delay_us(25); // 延时20-40us
}
// 检测DHT11是否存在
uint8_t DHT11_Check(void)
{
uint8_t retry = 0;
DHT11_Input_Mode();
// 等待DHT11拉低DQ引脚
while(DHT11_DQ_IN() && retry < 100)
{
retry++;
Delay_us(1);
}
if(retry >= 100) return 1;
else retry = 0;
// 等待DHT11拉高DQ引脚
while(!DHT11_DQ_IN() && retry < 100)
{
retry++;
Delay_us(1);
}
if(retry >= 100) return 1;
return 0;
}
数据读取函数: 读取数据位时,要注意时序的精确控制。每个数据位都以50us的低电平时隙开始,随后的高电平持续时间决定数据是0还是1(26-28us表示0,70us表示1)。
// 读取一个位
uint8_t DHT11_Read_Bit(void)
{
uint8_t retry = 0;
while(DHT11_DQ_IN() && retry < 100)
{
retry++;
Delay_us(1);
}
retry = 0;
while(!DHT11_DQ_IN() && retry < 100)
{
retry++;
Delay_us(1);
}
Delay_us(40); // 延时40us后检测电平
if(DHT11_DQ_IN()) return 1;
else return 0;
}
// 读取一个字节
uint8_t DHT11_ReadByte(void)
{
uint8_t i, Dat = 0;
for(i = 0; i < 8; i++)
{
Dat <<= 1;
Dat |= DHT11_Read_Bit();
}
return Dat;
}
完整数据读取函数: 最后是读取完整温湿度数据的函数,包括校验和验证。
// 读取温湿度数据
uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi)
{
uint8_t DHT11_Buff[5];
uint8_t i;
DHT11_Reset();
if(DHT11_Check() == 0)
{
for(i = 0; i < 5; i++)
{
DHT11_Buff[i] = DHT11_ReadByte();
}
// 校验数据
if(DHT11_Buff[0] + DHT11_Buff[1] + DHT11_Buff[2] + DHT11_Buff[3] == DHT11_Buff[4])
{
*humi = DHT11_Buff[0]; // 湿度整数
*temp = DHT11_Buff[2]; // 温度整数
return 0;
}
}
return 1;
}
在实际使用中,建议两次读取间隔至少1秒,因为DHT11的采样速率比较慢。如果连续读取,可能会得到错误数据。
4. OLED显示驱动实现
OLED显示屏采用I2C接口,驱动相对简单。我们需要实现初始化、清屏、显示字符和数字等基本功能。
I2C初始化: 首先配置I2C接口的GPIO和时钟。
// I2C初始化
void OLED_I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置PB6为SCL,PB7为SDA
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
OLED初始化序列: OLED屏幕需要发送一系列初始化命令才能正常工作。
// OLED初始化
void OLED_Init(void)
{
OLED_I2C_Init();
Delay_ms(100);
// 发送初始化命令序列
OLED_Write_Cmd(0xAE); // 关闭显示
OLED_Write_Cmd(0xD5); // 设置显示时钟分频比/振荡器频率
OLED_Write_Cmd(0x80);
OLED_Write_Cmd(0xA8); // 设置多路复用率
OLED_Write_Cmd(0x3F);
OLED_Write_Cmd(0xD3); // 设置显示偏移
OLED_Write_Cmd(0x00);
OLED_Write_Cmd(0x40); // 设置显示起始行
// ... 更多初始化命令
OLED_Write_Cmd(0xAF); // 开启显示
OLED_Clear(); // 清屏
}
显示函数实现: 实现字符和数字的显示函数,方便在主程序中调用。
// 在指定位置显示字符串
void OLED_ShowString(uint8_t x, uint8_t y, char *str)
{
uint8_t j = 0;
while(str[j] != '\0')
{
OLED_ShowChar(x, y, str[j]);
x += 8;
if(x > 120)
{
x = 0;
y += 2;
}
j++;
}
}
// 显示数字
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)
{
uint8_t t, temp;
uint8_t enshow = 0;
for(t = 0; t < len; t++)
{
temp = (num / OLED_Pow(10, len - t - 1)) % 10;
if(enshow == 0 && t < (len - 1))
{
if(temp == 0)
{
OLED_ShowChar(x + (t * 8), y, ' ');
continue;
}
else enshow = 1;
}
OLED_ShowChar(x + (t * 8), y, temp + '0');
}
}
这些显示函数封装好后,在主程序中调用就非常方便了。我可以直接指定显示位置和内容,不需要关心底层实现细节。
5. 主程序逻辑与系统集成
主程序负责协调各个模块的工作,实现完整的温湿度监测系统。下面是主函数的实现逻辑。
初始化部分: 系统上电后,先初始化各外设,然后检测DHT11是否存在。
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "DHT11.h"
uint8_t temperature, humidity;
int main(void)
{
// 初始化各模块
OLED_Init();
DHT11_Init();
// 检测DHT11是否存在
while(DHT11_Init())
{
// 显示错误信息
OLED_ShowString(1, 1, "NO DHT11");
Delay_ms(500);
OLED_ShowString(1, 1, " ");
Delay_ms(500);
}
// 清屏并显示表头
OLED_Clear();
OLED_ShowString(1, 1, "Temperature:");
OLED_ShowString(2, 1, "Humidity: %");
主循环部分: 在主循环中定期读取温湿度数据并更新显示。
// 主循环
while(1)
{
if(DHT11_ReadData(&temperature, &humidity) == 0)
{
// 显示温度值
OLED_ShowNum(1, 12, temperature, 2);
// 显示湿度值
OLED_ShowNum(2, 10, humidity, 2);
}
else
{
// 读取失败显示错误
OLED_ShowString(3, 1, "Read Error!");
}
Delay_ms(1000); // 每秒更新一次
}
}
错误处理机制: 在实际应用中,良好的错误处理机制很重要。我添加了超时重试机制,避免因偶尔的读取失败导致系统卡死。
// 带重试机制的数据读取
uint8_t DHT11_ReadData_WithRetry(uint8_t *temp, uint8_t *humi, uint8_t retry_count)
{
uint8_t i;
for(i = 0; i < retry_count; i++)
{
if(DHT11_ReadData(temp, humi) == 0)
{
return 0; // 读取成功
}
Delay_ms(100);
}
return 1; // 多次重试后仍然失败
}
在主循环中调用这个带重试机制的读取函数,系统会更加稳定可靠。
6. 常见问题与调试技巧
在实际项目中,我遇到过各种各样的问题,这里分享一些常见问题的解决方法。
DHT11读取失败: 这是最常见的问题。可能的原因和解决方法:
- 时序不准确:检查延时函数是否精确,特别是us级延时
- 上拉电阻缺失:确保DATA线有10kΩ上拉电阻
- 电源不稳定:DHT11对电源质量敏感,可以在VCC和GND之间加一个100nF电容
- 线缆过长:杜邦线不宜超过20cm,否则信号质量会下降
OLED不显示或显示异常:
- 检查电源:必须是3.3V,接5V会烧屏
- 检查I2C地址:常用地址是0x3C或0x78,有些屏幕需要调整电阻来设置地址
- 检查初始化序列:不同厂家的OLED可能需要不同的初始化命令
STM32无法下载程序:
- 检查Boot0和Boot1引脚:下载时Boot0接高电平,Boot1接低电平
- 检查ST-Link连接:SWDIO、SWCLK、GND必须连接正确
- 检查电源:确保STM32供电正常
调试技巧:
- 使用串口打印调试信息:在关键位置添加串口输出,帮助定位问题
- 用示波器观察波形:特别是DHT11的时序波形,可以直观看到通信是否正常
- 分模块调试:先单独测试OLED显示,再测试DHT11读取,最后整合
优化建议:
- 添加温度补偿:DHT11的精度一般,可以通过软件校准提高准确性
- 实现历史数据记录:添加SD卡模块,记录温湿度变化历史
- 设置报警阈值:当温湿度超过设定范围时,通过LED或蜂鸣器报警
- 低功耗优化:如果用于电池供电场景,可以优化为间歇工作模式
我在实际项目中发现,DHT11在极端环境下(高温高湿)精度会下降,如果需要更高精度的测量,可以考虑使用SHT30或DHT22等传感器。但对于大多数日常应用,DHT11已经完全够用了。
这个项目虽然看起来简单,但涵盖了嵌入式开发的多个重要知识点:GPIO控制、时序精确控制、I2C通信、外设驱动等。通过这个项目,我对STM32的理解更加深入了,特别是在时序控制方面积累了很多经验。
更多推荐



所有评论(0)