《Linux驱动开发(一)—环境搭建与hello world》
《Linux驱动开发(二)—驱动与设备的分离设计》
《Linux驱动开发(三)—设备树》
《Linux驱动开发(四)—树莓派内核编译》
《Linux驱动开发(五)—树莓派设备树配合驱动开发》
《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
《Linux驱动开发(七)—树莓派按键驱动开发》
继续宣传一下韦老师的视频
70天30节Linux驱动开发快速入门系列课程【实战教学、技术讨论、直播答疑】
后面的内容,就开始以实际设备进行驱动学习,学习为各种传感器,总线设备等进行驱动编写,熟悉驱动开发过程。
卡姿兰大眼睛
这是一款超声波测距传感器,共有四个引脚,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 \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
[三 7月 27 09:15:17 2022 < 0.000000>] drivers/char/mysr04.c gpio_button_drv_read line 49
[三 7月 27 09:15:17 2022 < 0.002369>] revice irq 200
[三 7月 27 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(),不涉及闰秒更新,用得比较少 |
用户侧和内核用的字符串操作函数有所区别,详细可以参考墨染锦年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
在root用户下,通过命令
cat /sys/kernel/debug/gpio
最后要注意的就是这个模块,VCC要用5V,开始接的3.3,怎么算时间都不对
男生的快乐真的就是很简单,能有自己的地方玩会游戏就好啊。
这马上就到七夕了,男生都喜欢电子产品,可不要直接送南孚电池啊。