【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)

参考:

原文地址:https://www.cnblogs.com/lizhuming/p/14593464.html