简介

在今年五月份我在4412上学习了platform总线设备的驱动编写,了解了引入platform总线的目的以及带来的方便之处,初步了解了Linux内核总线、设备、驱动的总体框架,最近我开始了基于设备树的驱动开发的学习,所以总结记录一下在如何使用设备树来编写platform设备驱动,再温习一下关于platform的知识。

关于platform

Linux系统要考虑驱动的可重用性,驱动的分离和分层
所以引入了platform设备驱动,平台设备驱动

驱动框架

在这里插入图片描述
驱动的分隔,将主机驱动和设备驱动分隔开,I2C、SPI等都会采用驱动分隔方式
实际驱动开发中,I2C主机驱动由半导体厂家编写好,设备驱动也有设备器件厂家编写好,我们只需要提供设备信息即可,比如连接到了哪个I2C接口、I2C的速率是多少
将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如设备树),然后根据获取到的信息来初始化设备

bus、driver、device

总线、驱动、设备是十分关键的三个概念
在这里插入图片描述

  • 当向系统注册一个驱动时总线会在右边的设备中寻找,如果有与之匹配的设备就将两者联系起来
  • 注册一个设备时总线会左侧的驱动中寻找并联系,如果有与之匹配的驱动就将两者联系起来

所以说总线的作用就是将驱动和设备匹配起来,作为两者中间的一个媒介

驱动的分层

在编写Linux驱动时,需要编写file_operations成员函数,还要考虑阻塞、非阻塞、多路复用、SIGIO等复杂的问题。但是当我们面对一个真实的硬件驱动时,总是懒惰的,想做尽可能少的事情,比如按键的驱动,我们只想收获一个按键中断、汇报一个键值,所以Linux内核通过驱动分层的方式来帮助我们“偷懒”,尽管file_operations、IO模型是不可或缺的,但是这部分的代码所有的按键都是一样的、甚至可以说所有的输入设备都是一样,所以Linux内核提供了input子系统来帮助我们解决输入相关的事项,相当于实现了一个中间件,把那些重复的事情搞定,而我们只要配置底层的与IO相关的,和调用它的上报函数。
同样的Linux内核不仅仅只有input子系统,与LCD显示相关的部分抽象出了Framebuffer子系统专门来管理所有与显示有关的硬件和软件;与RTC相关的部分抽象出了RTC子系统等等

platform驱动模型

platform总线

首先我们来看一下bus_type 结构体,这个结构体用于描述总线

struct bus_type {
	const char		*name;
	const char		*dev_name;
	struct device		*dev_root;
	const struct attribute_group **bus_groups;
	const struct attribute_group **dev_groups;
	const struct attribute_group **drv_groups;

	int (*match)(struct device *dev, struct device_driver *drv);
	int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
	int (*probe)(struct device *dev);
	int (*remove)(struct device *dev);
	void (*shutdown)(struct device *dev);

	int (*online)(struct device *dev);
	int (*offline)(struct device *dev);

	int (*suspend)(struct device *dev, pm_message_t state);
	int (*resume)(struct device *dev);

	int (*num_vf)(struct device *dev);

	const struct dev_pm_ops *pm;

	const struct iommu_ops *iommu_ops;

	struct subsys_private *p;
	struct lock_class_key lock_key;
};

match函数就是完成设备和驱动之间的匹配的
就是完成设备和驱动之间匹配的,总线就是使用 match函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。 match函数有两个参数: dev和 drv,这两个参数分别为 device和 device_driver类型,也就是设备和驱动。
platform总线是bus_type的一个具体实例,

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

既然总线的作用是匹配驱动和设备,所以我们要重点关注platform_match这个函数

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

	/* When driver_override is set, only bind to the matching driver */
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override, drv->name);

	/* Attempt an OF style match first */
	if (of_driver_match_device(dev, drv))//第一种匹配方式,OF类型匹配,设备树采用的匹配方式
		return 1;

	/* Then try ACPI style match */
	if (acpi_driver_match_device(dev, drv))//第二种匹配方式,ACPI匹配
		return 1;

	/* Then try to match against the id table */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;//第三种匹配方式,id_table匹配

	/* fall-back to driver name match */
	return (strcmp(pdev->name, drv->name) == 0);//第四种匹配方式,直接比较驱动和设备的name字段
}

可以看到,platform_match可以使用四种匹配方式

  • OF类型匹配,设备树采用的匹配方式
  • ACPI匹配
  • id_table匹配
  • 直接比较驱动和设备的name字段

platform驱动

首先我们来看一下最基本的platform_driver结构体

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;
	bool prevent_deferred_probe;
};

这个我们是很熟悉的
probe函数,驱动和设备匹配成功以后会执行
driver成员,面向对象,device_driver为基类,platform_driver集成了这个基类并添加了一些特有的成员变量
id_table表,数组,每个元素为platform_device_id

struct platform_device_id { 
	char name[PLATFORM_NAME_SIZE]; 
	kernel_ulong_t driver_data; 
};

我们可以看到在platform_driver 中包含了一个driver对象,它是device_driver 类型的,用于描述设备驱动,所以可以看做是platform_driver 的基类(Linux内核中有很多用到面向对象的思想),我们再来看一下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 */
	enum probe_type probe_type;

	const struct of_device_id	*of_match_table;
	const struct acpi_device_id	*acpi_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;
};

of_device_id类型的of_match_table就是采用设备树的时候驱动使用的匹配表

struct of_device_id {
	char	name[32];
	char	type[32];
	char	compatible[128];
	const void *data;
};

compatible非常重要,对于设备树而言,就是通过设备节点的compatiable属性值和of_match_table中每个项目的compatible成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功
可以看出在device_driver 结构体中也有probe、remove等函数,而在platform_driver 结构体中相当于是重写了这些方法,对这些函数进行了新的定义,如果子类中的方法与父类中的方法具有相同的方法名、返回列表和参数表,将会覆盖原有的方法,这是面向对象的“多态”设计思想,极大的提高了代码的可重用能力

在编写 platform驱动的时候,首先定义一个 platform_driver结构体变量,然后实现结构体中的各个成员变量,重点 是实现匹配方法以及 probe函数。当驱动和设备匹配成功以后 probe函数就会执行,具体的驱动程序在 probe函数里面编写,比如字符设备驱动等等。

platform设备

有了driver我们还需要platform_device

struct platform_device {
	const char	*name;
	int		id;
	bool		id_auto;
	struct device	dev;
	u32		num_resources;
	struct resource	*resource;

	const struct platform_device_id	*id_entry;
	char *driver_override; /* Driver name to force a match */

	/* MFD cell pointer */
	struct mfd_cell *mfd_cell;

	/* arch specific additions */
	struct pdev_archdata	archdata;
};

name 设备名字,要和所使用的platform驱动的name字段相同
num_resources 资源数量,一般为resource的大小
resource 表示资源,也就是设备信息,比如外设寄存器等,struct resource表示资源

struct resource { 
	resource_size_t start; 
	resource_size_t end; 
	const char *name; 
	unsigned long flags; 
	struct resource *parent, *sibling, *child; 
	};

start 和 end表示资源的起始和停止信息,对于内存类的资源就表示内存起始和终止地址
name表示资源名字
flags表示资源类型
可选择的资源类型都定义在ioport.h

引入设备树后的变化

在以前不支持设备树的Linux版本中,用户需要编写platform_device变量来描述设备信息(一般在平台文件中写),然后使用 platform_device_register将驱动注册到内核中
不再使用时可以通过platform_device_unregister注销掉对应的platform设备

#ifdef CONFIG_HELLO_CTL
struct platform_device s3c_device_hello_ctl={
	.name = "hello_ctl",
	.id             = -1,
};
#endif

引入设备说以后我们就不用在定义设备了,只需要在设备树中添加一个节点,如下

gpioled {
		#address-cells = <1>; 
		#size-cells = <1>; 
		compatible = "atkalpha-gpioled"; 
		pinctrl-names = "default"; 
		pinctrl-0 = <&pinctrl_gpio_leds>; 
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; 
		status = "okay";
	};

我们添加了一个gpioled 节点,我们要注意它的compatible 属性"atkalpha-gpioled",在驱动中我们要设置匹配表

static const struct of_device_id led_of_match[] = {
    { .compatible = "atkalpha-gpioled"},
    { /**/ }
};

我们将匹配表设置只有一项设备,就是我们在设备树中定义的节点
然后定义platform_driver,将匹配表初始化到platform_driver

static struct platform_driver led_driver = {
    .driver = {
        .name = "im6ul-led",
        .of_match_table = led_of_match,
    },
    .probe = led_probe,
    .remove = led_remove,
};

这样当驱动加载到内核后,就会进行匹配,匹配成功的话才会执行probe函数,在probe函数中做设备、驱动的初始化。

实验代码与分析

实验代码

#include <linux/init.h>
#include <linux/ide.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/platform_device.h>

#include <asm/io.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>


#define LEDDEV_CNT 1
#define LEDDEV_NAME "platformled"
#define LEDOFF 0
#define LEDON 1

//设备结构体
struct platled_dev
{
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node *nd;
    int led0;
};

struct platled_dev leddev;//leddev设备

//使用gpio操作开关LED
void led0_switch(u8 sta)
{
    if(sta == LEDON)
    {
        gpio_set_value(leddev.led0, 0);
    }
    else if(sta == LEDOFF)
    {
        gpio_set_value(leddev.led0, 1);
    }  
}

//file_operations的open函数
static int led_open(struct inode *inode,struct file *filp)
{
    printk(KERN_EMERG "led_open enter!\n");
    filp->private_data = &leddev;
    return 0;
}
//file_operations的write函数
static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)
{ 
    int retvalue;
    unsigned char databuf[2];
    unsigned char ledstat;
    printk(KERN_EMERG "led_write enter!\n");
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0)
    {
        printk(KERN_EMERG "Kernel write failed!\n");
        return -EFAULT;
    }
    ledstat = databuf[0];
    if(ledstat == LEDON)
    {
        led0_switch(LEDON);
    }
    else if(ledstat == LEDOFF)
    {
        led0_switch(LEDOFF);
    }
    return 0;
}

//file_operations结构体
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
};
//probe函数
static int led_probe(struct platform_device *dev)
{
    /*1.create device ID */
    if(leddev.major)
    {
        leddev.devid = MKDEV(leddev.major, 0);
        register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
    }
    else
    {
        alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
        leddev.major = MAJOR(leddev.devid);
        leddev.minor = MINOR(leddev.devid);
    }
    printk(KERN_EMERG "nemchrdev major=%d,minor=%d \r\n", leddev.major, leddev.minor);
	
    /*2.Init cdev */	
    leddev.cdev.owner = THIS_MODULE;
    cdev_init(&leddev.cdev, &led_fops);

    /*3.add cdev*/
    cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

    /*4.create class*/
    leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
    if(IS_ERR(leddev.class))
    {
        printk(KERN_EMERG "class error!\n");
        return PTR_ERR(leddev.class);
    }

    /*5.create device*/
    leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
    if(IS_ERR(leddev.device))
    {
        printk(KERN_EMERG "device error!\n");
        return PTR_ERR(leddev.device);
    }

    leddev.nd = of_find_node_by_path("/gpioled");
    if(leddev.nd == NULL)
    {
        printk(KERN_EMERG "gpioled node can't find!\n");
        return -EINVAL;
    }
    leddev.led0 = of_get_named_gpio(leddev.nd, "led-gpio", 0);
    if(leddev.led0  < 0)
    {
        printk(KERN_EMERG "can't get led-gpio!\n");
        return -EINVAL;
    }
    gpio_request(leddev.led0, "led0");
    gpio_direction_output(leddev.led0, 1);
    return 0;
}
//remove函数
static int led_remove(struct    *dev)
{
     gpio_set_value(leddev.led0, 1);

     /*Uninstall divece*/
	cdev_del(&leddev.cdev);//delete cdev
    unregister_chrdev_region(leddev.devid,LEDDEV_CNT);

    device_destroy(leddev.class,leddev.devid);
    class_destroy(leddev.class);
    return 0;
}

//设备匹配表
static const struct of_device_id led_of_match[] = {
    { .compatible = "atkalpha-gpioled"},
    { /**/ }
};

//platform_driver
static struct platform_driver led_driver = {
    .driver = {
        .name = "im6ul-led",
        .of_match_table = led_of_match,
    },
    .probe = led_probe,
    .remove = led_remove,
};

static int __init leddriver_init(void)
{
    return platform_driver_register(&led_driver);
}

static void __exit leddriver_exit(void)
{
    platform_driver_unregister(&led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("GYY");

代码分析

  • 在init函数中调用platform_driver_register,传入的参数为一个platform_driver类型的参数,对该参数要进行定义,定义的包括.driver .probe .remove
  • 在probe函数中完成设备的注册及初始化,IO口的注册及初始化
  • 在exit函数中调用platform_driver_unregister,传入的参数同register函数
  • 在probe函数中所做的操作就是以前在init函数中做的操作

关于在probe函数中的操作我们进行以下分析

  • 首先调用alloc_chrdev_region创建device_id
  • 使用cdev_init初始化cdev结构体,将file_operations结构体写到cdev中
  • 调用cdev_add注册cdev结构体
  • 调用class_create创建一个class
  • 调用device_create创建一个字符设备
  • 接下来使用of_find_node_by_path获取设备树节点,使用of_get_named_gpio获取GPIO
  • 调用gpio_request请求GPIO并使用gpio_direction_output将GPIO初始化为输出模式

总结

platform是一种虚拟的总线,主要用于那些没有实体总线的设备,要注意platform_device并不是与字符设备、块设备、和网络设备并列的概念,而是Linux系统提供的一种附加手段,我们通常会把SOC内部集成的I2C、RTC、LCD、看门狗等外设都归纳为platform_device,而它们本质上都是字符设备。
再一次看platform,确实有了许多新的感受,在4412迷迷糊糊的驱动、设备、总线也有了些基本的认识,但还是有很多可以深入的点的,比如如何选择驱动设备的匹配方式,如果从设备树中读取compatible并进行匹配,关于platform_device的资源部分也有很多不太清除的点,所有说还需要继续努力

Logo

更多推荐