平时一般都使用字符设备框架来编写简单的IO读写驱动,如果遇到复杂的总线设备时, 仅有字符设备框架是应付不过来的,复用性也很难保证,不符合 Linux 的设计思想。比如,SPI总线挂载了多个设备时,这些设备的驱动实现就需要用到驱动分离的思想,为此linux内核提供了platform机制
一,驱动分离思想:linux内核分离思想
1,描述
我们点亮 LED 实验中的驱动,到底是IO驱动还是LED的驱动呢?暂时结合起来看作是IO_LED驱动,这时候又用到按键设备,又有了IO_KEY驱动, 要使用蜂鸣器,就会出现IO_BUZZER驱动,每多一个设备,就会多一个 IO_XXX驱动。这些IO_XXX驱动中,IO的部分几乎是一样的,为了保证代码的简洁,把IO的部分单独拎出来,写成一个IO驱动,然后设备的驱动就只剩下设备自身的内容,只需要调用IO驱动中的接口即可。
linux的一大卖点是方便移植,现在需要在其他的硬件平台上运行使用返些设备驱动又会发成什么情况呢。不同厂家SOC上的GPIO底层的实现都有所差别,那每出现一个新的SOC,就需要重写一个 GPIO驱动。显然这是无法避免的,但是这些GPIO驱动中,还是有共同的部分,即IO和设备间的接口,把这些 GPIO 中的接口部分再拎出来,单独写成一个驱动。
这样就形成了驱动的分离,一边是SOC的硬件资源,另一边是用户设备,它们通过统一的接口来连接。驱动分离的思想带来了许多好处,linux长久的发展中,省去了很多冏余的代码,SOC厂家根据统一的接口提供SOC片上外设的驱动,设备厂家也根据结构统一的接口提供设备的驱动,用户需要选择好SOC和外设,就能径方便的关联到一起。这样的思想也延续到了单个SOC中,片上驱动和设备驱动通过总线协议来关联。当我们往内核中添加驱动时,总线就会查找对应的设备,添加设备时就去查找对应的驱动。 这就是linux 中的bus、driver、device模型,platform就是这种模型的实现。
2,本质:将原先驱动中的硬件和软件撤离分开, 软件一旦写好,将来硬件发生变化,无需改动软件,要改只改硬件部分即可!提高驱动的可移植性!这样驱动开发者的重心放在硬件部分即可,软件一旦写好需要改动!
3,linux内核分离思想的实现基于platform机制原理
总结:如果要采用platform机制实现一个硬件设备驱动,驱动开发者只需要关注两个数据结构即可:
struct platform_device
struct platform_driver
剩下的:什么遍历,什么匹配,什么调用(probe),什么传参(传硬件节点首地址)都四个"什么"统统由内核来帮你实现!
二,platform模型
SPI 等总线设备很容易去对应bus、driver、device模型,但是SOC上并不是所有资源都有总线的概念,比如 GPIO。为了对应 bus、driver、device模型,platform模型定义了platform_bus虚拟总线、platform_driver和platform_device分别来对应模型总线、驱动和设备。有一点要说明的是,platform平台设备模型虽然名字里有”平台设备”,但他并不是独立于linux 三种设备之外的新的设备类型,强调的模型只是一种框架,在使用中是无法和字符设备、块设备、网络设备脱离关系的。
1,platform_bus
内核中使用bus_type结构体表示总线,定义在文件 include/linux/device.h 中:
struct bus_type
{
……
int (*match)(struct device *dev, struct device_driver *drv);
……
};
成员中的 match函数就是驱动和设备匹配的关键,他的两个输入参数一个是 drv 一个是 dev 也就是驱动和设备,每个总线类型都必项实现这个函数。platform虚拟总线也是 bus_type 类型的变量,在文件 drivers/base/platform.c 中定义:
struct bus_type platform_bus_type =
{
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm= &platform_dev_pm_ops,
};
其中 match函数的实现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))
return 1;
/* Then try ACPI style match */
if (acpi_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_match函数提供了 4 种匹配方式:设备树下会使用的 OF 匹配表匹配方式,match的输入参数之一 drv的数据类型中有一个成员发量of_match_table,of_match_table 又有一个成员为 compatible,如果这个compatible能和设备树中的compatible属性相匹配,驱动代码中的probe就会被调用。 如果没有使用设备树,一般会使用return (strcmp(pdev->name, drv->name) == 0)匹配方式,直接比较驱动和设备中name成员。 platform 总线是不需要驱动层面去管理的,也就是说对于驱动开发,了解其中原理即可。
2,platform_driver
(1)定义并初始化platform_driver
platform驱动用platform_driver结构体来表示,在头文件include/linux/platform_device.h 中:
struct platform_driver
{
int (*probe)(struct platform_device *pdev);
int (*remove)(struct platform_device *pdev);
void (*shutdown)(struct platform_device *pdev);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *pdev);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
功能:描述硬件对应的纯软件信息(if...else,swtich..case,fops,miscdevice等)
成员:
driver:重点关注其中的name字段,将来用于匹配,必须要初始化
probe:硬件节点和软件节点匹配成功,内核调用此函数形参pdev指向匹配成功的硬件节点对象,将来probe函数可以通过此参数获取到纯硬件信息
remove:当从dev删除硬件节点或者从drv删除软件节点内核都会调用remove函数形参pdev指向匹配成功的硬件节点对象,将来probe函数可以通过此参数获取到纯硬件信息
总结:probe函数是否被调用至关重要!如果probe函数被内核调用,说明一个完整的驱动程序诞生,也就预示着硬件和软件匹配成功!probe函数一般所做的工作:
a,通过形参pdev获取纯硬件信息
b,处理获取到的纯硬件信息, 该申请的申请(GPIO资源,中断资源)
该映射的映射(物理地址映射到内核虚拟地址)
该初始化的初始化(配置GPIO为输出口等等)
该注册的注册(中断处理函数)
c,注册字符设备驱动或者混杂设备驱动,关键是为了给用户提供访问硬件的 *** 作接口
总结:采用platform机制实现驱动,也就是将原先驱动中的入口函数所做的事情现在统统移到probe函数中完成(因为probe函数的调用,预示着一个完整的驱动诞生,有了硬件还有了软件)remove和probe是死对头!
(2)platform_driver注册和注销
定义好 struct platform_driver 后,需要在驱动入口函数中调用下面的函数来注册 platform驱动,取代原先的初始化内容:
int platform_driver_register (struct platform_driver*driver);注册成功返回0,失败返回负。
在出口函数中做相应的注销 *** 作取代原先的注销内容:
void platform_driver_unregister(struct platform_driver *drv);
3,platform_device
platform设备结构体platform_device来表示。在支持设备树的内核中,可以使用设备树代替platform_device,但是platform_device仍然保留使用
(1)platform_device结构体体定义在 include/linux/platform_device.h 中
struct platform_device //数据结能:描述纯硬件信息
{
const char *name;//描述硬件节点名称,此字段将来用于匹配,必须要初始化!
int id;//描述硬件节点的编号,如果dev链表上只有一个名称为name的硬件节点,id给-1,
如果dev链表上有多个名称同时为name的硬件节点,通过id进行区分:0,1,2...
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_driver中的name相同。 成员 id 表示当前设备在这类设备中的编号,还有一个这种类型的设备是id赋值-1。 成员 num_resources 表示资源数量。 成员resource 是资源的数组。start 表示资源的起始地址,end 表示资源的结束地址,name是资源名称,flags表示资源类型,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:硬件结束信息
flags:硬件信息的标志,一般指定以下标志
IORESOURCE_MEM:表示这个硬件信息为地址信息 —— 例如:0xE0200080,这个信息就用此宏来表示;IORESOURCE_IRQ:表示这个硬件信息为GPIO信息——例如:GPC1_3中的3(GPC1组的第3个),这个信息就用此宏表示;+IRQ_EINT(0)(第一个中断),这个信息也可以用此宏来表示
例如:描述LED1,LED2的硬件信息:
struct resource led_info[] =
{
{ //描述寄存器的物理地址信息
.start = 0xE0200080, //起始地址
.end = 0xE0200080 + 8, //结束地址
.flags = IORESOURCE_MEM //地址信息 },
{ //描述LED1的GPIO编号信息
.start = 3, //第3个GPIO
.end = 3, //第3个GPIO
.flags = IORESOURCE_IRQ //GPIO信息
},
{ //描述LED2的GPIO编号信息
.start = 4, //第3个GPIO
.end = 4, //第3个GPIO
.flags = IORESOURCE_IRQ //GPIO信息
}
};
总结:如果将来硬件上发生变化,只需resource描述的,硬件信息即可!num_resources:用struct resource描述的硬件信息的个数ARRAY_SIZE(led_info)
(2)platform_device设备注册:int platform_device_register(struct platform_device *pdev);
在声明初始化platform_device结构体后,使用platform_driver_register ( &软件节点对象 )注册。
功能:
a.将定义初始化的软件节点添加到drv链表上
b.内核会帮你遍历dev链表,取出dev链表上每一个硬件节点,跟这个要注册的软件节点进行匹配,匹配通过内核调用总线提供的match,比较软件节点和硬件节点的name是否相等,如果相等,内核调用软件节点的probe函数,并且把 匹配成功的硬件节点的首地址传给probe函数,完成硬件和软件的再次结合!如果匹配不成功,没关系,软件节点静静等待着硬件节点的到来!
(3)platform_device设备注销时:void platform_device_unregister(struct platform_device *pdev);
platform_device_unregister(&硬件节点对象),功能如下:
a.将定义初始化的硬件节点添加到dev链表上
b.内核会帮你遍历drv链表,取出drv链表上每一个软件节点跟这个要注册的硬件节点进行匹配,匹配通过内核调用总线提供的match,比较软件节点和硬件节点的name是否相等,如果相等,内核调用软件节点的probe函数,并且把这个要注册的硬件节点的首地址传给probe函数,完成硬件和软件的再次结合!如果匹配不成功,没关系,硬件节点静静等待着软件节点的到来!从dev链表中将硬件节点对象删除,同时内核调用匹配成功的软件节点的remove函数
举例:利用platform机制优化LED驱动程序
#include
#include
#include
//定义初始化描述LED的硬件信息
//如果将来硬件发生改变,只需该此数据结构即可
static struct resource led_info[] =
{//寄存器地址信息
{
.start = 0xE0200080,//起始地址
.end = 0xE0200080 + 8, //结束地址
.flags = IORESOURCE_MEM //地址类信息
},
//GPC1_3的GPIO信息
{
.start = 3, //第3个GPIO
.end = 3,
.flags = IORESOURCE_IRQ //GPIO类信息
},
//GPC1_4的GPIO信息
{
.start = 4, //第3个GPIO
.end = 4,
.flags = IORESOURCE_IRQ //GPIO类信息
}
};
//仅仅是去除警告
static void led_release(struct device *dev) {}
//定义初始化描述LED的硬件节点对象
static struct platform_device led_dev =
{
.name = "subomb",//硬件节点对象名,用于匹配
.id = -1,//编号
.resource = led_info, //装载硬件信息
.num_resources = ARRAY_SIZE(led_info), //硬件信息个数
.dev =
{
.release = led_release //仅仅去除警告
}
};
static int led_dev_init(void)
{
//1.添加硬件节点到dev链表
//2.什么遍历,什么匹配,什么调用,什么传参由此函数完成
platform_device_register(&led_dev);
return 0;
}
static void led_dev_exit(void)
{
//从dev卸载硬件节点
platform_device_unregister(&led_dev);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
(4)当platform设备设置好资源后,platform驱动就可以通过下面的函数来获取资源信息:
struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int num);
dev:目标platform设备。 type:也就是platform_device结构体成员resource结构体的flags成员。 num:指定type的资源的下标。 返回值:资源的信息,成功时返回resource结构体类型指针,失败时返回NULL。
三,总结:驱动采用platform机制关键关注两个数据结构加四个配套函数
1, struct platform_device
描述纯硬件节点
.name 用于匹配
.id 编号
.resource 装在硬件信息
.start 起始信息
.end 结束信息
.flags
IORESOURCE_MEM:地址
IORESOURCE_IRQ:管脚和中断号
struct platform_driver
.driver
.name 用于匹配
.probe 匹配成功内核调用
(1).通过形参pdev获取硬件信息
硬件信息的首地址=platform_get_resource(硬件节点首地址,资源类型,同类资源的偏移量)
(2).处理硬件信息
申请
注册
映射
初始化
(3).注册字符或者混杂设备,关键
是提供硬件 *** 作接口
.remove 卸载软件或者硬件内核调用,跟probe对着干!
probe函数是被调用只管重要,如果调用,一个完成的驱动程序诞生!
2, platform_device_register
(1).添加硬件节点到内核dev链表
(2).遍历drv链表
(3).一一匹配
(4).调用probe,传递硬件节点首地址给probe
3, platform_device_unregister
(1).删除硬件节点
(2).调用remove函数
4,platform_driver_register
(1).添加软件节点到drv链表
(2).遍历dev链表
(3).一一匹配
(4).调用probe函数,传递硬件节点首地址给probe
5,platform_driver_unregister
(1).删除软件节点
(2).调用remove函数
四,platform驱动程序:驱动程序分为驱动设备备两个部分。
1,在 ax-platform-drv.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* 设备节点名称 */
#define DEVICE_NAME "gpio_leds"
/* 设备号个数 */
#define DEVID_COUNT 1
/* 驱动个数 */
#define DRIVE_COUNT 1
/* 主设备号 */
#define MAJOR
/* 次设备号 */
#define MINOR 0
/* gpio寄存器虚拟地址 */
static u32 *GPIO_DIRM_1;
/* gpio使能寄存器 */
static u32 *GPIO_OEN_1;
/* gpio控制寄存器 */
static u32 *GPIO_DATA_1;
/* AMBA外设时钟使能寄存器 */
static u32 *APER_CLK_CTRL;
/* 把驱动代码中会用到的数据打包进设备结构体 */
struct alinx_char_dev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
};
/* 声明设备结构体 */
static struct alinx_char_dev alinx_char = {
.cdev = {
.owner = THIS_MODULE,
},
};
/* open函数实现, 对应到Linux系统调用函数的open函数 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
{
/* 设置私有数据 */
file_p->private_data = &alinx_char;
return 0;
}
/* write函数实现, 对应到Linux系统调用函数的write函数 */
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
{
int rst;
char writeBuf[5] = {0};
rst = copy_from_user(writeBuf, buf, len);
if(0 != rst)
{
return -1;
}
if(1 != len)
{
printk("gpio_test len err\n");
return -2;
}
if(1 == writeBuf[0])
{
*GPIO_DATA_1 |= 0x00004000;
}
else if(0 == writeBuf[0])
{
*GPIO_DATA_1 &= 0xFFFFBFFF;
}
else
{
printk("gpio_test para err\n");
return -3;
}
return 0;
}
/* release函数实现, 对应到Linux系统调用函数的close函数 */
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
{
return 0;
}
/* file_operations结构体声明, 是上面open、write实现函数与系统调用函数对应的关键 */
static struct file_operations ax_char_fops =
{
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
/* probe函数实现, 驱动和设备匹配时会被调用 */
static int gpio_leds_probe(struct platform_device *dev)
{
/* 资源大小 */
int regsize[3];
/* 资源信息 */
struct resource *led_source[3];
int i;
for(i = 0; i < 3; i ++)
{
/* 获取dev中的IORESOURCE_MEM资源 */
led_source[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
/* 返回NULL获取资源失败 */
if(!led_source[i])
{
dev_err(&dev->dev, "get resource %d failed\r\n", i);
return -ENXIO;
}
/* 获取当前资源大小 */
regsize[i] = resource_size(led_source[i]);
}
/* 把需要修改的物理地址映射到虚拟地址 */
GPIO_DIRM_1 = ioremap(led_source[0]->start, regsize[0]);
GPIO_OEN_1 = ioremap(led_source[1]->start, regsize[1]);
GPIO_DATA_1 = ioremap(led_source[2]->start, regsize[2]);
/* MIO_0设置成输出 */
*GPIO_DIRM_1 |= 0x00004000;
/* MIO_0使能 */
*GPIO_OEN_1 |= 0x00004000;
/* 注册设备号 */
alloc_chrdev_region(&alinx_char.devid, MINOR, DEVID_COUNT, DEVICE_NAME);
/* 初始化字符设备结构体 */
cdev_init(&alinx_char.cdev, &ax_char_fops);
/* 注册字符设备 */
cdev_add(&alinx_char.cdev, alinx_char.devid, DRIVE_COUNT);
/* 创建类 */
alinx_char.class = class_create(THIS_MODULE, DEVICE_NAME);
if(IS_ERR(alinx_char.class))
{
return PTR_ERR(alinx_char.class);
}
/* 创建设备节点 */
alinx_char.device = device_create(alinx_char.class, NULL,
alinx_char.devid, NULL,
DEVICE_NAME);
if (IS_ERR(alinx_char.device))
{
return PTR_ERR(alinx_char.device);
}
return 0;
}
static int gpio_leds_remove(struct platform_device *dev)
{
/* 注销字符设备 */
cdev_del(&alinx_char.cdev);
/* 注销设备号 */
unregister_chrdev_region(alinx_char.devid, DEVID_COUNT);
/* 删除设备节点 */
device_destroy(alinx_char.class, alinx_char.devid);
/* 删除类 */
class_destroy(alinx_char.class);
/* 释放对虚拟地址的占用 */
*GPIO_OEN_1 &= 0xFFFFBFFF;
iounmap(GPIO_DIRM_1);
iounmap(GPIO_OEN_1);
iounmap(GPIO_DATA_1);
return 0;
}
/* 声明并初始化platform驱动 */
static struct platform_driver led_driver = {
.driver = {
/* 将会用name字段和设备匹配, 这里name命名为alinx-led */
.name = "alinx-led",
},
.probe = gpio_leds_probe,
.remove = gpio_leds_remove,
};
/* 驱动入口函数 */
static int __init gpio_led_drv_init(void)
{
/* 在入口函数中调用platform_driver_register, 注册platform驱动 */
return platform_driver_register(&led_driver);
}
/* 驱动出口函数 */
static void __exit gpio_led_dev_exit(void)
{
/* 在出口函数中调用platform_driver_register, 卸载platform驱动 */
platform_driver_unregister(&led_driver);
}
/* 标记加载、卸载函数 */
module_init(gpio_led_drv_init);
module_exit(gpio_led_dev_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("subomb");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("PLATFORM LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
2,ax-platform-dev.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* 寄存器首地址 */
/* gpio方向寄存器 */
#define GPIO_DIRM_1 0xFF0A0244
/* gpio使能寄存器 */
#define GPIO_OEN_1 0xFF0A0248
/* gpio控制寄存器 */
#define GPIO_DATA_1 0xFF0A0044
/* 寄存器大小 */
#define REGISTER_LENGTH 4
/* 删除设备时会执行此函数 */
static void led_release(struct device *dev)
{
printk("led device released\r\n");
}
/*一是 39 行开始的 struct resource结构体类型的数组,返里面是我们需要的讴备信息,每个
元素都需要初始化的三个成员发量时 start、end、flags,在 drv 中调用 resource_size 凼数
时,会根据 start 呾 end 迒回资源大小,flags 是 platform_get_resource函数获取资源信息的依据*/
/* 初始化LED的设备信息, 即寄存器信息 */
static struct resource led_resources[] =
{
{
.start = GPIO_DIRM_1,
.end = GPIO_DIRM_1 + REGISTER_LENGTH - 1,
/* 寄存器当作内存处理 */
.flags = IORESOURCE_MEM,
},
{
.start = GPIO_OEN_1,
.end = GPIO_OEN_1 + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM,
},
{
.start = GPIO_DATA_1,
.end = GPIO_DATA_1 + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM,
},
};
/* 声明并初始化platform_device */
static struct platform_device led_device =
{
/* 名字和driver中的name一致 */
.name = "alinx-led",
/* 只有一个设备 */
.id = -1,
.dev = {
/* 设置release函数 */
.release = &led_release,
},
/* 设置资源个数 */
.num_resources = ARRAY_SIZE(led_resources),
/* 设置资源信息 */
.resource = led_resources,
};
/* 入口函数 */
static int __init led_device_init(void)
{
/* 在入口函数中调用platform_driver_register, 注册platform驱动 */
return platform_device_register(&led_device);
}
/* 出口函数 */
static void __exit led_device_exit(void)
{
/* 在出口函数中调用platform_driver_register, 卸载platform驱动 */
platform_device_unregister(&led_device);
}
/* 标记加载、卸载函数 */
module_init(led_device_init);
module_exit(led_device_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("subomb");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("PLATFORM LED device");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
3,应用测试程序
#include
#include
#include
#include
int main(int argc, char **argv)
{
int fd;
char buf;
/* 验证输入参数个数 */
if(3 != argc)
{
printf("none para\n");
return -1;
}
/* 打开输入的设备文件, 获取文件句柄 */
fd = open(argv[1], O_RDWR);
if(fd < 0)
{
/* 打开文件失败 */
printf("Can't open file %s\r\n", argv[1]);
return -1;
}
/* 判断输入参数, on就点亮led, off则熄灭led */
if(!strcmp("on",argv[2]))
{
printf("ps_led1 on\n");
buf = 1;
write(fd, &buf, 1);
}
else if(!strcmp("off",argv[2]))
{
printf("ps_led1 off\n");
buf = 0;
write(fd, &buf, 1);
}
else
{
/* 输入参数错误 */
printf("wrong para\n");
return -2;
}
/* 操作结束后关闭文件 */
close(fd);
return 0;
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)