基于GD32E230C8T6的DHT11单总线温湿度传感器驱动移植与数据解析实战

最近在做一个环境监测的小项目,需要用到温湿度传感器,DHT11这个老朋友因为价格便宜、接口简单,自然成了首选。正好手头有立创的GD32E230C8T6开发板,就想着把DHT11的驱动移植上去。整个过程下来,发现单总线协议虽然简单,但时序要求严格,稍不注意数据就读取失败。今天我就把从时序分析、引脚配置到数据读取、校验处理的完整过程,手把手分享给大家,让你也能在自己的GD32E230项目里快速用上DHT11。

1. 认识我们的“搭档”:DHT11温湿度传感器

在开始写代码之前,咱们先花几分钟了解一下DHT11到底是个啥,以及它怎么和我们“说话”。

DHT11是一款数字输出的温湿度复合传感器。它最大的特点就是成本低、稳定性好,而且通信接口极其简单,只需要一根数据线(外加电源和地线)就能完成数据采集,这就是所谓的“单总线”通信。

它的基本性能参数如下:

参数规格
工作电压3.0V - 5.5V
工作电流约1mA
测量分辨率8位
湿度量程20% - 90% RH
湿度精度±5% RH
温度量程0℃ - 50℃
温度精度±2℃
通信协议单总线
接口3Pin (VCC, DATA, GND)

注意:DHT11的湿度小数部分输出恒为0,温度小数部分则有实际数据。所以它的湿度输出是整数,温度输出可以带一位小数。

它和我们单片机(MCU)通信,靠的是一套严格的时序规则。你可以把单总线想象成一条双向单车道的乡村小路,主机(MCU)和从机(DHT11)要轮流使用这条路,并且严格遵守“谁先走、谁让行、走多久”的交通规则,否则就会“撞车”(通信失败)。接下来,我们就来详细解读这份“交通规则”。

2. 核心:读懂DHT11的“语言”——单总线时序

驱动DHT11,最关键的就是严格按照它的时序图来操作。一次完整的通信分为四个阶段:起始信号、响应信号、数据传输、结束信号

2.1 通信四部曲

第一步:主机发起聊天(起始信号) MCU先把数据线(DATA)拉低,这个低电平必须保持至少18毫秒。然后MCU释放总线(拉高),并等待大约20-40微秒。这个长低电平就像是MCU对DHT11大喊一声:“喂!醒醒,我要读数据了!”

第二步:DHT11回应(响应信号) DHT11听到“呼喊”后,会先把数据线拉低大约80微秒作为应答,意思是“我听到了”,然后再拉高80微秒,表示“我准备好发数据了”。MCU这边需要把引脚模式从输出切换为输入,然后检测这个低-高的跳变。

第三步:DHT11发送数据(数据传输) DHT11接着会发送40位数据。每一位数据都以一个54微秒的低电平开始,然后是一个高电平。区分数据0和1,就看这个高电平的持续时间

  • 数据0:高电平持续约 27微秒
  • 数据1:高电平持续约 70微秒

所以,MCU在检测到低电平结束后,等待一小段时间(比如28微秒),再去采样数据线电平。如果还是高电平,那就是1;如果已经变低,那就是0

第四步:结束通信(结束信号) DHT11发送完40位数据后,会输出一个54微秒的低电平,然后释放总线,切换回输入状态。MCU这边也应该把引脚模式切换回输出,并输出高电平,让总线恢复到空闲状态。

2.2 数据格式与校验

DHT11发送的40位数据不是乱码,它有固定的格式:

数据段位数说明
湿度整数8位湿度值的整数部分(%)
湿度小数8位恒为0
温度整数8位温度值的整数部分(℃)
温度小数8位温度值的小数部分(℃)
校验和8位前32位数据相加和的低8位

校验和是防止数据在传输过程中出错的关键。它的计算方法是: 校验和 = 湿度高8位 + 湿度低8位 + 温度高8位 + 温度低8位(只取结果的低8位)。

MCU收到40位数据后,需要自己按这个公式算一遍校验和,然后和收到的第5个字节(校验和字节)进行比较。如果相等,说明数据基本可靠;如果不相等,这次数据就应该丢弃,重新读取。

3. 动手移植:为GD32E230编写驱动代码

理论清楚了,咱们就开始写代码。我习惯把传感器驱动放在独立的文件里,这里我们创建 bsp_dht11.cbsp_dht11.h

3.1 硬件连接与宏定义

首先,确定你把DHT11模块的哪根线接到了开发板的哪个引脚。我这里以连接到 PB0 为例。在 bsp_dht11.h 中,我们进行引脚和端口的宏定义,这样以后换引脚改起来也方便。

#ifndef _BSP_DHT11_H_
#define _BSP_DHT11_H_

#include "gd32e23x.h"

/************** 引脚配置(根据你的实际接线修改)****************/
#define RCU_DHT11   RCU_GPIOB      // 对应GPIOB的时钟
#define PORT_DHT11  GPIOB          // 端口
#define GPIO_DHT11  GPIO_PIN_0     // 引脚号,这里是PB0

// 设置DHT11数据引脚输出高或低电平的宏
#define DATA_GPIO_OUT(x)    gpio_bit_write(PORT_DHT11, GPIO_DHT11, (x)?SET:RESET)
// 获取DHT11数据引脚输入电平状态的宏
#define DATA_GPIO_IN        gpio_input_bit_get(PORT_DHT11, GPIO_DHT11)

// 声明全局变量,用于存储读取到的温湿度值
extern float temperature;
extern float humidity;

// 函数声明
void DHT11_GPIO_Init(void);
unsigned int DHT11_Read_Data(void);
float Get_temperature(void);
float Get_humidity(void);

#endif

3.2 引脚模式切换函数

DHT11通信要求MCU的同一个引脚能在输出和输入模式间快速切换。我们写两个辅助函数来实现这个功能。

bsp_dht11.c 中:

#include "bsp_dht11.h"
#include "systick.h" // 需要用到延时函数,请确保你的工程里有
#include "stdio.h"

float temperature = 0; // 全局温度变量
float humidity = 0;    // 全局湿度变量

/**
  * @brief  DHT11 GPIO初始化
  * @note   上电后调用一次,配置引脚初始状态为输出高电平(总线空闲状态)
  */
void DHT11_GPIO_Init(void)
{
    // 开启GPIOB的时钟
    rcu_periph_clock_enable(RCU_DHT11);
    // 配置为输出模式,上拉
    gpio_mode_set(PORT_DHT11, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_DHT11);
    // 配置为推挽输出,速度50MHz
    gpio_output_options_set(PORT_DHT11, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_DHT11);
    // 初始输出高电平,释放总线
    gpio_bit_set(PORT_DHT11, GPIO_DHT11);
}

/**
  * @brief  配置DHT11数据引脚为输出模式
  */
void DHT11_GPIO_Mode_OUT(void)
{
    gpio_mode_set(PORT_DHT11, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_DHT11);
    gpio_output_options_set(PORT_DHT11, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_DHT11);
}

/**
  * @brief  配置DHT11数据引脚为输入模式
  */
void DHT11_GPIO_Mode_IN(void)
{
    gpio_mode_set(PORT_DHT11, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_DHT11);
}

3.3 核心数据读取函数

这是整个驱动的灵魂,严格按照我们前面分析的时序来写。我加上了详细的注释,你可以对照着时序图看。

/**
  * @brief  读取DHT11的温湿度数据
  * @retval 0: 读取或校验失败
  *        其他: 返回未处理的40位数据中的前32位(即温湿度原始数据)
  */
unsigned int DHT11_Read_Data(void)
{
    int i;
    long long val = 0; // 用于存储40位数据,用64位变量防止溢出
    int timeout = 0;
    float small_point = 0;
    unsigned char verify_num = 0;

    /* 1. 主机发送起始信号 */
    DATA_GPIO_OUT(0);      // 拉低数据线
    delay_ms(19);          // 保持低电平至少18ms,这里给19ms留点余量
    DATA_GPIO_OUT(1);      // 释放总线,拉高
    delay_us(20);          // 等待20us

    /* 2. 等待DHT11的响应信号 */
    DHT11_GPIO_Mode_IN();  // 主机切换为输入模式,准备读取

    // 等待DHT11拉低(80us响应信号)
    timeout = 5000;
    while((!DATA_GPIO_IN) && (timeout > 0)) timeout--;
    if(timeout <= 0) return 0; // 超时,说明DHT11没响应

    // 等待DHT11拉高(80us准备信号)
    timeout = 5000;
    while(DATA_GPIO_IN && (timeout > 0)) timeout--;
    if(timeout <= 0) return 0; // 超时

    /* 3. 开始接收40位数据 */
    #define CHECK_TIME 28 // 这个值很关键!用于区分0和1,实测调整
    for(i = 0; i < 40; i++)
    {
        timeout = 5000;
        // 等待每一位开始的低电平(54us)结束
        while((!DATA_GPIO_IN) && (timeout > 0)) timeout--;
        if(timeout <= 0) return 0; // 超时

        // 等待超过数据0的高电平时间(27us)
        delay_us(CHECK_TIME);

        // 在CHECK_TIME时间点采样,判断是0还是1
        if(DATA_GPIO_IN) // 如果还是高电平,说明是1(高电平持续70us)
        {
            val = (val << 1) + 1;
        }
        else // 已经变低电平,说明是0(高电平持续27us)
        {
            val = val << 1;
        }

        // 等待当前位的高电平结束,准备接收下一位
        timeout = 5000;
        while(DATA_GPIO_IN && (timeout > 0)) timeout--;
    }

    /* 4. 通信结束,主机拉高总线 */
    DHT11_GPIO_Mode_OUT();
    DATA_GPIO_OUT(1);

    /* 5. 校验数据 */
    // 计算前32位数据的和(湿度高8+湿度低8+温度高8+温度低8)
    verify_num = (val >> 32) + ((val >> 24) & 0xFF) + ((val >> 16) & 0xFF) + ((val >> 8) & 0xFF);
    // 减去接收到的校验和(val的低8位)
    verify_num = verify_num - (val & 0xFF);

    if(verify_num != 0) // 校验和不匹配
    {
        return 0;
    }
    else // 校验成功,解析数据
    {
        // 解析湿度(整数部分 + 小数部分,湿度小数恒为0)
        humidity = (val >> 32) & 0xFF;          // 湿度整数
        small_point = (val >> 24) & 0xFF;       // 湿度小数
        small_point = small_point * 0.1;        // 转换为小数
        humidity = humidity + small_point;

        // 解析温度(整数部分 + 小数部分)
        temperature = (val >> 16) & 0xFF;       // 温度整数
        small_point = (val >> 8) & 0xFF;        // 温度小数
        small_point = small_point * 0.1;        // 转换为小数
        temperature = temperature + small_point;

        // 返回未处理的原始数据(前32位),方便调试查看
        return (unsigned int)(val >> 8);
    }
}

关键点提示CHECK_TIME 这个延时值(28us)是区分数据0和1的阈值。它需要大于数据0的高电平时间(27us),小于数据1的高电平时间(70us)。这个值可能因MCU主频、编译优化等因素有微小差异,如果数据读取不稳定,可以微调这个值(比如26-30us之间尝试)。

3.4 数据获取函数

为了方便使用,我们再封装两个简单的函数来获取解析好的温湿度值。

/**
  * @brief  获取温度值
  * @retval 温度值(摄氏度)
  * @note   必须在调用 DHT11_Read_Data() 成功后使用
  */
float Get_temperature(void)
{
    return temperature;
}

/**
  * @brief  获取湿度值
  * @retval 湿度值(百分比)
  * @note   必须在调用 DHT11_Read_Data() 成功后使用
  */
float Get_humidity(void)
{
    return humidity;
}

4. 在main函数中测试驱动

驱动写好了,我们来写个简单的main函数测试一下。记得在工程里添加好串口初始化代码(用于打印数据)和延时函数。

#include "gd32e23x.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_dht11.h"
#include "bsp_usart.h" // 假设你的串口驱动文件叫这个

int main(void)
{
    // 系统时钟和滴答定时器初始化
    systick_config();
    // 串口初始化,用于打印数据,波特率115200
    usart_gpio_config(115200);

    // DHT11引脚初始化
    DHT11_GPIO_Init();

    delay_ms(1000); // 上电后给DHT11一点稳定时间
    printf("DHT11 Demo Start\r\n");

    while(1)
    {
        // 读取一次DHT11数据
        if(DHT11_Read_Data() != 0) // 如果读取成功
        {
            // 打印温度数据,保留两位小数
            printf("Temperature = %.2f C\r\n", Get_temperature());
            // 打印湿度数据,保留一位小数(DHT11湿度小数位为0)
            printf("Humidity = %.1f %%RH\r\n", Get_humidity());
        }
        else
        {
            printf("DHT11 Read Error!\r\n");
        }

        // 每隔1秒读取一次
        delay_ms(1000);
    }
}

5. 调试心得与常见问题

移植完成后,你可能一次就成功了,也可能遇到数据全是0或者校验失败。这里分享几个我调试时踩过的坑:

1. 时序精度问题 GD32E230的主频是72MHz,一条指令的执行时间很短。delay_us() 函数的精度直接影响通信成败。务必确保你的微秒延时函数是准确的。可以用逻辑分析仪或者示波器抓一下数据线的波形,对照DHT11的时序图看时间对不对。

2. 引脚模式切换时机 在发送起始信号后,一定要在释放总线、拉高电平之后,把引脚模式从输出切换到输入。如果切换早了,可能会影响起始信号的波形。

3. 响应信号检测超时 代码里用了 while 循环加超时判断来等待DHT11的响应。如果DHT11没接好、电源不对或者已经损坏,程序会卡在循环里。加上超时判断并返回错误,能让程序更健壮。

4. CHECK_TIME的微调 这是最容易出问题的地方。如果这个值太接近27us,环境稍有干扰就可能把0误判为1;如果太接近70us,又可能把1误判为0。如果你发现数据偶尔不对,或者校验经常失败,优先调整这个参数。可以在28附近,比如26272930都试试。

5. 电源与上拉电阻 DHT11模块通常自带了一个4.7K或5.1K的上拉电阻。如果通信距离较长(比如超过1米),信号质量可能会变差,可以考虑在MCU端再加一个上拉电阻。同时确保电源稳定,纹波小。

按照上面的步骤,你应该就能在立创GD32E230C8T6开发板上稳定地读取DHT11的温湿度数据了。整个代码结构清晰,关键点都有注释,你可以直接拿去用到你的项目里。遇到问题,多看看波形,耐心调整一下时序参数,祝你好运!

更多推荐