随着技术不断进步,系统的拓扑结构越来越复杂,对热插拔、跨平台移植性的要求越来越高,早期的内核难以满足这些要求,从linux2.6内核开始,引入了总线设备驱动模型。其实在linux2.4总线的概念就已经提出来了,直到2.6版本的内核才运用。
Linux系统中有很多条总线,如I2C、USB、platform、PCI等。
以spi为例,假如有M种不同类型CPU,N中不同SPI外设,在写裸机驱动的时候,M种CPU驱动同一个外设需要M份代码,而N种外设使用同一个cpu又需要N份代码,所以需要M*N份代码,这是典型的高内聚低耦合架构。
这种网状的拓扑结构是不符合人的逻辑思维的,将M*N种耦合变成M+1+N中耦合,将大大减少linux移植工作。
在系统中抽象出一条SPI总线,然后总线中(总线注册的那个文件 spi.c 和spi.h,I2C总线注册是i2c-core.c和i2c-core.h)包含SPI控制器抽象结构体spi_master等,spi控制器和外设之间交互采用spi总线提供的标准api来进行,控制器设备和外设驱动填充相关结构体。
试想一下usb,当我们把鼠标或者键盘插入电脑时,是不是会有个驱动加载的过程?这就是在寻找总线上的驱动。总线有一种义务,就是感知设备在总线上的挂载和卸载,同时有义务去寻找与设备匹配的驱动。我们的spi也一样,当有外设挂载到spi总线上的时候,就会寻找总线上所有的驱动与之匹配,匹配成功,则由该驱动服务这个设备。反过来,总线有义务感知驱动在总线上的挂载和卸载,当驱动挂载到总线时,会寻找与之匹配的设备,该驱动就服务于匹配的设备。
总线在内核中的抽象
在linux内核中,总线由bus_type结构描述,定义在linux/device.h中。
struct bus_type {
const char *name; /*总线名称*/
int (*match) (struct device *dev, struct
device_driver *drv); /*驱动与设备的匹配函数*/
………
}
主要关注match函数,当有一个设备挂载到一条总线上的时候,总线要把这个设备和挂载到这条总线上的驱动一一进行匹配,匹配的函数就是这个match指针。
总线的注册与注销
注册:bus_register(struct bus_type *bus)若成功,新的总线将被添加进系统,并可在/sys/bus 下看到相应的目录。
注销:void bus_unregister(struct bus_type *bus)。
进入到板子的/sys/bus目录,ls一下,可以看到系统所有的总线。
随便进入一个目录,如SPI目录
Devices目录表示这条总线上所有挂载的设备。Drivers目录表示这条总线上所有的设备。
下面以一个示例来注册一条总线到系统中,一般情况下,是不需要另外添加总线到设备中的。添加的总线名字叫my_bus,加载驱动之后,会在/sys/bus目录下看到一个my_bus目录。
新建bus.c:
#include
#include
#include
#include
int my_match(struct device *dev, struct device_driver *drv)
{
printk("my_match was run\n");
return !strncmp(dev->kobj.name,drv->name,strlen(drv->name));
}
struct bus_type my_bus_type = {
.name = "my_bus",//总线名称
.match = my_match,//驱动与设备匹配函数
};
EXPORT_SYMBOL(my_bus_type);
static int my_bus_init()
{
int ret;
ret = bus_register(&my_bus_type);
return ret;
}
static void my_bus_exit()
{
bus_unregister(&my_bus_type);
}
module_init(my_bus_init);
module_exit(my_bus_exit);
MODULE_LICENSE("GPL");
首先,总线也是内核的一个模块,我们把它编译成.ko的方式加载到内核,总线的名字是”my_bus”,总线的匹配函数是my_match,当总线上的驱动和设备都挂载上去时,会调用my_match函数进行配对,配对也很简单,就是对比驱动和设备名字是否相同。返回非0表示my_match匹配成功,返回0表示匹配失败。
EXPORT_SYMBOL(my_bus_type);将my_bus_type结构导出给外部文件用,因为设备和驱动都需要指明要挂载到哪条总线上。
编写Makefile,使之生成.ko模块,加载bus.ko,然后在/sys/bus目录下会生成my_bus目录
驱动描述结构
在 Linux内核中, 驱动由 device_driver结构表示。
struct device_driver {
{
const char *name; /*驱动名称*/
struct bus_type *bus; /*驱动程序所在的总线*/
int (*probe) (struct device *dev);
………
}
Name表示驱动的名字;bus表示驱动要挂载到哪条总线上,待会儿将挂载到刚刚创建的my_bus总线上;probe表示驱动和设备匹配成功之后要运行的函数。
驱动的注册与注销:
驱动的注册使用:int driver_register(struct device_driver *drv)
驱动的注销使用:void driver_unregister(struct device_driver *drv)
接下来编写driver.c文件,编译成模块,将驱动加载到内核并挂载到my_bus总线上。
#include
#include
#include
#include
extern struct bus_type my_bus_type;
int my_probe(struct device *dev)
{
printk("driver found the devicre it can handle\n");
return 0;
}
struct device_driver my_driver =
{
.name = "yty",//驱动名字
.bus = &my_bus_type,//属于哪条总线
.probe = my_probe,
};
static int my_driver_init()
{
return driver_register(&my_driver);
}
static int my_driver_exit()
{
driver_unregister(&my_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
驱动的名字叫“yty”,属于bus.c中的my_bus_type这条总线,驱动和设备匹配成功之后,就会运行my_probe函数,也就是会打印出"driver found the devicre it can handle\n"信息。
编译成.ko文件,然后insmod,在/sys/bus/my_bus/drivers目录下就生成了yty目录。
设备描述结构
在 Linux内核中, 设备由struct device结构表示。
struct device {
{
const char *init_name; /*设备的名字*/
struct bus_type *bus; /*设备所在的总线*/
………
}
设备的注册使用int device_register(struct device *dev)
设备的注销使用:void device_unregister(struct device *dev)
编写device.c文件:
#include
#include
#include
#include
extern struct bus_type my_bus_type;
struct device my_dev =
{
.init_name = "yty",
.bus = &my_bus_type
};
static int my_device_init()
{
int ret;
ret = device_register(&my_dev);
return ret;
}
static void my_device_exit()
{
device_unregister(&my_dev);
}
module_init(my_device_init);
module_exit(my_device_exit);
MODULE_LICENSE("GPL");
.init_name要和驱动的.那么一样,要不然匹配不上,.bus仍然是要属于my_bus总线。
编译并加载.ko文件,然后会出现如下打印:
由图可知,当挂载设备到my_bus总线上时,先调用总线上的my_match函数,然后驱动来处理这个设备,驱动中的my_probe就运行了。
总线的感性认识就到此结束了。
SPI总线设备驱动分析
在sourceInsight中打开内核代码drivers/spi/spi.c文件,然后分析。
在spi_init函数中,调用了bus_register注册一条总线,总线的名字叫做spi,spi_bus_type结构就是我们需要关注的,顺便看看.match。
看内核代码挑重要的看,不要每一行都看,直接跳到strcmp函数去,可以知道总线上驱动和设备的配备是通过比较驱动和设备的名字。如果有多个相同的设备,那么就应该定义.id了,靠id来区别我这个驱动到底是服务哪个设备。
总线的注册就讲解完毕。在spi.c中,提供了注册设备和注册驱动的标准api、提供了spi收发函数、spi初始化函数等。可以理解为spi总线向我们提供了标准的API接口。
以系统提供的范例spidev.c为例:
我们知道,在注册一个spi驱动是调用系统给我们提供的函数-spi_register_driver,这个标准的api也是由spi.c提供给我们的。通过sourceInsight跳转到spi_register_driver函数,这个函数就在spi.c中。
由前面的范例代码知道,注册一个驱动使用driver_register。
sdrv->driver.bus = &spi_bus_type;表示这个驱动属于spi这条总线。另外spidev中的probe,remove都通过指针传到了spi_register_driver函数中。设备和驱动匹配成功,调用spi_drv_probe,它经过赋值之后,是指向spidev.c中的spidev_probe。在spi通用外设驱动spidev.c中,调用spi_async来实现发送和接收数据的,spi_async也是由spi.c提供的,即”总线提供标准API”。
Spi设备挂载分析:
添加外设之后,一般都是需要修改板级逻辑的,使用spi通用驱动也不例外。在borad-sam9x5ek.c中要添加。其它cpu类似。
在ek_board_init中调用了at91_add_device_spi函数,将设备注册到系统。
用sourceInsight继续追踪该函数。at91_add_device_spi调用spi_register_board_info 调用spi_register_board_info。spi_register_board_info这个函数就是在spi.c中,也就是说,总线提供标准的API注册设备到总线上。这个API其实最终还是调用device_register将设备注册到总线上。
接下来看看spi_async是如何访问到spi相关寄存器的。追踪spi_async,spi_async调用__spi_async,然后调用return master->transfer(spi, message);也就是调用master的transfer指针函数,这个函数在哪里被赋值了呢?
找到atmel_spi.c文件。S3c6410板子是spi_s3c64xx.c。然后找到probe函数,atmel是atmel_spi_probe。就会看到如下代码:
spi_alloc_master也是spi总线提供的标准API,用于申请一个spi_master结构,然后对这个结构初始化,所以spi_async将调用atmel_spi_transfer,然后我们进一步追踪代码,atmel_spi_transfer调用atmel_spi_next_message调用atmel_spi_next_xfer 调用atmel_spi_next_xfer_pio,atmel_spi_next_xfer_pio函数就是真正读写寄存器的操作了。访问寄存器不能直接写哦,需要iomap哦,而且要采用专门的读写函数,如readl、readb、writel、writeb、spi_wrtel等。
假如有多个控制器,那么外设怎么和某个控制器建立关系呢?这个任务是由板级逻辑来联系的。就以刚刚的spidev板级代码来说
max_speed_hz是说明我这个spidev外设,需要使控制器100万Hz的时钟频率,bus_num说明说明spidev外设需要使用spi0控制器。
总结:
SPI,I2C,USB等采用总线的方式,将主机驱动和外设驱动分离,这样就涉及到四个软件模块:
1.主机端的驱动。根据具体的cpu芯片手册操作IIC、SPI、USB等寄存器,产生各种波形。主机端驱动大部分由原厂实现好。
2.连接主机和外设的纽带。外设驱动不直接调用主机端的驱动来产生波形,而是调用一个标准的API,由这个标准的API把这个波形的传输请求间接转发给了具体的主机端驱动。
3.外设端驱动。外设挂载到IIC、SPI、USB等总线上,我们在probe()函数中去注册它的具体类型(I2C,SPI,USB等类型),当要去访问外设的时候,就调用标准的API。如SPI读写函数spi_async,I2C读写函数:i2c_smbus_read_byte i2c_smbus_write_byte 等。
4.板级逻辑。板级逻辑用来描述主机和外设如何联系在一起的,假如cpu有多个SPI控制器,cpu又接有多个SPI外设,那究竟用哪个SPI控制器去控制外设?这个管理属于板级逻辑的责任。如board-sam9x5ek.c中:.bus_num= 0,表示用SPI0去控制spi通用外设驱动spidev。