前言
DHT11 是一款可测量 温度 和 湿度 的传感器。比如市面上一些空气加湿器,会测量空气中湿度,再根据测量结果决定是否继续加湿。
一、DTH11 模块介绍
- DHT11通信过程:
主机通过一条数据线与DH11连接,主机通过这条线发命令给DHT11,DHT11再通过这条线把数据发送给主机。
当主机没有与DHT11通信时,总线处于空闲状态,此时总线电平由于上拉电阻的作用处于高电平。
- 主机将对应的GPIO管脚配置为输出,准备向DHT11发送数据;
- 主机发送一个开始信号:
开始信号 = 一个低脉冲 + 一个高脉冲。低脉冲至少持续18ms,高脉冲持续20-40us。 - 主机将对应的GPIO管脚配置为输入,准备接受DHT11传来的数据,这时信号由上拉电阻拉高;
- DHT11发出响应信号:
响应信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续80us,高脉冲持续80us。 - DHT11发出数据信号:
数据为0的一位信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续50us,高脉冲持续26~28us。
数据为1的一位信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续50us,高脉冲持续70us。
DHT11发出结束信号: - 最后1bit数据传送完毕后,DHT11拉低总线50us,然后释放总线,总线由上拉电阻拉高进入空闲状态。
- 数据格式:
8 bit 湿度整数数据 + 8 bit 湿度小数数据 + 8 bit 温度整数数据 + 8 bit 温度小数数据 + 8 bit 校验和。
(5 字节数据,共 40 位 )
数据传送正确时,校
- 验和等于“8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据”所得结果的末8位。
二、设备树设置
设备树 中 compatible 与 驱动程序 进行匹配。
将模块分别接到 开发板的 gpio4-19引脚。每一组 GPIO 有 32 个引脚。
配置设备树需要对 GPIO 引脚 以及相关的 pincontrol 配置。由于本实验是使用 DTH11 模块,所以不需要配置 pincontrol 。
三、驱动程序
- 根据框架编写基本驱动程序:
首先要 定义一个file_operations 结构体,在入口函数里对其 注册,在 出口函数里卸载。 实现辅助信息,使用 class_create 创建类 , device_create 创建设备节点。
定义一个 platform_driver。也是在入口函数里对其 注册,在 出口函数里卸载。
这些基本代码的详细实现可以参考我之前的文章 :SR501人体红外模块
- 实现 probe 函数。
总线处于空闲状态,此时总线电平由于上拉电阻的作用处于高电平。
static int dht11_probe(struct platform_device *pdev) { /* 获取引脚信息,将其设置为输出引脚高电平 */ dht11_data_pin = gpiod_get(&pdev->dev, NULL, GPIOD_OUT_HIGH); if (IS_ERR(dht11_data_pin)) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); } /* 创建设备节点 */ device_create(dht11_class, NULL, MKDEV(major, 0), NULL, "mydht11"); return 0; }
- 首先将 gpio 引脚设置为 输出引脚,准备向 DTH11 发送数据。
static void dht11_reset(void) { gpiod_direction_output(dht11_data_pin, 1); }
- 然后主机发送一个开始信号。
发送开始 信号完毕,将 引脚设置为 输入引脚,等待 DHT11 发送数据,准备接收数据。
static void dht11_start(void) { mdelay(30); gpiod_set_value(dht11_data_pin, 0); mdelay(20); gpiod_set_value(dht11_data_pin, 1); udelay(40); gpiod_direction_input(dht11_data_pin); udelay(2); }
- DHT11 发出响应信号,之后发送数据。
static int dht11_wait_for_ready(void) { int timeout_us = 20000; //设置超时时间 /* 等待低电平 */ while (gpiod_get_value(dht11_data_pin) && --timeout_us) { udelay(1); } if (!timeout_us) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -1; } /* 现在是低电平,等待高电平 */ timeout_us = 200; while (!gpiod_get_value(dht11_data_pin) && --timeout_us) { udelay(1); } if (!timeout_us) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -1; } /* 现在是高电平,等待低电平 */ timeout_us = 200; while (gpiod_get_value(dht11_data_pin) && --timeout_us) { udelay(1); } if (!timeout_us) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -1; } return 0; }
读一字节的数据。
每字节八位数据,按位读取。
怎么判断读出的数据是 0 还是 1 呢?
通过判断高电
- 平的持续时间可 得出写入的数据。当 高电平 持续26~28us,表示输出 0 。高脉冲持续70us,表明数据是 1 .
static int dht11_read_byte(unsigned char *buf) { int i; unsigned char data = 0; int timeout_us = 200; for (i = 0; i <8; i++) { /* 现在是低电平 */ /* 等待高电平 */ timeout_us = 400; while (!gpiod_get_value(dht11_data_pin) && --timeout_us) { udelay(1); } if (!timeout_us) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -1; } /* 现在是高电平 */ /* 等待低电平,累加高电平的时间 */ timeout_us = 20000000; udelay(40); if (gpiod_get_value(dht11_data_pin)) { /* get bit 1 */ data = (data << 1) | 1; /* 当前位的高电平未结束, 等待 */ timeout_us = 400; while (gpiod_get_value(dht11_data_pin) && --timeout_us) { udelay(1); } if (!timeout_us) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -1; } } else { /* get bit 0 */ data = (data << 1) | 0; } } *buf = data; return 0; }
- 读五字节数据。
将 读出的数据放入数组中,传递给应用程序。
static ssize_t dht11_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { unsigned long flags; int i,err; unsigned char data[5]; if (size != 4) return -EINVAL; local_irq_save(flags); // 关中断 /* 1. 发送高脉冲启动DHT11 */ dht11_reset(); dht11_start(); /* 2. 等待DHT11就绪 */ if (dht11_wait_for_ready()) { local_irq_restore(flags); // 恢复中断 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -EAGAIN; } /* 3. 读5字节数据 */ for (i = 0; i < 5; i++) { if (dht11_read_byte(&data[i])) { local_irq_restore(flags); // 恢复中断 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return -EAGAIN; } } dht11_reset(); local_irq_restore(flags); // 恢复中断 /* 4. 根据校验码验证数据 */ if (data[4] != (data[0] + data[1] + data[2] + data[3])) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); //return -1; } /* 5. copy_to_user */ /* data[0]/data[1] : 湿度 */ /* data[2]/data[3] : 温度 */ err = copy_to_user(buf, data, 4); return 4; }
四、测试程序
判断参数,打开文件,read 函数读出 测出的温度,湿度。
if (argc != 2) { printf("Usage: %s <dev>\n", argv[0]); return -1; } fd = open(argv[1], O_RDWR); if (fd == -1) { printf("can not open file %s\n", argv[1]); return -1; } while (1) { if (read(fd, data, 4) == 4) { printf("get humidity : %d.%d\n", data[0], data[1]); printf("get temprature: %d.%d\n", data[2], data[3]); } else { printf("get humidity/temprature: -1\n"); } sleep(5); // 等待 5 秒 } close(fd);
五、上机测试及效果
执行 insmod
命令可以将 .ko 文件加载到内核中,再 执行测试程序。(rmmod
命令可以卸载已加载的模块,lsmod
命令 可以观察已加载到内核的文件。)
/dev/mydht11 是 驱动程序中创建的设备节点( device_create )。