基于GD32E230C8T6的DHT11单总线温湿度传感器驱动移植与数据解析实战
基于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.c 和 bsp_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附近,比如26、27、29、30都试试。
5. 电源与上拉电阻 DHT11模块通常自带了一个4.7K或5.1K的上拉电阻。如果通信距离较长(比如超过1米),信号质量可能会变差,可以考虑在MCU端再加一个上拉电阻。同时确保电源稳定,纹波小。
按照上面的步骤,你应该就能在立创GD32E230C8T6开发板上稳定地读取DHT11的温湿度数据了。整个代码结构清晰,关键点都有注释,你可以直接拿去用到你的项目里。遇到问题,多看看波形,耐心调整一下时序参数,祝你好运!
更多推荐


所有评论(0)