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” 类型,常常是目标平台本地的整数类型)。
-
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
函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。在文件结构被释放时引用这个操作. 如同 open
,release
可以为 NULL。
5.unsigned int (*poll) (struct file *, struct poll_table_struct *);
这是一个设备驱动中的轮询函数,第一个参数为file
结构指针,第二个为轮询表指针
这个函数返回设备资源的可获取状态,即POLLIN
,POLLOUT
,POLLPRI
,POLLERR
,POLLNVAL
等宏的位“或”结果。每个宏都表明设备的一种状态,如: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");