【Linux】平台设备驱动

在设备驱动模型中,引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象,与我们SOC中物理总线的概念并不严格相等。

  • 物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
  • 驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为他们进行配对。

一般对于I2CSPIUSB这些常见类型的物理总线来说,Linux内核会自动创建与之相应的驱动总线,因此I2C设备、SPI设备、USB设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对他们进行控制并不需要特殊的时序。他们也就没有相应的物理总线,比如led、蜂鸣器和按键等,Linux内核将不会为他们创建相应的驱动总线。为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux内核引入了一种虚拟的总线--平台总线(platform_bus)

平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来进行表示,其继承了设备驱动模型中的device结构体。而平台驱动使用platform_driver结构体来进行表示,其则是继承了设备驱动模中的device_driver结构体。

平台设备

platform_device结构体
c 复制代码
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;
     /* 省略部分成员 */
 };
  • name: 设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致;
  • id: 指定设备的编号,Linux支持同名的设备,而同名设备之间则是通过该编号进行区分;
  • dev: Linux设备模型中的device结构体,linux内核大量使用了面向对象思想,platform_device通过继承该结构体可复用它的相关代码,方便内核管理平台设备;
  • num_resources: 记录资源的个数,当结构体成员resource存放的是数组时,需要记录resource数组的个数,内核提供了宏定义ARRAY_SIZE用于计算数组的个数;
  • resource: 平台设备提供给驱动的资源,如irq,dma,内存等等。该结构体会在接下来的内容进行讲解;
  • id_entry: 平台总线提供的另一种匹配方式,原理依然是通过比较字符串,这部分内容会在平台总线小节中讲,这里的id_entry用于保存匹配的结果;
何为设备信息

平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。

  • 硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源和IO口等。
  • 软件信息:以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等。

对于硬件信息,使用结构体struct resource来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下:

c 复制代码
resource结构体(内核源码/include/linux/ioport.h)
/*
* Resources are tree-like, allowing
* nesting etc..
*/

struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    /* 省略部分成员 */
};
  • name: 指定资源的名字,可以设置为NULL;
  • start、end: 指定资源的起始地址以及结束地址
  • flags: 用于指定该资源的类型,在Linux中,资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型,最常见的有以下几种:
资源宏定义 描述
IORESOURCE_IO 用于IO地址空间,对应于IO端口映射方式
IORESOURCE_MEM 用于外设的可直接寻址的地址空间
IORESOURCE_IRQ 用于指定该设备使用某个中断
IORESOURCE_DMA 用于指定使用的DMA通道

设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口,主要有IO端口映射和IO內存映射两种方式。 对应于IO端口映射方式,只能通过专门的接口函数(如inb、outb)才能访问; 采用IO内存映射的方式,可以像访问内存一样,去读写寄存器。在嵌入式中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM。

在资源的起始地址和结束地址中,对于IORESOURCE_IO或者是IORESOURCE_MEM,他们表示要使用的内存的起始位置以及结束位置; 若是只用一个中断引脚或者是一个通道,则它们的start和end成员值必须是相等的。

而对于软件信息,这种特殊信息需要我们以私有数据的形式进行封装保存,我们注意到platform_device结构体中, 有个device结构体类型的成员dev。在前面章节,我们提到过Linux设备模型使用device结构体来抽象物理设备, 该结构体的成员platform_data可用于保存设备的私有数据。platform_data是void *类型的万能指针, 无论你想要提供的是什么内容,只需要把数据的地址赋值给platform_data即可, 还是以GPIO引脚号为例,示例代码如下:

c 复制代码
unsigned int pin = 10;

struct platform_device pdev = {
    .dev = {
        .platform_data = &pin;
    }
}

将保存了GPIO引脚号的变量pin地址赋值给platform_data指针,在驱动程序中通过调用平台设备总线中的核心函数,可以获取到我们需要的引脚号。

注册/注销平台设备
c 复制代码
platform_device_register函数(内核源码/drivers/base/platform.c)
    
int platform_device_register(struct platform_device *pdev)

函数参数和返回值如下:

参数: pdev: platform_device类型结构体指针

返回值:

  • 成功: 0
  • 失败: 负数
c 复制代码
platform_device_unregister函数(内核源码/drivers/base/platform.c)
    
void platform_device_unregister(struct platform_device *pdev)

函数参数和返回值如下:

参数: pdev: platform_device类型结构体指针

返回值:

平台驱动

platform_driver结构体
c 复制代码
platform_driver结构体(内核源码/include/platform_device.h)
    
struct platform_driver {

    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    .......
};
  • probe: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化。
  • remove: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是probe函数实现操作的逆过程。
  • driver: Linux设备模型中用于抽象驱动的device_driver结构体,platform_driver继承该结构体,也就获取了设备模型驱动对象的特性;
  • id_table: 表示该驱动能够兼容的设备类型。

platform_device_id结构体原型如下所示:

c 复制代码
id_table结构体(内核源码/include/linux/mod_devicetable.h)
    
struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];
    kernel_ulong_t driver_data;

};
  • name是数组用于指定驱动的名称,总线进行匹配时,会依据该结构体的name成员与platform_device中的变量name进行比较匹配
  • driver_data则是用于来保存设备的配置。我们知道在同系列的设备中,往往只是某些寄存器的配置不一样,为了减少代码的冗余, 尽量做到一个驱动可以匹配多个设备的目的
注册/注销平台驱动
c 复制代码
platform_driver_register函数

int platform_driver_register(struct platform_driver *drv);

函数参数和返回值如下:

参数: drv: platform_driver类型结构体指针

返回值:

  • 成功: 0
  • 失败: 负数
c 复制代码
platform_driver_unregister函数(内核源码/drivers/base/platform.c)
    
void platform_driver_unregister(struct platform_driver *drv);

参数: drv: platform_driver类型结构体指针

返回值:

平台驱动获取设备信息

platform_get_resource()函数通常会在驱动的probe函数中执行,用于获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针,该函数原型如下:

c 复制代码
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

参数:

  • dev: 指定要获取哪个平台设备的资源;
  • type: 指定获取资源的类型,如IORESOURCE_MEM、IORESOURCE_IO等;
  • num: 指定要获取的资源编号。每个设备所需要资源的个数是不一定的,为此内核对这些资源进行了编号,对于不同的资源,编号之间是相互独立的。

返回值:

  • 成功: struct resource结构体类型指针
  • 失败: NULL

假若资源类型为IORESOURCE_IRQ,平台设备驱动还提供以下函数接口,来获取中断引脚,

c 复制代码
int platform_get_irq(struct platform_device *pdev, unsigned int num)

参数:

  • pdev: 指定要获取哪个平台设备的资源;
  • num: 指定要获取的资源编号。

返回值:

  • 成功: 可用的中断号
  • 失败: 负数

对于存放在device结构体中成员platform_data的软件信息,我们可以使用dev_get_platdata函数来获取,函数原型如下所示:

c 复制代码
static inline void *dev_get_platdata(const struct device *dev)
{
    return dev->platform_data;
}

参数:

  • dev: struct device结构体类型指针

返回值: device结构体中成员platform_data指针

平台总线

平台总线注册和匹配方式

在Linux的设备驱动模型中,总线是最重要的一环。每当有新的设备或者新的驱动加入到总线时,总线便会调用platform_match函数对新增的设备或驱动,进行配对。内核中使用bus_type来抽象描述系统重的总线,平台总线结构体原型如下:

c 复制代码
platform_bus_type结构体(内核源码/driver/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,

};

EXPORT_SYMBOL_GPL(platform_bus_type);

内核用platform_bus_type来描述平台总线,该总线在linux内核启动的时候自动进行注册。这里重点是platform总线的match函数指针,该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线, 它都必须实例化该函数指针。

id_table匹配方式

在定义结构体platform_driver时,我们需要提供一个id_table的数组,该数组说明了当前的驱动能够支持的设备。当加载该驱动时,总线的match函数发现id_table非空, 则会比较id_table中的name成员和平台设备的name成员,若相同,则会返回匹配的条目。

示例

编译一个driver.ko和两个device.ko文件,两个device都是杂项设备,共用主设备号,动态分配不同的次设备号,共用一个驱动。

c 复制代码
haptic_dev0.c

/*
 * @Date: 2024-10-17 16:10:22
 * @LastEditors: zdk
 * @LastEditTime: 2024-10-18 10:50:21
 * @FilePath: \kernel\drivers\haptics\haptic_dev0.c
 */
/*
 *  Silicon Integrated Co., Ltd haptic sih688x haptic driver file
 *
 *  Copyright (c) 2021 heater <daokuan.zhu@si-in.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 as published by
 * the Free Software Foundation
 */

#include <linux/init.h>  //包含宏定义的头文件
#include <linux/module.h>   //包含初始化加载模块的头文件
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/platform_device.h>

//定义的资源
//寄存器地址 和 大小
static struct resource haptic_dev_res[]=
{
	[0]= DEFINE_RES_MEM(0x01,1),
	[1]= DEFINE_RES_MEM(0x02,1)
};

//定义私有数据
//整形数据表示状态 0,1,2等(可自定义)
static int status = 0;

static void haptic_dev_release(struct device* dev)
{
	printk("%s\n",__func__);
}


static struct platform_device haptic_dev=
{
	.name = "haptic_dev0",
	.id = 0,
	.num_resources = ARRAY_SIZE(haptic_dev_res),
	.resource = haptic_dev_res,
	.dev=
	{
		.platform_data = &status,
		.release = haptic_dev_release,
	},
};


static int __init haptic_dev_init(void)
{
	int ret = 0;
	//内核层只能使用printk,不能使用printf
    	printk(KERN_EMERG "%s\n",__FUNCTION__); 

	ret = platform_device_register(&haptic_dev);
	
    	return ret;
}

static void __exit haptic_dev_exit(void)
{
	platform_device_unregister(&haptic_dev);
	printk(KERN_EMERG "%s\n",__FUNCTION__); 
}

module_init(haptic_dev_init);//模块入口
module_exit(haptic_dev_exit);//模块出口

MODULE_AUTHOR("<daokuan.zhug@si-in.com>");//声明作者信息
MODULE_DESCRIPTION("Haptics Device V1.0.0"); //对这个模块作一个简单的描述
MODULE_LICENSE("GPL v2");//声明开源许可证
			 // "GPL" 是指明 这是GNU General Public License的任意版本
                         // "GPL v2" 是指明 这仅声明为GPL的第二版本
c 复制代码
haptic_dev1.c

/*
 * @Date: 2024-10-17 16:10:22
 * @LastEditors: zdk
 * @LastEditTime: 2024-10-18 10:50:28
 * @FilePath: \kernel\drivers\haptics\haptic_dev1.c
 */
/*
 *  Silicon Integrated Co., Ltd haptic sih688x haptic driver file
 *
 *  Copyright (c) 2021 heater <daokuan.zhu@si-in.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 as published by
 * the Free Software Foundation
 */

#include <linux/init.h>  //包含宏定义的头文件
#include <linux/module.h>   //包含初始化加载模块的头文件
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/platform_device.h>

//定义的资源
//寄存器地址 和 大小
static struct resource haptic_dev_res[]=
{
	[0]= DEFINE_RES_MEM(0x03,1),
	[1]= DEFINE_RES_MEM(0x04,1)
};

//定义私有数据
//整形数据表示状态 0,1,2等(可自定义)
static int status = 1;

static void haptic_dev_release(struct device* dev)
{
	printk("%s\n",__func__);
}


static struct platform_device haptic_dev=
{
	.name = "haptic_dev1",
	.id = 1,
	.num_resources = ARRAY_SIZE(haptic_dev_res),
	.resource = haptic_dev_res,
	.dev=
	{
		.platform_data = &status,
		.release = haptic_dev_release,
	},
};


static int __init haptic_dev_init(void)
{
	int ret = 0;
	//内核层只能使用printk,不能使用printf
    	printk(KERN_EMERG "%s\n",__FUNCTION__); 

	ret = platform_device_register(&haptic_dev);
	
    	return ret;
}

static void __exit haptic_dev_exit(void)
{
	platform_device_unregister(&haptic_dev);
	printk(KERN_EMERG "%s\n",__FUNCTION__); 
}

module_init(haptic_dev_init);//模块入口
module_exit(haptic_dev_exit);//模块出口

MODULE_AUTHOR("<daokuan.zhug@si-in.com>");//声明作者信息
MODULE_DESCRIPTION("Haptics Device V1.0.0"); //对这个模块作一个简单的描述
MODULE_LICENSE("GPL v2");//声明开源许可证
			 // "GPL" 是指明 这是GNU General Public License的任意版本
                         // "GPL v2" 是指明 这仅声明为GPL的第二版本
c 复制代码
haptic_drv.c

/*
 * @Date: 2024-10-17 16:08:08
 * @LastEditors: zdk
 * @LastEditTime: 2024-10-18 17:18:05
 * @FilePath: \kernel\drivers\haptics\haptic_drv.c
 */

#include <linux/init.h>  //包含宏定义的头文件
#include <linux/module.h>   //包含初始化加载模块的头文件
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include "haptic_ioctl.h"


//打开设备
static int haptics_open(struct inode* inode,struct file * filp)
{	
	//驱动最好不使用全局变量,所以在probe函数中获取的资源,该如何在这里拿到呢???
	//使用字符设备的时候 可以使用container_of结合inode->i_cdev拿到
	//但是杂项设备该怎么办呢?
	printk("%s minor=%d\n",__FUNCTION__, MINOR(inode->i_rdev));
	return 0;
}

//关闭设备
static int haptics_release(struct inode* inode ,struct file* filp)
{
	printk("%s\n",__FUNCTION__);
	return 0;
}

//ioctl
static long haptics_ioctl(struct file * filp, unsigned int cmd, unsigned long arg)
{
	int ret = 0;
	ioctl_protocol_t msg;
	//反解cmd中的字段
	int type = _IOC_TYPE(cmd);
        int dir = _IOC_DIR(cmd);
	int nr = _IOC_NR(cmd);
	int size = _IOC_SIZE(cmd);
        printk("dir=%d size=%d\n",dir,size);
	//检验cmd是否正确
	//1.校验cmd_type
	if(DEVICE_TYPE != type)
	{
		printk(KERN_ERR "cmd type error\n");
		ret =-1;
		return ret;
	}

	if(HAPTICS_READ_REG == cmd)//读寄存器
	{
		//校验nr
		if(HAPTICS_READ_REG_NR == nr)
		{
			 ret = copy_from_user(&msg, (ioctl_protocol_t __user *)arg, sizeof(ioctl_protocol_t));
			 printk("read_reg:%#02x\n",msg.reg_addr);
			 msg.reg_value=0xff;//这里模拟读寄存器的值

			 //将读取到的值传给用户空间
			 ret = copy_to_user((ioctl_protocol_t __user *)arg, &msg, sizeof(ioctl_protocol_t));
		}

	}else if(HAPTCIS_WRITE_REG == cmd)//写寄存器
	{
		//校验nr
		if(HAPTICS_WRITE_REG_NR == nr)
		{
			 ret = copy_from_user(&msg, (ioctl_protocol_t __user *)arg, sizeof(ioctl_protocol_t));
			 printk("write_reg:%#02x=%#02x\n",msg.reg_addr,msg.reg_value);//模拟写寄存器的值
		}
	}else//
	{
		printk(KERN_ERR "unknown cmd\n");
	}

	return ret;
}

typedef struct
{
	struct miscdevice miscdev;//定义一个杂项设备结构体
	struct resource* res;
	int status;
}haptic_miscdev_t;


static int haptic_drv_probe(struct platform_device *pdev)
{
	int ret = 0;
	int* status=NULL;
	struct resource* res0=NULL;
	struct resource* res1=NULL;
	haptic_miscdev_t *hap_miscdev=NULL;
	struct file_operations *haptics_fops=NULL;

	res0 = platform_get_resource(pdev,IORESOURCE_MEM,0);
	res1 = platform_get_resource(pdev,IORESOURCE_MEM,1);
	printk("res0 start=%d size=%d\n",(int)res0->start,(int)(res0->end-res0->start+1));
	printk("res1 start=%d size=%d\n",(int)res1->start,(int)(res1->end-res1->start+1));

	status = dev_get_platdata(&pdev->dev);
	printk("status=%d\n",*status);
	
	haptics_fops = devm_kzalloc(&pdev->dev,sizeof(struct file_operations), GFP_KERNEL);
	haptics_fops->open = haptics_open;
	haptics_fops->release = haptics_release;
	haptics_fops->unlocked_ioctl = haptics_ioctl;

	hap_miscdev = devm_kzalloc(&pdev->dev,sizeof(haptic_miscdev_t), GFP_KERNEL);
	hap_miscdev->res = res0;
	hap_miscdev->status = *status;
	hap_miscdev->miscdev.name = pdev->name;
        hap_miscdev->miscdev.fops = haptics_fops;
	hap_miscdev->miscdev.minor = MISC_DYNAMIC_MINOR,

	ret = misc_register(&hap_miscdev->miscdev);

	/* save as drvdata */
    	//platform_set_drvdata函数,将设备数据信息存入在平台驱动结构体中pdev->dev->driver_data中
    	platform_set_drvdata(pdev, hap_miscdev);
	return ret;
}

static int haptic_drv_remove(struct platform_device *pdev)
{
	int ret = 0;

 	haptic_miscdev_t *hap_miscdev = platform_get_drvdata(pdev);
	misc_deregister(&hap_miscdev->miscdev);

	return ret;
}

static struct platform_device_id haptic_dev_id[] =
{
	{.name = "haptic_dev0"},
	{.name = "haptic_dev1"},
	{}
};

static struct platform_driver haptic_drv=
{
	.driver.name = "haptics_drv",
	.probe = haptic_drv_probe,
	.remove = haptic_drv_remove,
	.id_table = haptic_dev_id,
};


static int __init haptic_drv_init(void)
{
	int ret = 0;
	//内核层只能使用printk,不能使用printf
    	printk(KERN_EMERG "%s\n",__FUNCTION__); 

	ret = platform_driver_register(&haptic_drv);
    	return ret;
}

static void __exit haptic_drv_exit(void)
{
	platform_driver_unregister(&haptic_drv);
	printk(KERN_EMERG "%s\n",__FUNCTION__); 
}

module_init(haptic_drv_init);//模块入口
module_exit(haptic_drv_exit);//模块出口

MODULE_AUTHOR("<daokuan.zhug@si-in.com>");//声明作者信息
MODULE_DESCRIPTION("Haptics Device V1.0.0"); //对这个模块作一个简单的描述
MODULE_LICENSE("GPL v2");//声明开源许可证
			 // "GPL" 是指明 这是GNU General Public License的任意版本
                         // "GPL v2" 是指明 这仅声明为GPL的第二版本
c 复制代码
haptic_ioctl.h

#ifndef __HAPTCIS_IOCTL_H__
#define __HAPTCIS_IOCTL_H__

/*
 * @Date: 2024-10-12 15:53:37
 * @LastEditors: zdk
 * @LastEditTime: 2024-10-18 17:22:18
 * @FilePath: \kernel\drivers\haptics\haptic_ioctl.h
 */


#include <linux/ioctl.h>
// #include <sys/ioctl.h>   // 用户空间

/*这里使用ioctl定义两个协议,读寄存器和写寄存器
*用户空间和内核空间共用的头文件,包含ioctl命令及相关宏定义,可以理解为一份"协议"文件
*/
//cmd中的type
#define DEVICE_TYPE           'H'
#define HAPTICS_READ_REG_NR   (0)
#define HAPTICS_WRITE_REG_NR  (1)
#define HAPTICS_READ_REG      _IO(DEVICE_TYPE,HAPTICS_READ_REG_NR)  
#define HAPTCIS_WRITE_REG     _IO(DEVICE_TYPE,HAPTICS_WRITE_REG_NR)

typedef struct
{
	uint8_t reg_addr;
	uint8_t reg_value;
}ioctl_protocol_t;

#endif

再来看看sys目录下

总结

  • 驱动代码中最好不要使用全局变量,因为驱动一般是支持多设备的,如果有全局变量会有冲突。

  • 字符设备驱动中在open函数中可以使用container_of拿到设备信息,但是杂项设备驱动中怎么拿到设备信息暂时未知。

相关推荐
羑悻的小杀马特8 分钟前
环境变量简介
linux
小陈phd41 分钟前
Vscode LinuxC++环境配置
linux·c++·vscode
是阿建吖!1 小时前
【Linux】进程状态
linux·运维
明明跟你说过1 小时前
Linux中的【tcpdump】:深入介绍与实战使用
linux·运维·测试工具·tcpdump
Komorebi.py2 小时前
【Linux】-学习笔记05
linux·笔记·学习
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
内核程序员kevin5 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ10 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream10 小时前
Linux的桌面
linux
xiaozhiwise10 小时前
Makefile 之 自动化变量
linux