在嵌入式开发中,温湿度传感器DHT11因其简单易用、成本低廉而广受欢迎。然而,当开发者尝试在树莓派4B上通过GPIO口直接驱动DHT11时,往往会遇到数据读取不稳定、时序控制不准确等问题。本文将深入剖析DHT11的通信协议细节,结合树莓派4B的GPIO寄存器操作,提供一份从原理到实践的完整指南。
DHT11是一款数字式温湿度复合传感器,采用单总线通信协议。理解其工作原理是成功驱动的关键。
DHT11通常有4个引脚(部分型号为3引脚),但实际只使用3个:
电气特性:
DHT11的通信过程分为三个阶段:主机启动信号、传感器响应和数据传输。
关键时序参数:
注意:所有时间参数都必须严格控制在规定范围内,特别是μs级的时间控制,偏差超过±10μs就可能导致读取失败。
树莓派4B的GPIO控制器提供了丰富的寄存器接口,直接操作这些寄存器可以实现精确的时序控制。
树莓派4B的主要GPIO相关寄存器:
以GPIO25为例(BCM编号),其寄存器操作如下:
c复制#define GPIO25_MASK (1 << 25)
// 设置为输出
*GPFSEL2 |= (0x1 << 15);
// 设置为输入
*GPFSEL2 &= ~(0x7 << 15);
// 输出高电平
*GPSET0 = GPIO25_MASK;
// 输出低电平
*GPCLR0 = GPIO25_MASK;
// 读取电平
int level = (*GPLEV0 >> 25) & 1;
在Linux内核模块中,常用的延时函数有:
ndelay(ns):纳秒级延时udelay(us):微秒级延时mdelay(ms):毫秒级延时对于DHT11驱动,主要使用udelay()实现微秒级延时。需要注意的是,这些延时函数是忙等待,会占用CPU资源。
下面是一个完整的DHT11内核模块驱动实现,包含详细的注释和调试技巧。
c复制#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#define DHT11_GPIO 25 // 使用GPIO25
static int __init dht11_init(void)
{
// 检查GPIO是否可用
if (!gpio_is_valid(DHT11_GPIO)) {
printk(KERN_ERR "Invalid GPIO\n");
return -EINVAL;
}
// 申请GPIO
if (gpio_request(DHT11_GPIO, "DHT11") < 0) {
printk(KERN_ERR "Failed to request GPIO\n");
return -EBUSY;
}
return 0;
}
static void __exit dht11_exit(void)
{
gpio_free(DHT11_GPIO);
}
module_init(dht11_init);
module_exit(dht11_exit);
c复制// GPIO方向设置
static void dht11_set_output(void)
{
gpio_direction_output(DHT11_GPIO, 1);
}
static void dht11_set_input(void)
{
gpio_direction_input(DHT11_GPIO);
}
// 读取一位数据
static int dht11_read_bit(void)
{
int retry = 1000;
// 等待低电平开始
while (gpio_get_value(DHT11_GPIO) && retry--)
udelay(1);
if (retry <= 0) return -1;
retry = 1000;
// 等待高电平开始
while (!gpio_get_value(DHT11_GPIO) && retry--)
udelay(1);
if (retry <= 0) return -1;
// 延时40μs后读取电平
udelay(40);
return gpio_get_value(DHT11_GPIO);
}
// 读取一个字节
static int dht11_read_byte(void)
{
int i, byte = 0;
for (i = 0; i < 8; i++) {
int bit = dht11_read_bit();
if (bit < 0) return -1;
byte = (byte << 1) | bit;
}
return byte;
}
// 读取温湿度数据
static int dht11_read_data(unsigned char *humidity, unsigned char *temperature)
{
unsigned char data[5] = {0};
int i, retry = 10000;
// 发送开始信号
dht11_set_output();
gpio_set_value(DHT11_GPIO, 0);
mdelay(20); // 拉低至少18ms
gpio_set_value(DHT11_GPIO, 1);
udelay(30); // 拉高20-40μs
// 等待响应
dht11_set_input();
while (gpio_get_value(DHT11_GPIO) && retry--)
udelay(1);
if (retry <= 0) return -1;
retry = 10000;
while (!gpio_get_value(DHT11_GPIO) && retry--)
udelay(1);
if (retry <= 0) return -1;
// 读取40位数据
for (i = 0; i < 5; i++) {
data[i] = dht11_read_byte();
if (data[i] < 0) return -1;
}
// 校验和检查
if (data[4] != (data[0] + data[1] + data[2] + data[3])) {
return -1;
}
*humidity = data[0];
*temperature = data[2];
return 0;
}
在实际开发中,可能会遇到各种问题。以下是几个常见问题及其解决方案。
可能原因:
解决方案:
常见错误:
调试方法:
bash复制# 查看GPIO使用情况
cat /sys/kernel/debug/gpio
# 查看内核日志
dmesg | tail -n 20
减少中断延迟:
精确延时替代方案:
为了方便应用程序读取数据,我们可以通过字符设备或sysfs接口暴露传感器数据。
c复制static ssize_t dht11_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
unsigned char humidity, temperature;
char data[4];
if (dht11_read_data(&humidity, &temperature) < 0)
return -EIO;
data[0] = humidity;
data[1] = temperature;
if (copy_to_user(buf, data, 2))
return -EFAULT;
return 2;
}
static struct file_operations dht11_fops = {
.owner = THIS_MODULE,
.read = dht11_read,
};
static int __init dht11_init(void)
{
// ...之前的初始化代码...
// 注册字符设备
alloc_chrdev_region(&devno, 0, 1, "dht11");
cdev_init(&dht11_cdev, &dht11_fops);
cdev_add(&dht11_cdev, devno, 1);
return 0;
}
c复制#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd;
char data[2];
fd = open("/dev/dht11", O_RDONLY);
if (fd < 0) {
perror("open");
return -1;
}
if (read(fd, data, 2) != 2) {
perror("read");
close(fd);
return -1;
}
printf("Humidity: %d%%, Temperature: %dC\n", data[0], data[1]);
close(fd);
return 0;
}
在实际项目中,我发现最关键的还是时序控制的精确性。特别是在树莓派这种非实时系统上,使用udelay()实现的微秒级延时可能会受到系统调度的影响。如果对精度要求极高,建议考虑使用硬件定时器或者换用I2C/SPI接口的传感器。