前言
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)
参考:
- 《IMX6ULLRM(6ULL用户手册).pdf》
- 内核文档html
- 李柱明博客:https://www.cnblogs.com/lizhuming/
- 本文链接:https://www.cnblogs.com/lizhuming
- 推荐去看源码: