- 一、基础简介
- 1.1、Linux设备驱动分类
- 1.2、字符设备驱动概念
- 二、驱动基本组成
- 2.1、驱动模块的加载和卸载
- 2.2、添加LICENNSE以及其他信息
- 三、字符设备驱动开发步骤
- 3.1、分配主次设备号
- 3.1.1 主次设备号
- 3.1.2静态注册设备号
- 3.1.3动态注册设备号
- 3.1.4释放设备号
- 3.2、文件操作函数fops设置
- 3.2.2数据交互
- 3.2.3ioctl实现
- 3.3、字符设备结构的分配和初始化
- 3.3.1分配cdev 结构体
- 3.3.2初始化cdev结构体
- 3.3.3注册字符设备
- 3.3.4注销字符设备
- 3.4、创建设备节点
- 3.4.1创建和删除类
- 3.4.2创建和删除设备文件
- 3.1、分配主次设备号
- 四、代码演示
- 4.1、驱动部分演示
- 4.1.1驱动代码
- 4.1.2驱动编译
- 4.1.3安装驱动
- 4.2、应用空间程序测试
- 4.2.1测试代码
- 4.2.2编译测试
- 4.1、驱动部分演示
- 五、 总结
一、基础简介
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_MMUunsigned (*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;elsenbytes = 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);
#elsedev.class = class_create(THIS_MODULE, DEV_NAME);
#endifif(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.omodules:$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules@make clearclear:@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 *.unsignedclean:@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
程序执行后,出现下图这样,就证明我们的驱动时没有问题的。
五、 总结
最后根据我的理解,画一张草图方便大家记住字符设备的驱动框架。做驱动开发,框架非常重要!