从0开始学linux韦东山教程Linux驱动入门实验班(7)

发布于:2025-08-01 ⋅ 阅读:(17) ⋅ 点赞:(0)

  本人从0开始学习linux,使用的是韦东山的教程,在跟着课程学习的情况下的所遇到的问题的总结,理论虽枯燥但是是基础。本人将前几章的内容大致学完之后,考虑到后续驱动方面得更多的开始实操,后续的内容将以韦东山教程Linux驱动入门实验班的内容为主,学习其中的代码并手敲。做到锻炼动手能力的同时钻研其中的理论知识点。
摘要:DHT11这篇文章是我第一次结合视频靠自己去分析原理图去撰写驱动代码
摘要关键词:DHT11

1.DHT11原理分析

在这里插入图片描述
这段引脚的说明本人是从DHT11 产品手册截取出来的。

在这里插入图片描述
手册中写道当你使用此模块的时候一般是推荐接一个4.7K上拉电阻的,多数模块是配有这个上拉电阻的。
在这里插入图片描述
手册中写道数据为40位,以及数据的构成。
在这里插入图片描述
手册中是这么写的,单片机起始信号先拉低至少18ms,然后单片机会接收到传感器发来的拉低83us,再拉高87us。然后发送40位数据。

在这里插入图片描述
其实你从它的示例中看的很清楚,校验位是由这几个数相加得到的。具体的计算步骤也很容易,就是将整数计算得到整数,小数计算得到小数。
在这里插入图片描述
手册中的这张图很抽象?我就直说吧,不管它。手册里面将单片机要干的事情写的很清楚。
步骤一:
DHT11上电后(DHT11上电后要等待1S以越过不稳定状态在此期间不能发送任何指令),测
试环境温湿度数据,并记录数据,同时DHT11的DATA数据线由上拉电阻拉高一直保持高电平;
此时DHT11的DATA引脚处于输入状态,时刻检测外部信号。

步骤二:
微处理器的I/O设置为输出同时输出低电平,且低电平保持时间不能小于18ms(最大不得
超过30ms),然后微处理器的I/O设置为输入状态,由于上拉电阻,微处理器的I/O即DHT11的
DATA数据线也随之变高,等待DHT11作出回答信号。发送信号如图4所示:
在这里插入图片描述

按道理来说,引脚应该设置成输入模式,不应该是输出模式输出高电平。输入模式理论上也有高低电平,这是stm32能做到的。后来发现linux不行,只能设置输出高低电平或者输入读取。

步骤三:
DHT11的DATA引脚检测到外部信号有低电平时,等待外部信号低电平结束,延迟后DHT11的
DATA引脚处于输出状态,输出83微秒的低电平作为应答信号,紧接着输出87微秒的高电平通知
外设准备接收数据,微处理器的I/O此时处于输入状态,检测到I/O有低电平(DHT11回应信号)
后,等待87微秒的高电平后的数据接收,发送信号如图5所示:
在这里插入图片描述
说直白一点就是模块在这发消息让你接告诉你,等会会发数据给你了。

步骤四:
由DHT11的DATA引脚输出40位数据,微处理器根据I/O电平的变化接收40位数据,
位数据“0”的格式为:54微秒的低电平和23-27微秒的高电平
位数据“1”的格式为:54微秒的低电平加68-74微秒的高电平。
位数据“0”、“1”格式信号如图6所示:
在这里插入图片描述
这里告诉你了数据中的0,1是怎么构造的。

结束信号:

DHT11的DATA引脚输出40位数据后,继续输出低电平54微秒后转为输入状态,由于上拉电
阻随之变为高电平。但DHT11内部重测环境温湿度数据,并记录数据,等待外部信号的到来。

也就是你叫我一次,我告诉你一次。54微秒后转为输入状态,别忘了你是接了一个上拉电阻的,所以输入状态变为1了。
小结:通过以上你应该明白,怎么叫醒dht11了,叫醒后它会回答一个“到!”,然后它就给你汇报它的内容,汇报的内容格式你也知道怎么处理了。汇报完成后它会回复一个”汇报结束!”这就是以上它的工作原理了。根据原理设计驱动程序。

驱动程序设计

首先驱动程序设计得先制定思路,板子只要3个引脚VCC,GND,GPIO4_19,设置为输入模式。需要用引脚的边缘触发去读取信息,也就需要环形缓冲区去处理数据。可能需要定时器中断,当我异常阻塞的时候,当作看门狗跳出程序。以上就是大致思路。
84的计算由来:

DHT11通信协议时序:
起始信号后DHT11的响应:
80μs低电平(产生下降沿)
80μs高电平(产生上升沿)
边沿数:2

40位数据(5字节)传输:
每1位数据由2个边沿组成:
50μs低电平(下降沿)
高电平持续时间决定数据值:
26-28μs:表示"0"(产生上升沿)
70μs:表示"1"(产生上升沿)
边沿数:40位 × 2 = 80

结束信号:
50μs低电平(下降沿)
释放总线(上升沿)
边沿数:2

1.驱动初始化

头文件初始化,注册dht11中断服务函数;注册引脚配置结构体,设置功能;环形缓冲区。

#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;
}

2.驱动初始化入口函数

初始化引脚功能,初始化定时器中断。设置引脚中断,将引脚设置为输出,拉高引脚。

/* 在入口函数 */
static int __init dht11_init(void)
{
    int err;
    int i;
    int count = sizeof(gpios)/sizeof(gpios[0]);
    
	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_direction_output(gpios[i].gpio, 1);
		gpio_free(gpios[i].gpio);

		setup_timer(&gpios[i].key_timer, key_timer_expire, (unsigned long)&gpios[i]);
	 	//timer_setup(&gpios[i].key_timer, key_timer_expire, 0);
		//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;
}

3.dht11读取函数

首先发送18ms低脉冲后,引脚变为输入方向, 由上拉电阻拉为1,注册引脚边沿触发中断,以及定时器中断。休眠等待数据,调用 wait_event_interruptible 进入休眠,等待条件 !is_key_buf_empty() 成立(即环形缓冲区有数据)。释放引脚中断,设置引脚为高电平。将kern_buf读取环形缓冲区的数据,kern_buf[0] 读取湿度整数,kern_buf[1]读取温度整数。 先计划读取datas[0]和datas[2]。最后将kern_buf的数据给应用程序。
datas[0] // 湿度整数
datas[1] // 湿度小数(DHT11固定为0)
datas[2] // 温度整数
datas[3] // 温度小数(DHT11固定为0)
datas[4] // 校验和

/* 实现对应的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, 1);
    mdelay(30);
    gpio_direction_output(gpios[0].gpio, 0);
    mdelay(20);
    gpio_direction_output(gpios[0].gpio, 1);
    udelay(40);

	gpio_direction_input(gpios[0].gpio);  /* 引脚变为输入方向, 由上拉电阻拉为1 */

	/* 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 + 20);	

	/* 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 */
	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;
}

4.dht11中断服务函数

当边沿触发时,记录触发的时间,g_dht11_irq_time数组中记录每一次变化的时间。次数足够: 解析数据调用数据处理函数, 放入环形buffer, 唤醒APP,关闭定时器中断。

static irqreturn_t dht11_isr(int irq, void *dev_id)
{
	struct gpio_desc *gpio_desc = dev_id;
	u64 time;
	
	/* 1. 记录中断发生的时间 */
	time = ktime_get_ns();
	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;
}

5.数据处理函数

这个时候你就得想到,前面的中断可能丢失了,可能是81,82,83,84。但是低于82位的数据其实从某种意义来说已经没有意义了。
当数据个数小于81时,反馈出错,读取失败,终止阻塞。当数据大于81位时,从i = g_dht11_irq_cnt - 80位开始,i+=2位移1。
这个时候就得明白为什么高电平是high_time = g_dht11_irq_time[i] - g_dht11_irq_time[i-1];首先图5中可以看到,当接收数据的时候已经进入低电平了,也就是第0位g_dht11_irq_time[i-1]就是第一次触发的那个上升沿触发。而g_dht11_irq_time[i]则是那个下降沿,通过计算这个上升沿和下降沿之间的时间从而得到其为0,还是1。每次循环首先左移一位data,bits++。最终将datas[0]和datas[2]放入环形缓冲区。完成以上初级处理后 唤醒APPwake_up_interruptible(&gpio_wait);

static void parse_dht11_datas(void)
{
	int i;
	u64 high_time;
	unsigned char data = 0;
	int bits = 0;
	unsigned char datas[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;
	}

	// 解析数据
	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;

		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);
}

6.终止退出函数

/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 */
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字节的数据。


#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/100ask_button0
 *
 */
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(1);
	}

	//sleep(30);

	close(fd);
}

命令行

make
adb push gpio_drv.ko button_test root
insmod  gpio_drv.ko
rmmod gpio_drv.ko 
./button_test /dev/mydht11

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
可以看到我的中断也经常丢,3666-3593=74,连数据都丢了。而阻塞的优势:对于微秒级信号,使用内核gpiod_get_value轮询比中断更可靠,而 gpio_direction_output,gpio_request这一类函数又非常需要时间。所以对于这种单线的最好的处理方式就是轮询。


网站公告

今日签到

点亮在社区的每一天
去签到