版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/ 发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
3.4 Linux驱动模型——按键驱动
在前面字符设备驱动中,我们发现编写驱动有个固定的模式只有往里面套代码就可以了,它们之间的大致流程可以总结如下:
实现入口函数xxx_init()和卸载函数xxx_exit()
申请设备号 register_chrdev_region()
初始化字符设备,cdev_init函数、cdev_add函数
硬件初始化,如时钟寄存器配置使能,GPIO设置为输入输出模式等。
构建file_operation结构体内容,实现硬件各个相关的操作
在终端上使用mknod根据设备号来进行创建设备文件(节点) (也可以在驱动使用class_create创建设备类、在类的下面device_create创建设备节点)
在内核源码的/home/imx6ull/bsp/kernel/linux-imx/drivers中存放了大量的设备驱动代码, 在我们写驱动之前先查看这里的内容,说不定可以在这些目录找到想要的驱动代码。如图所示:
GuoWenxue@9d57f9229b66:~/imx6ull/bsp/kernel/linux-imx/drivers$ ls
accessibility fsi modules.order s390
acpi gnss most sbus
amba gpio mtd scsi
android gpu mux sh
只要这样根据步骤来编写我们的驱动代码简单粗暴,它有一个严重的问题,就是设备信息和驱动代码杂糅在一起,在我们驱动程序中各种硬件寄存器地址随处可见。本质上,这种驱动开发方式与单片机的驱动开发并没有太大的区别,一旦硬件信息发生变化甚至设备已经不在了,就必须要修改驱动源码。
Linux引入了设备驱动模型分层的概念, 将我们编写的驱动代码分成了两层:设备与驱动。设备负责提供硬件资源,而驱动代码负责去使用这些设备提供的硬件资源。 并由总线将它们联系起来。这样子就构成以下图形中的关系。
设备模型通过几个数据结构来反映当前系统中总线、设备以及驱动的工作状况,提出了以下几个重要概念:
设备(device) :挂载在某个总线的物理设备;
驱动(driver) :与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
总线(bus) :负责管理挂载对应总线的设备以及驱动;
类(class) :对于具有相同功能的设备,归结到一种类别,进行分类管理;
我们知道在Linux中一切皆“文件”,在根文件系统中有个/sys文件目录,里面记录各个设备之间的关系。 下面介绍/sys下几个较为重要目录的作用。
GuoWenxue@9d57f9229b66:/sys$ ls
block class devices fs kernel power
bus dev firmware hypervisor module
/sys/bus目录下的每个子目录都是注册好了的总线类型。这里是设备按照总线类型分层放置的目录结构, 每个子目录(总线类型)下包含两个子目录——devices和drivers文件夹;
GuoWenxue@9d57f9229b66:/sys/bus/usb$ ls
devices drivers
devices下是该总线类型下的所有设备, 而这些设备都是符号链接,它们分别指向真正的设备(/sys/devices/下);
drivers下是所有注册在这个总线上的驱动,每个driver子目录下是一些可以观察和修改的driver参数。
/sys/devices目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。一般来说, 所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices是内核对系统中所有设备的分层次表达模型, 也是/sys文件系统管理设备的最重要的目录结构。
GuoWenxue@9d57f9229b66:/sys/devices$ ls
breakpoint system uncore_imc_1
cpu tracepoint uncore_imc_4
cstate_core uncore_cbox_0 uncore_imc_5
cstate_pkg uncore_cbox_1 uncore_pcu
/sys/class目录下则是包含所有注册在kernel里面的设备类型,这是按照设备功能分类的设备模型, 我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,按照设备功能分类无论它挂载在哪条总线上都是归类到/sys/class/input下。
GuoWenxue@9d57f9229b66:/sys/class$ ls
ata_device devfreq gpio lirc nvme-generic printer scsi_disk usb_role
ata_link devfreq-event graphics mdio_bus nvme-subsystem ptp scsi_generic vc
ata_port devlink hwmon mem pci_bus pwm tty
将它们统一起来就形成了上面的拓扑图,记录着设备与设备之间的关系。在bus文件夹目录下,创建自己的总线类型以及devices和drivers。
了解上面设备与设备的拓扑图之后,让我们再回来“总线-设备-驱动”模型中来。“总线-设备-驱动”它们之间是如何相互配合工作的呢?
在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动, 同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。在插入的同时总线会执行一个bus_type结构体中match的方法对新插入的设备/驱动进行匹配。 (它们之间最简单的匹配方式则是对比名字,存在名字相同的设备/驱动便成功匹配)。
在匹配成功的时候会调用驱动device_driver结构体中probe方法(通常在probe中获取设备资源,具体的功能可由驱动编写人员自定义), 并且在移除设备或驱动时,会调用device_driver结构体中remove方法。
以上只是设备驱动模型的 机制 ,上面的match、probe、remove等方法需要我们来实现需要的功能。 sysfs文件系统用于把内核的设备驱动导出到用户空间, 用户便可通过访问sys目录及其下的文件,来查看甚至控制内核的一些驱动设备。
3.4.1 platform 设备驱动
在设备驱动模型中, 引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象,与我们SOC中物理总线的概念并不严格相等:
物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为它们进行配对。
一般对于I2C、SPI、USB这些常见类型的物理总线来说,Linux内核会自动创建与之相应的驱动总线,因此I2C设备、SPI设备、 USB设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对它们进行控制并不需要特殊的时序。 它们也就没有相应的物理总线,比如led、rtc时钟、蜂鸣器、按键等等,Linux内核将不会为它们创建相应的驱动总线。
为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux内核引入了一种虚拟的总线——平台总线(platform bus)。 平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。 平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来进行表示,其继承了设备驱动模型中的device结构体。 而平台驱动使用platform_driver结构体来进行表示,其则是继承了设备驱动模型中的device_driver结构体。
3.4.2 platform设备
3.4.2.1 platfrom_device结构体
内核使用platform_device结构体来描述平台设备,结构体原型如下:
platform_device结构体在内核源码/include/linux/platform_device.h中
struct platform_device {
const char *name;
int id;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
/* 省略部分成员 */
};
name: 设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致;
id: 指定设备的编号,Linux支持同名的设备,而同名设备之间则是通过该编号进行区分;
dev: Linux设备模型中的device结构体,linux内核大量使用了面向对象思想,platform_device通过继承该结构体可复用它的相关代码,方便内核管理平台设备;
num_resources: 记录资源的个数,当结构体成员resource存放的是数组时,需要记录resource数组的个数,内核提供了宏定义ARRAY_SIZE用于计算数组的个数;
resource: 平台设备提供给驱动的资源,如irq,dma,内存等等。该结构体会在接下来的内容进行讲解;
id_entry: 平台总线提供的另一种匹配方式,原理依然是通过比较字符串,这部分内容会在平台总线小节中讲,这里的id_entry用于保存匹配的结果;
平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。
硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源、IO口等等
软件信息:以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等
对于硬件信息,使用结构体struct resource来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下
3.4.2.2 注册/注销平台设备
当我们定义并初始化好platform_device结构体后,需要把它注册、挂载到平台设备总线上。注册平台设备需要使用platform_device_register()函数,该函数原型如下:
int platform_device_register(struct platform_device *pdev)
函数参数和返回值如下:
参数: pdev: platform_device类型结构体指针
返回值:
成功: 0
失败: 负数
同样,当需要注销、移除某个平台设备时,我们需要使用platform_device_unregister函数,来通知平台设备总线去移除该设备。
void platform_device_unregister(struct platform_device *pdev)
函数参数和返回值如下:
参数: pdev: platform_device类型结构体指针
返回值: 无
平台设备的主要内容是将硬件部分的代码与驱动部分的代码分开,注册到平台设备总线中。平台设备总线为设备和驱动之间搭建 了一座桥——统一的数据结构以及函数接口,设备和驱动的数据交互直接在“这座桥上”进行。
3.4.3 platform 驱动
3.4.3.1 platform_driver结构体
内核中使用platform_driver结构体来描述平台驱动,结构体原型如下所示:
platform_driver结构体在内核源码/include/platform_device.h中
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
};
probe: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化。
remove: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是probe函数实现操作的逆过程。
driver: Linux设备模型中用于抽象驱动的device_driver结构体,platform_driver继承该结构体,也就获取了设备模型驱动对象的特性;
id_table: 表示该驱动能够兼容的设备类型。
3.4.3.2 注册/注销平台驱动
当我们初始化了platform_driver之后,通过platform_driver_register()函数来注册我们的平台驱动,该函数原型如下:
int platform_driver_register(struct platform_driver *drv);
函数参数和返回值如下:
参数: drv: platform_driver类型结构体指针
返回值:
成功: 0
失败: 负数
由于platform_driver继承了driver结构体,结合Linux设备模型的知识, 当成功注册了一个平台驱动后,就会在/sys/bus/platform/driver目录下生成一个新的目录项。
当卸载的驱动模块时,需要注销掉已注册的平台驱动,platform_driver_unregister()函数用于注销已注册的平台驱动,该函数原型如下:
void platform_driver_unregister(struct platform_driver *drv);
参数: drv: platform_driver类型结构体指针
返回值: 无
上面的内容是最基本的平台驱动框架,只需要实现probe函数、remove函数,初始化platform_driver结构体,并调用platform_driver_register进行注册即可。
3.4.3.3 平台总线注册和匹配方式
在Linux的设备驱动模型中总线是负责匹配设备和驱动, 它维护着两个链表,里面记录着各个已经注册的平台设备和平台驱动。每当有新的设备或者是新的驱动加入到总线时, 总线便会调用platform_match函数对新增的设备或驱动,进行配对。
内核用platform_bus_type来描述平台总线,platform_bus_type结构体在内核源码/driver/base/platform.c中,该总线在linux内核启动的时候自动进行注册。
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);
用platform_bus_init函数向linux内核注册platform平台总线。
这里重点是platform总线的match函数指针,该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线, 它都必须实例化该函数指针。
platform_match函数在内核源码/driver/base/platform.c中:
static int platform_match(struct device *dev, struct device_driver *drv)
{
/*调用了to_platform_device()和to_platform_driver()宏*/
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);
}
#define to_platform_device(x) (container_of((x), struct platform_device, dev)
#define to_platform_driver(drv) (container_of((drv), struct platform_driver, driver))
通过container_of宏可以获取到正在进行匹配的platform_driver和platform_device。
platform总线提供了四种匹配方式,并且这四种方式存在着优先级:设备树机制>ACPI匹配模式>id_table方式>字符串比较。
虽然匹配方式五花八门,但是并没有涉及到任何复杂的算法,都只是在匹配的过程中,比较一下设备和驱动提供的某个成员的字符串是否相同。
设备树是一种描述硬件的数据结构,它用一个非C语言的脚本来描述这些硬件设备的信息。驱动和设备之间的匹配时通过比较compatible的值。
acpi主要是用于电源管理,基本上用不到。
3.4.3.4 平台驱动获取设备信息
平台设备使用结构体resource来抽象表示硬件信息,而软件信息则可以利用设备结构体device中的成员platform_data来保存。
platform_get_resource()函数通常会在驱动的probe函数中执行,用于获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针,该函数原型如下
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);
参数:
dev: 指定要获取哪个平台设备的资源;
type: 指定获取资源的类型,如IORESOURCE_MEM、IORESOURCE_IO等;
num: 指定要获取的资源编号。每个设备所需要资源的个数是不一定的,为此内核对这些资源进行了编号,对于不同的资源,编号之间是相互独立的。
返回值:
成功: struct resource结构体类型指针
失败: NULL
假若资源类型为IORESOURCE_IRQ,平台设备驱动还提供以下函数接口,来获取中断引脚
int platform_get_irq(struct platform_device *pdev, unsigned int num)
参数:
pdev: 指定要获取哪个平台设备的资源;
num: 指定要获取的资源编号。
返回值:
成功: 可用的中断号
失败: 负数
3.4.4 Linux设备树
Linux3.x以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节(开发板上的设备信息),比如CPU数量,内存基地址,IIC接口上接了哪些设备、SI接口接了哪些设备。 在早些的linux内核,这些“硬件平台的板级细节”保存在linux内核目录“/arch”, 以ARM平台为例“硬件平台的板级细节”保存在“/arch/arm/plat-xxx”和“/arch/arm/mach-xxx”目录下。 随着处理器数量的增多用于描述“硬件平台板级细节”的文件越来越多导致Linux内核非常臃肿, Linux之父发现这个问题之后决定使用设备树解决这个问题。设备树简单、易用、可重用性强, linux3.x之后大多采用设备树编写驱动。
关于设备树的详细请参考:https://www.devicetree.org/
3.4.4.1 设备树简介
设备树的作用就是描述一个硬件平台的硬件资源。这个“设备树”可以被bootloader(uboot)传递到内核, 内核可以从设备树中获取硬件信息。
设备树描述硬件资源时有两个特点:
①树的主干就是系统总线,在设备树里面称为“根节点”。IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支,在设备树里称为“根节点的子节点”。
②设备树可以像头文件(.h文件)那样,一个设备树文件引用另外一个设备树文件, 这样可以实现“代码”的重用。例如多个硬件平台都使用i.MX6ULL作为主控芯片, 那么我们可以将i.MX6ULL芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀, 其他设备树文件直接使用“# includexxx”引用即可。
DTS、DTC和DTB它们是文档中常见的几个缩写。
DTS 是指.dts格式的文件,是一种ASII 文本格式的设备树描述,也是我们要编写的设备树源码,一般一个.dts文件对应一个硬件平台,位于Linux源码的“/arch/arm/boot/dts”目录下。
DTC 是指编译设备树源码的工具,一般情况下我们需要手动安装这个编译工具。
DTB 是设备树源码编译生成的文件,类似于我们C语言中“.C”文件编译生成“.bin”文件。
3.4.4.2 设备树框架
简单了解了设备树的作用,我们还不知道“设备树”是究竟是个什么。可以直接打开内核源码了解设备树的框架和基本语法。
设备树给我们最直观的感受是它由一些嵌套的大括号“{}”组成, 每一个“{}”都是一个“节点”。
GuoWenxue@9d57f9229b66:~$ cd imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/
GuoWenxue@9d57f9229b66:~/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts$ vim igkboard.dts
/*
* Device Tree Source for LingYun IGKBoard(IoT Gateway Kit Board)
* Based on imx6ul-14x14-evk.dts/imx6ul-14x14-evk.dtsi
*
* Copyright (C) 2022 LingYun IoT System Studio.
* Author: Guo Wenxue<guowenxue@gmail.com>
*/
/dts-v1/;
#include "imx6ull.dtsi" /*头文件*/
/*设备树根节点*/
/ {
model = "LingYun IoT System Studio IoT Gateway Board"; /*model属性,用于指定设备的制造商和型号*/
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull"; /*compatible属性,系统用来决定绑定到设备驱动的关键,用来查找节点的方法之一*/
/*根节点的子节点*/
chosen {
stdout-path = &uart1;
};
/*根节点的子节点*/
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
/*根节点的子节点*/
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0xa000000>;
linux,cma-default;
};
};
/*根节点的子节点*/
leds {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio_leds>;
status = "okay";
sysled {
lable = "sysled";
gpios = <&gpio4 16 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "off";
};
};
/*-------------以下内容省略-------------*/
};
/*设备树节点追加内容*/
/*+--------------+
| Misc Modules |
+--------------+*/
/*而是向原有节点追加内容*/
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
&pwm1 { /* backlight */
#pwm-cells = <2>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm1>;
status = "okay";
};
&pwm2 {
#pwm-cells = <2>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm2>;
status = "okay";
};
设备树源码分为三部分:
头文件,设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。imx6ull.dtsi由NXP官方提供,是一个imx6ull平台“共用”的设备树文件。
设备树节点,“/ {…};”表示“根节点”,每一个设备树只有一个根节点。不同文件的根节点最终会合并为一个。在根节点内部的“chosen{…}”、memory{…}”、“reserved-memory{…}”、“leds{…}”等字符,都是根节点的子节点。
设备树节点追加内容,子节点比根节点下的子节点多了一个“&”, 这表示该节点在向已经存在的子节点追加数据。本代码中的“&pwm1{…}”、“&uart1{…}”等等追加的目标节点,就是定义在“imx6ul.dtsi”中。
imx6ull.dtsi头文件,在内核源码/arch/arm/boot/dts/imx6ull.dtsi
pwm1: pwm@2080000 { /*节点标签:节点名称@单元地址*/
compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm"; /*model属性用于指定设备的制造商和型号*/
reg = <0x02080000 0x4000>; /*reg属性描述设备资源在其父总线定义的地址空间内的地址*/
interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>; /*描述中断相关的信息*/
clocks = <&clks IMX6UL_CLK_PWM1>, /*初始化GPIO外设时钟信息*/
<&clks IMX6UL_CLK_PWM1>;
clock-names = "ipg", "per";
#pwm-cells = <3>; /*表示有多少个cells来描述pwm引脚*/
status = "disabled"; /*状态属性用于指示设备的“操作状态”*/
};
到目前为止我们知道设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。 设备树的组成很简单,下面我们一起来看看节点的基本格式和节点属性。
3.4.4.3 节点基本格式
设备树中的每个节点都按照以下约定命名:
node-name@unit-address{
属性1 = …
属性2 = …
属性3 = …
子节点…
}
node-name 节点名称,用于指定节点的名称。它的长度为1至31个字符,只能由“数字、大小字母、英文逗号句号、下划线和加减号”组成,节点名应当使用大写或小写字母开头并且能够描述设备类别。
@unit-address,其中的符号“@”可以理解为是一个分割符,“unit-address”用于指定“单元地址”, 它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unit-address”。注意同级别的设备树下相同级别的子节点节点名唯一
node-name@unit-address
的整体要求同级唯一。节点标签,节点名的简写,当其它位置需要引用时可以使用节点标签来向该节点中追加内容。在imx6ul.dtsi头文件中,节点名“pwm”前面多了个“pwm1”,这个“pwm1”就是我们所说的节点标签。
节点路径,通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,“不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一”。类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。
节点属性:节点的“{}”中包含的内容是节点属性,通常情况下一个节点包含多个属性信息, 这些属性信息就是要传递到内核的“板级硬件描述信息”,驱动中会通过一些API函数获取这些信息。
例如根节点“/”就有属性compatible = “fsl,imx6ull-14x14-evk”, “fsl,imx6ull”。 我们可以通过该属性了解到硬件设备相关的名字叫“imx6ull-14x14-evk”,设备所使用的的是“imx6ull”这颗 SOC。
设备树最主要的内容是编写节点的节点属性,通常情况下一个节点代表一个设备。
1.compatible属性
compatible属性值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。
设备树中的每一个设备的节点都要有一个compatible属性。系统通过compatible属性决定绑定哪一个设备的设备驱动,是用来查找节点的方法之一,也可以通过节点名或节点路径查找指定节点。
例如系统初始化时会初始化platform总线上的设备时,根据设备节点”compatible”属性和驱动中of_match_table对应的值加载对应的驱动。
2.model属性
model属性用于指定设备的制造商和型号。
3.status属性
状态属性用于指示设备的“操作状态”,通过status可以去禁止设备或者启用设备,默认情况下不设置status属性设备是使能的。
4.#address-cells 和 #size-cells
#size-cells和#address-cells决定了子节点的reg属性中哪些数据是“地址”,哪些数据是“长度”信息。
#address-cells,用于指定子节点reg属性“地址字段”所占的长度(单元格cells的个数)。
#size-cells,用于指定子节点reg属性“大小字段”所占的长度(单元格cells的个数)。
例如#address-cells=2,#address-cells=1,则reg内的数据含义为reg =
, 每个cells是一个32位宽的数字。5.reg属性
ret属性的书写格式为reg = < cells cells cells cells cells cells…>
reg属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度, 在特定情况下也有不同的含义。
例如#address-cells = <1>,#address-cells = <1>,reg = <0x9000000 x4000>, 其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的reg属性指定了起始地址为0x9000000,长度为0x4000的一块地址空间。
6.ranges
该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是 <子地址、父地址、地址长度>。如果父地址空间和子地址空间相同则无需转换。
比如对于#address-cells和#size-cells都为1的话,以ranges=<0x0 0x10 0x20>为例,表示将子地址的从0x0~(0x0 + 0x20)的地址空间映射到父地址的0x10~(0x10 + 0x20)。
7.name和device_type
这两个属性很少用(已经被废弃),不推荐使用。name用于指定节点名,在旧的设备树中它用于确定节点名, 现在我们使用的设备树已经弃用。device_type属性也是一个很少用的属性,只用在CPU和内存的节点上。 如上例中所示,device_type用在了CPU节点。
我们在设备树中添加了一个“led”节点, 正常情况下我们可以从这个节点获取编写led驱动所用到的所有信息,例如led相关控制寄存器地址、 led时钟控制寄存器地址等等。
内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数以of_开头,称为OF操作函数。
3.4.5 Pinctrl子系统和gpio子系统
我们学会了使用设备树来描述外设的各种信息(比如寄存器地址), 而不是将寄存器的这些内容放在驱动代码里。 这样即使设备信息修改了,我们还是可以通过设备树的接口函数,去灵活的获取设备的信息。 极大得提高了驱动的复用能力。
我们可以通过在驱动程序代码里使用设备树接口,来获取到外设的信息了。 但是,我们还是将寄存器操作具体细节体现在了驱动中,比如置位操作。
那么,在驱动中有没有更通用的方法,可以不涉及到具体的寄存器操作的内容呢? 对于有些外设,是具备抽象条件的,也就是说我们可以将对这些外设的操作统一起来。
3.4.5.1 pinctrl子系统
无论是裸机还是驱动, 一般首先要设置引脚的复用功能并且设置引脚的PAD属性(驱动能力、上下拉等等)。在驱动程序中我们需要手动设置每个引脚的复用功能,不仅增加了工作量,编写的驱动程序不方便移植, 可重用性差等。更糟糕的是缺乏对引脚的统一管理,容易出现引脚的重复定义。
pinctrl子系统是由芯片厂商来实现的主要用于管理芯片的引脚。imx6ull芯片拥有众多的片上外设, 大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制。芯片的设计厂商为了提高硬件设计的灵活性, 一个芯片引脚往往可以做为多个片上外设的功能引脚。pinctrl子系统用于帮助我们管理芯片引脚并自动完成引脚的初始化, 而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。
3.4.5.2 pinctrl子系统编写格式
iomuxc节点介绍
首先我们在/home/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/imx6ul.dtsi文件中查找iomuxc节点,可以看到如下定义
iomuxc: pinctrl@20e0000 {
compatible = "fsl,imx6ul-iomuxc"; /*pinctrl子系统的平台驱动做匹配的名字*/
reg = <0x020e0000 0x4000>; /*引脚配置寄存器的基地址*/
};
imx6ull.dtsi这个文件是芯片厂商官方将芯片的通用的部分单独提出来的一些设备树配置。 在iomuxc节点中汇总了所需引脚的配置信息,pinctrl子系统存储使用着iomux节点信息。
我们的设备树主要的配置文件在~/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/imx6ull-myir-mys-6ulx.dtsi 中,在文件中搜索“&iomuxc”找到设备树中引用“iomuxc”节点的位置如下所示。
/*+----------------------+
| Basic pinctrl iomuxc |
+----------------------+*/
&iomuxc {
pinctrl-names = "default"; /*指定PIN的状态列表,默认设置为“default”*/
pinctrl_camera_clock: cameraclockgrp {
fsl,pins = <
MX6UL_PAD_CSI_MCLK__CSI_MCLK 0x1b088 /*将0x1b088写给电气属性寄存器,以确定pin的电器属性*/
>;
};
pinctrl_gpio_leds: gpio-leds { /*pinctrl子节点*/ */
fsl,pins = <
MX6UL_PAD_NAND_DQS__GPIO4_IO16 0x17059 /* led run */
>;
};
pinctrl子节点编写格式
这里我们需要知道每个芯片厂商的pinctrl子节点的编写格式并不相同,这不属于设备树的规范,是芯片厂商自定义的。 如果我们想添加自己的pinctrl节点,只要依葫芦画瓢按照上面的格式编写即可。
pinctrl_自定义名字: 自定义名字 {
fsl,pins = <
引脚复用宏定义 PAD(引脚)属性 /*引脚配置信息*/
引脚复用宏定义 PAD(引脚)属性
>;
};
引脚配置信息介绍
引脚的配置信息由一个宏定义和一个16进制数组成。
MX6UL_PAD_NAND_DQS__GPIO4_IO16 是定义在“./arch/arm/boot/dts/imx6ul-pinfunc.h”文件内的一个宏定义。
#define MX6UL_PAD_NAND_DQS__GPIO4_IO16 0x01b8 0x0444 0x0000 5 0
mux_reg |
conf_reg |
input_reg |
mux_mode |
input_val |
---|---|---|---|---|
复用寄存器 |
电气属性寄存器 |
偏移为0,表示没有input功能 |
复用为GPIO4_IO016,将0x5写入复用寄存器 |
写入input寄存器的值 |
0x01b8 |
0x0444 |
0x0000 |
0x5 |
0x0 |
3.4.5.3 将led灯引脚添加到pinctrl子系统
查找原理图找到LED灯对应的引脚,这里使用GPIO5_IO08
找到引脚配置宏定义,在imx6ul-pinfunc.h查找GPIO5_IO08
设置引脚属性为0x17059(参照内核的设备树,默认设置成0x17059)。
在igkboard.dts文件中iomuxc节点下添加pinctrl子节点
pinctrl_my_gpio_leds: my-gpio-leds {
fsl,pins = <
MX6UL_PAD_SNVS_TAMPER8_GPIO5_IO08 0x17059 /* led run */
>;
};
3.4.5.4 GPIO子系统
pinctrl子系统重点是设置PIN的复用和电气属性,如果pinctrl子系统将一个PIN复用位GPIO的话,那么接下来就要用到GPIO子系统了。
GPIO子系统顾名思义就是用于初始化GPIO并且提供相应的API函数,比如设置GPIO位输入输出,读取GPIO的值。gpio子系统的主要目的就是方便驱动开发者使用GPIO。
在设备树中添加gpio相关信息,然后就可以在驱动程序使用gpio子系统提供的API函数来操作GPIO。
3.4.5.5 在设备树中添加LDE灯的设备树节点
my_leds {
compatible = "my-gpio-leds"; /*设置“compatible”属性值,与led的平台驱动做匹配*/
pinctrl-names = "default"; /*定义引脚状态*/
pinctrl-0 = <&pinctrl_my_gpio_leds>; /*指定LED灯的引脚pinctrl信息*/
status = "okay";
led-gpios = <&gpio5 8 GPIO_ACTIVE_HIGH>; /*指定引脚使用的哪个GPIO 引脚名字= <&GPIO组 GPIO编号 有效电平>*/
default-state = "off";
};
3.4.5.6 编译、下载设备树验证修改结果
我们分别在设备树中将LED灯使用的引脚添加到pinctrl子系统,然后又在设备树中添加了my_leds设备树节点。编译、下载修改后的设备树,用新的设备树启动系统,然后检查是否有my_leds设备树节点产生。
在内核目录下cp /home/imx6ull/bsp/kernel/linux-imx执行如下命令,只编译设备树:
make dtbs
编译成功后会在“./arch/arm/boot/dts”目录下生成“.dtb”文件,将其替换掉板子目录下的.dtb文件并输入 sudo reboot
重启开发板。
#这里操作命令仅作为参考,实际根据自己电脑情况进行修改#
#1.新的dtb文件#
cp ./dts/igkboard.dtb /tftp/xxx -f
#2.zImage文件在boot下#
cp zImage /tftp/xxx -f
然后重启开发板:(随便按那个按键,进入NetBoot) 并输入 sudo reboot 重启开发板。
使用新的设备树重新启动之后正常情况下会在开发板的“/proc/device-tree”目录下生成“my_leds”设备树节点。如下所示。
root@igkboard:/proc/device-tree# ls
'#address-cells' aliases clock-di1 leds name regulator-sd1-vmmc sound-mqs
'#size-cells' backlight-lcd clock-osc memory@80000000 pmu regulator@0 timer
1p8v chosen compatible model pwm-buzzer reserved-memory w1
3p3v clock-cli cpus mqs pxp_v4l2 serial-number
__symbols__ clock-di0 keys my_leds regulator-peri-3v3 soc
root@igkboard:/proc/device-tree/my_leds# ls
compatible default-state gpios name pinctrl-0 pinctrl-names status
查看my_leds文件,可以看到我们设置的gpio子系统的属性。
3.4.5.7 在驱动中调用GPIO子系统
在设备树中指定了 GPIO 引脚,在驱动代码中如何使用?也就是 GPIO 子系统的接口函数是什么?
GPIO子系统有两套接口:
一是基于描述符(
descriptor-based
)的,相关api函数都是以”gpiod_
”为前缀,它使用gpio_desc
结构来表示一个引脚。另一种是老(
legency
)的,相关api函数都是以”gpio_
”为前缀,它使用一个整数来表示一个引脚,强烈建议不要使用legacy的接口函数。
要操作一个引脚,首先要 get 引脚,然后设置方向,读值、写值。
驱动程序中要包含头文件:
#include <linux/gpio/consumer.h> // descriptor-based
或
#include <linux/gpio.h> // legacy
下表列出常用的函数:
descriptor-based |
legacy |
说明 |
---|---|---|
获得 GPIO |
||
gpiod_get |
gpio_request |
|
gpiod_get_index |
||
gpiod_get_array |
gpio_request_array |
|
devm_gpiod_get |
||
devm_gpiod_get_index |
||
devm_gpiod_get_array |
||
设置方向 |
||
gpiod_direction_input |
gpio_direction_input |
|
gpiod_direction_output |
gpio_direction_output |
|
读值、写值 |
||
gpiod_get_value |
gpio_get_value |
|
gpiod_set_value |
gpio_set_value |
|
释放 GPIO |
||
gpio_free |
gpio_free |
|
gpiod_put |
gpio_free_array |
|
gpiod_put_array |
||
devm_gpiod_put |
||
devm_gpiod_put_array |
有前缀“devm_”的含义是“设备资源管理”(Managed Device Resource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。
比如在 Linux 开发过程中,先申请了 GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放 GPIO 资源。
如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的 GPIO 资源。
建议使用“devm_”版本的相关函数。
举例,假设备在设备树中有如下节点:
led_device {
compatible = "gpio_led";
...
led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */
<&gpio 16 GPIO_ACTIVE_HIGH>, /* green */
<&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */
power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
};
那么可以使用下面的函数获得引脚:
struct gpio_desc *red, *green, *blue, *power;
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);
要注意的是,gpiod_set_value 设置的值是“逻辑值”,不一定等于物理值。
旧的“gpio_”函数没办法根据设备树信息获得引脚,它需要先知道引脚号。
在 GPIO 子系统中,每注册一个 GPIO Controller 时会确定它的“base number”,那么这个控制器里的第 n 号引脚的号码就是:base number + n。
但是如果硬件有变化、设备树有变化,这个 base number 并不能保证是固定的,应该查看 sysfs 来确定 base number。
3.4.6 ioctl 接口
大部分驱动需要除了读写设备的能力,还需要有通过设备驱动进行各种硬件控制的能力。大部分设备可进行超出简单的数据传输之外的操作;用户空间必须常常能够请求。 例如,设备上锁, 报告错误信息,改变波特率, 或者自我销毁。 这些操作常常通过 ioctl 方法来支持, 它通过相同名子的系统调用来实现。
在用户空间, ioctl 系统调用有下面的原型:
int ioctl(int fd, unsigned long cmd, ...);
原型中的点(…)不表示一个变数目的参数,而是一个单个可选的参数,传统上标识为 char *argp。这些点在那里只是为了阻止在编译时的类型检查。 第 3 个参数的实际特点依赖所发出的特定的控制命令( 第 2 个参数 )。 一些命令不用参数,一些用一个整数值,以及一些使用指向其他数据指针。使用一个指针是传递任意数据到 ioctl 调用的方法; 设备接着可与用户空间交换任何数量的数据。
ioctl 驱动函数有和用户空间版本不同的原型:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数。
cmd 参数从用户那里不改变地传下来,并且可选的参数。
arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针。
因为类型检查在这个额外参数上被关闭,编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找。
大部分 ioctl 实现使用一个大的 switch 语句来根据 cmd 参数,选择正确的做法。不同的命令有不同的数值,它们常常被给予符号名来简化编码。 符号名通过一个预处理定义来安排。定制的驱动常常声明这样的符号在它们的头文件中; scull.h 为 scull 声明它们。 用户程序必须,当然,包含那个头文件来存取这些符号。
3.4.6.1 选择 ioctl 命令
在为 ioctl 编写代码之前, 你需要选择对应命令的数字。许多程序员的第一个本能的反应是选择一组小数从0或1开始,并且从此开始向上。 但是,ioctl 命令数字应当在这个系统是唯一的, 为了阻止向错误的设备发出正确的命令而引起的错误。这样的不匹配是可能发生的, 如果 ioctl 号是唯一的,这个应用程序得到一个 EINVAL 错误而不是继续做不应当做的事情。
根据 Linux 内核惯例来为你的驱动选择 ioctl 号,你应当首先检查 include/asm/ioctl.h 和 Documentation/ioctl-number.txt。这个头文件定义你将使用的位段: type(魔数), 序号, 传输方向, 和参数大小。 ioctl-number.txt 文件列举了在内核中使用的魔数,因此你将可选择你自己的魔数并且避免交叠。 这个文本文件也列举了为什么应当使用惯例的原因。
定义 ioctl 命令号的正确方法使用 4 个位段, 它们有下列的含义。 这个列表中介绍的新符号定义在 <linux/ioctl.h>。
type
魔数。 只是选择一个数(在参考了 ioctl-number.txt之后)并且使用它在整个驱动中。 这个成员是 8 位宽(_IOC_TYPEBITS)。
number
序(顺序)号。它是 8 位(_IOC_NRBITS)宽。
direction
数据传送的方向,如果这个特殊的命令涉及数据传送。 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE,和 _IOC_READ|_IOC_WRITE (数据在2个方向被传送)。数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间。 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取。
size
涉及到的用户数据的大小。 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位。你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值。 你使用这个 size 成员不是强制的 - 内核不检查它 – 但是它是一个好主意。 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容,如果你曾需要改变相关数据项的大小。如果你需要更大的数据结构,但是, 你可忽略这个 size 成员。我们很快见到如何使用这个成员。
头文件 <asm/ioctl.h>, 它包含在 <linux/ioctl.h> 中,定义宏来帮助建立命令号, 如下: _IO(type,nr)(给没有参数的命令), _IOR(type, nre, datatype)(从驱动中读数据的), _IOW(type,nr,datatype)(写数据),和 _IOWR(type,nr,datatype)(双向传送)。 type 和 number 成员作为参数被传递,并且 size 成员通过应用 sizeof 到 datatype 参数而得到。
3.4.7 Led驱动编写
驱动程序大致分为三个部分
第一部分,编写平台设备驱动的入口和出口函数。
第二部分,编写平台设备的.probe函数, 在probe函数中实现字符设备的注册和LED灯的初始化。
第三部分,编写字符设备函数集,实现open和write函数。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#define DEV_NAME "my_led" //最后在/dev路径下的设备名称,应用层open的字符串名
/*Set the LED dev major number*/
//#define DEV_MAJOR 79
#ifndef DEV_MAJOR
#define DEV_MAJOR 0
#endif
#define PLATDRV_MAGIC 0x60 //魔术字
#define LED_OFF _IO (PLATDRV_MAGIC, 0x18)
#define LED_ON _IO (PLATDRV_MAGIC, 0x19)
static int dev_major = DEV_MAJOR; /*主设备号*/
struct led_device {
dev_t devid; /* 设备号 */
struct cdev cdev; /*cdev结构体*/
struct class *class; /*定义一个class用于创建类 */
struct device *device; /*设备 */
struct device_node *node; /* led设备节点 */
struct gpio_desc *led_gpio; /*led灯GPIO描述符 */
};
struct led_device led_dev; //led设备
/*字符设备操作函数集,write函数*/
static int led_open(struct inode *inode, struct file *file)
{
file->private_data = &led_dev; //设置私有数据
printk(KERN_DEBUG "/dev/led%d opened.\n", led_dev.devid);
return 0;
}
static int led_release(struct inode *inode, struct file *file)
{
printk(KERN_DEBUG "/dev/led%d opened.\n", led_dev.devid);
return 0;
}
static void print_led_help(void)
{
printk("Follow is the ioctl() command for LED driver:\n");
printk("Turn LED on command : %u\n", LED_ON);
printk("Turn LED off command : %u\n", LED_OFF);
}
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd)
{
case LED_OFF:/* variable case */
gpiod_set_value(led_dev.led_gpio, 0);
break;
case LED_ON:
gpiod_set_value(led_dev.led_gpio, 1);
break;
default:
printk("%s driver don't support ioctl command=%d\n", DEV_NAME, cmd);
print_led_help();
return -EINVAL;
}
return 0;
}
/*字符设备操作函数集*/
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.unlocked_ioctl = led_ioctl,
};
/*probe函数实现字符设备的注册和LED灯的初始化*/
static int led_probe(struct platform_device *pdev)
{
int result = 0; //用于保存申请设备号的结果
printk( "\t match successed \n");
memset(&led_dev, 0, sizeof(led_dev));
/*获取led的设备树节点*/
led_dev.led_gpio = gpiod_get(&pdev->dev, "led", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if (IS_ERR(led_dev.led_gpio))
{
printk("gpiod request failure\n");
return PTR_ERR(led_dev.led_gpio);
}
result = gpiod_direction_output(led_dev.led_gpio, 0);//设置GPIO的方向为输出,默认电平为低
if (0 != result)
{
printk("gpiod drection output set failure\n");
return result;
}
/*---------------------注册 字符设备部分-----------------*/
/*1.分配主次设备号,这里即支持静态指定,也至此动态申请*/
if (0 != dev_major) /*static*/
{
led_dev.devid = MKDEV(dev_major, 0);
result = register_chrdev_region(led_dev.devid, 1, DEV_NAME); /* /proc/devices/my_led */
}
else
{
result = alloc_chrdev_region(&led_dev.devid, 0, 1, DEV_NAME); /*动态申请字符设备号*/
dev_major = MAJOR(led_dev.devid); /*获取主设备号*/
}
/*自动分配设备号失败*/
if (result < 0)
{
printk("%s driver can't get major %d", DEV_NAME, dev_major);
return result;
}
printk(" %s driver use major %d\n", DEV_NAME, dev_major);
/*2.分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核*/
led_dev.cdev.owner = THIS_MODULE; /*.owner这表示谁拥有你这个驱动程序*/
cdev_init(&(led_dev.cdev), &led_fops); /*初始化cdev,把fops添加进去*/
result = cdev_add(&(led_dev.cdev), led_dev.devid, 1); /*注册给内核,设备数量1个*/
if (0 != result)
{
printk( " %s driver can't register cdev:result=%d\n", DEV_NAME, result);
goto ERROR;
}
printk( " %s driver can register cdev:result=%d\n", DEV_NAME, result);
//3.创建类,驱动中进行节点创建
led_dev.class = class_create(THIS_MODULE, DEV_NAME);/* sys/class/my_led 创建类*/
if(IS_ERR(led_dev.class))
{
printk("%s driver create class failure\n", DEV_NAME);
result = -ENOMEM;
goto ERROR;
}
//4.创建设备
led_dev.device = device_create(led_dev.class, NULL, led_dev.devid, NULL, DEV_NAME);/* /dev/my_led 创建设备节点*/
if(IS_ERR(led_dev.device)) {
result = -ENOMEM; //返回错误码,应用空间strerror查看
goto ERROR;
}
return 0;
ERROR:
printk(KERN_ERR" %s driver installed failure.\n", DEV_NAME);
cdev_del(&(led_dev.cdev)); //删除字符设备
unregister_chrdev_region(led_dev.devid, 1);//释放主次设备号
return result;
}
static int led_remove(struct platform_device *pdev)
{
gpiod_set_value(led_dev.led_gpio, 0); //低电平关闭灯
gpiod_put(led_dev.led_gpio); //释放gpio
cdev_del(&(led_dev.cdev)); //删除cdev
unregister_chrdev_region(led_dev.devid, 1);//释放设备号
device_destroy(led_dev.class, led_dev.devid);//注销设备
class_destroy(led_dev.class); //注销类
return 0;
}
/*------------------第一部分----------------*/
static const struct of_device_id leds_match_table[] = {
{.compatible = "my-gpio-leds"},
{/* sentinel */},
};
MODULE_DEVICE_TABLE(of, leds_match_table);
/*定义平台驱动结构体*/
static struct platform_driver gpio_led_driver = {
.probe = led_probe, //驱动安装时候会执行的钩子函数
.remove = led_remove, //驱动卸载时候
.driver = { //描述这个驱动的属性
.name = "my_led", //不建议用的name域
.owner = THIS_MODULE,
.of_match_table = leds_match_table,
},
};
/*------------------第二部分----------------*/
/*驱动初始化函数*/
static int __init platdrv_led_init(void)
{
int rv = 0;
rv = platform_driver_register(&gpio_led_driver); //注册platform的led驱动
if(rv)
{
printk(KERN_ERR "%s:%d: Can't register platform driver %d\n", __FUNCTION__, __LINE__, rv);
return rv;
}
printk("Regist LED Platform Driver successfully!\n");
return 0;
}
/*------------------第三部分----------------*/
/*驱动注销函数*/
static void __exit platdrv_led_exit(void)
{
printk("%s():%d remove LED platform driver\n", __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_led_driver); //卸载驱动
}
module_init(platdrv_led_init);
module_exit(platdrv_led_exit);
MODULE_AUTHOR("Lingyun");
MODULE_LICENSE("GPL");
代码的第一部分,仅实现.probe函数和.driver,当驱动和设备匹配成功后会执行该函数。
.driver描述这个驱动的属性包括.name驱动的名字,.owner驱动的所有者, .of_match_table驱动匹配表,用于匹配驱动和设备。
驱动设备匹配表定义为“my_leds”在这个表里只有一个匹配值 “.compatible = “my_gpio_led” ”这个值要与我们在设备树中my_leds设备树节点的“compatible”属性相同。
第二、三部分是平台设备的入口和出口函数,函数实现很简单,在入口函数中注册平台驱动,在出口函数中注销平台驱动。
MODULE_DEVICE_TABLE的两个功能:
MODULE_DEVICE_TABLE(of,match_table)
of:设备类型
match_table:设备表
一是:将设备加入到外设队列中
二是:告诉程序阅读者该设备是热插拔设备或是说该设备支持热插拔功能。
将match_table结构输出到用户空间,这样模块加载系统在加载模块时,就知道了什么模块对应什么硬件设备。
Makefile
GuoWenXue@9d57f9229b66:~/driver/04led_gpiod$ vim Makefile
KERNAL_DIR := /home/zying/imx6ull/bsp/kernel/linux-imx
CROSS_COMPILE := /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-
TFTP_DTR := /tftp/zouying
PWD := $(shell pwd)
obj-m := led_gpio.o
modules:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
$(CROSS_COMPILE)gcc led_App.c -o led_App
@make clear
cp led_gpio.ko led_App $(TFTP_DTR) -f
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
clean:
@rm -f *.ko
3.4.8 Led设备驱动测试
3.4.8.1 编译Led设备驱动
在linux开发主机上,编译Linux驱动,并安装到tftp服务器根路径下。
GuoWenXue@9d57f9229b66:~/driver/04led_gpiod$ ls
led_App led_App.c led_gpio.c led_gpio.ko Makefile
3.4.8.2 ARM板上下载并安装驱动
root@igkboard:~/dirver/04led_gpio# ls
root@igkboard:~/dirver/04led_gpio# wget http://studio.iot-yun.club:7000/pub/zouying/led_App
Connecting to studio.iot-yun.club:7000 (27.16.213.10:7000)
saving to 'led_App'
led_App 100% |***************************************************************************| 11908 0:00:00 ETA
'led_App' saved
root@igkboard:~/dirver/04led_gpio# wget http://studio.iot-yun.club:7000/pub/zouying/led_gpio.ko
Connecting to studio.iot-yun.club:7000 (27.16.213.10:7000)
saving to 'led_gpio.ko'
led_gpio.ko 100% |***************************************************************************| 10732 0:00:00 ETA
'led_gpio.ko' saved
root@igkboard:~/dirver/04led_gpio# insmod led_gpio.ko
root@igkboard:~/dirver/04led_gpio# lsmod
Module Size Used by
led_gpio 16384 0
rtl8188fu 991232 0
imx_rngc 16384 0
rng_core 20480 1 imx_rngc
secvio 16384 0
error 20480 1 secvio
root@igkboard:~/dirver/04led_gpio# dmesg
[ 264.308713] Regist LED Platform Driver successfully!
在驱动程序中,我们在.probe函数中注册字符设备并创建了设备文件,设备和驱动匹配成功后.probe函数已经执行, 所以正常情况下在“/dev/”目录下已经生成了“my-leds”设备节点,如下所示。
root@igkboard:/dev# ls
autofs input mmcblk1p2 ram12 stderr tty21 tty38 tty54 v4l vcsu3
block kmsg mmcblk1rpmb ram13 stdin tty22 tty39 tty55 vcs vcsu4
bus log mqueue ram14 stdout tty23 tty4 tty56 vcs1 vcsu5
char loop-control mxc_asrc ram15 tty tty24 tty40 tty57 vcs2 vcsu6
console loop0 my_led ram2 tty0 tty25 tty41 tty58 vcs3 vga_arbiter
cpu_dma_latency loop1 net ram3 tty1 tty26 tty42 tty59 vcs4 vhci
3.4.9 Led应用编写
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#define LED_CNT 1
#define DEVNAME_LEN 30
#define PLATDRV_MAGIC 0x60
#define LED_OFF _IO (PLATDRV_MAGIC, 0x18)
#define LED_ON _IO (PLATDRV_MAGIC, 0x19)
static inline msleep(unsigned long ms)
{
struct timeval tv;
tv.tv_sec = ms/1000;
tv.tv_usec = (ms%1000)*1000;
select(0, NULL, NULL, NULL, &tv);
}
int main(int argc, char **argv)
{
int fd[LED_CNT];
char dev_name[DEVNAME_LEN];
memset(dev_name, 0, sizeof(dev_name));
snprintf(dev_name, sizeof(dev_name), "/dev/my_led");
fd[LED_CNT] = open(dev_name, O_RDWR, 0755);
if(fd[LED_CNT] < 0)
{
printf("file %s open failure!\n", dev_name);
goto err;
}
printf("open fd : %s [%d] successfully.\n", dev_name, fd[LED_CNT]);
while(1)
{
ioctl(fd[LED_CNT], LED_ON);
msleep(300);
ioctl(fd[LED_CNT], LED_OFF);
msleep(300);
}
close(fd[LED_CNT]);
return 0;
err:
close(fd[LED_CNT]);
return -1;
}
3.4.9.1 编译Led测试程序
GuoWenXue@9d57f9229b66:~/driver/04led_gpiod$ ls
led_App led_App.c led_gpio.c led_gpio.ko Makefile
3.4.10 ARM开发板上测试Led设备
root@igkboard:~/dirver/04led_gpio# ls
led_App led_gpio.ko
root@igkboard:~/dirver/04led_gpio# ./led_App
open fd : /dev/my_led [3] successfully.
这时我们就可以看到连接开发板的LED灯开始闪烁了,时间间隔为300ms。