在今年五月份我在4412上学习了platform总线设备的驱动编写,了解了引入platform总线的目的以及带来的方便之处,初步了解了Linux内核总线、设备、驱动的总体框架,最近我开始了基于设备树的驱动开发的学习,所以总结记录一下在如何使用设备树来编写platform设备驱动,再温习一下关于platform的知识。
Linux系统要考虑驱动的可重用性,驱动的分离和分层
所以引入了platform设备驱动,平台设备驱动
驱动的分隔,将主机驱动和设备驱动分隔开,I2C、SPI等都会采用驱动分隔方式
实际驱动开发中,I2C主机驱动由半导体厂家编写好,设备驱动也有设备器件厂家编写好,我们只需要提供设备信息即可,比如连接到了哪个I2C接口、I2C的速率是多少
将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如设备树),然后根据获取到的信息来初始化设备
所以说总线的作用就是将驱动和设备匹配起来,作为两者中间的一个媒介
在编写Linux驱动时,需要编写file_operations成员函数,还要考虑阻塞、非阻塞、多路复用、SIGIO等复杂的问题。但是当我们面对一个真实的硬件驱动时,总是懒惰的,想做尽可能少的事情,比如按键的驱动,我们只想收获一个按键中断、汇报一个键值,所以Linux内核通过驱动分层的方式来帮助我们“偷懒”,尽管file_operations、IO模型是不可或缺的,但是这部分的代码所有的按键都是一样的、甚至可以说所有的输入设备都是一样,所以Linux内核提供了input子系统来帮助我们解决输入相关的事项,相当于实现了一个中间件,把那些重复的事情搞定,而我们只要配置底层的与IO相关的,和调用它的上报函数。
同样的Linux内核不仅仅只有input子系统,与LCD显示相关的部分抽象出了Framebuffer子系统专门来管理所有与显示有关的硬件和软件;与RTC相关的部分抽象出了RTC子系统等等
首先我们来看一下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可以使用四种匹配方式
首先我们来看一下最基本的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函数里面编写,比如字符设备驱动等等。
有了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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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");
关于在probe函数中的操作我们进行以下分析
platform是一种虚拟的总线,主要用于那些没有实体总线的设备,要注意platform_device并不是与字符设备、块设备、和网络设备并列的概念,而是Linux系统提供的一种附加手段,我们通常会把SOC内部集成的I2C、RTC、LCD、看门狗等外设都归纳为platform_device,而它们本质上都是字符设备。
再一次看platform,确实有了许多新的感受,在4412迷迷糊糊的驱动、设备、总线也有了些基本的认识,但还是有很多可以深入的点的,比如如何选择驱动设备的匹配方式,如果从设备树中读取compatible并进行匹配,关于platform_device的资源部分也有很多不太清除的点,所有说还需要继续努力