文章目录
- 前言
- 一、系列文章的目录
- 二、框架对比
-
- [2.1 基本框架(字符设备)](#2.1 基本框架(字符设备))
- [2.2 基于设备树的框架与基本框架的对比](#2.2 基于设备树的框架与基本框架的对比)
- [2.3 基于platform平台设备的框架与基本框架等的对比](#2.3 基于platform平台设备的框架与基本框架等的对比)
- [2.4 MISC驱动开发框架及其精简性的讨论](#2.4 MISC驱动开发框架及其精简性的讨论)
- [2.5 基于input子系统的驱动开发框架及其精简性的讨论](#2.5 基于input子系统的驱动开发框架及其精简性的讨论)
- [2.6 基于pinctrl和GPIO子系统的驱动开发框架及其精简性的讨论](#2.6 基于pinctrl和GPIO子系统的驱动开发框架及其精简性的讨论)
- [2.7 I2C驱动开发框架与platform平台驱动开发框架中驱动部分的对比](#2.7 I2C驱动开发框架与platform平台驱动开发框架中驱动部分的对比)
- [2.8 DMA驱动开发框架与基本框架的对比](#2.8 DMA驱动开发框架与基本框架的对比)
- [2.9 块设备驱动开发框架与基本框架的对比](#2.9 块设备驱动开发框架与基本框架的对比)
- [2.10 网卡设备驱动开发框架与基本框架的对比](#2.10 网卡设备驱动开发框架与基本框架的对比)
- 三、感悟
前言
本篇主要对比前述移植过程中的不同驱动开发框架,以便于清楚不同开发框架的差异点和相同点。
这篇应该是这个系列的最终篇了!
首先还是感谢野火的驱动开发教程和Alinx的驱动开发教程,感谢两家厂商开源了开发教程,可以让小子我站在巨人的肩膀上学习!
一、系列文章的目录
嵌入式系统内核镜像相关(一) 主要讲解操作系统启动过程所需的文件、启动方式以及启动过程的打印输出及其解释。
嵌入式系统内核镜像相关(二) 演示从Vivado开始到Vitis裸机开发、Petalinux镜像制作、镜像启动和内置驱动测试的完整例子。
嵌入式系统内核镜像相关(三) 主要负责解释清楚Petalinux内的组件,尤其是Yocto、Bitbake、Buildroot、BusyBox和FSBL。
嵌入式系统内核镜像相关(四) 翻译了devicetree官网的《Devicetree Specification Release v0.4》。
嵌入式系统内核镜像相关(五) 主要介绍了U-Boot以及Xilinx Wiki设定的U-boot驱动程序。
嵌入式系统内核镜像相关(六) 围绕璞致的设备树结合bindings文档和内核驱动文件展开解释节点的属性和属性值含义,本篇是第一篇,重点解释了ethernet节点、LED节点以及GPIO子系统。
嵌入式系统内核镜像相关(七) 围绕璞致的设备树结合bindings文档和内核驱动文件展开解释节点的属性和属性值含义,本篇是第二篇,重点解释了USB节点、UART节点和PL子系统部分的设备树。
嵌入式系统内核镜像相关(八) 实现开发板和主机以及外部网站之间的通信。
嵌入式系统内核镜像相关(九) 以LED驱动开发作为例子,介绍驱动开发流程、驱动移植教程中的坑点和file_operation。
嵌入式系统内核镜像相关(十) 本篇是移植Alinx教程的第一篇,完整介绍多LED驱动开发过程。
嵌入式系统内核镜像相关(十一) 本篇是移植Alinx教程的第二篇,完成字符设备新写法、设备树和of函数、并发处理等3个例程的移植以及对存在的移植难点进行介绍,也存在因为璞致开发板硬件支持条件不足而无法移植pinctrl和gpio子系统移植。gpio输入例程的移植在后面已经顺利完成了!
嵌入式系统内核镜像相关(十二) 本篇是移植Alinx教程的第三篇,介绍了gpio输入例程和定时器例程的移植及其难点,并且补充了有可能因为编译失败或者编译后重新修改再次编译的正规流程。
嵌入式系统内核镜像相关(十三) 本篇是移植Alinx教程的第四篇,介绍了platform平台设备、MISC设备驱动、USB驱动等的移植。
嵌入式系统内核镜像相关(十四) 本篇是移植Alinx教程的第五篇,介绍了块设备驱动和DMA驱动等的移植。
嵌入式系统内核镜像相关(十五) 本篇是移植Alinx教程的第六篇,介绍了网卡设备驱动的移植和异构多核通信的裸机开发。
嵌入式系统内核镜像相关(十六) 本篇是移植Alinx教程的第七篇,介绍了ZYNQ-7000中的中断、UG585的中断以及对基于中断的驱动开发移植过程的思考。
以上基本涵盖了:
- 从硬件开发、裸机开发再到镜像制作和驱动开发的完整流程(以
璞致开发板
为例)。 - 操作系统启动镜像制作和启动流程。
- 驱动开发和测试流程以及经典的设备驱动移植,及移植过程中的难点分析。
- 镜像制作和驱动开发所在Petalinux平台的组成分析以及与其他手段如Buildroot等的对比与关联。
- 设备树开发所需官方spec、xilinx wiki、bindings文档以及通过内置驱动的of函数来推敲某些属性的属性值。
- 常见外设的驱动开发框架。
二、框架对比
2.1 基本框架(字符设备)
参考嵌入式系统内核镜像相关(十)的内容,提取开发框架的骨干:
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h>
/* 驱动名称 */
#define DEVICE_NAME "gpio_leds_our"
/* 驱动主设备号 */
#define GPIO_LED_MAJOR 200
/* gpio 寄存器虚拟地址 */
static unsigned int gpio_add_minor;
/* gpio 寄存器物理基地址 */
#define GPIO_BASE 0xE000A000
/* gpio 寄存器所占空间大小 */
#define GPIO_SIZE 0x1000
/* gpio 方向寄存器 */
#define GPIO_DIRM_0 (unsigned int *)(0xE000A284 - GPIO_BASE + gpio_add_minor)
/* gpio 使能寄存器 */
#define GPIO_OEN_0 (unsigned int *)(0xE000A288 - GPIO_BASE + gpio_add_minor)
/* gpio 控制寄存器 */
#define GPIO_DATA_0 (unsigned int *)(0xE000A048 - GPIO_BASE + gpio_add_minor)
/* 时钟使能寄存器虚拟地址 */
static unsigned int clk_add_minor;
/* 时钟使能寄存器物理基地址 */
#define CLK_BASE 0xF8000000
/* 时钟使能寄存器所占空间大小 */
#define CLK_SIZE 0x1000
/* AMBA 外设时钟使能寄存器 */
#define APER_CLK_CTRL (unsigned int *)(0xF800012C - CLK_BASE + clk_add_minor)
/* open 函数实现 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p);
/* write 函数实现 */
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p);
/* release 函数实现 */
static int gpio_leds_release(struct inode *inode_p, struct file *file_p);
/* file_operations 结构体声明 */
static struct file_operations gpio_leds_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
/* 模块加载时会调用的函数 */
static int __init gpio_led_init(void);
/* 卸载模块 */
static void __exit gpio_led_exit(void);
/* 标记加载、卸载函数 */
module_init(gpio_led_init);
module_exit(gpio_led_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("GPIO LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
从规定驱动名称、主设备号到寄存器地址的定义,然后就是file_operations中必要函数的实现,最后再到模块的加载与卸载。其中次设备号依赖mknod
申请!
基本框架是寄存器硬编码
,直接在驱动代码里写死寄存器地址与位操作,一旦芯片发生变化就得大面积重写驱动,因此驱动的复用性几乎为零。
因此需要引入基于设备树的开发框架,这种框架的好处是将寄存器单独放到一个可以起到参数配置作用的设备树中,以便于统一管理寄存器。驱动通过of_xxx()
接口动态获取参数诸如寄存器基地址
、存储空间容量
、中断编号
、中断类型
以及中断触发方式
等等。硬件改动只改设备树,驱动基本不动。不过这种方式还是会牵扯到寄存器的编写。
2.2 基于设备树的框架与基本框架的对比
我把没必要展示的功能精简了一下,仅展示框架以及相比于基本框架的改进点:
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/device.h>
#include <asm/uaccess.h>
/* 设备节点名称 */
#define DEVICE_NAME "gpio_leds_our3"
/* 设备号个数 */
#define DEVID_COUNT 1
/* 驱动个数 */
#define DRIVE_COUNT 1
/* 定义结构体 */
struct alinx_char_dev {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
};
/* 声明设备结构体 */
static struct alinx_char_dev alinx_char;
/* 定义寄存器指针 */
static u32 *GPIO_DIRM_0;
static u32 *GPIO_OEN_0;
static u32 *GPIO_DATA_0;
static u32 *APER_CLK_CTRL;
/* 文件操作函数 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p);
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p);
static int gpio_leds_release(struct inode *inode_p, struct file *file_p);
/* 文件操作结构体 */
static struct file_operations ax_char_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
/* 模块加载函数 */
static int __init gpio_led_init(void) {
/* 获取设备树节点 */
alinx_char.nd = of_find_node_by_name(NULL, "alinxled");
/* 获取寄存器地址 */
u32 reg_data[10];
int ret = of_property_read_u32_array(alinx_char.nd, "reg", reg_data, 8);
if (ret < 0) {
printk("get reg failed!\r\n");
return -1;
}
/* 映射寄存器地址 */
GPIO_DIRM_0 = ioremap(reg_data[0], reg_data[1]);
GPIO_OEN_0 = ioremap(reg_data[2], reg_data[3]);
GPIO_DATA_0 = ioremap(reg_data[4], reg_data[5]);
APER_CLK_CTRL = ioremap(reg_data[6], reg_data[7]);
/* 注册设备号 */
alloc_chrdev_region(&alinx_char.devid, MINOR, DEVID_COUNT, DEVICE_NAME);
/* 初始化字符设备结构体 */
cdev_init(&alinx_char.cdev, &ax_char_fops);
/* 注册字符设备 */
cdev_add(&alinx_char.cdev, alinx_char.devid, DRIVE_COUNT);
/* 创建类 */
alinx_char.class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(alinx_char.class)) {
return PTR_ERR(alinx_char.class);
}
/* 创建设备节点 */
alinx_char.device = device_create(alinx_char.class, NULL, alinx_char.devid, NULL, DEVICE_NAME);
if (IS_ERR(alinx_char.device)) {
return PTR_ERR(alinx_char.device);
}
return 0;
}
/* 模块卸载函数 */
static void __exit gpio_led_exit(void) {
/* 注销字符设备 */
cdev_del(&alinx_char.cdev);
/* 注销设备号 */
unregister_chrdev_region(alinx_char.devid, DEVID_COUNT);
/* 删除设备节点 */
device_destroy(alinx_char.class, alinx_char.devid);
/* 删除类 */
class_destroy(alinx_char.class);
/* 释放对虚拟地址的占用 */
iounmap(GPIO_DIRM_0);
iounmap(GPIO_OEN_0);
iounmap(GPIO_DATA_0);
iounmap(APER_CLK_CTRL);
printk("gpio_led_dev_exit_ok\n");
}
/* 标记加载、卸载函数 */
module_init(gpio_led_init);
module_exit(gpio_led_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("DEVICE TREE GPIO LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
相比于基本框架,基于设备树的框架改进了不少:
1、引入了alinx_char_dev结构体
,将字符设备相关的数据封装在一起,包括设备号、字符设备、类和设备节点等。好处嘛,太多了,便于管理数据结构且更易于模块化,方便移植。
2、注意改进后的代码没有定义主设备号,反而在模块加载时动态创建类和设备节点(也就是alloc_chrdev_region
函数)。这允许驱动在系统启动时自动注册设备,而不需要手动指定设备号。好处就是可以让开发人员避免手动查找空闲的设备号!
3、留意下模块的加载和卸载函数的功能实现。
- 首先就是因为引入了设备树,所以可通过
of_xxx
函数自动获取设备的寄存器信息,从而避开了在驱动代码中直接引入寄存器虚拟地址的define
操作。 - 其次在
模块加载函数
中,依次通过获取设备树节点(of_find_node_by_name函数)
、获取寄存器地址(of_property_read_u32_array函数)
、映射寄存器地址(ioremap函数)
、注册设备号(alloc_chrdev_region函数)
、初始化字符设备结构体(cdev_init函数)
、注册字符设备(cdev_add函数)
、创建类(class_create函数)
和创建设备节点(device_create函数)
等操作完成模块加载。 - 最后在
模块卸载函数
中,依次通过注销字符设备(cdev_del函数)
、注销设备号(unregister_chrdev_region函数)
、删除设备节点(device_destroy函数)
、删除类(class_destroy函数)
和释放对虚拟地址的占用(iounmap函数)
等操作完成模块卸载。
4、可以对比和基本框架的代码差异:
- 在
基本框架
的模块加载函数
中通过register_chrdev
完成设备注册,此外,在open
函数中使用ioremap函数
实现物理地址到虚拟地址的映射!但在基于设备树的驱动开发框架
中open
函数只负责对寄存器赋值,包括输入输出的规定、时钟使能和EMIO使能,将ioremap函数
转移到模块加载函数
中。不过,关于ioremap函数
在open
函数中实现还是在模块加载函数
中实现,并没有什么区别! - 在
基本框架
的模块卸载函数
中通过unregister_chrdev
完成设备注销,并提前引入iounmap函数
实现虚拟地址的资源释放。而在基于设备树的驱动开发框架
中,unregister_chrdev_region函数
更为灵活的释放设备号资源。
评价一下这种模式吧!
好不好?相比于基本框架肯定是好!体现在设备树实现了设备信息与驱动代码的解耦,也体现在设备节点的申请摆脱了开发人员对空闲节点的手动确认,也体现在流程更加规范、更加可扩展、更加便于移植等等!
局限性有没有?当然有!驱动代码还是通过读取设备树信息对寄存器的值进行操纵,参考open
函数或者模块加载函数
中的ioremap函数
!很显然,这种模式还是一次性使用驱动代码,并且我们到现在为止没有很好地区分设备和驱动!请注意,设备在咱这个主题的范畴里面只是提供信息的结构体,而驱动才是利用这个结构体的程序,因此基于设备树的驱动
仅仅只是实现了设备信息的一部分提取,并在驱动代码里面混淆了设备信息和驱动实现。所以,这种方式还值得改进!
这也是引入platform
平台设备的一个原因!
当然,对于简单设备而言,2.1
和2.2
已经是很好的开发模式了!毕竟技术的好坏评价除了信息是否耦合这一评价外,还得看实际的应用场景在各个维度上的约束!
2.3 基于platform平台设备的框架与基本框架等的对比
重申一下!
驱动代码和设备信息的耦合问题
:设备信息与驱动代码紧密混合,驱动程序中充斥着硬件寄存器地址。本质上,这种开发方式与单片机驱动开发无异,一旦硬件信息变更或设备移除,就必须修改驱动源码。
上述问题的典型例子
:在字符设备驱动程序中,通过调用open()函数打开设备文件后,可使用read()/write()函数经由file_operations文件操作接口控制硬件。此方式虽简单直观,但存在驱动代码和设备信息的耦合问题,导致内核的难以维护和代码的难以复用问题。
那怎么解决上述问题?Linux提出设备驱动模型。设备驱动模型引入总线
概念,实现驱动代码与设备信息的分离,也就是驱动总线
。
驱动总线
和物理总线
(也就是常用于SoC的那种)之间的区别:
(1) 物理总线:芯片与外设间传输信息的公共通信干线,包括数据总线、地址总线和控制总线,用于传输通信时序。
(2) 驱动总线:管理设备和驱动,制定匹配规则,尝试为新注册的设备或驱动进行配对。
(3) 有物理总线的设备必然有驱动总线,但反之不一定。对于I2C、SPI、USB等常见物理总线,Linux内核会自动创建对应的驱动总线,相应设备自然注册挂载其上。然而,实际开发中许多结构简单设备,如LED、RTC时钟、蜂鸣器、按键等,并无特殊时序需求,也无相应物理总线,Linux内核不会为它们创建驱动总线。
(4) 在设备驱动模型下,Linux内核引入了平台总线(platform bus),一种虚拟总线。平台总线用于管理无物理总线的设备,这些设备称为平台设备,对应的驱动称为平台驱动。平台设备驱动的核心依然是Linux设备驱动模型,平台设备用platform_device结构体表示,继承自device结构体;平台驱动用platform_driver结构体表示,继承自device_driver结构体。
同2.3的处理操作,把代码给精简一下:
设备代码部分:
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/* 寄存器首地址 */
/* gpio方向寄存器 */
#define GPIO_DIRM_0 0xE000A284
/* gpio使能寄存器 */
#define GPIO_OEN_0 0xE000A288
/* gpio控制寄存器 */
#define GPIO_DATA_0 0xE000A048
/* AMBA外设时钟使能寄存器 */
#define APER_CLK_CTRL 0xF800012C
/* 寄存器大小 */
#define REGISTER_LENGTH 4
/* 删除设备时会执行此函数 */
static void led_release(struct device *dev)
{
printk("led device released\r\n");
}
/* 初始化LED的设备信息, 即寄存器信息 */
static struct resource led_resources[] =
{
{
.start = GPIO_DIRM_0,
.end = GPIO_DIRM_0 + REGISTER_LENGTH - 1,
/* 寄存器当作内存处理 */
.flags = IORESOURCE_MEM,
},
{
.start = GPIO_OEN_0,
.end = GPIO_OEN_0 + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM,
},
{
.start = GPIO_DATA_0,
.end = GPIO_DATA_0 + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM,
},
{
.start = APER_CLK_CTRL,
.end = APER_CLK_CTRL + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM,
},
};
/* 声明并初始化platform_device */
static struct platform_device led_device =
{
/* 名字和driver中的name一致 */
.name = "alinx-led",
/* 只有一个设备 */
.id = -1,
.dev = {
/* 设置release函数 */
.release = &led_release,
},
/* 设置资源个数 */
.num_resources = ARRAY_SIZE(led_resources),
/* 设置资源信息 */
.resource = led_resources,
};
/* 入口函数 */
static int __init led_device_init(void)
{
/* 在入口函数中调用platform_driver_register, 注册platform驱动 */
return platform_device_register(&led_device);
}
/* 出口函数 */
static void __exit led_device_exit(void)
{
/* 在出口函数中调用platform_driver_register, 卸载platform驱动 */
platform_device_unregister(&led_device);
}
/* 标记加载、卸载函数 */
module_init(led_device_init);
module_exit(led_device_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("PLATFORM LED device");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
驱动代码部分:
c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/* 设备节点名称 */
#define DEVICE_NAME "gpio_leds"
/* 设备号个数 */
#define DEVID_COUNT 1
/* 驱动个数 */
#define DRIVE_COUNT 1
/* 主设备号 */
#define MAJOR
/* 次设备号 */
#define MINOR 0
/* gpio寄存器虚拟地址 */
static u32 *GPIO_DIRM_0;
/* gpio使能寄存器 */
static u32 *GPIO_OEN_0;
/* gpio控制寄存器 */
static u32 *GPIO_DATA_0;
/* AMBA外设时钟使能寄存器 */
static u32 *APER_CLK_CTRL;
/* 把驱动代码中会用到的数据打包进设备结构体 */
struct alinx_char_dev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
};
/* 声明设备结构体 */
static struct alinx_char_dev alinx_char = {
.cdev = {
.owner = THIS_MODULE,
},
};
/* open函数实现, 对应到Linux系统调用函数的open函数 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
/* write函数实现, 对应到Linux系统调用函数的write函数 */
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
/* release函数实现, 对应到Linux系统调用函数的close函数 */
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
/* file_operations结构体声明, 是上面open、write实现函数与系统调用函数对应的关键 */
static struct file_operations ax_char_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
/* probe函数实现, 驱动和设备匹配时会被调用 */
static int gpio_leds_probe(struct platform_device *dev)
{
/* 资源大小 */
int regsize[4];
/* 资源信息 */
struct resource *led_source[4];
int i;
for(i = 0; i < 4; i ++)
{
/* 获取dev中的IORESOURCE_MEM资源 */
led_source[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
/* 返回NULL获取资源失败 */
if(!led_source[i])
{
dev_err(&dev->dev, "get resource %d failed\r\n", i);
return -ENXIO;
}
/* 获取当前资源大小 */
regsize[i] = resource_size(led_source[i]);
}
/* 把需要修改的物理地址映射到虚拟地址 */
GPIO_DIRM_0 = ioremap(led_source[0]->start, regsize[0]);
GPIO_OEN_0 = ioremap(led_source[1]->start, regsize[1]);
GPIO_DATA_0 = ioremap(led_source[2]->start, regsize[2]);
APER_CLK_CTRL = ioremap(led_source[3]->start, regsize[3]);
/* MIO_0时钟使能 */
*APER_CLK_CTRL |= 0x00400000;
/* MIO_0设置成输出 */
*GPIO_DIRM_0 |= 0x0000000F;
/* MIO_0使能 */
*GPIO_OEN_0 |= 0x0000000F;
/* 注册设备号 */
alloc_chrdev_region(&alinx_char.devid, MINOR, DEVID_COUNT, DEVICE_NAME);
/* 初始化字符设备结构体 */
cdev_init(&alinx_char.cdev, &ax_char_fops);
/* 注册字符设备 */
cdev_add(&alinx_char.cdev, alinx_char.devid, DRIVE_COUNT);
/* 创建类 */
alinx_char.class = class_create(THIS_MODULE, DEVICE_NAME);
if(IS_ERR(alinx_char.class))
{
return PTR_ERR(alinx_char.class);
}
/* 创建设备节点 */
alinx_char.device = device_create(alinx_char.class, NULL,
alinx_char.devid, NULL,
DEVICE_NAME);
if (IS_ERR(alinx_char.device))
{
return PTR_ERR(alinx_char.device);
}
return 0;
}
static int gpio_leds_remove(struct platform_device *dev)
{
/* 注销字符设备 */
cdev_del(&alinx_char.cdev);
/* 注销设备号 */
unregister_chrdev_region(alinx_char.devid, DEVID_COUNT);
/* 删除设备节点 */
device_destroy(alinx_char.class, alinx_char.devid);
/* 删除类 */
class_destroy(alinx_char.class);
/* 释放对虚拟地址的占用 */
iounmap(GPIO_DIRM_0);
iounmap(GPIO_OEN_0);
iounmap(GPIO_DATA_0);
return 0;
}
/* 声明并初始化platform驱动 */
static struct platform_driver led_driver = {
.driver = {
/* 将会用name字段和设备匹配, 这里name命名为alinx-led */
.name = "alinx-led",
},
.probe = gpio_leds_probe,
.remove = gpio_leds_remove,
};
/* 驱动入口函数 */
static int __init gpio_led_drv_init(void)
{
/* 在入口函数中调用platform_driver_register, 注册platform驱动 */
return platform_driver_register(&led_driver);
}
/* 驱动出口函数 */
static void __exit gpio_led_dev_exit(void)
{
/* 在出口函数中调用platform_driver_register, 卸载platform驱动 */
platform_driver_unregister(&led_driver);
}
/* 标记加载、卸载函数 */
module_init(gpio_led_drv_init);
module_exit(gpio_led_dev_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("PLATFORM LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
注意platform
平台设备的驱动开发框架没设备树什么事儿了!不过两者是属于技术交叉的关系,不存在彼此排斥!
回到对框架的分析上来:
首先是设备代码:
- 第一步、定义并初始化寄存器首地址和寄存器大小,然后在
resource
这个结构体中初始化设备信息(也就是寄存器信息)。 - 第二步、声明并初始化
platform_device
结构体,把上述的resource
结构体丢进去,同时将定义好功能的release
函数也丢进去!注意,还得加个.name
以便于找到驱动代码
,不然没法儿使用! - 第三步、在
入口函数
中(也就是初始化设备函数
)使用platform_device_register
实现platform_device
结构体的注册,从而实现platform设备的成功注册。 - 第四步、在
出口函数
中(也就是退出设备函数
)使用platform_device_unregister
实现platform_device
结构体的注销。 - 第五步、分别使用
module_init
和module_exit
绑定入口函数
和出口函数
。
然后是驱动代码:
- 第一步、定义寄存器,然后定义设备结构体,如下:
c
/* 把驱动代码中会用到的数据打包进设备结构体 */
struct alinx_char_dev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
};
/* 声明设备结构体 */
static struct alinx_char_dev alinx_char = {
.cdev = {
.owner = THIS_MODULE,
},
};
- 第二步、实现
open
、write
和release
函数,并绑定到file_operations
结构体中。 - 第三步、为了实现驱动和设备的匹配,我们需要使用
probe
函数。这个probe
函数的功能实现包括:定义资源信息和大小
、获取设备中的资源并赋值到上一步中(platform_get_resource函数)
、将物理地址映射到虚拟地址(ioremap函数)
、寄存器赋值
、注册设备号(alloc_chrdev_region函数)
、初始化设备结构体(cdev_init函数)
、注册设备(cdev_add函数)
、创建类(class_create函数)
、创建设备节点(device_create函数)
等操作。 - 第四步、使用
remove
函数进行设备注销,功能实现包括:注销设备(cdev_del函数)
、注销设备号(unregister_chrdev_region函数)
、删除设备节点(device_destroy函数)
、删除类(class_destroy函数)
、释放对虚拟地址的占用(iounmap函数)
等操作。 - 第五步、使用
platform_driver
结构体将上述的probe
函数和remove
函数进行绑定,然后将设备代码
中定义的.name
也绑定进去! - 第六步、在
入口函数
中(也就是初始化驱动函数
)使用platform_driver_register
实现platform_driver
结构体的注册,从而实现platform驱动的成功注册。 - 第七步、在
出口函数
中(也就是退出驱动函数
)使用platform_driver_unregister
实现platform_driver
结构体的注销。 - 第八步、分别使用
module_init
和module_exit
绑定入口函数
和出口函数
。
然后我们看看和基本框架的差异!这个太多了!
- 尽管描述设备的信息和基本框架相似,但该框架实现了驱动和设备分离。
probe
函数和remove
函数实现了新模式的驱动注册和注销方式!- 还有很多,不列举了
还是来看看与基于设备树开发框架的差异!
- 注意,基于platform的平台设备驱动开发框架可以没有设备树!当然也可以使用设备树和
of
函数来获取寄存器信息。 - 在驱动的注册和注销上,和基于设备树的开发框架差异不大,也就是
probe
函数和前述的init
函数的功能相近,而remove
函数和前述的exit
函数的功能也相近。probe
函数和init
函数的差异仅仅在于前者使用platform_get_resource函数
获取platform设备中的信息,后者使用了of函数
获取设备在设备树中的信息。 - 描述LED的设备树信息几乎等同于platform设备信息,不过后者复杂了不少!
好处呢?
- 平台设备驱动模型将驱动和设备分离,驱动通过probe和remove函数与设备进行交互。这使得驱动和设备更加解耦,便于维护和扩展。
- 平台设备驱动模型支持动态创建和删除设备节点,而不需要在系统启动时静态创建。这使得设备管理更加灵活。
- 便于移植和封装,提高了代码的复用和可扩展性!
- 这个例子让我认识到了设备树的替代方式!
2.4 MISC驱动开发框架及其精简性的讨论
为什么引入MISC驱动开发框架?
和前面的驱动开发有冲突吗?
答案是不冲突,是对字符型设备的补充!
首先不同类型字符设备驱动代码中存在大量重复代码,怎么办?Linux内核通过引入MISC( Miscellaneous Interruptible Slave,杂项可中断从设备)驱动模型,将这些共通代码进行封装。
MISC框架的适用性:MISC框架适用于驱动难以归类的设备,如蜂鸣器、ADC、LED等。
MISC的本质:MISC驱动模型是对字符设备模型的封装,其本质仍是字符设备。源代码位于/drivers/char/misc.c。
MISC框架的优势:
(1) 节省主设备号:MISC设备使用固定主设备号10,通过MISC_MAJOR宏注册次设备号,从而节省主设备号资源。
(2) 简化设备创建:MISC框架通过misc_register()函数简化设备创建过程,相较于字符设备框架的多步骤,MISC提供了更简洁的设备创建方法。
(3) 驱动分层:MISC框架体现了Linux驱动分层思想,为难以分类的设备提供统一类型,便于管理和使用。
和前面一样,对代码进行精简,然后再进行比较。代码框架如下:
c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/* 设备节点名称 */
#define DEVICE_NAME "gpio_leds"
/* 设备号个数 */
#define DEVID_COUNT 1
/* 驱动个数 */
#define DRIVE_COUNT 1
/* 主设备号 */
#define MAJOR_AX
/* 次设备号 */
#define MINOR_AX 20
/* LED点亮时输入的值 */
#define ALINX_LED_ON 1
/* LED熄灭时输入的值 */
#define ALINX_LED_OFF 0
/* 定义寄存器指针 */
static u32 *GPIO_DIRM_0;
static u32 *GPIO_OEN_0;
static u32 *GPIO_DATA_0;
static u32 *APER_CLK_CTRL;
/* 定义设备结构体 */
struct alinx_char_dev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct device_node *nd; //设备树的设备节点
};
/* 声明设备结构体 */
static struct alinx_char_dev alinx_char = {
.cdev = {
.owner = THIS_MODULE,
},
};
/* 文件操作函数 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p);
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p);
static int gpio_leds_release(struct inode *inode_p, struct file *file_p);
/* 文件操作结构体 */
static struct file_operations ax_char_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
/* MISC设备结构体 */
static struct miscdevice led_miscdev = {
.minor = MINOR_AX,
.name = DEVICE_NAME,
.fops = &ax_char_fops,
};
/* probe函数 */
static int gpio_leds_probe(struct platform_device *dev)
{
/* 用于接受返回值 */
u32 ret = 0;
/* 存放 reg 数据的数组 */
u32 reg_data[10];
/* 通过节点名称获取节点 */
alinx_char.nd = of_find_node_by_name(NULL, "alinxled");
/* 4、获取 reg 属性内容 */
ret = of_property_read_u32_array(alinx_char.nd, "reg", reg_data, 8);
if(ret < 0)
{
printk("get reg failed!\r\n");
return -1;
}
else
{
/* do nothing */
}
/* 把需要修改的物理地址映射到虚拟地址 */
GPIO_DIRM_0 = ioremap(reg_data[0], reg_data[1]);
GPIO_OEN_0 = ioremap(reg_data[2], reg_data[3]);
GPIO_DATA_0 = ioremap(reg_data[4], reg_data[5]);
APER_CLK_CTRL = ioremap(reg_data[6], reg_data[7]);
/* 注册misc设备 */
ret = misc_register(&led_miscdev);
if(ret < 0) {
printk("misc device register failed\r\n");
return -EFAULT;
}
return 0;
}
/* remove函数 */
static int gpio_leds_remove(struct platform_device *dev)
{
/* 释放对虚拟地址的占用 */
iounmap(GPIO_DIRM_0);
iounmap(GPIO_OEN_0);
iounmap(GPIO_DATA_0);
iounmap(APER_CLK_CTRL);
/* 注销misc设备 */
misc_deregister(&led_miscdev);
printk("gpio_led_dev_exit_ok\n");
return 0;
}
/* 初始化of_match_table */
static const struct of_device_id led_of_match[] = {
{ .compatible = "alinxled" },
{/* Sentinel */}
};
/* 声明并初始化platform驱动 */
static struct platform_driver led_driver = {
.driver = {
.name = "alinxled",
.of_match_table = led_of_match,
},
.probe = gpio_leds_probe,
.remove = gpio_leds_remove,
};
/* 驱动入口函数 */
static int __init gpio_led_drv_init(void) {
/* 注册platform驱动 */
}
/* 驱动出口函数 */
static void __exit gpio_led_dev_exit(void) {
/* 注销platform驱动 */
}
module_init(gpio_led_drv_init);
module_exit(gpio_led_dev_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("MISC LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
现在问一个问题,MISC
到底精简了框架还是函数?
答案很简单,精简了基本框架和基于设备树的框架的init函数
和exit函数
,以及platform平台设备驱动开发框架中的probe函数
和remove函数
。
MISC
的驱动开发框架总体上和基于设备树的驱动开发框架差不多!因此流程解释大体上可以参考基于设备树的驱动开发框架。
那和基于设备树的驱动开发框架有什么不同呢?
probe
函数或者说init
函数省略了注册设备号(alloc_chrdev_region函数)
、初始化字符设备结构体(cdev_init函数)
、注册字符设备(cdev_add函数)
、创建类(class_create函数)
和创建设备节点(device_create函数)
等操作,而是按照获取设备树节点(of_find_node_by_name函数)
、获取寄存器地址(of_property_read_u32_array函数)
、映射寄存器地址(ioremap函数)
、注册misc设备(misc_register函数)
等操作完成模块注册
。remove
函数或者说exit
函数省略了注销字符设备(cdev_del函数)
、注销设备号(unregister_chrdev_region函数)
、删除设备节点(device_destroy函数)
、删除类(class_destroy函数)
等操作,而是按照释放对虚拟地址的占用(iounmap函数)
和注销misc设备(misc_deregister函数)
等操作完成模块注销
。
所以到底精简了什么,到底哪些代码是冗余的?冗余和被精简的内容如下:
c
.probe函数:
1、alloc_chrdev_region函数
2、cdev_init函数
3、cdev_add函数
4、class_create函数
5、device_create函数
.remove函数:
1、cdev_del函数
2、unregister_chrdev_region函数
3、device_destroy函数
4、class_destroy函数
好处显而易见,那有没有缺点?
应用场景比较受限吧!
但总的来说,对于字符型设备的驱动开发而言,多了一条少写代码的路子!
2.5 基于input子系统的驱动开发框架及其精简性的讨论
为什么要引入input子系统?
计算机的输入设备繁多,有按键、鼠标、键盘、触摸屏、游戏手柄等等,Linux内核为了能够将所有的输入设备进行统一的管理,设计了输入子系统。为上层应用提供了统一的抽象层,各个输入设备的驱动程序只需上报产生的输入事件即可。
input子系统怎么划分层次?
输入子系统是为了统一管理各种输入设备而设计的框架。它将输入设备的驱动程序、核心处理逻辑和事件处理逻辑分成了三个主要部分:Drivers(驱动层)
、Input Core(输入子系统核心层)
和 Handlers(事件处理层)
。
Drivers(驱动层)
:驱动层是与具体硬件设备直接交互的部分,负责管理输入设备的硬件细节,包括如下:
- 设备初始化:初始化输入设备,设置硬件寄存器,配置中断等。
- 数据采集:从硬件设备读取输入数据(如按键、触摸屏的坐标、鼠标移动等)。
- 事件生成:将硬件设备的原始数据转换为标准化的输入事件(如按键按下、触摸坐标等)。
比如,对于一个键盘设备,键盘驱动程序会处理键盘的按键扫描,检测按键按下和释放,并生成相应的按键事件。
Input Core(输入子系统核心层)
:输入子系统核心层是输入子系统的中间层,负责管理和协调输入事件的传递,包括如下:
- 设备注册:管理输入设备的注册和注销,维护一个设备列表。
- 事件分发:将驱动层生成的输入事件分发给上层的事件处理层。
- 设备属性管理:维护输入设备的属性(如设备名称、设备类型、支持的事件类型等)。
比如,当键盘驱动程序生成一个按键事件时,输入子系统核心层会接收这个事件,并将其分发给注册了该事件类型的事件处理层。
Handlers(事件处理层)
:事件处理层是输入子系统的上层部分,负责处理输入事件并将其传递给用户空间,包括如下:
- 事件处理:根据事件类型(如按键事件、触摸事件等)进行相应的处理。
- 用户空间接口:提供用户空间应用程序访问输入事件的接口,如 /dev/input/eventX 设备文件。
比如,对于按键事件,事件处理层会将事件封装为用户空间可以识别的格式,并通过 /dev/input/eventX 设备文件暴露给用户空间应用程序。
代码(来自Alinx的input子系统)如下:
c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>
#include <asm/io.h>
/* 设备节点名称 */
#define INPUT_DEV_NAME "input_key"
/* 把驱动代码中会用到的数据打包进设备结构体 */
struct alinx_char_dev {
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct device_node *nd; //设备树的设备节点
spinlock_t lock; //自旋锁变量
int alinx_key_gpio; //gpio号
unsigned int irq; //中断号
struct timer_list timer; //定时器
struct input_dev *inputdev; //input_dev结构体
unsigned char code; //input事件码
};
/* 声明设备结构体 */
static struct alinx_char_dev alinx_char = {
.cdev = {
.owner = THIS_MODULE,
},
};
/* 中断服务函数 */
static irqreturn_t key_handler(int irq, void *dev)
{
/* 按键按下或抬起时会进入中断 */
struct alinx_char_dev *cdev = (struct alinx_char_dev *)dev;
/* 开启50毫秒的定时器用作防抖动 */
mod_timer(&cdev->timer, jiffies + msecs_to_jiffies(50));
return IRQ_RETVAL(IRQ_HANDLED);
}
/* 定时器服务函数 */
void timer_function(struct timer_list *timer)
{
unsigned long flags;
struct alinx_char_dev *dev = &alinx_char;
/* value用于获取按键值 */
unsigned char value;
/* 获取锁 */
spin_lock_irqsave(&dev->lock, flags);
/* 获取按键值 */
value = gpio_get_value(dev->alinx_key_gpio);
if(value == 0)
{
/* 按键按下, 状态置1 */
input_report_key(dev->inputdev, dev->code, 1);
input_sync(dev->inputdev);
}
else
{
/* 按键抬起 */
input_report_key(dev->inputdev, dev->code, 0);
input_sync(dev->inputdev);
}
/* 释放锁 */
spin_unlock_irqrestore(&dev->lock, flags);
}
/* 模块加载时会调用的函数 */
static int __init char_drv_init(void)
{
/* 用于接受返回值 */
u32 ret = 0;
/* 初始化自旋锁 */
spin_lock_init(&alinx_char.lock);
/* 获取设备节点 */
alinx_char.nd = of_find_node_by_path("/alinxkey");
if(alinx_char.nd == NULL)
{
printk("alinx_char node not find\r\n");
return -EINVAL;
}
else
{
printk("alinx_char node find\r\n");
}
/* 获取节点中gpio标号 */
alinx_char.alinx_key_gpio = of_get_named_gpio(alinx_char.nd, "alinxkey-gpios", 0);
if(alinx_char.alinx_key_gpio < 0)
{
printk("can not get alinxkey-gpios");
return -EINVAL;
}
printk("alinxkey-gpio num = %d\r\n", alinx_char.alinx_key_gpio);
/* 申请gpio标号对应的引脚 */
ret = gpio_request(alinx_char.alinx_key_gpio, "alinxkey");
if(ret != 0)
{
printk("can not request gpio\r\n");
return -EINVAL;
}
/* 把这个io设置为输入 */
ret = gpio_direction_input(alinx_char.alinx_key_gpio);
if(ret < 0)
{
printk("can not set gpio\r\n");
return -EINVAL;
}
/* 获取中断号 */
alinx_char.irq = gpio_to_irq(alinx_char.alinx_key_gpio);
/* 申请中断 */
ret = request_irq(alinx_char.irq,
key_handler,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"alinxkey",
&alinx_char);
if(ret < 0)
{
printk("irq %d request failed\r\n", alinx_char.irq);
return -EFAULT;
}
__init_timer(&alinx_char.timer, timer_function, 0);
/* 设置事件码为KEY_0 */
alinx_char.code = KEY_0;
/* 申请input_dev结构体变量 */
alinx_char.inputdev = input_allocate_device();
alinx_char.inputdev->name = INPUT_DEV_NAME;
/* 设置按键事件 */
__set_bit(EV_KEY, alinx_char.inputdev->evbit);
/* 设置按键重复事件 */
__set_bit(EV_REP, alinx_char.inputdev->evbit);
/* 设置按键事件码 */
__set_bit(KEY_0, alinx_char.inputdev->keybit);
/* 注册input_dev结构体变量 */
ret = input_register_device(alinx_char.inputdev);
if(ret) {
printk("register input device failed\r\n");
return ret;
}
return 0;
}
/* 卸载模块 */
static void __exit char_drv_exit(void)
{
/* 删除定时器 */
del_timer_sync(&alinx_char.timer);
/* 释放中断号 */
free_irq(alinx_char.irq, &alinx_char);
/* 注销input_dev结构体变量 */
input_unregister_device(alinx_char.inputdev);
/* 释放input_dev结构体变量 */
input_free_device(alinx_char.inputdev);
}
/* 标记加载、卸载函数 */
module_init(char_drv_init);
module_exit(char_drv_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("alinx char");
MODULE_DESCRIPTION("INPUT LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
这段代码需要配合设置了按键信息的设备树一块儿使用!
前面说了input子系统有3个组件,那我们都要实现呢还是?答案是只写驱动
。input子系统的层次封装挺好,无需复杂的file_operations
结构体实现!
这个例子中只体现了probe
函数、remove
函数和module_init
、module_exit
函数!
在probe
函数中:
- 首先,通过gpio子系统的内置函数获取设备树节点信息,并完成寄存器的初始化。同时,获取设备树节点中的中断信息,并申请中断。
- 随后,通过
input_allocate_device()
函数申请input_dev
结构体变量,并提交注册事件。 - 最后,通过
input_register_device()
函数注册input_dev结构体变量。
在remove
函数中:
- 首先,通过
input_unregister_device
注销input_dev结构体变量。 - 最后,通过
input_free_device
释放input_dev结构体变量。
一个问题,有没有精简?
答案是相比于MISC
驱动开发框架精简了不少代码,并且input子系统的精简程度是最大的,这一点毫无疑问!
所以,input子系统的驱动开发框架比基本框架少了不少代码!
2.6 基于pinctrl和GPIO子系统的驱动开发框架及其精简性的讨论
问个问题,基于pinctrl和GPIO子系统做了什么?存在什么精简操作没?
先回答第一个问题吧?基于pinctrl和GPIO子系统做了什么?
前面提到过:
(1) 寄存器硬编码:驱动代码里直接写死寄存器地址与位操作。芯片一变就得大面积重写,复用性几乎为零。
(2) 设备树解耦:把寄存器基址、中断号等硬件信息移到.dts,驱动通过of_xxx()接口动态获取。硬件改动只改设备树,驱动基本不动。但还是会涉及到寄存器的编写。
而pinctrl子系统和GPIO子系统是更进一步的解决方案:pinctrl子系统负责管脚复用、电气特性;GPIO子系统提供通用gpio_set_value()、gpio_direction_output()等API。驱动不再出现具体寄存器/位操作,只调统一接口,进一步实现跨SoC的无缝迁移。
pinctrl子系统的必要性:
- 硬件引脚稀缺------STM32MP1 等SoC的可用引脚有限,但片上外设(I²C、SPI、LCD、USDHC...)数量众多。但其实也允许一个物理引脚可映射到多个外设功能(如 某些引脚既可做UART_RX,也可做GPIO/I²C_SCL)
- 手动固定引脚会导致功能锁定------工程师需手动选择每个引脚的最终功能(如将 某些引脚固定为UART_RX),硬件连线后功能即锁定。
- 在传统裸机/驱动开发中,手动配置的低效性------需在代码中逐一手动设置1)引脚映射到哪个外设;2)PAD属性(包括驱动能力、上拉/下拉等),进而导致代码冗长、移植困难,更换芯片需要重写寄存器操作。
- 缺乏统一管理的风险------不同驱动可能误将同一引脚配置为不同功能。如I²C驱动占用UART引脚,UART驱动不知情再次初始化,导致I²C失效且难调试。
第二个问题存在什么精简操作没?
的答案是当然有!后面结合代码分析!
还有个很重要的问题,我也一直很关心!这个驱动开发框架是否是可扩展的?这个问题我打算最后回答!
先看代码吧:
c
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/gpio.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/mach/map.h>
#include <asm/io.h>
#define DEVICE_NAME "gpio_leds"
#define DEVID_COUNT 1
#define DRIVE_COUNT 1
#define MAJOR1
#define MINOR1 0
#define ALINX_LED_ON 1
#define ALINX_LED_OFF 0
struct alinx_char_dev {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int alinx_led_gpio;
};
static struct alinx_char_dev alinx_char = {
.cdev.owner = THIS_MODULE,
};
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
static struct file_operations ax_char_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
static int __init gpio_led_init(void) {
alinx_char.nd = of_find_node_by_path("/alinxled");
if(!alinx_char.nd) {
printk("alinx_char node not find\r\n");
return -EINVAL;
}
alinx_char.alinx_led_gpio = of_get_named_gpio(alinx_char.nd, "alinxled-gpios", 0);
if(alinx_char.alinx_led_gpio < 0) {
printk("can not get alinxled-gpios");
return -EINVAL;
}
gpio_request(alinx_char.alinx_led_gpio, "alinxled");
gpio_direction_output(alinx_char.alinx_led_gpio, 1);
alloc_chrdev_region(&alinx_char.devid, MINOR1, DEVID_COUNT, DEVICE_NAME);
cdev_init(&alinx_char.cdev, &ax_char_fops);
cdev_add(&alinx_char.cdev, alinx_char.devid, DRIVE_COUNT);
alinx_char.class = class_create(THIS_MODULE, DEVICE_NAME);
if(IS_ERR(alinx_char.class))
return PTR_ERR(alinx_char.class);
alinx_char.device = device_create(alinx_char.class, NULL, alinx_char.devid, NULL, DEVICE_NAME);
if (IS_ERR(alinx_char.device))
return PTR_ERR(alinx_char.device);
return 0;
}
static void __exit gpio_led_exit(void) {
gpio_free(alinx_char.alinx_led_gpio);
cdev_del(&alinx_char.cdev);
unregister_chrdev_region(alinx_char.devid, DEVID_COUNT);
device_destroy(alinx_char.class, alinx_char.devid);
class_destroy(alinx_char.class);
printk("gpio_led_dev_exit_ok\n");
}
module_init(gpio_led_init);
module_exit(gpio_led_exit);
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("PINCTRL AND GPIO LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
由于pinctrl子系统的存在,GPIO可以顺利地使用内置函数去获取设备树信息、获取中断编号、设置初始值、获取寄存器的实时值,因此这种方式就不同于寄存器操纵了。得益于前者的封装,基于pinctrl子系统和GPIO的驱动开发框架属实精简了不少!
以上也是和基本框架之间的区别!
在.probe
函数中:
- 首先,使用
of_find_node_by_path
和of_get_named_gpio
获取节点指针, - 随后,使用
gpio_request
获取节点信息,使用gpio_direction_output
设置寄存器值。 - 然后,就和之前一样了,依次使用
注册设备号(alloc_chrdev_region函数)
、初始化字符设备结构体(cdev_init函数)
、注册字符设备(cdev_add函数)
、创建类(class_create函数)
和创建设备节点(device_create函数)
等操作实现模块注册。
在.remove
函数中:
- 首先,使用
gpio_free
实现节点释放。 - 然后,就和之前一样了,依次使用
注销字符设备(cdev_del函数)
、注销设备号(unregister_chrdev_region函数)
、删除设备节点(device_destroy函数)
、删除类(class_destroy函数)
等操作实现模块注销。
好,回到这个问题这个驱动开发框架是否是可扩展的?
,参考野火的说法,我们可以在设备树中添加新的引脚绑定!因此,理论上来讲,只要设备的引脚是基于MIO或者EMIO的,这个驱动开发框架都可以利用!不过我感觉这一套添加新的引脚绑定可能需要修改内核的某些文件才能实现!
有缺点没?
这个太有了!我这段时间的移植工作一直都在克服这个框架为不支持部分功能的开发板带来的种种不便!首先就是体现在中断开发上!一言难尽!
2.7 I2C驱动开发框架与platform平台驱动开发框架中驱动部分的对比
参照野火的说法:
I2C设备涵盖传感器、存储器、显示屏、电源管理等多个领域:
(1) 传感器:温湿度传感器、MPU6050运动检测传感器、光学传感器。
(2) 存储设备:E2PROM、FRAM铁电存储器。
(3) 显示设备:OLED显示屏、LCD字符显示器、触摸屏控制器。
(4) 电源管理:电压/电流监测芯片。
(5) 音频设备:数字音频编解码器、音量控制芯片。
(6) 其他:RTC实时时钟芯片、摄像头控制。
Linux系统采用总线、设备驱动模型。因此,要使用mpu6050就需要拥有"两个驱动",分别是i2c总线驱动和mpu6050设备驱动。
所以,我们需要写的是i2c总线驱动!
先贴个精简后的代码:
c
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <asm/uaccess.h>
#define AX_I2C_CNT 1
#define AX_I2C_NAME "ax_i2c_e2p"
struct ax_i2c_dev {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
void *private_data;
};
static struct ax_i2c_dev axi2cdev;
static int ax_i2c_read_regs(struct ax_i2c_dev *dev, u8 reg, void *val, int len);
static s32 ax_i2c_write_regs(struct ax_i2c_dev *dev, u8 reg, u8 *buf, int len);
static int ax_i2c_open(struct inode *inode, struct file *filp);
static ssize_t ax_i2c_read(struct file *file, char __user *buf, size_t size, loff_t *offset);
static ssize_t ax_i2c_write(struct file *file, const char __user *buf, size_t size, loff_t *offset);
static int ax_i2c_release(struct inode *inode, struct file *filp);
static const struct file_operations ax_i2c_ops = {
.owner = THIS_MODULE,
.open = ax_i2c_open,
.read = ax_i2c_read,
.write = ax_i2c_write,
.release = ax_i2c_release,
};
static int axi2c_probe(struct i2c_client *client, const struct i2c_device_id *id);
{
printk("eeprom probe\r\n");
/* 构建设备号 */
alloc_chrdev_region(&axi2cdev.devid, 0, AX_I2C_CNT, AX_I2C_NAME);
/* 注册设备 */
cdev_init(&axi2cdev.cdev, &ax_i2c_ops);
cdev_add(&axi2cdev.cdev, axi2cdev.devid, AX_I2C_CNT);
/* 创建类 */
axi2cdev.class = class_create(THIS_MODULE, AX_I2C_NAME);
if(IS_ERR(axi2cdev.class))
{
return PTR_ERR(axi2cdev.class);
}
/* 创建设备 */
axi2cdev.device = device_create(axi2cdev.class, NULL, axi2cdev.devid, NULL, AX_I2C_NAME);
if(IS_ERR(axi2cdev.device))
{
return PTR_ERR(axi2cdev.device);
}
axi2cdev.private_data = client;
return 0;
}
static void axi2c_remove(struct i2c_client *client)
{
/* 删除设备 */
cdev_del(&axi2cdev.cdev);
unregister_chrdev_region(axi2cdev.major, AX_I2C_CNT);
/* 注销类 */
device_destroy(axi2cdev.class, axi2cdev.devid);
class_destroy(axi2cdev.class);
}
static const struct of_device_id axi2c_of_match[] = {
{ .compatible = "ax-e2p1"},
{/* sentinel */}
};
static const struct i2c_device_id axi2c_id[] = {
{"ax-e2p1"},
{}
};
static struct i2c_driver axi2c_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "ax-e2p1",
.of_match_table = axi2c_of_match,
},
.id_table = axi2c_id,
.probe = axi2c_probe,
.remove = axi2c_remove,
};
static int __init ax_i2c_init(void) {
return i2c_add_driver(&axi2c_driver);
}
static void __exit ax_i2c_exit(void) {
i2c_del_driver(&axi2c_driver);
}
module_init(ax_i2c_init);
module_exit(ax_i2c_exit);
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("pwm_led");
MODULE_DESCRIPTION("I2C EEPROM driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
不妨对比platform
平台设备驱动开发框架中的驱动代码部分!两者在代码结构上的相似性非常高!
- 首先,定义了设备结构体
ax_i2c_dev
和声明变量axi2cdev
,存储设备相关的信息,如设备号、字符设备、类、设备节点、主设备号和私有数据。 - 然后,声明
file_operations
内的必要函数,并丢到fops
结构体里面。 - 随后,和前述一样的
probe
和remove
函数,定义了of_device_id
设备树匹配表和驱动id,并丢到i2c_driver
驱动结构体里面。 - 最后就是模块的加载和卸载函数。
2.8 DMA驱动开发框架与基本框架的对比
精简后的代码如下:
c
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/dmaengine.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/of_device.h>
#include <linux/async_tx.h>
#include <asm/uaccess.h>
#include <asm/delay.h>
#define DEVICE_NAME "ax_dma"
#define MAX_SIZE (512*64)
static char *src;
static char *dst;
dma_addr_t dma_src;
dma_addr_t dma_dst;
struct ax_dma_drv {
struct dma_chan *chan;
struct dma_device *dev;
struct dma_async_tx_descriptor *tx;
enum dma_ctrl_flags flags;
dma_cookie_t cookie;
};
struct ax_dma_drv ax_dma;
void dma_cb(void *dma_async_param);
static int dma_open(struct inode *inode, struct file *file);
static int dma_release(struct inode *inode, struct file *file);
static ssize_t dma_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
static const struct file_operations ax_fops = {
.owner = THIS_MODULE,
.open = dma_open,
.read = dma_read,
.release = dma_release,
};
static struct miscdevice dma_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &ax_fops,
};
static int __init dma_init(void) {
int ret = 0;
dma_cap_mask_t mask;
src = dma_alloc_coherent(dma_misc.this_device, MAX_SIZE, &dma_src, GFP_KERNEL);
dst = dma_alloc_coherent(dma_misc.this_device, MAX_SIZE, &dma_dst, GFP_KERNEL);
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);
ax_dma.chan = dma_request_channel(mask, NULL, NULL);
ax_dma.flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
ax_dma.dev = ax_dma.chan->device;
memset(src, 0x5A, MAX_SIZE);
memset(dst, 0xA5, MAX_SIZE);
return 0;
}
static void __exit dma_exit(void) {
dma_release_channel(ax_dma.chan);
dma_free_coherent(dma_misc.this_device, MAX_SIZE, src, dma_src);
dma_free_coherent(dma_misc.this_device, MAX_SIZE, dst, dma_dst);
misc_deregister(&dma_misc);
}
module_init(dma_init);
module_exit(dma_exit);
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("dma");
MODULE_DESCRIPTION("DMA driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
大体上,开发框架与基本框架大差不差!
我们重点关注init
函数和exit
函数的实现。
在init
函数上:
dma_cap_mask_t mask
定义DMA能力掩码变量。dma_alloc_coherent()函数
为源地址/目的地址分配连续的内核缓存内存。dma_cap_zero()函数
将DMA能力掩码初始化。dma_request_channel()函数
请求DMA通道。memset(src, 0x5A, MAX_SIZE);
:将源内存区域初始化为0x5A。memset(dst, 0xA5, MAX_SIZE);
:将目标内存区域初始化为0xA5。
在exit
函数上:
dma_release_channel()函数
释放之前请求的DMA通道。dma_free_coherent()函数
释放之前为源地址/目的地址分配的连续内核缓存内存。misc_deregister()函数
用于注销DMA设备。
2.9 块设备驱动开发框架与基本框架的对比
和DMA很类似,所以大体开发框架和基本框架也有共同点!
精简后的代码如下:
c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/init.h>
#include <linux/dma-direction.h>
#define AX_BLOCK_SIZE (1024*64)
#define SECTOR_SIZE 512
#define SECTORS_NUM (AX_BLOCK_SIZE / SECTOR_SIZE)
#define AX_BLOCK_NAME "ax_block"
#define AX_BLOCK_MAJOR 40
struct ax_block {
struct gendisk *block_disk;
struct request_queue *block_request;
unsigned char *block_buf;
};
static DEFINE_SPINLOCK(ax_block_lock);
static struct block_device_operations ax_block_fops = {
.owner = THIS_MODULE,
};
static void ax_block_request(struct request_queue *q);
static int __init ax_block_init(void)
{
/* 分配块设备 */
ax_block_drv.block_disk = alloc_disk(1);
/* 分配申请队列, 提供队列处理函数 */
ax_block_drv.block_request = blk_init_queue(ax_block_request, &ax_block_lock);
/* 设置申请队列 */
ax_block_drv.block_disk->queue = ax_block_drv.block_request;
/* 向内核注册块设备 */
register_blkdev(AX_BLOCK_MAJOR, AX_BLOCK_NAME);
/* 主设备号赋给块设备得主设备号字段 */
ax_block_drv.block_disk->major = AX_BLOCK_MAJOR;
/* 设置其他参数 */
ax_block_drv.block_disk->first_minor = 0;
ax_block_drv.block_disk->fops = &ax_block_fops;
sprintf(ax_block_drv.block_disk->disk_name, AX_BLOCK_NAME);
/* 设置扇区数 */
set_capacity(ax_block_drv.block_disk, SECTORS_NUM);
/* 获取缓存, 把内存模拟成块设备 */
ax_block_drv.block_buf = kzalloc(AX_BLOCK_SIZE, GFP_KERNEL);
/* 向内核注册gendisk结构体 */
add_disk(ax_block_drv.block_disk);
return 0;
}
static void __exit ax_block_exit(void)
{
/* 注销gendisk结构体 */
put_disk(ax_block_drv.block_disk);
/* 释放gendisk结构体 */
del_gendisk(ax_block_drv.block_disk);
/* 释放缓存 */
kfree(ax_block_drv.block_buf);
/* 清空内存中的队列 */
blk_cleanup_queue(ax_block_drv.block_request);
/* 卸载快设备 */
unregister_blkdev(AX_BLOCK_MAJOR, AX_BLOCK_NAME);
}
module_init(ax_block_init);
module_exit(ax_block_exit);
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("block test");
MODULE_DESCRIPTION("BLOCK driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
重点关注init
函数和exit
函数的实现。
在init
函数上:
- 首先,
alloc_disk函数
分配一个gendisk结构体,用于表示块设备。 - 然后,
blk_init_queue函数
初始化请求队列,用于处理块设备的I/O请求。 - 随后,
blk_queue函数
设置块设备的请求队列,用于管理I/O请求。 - 随后,
register_blkdev函数
注册块设备,分配主设备号并使其在内核中可用。 - 将主设备号赋值给块设备的主设备号字段。
set_capacity函数
用于设置块设备的容量,即扇区数量。- 随后,
kzalloc函数
分配内核缓存内存,用于模拟块设备的数据存储。 - 最后,
add_disk函数
将gendisk结构体添加到内核,使其可以被系统识别和使用。
在exit
函数上:
- 使用
put_disk函数
注销gendisk结构体。 - 使用
del_gendisk函数
释放结构体。 - 使用
kfree函数
释放缓存。 - 使用
blk_cleanup_queue函数
清空内存中的队列。 - 使用
unregister_blkdev函数
卸载块设备。
2.10 网卡设备驱动开发框架与基本框架的对比
代码(来源是alinx的网卡设备驱动)如下:
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/ioport.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/string.h>
#include <linux/init.h>
#include <linux/bitops.h>
#include <linux/ip.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <asm/io.h>
#include <asm/irq.h>
/* 定义一个net_device结构体变量 */
static struct net_device *ax_net_dev;
/* 模拟接收, 构造一个虚拟的sk_buff上报, 并更新统计信息 */
static void ax_net_rx(struct sk_buff *skb, struct net_device *dev)
{
unsigned char *type;
struct iphdr *ih;
__be32 *saddr, *daddr, tmp;
unsigned char tmp_dev_addr[ETH_ALEN];
struct ethhdr *ethhdr;
struct sk_buff *rx_skb;
/* 交换接受和发送方的mac地址 */
ethhdr = (struct ethhdr *)skb->data;
memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN);
memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN);
memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN);
/* 交换接受和发送方的ip地址 */
ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
saddr = &ih->saddr;
daddr = &ih->daddr;
tmp = *saddr;
*saddr = *daddr;
*daddr = tmp;
type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
/* 修改类型, 0表示reply */
*type = 0;
ih->check = 0;
ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
/* 构造sk_buff */
rx_skb = dev_alloc_skb(skb->len + 2);
skb_reserve(rx_skb, 2);
memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len);
rx_skb->dev = dev;
rx_skb->protocol = eth_type_trans(rx_skb, dev);
rx_skb->ip_summed = CHECKSUM_UNNECESSARY;
dev->stats.rx_packets++;
dev->stats.rx_bytes += skb->len;
/* 提交sk_buff */
netif_rx(rx_skb);
}
static netdev_tx_t ax_net_tx(struct sk_buff *skb, struct net_device *dev)
{
static int cnt = 0;
/* 停止上层数据下传队列 */
netif_stop_queue(dev);
/* 模拟接收, 以达到一个完成的发送接收过程 */
ax_net_rx(skb, dev);
/* 释放skb */
dev_kfree_skb (skb);
/* 发送完成, 恢复上层数据下传队列 */
netif_wake_queue(dev);
/* 更新统计信息 */
dev->stats.tx_packets++;
dev->stats.tx_bytes += skb->len;
return NETDEV_TX_OK;
}
/* 网卡设备操作函数集 */
static const struct net_device_ops ax_netdev_ops =
{
.ndo_start_xmit = ax_net_tx,
};
/* 驱动入口函数 */
static int __init ax_net_init(void)
{
/* 分配net_device结构体 */
ax_net_dev = alloc_netdev(0, "ax_net%d", NET_NAME_UNKNOWN, ether_setup);
/* 设置操作函数集 */
ax_net_dev->netdev_ops = &ax_netdev_ops;
/* 设置MAC地址 */
ax_net_dev->dev_addr[0] = 0x0A;
ax_net_dev->dev_addr[1] = 0x0B;
ax_net_dev->dev_addr[2] = 0x0C;
ax_net_dev->dev_addr[3] = 0x0D;
ax_net_dev->dev_addr[4] = 0x0E;
ax_net_dev->dev_addr[5] = 0x0F;
/* 设置ping功能 */
ax_net_dev->flags |= IFF_NOARP;
ax_net_dev->features |= NETIF_F_CSUM_MASK;
/* 注册网卡驱动 */
register_netdev(ax_net_dev);
return 0;
}
/* 驱动出口函数 */
static void __exit ax_net_exit(void)
{
unregister_netdev(ax_net_dev);
free_netdev(ax_net_dev);
}
module_init(ax_net_init);
module_exit(ax_net_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("net card test");
MODULE_DESCRIPTION("NET CARD driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
框架和基本框架有些类似,但与块设备的开发框架更接近!
注意块设备和网卡设备的fops
和基本框架(字符型设备)非常不一样,也就是并不是对file_operations
内的必要函数进行声明和定义!
块设备和网卡设备的fops
这俩的定义不是那么典型!
三、感悟
这次学习镜像启动、驱动开发和对开发平台的了解相对比较系统,通过将教程移植到不同板子上来探索成功移植的可行性和移植过程中的难点,并通过摸索解决方案、查阅资料进一步增强对驱动开发的认识。相比裸机开发,驱动开发无疑更加复杂!不止一次的尝试验证了这一点,裸机开发仍可以比较方便地找到教程可以参考,但驱动开发相对来说,需要从浩如烟海的资料里面找对合适的参考教程,因此投入的精力和时间更多!
应该说,尽管移植教程中的知识点比较零散,但到目前为止,操作系统的一些重要知识点已经通过上手驱动获得了不浅的认识!十分感谢野火的驱动开发教程和Alinx的驱动开发教程以及璞致的开发教程帮助我从零开始学习,基于此逐渐过渡到官方网站的知识库比如Xilinx wiki和Xilinx其他开发板的示例教程,以及ARM的中断文档。
尽管没有可以容纳所有知识点的移植例子,但通过这段时间的自我学习和训练,以后碰到复杂的驱动开发和测试也能做到不怵,并且心中有数,知道解决方案的大致方向!