自学内容网 自学内容网

嵌入式Linux驱动开发之从设备树到点亮LED

关键词:设备树 rk3399 嵌入式Linux

设备树是什么

  • 一种描述硬件数据结构的机制
  • 它是数据结构或者语言
  • 设备树的实体表现是设备树文件.dtsi和.dts

设备树的作用

将硬件信息都描述在设备树文件内,供驱动程序调用

设备树可以描述的硬件数据包括哪些

设备树可以描述的信息包括CPU的数量和类别、内存基地址和大小、总线和桥、外设连接、中断控制器和中断使用情况、GPIO控制器和GPIO使用情况、Clock控制器和Clock使用情况

设备树文件

设备树通常包含以下几种扩展名的文件,它们都是ASCII文本文件

.dtsi
.dts
.dtb

  • DTSI
    .dtsi格式的文件,设备树源文件,一般由芯片厂商提供,例如多个硬件平台都使用RK3368作为主控芯片, 那么我们可以将RK3368芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀, 其他设备树文件直接使用“#include xxx”引用即可。DTSI文件语法与DTS文件相同,通常描述芯片级的信息,DTSI文件通常与DTS文件在同一目录下,位于Linux源代码的/arch/arm/boot/dts/arch/arm64/boot/dts目录下

  • DTS(Device Tree Source
    .dts格式的文件,一般称为DTS(Device Tree Source)文件,设备树源文件。DTS一般描述板级信息,是一种ASII文本格式。于ARM而言,源文件位于linux源码的/arch/arm/boot/dts/arch/arm64/boot/dts目录下

  • DTB
    .dtb格式的文件,DTB文件是DTS文件编译而来的二进制文件

将DTS文件编译成DTB文件的是DTC(Device tree compiler) 设备树编译器DTC的源代码位于内核源码的scripts/dtc目录中,Linux内核使能了设备树的情况下,编译内核的时候,DTC也会被编译出来

在linux内核源码的arch/arm/boot/dts/Makefilearch/arm64/boot/dts/Makefile中描述了当某种SoC被选中后,哪些.dtb文件会被编译出来

设备树语法

在设备树文件内使用DTS语法来描述硬件,虽然.dtsi文件和.dts文件内容存在差异,二者都是使用DTS语法
如下示意简单的设备树数据结构(.dtsi内):

/ {
    compatible = "example,my-soc"; --------------(A)
    model = "Example Custom SoC";
    interrupt-parent = <&intc>; ----------------(B)

    aliases {
        uart0 = &serial0; -------------------(C)
    };

    memory {
        reg = <0x80000000 0x4000000>; ------------(D)
    };

    intc: interrupt-controller@40000000 { ----------(E)
        compatible = "example,irq-controller";
        reg = <0x40000000 0x100>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    timer@41000000 { ----------------------(F)
        compatible = "example,my-timer";
        reg = <0x41000000 0x100>;
        interrupts = <0 1 1>;
        clock-frequency = <1000000>;
        status = "okay";
    };

    serial0: uart@42000000 { ------------------(G)
        compatible = "example,my-uart";
        reg = <0x42000000 0x1000>;
        interrupts = <1 0 1>;
        clock-frequency = <24000000>;
        status = "okay";
    };

    gpio: gpio@43000000 { -------------------(H)
        compatible = "example,my-gpio","example,my-gpio";
        reg = <0x43000000 0x100>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupts = <2 0 1>;
        status = "okay";
    };

    spi4{
        compatible = "spi-gpio";
        #address-cells = <1>;
        #size-cells = <0>;

        gpio_spi:gpio_spi@0{
            compatible = "rockchip,rk3399_spi";
            reg = <0>;
        };

    };

};

设备树源码由描述设备节点和节点属性的语句组成

节点

节点分为根节点和节点

  • 根节点
    • 设备树有且只有一个根节点,.dts和.dtsi里都可以有根节点,但最终会合为一个
    • 根节点没有名字,用“/”表示,后面跟"{}"
  • 节点/设备节点
    • 系统中的每个设备都表示为一个设备树节点,节点即设备
    • 在.dts和.dtsi中节点由节点名称和其后由{}号包围内容,节点的命名格式是:节点名@地址,节点名用于描述设备类型,地址是设备的物理地址,如timer@41000000,如果节点描述的设备有一个地址的话就应该加上单元地址,并在后面的reg属性中列出
    • 常见的节点名称也有如下形式:代号:名称@地址,其中:前面的是这个设备的代号,如gpio:gpio@43000000方便对应,可以通过&代号来访问这个节点如&gipo
  • 特殊节点
    • aliases节点:出现在.dtsi文件中,用来给外设定义一个别名,如上例所示
    • chosen节点:出现在.dts文件中,并非一个真实的设备,主要是为了bootloader向linux内核传递数据,主要是bootargs参数,一般改节点为空或者较少内容

属性

节点表示设备,不同设备的属性不同
属性由键-值对组成,值可以为空,或者包含任意字节流,键-值对有如下几种形式

  • 文本字符串(无结束符),可以用双引号表示,如a-string-property = "A string"
  • Cells,32位无符号整数,用尖括号限定,如second-child-property = <1>
  • 二进制数据,用方括号限定,如a-byte-data-property = [0x01 0x23 0x34 0x45]
  • 混合表示,用逗号连接在一起,如mixed-property = "a string",[0x01 0x23 0x34],<0x4444>
  • 字符串列表,使用逗号连在一起,如`string-list = “red,cat”,“blue,cat”
compatible属性

compatible属性也称为"兼容性"属性,是一个非常重要的属性
compatible属性的值是一个字符串列表,设备节点的属性用于将设备和驱动绑定起来,如上例中,gpio的compatible属性:
compatible = "example,my-gpio",example,my-gpio;
其中:

  • example 一般是厂商名称
  • my-gpio 一般是驱动模块名字
    一般驱动程序文件都会有一个OF匹配表,此表保存着一些compatible值,如果设备节点的compaltible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动

每个节点都有compatible属性

根节点的compatible属性的值也是一个字符串列表,如上例,与设备节点不同的是,根节点两个compatable属性值的第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的SOC

model属性

model属性一般描述设备的信息,比如名字之类
model属性的值也是一个字符串,如上面的示例

status属性

status属性和设备状态有关,其属性值也是字符串,如上例,其属性值可选状态如下:

描述
“okay”表明设备是可操作的
“disabled”表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入后,disable的具体含义要看绑定文档
“fail”表明设备不可操作
“fail-sss”与fail相同,sss表示检测到的错误内容
#address-cells#size-cells属性

这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息,属性值都是无符号32位整型数据
#address-cells属性决定了子节点的reg属性中地址信息所占用的字长(32位的个数)
#size-cells属性决定了子节点的reg属性中长度信息所占的字长(32位)
上例中,#address-cells = <1>; #size-cells = <0>;说明spi4的子节点reg属性中起始地址所占用的字长为1,地址长度所占用的字长为0

reg属性

reg属性用于描述设备地址空间资源信息,就是某个外设的寄存器地址范围信息
reg属性的值一般是(address,length)形式,address一般表示设备寄存器首地址,length一般根据#size-cells属性设置或按照实际长度填写

ranges属性

ranges是一个地址映射/转换表,其属性值按照(child-bus-address,parent-bus-address,length)格式编写,即子地址,父地址和地址空间长度3部分

  • child-bus-address表示子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长
  • parent-bus-address表示父总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长
  • length表示子地址空间的长度,由父节点的#size-cells确定地址长度所占用的字节
    如果ranges属性值为空,则说明子地址空间和父地址空间完全相同,不需要进行地址转换

设备树文件操作

设备厂商一般都会提供给我们一个经过测试的.dtsi和.dts文件,我们大概率不会从头到尾重新写一个,如果我们使用的SOC厂商提供的设备树文件中没有我们想要的设备节点,则需要自己在.dts文件中添加,其类型大概如下:以I2C为例

 &i2c4{
    /*要添加的内容*/
 };

在添加设备节点时,其属性可根据绑定文档给出的相关说明来编写,绑定文档在Linux源码目录/Documentation/devicetree/bindings/路径下

设备树文件修改完成后,使用命令make dtbs重新编译,获得新的.dtb文件,使用新的dtb文件启动Linux内核

设备树在嵌入式Linux系统中的体现

Linux内核在启动时会解析DTB文件,然后在/proc/device-tree目录下生成设备节点文件,每个节点都会形成一个文件夹

Linux内核使用device_node结构体来描述一个节点,也就是一个设备,此结构体定义在Linux内核源码/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;     //removed属性
    struct device_node       *parent;     //父节点
    struct device_node       *child;      //子节点
    struct device_node       *sibling;    
    struct kobject            kobj;
    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
};

OF函数

设备树文件内描述了设备的详细信息,在编写驱动程序时,需要用到这些信息,比如设备树使用reg属性描述了一个GPIO的寄存器地址,我们在编写驱动程序时要控制这个寄存器。Linux内核提供了一系列函数来获取设备树文件中的设备节点或节点属性信息,这些函数名称都有统一的前缀of_ ,因此这一系列的函数也被称为OF函数

节点相关

  • of_find_node_by_name()通过节点名称查找结点
struct device_node *of_find_node_by_name(struct device_node *from,
                                         const char         *name);
- from 开始查找的节点,NULL为从根节点开始查找
- name 要查找的节点名称
- 返回值为找到的节点,如果为NULL则表示查找失败
  • of_find_node_by_type()通过节点类型查找结点
struct device_node *of_find_node_by_type(struct device_node *from,
                                         const char         *type);
- from 开始查找的节点,NULL为从根节点开始查找
- type 要查找的节点type属性,即device_tpye属性值
- 返回值为找到的节点,如果为NULL则表示查找失败
  • of_find_compatible_node()通过device_type和compatible两个属性查找节点
struct device_node *of_find_compatible_node(struct device_node *from,
                                         const char            *type,
                                         const char            *compatible);
- from 开始查找的节点,NULL为从根节点开始查找
- type 要查找device_type的属性值,如果为NULL,表示忽略掉device_type属性
- compatible 要查找节点的compatible属性列表
- 返回值为找到的节点,如果为NULL则表示查找失败
  • of_find_matching_node_and_match()通过of_device_id匹配表查找
struct device_node *of_find_matching_node_and_match(struct device_node *from,
                                    const struct of_device_id         *matches,
                                    const struct of_device_id         *match);
- from 开始查找的节点,NULL为从根节点开始查找
- matches of_device_id匹配表,即在此表内查找节点
- match 找到的匹配的of_device_id
- 返回值为找到的节点,如果为NULL则表示查找失败
  • of_find_node_by_path()通过路径来查找结点
inline struct device_node *of_find_node_by_path(const char *path);
- paty 带有全路径的节点名,可以使用的节点的别名
- 返回值为找到的节点,如果为NULL则表示查找失败
  • of_get_parent()获取指定节点的父节点
struct device_node *of_get_parent(const struct device_node *node)
- node 要查找父节点的节点
- 返回值为找到的节点,如果为NULL则表示查找失败
  • of_get_next_child()用迭代的方式查找子节点
struct device_node *of_get_next_child(const struct device_node *node,
                                            struct device_node *prev)
  • node 要查找子节点的节点
  • prev 前一个子节点,也就是从哪一个子节点开始迭代地查找下一个子节点,如果为NULL表示从第一个子节点开始
    • 返回值为找到的节点,如果为NULL则表示查找失败

属性相关

Linux内核使用结构体property表示属性,此结构体定义在Linux内核源码/include/linux/of.h中:

struct property{
    char                *name;     //名字
    int                  length;   //长度
    void                *value;    //属性值
    struct property     *next;      //下一个属性
    unsigned             long_flags;
    unsigned int         unique_id;
    struct bin_attribute attr;

};
  • 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()获取属性中元素的数量,比如reg属性值是一个数组,使用此函数可以获取这个数组的大小
int of_property_count_elems_of_size(const struct device_node  *np,
                                    const char                *propname,
                                    int                        elem_size)
- np 设备节点
- propname 属性名字
- 元素长度
- 返回值 属性元素数量
  • of_property_read_u32_index()从指定的属性中获取指定标号的u32类型数据值
int of_property_read_u32_index(const struct device_node  *np,
                                const char               *propname,
                                u32                       index,
                                u32                      *out_value)
- np 设备节点
- propname 属性名字
- index 要读取的值标号
- out_value 读取到的值
- 返回值 0表示读取成功,负值表示读取失败
  • of_property_read_u8_array()
  • of_property_read_u16_array()
  • of_property_read_u32_array()
  • of_property_read_u64_array()读取属性中u8,u16,u32,u64类型的数组数据
int of_property_read_u8_array(const struct device_node  *np,
                                const char               *propname,
                                u8                       *out_values,
                                size_t                   sz)

int of_property_read_u16_array(const struct device_node  *np,
                                const char               *propname,
                                u16                      *out_values,
                                size_t                   sz)

int of_property_read_u32_array(const struct device_node  *np,
                                const char               *propname,
                                u32                      *out_values,
                                size_t                   sz)

int of_property_read_u64_array(const struct device_node  *np,
                                const char               *propname,
                                u64                      *out_values,
                                size_t                   sz)
- np 设备节点
- propname 属性名字
- out_value 读取到的值,分别是u8,u16,u32,u64
- sz 要读取的数组元素数量
- 返回值 0表示读取成功,负值表示读取失败
  • of_property_read_u8()
  • of_property_read_u16()
  • of_property_read_u32()
  • of_property_read_u64()读取属性中u8,u16,u32,u64类型的数据
int of_property_read_u8(const struct device_node  *np,
                                const char               *propname,
                                u8                       *out_value)

int of_property_read_u16(const struct device_node  *np,
                                const char               *propname,
                                u16                      *out_value)

int of_property_read_u32(const struct device_node  *np,
                                const char               *propname,
                                u32                      *out_value)

int of_property_read_u64(const struct device_node  *np,
                                const char               *propname,
                                u64                      *out_value)
- np 设备节点
- propname 属性名字
- out_value 读取到的值,分别是u8,u16,u32,u64
- 返回值 0表示读取成功,负值表示读取失败
  • of_property_read_string()从指定的属性中字符串值
int of_property_read_string(const struct device_node  *np,
                            const char               *propname,
                            const char               **out_string)
- np 设备节点
- propname 属性名字
- out_string 读取到的字符串值
- 返回值 0表示读取成功,负值表示读取失败
  • of_n_addr_cells()用于获取#address-cells属性值
int of_n_addr_cells(const struct device_node  *np)
- np 设备节点
- 返回值 获取到的属性值
  • of_n_size_cells()用于获取#size-cells属性值
int of_n_addr_cells(const struct device_node  *np)
- np 设备节点
- 返回值 获取到的属性值

其他常用

  • of_device_is_compatible()用于查看节点的compatible属性是否包含指定的字符串
int of_device_is_compatible(const struct device_node  *device,
                            const char                *compat)
- device 设备节点
- compat 要查看的字符串
- 返回值 0表示节点的cmpatible属性中不包含compat指定的字符串,整数表示包含
  • of_get_address()获取地址相关属性
const__be32 *of_get_address(const struct device_node  *dev,
                            int                        index,
                            u64                       *size,
                            unsigned int              *flags)
- dev 设备节点
- index 要读取的地址
- size 地址长度
- flags 参数
- 返回值 读取到的地址首地址,若为NULL,表示读取失败
  • of_translate_address()将从设备树获取到的地址转换为物理地址
u64 of_translate_address(const struct device_node     *dev,
                         const__be32                  *in_addr)
- dev 设备节点
- in_addr 要转换的地址
- 返回值 得到的地址,若为OF_BAD_ADDR,表示转换失败
  • of_address_to_resource()将节点的属性转换为resource结构体类型
int of_address_to_resource(const struct device_node     *dev,
                             int                         index,
                             struct resource            *r)
- dev 设备节点
- index 地址资源标号
- r 得到的resourc类型值
- 返回值 0成功,复数失败
  • of_iomap()将物理地址映射为虚拟地址
void __iomem *of_iomap(const struct device_node     *np,
                             int                     index)
- np 设备节点
- index 要完成内存映射的段,如果只有一段,则设置为0
- 返回值 映射后的虚拟内存首地址,如果为NULL表示映射失败

利用设备树方式驱动GPIO-点亮LED

驱动程序的开发流程与之前相同,利用设备树方式点亮LED除了要编写驱动代码外,还要修改设备树文件。用设备树向Linux内核传递相关寄存器地址,在驱动程序中使用OF函数获取节点属性值,完成I/O初始化
硬件平台与之前相同,RK3399型号的SOC,引脚GPIO0B_5输出高电平点亮LED,低电平熄灭LED

修改设备树文件.dts

本文这里还是选用Firefly-RK3399开发板,前面移植内核时,下载了瑞芯微提供的Linux内核源代码,内有测试过的设备树文件,这里直接修改rk3399-firefly-linux.dts文件,在源代码的/arch/arm64/boot/dts/rockchip/
在根节点"/"后面输入以下内容

gumpleddts{
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "gump-led";
    status = "okay";
    reg = < 0XFF750104 0X04    /*PMUCRU_CLKGATE_CON1 */
            0XFF320004 0X04    /*PMUGRF_GPIO0B_IOMUX */
            0XFF720004 0X04    /*GPIO_SWPORTA_DDR    */
            0XFF720000 0X04    /*GPIO_SWPORTA_DR     */
    >;
};

其中reg属性中设置了驱动程序中所需要的寄存器(与上一篇驱动文章中使用相同的引脚),
添加以上内容后,在Linux内核源码文件内,对所有dts进行编译:

gump@gump:~/rk3399/kernel-develop-4.4$ make dtbs ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

编译后可以看到在Linux内核源码文件内/arch/arm64/boot/dts/rockchip/文件夹内,rk3399-firefly-linux.dtb文件的日期变为当前日期

打包dtb文件传递

在前面给RK3399移植Linux的文章中,打包内核启动文件时,指定了dtb文件,现在用相同的打包方法将最新的dtb文件打包进启动文件,操作方法请参考之前的文章RK3399移植u-boot&linux内核&根文件系统

用新的内核启动文件启动后,在嵌入式linux系统内进入/proc/device-tree/目录下可以看到添加进来的节点文件夹,如图所示
在这里插入图片描述

查看文件夹内部的设备属性文件,如图
在这里插入图片描述

修改驱动程序

设备树驱动程序与非设备树驱动的程序基本相同,前者程序中不再直接写明寄存器地址,而是用OF函数从设备树中获取,程序如下:

#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.h>
#include <linux/of_address.h>

/*************************************
 *FileName:rk3399_dts.c
 *Fuc:     RK3399,GPIO0B_5 driver
 *
 *Author:  PineLiu
 ************************************/

#define CHAR_CNT     1          //设备数量
#define LED_NAME     "gumpled"  //设备名称

#define LEDON        1      //点亮
#define LEDOFF       0      //熄灭

#if 0
//寄存器物理地址
#define PMUCRU_CLKGATE_CON1 0XFF750104
#define PMUGRF_GPIO0B_IOMUX 0XFF320004
#define GPIO_SWPORTA_DDR    0XFF720004
#define GPIO_SWPORTA_DR     0XFF720000
#endif

//重映射后的寄存器虚拟地址
static void __iomem *GPIO0_DR;    //数据寄存器
static void __iomem *GPIO0_DDR;   //方向寄存器
static void __iomem *GPIO0_IOMUX; //复用寄存器
static void __iomem *GPIO0_CRU;   //时钟寄存器


//声明设备结构体(该设备包含的属性)
struct newexternal_dev{
    dev_t           devid;   //设备号
    struct cdev     cdev;    //字符设备
    struct class   *class;   //设备节点类 主动创建设备节点文件用
    struct device  *device;  //设备节点 主动创建设备节点文件用
    int             major;   //主设备号
    int             minor;   //次设备号
    struct device_node  *nd;
};


//声明一个外部设备,本文这里是字符设备
struct newexternal_dev charled;


//LED打开/关闭
void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON){
        val = readl(GPIO0_DR);
        val |= (1<<13);
        writel(val,GPIO0_DR);
    }else if(sta == LEDOFF){
        val = readl(GPIO0_DR);
        val &= ~(1<<13);
        writel(val, GPIO0_DR);
    }
}


//===========================================================================
//以下实现设备的具体操作函数:open函数、read函数、write函数和release函数
//===========================================================================

//打开设备
static int led_open(struct inode *inode, struct file *filp)
{
    filp -> private_data = &charled;    //设置私有数据
    return 0;
}

//从设备读取数据
static ssize_t led_read(struct file *filp,char __user *buf,
            size_t cnt,loff_t *offt)
{
    return 0;
}

//向设备写数据
//filp:设备文件,表示打开的文件描述
//buf :保存着要向设备写入的数据
//cnt :要写入的数据长度
//offt:相对于文件首地址的偏移
//
static ssize_t led_write(struct file *filp,const char __user *buf,
            size_t cnt,loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(databuf,buf,cnt);
    if(retvalue < 0){
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];   //获取状态值
    
    if(ledstat == LEDON){
        led_switch(LEDON);
    }else if(ledstat == LEDOFF){
        led_switch(LEDOFF);
    }

    return 0;
    
}

//关闭,释放设备
//filp: 要关闭的设备文件描述
//
static int led_release(struct inode *inode,struct file *filp)
{
    return 0;                                                                                                           
}


//==========================================================================
//以下实现的设备具体函数与内核的对应函数的映射
//==========================================================================
//映射设备操作函数
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open  = led_open,
    .read  = led_read,
    .write = led_write,
    .release = led_release,
};


//==========================================================================
//内核模块相关
//==========================================================================

//内核模块加载函数或称为驱动入口函数
static int __init led_init(void)
{
    u32 val = 0;
    int ret;
    u32 regdata[14];
    const char *str;
    struct property *proper;

    //===================获取节点属性
    //获取节点
    charled.nd = of_find_node_by_path("/gumpleddts");
    if(charled.nd == NULL){
        printk("gumpleddts node can not found!\r\n");
        return -EINVAL;
    }else{
        printk("gumpleddts node has been found!\r\n");
    }

    //获取compatible属性
    proper = of_find_property(charled.nd,"compatible",NULL);
    if(proper == NULL){
        printk("compatible property can not found!\r\n");
    }else{
        printk("compatibel = %s\r\n",(char *)proper -> value);
    }

    //获取status属性
    ret = of_property_read_string(charled.nd,"status",&str);
    if(ret < 0){
        printk("status read failed!\r\n");
    }else{
        printk("status = %s\r\n",str);
    }

    //获取reg属性
    ret = of_property_read_u32_array(charled.nd,"reg",regdata,8);
    if(ret < 0){
        printk("reg property read failed!\r\n");
    }else{
        u8 i = 0;
        printk("reg data:\r\n");
        for(i = 0;i<8;i++)
            printk("%#X",regdata[i]);
        printk("\r\n");
    }
    //=========初始化LED
    //寄存器地址映射
    #if 1
    GPIO0_CRU   = ioremap(regdata[0],regdata[1]);
    GPIO0_IOMUX = ioremap(regdata[2],regdata[3]);
    GPIO0_DDR   = ioremap(regdata[4],regdata[5]);
    GPIO0_DR    = ioremap(regdata[6],regdata[7]);
    #else
    GPIO0_CRU   = of_iomap(charled.nd,0);
    GPIO0_IOMUX = of_iomap(charled.nd,1);
    GPIO0_DDR   = of_iomap(charled.nd,2);
    GPIO0_DR    = of_iomap(charled.nd,3);
    #endif

    //使能时钟
//  GPIO0_CRU |= (1 << (3 + 16));
//  GPIO0_CRU &= ~(1 << 3);
    val = readl(GPIO0_CRU);
    val |= (1 << (3+16));
    val &= ~(1 << 3);
    writel(val,GPIO0_CRU);

    //设置GPIO0B_5引脚复用功能为GPIO
//  GPIO0_IOMUX |= (3 << (10+16));
//  GPIO0_IOMUX &= ~(3 << 10);
    val = readl(GPIO0_IOMUX);
    val |= (3 << (10+16));
    val &= ~(3 << 10);
    writel(val,GPIO0_IOMUX);

    //配置GPIO0B_5引脚为输出模式
//  GPIO0_DDR |= (1 << 13);
    val = readl(GPIO0_DDR);
    val |= (1 << 13);
    writel(val,GPIO0_DDR);
    
    //配置GPIO0B_5引脚默认关闭LED
//  GPIO0_DR &= ~(1 << 13);
    val = readl(GPIO0_DR);
    val &= ~(1 << 13);
    writel(val,GPIO0_DR);


    //===========注册设备   
    //创建设备号
    if(charled.major){ //定义了设备号
        charled.devid = MKDEV(charled.major,0);
        register_chrdev_region(charled.devid,CHAR_CNT,LED_NAME);
    }else{ //没有定义设备号,要向系统申请设备号
        alloc_chrdev_region(&(charled.devid),0,CHAR_CNT,LED_NAME);
        charled.major = MAJOR(charled.devid);
        charled.minor = MINOR(charled.devid);
    }
    printk("charled major = %d,minor = %d\r\n",charled.major,charled.minor);

    //初始化cdev
    charled.cdev.owner = THIS_MODULE;
    cdev_init(&charled.cdev,&led_fops);
    //向系统注册设备
    cdev_add(&charled.cdev,charled.devid,CHAR_CNT);
    //创建类
    charled.class = class_create(THIS_MODULE,LED_NAME);
    if(IS_ERR(charled.class)){
        return PTR_ERR(charled.class);
    }
    //创建设备节点
    charled.device = device_create(charled.class,NULL,charled.devid,NULL,LED_NAME);
    if(IS_ERR(charled.device)){
        return PTR_ERR(charled.device);
    }

    return 0;

}


//内核模块卸载函数
static void __exit led_exit(void)
{
    //取消映射
    iounmap(GPIO0_CRU);
    iounmap(GPIO0_IOMUX);
    iounmap(GPIO0_DDR);
    iounmap(GPIO0_DR);

    //注销设备
    cdev_del(&charled.cdev);
    unregister_chrdev_region(charled.devid,CHAR_CNT);

    device_destroy(charled.class,charled.devid);
    class_destroy(charled.class);
}

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

程序中,相比之前的驱动代码:

  • 增加了依赖文件device.h、of.h和of_address.h
  • 屏蔽了直接声明寄存器地址的代码,寄存器地址已在设备树文件中声明
  • 在设备结构体中,新加了节点成员struct device_node
  • 在模块加载函数led_init()中用OF函数获取了设备节点的,节点、compatible、status、reg属性,

编译为内核模块并安装

在PC平台交叉编译驱动程序成为.ko文件,再加载到内核运行。交叉编译相关知识及交叉编译工具链请查询其他资料,本文这里直接说明Makefile文件的内容,如下:

KERNELDIR:=/home/gump/rk3399/kernel-develop-4.4
CURRENT_PATH:=$(shell pwd)
obj-m:=rk3399_dts.o

build: kernel_modules

kernel_modules:
                $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
                $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译命令:(可根据自己的交叉编译工具更改)

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-`

得到ko文件放到嵌入式Linux系统内,并安装内核模块:

sudo insmod -f rk3399_dts.ko

安装后可用dmesg命令查看信息反馈,本文如下:
在这里插入图片描述

测试

测试程序:

测试程序:
```C
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/*************************************************
 *FileName:testDTS.c
 *Func:    GPIO driver module test
 *
 *Author:  pineliu
 * **********************************************/

#define LEDOFF  0
#define LEDON   1

int main(int argc,char *argv[])
{
    int fd, retvalue;
    char *filename;
    unsigned char databuf[1];

    if(argc != 3){
        printf("Error Usage!\r\n");
        return -1;
    }
    
    filename = argv[1];
    
    fd = open(filename,O_RDWR);
    
    if (fd < 0){
        printf("file %s open failed!\r\n",argv[1]);
        return -1;
    }
    databuf[0] = atol(argv[2]);

    retvalue = write(fd,databuf,sizeof(databuf));
    if(retvalue < 0){
        printf("led Control Error!\r\n");
        close(fd);
        return -1;
    }

    retvalue = close(fd);
    if(retvalue < 0){
        printf("file %s close failed!\r\n",argv[1]);
        return -1;
    }
    
    return 0;
}

将测试程序进行编译:

aarch64-linux-gnu-gcc testDTS.c -o testDTS

得到可执行文件testDTS
放到嵌入式Linux系统内,并赋予可执行权限

chmod +x testDTS

测试,点亮led:

sudo ./testDTS /dev/gumpled 1

实验证明可行

后记

为什么称为设备树,个人理解是,对于CPU而言,外设本身就像树型结构一样,挂载在SOC主线上,将描述外设的机制称为设备–树比较形象


原文地址:https://blog.csdn.net/u012660296/article/details/145261180

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