Linux驱动开发(八)---树莓派SR04驱动开发

54 篇文章 37 订阅
22 篇文章 80 订阅

前文回顾

《Linux驱动开发(一)—环境搭建与hello world》
《Linux驱动开发(二)—驱动与设备的分离设计》
《Linux驱动开发(三)—设备树》
《Linux驱动开发(四)—树莓派内核编译》
《Linux驱动开发(五)—树莓派设备树配合驱动开发》
《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
《Linux驱动开发(七)—树莓派按键驱动开发》

继续宣传一下韦老师的视频

70天30节Linux驱动开发快速入门系列课程【实战教学、技术讨论、直播答疑】

在这里插入图片描述
后面的内容,就开始以实际设备进行驱动学习,学习为各种传感器,总线设备等进行驱动编写,熟悉驱动开发过程。

SR04

卡姿兰大眼睛
在这里插入图片描述
这是一款超声波测距传感器,共有四个引脚,VCC和GND就不说了,Trip是触发信号,Echo是回响信号
在这里插入图片描述
1、Trig引脚接收至少10us的高电平信号,用于触发超声波模块工作;
2、模块会自动发送8个40KHz的方波信号,自动检测是否有信号返回;
3、有信号返回,通过Echo引脚连接单片机的I/O口输出一高电平,高电平持续时间T就是超声波从发射到返回的时间;
4、声音在空气中的传播速度为340米/秒,即可计算出所测的距离:D = 340*T/2。

原理是很简单的。
在这里插入图片描述

驱动原理

这里就需要用到两个GPIO,一个负责Trip,发出触发信号,一个负责Echo,接收Echo高电平信号,并计算高电平时间。

这里的Trip简单,持续一个大于10us的高电平即可,Echo引脚,我们需要监听上下边沿,然后计算出中间的时间,这里就需要用到中断。通过中断得到两个时间点,然后计算差值,传给用户。

其实前面两个大眼睛,其实一个是嘴巴,一个是耳朵。一个喊一个听。
在这里插入图片描述

设备树

设备树的编写如下
在这里插入图片描述
这里定义了两个引脚,17和是18,用来分别接Trip和Echo。
读取方法就是通过gpiod_get_index,可以读取多个引脚的描述信息(handle)。

struct gpio_desc *trip, *echo;
trip= gpiod_get_index(dev, "sr04", 0, GPIOD_OUT_HIGH);
echo= gpiod_get_index(dev, "sr04", 1, GPIOD_OUT_HIGH);

也可以分开定义不同的名字,看个人喜好罢了。

在这里插入图片描述

驱动编写第一步—测试硬件及中断

模块加载卸载部分就不说了,没什么好注意的,从probe函数开始说起吧。
首先定一个结构,用来存储gpio的信息,描述信息,终端信息。

struct sr04_gpios{
	struct gpio_desc *trip;
	struct gpio_desc *echo;
	int echo_irq;
} ;

struct sr04_gpios my_sr04_gpios;

然后probe函数中。获取引脚描述和中断,并且注册中断,采用上下边沿触发

static int mysr04_probe(struct platform_device *pdev)
{

	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	

	//读取关键引脚描述信息
	my_sr04_gpios.trip= gpiod_get_index(&pdev->dev, "sr04", 0, GPIOD_OUT_HIGH);
	my_sr04_gpios.echo= gpiod_get_index(&pdev->dev, "sr04", 1, GPIOD_OUT_HIGH);


	//配置方向
	gpiod_direction_output(my_sr04_gpios.trip,0);
	gpiod_direction_input(my_sr04_gpios.echo);

	//获取中断
	my_sr04_gpios.echo_irq = gpiod_to_irq(my_sr04_gpios.echo);

	//注册中断
	request_irq(my_sr04_gpios.echo_irq, my_sr04_echo_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "mysr04_irq", NULL);//IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING


	/* 注册file_operations 	*/
	major = register_chrdev(0, "pgg_sr04", &gpio_button_drv);  

	mysr04_class = class_create(THIS_MODULE, "mysr04_class");
	if (IS_ERR(mysr04_class)) 
	{
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "pgg_sr04");
		return PTR_ERR(mysr04_class);
	}

	device_create(mysr04_class, NULL, MKDEV(major, 0), NULL, "pgg_sr04"); /* /dev/pgg_sr04 */
    return 0;

}

在这里插入图片描述

同理,remove函数中反向操作

static int mysr04_remove(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(mysr04_class, MKDEV(major, 0));
	class_destroy(mysr04_class);
	unregister_chrdev(major, "pgg_sr04");
	gpiod_put(my_sr04_gpios.trip);
	gpiod_put(my_sr04_gpios.echo);
	free_irq(my_sr04_gpios.echo_irq,NULL);
    return 0;
}

中断函数中,我们先调试一下,看看是否能收到上下边沿的终端信息。所以简单的添加一个打印

static irqreturn_t my_sr04_echo_isr(int irq, void *dev_id)
{
	printk("revice irq %d\n",  irq);
	return IRQ_HANDLED;
}

然后,我们在read函数中,将trip引脚拉高100us。随后等待看看能否收到两次中断。

static ssize_t gpio_button_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	gpiod_set_value(my_sr04_gpios.trip,1);
	udelay(100);
	gpiod_set_value(my_sr04_gpios.trip,0);
	return 0;
}

用户侧程序

int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	int ret;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open("/dev/pgg_sr04", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/pgg_sr04\n");
		return -1;
	}
	
	printf("open file /dev/pgg_sr04 ok\n");

	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		ret = write(fd, argv[2], len);
		printf("write driver: %d\n", ret);
	}
	else
	{
		len = read(fd, buf, 1024);		
		printf("read driver: %d\n", len);
		buf[1023] = '\0';
		printf("APP read : %s\n", buf);
	}
	
	close(fd);
	
	return 0;
}

开始试验,更新DTD,上传ko,用户程序编译

pgg@raspberrypi:~/work/dirver $ sudo insmod mysr04.ko 
pgg@raspberrypi:~/work/dirver $ gcc -o mysr04_user mysr04_user.c 
pgg@raspberrypi:~/work/dirver $ sudo ./mysr04_user -r
open file /dev/pgg_sr04 ok
read driver: 0
APP read : 
pgg@raspberrypi:~/work/dirver $ dmesg 
[  477.269477] drivers/char/mysr04.c gpio_button_drv_read line 49
[  477.271846] revice irq 200
[  477.271888] revice irq 200
pgg@raspberrypi:~/work/dirver $ dmesg -d -T
[727 09:15:17 2022 <    0.000000>] drivers/char/mysr04.c gpio_button_drv_read line 49
[727 09:15:17 2022 <    0.002369>] revice irq 200
[727 09:15:17 2022 <    0.000042>] revice irq 200

果然是收到了两次中断。看来思路差不多。
在这里插入图片描述

驱动编写第二步—计算时间差值

继续优化。来计算一下两次中断的时间间隔。我们在中断中计算下时间差

static irqreturn_t my_sr04_echo_isr(int irq, void *dev_id)
{

	int val = gpiod_get_value(my_sr04_gpios.echo);

	if (val) /* 上升沿 */
	{
		/* 1. 记录数据 */
		a = ktime_get_ns();
		printk("revice irq up a=%llu\n",a);
	}
	else /* 下降沿 */
	{
		b = ktime_get_ns();
		printk("revice irq down b=%llu\n",b);

		printk("revice irq %llu\n",b-a);

	}
	return IRQ_HANDLED;
}
pgg@raspberrypi:~/work/dirver $ sudo ./mysr04_user -r
open file /dev/pgg_sr04 ok
read driver: 0
APP read : 
pgg@raspberrypi:~/work/dirver $ sudo dmesg
[ 4987.578948] drivers/char/mysr04.c gpio_button_drv_read line 51
[ 4987.581316] revice irq up a=4987648598062
[ 4987.581979] revice irq down b=4987649263185
[ 4987.581994] revice irq 665123
[ 5067.627410] drivers/char/mysr04.c gpio_button_drv_read line 51
[ 5067.629782] revice irq up a=5067699231916
[ 5067.630045] revice irq down b=5067699497079
[ 5067.630059] revice irq 265163
[ 5126.754375] drivers/char/mysr04.c gpio_button_drv_read line 51
[ 5126.756742] revice irq up a=5126827662096
[ 5126.767534] revice irq down b=5126838453813
[ 5126.767564] revice irq 10791717

[ 4987.581994] revice irq 665123这里是一个挡板,计算距离大概是10几厘米
[ 5067.630059] revice irq 265163这里是用手遮挡,大概三厘米左右
[ 5126.767564] revice irq 10791717 这里是计算到房顶的距离,大概1.8米。
感觉误差还是有的。暂时不管他。
在这里插入图片描述

驱动编写第三步—休眠唤醒

没事,先把整体完成,在用户侧直接返回时间差。还是通过休眠唤醒方式,传出数据。
中断函数,仅保留获得时间戳,然后唤醒read.

static irqreturn_t my_sr04_echo_isr(int irq, void *dev_id)
{

	int val = gpiod_get_value(my_sr04_gpios.echo);

	if (val) /* 上升沿 */
	{
		a = ktime_get_ns();
	}
	else /* 下降沿 */
	{
		b = ktime_get_ns();
		dataready = 1;
		wake_up_interruptible(&mysr04_wait);
	}
	return IRQ_HANDLED;
}

读取函数中,传出计算出来的时间差

static ssize_t mysr0_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	char result[64]={0};
	int reslen=0;
	static u64 c=0;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	a=0;
	b=0;
	dataready = 0;
	
	gpiod_set_value(my_sr04_gpios.trip,1);
	udelay(100);
	gpiod_set_value(my_sr04_gpios.trip,0);
	
	wait_event_interruptible(mysr04_wait, dataready);
	dataready = 0;
	c = b-a;
	sprintf(result,"%llu",c);
	
	reslen=strlen(result)+1;
	copy_to_user(buf, result, reslen);
	
	return reslen;
}

注意这里的时间返回是纳秒。
在这里插入图片描述

驱动测试

用户侧函数,读取时间差,转化为long,然后再计算出距离。

int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	int ret;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open("/dev/pgg_sr04", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/pgg_sr04\n");
		return -1;
	}
	
	//printf("open file /dev/pgg_sr04 ok\n");

	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-r")) && (argc == 2))
	{
		len = read(fd, buf, 1024);		
		buf[1023] = '\0';
		printf("driver read : %s\n", buf);

		long res= strtol(buf, NULL, 0);
		double resm=0.00000017*((double)res);
		printf("sr04 距离 : %lf 米\n", resm);

	}
	else
	{
		printf("	   %s -r\n", argv[0]);
	}
	
	close(fd);
	
	return 0;
}

来吧展示
在这里插入图片描述

pgg@raspberrypi:~/work/dirver $ sudo ./mysr04_user -r
driver read : 647500
sr04 距离 : 0.110075

知识点

今天的知识点比较复杂,也比较多。
在这里插入图片描述

时间延迟

时间延迟分为了忙等待或者睡眠等待。前者死等,后者让出CPU,根据情况不同,分开使用。
可参考Leon_George《Linux内核中的延时函数详解》

时间获取

内核中获取时间的函数如下

函数功能
ktime_get_ns()获取内核启动到现在的时间,在挂起时会暂停
ktime_get_boottime_ns()获取内核启动到现在的时间,不受挂起影响,是绝对时间
ktime_get_real_ns()获取Unix时间(1970年)到现在的时间,可能涉及闰秒更新,用得比较少
ktime_get_raw_ns()类似ktime_get_ns(),不涉及闰秒更新,用得比较少

注意时间的类型,可能是u64,所以输出的时候,用%llu。
在这里插入图片描述

内核可用的字符操作函数

用户侧和内核用的字符串操作函数有所区别,详细可以参考墨染锦年syx的《linux 内核库函数》
可不要傻乎乎的直接调用用户侧之前的那些函数啊。
在这里插入图片描述

关键函数返回值判断

内核中有一些函数调用的时候,必须检查返回值,否则会报错,例如

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

如果你没有检查,编译的时候,会提醒

warning: ignoring return value of ‘request_irq’, declared with attribute warn_unused_result [-Wunused-result]

在这里插入图片描述

函数返回值

在error-base.h中定义了如下基础错误,在开发过程中,可以适当使用。

#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define	EPERM		 1	/* Operation not permitted */
#define	ENOENT		 2	/* No such file or directory */
#define	ESRCH		 3	/* No such process */
#define	EINTR		 4	/* Interrupted system call */
#define	EIO		 5	/* I/O error */
#define	ENXIO		 6	/* No such device or address */
#define	E2BIG		 7	/* Argument list too long */
#define	ENOEXEC		 8	/* Exec format error */
#define	EBADF		 9	/* Bad file number */
#define	ECHILD		10	/* No child processes */
#define	EAGAIN		11	/* Try again */
#define	ENOMEM		12	/* Out of memory */
#define	EACCES		13	/* Permission denied */
#define	EFAULT		14	/* Bad address */
#define	ENOTBLK		15	/* Block device required */
#define	EBUSY		16	/* Device or resource busy */
#define	EEXIST		17	/* File exists */
#define	EXDEV		18	/* Cross-device link */
#define	ENODEV		19	/* No such device */
#define	ENOTDIR		20	/* Not a directory */
#define	EISDIR		21	/* Is a directory */
#define	EINVAL		22	/* Invalid argument */
#define	ENFILE		23	/* File table overflow */
#define	EMFILE		24	/* Too many open files */
#define	ENOTTY		25	/* Not a typewriter */
#define	ETXTBSY		26	/* Text file busy */
#define	EFBIG		27	/* File too large */
#define	ENOSPC		28	/* No space left on device */
#define	ESPIPE		29	/* Illegal seek */
#define	EROFS		30	/* Read-only file system */
#define	EMLINK		31	/* Too many links */
#define	EPIPE		32	/* Broken pipe */
#define	EDOM		33	/* Math argument out of domain of func */
#define	ERANGE		34	/* Math result not representable */

#endif

查看中断信息

通过命令

cat /proc/interrupts 

可以查看注册过的中断信息。
在这里插入图片描述

查看gpio信息

在root用户下,通过命令

cat /sys/kernel/debug/gpio 

可以查看当前各个GPIO的状态,电平,以及是否注册中断
在这里插入图片描述

最后注意

最后要注意的就是这个模块,VCC要用5V,开始接的3.3,怎么算时间都不对
在这里插入图片描述

结束语

男生的快乐真的就是很简单,能有自己的地方玩会游戏就好啊。
在这里插入图片描述
这马上就到七夕了,男生都喜欢电子产品,可不要直接送南孚电池啊。
在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖哥王老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值