BSP开发学习2平台设备驱动

Posted 与光同程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BSP开发学习2平台设备驱动相关的知识,希望对你有一定的参考价值。

文章目录

Linux设备驱动模型

Linux 中的设备驱动模型组成

(1)类class、总线bus、设备device、驱动driver
(2)kobject和对象生命周期
(3)sysfs
(4)udev

为什么要使用设备驱动模型

我对于Linux 引入设备驱动模型的理解是就在于将一份驱动代码分成两份,一份代码是通用的也就是驱动driver ,令一份代码不是通用的会随着板子CPU的不同,发生改变。

设备驱动模型的底层架构

sysfs 目录

通过sys文件系统下面的目录和文件可以清楚的了解到Linux系统中的设备情况和组织关系。

sysfs提供一种可以显式描述内核对象,对象属性以及对象关系的方法。

sysfs在内核空间的组成要素在用户空间的体现
kobject目录
attribute文件
relationship链接

kobject

树形结构中每一个目录与一个kobject对象相对应,其包含了目录的组织结构和名字等信息。在Linux系统中, kobject结构体是组成设备驱动模型的基本结构。

(1)kobject提供了最基本的设备对象管理能力,每一个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录,而不是文件。

(2)各种对象最基本单元,提供一些公用型服务如:对象引用计数、维护对象链表、对象上锁、对用户空间的表示
(3)设备驱动模型中的各种对象其内部都会包含一个kobject
(4)地位相当于面向对象体系架构中的总基类

struct kobject 
 
const char		*name;//kobject的名字,且作为一个目录的名字
struct list_head	entry;//连接下一个kobject结构
struct kobject		*parent;//指向父亲kobject结构体,如果父设备存在
struct kset		*kset;  //指向kset集合
struct kobj_type	*ktype;  //指向kobject的属性描述符
struct sysfs_dirent	*sd;     //对应sysfs的文件目录
struct kref		kref;   //kobject的引用计数
unsigned int state_initialized:1; //表示该kobject是否初始化
unsigned int state_in_sysfs:1;   //表示是否加入sysfs中
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
;

kobj_type

使用该kobject设备的共同属性
(1)很多书中简称为ktype,每一个kobject都需要绑定一个ktype来提供相应功能
(2)关键点1:sysfs_ops,提供该对象在sysfs中的操作方法(show和store)
(2)关键点2:attribute,提供在sysfs中以文件形式存在的属性,其实就是应用接口


struct kobj_type 
	void (*release)(struct kobject *kobj);//释放kobject和其占用的函数
	const struct sysfs_ops *sysfs_ops;  //操作一个属性数组的方法
	struct attribute **default_attrs;  //属性数组的方法
	const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
	const void *(*namespace)(struct kobject *kobj);
;

kset

kset是具有相同类型的kobject的集合

kset包含了一个kobject,其实它相当于一个链表节点,虽然Kobject包含了kset元素
(1)kset的主要作用是做顶层kobject的容器类
(2)kset的主要目的是将各个kobject(代表着各个对象)组织出目录层次架构
(3)可以认为kset就是为了在sysfs中弄出目录,从而让设备驱动模型中的多个对象能够有层次有逻辑性的组织在一起

struct kset 
	struct list_head list;  //连接链表
	spinlock_t list_lock;  //链表的自旋锁
	struct kobject kobj;  //内嵌的kobject结构体,说明kset本身也是一个目录
	const struct kset_uevent_ops *uevent_ops;  //热插拔事件
;

设备驱动模型三大组件

不管是平台总线还是IIC总线都都有这样的调用路线:

当系统发现了新设备或者新驱动就会掉用相应总线的Match()进行匹配,当找到后就会掉用相对应的总线的Probe函数,最后Probe函数再调用驱动自己的Probe函数

总线

struct bus_type 
 
const char		*name;  //总线类型名
struct bus_attribute	*bus_attrs;  //总线属性和导出到sysfs中的方法
struct device_attribute	*dev_attrs;  //设备属性和导出到sysfs中的方法
struct driver_attribute	*drv_attrs;  //驱动程序属性和导出到sysfs中的方法
 
//匹配函数,检验参数2中的驱动是否支持参数1中的设备
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);  //探测设备
int (*remove)(struct device *dev); //移除设备
void (*shutdown)(struct device *dev); //关闭函数
 
int (*suspend)(struct device *dev, pm_message_t state);//改变设备供电状态,使其节能
int (*resume)(struct device *dev);  //恢复供电状态,使其正常工作
 
const struct dev_pm_ops *pm;  //关于电源管理的操作符
 
struct bus_type_private *p;  //总线的私有数据
;

设备

在Linux设备驱动模型中,每一个设备都由一个device结构体来描述。device结构体包含了设备所具有的一些通用信息。对于驱动开发人员来说,当遇到新设备时,需要定义一个新的设备结构体,将device作为新结构体的成员。这样就可以在新结构体中定义新设备的一些信息,而设备通用的信息就使用device结构体来表示。使用device结构体的另一个好处是,可以通过device轻松地将新设备加入设备驱动模型的管理中。

device中的大多数函数被内核使用,驱动开发人员不用关注

(1)struct device是硬件设备在内核驱动框架中的抽象
(2)device_register用于向内核驱动框架注册一个设备
(3)通常device不会单独使用,而是被包含在一个具体设备结构体中,如struct usb_device

struct device 
 
struct klist_klist children;/*连接子设备的链表*/
struct device *parent;/*指向父设备的指针*/
struct kobject kobj;/*内嵌的kobject结构体*/
char bus_id[BUS ID SIZE];/*连接到总线上的位置*/ 
unsigned uevent suppress:1;/*是否支持热插拔事件*/
const char init_name;/*设备的初始化名字*/
struct device_type *type;/*设备相关的特殊处理函数*/
struct bus_type *bus;/*指向连接的总线指针*/
struct device_driver *driver;/*指向该设备的驱动程序*/
void *driver data;/*指向驱动程序私有数据的指针*/
struct dev_pm info power;/*电源管理信息*/ 
dev t deyt;/*设备号*/
struct class *class;/*指向设备所属类*/ 
struct attribute_group **groups;/*设备的组属性*/ 
void (*release) (struct device *dev);/*释放设备描述符的回调函数*/

驱动

(1)struct device_driver是驱动程序在内核驱动框架中的抽象
(2)关键元素1:name,驱动程序的名字,很重要,经常被用来作为驱动和设备的匹配依据
(3)关键元素2:probe,驱动程序的探测函数,用来检测一个设备是否可以被该驱动所管理

struct device_driver 
	const char		*name;//设备驱动程序的名字
	struct bus_type		*bus;//指向驱动属于的总线
 
	struct module		*owner;//设备驱动自身模块
	const char		*mod_name;	/* used for built-in modules */
 
	bool suppress_bind_attrs;	/* disables bind/unbind via sysfs */
 
#if defined(CONFIG_OF)
	const struct of_device_id	*of_match_table;
#endif
 
	int (*probe) (struct device *dev);//探测设备的方法,并检测设备驱动可以控制哪些设备
	int (*remove) (struct device *dev);//移除设备调用的方法
	void (*shutdown) (struct device *dev);//关闭设备的方法
	int (*suspend) (struct device *dev, pm_message_t state);//设备处于低功耗的方法
	int (*resume) (struct device *dev);//恢复正常的方法
	const struct attribute_group **groups;//属性组
 
	const struct dev_pm_ops *pm;//电源管理
 
	struct driver_private *p;//设备驱动私有数据
;

平台设备驱动

概述

并不所有设备都会有实体总线,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动称为platform_driver。

系统为platform总线定义了一个bus_type的实例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_device和platform_driver之间是如何进行匹配
有4种可能性,
一是基于设备树风格的匹配;
二是基于ACPI风格的匹配;
三是匹配ID表(即platform_device设备名是否出现在platform_driver的ID表内);
第四种是匹配platform_device设备名和驱动的名字。

由以上分析可知,在设备驱动中引入platform的概念至少有如下好处。
1)使得设备被挂接在一个总线上,符合Linux 2.6以后内核的设备模型。其结果是使配套的sysfs节点、设备电源管理都成为可能。
2)隔离BSP和驱动。在BSP中定义platform设备和设备使用的资源、设备的具体配置信息,而在驱动中,只需要通过通用API去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
3)让一个驱动支持多个设备实例。譬如DM9000的驱动只有一份,但是我们可以在板级添加多份DM9000的platform_device,它们都可以与唯一的驱动匹配

平台设备驱动工作原理

核心变量与函数

(1)platform工作体系都定义在drivers/base/platform.c中
(2)两个结构体:platform_device和platform_driver
(3)两个接口函数:platform_device_register和platform_driver_register

struct platform_device 
    const char    * name;            // 平台总线下设备的名字
    int        id;    //设备名加ID名就得到了设备文件文件名
    struct device    dev;        // 所有设备通用的属性部分
    u32        num_resources;        // 设备使用到的resource的个数
    struct resource    * resource;    // 设备使用到的资源数组的首地址

    const struct platform_device_id    *id_entry;    // 设备ID表

    /* arch specific additions */
    struct pdev_archdata    archdata;            // 自留地,用来提供扩展性的
;

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;    // 设备ID表
;

在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。

工作流程

(1)第一步:系统启动时在bus系统中注册platform
(2)第二步:内核移植的人负责提供platform_device
(3)第三步:写驱动的人负责提供platform_driver
(4)第四步:platform的match函数发现driver和device匹配后,调用driver的probe函数来完成驱动的初始化和安装,然后设备就工作起来了

(1)platdata其实就是设备注册时提供的设备有关的一些数据(譬如设备对应的gpio、使用到的中断号、设备名称····)
(2)这些数据在设备和驱动match之后,会由设备方转给驱动方。驱动拿到这些数据后,通过这些数据得知设备的具体信息,然后来操作设备。
(3)这样做的好处是:驱动源码中不携带数据,只负责算法(对硬件的操作方法)。现代驱动设计理念就是算法和数据分离,这样最大程度保持驱动的独立性和适应性。

不使用设备树的平台设备驱动

使用平台设备API只需要引用下面的头文件
include/linux/platform_device.h

platform_driver

1 struct platform_driver 
2 int (*probe)(struct platform_device *);
3 int (*remove)(struct platform_device *);
4 void (*shutdown)(struct platform_device *);
5 int (*suspend)(struct platform_device *, pm_message_t state);
6 int (*resume)(struct platform_device *);
7 struct device_driver driver;
8 const struct platform_device_id *id_table;
9 bool prevent_deferred_probe;
10 ;

当我们定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用
platform_driver_register 函数向 Linux 内核注册一个 platform 驱动,platform_driver_register 函数
原型如下所示:

int platform_driver_register (struct platform_driver *driver)
函数参数和返回值含义如下:
driver:要注册的 platform 驱动。
返回值:负数,失败;0,成功。

还需要在驱动卸载函数中通过 platform_driver_unregister 函数卸载 platform 驱动,
platform_driver_unregister 函数原型如下:

void platform_driver_unregister(struct platform_driver *drv)
函数参数和返回值含义如下:
drv:要卸载的 platform 驱动。

platform_device

22 struct platform_device 
23 const char *name;
24 int id;
25 bool id_auto;
26 struct device dev;
27 u32 num_resources;
28 struct resource *resource;
29
30 const struct platform_device_id *id_entry;
31 char *driver_override; /* Driver name to force a match */
32
33 /* MFD cell pointer */
34 struct mfd_cell *mfd_cell;
35
36 /* arch specific additions */
37 struct pdev_archdata archdata;
38 ;

include/linux/ioport.h
18 struct resource 
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 struct resource *parent, *sibling, *child;
24 ;

在以前不支持设备树的Linux版本中,用户需要编写platform_device变量来描述设备信息,
然后使用 platform_device_register 函数将设备信息注册到 Linux 内核中,此函数原型如下所示:

int platform_device_register(struct platform_device *pdev)
函数参数和返回值含义如下:
pdev:要注册的 platform 设备。
返回值:负数,失败;0,成功。
如果不再使用 platform 的话可以通过 platform_device_unregister 函数注销掉相应的 platform
设备,platform_device_unregister 函数原型如下:
void platform_device_unregister(struct platform_device *pdev)
函数参数和返回值含义如下:
pdev:要注销的 platform 设备。
返回值:无。
platform 设备信息框架如下所示:

平台设备驱动代码编写

该代码直接在globalmem驱动程序上修改 其实就上将之前在init 函数中做的工作转移到probe 函数 ,exit 函数中所作的工作转移到remove函数中,同时添加从设备驱动中获取参数的代码。

/*
 * @Copyright: 
 * @FileNames: 
 * @Description: 申请全局内存4096 并使用该内存进行 用户和内核之间的数据交换
 * @Author: 
 * @Date: 2022-07-29 09:03:02
 * @Version: V1.0
 * @LastEditTime: 2022-08-01 15:05:34
 */
#include <linux/module.h>//MOUDLE_XXX moudle_xxx
#include <linux/fs.h>    //file_operations register_chrdev_region alloc_chrdev_region
#include <linux/init.h>  //__init __exit
#include <linux/cdev.h>  //cdev
#include <linux/uaccess.h>//copy_to_user copy_from_user 
#include <linux/device.h> // device class 
#include <linux/platform_device.h>

#define GLOBAL_MEM_SIZE 0X1000
#define GLOBAL_MEM_DEBUG 1

#define GLOBAL_MEM_CLASS_NAME "global_mem" //创建的类名称
#define GLOBAL_MEM_NODE_NAME  "global_mem_0" //创建的节点名称


#define GLOBAL_MEM_MAJOR 250

//CMD
#define GLOBAL_MEM_CLEAR 0X01 //清理内存 命令


typedef struct 
    //设备驱动变量
    struct cdev cdev;
    //设备号变量
    dev_t devid;
    int major;
    int minor;      
    //设备节点相关变量
    struct class *class; 
    struct device *device;
    //用户变量
    unsigned char mem[GLOBAL_MEM_SIZE];

global_mem_t;

global_mem_t global_mem;

/**
 * @function: global_mem_open
 * @description: 打开设备 并将内存清零
 * @input: 
 * @output: 
 * @return *
 * @param inode *inode
 * @param file *flip
 */
static int global_mem_open(struct inode *inode ,struct file *flip)

    printk("OPEN GLOBAL MEM\\r\\n");
    flip->private_data=(void *)(&global_mem);
    return 0;

/**
 * @function: global_mem_release
 * @description: 释放设备节点
 * @input: 
 * @output: 
 * @return *
 * @param inode *inode
 * @param file *flip
 */
static int global_mem_release(struct inode *inode ,struct file *flip)

    printk("RELEASE GLOBAL MEM\\r\\n");
    return 0;


/**
 * @function: global_mem_read
 * @description: 拷贝数据到用户区
 * @input: 
 * @output: 
 * @return *
 * @param file* flip  文件结构体
 * @param char __user *buf 从用户区拷贝出来的数据
 * @param size_t size      传入数据大小
 * @param loff_t *ppos     当前数据位置
 */
static ssize_t global_mem_read(struct file* flip, char __user *buf,size_t size ,loff_t *ppos)

    int ret=0;
    printk("READ GLOBAL MEM\\r\\n");
    
    global_mem_t* dev=(global_mem_t*) flip->private_data;
    //当前文件指针所处于的位置
    unsigned long p=*ppos;
    //需要读出的个数
    unsigned int count=(unsigned int)size;
    //检查读取位置合法性
    if(p>=GLOBAL_MEM_SIZE)return 0;
    if(count+p>GLOBAL_MEM_SIZE)count=GLOBAL_MEM_SIZE-p;
    //拷贝数据到用户区
    ret=copy_to_user(buf,dev->mem+p,count);
    if (ret<0)
        ret=-EFAULT;
    else
        *ppos+=count;
        ret=count;
        if(GLOBAL_MEM_DEBUG)printk("READ %d BYTES FROM KERNEL At %d\\n",count,p);
    
    return ret;


/**
 * @function: global_mem_write
 * @description: 
 * @input: 
 * @output: 
 * @return *
 * @param file* flip
 * @param char __user *buf
 * @param size_t size
 * @param loff_t *ppos
 */
static ssize_t global_mem_write(struct file* flip, const char __user *buf,size_t size ,loff_t *ppos)

    int ret=0;
    printk("WRITE GLOBAL MEM\\r\\n");
    
    global_mem_t* dev=(global_mem_t*) flip->private_data;
    //当前文件指针所处于的位置
    unsigned long p=*ppos;
    //需要读出的个数
    unsigned int count=(unsigned int)size;
    //检查读取位置合法性
    if(p>=GLOBAL_MEM_SIZE)return 0;
    if(count+p>GLOBAL_MEM_SIZE)count=GLOBAL_MEM_SIZE-p;
    ret=copy_from_user(dev->mem+p,buf,count);
    if(ret<0)ret=-EFAULT;
    else
        *ppos+=count;
        ret=count;
        if(GLOBAL_MEM_DEBUG)printk("WRITE %d BYTES TO KERNEL AT %d\\n",(int

以上是关于BSP开发学习2平台设备驱动的主要内容,如果未能解决你的问题,请参考以下文章

bsp开发之驱动开发

BSP开发学习1通用字符设备开发

BSP开发学习1通用字符设备开发

单片机是bsp驱动吗

RK3399平台开发系列讲解(USB设备驱动)5.43USB BC1.2充电协议学习

SylixOS在x86平台的快速构建