Linux驱动开发-03字符设备驱动框架搭建

news2024/9/30 23:32:38

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

  1. 驱动模块的加载和卸载(将驱动编译模块,insmod加载驱动运行)
  2. 字符设备注册与注销(我们的驱动实际上是去操作底层的硬件,所以需要向系统注册一个设备,告诉Linux系统,我有这个设备,当驱动模块加载的时候我们去注册设备,当驱动模块卸载的时候我们注销设备)
  3. 实现设备的具体操作函数(我们注册完设备后,需要对设备进行读写操作,是通过一个结构体来进行的)
  4. 添加LICENSE 和作者信息

通过上面的字符设备的驱动开发步骤的了解,其实我们只需要掌握每一种设备的驱动框架按照框架去进行开发就行

 1.1 设备号

        Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux提供了一个名为 dev_t的数据类型表示设备号

//设备号数据类型在内核中的定义
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
typedef unsigned int __u32;
//可以看到设备号就是一个u32的数据类型

         32位的数据构成了主设备号和次设备号两部分,其中高12位为主设备号, 低 20位为次设备号。因此 Linux系统中主设备号范围为 0~4095

 1.2 设备号的分配

(1)静态分配设备号

使用“ cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,选择一个没有被使用的设备号进行使用即可

(2)动态分配设备号

设备号申请函数和设备号释放函数

/*
* alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
/*
 * __register_chrdev() - create and register a cdev occupying a range of minors
 * @major: major device number or 0 for dynamic allocation
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: name of this range of devices
 * @fops: file operations associated with this devices
*/
int __register_chrdev(unsigned int major, unsigned int baseminor,
		      unsigned int count, const char *name,
		      const struct file_operations *fops)

 1.3 file_operations

file_operations结构体就是设备的具体操作函数

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
};

二、实战之驱动文件的编写

2.1 应用程序与驱动程序的交互方式

Linux 应用程序对驱动程序的调用

         在Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

        应用程序运行在用户空间,而Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。

应用程序运行在用户空间,驱动是属于内核的一部分;我们的应用程序想对内核空间进行操作,就需要通过系统调用的方式(例如下面用户空间的open就需要通过系统调用,来通过file_operations结构体里面的open对设备进行具体)

2.2 驱动中的open、close、read、release函数介绍

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp);

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt);

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt);

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp);

2.3 内核区与用户区数据交互 

驱动不能直接访问应用程序的数据,应用程序也不能直接访问内核数据,必须借助其他函数

unsigned long copy_from_user(void *to, const void *from, unsigned long n);
//将用户空间的数据复制到内核空间

unsigned long copy_to_user(void *to, const void *from, unsigned long n);
//将内核空间的数据复制到用户空间

 所以,我们的内核read的时候,应该使用copy_to_user;内核write的时候,应该使用copy_from_user;

 2.4 chrdevbase.c和 chrdevbaseAPP.c

chrdevbase.c

#include <linux/init.h> //包含宏定义的头文件(printk的头文件)
#include <linux/module.h> //包含初始化加载模块的头文件
#include <linux/fs.h>//注册设备和注销设备的头文件
#include<linux/kernel.h>
#include <linux/ide.h>
#include <linux/types.h>

#define CHRDEVBASE_MAJOR 200 //主设备号
#define CHRDEVBASE_NAME  "chrdevbase"//设备名字

/*驱动不能直接访问应用程序的数据,应用程序也不能直接访问内核数据,必须借助其他函数(copy_from_user\copy_to_user)*/

/*我们的应用程序需要从驱动中去读取数据,并且还要向驱动写数据*/
    static char kenelreadbuf[100],kenelwritebuf[100];//我们驱动的读写缓冲区,file_operation结构体就是操作这两个
    static char keneldata[]={"keneldata data!"};//内核的数据,我们驱动read的时候就是把这个字符串返给应用程序


//从设备读数据
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count,loff_t *ppos){
/*参数解释:
filp:要打开的设备文件(文件描述符),也就是驱动文件的fd,因为我们是应用程序去read驱动的数据
buf:发送的数据就发送到buf里面去(我们的应用程序提供了一个buf,我们的驱动程序写到那个buf里面去)
count:驱动程序需要向应用程序发送多少个字节的数据
*/

    int ret=0;

    memcpy(kenelreadbuf,keneldata,sizeof(keneldata));//把keneldata中的数据拷贝到kenelreadbuf中,最后一个参数是拷贝的长度

    ret=copy_to_user(buf,kenelreadbuf,count);//把内核数据给应用程序,函数中的buf和count都是由应用程序决定,返回值为0表示成功
 
	if(ret == 0){
        printk("kernel send data :%s\r\n",kenelreadbuf);
	}else{
		printk("kernel send data failed!\r\n");
	}

    return 0;
}
//向设备写数据(应用程序把数据写给驱动程序)
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos){
    int ret=0;

    ret=copy_from_user(kenelwritebuf,buf,count);//把应用程序的数据给驱动,函数中的buf和count都是由应用程序决定

    if(ret == 0){
		printk("kernel receive data :%s\r\n",kenelwritebuf);
	}else{
		printk("kernel receive data failed!\r\n");
	}
                
    return 0;
}
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp){
    return 0;
}
//关闭设备
static int chrdevbase_close(struct inode *inode, struct file *filp){
    return 0;
}
/*
字符设备:操作集合
*/
static const struct file_operations chrdevbase_fops = {
	.owner	= THIS_MODULE,//owner拥有该结构体的模块的指针,一般设置为 THIS_MODULE
	.read	= chrdevbase_read,
	.write	= chrdevbase_write,
	.open	= chrdevbase_open,
	.release= chrdevbase_close,
};

//驱动入口函数
static int __init chrdevbase_init(void)
{
     int ret=0;
    /*
    注册字符设备驱动函数:主设备号、设备名字、file_operations结构体
    */
    ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
    //使用这个函数注册时,会把当前主设备号下的所有次设备号给占用
	if (ret<0)
    {
        printk("chrdevbase init failed!\r\n");
    }
    printk("module_init!\r\n");
    return 0;
}
//驱动出口函数
static void __exit chrdevbase_exit(void)
{
    //注销字符设备驱动
    unregister_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME);
    printk("module_exit!\r\n");
};
/*模块的出口与入口*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_AUTHOR("Chao");//作者是谁
MODULE_LICENSE("GPL");//开源协议

 chrdevbaseAPP.c

int atoi(const char *nptr);把输入的字符串转化为整型变量,因为我们的最后一个字符串1、2分别代表了对驱动设备的读写

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/*
/dev/chardevbase:驱动最终表现就是/dev/xxx文件,对文件的读写、打开关闭
argc:应用程序参数
argv[]:具体的参数内容,是以字符串形式
./chrdevbaseAPP  <filename>  <1:2>
./chrdevbaseAPP /dev/chardevbase 1表示向驱动里面读数据
./chrdevbaseAPP /dev/chardevbase 2表示向驱动里面写数据
*/
int main(int argc,char*argv[]){
    int ret=0;
    int fd=0;
    char*filename;//输入的字符串
    char readbuf[100],writebuf[100];
    static char usrdata[]={"usr data!"};

    if (argc!=3)//如果输入的参数不足三个打印错误
    {
        printf("Error usage!\r\n");
        return -1;
    }
    
    filename=argv[1];//驱动文件就是/dev/chardevbase,也就是输入的第二个字符串

    fd=open(filename,O_RDWR);//打开驱动文件
    if(fd < 0){
		printf("Can't open file %s\r\n", filename);
		return -1;
	}
if (atoi(argv[2])==1)//判断是不是读操作,atoi把字符串转化为整型变量
{
    //从驱动中读取数据
    ret = read(fd, readbuf, 50);//返回值是实际读取到的字节数
    /*这里我们从驱动中读取50个字节,把内容写到readbuf中*/
    if(ret < 0){
        printf("read file %s failed!\r\n", filename);
    }else{
        /*  读取成功,打印出读取成功的数据 */
        printf("read data:%s\r\n",readbuf);
    }
    
}

    
if (atoi(argv[2])==2){
    memcpy(writebuf,usrdata,sizeof(usrdata));//把usrdata内容拷贝到writebuf中
    //向驱动中写数据
    ret = write(fd, writebuf, 50);//返回值是实际写入的字节数
    /*这里我们从buf中的50个字节,把内容写到驱动中*/
    if(ret < 0){
        printf("write file %s failed!\r\n", filename);
    }else{
        /*  写入成功,打印出写入成功的数据 */
        printf("write data:%s\r\n",writebuf);
    }

}

	/* 关闭设备 */
	ret = close(fd);
	if(ret < 0){
		printf("Can't close file %s\r\n", filename);
		return -1;
	}

    return 0;
}

 三、测试运行

(1)编译chrdevbase.c生成chrdevbase.ko文件

(2)编译 chrdevbaseAPP.c使用交叉编译工具链编程成chrdevbaseAPP可执行文件

arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP

 (3)ls查看编译后的驱动文件和应用程序文件

 (4)拷贝到/home/alientek/linux/nfs/rootfs/lib/modules/4.1.15文件下

这里存放着.ko文件

sudo cp chrdevbase.ko chrdevbaseAPP /home/alientek/linux/nfs/rootfs/lib/modules/4.1.15 -f

 我们这里是使用nfs挂载根文件系统,所以我们使用串口终端去查看该目录下有没有对应的文件

(5)加载驱动modprobe chrdevbase.ko

 (6)输入命令“ cat /proc/devices”可以注册的设备号、设备名字是否正确

我们在模块加载时,已经注册字符设备,我们使用这个命令来看看内核中是否有200这个设备号,设备名字是否正确

(7) 创建设备节点文件

在 Linux 内核中,当使用 register_chrdev() 函数,注册一个字符设备时,您实际上是在内核中注册了该设备的存在,并为其分配了一个主设备号;这个注册过程确保了内核知道如何处理对该设备的操作请求,但它并不在文件系统中创建任何可见的设备文件

设备文件(如 /dev/chrdevbase)是用户空间与内核中设备驱动程序进行交互的接口。这些文件不是普通的文件,而是特殊文件,通常称为设备特殊文件或设备节点。当您在用户空间中对这些文件执行读写操作时,内核会将这些操作转换为对相应设备驱动程序的调用。

 因此,即使已经在内核中注册了设备并为其分配了主设备号,仍然需要在文件系统中创建对应的设备文件,以便用户空间程序能够访问它。这就是为什么在注册字符设备之后,还需要用 mknod 命令来创建设备文件的原因。

创建设备文件节点

mknod /dev/chrdevbase c 200 0

其中“ mknod”是创建节点命令 ,“/dev/chrdevbase”是要创建的节点文件 c”表示这是个字符设备,“ 200”是设备的主设备号 0”是设备的次设备号。创建完成以后就会存在

 查看/dev/目录下有没有对应的驱动文件(chrdevbase 

ls /dev/chrdevbase -l

结果 

 (8)运行chrdevbaseAPP可执行文件

./chrdevbaseAPP /dev/chrdevbase 1 (向驱动中去读数据)

./chrdevbaseAPP /dev/chrdevbase 2 (向驱动中去写数据)

至此我们的字符设备驱动开发流程结束 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最新知识付费系统3.0整站+自动采集同步插件

支持分类替换 将主站同步过来的文章分类进行替换 支持自定义文章作者&#xff08;选择多个作者则同步到的文章作者将会随机分配&#xff09; 支持添加黑名单分类 添加后 如果同步过来的文章包含黑名单分类将不会发布文章 自动检测同步&#xff0c;无需人工值守以及挂采集软件…

ProFuzzBench入门教学——使用(Ubuntu22.04)

ProFuzzBench是网络协议状态模糊测试的基准测试。它包括一套用于流行协议&#xff08;例如 TLS、SSH、SMTP、FTP、SIP&#xff09;的代表性开源网络服务器&#xff0c;以及用于自动执行实验的工具。详细参考&#xff1a;阅读笔记——《ProFuzzBench: A Benchmark for Stateful …

研华运动控制卡在LabVIEW中的应用

在现代工业和科研领域中&#xff0c;精密运动控制系统的需求日益增加。这些系统广泛应用于自动化生产线、精密机械加工、机器人控制、光学仪器调试和实验室自动化设备等诸多领域。本文以研华公司的运动控制卡为例&#xff0c;详细介绍其在LabVIEW中的应用&#xff0c;展示如何通…

习题练习以

题意&#xff1a;求i&M的popcount的和&#xff0c;i属于0……N 主要思路还是变加为乘。 举个例子N22&#xff0c;即10110 假设M的第3位是1&#xff0c;分析N中&#xff1a; 00110 00111 00100 00101 发现其实等价于 0010 0011 0000 0001 也就是左边第4位和第5…

数据库基础练习4

准备 create table dept (dept1 int ,dept_name varchar(11)) charsetutf8; create table emp (sid int ,name varchar(11),age int,worktime_start date,incoming int,dept2 int) charsetutf8;insert into dept values(101,财务),(102,销售),(103,IT技术),(104,行政);INSERT …

诸葛亮的连环计 责任链模式

“万事谋定而后动&#xff0c;一环扣一环&#xff0c;方能成大事。” 在三国时期&#xff0c;诸葛亮以其超凡的智慧闻名天下。在他众多的计策中&#xff0c;有一个鲜为人知却极具智慧的连环计&#xff0c;完美诠释了现代软件设计中的责任链模式。让我们一同探索这个巧妙的计策…

【安全设备】堡垒机

一、什么是堡垒机 安全运维审计与风险控制系统即堡垒机&#xff0c;前身为跳板机&#xff0c;跳板机是一个简单的管理设备&#xff0c;但缺乏对运维操作的控制和审计能力。堡垒机是一种特定的网络安全设备&#xff0c;用于在一个网络环境中保护数据和网络不受外部和内部用户的…

Python酷库之旅-第三方库Pandas(013)

目录 一、用法精讲 31、pandas.read_feather函数 31-1、语法 31-2、参数 31-3、功能 31-4、返回值 31-5、说明 31-6、用法 31-6-1、数据准备 31-6-2、代码示例 31-6-3、结果输出 32、pandas.DataFrame.to_feather函数 32-1、语法 32-2、参数 32-3、功能 32-4、…

Go语言入门之基础语法

Go语言入门之基础语法 1.简单语法概述 行分隔符&#xff1a; 一行代表一个语句结束&#xff0c;无需写分号。将多个语句写在一行可以用分号分隔&#xff0c;但是不推荐 注释&#xff1a; // 或者/* */ 标识符&#xff1a; 用来命名变量、类型等程序实体。 支持大小写字母、数字…

Golang | Leetcode Golang题解之第226题翻转二叉树

题目&#xff1a; 题解&#xff1a; func invertTree(root *TreeNode) *TreeNode {if root nil {return nil}left : invertTree(root.Left)right : invertTree(root.Right)root.Left rightroot.Right leftreturn root }

Python函数 之 模块和包

1.模块 1, 在Python 中, 每个以 .py 结尾的 Python 代码⽂件 都可以称为是⼀个模块。 2, 在模块中 别⼈书写好的功能(变量, 函数, 类)&#xff0c;我们可以拿来直接使⽤。 3, 我们自己写的代码文件&#xff0c; 想要作为模块让别⼈使⽤, 你的代码⽂件名(模块名) 满足标识符的规…

大话光学原理:4.散射:瑞利、拉曼、米氏和布里渊

这是一缕柔和的光&#xff0c;在空气的舞台上轻盈地跳跃。它悠然自得&#xff0c;在宁静的空间中缓缓前行。然而&#xff0c;一片细薄透明的介质挡住了它的脚步&#xff0c;它毫无预兆地撞上了这片障碍。在这短暂的接触中&#xff0c;它被分解成无数微小的粒子&#xff0c;被迫…

LabVIEW电滞回线测试系统

铁电材料的性能评估依赖于电滞回线的测量&#xff0c;这直接关系到材料的应用效果和寿命。传统的电滞回线测量方法操作复杂且设备成本高。开发了一种基于LabVIEW的电滞回线测试系统&#xff0c;解决传统方法的不足&#xff0c;降低成本&#xff0c;提高操作便捷性和数据分析的自…

前端面试题38(js原型与对象)

在JavaScript中&#xff0c;原型&#xff08;prototype&#xff09;是核心特性之一&#xff0c;它用于实现对象之间的继承和属性方法的共享。理解原型和对象的关系对于深入学习JavaScript至关重要。下面我会详细解释这两个概念以及它们是如何工作的&#xff0c;并给出一些示例代…

关于Python的类的一些理解

才发现python的类对象只能调用类方法 我想使用对类对象a使用系统调用的len方法就会报错 2.类对象a是什么&#xff1f; 答&#xff1a;是所有的带有self的成员变量 举例说明&#xff1a;红色的就是a里面的东西 class A:def __init__(self,data):self.datadataself.b1self.d{a…

详细分析@FunctionalInterface的基本知识(附Demo)

目录 前言1. 基本知识2. Demo 前言 Java的基本知识推荐阅读&#xff1a; java框架 零基础从入门到精通的学习路线 附开源项目面经等&#xff08;超全&#xff09;Spring框架从入门到学精&#xff08;全&#xff09; 1. 基本知识 FunctionalInterface 是 Java 8 引入的一个注…

理解负载组电路-EAK负载电路解释

负载组具有安全、可靠、操作方便、使用寿命长等特点。了解控制、冷却和负载元件电路的布局和功能对于理解负载组的运行、为应用选择负载组和维护负载组非常重要。以下各节将描述这些电路。 EAK负荷组运行概述 负载组接收来自电源的电力&#xff0c;将其转换为热量&#xff0c;…

【漏洞复现】通达OA v2017 video_file.php 任意文件下载漏洞

免责声明&#xff1a; 本文内容旨在提供有关特定漏洞或安全漏洞的信息&#xff0c;以帮助用户更好地了解可能存在的风险。公布此类信息的目的在于促进网络安全意识和技术进步&#xff0c;并非出于任何恶意目的。阅读者应该明白&#xff0c;在利用本文提到的漏洞信息或进行相关测…

数据结构之折半查找

折半查找的算法思想&#xff1a; 折半查找又称二分查找&#xff0c;它仅仅适用于有序的顺表。 折半查找的基本思想&#xff1a;首先将给定值key与表中中间位置的元素&#xff08;mid的指向元素&#xff09;比较。midlowhigh/2&#xff08;向下取整&#xff09; 若key与中间元…

字典树(Tire树)

字典树(Tire树) 字典树是一种多叉树&#xff0c;又称为前缀树。核心思想是利用字符串的公共前缀。 字典树的根节点为空&#xff0c;从根节点到某一节点路径上的字符连接起来构成字符串&#xff0c;完整的字符串在链上而非结点上&#xff0c;一个节点的所有子节点都具有相同公…