Linux的设备模型
在前面字符设备驱动中,我们发现编写驱动有个固定的模式只要往里面套代码就可以了,它们之间的大致流程可以总结如下:
- 实现入口函数xxx_init()和卸载函数xxx_exit()
- 申请设备号 register_chrdev_region()
- 初始化字符设备,cdev_init函数、cdev_add函数 硬件初始化,如时钟寄存器配置使能,GPIO设置为输入输出模式等。
- 构建file_operation结构体内容,实现硬件各个相关的操作 在终端上使用mknod根据设备号来进行创建设备文件(节点)
- (也可以在驱动使用class_create创建设备类、在类的下面device_create创建设备节点)
因此,在Linux开发驱动,只要能够掌握了这些“套路”,开发一个驱动便不是难事。在内核源码的drivers中存放了大量的设备驱动代码, 在我们写驱动之前先查看这里的内容,说不定可以在这些目录找到想要的驱动代码。通过查看这些代码可以帮助我们熟悉开发流程。例如查看子文件夹spi下的spi-amd.c文件。如图所示:
只要这样根据步骤来编写我们的驱动代码简单粗暴,但是这存在着问题,我们将硬件的信息都写进了驱动里了, 根据某个硬件编写的驱动只要修改了一下引脚接口,这个驱动代码就得重新修改才能使用,这显然是不合理的, 那有没有合适的解决方案呢?答案是肯定的,Linux引入了设备驱动模型分层的概念, 将我们编写的驱动代码分成了两块:设备与驱动。设备负责提供硬件资源而驱动代码负责去使用这些设备提供的硬件资源。 并由总线将它们联系起来。这样子就构成以下图形中的关系。
设备模型通过几个数据结构来反映当前系统中总线、设备以及驱动的工作状况,
提出了以下几个重要概念:
- 设备(device) :挂载在某个总线的物理设备;
- 驱动(driver):与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
- 总线(bus) :负责管理挂载对应总线的设备以及驱动;
- 类(class) :对于具有相同功能的设备,归结到一种类别,进行分类管理;
我们知道在Linux中一切皆“文件”,在根文件系统中有个/sys文件目录,里面记录各个设备之间的关系。 下面介绍/sys下几个较为重要目录的作用。
/sys/bus目录下的每个子目录都是注册好了的总线类型。这里是设备按照总线类型分层放置的目录结构, 每个子目录(总线类型)下包含两个子目录——devices和drivers文件夹;其中devices下是该总线类型下的所有设备, 而这些设备都是符号链接,它们分别指向真正的设备(/sys/devices/下);如下图bus下的usb总线中的device则是Devices目 录下/pci()/dev 0:10/usb2的符号链接。而drivers下是所有注册在这个总线上的驱动,每个driver子目录下 是一些可以观察和修改的driver参数。
/sys/devices目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。一般来说, 所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices是内核对系统中所有设备的分层次表达模型, 也是/sys文件系统管理设备的最重要的目录结构。
/sys/class目录下则是包含所有注册在kernel里面的设备类型,这是按照设备功能分类的设备模型, 我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,按照设备功能分类无论它 挂载在哪条总线上都是归类到/sys/class/input下。
将它们统一起来就形成了上面的拓扑图,记录着设备与设备之间的关系。而我们的重心则放在bus文件夹目录下,创建自己的总线类型以及devices和drivers。
了解上面设备与设备的拓扑图之后,让我们再回来“总线-设备-驱动”模型中来。“总线-设备-驱动”它们之间是如何相互配合工作的呢?
在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动, 同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。在插入的同时总线会执行一个bus_type结构体中match的方法对新插入的设备/驱动进行匹配。 (它们之间最简单的匹配方式则是对比名字,存在名字相同的设备/驱动便成功匹配)。 在匹配成功的时候会调用驱动device_driver结构体中probe方法(通常在probe中获取设备资源,具体的功能可由驱动编写人员自定义), 并且在移除设备或驱动时,会调用device_driver结构体中remove方法。
以上只是设备驱动模型的机制,上面的match、probe、remove等方法需要我们来实现需要的功能。看到这里相信我们都已经对设备驱动模型有了粗略的整体认识。 无论以后学习平台设备驱动、块设备驱动或者是其他总线设备,都跟Linux设备模型息息相关。sysfs文件系统用于把内核的设备驱动导出到用户空间, 用户便可通过访问sys目录及其下的文件,来查看甚至控制内核的一些驱动设备。 接下来对总线、驱动、设备进行进一步的了解了,具体了解如何使用代码来实现创建自己的总线并在自己的总线上创建设备及驱动。 同时也可以将我们驱动的某个控制变量,导出到用户空间。
平台设备驱动
在设备驱动模型中,引入总线的概念可以对驱动代码和设备信息进行分离。 但是驱动中总线的概念是软件层面的一种抽象,与我们SOC中物理总线的概念并不严格相等:
- 物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。
- 驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为它们进行配对。
一般对于I2C、SPI、USB这些常见类型的物理总线来说,Linux内核会自动创建与之相应的驱动总线,因此I2C设备、SPI设备、 USB设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对它们进行控制并不需要特殊的时序。 它们也就没有相应的物理总线,比如led、rtc时钟、蜂鸣器、按键等等,Linux内核将不会为它们创建相应的驱动总线。 为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux内核引入了一种虚拟的总线——平台总线(platform bus)。
平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。 平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来进行表示,其继承了设备驱动模型中的device结构体。 而平台驱动使用platform_driver结构体来进行表示,其则是继承了设备驱动模型中的device_driver结构体。
平台总线
平台总线注册和匹配方式
在Linux的设备驱动模型中,总线是最重要的一环。总线是负责匹配设备和驱动, 它维护着两个链表,里面记录着各个已经注册的平台设备和平台驱动。每当有新的设备或者是新的驱动加入到总线时, 总线便会调用platform_match函数对新增的设备或驱动,进行配对。内核中使用bus_type来抽象描述系统中的总线,平台总线结构体原型如下所示:
platform_bus_type结构体(内核源码/driver/base/platform.c)
struct platform_device {
const char *name;
int id;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
/* 省略部分成员 */
};
内核用platform_bus_type来描述平台总线,该总线在linux内核启动的时候自动进行注册。
platform_bus_init函数(内核源码/driver/base/platform.c)
int __init platform_bus_init(void)
{
int error;
...
error = bus_register(&platform_bus_type);
...
return error;
}
这里重点是platform总线的match函数指针,该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线, 它都必须实例化该函数指针。platform_match的函数原型如下:
platform_match函数(内核源码/driver/base/platform.c)
static int platform_match(struct device *dev, struct device_driver *drv)
{
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);
}
第4-5行:这里调用了to_platform_device()和to_platform_driver()宏。这两个宏定义的原型如下:
to_platform_xxx宏定义(内核源码/include/linux/platform_device.h)
#define to_platform_device(x) (container_of((x), struct platform_device, dev)
#define to_platform_driver(drv) (container_of((drv), struct platform_driver, driver))
其中,to_platform_device和to_platform_driver实现了对container_of的封装, dev、driver分别作为platform_device、platform_driver的成员变量, 通过container_of宏可以获取到正在进行匹配的platform_driver和platform_device。
- 第8-21行:platform总线提供了四种匹配方式,并且这四种方式存在着优先级:设备树机制>ACPI匹配模式>id_table方式>字符串比较。虽然匹配方式五花八门,但是并没有涉及到任何复杂的算法,都只是在匹配的过程中,比较一下设备和驱动提供的某个成员的字符串是否相同。设备树是一种描述硬件的数据结构,它用一个非C语言的脚本来描述这些硬件设备的信息。驱动和设备之间的匹配时通过比较compatible的值。acpi主要是用于电源管理,基本上用不到。
container_of
container_of的作用的通过结构体成员变量地址获取这个结构体的地址,内核函数调用常常给函数传入的是结构体成员地址,然后在函数里面又想使用这个结构体里面的其他成员变量,所以就引发了这样的问题,这个也是用C实现面向对象编程的一种方法。
container_of需要传入三个参数,第一个参数是一个指针,第二个参数是结构体类型,第三个是对应第二个参数里面的结构体里面的成员。
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
* WARNING: any const qualifier of @ptr is lost.
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
static_assert(__same_type(*(ptr), ((type *)0)->member) || \
__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
({}) 这个表达式返回最后一个表达式的值。比如x=({a;b;c;d;}),最终x的值应该是d。
(type *)0
(type *)0,该如何理解这个表达式呢?
首先,type是我们传入进来的结构体类型,比如上面讲到的struct test,而这里所做的可以理解为强制类型转换:(struct test *)addr。addr可以表示内存空间的任意的地址,我们在强制转换后,默认后面一片的内存空间存储的是该数据结构。
而(type *)0的作用,也就是默认将0地址处的内存空间,转换为该数据类型。
我们就把0,当作我们正常的addr地址变量来操作,((type *)0)->member,就是获取我们结构体的成员对象。
((type *)0)->member:是一种常见的技巧,用于直接获取结构体type的成员member的类型,而不需要定义一个type类型的对象。
offsetof
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
同样用到了((TYPE *)addr),上面我们知道
((TYPE *)addr)->MEMBER:表示获取该结构体的成员
&((TYPE *)addr)->MEMBER):加了一个&,表示地址,取该成员的内存地址。
比如我们addr=0x00000010,那么&((TYPE *)0x00000010)->MEMBER)就相当于0x00000010+size
比如我们addr=0,那么&((TYPE *)0)->MEMBER)就相当于size.
id_table匹配方式
先来分析平台总线id_table匹配方式,在定义结构体platform_driver时,我们需要提供一个id_table的数组,该数组说明了当前的驱动能够支持的设备。当加载该驱动时,总线的match函数发现id_table非空, 则会比较id_table中的name成员和平台设备的name成员,若相同,则会返回匹配的条目,具体的实现过程如下:
platform_match_id函数(内核源码/drivers/base/platform.c)
static const struct platform_device_id *platform_match_id(
const struct platform_device_id *id,
struct platform_device *pdev)
{
while (id->name[0]) {
if (strcmp(pdev->name, id->name) == 0) {
pdev->id_entry = id;
return id;
}
id++;
}
return NULL;
}
可以看到这里的代码实现并不复杂,只是通过字符串进行配对。每当有新的驱动或者设备添加到总线时,总线便会调用match函数对新的设备或者驱动进行配对。 platform_match_id函数中第一个参数为驱动提供的id_table,第二个参数则是待匹配的平台设备。当待匹配的平台设备的name字段的值等于驱动提供的id_table中的值时, 会将当前匹配的项赋值给platform_device中的id_entry,返回一个非空指针。若没有成功匹配,则返回空指针。
倘若我们的驱动没有提供前三种方式的其中一种,那么总线进行匹配时,只能比较platform_device中的name字段以及嵌在platform_driver中的device_driver的name字段。
平台设备
platform_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来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下:
resource结构体(内核源码/include/linux/ioport.h)
/*
* Resources are tree-like, allowing
* nesting etc..
*/
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
/* 省略部分成员 */
};
- name: 指定资源的名字,可以设置为NULL;
- start、end: 指定资源的起始地址以及结束地址
- flags: 用于指定该资源的类型,在Linux中,资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型,最常见的有以下几种:
资源宏定义 | 描述 |
---|---|
IORESOURCE_IO | 用于IO地址空间,对应于IO端口映射方式 |
IORESOURCE_MEM | 用于外设的可直接寻址的地址空间 |
IORESOURCE_IRQ | 用于指定该设备使用某个中断 |
IORESOURCE_DMA | 用于指定使用的DMA通道 |
设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口,主要有IO端口映射和IO內存映射两种方式。 对应于IO端口映射方式,只能通过专门的接口函数(如inb、outb)才能访问; 采用IO内存映射的方式,可以像访问内存一样,去读写寄存器。在嵌入式中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM。
在资源的起始地址和结束地址中,对于IORESOURCE_IO或者是IORESOURCE_MEM,他们表示要使用的内存的起始位置以及结束位置; 若是只用一个中断引脚或者是一个通道,则它们的start和end成员值必须是相等的。
而对于软件信息,这种特殊信息需要我们以私有数据的形式进行封装保存,我们注意到platform_device结构体中,有个device结构体类型的成员dev。在前面章节,我们提到过Linux设备模型使用device结构体来抽象物理设备, 该结构体的成员platform_data可用于保存设备的私有数据。platform_data是void *类型的万能指针,无论你想要提供的是什么内容,只需要把数据的地址赋值给platform_data即可,还是以GPIO引脚号为例,示例代码如下:
示例代码
unsigned int pin = 10;
struct platform_device pdev = {
.dev = {
.platform_data = &pin;
}
}
将保存了GPIO引脚号的变量pin地址赋值给platform_data指针,在驱动程序中通过调用平台设备总线中的核心函数,可以获取到我们需要的引脚号。
注册/注销平台设备
当我们定义并初始化好platform_device结构体后,需要把它注册、挂载到平台设备总线上。注册平台设备需要使用platform_device_register()函数,该函数原型如下:
platform_device_register函数(内核源码/drivers/base/platform.c)
int platform_device_register(struct platform_device *pdev)
函数参数和返回值如下:
参数: pdev: platform_device类型结构体指针
返回值:
- 成功: 0
- 失败: 负数
同样,当需要注销、移除某个平台设备时,我们需要使用platform_device_unregister函数,来通知平台设备总线去移除该设备。
platform_device_unregister函数(内核源码/drivers/base/platform.c)
void platform_device_unregister(struct platform_device *pdev)
函数参数和返回值如下:
参数: pdev: platform_device类型结构体指针
返回值: 无
到这里,平台设备的知识已经讲解完毕,平台设备的主要内容是将硬件部分的代码与驱动部分的代码分开,注册到平台设备总线中。平台设备总线为设备和驱动之间搭建 了一座桥——统一的数据结构以及函数接口,设备和驱动的数据交互直接在“这座桥上”进行。
平台驱动
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: 表示该驱动能够兼容的设备类型
platform_device_id结构体原型如下所示:
id_table结构体(内核源码/include/linux/mod_devicetable.h)
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
注册/注销平台驱动
当我们初始化了platform_driver之后,通过platform_driver_register()函数来注册我们的平台驱动,该函数原型如下:
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()函数用于注销已注册的平台驱动,该函数原型如下:
platform_driver_unregister函数(内核源码/drivers/base/platform.c)
void platform_driver_unregister(struct platform_driver *drv);
参数: drv: platform_driver类型结构体指针
返回值: 无
上面所讲的内容是最基本的平台驱动框架,只需要实现probe函数、remove函数,初始化platform_driver结构体,并调用platform_driver_register进行注册即可。
平台驱动获取设备信息
在学习平台设备的时候,我们知道平台设备使用结构体resource来抽象表示硬件信息,而软件信息则可以利用设备结构体device中的成员platform_data来保存。 先看一下如何获取平台设备中结构体resource提供的资源。
platform_get_resource()函数通常会在驱动的probe函数中执行,用于获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针,该函数原型如下:
platform_get_resource()函数通常会在驱动的probe函数中执行,用于获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针,该函数原型如下:
platform_get_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,平台设备驱动还提供以下函数接口,来获取中断引脚,
platform_get_irq函数
int platform_get_irq(struct platform_device *pdev, unsigned int num)
参数:
- pdev: 指定要获取哪个平台设备的资源;
- num: 指定要获取的资源编号。
返回值:
- 成功: 可用的中断号
- 失败: 负数
对于存放在device结构体中成员platform_data的软件信息,我们可以使用dev_get_platdata函数来获取,函数原型如下所示:
dev_get_platdata函数
static inline void *dev_get_platdata(const struct device *dev)
{
return dev->platform_data;
}
参数:
- dev: struct device结构体类型指针
返回值: device结构体中成员platform_data指针
以上几个函数接口就是如何从平台设备中获取资源的常用的几个函数接口,到这里平台驱动部分差不多就结束了。总结一下平台驱动需要 实现probe函数,当平台总线成功匹配驱动和设备时,则会调用驱动的probe函数,在该函数中使用上述的函数接口来获取资源, 以初始化设备,最后填充结构体platform_driver,调用platform_driver_register进行注册。
Linux设备树
Linux3.x以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节。 在早些的linux内核,这些“硬件平台的板级细节”保存在linux内核目录“/arch”, 以ARM平台为例“硬件平台的板级细节”保存在“/arch/arm/plat-xxx”和“/arch/arm/mach-xxx”目录下。 随着处理器数量的增多用于描述“硬件平台板级细节”的文件越来越多导致Linux内核非常臃肿, Linux之父发现这个问题之后决定使用设备树解决这个问题。设备树简单、易用、可重用性强, linux3.x之后大多采用设备树编写驱动。
关于设备树的详细请参考:https://www.devicetree.org/
设备树简介
设备树的作用就是描述一个硬件平台的硬件资源。这个“设备树”可以被bootloader(uboot)传递到内核, 内核可以从设备树中获取硬件信息。
设备树描述硬件资源时有两个特点。
- 第一,以“树状”结构描述硬件资源。例如本地总线为树的“主干”在设备树里面称为“根节点”,挂载到本地总线的IIC总线、SPI总线、UART总线为树的“枝干”在设备树里称为“根节点的子节点”, IIC总线下的IIC设备不止一个,这些“枝干”又可以再分。
- 第二,设备树可以像头文件(.h文件)那样,一个设备树文件引用另外一个设备树文件,这样可以实现“代码”的重用。例如多个硬件平台都使用i.MX6ULL作为主控芯片,那么我们可以将i.MX6ULL芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀, 其他设备树文件直接使用“#includexxx”引用即可。
DTS、DTC和DTB它们是文档中常见的几个缩写。
-
DTS 是指.dts格式的文件,是一种ASII文本格式的设备树描述,也是我们要编写的设备树源码,一般一个.dts文件对应一个硬件平台,位于Linux源码的“/arch/arm/boot/dts”目录下。
-
DTC 是指编译设备树源码的工具,一般情况下我们需要手动安装这个编译工具。
-
DTB 是设备树源码编译生成的文件,类似于我们C语言中“.C”文件编译生成“.bin”文件。
设备树框架
简单了解了设备树的作用,我们还不知道“设备树”是究竟是个什么。可以直接打开内核源码了解设备树的框架和基本语法。
设备树给我们最直观的感受是它由一些嵌套的大括号“{}”组成, 每一个“{}”都是一个“节点”。
note上述文件需要从制作好的系统中复制过来
imx6ull.dtsi头文件 (内核源码/arch/arm/boot/dts/imx6ull.dtsi)
设备树源码分为三部分,介绍如下:
- 头文件。 设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。imx6ull.dtsi由NXP官方提供,是一个imx6ull平台“共用”的设备树文件。
- 设备树节点。 设备树给我们最直观的感受是它由一些嵌套的大括号“{}”组成, 每一个“{}”都是一个“节点”。“/ {…};”表示“根节点”,每一个设备树只有一个根节点。如果打开“imx6ull.dtsi”文件可以发现它也有一个根节点,虽然“imx6ull-seeed-npi.dts”引用了“imx6ull.dtsi”文件, 但这并不代表“imx6ull-seeed-npi.dts”设备树有两个根节点,因为不同文件的根节点最终会合并为一个。在根节点内部的“aliases {…}”、“chosen {…}”、“memory {…}”等字符,都是根节点的子节点。
- 设备树节点追加内容。 第三部分的子节点比根节点下的子节点多了一个“&”, 这表示该节点在向已经存在的子节点追加数据。这些“已经存在的节点”可能定义在“imx6ull-seeed-npi.dts”文件,也可能定义在“imx6ull-seeed-npi.dts”文件所包含的设备树文件里。 本代码中的“&cpu0 {…}”、“&clks {…}”、“&fec1 {…}”等等追加的目标节点,就是定义在“imx6ull.dtsi”中。
到目前为止我们知道设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。 设备树的组成很简单,下面我们一起来看看节点的基本格式和节点属性。
节点基本格式
设备树中的每个节点都按照以下约定命名:
节点基本格式
node-name@unit-address{
属性1 = …
属性2 = …
属性3 = …
子节点…
}
node-name 节点名称
节点名称,用于指定节点的名称。它的长度为1至31个字符,只能由“数字、大小字母、英文逗号句号、下划线和加减号”组成,节点名应当使用大写或小写字母开头并且能够描述设备类别。
注意,根节点没有节点名,它直接使用“/”指代这是一个根节点。
@unit-address
@unit-address
,其中的符号“@”可以理解为是一个分割符,“unit-address”用于指定“单元地址”, 它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unit-address”, 不过要注意这时要求同级别的设备树下(相同级别的子节点)节点名唯一,从这个侧面也可以了解到, 同级别的子节点的节点名可以相同,但是要求“单元地址”不同,node-name@unit-address
的整体要求同级唯一。
节点标签
在imx6ull.dtsi头文件中,节点名“cpu”前面多了个“cpu0”,这个“cpu0”就是我们所说的节点标签。 通常节点标签是节点名的简写,所以它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。
节点路径
通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一。 这有点类似于我们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
示例:
#address-cells和 #size-cells
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
ocrams: sram@900000 {
compatible = "fsl,lpm-sram";
reg = <0x900000 0x4000>;
};
};
#address-cells和 #size-cells属性同时存在,在设备树ocrams结构中, 它们用在有子节点的设备节点(节点),用于设置子节点的“reg”属性的“书写格式”。
补充:reg属性值由一串数字组成,如上图中的reg = <0x900000 0x4000>, reg属性的书写格式为reg = < cells cells cells cells cells cells…>,长度根据实际情况而定, 这些数据分为地址数据(地址字段),长度数据(大小字段)。
#address-cells,用于指定子节点reg属性“地址字段”所占的长度(单元格cells的个数)。 #size-cells,用于指定子节点reg属性“大小字段”所占的长度(单元格cells的个数)。
例如#address-cells=2,#size-cells=1,则reg内的数据含义为reg =
, 因为每个cells是一个32位宽的数字,例如需要表示一个64位宽的地址时,就要使用两个address单元来表示。 而假如#address-cells=1,#size-cells=1,则reg内的数据含义为reg = < address size address size address size>。总之#size-cells和#address-cells决定了子节点的reg属性中哪些数据是“地址”,哪些数据是“长度”信息。
5.reg属性
reg属性的书写格式为reg = < cells cells cells cells cells cells…>
reg属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度, 在特定情况下也有不同的含义。例如上例中#address-cells = <1>,#size-cells = <1>,reg = <0x9000000 x4000>, 其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的reg属性指定了起始地址为0x9000000,长度为0x4000的一块地址空间。
6.ranges
示例:
*ranges属性*
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
busfreq {
/*-------------以下内容省略--------------*/
};
}
该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = <子地址, 父地址, 转换长度>。 如果父地址空间和子地址空间相同则无需转换,如示例中所示,只写了renges,内容为空,我们也可以直接省略renges属性。
比如对于#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操作函数。
Pinctrl子系统和gpio子系统
我们学会了使用设备树来描述外设的各种信息(比如寄存器地址), 而不是将寄存器的这些内容放在驱动代码里。 这样即使设备信息修改了,我们还是可以通过设备树的接口函数,去灵活的获取设备的信息。 极大得提高了驱动的复用能力。
我们可以通过在驱动程序代码里使用设备树接口,来获取到外设的信息了。 但是,我们还是将寄存器操作具体细节体现在了驱动中,比如置位操作。
那么,在驱动中有没有更通用的方法,可以不涉及到具体的寄存器操作的内容呢? 对于有些外设,是具备抽象条件的,也就是说我们可以将对这些外设的操作统一起来。
pinctrl子系统
无论是裸机还是驱动, 一般首先要设置引脚的复用功能并且设置引脚的PAD属性(驱动能力、上下拉等等)。在驱动程序中我们需要手动设置每个引脚的复用功能,不仅增加了工作量,编写的驱动程序不方便移植, 可重用性差等。更糟糕的是缺乏对引脚的统一管理,容易出现引脚的重复定义。
pinctrl子系统是由芯片厂商来实现的主要用于管理芯片的引脚。imx6ull芯片拥有众多的片上外设, 大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制。芯片的设计厂商为了提高硬件设计的灵活性, 一个芯片引脚往往可以做为多个片上外设的功能引脚。pinctrl子系统用于帮助我们管理芯片引脚并自动完成引脚的初始化, 而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。
iomuxc节点介绍
首先我们在源码/arch/arm/boot/dts/imx6ull.dtsi文件中查找iomuxc节点,可以看到如下定义
imx6ull.dtsi中iomuxc部分
- compatible: 修饰的是与平台驱动做匹配的名字,这里则是与pinctrl子系统的平台驱动做匹配。
- reg: 表示的是引脚配置寄存器的基地址。
imx6ull.dtsi这个文件是芯片厂商官方将芯片的通用的部分单独提出来的一些设备树配置。 在iomuxc节点中汇总了所需引脚的配置信息,pinctrl子系统存储使用着iomux节点信息。
我们的设备树主要的配置文件在~/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/igkboard-imx6ull.dts中,在文件中搜索“&iomuxc”找到设备树中引用“iomuxc”节点的位置如下所示。
pinctrl子节点编写格式
这里我们需要知道每个芯片厂商的pinctrl子节点的编写格式并不相同,这不属于设备树的规范,是芯片厂商自定义的。 如果我们想添加自己的pinctrl节点,只要依葫芦画瓢按照上面的格式编写即可。
pinctrl_自定义名字: 自定义名字 {
fsl,pins = <
引脚复用宏定义 PAD(引脚)属性 /*引脚配置信息*/
引脚复用宏定义 PAD(引脚)属性
>;
};
引脚的配置信息由一个宏定义和一个16进制数组成。
#define MX6UL_PAD_NAND_DQS__GPIO4_IO16 0x01b8 0x0444 0x0000 5 0
define MX6UL_PAD_NAND_DQS__GPIO4_IO16是定义在“./arch/arm/boot/dts/imx6ul-pinfunc.h”文件内的一个宏定义。
每个宏定义后面有5个参数,名字依次为 mux_reg、conf_reg、input_reg、mux_mode、input_val。
mux_reg | conf_reg | input_reg | mux_mode | input_val |
复用寄存器 | 电气属性寄存器 | 偏移为0,表示没有input功能 | 复用为GPIO4_IO016,将0x5写入复用寄存器 | 写入input寄存器的值 |
0x01b8 | 0x0444 | 0x0000 | 0x5 | 0x0 |
将led灯引脚添加到pinctrl子系统
查找原理图找到LED灯对应的引脚,这里使用GPIO5_IO08
找到引脚配置宏定义,在imx6ul-pinfunc.h查找GPIO5_IO08
设置引脚属性为0x17059(参照内核的设备树,默认设置成0x17059)。
在igkboard-imx6ull.dts文件中iomuxc节点下添加pinctrl子节点。
GPIO子系统
pinctrl子系统重点是设置PIN的复用和电气属性,如果pinctrl子系统将一个PIN复用位GPIO的话,那么接下来就要用到GPIO子系统了。
GPIO子系统顾名思义就是用于初始化GPIO并且提供相应的API函数,比如设置GPIO位输入输出,读取GPIO的值。gpio子系统的主要目的就是方便驱动开发者使用GPIO。
在设备树中添加gpio相关信息,然后就可以在驱动程序使用gpio子系统提供的API函数来操作GPIO。
在没有使用GPIO子系统之前,如果我们想点亮一个LED,首先要得到led相关的配置寄存器,再手动地读、改、写这些配置寄存器实现 控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统帮我们完成,我们只需要调用GPIO子系统提供的API函数即可完成GPIO的 控制动作。
在设备树中添加LED灯的设备树节点
igkboard-imx6ull.dts
编译、下载设备树验证修改结果
编译Linux内核源码
下并解压内核源码后:
配置基础config文件
这里直接copy imx的配置文件
cp .config /home/chenyujiang/imx6ull/bsp/kernel/linux-imx
运行如下命令编译
make ARCH=arm CROSS_COMPILE=/opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-
在编译模块时Makefile文件如下:
ARCH := arm
CROSS_COMPILE := /opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-
TFTP_DIR := /tftp/chenyujiang/
#KERNEL_DIR := /home/lingyun/build-imxboard/igkboard-imx6ull/kernel/linux-imx
KERNEL_DIR := /home/chenyujiang/imx6ull/bsp/kernel/linux-imx/
PWD :=$(shell pwd)
obj-m := chrdevbase.o
NAME := chrdevbaseApp
modules:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
$(CROSS_COMPILE)gcc $(NAME).c -o $(NAME)
@make clear
cp $(NAME) chrdevbase.ko $(TFTP_DIR) -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
在内核源码中编译内核
编译内核时会自动编译设备树,我们所要用到的设备树文件都存放在 内核源码/arch/arm/boot/dts/ 里面,但是编译内核很耗时, 所以我们推荐使用如下命令只编译设备树。
sudo make dtbs ARCH=arm CROSS_COMPILE=/opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-
注意上调命令必须保证编译过内核即执行过上一小节的内容!
编译成功后会在“./arch/arm/boot/dts”目录下生成“.dtb”文件,将其替换掉板子目录下的.dtb文件并重启开发板。
使用新的设备树重新启动之后正常情况下会在开发板的“/proc/device-tree”目录下生成“my_leds”设备树节点。如下所示。
查看my_leds文件,可以看到我们设置的gpio子系统的属性。
在驱动中调用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。
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 声明它们。 用户程序必须,当然,包含那个头文件来存取这些符号。
选择 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 参数而得到。
Led驱动编写
驱动程序大致分为三个部分
第一部分,编写平台设备驱动的入口和出口函数。
第二部分,编写平台设备的.probe函数, 在probe函数中实现字符设备的注册和LED灯的初始化。
第三部分,编写字符设备函数集,实现open和write函数。
/*********************************************************************************
* Copyright: (C) 2024 chenyujiang<yjchen81@foxmail.com>
* All rights reserved.
*
* Filename: led-gpio.c
* Description: This file
*
* Version: 1.0.0(2024年03月27日)
* Author: chenyujiang <yjchen81@foxmail.com>
* ChangeLog: 1, Release initial version on "2024年03月27日 13时55分25秒"
*
********************************************************************************/
#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"
#ifndef DEV_MAJOR
#define DEV_MAJOR 0
#endif
//type can be understand to the dev type
#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;
struct class *class;
struct device *device;
struct device_node *node;
struct gpio_desc *led_gpio;
};
struct led_device led_dev;
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 released.\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:
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,
};
static int led_probe(struct platform_device *pdev)
{
int result = 0;
printk("\t match successed \n");
memset(&led_dev,0,sizeof(led_dev));
led_dev.led_gpio = gpiod_get(&pdev->dev,"led", 0);
if(IS_ERR(led_dev.led_gpio))
{
printk("gpiod request failure\n");
return PTR_ERR(led_dev.led_gpio);
}
// why set the direction as output
result = gpiod_direction_output(led_dev.led_gpio, 0);
if(0 != result)
{
printk("gpiod direction output set failed\n");
return result;
}
if(0 != dev_major)
{
led_dev.devid = MKDEV(dev_major,0);
result = register_chrdev_region(led_dev.devid, 1, DEV_NAME);
}
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 driv use major %d\n", DEV_NAME, dev_major);
led_dev.cdev.owner = THIS_MODULE;
cdev_init(&(led_dev.cdev),&led_fops);
result = cdev_add(&(led_dev.cdev), led_dev.devid, 1);
if(result != 0)
{
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);
led_dev.class = class_create(THIS_MODULE,DEV_NAME);
if(IS_ERR(led_dev.class))
{
printk("%s driver create class failed\n", DEV_NAME);
result = -ENOMEM;
goto ERROR;
}
led_dev.device = device_create(led_dev.class, NULL, led_dev.devid, NULL, DEV_NAME);
if(IS_ERR(led_dev.device))
{
result = -ENOMEM;
goto ERROR;
}
return 0;
ERROR:
printk(KERN_ERR" %s driver installed failed.\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);
cdev_del(&(led_dev.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"},
{},
};
MODULE_DEVICE_TABLE(of, leds_match_table);
static struct platform_driver gpio_led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "my_led",
.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);
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("chenyujiang");
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
ARCH := arm
CROSS_COMPILE := /opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-
#KERNEL_DIR := /home/lingyun/build-imxboard/igkboard-imx6ull/kernel/linux-imx
KERNEL_DIR := /home/chenyujiang/linux-imx-lf-6.1.36-2.1.0/
PWD :=$(shell pwd)
NAME := led_gpio
obj-m := $(NAME).o
NAME_APP := $(NAME)_App
modules:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
$(CROSS_COMPILE)gcc $(NAME_APP).c -o $(NAME_APP)
@make clear
#cp $(NAME_APP) $(NAME).ko $(TFTP_DIR) -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
Led应用编写
/*********************************************************************************
* Copyright: (C) 2024 chenyujiang<yjchen81@foxmail.com>
* All rights reserved.
*
* Filename: led_gpio_App.c
* Description: This file
*
* Version: 1.0.0(2024年03月27日)
* Author: chenyujiang <yjchen81@foxmail.com>
* ChangeLog: 1, Release initial version on "2024年03月27日 16时41分16秒"
*
********************************************************************************/
#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 void 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;
}