【字符设备驱动开发–IMX6ULL】(一)简介

发布于:2025-03-30 ⋅ 阅读:(28) ⋅ 点赞:(0)

【字符设备驱动开发–IMX6ULL】(一)简介

一、Linux驱动与裸机开发区别

1.裸机驱动开发回顾

​ 1、底层,跟寄存器打交道,有些MCU提供了库。

spi.c:主机驱动(换成任何一个设备之后只需要调用此文件里面的函数即可)

image-20250106215038846

**w25q.c:**设备驱动(根据具体的设备来写的驱动)

image-20250106215223475

2.Linux驱动开发思维

​ 1、Linux下驱动开发直接操作寄存器不现实。

​ 2、根据Linux下的各种驱动框架进行开发。一定要满足框架,也就是Linux下各种驱动框架的掌握。

​ 3、驱动最终表现就是/dev/xxx文件。打开、关闭、读写、。。。

​ 4、现在新的内核支持设备树,这个一个.dts文件,此文件 描述了板子的设备信息。

3.Linux驱动开发分类

​ linux驱动分为三大类:

​ 1、字符设备驱动,最多的。

​ 2、块设备驱动,存储

​ 3、网络设备驱动,

​ 一个设备不说是一定只属于某一个类型。比如USB WIFI,SDIO WIFI,属于网络设备驱动,因为他又有USB和SDIO,因此也属于字符设备驱动。

二、字符设备开发基础

1.应用程序和驱动的交互原理

1.驱动就是获取外设、或者传感器数据,控制外设。数据会提交给应用程序。Linux驱动编译既要编写一个驱动,还要我们编写一个简单的测试应用程序,APP。

单片机下驱动和应用都是放到一个文件里面,也就是杂糅到一起。Linux下驱动和应用是完全分开的。

2.用户空间(用户态)和内核空间(内核态):

Linux操作系统内核和驱动程序运行在内核空间、应用程序运行在用户空间。

3.应用程序想要访问内核资源,怎么办,有三种方法:系统调用、异常(中断)和陷入。

应用程序不会直接调用系统调用,而是通过API函数来间接的调用系统调用,比如POSIX、API和C库等。unix类操作系统中最常用的编程接口就是POSIX。

应用 程序使用open函数 打开一个设备文件。

4.每个系统调用都有一个系统调用号。

系统调用处于内核空间,应用程序无法直接访问,因此需要“陷入“到内核,方法就是软中断。陷入内核以后还要指定系统调用号。

image-20250107164936038

2.字符设备驱动开发流程

1,Linux里面一切皆文件,驱动设备表现就是一个/dev/下的文件,/dev/led。应用程序调用open函数打开设备,比如led。应用程序通过write函数向/dev/led写数据,比如写1表示打开,写0表示关闭。如果要关闭设备那么就是close函数。

2,编写驱动的 时候也需要编写驱动对应的open、close,write函数。字符设备驱动file_operations

1、驱动最终是被应用调用的,在写驱动的时候要考虑应用开发的便利性。

2、驱动是分驱动框架的,要按照驱动框架来编写,对于字符设备驱动来说,重点编写应用程序对应的open、close、read、write等函数。

三、字符设备驱动简介

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的,Linux 应用程序对驱动程序的调用如图所示:

image-20250116163923224

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。

  • 1.应用程序使用 open 函数来打开文件/dev/led
  • 2.使用完成以后使用 close 函数关闭/dev/led 这个文件。open和 close 就是打开和关闭 led 驱动的函数,
  • 3.如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。
  • 4.如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。

应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。

open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的
时候流程如图所示:

image-20250116164450394

其中关于 C 库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,
比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:

一般在1550行左右

struct file_operations {
//owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
	struct module *owner;
//llseek 函数用于修改文件当前的读写位置。
	loff_t (*llseek) (struct file *, loff_t, int);
//read 函数用于读取设备文件。
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//write 函数用于向设备文件写入(发送)数据。
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*iterate) (struct file *, struct dir_context *);
//poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
//unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
//compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,
//32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
/*mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,
比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,
这样就不用在用户空间和内核空间之间来回复制。*/
	int (*mmap) (struct file *, struct vm_area_struct *);
//open 函数用于打开设备文件。
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
//release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
	int (*release) (struct inode *, struct file *);
//fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
//aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
	int (*aio_fsync) (struct kiocb *, 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 **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

在字符设备驱动开发中最常用的函数已经标上注释,关于其他的函数大家可以查阅相关文档。

我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,
但是像 open、release、write、read 等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。