• 【linux】驱动-5-驱动框架分层分离&实战



    前言

    5. 分离分层

    本章节记录实现LED驱动的大概步骤,且编程框架实现分离分层。
    分离分层:

    • 上层:系统 相关。如模块注册于注销。

    • 下层:硬件操作。如提供 file_operations 。分离

      • 设备。提供板卡信息,如使用哪一个引脚。
      • 驱动。引脚的具体操作。
    • 以下以 LED 为例。

    5.1 回顾-设备驱动实现

    步骤

    • 模块

      • 入口函数
      • 出口函数
      • 协议
    • 驱动

      • 驱动代码:实现 file_operations
      • 申请设备号
      • 初始化内核设备文件结构体+绑定驱动代码 file_operations
      • 添加内核设备文件结构体到内核+绑定设备号
      • 创建设备类
      • 创建设备节点+绑定设备号
    • 具体实现参考《linux-驱动-3-字符设备驱动》或往下看。

    5.2 分离分层

    把一个字符设备驱动工程分层分离。(看章前分析
    得出以下目录树:

    dev_drv
    |__ xxx_module.c
    |__ include
    |    |__ xxx_resource.h
    |__ device
    |    |__ xxx_dev_a.c
    |    |__ xxx_dev_a.h
    |__ driver
        |__ xxx_drv.c
        |__ xxx_drv.h
    

    目录树分析:

    • dev_drv:字符设备模块目录
      • xxx_module.c:上层。系统。用于注册、注销模块,及操作驱动与内核的联系部分。
      • include:系统、设备、驱动共用的自定义头文件。
        • xxx_resource.h:资源文件。包含了设备资源传给驱动文件的结构体。
      • device:设备目录。硬件设备内容,提供给驱动文件使用,即是提供资源。
        • xxx_dev_a.c:板卡a。以规定的格式提供硬件资源。
        • xxx_dev_a.h:板卡a。头文件。
      • driver:驱动目录。实现驱动 file_operations 的目录。
        • xxx_drv.c:驱动。实现驱动内容。
        • xxx_drv.c:驱动。头文件。

    5.3 设备

    主要内容:

    • 提供设备资源;
    • 提供获取设备资源接口。

    现在设备资源格式文件中第一好格式:

    • 设备资源:(led_resource.h)
    /* led 资源结构体 */
    struct LED_RESOURCE_T
    {
        unsigned long pa_dr; // 数据寄存器  物理地址
        unsigned long pa_gdir; // 输入输出寄存器  物理地址
        unsigned long pa_iomuxc_mux; // 端口复用寄存器  物理地址
        unsigned long pa_ccm_ccgrx; // 端口时钟寄存器  物理地址
        unsigned long pa_iomux_pad; // 电气属性寄存器  物理地址
        unsigned int pin; // 引脚号
        unsigned int clock_offset; // 时钟偏移
    };
    typedef struct LED_RESOURCE_T led_resource_t;
    
    • 获取设备资源接口:
    /** @brief  get_led_resource  获取资源句柄
      * @param  led 参数
      * @retval 
      * @author lzm
      */
    led_resource_t *get_led_resource(char ch)
    {
        if(ch == 'R' || ch == 'r' || ch == '0')
            return &led_r;
        else if(ch == 'G' || ch == 'g' || ch == '1')
            return &led_g;
        else if(ch == 'B' || ch == 'b' || ch == '2')
            return &led_b;
        
        return 0;
    }
    

    5.4 驱动

    实现驱动内容:

    • file_operations

    • 使用设备数组模式,实现统一管理,且达到时间复杂度为 O(1) 的性能。

    • file_operations

      • int led_dev_open(struct inode *inode, struct file *filp):打开设备节点。
      • int led_dev_release(struct inode *inode, struct file *filp):关闭设备节点。
      • ssize_t led_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos):写函数。
      • ssize_t led_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos):读函数。
    • 设备数组:

      • static led_dev_t led_dev_elem[LED_DEV_CNT];:led 设备列表。使用 id 作为下标去定位哪一个设备。
      • 设备结构体:
    /* my led device struct */
    typedef void (*led_init_f)(unsigned char);
    typedef void (*led_exit_f)(unsigned char);
    typedef void (*led_ctrl_f)(unsigned char, unsigned char);
    struct LED_DEV_T
    {
        /* 设备 ID 次设备号-1 因为次设备号0一般代表所有设备*/
        unsigned char id;
        /* 设备名称 */
        char name[10]; // 限定十个字符
        /* 设备参数 */
        dev_t dev_num; // 设备号
        struct cdev dev; // 内核设备文件 结构体
        /* led 状态 */
        unsigned char status; // 0: 关闭; 1:开
        /* 引脚参数 */
        led_pin_t *pin_data;
        /* 设备函数 */
        led_init_f init; // 初始化函数
        led_exit_f exit; // 出口函数
        led_ctrl_f ctrl; // 控制函数
    };
    typedef struct LED_DEV_T led_dev_t; 
    

    5.5 系统,模块

    万事俱备,只欠东风。
    下层硬件的资源和驱动函数都准备好了,现在只需要实现模块即可。
    主要三个点:

    • static int __init led_chrdev_init(void):入口函数。(module_init(led_chrdev_init)
    • static void __exit led_chrdev_exit(void):出口函数。(module_exit(led_chrdev_exit)
    • MODULE_LICENSE("GPL"):协议。

    以上两个函数的内容可以参考字符设备驱动实现步骤来实现。
    除了以上三个函数外,还有把驱动内容填入驱动文件中 file_operations 实体。

    给出 led_module.c 文件参考:

    /** @file         led_module.c
     *  @brief        驱动。
     *  @details      led 模块文件。
     *  @author       lzm
     *  @date         2021-03-06 10:23:03
     *  @version      v1.0
     *  @copyright    Copyright By lizhuming, All Rights Reserved
     *
     **********************************************************
     *  @LOG 修改日志:
     **********************************************************
    */
    /* 系统库 */
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/cdev.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>
    #include <linux/io.h>
    /* 私人库 */
    #include "led_resource.h"
    #include "led_drv.h"
    /* 变量 */
    dev_t led_dev_num_start; // 开始设备号
    static struct cdev led_cdev[LED_DEV_CNT+1]; // 全设备+LED_DEV_CNT个子设备
    struct class *led_dev_class; // 设备类 (*用于设备节点*)
    /* [drv][file_operations] */
    static struct file_operations led_dev_fops = 
    {
        .owner = THIS_MODULE,
        .open = led_dev_open,
        .release = led_dev_release,
        .write = led_dev_write,
        .read = led_dev_read,
    };
    /* [module][1] */
    /** @brief   led_chrdev_init
      * @details led module 入口函数
      * @param  
      * @retval 
      * @author lzm
      */
    static int __init led_chrdev_init(void)
    {
        unsigned char i;
        printk("chrdev_init
    ");
        /* my 设备文件初始化 */
        led_dev_init();
        /* [module][1][1] 申请设备号 */
        alloc_chrdev_region(&led_dev_num_start, 0, LED_DEV_CNT+1, LED_DEV_NAME);
        /* [module][1][2] 创建设备节点 */
        led_dev_class = class_create(THIS_MODULE, LED_DEV_CLASS);    
        for(i=0; i<LED_DEV_CNT+1; i++)
        {
            /* [module][1][3] 初始化内核设备文件 */
            cdev_init(&led_cdev[i], &led_dev_fops); // 把驱动程序初始化到内核设备文件中
            /* [module][1][4] 把内核设备文件注册到内核 */
            cdev_add(&led_cdev[i], MKDEV(MAJOR(led_dev_num_start), MINOR(led_dev_num_start)+i), 1); // 内核设备文件绑定设备号,并注册到内核
            /* [module][1][5] 创建设备节点 */
            if(!i)
                device_create(led_dev_class, NULL, MKDEV(MAJOR(led_dev_num_start), MINOR(led_dev_num_start)+i), NULL, LED_DEV_NAME); // 总设备
            else
                device_create(led_dev_class, NULL, MKDEV(MAJOR(led_dev_num_start), MINOR(led_dev_num_start)+i), NULL, LED_DEV_NAME"_%d",i);
        }
        return 0;
    }
    module_init(led_chrdev_init);
    /* [module][2] */
    /** @brief   led_chrdev_exit
      * @details led module 出口函数
      * @param  
      * @retval 
      * @author lzm
      */
    static void __exit led_chrdev_exit(void)
    {
        unsigned char i;
        printk("chrdev_exit!
    ");
        for(i=0; i<LED_DEV_CNT+1; i++)
        {
            /* [module][2][1] 删除设备节点 */
            device_destroy(led_dev_class, MKDEV(MAJOR(led_dev_num_start), MINOR(led_dev_num_start)+i));
            /* [module][2][2] 注销设备文件 */
            cdev_del(&led_cdev[i]);
        }
        /* [module][2][3] 归还设备号 */
        unregister_chrdev_region(led_dev_num_start, LED_DEV_CNT+1);
        /* [module][2][4] 删除设备类 */
        class_destroy(led_dev_class);
        return;
    }
    module_exit(led_chrdev_exit);
    /* [module][3] 协议 */
    MODULE_AUTHOR("lizhuming");
    MODULE_LICENSE("GPL");
    

    5.6 Makefile

    参考 《一个通用驱动Makefile-V2-支持编译多目录》

    以下只给出源码:

    # @file         Makefile
    # @brief        驱动。
    # @details      led 驱动模块 Makefile 例程。
    # @author       lzm
    # @date         2021-03-14 10:23:03
    # @version      v1.1
    # @copyright    Copyright By lizhuming, All Rights Reserved
    #
    # ********************************************************
    # @LOG 修改日志:
    # ********************************************************
    
    # 编译后内核路径
    KERNEL_DIR = /home/lss/work/kernel/imx6/ebf-buster-linux/build_image/build
    # 定义框架
    # ARCH 为 x86 时,编译链头为 
    # ARCH 为 arm 时,编译链头为 arm-linux-gnueabihf-
    ARCH = arm
    ifeq ($(ARCH),x86)
    CROSS_COMPILE = # 交叉编译工具头,如:
    else
    CROSS_COMPILE = arm-linux-gnueabihf-# 交叉编译工具头,如:arm-linux-gnueabihf-
    endif
    CC      = $(CROSS_COMPILE)gcc # 编译器,对 C 源文件进行编译处理,生成汇编文件
    # 共享到sub-Makefile
    export  ARCH  CROSS_COMPILE
    
    # 路径
    PWD := $(shell pwd)
    # 当前模块路径
    # $(src) 是内和文件定义并传过来的当前模块 M= 的路径。
    MODDIR := $(src)
    
    # 注意:驱动目标不要和文件名相同
    TARGET_DRV := led_device_driver
    TARGET_APP := led_app
    
    # 本次整个编译需要源 文件 和 目录
    # 这里的“obj-m” 表示该模块不会编译到zImage ,但会生成一个独立的xxx.ko 静态编译(由内核源码顶层Makefile识别)
    # 模块的多文件编译:obj-m 是告诉 makefile 最总的编译目标。而 $(TARGET)-y 则是告诉 makefile 该总目标依赖哪些目标文件。(也可以使用 xxx-objs)
    $(TARGET_DRV)-y += led_module.o
    $(TARGET_DRV)-y += ./device/led_dev_a.o
    $(TARGET_DRV)-y += ./driver/led_drv.o
    obj-m := $(TARGET_DRV).o
    # obj-m += $(patsubst %.c,%.o,$(shell ls *.c))
    
    # 编译条件处理
    # 指定头文件 由于该文件是 -C 后再被调用的,所以部分参数不能使用 $(shell pwd)
    # $(src) 是内和文件定义并传过来的当前模块 M= 的路径。
    ccflags-y := -I$(MODDIR)/include
    ccflags-y += -I$(MODDIR)/device
    ccflags-y += -I$(MODDIR)/driver
    
    # 第一个目标 CURDIR 是该makefile内嵌变量,自动设置为当前目录
    all :
        @$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR)  modules
    #   make mobailes 就是是编译模块,上面是其添加参数的指令
    #   $(CROSS_COMPILE)gcc -o $(TARGET_APP) $(TARGET_APP).c
        
    # 清理
    .PHONY:clean
    clean:
        $(MAKE)  -C $(KERNEL_DIR) M=$(CURDIR) clean
    #   rm $(TARGET_APP)
    

    参考:

  • 相关阅读:
    家庭养花秘笈1000问
    生活中来3000例·健康篇
    生命的奥秘百科(套装共10册)
    海军陆战队6:太空战舰
    历史文明探秘百科(套装共10册)
    中医养生知识读本
    上工养生话刮痧
    古法艾灸
    钻井液处理剂及其作用原理
    重金属污泥处理技术与管理
  • 原文地址:https://www.cnblogs.com/lizhuming/p/14593464.html
Copyright © 2020-2023  润新知