###你好!这里是风筝的博客,

###欢迎和我一起交流。

前几天去面试,被问到Linux设备驱动模型这个问题,没答好,回来后恶补知识,找了些资料,希望下次能答出个满意答案。


Linux早期时候,一个驱动对应一个设备,也就对应一个硬件地址,那当有两个一样的设备的时候,就要写两个驱动,显然是不合理的。应该是从Linux2.5开始,就引入了device-bus-driver模型。
其中设备驱动模型主要结构分为kset、kobject、ktype。

kset是同类型kobject对象的集合,可以说是一个容器。
kobject是总线、驱动、设备的三种对象的一个基类,实现公共接口。
ktype,记录了kobject对象的一些属性。

设备驱动模型的核心即是kobject,是为了管理日益增多的设备,使得设备在底层都具体统一的接口。他与sysfs文件系统紧密相连,每个注册的kobject都对应sysfs文件系统中的一个目录。为了直观管理,统一存放的路径,使用了kset。但是仅仅有这些目录没有意义,这两个结构体只能表示出设备的层次关系,所以基本不单独使用,会嵌入到更大的结构体中,(如希望在驱动目录下能看到挂在该总线上的各种驱动,而在设备目录下能看到挂在该总线的各种设备,就将kobject嵌入到描述设备以及驱动的结构体中,这样每次注册设备或驱动,都会在sys目录下有描述)
放上一个经典的图:

这个图其实还漏了一个ktype,kobject都应该包含一个ktype。

Linux设备模型的目的是:为内核建立起一个统一的设备模型,从而有一个对系统结构的一般性抽象描述。

我们可以先看下一个小的测试程序:

#include <linux/device.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sysfs.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/slab.h>
#include <linux/string.h>static struct kset * my_kset;
struct test_kobj {int number;struct kobject kobj;/*嵌入更大的结构体*/
};
static struct test_kobj * test1;
static struct attribute my_attr = {  .name = "name",  .mode = S_IRWXUGO,
};
/*attribute数组*/
static struct attribute *my_attrs[] = {  &my_attr, NULL,  /*最后必须为NULL*/
};static ssize_t kobject_attr_show(struct kobject *kobj, struct attribute *attr,   char *buf)
{  struct test_kobj *obj = container_of(kobj, struct test_kobj, kobj);ssize_t count = 0;  printk("kobject 's number is %d\n", obj->number); printk("kobject 's name is "); count = sprintf(buf, "%s\n", kobject_name(kobj) );  return count;
}  static ssize_t kobject_attr_store(struct kobject *kobj, struct attribute *attr,  const char *buf, size_t count)
{ struct test_kobj *obj = container_of(kobj, struct test_kobj, kobj);sscanf(buf, "%d", &obj->number);printk("%s\n", __FUNCTION__);  return count;
}static struct sysfs_ops my_sys_ops = {  .show = kobject_attr_show,  .store   = kobject_attr_store,
}; void kobject_release(struct kobject *kobj)
{ struct test_kobj *obj = container_of(kobj, struct test_kobj, kobj);kfree(obj);printk("%s\n", __FUNCTION__);
}static struct kobj_type my_ktype = {  .release        = kobject_release,  .sysfs_ops = &my_sys_ops,  .default_attrs = my_attrs,
}; static int __init kobject_init_test(void)
{  int error; my_kset = kset_create_and_add("kobject_test", NULL, NULL);  if (!my_kset) {  goto out;  }  test1 = kzalloc(sizeof(struct test_kobj), GFP_KERNEL);if (!test1) {kset_unregister(my_kset);return -ENOMEM;}test1->number= 1;error = kobject_init_and_add(&test1->kobj, &my_ktype, &my_kset->kobj, "test1");if(error){  kobject_put(&test1->kobj);  goto out;  }printk("%s success.\n", __FUNCTION__);  return 0;  out:  printk("%s failed!\n", __FUNCTION__);  return -1;
}static void __exit kobject_exit_test(void)
{  kobject_del(&test1->kobj);kobject_put(&test1->kobj);kset_unregister(my_kset);  printk("%s\n", __FUNCTION__);
}module_init(kobject_init_test);
module_exit(kobject_exit_test);  MODULE_DESCRIPTION("kobject test");
MODULE_LICENSE("GPL");

可以看到,我们在使用kobject、kset、ktype结构,就在sysfs虚拟文件系统下创建(通过kset_create_and_add和kobject_init_and_add函数)了一些子目录(kobject_test)和属性文件。kset和kobject都可以创建出目录,但是kset的目录下存放kobject目录,kobject下存放属性文件(可以对属性文件进行读写操作,如上图name属性文件,而且kobject目录下也可以存放kobject目录,只需parent指向它即可)。
这个小程序没看懂?没关系,先看下面的分析:

我们对着Linux kernel源码分析下,可以下看看三个结构体的成员:

struct kset {struct list_head list;//包含kobject的链表spinlock_t list_lock;//在访问链表时加锁struct kobject kobj;//嵌入的kobject const struct kset_uevent_ops *uevent_ops;//对发往用户空间的uevent的处理,如热拔插
};
struct kobject {const char       *name;//名字struct list_head  entry;//连接到kset建立层次结构struct kobject     *parent;//指向父节点,面向对象的层次架构struct kset     *kset;//指向所属的kset struct kobj_type  *ktype;//属性文件 struct kernfs_node    *sd; /* sysfs directory entry */struct kref     kref;//引用计数
#ifdef CONFIG_DEBUG_KOBJECT_RELEASEstruct delayed_work  release;
#endifunsigned int state_initialized:1;//初始化状态unsigned int state_in_sysfs:1;//是否处在sysfs下了unsigned int state_add_uevent_sent:1;unsigned int state_remove_uevent_sent:1;unsigned int uevent_suppress:1;
};
struct kobj_type {void (*release)(struct kobject *kobj);/*用于释放kobject占用的资源*/ const struct sysfs_ops *sysfs_ops;/*提供实现以下属性的方法*/ struct attribute **default_attrs;/*用于保存类型属性列表(指针的指针)*/  const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);const void *(*namespace)(struct kobject *kobj);
};

其实说到设备驱动模型,很容易想到platform,之前我们也说过:嵌入式Linux驱动学习笔记(五)------学习platform设备驱动
那我们现在就来具体分析这个吧:
init/main.c里:

kernel_init->kernel_init_freeable->do_basic_setup->driver_init

这是driver_init函数:

void __init driver_init(void)
{/* These are the core pieces */devtmpfs_init();devices_init();/*device、dev目录*/buses_init();/*bus目录*/classes_init();/*class目录*/firmware_init();/*firmware目录*/hypervisor_init();/*hypervisor目录*//* These are also core pieces, but must come after the* core core pieces.*/platform_bus_init();cpu_dev_init();memory_dev_init();container_dev_init();of_core_init();
}

我们看下devices_init函数:

int __init devices_init(void)
{devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);if (!devices_kset)return -ENOMEM;dev_kobj = kobject_create_and_add("dev", NULL);if (!dev_kobj)goto dev_kobj_err;sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);if (!sysfs_dev_block_kobj)goto block_kobj_err;sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);if (!sysfs_dev_char_kobj)goto char_kobj_err;return 0;char_kobj_err:kobject_put(sysfs_dev_block_kobj);//删除block_kobj_err:kobject_put(dev_kobj);dev_kobj_err:kset_unregister(devices_kset);return -ENOMEM;
}

这里面调用kset_create_and_add创建kset并返回给devices_kset,注意这里的devices_kset,可以说是/sys下最大的boss之一了,所有的物理设备都会在device目录下管理,/sys/device/目录是内核对系统中所有设备的分层次表达模型,保存了系统所有的设备。
然后调用kobject_create_and_add函数在/sys/目录下创建dev目录,/sys/dev目录下维护一个按照字符设备和块设备的主次号码(major:minor)链接到真实设备(/sys/devices)的符号链接文件,应用程序通过对这些文件的读写和控制,可以访问实际的设备。
最后再以dev_kobj为父节点,在/sys/dev/目录下创建block和char目录。

这里我们先看kobject_create_and_add函数,再分析kset_create_and_add函数:

struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
{struct kobject *kobj;int retval;kobj = kobject_create();if (!kobj)return NULL;retval = kobject_add(kobj, parent, "%s", name);/*忽略部分无关代码*/return kobj;
}

其实里面函数也没啥,先创建kobject,初始化它,再添加,没啥好说的。
倒是除了kobject_create_and_add函数,还有一个类似的函数:kobject_init_and_add。
kobject_init_and_add传入一个kobject指针和kobj_type指针,然后进行初始化
kobject_create_and_add创建一个kobject变量,并返回其指针,它不用传入kobj_type指针

在kset_create_and_add函数里也会用到kobject,所以我们现在来分析下kset_create_and_add函数:

struct kset *kset_create_and_add(const char *name,const struct kset_uevent_ops *uevent_ops,struct kobject *parent_kobj)
{struct kset *kset;int error;kset = kset_create(name, uevent_ops, parent_kobj);if (!kset)return NULL;error = kset_register(kset);if (error) {kfree(kset);return NULL;}return kset;
}

里面就是具体的创建和注册kset了。
先说创建函数:

static struct kset *kset_create(const char *name,const struct kset_uevent_ops *uevent_ops,struct kobject *parent_kobj)
{struct kset *kset;int retval;kset = kzalloc(sizeof(*kset), GFP_KERNEL);//分配kset空间if (!kset)return NULL;//失败就返回retval = kobject_set_name(&kset->kobj, "%s", name);//设置kset的名字,也即内嵌kobject的名字if (retval) {kfree(kset);return NULL;}kset->uevent_ops = uevent_ops;//kset属性操作kset->kobj.parent = parent_kobj;//设置其parent kset->kobj.ktype = &kset_ktype;//ktype指定为kset_ktype kset->kobj.kset = NULL;return kset;
}

可以看出kset_create函数内容为:
1)调用kobject_set_name函数设置kobject的名称
2)设置kobject的uevent_ops、parent为传入的形参uevent_ops、parent_kobj
3)设置kobject的ktype为系统定义好的ktype变量
4)设置kobject的所属kset为NULL,意思是kobject所属的kset就是kset本身,因为kset结构体包含了一个kobject成员。

这里需要一个注意的,就是ktype 这个结构,即kset_ktype:

static struct kobj_type kset_ktype = {.sysfs_ops    = &kobj_sysfs_ops,.release = kset_release,
};

这里填充了一个释放函数,每个kobject必须有一个释放函数,并且这个kobject必须保持直到这个释放函数被调用到。如果这个条件不能被满足,则这个代码是有缺陷的。注意,假如你忘了提供释放函数,内核会提出警告的;不要尝试提供一个空的释放函数来消除这个警告,你会受到kobject维护者的无情嘲笑。
至于kobj_sysfs_ops,则是关于读写操作相关的操作集:

static const struct sysfs_ops sysfs_ops = {.show    = show,.store  = store,
};

读文件时,会调用到.show的回调函数。
写文件时,会调用到.store的回调函数。

看完了创建函数,接下来是注册函数:

int kset_register(struct kset *k)
{int err;if (!k)return -EINVAL;kset_init(k);//初始化kseterr = kobject_add_internal(&k->kobj);/*初始化kobject,创建对应的sys目录*/  if (err)return err;kobject_uevent(&k->kobj, KOBJ_ADD);return 0;
}

kset_init函数主要是对kset初始化,会将初始化引用计数器(即kobj->kref)为1(当计数器引用计数没到0之前不可以被释放)。接着初始化entry链表结点,用于与所属的kset的list成员组成链表(INIT_LIST_HEAD(&kobj->entry)),以及一些参数的赋值。最后,还初始化以list成员为头结点的链表,它和子kobject的entry成员组成链表(INIT_LIST_HEAD(&k->list))。

kobject_add_internal函数就是关键的kobject函数了:

static int kobject_add_internal(struct kobject *kobj)
{int error = 0;struct kobject *parent;if (!kobj)return -ENOENT;if (!kobj->name || !kobj->name[0]) {//如果kobject的名字为空.退出 WARN(1, "kobject: (%p): attempted to be registered with empty ""name!\n", kobj);return -EINVAL;}parent = kobject_get(kobj->parent);//如果kobj-parent为真,则增加kobj->kref计数,即父节点的引用计数/* join kset if set, use it as parent if we do not already have one */if (kobj->kset) {if (!parent)parent = kobject_get(&kobj->kset->kobj);//如果parent父节点为NULL那么就用kobj->kset->kobj作其父节点,并增加其引用计数kobj_kset_join(kobj);//把kobj的entry成员添加到kobj->kset>list的尾部,现在的层次就是kobj->kset->list指向kobj->entrykobj->parent = parent;}/*删除了部分调试内容*/error = create_dir(kobj);//利用kobj创建目录和属性文件,其中会判断,如果parent为NULL那么就在sysfs_root_kn下创建if (error) {/*删除了部分内容*/} elsekobj->state_in_sysfs = 1;//如果创建成功。将state_in_sysfs建为1。表示该object已经在sysfs中了return error;
}

kobject_add_internal函数内容在注释里都写好了,可以概括为:
1)如果kobject的parent成员为NULL,则把它指向kset的kobject成员。
2)如果kobject的kset成员不为NULL,它会调用kobj_kset_join函数把kobject的entry成员添加到kset的list链表中
3)最后调用create_dir函数创建sys目录

注册函数里最后一个调用就是kobject_uevent函数了,应该是关于热拔插机制的,这不是我们现在关心的内容。
好了,经过上面的折腾,就会在/sys/目录下建立一个devices目录。

接下来继续回到文章开头进入到的devices_init函数:

void __init driver_init(void)
{/* These are the core pieces */devtmpfs_init();devices_init();/*device、dev目录*/buses_init();/*bus目录*/classes_init();/*class目录*/firmware_init();/*firmware目录*/hypervisor_init();/*hypervisor目录*//* These are also core pieces, but must come after the* core core pieces.*/platform_bus_init();cpu_dev_init();memory_dev_init();container_dev_init();of_core_init();
}

我们之前分析的是devices_init函数,其实接下来几个函数都是一样的,在/sys/目录下创建各个目录。
只需要记住
devices_kset对应/sys/devices目录
bus_kset对应/sys/bus目录
devices_kset对应/sys/devices目录
system_kset对应/sys/devices/system目录
class_kset对应/sys/class目录
firmware_kobj对应/sys/firmware目录
hypervisor_kobj对应/sys/hypervisor目录

接下来看下platform_bus_init函数
也就是我们之前用的platform总线了!!
在driver/base/platform.c文件:

struct bus_type platform_bus_type = {.name      = "platform",.dev_groups = platform_dev_groups,.match       = platform_match,//各种关键字匹配.uevent      = platform_uevent,.pm      = &platform_dev_pm_ops,
};
struct device platform_bus = {.init_name   = "platform",
};int __init platform_bus_init(void)
{int error;early_platform_cleanup();error = device_register(&platform_bus);if (error)return error;error =  bus_register(&platform_bus_type);if (error)device_unregister(&platform_bus);of_platform_register_reconfig_notifier();return error;
}

这里,device_register就是在/sys/device/目录下创建platform

int device_register(struct device *dev)
{device_initialize(dev);return device_add(dev);
}

其实也就包含两个函数,一个初始化,一个添加:

void device_initialize(struct device *dev)
{dev->kobj.kset = devices_kset;//设置设备的kobject所属集合,devices_kset即对应/sys/devices/ kobject_init(&dev->kobj, &device_ktype);//初始化设备的kobject INIT_LIST_HEAD(&dev->dma_pools);//初始化设备的DMA池,用于传递大数据mutex_init(&dev->mutex);lockdep_set_novalidate_class(&dev->mutex);spin_lock_init(&dev->devres_lock);//初始化自旋锁,用于同步子设备链表 INIT_LIST_HEAD(&dev->devres_head);//初始化子设备链表头device_pm_init(dev);set_dev_node(dev, -1);
#ifdef CONFIG_GENERIC_MSI_IRQINIT_LIST_HEAD(&dev->msi_list);
#endif
}

注释都写好了,看下device_add函数:

int device_add(struct device *dev)
{struct device *parent = NULL;struct kobject *kobj;struct class_interface *class_intf;int error = -EINVAL;struct kobject *glue_dir = NULL;dev = get_device(dev);//增加设备的kobject的引用计数if (!dev)goto done;if (!dev->p) {error = device_private_init(dev);//初始化dev的私有成员,及其链表操作函数if (error)goto done;}if (dev->init_name) {//保存设备名,以后需要获取时使用dev_name函数获取dev_set_name(dev, "%s", dev->init_name);dev->init_name = NULL;}/* subsystems can specify simple device enumeration */if (!dev_name(dev) && dev->bus && dev->bus->dev_name)dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);if (!dev_name(dev)) {error = -EINVAL;goto name_error;}pr_debug("device: '%s': %s\n", dev_name(dev), __func__);parent = get_device(dev->parent);//返回父节点,增加父节点引用计数,如果没有返回NULL kobj = get_device_parent(dev, parent);//以上层devices为准重设dev->kobj.parent  if (kobj)dev->kobj.parent = kobj;/* use parent numa_node */if (parent && (dev_to_node(dev) == NUMA_NO_NODE))set_dev_node(dev, dev_to_node(parent));/* first, register with generic layer. *//* we require the name to be set before, and pass NULL */error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);//设置dev->kobj的名字和父对象,并建立相应目录 if (error) {glue_dir = get_glue_dir(dev);goto Error;}/* notify platform of device entry */if (platform_notify)platform_notify(dev);error = device_create_file(dev, &dev_attr_uevent);//建立uevent属性文件  if (error)goto attrError;error = device_add_class_symlinks(dev);if (error)goto SymlinkError;error = device_add_attrs(dev);if (error)goto AttrsError;error = bus_add_device(dev);if (error)goto BusError;error = dpm_sysfs_add(dev);if (error)goto DPMError;device_pm_add(dev);if (MAJOR(dev->devt)) {error = device_create_file(dev, &dev_attr_dev);//在sys下产生dev属性文件 if (error)goto DevAttrError;error = device_create_sys_dev_entry(dev);//在/sys/dev目录建立对设备的软链接if (error)goto SysEntryError;devtmpfs_create_node(dev);}/* Notify clients of device addition.  This call must come* after dpm_sysfs_add() and before kobject_uevent().*/if (dev->bus)blocking_notifier_call_chain(&dev->bus->p->bus_notifier,BUS_NOTIFY_ADD_DEVICE, dev);kobject_uevent(&dev->kobj, KOBJ_ADD);//向用户空间发出KOBJ_ADD 事件 bus_probe_device(dev);//检测驱动中有无适合的设备进行匹配,现在只添加了设备,还没有加载驱动,所以不会进行匹配 if (parent)klist_add_tail(&dev->p->knode_parent,&parent->p->klist_children);//把该设备的节点挂到其父节点的链表  if (dev->class) {mutex_lock(&dev->class->p->mutex);/* tie the class to the device */klist_add_tail(&dev->knode_class,&dev->class->p->klist_devices);/* notify any interfaces that the device is here */list_for_each_entry(class_intf,&dev->class->p->interfaces, node)if (class_intf->add_dev)class_intf->add_dev(dev, class_intf);mutex_unlock(&dev->class->p->mutex);}/*省略部分error内容*/
}

device_add函数是比较重要的,注释基本都写好了,可以概括为:
1)增加kobj->kref计数
2)初始化dev的私有成员
3)设置设备名称
4)增加父节点引用计数
5)将dev->kobj添加到dev->kobj.parent对应目录下
6)dev->kobj下创建属性文件
7)在/sys/dev目录建立对设备的软链接
8)驱动检测

其中,驱动检测函数:bus_probe_device
我在嵌入式Linux驱动笔记(五)------学习platform设备驱动分析有,可以看看。

最后,我们接着看 bus_register(&platform_bus_type);
篇幅有点长了,函数我就写点重要的即可

int bus_register(struct bus_type *bus)
{int retval;struct subsys_private *priv;struct lock_class_key *key = &bus->lock_key;priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);if (!priv)return -ENOMEM;priv->bus = bus;bus->p = priv;BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);if (retval)goto out;priv->subsys.kobj.kset = bus_kset;priv->subsys.kobj.ktype = &bus_ktype;priv->drivers_autoprobe = 1;retval = kset_register(&priv->subsys);if (retval)goto out;retval = bus_create_file(bus, &bus_attr_uevent);if (retval)goto bus_uevent_fail;priv->devices_kset = kset_create_and_add("devices", NULL,&priv->subsys.kobj);if (!priv->devices_kset) {retval = -ENOMEM;goto bus_devices_fail;}priv->drivers_kset = kset_create_and_add("drivers", NULL,&priv->subsys.kobj);if (!priv->drivers_kset) {retval = -ENOMEM;goto bus_drivers_fail;}/*后面的省略*/
}

再次强调:
priv->subsys.kobj.kset = bus_kset;
priv->subsys.kobj.ktype = &bus_ktype;
这里设置了所属的kset和ktype。
ktype结构体里包含了sysfs_ops结构体,里面就是对文件的读写操作:

static const struct sysfs_ops bus_sysfs_ops = {.show    = bus_attr_show,//读文件.store    = bus_attr_store,//写文件
};

最后,bus_register函数里还调用了kset_create_and_add函数在/sys/platform/目录下创建devices和drivers目录,里面存放我们platform平台下注册的设备和驱动。

好了,到此,我们就来再次小小归纳下
*在kset下还可能会有更深的kset
*kset包含一个或多个kobject,方便管理
*kobject并不一定需要kset
*kobject下有属性文件,·向用户层提供了表示和操作这个 kobject 的属性特征的接口
*kobject 下还有一些符号链接文件,指向其它的 kobject

现在,是不是对设备驱动模型有了更为直观的认识?现在回头看看文章开头的小程序,是不是轻而易举的理解了呢?

udev就会读取/sys目录下的信息,在/dev目录下创建设备了。

最后,强烈建议观看《Linux设备模型浅析之设备篇》和《Linux那些事儿之我是Sysfs》这两篇文章
这里有篇文章,是翻译了内核文档(Documentation\kobject.txt),可以看看:
http://www.cnblogs.com/helloahui/p/3674933.html

后记:关于uevent,在这里有描述:http://blog.csdn.net/fanqipin/article/details/8287343

嵌入式Linux驱动笔记(十六)------设备驱动模型(kobject、kset、ktype)相关推荐

  1. 移植嵌入式linux到arm处理器,移植嵌入式Linux到ARM处理器S3C2410:设备驱动

    设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说,Linux的设备驱动程序需要完成如下功能: ·设备初始化.释放: ·提供各类设备服务: ·负责内核和设备之间的数据 ...

  2. 嵌入式Linux驱动笔记(十一)------i2c设备之mpu6050驱动

    ###你好!这里是风筝的博客, ###欢迎和我一起交流. 上一节讲了i2c框架: 嵌入式Linux驱动笔记(十)------通俗易懂式了解i2c框架 这次就来写一写真正的i2c设备驱动: mpu605 ...

  3. Linux-USB驱动笔记(六)--设备驱动框架

    Linux-USB驱动笔记(六)--设备驱动框架 1.前言 2.USB设备驱动 3.重要结构体 3.1.usb_driver -- USB设备驱动 3.2.usb_device_id -- 支持的US ...

  4. 嵌入式Linux视频笔记----驱动开发

    https://www.bilibili.com/video/BV1pf4y1974n/?spm_id_from=333.788.videocard.1 基本看完了,基本只看视频没看详细的pdf,试验 ...

  5. 基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

    基于嵌入式Linux的千兆以太网卡驱动程序设计及测试 一. 引言 千兆以太网是一种具有高带宽和高响应的新网络技术,相关协议遵循IEEE 802.3规范标准.采用和10M以太网相似的帧格式.网络协议和布 ...

  6. STC8H开发(十六): GPIO驱动XL2400无线模块

    目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解) STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解) ST ...

  7. linux i2c核心,总线与设备驱动,Linux2.6.37 I2C驱动框架分析(一)

    最近工作中又使用到了I2C,所以借S3C2440开发板GT2440为硬件平台温习一遍I2C驱动体系. linux内核中IIC驱动的体系框架 linux内核中IIC部分驱动代码位于:/drivers/i ...

  8. Linux内核学习-字符设备驱动学习(二)

    在Linux内核学习-字符设备驱动学习(一)中编写字符设备驱动的一种方法,但是需要手动创建设备节点. 有没有能够自动的创建设备节点的呢? 有!使用class_create()和device_creat ...

  9. 【linux驱动分析】misc设备驱动

        misc设备驱动.又称混杂设备驱动. misc设备驱动共享一个设备驱动号MISC_MAJOR.它在include\linux\major.h中定义:         #define MISC_ ...

最新文章

  1. 转载:【OpenCV入门教程之四】 ROI区域图像叠加初级图像混合 全剖析
  2. 操作系统页面置换算法
  3. flowjo软件使用方法_管家婆软件使用方法出库教程,管家婆软件做账流程视频_双全科技...
  4. 计算机英语讲课笔记02
  5. SpringBoot Jar包外部application文件
  6. 刨根问底(二):从INode客户端看如何培养兴趣 (续)
  7. 使用IB时设置textView属性崩溃
  8. java画一个小车_小轿车简笔画怎么画
  9. 深入理解数据类型、变量类型属性、内存四区和指针
  10. 实例讲解朴素贝叶斯分类器
  11. 【Android笔记】Android引用第三方依赖包library报错解决方法
  12. 手机APP逆向工具介绍
  13. QTableView样式
  14. 微服务系列:分布式日志 ELK 搭建指南
  15. 16位图xxxxxxxxxxxx
  16. HTML 表单 (form) 的作用解释
  17. 阿里云返回的视频截图问题
  18. 基于ZigBee的智能监控系统-上位机代码
  19. 读曾仕强《管理的方与圆》笔记三
  20. 《SteamVR2.2.0官方教程(二)》(Yanlz+Unity+XR+VR+AR+MR+SteamVR+Valve+Tutorials+Interaction+Oculus+立钻哥哥++ok++)

热门文章

  1. 给定经纬度计算距离_根据经纬度计算两地间的距离
  2. vc工程下的文件后缀解析
  3. 解决 Xshell6|Xftp6 强制升级问题
  4. potplay显示服务器关闭,PotPlayer关掉左上角显示播放时间的操作教程
  5. 斑斑驳驳。伤情时节,镌为别离的箫瑟
  6. 如何设计出别具一格的全息投影餐厅
  7. 实例分割研究综述总结
  8. win732位oracle,win7 32位安装oracle10g步骤
  9. 【C初阶】第一篇——初识C语言(万字篇,带你敲响C语言的大门)
  10. 网络封包截取工具Charles