STM32F103C8T6与DHT22温湿度传感器的实战开发指南

第一次接触嵌入式开发时,传感器数据采集总是让人既兴奋又忐忑。记得我刚开始用STM32读取DHT22数据时,因为时序问题折腾了整整两天。本文将分享一个完整的项目实践——用STM32F103C8T6驱动DHT22温湿度传感器,并通过OLED实时显示数据。不同于简单的代码展示,我会重点讲解硬件连接中的"坑点"、时序控制的精髓,以及如何优化显示效果。

1. 硬件准备与电路设计

1.1 元器件选型与特性分析

选择STM32F103C8T6作为主控有几个实际考虑:首先它价格亲民(约10元),其次72MHz的主频完全能满足传感器数据处理需求,最重要的是它丰富的GPIO和内置硬件I2C非常适合这个项目。

DHT22是项目中关键的传感器元件,与DHT11相比,它的精度更高:

  • 温度测量范围:-40~80℃ ±0.5℃
  • 湿度测量范围:0~100%RH ±2%RH
  • 单总线通信,节省IO资源

OLED显示屏我们选用0.96寸SSD1306驱动的I2C接口型号,对比SPI版本,它只需要4根线就能工作,特别适合引脚资源紧张的场景。

1.2 硬件连接详解

核心接线图如下:

设备引脚 STM32对应引脚 备注
DHT22 VCC 3.3V/5V 建议使用3.3V降低功耗
DHT22 GND GND 共地非常重要
DHT22 DATA PB9 需配置为上拉输入/推挽输出
OLED VCC 3.3V 超过3.3V可能损坏屏幕
OLED GND GND 必须与MCU共地
OLED SCL PB6 硬件I2C1_SCL
OLED SDA PB7 硬件I2C1_SDA

实际布线时,DHT22的信号线建议不超过20cm,过长的导线会导致信号衰减。我曾遇到因使用50cm杜邦线导致数据不稳定的情况,缩短线材后问题立即解决。

1.3 常见硬件问题排查

  • OLED不显示:首先检查I2C地址是否正确(通常是0x78或0x7A),用万用表测量SCL/SDA电压(正常应为3.3V脉冲)
  • DHT22无响应:确认上拉电阻(4.7KΩ)已接好,测量DATA线在空闲时的电压(应为高电平)
  • 数据跳动严重:检查电源是否稳定,可在VCC与GND之间加一个100μF电容

2. 开发环境搭建与工程配置

2.1 工具链选择

推荐两种开发环境方案:

方案一:Keil MDK

  1. 安装Keil uVision5(需注册)
  2. 安装STM32F1xx_DFP设备支持包
  3. 配置工程时勾选"Use MicroLIB"以减小代码体积

方案二:STM32CubeIDE

  1. 官网下载集成开发环境
  2. 新建工程时选择STM32F103C8系列
  3. 在.ioc配置文件中启用I2C1和外设时钟
// 关键时钟配置代码(CubeMX生成)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

2.2 工程文件结构

合理的文件组织能大幅提高开发效率:

/Project
  ├── /CMSIS              // 内核支持文件
  ├── /Drivers
  │    ├── OLED           // OLED驱动
  │    └── DHT22         // 温湿度传感器驱动
  ├── /Inc
  │    ├── main.h
  │    ├── oled.h
  │    └── dht22.h
  └── /Src
       ├── main.c
       ├── oled.c
       └── dht22.c

2.3 编译配置要点

  • 在"Options for Target"中设置正确的Flash下载算法(STM32F10x Medium-density)
  • 优化等级建议选择-O1,兼顾性能和调试便利性
  • 勾选"Create HEX File"以便烧录

3. DHT22驱动开发详解

3.1 单总线通信时序剖析

DHT22采用严格的单总线协议,时序误差必须小于20μs。下面是关键时序参数:

操作 时间要求 容错范围
主机拉低 ≥1ms 0.8-20ms
主机释放 20-40μs 必须严格
从机响应 80μs低电平 60-100μs
数据0 26-28μs高电平 ±5μs
数据1 70μs高电平 ±10μs
// 关键时序实现代码
void DHT22_Rst(void) {
    DHT22_IO_OUT();  // 设置为输出模式
    DHT22_DQ_OUT = 0;  // 拉低总线
    delay_ms(1);     // 保持1ms
    DHT22_DQ_OUT = 1; // 释放总线
    delay_us(30);    // 等待30μs
}

3.2 数据读取优化技巧

原始代码中的轮询方式会占用大量CPU资源,我们可以用定时器中断来改进:

  1. 配置一个基本定时器(如TIM4)
  2. 设置1μs的时间基准
  3. 在下降沿触发外部中断
  4. 在定时器中断中记录高电平持续时间
// 改进的数据位判断逻辑
uint8_t DHT22_Read_Bit(void) {
    uint16_t cnt = 0;
    while(DHT22_DQ_IN && cnt<200) cnt++; // 等待低电平
    cnt = 0;
    while(!DHT22_DQ_IN && cnt<200) cnt++; // 等待高电平
    delay_us(40); // 等待40μs
    return DHT22_DQ_IN ? 1 : 0;
}

3.3 数据校验与错误处理

DHT22传输的40位数据包含校验和,正确的处理流程应该是:

  1. 读取5字节原始数据(湿度高8位、湿度低8位、温度高8位、温度低8位、校验和)
  2. 计算前4字节的和
  3. 比对校验字节
uint8_t DHT22_Read_Data(uint16_t *temp, uint16_t *humi) {
    uint8_t buf[5], sum;
    // ...读取数据代码...
    sum = (buf[0] + buf[1] + buf[2] + buf[3]) & 0xFF;
    if(sum == buf[4]) { // 校验通过
        *humi = buf[0]<<8 | buf[1];
        *temp = buf[2]<<8 | buf[3];
        return 0;
    }
    return 1; // 校验失败
}

4. OLED显示优化实战

4.1 I2C驱动深度优化

标准的I2C库函数调用开销较大,我们可以直接操作寄存器提升速度:

void I2C_WriteByte(uint8_t addr, uint8_t data) {
    while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
    I2C_GenerateSTART(I2C1, ENABLE);
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
    I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    I2C_SendData(I2C1, data);
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    I2C_GenerateSTOP(I2C1, ENABLE);
}

4.2 显示缓存管理技巧

SSD1306支持显存(GRAM)操作,合理利用可以大幅减少I2C通信:

  1. 定义128x64的显示缓存数组
  2. 修改像素点时只更新数组
  3. 定期调用刷新函数统一更新屏幕
uint8_t OLED_GRAM[128][8]; // 每字节存储8个垂直像素

void OLED_Refresh(void) {
    for(uint8_t page=0; page<8; page++) {
        I2C_WriteByte(0x00, 0xB0+page); // 设置页地址
        I2C_WriteByte(0x00, 0x00); // 列地址低4位
        I2C_WriteByte(0x00, 0x10); // 列地址高4位
        for(uint8_t col=0; col<128; col++) {
            I2C_WriteByte(0x40, OLED_GRAM[col][page]);
        }
    }
}

4.3 用户界面设计建议

  • 温度显示:使用16x16大字体突出关键数据
  • 湿度显示:添加百分比符号和小数点
  • 状态指示:当温度超过阈值时显示警告图标
  • 刷新策略:每2秒更新一次数据,避免屏幕闪烁
void Display_TempHum(uint16_t temp, uint16_t humi) {
    char buf[16];
    // 温度显示
    sprintf(buf, "%2d.%1d C", temp/10, temp%10);
    OLED_ShowStr(20, 2, buf, 16, 1);
    // 湿度显示
    sprintf(buf, "%2d.%1d %%", humi/10, humi%10);
    OLED_ShowStr(20, 5, buf, 16, 1);
    // 添加单位标签
    OLED_ShowStr(0, 2, "Temp:", 16, 1);
    OLED_ShowStr(0, 5, "Humi:", 16, 1);
}

5. 系统整合与性能优化

5.1 主循环架构设计

避免在main函数中使用阻塞式延迟,采用状态机模式更高效:

typedef enum {
    STATE_READ_SENSOR,
    STATE_UPDATE_DISPLAY,
    STATE_DELAY
} SystemState;

SystemState state = STATE_READ_SENSOR;
uint32_t lastTick = 0;

while(1) {
    switch(state) {
        case STATE_READ_SENSOR:
            if(DHT22_Read_Data(&temperature, &humidity) == 0) {
                state = STATE_UPDATE_DISPLAY;
            }
            break;
        case STATE_UPDATE_DISPLAY:
            Display_TempHum(temperature, humidity);
            lastTick = HAL_GetTick();
            state = STATE_DELAY;
            break;
        case STATE_DELAY:
            if(HAL_GetTick() - lastTick >= 2000) {
                state = STATE_READ_SENSOR;
            }
            break;
    }
}

5.2 低功耗优化策略

  1. 在延迟期间将MCU切换到Sleep模式
  2. 降低系统时钟频率(从72MHz降到36MHz)
  3. 关闭未使用的外设时钟
  4. 间隔式刷新OLED(如每5秒刷新一次)
void Enter_LowPowerMode(void) {
    __WFI(); // 等待中断唤醒
    SystemCoreClockUpdate(); // 唤醒后重新配置时钟
}

5.3 抗干扰设计

  • 在DHT22数据线加10KΩ上拉电阻
  • VCC与GND之间并联0.1μF和100μF电容
  • 避免将传感器放置在发热元件附近
  • 软件上采用多次读取取平均值的策略
#define SAMPLE_TIMES 3

uint16_t Get_Average_Temperature(void) {
    uint32_t sum = 0;
    uint16_t temp, humi;
    uint8_t valid = 0;
    
    for(uint8_t i=0; i<SAMPLE_TIMES; i++) {
        if(DHT22_Read_Data(&temp, &humi) == 0) {
            sum += temp;
            valid++;
        }
        delay_ms(100);
    }
    
    return valid ? (sum/valid) : 0xFFFF;
}

6. 进阶功能扩展

6.1 添加历史数据记录

利用STM32内部Flash模拟EEPROM存储历史数据:

#define FLASH_PAGE_ADDR 0x0801F000 // 最后一页Flash

void Save_To_Flash(uint16_t temp, uint16_t humi) {
    FLASH_Unlock();
    FLASH_ErasePage(FLASH_PAGE_ADDR);
    FLASH_ProgramHalfWord(FLASH_PAGE_ADDR, temp);
    FLASH_ProgramHalfWord(FLASH_PAGE_ADDR+2, humi);
    FLASH_Lock();
}

void Read_From_Flash(uint16_t *temp, uint16_t *humi) {
    *temp = *(__IO uint16_t*)FLASH_PAGE_ADDR;
    *humi = *(__IO uint16_t*)(FLASH_PAGE_ADDR+2);
}

6.2 通过串口输出数据

方便调试和数据采集:

void USART_Send_TempHum(uint16_t temp, uint16_t humi) {
    char buffer[64];
    sprintf(buffer, "Temperature: %d.%d C, Humidity: %d.%d %%\r\n", 
            temp/10, temp%10, humi/10, humi%10);
    for(uint16_t i=0; i<strlen(buffer); i++) {
        USART_SendData(USART1, buffer[i]);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    }
}

6.3 添加蓝牙传输功能

使用HC-05模块实现无线监控:

  1. 连接蓝牙模块的TXD到STM32的USART2_RX
  2. 连接蓝牙模块的RXD到STM32的USART2_TX
  3. 修改串口初始化代码
  4. 手机安装蓝牙串口APP即可接收数据
void Bluetooth_Init(void) {
    USART_InitTypeDef USART_InitStruct;
    // 时钟使略...
    USART_InitStruct.USART_BaudRate = 9600;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART2, &USART_InitStruct);
    USART_Cmd(USART2, ENABLE);
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐