Linux_平台设备总线

1.platform总线模型

1.1.Linux设备驱动模型回顾

1.实现驱动加载函数xxx_init()和驱动卸载函数xxx_exit()(与内核相关)
2.申请设备号,字符设备注册(与内核相关)
3.利用udev/mdev机制创建设备文件节点class_create,device_create(与内核相关)
4.硬件部分初始化(与硬件相关)
IO资源映射ioremap
注册中断request_irq
5.构建file_operation结构(与内核相关)
6.实现操作硬件方法xxx_open、xxx_read、xxx_write、xxx_close
根据前面阶段的学习,我们知道每写一个驱动都需要按照上面的步骤来进行编写!在编写设备驱动程序的时候会发现,对于不同的硬件设备,其驱动程序肯定不一样,但是也仅仅只有一点点不一样(与硬件操作相关的部分),大部分的驱动代码都是重复的。

1.2.设备驱动分离思想

对于Linux这样一个成熟、庞大、复杂的操作系统,代码的重用性非常重要,否则的话就会在Linux内核中存在大量无意义的重复代码。尤其是驱动程序,因为驱动程序占用了Linux内核代码量的大头,如果不对驱动程序加以管理,任由重复的代码肆意增加,那么用不了多久Linux内核的文件数量就庞大到无法接受的地步。
假如现有三个硬件平台(SOC)A、B、C,每个平台都有MPU6050这个I2C接口的六轴传感器,按照写裸机I2C驱动的时候的思路,每个平台都有一个MPU6050设备驱动,因此编写出来的驱动框架如下图所示:
在这里插入图片描述
从上图可以看到,每种平台都有一个主机驱动和一个设备驱动,不同的SOC其I2C控制器不同,主机驱动也不同,但是MPU6050的使用对于不同的SOC来说都是一样的,都是通过I2C接口读取数据就行了,所以只需要一个MPU6050的驱动程序即可。因此,我们可以将以上驱动模型进行简化,将每个平台的I2C控制器都提供一个统一的接口,MPU6050也只提供一个驱动程序,那么MPU6050就可以通过一个I2C接口驱动访问,这样大大简化驱动文件,驱动框架如下如所示:
在这里插入图片描述
实际上,硬件平台上的I2C设备会有很多种,不止MPU6050一个,那么所有的I2C设备都可以通过这个统一的接口来访问,那么驱动框架便如下图所示:
在这里插入图片描述
这个就是驱动的分离,就是将主机驱动与设备驱动分隔开来。这个就是Linux中的总线(bus)、驱动(driver)、设备(device)模型,即驱动分离。总线类似于驱动和设备信息的月老,负责给两者牵线。总线、驱动、设备模型就如下图所示:
在这里插入图片描述
当向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统注册一个设备的时候,总线就会在左侧的驱动中查找有没有匹配的驱动,有的话也联系起来。Linux内核中大量的驱动程序都采用总线、驱动和设备的驱动模型。比如I2C子系统、SPI子系统。

1.3.platform总线模型由来

在SOC中,有些外设是没有总线这个概念的,比如LED、KEY、LCD等,但是又想要使用总线、驱动和设备的模型怎么办?因此,Linux便提出了platform这个虚拟总线。这个虚拟总线相应的设备称为platform_device,相应的驱动称为platform_driver。platform_device记录了设备的所占CPU资源(寄存器、中断号),platform_driver就可以根据platform_device中的资源进行字符设备注册,提高了代码的重用性,可移植性。

1.4.platform总线模型特点

1.platform总线模型采用了分层结构,把一个设备驱动程序分成了俩个部分:平台设备(platform_device)和平台驱动(platform_driver)。
2.平台设备将设备本身的资源(占用哪些中断号,内存资源,IO口等)注册进内核,可以由内核统一管理,驱动更加安全,可靠。
3.统一了设备驱动模型,提高了设备驱动的可移植型,通用性更好。
注: 所谓的platform驱动并不是独立于字符设备驱动、块设备驱动和网络设备驱动之外的其他种类的驱动。platform驱动只是为了驱动的分离与分层而提出来的一种框架,其驱动的具体实现还是需要字符设备驱动、块设备驱动或网络设备驱动。

2.platform设备

2.1.platform设备核心数据结构

Linux使用platform_device结构体来描述设备,该结构体定义在include/linux/platform_device.h文件中。

struct platform_device {
	const char		*name;		//设备名,用来和驱动层代码做匹配
	int				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;
};

重要成员说明:
name:设备名,用来和驱动层代码做匹配,必须和驱动层中的platform_driver.driver.name或platform_driver.id_table.name相同,这样安装设备和驱动模块才可以匹配上。
num_resources:表示资源数量,实际上就是resource数组元素个数。可以让其直接等于ARRAY_SIZE(struct resource结构体变量名)。
resource:表示资源,也就是设备信息(设备占用了哪些内存,IO口,中断号,DMA内存等)。Linux内核使用resource结构体表示设备资源,resource结构体如下:

所在路径:include/linux/ioport.h	
struct resource {
	resource_size_t start;		//资源的起始值
	resource_size_t end;		//资源的结束值
	const char *name;		    //资源名字
	unsigned long flags;		//资源类型
	struct resource *parent, *sibling, *child;//内核使用到,驱动开发者不需要接触到
};

start,end的含义和flags值有关,flags用来描述资源类型的,其可取值内核定义如下:
#define IORESOURCE_IO 0x00000100 //表示资源是IO口
#define IORESOURCE_MEM 0x00000200 //表示资源是一段物理内存
#define IORESOURCE_IRQ 0x00000400 //表示资源是中断编号
#define IORESOURCE_DMA 0x00000800 //表示资源是DMA
#define IORESOURCE_BUS 0x00001000 //表示资源是总线编号
常用是IORESOURCE_MEM和IORESOURCE_IRQ两种。
对于内存类资源,start和end表示内存起始和终止地址。
对于中断类资源,start和end表示中断号起始编号和终止编号

dev:内嵌的设备模型结构,linux内核使用device结构体来描述所有设备,是所有设备的基础。device结构体定义如下:

所在路径:include/linux/device.h
struct device {    //637
	……
	void		*platform_data;	/* Platform specific data, device core doesn't touch it */
	……
	void	(*release)(struct device *dev);	//指向卸载模块时会执行释放函数指针
};

release:若没有指定该成员的值,则在设备与驱动匹配的情况下,卸载平台设备驱动会出现警告。
platform_data:
表示平台数据,是void*类型的,可以接收任何类型的指针,在实际编程中使用非常多,当要实现复杂的驱动功能时一般需要使用到这个成员,存放用户自定义的数据结构变量地址。

2.2.platform设备相关API

所在路径:include/linux/platform_device.h

原型:int platform_device_register(struct platform_device *pdev)
功能:向内核注册一个平台设备
参数:pdev:要注册平台设备结构指针
返回值:0:注册成功;负数:注册失败

原型:void platform_device_unregister(struct platform_device *pdev)
功能:从内核中注销一个平台设备
参数:pdev:要注销平台设备结构指针
返回值:无

2.3.platform设备编程思路

1.先定义struct platform_device结构变量
2.初始化上一步定义的struct platform_device结构变量必须的元素,一般在定义时候就初始化了。
初始化的核心:
1)分析清楚设备占用的资源情况:分析硬件原理图得到。
2)定义设备占用的资源数组
3)初始化设备结构的num_resources,resource
4)根据需要决定是否需要传递平台数据:dev.platform_data成员
3.在模块加载函数中调用platform_device_register函数注册已经初始化号好的平台设备结构变量
4.在模块卸载函数中调用platform_device_unregister函数注销已经注册的平台设备

3.platform驱动

3.1.platform驱动核心数据结构

Linux使用platform_driver表示platform驱动,该结构体定义在include/linux/platform_device.h文件中。

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

重要成员:
probe:指向探测函数的函数指针,当设备和驱动匹配时候,其指向函数(探测函数)会被自动执行。
remove:指向设备移除函数的函数指针,当卸载已经匹配好的设备和驱动中的任意一个时候,都会执行其指向的函数(移除函数)。
因此,探测函数和移除函数所要执行的操作是相反的。比如:在probe函数中动态申请了一块内存,则在remove函数中必须释放所申请的内存.
注:以上两个是必须实现。
driver:为device_driver结构体变量,device_driver相当于基类,提供了最基础的驱动框架。platform_driver继承了这个基类,然后在此基础上又添加一些特有的成员变量,device_driver结构体类型如下:

所在路径:include/linux/device.h
struct device_driver {
	const char		*name;//设备名,可用于匹配设备层代码使用
	……
	const struct of_device_id	*of_match_table;//高版本内核平台设备可以使用设备树方式实现设备层,
	该成员就是当使用设备树来实现时候,可以使用这个成员进行匹配
	……
};

对于device_driver结构,一般驱动只需要实现name成员,用来匹配platform设备。
id_table:也可以用来和设备层进行匹配,如果实现了这个成员,则不会使用.name成员进行匹配。该变量结构体类型如下:

所在路径:include/linux/mod_devicetable.h
struct platform_device_id {
	char name[PLATFORM_NAME_SIZE];
	kernel_ulong_t driver_data
			__attribute__((aligned(sizeof(kernel_ulong_t))));
};

当你需要实现一个驱动可以匹配多个设备的时候可以使用这种方式实现。
具体操作:是定义一个数组,然后实现其中name成员。所以在这个数组中的name字符都表示支持一个平台设备,当你安装一个平台设备,名字和数组中任意一个name成员相同时候,则匹配上,调用驱动程序的probe函数。示例如下:

struct platform_device_id id_tables[] = 
{
	{.name = "myled"},
	{.name = "mykey"},
	{.name = "mybeep"}
};
struct platform_driver led_drv = 
{
	.probe = led_probe,
	.remove = led_remove,
	.driver = {
		.name = "led"     //若使用了id_table方式匹配,该成员就无效
	},
	.id_table = id_tables
};

3.2.platform驱动相关API

头文件路径:include/linux/platform_device.h

1int platform_driver_register(struct platform_driver *drv);
功能:向内核注册一个平台驱动。
如果此时有匹配的平台设备则会引发内核执行平台驱动结构中的probe函数
参数:drv:要注册的平台驱动结构指针
返回值:0:注册成功;负数:注册失败

2void platform_driver_unregister(struct platform_driver *drv);
功能:从内核中注销一个平台设备驱动。
如果此时已匹配平台设备,则会引发内核执行平台驱动结构中的remove函数
参数:drv:要注册的平台驱动结构指针 
返回值:无

3.3.platform驱动编程思路

1.定义一个平台驱动结构变量
2.初始化上一步定义平台驱动结构变量
1)设备名:.driver.name:保持和设备层中的相同即可
2)probe:
a.探测平台设备资源
b.申请使用平台设备资源
c.使用申请到资源:内存资源–映射成虚拟地址再使用,中断资源–注册中断。
d.注册字符设备:以前应该怎么写还怎么写
3)remove:功能和probe相反,所以它所做事情是释放再probe函数占用的资源。
3.在模块加载函数中调用平台驱动注册函数注册上一步初始化好的平台驱动结构变量
4.在模块卸载函数中调用平台驱动注销函数注销平台驱动。

4.platform模型小结

4.1.设备与驱动的匹配依据

1.根据驱动代码中platform_driver.driver.name是否和设备代码中platform_device.name是否相同。
2.根据驱动代码中platform_driver.id_table.name是否和设备代码中的platform_device.name相同。
3.使用设备树方式进行匹配,驱动需要使用到platform_driver.driver.of_device_id来实现,这个是高版本内核中新出现的一种匹配方式(仅做了解)。
注:使用id_table方式进行匹配可以做到多个设备匹配一个驱动的效果,且有id_table就不去匹配name了。

4.2.platform总线模型完善

所谓的platform驱动并不是独立于字符设备驱动、块设备驱动和网络设备驱动之外的其他种类的驱动。
platform只是为了驱动的分离与分层而提出来的一种框架,其驱动的具体实现还是需要字符设备驱动、块设备驱动或网络设备驱动。当驱动与设备匹配成功以后会执行探测函数,当驱动与设备断开连接会执行移除函数。因此以前在驱动加载函数中编写的字符设备驱动程序就全部放在探测函数中,在驱动卸载函数中编写的字符设备驱动程序就全部放在移除函数中。

4.3.在platform驱动中获取设备资源

在探测函数中调用以下函数就可以获取来自platform_device中的设备资源了。

1struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)
功能:通过探测函数中的设备指针获得设备结构中的指定类型的资源结构地址。
这个函数是在驱动层的探测函数使用
参数:
	dev:设备指针,实际就是探测函数参数
	type:资源类型
	num:同类资源进行重新编号后的下标编号,和设备层中的资源数组不相同。(要注意这一点)
返回值:设备层资料结构数组中对应的资源结构首地址
2int platform_get_irq(struct platform_device *dev, unsigned int num)
功能:通过设备指针获得设备结构中的指定编号的中断资源起始编号
参数:
	dev:设备指针,实际就是探测函数参数
	num:同类资源进行重新编号后的下标编号,和设备层中的资源数组不相同。(要注意这一点)
返回值:>0:中断资源中的起始编号;-ENXIO:失败
3struct resource *platform_get_resource_byname(struct platform_device *dev ,
unsigned int type , const char *name)
功能:通过设备指针获得设备结构中的指定名字指定类型的资源结构内存地址
参数:
	dev:设备指针,实际就是探测函数参数
	type:资源类型
	num:资源名
返回值:设备层资源结构数组中对应的资源结构首地址。NULL:失败。
4int platform_get_irq_byname(struct platform_device *dev, const char *name)
功能:通过设备指针获得设备结构中的指定名字的中断资源起始编号
参数:
	dev:设备指针,实际就是探测函数参数
	num:中断资源名
返回值:>0:中断资源中的起始编号;-ENXIO:失败

5.平台数据的使用

5.1.使用platform总线模型问题

在使用platform总线模型的时候,如果只是把原来的代码简单的分离,体现不出上面所说移植性,维护性更优越。真正优秀的平台驱动代码可以做到只要CPU相同,一个驱动程序可以适应任何型号具体开发板,对不同型号开发板只需要修改平台设备层的资源及平台数据即可。
如果要实现个平台驱动层代码可以适应任意数量的led灯,任意IO口位置的效果,如何实现?
利用平台数据(platform_data)传递设备信息即可。

5.2.定义platform_data平台数据结构

分析:要实现以上功能平台设备层必须传递以下两个信息给平台驱动层
1)平台设备层要传递led灯数量
2)每一个led的引脚在本端口的引脚编号
前提:
1)当驱动和设备匹配时候probe函数的参数实际就是设备层的平台设备结构的变量地址,所以在平台设备结构中存放的内容驱动都可以通过参数取得
2)设备层通过platform_device.dev.platform_data成员来向驱动层传递信息
设计思想:
把灯设备看成一个对象:需知道led灯连接在哪个引脚编号上。(如果不在一个组上还需知道在哪一个组)。还需知道总共有几个灯。因此可以把这些内容设计成一个结构体:

typedef struct{
	int *led_pin;	//led灯的引脚编号
	int led_num;	//led灯的数量
}LedInfo;

5.3.platform设备改进地方

1.在平台设备层定义并初始化这个结构体变量

int ledPins[4] = {0,1,2,3};
LedInfo led_data = {
	.led_pin = ledPins,
	.led_num = ARRAY_SIZE(ledPins),
};

2.为平台数据platform_data赋值

struct platform_device led_pdev = {
	.name = "led_platform",
	.dev = {
		.platform_data = (void *)&led_data,
		.release = led_release,
	},
	.num_resources = ARRAY_SIZE(led_resource),
	.resource = led_resource,
};

3.4.platform驱动改进地方

1.定义全局变量:static LedInfo *led_data;
2.在探测函数中,接收来自设备层的平台数据:led_data = pdev->dev.platform_data;
3.在open接口函数中,将led初始化代码进行修改:

int ledDev_open(struct inode *inode, struct file *file)
{
	int i;
	printk("*****%s*****\n",__FUNCTION__);
	led_con = ioremap((int)ledcon_res->start,(int)ledcon_res->end-ledcon_res->start+1);
	led_dat = ioremap((int)leddat_res->start,(int)leddat_res->end-leddat_res->start+1);
	/* led初始化 */
	for(i=0;i<led_data->led_num;i++){
		*led_con |= (1<<4*led_data->led_pin[i]);
	}
	/* 关闭led */
	for(i=0;i<led_data->led_num;i++){
		*led_dat |= (1<<led_data->led_pin[i]);
	}
	return 0;
}

4.在close接口函数中,将关闭LED灯代码进行修改:

int ledDev_close(struct inode *inode, struct file *file)
{
	int i;
	printk("*****%s*****\n",__FUNCTION__);
	/* 熄灭LED灯 */
	for(i=0;i<led_data->led_num;i++){
		*led_dat |= (1<<led_data->led_pin[i]);
	}
	iounmap(led_con);
	iounmap(led_dat);
	return 0;	
}

5.在write接口函数中,做以下修改:

ssize_t ledDev_write(struct file *file, const char __user *buff, size_t count, loff_t *loff)
{
	int i;
	//存放接收来自应用层的数据,数组的每一个元素对应一个LED灯,值为'1'为亮灯,值为'0'为灭灯
	char led_buff[4] = {0};
	printk("*****%s*****\n",__FUNCTION__);
	if(copy_from_user(led_buff, buff, count) != 0){
		printk("copy_from_user error\n");
		return -1;
	}
	printk("led_buff = %s\n",led_buff);
	for(i=0;i<led_data->led_num;i++){
		if(led_buff[i] == '1'){
			*led_dat &= ~(1<<led_data->led_pin[i]);
		}else{
			*led_dat |= 1<<led_data->led_pin[i];
		}
	}
	return 0;	
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值