最全Linux驱动开发八股文(十五)

你好,我是拉依达。

这是我的Linux驱动开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。

全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料

现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询

———————————————————————————————————————————————————

十五、Platform设备驱动框架

15.1 设备驱动的分层思想

Linux 内核完全由 C 语言和汇编语言写成, 但是却频繁用到了面向对象的设计思想

设备驱动方面,为同类的设备设计了一个框架框架中的核心层则实现了该设备通用的一些功能。同样的, 如果具体的设备不想使用核心层的函数, 它可以重载之。

return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
    if(bottom_dev->funca)
        return bottom_dev->funca(param1, param2);
    
    /* 核心层通用的 funca 代码 */
  
    bottom_dev->funca_ops1(); 	/*通用的步骤代码 A */
    ...
    bottom_dev->funca_ops2();	/*通用的步骤代码 B */   
    ...
    bottom_dev->funca_ops3();	/*通用的步骤代码 C */
}

上述 core_funca 的实现中, 会检查底层设备是否重载了 funca(), 如果重载了, 就调用底层的代码, 否则直接使用通用层的。 这样做的好处是, 核心层的代码可以处理绝大多数该类设备的funca()对应的功能,只有少数特殊设备需要重新实现 funca()。

15.2 驱动的分离与分层

设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息 (比如从设备树中获取到设备信息),根据获取到的设备信息来初始化设备。

驱动只负责驱动,设备只负责设备,总线法将两者进行匹配

这就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离

alt

当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有,就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动中查找与之匹配的驱动,如果有,也联系起来。 Linux 内核中大量的驱动程序都采用总线、驱动和设备模式。

15.3 Platform 平台总线驱动模型

在 Linux 2.6 以后的设备驱动模型中, 需关心总线、 设备和驱动这 3 个实体, 总线将设备和驱动绑定

Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等的设备而言, 这自然不是问题。但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。

基于这一背景, Linux 发明了一种虚拟的总线称为 platform 总线, 相应的设备称为 platform_device, 驱动称为 platform_driver。 平台总线模型就是把原来的驱动C文件给分成了俩个 C 文件,一个是 device.c(描述硬件信息,设备树可替代), 一个是 driver.c(驱动信息)

alt

一、platform 驱动

在 Linux 内核中, 用platform_driver结构体表示platform驱动,platform_driver 结构体定义指定名称平台设备驱动注册函数平台设备驱动注销函数

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;
    bool prevent_deferred_probe;
};
  • probe 函数: 当驱动与设备匹配成功以后 probe 函数就会执行, 一般驱动的提供者会编写
  • remove函数:当 driver 和 device 任意一个 remove 的时, 就会执行该函数
  • driver: device_driver 结构体变量, Linux 内核里面大量使用到了面向对象的思维, device_driver相当于基类,提供了最基础的驱动框架。 plaform_driver 继承了这个基类,在此基础上又添加了一些特有的成员变量
  • id_table 表保存 id 信息。 这些 id 信息存放着platformd 驱动所支持的驱动类型。 id_table是个表(数组), 每个元素的类型为 platform_device_id,

platform_device_id 结构体内容如下:

struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];
    kernel_ulong_t driver_data;
};

device_driver 结构体定义内容如下:

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 */
 
    const struct of_device_id *of_match_table; //设备树的驱动匹配表
    const struct acpi_device_id *acpi_match_table;
    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;
};

of_match_table表就是采用设备树驱动使用的匹配表,也是数组,每个匹配项都为 of_device_id 结构体类型

struct of_device_id {
    char name[32];
    char type[32];
    char compatible[128];
    const void *data;
};

compatible: 在支持设备树的内核中, 就是通过设备节点的compatible属性值of_match_table中每个项目的 compatible 成员变量进行比较, 如果有相等的就表示设备和此驱动匹配成功

驱动和设备匹配成功后驱动会从设备里面获得硬件资源, 匹配成功了后, driver.c 要从 device.c(或者是设备树) 中获得硬件资源, 那么 driver.c 就是在 probe 函数中获得的。

二、platform 设备(可以被设备树替代)

在 platform 平台下用platform_device结构体表示platform设备, 如果内核支持设备树的话就不用使用 platform_device 来描述设备, 使用设备树去描述platform_device即可。

struct platform_device {
    const char *name;
    int id;
    bool id_auto;
    struct device dev;
    u32 num_resources;
    struct resource *resource;
 
    const struct platform_device_id *id_entry;
    char *driver_override; /* Driver name to force a match */
 
    /* MFD cell pointer */
    struct mfd_cell *mfd_cell;
 
    /* arch specific additions */
    struct pdev_archdata archdata;
};
  • name:设备名字,要和所使用的 platform 驱动的 name 字段相同,否则设备就无法匹配到对应的驱动。比如对应的 platform 驱动的name字段为xxx-gpio,则此name字段也要设置为xxx-gpio。
  • id :用来区分如果设备名字相同的时,通过在后面添加一个数字来代表不同的设备
  • dev:内置的device结构体
  • num_resources :资源数量,一般为resource 资源的大小(个数),ARRAY_SIZE 来测量一个数组的元素个数。
  • resource:指向一个资源结构体数组,即设备信息,比如外设寄存器等。

Linux 内核使用 resource结构体表示资源

struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    struct resource *parent, *sibling, *child;
};

start 和 end 分别表示资源的起始和终止信息,对于内存类的资源,表示内存起始和终止地址, name 表示资源名字, flags 表示资源类型

使用 platform_device_register 函数设备信息注册到 Linux 内核中

int platform_device_register(struct platform_device *pdev)
  • pdev:要注册的 platform 设备
  • 返回值: 负数,失败; 0,成功

如果不再使用 platform可以通过 platform_device_unregister 函数注销掉相应的 platform设备

void platform_device_unregister(struct platform_device *pdev)
  • pdev:要注销的 platform 设备
  • 返回值: 无

三、platform 总线

platform设备和platform驱动,相当于把设备和驱动分离了, 需要 platform 总线进行配, platform 设备和 platform 驱动进行内核注册时, 都是注册到总线上。

在 Linux 内核中使用 bus_type 结构体表示总线

struct bus_type {
    const char *name; /* 总线名字 */
    const char *dev_name;
    struct device *dev_root;
    struct device_attribute *dev_attrs;
    const struct attribute_group **bus_groups; /* 总线属性 */
    const struct attribute_group **dev_groups; /* 设备属性 */
    const struct attribute_group **drv_groups; /* 驱动属性 */
 
    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 (*online)(struct device *dev);
    int (*offline)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    const struct dev_pm_ops *pm;
    const struct iommu_ops *iommu_ops;
    struct subsys_private *p;
    struct lock_class_key lock_key;
};

match 函数:完成设备和驱动之间匹配的,总线使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。

match 函数有两个参数dev 和 drv,这两个参数分别为 device 和 device_driver 类型,即设备和驱动

platform 总线是 bus_type 的一个具体实例

struct bus_type platform_bus_type = {
    .name = "platform",
    .dev_groups = platform_dev_groups,
    .match = platform_match,
    .uevent = platform_uevent,
    .pm = &platform_dev_pm_ops,
};

platform_match 匹配函数, 用来匹配注册到 platform 总线的设备和驱动。

四、platform 总线具体匹配方法

查看platform_match函数,如何匹配驱动和设备的

static int platform_match(struct device *dev,struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);
 
    /*When driver_override is set,only bind to the matching driver*/
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);
 
    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;
 
    /* Then try ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;
    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;
 
    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);
}

驱动和设备的匹配有四种方法

  1. OF 类型的匹配 设备树采用的匹配方式,of_driver_match_device 函数定义在文件 include/linux/of_device.h 中。

    device_driver 结构体(设备驱动)中有个名为of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表

    设备树中的每个设备节点的 compatible 属性会和 of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行

  2. ACPI 匹配方式

  3. id_table 匹配 每个 platform_driver 结构体(设备驱动)有一个 id_table成员变量,保存了很多 id 信息。这些 id 信息存放着这个 platformd 驱动所支持的驱动类型

  4. 名字匹配 如果第三种匹配方式的 id_table 不存在的话就直接比较驱动和设备的 name 字段,如果相等的话就匹配成功

对于支持设备树的 Linux 版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式。即第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,直接比较驱动和设备的 name 字段

#嵌入式##校招##八股文##Linux##linux驱动#
拉依达的Linux驱动八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

对CPU、寄存器、缓存、内存的大概了解:一般市场上的CPU和寄存器、缓存封装出售,CPU每次都要在寄存器存取,寄存器是个临时存取空间,寄存器去访问CPU,在寄存器和CPU之间有个缓存(cache),将常用的数据存到缓存上,缓存有三级缓存(1、2、3),价格和速度依次降低。寄存器要获取的变量能不能在缓存中获取到就涉及到命中率问题,如果获取不到,就直接从内存去拿。栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很高,但是分配的内存量有限,比如iOS中栈区的大小是2M。堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。在iOS开发中所说的“内存泄漏”就是堆区的内存。静态区:全局变量和静态变量(在iOS中就是用static修饰的局部变量或者是全局变量)的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。常量区:常量存储在这里,不允许修改。代码区:存放函数体的二进制代码。类型说明符:类型说明符 void、char、short、int、long、signed、unsigned、float、double、struct、enum、union。存储类型 extern、static、register、auto、typedef(变量有且只能有一个限定)。类型限制符 const(只读)、volatile。对变量的理解:extern:用在全局变量上表示该变量在其他文件中已经定义;用在函数上作用同全局变量。static:用在全局变量上,和非静态全局变量相比,限定了作用空间;用在局部变量上,把局部变量存到了静态存储区,延长了变量生存空间;用在函数上表示仅限当前文件使用。register:将频繁使用的变量放到通用寄存器中,避免频繁访问内存,直接从寄存器中取值,提高CPU的工作效率;注:只能将局部变量或形式参数定义为寄存器变量,一般较短的变量类型适合定义为寄存器变量,如short、char等。auto:C语言变量缺省存储类型就是auto。typedef:给变量或变量表达式换一个别名。const:只读变量;但是能通过指针去修改局部const变量,const变量是一个编译期间的常量。volatile:1. 易变性;所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。volatile的这个特性,相信也是大部分朋友所了解的特性。2. “不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。3. C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。(中断、多线程)。C程序的内存分配:堆、栈、静态存储区、文件分配区(常量字符串)、程序分配区(二进制代码)。sizeof与strlen:sizeof是关键字,对于数组来说就是申请的长度,对于指针来说就是4/8(看系统位数)。strlen对于字符串来说,就是计算‘\0’之前的长度。函数的参数传递:值传递,引用传递,指针传递,数组地址传递。Const、指针、int/char等组合的意义:const int x:表示变量只读,不可更改。const char * x/char const *x:声明该指针变量指向的是常量,即该指针变量的内容可以改变,但是该内容指向的内容不可改变;如:const char *x = “helloworld”;可以直接更改x,如x = ”hi“就可以成功,但更改x[0] = ‘u’就会失败。char *const x:声明该指针变量为常变量,即指针变量里面的内容不可改变,但是该内容指向的内容可以改变;const char *x = “helloworld”;不可以直接更改x,如x = ”hi“会失败,但更改x[0] = ‘u’就会成功。const char *const x:声明该指针变量里面的内容不可改变,同时该内容指向的内容亦不可改变。数组和指针:sizeof的用法,占用内存(连续\不确定),物理地址和逻辑地址。物理地址:加载到内存地址寄存器的地址,内存单元的真正地址;逻辑地址:CPU所生成的地址。Linux进程间通信:有几种方式:管道(pipe)、信号量、消息队列、信号、共享内存、套接字。strcpy和memcpy区别:strcpy只能复制字符串,memcpy能复制任何内容,memcpy会指定复制长度。内存泄漏和内存溢出:内存泄露是指申请的内存没有释放,导致可用内存越来越少;内存溢出指程序要用的内存大于可用的内存,如数组的使用,strcpy的使用。switch的变量允许哪些类型?不允许哪些类型?整形、bool、字符、枚举;不允许字符串等非基本类型。怎么防止头文件重复调用导致的编译问题:在头文件中添加#ifndef变量 /#pragma once。实时操作系统有哪些?怎么理解?路由器用的什么操作系统?FreeRTOS、Ucos。指针数组和数组指针,双重指针:对指针数组来说,首先它是一个数组,数组的元素都是指针,也就是说该数组存储的是指针,数组占多少个字节由数组本身决定;而对数组指针来说,首先它是一个指针,它指向一个数组,也就是说它是指向数组的指针,在32位系统下永远占4字节,至于它指向的数组占多少字节,这个不能够确定,要看具体情况。指针数组如char *x[10],指向数组的指针,如char *x[10]={“hello”,“world”};x[0]=“hello”,x[1]=“world”。数组指针如char (*x)[10],表示指针x指向char [10]数组,为匿名数组。双重指针如char **x。结构体自增的含义,双重指针自增:自增从右向左进行。寄存器怎么用,怎么操作?保存一些经常调用的数据,不再访问内存。怎么获取全局变量和局部变量的地址?(gdb)backtrace bt。进程中的同步、异步怎么用?同步一般通过同步锁实现;同步锁和自旋锁区别:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。进程和线程的关系和区别?进程:进程是能在系统中独立运行并作为资源分配的基本单位,是CPU分配资源的最小单位,它包括独立的地址空间、资源以及一至多个线程。线程:线程是进程中的一个实体,是CPU调度的最小单位。树的遍历(递归&&非递归):如利用中序遍历,左子树-根节点-右子树,根据堆栈的push和pop进行进栈和出栈的操作。嵌入式C++面经推荐大佬面经  链接在下边  c++/嵌入式面经专栏-牛客网 https://www.nowcoder.com/creation/manager/columnDetail/MJNwoM
点赞 评论 收藏
分享
1. 嵌入式实现多任务处理?2. 在STM32中配置ADC进行模拟信号采集?3. UART通信中的数据帧结构是什么样的?4. 嵌入式实现看门狗定时器?5. 在RT-Thread中使用消息队列进行任务间通信?6. SPI与I2C的时序特征有哪些不同?7. 嵌入式实现动态内存分配?8. 在STM32中配置定时器进行周期性中断?9. 嵌入式实现任务优先级调度?10. 在FreeRTOS中实现任务的延时功能?11. 嵌入式处理外部中断?12. 在STM32中使用DMA进行数据传输?13. 嵌入式实现低功耗睡眠模式?14. 在RTOS中使用互斥量保护共享资源?15. 嵌入式实现串口通信协议?16. 在STM32中配置GPIO引脚的复用功能?17. 实现数据的CRC校验?18. 嵌入式在FreeRTOS中实现任务的优先级继承?19. 进行固件版本管理?20. 嵌入式在STM32中配置PWM输出信号?21. 实现实时数据监控?22. 在RT-Thread中使用事件组进行任务同步?23. 嵌入式实现网络通信协议栈?24. 在STM32中使用外部存储器(如SD卡)?25. 实现数据采集的滤波算法?26. 嵌入式在FreeRTOS中实现软件定时器?27. 进行系统时钟的精确配置?28. 嵌入式在STM32中配置和使用CAN总线?29. 进行设备驱动的开发?30. 在RT-Thread中实现内存池的管理?答案整理在面经中  c++/嵌入式面经专栏-牛客网 https://www.nowcoder.com/creation/manager/columnDetail/MJNwoM
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务