Linux驱动开发(十):设备树下的platform平台设备驱动
这里写自定义目录标题简介关于platform驱动框架bus、driver、device驱动的分层platform驱动模型platform总线platform驱动platform设备引入设备树后的变化实验代码与分析实验代码代码分析总结简介在今年五月份我在4412上学习了platform总线设备的驱动编写,了解了引入platform总线的目的以及带来的方便之处,初步了解了Linux内核总线、设备、..
这里写自定义目录标题
简介
在今年五月份我在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的资源部分也有很多不太清除的点,所有说还需要继续努力
更多推荐
所有评论(0)