一、DHT11简介
DHT11是一款可测量温度和湿度的传感器。比如市面上一些空气加湿器,会测量空气中湿度,再根据测量结果决定是否继续加湿。
DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,具有超小体积、极低功耗的特点,使用单根总线与主机进行双向的串行数据传输。DHT11测量温度的精度为± 2℃,检测范围为-20℃ -60℃。湿度的精度为± 5%RH,检测范围为 5%RH-95%RH,常用于对精度和实时性要求不高的温湿度测量场合。
1.1 DHT11模块硬件设计
主机通过一条数据线与DH11连接,主机通过这条线发命令给DHT11,DHT11再通过这条线把数据发送给主机。
1.2 DHT11模块软件设计
DHT11的硬件电路比较简单,核心要点就是:主机发给DHT11的命令格式和DHT11返回的数据格式。
1.3 DHT11通讯协议
通讯过程如图所示:
当主机没有与 DHT11 通信时,总线处于空闲状态,此时总线电平由于上拉电阻的作用处于高电平。
当主机与 DHT11 正在通信时,总线处于通信状态,一次完整的通信过程如下:
a) 主机将对应的 GPIO 管脚配置为输出,准备向 DHT11 发送数据;
b)主机发送一个开始信号:开始信号 = 一个低脉冲 + 一个高脉冲。低脉冲至少持续 18ms,高脉冲持续 20-40us。
c) 主机将对应的 GPIO 管脚配置为输入,准备接受 DHT11 传来的数据,这时信号由上拉电阻拉高;
d) DHT11 发出响应信号:响应信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续 80us,高脉冲持续 80us。
e) DHT11 发出数据信号:
- 数据为 0 的一位信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续50us,高脉冲持续 26~28us。
- 数据为 1 的一位信号 = 一个低脉冲 + 一个高脉冲。低脉冲持续50us,高脉冲持续 70us。
f) DHT11 发出结束信号: 最后 1bit 数据传送完毕后, DHT11 拉低总线 50us,然后释放总线,总线由上拉电阻拉高进入空闲状态。
1.4 DHT11数据格式
8bit 湿度整数数据 + 8bit 湿度小数数据 + 8bi 温度整数数据 + 8bit 温度小数数据 + 8bit 校验和。数据传送正确时,校验和等于“8bit 湿度整数数据+8bit 湿度小数数据+8bi温度整数数据+8bit 温度小数数据”所得结果的末 8 位。
二、相关代码
2.1 驱动代码
#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "linux/jiffies.h"
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
struct gpio_desc{
int gpio;
int irq;
char *name;
int key;
struct timer_list key_timer;
} ;
static struct gpio_desc gpios[] = {
{115, 0, "dht11", },
};
/* 主设备号 */
static int major = 0;
static struct class *gpio_class;
static u64 g_dht11_irq_time[84];
static int g_dht11_irq_cnt = 0;
/* 环形缓冲区 */
#define BUF_LEN 128
static char g_keys[BUF_LEN];
static int r, w;
struct fasync_struct *button_fasync;
static irqreturn_t dht11_isr(int irq, void *dev_id);
static void parse_dht11_datas(void);
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void)
{
return (r == w);
}
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
static void put_key(char key)
{
if (!is_key_buf_full())
{
g_keys[w] = key;
w = NEXT_POS(w);
}
}
static char get_key(void)
{
char key = 0;
if (!is_key_buf_empty())
{
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);
// static void key_timer_expire(struct timer_list *t)
static void key_timer_expire(unsigned long data)
{
// 解析数据, 放入环形buffer, 唤醒APP
parse_dht11_datas();
}
/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t dht11_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
char kern_buf[2];
if (size != 2)
return -EINVAL;
g_dht11_irq_cnt = 0;
/* 1. 发送18ms的低脉冲 */
err = gpio_request(gpios[0].gpio, gpios[0].name);
gpio_direction_output(gpios[0].gpio, 0);
gpio_free(gpios[0].gpio);
mdelay(18);
/* 引脚变为输入方向, 由上拉电阻拉为1,*/
/* 当主机没有与DHT11通信时,总线处于空闲状态,
此时总线电平由于上拉电阻的作用处于高电平*/
gpio_direction_input(gpios[0].gpio);
/* 2. 注册中断 后面的语句执行时可能会导致前几次的中断丢失*/
err = request_irq(gpios[0].irq, dht11_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpios[0].name, &gpios[0]);
mod_timer(&gpios[0].key_timer, jiffies + 10);修改定时器的超时时间= jiffies(当前时间) + 10
/* 3. 休眠等待数据 */
//等待 条件为真(有数据时),才会被唤醒并执行后面语句
wait_event_interruptible(gpio_wait, !is_key_buf_empty());
free_irq(gpios[0].irq, &gpios[0]);
//printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 设置DHT11 GPIO引脚的初始状态: output 1 保险起见手动设置高电平
最后 1bit 数据传送完毕后, DHT11 拉低总线 50us,然后释放总线,总线由
上拉电阻拉高进入空闲状态*/
err = gpio_request(gpios[0].gpio, gpios[0].name);
if (err)
{
printk("%s %s %d, gpio_request err\n", __FILE__, __FUNCTION__, __LINE__);
}
gpio_direction_output(gpios[0].gpio, 1);
gpio_free(gpios[0].gpio);
/* 4. copy_to_user */
kern_buf[0] = get_key();
kern_buf[1] = get_key();
printk("get val : 0x%x, 0x%x\n", kern_buf[0], kern_buf[1]);
if ((kern_buf[0] == (char)-1) && (kern_buf[1] == (char)-1))
{
printk("get err val\n");
return -EIO;
}
err = copy_to_user(buf, kern_buf, 2);
return 2;
}
static int dht11_release (struct inode *inode, struct file *filp)
{
return 0;
}
/* 定义自己的file_operations结构体 */
static struct file_operations dht11_drv = {
.owner = THIS_MODULE,
.read = dht11_read,
.release = dht11_release,
};
//解析数据
static void parse_dht11_datas(void)
{
int i;
u64 high_time;
unsigned char data = 0;
int bits = 0;
unsigned char datas[5];//40位 5个字节
int byte = 0;
unsigned char crc;
/* 中断发生次数: 可能是81、82、83、84 */
if (g_dht11_irq_cnt < 81)
{
/* 出错 */
put_key(-1);
put_key(-1);
// 唤醒APP
wake_up_interruptible(&gpio_wait);
g_dht11_irq_cnt = 0;
return;
}
// 解析数据, 81、82、83、84
for (i = g_dht11_irq_cnt - 80; i < g_dht11_irq_cnt; i += 2)
{
high_time = g_dht11_irq_time[i] - g_dht11_irq_time[i-1];//高脉冲时间
data <<= 1;//左移一位
//50us = 50000ns
//如果数据是高电平
if (high_time > 50000) /* data 1 */
{
data |= 1;//或
}
bits++;
if (bits == 8)
{
datas[byte] = data;
data = 0;
bits = 0;
byte++;
}
}
// 放入环形buffer
crc = datas[0] + datas[1] + datas[2] + datas[3];
if (crc == datas[4])
{
put_key(datas[0]);
put_key(datas[2]);
}
else
{
put_key(-1);
put_key(-1);
}
g_dht11_irq_cnt = 0;
// 唤醒APP
wake_up_interruptible(&gpio_wait);
}
static irqreturn_t dht11_isr(int irq, void *dev_id)
{
struct gpio_desc *gpio_desc = dev_id;
u64 time;
/* 1. 记录中断发生的时间 */
time = ktime_get_ns();//单位ns 精准的到当前时间
//static u64 g_dht11_irq_time[84]; static int g_dht11_irq_cnt = 0;
g_dht11_irq_time[g_dht11_irq_cnt] = time;
/* 2. 累计次数 */
g_dht11_irq_cnt++;
/* 3. 次数足够: 解析数据, 放入环形buffer, 唤醒APP */
if (g_dht11_irq_cnt == 84)
{
del_timer(&gpio_desc->key_timer);
parse_dht11_datas();//解析数据
}
return IRQ_HANDLED;
}
/* 在入口函数 */
static int __init dht11_init(void)
{
int err;
int i;
int count = sizeof(gpios)/sizeof(gpios[0]);//count = 1
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
for (i = 0; i < count; i++)
{
gpios[i].irq = gpio_to_irq(gpios[i].gpio);
/* 设置DHT11 GPIO引脚的初始状态: output 1 */
err = gpio_request(gpios[i].gpio, gpios[i].name); //申请gpio
gpio_direction_output(gpios[i].gpio, 1); //输出方向,高电平
gpio_free(gpios[i].gpio); //释放引脚
//设置定时器:定时器结构体,定时器超时函数,传给超时函数的参数
setup_timer(&gpios[i].key_timer, key_timer_expire, (unsigned long)&gpios[i]);
//gpios[i].key_timer.expires = ~0;
//add_timer(&gpios[i].key_timer);
//err = request_irq(gpios[i].irq, dht11_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpios[i]);
}
/* 注册file_operations */
major = register_chrdev(0, "100ask_dht11", &dht11_drv); /* /dev/gpio_desc */
gpio_class = class_create(THIS_MODULE, "100ask_dht11_class");
if (IS_ERR(gpio_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_dht11");
return PTR_ERR(gpio_class);
}
device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "mydht11"); /* /dev/mydht11 */
return err;
}
/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit dht11_exit(void)
{
int i;
int count = sizeof(gpios)/sizeof(gpios[0]);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(gpio_class, MKDEV(major, 0));
class_destroy(gpio_class);
unregister_chrdev(major, "100ask_dht11");
for (i = 0; i < count; i++)
{
//free_irq(gpios[i].irq, &gpios[i]);
del_timer(&gpios[i].key_timer);
}
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE("GPL");
2.2 测试代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
static int fd;
/*
* ./button_test /dev/mydht11
*
*/
int main(int argc, char **argv)
{
char buf[2];
int ret;
int i;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR | O_NONBLOCK);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
while (1)
{
if (read(fd, buf, 2) == 2)
printf("get Humidity: %d, Temperature : %d\n", buf[0], buf[1]);
else
printf("get dht11: -1\n");
sleep(5);
}
close(fd);
return 0;
}
2.3 上板子测试
可以看到测到的温度和湿度数值都是正常的,但偶尔会测得失败的数据,这是因为在驱动程序里注册中断函数后,后面的语句执行花时间,可能会导致前几次的中断丢失。经测试如果中断发生次数小于81,则会测得错误数据;中断发生次数在81~84(含等于)就可测得准确数值。