目录

一、基础简介

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

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

五、 总结

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

Linux驱动---字符设备的更多相关文章

  1. linux驱动---字符设备的注册register_chrdev说起

    首先我们在注册函数里面调用了register_chrdev(MEM_MAJOR,"mem",&memory_fops),向内核注册了一个字符设备. 第一个参数是主设备号,0 ...

  2. linux kernel 字符设备详解

    有关Linux kernel 字符设备分析: 参考:http://blog.jobbole.com/86531/ 一.linux kernel 将设备分为3大类,字符设备,块设备,网络设备. 字符设备 ...

  3. Linux高级字符设备驱动

    转载:http://www.linuxidc.com/Linux/2012-05/60469p4.htm 1.什么是Poll方法,功能是什么? 2.Select系统调用(功能)      Select ...

  4. Linux实现字符设备驱动的基础步骤

    Linux应用层想要操作kernel层的API,比方想操作相关GPIO或寄存器,能够通过写一个字符设备驱动来实现. 1.先在rootfs中的 /dev/ 下生成一个字符设备.注意主设备号 和 从设备号 ...

  5. linux 高级字符设备驱动 ioctl操作介绍 例程分析实现【转】

    转自:http://my.oschina.net/u/274829/blog/285014 1,ioctl介绍 ioctl控制设备读写数据以及关闭等. 用户空间函数原型:int ioctl(int f ...

  6. 【驱动】linux设备驱动·字符设备驱动开发

    Preface 前面对linux设备驱动的相应知识点进行了总结,现在进入实践阶段! <linux设备驱动入门篇>:http://infohacker.blog.51cto.com/6751 ...

  7. Linux高级字符设备驱动 poll方法(select多路监控原理与实现)

    1.什么是Poll方法,功能是什么? 2.Select系统调用(功能)      Select系统调用用于多路监控,当没有一个文件满足要求时,select将阻塞调用进程.      int selec ...

  8. linux学习--字符设备驱动

    目录 1.字符设备驱动抽象结构 2.设备号及设备节点 2.1 设备号分配与管理 2.2 设备节点的生成 3.打开设备文件 linux驱动有基本的接口进行注册和卸载,这里不再做详细说明,本文主要关注li ...

  9. linux driver ------ 字符设备驱动 之 “ 创建设备节点流程 ”

    在字符设备驱动开发的入门教程中,最常见的就是用device_create()函数来创建设备节点了,但是在之后阅读内核源码的过程中却很少见device_create()的踪影了,取而代之的是device ...

  10. 【转】Linux高级字符设备之Poll操作

    原文网址:http://www.cnblogs.com/geneil/archive/2011/12/04/2275559.html 在用户程序中,select()和poll()也是与设备阻塞与非阻塞 ...

随机推荐

  1. PHP之soap

    扩展安装: 1.下载源码包 cd /root & wget -O php7.1.27.tar.gz http://cn2.php.net/get/php-7.1.27.tar.gz/from/ ...

  2. 为了改一行代码,我花了10多天时间,让性能提升了40多倍---Pascal架构GPU在vllm下的模型推理优化

    ChatGPT生成的文章摘要 这篇博客记录了作者在家中使用Pascal显卡运行大型模型时遇到的挑战和解决方案.随着本地大型模型性能的提升,作者选择使用vllm库进行推理.然而,作者遇到了多个技术难题, ...

  3. StarBlog博客Vue前端开发笔记:(4)使用FontAwesome图标库

    前言 在现代前端开发中,图标已成为构建用户友好界面的重要元素.Font Awesome 是全球最流行的图标库之一,提供了大量的矢量图标,支持多种平台和框架.无论是网站.应用程序,还是管理面板,Font ...

  4. 【Amadeus原创】Docker安装wikijs wiki系统

    拉取mysql8的镜像并运行 docker pull mysql docker run -d -v /data/mysql/data:/var/lib/mysql -v /data/mysql/con ...

  5. C/C++实例汇集(1)

    1.用代码判断一个系统是16位系统还是32位系统? 以下是几种常见编程语言中判断系统是 16 位还是 32 位的代码示例 C语言: #include <stdio.h> int main( ...

  6. 痞子衡嵌入式:MCUXpresso IDE下C++源文件中嵌套定义的复合数据类型命名空间认定

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是MCUXpresso IDE下C++源文件中嵌套定义的复合数据类型命名空间认定. 痞子衡之前写过一篇文章 <MCUXpresso ...

  7. Vue-Router 面试题 (2023-09-13更新)

    路由导航守卫 和 Vue 实例生成周期钩子函数的执行顺序? 路由导航守卫 都是在 Vue 实例生命周期钩子函数 之前执行的 Vue-Router 有哪几种导航钩子? 1. 全局守卫 全局前置守卫:be ...

  8. 准备好"干翻世界"之前,不妨先听听我们的意见

    期待大家的到来哦~    

  9. 使用archlinux AUR源下载安装的方法 以及 解决makepkg网络连接超时(time out)的问题

    1.使用archlinux(AUR)源下载安装软件/驱动的方式 2.解决使用此方时无法通过网络下载资源文件的问题(网络连接超时/time out) 1.使用archlinux(AUR)源下载安装软件/ ...

  10. rsync+ssh同步备份文件

    定期对web代码或重要的文件做同步异地服务器备份,防止服务器故障严重磁盘损坏时文件丢失的问题. 备份服务器:192.168.200.134 目标服务器:192.168.201.65 rsync同步命令 ...