版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/ 发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2021 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

3.3 Linux设备驱动简介

在单片机驱动编程中,这些硬件的设备驱动由我们自己编写,然后在代码中由我们自己调用,他们没有一个统一的规范,一百个人有一百种驱动的写法。而在linux系统下编写驱动,他有严格的规范,哪些该驱动做,哪些该应用程序做;驱动程序编写要先做什么,然后再做什么、然后再做什么都有严格的定义。正因为这样的规范,所以每个人只需要注重自己的角色,做自己该做的事,这也将嵌入式Linux开发的岗位分为两个,一个是底层驱动开发的BSP(Board Support Packet)开发,另外就是应用程序(Application)开发。

对于BSP开发岗位,相关的开发人员应该了解各种硬件知识,如电路基础、各种接口技术和硬件调试工具的使用(如万用表、示波器、甚至逻辑分析仪等),此外还需要计算机组成、操作系统原理等理论基础,同时还要了解各种体系架构的CPU、汇编语言等,当然最重要的是C语言编程能力和数据结构的知识以及对Linux内核源码大量阅读和分析。

对于嵌入式应用开发的岗位,我们不需要了解底层驱动的具体实现细节,而只需要知道怎么使用他们即可,因为Linux是一个模块化、严格分成的系统,所以应用程序人员只需要了解Linux的驱动调用的统一API(Application Program Interface,应用程序编程接口)即可,这些API就是Linux的系统调用(System call),他们在《UNIX环境高级编程》这本书里较为详细的描述,对于嵌入式应用程序的开发,我们大部分使用C、C++或python(树莓派)和数据结构,根据应用程序的不同需求,我们需要补充额外的计算机网络数据库等知识。

Linux内核设计哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文件I/O来进行操作。Linux内核将设备按照访问特性分为三类:字符设备、块设备、网络设备;

设备分类

字符设备驱动程序适合于大多数简单的硬件设备,而且比起块设备或网络驱动更加容易理解,因此我们从字符设备开始慢慢熟悉驱动的软件框架。

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

设备分类

应用程序运行在用户空间,而Linux驱动属于内核一部分,因此驱动运行于内核空间,当用户想要实现对内核操作时,必须使用系统调用来实现从用户空间到内核空间的操作。

3.3.1 字符设备驱动

3.3.1.1 Linux主次设备号

字符设备通过文件系统中的设备名来存取,惯例上它们位于/dev目录。字符驱动的特殊文件由使用ls -l的输出的第一列的“c”标识。块设备也出现在/dev中,但是它们由“b”标识。如果输入ls -l命令,设备文件项中有两个数(由一个逗号分隔)在最后修改日期前面,这里通常是文件长度出现的地方。这些数字是给特殊设备的主次设备编号。其中逗号前面为主设备号,逗号后面为次设备号。

例如下列代码中,fb0设备即为字符设备,ls -l的第一个字符’c’说明它是字符(character)设备,它的主设备号为29,次设备号为0。其中fb0设备就是我们的LCD显示屏设备,从驱动层面上来讲,如果我们想让LCD显示某些内容,我们只需要open()打开该设备获取文件描述符(fd , file description),然后往里面按照相应格式write()相应数据即可,而如果我们想要截屏,只需要read()读该设备的内容就行。当然,对LCD的操作并没表面上的这么简单。

zouying@ubuntu:~$ ls -l /dev/
total 0
crw-------  1 root    root     10,  62 Aug 25 06:07 ecryptfs
crw-rw----  1 root    video    29,   0 Aug 25 06:06 fb0
lrwxrwxrwx  1 root    root          13 Aug 22 00:10 fd -> /proc/self/fd

传统上,主编号标识设备相连的驱动。例如,/dev/ecryptfs由驱动1来管理,而fb0由驱动29来管理;现代Linux内核允许多个驱动共享主编号,但是你看到的大部分设备任然按照一个主设备号一个驱动的原则来组织。

次设备号被内核来决定应用哪个设备。依据你的驱动是如何编写的,你可以从内核得到一个你的设备的直接指针,或者可以自己使用次编号作为本地设备数组的索引。不论哪个方法,内核几乎都不知道次编号的任何事情,除了他们指向你的驱动实现的设备。

在内核编程中,使用dev_t类型(在<linux/types.h>中定义)来定义设备编号,对于5.15.32内核,dev_t是32位的量,其中12位用作主编号,20位用作次编号。在编码时,我们不应该管哪些位是主设备号,哪些位是次设备号。而是应当利用在<linux/kdev_t.h>中的一套宏定义来获取一个dev_t的主、次编号:

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) 
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
  • 宏 MINORBITS 表示次设备号位数,一共是 20 位。

  • 宏 MINORMASK 表示次设备号掩码。

  • 宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。

  • 宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。

相反,如果我们有主、次编号需要将其转换为一个dev_t,则使用MKDEV宏;

  • 宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

dev_t 定义在文件 include/linux/types.h
typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;
可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里
面,定义如下:
typedef unsigned int __u32;
综上所述,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。

主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。

3.3.2 分配和释放设备编号

在建立一个字符驱动时你的驱动需要做的第一件事是获取一个或多个设备号来使用,在Linux内核里,一些主设备号是静态分配给最普通的设备的,这些设备列表在内核源码树的Documentation/devices.txt中列出。因此,作为一个驱动编写者,我们有两个选择:一是简单地捡一个看来没有用的主设备号,二是让内核以动态方式分配一个主设备号给你。只要你是你的驱动的唯一用户就可以捡一个编号用;一旦你的驱动更广泛的被使用了,一个随机捡来的主编号将导致冲突和麻烦。

因此,对于新驱动,我们强烈建议使用动态分配来获取你的主设备编号,而不是随机选取一个当前空闲的编号。

3.3.2.1 设备号分配

1、静态分配设备号

静态设备指定主次设备号是指我们根据当前系统的主设备号分配情况,自己选择一个主设备号。当然我们自己随机选择的话,会跟Linux内核其他的驱动冲突,这时我们可以先查看当前系统已经使用了哪些主设备号,然后我们选择一个没有使用的作为我们新的驱动使用。Linux系统中正在使用的主设备号会保存在/proc/devices文件中:

root@ATK-IMX6U:~# uname -a
Linux ATK-IMX6U 4.1.15-gb8ddbbc #1 SMP PREEMPT Wed Apr 29 17:39:59 CST 2020 armv7l armv7l armv7l GNU/Linux
root@ATK-IMX6U:~# cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  7 vcs
 10 misc
 13 input

上面列出的是当前运行的Linux内核里所有设备驱动使用的主设备号,此时我们在编写驱动时可以选定一个未用的主设备号,如251来使用

dev_t		devno;
int			result;
int			major = 251;

devno = MKDEV(major, 0);

result = register_chrdev_region(devno, 4, "chrdev");//静态的申请和注册设备号
if(result < 0)
{
    printk(KERN_ERR "chrdev can't use major %d\n", major);
    return -ENODEV;
}

这里register_chrdev_region()函数的原型为:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

参数:

  • first:要分配的起始设备号。first的次编号部分通常是从0开始,但不是强制的。

  • count:请求分配的设备号的总数。注意,如果count太大,你要求的范围可能溢出到下一次编号;但是只要你要求的编号范围可用,一切都任然会正确工作。

  • name:设备名字;它会出现在/proc/devices和sysfs中。

返回值:

  • register_chrdev_region的返回值:是0。出错的情况下,返回一个负的错误码。

当驱动的主、次设备号申请成功后,/proc/devices里将会出现该设备,但是/dev路径下并不会创建该设备文件。

2、主设备号动态分配

静态分配设备号就是挑选一个没有使用的设备号很容易带来冲突问题。假设将来我们Linux内核系统升级需要使能其他的设备驱动,如果某个需要的驱动所用的主设备号刚好和我们的设备驱动冲突,那么我们驱动不得不对这个主设备号进行调整,而如果产品已经部署了,这种召回升级是非常致命的。所以我们在写驱动时,不应该静态指定一个设备号,而是由Linux内核根据当前主设备号使用情况动态分配一个未用的给我们的驱动使用,这样就永远不会冲突了。

设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

函数 alloc_chrdev_region用于申请设备号,此函数有 4 个参数:

  • dev:只是一个输出参数,保存申请到的设备号。

  • baseminor:次设备号,它常常是0;

  • count:要申请的设备号数量。

  • name:设备名字。

动态分配的缺点是你无法提前创建设备节点,因为分配给你的主设备号会发生变化,对于驱动的正常使用这不是问题,但是一旦编号分配了,只能通过 查看 /proc/devices文件才能知道它的值,然后再创建设备节点。

3.3.2.2 释放主次设备号

注销字符设备之后要释放掉设备号,设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count)

此函数有两个参数:

  • from:要释放的设备号。

  • count:表示从 from 开始,要释放的设备号数量。

3.3.2.3 创建设备节点

1.手动创建设备节点

如果我们需要创建该文件,则需要使用mknod命令创建。当然我们也可以在驱动里调用相应的函数,来通知应用程序空间自动创建该设备文件。

Usage: mknod [OPTION]... NAME TYPE [MAJOR MINOR]
Create the special file NAME of the given TYPE.

输入如下命令创建/dev/chrdevbase 这个设备节点文件: mknod /dev/chrdevbase c 251 0

“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个字符设备,“251”是设备的主设备号,“0”是设备的次设备号。

如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。

2.自动创建设备节点

在刚开始写Linux设备驱动程序的时候,很多时候都是利用mknod命令手动创建设备节点,实际上Linux内核为我们提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提条件是用户空间移植了udev。

class_create()和device_create()

先来了解一下跟设备文件创建相关的两个函数。

class_create():在调用device_create()前要先用class_create()创建一个类。类这个概念在Linux中被抽象成一种设备的集合。类在/sys/class目录中。

root@igkboard:~# cd /sys/class/
root@igkboard:/sys/class# ls
ata_device  devcoredump  gpio       leds      net           pwm          scsi_host   tty          wakeup
ata_link    devlink      graphics   mdio_bus  pci_bus       rc           sound       ubi          watchdog
ata_port    dma          hwmon      mem       pci_epc       regulator    spi_master  udc
backlight   drm          i2c-dev    misc      phy           remoteproc   spi_slave   usb_role
bdi         dvb          ieee80211  mmc_host  power_supply  rtc          spidev      vc
block       extcon       input      mtd       pps           scsi_device  tee         video4linux
bluetooth   firmware     lcd  

Linux内核中有各种类,比如gpio、rtc、led等。

class_create()这个函数使用非常简单,在内核中是一个宏定义。

/include/linux/device.h中:

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

此函数有两个参数:

  • owner:struct module结构体类型的指针,一般赋值为THIS_MODULE。

  • name:char类型的指针,类名。

device_create()用于创建设备。

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;
}

参数:

  • class:该设备依附的类

  • parent:父设备

  • devt:设备号(此处的设备号为主次设备号)

  • drvdata:私有数据

  • fmt:设备名。

device_create能自动创建设备文件是依赖于udev这个应用程序。udev是一种工具,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备。

3.3.2.4 删除设备节点

函数class_destroy()用于从Linux内核系统中删除设备节点。此函数执行的效果是删除函数__class_create()或宏class_create()在/sys/class目录下创建的节点对应的文件夹。

/include/linux/device.h中:

void class_destroy(struct class *cls)
{
	if ((cls == NULL) || (IS_ERR(cls)))
	return;
 
	class_unregister(cls);
}

参数说明

  • cls:struct class结构体类型的变量,代表通过class_create创建的设备的节点。

3.3.3 字符设备重要数据结构

3.3.3.1 file_operations结构体

file_operations就是把系统调用和驱动函数关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用,相应的系统调用将读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序工作。在系统内部,I/O设备的存取操作通过特定的入口点来进行,而这组特定的入口点恰恰是由设备驱动提供的。通常这组设备驱动程序接口是由结构file_operation结构体向系统说明的,它定义在include/linux/fs.h中。传统上,一个file_operation结构或者其一个指针称为fops(或者它的一些变体),结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作,或者对于不支持的操作留置为NULL。当指定为NULL指针时内核的确切的行为是每个函数不同的。

struct file_operations {
	struct module *owner;
	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 *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsignedlong);
    long (*compat_ioctl) (struct file *, unsigned int, unsignedlong);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*mremap)(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 (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ...
};	

简单介绍一下 file_operation 结构体中比较重要的、常用的函数:

  • owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。

  • read 函数用于读取设备文件。

  • write 函数用于向设备文件写入(发送)数据。 第 1596 行,poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。

  • unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。

  • compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。

  • mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。

  • open 函数用于打开设备文件。

  • release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。

  • fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

  • aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。

3.3.3.2 inode结构体

Linux中一切皆文件,当我们在Linux中创建一个文件时,就会在相应的文件系统中创建一个inode与之对应,文件实体和文件inode是一一对应的,创建好一个inode会存在存储器中。第一次open就会将inode在内存中有一个备份,同一个文件被多次打开并不会产生多个inode,当所有被打开的文件都被close之后,inode在内存中的实例才会被释放。既然如此,当我们使用mknod(或其他方法)创建一个设备文件时,也会在文件系统中创建一个inode,这个inode和其他的inode一样,用来存储关于这个文件的静态信息(不变的信息),包括这个设备文件对应的设备号,文件的路径以及对应的驱动对象等。

对于不同的文件类型,inode被填充的成员内容也会有所不同,以创建字符设备为例,我们知道,add_chrdev_region其实是把一个驱动对象和一个(一组)设备号联系到一起。而创建设备文件,其实就是把设备文件和设备号联系在一起。至此,这三者就被绑定在一起了。这样,内核就有能力创建一个struct inode实例了。

inode一样定义在inlude/linux/fs.h文件中:

struct inode {
	umode_t			i_mode;//
	unsigned short		i_opflags;
	kuid_t			i_uid;//
	kgid_t			i_gid;//
	unsigned int		i_flags;

#ifdef CONFIG_FS_POSIX_ACL
	struct posix_acl	*i_acl;
	struct posix_acl	*i_default_acl;
#endif

	const struct inode_operations	*i_op;//
	struct super_block	*i_sb;
	struct address_space	*i_mapping;

...
 	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct file_lock_context	*i_flctx;
	struct address_space	i_data;
	struct list_head	i_devices;
    
union {
	struct pipe_inode_info	*i_pipe;
	struct block_device	*i_bdev;
	struct cdev		*i_cdev;
};

inod结构体包含大量关于文件的信息,作为一个通用规则,这个结构体只有两个成员对于编写驱动代码有用:

dev_t i_rdev; 代表设备文件的节点,这个成员包含实际的设备编号

struct cdev *i_cdev; 这个结构体代表字符设备,这个成员包含一个指针,指向这个结构体,当节点指的是一个字符设备文件时。

3.3.2.3 file结构体

file结构体代表一个打开的文件。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后的关闭。在文件的所有实例都关闭后,内核释放这个数据结构。

在内核源码中。struct file的指针常常称为file或者filp(file pointer)。 我们将一直成这个指针为filp以避免核结构自身混淆,因此,file指的是结构体,而filp指的是结构体指针。

下面是file的主要内容,它一样是定义在include/linux/fs.h文件中:

struct file {
	union {
		struct llist_node	fu_llist;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;//

...
};

3.3.4 字符设备注册

3.3.4.1 cdev结构体

内核在内部使用类型struct cdev的结构体来代表字符设备。在内核调用你的设备操作之前,你必须分配一个这样的结构体并注册给linux内核,在这个结构体里有对于这个设备进行操作的函数,具体定义在file_operation结构体中。该结构体定义在include/linux/cdev.h文件中:

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

在内核编程中,我们可以使用两种方法获取结构体。

一是运行时想获取一个独立的cdev结构:

struct cdev *chrtest;
if(NULL == chrtest = cdev_alloc())
{
	printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
	unregister_chrdev_region_region(devno, dev_vount);
	return -ENOMEM;
}

chrtest->ops = &my_fops;

但是,偶尔你会想将cdev结构体嵌入一个你自己的设备特定结构。这样的情况下你需要初始化已经分配的结构体。

cdev_init(struct cdev *dev, struct file_operations *fops);

struct cdev有一个拥有者成员,应当设置为THIS_MODULE,一旦cdev结构建立,最后的步骤就是告诉内核。

3.3.4.2 注册cdev到内核

在分配到cdev结构体后,接下来我们将它初始化,并将对该设备驱动所支持的系统调用函数存放在file_operations结构体添加进来,然后我们通过cdev_add函数将他们注册给Linux内核,这样完成整个Linux设备的注册过程。其中cdev_add的函数原型如下:

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

  • dev是cdev结构

  • num是这个设备相应的第一个设备号

  • count是应当关联到设备的设备号的数目。

下面是字符设备驱动cdev的分配和注册过程:

static struct file_operations chrtest_fods =
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .release = chrtest_release,
    .unlocked_ioctl = chrtest_ioctl,
};

struct cdev *chrtest_cdev;
if(NULL == (led_cdev = cdev_alloc))
{
    printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
    unregister_chdev_region(devno, dev_count);
    return -ENOMEM;   
}

led_cdev->owner = THIS_MODULE;
cdev_init(led_cdev, &led_fops);

result = cdev_add(led_cdev, devno, dev_count);

if(0 != result)
{
    printk(KERN_INFO "S3C %s drive can't register cdev:result = %d\n", DEV_NAME, result);
    goto ERROR;
}

3.3.5 Linux内核驱动和系统调用之间的联系

linux_syscall

3.3.6 字符设备驱动开发步骤

①相应的设备硬件初始化

②分配主次设备号,这里即支持静态指定,也支持动态申请

③分配cdev结构体,我们这里使用动态申请的方式

④绑定主次设备号、fops到cdev结构体中,并注册给Linux内核

3.3.7 字符设备驱动源码

02chrdevbase.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/* 确定主设备号 */
//#define DEV_MAJOR  79
#ifndef DEV_MAJOR	
#define DEV_MAJOR  0
#endif
int dev_major = DEV_MAJOR;	/*主设备号*/
    
#define DEV_NAME  "chrdev"	/*设备名字*/

static struct cdev *chrtest_cdev;	 /*cdev结构体*/

//static struct class *chrdev_class; /*定义一个class用于自动创建类*/

static char kernel_buf[1024];

#define MIN(a, b) (a < b ? a : b)

/*实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t chrtest_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));	/*内核空间的数据到用户空间的复制*/
	return MIN(1024, size);
}

static ssize_t chrtest_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
 	int err;
 	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
 	err = copy_from_user(kernel_buf, buf, MIN(1024, size));	/*将buf中的数据复制到写缓冲区kernel_buf中,因为用户空间内存不能直接访问内核空间的内存*/
	return MIN(1024, size);
}

static int chrtest_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int chrtest_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 定义自己的file_operations结构体*/
static struct file_operations chrtest_fops = {
	.owner   = THIS_MODULE,
	.open    = chrtest_drv_open,
	.read    = chrtest_drv_read,
	.write   = chrtest_drv_write,
	.release = chrtest_drv_close,
};

/*把file_operations结构体告诉内核:register_chrdev */
/*注册驱动函数:写入口函数,安装驱动程序时就会调用这个入口函数 */
static int __init chrdev_init(void)
{
	int result;
	dev_t devno;/*定义一个dev_t 的变量表示设备号*/

	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	/*字符设备驱动注册流程第二步:分配主次设备号,这里即支持静态指定,也至此动态申请*/
    if(0 != dev_major)	/*static*/
    {
    	devno = MKDEV(dev_major, 0);
    	result = register_chrdev_region(devno, 1, DEV_NAME);	/* /proc/devices/chrdev*/
    }
    else
    {
   		result = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);	
		dev_major = MAJOR(devno);	/*获取主设备号*/
    }
    
    /*自动分配设备号失败*/
	if(result < 0)
	{
        printk(KERN_ERR " %s driver can't use major %d\n", DEV_NAME, dev_major);
        return -ENODEV;
	}
    printk(KERN_DEBUG " %s driver use major %d\n", DEV_NAME, dev_major);
    
    
    /*字符设备驱动注册流程第三步:分配cdev结构体,我们这里使用动态申请的方式*/
	if(NULL == (chrtest_cdev = cdev_alloc()))
    {
        printk(KERN_ERR " %s driver can't alloc for the cdev\n", DEV_NAME);
        unregister_chrdev_region(devno, 1); 
        return -ENOMEM;
    }
    
    
    /*字符设备驱动注册流程第四步:分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核*/
  
    chrtest_cdev->owner = THIS_MODULE; /*.owner这表示谁拥有你这个驱动程序*/
    cdev_init(chrtest_cdev, &chrtest_fops);	/*初始化设备*/
	result = cdev_add(chrtest_cdev, devno, 1);	/*将字符设备注册进内核*/
	if(0 != result)
    {
        printk(KERN_INFO " %s driver can't register cdev:result=%d\n", DEV_NAME, result);
        goto ERROR;
    }
    printk(KERN_INFO " %s driver can register cdev:result=%d\n", DEV_NAME, result);
   
    
    /*自动创建设备类型、/dev设备节点*/
    #if 0
    chrdev_class = class_create(THIS_MODULE, DEV_NAME); /*创建设备类型 sys/class/chrdev*/
	if (IS_ERR(chrdev_class)) {
		result = PTR_ERR(chrdev_class);
		goto ERROR;
	}
    device_create(chrdev_class, NULL, MKDEV(dev_major, 0), NULL, DEV_NAME); /* /dev/chrdev 注册这个设备节点*/
    #endif
    
    
	return 0;
   
    
ERROR:
	printk(KERN_ERR" %s driver installed failure.\n", DEV_NAME);
    cdev_del(chrtest_cdev);
    unregister_chrdev_region(devno, 1); 
    return result;
	

}

/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*/
static void __exit chrdev_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    
    /* 注销设备类型、/dev设备节点*/
    #if 0
	device_destroy(chrdev_class, MKDEV(dev_major, 0));  /*注销这个设备节点*/
	class_destroy(chrdev_class);	/*删除这个设备类型*/
	#endif
    
	cdev_del(chrtest_cdev);	/*注销字符设备*/
	unregister_chrdev_region(MKDEV(dev_major,0), 1);	/*释放设备号*/
    
    printk(KERN_ERR" %s driver version 1.0.0 removed!\n", DEV_NAME);
    return;
}


/* 其他完善:提供设备信息,自动创建设备节点*/

module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GuoWenxue <guowenxue@gmail.com>");

copy_to_usercopy_from_user是在进行驱动相关程序设计的时候,要经常遇到的函数。由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成内核空间到用户空间的复制,函数copy_from_user()完成用户空间到内核空间的复制。

copy_to_user 函数来完成内核空间的数据到用户空间的复制。

static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

  • to 目标地址,这个地址是用户空间的地址;

  • from 源地址,这个地址是内核空间的地址;

  • n 将要拷贝的数据的字节数。

copy_from_user 函数完成用户空间到内核空间的复制。

unsigned long copy_from_user (void * to, const void __user * from, unsigned long n);

  • to 目标地址,这个地址是内核空间的地址;

  • from 源地址,这个地址是用户空间的地址;

  • n 将要拷贝的数据的字节数。

Makefile

KERNAL_DIR := /home/lingyun/imx6ull/bsp/kernel/linux-imx
CROSS_COMPILE := /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-   
TFTP_DTR := /tftp/lingyun
PWD := $(shell pwd)
obj-m := chrdevbase.o

modules:
        $(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
        $(CROSS_COMPILE)gcc chrdevbaseApp.c -o chrdevbaseApp
        @make clear
        cp chrdevbase.ko chrdevbaseApp $(TFTP_DTR) -f

clear:
        @rm -f *.o *.cmd *.mod *.mod.c
        @rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
        @rm -f .*ko.cmd .*.o.cmd .*.o.d
        @rm -f *.unsigned

clean:
        @rm -f *.ko                    

3.3.8 字符设备驱动测试

3.3.8.1 编译字符设备驱动

在linux开发主机上,编译Linux驱动,并安装到tftp服务器根路径下。

guowenxue@9d57f9229b66:~/driver/02chrdevbase$ ls
chrdevbaseApp  chrdevbaseApp.c  chrdevbase.c  chrdevbase.ko  Makefile

3.3.8.2 ARM板上下载并安装驱动

root@igkboard:~/dirver/02chrdevbase# ls
chrdevbase.ko  chrdevbaseApp
root@igkboard:~/dirver/02chrdevbase# uname -a
Linux igkboard 5.15.32 #1 SMP PREEMPT Mon Oct 17 10:19:02 CST 2022 armv7l armv7l armv7l GNU/Linux
root@igkboard:~/dirver/02chrdevbase# insmod chrdevbase.ko 
root@igkboard:~/dirver/02chrdevbase# lsmod
Module                  Size  Used by
chrdevbase             16384  0
rtl8188fu             991232  0
imx_rngc               16384  0
rng_core               20480  1 imx_rngc
secvio                 16384  0
error                  20480  1 secvio
root@igkboard:~/dirver/02chrdevbase# rmmod chrdevbase
root@igkboard:~/dirver/02chrdevbase# dmesg
[  948.550409] /home/lingyun/driver/02chrdevbase/chrdevbase.c chrdev_init line 78
[  948.550465]  chrdev driver use major 243
[  948.550488]  chrdev driver can register cdev:result=0
[  960.175119] /home/lingyun/driver/02chrdevbase/chrdevbase.c chrdev_exit line 145
[  960.182392]  chrdev driver version 1.0.0 removed!
root@igkboard:~/dirver/02chrdevbase# insmod chrdevbase.ko 
root@igkboard:~/dirver/02chrdevbase# cat /proc/devices|grep chrdev
243 chrdev

3.3.9 编写字符测试 APP

3.3.9.1 编写字符测试程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./chrdevbaseApp -w abc
 * ./chrdevbaseApp -r
 */
int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open("/dev/chrdev", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}

	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		write(fd, argv[2], len);
	}
	else if((0 == strcmp(argv[1], "-r")) && (argc == 2))
	{
		len = read(fd, buf, 1024);		
		buf[1023] = '\0';
		printf("APP read : %s\n", buf);
	}
	else
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}
	
	close(fd);
	
	return 0;
}



3.3.9.2 编译字符设备测试程序

guowenxue@9d57f9229b66:~/driver/02chrdevbase$ ls
chrdevbaseApp.c  chrdevbase.c  chrdevbase.ko  Makefile
guowenxue@9d57f9229b66:~/driver/02chrdevbase$ /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
guowenxue@9d57f9229b66:~/driver/02chrdevbase$ ls
chrdevbaseApp  chrdevbaseApp.c  chrdevbase.c  chrdevbase.ko  Makefile

3.3.10 ARM开发板上测试字符设备

在上述字符设备驱动的代码中我们先把自动创建设备节点的部分屏蔽掉,便于大家理解/dev下设备文件的作用。

当主、次设备号申请成功后,/proc/devices里会出现该设备,但是/dev路径下并不会创建该设备文件。

如果我们想要创建该文件,则需要使用mknod命令创建。如果想自动创建设备类型和设备节点、把屏蔽的部分代码打开即可。

root@igkboard:~/dirver/02chrdevbase# ls
chrdevbase.ko  chrdevbaseApp
root@igkboard:~/dirver/02chrdevbase# ./chrdevbaseApp 
Usage: ./chrdevbaseApp -w <string>
       ./chrdevbaseApp -r
root@igkboard:~/dirver/02chrdevbase# ./chrdevbaseApp -w hello
can not open file /dev/chrdev
root@igkboard:~/dirver/02chrdevbase# mknod -m 755 /dev/chrdev c 243 0
root@igkboard:/dev# ls
autofs           i2c-1         mmcblk1rpmb  ram3       tty11  tty30  tty5     ttymxc2             vcs
....  省略  ....
chrdev       

root@igkboard:~/dirver/02chrdevbase# ./chrdevbaseApp -w hello
root@igkboard:~/dirver/02chrdevbase# ./chrdevbaseApp -r      
APP read : hello
root@igkboard:~/dirver/02chrdevbase# ./chrdevbaseApp -w lingyun
root@igkboard:~/dirver/02chrdevbase# ./chrdevbaseApp -r        
APP read : lingyun