Linux驱动---字符设备

一、基础简介

1.1、Linux设备驱动分类

有一句话,相信大家一定不会感觉到陌生---“Linux下一切皆是文件”!所以我们可以这样理解,Linux内核会将设备抽象成文件,然后我们通过文件I/O就可以对设备进行操作。而Linux内核又按照访问特性将其分成三类:字符设备、块设备、网络设备

  • 字符设备:在数据读取操作时,以字节为单位进行的,比如串口、LED、蜂鸣器等等。
  • 块设备:在数据读取操作时,以块或扇区为单位进行的,比如硬盘、U盘、eMMC等等。
  • 网络设备:通过数据包传输的设备,比如以太网卡、无线网卡等。这类设备在/dev/下没有对应的设备节点,如果想要查看,需要使用 ifconfig 。

1.2、字符设备驱动概念

接下来,我们将从最简单的字符设备入手,开始学习驱动的概念。我们需要先了解一下Linux下的应用程序是如何调用驱动程序的,其关系如图所示:



我们的驱动程序成功加载后,会在 /dev 目录下生成一个对应的文件,应用程序通过这个名为 /dev/xxx 的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫 /dev/led 这个文件,我们在应用程序中调用了open()函数,它会通过系统调用从用户空间切换到内核空间,在执行驱动程序中对应的open()函数,从而实现了对硬件的操作。

我们会发现,每一个系统调用都会有一个与之对应的驱动函数。说到这里,就必须要提到file_operations结构体,此结构体就是Linux内核操作函数的集合,会在 3.3.1 进行详细整理。

二、驱动基本组成

在正式写驱动代码前,我们需要知道驱动程序必不可少的几部分,这也是与应用程序不同的地方。

2.1、驱动模块的加载和卸载

Linux驱动有两种运行方式:1、将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序;2、将驱动编译成模块(Linux下模块拓展名为.ko),在Linux内核启动之后,通过 insmod 命令加载内核模块

我们平时调试的时候一般都选择第二种方法,因为在调试过程中我们只需要加载或者卸载驱动模块即可,不需要重新编译整个内核。
我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数


module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用 insmod 命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用 rmmod 命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

2.2、添加LICENNSE以及其他信息

Linux 是以 GNU 通用公共版权( GPL )的版本 2 作为许可的,所以LICENSE是必须添加的!模块的作者等其他信息是可选择性添加的。

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

三、字符设备驱动开发步骤

当我们了解了字符设备驱动的基础知识,我们就要开始学习字符设备设备的开发步骤了。与应用层开发不同,驱动开发的框架是固定的,所以学习框架是十分重要的!

3.1、分配主次设备号

3.1.1 主次设备号

Linux中,每个设备都有一个设备号。设备号由两部分组成,分别是主设备号和次设备号。主设备号用于标识某一个具体的驱动,次设备号用于标识使用该驱动的某一个设备。在编写Linux内核驱动时,每个设备都要有一个独一无二的设备号(包括主、次设备号),它通常使用 dev_t 类型(在<linux/types.h>中)来定义。

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;


我们可以看到,dev_t是一个32位的数据,其中高12位为主设备号(0~4095),低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))


其中,宏MAJOR用于从dev_t中获取主设备号;宏MINOR用于从dev_t中获取次设备号;宏MKDEV用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号

3.1.2静态注册设备号

我们可以通过 cat /proc/devices  来查看所有已被系统使用的设备号,我们可以选择一个未被使用的设备号来进行静态注册,其中静态注册设备号的API函数如下:

int register_chrdev_region(dev_t first, unsigned int count, char *name);
//first:要分配的起始设备号,其为 dev_t 类型,可以由 MKDEV() 宏来生成 。first的次编号部分通常是从0开始,但不是强制的
//count:请求分配的设备号的总数
//name:设备名称
//成功返回值是0。出错的情况下,返回一个负的错误码

3.1.3动态注册设备号

我们可以使用动态注册一个设备号,在根据宏来获取它的主次设备号,其中动态注册设备号的API函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//dev:这是一个输出参数,用来保存申请到的 dev_t 类型设备号。这样我们可以使用 MAJOR() 宏从它里面提取出相应设备的主设备号
//baseminor:传入给内核的次设备号起始值,通常次设备号从0开始编号
//count:要申请的设备号数量
//name:设备名称
//成功返回值是0。出错的情况下,返回一个负的错误码


动态分配的缺点是我们无法提前创建设备节点,因为分配给我们的主设备号会发生变化,只能通过查看 /proc/devices 文件才能知道它的值,然后再创建设备节点。

3.1.4释放设备号

通常我们在驱动安装时会申请主、次设备号,那很显然我们应该在驱动卸载时应该释放主次设备号。设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count)
//from:要释放的设备号
//count:表示从 from 开始,要释放的设备号数量

3.2、文件操作函数fops设置

我们在上文提到了Linux内核操作函数的集合---file_operations结构体,接下来我们详细整理一下。

点击查看代码
#include <linux/fs.h>

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 *);
	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 (*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 *);
	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
}

挑几个重要的整理一下:

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

struct module *owner;


当用户打开设备文件时,内核会调用此函数。通常用于初始化设备资源。成功返回 0,失败返回负的错误码。

int (*open) (struct inode *, struct file *)
//inode用于存储文件或目录的元数据,每个文件或目录在文件系统中都有一个唯一的 inode。其中dev_t i_rdev指向设备号;struct cdev   *i_cdev指向字符设备的地址
//file用于表示一个已打开的文件,其中f_inode指向了文件的inode节点,f_op指向了file_operations


从设备读取数据。成功返回实际读取的字节数,失败返回负的错误码。

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
//char __user *buf:用户空间的缓冲区,用于存放读取的数据。
//size_t count:要读取的数据长度。
//loff_t *pos:文件的当前位置。


向设备写入数据。成功返回实际写入的字节数,失败返回负的错误码。

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
//const char __user *buf:用户空间的缓冲区,包含要写入的数据。
//size_t count:要写入的数据长度。
//loff_t *pos:文件的当前位置。


当用户关闭设备文件时,内核会调用此函数。通常用于释放设备资源。成功返回 0,失败返回负的错误码。

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


用于设备控制操作,例如设置设备参数、获取设备状态等。成功返回 0 或正的值,失败返回负的错误码。

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
//unsigned int cmd:控制命令。
//unsigned long arg:命令参数。

3.2.2数据交互

在字符设备进行读写操作时,copy_from_user 和 copy_to_user 这两个函数十分重要,它们用于将数据从用户空间传输到内核空间,或将数据从内核空间传输到用户空间,其原型如下:

long copy_from_user(void *to, const void __user *from, unsigned long len);
//to: 指向内核空间的目标地址,数据将复制到这个地址。
//from: 指向用户空间的源地址,要从该地址读取数据。
//len: 要复制的字节数。
//成功返回未复制的字节数(如果返回值为零,表示完全成功复制),如果发生错误(例如,访问无效的用户空间地址),则返回未复制的字节数。

long copy_to_user(void __user *to, const void *from, unsigned long len);
//to: 指向用户空间的目标地址,数据将被复制到这个地址。
//from: 指向内核空间的源地址,要从该地址读取数据。
//len: 要复制的字节数。
//成功返回未复制的字节数(如果返回值为零,表示完全成功复制)。如果发生错误(例如,访问无效的用户空间地址),则返回未复制的字节数。

3.2.3ioctl实现

在Linux系统设备驱动中,并不是所有的设备的操作都适合通过标准的 read、write、open 和 close 系统调用完成。如 Led 灯的驱动,它就不适合使用 write() 来控制 Led 灯的亮灭,在这种情况下通常使用 ioctl() 会更加合适,所以学习了解 Linux 系统下的 ioctl() 系统调用实现非常有必要。

我们先看一下系统调用ioctl()的原型:

int ioctl(int fd, unsigned long request, ...);

其中 request 表示要执行的操作。它告诉内核应该执行哪种类型的控制操作,这个命令通常是通过宏定义的。 我们看 request 的数据类型发现,它是一个32位数字,主要分为四部分。

  • direction:表示ioctl命令的访问模式,分为无数据(_IO)、读数据(_IOR)、写数据(_IOW)、读写数据(_IOWR) 四种模式。
#define _IO(type, nr)            _IOC(_IOC_NONE,  type, nr, 0)
#define _IOR(type, nr, size)     _IOC(_IOC_READ, type, nr, size)
#define _IOW(type, nr, size)     _IOC(_IOC_WRITE, type, nr, size)
#define _IOWR(type, nr, size)    _IOC(_IOC_READ | _IOC_WRITE, type, nr, size)
  • type:即魔术字(8位),表示设备类型,可以是任意一个 char 型字符,不过有很多魔术字在Linux 内核中已经被使用了,如 S 代表串口设备、B代表块设备。
  • nr:命令编号/序数,取值范围0~255,在定义了多个ioctl命令的时候,通常从0开始顺次往下编号。
  • size:占据13bit或14bit,这个与体系有关,arm使用14bit。

3.3、字符设备结构的分配和初始化

3.3.1分配cdev 结构体

每个字符设备在内核中都对应一个 struct cdev 结构体,该结构体同样有两种方式来获取,一种是静态定义,另外一种是使用 cdev_alloc() 函数来动态分配。其中 cdev 结构体定义在/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的API函数如下:

struct cdev *cdev_alloc(void);
//成功时返回指向分配的 struct cdev 的指针;失败时返回 NULL

3.3.2初始化cdev结构体

分配好的 cdev 结构体我们需要进行初始化才能使用,初始化的API函数如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
//cdev :要初始化的 cdev 结构体变量
// fops :字符设备文件操作函数集合(file_operations结构体)

3.3.3注册字符设备

接下来,我们需要将初始化好的 cdev 注册到内核中,使设备能够被用户空间访问。其中向内核注册的API函数如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
// p :指向要添加的字符设备(cdev 结构体变量)
// dev :设备所使用的设备号
//count :添加的设备数量
//成功时返回 0。失败时返回负的错误码

3.3.4注销字符设备

在卸载驱动的时候一定要删除Linux中对应的字符设备,其中注销的API函数如下:

void cdev_del(struct cdev *p)
// p :要删除的字符设备

3.4、创建设备节点

我们的驱动程序需要提供接口供应用空间程序使用(这个接口就是我们说的设备节点),我们可以手动使用 mknod 创建设备节点,但这样的话效率会比较低。我们可以直接在驱动程序中实现自动创建设备节点,这样模块在成功加载后,会自动在 /dev 下创建对应的设备节点。

3.4.1创建和删除类

创建一个新的设备类。设备类是一种抽象,它使得多个设备可以按照一定的规则进行组织。创建类的函数原型如下:

struct class *class_create(struct module *owner, const char *name);
//owner: 模块所有者,通常是THIS_MODULE
//name: 类的名称,该名称将用于创建设备文件时的路径,通常是 /sys/class/<name>
//如果成功,返回一个指向创建的 struct class 的指针。如果失败,返回 NULL,此时可以使用 ptr_err() 或 IS_ERR() 来检查错误。


卸载驱动程序的时候我们需要删除掉类,删除类的函数原型如下:

void class_destroy(struct class *cls);

3.4.2创建和删除设备文件

接下来我们要在 /dev 目录下创建设备文件,使得用户空间可以通过文件操作接口访问设备。它会将设备与一个已创建的设备类相关联。创建设备文件的函数原型如下:

struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...)
//class: 设备类,通常是通过 class_create 创建的类。
//parent: 设备的父设备,如果没有可以传 NULL。
//devt: 设备号,通常是通过 MKDEV() 宏生成的主次设备号。
//drvdata: 指向驱动数据的指针,通常是设备特有的私有数据。
//fmt: 设备文件的名称,通常是 /dev/<fmt>。
//如果成功,返回一个指向 struct device 的指针。如果失败,返回 NULL。


同样的,卸载驱动的时候需要删除掉创建的设备,设备删除的函数原型如下:

void device_destroy(struct class *class, dev_t devt)

四、代码演示

上文已经将Linux下字符设备的驱动框架主要知识点整理出来了,接下来我将在自己的Ubuntu下通过代码进行演示。

4.1、驱动部分演示

4.1.1驱动代码

vim chrdev.c
点击查看代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/version.h>
#include <linux/uaccess.h>
#include <linux/moduleparam.h>
#include <linux/ioctl.h>
#include <linux/device.h>

/* device name and major number */
#define DEV_NAME			"chrdev"
int							dev_major = 0;

module_param(dev_major, int, S_IRUGO);

#define BUF_SIZE			1024

/* Encapsulate device information and data buffers in character device drivers */
typedef struct chrdev_s
{
	struct cdev		cdev;
	struct class	*class;
	struct device	*device;
	char			*data;			/* data buffer */
	uint32_t		size;			/* data buffer size */
	uint32_t		bytes;			/* data bytes in the buffer */
}chrdev_t;

static struct chrdev_s		dev;

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 0, 0)
#define access_ok_wrapper(type, arg, cmd)	access_ok(type, arg, cmd)
#else
#define access_ok_wrapper(type, arg, cmd)	access_ok(arg, cmd)
#endif

/* ioctl definitions, use 'c' as magic number  */
#define CHR_MAGIC			'c'
#define CHR_MAXNR			2
#define CMD_READ			_IOR(CHR_MAGIC, 0, int)
#define CMD_WRITE			_IOW(CHR_MAGIC, 1, int)

static ssize_t chrdev_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos)
{
	struct chrdev_s			*dev = file->private_data;
	ssize_t					nbytes;
	ssize_t					rv = 0;

	/* no data in buffer */
	if( !dev->bytes )
		return 0;

	/* copy data to user space */
	nbytes = count>dev->bytes ? dev->bytes : count;
	if( copy_to_user(buf, dev->data, nbytes) )
	{
		rv = -EFAULT;
		goto out;
	}

	/* update return value and data bytes in buffer */
	rv = nbytes;
	dev->bytes -= nbytes;

out:
	return rv;
}

static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
	struct chrdev_s			*dev = file->private_data;
	ssize_t					nbytes;
	ssize_t					rv = 0;

	/* no space left */
	if( dev->bytes >= dev->size )
		return -ENOSPC;

	/* check copy data bytes */
	if( dev->size - dev->bytes < count )
		nbytes = dev->size - dev->bytes;
	else
		nbytes = count;

	/* copy data from user space */
	if( copy_from_user(&dev->data[dev->bytes], buf, nbytes) )
	{
		rv = -EFAULT;
		goto out;
	}

	/* update return value and data bytes in buffer */
	rv = nbytes;
	dev->bytes += nbytes;

out:
	return rv;
}

static int chrdev_open(struct inode *inode, struct file *file)
{
	struct chrdev_s		*dev;

	/* get the device struct address by container_of() */
	dev = container_of(inode->i_cdev, struct chrdev_s, cdev);

	/* save the device struct address for other methods */
	file->private_data = dev;

	return 0;
}

static int chrdev_close(struct inode *node, struct file *file)
{
	return 0;
}

static long chrdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	static int 	value = 0xdeadbeef;
	int			rv    = 0;

	if(_IOC_TYPE(cmd) != CHR_MAGIC)
		return -ENOTTY;
	if(_IOC_NR(cmd) > CHR_MAXNR)
		return -ENOTTY;

	/* Checks whether the user space can be written to or read from the operation flag */
	if(_IOC_DIR(cmd) & _IOC_READ)
		rv = !access_ok_wrapper(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
	else if(_IOC_DIR(cmd) & _IOC_WRITE)
		rv = !access_ok_wrapper(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));

	if( rv )
		return -EFAULT;

	switch(cmd){
		case CMD_READ:
			if(copy_to_user((int __user *)arg, &value, sizeof(value)))
				return -EFAULT;
			break;

		case CMD_WRITE:
			if(copy_from_user(&value, (int __user *)arg, sizeof(value)))
				return -EFAULT;
			break;

		default:
			return -EINVAL;
	}

	return 0;
}

static struct file_operations		chrdev_fops = {
	.owner			= THIS_MODULE,
	.open			= chrdev_open,
	.read			= chrdev_read,
	.write			= chrdev_write,
	.unlocked_ioctl = chrdev_ioctl,
	.release		= chrdev_close,
};

static int __init chrdev_init(void)
{
	dev_t		devno;
	int			rv;

	/* malloc and initial device read/write buffer */
	dev.data = kmalloc(BUF_SIZE, GFP_KERNEL);
	if( !dev.data )
	{
		printk(KERN_ERR"%s driver kmalloc() failed\n", DEV_NAME);
		return -ENOMEM;
	}
	dev.size = BUF_SIZE;
	dev.bytes = 0;
	memset(dev.data, 0, dev.size);

	/* allocate device number */
	if(0 != dev_major)
	{
		devno = MKDEV(dev_major, 0);
		rv = register_chrdev_region(devno, 1, DEV_NAME);
	}
	else
	{
		rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
		dev_major = MAJOR(devno);
	}

	if(rv < 0)
	{
		printk(KERN_ERR"%s driver can't use major %d\n", DEV_NAME, dev_major);
		return -ENODEV;
	}

	/* initialize cdev and setup fops */
	cdev_init(&dev.cdev, &chrdev_fops);
	dev.cdev.owner = THIS_MODULE;

	/* register cdev to linux kernel */
	rv = cdev_add(&dev.cdev, devno, 1);
	if( rv )
	{
		rv = -ENODEV;
		printk(KERN_ERR"%s driver regist failed, rv=%d\n", DEV_NAME, rv);
		goto failed1;
	}

	/* create device node in user space */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 5, 0)
	dev.class = class_create(DEV_NAME);
#else
	dev.class = class_create(THIS_MODULE, DEV_NAME);
#endif
	if(IS_ERR(dev.class))
	{
		rv = PTR_ERR(dev.class);
		goto failed2;
	}
	
	dev.device = device_create(dev.class, NULL, MKDEV(dev_major, 0), NULL, "%s%d", DEV_NAME, 0);
	if( !dev.device )
	{
		rv = -ENODEV;
		printk(KERN_ERR"%s driver create device failed\n", DEV_NAME);
		goto failed3;
	}

	printk(KERN_INFO"%s driver on major[%d] installed.\n", DEV_NAME, dev_major);
	return 0;

failed3:
	class_destroy(dev.class);

failed2:
	cdev_del(&dev.cdev);

failed1:
	unregister_chrdev_region(devno, 1);
	kfree(dev.data);

	printk(KERN_ERR"%s driver installed failed.\n", DEV_NAME);
	return rv;
}

static void __exit chrdev_exit(void)
{
	device_del(dev.device);
	class_destroy(dev.class);
	
	cdev_del(&dev.cdev);
	unregister_chrdev_region(MKDEV(dev_major, 0), 1);
	
	kfree(dev.data);

	printk(KERN_INFO"%s driver removed!\n", DEV_NAME);
	return;
}

module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("XiaoXin<13591695723@163.com>");

4.1.2驱动编译

这里我们可以通过 Makefile 来自动化编译我们的驱动程序。

vim Makefile
点击查看代码
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m += chrdev.o

modules:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	@make clear

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
make

4.1.3安装驱动

我们需要先确定  /dev 下没有同名设备节点,如果有,我们需要先删除该设备节点。

sudo rm -f /dev/chrdev0


接下来我们在进行安装驱动。

sudo insmod chrdev.ko


驱动安装成功之后,我们会发现系统自动创建了设备节点文件--- /dev/chrdev0 。在移除该设备驱动后,此设备节点也会被自动移除。

4.2、应用空间程序测试

4.2.1测试代码

最后,我们在应用空间写一段程序,通过访问刚刚创建的设备节点来验证驱动的读功能、写功能和 ioctl 是否有问题。

vim chrdev_test.c
点击查看代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define CHR_MAGIC		'c'
#define CMD_READ		_IOR(CHR_MAGIC, 0, int)
#define CMD_WRITE		_IOW(CHR_MAGIC, 1, int)

int main (int argc, char **argv)
{
    char      *devname = "/dev/chrdev0";
    char       buf[1024];
    int        rv = 0;
    int        fd;
	int		   value;

    fd = open(devname, O_RDWR);
    if( fd < 0 )
    {
        printf("Open device %s failed: %s\n", devname, strerror(errno));
        return 1;
    }

    rv = write(fd, "Hello", 5);
    if( rv< 0)
    {
        printf("Write data into device failed, rv=%d: %s\n", rv, strerror(errno));
        rv = 2;
        goto cleanup;
    }
    printf("Write %d bytes data okay\n", rv);

    memset(buf, 0, sizeof(buf));
    rv = read(fd, buf, sizeof(buf));
    if( rv< 0)
    {
        printf("Read data from device failed, rv=%d: %s\n", rv, strerror(errno));
        rv = 3;
        goto cleanup;
    }
    printf("Read %d bytes data: %s\n", rv, buf);

	if(ioctl(fd, CMD_READ, &value) < 0)
	{
		printf("ioctl() faile:%s\n", strerror(errno));
		goto cleanup;
	}
	printf("Default value in driver:0x%0x\n", value);

	value = 0x12345678;
	if(ioctl(fd, CMD_WRITE, &value) < 0)
	{
		printf("ioctl() failed:%s\n", strerror(errno));
		goto cleanup;
	}
	printf("write value into driver:0x%0x\n", value);

	value = 0;
	if(ioctl(fd, CMD_READ, &value) < 0)
	{
		printf("ioctl() failed:%s\n", strerror(errno));
		goto cleanup;
	}
	printf("Read value from driver:0x%0x\n", value);

cleanup:
    close(fd);
    return rv;
}

4.2.2编译测试

我们在运行程序时一定要加上 sudo 权限,因为设备节点是属于 root 的,普通用户一般没有权限操作这些设备。

gcc chrdev_test.c -o chrdev_test
sudo ./chrdev_test


程序执行后,出现下图这样,就证明我们的驱动时没有问题的。

五、 总结

最后根据我的理解,画一张草图方便大家记住字符设备的驱动框架。做驱动开发,框架非常重要!

本文作者:小信嵌梦

本文链接:https://www.cnblogs.com/Xin-Code9/p/18705239

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   小信嵌梦  阅读(57)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起