ARM_Linux驱动开发——字符设备驱动开发(上)

时间:2024-07-06 12:35:26

 

目录

一、Linux驱动开发思维

二、Linux驱动开发分类

三、“ ARM_Linux驱动开发——字符设备驱动开发 ”

字符设备驱动简介


前言

在分享Linux驱动开发之前,我想带大家首先回顾一下裸机驱动开发和Linux驱动开发的区别。

1、运行环境和操作系统:

裸机驱动开发:在裸机开发中,通常是针对没有操作系统支持的嵌入式系统或特定的硬件平台开发驱动程序。这些系统通常直接操作硬件资源,没有操作系统的抽象层。

Linux驱动开发:Linux驱动开发则是针对运行Linux操作系统的计算机或嵌入式系统开发。Linux作为一个开源操作系统,提供了广泛的设备支持和抽象接口,开发者可以利用Linux内核提供的接口来开发设备驱动。

2、开发流程和工具链:

裸机驱动开发:开发者需要了解硬件的具体细节,通常使用特定的开发工具链(如ARM Cortex-M系列开发可以使用Keil、IAR等),编写底层的驱动代码,直接操作硬件寄存器和控制器。

Linux驱动开发:需要熟悉Linux内核的架构和API,使用C语言或者特定的Linux设备驱动框架(如Platform Driver、USB Driver等)进行开发。Linux提供了丰富的设备驱动开发文档和示例代码,以及调试工具。

3、硬件抽象层和接口标准化:

裸机驱动开发:由于没有操作系统提供的标准接口,开发者需要自行处理硬件之间的通信和资源管理。每种硬件平台的驱动开发可能具有很高的定制性和特定性。

Linux驱动开发:Linux内核提供了一套标准的设备驱动开发框架和API,开发者可以利用这些框架来开发驱动,这样的驱动通常更具有通用性和可移植性。

4、调试和测试:

裸机驱动开发:调试通常需要通过硬件调试器或者仿真器进行,对于实际硬件的测试需要特定的硬件平台。

Linux驱动开发:Linux内核提供了丰富的调试工具和接口,如printk日志、sysfs接口等,同时也可以使用标准的Linux调试工具(如gdb、strace等)来进行驱动调试和性能分析。

总之,裸机驱动开发更加依赖于具体的硬件平台和细节,开发的驱动程序更加定制化和特定化;而Linux驱动开发则更多依赖于操作系统提供的标准接口和抽象层,开发的驱动程序具有更好的可移植性和通用性。 

一、Linux驱动开发思维

1、首先需要跳出曾经的裸机开发思维,因为Linux下的驱动开发直接操作寄存器已经不现实了;

2、开发者需要根据Linux下的各种驱动框架进行开发。即必须满足驱动开发的框架;

3、驱动最终的执行形式就是:/dev/xxx文件、open、close、write、read……

4、新的驱动支持设备树,这是一个.dts文件,此文件描述的是设备信息。


二、Linux驱动开发分类

Linux驱动开发可以按照不同的分类进行归类,主要包括以下几类:

1、字符设备驱动(Character Device Drivers):

这类驱动程序处理以字符流方式进行数据传输的设备,如串口、终端、声音卡等。通常通过文件操作接口(open, read, write, close)来与用户空间进行通信。

2、块设备驱动(Block Device Drivers):

这类驱动程序用于管理块设备,如硬盘、闪存存储器等,它们以固定大小的块为单位进行数据传输。块设备驱动通常需要实现块设备的缓存、IO调度等功能。

3、网络设备驱动(Network Device Drivers):

网络设备驱动用于控制网络接口卡(NIC),如以太网卡。这类驱动程序处理网络数据包的收发、协议栈的处理等。

4、USB设备驱动(USB Device Drivers):

USB设备驱动用于控制连接到Linux系统的USB设备,如USB存储设备、USB网卡等。这类驱动程序需要处理USB协议栈、设备的插拔事件等。

5、文件系统驱动(Filesystem Drivers):

文件系统驱动用于支持特定类型的文件系统,如ext4、NTFS等。这类驱动程序负责管理存储设备上的文件和目录结构。

6、总线设备驱动(Bus Drivers):

总线设备驱动用于管理系统总线,如PCI总线、SPI总线、I2C总线等。这类驱动程序负责枚举、初始化和管理连接到总线上的设备。

7、平台设备驱动(Platform Drivers):

平台设备驱动用于支持特定硬件平台上的设备,如嵌入式系统中的特定传感器、LED控制器等。

8、虚拟设备驱动(Virtual Device Drivers):

这类驱动程序不与物理硬件设备直接交互,而是创建虚拟设备,如虚拟网卡、虚拟磁盘等。

9、字符驱动和块驱动的子类:

这些驱动可能根据设备的特定需求进一步分类,例如音频设备驱动、输入设备驱动(键盘、鼠标)、显示设备驱动等。

Linux驱动开发涵盖了广泛的设备类型和功能,开发者根据具体的设备类型选择适合的驱动开发模块和接口。

三、“ ARM_Linux驱动开发——字符设备驱动开发 ”


字符设备驱动简介

字符设备就是一个一个字节,按照字节流来进行读写操作的设备,读写数据是分先后顺序的。常见的比如点灯、IIC、ISP……都是字符操作的设备,对于这些设备的驱动就叫做字符设备驱动。

了解字符设备驱动架构之前,我们可以先来看一下Linux下的应用程序是如何调用驱动程序的:

在Linux中,一切皆为文件。要实现对硬件的操作,需要应用程序对名为"/dev/xxx"的文件将进行操作。这个文件是由驱动加载成功后,会在"/dev"目录下生成相应的文件。

举一个简单的例子:如果你要操作点亮一个灯,你就可以使用open函数打开"/dev/Led"文件,使用完成以后用close函数关闭"/dev/Led"这个文件。需要点亮灯的话,则使用函数write函数来操作,即向此驱动写入数据,这个数据就是决定是否打开灯。如果要获取Led灯的状态,则使用read函数来从驱动中读取相应的状态。

应用程序是运行在用户空间,但是Linux驱动属于内核的一部分,所以驱动运行在内核空间。这样的话问题就来了,如果用户要直接对内核进行操作,直接通过用户空间来操作可行吗?

答案肯定是不行的!

用户空间是不能直接对内核进行操作的,他需要找一个中间人,这个中间人就是通过Linux驱动函数。即使用"系统调用"的方法来实现用户空间“侵入”到内核空间,这样才能实现对底层驱动的操作。

上面提到的open、close、write、read等这些驱动操作函数,都是由C库提供的。在Linux中,系统调用作为C库的一部分。

例如:调用open函数的时候流程如下:

(应用程序)step1:应用调用open()函数 ——> (C库)step2:C库中的open()函数 ——> (内核)step3:open()系统调用 ——> (具体驱动)step4: 驱动的open()函数

图片

那问题又来了,应用程序和具体的驱动又是如何联系起来的呢?

通过上面的讲解,我们能初步了解到应用程序使用到的函数在具体驱动程序中都有与之对应的函数, 比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。

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 (*iopoll)(struct kiocb *kiocb, bool spin);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*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 *);
	unsigned long mmap_supported_flags;
	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_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

下面我们对内核驱动操作函数进行初步解释:

  1. llseek:

    • 定义:文件定位函数,用于改变文件的读写位置。

    • 参数:struct file * 文件指针,loff_t 偏移量,int 偏移基准。

    • 返回值:loff_t 新的文件位置。

  2. read:

    • 定义:读取文件内容到用户空间。

    • 参数:struct file * 文件指针,char __user * 用户空间缓冲区,size_t 请求读取的字节数,loff_t * 读取位置的指针。

    • 返回值:ssize_t 实际读取的字节数,错误时返回负值。

  3. write:

    • 定义:将用户空间的数据写入文件。

    • 参数:struct file * 文件指针,const char __user * 用户空间缓冲区,size_t 请求写入的字节数,loff_t * 写入位置的指针。

    • 返回值:ssize_t 实际写入的字节数,错误时返回负值。

  4. read_iter:

    • 定义:迭代式地从文件读取数据到用户空间。

    • 参数:struct kiocb * 异步IO控制块,struct iov_iter * 数据迭代器。

    • 返回值:ssize_t 实际读取的字节数。

  5. write_iter:

    • 定义:迭代式地将用户空间的数据写入文件。

    • 参数:struct kiocb * 异步IO控制块,struct iov_iter * 数据迭代器。

    • 返回值:ssize_t 实际写入的字节数。

  6. iopoll:

    • 定义:用于异步IO中的轮询操作。

    • 参数:struct kiocb * 异步IO控制块,bool 是否自旋。

    • 返回值:int 操作结果。

  7. iterate:

    • 定义:在目录中迭代项。

    • 参数:struct file * 目录文件指针,struct dir_context * 目录上下文。

    • 返回值:int 操作结果。

  8. iterate_shared:

    • 定义:在共享目录中迭代项。

    • 参数:struct file * 目录文件指针,struct dir_context * 目录上下文。

    • 返回值:int 操作结果。

  9. poll:

    • 定义:注册文件的poll/epoll事件。

    • 参数:struct file * 文件指针,struct poll_table_struct * poll表。

    • 返回值:__poll_t 事件掩码。

关于字符设备驱动开发步骤,敬请关注下文更新——《ARM_Linux驱动开发——字符设备驱动开发(下)》!