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

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

  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.mfbz.cn/a/788587.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

寂静孤独的404页面源码

寂静孤独的404页面源码&#xff0c;灯光闪烁动态效果&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;重定向这个界面 寂静孤独的404页面源…

普中51单片机:中断系统与寄存器解析(六)

文章目录 引言中断流程图中断优先级下降沿中断结构图中断相关寄存器IE中断允许寄存器&#xff08;可位寻址&#xff09;XICON辅助中断控制寄存器&#xff08;可位寻址&#xff09;TCON标志控制寄存器SCON串行口控制寄存器 中断号中断响应条件中断函数代码模板电路图开发板IO连接…

Spin Image(旋转图像)

Spin Image特征描述子原理 Spin Image是Johnson于1999年提出&#xff0c;Lazebnik于2005年完善的基于点云空间分布的特征描述方法&#xff0c;其思想是将一定区域的点云分布转换成二维的Spin Image&#xff0c;然后对场景和模型的Spin Image进行相似性度量。Spin Image方法与通…

OJhelper一款帮助你获取各大oj信息的软件

项目地址 应用功能 目前应用支持&#xff1a;查询、自定义、收藏各大oj比赛信息&#xff0c;跳转比赛界面。查询各大oj的Rating分以及题量&#xff0c;查看题量饼状图。 应用环境 windows和安卓端 应用预览&#xff1a; 维护概况 后期会提供持续更新&#xff0c;具体可以…

无法访问。你可能没有权限使用网络资源。请与这台服务器的管理员联系以查明你是否有访问权限。【解决办法】

问题描述 新建好一台windows虚拟机&#xff0c;两台设备网络是互通的&#xff0c;但是物理机在访问虚拟机的网络共享文件资源时&#xff0c;出现图下所示的报错&#xff1a;XXX无法访问。你可能没有权限使用网络资源。请与这台服务器的管理员联系以查明你是否有访问权限。用户…

在FPGA程序中Handshake(握手)和Register(寄存器)区别

在FPGA程序中&#xff0c;Handshake&#xff08;握手&#xff09;和Register&#xff08;寄存器&#xff09;是两种不同的通信和数据传输机制。它们各有特点和适用场景。以下是它们的区别和应用场景的详细解释&#xff1a; Register&#xff08;寄存器&#xff09; 特点&#…

RFID智能锁控系统在物流安全运输中的应用与效益分析

一、物流锁控系统现状与挑战 1.1 传统锁控系统的局限性 安全性不足&#xff1a;机械锁容易被撬开或钥匙被复制&#xff0c;导致货物在运输过程中面临被盗风险。 无法实时追踪&#xff1a;一旦货物离开发货点&#xff0c;物流公司无法实时监控货物状态&#xff0c;增加了货物…

全终端自动化测试框架wyTest

突然有一些觉悟&#xff0c;程序猿不能只会吭哧吭哧的低头做事&#xff0c;应该学会怎么去展示自己&#xff0c;怎么去宣传自己&#xff0c;怎么把自己想做的事表述清楚。 于是&#xff0c;这两天一直在整理自己的作品&#xff0c;也为接下来的找工作多做点准备。接下来…

初始化线程的4种方式

1. 继承Thread 缺点&#xff1a;无法获取线程的运算结果。 public class ThreadTest{public static void main(String[] args){Thread01 thread new Thread01();thread.start();}public static class Thread01 extends Thread{public void run(){System.out.println("当前…

基于蓝牙iBeacon定位技术的商场3D楼层导视软件功能详解与实施效益

在现代商场的繁华与复杂中&#xff0c;寻找目的地往往令人头疼。维小帮3D楼层导视软件以其创新技术&#xff0c;为顾客带来无缝、直观的跨楼层导航体验&#xff0c;让每一次商场消费都成为享受。 商场3D楼层导视软件功能服务 3D多楼层导视地图&#xff0c;商场布局一览无遗 …

数字经济时代,你有数商吗?

引言&#xff1a;随着科技的飞速发展&#xff0c;我们正步入一个全新的数字经济时代。在这个时代里&#xff0c;数据成为了新的石油&#xff0c;是推动经济增长和社会进步的关键要素。而在这个数据洪流中&#xff0c;一个新兴的概念——“数商”&#xff0c;正逐渐进入公众的视…

C#创建windows服务程序

步骤 1: 创建Windows服务项目 打开Visual Studio。选择“创建新项目”。在项目类型中搜索“Windows Service”并选择一个C#模板&#xff08;如“Windows Service (.NET Framework)”&#xff09;&#xff0c;点击下一步。输入项目名称、位置和其他选项&#xff0c;然后点击“创…

C#中简单Socket编程

C#中简单Socket编程 Socket分为面向连接的套接字(TCP套接字)和面向消息的套接字(UDP 套接字)。我们平时的网络编程是对Socket进行操作。 接下来&#xff0c;我用C#语言来进行简单的TCP通信和UDP通信。 一、TCP通信 新建项目SocketTest&#xff0c;首先添加TCP通信的客户端代…

6.MkDocs附录

安装插件 在 MkDocs 中&#xff0c;插件通常是通过 pip​ 工具安装的。你可以使用以下步骤来安装和配置 MkDocs 插件。 1.使用 pip​ 命令安装你需要的插件。例如 pip install pymdown-extensions‍ 2.更新 mkdocs.yml​ 文件。 ‍ 3.使用 mkdocs serve​ 命令本地预览你…

H265码率解析

概述 H.265技术的应用 编码技术主要运用于视频播放设备、软件应用以及拍摄、录制视频的设备。人们最熟悉的莫过于PPS网络视频播放器。在PC屏客户端产品上面&#xff0c;PPS已经于2013年初推出了基于H.265标准的高清视频&#xff0c;并命名“臻高清”为自己的高清品牌。同时 P…

mirthConnect 常用示例和语法整理

mirthConnect 常用示例和语法整理 1、jolt json常用语法 https://please.blog.csdn.net/article/details/140137463 2、常用方法 2.1 WinningDateUtils 所有的时间工具在WinningDateUtils里面 获取当前时间&#xff1a;var nowStrWinningDateUtils.getStandardNowStr()获取…

JVM是如何创建一个对象的?

哈喽&#xff0c;大家好&#x1f389;&#xff0c;我是世杰。 本文我为大家介绍面试官经常考察的**「Java对象创建流程」** 照例在开头留一些面试考察内容~~ 面试连环call Java对象创建的流程是什么样?JVM执行new关键字时都有哪些操作?JVM在频繁创建对象时&#xff0c;如何…

The First项目报告:引领L2解决方案新纪元的模块化协议AltLayer

在区块链演进中&#xff0c;可扩展性与定制化成为开发者核心诉求。ZK Rollups与Optimistic Rollups虽显著提升以太坊等区块链性能&#xff0c;却面临访问性、定制难、中心化风险及流动性分散等挑战。AltLayer以Rollups-as-a-Service创新模式&#xff0c;赋予开发者直接管理roll…

爆破器材期刊

《爆破器材》简介   《爆破器材》自1958年创刊以来&#xff0c;深受广大读者喜爱&#xff0c;是中国兵工学会主办的中央级技术刊物&#xff0c;在国内外公开发行&#xff0c;近几年已发行到10个国家和地区。《爆破器材》杂志被美国著名检索机构《化学文摘》&#xff08;CA&a…

【Linux】System V信号量详解以及semget()、semctl()和semop()函数讲解

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …