【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拿到设备信息,但是杂项设备驱动中怎么拿到设备信息暂时未知。

相关推荐
FoldWinCard3 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计5 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud1235 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质5 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX5 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.5 小时前
Linux系统管理
linux·运维
阿龙的工作记录6 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手6 小时前
Linux的基本指令
linux·运维·服务器
hkj88087 小时前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动8 小时前
Linux编程笔记3【个人用】
linux·笔记