linux驱动的软件架构
- 我们知道linux驱动软件是为了挂接在cpu总线上的设备而出现的,这些设备有例如速度传感器、键盘输入、lcd显示等。我们的cpu通过总线访问这些设备,例如读、写 、控制等操作,访问的这些动作实现就是我们写的驱动。
- 从上面我们可以看出,不管访问哪种设备,我们几乎都要有读、写、控制等这些通用操作。所以为了追求代码上的复用性,我们可以把上面那些通用的逻辑操作封装成一个类似c++语法中类的对象,当调用具体的设备时,我们再把这些设备的具体信息融合到这个通用的封装好的对象中。
- 上面那个通用的封装对象我们内核中用input输入设备核心层这一层次的代码来实现。比如我们要写按键设备的驱动时,我们使用input核心层来写我们的驱动代码,这时我们只用实现接收按键值、按键中断处理函数等,前面那些读、写、控制啊等操作都由input核心层搞定了,这既简化了开发者编写驱动代码步骤,也提高了内核中驱动代码的复用性。
下面是内核中输入设备的驱动分层示意图: - 随着内核的发展,内核中的驱动代码变得越来越具有可以移植性。 我们现在通常将设备驱动分为设备和驱动2个方面,为什么这样分呢,因为同一类的设备访问具有通用性,这种通用性操作就是驱动的表现,我们把这驱动分离出来,以后要是再加入一个类似的设备,那么我们只用修改这个设备的设备信息部分,驱动实现部分就不用再改了。
下面是设备与驱动的分离示意图: - 把驱动与设备分离开来,那么我们该怎样把我们想要的设备与指定的驱动融合起来呢。我们内核中采用了platform总线这种层次的代码将二者融合起来使用。platform总线通过匹配注册到platform总线的设备和注册到platform总线的驱动,当二者匹配成功后,驱动就开始把设备信息通过指定的读取路径读入到驱动中。这样的话这个platform总线就相当于月老的工作了,匹配设备与驱动的,不过驱动是少量的,设备是大量的。
platform设备驱动
- platform总线
这个总线是虚拟的,用于将总线上的设备与最小的基础设施连接起来。 - platform设备
platform设备是系统中的一部分设备实例。它的结构体如下:
// include/linux/platform_device.h
struct platform_device {
const char * name; // 这个名字用来绑定驱动的
int id; // 设备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;
};
- platform驱动
platform驱动也是驱动,遵循着标准驱动模型架构,不过多了像probe、remove这种接口。它的结构体如下:
// include/linux/platform_device.h
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
// 下面3个接口是为了实现电源管理方面的功能接口,为低功耗而实现的
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;
};
将globalfifo作为platform设备
现在我们要将前面写的globalfifo驱动挂接到platform总线上。
- 将globalfifo驱动移植为platform驱动
diff --git a/kernel/drivers/char/globalfifo/globalfifo.c b/kernel/drivers/char/globalfifo/globalfifo.c
index 85e007c..99a9578 100644
--- a/kernel/drivers/char/globalfifo/globalfifo.c
+++ b/kernel/drivers/char/globalfifo/globalfifo.c
@@ -21,6 +21,7 @@
#include "linux/sched.h"
#include "linux/types.h"
#include "linux/poll.h"
+#include "linux/platform_device.h"
#define GLOBALFIFO_SIZE (0X1000)
#define GLOBALFIFO_MAJOR (231)
@@ -482,8 +483,97 @@ static void __exit globalfifo_exit(void)
printk("globalfifo_exit success\n");
}
-module_init(globalfifo_init)
-module_exit(globalfifo_exit)
+static int glboalfifo_probe(struct platform_device *pdev)
+{
+ int i = 0;
+ int ret = 0;
+ // 1. get the device id
+ dev_t devno = MKDEV(globalfifo_major, 0);
+#ifdef globalfifo_debug
+ printk(KERN_NOTICE "globalfifo_init\n");
+#endif
+ // 2. register the DEVICE_NUM of the char device
+ if (globalfifo_major)
+ {
+ ret = register_chrdev_region(devno, DEVICE_NUM, "globalfifo");
+ }
+ else
+ {
+ // automatic register char device
+ ret = alloc_chrdev_region(&devno, 0, DEVICE_NUM, "globalfifo");
+ }
+ // check the error code
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ // 3. construct the globalfifo devices structure in the heap
+ globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev) * DEVICE_NUM, GFP_KERNEL);
+ if (!globalfifo_devp)
+ {
+ ret = -ENOMEM;
+#ifdef globalfifo_debug
+ printk(KERN_NOTICE "globalfifo_init = %d\n", __LINE__);
+#endif
+ goto fail_malloc;
+ }
+
+ // initialize the mutex
+ mutex_init(&globalfifo_devp->mutex);
+
+ // initialize the write and read wait queue head
+ init_waitqueue_head(&globalfifo_devp->r_wait);
+ init_waitqueue_head(&globalfifo_devp->w_wait);
+
+ // 4. add the globalfifo decices structure pointer to the kobjct map
+ for (i = 0; i < DEVICE_NUM; i++)
+ {
+ globalfifo_init_dev(globalfifo_devp + i, i);
+ }
+ printk("globalfifo_init success\n");
+ return 0;
+
+ fail_malloc:
+ unregister_chrdev_region(devno, DEVICE_NUM);
+ return ret;
+}
+
+static int glboalfifo_remove(struct platform_device *pdev)
+{
+ int i = 0;
+#ifdef globalfifo_debug
+ printk(KERN_NOTICE "globalfifo_exit\n");
+#endif
+ // 1. remove the globalfifo structure from teh kobject map
+ for (i = 0; i < DEVICE_NUM; i++)
+ {
+ cdev_del(&(globalfifo_devp + i)->chrdev);
+ }
+
+ // 2. free the glboalmem structure in the heap
+ kfree(globalfifo_devp);
+
+ // 3. unregister the device id
+ unregister_chrdev_region(MKDEV(globalfifo_major, 0), DEVICE_NUM);
+
+ // 4. remove the device id
+ printk("globalfifo_exit success\n");
+}
+
+static struct platform_driver globalfifo_driver = {
+ .driver = {
+ .name = "glboalfifo", // used in devices binding
+ .owner = THIS_MODULE,
+ },
+ .probe = glboalfifo_probe,
+ .remove = glboalfifo_remove,
+};
+
+//module_init(globalfifo_init)
+//module_exit(globalfifo_exit)
+module_platform_driver(globalfifo_driver)
+
// the declaration of the author
MODULE_AUTHOR("ZhongHuan Duan <15818411038@163.com>");
// the declaration of the licence
- 在板文件中添加globalfifo这个platform设备
// arch/arm/mach-<soc名>/mach-<板名>.c
static struct platform_device globalfifo_device = {
.name = "globalfifo",
.id = -1,
};
static void __init globalfifo_machine_init(void)
{
...
platform_add_devices(globalfifo_device, ARRAY_SIZE(globalfifo_device));
...
}
- 上面会把定义的设备信息注册到系统中。如果一切顺利,我们会在/sys/devices/platform目录下看到一个名字叫globalfifo的子目录,/sys/devices/platform/globalfifo中会有一个driver文件,它是指向/sys/bus/platform/drivers/globalfifo的符号链接,这证明驱动和设备匹配上了。
platform设备资源和数据
资源结构体定义:
// include/linux/ioport.h
struct resource {
resource_size_t start; // 资源的开始值
resource_size_t end; // 资源的结束值
const char *name; // 资源名称
unsigned long flags; // 资源的标志,可以表示io、内存、中断等种类
struct resource *parent, *sibling, *child; // 资源的基类、同类、子类
};
我们通常关心start、end和flags这3个字段,它们分别标明了资源的开始值、结束值和类型,flags可以为IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORE-SOURCE_DMA等。start、end的含义会随着flags而变更,如当flags为IORESOURCE_MEM时,start、end分别表示该platform_device占据的内存的开始地址和结束地址;当flags为IORESOURCE_IRQ时,start、end分别表示该platform_device使用的中断号的开始值和结束值,如果只使用了1个中断号,开始和结束值相同。对于同种类型的资源而言,可以有多份,例如说某设备占据了两个内存区域,则可以定义两个IORESOURCE_MEM资源。
资源的定义通常在板文件中,可以看一下网卡dm9000的资源定义:
#if defined(CONFIG_DM9000)
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] = {
.flags = IORESOURCE_IRQ
| IORESOURCE_IRQ_LOWEDGE | IORESOURCE_IRQ_HIGHEDGE,
}
};
对于上面的网卡资源获取可以使用platform_get_resource获得,使用以下方法可以获得上面的3份资源:
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);
设备驱动的分层思想
输入设备驱动
- linux内核输入子系统框架
- 分配、释放一个输入设备
struct input_dev *input_allocate_device(void);
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);
- gpio通用驱动的探测函数
static int __devinit gpio_keys_probe(struct platform_device *pdev)
{
const struct gpio_keys_platform_data *pdata = pdev->dev.platform_data;
struct gpio_keys_drvdata *ddata;
struct device *dev = &pdev->dev;
struct gpio_keys_platform_data alt_pdata;
struct input_dev *input;
int i, error;
int wakeup = 0;
if (!pdata) {
error = gpio_keys_get_devtree_pdata(dev, &alt_pdata);
if (error)
return error;
pdata = &alt_pdata;
}
ddata = kzalloc(sizeof(struct gpio_keys_drvdata) +
pdata->nbuttons * sizeof(struct gpio_button_data),
GFP_KERNEL);
input = input_allocate_device();
if (!ddata || !input) {
dev_err(dev, "failed to allocate state\n");
error = -ENOMEM;
goto fail1;
}
ddata->input = input;
ddata->n_buttons = pdata->nbuttons;
ddata->enable = pdata->enable;
ddata->disable = pdata->disable;
mutex_init(&ddata->disable_lock);
platform_set_drvdata(pdev, ddata);
input_set_drvdata(input, ddata);
input->name = pdata->name ? : pdev->name;
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;
/* Enable auto repeat feature of Linux input subsystem */
if (pdata->rep)
__set_bit(EV_REP, input->evbit);
for (i = 0; i < pdata->nbuttons; i++) {
const struct gpio_keys_button *button = &pdata->buttons[i];
struct gpio_button_data *bdata = &ddata->data[i];
error = gpio_keys_setup_key(pdev, input, bdata, button);
if (error)
goto fail2;
if (button->wakeup)
wakeup = 1;
}
error = sysfs_create_group(&pdev->dev.kobj, &gpio_keys_attr_group);
if (error) {
dev_err(dev, "Unable to export keys/switches, error: %d\n",
error);
goto fail2;
}
error = input_register_device(input);
if (error) {
dev_err(dev, "Unable to register input device, error: %d\n",
error);
goto fail3;
}
/* get current state of buttons that are connected to GPIOs */
for (i = 0; i < pdata->nbuttons; i++) {
struct gpio_button_data *bdata = &ddata->data[i];
if (gpio_is_valid(bdata->button->gpio))
gpio_keys_gpio_report_event(bdata);
}
input_sync(input);
device_init_wakeup(&pdev->dev, wakeup);
return 0;
fail3:
sysfs_remove_group(&pdev->dev.kobj, &gpio_keys_attr_group);
fail2:
while (--i >= 0)
gpio_remove_key(&ddata->data[i]);
platform_set_drvdata(pdev, NULL);
fail1:
input_free_device(input);
kfree(ddata);
/* If we have no platform_data, we allocated buttons dynamically. */
if (!pdev->dev.platform_data)
kfree(pdata->buttons);
return error;
}
- 主要注意使用input核心层的流程。
- 注意学习内核中已有的代码例程。
RTC设备驱动
- RTC(实时钟)借助电池供电,在系统掉电的情况下依然可以正常计时。它通常还具有产生周期性中断以及闹钟(Alarm)中断的能力,是一种典型的字符设备。
- rivers/rtc/rtc-dev.c实现了RTC驱动通用的字符设备驱动层,它实现了file_opearations的成员函数以及一些通用的关于RTC的控制代码。
- s3c系列rtc驱动代码:
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,
.proc = s3c_rtc_proc,
.alarm_irq_enable = s3c_rtc_setaie,
};
- drivers/rtc/rtc-dev.c以及其调用的drivers/rtc/interface.c等RTC核心层相当于把file_operations中的open()、release()、读取和设置时间等都间接“转发”给了底层的实例。
- 有空可以研究下RTC核心层调用具体底层驱动callback的过程。
FrameBuffer设备驱动
- Framebuffer(帧缓冲)是Linux系统为显示设备提供的一个接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。对于帧缓冲设备而言,只要在显示缓冲区中与显示点对应的区域内写入颜色值,对应的颜色会自动在屏幕上显示。
- framebuffer程序结构示意图:
- 上图所示为Linux帧缓冲设备驱动的主要结构,帧缓冲设备提供给用户空间的file_operations结构体由drivers/video/fbdev/core/fbmem.c中的file_operations提供,而特定帧缓冲设备fb_info结构体的注册、注销以及其中成员的维护,尤其是fb_ops中成员函数的实现则由对应的xxxfb.c文件实现,fb_ops中的成员函数最终会操作LCD控制其硬件寄存器。
终端设备驱动
- 在Linux系统中,终端是一种字符型设备,它有多种类型,通常使用tty(Teletype)来简称各种类型的终端设备。对于嵌入式系统而言,最普遍采用的是UART(Universal Asynchronous Receiver/Transmitter)串行端口,日常生活中简称串口。
- tty的层次结构图:
- ty_io.c本身是一个标准的字符设备驱动,它对上有字符设备的职责,实现file_operations成员函数。但是tty核心层对下又定义了tty_driver的架构,这样tty设备驱动的主体工作就变成了填充tty_driver结构体中的成员,实现其中的tty_operations的成员函数,而不再是去实现file_operations这一级的工作。
misc设备驱动
- 有部分类似globalmem、globalfifo的字符设备,确实不知道它属于什么类型,我们一般推荐大家采用miscdevice框架结构。miscdevice本质上也是字符设备,只是在miscdevice核心层的misc_init()函数中,通过register_chrdev(MISC_MAJOR,“misc”,
&misc_fops)注册了字符设备,而具体miscdevice实例调用misc_register()的时候又自动完成了device_create()、获取动态次设备号的动作。 - miscdevice的主设备号是固定的,MISC_MAJOR定义为10。
- miscdevice结构体:
struct miscdevice {
// 次设备号,MISC_DYNAMIC_MINOR,miscdevice核心层会自动找一个空闲的次设备号,
// 否则用minor指定的次设备号
int minor;
const char *name; // 设备名称
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const char *nodename;
umode_t mode;
};
- miscdevice驱动的注册和注销
int misc_register(struct miscdevice * misc);
int misc_deregister(struct miscdevice *misc);
- miscdevice驱动一般结构(摘抄自宋宝华老师的设备驱动开发书籍)
static const struct file_operations xxx_fops = {
.unlocked_ioctl = xxx_ioctl,
.mmap
= xxx_mmap,
...
};
static struct miscdevice xxx_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name
= "xxx",
.fops
= &xxx_fops
};
static int __init xxx_init(void){
pr_info("ARC Hostlink driver mmap at 0x%p\n", __HOSTLINK__);
return misc_register(&xxx_dev);
}
- 在调用misc_register(&xxx_dev)时,该函数内部会自动调用device_create(),而device_create()会以xxx_dev作为drvdata参数。其次,在miscdevice核心层misc_open()函数的帮助下,在file_operations的成员函数中,xxx_dev会自动成为file的private_data(misc_open会完成file->private_data的赋值操作).
主机驱动与外设驱动分离
Linux中的SPI、I 2 C、USB等子系统都利用了典型的把主机驱动和外设驱动分离的想法,让主机端只负责产生总线上的传输波形,而外设端只是通过标准的API来让主机端以适当的波形访问自身。(摘抄自宋宝华老师的设备驱动书籍)
- 主机端的驱动。根据具体的I 2 C、SPI、USB等控制器的硬件手册,操作具体的I 2 C、SPI、USB等控制器,产生总线的各种波形。
- 连接主机和外设的纽带。外设不直接调用主机端的驱动来产生波形,而是调一个标准的API。由这个标准的API把这个波形的传输请求间接“转发”给了具体的主机端驱动。当然,在这里,最好把关于波形的描述也以某种数据结构标准化。
- 外设端的驱动。外设接在I 2 C、SPI、USB这样的总线上,但是它们本身可以是触摸屏、网卡、声卡或者任意一种类型的设备。我们在相关的i2c_driver、spi_driver、usb_driver这种xxx_driver的probe()函数中去注册它具体的类型。当这些外设要求I 2 C、SPI、USB等去访问它的时候,它调用“连接主机和外设的纽带”模块的标准API。
- 板级逻辑。板级逻辑用来描述主机和外设是如何互联的,它相当于一个“路由表”。假设板子上有多个SPI控制器和多个SPI外设,那究竟谁接在谁上面管理互联关系,既不是主机端的责任,也不是外设端的责任,这属于板级逻辑的责任。这部分通常出现在arch/arm/mach-xxx下面或者arch/arm/boot/dts下面.
总结
- 有空分析下内核中spi驱动代码,感受编码分层和复用的魅力。
- 有空分析下linux下的dma编程,熟悉应用技能。