Linux驱动---字符设备

Linux驱动---字符设备

目录一、基础简介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创建和删除设备文件四、代码演示4.1、驱动部分演示4.1.1驱动代码4.1.2驱动编译4.1.3安装驱动4.2、应用空间程序测试4.2.1测试代码4.2.2编译测试五、 总结

一、基础简介

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 类型(在中)来定义。

typedef __u32 __kernel_dev_t;

typedef __kernel_dev_t dev_t;

我们可以看到,dev_t是一个32位的数据,其中高12位为主设备号(0~4095),低20位为次设备号。在驱动编程中,我们不应该管哪些位是主设备号,哪些位是次设备号,而应该统一使用 中的一套宏设置/获取一个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

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/

//如果成功,返回一个指向创建的 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/

//如果成功,返回一个指向 struct device 的指针。如果失败,返回 NULL。

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

void device_destroy(struct class *class, dev_t devt)

四、代码演示

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

4.1、驱动部分演示

4.1.1驱动代码

vim chrdev.c

点击查看代码

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

/* 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

#include

#include

#include

#include

#include

#include

#include

#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

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

五、 总结

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

相关推荐

宝宝辅食泥全攻略:从制作到保存的实用指南
365真人注册

宝宝辅食泥全攻略:从制作到保存的实用指南

📅 10-10 👁️ 7879
C罗向乔治娜求婚成功!巨型鸽子蛋钻戒太抢镜
365真人注册

C罗向乔治娜求婚成功!巨型鸽子蛋钻戒太抢镜

📅 11-28 👁️ 4402
桔子商城分期APP下载指南
365bet官方投注网站

桔子商城分期APP下载指南

📅 07-30 👁️ 5437