Linux驱动学习之平台总线设备树驱动模型

devtools/2025/3/10 19:36:39/
0.步骤零:通用操作指针变量
static int major = 0;                       //主设备号
static struct class *sr501_class;            //定义类
static struct gpio_desc *sr501_gpio;        //GPIO结构体操作指针
static int sr501_irq;                         //GPIO中断号
static wait_queue_head_t sr501_wq;          //等待队列/* 环形缓冲区 */
#define BUF_LEN 128
static int sr501_buff[BUF_LEN];
​
static int r, w;  //缓冲区读和写的索引struct fasync_struct *sr501_fasync;         //异步通知结构体#define NEXT_POS(x) ((x+1) % BUF_LEN)/* 缓冲区是否为空 */
static int is_sr501_buf_empty(void)
{return (r == w);
}/* 缓冲区是否为满 */
static int is_sr501_buf_full(void)
{return (r == NEXT_POS(w));
}/* 向缓冲区放入数据 */
static void put_sr501(int sr501_val)
{if (!is_sr501_buf_full()) /* 缓冲区没有满 */{sr501_buff[w] = sr501_val; // key放入缓冲区w = NEXT_POS(w);    //更新写索引}
}/* 从缓冲区取出数据 */
static int get_sr501(void)
{int sr501_val = 0;if (!is_sr501_buf_empty()) // 缓冲区数据不为空{sr501_val = sr501_buff[r]; // 取出缓冲区数据r = NEXT_POS(r);    //更新读索引}return sr501_val;
}
1步骤一:实现file_operations结构体

步骤一与使用gpio设备号的方式比较来看是没有任何区别的,是通用的操作

该结构体定义如下

struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);int (*iterate_shared) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endifssize_t (*copy_file_range)(struct file *, loff_t, struct file *,loff_t, size_t, unsigned int);int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,u64);
};

根据模块来看添加哪些,一般常用的就是:

1.ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);

指针参数 filp 为进行读取信息的目标文件,指针参数buffer为对应放置信息的缓冲区(即用户空间内存地址)。参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值

这个函数用来从设备中获取数据。在这个位置的一个空指针导致 read 系统调用以 -EINVAL(“Invalid argument”) 失败。一个非负返回值代表了成功读取的字节数( 返回值是一个 “signed size” 类型,常常是目标平台本地的整数类型)。

  1. ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);

参数filp为目标文件结构体指针; buffer为要写入文件的信息缓冲区; count为要写入信息的长度; ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界

函数的作用是发送数据给设备。如果 NULL, -EINVAL 返回给调用write 系统调用的程序. 如果非负, 返回值代表成功写的字节数。

注:这个操作和上面的对文件进行读的操作均为阻塞操作

3.int (*open) (struct inode * inode , struct file * filp ) ;

inode为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息。

尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法。如果这个项是 NULL,设备打开一直成功,但是你的驱动不会得到通知。open()函数对应的是release()函数。

4.int (*release) (struct inode *, struct file *);

release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数

void release(struct inode inode,struct file *file)release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。在文件结构被释放时引用这个操作. 如同 openrelease可以为 NULL。

5.unsigned int (*poll) (struct file *, struct poll_table_struct *);

这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针

这个函数返回设备资源的可获取状态,即POLLINPOLLOUTPOLLPRIPOLLERRPOLLNVAL等宏的位“或”结果。每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。

poll 方法是 3 个系统调用的后端: poll, epoll, 和 select,都用作查询对一个或多个文件描述符的读或写是否会阻塞。poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的,并且,可能地,提供给内核信息用来使调用进程睡眠直到 I/O 变为可能。如果一个驱动的 poll 方法为 NULL,设备假定为不阻塞地可读可写.

这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果

6.int (*fasync) (int, struct file *, int);

这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:

//static int _fasync(int fd,struct file *filp,int mode)
​
static int gpio_drv_fasync(int fd, struct file *file, int on)
{if (fasync_helper(fd, file, on, &button_fasync) >= 0)return 0;elsereturn -EIO;
}

此操作用来通知设备它的 FASYNC 标志的改变。异步通知是一个高级的主题,在第 6 章中描述。这个成员可以是NULL,如果驱动不支持异步通知。

2步骤二:中断函数---根据设备是否需要进行选择

步骤二与使用gpio设备号的方式比较来看是没有任何区别的,是通用的操作

例如:

/*** gpio_sr501_isr - GPIO r501中断服务程序* @irq: 中断号* @dev_id: 设备标识,这里指向结构体gpio_desc** 该函数处理GPIO下的sr501中断事件。当sr501触发中断时,此函数将被调用。* 它通过读取GPIO值来确定sr501的数值,并将sr501事件传递给环形缓冲区。* 同时,它还会唤醒任何在等待这个事件的进程,并处理异步I/O的请求。** 返回值: IRQ_HANDLED 表示中断已成功处理。*/
static irqreturn_t sr501_isr(int irq, void *dev_id)
{int sr501_val;// 打印中断发生的GPIO编号-可以不要printk("sr501_isr %d irq happened\n", sr501_irq);// 读取GPIO的当前值sr501_val = gpiod_get_value(sr501_gpio);put_sr501(sr501_val); // 将sr501值放入环形缓冲区/* ------通知用户空间的进程有关设备状态的变化------*/// 唤醒任何在sr501_wq上等待的进程wake_up_interruptible(&sr501_wq);// 发送SIGIO信号给sr501_fasync队列,通知有异步事件发生kill_fasync(&sr501_fasync, SIGIO, POLL_IN);// 返回IRQ_HANDLED,表示中断已处理return IRQ_HANDLED;
}
3步骤三:定义probe函数--重要

在总线上驱动和设备的名字匹配的时候,就会调用驱动的probe函数,其主要目的就是初始化驱动程序并且注册设备,使得能够与内核和其他驱动程序进行交互

函数举例:

static int sr501_probe(struct platform_device *pdev)
{int err;/* 设备树中定义有: sr501-gpios=<...>;    *//*该行代码的作用:从设备树中获取与 pdev 关联的设备上的 "sr501" GPIO 线。&pdev->dev: 指向 struct device 结构体的指针,这个结构体描述了一个设备gpiod_get():从设备树(Device Tree)中查找并获取一个 GPIO 控制器上的 GPIO线。*/printk("sr501 match success!\n");/*1.从设备树中获取名称为“sr501”的GPIO控制器,*/sr501_gpio = gpiod_get(&pdev->dev, "sr501", 0);if (IS_ERR(sr501_gpio)){dev_err(&pdev->dev, "Failed to get GPIO for sr501\n");            return PTR_ERR(sr501_gpio);}/*2.设置为输入状态*/gpiod_direction_input(sr501_gpio);   //设置为输入状态/*3.申请终端号*/sr501_irq = gpiod_to_irq(sr501_gpio); // 为GPIO申请中断号。/*4.注册中断处理程序 */err = request_irq(sr501_irq, sr501_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "sr501", NULL);if (err){dev_err(&pdev->dev, "Failed to request IRQ for sr501\n");free_irq(sr501_irq, &pdev->dev);gpiod_put(sr501_gpio);return err;}/* 5.注册file_operations--注册字符设备  */major = register_chrdev(0, "sr501_chrdev", &sr501_fops); /*6.创建设备类*/sr501_class = class_create(THIS_MODULE, "sr501_class");if (IS_ERR(sr501_class)){dev_err(&pdev->dev, "Failed to create class\n");unregister_chrdev(major, "sr501_chrdev");return PTR_ERR(sr501_class);}/*7.创建设备节点*/device_create(sr501_class, NULL, MKDEV(major, 0), NULL, "sr501"); /* /dev/sr501 */// printk("===== init ok =====err:%d \n", err);dev_info(&pdev->dev, "sr501 initialized successfully\n");return err;
}

重要函数解释:

1.gpiod_get函数

该函数允许开发者直接从设备树(Device Tree)中获取GPIO,而无需使用of函数。例如,如果设备树中定义了一个名为enable-gpios的GPIO属性,可以使用以下方式获取该GPIO。

struct gpio_desc *__must_check gpiod_get(struct device *dev, const char *con_id,enum gpiod_flags flags)struct gpio_desc *enable;
enable = gpiod_get_index(&pdev->dev, "设备树中设备的名称", 0, GPIOD_OUT_LOW(根据需要进行设置));
if (IS_ERR(enable)) {
printk("Cannot find enable-gpios!\n");
}
​

在这个例子中,gpiod_get_index函数获取了设备树中的enable-gpios属性,并将其初始化为输出低电平。如果获取失败,将打印错误信息。

2.gpiod_direction_input函数

该函数用于将指定的gpio引脚设置为输入模式

int gpiod_direction_input(struct gpio_desc *desc)
{struct gpio_chip    *chip;int         status = -EINVAL;
​VALIDATE_DESC(desc);chip = desc->gdev->chip;
​if (!chip->get || !chip->direction_input) {gpiod_warn(desc,"%s: missing get() or direction_input() operations\n",__func__);return -EIO;}
​status = chip->direction_input(chip, gpio_chip_hwgpio(desc));if (status == 0)clear_bit(FLAG_IS_OUT, &desc->flags);
​trace_gpio_direction(desc_to_gpio(desc), 1, status);
​return status;
}
3.gpiod_to_irq函数

用于将通用输入/输出(GPIO)转换为中断请求(IRQ)线的重要函数。这个函数允许开发者获取能够产生中断信号的GPIO引脚对应的中断号。这样,开发者就可以使用这个中断号来调用request_irq()函数,注册相应的中断处理函数。

int gpiod_to_irq(const struct gpio_desc *desc)
{struct gpio_chip *chip;int offset;
​/** Cannot VALIDATE_DESC() here as gpiod_to_irq() consumer semantics* requires this function to not return zero on an invalid descriptor* but rather a negative error number.*/if (!desc || IS_ERR(desc) || !desc->gdev || !desc->gdev->chip)return -EINVAL;
​chip = desc->gdev->chip;offset = gpio_chip_hwgpio(desc);if (chip->to_irq) {int retirq = chip->to_irq(chip, offset);
​/* Zero means NO_IRQ */if (!retirq)return -ENXIO;
​return retirq;}return -ENXIO;
}
4.request_irq函数

是Linux内核中用于申请中断的关键函数。当开发者需要在内核中注册一个中断处理程序时,就需要使用到这个函数。它的基本作用是将一个给定的处理函数与一个特定的中断号关联起来,当该中断被触发时,内核就会调用这个处理函数。

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
5.free_irq函数

搭配request_irq函数使用,当申请中断失败的时候,使用这个函数

此函数用于卸载IRQ链表中与输入参数相对应的irqaction描述符,并释放其所占用的内存空间。

void free_irq(unsigned int irq, void *dev_id)
{struct irq_desc *desc = irq_to_desc(irq);
​if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))return;
​
#ifdef CONFIG_SMPif (WARN_ON(desc->affinity_notify))desc->affinity_notify = NULL;
#endif
​kfree(__free_irq(irq, dev_id));
}
6.gpiod_put函数

是一个简单的辅助函数,用于释放GPIO插针对象。这通常在从GPIO子系统中删除GPIO行时使用。

void gpiod_put(struct gpio_desc *desc)
{gpiod_free(desc);
}
7.register_chrdev函数

用来注册字符设备,使得用户空间和内核中的设备进行交互。

第一个参数:register_chrdev函数的major参数如果等于0,则表示采用系统动态分配的主设备号,否则就是静态注册

第二个参数:设备的名字,和设备树的名字区别开来,这个是注册字符设备的名称

第三个参数:文件操作指针,也就是file_operation结构体

static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}

扩展:

register_chrdev_region以及alloc_chrdev_region就是将上述函数的静态和动态注册设备号进行了拆分,前者为静态注册,后者为动态注册。

8.class_create函数

用于创建一个设备类,该类可以将设备按照功能或者类型进行分组,创建类的时候,需要为这个类指定一个唯一的名称

#define class_create(owner, classname)      \
({                      \static struct lock_class_key __key; \__class_create(owner, name, &__key);    \
})
9.unregister_chrdev函数

该函数用于从Linux内核中注销字符设备的函数

static inline void unregister_chrdev(unsigned int major, const char *name)
{__unregister_chrdev(major, 0, 256, name);
}
10.device_create函数

用于在加载模块时自动创建/dev目录下的设备节点,并在卸载模块时删除这些节点。这个过程依赖于用户空间的udev系统,它负责管理设备节点的动态创建和删除。

struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
{va_list vargs;struct device *dev;
​va_start(vargs, fmt);dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);va_end(vargs);return dev;
}
11.调试函数专栏

参考博客:

Linux内核中dev_info、dev_dbg、dev_err及动态调试是怎样的

Linux内核中dev_info、dev_dbg、dev_err及动态调试是怎样的 - 系统运维 - 亿速云

目前在kernel驱动代码中,都不再建议直接使用printk直接添加打印信息,而是使用dev_info,dev_dbg,dev_err之类的函数代替,虽然这些dev_xxx函数的本质还是使用printk打印的,但是相比起printk:

  • 支持打印模块信息、dev信息

  • 支持动态调试(dynamic debug)方式

下面简述下这几个dev_xxx函数的基本使用规则,以及动态调试使用方式。

  • dev_info():启动过程、或者模块加载过程等“通知类的”信息等,一般只会通知一次,例如probe函数;

  • dev_dbg():一般使用在普通错误,如-EINVAL、-ENOMEM等errno发生处,用于调试;

  • dev_err():一般使用在严重错误,尤其是用户无法得到errno的地方,或者程序员不容易猜测系统哪里出了问题的地方;

4步骤四:定义remove函数

主要作用就是对前面注册的字符驱动设备,设备类以及设备进行卸载

static int sr501_remove(struct platform_device *pdev)
{// int i;// int count = sizeof(gpios) / sizeof(gpios[0]);printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(sr501_class, MKDEV(major, 0));class_destroy(sr501_class);unregister_chrdev(major, "sr501_chrdev");gpiod_put(sr501_gpio);return 0;
}
5步骤五:定义设备匹配表-of_device_id
struct of_device_id {char    name[32];char    type[32];char    compatible[128];const void *data;
};

主要是用来与设备树中定义的compatible进行匹配

static const struct of_device_id sr501_match_table[] = {/* 匹配字符串 "fire,sr501" 用于标识 */{.compatible = "my_sr501"},/* 空项作为匹配表的结束标志 */{},
};
6步骤六:定义platform_driver
struct platform_driver {int (*probe)(struct platform_device *);int (*remove)(struct platform_device *);void (*shutdown)(struct platform_device *);int (*suspend)(struct platform_device *, pm_message_t state);int (*resume)(struct platform_device *);struct device_driver driver;const struct platform_device_id *id_table;bool prevent_deferred_probe;
};例如:static struct platform_driver sr501_driver = {.probe =  sr501_probe,   // 设置探测函数,当设备被探测到时调用.remove = sr501_remove, // 设置移除函数,当设备被移除时调用/* 设置<驱动程序的名称>和<设备树匹配表> */.driver = {.name = "sr501",         // 字符设备名.of_match_table = sr501_match_table, // 设置设备树匹配表,用于设备的匹配},
};
7步骤七:定义入口函数,出口函数

入口函数:注册平台总线设备

static int __init sr501_drv_init(void)
{int err;init_waitqueue_head(&sr501_wq);err = platform_driver_register(&sr501_driver);return err;
}

出口函数:卸载平台总线设备

static void __exit sr501_drv_exit(void)
{platform_driver_unregister(&sr501_driver);printk("=====exit=====\n");
}module_init(sr501_drv_init); module_exit(sr501_drv_exit);MODULE_LICENSE("GPL");


http://www.ppmy.cn/devtools/166085.html

相关文章

Apache SeaTunnel 人物专访 | 张东浩:从使用者到Committer的开源历程

时光飞逝&#xff0c;转眼间&#xff0c;Apache SeaTunnel 社区已经成为顶级项目快两周年了&#xff0c;其社区贡献者和用户群体也日益壮大。SeaTunnel 凭借其高性能和插件灵活的特性&#xff0c;已经成为国内众多知名企业数据同步的基础工具。作为 SeaTunnel 的贡献者&#xf…

C++之序列容器(vector,list,dueqe)

1.大体对比 在软件开发的漫长历程中&#xff0c;数据结构与算法始终占据着核心地位&#xff0c;犹如大厦的基石&#xff0c;稳固支撑着整个程序的运行。在众多编程语言中&#xff0c;数据的存储与管理方式各有千秋&#xff0c;而 C 凭借其丰富且强大的工具集脱颖而出&#xff…

深入探索 Django 内置的 User 模型及其自定义扩展

深入探索 Django 内置的 User 模型及其自定义扩展 在 Django 框架中&#xff0c;内置的 User 模型是处理用户认证和授权的核心组件。它提供了一系列预定义的属性和方法&#xff0c;使得开发者能够轻松管理用户信息、进行用户认证以及控制用户权限。本文将详细介绍 Django 内置…

批量在 Word 的指定位置插入页,如插入封面、末尾插入页面

我们经常会碰到需要在 Word 文档中插入新的页面的需求&#xff0c;比如在 Word 文档末尾插入一个广告页、给 Word 文档插入一个说明封面&#xff0c;在 Word 文档的中间位置插入新的页面等等。相信这个操作对于大部分小伙伴来说都不难&#xff0c;难的是同时给多个 Word 文档插…

视频输入设备-V4L2的开发流程简述

一、摄像头的工作原理与应用 基本概念 V4L2的全称是Video For Linux Two&#xff0c;其实指的是V4L的升级版&#xff0c;是linux系统关于视频设备的内核驱动&#xff0c;同时V4L2也包含Linux系统下关于视频以及音频采集的接口&#xff0c;只需要配合对应的视频采集设备就可以实…

蓝桥杯嵌入式组第七届省赛题目解析+STM32G431RBT6实现源码

文章目录 1.题目解析1.1 分而治之&#xff0c;藕断丝连1.2 模块化思维导图1.3 模块解析1.3.1 KEY模块1.3.2 ADC模块1.3.3 IIC模块1.3.4 UART模块1.3.5 LCD模块1.3.6 LED模块1.3.7 TIM模块 2.源码3.第七届题目 前言&#xff1a;STM32G431RBT6实现嵌入式组第七届题目解析源码&…

阿里云操作系统(AliOS)

引言 阿里云操作系统&#xff08;AliOS&#xff09;是阿里巴巴集团专为物联网&#xff08;IoT&#xff09;和智能设备开发的操作系统&#xff0c;致力于为智能汽车、智能家居、工业设备等提供高效、安全、智能化的解决方案。作为一款云端一体的操作系统&#xff0c;AliOS深度融…

深度学习---卷积神经网络

一、卷积尺寸计算公式 二、池化 池化分为最大池化和平均池化 最常用的就是最大池化&#xff0c;可以认为最大池化不需要引入计算&#xff0c;而平均池化需要引出计算&#xff08;计算平均数&#xff09; 每种池化还分为Pooling和AdaptiveAvgPool Pooling(2)就是每2*2个格子…