Linux驱动---字符设备

news/2025/2/8 20:19:53/文章来源:https://www.cnblogs.com/Xin-Code9/p/18705239

目录
  • 一、基础简介
    • 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 类型(在<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


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

五、 总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/880957.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2.8 系统基本完成

今天将系统进行完善并通过了测试。 部分界面:数据库成功插入:在测试过程中遇到了一个问题:数据库中查到的某些元素无法正确传递(即为null值),在多次检查测试后发现是命名格式问题(我的程序主要是因为名字中带了“_”符号),在传递过程中无法识别,导致值出现错误,这让…

探索5款强大的报表软件:助力企业决策和数据分析

概述: 随着数据分析和决策的重要性不断提升,报表软件已经成为企业管理不可或缺的工具。报表软件能够将复杂的数据转化为直观的图表和报告,帮助管理层做出准确决策。以下是5款功能强大的报表软件,其中包括了山海鲸报表,每款软件都有其独特的优势,适用于不同的使用场景。 1…

1分钟学会DeepSeek本地部署,小白也能搞定!

DeepSeek 是国内顶尖 AI 团队「深度求索」开发的多模态大模型,具备数学推理、代码生成等深度能力,堪称"AI界的六边形战士"。 DeepSeek 身上的标签有很多,其中最具代表性的标签有以下两个:低成本(不挑硬件、开源) 高性能(推理能力极强、回答准确)一、为什么要…

内存占用与监控方式介绍

1.内存占用 神经网络模型常见的内存占用可以分为以下几个部分: 1.1 模型参数内存定义:神经网络的权重和偏置等参数会占用内存。 计算方法:参数总量 = 各层参数数量的总和。 每个参数的大小取决于数据类型(如 float32 为 4 字节,float16 为 2 字节,int8 为 1 字节)。公式…

WebGPU 版 Kokoro:浏览器端零成本使用高质量 TTS;苹果 ELEGNT 台灯机器人:赋予非人形机器人「情感」

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑…

尝试使用阿里云计算巢部署 DeepSeek-R1

记录一下用阿里云计算巢部署 DeepSeek-R1 的经过。进入阿里云计算巢控制台的服务目录,选择 DeepSeek 社区版,点击「开始部署」,选择最便宜的 ECS 实例 GRID 虚拟化4核30G,费用是 1.748/小时。点击「立即创建」,然后控制台会显示正在部署的状态。部署完成后,控制台会显示公…

未来已来:云手机+AI如何重塑Facebook、Google的智能营销生态

未来已来:云手机+AI如何重塑Facebook、Google的智能营销生态 在数字化浪潮奔涌的当下,科技融合正以令人惊叹的速度重塑各个行业,智能营销领域更是首当其冲。云手机与AI自动化工具的深度融合,为Facebook、Google构建的庞大智能营销生态带来了颠覆性的变革,开拓出全新的发展…

Burp Suite 2024激活汉化

转载自https://blog.csdn.net/m0_52985087/article/details/140299827 前言在项目即将上线阶段,迈入生产环境之际,确保其安全性成为我们不可忽视的首要任务。为筑起一道坚不可摧的安全防线,我们借助业界公认的网络安全利器——Burp Suite,我们将展开一场全面的安全测试,旨…

清华权威出品!104页《DeepSeek从入门到精通》免费领,解锁AI时代的核心竞争力!

引言: 在AI技术席卷全球的今天,如何高效驾驭大模型工具已成为个人与企业脱颖而出的关键。清华大学新闻与传播学院新媒体研究中心元宇宙文化实验室余梦珑博士后团队倾力打造的《DeepSeek从入门到精通》电子书重磅发布!全书104页,从基础操作到高阶技巧,手把手教你玩转国产顶…

win11家庭中文版登录应用提示:“为了对电脑进行保护,已经阻止此应用”

1.家庭中文版组策略里面禁用:以管理员批准模式运行所有管理员 win11打不开组策略,需要复制文本内容到记事本,修改为bat后缀执行 @echo off pushd "%~dp0" dir /b c:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum …

4种比常见的线程池和线程同步买票问题

线程池 所谓的线程池:其实就是线程对象的容器。 可以根据需要,在启动时,创建1个或者多个线程对象。 java中有4种比较常见的线程池。 1.固定数量的线程对象。 2.根据需求动态创建线程:动态创建线程:根据需求来创建线程的个数,会自动给我们分配合适的线程个数来完成任务。 3.…