学习交流加

  • 个人qq:1126137994
  •  个人微信:liu1126137994
  •  学习交流资源分享qq群:962535112

之前一直做项目,做项目的过程虽然也学习到了不少知识,但是,一直没有好好研究总线设备驱动的机制,今天来学习总结一下!

1、设备驱动模型的需求:

    总线,设备驱动程序,其实就是软件工程中的高内聚,低耦合!所谓高内聚低耦合是模块内各元素联系越紧密就代表内聚性就越高,模块间联系越不紧密就代表耦合性低。所以高内聚、低耦合强调的就是内部要紧紧抱团。设备和驱动就是基于这种模型去实现彼此隔离不相干的。

    那么linux内核为什么要采取这种高内聚,低耦合的特性来设计设备驱动呢?

我们拿DM9000网卡驱动程序来分析。DM9000网卡驱动程序是需要接在CPU的内部总线的,需要地址总线,控制总线,数据总线!以及pin管脚等!

那么我们就需要在DM9000网卡驱动里面定义基地址、中断号等!我们使用的DM9000网卡的基地址0x20000000+0x300=0x20000300,中断号是7。那么:

#define DM9000_BASE 0x20000300
#define DM9000_INTERRUPT 7

int DM9000_send()
{
    writel(GITCHAT_BASE + REG, 1);
    ...
}

int DM9000_init()
{
    request_init(GITCHAT_INTERRUPT, ...);
    ...
}0x20000300
#define DM9000_INTERRUPT 7

int DM9000_send()
{
    writel(GITCHAT_BASE + REG, 1);
    ...
}

int DM9000_init()
{
    request_init(GITCHAT_INTERRUPT, ...);
    ...
}

    但是世界上的板子千千万,有三星、华为、飞思卡尔……每个板子的信息也都不一样,站在驱动的角度看,当每次重新换板子的时候,DM9000_BASE 和DM9000_INTERRUPTR 不再一样,那驱动代码也要随之改变。这样的话一万个开发板要写一万个驱动了,这就是文章刚开始提到的高内聚、低耦合的应用场景。

    驱动想以不变应万变的姿态适配各种设备连接的话就要实现设备驱动模型。基本上我们可以认为驱动不会因为 CPU 的改变而改变,所以它应该是跨平台的。自然像 #define DM9000_BASE 0x20000300,#define DM9000_INTERRUPT 7”这样描述和 CPU 相关信息的代码不应该出现在驱动里。

 

 

 

2、设备驱动模型的实现:

    现在 CPU 板级信息和驱动分开的需求已经刻不容缓。但是基地址、中断号等板级信息始终和驱动是有一定联系的,因为驱动毕竟要取出基地址、中断号等。怎么取?有一种方法是 GITCHAT 驱动满世界去询问各个板子:请问你的基地址是多少?中断号是几?这仍然是一个耦合的情况!!!

    对软件工程熟悉的读者肯定立刻想到能不能设计一个类似接口适配器的类(adapter)去适配不同的板级信息,这样板子上的基地址、中断号等信息都在一个 adapter 里去维护,然后驱动通过这个 adapter 不同的 API 去获取对应的硬件信息。没错,Linux 内核里就是运用了这种设计思想去对设备和驱动进行适配隔离的,只不过在内核里我们不叫做适配层,而取名为总线,意为通过这个总线去把驱动和对应的设备绑定一起,如图:

 

 

基于这种设计思想,Linux 把设备驱动分为了总线、设备和驱动三个实体,这三个实体在内核里的职责分别如下:

 

    模型设计好后,下面来看一下具体驱动的实践,首先把板子的硬件信息填入设备端,然后让设备向总线注册,这样总线就间接的知道了设备的硬件信息。比如一个板子上有一个 DM9000,首先向总线注册:

/* lyy 以下为添加
 * The DM9000 has no eeprom, and it's MAC address is set by
 * the bootloader before starting the kernel.
 */


/* DM9000AEP 10/100 ethernet controller */

#define MACH_SMDK2440_DM9K_BASE (S3C2410_CS4 + 0x300)


static struct resource smdk2440_dm9k_resource[] = {
    [0] = {
        .start = MACH_SMDK2440_DM9K_BASE,
        .end   = MACH_SMDK2440_DM9K_BASE + 3,
        .flags = IORESOURCE_MEM
    },
    [1] = {
        .start = MACH_SMDK2440_DM9K_BASE + 4,
        .end   = MACH_SMDK2440_DM9K_BASE + 7,
        .flags = IORESOURCE_MEM
    },
    [2] = {
        .start = IRQ_EINT7,
        .end   = IRQ_EINT7,
        .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE,
    }
};


static struct dm9000_plat_data smdk2440_dm9k_pdata = {
    .flags      = (DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM),
};

static struct platform_device smdk2440_device_eth = {
    .name       = "dm9000",
    .id     = -1,
    .num_resources  = ARRAY_SIZE(smdk2440_dm9k_resource),
    .resource   = smdk2440_dm9k_resource,
    .dev        = {
        .platform_data  = &smdk2440_dm9k_pdata,
    },
};

/* lyy:以上为添加 */

在结构体smdk2440_devices中添加网卡成员:

static struct platform_device *smdk2440_devices[] __initdata = {
    &s3c_device_ohci,
    &s3c_device_lcd,
    &s3c_device_wdt,
    &s3c_device_i2c0,
    &s3c_device_iis,
    &smdk2440_device_eth, /* lyy:添加 */
};

在static void __init smdk2440_machine_init(void)函数中添加设备到bus总线:

static void __init smdk2440_machine_init(void)
{
	s3c24xx_fb_set_platdata(&smdk2440_fb_info);
	s3c_i2c0_set_platdata(NULL);

	platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));//lyy添加
	smdk_machine_init();
}

    现在 platform 总线上自然知道了板子上关于 DM9000网卡设备的硬件信息,一旦 DM9000 的驱动也被注册的话,总线就会把驱动和设备绑定起来,从而驱动就获得了基地址、中断号等板级信息。总线存在的目的就是把设备和对应的驱动绑定起来,让内核成为该是谁的就是谁的和谐世界,有点像我们生活中红娘的角色,把有缘人通过红线牵在一起。设备注册总线的代码例子看完了,下面看下驱动注册总线的代码示例:

static int __devinit
dm9000_probe(struct platform_device *pdev)
{}
        ....        
        db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
	db->irq_res  = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
        ....
}

    从代码中看到驱动是通过总线 API 接口 platform_get_resource 取得板级信息,这样驱动和设备之间就实现了高内聚、低耦合的设计,无论你设备怎么换,我驱动就可以岿然不动。

    设备向总线注册了板级信息,驱动也向总线注册了驱动模块,但总线是怎么做到驱动和设备匹配的呢?接下来就讲下设备和驱动是怎么通过总线进行“联姻”的。

    总线里有很多匹配方式,比如:

static int platform_match(struct device *dev, struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);

	/* Attempt an OF style match first */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* Then try to match against the id table */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;

	/* fall-back to driver name match */
	return (strcmp(pdev->name, drv->name) == 0);
}

    从上面可知 platform 总线下的设备和驱动是通过名字进行匹配的,先去匹配 platform_driver 中的 id_table 表中的各个名字与 platform_device->name 名字是否相同,如果相同则匹配。

 

3、设备驱动模型的改善:

 

相信通过上面的学习,相信对于设备、驱动通过总线来匹配的模型已经有所了解。如果写代码的话应该是下面结构图所示:

    最底层是不同板子的板级文件代码,中间层是内核的总线,最上层是对应的驱动,现在描述板级的代码已经和驱动解耦了,这也是 Linux 设备驱动模型最早的实现机制,但随着时代的发展,就像是人类的贪婪促进了社会的进步一样,开发人员对这种模型有了更高的要求,虽然驱动和设备解耦了,但是天下设备千千万,每次设备的需求改动都要去修改 board-xxx.c 设备文件的话,这样下去,有太多的板级文件需要维护。完美的 Linux 怎么会允许这样的事情存在,于是乎,设备树(DTS)就登向了历史舞台,下一篇内容将探讨设备树的实现原理和用法。

 

想一起探讨以及获得各种学习资源加我: 
qq:1126137994 
微信:liu1126137994 
可以共同交流关于嵌入式,操作系统,C++语言,C语言,数据结构与算法等技术问题。