在设备驱动模型中,引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象,与我们SOC中物理总线的概念并不严格相等。
- 物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
- 驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为他们进行配对。
一般对于I2C
、SPI
、USB
这些常见类型的物理总线来说,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拿到设备信息,但是杂项设备驱动中怎么拿到设备信息暂时未知。