一张图掌握 Linux 字符设备驱动架构!【建议收藏】

在这里插入图片描述
面对疾风吧:
在这里插入图片描述
上面经过爆肝几夜总结的,建议保存下来再放大看,结合文章的话更香哦~


所有的热爱都要不遗余力,真正喜欢它便给它更高的优先级,和更多的时间吧!

Linux系统&驱动其它文章:     Linux


一. Linux 中字符设备驱动简介

Linux 中的有三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。

其中字符设备驱动是占用篇幅最大的一类驱动,因为字符设备最多,从最简单的点灯到 I2C、SPI、音频等都属于字符设备驱动的类型。

块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。

所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。

网络设备驱动就更好理解了,就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。

一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

二. 字符设备驱动快速入门(超简单demo)

如果拿到一个新的代码,我觉得第一步应该先直接运行,让它跑起来,这样会有更直观的认识,即便是最底层的驱动, 咱们先上个最简单的字符驱动代码,不用管具体的含义。

1. demo

//test_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

MODULE_LICENSE("GPL");

dev_t dev;						//定义设备号
unsigned int major , minor ;	//定义主设备号和次设备号
struct cdev test_cdev;          //定义一个测试的cdev变量

/*
 * @description : 打开设备
 * @param – inode : 传递给驱动的 inode
 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量,一般在 open 的时候将private_data 指向设备结构体。
 */
int test_open(struct inode *inode, struct file *filp)
{
    printk("call %s\n", __func__);
    return 0;
}

int test_close(struct inode *inode, struct file *filp)
{
    printk("call %s\n", __func__);
    return 0;
}

//3. 文件操作集合的绑定
struct file_operations test_fops=
{
    .owner = THIS_MODULE,
    .open  = test_open,
    .release = test_close,
};

//初始化函数,在加载时调用
int __init char_drv_init(void)
{
	/* 注册字符设备驱动 */
    //2. 创建设备号
    if(major) {										//定义了主设备号, 就是用静态分配
        dev = MKDEV(major, minor);					 //大部分驱动次设备号都选择 0
        register_chrdev_region(dev, 1, "test_drv");  //注册设备号,当驱动加载后  查看 cat /pro/devices
    }
    else { 											//没有定义设备号,则动态分配			
        alloc_chrdev_region(&dev,minor,1,"test_drv");	//申请设备号
        major = MAJOR(dev);							//获取分配号的主设备号
        minor = MINOR(devid); 						//获取分配号的次设备号
        printk("major=%d, minor =%d\n", major, minor);				
    }

	//4. 注册cdev
    //4.1 初始化 cdev
    cdev_init(&test_cdev, &test_fops);
    
    //4.2 添加一个cdev (向 Linux 系统添加这个字符设备)
    cdev_add(&test_cdev, dev, 1);
    return 0;
}

//退出函数,在卸载时调用
void __exit char_drv_exit(void)
{
    /*注销cdev*/
    cdev_del(&test_cdev);
    /*注销设备号*/
    unregister_chrdev_region(dev, 1);
}

//1. 驱动模块加载和卸载的入口函数
module_init(char_drv_init);
module_exit(char_drv_exit);

用户测试代码:

//test_app.c
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
	 //fd :文件描述符
    int fd = 0;
   	//打开驱动文件 
    fd = open("/dev/myleds", O_RDWR);
    
    if(fd < 0)
    {
        perror("open failed");
        return -1;
    }

    printf("open myleds successed,using device...\n");
    sleep(5);

    printf("end of using device,closing device\n");
    //关闭设备
    close(fd);
    printf("exit from main\n");
    return 0;
}

2. 代码编译

Makefile :

KERNELDIR := /home/chunn/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := test_drv.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
1 行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径
:= 表示定义的变量不能修改(类似于const);
第 3 行,obj-m 表示将 test_drv.c 这个文件编译为 test_drv.ko 模块。
第 8 行,具体的编译命令,后面的 modules 表示编译模块,-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。M 表示模块源码目录,“make modules” 命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
 make //执行make

● 编译测试APP 和查看编译后文件:

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

在这里插入图片描述

3. 加载驱动模块

insmod chrdevbase.ko

打印相关信息:
在这里插入图片描述
用 lsmod 查看下test_drv 模块:
在这里插入图片描述
运行了char_drv_init() , 且为自动分配的设备号,说明模块加载成功了!

4. 创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/test_drv 这个设备节点文件:

mknod /dev/test_drv c 248 0

其中 “mknod” 是创建节点命令,“/dev/test_drv ” 是要创建的节点文件,“c” 表示这是个字符设备,“248” 是设备的主设备号,“0”是设备的次设备号。

创建完成以后就会存在 /dev/test_drv 这个文件,可以使用 “ls /dev/test_drv -l” 命令查看

5. APP设备文件操作

直接执行

./test_app

在这里插入图片描述
后续可以通过 APP 的 main 函数入口传入用户指令,就可以进行相关操作了如下:

./test_app /dev/test_drv xx

xx 为入口参数

6. 卸载驱动模块

rmmod chrdevbase.ko

应用大概就是这个流程:

在这里插入图片描述

三. 字符设备驱动开发流程介绍:

1. 驱动模块加载和卸载的入口函数

Linux 驱动有两种运行方式:
① 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
② 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。

在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。

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. 字符设备注册与注销

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成:
在这里插入图片描述

2.1 静态注册与注销

选出一个内核中未被使用的主设备,为我所用(主设备号为0~255)

查看内核中哪些主设备号已经被占用:

 1. cat /proc/devices
 2. vi Documentation\devices.txt (具体分配的内容)
/ *
  * @ 作用:注册连续的多个设备号
  * @from: 起始设备号
  * @count:连续注册的个数
  * @name: 名称
  * @返回值:成功返回0
      失败返回<0的错误编号
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name) 

//作用:注销从from开始的连续count个设备号
void unregister_chrdev_region(dev_t from, unsigned count)
             

用户查看:

insmod char_drv.ko
 cat /proc/devices

2.2 动态注册

由内核帮我们挑一个未被使用的主设备号,和我们自己规定的次设备号拼成设备号

 /*
  *  @作用:注册连续多个设备号
  *  @dev, 注册成功的第一个设备号
  *  @baseminor, 起始次设备号
  *  @count, 连续注册的个数
  *  @name
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 
            
//作用:注销从from开始的连续count个设备号
void unregister_chrdev_region(dev_t from, unsigned count)
                 

测试用例,两者都使用了:

//1. 创建设备号
if(major) {										//定义了主设备号, 就是用静态分配
    dev = MKDEV(major, minor);					 //大部分驱动次设备号都选择 0
    register_chrdev_region(dev, 1, "test_drv");  //注册设备号,当驱动加载后就生成 /dev/test_drv这个设备文件
}
else { 											//没有定义设备号,则动态分配			
    alloc_chrdev_region(&dev,minor,1,"test_drv");	//申请设备号
    major = MAJOR(dev);							//获取分配号的主设备号
    minor = MINOR(devid); 						//获取分配号的次设备号
    printk("major=%d, minor =%d\n", major, minor);				
}

3. struct file_operations 文件操作集合

实现一个字符设备驱动, 实则就是实例化一个cdev,实例化一个cdev就是定义一个struct cdev 类型的变量,并做好初始化,其中关键就是是如何实现函数操作集合 file_operations ,它也是字符设备驱动开发过程中最主要的工作。

//文件操作集合的绑定
struct file_operations test_fops=
{
    .owner = THIS_MODULE,
    .open  = test_open,    
    .release = test_close,
    .read = xxx,
    .write = xxx,
    .unloched_ioctrl = xxx,
};

open、release对应应用层的open()、close()函数。实现比较简单,

其中read、write、unloched_ioctrl 函数的实现需要涉及到用户空间和内存空间的数据拷贝。

在Linux操作系统中,用户空间和内核空间是相互独立的。也就是说内核空间是不能直接访问用户空间内存地址,同理用户空间也不能直接访问内核空间内存地址。

如果想实现,将用户空间的数据拷贝到内核空间或将内核空间数据拷贝到用户空间,就必须借助内核给我们提供的接口来完成。

此部分后续文章再补充。

4. 注册cdev

定义好 file_operations 结构体,就可以通过函数cdev_init()、 cdev_add()注册字符设备驱动了。

	struct cdev test_cdev;          				//上文 定义的cdev变量

   //3.1. 初始化 cdev
   cdev_init(&test_cdev,  &test_fops);
   
   //3.2 添加一个cdev (向 Linux 系统添加这个字符设备)
   cdev_add(&test_cdev, dev, 1);      

cdev_init() 中最重要的是 绑定自定义的函数操作集合

cdev_add() 中最重要的是添加一个cdev (向 Linux 系统添加这个字符设备)

注意如果使用了函数register_chrdev(),就不用了执行上述操作,因为该函数已经实现了对cdev的封装。

5. 创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/test_drv 这个设备节点文件:

mknod /dev/test_drv c 248 0

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

这样 APP程序 就可以直接对 /dev/test_drv 进行文件操作了。相当于/dev/test_drv 这个文件是 test_drv 设备在用户空间中的实现。 Linux 下一切皆文件,这个设备文件也当然是文件。

总的说来设备文件是用户空间访问内核空间驱动的介质,将对文件的访问变成了对特定cdev的访问(主设备号和次设号)

● 设备节点的自动创建

第一步 :通过宏class_create() 创建一个class类型的对象;

第二步: 导出我们的设备信息到用户空间 device_create()

这里只是提一下,后续有时间再详细介绍。

当然 Linux 字符驱动远远不止这些 ,如关于_Kobject 这一块还是一知半解,还有read 、write 等都未列出,路漫漫其修远~


【参考】

● 公众号:一口Linux ,作者土豆居士

● 公众号:老吴的嵌入式之旅,作者吴伟东Jack

● 《Linux设备驱动程序》

● 《深入Linux内核架构》

● 《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.1》

● Linux内核源码 4.1.15

《Linux 字符设备驱动结构(一)》


Linux系统&驱动其它文章:     Linux

  • 32
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 51
    评论
### 回答1: Linux字符设备驱动开发是指在Linux系统中编写驱动程序,使得用户可以通过字符设备接口来访问硬件设备。这种驱动程序通常用于控制串口、并口、USB设备等。开发Linux字符设备驱动需要掌握Linux内核的基本知识,包括进程管理、内存管理、中断处理、设备驱动等方面。此外,还需要了解字符设备驱动的编写流程、驱动程序的结构和接口等。开发Linux字符设备驱动需要使用C语言和Linux内核编程接口。 ### 回答2: Linux字符设备驱动开发是Linux系统中的一部分,它允许开发人员在Linux系统上使用字符设备,这些字符设备可以包括串口、USB口、网卡等。Linux字符设备驱动开发可帮助开发人员实现各种各样的设备驱动,从而增强Linux系统的功能。 在Linux字符设备驱动的开发过程中,需要注意以下几点: 1. 实现设备驱动的一个基本框架,包括注册设备、设备的初始化,以及对设备进行读写操作等。 2. 开发人员不仅需要熟悉驱动程序开发技术,还需要了解Linux内核系统的相关知识,例如进程、中断、内存管理等。 3. 应该在代码注释中提供详细的文档,以方便其他开发人员进行维护和修改。 4. 在实现字符设备驱动过程中,必须保证安全性和可靠性,防止设备出现故障或者损坏用户的数据。 5. 在测试和维护设备驱动时,需要使用一些常见的工具和技术,例如devfs、udev等。 总之,Linux字符设备驱动开发是一个需要熟练技能和丰富经验的过程。开发人员需要有足够的专业知识和经验来确保设备驱动的高效和稳定性。通过精心设计和开发,Linux字符设备驱动可以提供高性能、高可靠性、易于使用的设备驱动,从而大大增强了Linux系统的功能和灵活性。 ### 回答3: Linux字符设备驱动开发是Linux系统中的一个重要领域。其主要任务是开发一些支持字符设备驱动程序,从而使用户能够在Linux系统中使用各种不同类型的字符设备,例如串口、打印机、读卡器和磁盘等。同时,这些驱动程序还要保证设备完全可靠和高效地工作,确保系统的安全性和性能。 Linux字符设备驱动开发需要掌握以下基本知识: 1.了解Linux系统体系结构和内核架构 Linux系统由内核和用户空间组成,内核作为系统的核心组件,是实现系统功能的主要部分,因此了解内核体系结构和架构是开发Linux字符设备驱动所必须掌握的知识。 2.熟悉字符设备的相关知识 字符设备Linux系统中的一种重要的设备类型,它与其他类型设备不同之处在于它只能逐个字符地进行读写操作。因此需要深入了解字符设备的相关知识,例如驱动的主要功能、驱动程序与设备的交互方式、设备控制结构等。 3.熟练掌握C语言及Linux内核编程技术 编写Linux字符设备驱动程序需要掌握良好的C语言编程知识以及熟练的Linux内核编程技术,包括内存管理、进程管理、文件系统、中断处理等。同时,还需要了解Linux内核代码的结构和代码的编写规范,以便于编写出符合内核标准的驱动程序。 4.掌握Linux驱动框架的使用方法 为了简化Linux驱动的开发流程,Linux提供了一些驱动框架,这些框架定义了一些驱动程序中常用的接口和函数,能够方便驱动程序的开发和调试。因此,Linux字符设备驱动开发者需要掌握其中的一些驱动框架,如字符驱动框架。 5.熟悉Linux字符设备驱动的开发过程 Linux字符设备驱动的开发过程主要包括驱动程序的初始化、驱动程序的主要功能实现、驱动程序的卸载等环节。在开发过程中,需要合理使用系统提供的工具和调试手段,如gdb、strace、make等,以便于分析和排查驱动程序出现的问题,确保驱动程序的稳定和可靠性。 总之,在Linux字符设备驱动开发过程中,开发者需要掌握相关的知识和技能,以实现对字符设备的编程和调试,开发出满足用户需求的高质量驱动程序。同时,Linux字符设备驱动开发也是一项长期持续的工作,开发者需要时刻关注最新的技术发展和硬件设备变化,才能更好地适应市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值