Linux设备驱动中的软件架构思想

时间:2021-06-03 08:46:32

目录

更新记录

version status description date author
V1.0 C Create Document 2019.3.31 John Wan

status:

C―― Create,

A—— Add,

M—— Modify,

D—— Delete。

注:内核版本 3.0.15,迅为iTop4412开发板

一、Linux驱动的软件架构

1.1 出发点

  为适应多种体系架构的硬件,增强系统的可重用和跨平台能力。

1.2 分离思想

  为达到一个驱动最好一行都不改就可以适用任何硬件平台的目的,将驱动与设备分离开来,驱动只管驱动,设备只管设备,而驱动以某种通用的标准途径去拿板级信息,从而降低驱动与设备的耦合程度。

Linux设备驱动中的软件架构思想

1.3 分层思想

  对于同类设备,其基本框架都是一样的,那么提炼出一个中间层,例如:对于 Input 设备(按键、键盘、触摸屏、鼠标)来说,尽管 file_operation、I/O模型不可或缺,但基本框架都是一样的,因此可提炼出一个 Input 核心层,把跟 Linux 接口以及整个一套 input 事件的汇报机制都在这里面实现。

Linux设备驱动中的软件架构思想

二、platform设备驱动

  platform:linux中的一种虚拟总线。一个现实的linux设备和驱动通常都需要挂接在一种总线上(方便管理),例如PCI、USB、I2C、SPI等,但是对于在Soc系统中集成的独立外设控制器、挂接在Soc内存空间的外设等却不能依附于上述总线,这时候linux就发明了一种虚拟总线,来管理这一类的设备(没有实体的硬件总线电路)。

  platform设备驱动模型中,分为设备、驱动、总线3个实体,分别称为 platform_devicepaltform_driverplatform总线,总线负责将设备和驱动进行绑定。在系统每注册一个设备时,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动时,会寻找与之匹配的设备,而匹配的过程则由总线完成。

2.1 platform设备

  platform设备:由 platform_device 结构体构成,负责管理外设的资源,例如 I/O资源、内存资源、中断资源等等。

  原型:linux/platform_device.h

struct platform_device {
const char * name;
int id;
struct device dev;
u32 num_resources;
struct resource * resource; const struct platform_device_id *id_entry; /* MFD cell pointer */
struct mfd_cell *mfd_cell; /* arch specific additions */
struct pdev_archdata archdata;
};

2.1.1 resource 结构体

   resource 结构体,描述了 platform_device 的资源:

struct resource {
resource_size_t start; //资源的开始
resource_size_t end; //资源的结束
const char *name;
unsigned long flags; //资源的类型
struct resource *parent, *sibling, *child;
};

  参数 flags 常用类型 IORESOURCE_IOIORESOURCE_MEMIORESOURCE_IRQIORESOURCE_DMA等。参数 startend 的含义会随着 flags 的不同有所变化。

1)flagsIORESOURCE_MEMstartend 分别表示该platform_device占据的内存的开始与结束地址;

2)flagsIORESOURCE_IRQstartend 分别表示该`platform_device 使用的中断号的开始值与结束值,如果使用 1个中断号,开始与结束值相同;

  同类型的资源可以有多份,例如某设备占据了多个内存区域,则可以定义多个 IORESOURCE_MEM

  例如在 arch/arm/mach-at91/board-sam9261ek.c 板文件中为 DM9000 网卡定义的 resource:

static struct resource dm9000_resource[] = {
[0] = {
.start = AT91_CHIPSELECT_2,
.end = AT91_CHIPSELECT_2 + 3,
.flags = IORESOURCE_MEM
},
[1] = {
.start = AT91_CHIPSELECT_2 + 0x44,
.end = AT91_CHIPSELECT_2 + 0xFF,
.flags = IORESOURCE_MEM
},
[2] = {
.start = AT91_PIN_PC11,
.end = AT91_PIN_PC11,
.flags = IORESOURCE_IRQ
| IORESOURCE_IRQ_LOWEDGE | IORESOURCE_IRQ_HIGHEDGE,
}
};

2.1.2 device 结构体中的 platform_data 资源

  设备除了可在 BSP 中定义资源以外,还可以附加一些数据信息,因为对设备的硬件描述除了中断、内存等标准资源以外,可能还会有一些配置信息,而这些配置信息也依赖于板,不适宜直接放在设备驱动上。

  因此,platform_device 提供可供每个设备驱动自定义的 platform_data 形式以支持添加一些数据信息,即 Linux 内核不对这块的数据做要求。

  device 结构体:

/**
* struct device - The basic device structure
......
* @platform_data: Platform data specific to the device.
* Example: For devices on custom boards, as typical of embedded
* and SOC based hardware, Linux often uses platform_data to point
* to board-specific structures describing devices and how they
* are wired. That can include what ports are available, chip
* variants, which GPIO pins act in what additional roles, and so
* on. This shrinks the "Board Support Packages" (BSPs) and
* minimizes board-specific #ifdefs in drivers.
......
*/
struct device {
......
void *platform_data; /* Platform specific data, device
core doesn't touch it */
......
};

  例如在 arch/arm/mach-at91/board-sam9261ek.c 板文件中,将 platform_data 定义了 dm9000_plat_data 结构体,完成定义后,将MAC地址、总线宽度、板上有无EEPROM信息等放入:

static struct dm9000_plat_data dm9000_platdata = {
.flags = DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM,
}; static struct platform_device dm9000_device = {
.name = "dm9000",
.id = 0,
.num_resources = ARRAY_SIZE(dm9000_resource),
.resource = dm9000_resource,
.dev = {
.platform_data = &dm9000_platdata,
}
};

2.1.3 platform_device 的注册

  对于Linux 2.6 ARM 平台而言,对 platform_device 的定义通常在 BSP 的板文件中实现,在板文件中,将 platform_device 归纳为一个数组,随着板文件的加载,最终通过 platform_add_devices() 函数统一注册。

  platform_add_devices() 函数可以将平台设备添加到系统中,这个函数的原型为:

int platform_add_devices(struct platform_device **devs, int num)

  第一个参数为平台设备数组的指针,第二个参数为平台设备的数量,函数的内部是调用 platform_device_register() 函数逐一注册平台设备。

  如果注册顺利,可在 sys/devices/platform 目录下看到相应名字的子目录。

  Linux 3.x 之后,ARM Linux 不太以编码的形式去填写 platform_device 和注册,更倾向于根据设备树中的内容自动展开platform_device

2.2 platform驱动

  platform驱动:由 platform_driver 结构体构成,负责驱动的操作实现,例如加载、卸载、关闭、悬挂、恢复等。原型位于 linux/platform_driver.h中:

struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
}; /* @probe: Called to query the existence of a specific device,
* whether this driver can work with it, and bind the driver
* to a specific device.
* @remove: Called when the device is removed from the system to
* unbind a device from this driver.
* @shutdown: Called at shut-down time to quiesce the device.
* @suspend: Called to put the device to sleep mode. Usually to a
* low power state.
* @resume: Called to bring a device from sleep mode.
*/

  probe()remove() 分别对应驱动在加载、卸载时执行的操作。

  而直接填充 platform_driversuspend()resume() 做电源管理回调的方法目前已经过时,较好的做法是实现 platfrom_driverdevice_driverdev_pm_ops 结构体成员(详细的参考电源管理章节)。

2.2.1 device_driver 结构体

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 */ const struct of_device_id *of_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; struct driver_private *p;
};

  与 platform_driver 地位对等的 i2c_driverspi_driverusb_driverpci_driver中都包含了 device_driver结构体实例成员。它其实描述了各种 xxx_driver(xxx是总线名)在驱动意义上的一些共性。

2.2.2 驱动中获取板的资源

  获取设备中 resource 资源: drivers/net/dm9000.c 中的 dm9000_probe()函数

	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);

  或者:

db->irq_wake = platform_get_irq(pdev, 1);

  实际上是调用了 platform_get_resource(dev, IORESOURCE_IRQ, num);

  获取设备中 platform_data 资源: drivers/net/dm9000.c 中的 dm9000_probe()函数

struct dm9000_plat_data *pdata = pdev->dev.platform_data;

2.2.3 platform_driver 的注册

  通过 platform_driver_register()platform_driver_unregister() 进行 platform_driver 的注册于注销。

static int __init
dm9000_init(void)
{
printk(KERN_INFO "%s Ethernet Driver, V%s\n", CARDNAME, DRV_VERSION); return platform_driver_register(&dm9000_driver);
} static void __exit
dm9000_cleanup(void)
{
platform_driver_unregister(&dm9000_driver);
} module_init(dm9000_init);
module_exit(dm9000_cleanup);

  而原本的字符设备(或其它设备)的注册和注销工作移交到 platform_driverprobe()remove() 成员函数中。以这样的形式对字符设备驱动进行注册,只是套了一层 platform_driver 的外壳,并没有改变是字符设备的本质。

  例如在 drivers/net/dm9000.c 中,还是将其定义为网络设备,只是将网络设备驱动的注册流程放在 probe() 中:

static const struct net_device_ops dm9000_netdev_ops = {
.ndo_open = dm9000_open,
.ndo_stop = dm9000_stop,
.ndo_start_xmit = dm9000_start_xmit,
.ndo_tx_timeout = dm9000_timeout,
.ndo_set_multicast_list = dm9000_hash_table,
.ndo_do_ioctl = dm9000_ioctl,
.ndo_change_mtu = eth_change_mtu,
.ndo_set_features = dm9000_set_features,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = eth_mac_addr,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = dm9000_poll_controller,
#endif
};

2.3 platform总线

  platform总线:负责管理外设与驱动之间的匹配。

  系统为 platfrom总线 定义了一个 bus_type 的实例 platform_bus_type,其定义位于 drivers/base/platform.c下:

struct bus_type platform_bus_type = {
.name = "platform",
.dev_attrs = platform_dev_attrs,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};

2.3.1 .match 成员函数

  重点关注其 match() 成员函数,此成员函数确定了 platform_deviceplatform_driver 之间是如何进行匹配的。

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_deviceplatform_driver 之间匹配有 3 种可能性:

1)基于设备树风格的匹配;

2)匹配 ID 表(即 platform_device 设备名是否出现在 platform_driver 的 ID 表内);

3)匹配 platform_device 设备名和驱动的名字。

2.3.2 platform总线的注册

start_kernel()
rest_init()
kernel_init()
do_basic_setup()
driver_init()
platform_bus_init() int __init platform_bus_init(void)
{
int error; early_platform_cleanup(); //早期的平台清理 error = device_register(&platform_bus); //注册设备 (在/sys/devices/目录下建立 platform目录对应的设备对象 /sys/devices/platform/)
if (error)
return error;
error = bus_register(&platform_bus_type);//总线注册
if (error)
device_unregister(&platform_bus);
return error;
}

2.3.3 platform总线自动匹配

platform_device_register()
platform_device_add()
device_add()
bus_probe_device()
device_attach()
bus_for_each_drv() ---------- platform_driver_register()
driver_register()
bus_add_driver()
driver_attach()
bus_for_each_dev() ---------

  无论是先注册设备还是先注册设备驱动,都会进行一次设备与设备驱动的匹配过程,匹配成功之后就会将其进行绑定,匹配的原理就是去遍历总线下设备或者设备驱动的链表。

2.4 platform 的优点

1)使得设备被挂接在一个总线上,符合 Linux 2.6 以后内核的设备模型。其结果是使配套的 sysfs 节点、设备电源管理都成为可能。

2)将 BSP 和 驱动隔离。在 BSP 中定义 platform 设备和设备使用的资源、设备的具体配置信息,而在驱动中,只需要通过通用 API 去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。

3)让一个驱动支持多个设备实例。譬如 DM9000 的驱动只有一份,但是我们可以在板级添加多份 DM9000 的 platform_device,他们都可以与唯一的驱动匹配。

4)在 Linux 3.x之后的内核中,DM9000 驱动可通过设备树的方法被枚举,添加的动作只需要简单的修改 dts 文件。(详细的后续再贴链接)

三、设备驱动的分层思想

  在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类中的函数。如果对于继承的这个事物而言,某成员函数的实现与基类一致,那它就可以直接继承基类的函数;相反,它也可以重写(Overriding),对父类的函数进行重新定义。若子类中的方法与父类中的某方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。这样可以极大的提高代码的可重用能力。

  虽然 Linux 内核完全是由 C 和 汇编写的,但却频繁用到了面向对象的设计思想。在设备驱动方面,往往为同类的设备设计一个框架,而框架中的核心层则实现了该设备通用的一些功能。同样的,如果具体的设备不想使用核心层的函数,也可以重写。

  • 例1:
return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
if (bottom_dev->funca)
return bottom_dev->funca(param1, param2);
/* 核心层通用的funca代码 */
...
}

  在 core_funca() 函数的实现中,会检查底层设备是否重载了 core_funca()。如果重载了,就调用底层的代码,否则,直接使用通用层的。这样做的好处是,核心层的代码可以处理绝大多数该类设备的 core_funca() 对应的功能,只有少数特殊设备需要重新实现 core_funca()

  • 例2:
return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
/* 通用的步骤代码A */
typea_dev_commonA();
... /* 底层操作 ops1 */
bottom_dev->funca_ops1(); /* 通用的步骤代码B */
typea_dev_commonB();
...
/* 底层操作 ops2 */
bottom_dev->funca_ops2(); /* 通用的步骤代码C */
typea_dev_commonC();
... /* 底层操作 ops3 */
bottom_dev->funca_ops3();
}

  上述代码假定为了实现funca(),对于同类设备而言,操作流程一致,都要经过“通用代码A、底层ops1、通用代码B、底层ops2、通用代码C、底层ops3”这几步,分层设计明显带来的好处是,对于通用代码A、B、C,具体的底层驱动不需要再实现(抽离出来,放到核心层实现),而仅仅只关心其底层的操作ops1、ops2、ops3。下图明确反映了设备驱动的核心层与具体设备驱动的关系,实际上,这种分层可能只有2层,也可能是多层。

Linux设备驱动中的软件架构思想

  这样的分层设计在 Linux 的 Input、RTC、MTD、I2C、SPI、tty、USB等诸多类型设备驱动中都存在。

3.1 输入设备驱动

  输入设备(如按键、键盘、触摸屏、鼠标等)是典型的字符设备,其一般的工作机理是底层在按键、触摸等动作发送时产生一个中断(或驱动通过 Timer 定时查询),然后CPU通过SPI、I2C 或外部存储器总线读取键值、坐标等数据,放入1个缓冲区,字符设备驱动管理该缓冲区,而驱动的 read() 接口让用户可以读取键值、坐标等数据。

  显然,在这些工作中,只有中断、读值是设备相关的,而输入事件的缓冲区管理以及字符设备驱动的 file_operations 接口则对输入设备是通用的。基于此,内核设计了输入子系统,由核心层处理公共的工作。

Linux设备驱动中的软件架构思想

3.1.1 输入核心提供了底层输入设备驱动程序所需的API

如分配/释放一个输入设备:

struct input_dev *input_allocate_device(void);	//返回的结构体用于表征1个输入设备。

void input_free_device(struct input_dev *dev);

注册/注销输入设备用的如下接口:

int __must_check input_register_device(struct input_dev *);

void input_unregister_device(struct input_dev *);

报告输入事件用的如下接口:

/* 报告指定type、code的输入事件 */
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value); /* 报告键值 */
void input_report_key(struct input_dev *dev, unsigned int code, int value); /* 报告相对坐标 */
void input_report_rel(struct input_dev *dev, unsigned int code, int value); /* 报告绝对坐标 */
void input_report_abs(struct input_dev *dev, unsigned int code, int value); /* 报告同步事件 */
void input_sync(struct input_dev *dev);

  而所有的输入事件,内核都用统一的数据结构来描述,这个数据结构是input_event:

struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};

3.1.2 案例:gpio按键驱动

  drivers/input/keyboard/gpio_keys.c 是基于 input 架构实现的一个通用的 GPIO 按键驱动。该驱动基于 platform_driver架构,名为 “gpio-keys”。它将硬件相关的信息(如使用的GPIO号,电平等)屏蔽在板文件 platform_device 的 platform_data 中,因此该驱动可应用于各个处理器,具有良好的跨平台性。

  该驱动的 probe() 函数:

static int __devinit gpio_keys_probe(struct platform_device *pdev)
{
......
input = input_allocate_device(); //分配一个输入设备
......
input->name = pdata->name ? : pdev->name; //初始化该 input_dev 的一些属性
input->phys = "gpio-keys/input0";
input->dev.parent = &pdev->dev;
input->open = gpio_keys_open;
input->close = gpio_keys_close; input->id.bustype = BUS_HOST;
input->id.vendor = 0x0001;
input->id.product = 0x0001;
input->id.version = 0x0100;
...... for (i = 0; i < pdata->nbuttons; i++) { //初始化所用到的 GPIO
struct gpio_keys_button *button = &pdata->buttons[i];
struct gpio_button_data *bdata = &ddata->data[i];
unsigned int type = button->type ?: EV_KEY; bdata->input = input;
bdata->button = button; error = gpio_keys_setup_key(pdev, bdata, button);
if (error)
goto fail2; if (button->wakeup)
wakeup = 1; input_set_capability(input, type, button->code);
}
......
error = input_register_device(input); //注册输入设备
......
}

  在注册输入设备后,底层输入设备驱动的核心工作只剩下在按键、触摸等人为动作发生时报告事件。在中断服务函数中,GPIO 按键驱动通过 input_event()input_sync() 这样的函数来汇报按键事件以及同步事件。

  从底层的 GPIO 按键驱动可以看出,该驱动中没有任何 file_operation 的动作,也没有各种 I/O 模型,注册进入系统也用的是 input_register_device() 这样与 input 相关的 API。

  这是由于与 Linux VFS 接口的这一部分代码全部都在 drivers/input/evdev.c 中实现了:

  input 核心层的 file_operations 和 read() 函数:

static ssize_t evdev_read(struct file *file, char __user *buffer,
size_t count, loff_t *ppos)
{
struct evdev_client *client = file->private_data;
struct evdev *evdev = client->evdev;
struct input_event event;
int retval; if (count < input_event_size())
return -EINVAL; if (!(file->f_flags & O_NONBLOCK)) { //检查是否是非阻塞访问
retval = wait_event_interruptible(evdev->wait,
client->packet_head != client->tail || !evdev->exist);
if (retval)
return retval;
} if (!evdev->exist)
return -ENODEV; while (retval + input_event_size() <= count && //处理了阻塞的睡眠情况
evdev_fetch_next_event(client, &event)) { if (input_event_to_user(buffer + retval, &event))
return -EFAULT; retval += input_event_size();
} if (retval == 0 && file->f_flags & O_NONBLOCK)
retval = -EAGAIN;
return retval;
}

3.2 RTC 设备驱动

  RTC (实时时钟)借助电池供电,在系统掉电的情况下依然可以正常计时。通常还具有产生周期性中断以及闹钟中断的能力,是一种典型的字符设备。

  作为一种字符设备驱动,RTC 需要实现 file_operations 中的接口函数,例如 open()、read()等等。而 RTC 典型的 IOCTL 包括 RTC_SET_TIMERTC_ALM_READRTC_ALM_SETRTC_IRQP_SETRTC_IRQP_READ等,这些对于 RTC 来说是通用的,那么这些通用的就放在 RTC 的核心层,而与设备相关的具体实现则放在底层。

与 RTC 核心有关的文件有:
/drivers/rtc/class.c //该文件向linux设备模型核心注册了一个类RTC,然后向驱动程序提供了注册/注销接口
/drivers/rtc/rtc-dev.c //该文件定义了基本的设备文件操作函数,如:open,read等
/drivers/rtc/interface.c //该文件主要提供用户程序与RTC驱动的接口函数,用户程序一般通过ioctl与RTC //驱动交互,这里定义了每个ioctl命令需要调用的函数
/drivers/rtc/rtc-sysfs.c //与sysfs有关
/drivers/rtc/rtc-proc.c //与proc文件系统有关
/include/linux/rtc.h //定义了与RTC有关的数据结构

RTC 驱动模型如下图:

Linux设备驱动中的软件架构思想

下面主要了解 RTC 核心 的以下几点:

1)实现 file_operations 的成员函数以及一些通用的关于 RTC 的控制代码;

2)向底层导出 rtc_device_register()rtc_device_unregister()以注册和注销 RTC;

3)导出 rtc_class_ops 结构体以描述底层的 RTC 硬件操作。

  在这样的驱动模型下,底层的 RTC 驱动不再需要关心 RTC 作为字符设备驱动的具体实现,也无需关心一些通用的 RTC 控制逻辑。关系如下:

Linux设备驱动中的软件架构思想

以S3C6410 的 RTC驱动为例:

RTC 核心:

   1) 在文件 drivers/rtc/rtc-dev.c 中:实现 file_operations 相关成员函数

static const struct file_operations rtc_dev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = rtc_dev_read,
.poll = rtc_dev_poll,
.unlocked_ioctl = rtc_dev_ioctl,
.open = rtc_dev_open,
.release = rtc_dev_release,
.fasync = rtc_dev_fasync,
};

   2)在文件 drivers/rtc/class.c中:向底层提供注册/注销接口

struct rtc_device *rtc_device_register(const char *name, struct device *dev,
const struct rtc_class_ops *ops,
struct module *owner) void rtc_device_unregister(struct rtc_device *rtc)

   3)在文件 drivers/rtc/class.h中:导出 rtc_class_ops 结构体

struct rtc_class_ops {
int (*open)(struct device *);
void (*release)(struct device *);
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*proc)(struct device *, struct seq_file *);
int (*set_mmss)(struct device *, unsigned long secs);
int (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};

S3C6410底层:在drivers/rtc/rtc-s3c.c 文件中

   其注册 RTC 以及绑定 rtc_class_ops:

static const struct rtc_class_ops s3c_rtcops = {
.read_time = s3c_rtc_gettime,
.set_time = s3c_rtc_settime,
.read_alarm = s3c_rtc_getalarm,
.set_alarm = s3c_rtc_setalarm,
.alarm_irq_enable = s3c_rtc_setaie,
};
static int __devinit s3c_rtc_probe(struct platform_device *pdev)
{
......
/* register RTC and exit */
rtc = rtc_device_register("s3c", &pdev->dev, &s3c_rtcops,
THIS_MODULE);
......
}

  drivers/rtc/rtc-dev.c 以及其调用的drivers/rtc/interface.c 等 RTC 核心层相当于把 file_operations 中的 open()、release()、读取和设置时间等,都间接 “转发” 给了底层的实例。如下摘取部分 RTC 核心层调用具体底层驱动 callback 的过程:

1)open:

/* 文件 drivers/rtc/rtc-dev.c 中: */

static int rtc_dev_open(struct inode *inode, struct file *file)
{
const struct rtc_class_ops *ops = rtc->ops;
......
err = ops->open ? ops->open(rtc->dev.parent) : 0;
......
}

2)IOCTL的 命令:

/* 文件 drivers/rtc/rtc-dev.c 中 */
static long rtc_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
......
switch (cmd) {
case RTC_ALM_READ:
......
err = rtc_read_alarm(rtc, &alarm);
......
case RTC_ALM_SET:
......
case RTC_SET_TIME:
......
return rtc_set_time(rtc, &tm);
......
}
......
} /* 文件 drivers/rtc/interface.c 中 */
static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
{
int err;
if (!rtc->ops)
err = -ENODEV;
else if (!rtc->ops->read_time) //回调
err = -EINVAL;
......
} int rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
{
......
err = __rtc_read_time(rtc, tm);
......
}

3.3 Framebuffer 设备驱动

  未深入,参考《Linux设备驱动开发详解:基于最新的Linux 4.0内核》

3.4 终端设备驱动

  在 Linux 系统中,终端是一种字符型设备,它有多种类型,通常使用 tty (Teletype)来简称各种类型的终端设备。在嵌入式系统中,最常用的是 UART 串行端口。

3.4.1 内核中 tty 的层次结构

Linux设备驱动中的软件架构思想

  图中包含三个层次:

1)tty_io.c:tty 核心;

2)n_tty.c:tty 线路规程;

3)xxx_tty.c:tty 驱动实例。

3.4.1.1 tty_io.c

  tty_io.c 本身是一个标准的字符设备驱动,因此,它对上有字符设备的职责,需实现 file_operations 结构体成员函数。

  但 tty 核心层对下又定义了 tty_driver 的架构,因此 tty 设备驱动的主体工作就变成了填充 tty_driver 结构体中的成员,实现其成员 tty_operations结构体的成员函数,而不再是去实现file_operations 结构体成员函数这一级的工作。

struct tty_driver {
......
/*
* Driver methods
*/ const struct tty_operations *ops;
struct list_head tty_drivers;
}; struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
......
const struct file_operations *proc_fops;
};
3.4.1.2 n_tty.c

  n_tty.c:tty 线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式。

3.4.2 tty 设备的发送/接收流程

Linux设备驱动中的软件架构思想

  发送流程: tty 核心从一个用户获取将要发送给一个 tty 设备的数据,tty 核心将数据传递给 tty 线路规程驱动,接着数据被传递到 tty 驱动,tty 驱动将数据转换为可以发送给硬件的格式。

  从 tty_driver 操作集 tty_operations 的成员函数 write() 函数接收3个参数: tty_struct、发送数据指针和发送的字节数。该函数是被 file_operations 的 write() 成员函数间接触发调用的。

  接收流程:从 tty 硬件接收到的数据向上交给 tty 驱动,接着进入 tty 线路规程驱动,再进入 tty 核心,在这里它被一个用户获取。

  tty 驱动一般收到字符后会通过 tty_flip_buffer_push() 将接收缓冲区推到线路规程。

3.4.3 串口核心层

  尽管一个特定的底层 UART 设备驱动完全可以遵循上述 tty_driver 的方法来设计,即定义tty_driver 并实现 tty_operations 中的成员函数,但是鉴于串口之间的共性,Linux 考虑在文件 drivers/tty/serial/serial_core.c 中实现 UART 设备的通用 tty 驱动层(称为串口核心层)。这样,UART 驱动的主要任务就进一步演变成了实现 文件 serial_core.c中定义的一组 uart_xxx 接口,而不是 tty_xxx 接口。

  按照面向对象的思想,可认为 tty_driver 是字符设备的泛化、serial_core 是 tty_driver 的泛化,而具体的串口驱动又是 serial_core 的泛化。

Linux设备驱动中的软件架构思想

  在串口核心层又定义新的 uart_driver 结构体和其操作集 uart_ops。一个底层的 UART 驱动需要创建和通过 uart_register_driver() 注册一个 uart_driver 而不是 tty_driver

struct uart_driver {
struct module *owner;
const char *driver_name;
const char *dev_name;
int major;
int minor;
int nr;
struct console *cons; /*
* these are private; the low level driver should not
* touch these; they should be initialised to NULL
*/
struct uart_state *state;
struct tty_driver *tty_driver;
}; int uart_register_driver(struct uart_driver *drv);
void uart_unregister_driver(struct uart_driver *drv);

  uart_driver 结构体在本质上是派生自 tty_driver 结构体,因此,uart_driver 结构体中包含 tty_dirver 结构体成员。

  tty_operations 在UART 这个层面上也被进一步泛化为 uart_ops

struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*send_xchar)(struct uart_port *, char ch);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int ctl);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*flush_buffer)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *new,
struct ktermios *old);
void (*set_ldisc)(struct uart_port *, int new);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);
int (*set_wake)(struct uart_port *, unsigned int state);
void (*wake_peer)(struct uart_port *); /*
* Return a string describing the type of the port
*/
const char *(*type)(struct uart_port *); /*
* Release IO and memory resources used by the port.
* This includes iounmap if necessary.
*/
void (*release_port)(struct uart_port *); /*
* Request IO and memory resources used by the port.
* This includes iomapping the port if necessary.
*/
int (*request_port)(struct uart_port *);
void (*config_port)(struct uart_port *, int);
int (*verify_port)(struct uart_port *, struct serial_struct *);
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
#endif
};

  由于 driver/tty/serial/serial_core.c 是一个 tty_driver ,因此在 serial_core.c 中,存在一个 tty_operations 的实例,这个实例的成员函数会进一步调用 struct uart_ops 的成员函数,这样就把 file_operaions 里的成员函数、tty_operations 的成员函数和 uart_ops 的成员函数串起来。

3.5 misc 设备驱动

3.6 驱动核心层

  核心层的 3 大职责:

  1)对上提供接口。file_operations 的读、写、ioctl 都被中间层搞定,各种 I/O 模型也被处理掉了。

  2)中间层实现通用逻辑。可以被底层各种实例共享的代码都被中间层搞定,避免底层重复实现。

  3)对下定义框架。底层的驱动不再需要关心 Linux 内核 VFS 的接口和各种可能的 I/O 模型,而只需处理与具体硬件相关的访问。

  这种分层有时候还不是两层,可以有更多层,在软件上呈现为面向对象里类继承和多态的状态。

Linux设备驱动中的软件架构思想

四、主机驱动与外设驱动分离的设计思想

4.1 主机驱动与外设驱动分离

  Linux 中的 SPI、I2C、USB 等子系统都是典型的利用主机驱动和外设驱动分离的思想。

  让主机端只负责产生总线上的传输波形,而外设端只是通过标准的 API 来让主机端以适当的波形访问自身。涉及 4 个软件模块:

  1)主机端的驱动。根据具体的 SPI、I2C、USB 等控制器的硬件手册,操作具体的控制器,产生总线的各种波形。

  2)连接主机和外设的纽带。外设不直接调用主机端的驱动来产生波形,而是调用一个标准的 API。由这个标准的 API 把这个波形的传输请求间接 “转发” 给具体的主机端驱动。最好在这里把关于波形的描述也以某种数据结构标准化。

  3)外设端的驱动。外设接在 SPI、I2C、USB 这样的总线上,但是它们本身可以是触摸屏、网卡、声卡或任意一种类型的设备。当这些外设要求 SPI 、I2C、USB等去访问它的时候,它调用 “连接主机和外设的纽带” 模块的标准 API。

  4)板级逻辑。用来描述主机和外设是如何互联的,它相当于一个 “路由表”。假设板子上有多个 SPI 控制器和多个 SPI 外设,那究竟谁接在谁上面?管理互联关系,既不是主机端的责任,也不是外设端的责任,这属于板级逻辑的责任。

  linux 通过上述设计方法,划分为 4 个轻量级的小模块,各个模块各司其职。

4.2 Linux SPI 主机和设备驱动

4.2.1 SPI 主机驱动

  在 Linux 中,通过 spi_master 结构体来描述一个 SPI 主动控制器驱动其主要成员由主机控制器的序号、片选数量、SPI 模式、时钟设置相关函数 和 数据传输相关函数。

  文件spi/spi.h

struct spi_master {
struct device dev; struct list_head list; /* other than negative (== assign one dynamically), bus_num is fully
* board-specific. usually that simplifies to being SOC-specific.
* example: one SOC has three SPI controllers, numbered 0..2,
* and one board's schematics might show it using SPI-2. software
* would normally use bus_num=2 for that controller.
*/
s16 bus_num; /* chipselects will be integral to many controllers; some others
* might use board-specific GPIOs.
*/
u16 num_chipselect; /* some SPI controllers pose alignment requirements on DMAable
* buffers; let protocol drivers know about these requirements.
*/
u16 dma_alignment; /* spi_device.mode flags understood by this controller driver */
u16 mode_bits; /* other constraints relevant to this driver */
u16 flags;
#define SPI_MASTER_HALF_DUPLEX BIT(0) /* can't do full duplex */
#define SPI_MASTER_NO_RX BIT(1) /* can't do buffer read */
#define SPI_MASTER_NO_TX BIT(2) /* can't do buffer write */ /* lock and mutex for SPI bus locking */
spinlock_t bus_lock_spinlock;
struct mutex bus_lock_mutex; /* flag indicating that the SPI bus is locked for exclusive use */
bool bus_lock_flag; /* Setup mode and clock, etc (spi driver may call many times).
*
* IMPORTANT: this may be called when transfers to another
* device are active. DO NOT UPDATE SHARED REGISTERS in ways
* which could break those transfers.
*/
int (*setup)(struct spi_device *spi); /* bidirectional bulk transfers
*
* + The transfer() method may not sleep; its main role is
* just to add the message to the queue.
* + For now there's no remove-from-queue operation, or
* any other request management
* + To a given spi_device, message queueing is pure fifo
*
* + The master's main job is to process its message queue,
* selecting a chip then transferring data
* + If there are multiple spi_device children, the i/o queue
* arbitration algorithm is unspecified (round robin, fifo,
* priority, reservations, preemption, etc)
*
* + Chipselect stays active during the entire message
* (unless modified by spi_transfer.cs_change != 0).
* + The message transfers use clock and SPI mode parameters
* previously established by setup() for this device
*/
int (*transfer)(struct spi_device *spi,
struct spi_message *mesg); /* called on release() to free memory provided by spi_master */
void (*cleanup)(struct spi_device *spi);
};

  分配、注册和注销 SPI 主机的 API 由 SPI 核心提供:文件 drivers/spi/spi.c

struct spi_master *spi_alloc_master(struct device *dev, unsigned size);
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);

  SPI 主机控制器驱动主体是实现了 spi_master 的 transfer()、setup() 这样的成员函数。也可能实现 spi_bitbang 的 txrx_buf()、setup_transfer()、chipselect() 这样的成员函数。

  例如在文件 driver/spi/spi_s3c24xx.c 中:

static int __init s3c24xx_spi_probe(struct platform_device *pdev)
{
struct s3c2410_spi_info *pdata;
struct s3c24xx_spi *hw;
struct spi_master *master;
struct resource *res;
......
/* initialise fiq handler */ s3c24xx_spi_initfiq(hw); /* setup the master state. */ /* the spi->mode bits understood by this driver: */
master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH; //设置模式 master->num_chipselect = hw->pdata->num_cs; //设置片选序号
master->bus_num = pdata->bus_num; //主机控制器的序号 /* setup the state for the bitbang driver */ hw->bitbang.master = hw->master;
hw->bitbang.setup_transfer = s3c24xx_spi_setupxfer;
hw->bitbang.chipselect = s3c24xx_spi_chipsel;
hw->bitbang.txrx_bufs = s3c24xx_spi_txrx; hw->master->setup = s3c24xx_spi_setup;
hw->master->cleanup = s3c24xx_spi_cleanup;
......
}

4.2.2 纽带

4.2.3 SPI 外设驱动

  在 Linux 中,通过 spi_driver 结构体来描述一个 SPI 外设驱动,这个外设驱动可以认为是 spi_mater 的客户端驱动。SPI 只是一种总线,spi_driver 的作用只是将 SPI 外设挂接在该总线上,因此在 spi_driver 的 probe() 成员函数中,将注册 SPI 外设本身所属设备驱动的类型。

  文件 spi/spi.h 中:

struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
int (*suspend)(struct spi_device *spi, pm_message_t mesg);
int (*resume)(struct spi_device *spi);
struct device_driver driver;
}; static int spi_drv_probe(struct device *dev)
{
const struct spi_driver *sdrv = to_spi_driver(dev->driver); return sdrv->probe(to_spi_device(dev));
} int spi_register_driver(struct spi_driver *sdrv)
{
sdrv->driver.bus = &spi_bus_type;
if (sdrv->probe)
sdrv->driver.probe = spi_drv_probe;
if (sdrv->remove)
sdrv->driver.remove = spi_drv_remove;
if (sdrv->shutdown)
sdrv->driver.shutdown = spi_drv_shutdown;
return driver_register(&sdrv->driver);
}

  可看出,spi_driver 结构体 和 platform_driver 结构体有极大的相似性,都有 prob()、remove()、suspend()、resume()这样的接口和 device_driver 的实例。(这几乎是一切客户端驱动的常用模板)

在SPI 外设驱动中(文件 spi/spi.hdriver/spi/spi.c ):

  1)spi_tansfer 结构体:通过 SPI 总线进行数据传输的接口。

  2)spi_message 结构体:组织一个或多个spi_transfer,从而完成一次完整的 SPI 传输流程。

  3)初始化 spi_message

static inline void spi_message_init(struct spi_message *m)

  4)将 spi_transfer 添加到 spi_message 队列:

spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

  5)spi_message 的同步传输 API,阻塞等待这个消息被处理完:

spi_sync(struct spi_device *spi, struct spi_message *message);

  6)spi_message 的异步传输 API,不会阻塞等待这个消息被处理完,但可在 spi_messagecomplete 字段挂接一个回调函数,当消息被处理完成后,该函数会被调用:

spi_async(struct spi_device *spi, struct spi_message *message);

  7)初始化 spi_transferspi_message 并进行 SPI 数据传输的例子,同时 spi_write()spi_read() 也是SPI 核心层的两个通用API,在外设驱动中可直接调用进行简单的纯写、纯读操作:

static inline int
spi_write(struct spi_device *spi, const void *buf, size_t len)
{
struct spi_transfer t = {
.tx_buf = buf,
.len = len,
};
struct spi_message m; spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
} static inline int
spi_read(struct spi_device *spi, void *buf, size_t len)
{
struct spi_transfer t = {
.rx_buf = buf,
.len = len,
};
struct spi_message m; spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}

4.2.4 SPI 板级逻辑

  通 platform_driver 对应着一个platform_device一样,spi_driver 也对应着一个 spi_device;platform_device 需要在 BSP 的板文件中添加板信息数据,同样的 spi_device 也需要。

  spi_device 的板信息用 spi_board_info 结构体描述,该结构体记录着 SPI 外设使用的主机控制器序号、片选序号、数据比特率、SPI 传输模式等。

  两种方式添加板级信息:

  1)与 platfrom_add_devices 添加 platform_device 类似,通过 spi_register_board_info() 在 Linux 启动过程中的 机器 init_machine() 函数中进行注册:

  在文件 arch/arm/mach-exynos/mach-itop4412.c 中:

static struct spi_board_info spi_board_info[] __initdata = {
{
.modalias = "lms501kf03",
.platform_data = NULL,
.max_speed_hz = 1200000,
.bus_num = LCD_BUS_NUM,
.chip_select = 0,
.mode = SPI_MODE_3,
.controller_data = (void *)DISPLAY_CS,
}
}; spi_register_board_info(spi_board_info, ARRAY_SIZE(spi_board_info));

  2)在 ARM Linux 3.x 之后的内核在改为设备树后,不再需要正在 arch/arm/mach-xxx 中编码 SPI 的板级信息了,而倾向于在 SPI 控制器节点下填写子节点。

参考

  1. 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》第12章 - 宋宝华
  2. linux RTC 驱动模型分析
  3. 迅为iTop4412资料