目录
本章目标
一、设备模型基础
二、总线、设备和驱动
---------------------------------------------------------------------------------------------------------------------------------
工作两周了,真吃不消啊,虽然年轻但是早8.30晚10点还是不太行,第一种嗷嗷叫中午他们午休我在看内核源码,这周遭不住了以前接触的SOC都是M核或者A核的,真没想到还有ARM核和FPGA结合的SOC,查了资料发现ARM+FPGA+DSP三合一的SOC都有,真是世界之大我就是个渣。由于是国产化的芯片测试我们测试设备的主控包括FPGA都是国产的。之前还在学习阶段老是听一堆人在那说国产芯片手册不行。我用了之后发现和三星的没什么区别,就是把多写了点高大上但是对开发没用的东西。剩下都一样也没有不详细什么的。国产芯片总得来说还是很不错的。最后我想吐槽一下,研究生究竟比本科生多了什么。刚来什么都干不了还得学习。然后每天到点就下班不用加班工作还比本科生高。多学了三年除了浪费青春感觉没多学什么啊。除了这种也有离谱的怪胎,加拿大留学生高二暑假回国兼职能直接上手干活。13岁的初中生报培训班课余时间学嵌入式。真离谱哇卷死我啦。好了就到这里后面介绍嵌入式linux的设备模型。
---------------------------------------------------------------------------------------------------------------------------------
本章目标
之前编写的驱动程序虽然都能正常工作,但是还是存在着一些弊端。本章首先罗列出了这些弊端,然后引出了 Linux的设备模型,并对核心的底层技术进行了讨论。接下来详述平台设备和驱动的实现方法,在这个过程中将会看到所提到的弊端是如何一个个被解决的。最后讨论了 Linux 内核引入的设备树,并实现了对应的驱动。本章的内容较抽象,需要具备一些面向对象的编程思想。
在前面的基础上,我们已经能够开发一个功能较完备的字符设备驱动了,但是仔细考虑这个驱动,还是会发现一些不足,主要存在下面的一些问题。
(1)设备和驱动没有分离,也就是说,设备的信息是硬编码在驱动代码中的,这给驱动程序造成了极大的限制。如果硬件有所改动,那么必然要修改照动代码(比知对于第前面的 LED 硬件,如果改变了驱动 LED 的管脚,那么就必然要修改 LED 的驱动代码)这样驱动的通用性将会非常差。这是最突出的一个问题,必须要很好地解决。
(2)没有类似于 Windows 系统中的设备管理器,不可以方便查看设备和驱动的信息。
(3)不能自动创建设备节点。
(4)驱动不能自动加载。
(5)U 盘 SD 卡等不能自动挂载。
(6) 没有电源管理。
其实这些问题在 Linux下都是有解决方法的,这些间题的解决主要依托于 Linux 的设备驱动模型,本章后面的内容都会围绕怎样解决这些问题来展开。
一、设备模型基础
首先我们来看上面提到的第二个问题,就是关于设备和驱动信息的展示。在 Linux系统中有一个sysfs伪文件系统,挂于/sys 目录下,该目录详细罗列了所有与设备、驱动和硬件相关的信息。例如,在FS4412 的终端上,可以使用下面的命令来查看。
bus下面是一些总线接口,我最近pcie用的挺多的,咱们三星这块板子里面没有这个接口
devices下面是一些具体的设备
这个是我们的网卡设备
这个是dm9000的驱动
在/sys目录下有很多子目录,例如 block 目录下是块设备、bus 目录下是系统中的有总线(如12C、SPI和USB 等)、class 目录下是一些设备类 (如 input 输入设备类、t终端设备类)、devices 目录下是系统中所有的设备。再仔细查看sys/bus/platfom/devices/5000000.ethemet/目录,它是一个挂接在一个叫 platform 总线下的以太网设备,其目录下的 driver 是一个软链接,指向了../../../bus/platform/drivers/dm9000,也就是说,该设备是被注册在 platfor总线下的一个名叫dm9000的驱动程序所驱动再看对应的驱动目录/sys/bus/platform/drivers/dm9000/,会发现该驱动程序驱动了../../.J../devices/5000000.srom-cs1/5000000.ethermet 设备,即驱动了 devices 目录下的以太网设备,而/sys/busplatform/devices/5000000.ethemnet 又是指向.J.././devices/5000000.srom-csl/5000000.ethemet的软链接。所以也可以说前面的驱动程序驱动了/sys/bus/platform/drivers/dm9000/设备。
上面的内容看起来有点乱,但思路是清晰的,即在总线 bus 目录下有很多具体的总线,而具体的总线目录下有注册的驱动和挂接的设备,注册的驱动程序驱动对应总线目录下的某些具体设备,总线目录下的某些设备被对应总线下的某个驱动程序所驱动。
那么上面这些信息是怎么来的呢。我们知道,伪文件系统在系统运行时才会有内容也就是说,伪文件系统的目录、文件以及软链接都是动态生成的,这些内容都是反映核的相关信息,回顾我们之前学习的 proc 接口,不难猜想得出这些信息的生成可以在驱动中来实现。接下来我们就来讨论要生成这些信息的一个重要内核数据结构一一struct kobject。
了解 MFC 或者 QT 的人都知道那些窗口部件都是一层一层继承下来的,而在最上层有一个最基础的类,MFC 的根类是 CObject,而QT的根类是QObject。这里我们将结构看成类。那么kobject就是linux设备驱动模型的中的根类。作为驱动开发者,我们没有必要了解kobject的详细信息,就像作为一个QT应用程序开发者不需要了解QObject的详细信息一样。在这里,我们只需要知道他和/sys目录下的目录和文件的关系。
当向内核成功添加一个 kobject 对象后,底层的代码会自动在/sys 目录下生成一个子目录。另外,kobject 可以附加一些属性,并绑定操作这些属性的方法,当向内核成功添加一个 kobject 对象后,其附加的属性会被底层的代码自动实现为对象对应目录下的文件,用户访问这些文件最终就变成了调用操作属性的方法来访问其属性。最后,通过 sys 的API接口可以将两个 kobject 对象关联起来,形成软链接。
除了 struct kobject,还有一个叫 struct kset 的类,它是多个 kobject 对象的集合,也就是多个 kobiect 对象可以通过一个 kset 集合在一起。kset 本身也内了一个kobject,它可以作为集合中的 kobiect 对象的父对象,从而在 kobject 之间形成父子关系,这种父子关系在/sys目录下体现为父目录和子目录的关系。而属于同一集合的 kobject 对象形成兄弟关系,在/sys目录下体现为同级目录。kset 也可以附加属性,从而在对应的目录下产生文件。
为了能更好地了解这部分内容,而又不过分深入细节,特别编写了一个非常简单的模块,为了突出主线,省略了出错处理
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kobject.h>
static struct kset *kset;
static struct kobject *kobj1;
static struct kobject *kobj2;
static unsigned int val = 0;
static ssize_t val_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
return snprintf(buf, PAGE_SIZE, "%d\n", val);
}
static ssize_t val_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
char *endp;
printk("size = %d\n", count);
val = simple_strtoul(buf, &endp, 10);
return count;
}
static struct kobj_attribute kobj1_val_attr = __ATTR(val, 0666, val_show, val_store);
static struct attribute *kobj1_attrs[] = {
&kobj1_val_attr.attr,
NULL,
};
static struct attribute_group kobj1_attr_group = {
.attrs = kobj1_attrs,
};
static int __init model_init(void)
{
int ret;
kset = kset_create_and_add("kset", NULL, NULL);
kobj1 = kobject_create_and_add("kobj1", &kset->kobj);
kobj2 = kobject_create_and_add("kobj2", &kset->kobj);
ret = sysfs_create_group(kobj1, &kobj1_attr_group);
ret = sysfs_create_link(kobj2, kobj1, "kobj1");
return 0;
}
static void __exit model_exit(void)
{
sysfs_remove_link(kobj2, "kobj1");
sysfs_remove_group(kobj1, &kobj1_attr_group);
kobject_del(kobj2);
kobject_del(kobj1);
kset_unregister(kset);
}
module_init(model_init);
module_exit(model_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A simple module for device model");
代码第 42行使用 kset_create_and_add 创建并向内核添加了一个名叫 kset 的 kset 对象.代码第 43 行和第 44 行用 kobject_creae_and _add 分别创建并向内添加两个名叫 kobj1和kobj2 的 kobject 对象。代码第 46 行为kobj1添加了一组属性 kobj1_attr_group.这组属性中只有一个属性叫 kobj1_attr_group,属性的名字叫val,所绑定的读和写的方法分别是val show 和 val_store。对应的文件访间权限是 0666。代码第47 行使用 sysfs_create_link在 kobj2 下创建了一个kobj1 的软链接,名叫 kobj1。
代码第 54 行至第 58 行是初始化操作的反操作,用于删除软链接、属性和对象。
属性 val 的读方法将 val 的值以格式%d 打印在 buf 中,那么读相应的属性文件则会得到 val 的十进制字符串。属性 val 的写方法是将用户写入文件的内容,即 buf中的字符串通过simple_strtoul 将字符申转换成十进制的数值再赋值给 val。
没有安装tree回到ubuntu环境
哪天有时间我重新规划一下内核用chroot来给开发板加点命令
在创建 kset 对象时,由于没有指定其父对象,所以 kset 位于/sys 目录下,在创建 kobjl和 kobj2 时,指定其父对象为 kset 中内嵌的 kobject,所以 kobj1和 kobj2位于 kset 目录之下。kobi1 附加了一个属性叫 val,所以在 kobj1 目录下有一个val 的文件,对该文件可以进行读写,其实就是对属性 val 进行读写。在 kobj2 下创建了一个软链接 kobj1,所以在kobj2目录下有 kobj1的软链接。对象的关系如图所示。其中,虚线表示 kobj1、kobj2属于集合 kset,kobjl和 kobj2 实线指向 kset 内的 kobject 表示它们的父对象是 kset内嵌的 kobject。
二、总线、设备和驱动
如图所示,在一台拥有 USB 总线的计算机系统上,USB,总线会在外部流出很多USB 接口,挂接很多 USB 设备。为了让这些设备能正常工作,系统上也会安装其对应的驱动。虽然这些驱动在硬件上和 USB 总线没有直接的连接,但是从软件层面来看,它们是注册在 USB 总线下面的。一个便于理解的简化后的情况是这样的: 当接入一个 USB设备时,USB 总线会立即感知到这件事,并去追历所有注册在 USB 总线上的驱动(在这个过程中可能会自动加载一个匹配的 USB 驱动),然后调用驱动中的一段代码来探测是否能够驱动刚插入的 USB 设备,如果可以,那么总线完成驱动和设备之间的绑定。
为了刻画上面的三种对象,Linux 设备模型为这三种对象各自定义了对应的类:struct bus_type 代表总线、struct device 代表设备、struct device_driver 代表驱动。这三者都内嵌了struct kobject或struct kset,于是就会生成对应的总线、设备和驱动的目录。另外,Linux内核还为这些kobject和 kset 对象附加了很多属性,于是也产生了很多对应目录下的文件可以这样认为,总线、设备和驱动都继承自同一个基类 struct kobject,使用面向对象的思想来理解它们之间的关系会非常容易。将这三者分开来刻画,不仅和现实生活中的情景相符合,更重要的是解决了本章开始提出的第一个问题,那就是实现了设备和驱动的分离。设备专门用来描述设备所占有的资源信息,而驱动和设备绑定成功后,驱动负责从设备中动态获取这些资源信息,当设备的资源改变后,只是设备改变而已,驱动的代码可以不做任何修改,这就大大提高了驱动代码的通用性。另外,总线是联系两者的桥梁是一条重要的纽带。
为了能够更好地理解这个设备模型对驱动编程带来的影响,又不用过多地深入细节,我们以一个最简单的例子来进行说明:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
static int vbus_match(struct device *dev, struct device_driver *drv)
{
return 1;
}
static struct bus_type vbus = {
.name = "vbus",
.match = vbus_match,
};
EXPORT_SYMBOL(vbus);
static int __init vbus_init(void)
{
return bus_register(&vbus);
}
static void __exit vbus_exit(void)
{
bus_unregister(&vbus);
}
module_init(vbus_init);
module_exit(vbus_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A virtual bus");
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
extern struct bus_type vbus;
static struct device_driver vdrv = {
.name = "vdrv",
.bus = &vbus,
};
static int __init vdrv_init(void)
{
return driver_register(&vdrv);
}
static void __exit vdrv_exit(void)
{
driver_unregister(&vdrv);
}
module_init(vdrv_init);
module_exit(vdrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A virtual device driver");
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
extern struct bus_type vbus;
static void vdev_release(struct device *dev)
{
}
static struct device vdev = {
.init_name = "vdev",
.bus = &vbus,
.release = vdev_release,
};
static int __init vdev_init(void)
{
return device_register(&vdev);
}
static void __exit vdev_exit(void)
{
device_unregister(&vdev);
}
module_init(vdev_init);
module_exit(vdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A virtual device");
在 vbus.c 文件中,代码第 12 行至第 15 行定义了一个代表总线的 vbus 对象,该总线的名字是 vbus,用于匹配驱动和设备的函数是 vbus_match。代码第21 行向内核注册了该总线。代码第 26 行是总线的注销。为了简单起见,vbus_match 仅仅返回1,表示传入的设备和驱动匹配成功,而更一般的情况是考察它们的 ID 号是否匹配。
在 vdrv.c 文件中,代码第 9 行至第 12 行定义了一个代表驱动的 vdrv 对象,该驱动的名字是 vdrv,所属的总线是 vbus,这样注册这个驱动时,就会将之注册在 vbus 总线之下代码第 16行和第21行分别是驱动的注册和注销操作。模块中使用了 bus 模块导出的符号 vbus。
在 vdev.c 文件中,代码第 13 行至第 17 行定义了一个代表设备的 vdev 对象,该设备的名字是 vdev,是完全用代码虚拟出来的一个设备。所属的总线是 vbus,这样注册这个设备时,就会将之挂接到 vbus 总线之下。还有一个用于释放的函数 dev_release,为了简单起见,这个函数什么都没做。代码第 21 行和第 26 行分别是设备的注册和注销。模块中使用了 vbus 模块导出的符号 vbus。
下面是编译和测试的命令。
在加载了 vbus 模块后,/sys/bus 目录下自动生成了 bus 目录,并且在 vbus 目录下生成了 devices 和 drivers 两个目录,分别来记录挂接在 vbus 总线上的设备和注册在 vbus 总线上的驱动。当加载了vdrv 模块后,/sys/bus/vbus/drivers 目录下自动生成了 vdrv 目录此时还没有设备与之绑定。当加载了 vdev 模块后,/sys/bus/vbus/devices 目录下自动生成了vdev 目录,并且和../../bus/vbus/drivers/vdrv 的驱动绑定成功,在/sys/devices 目录下也自动生成了 vdev目录,其实/sys/bus/vbus/devices/vdev 是指向/sys/bus/vbus/devices/vdev的软链接。最后 /sys/bus/vbus/drivers/vdrv/中的 vdev 也指定了其绑定的设备为.././..../devicesvdev。
这和我们在前面看到的 DM9000 网卡非常类似,只是 DM9000 网卡设备是挂接在platform总线下的,而驱动也是注册在 platform 总线下的。
虽然使用 struct bus_type struct device 和 struct device_driver 能够实现 Linux 设备模型,但是它们的抽象层次还是太高,不能具体地刻画某一种特定的总线。所以一种具体的总线会在它们的基础上派生出来,形成更具体的子类,这些子类对象能够更好地描述相应的对象。比如,针对USB 总线就派生出了 struct usb_bus_type、struct usb_device和struct usb_driver,分别代表具体的USB 总线、USB 设备和USB 驱动。通常情况下,总线已经在内核中实现好,我们只需要写对应总线的驱动即可,有时候还会编写相应的设备注册代码。