IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> linux驱动开发篇(三)—— 总线设备驱动模型 -> 正文阅读

[系统运维]linux驱动开发篇(三)—— 总线设备驱动模型

linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程
linux基础篇(二)——静态和动态链接
ARM裸机篇(一)——i.MX6ULL介绍
ARM裸机篇(二)——i.MX6ULL启动过程
ARM裸机篇(三)——i.MX6ULL第一个裸机程序
ARM裸机篇(四)——重定位和地址无关码
ARM裸机篇(五)——异常和中断
linux系统移植篇(一)—— linux系统组成
linux系统移植篇(二)—— Uboot使用介绍
linux系统移植篇(三)—— Linux 内核使用介绍
linux系统移植篇(四)—— 根文件系统使用介绍
linux驱动开发篇(一)—— Linux 内核模块介绍
linux驱动开发篇(二)—— 字符设备驱动框架
linux驱动开发篇(三)—— 总线设备驱动模型
linux驱动开发篇(四)—— platform平台设备驱动
linux驱动开发篇(五)—— linux设备驱动面向对象的编程思想
linux驱动开发篇(六)—— 设备树的引入


一、设备驱动模型分层的引入

在之前的字符设备程序中驱动程序,我们只要调用 open() 函数打开了相应的设备文件,就可以使用 read()/write() 函数,通过 file_operations 这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观,但是从软件设计的角度看,却是一种十分糟糕的方式。

它有一个严重的问题,就是设备信息和驱动代码杂糅在一起,在我们驱动程序中各种硬件寄存器地址随处可见。本质上,这种驱动开发方式与单片机的驱动开发并没有太大的区别,一旦硬件信息发生变化甚至设备已经不在了,就必须要修改驱动源码。我们之前做的事情只不过是简单地给它套了一个文件操作接口的外壳。

为了解决这种驱动代码和设备信息耦合的问题, linux 提出了设备驱动模型。将我们编写的驱动代码分成了两块:设备与驱动。设备负责提供硬件资源,而驱动代码负责去使用这些设备提供的硬件资源。并由总线将它们联系起来。这样子就构成以下图形中的关系。
在这里插入图片描述

  • 设备 (device) :挂载在某个总线的物理设备;
  • 驱动 (driver) :与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
  • 总线(bus) :负责管理挂载对应总线的设备以及驱动;

在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动,同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。
在插入的同时总线会执行一个 bus_type 结构体中 match 的方法对新插入的设备/驱动进行匹配。 (它们之间最简单的匹配方式则是对比名字,存在名字相同的设备/驱动便成功匹配)。
在匹配成功的时候会调用驱动 device_driver 结构体中 probe 方法 (通常在 probe 中获取设
备资源,具体的功能可由驱动编写人员自定义),并且在移除设备或驱动时,会调用 device_driver结构体中 remove 方法。

1、总线

在内核中使用结构体 bus_type 来表示总线(内核源码/include/linux/device.h),如下所示:

struct bus_type {
        const char              *name;
        const char              *dev_name;
        struct device           *dev_root;
        const struct attribute_group **bus_groups;
        const struct attribute_group **dev_groups;
        const struct attribute_group **drv_groups;

        int (*match)(struct device *dev, struct device_driver *drv);
        int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
        int (*probe)(struct device *dev);
        int (*remove)(struct device *dev);
        void (*shutdown)(struct device *dev);

        int (*online)(struct device *dev);
        int (*offline)(struct device *dev);

        int (*suspend)(struct device *dev, pm_message_t state);
        int (*resume)(struct device *dev);

        int (*num_vf)(struct device *dev);

        int (*dma_configure)(struct device *dev);

        const struct dev_pm_ops *pm;

        const struct iommu_ops *iommu_ops;

        struct subsys_private *p;
        struct lock_class_key lock_key;

        bool need_parent_lock;
};

  • name : 指定总线的名称,当新注册一种总线类型时,会在/sys/bus 目录创建一个新的目录,目录名就是该参数的值;
  • drv_groups、 dev_groups、 bus_groups : 分别表示驱动、设备以及总线的属性。这些属性可以是内部变量、字符串等等。通常会在对应的/sys 目录下在以文件的形式存在,对于驱动而言,在目录/sys/bus/< bus-name >/driver/< driver-name > 存放了设备的默认属性;设备则在目录/sys/bus/< bus-name >/devices/< driver-name > 中。这些文件一般是可读写的,用户可以通过读写操作来获取和设置这些 attribute 的值。
  • match : 当向总线注册一个新的设备或者是新的驱动时,会调用该回调函数。该回调函数主要负责判断是否有注册了的驱动适合新的设备,或者新的驱动能否驱动总线上已注册但没有驱动匹配的设备;
  • uevent : 总线上的设备发生添加、移除或者其它动作时,就会调用该函数,来通知驱动做出相应的对策。
  • probe : 当总线将设备以及驱动相匹配之后,执行该回调函数, 最终会调用驱动提供的 probe函数。
  • remove : 当设备从总线移除时,调用该回调函数;
  • suspend、 resume : 电源管理的相关函数,当总线进入睡眠模式时,会调用 suspend 回调函数;而 resume 回调函数则是在唤醒总线的状态下执行;
  • pm : 电源管理的结构体,存放了一系列跟总线电源管理有关的函数,与 device_driver 结构体中的 pm_ops 有关;
  • p : 该结构体用于存放特定的私有数据,其成员 klist_devices 和 klist_drivers 记录了挂载在该总线的设备和驱动;

(1)注册/注销总线 API(内核源码/drivers/base/bus.c)

int bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);

当我们成功注册总线时,会在/sys/bus/目录下创建一个新目录,目录名为我们新注册的总线名。

2、设备

在内核使用 device 结构体来描述我们的物理设备,(内核源码/include/linux/device.h)如下所示:

struct device {
        struct device           *parent;

        struct device_private   *p;

        struct kobject kobj;
        const char              *init_name; /* initial name of the device */
        const struct device_type *type;

        struct mutex            mutex;  /* mutex to synchronize calls to
                                         * its driver.
                                         */

        struct bus_type *bus;           /* type of bus device is on */
        struct device_driver *driver;   /* which driver has allocated this
                                           device */
        void            *platform_data; /* Platform specific data, device
                                           core doesn't touch it */
        void            *driver_data;   /* Driver data, set and get with
                                           dev_set/get_drvdata */
        struct dev_links_info   links;
        struct dev_pm_info      power;
        struct dev_pm_domain    *pm_domain;
       ...
}
  • init_name : 指定该设备的名称,总线匹配时,一般会根据比较名字,来进行配对;
  • parent : 表示该设备的父对象,前面提到过,旧版本的设备之间没有任何关联,引入 Linux设备模型之后,设备之间呈树状结构,便于管理各种设备;
  • bus : 表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。
  • of_node : 存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的
    of_match_table 以及设备树的 compatible 属性进行比较之后,将匹配的节点保存到该变量。
  • platform_data : 特定设备的私有数据,通常定义在板级文件中;
  • driver_data : 同上,驱动层可通过 dev_set/get_drvdata 函数来获取该成员;
  • class : 指向了该设备对应类;
  • dev :dev_t 类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys 目录中导出对应的设备。
  • release : 回调函数,当设备被注销时,会调用该函数。如果我们没定义该函数时,移除设备时,会提示“Device‘ xxxx’ does not have a release() function, it is broken and must be fixed”的错误。
  • group : 指向 struct attribute_group 类型的指针,指定该设备的属性;

(1)内核注册/注销设备 (内核源码/driver/base/core.c)

int device_register(struct device *dev);
void device_unregister(struct device *dev);

当成功注册总线时,会在/sys/bus 目录下创建对应总线的目录,该目录下有两个子目录,分别是 drivers 和 devices,我们使用 device_register 注册的设备从属于某个总线时,该总线的 devices 目录下便会存在该设备文件。

3、驱动

在内核中,使用 device_driver 结构体来描述我们的驱动,(内 核 源码/include/linux/device.h)如下所示:

struct device_driver {
        const char              *name;
        struct bus_type         *bus;

        struct module           *owner;
        const char              *mod_name;      /* used for built-in modules */

        bool suppress_bind_attrs;       /* disables bind/unbind via sysfs */
        enum probe_type probe_type;

        const struct of_device_id       *of_match_table;
        const struct acpi_device_id     *acpi_match_table;

        int (*probe) (struct device *dev);
        int (*remove) (struct device *dev);
        void (*shutdown) (struct device *dev);
        int (*suspend) (struct device *dev, pm_message_t state);
        int (*resume) (struct device *dev);
        const struct attribute_group **groups;

        const struct dev_pm_ops *pm;
        void (*coredump) (struct device *dev);

        struct driver_private *p;
};

  • name : 指定驱动名称,总线进行匹配时,利用该成员与设备名进行比较;
  • bus : 表示该驱动依赖于哪个总线,内核需要保证在驱动执行之前,对应的总线能够正常工作;
  • suppress_bind_attrs : 布尔量,用于指定是否通过 sysfs 导出 bind 与 unbind 文件, bind 与unbind 文件是驱动用于绑定/解绑关联的设备。
  • owner : 表示该驱动的拥有者,一般设置为 THIS_MODULE;
  • of_match_table : 指定该驱动支持的设备类型。当内核使能设备树时,会利用该成员与设备树中的 compatible 属性进行比较。
  • remove : 当设备从操作系统中拔出或者是系统重启时,会调用该回调函数;
  • probe : 当驱动以及设备匹配后,会执行该回调函数,对设备进行初始化。通常的代码,都是以 main 函数开始执行的,但是在内核的驱动代码,都是从 probe 函数开始的。
  • group : 指向 struct attribute_group 类型的指针,指定该驱动的属性;

(1)内核注册/注销设备 (内 核 源码/include/linux/device.h)

int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);

成功注册的驱动会记录在/sys/bus//drivers 目录

总线、设备、驱动大致注册流程如下
在这里插入图片描述
系统启动之后会调用 buses_init 函数创建/sys/bus 文件目录,这部分系统在开机时已经帮我们准备好了,接下去就是通过总线注册函数 bus_register 进行总线注册,注册完总线后在总线的目录下生成 devices 文件夹和 drivers 文件夹,最后分别通过 device_register 以及 driver_register 函数注册相对应的设备和驱动。

二、attribute 属性文件

/sys 目录有各种子目录以及文件,前面讲过当我们注册新的总线、设备或驱动时,内核会在对应的地方创建一个新的目录,目录名为各自结构体的 name 成员。内核中以 attribute 结构体来描述/sys 目录下的文件,每个子目录下的文件,都是内核导出到用户空间,用于控制我们的设备的。

1.设备属性文件

设备属性文件接口内核源码/include/linux/device.h):

struct device_attribute {
	 struct attribute attr;
	 ssize_t (*show)(struct device *dev, struct device_attribute *attr,
	 char *buf);
	 ssize_t (*store)(struct device *dev, struct device_attribute *attr,
	 const char *buf, size_t count);
 };
 #define DEVICE_ATTR(_name, _mode, _show, _store) \
  struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
extern int device_create_file(struct device *device,const struct device_attribute *entry);
extern void device_remove_file(struct device *dev, const struct device_attribute *attr);
  • DEVICE_ATTR 宏定义用于定义一个 device_attribute 类型的变量, ## 表示将 ## 左右两边的标签拼接在一起,因此,我们得到变量的名称应该是带有 dev_attr_ 前缀的。该宏定义需要传入四个参数 _name, _mode, _show, _store,分别代表了文件名,文件权限, show 回调函数, store 回调函数。 show 回调函数以及 store 回调函数分别对应着用户层的 cat 和 echo命令,当我们使用 cat 命令,来获取/sys 目录下某个文件时,最终会执行 show 回调函数;使用 echo 命令,则会执行 store 回调函数。参数 _mode 的值,可以使用 S_IRUSR、 S_IWUSR、S_IXUSR 等宏定义,更多选项可以查看读写文件章节关于文件权限的内容。
  • device_create_file 函数用于创建文件,它有两个参数成员,第一个参数表示的是设备,前面讲解 device 结构体时,其成员中有个 bus_type 变量,用于指定设备挂载在某个总线上,并且会在总线的 devices 子目录创建一个属于该设备的目录, device 参数可以理解为在哪个设备目录下,创建设备文件。第二个参数则是我们自己定义的 device_attribute 类型变量。
  • device_remove_file 函数用于删除文件,当我们的驱动注销时,对应目录以及文件都需要被移除。其参数和 device_create_file 函数的参数是一样。

2.驱动属性文件

驱动属性文件接口 (内 核 源码/include/linux/device.h):

struct driver_attribute {
	struct attribute attr;
	ssize_t (*show)(struct device_driver *driver, char *buf);
	ssize_t (*store)(struct device_driver *driver, const char *buf,size_t count);
};

#define DRIVER_ATTR_RW(_name) \
struct driver_attribute driver_attr_##_name = __ATTR_RW(_name)
#define DRIVER_ATTR_RO(_name) \
struct driver_attribute driver_attr_##_name = __ATTR_RO(_name)
#define DRIVER_ATTR_WO(_name) \
struct driver_attribute driver_attr_##_name = __ATTR_WO(_name)

extern int __must_check driver_create_file(struct device_driver *driver,const struct driver_attribute *attr);
extern void driver_remove_file(struct device_driver *driver,const struct driver_attribute *attr);
  • DRIVER_ATTR_RW、 DRIVER_ATTR_RO 以及 DRIVER_ATTR_WO 宏定义用于定义一个 driver_attribute 类型的变量,带有 driver_attr_ 的前缀,区别在于文件权限不同, RW 后缀表示文件可读写, RO 后缀表示文件仅可读, WO 后缀表示文件仅可写。而且你会发现,DRIVER_ATTR 类型的宏定义没有参数来设置 show 和 store 回调函数,那如何设置这两个参数呢?在写驱动代码时,只需要你提供 xxx_store 以及 xxx_show 这两个函数,并确保两个函数的 xxx 和 DRIVER_ATTR 类型的宏定义中名字是一致的即可。
  • driver_create_file 和 driver_remove_file 函数用于创建和移除文件,使用 driver_create_file 函数,会在/sys/bus/< bus-name >/drivers/< driver-name >/目录下创建文件。

3.总线属性文件

总线属性文件接口 (内 核 源码/include/linux/device.h)

struct bus_attribute {
	struct attribute attr;
	ssize_t (*show)(struct bus_type *bus, char *buf);
	ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count);
};
#define BUS_ATTR(_name, _mode, _show, _store) \
struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show,_store)

extern int __must_check bus_create_file(struct bus_type *,struct bus_attribute *);
extern void bus_remove_file(struct bus_type *, struct bus_attribute *);
  • BUS_ATTR 宏定义用于定义一个 bus_attribute 变量,
  • 使用 bus_create_file 函数,会在/sys/bus/< bus-name> 下创建对应的文件。
  • bus_remove_file 则用于移除该文件。

三、设备驱动实验

1、编程思路

  1. 编写 Makefile 文件
  2. 声明一个总线结构体并创建一个总线 xbus,实现 match 方法,对设备和驱动进行匹配
  3. 声明一个设备结构体,挂载到我们的 xbus 总线中
  4. 声明一个驱动结构体,挂载到 xbus 总线,实现 probe、 remove 方法
  5. 将总线、设备、驱动导出属性文件到用户空间。

2、编写xbus.c

#include <linux/init.h>
#include <linux/module.h>

#include <linux/device.h>

/************************************************************************
	* 函数负责总线下的设备以及驱动匹配
	* 使用字符串比较的方式,通过对比驱动以及设备的名字来确定是否匹配,
	* 如果相同, 则说明匹配成功,返回1;反之,则返回0
	***********************************************************************/
int xbus_match(struct device *dev, struct device_driver *drv)
{
	printk("%s-%s\n", __FILE__, __func__);
	if (!strncmp(dev_name(dev), drv->name, strlen(drv->name))) {
		printk("dev & drv match\n");
		return 1;
	}
	return 0;

}

//定义了一个bus_name变量,存放了该总线的名字
static char *bus_name = "xbus";
//提供show回调函数,这样用户便可以通过cat命令, 来查询总线的名称
ssize_t xbus_test_show(struct bus_type *bus, char *buf)
{
	return sprintf(buf, "%s\n", bus_name);
}
//设置该文件的文件权限为文件拥有者可读,组内成员以及其他成员不可操作
BUS_ATTR(xbus_test, S_IRUSR, xbus_test_show, NULL);

//定义了一种新的总线,名为xbus,总线结构体中最重要的一个成员,便是match回调函数
static struct bus_type xbus = {
	.name = "xbus",
	.match = xbus_match,
};

EXPORT_SYMBOL(xbus);

//注册总线
static __init int xbus_init(void)
{
	printk("xbus init\n");

	bus_register(&xbus);
	bus_create_file(&xbus, &bus_attr_xbus_test);
	return 0;
}

module_init(xbus_init);

//注销总线
static __exit void xbus_exit(void)
{
	printk("xbus exit\n");
	bus_remove_file(&xbus, &bus_attr_xbus_test);
	bus_unregister(&xbus);
}

module_exit(xbus_exit);

MODULE_AUTHOR("embedfire");
MODULE_LICENSE("GPL");

3、编写xdev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>

extern struct bus_type xbus;

void xdev_release(struct device *dev)
{
	printk("%s-%s\n", __FILE__, __func__);
}

unsigned long id = 0;

//show回调函数中,直接将id的值通过sprintf函数拷贝至buf中。
ssize_t xdev_id_show(struct device *dev, struct device_attribute *attr,
		     char *buf)
{
	return sprintf(buf, "%ld\n", id);
}

/*********************************************************************************************
	* store回调函数则是利用kstrtoul函数,
	* 该函数有三个参数,其中第二个参数是采用几进制的方式, 这里我们传入的是10,意味着buf中的内容将转换为10进制的数传递给id,
	* 实现了通过sysfs修改驱动的目的。
	*********************************************************************************************/
ssize_t xdev_id_store(struct device * dev, struct device_attribute * attr,
		      const char *buf, size_t count)
{
	kstrtoul(buf, 10, &id);
	return count;
}

//DEVICE_ATTR宏定义定义了xdev_id,设置该文件的文件权限是文件拥有者可读可写,组内成员以及其他成员不可操作
DEVICE_ATTR(xdev_id, S_IRUSR | S_IWUSR, xdev_id_show, xdev_id_store);

static struct device xdev = {
	.init_name = "xdev",
	.bus = &xbus,
	.release = xdev_release,
};

//设备结构体以及属性文件结构体注册
static __init int xdev_init(void)
{
	printk("xdev init\n");
	device_register(&xdev);
	device_create_file(&xdev, &dev_attr_xdev_id);
	return 0;
}

module_init(xdev_init);

//设备结构体以及属性文件结构体注销。
static __exit void xdev_exit(void)
{
	printk("xdev exit\n");
	device_remove_file(&xdev, &dev_attr_xdev_id);
	device_unregister(&xdev);
}

module_exit(xdev_exit);

MODULE_AUTHOR("embedfire");
MODULE_LICENSE("GPL");

4、编写xdrv.c

#include <linux/init.h>
#include <linux/module.h>

#include <linux/device.h>

extern struct bus_type xbus;

char *name = "xdrv";

//保证store和show函数的前缀与驱动属性文件一致,drvname_show()的前缀和drvname
ssize_t drvname_show(struct device_driver *drv, char *buf)
{
	return sprintf(buf, "%s\n", name);
}

//DRIVER_ATTR_RO定义了一个drvname属性文件
DRIVER_ATTR_RO(drvname);

int xdrv_probe(struct device *dev)
{
	printk("%s-%s\n", __FILE__, __func__);
	return 0;
}

int xdrv_remove(struct device *dev)
{
	printk("%s-%s\n", __FILE__, __func__);
	return 0;
}

//定义了一个驱动结构体xdrv,名字需要和设备的名字相同,否则就不能成功匹配
static struct device_driver xdrv = {
	.name = "xdev",
	//该驱动挂载在已经注册好的总线xbus下。
	.bus = &xbus,
	//当驱动和设备匹配成功之后,便会执行驱动的probe函数
	.probe = xdrv_probe,
	//当注销驱动时,需要关闭物理设备的某些功能等
	.remove = xdrv_remove,
};

//调用driver_register函数以及driver_create_file函数进行注册我们的驱动以及驱动属性文件
static __init int xdrv_init(void)
{
	printk("xdrv init\n");
	driver_register(&xdrv);
	driver_create_file(&xdrv, &driver_attr_drvname);
	return 0;
}

module_init(xdrv_init);

//注销驱动以及驱动属性文件
static __exit void xdrv_exit(void)
{
	printk("xdrv exit\n");
	driver_remove_file(&xdrv, &driver_attr_drvname);
	driver_unregister(&xdrv);
}

module_exit(xdrv_exit);

MODULE_AUTHOR("embedfire");
MODULE_LICENSE("GPL");

5、编译安装

1.编译

makefile文件

KERNEL_DIR=../ebf_linux_kernel

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE

obj-m := xdev.o xbus.o xdrv.o

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
modules clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean	

运行make编译成功后,会分别生成xbus.ko、xdev.ko、xdrv.ko内核模块文件。

2.安装xbus总线内核模块

sudo insmod /mnt/xbus.ko

当我们成功加载该内核模块时,内核便会出现一种新的总线 xbus.
在这里插入图片描述
我们可以看到,总线的 devices 和 drivers 目录都是空的,并没有什么设备和驱动挂载在该总线下。红框处便是我们自定义的总线属性文件,当我们执行命令“cat xbus_test”时,可以看到终端上会打印一行字符串: xbus。
在这里插入图片描述

3.安装xdev设备内核模块

sudo insmod /mnt/xdev.ko

加载内核模块后,我们可以看到在/sys/bus/xbus/devices/中多了个设备 xdev,它是个链接文件,最终指向了/sys/devices 中的设备。
在这里插入图片描述
切换到 xdev 的目录下,可以看到,我们自定义的属性文件 xdev_id
在这里插入图片描述

4.安装xdrv驱动内核模块

sudo insmod /mnt/xdrv.ko

成功加载驱动后,可以看到/sys/bus/xbus/driver 多了个驱动 xdev 目录,如图所示:在该目录下存在一个我们自定义的属性文件,使用 cat 命令读该文件的内容,终端会打印字符串“xdrv”
在这里插入图片描述
使用命令 dmesg | tail 来查看模块加载过程的打印信息,当我们加载完设备和驱动之后,总线开始进行匹配,执行 match 函数,发现这两个设备的名字是一致的,就将设备和驱动关联到一起,最后会执行驱动的 probe 函数。
在这里插入图片描述

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-06-08 19:16:18  更:2022-06-08 19:16:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 1:42:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码