树莓派4bwlan驱动_树莓派的内核模块编程之字符设备驱动

引言

在linux系统中,硬件设备是通过特殊的设备文件与内核进行通信,这些设备文件根据所传输的数据量与传输速度被分为字符设备(character device)与块设备(block device),并且都被放在/dev目录下。本文主要介绍字符设备的相关内容,并说明如何在树莓派上编写字符设备驱动。

一、字符设备与块设备

linux系统通过特殊的设备文件来控制硬件设备与linux内核之间的通信,这些设备文件统一放在/dev目录下。在linux系统中设备文件分为两类,分别是字符设备(character device)与块设备(block device)。字符设备的特点是数据传输速度较慢、数据传输量小并且数据的查询频率较低。通常来说,键盘、鼠标与声卡硬件设备等都属于字符设备。与字符设备相反,块设备的特点是数据传输量较大并且数据搜索频率较高,块设备通常包括硬盘驱动、cdrom与ramdisk。

此外,linux内核分别针对字符设备与块设备提供了两种不同的API(应用编程接口)。对于字符设备而言,由于其传输的数据量小,字符设备的系统调用直接与字符设备驱动通信。块设备则不同,用户空间与字符设备驱动的通信是通过文件管理子系统与块设备子系统来完成的。这两个子系统的作用是为块设备驱动准备所需的系统资源(例如缓存空间),从而保证读写的数据有足够的缓存空间,同时也负责合理地安排设备驱动的读写操作,从而实现更高的读写性能。

本文主要介绍字符设备的相关内容,并通过一个简单的树莓派字符设备驱动器来演示如何编写字符设备驱动。

二、设备的主次设备号

在linux系统中,用户空间可以通过文件系统中的文件名与字符设备进行通信,这里的文件名通常被称为特殊文件、设备文件或者文件系统树(file system tree)中的节点。根据linux系统管理惯例,设备文件通常存放在/dev目录下,如果在/dev目录下输入命令ls -l命令,我们可以看到如下图所示的结果。如图所示,输出结果第一列只有字母c和b两种情况,其中字母c表示字符设备,字母b表示块设备。

a22c5ad522e10f11d48ed44eb69d175e.png

此外,在上图中我们还可以看到每个设备条目中日期前面有两个数字,这些是设备的主次设备号。例如,120是chardev的主设备号(MajorNumber),1是chardev的次设备号(Minor Number)。通常来说,主设备号用于表示设备文件所使用的驱动,次设备号用于标识同一个驱动所服务的不同设备文件。

linux内核使用dev_t数据类型来存储主次设备号,该数据类型的定义在中给出。dev_t有32位比特,其中12个比特存储主设备号,20个比特存储次设备号。如果需要获得主次设备号,则需要通过中定义的两个函数来获取,分别是:MAJOR(dev_tdev)与MINOR(dev_t dev)。反之,如果需要将自定义的主次设备号转换成dev_t类型,则可以使用函数MKDEV(int major, int minor)。

三、主次设备号的分配与释放

实现字符设备驱动的第一步是分配与该驱动关联的主次设备号,完成这一操作的函数定义在中,函数名为register_chrdev_region,函数原型如下:

intregister_chrdev_region(dev_t first, unsigned int count, char *name)

在register_chrdev_region函数中,参数first是字符设备所使用设备号中的第一个设备号,通常来说first参数的次设备号部分为0,但这并不是强制要求。参数count是所请求设备号的总个数。参数name是与设备号相关联的字符设备名称,该名称将会出现在/proc/devices与sysfs中。与大多数内核函数一样,如果register_chrdev_region函数的返回值为0,说明设备号分配成功,如果该函数返回负值,则说明设备号分配失败。

值得一提的是,使用register_chrdev_region函数需要提前知道系统当前未使用的主设备号,如果出现主设备号冲突的情况会造成设备号分配失败。/proc/devices文件列举了正在使用的主设备号,从中选择一个未使用的主设备号即可。

当需要卸载字符设备驱动时,需要释放刚才所获取的设备号,可以通过unregister_chrdev_region函数来完成该操作,函数原型如下:

unregister_chrdev_region(dev_tfirst, unsigned int count)

通常unregister_chrdev_region函数放置在模块的退出函数中。

四、字符设备的重要数据结构体

本节主要介绍与字符设备驱动紧密相关的三个数据结构体,分别是file_operations,file与inode。

file_operations

file_operations结构体的定义在中给出,主要作用是将字符设备驱动的操作函数与字符设备的设备号相关联。下面我们会介绍该结构体中重要的成员,在这些成员中会发现一些指针参数包含一个特殊的字符串__user,该字符串主要作用是告知大家该指针指向用户空间的地址,不能被内核解析。在模块程序编译的过程中,__user并不会产生任何实际作用,但可以帮助linux内核找出用户空间的地址误用错误。

下面我们介绍file_operations结构体中重要的成员:

struct module *owner

file_operations中的第一个成员并不是一个操作函数,而是一个指向file_operations所有者模块的指针,该字段的目的是为了防止file_operations中操作函数在执行过程中被卸载。通常来说,该字段被初始化为THIS_MODULE,其定义在中给出。

ssize_t (*read) (struct file *,char __user *, size_t, loff_t *)

该函数的功能是从内核中获取数据。如果在该函数指针为空的情况下字符设备驱动调用了该函数,则会产生-EINVAL错误。正常运行的情况下,该函数的返回值为正数,表示成功读入的字节数。

ssize_t (*write) (struct file *,const char __user *, size_t, loff_t)

该函数的功能是向内核中写入数据。如果该函数指针为空时字符设备驱动调用了该函数,则会产生-EINVAL错误。正常运行的情况下,该函数的返回值为正数,表示成功写入的字节数。

int (*open) (struct inode *,struct file *)

该函数通常是字符设备驱动所进行的第一个操作,与读写函数不同的是,该函数指针可以为空。

int (*release) (struct inode *)

该方法通常在file数据结构体被释放时使用,与open类似,release可以为空指针。

file

file结构体的定义在中给出的,该结构体表示一个打开的文件,由open函数创建,并以参数的形式传递给其他函数。当调用close函数后,该结构体会被释放。值得注意的是,这里的file结构体与用户空间的FILE指针无任何关系,FILE是标准C函数库中定义的指针,绝不会出现在内核中。file结构体中重要的成员如下:

mode_t f_mode

文件的模式通过FMODE_READ与FMODE_WRITE表明了该文件是可读、可写还是可读写的。

loff_t f_pos

该成员表示了当前文件读写的位置。loff_t有64位比特,字符设备驱动可以通过read函数与write函数来获取当前文件的位置。我们不能直接更改该参数的取值,只能通过read函数与write函数来更新该取值。

struct file_operations *f_op

该成员说明了与file相关联的操作函数。f_op中的取值并不会保存在内核中,这意味着我们可以在根据设备文件的不同来改变文件操作函数,也说明我们可以在不增加系统调用负载的情况下,在同一个主设备号中使用多种文件操作行为。

void *private_data

在调用open方法前,该指针会被设置为空。这里我们可以随便设置该成员,我们可以让该指针指向已分配好的数据,但我们需要记住在release方法中释放该数据的存储空间。private_data经常用于存储系统调用的状态信息。

inode

inode结构体主要用于在内核中表示文件,与file不同的是,file表示的是已经打开的文件,也就是说我们可以针对同一个文件建立多个file结构体,但所有这些file结构体都指向同一个inode结构体。

inode结构体中包含大量与文件相关的信息,但通常我们只关心如下两个成员:

dev_t i_rdev

该成员存储了设备文件的真实设备号。

struct cdev *i_cdev

cdev结构体在内核中表示字符设备。

此外,内核还提供了两种宏从inode中获取主次设备号,分别是:

unsigned int iminor(struct inode*inode)

unsigned int imajor(struct inode*inode)

五、字符设备的注册

正如之前所述,内核中使用cdev结构体代表字符设备。在调用字符设备的相关操作函数前,我们首先要分配并注册一个或多个cdev结构体,cdev的相关注册函数包含在中。

目前有两种方法可以分配并初始化cdev结构体,如果需要在模块运行过程中初始化一个独立的cdev结构体,可以通过如下代码实现:

struct cdev *my_dev =cdev_alloc();

my_cdev -> ops = &my_fops;

有时候我们也需要将cdev嵌入在我们自己的设备结构体中,在这种情况下我们需要提前初始化cdev结构体,初始化方法如下:

void cdev_init(struct cdev *cdev,struct file_operation *fops);

无论采用上述哪种方法来初始化cdev结构体,cdev结构体中的owner成员都应该被设置为THIS_MODULE。

完成cdev结构体的初始化后,最后一步就是要告诉内核关于该结构体的信息,我们可以使用如下函数:

int cdev_add(struct cdev *dev,dev_t num, unsigned int count)

这里dev是cdev结构体,num是该设备的第一个设备号,count该设备所关联的所有设备数量,通常来说count被设置为1。如果cdev_add返回值为负数,说明该设备并未加入到系统内核中。此外我们还应该确保我们的驱动器可以处理所需的所有操作后,再调用cdev_add。

如果要将字符设备从系统中移除,则需要调用如下函数:

void cdev_del(struct cdev *dev)

六、字符设备的重要方法

open方法

open方法主要用于驱动器在准备阶段完成初始化操作,为以后的操作函数提供便利。在大多数驱动器中,open方法应该完成如下任务:

  1. 检查设备相关的错误;
  2. 如果设备是第一次被启动,则初始化设备;
  3. 更新f_op指针;
  4. 分配并填充需要放入private_data中的数据。

release方法

release方法的功能是逆转open方法,有时我们也会发现该方法会调用device_close,而不是device_release。不管用何种方法,release方法主要完成如下任务:

  1. 撤销private_data;
  2. 关闭设备。

read方法与write方法

read与write方法所进行的操作类似,即完成内核空间与用户空间的数据交互,这两个方法的函数原型如下:

ssize_t read(struct file *filp,char __user *buff, size_t count, loft_t *offp)

ssize_t write(struct file *filp,const char __user *buff, size_t count, loff_t *offp)

上述两种方法中,filp是文件指针,count是所需传递的数据量,buff参数指向了缓存读写数据的用户缓存空间,offp表明了当前用户接入文本的位置。

值得注意的是,read与write方法中的buff参数是用户空间的指针,内核并不能直接解析该地址。因此我们需要通过内核提供的函数来读取buff参数所指向的缓存空间。这里我们可以使用两个定义在中的内核函数,分别是put_user()函数与get_user()函数。对于实际的设备方法而言,read方法的任务是将数据从设备文件读取到用户空间(使用put_user),而write方法则是将数据从用户空间写入到设备(使用get_user)。

七、示例程序

字符设备驱动代码下载方式如下:

链接:https://pan.baidu.com/s/13-0DWZY17huWPFRVjkY64g

提取码:kqyj

将上述代码下载至树莓派后,按下图命令运行即可:

f6737905dcf887e9416418117fd85211.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值