自学内容网 自学内容网

嵌入式Linux学习: 设备树实验

设备树(DeviceTree)是一种硬件描述机制,用于在嵌入式系统和操作系统中描述硬件设备的特性、连接关系和配置信息。它提供了一种与平台无关的方式来描述硬件,使得内核与硬件之间的耦合度降低,提高了系统的可移植性和可维护性。

1. 为什么需要设备树这种机制?

在Linux内核V2.6版本之前,尤其是ARM架构下,用于描述不同硬件信息的文件大多存放在arch/arm/plat-xxxarch/arm/mach-xxx文件夹下。这些文件包含了大量的板级硬件信息,而这些信息对于内核的通用性来说并无实质性帮助,反而导致了内核代码的冗余和复杂化。随着每年新推出的ARM架构芯片和基于这些芯片的板子数量不断增加,板级信息文件也呈指数级增长,这使得内核维护变得异常困难
设备树通过一种结构化的方式来描述硬件平台,包括CPU、内存、外设、总线等资源。这种描述方式使得驱动程序和内核代码能够更容易地适应不同的硬件平台,而无需对内核代码进行大量的修改。通过更换设备树文件(.dtb),即可实现不同主板的无差异支持,从而大大提高了代码的可移植性和可重用性

2. 设备树基础知识

设备树专业术语介绍:

DTS(Device Tree Source) : DTS 是设备树的源文件, 采用一种类似于文本的语法来描述硬件设备的结构、 属性和连接关系。 DTS 文件以.dts 为扩展名, 通常由开发人员编写。 它是人类可读的形式, 用于描述设备树的层次结构和属性信息。

DTSI(Device Tree Source Include) : DTSI 文件是设备树源文件的包含文件。 它扩展了 DTS文件的功能, 用于定义可重用的设备树片段。DTSI 文件以.dtsi 为扩展名, 可以在多个 DTS 文件中包含和共享。 通过使用 DTSI, 可以提高设备树的可重用性和可维护性(和 C 语言中头文件的作用相同) 。

DTB(Device Tree Blob) : DTB 是设备树的二进制表示形式。 DTB 文件是通过将 DTS 或 DTSI文件编译而成的二进制文件, 以.dtb 为扩展名。 DTB 文件包含了设备树的结构、 属性和连接信息, 被操作系统加载和解析。 在运行时, 操作系统使用 DTB 文件来动态识别和管理硬件设备。

DTC(Device Tree Compiler) : DTC 是设备树的编译器。 它是一个命令行工具, 用于将 DTS和 DTSI 文件编译成 DTB 文件。 DTC 将文本格式的设备树源代码转换为二进制的设备树表示形 式, 以便操作系统能够加载和解析。 DTC 是设备树开发中一个重要的工具。

DTS、 DTSI、 DTB 和 DTC 之间的关系:

  • 开发人员使用文本编辑器编写 DTS 和 DTSI 文件, 描述硬件设备的层次结构、 属性和连接关系。
  • DTSI 文件可以在多个 DTS 文件中包含和共享, 以提高设备树的可重用性和可维护性。
  • 使用 DTC 编译器, 开发人员将 DTS 和 DTSI 文件编译成二进制的 DTB 文件
    在这里插入图片描述
  • 操作系统在启动过程中加载和解析 DTB 文件, 以识别和管理硬件设备。

设备树文件路径
ARM体系结构: I.MX6ULL

(内核目录)/arch/arm/boot/dts

瑞芯微的RK3568

(内核目录)/arch/arm64/boot/dts/rockchip
DTC工具的使用

在 Linux 内核源码中, DTC(Device Tree Compiler) 的源代码和相关工具通常存放在(内核目录)/scripts/dtc/目录中在这里插入图片描述
在编译完源码之后 dtc 设备树编译器会默认生成, 如果没有生成相应的 dtc 可执行文件,可以查看在内核默认配置文件中((内核目录)/.config) CONFIG_DTC 是否使能。
在这里插入图片描述

① 设备树的编译

dtc -I dts -O dtb -o output.dtb input.dts

其中, input.dts是输入的设备树源文件, output.dtb是编译后的二进制设备树文件。
编译器会验证设备树源文件的语法和语义, 生成与硬件描述相对应的设备树表示形式。

②设备树的反编译

dtc -I dtb -O dts -o output.dts input.dtb

将二进制设备树文件反编译为设备树源文件
input.dtb 是输入的二进制设备树文件,output.dts是反编译后的设备树源文件

3.设备树基本语法

3.1根节点

设备树使用一种层次结构,其中的根节点(Root Node)是整个设备树的起始点和顶层节点。

/dts-v1/;   // 设备树版本信息
/{ // 根节点开始
// 可以在里面添加描述根节点的属性和配置
};  

子节点格式如下

[label:] node-name@[unit-address] {
[properties definitions]
[child nodes]
};
  • 节点标签(Label) (可选) : 节点标签是一个可选的标识符, 用于在设备树中引用该节点。 标签允许其他节点直接引用此节点, 以便在设备树中建立引用关系。
  • 节点名称(Node Name) : 节点名称是一个字符串, 用于唯一标识该节点在设备树中的位置。 节点名称通常是硬件设备的名称, 但必须在设备树中是唯一的。
  • 单元地址(Unit Address)(可选):单元地址用于标识设备的实例。 它可以是一个整数、 一个十六进制值或一个字符串, 具体取决于设备的要求。 单元地址的目的是区分相同类型的设备的不同实例, 例如在下图 中名为 cpu 的节点通过它们的单元地址值 0 和 1 来区分, 名称为 ethernet 的节点通过其单元地址值 fe002000 和 fe003000 来区分。
    在这里插入图片描述
  • 属性定义(Properties Definitions) : 属性定义是一组键值对, 用于描述设备的配置和特性。 属性可以根据设备的需求进行定义, 例如寄存器地址、 中断号、 时钟频率等
  • 子节点(Child Nodes) : 子节点是当前节点的子项, 用于进一步描述硬件设备的子组件或配置。 子节点可以包含自己的属性定义和更深层次的子节点, 形成设备树的层次结构。

例子如下:

/dts-v1/;
/{
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};

可以使用以下方法来修改uart@fe001000这个node

// 根节点之外使用label来引用node
&uart0 {
status = “disabled”;
};
根节点之外使用全路径:
&{/uart@fe001000} {
status = “disabled”;
};

address-cells 和 size-cells 属性

  • #address-cells 属性是一个位于设备树根节点的特殊属性, 它指定了设备树中地址单元的位数。 地址单元是设备树中用于表示设备地址的单个单位。 它通常是一个整数, 可以是十进制或十六进制值
    *#size-cells属性也是一个位于设备树根节点的特殊属性, 它指定了设备树中大小单元的位数。 大小单元是设备树中用于表示设备大小的单个单位。 它通常是一个整数, 可以是十进制或十六进制值。

示例1:

node1 {
#address-cells = <1>;
#size-cells = <1>;
node1-child {
reg = <0x02200000 0x4000>;
// 其他属性和子节点的定义
};
};
/* 解释: node1-child 节点的 reg 属性使用了 <0x02200000 0x4000> 表示地址和大小。
由于 #address-cells 的值为 <1>, 表示使用一个单元来表示地址。 #size-cells 的值也为 <1>, 表示使用一个单元来表示大小。
解释后的地址和大小值如下:
地址部分: 0x02200000 被解释为一个地址单元, 地址为 0x02200000。
大小部分: 0x4000 被解释为一个大小单元, 大小为 0x4000   
*/

示例2:

node1 {
#address-cells = <2>;
#size-cells = <0>;
node1-child {
reg = <0x0000 0x0001>;
// 其他属性和子节点的定义
};
};
/* 解释: node1-child 节点的 reg 属性使用了 <0x0000 0x0001> 表示地址。 由于#address-cells 的值为 <2>, 表示使用两个单元来表示地址。 #size-cells 的值为 <0>, 表示不使用单元来表示大小。
解释后的地址值如下:
地址部分: 0x0000 0x0001 被解释为两个地址单元, 其中第一个地址单元为 0x0000, 第二个地址单元为 0x0001。
*/ 

model属性

model 属性用于描述设备的型号或者名称(可选)

my_device{
compatible = "device";
model = "My Device";
};

status属性

status 属性用于描述设备或节点的状态

  • “okay”: 表示设备或节点正常工作, 可用。
  • “disabled”: 表示设备或节点被禁用, 不可用。
  • “reserved”: 表示设备或节点已被保留, 暂时不可用。
  • “fail”: 表示设备或节点初始化或操作失败, 不可用
my_device{
compatible = "device";
model = "My Device";
status = "okay";
};

compatible属性

compatible 属性用于描述设备的兼容性信息,用于识别设备节点与驱动程序之间的匹配关系
compatible 属性的值是一个字符串或字符串列表
建议取这样的形式: “manufacturer,model”,即“厂家名,模块名”。

示例:

my_device {
compatible = "vendor,device";  // 指定设备节点与特定厂商的特定设备兼容
// 其他属性和子节点的定义
};

compatible =  ["vendor,device1", "vendor,device2"]; 
//用于指定设备节点与多个设备兼容, 通常用于设备节点具有多种变体或配置

compatible = "vendor,*"
// 指定设备节点与特定厂商的所有设备兼容, 不考虑具体的设备标识。

3.2 aliases节点

aliases 节点是一个特殊的节点, 用于定义设备别名,位于设备树的根部

/dts-v1/;
/{
aliases{
mmc0 = &sdmmc0;
serial0 = "/simple@fe000000/seria1@11c500";
};
};
/*
1. mmc0 别名与设备树中的 sdmmc0 节点相关联。 通过使用别名 mmc0, 其他设备节点或客户端程序可以更方便地引用 sdmmc0 节点, 而不必直接使用其完整路径。
2. serial0 别名与设备树中的路径 /simple@fe000000/seria1@11c500 相关联。 通过使用别名 serial0, 其他设备节点或客户端程序可以更方便地引用该路径, 而不必记住整个路径字符串。
*/

注: aliases 节点中定义的别名只在设备树内部可见, 不能在设备树之外引用。它们主要用于设备树的内部组织和引用, 以提高可读性和可维护性。

3.3 chosen节点

chosen 节点是设备树中的一个特殊节点, 用于传递和存储系统引导和配置的相关信息。它位于设备树的根部。
chosen节点包含以下子节点和属性:

  • bootargs: 用于存储引导内核时传递的命令行参数。 它可以包含诸如内核参数、 设备树参数等信息。 在引导过程中, 操作系统或引导加载程序可以读取该属性来获取启动参数。
  • stdout-path:用于指定用于标准输出的设备路径。 在引导过程中, 操作系统可以使用该属性来确定将控制台输出发送到哪个设备, 例如串口或显示屏。
  • firmware-name:用于指定系统固件的名称。 它可以用于标识所使用的引导加载程序或固件的类型和版本。
  • linux,initrd-start linux,initrd-end: 这些属性用于指定 Linux 内核初始化 RAM 磁盘(initrd) 的起始地址和结束地址。 这些信息在引导过程中被引导加载程序使用, 以将 initrd 加载到内存中供内核使用。
  • 其他自定义属性: chosen 节点还可以包含其他自定义属性, 用于存储特定于系统引导和配置的信息。
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
/*
通过这些命令行参数, 操作系统或引导加载程序可以配置内核在引导过程中正确地加载NFS 根文件系统, 并将控制台输出发送到指定的串口设备。
*/

通过使用 chosen 节点, 系统引导过程中的相关信息可以方便地传递给操作系统或引导加载程序。

3.3 device_type节点

device_type 节点是用于描述设备类型的节点

 1. cpu:   表示中央处理器
 2. memory: 表示内存设备
 3. display:   显示设备,液晶显示屏
 4. serial:    表示串行通信设备,串口
 5. ethernet:  表示以太网设备
 6. usb:表示通用串行总线设备
 7. i2c:      表示使用I2C(Inter-Integrated Circuit)总线通信的设备
 8. spi:       表示使用SPI(Serial Peripheral Interface)总线通信的设备
 9. gpio:       表示通用输入/输出设备
 10.pwm:  表示脉宽调制设备 

3.4 板子启动后查看设备树

# ls /sys/firmware/
devicetree fdt
  • /sys/firmware/devicetree 目录下是以目录结构程现的 dtb 文件, 根节点对应 base 目录,每一个节点对应一个目录, 每一个属性对应一个文件。
  • 这些属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用hexdump 把它打印出来。

4. 内核对设备树的处理

从源代码文件 dts 文件开始,设备树的处理过程为:
在这里插入图片描述
①dts 在 PC 机上被编译为 dtb 文件;
② u-boot 把 dtb 文件传给内核;
③ 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
④ 对于某些 device_node 结构体,会被转换为 platform_device 结构体。

device_node 结构体定义和property结构体在内核源码的“/include/linux/of.h” 文件中

struct device_node {
const char *name; // 设备节点的名称
const char *type; // 设备节点的类型
phandle phandle; // 设备节点的句柄
const char *full_name; // 设备节点的完整名称
struct fwnode_handle fwnode; // 设备节点的固件节点句柄
struct property *properties; // 设备节点的属性列表
struct property *deadprops; // 已删除的属性列表
struct device_node *parent; // 父设备节点指针
struct device_node *child; // 子设备节点指针
struct device_node *sibling; // 兄弟设备节点指针
struct kobject kobj; // 内核对象(用于 sysfs)
unsigned long _flags; // 设备节点的标志位
void *data; // 与设备节点相关的数据指针
#if defined(CONFIG_SPARC)
const char *path_component_name; // 设备节点的路径组件名称
unsigned int unique_id; // 设备节点的唯一标识
struct of_irq_controller *irq_trans; // 设备节点的中断控制器
#endif
};
struct property {
char*name; // 属性的名称
intlength; // 属性值的长度(字节数)
void*value; // 属性值的指针
struct property *next; // 下一个属性节点指针
unsigned long _flags; // 属性的标志位
unsigned int unique_id; // 属性的唯一标识 
struct bin_attribute attr;// 内核对象二进制属性
};

哪些设备树节点会被转换为 platform_device

  • 根据规则 1, 首先遍历根节点下包含 compatible 属性的子节点, 对于每个子节点, 创建一个对应的 platform_device。
  • 根据规则 2, 遍历包含 compatible 属性为 “simple-bus”、 “simple-mfd” 、“arm、amba-bus”、“isa” 的节点以及它们的子节点。 如果子节点包含 compatible 属性值则会创建一个对应的 platform_device。
  • 根据规则 3, 总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。

5. of操作函数

5.1获取设备树节点

of_find_node_by_name 函数

函数原型:
struct device_node *of_find_node_by_name(struct device_node *from, const char *nam
e);
函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败。

of_find_node_by_path 函数

函数原型:
struct device_node *of_find_node_by_path(const char *path);
函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个
节点的全路径。
返回值: 找到的节点,如果为 NULL 表示查找失败

of_get_parent 函数

函数原型:
struct device_node *of_get_parent(const struct device_node *node);
头文件:
#include <linux/of.h>
函数作用:
该函数接收一个指向设备树节点的指针 node, 并返回该节点的父节点的指针。
参数含义:
node: 要获取父节点的设备树节点指针。
返回值:
如果找到匹配的节点, 则返回对应的 struct device_node 指针。
如果未找到匹配的节点, 则返回 NULL

of_get_next_child 函数

函数原型:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);
头文件:
#include <linux/of.h>
函数作用:
该函数接收两个参数: node 是当前节点, prev 是上一个子节点。 它返回下一个子节点的指针。
参数含义:
node: 当前节点, 用于指定要获取子节点的起始节点。
prev: 上一个子节点, 用于指定从哪个子节点开始获取下一个子节点。 如果为 NULL, 则从起始节点的第一个子节点开始。
返回值:
如果找到匹配的节点, 则返回对应的 struct device_node 指针。
如果未找到匹配的节点, 则返回 NULL

5.2提取属性值

of_find_property函数

函数原型:
property *of_find_property(const struct device_node *np,
const char *name,int *lenp)
函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值: 找到的属性。

of_property_count_elems_of_size 函数

int of_property_count_elems_of_size(const struct device_node *np,
const char *propname, int elem_size)
函数参数和返回值含义如下:
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值: 得到的属性元素数量。

of_property_read_u32_index 函数

int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值: 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

of_property_read_string 函数

int of_property_read_string(struct device_node *np,
const char *propname,const char **out_string)
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值: 0,读取成功,负值,读取失败。

of_n_addr_cells 函数

int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值: 获取到的#address-cells 属性值。

of_n_size_cells 函数

int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值: 获取到的#size-cells 属性值。

6.pinctrl和gpio子系统

在设备树实验开始之前,我们先了解一下pinctrl和gpio子系统

Pinctrl子系统

pinctrl(引脚控制) 用于描述和配置硬件设备上的引脚功能和连接方式。 它是设备树的一部分, 用于在启动过程中传递引脚配置信息给操作系统和设备驱动程序, 以便正确地初始化和控制引脚。
在设备树中, pinctrl(引脚控制) 使用了客户端和服务端的概念来描述引脚控制的关系和配置。

现在的芯片动辄几百个引脚,在使用到 GPIO 功能时,让你一个引脚一个引脚去找对应的寄存器,配置过程烦躁且费劲。
通过把引脚的复用、配置分离出来,做成Pinctrl子系统,给GPIO、I2C等使用。

    my_device_100ask_imx6ull{
        pinctrl-names = "default";
        pinctrl-0 = <&myled_for_gpio_subsys>;
        status = "okay";
    };

解释如下:
pinctrl-names 属性:

pinctrl-names = "default";
这一行定义了一个或多个引脚配置状态的名称。在这个例子中,只定义了一个名为 "default" 的状态。这意味着当设备启动或需要配置引脚时,会查找名为 "default" 的引脚配置。

pinctrl-0 属性:

pinctrl-0 = <&myled_for_gpio_subsys>;
这一行指定了对应于 "default" 状态(由 pinctrl-names 属性定义)的引脚配置引用。<&myled_for_gpio_subsys> 是一个对设备树中另一个节点的引用,这个节点应该包含了具体的引脚配置信息,比如哪些引脚被选中、它们的电气特性(如驱动能力、上拉/下拉等)以及它们被配置为什么功能(如GPIO、I2C等)。

GPIO子系统
GPIO(General Purpose Input/Output,通用输入输出)子系统是Linux内核中用于管理通用输入输出引脚的一个子系统。GPIO引脚是芯片上的一种常见资源,可以用于实现各种简单的输入输出功能。

以前我们通过寄存器来操作 GPIO 引脚,即使 LED 驱动程序,对于不同的板子它的代码也完全不同。
当 BSP 工程师实现了 GPIO 子系统后,我们就可以:

  • 在设备树里指定 GPIO 引脚
  • 在驱动代码中:使用 GPIO 子系统的标准函数获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值
/dts-v1/;
/{
...
my_device_100ask_imx6ull{
        compatible = "100ask,leddrv";
        pinctrl-names = "default";
        pinctrl-0 = <&myled_for_gpio_subsys>;
        led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

&iomuxc_snvs {
...
imx6ul-evk {
  ... /* 省略其他代码 */
        myled_for_gpio_subsys: myled_for_gpio_subsys {        /*!< Function assigned for the core: Cortex-A7[ca7] */
         fsl,pins = <
              MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x000110A0
              /*  数值定义在IMX6ULLRM.pdf文档中的1529页*/
         >;
        };
};
};

解释如下:
led-gpios 属性:

&gpio5:这是一个对GPIO控制器的引用,指向设备树中定义的GPIO控制器节点之一。在这个例子中,它指的是编号为5的GPIO控制器。这个引用告诉系统LED灯连接到了哪个GPIO控制器上。

3:这个数字指定了GPIO控制器上的具体引脚编号。在这个例子中,LED灯连接到了GPIO控制器5的第3个引脚上。

GPIO_ACTIVE_LOW:这是一个标志,用于指定GPIO引脚的活动状态。GPIO_ACTIVE_LOW意味着当引脚处于低电平时(即0V或接近0V),LED灯被认为是激活的(即点亮)。相反,如果引脚处于高电平(通常是3.3V或5V,取决于系统的电源电压),LED灯则被认为是非激活的(即熄灭)。

pinctrl-names 和 pinctrl-0 属性(虽然它们本身不直接属于GPIO子系统,但与GPIO引脚的使用密切相关)

总的来说,这些代码片段共同描述了如何将一个GPIO引脚(GPIO5的引脚3)配置为LED灯的控制引脚,并指定了该引脚在低电平时激活LED灯的行为。同时,它们还引用了一个引脚配置(通过myled_for_gpio_subsys)

这里我们使用"Pins_Tool_for_i.MX_Processors_v6_x64"工具打IMX6ULL 的配置文件“MCIMX6Y2xxx08.mex”,就可以在 GUI 界面中选择引脚,
配置它的功能,这就可以自动生成 Pinctrl 的子节点信息。
在这里插入图片描述

7. 设备树下的 LED 驱动实验

注:本实验使用的是韦东山I.MX6U开发板

100ask_imx6ull-14x14.dts 完成编写后,在(内核目录)编译dts文件

book@100ask:~/100ask_imx6ull-sdk/Linux-4.9.88$ make dtbs

将编译好的dtb文件拷贝到网络文件系统中

cp /home/book/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs/

7.1编写驱动文件

led_driver.c

#include "asm-generic/errno-base.h"
#include "asm-generic/int-ll64.h"
#include "asm/gpio.h"
#include "linux/compiler.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/consumer.h"
#include "linux/gpio/driver.h"
#include "linux/ioport.h"
#include "linux/kdev_t.h"
#include "linux/leds.h"
#include "linux/printk.h"
#include "linux/stddef.h"
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.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/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 LED_DEV_COUNT 1
#define LED_DEV_NAME "dts_plat_led"

/* led_dev结构体 */
struct led_dev {
    dev_t dev_id; /* 设备id */
    struct cdev cdev; /* cdev*/
    struct class *class; /* class*/
    struct device *device; /* device*/
    int major; /* major*/
    struct device_node *node; /* led_node*/
    struct gpio_desc    *led_gpio; /* led_gpio*/
};


static struct led_dev led_dev;

static int led_open (struct inode *node, struct file *filp){
    printk("%s %s line:%d\n", __FILE__,__FUNCTION__,__LINE__);

    /* 设置GPIO引脚为输出模式,输出低电平 */
    gpiod_direction_output(led_dev.led_gpio, 0);
    return 0;
}


static ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *offset){
   
   int status;
   int err;
    printk("%s %s line:%d\n", __FILE__,__FUNCTION__,__LINE__);
    err = copy_from_user(&status, buf, 1);

    /* 设置GPIO的值 */
    gpiod_set_value(led_dev.led_gpio,status);

    return 1;
}


static const struct file_operations led_fops = {
    .owner= THIS_MODULE,
    .open   = led_open,
    .write  = led_write,
};


/*  当驱动与设备匹配成功后执行此函数   */
int led_probe(struct platform_device *led_device){
    printk("%s %s line:%d\n", __FILE__,__FUNCTION__,__LINE__);
    // 1.gpio
    /*  led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; */
    led_dev.led_gpio = gpiod_get(&led_device->dev, "led", 0);
    if(IS_ERR(led_dev.led_gpio))
    {
    dev_err(&led_device->dev, "Failed to get GPIO for led");
    return PTR_ERR(led_dev.led_gpio);
    }

    // 2.注册file_operations结构体
      // 2.1注册设备号
     if(led_dev.major){
        led_dev.dev_id = MKDEV(led_dev.major, 0);
        register_chrdev_region(led_dev.dev_id, LED_DEV_COUNT, LED_DEV_NAME);
     }else{
        alloc_chrdev_region(&led_dev.dev_id,0, LED_DEV_COUNT, LED_DEV_NAME);
        led_dev.major = MAJOR(led_dev.dev_id);
     }
      // 2.2添加cdev结构体
      cdev_init(&led_dev.cdev, &led_fops);
      cdev_add(&led_dev.cdev, led_dev.dev_id, LED_DEV_COUNT);
      // 2.3创建class
      led_dev.class = class_create(THIS_MODULE, LED_DEV_NAME);
      if(IS_ERR(led_dev.class)){
        return PTR_ERR(led_dev.class);
      }
       // 2.4 创建设备 /dev/dts_plat_led
       led_dev.device = device_create(led_dev.class, NULL, led_dev.dev_id, NULL, LED_DEV_NAME);
       if(IS_ERR(led_dev.device)){
        return PTR_ERR(led_dev.device);
       }
    
    return 0;
}

/*  remove 函数,移除 platform 驱动的时候此函数会执行    */
int led_remove(struct platform_device *led_device){
        gpio_set_value((unsigned int)led_dev.led_gpio,1); // turn off LED

        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_id, LED_DEV_COUNT);
        device_destroy(led_dev.class, led_dev.dev_id);
        class_destroy(led_dev.class);
        gpiod_put(led_dev.led_gpio); // release gpio
        return 0;
    }

static const struct of_device_id leds_table[]   = { 
    {.compatible = "100ask,leddrv"}, /* 100ask_imx6ull-14x14.dts中定义,compatible相同时才匹配成功 */
    {}
};

static struct platform_driver led_driver = {
    .driver = {
        .name = "imx6ul-led", /* driver name */
        .owner = THIS_MODULE,
        .of_match_table = leds_table, /* device tree match table */   
     },
     .probe = led_probe, /* device probe function */    
     .remove = led_remove, /* device remove function */
} ;

static int __init led_init(void){
    
    printk("%s %s line %d\n", __FILE__,__FUNCTION__,__LINE__);
    return platform_driver_register(&led_driver);
}
static void __exit led_exit(void){
    printk("%s %s line %d\n", __FILE__,__FUNCTION__,__LINE__);
     platform_driver_unregister(&led_driver);
}

module_init(led_init); 
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Pumpk1n");

led_test.c

#include "asm-generic/fcntl.h"
#include "linux/string.h"
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <unistd.h>


int main(int argc, char **argv) {

    int fd;
    int status;
    if(argc != 3)
    {
        printf("Usage: %s /dev/dts_plat_led on\n", argv[0]); //  turn on led
        printf("Usage: %s /dev/dts_plat_led off\n", argv[0]);// turn off led
        return -1;
    }

   fd = open(argv[1],O_RDWR);
   if(fd < 0)
   {
    printf("Error opening /dev/dts_plat_led\n");
    return -1;
   }

   if(strcmp(argv[2], "on") == 0)
   {
        // turn on the LED
        status = 1;
        write(fd,&status,1);
   }
   else if(strcmp(argv[2], "off") == 0)
   {
    // turn off the LED
    status = 0;
    write(fd,&status,1);
   }
    return 0;
}

Makefile文件

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

obj-m += led_driver.o

all: 
$(MAKE) -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test led_test.c 

clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_test

执行命令: make

编译 led_driver.c文件为 led_driver.ko内核模块文件
编译led_test.c文件为led_test可执行文件

拷贝这两个文件到网络文件系统中

cp led_test led_driver.ko ~/nfs_rootfs/

串口连接开发板

挂载到网络文件系统中

[root@100ask:~]# mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

将/mnt/100ask_imx6ull-14x14.dtb文件复制到/boot/下面

[root@100ask:~]# cp /mnt/100ask_imx6ull-14x14.dtb /boot/

重启开发板,等待重启完成后重新挂载网络文件系统中

[root@100ask:~]# reboot
[root@100ask:~]# mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

进入到/mnt/目录中,并向内核加载模块文件led_driver.ko

[root@100ask:~]# cd /mnt
[root@100ask:mnt]# insmod led_driver.ko

运行测试文件测试实验结果

[root@100ask:/mnt]# ./led_test /dev/dts_plat_led on
[root@100ask:/mnt]# ./led_test /dev/dts_plat_led off

原文地址:https://blog.csdn.net/qq_54735854/article/details/140672971

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!