Linux驱动之设备树添加LED驱动

目录

一、开发环境

二、设备树中添加LED节点

三、驱动程序编写

四、测试程序编写

五、运行验证


本章我们就开始第一个基于设备树的 Linux 驱动实验,还是点LED灯。

本章重点考察如下知识:

①、在 imx6ul-14x14-evk.dtsi 文件中创建相应的设备节点。

②、编写驱动程序,获取设备树中的相关属性值。

③、使用获取到的有关属性值来初始化 LED 所使用的 GPIO。

一、开发环境

  • CPU:IMX6ULL

  • 内核版本:Linux-5.19

二、设备树中添加LED节点

        在根节点“/”下创建一个名为“dts_led”的子节点,打开 imx6ul-14x14-evk.dtsi 文件,在根节点“/”最后面输入如下所示内容:

dts_led {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "dts_led";
    status = "okay";

    reg = <
        0x020C406C 0x04 /* CCM_CCGR1_REGBASE */
        0x020E0068 0x04 /* SW_MUX_GPIO1_IO03_REGBASE */
        0x020E02F4 0x04 /* SW_PAD_GPIO1_IO03_REGBASE */
        0x0209C000 0x04 /* GPIO1_DR_BASE */
        0x0209C004 0x04 /* GPIO1_GDIR_BASE */
    >;
}
  • 第 2、 3 行,属性#address-cells#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长(cell),地址长度也占用一个字长(cell)。

  • 第 4 行,属性 compatbile 设置 dts_led 节点兼容性为“dts_led ”。

  • 第 5 行,属性 status 设置状态为“okay”。

  • 第 6~10 行, reg 属性,非常重要! reg 属性设置了驱动里面所要使用的寄存器物理地址,比如第 6 行的“0X020C406C 0X04”表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地址为 0X020C406C,长度为 4 个字节。

        设备树修改完成以后输入如下命令重新编译一下 imx6ul-14x14-evk.dtsi

make dtbs

        编译完成以后得到 imx6ull-toto.dtb,使用新的 imx6ull-toto.dtb 启动Linux 内核。 Linux 启动成功以后进入到/proc/device-tree/目录中查看是否有“dts_led”这个节点,结果如下所示:

/ # ls /proc/device-tree/
#address-cells      clock-osc           pmu
#size-cells         compatible          regulator-can-3v3
aliases             cpus                regulator-peri-3v3
backlight-display   dts_led              regulator-sd1-vmmc
chosen              memory@80000000     soc
clock-cli           model               sound-wm8960
clock-di0           name                spi4
clock-di1           panel               timer
/ # 

如果没有“dts_led”节点的话请重点查看下面两点:

①、检查设备树修改是否成功,也就是 dts_led 节点是否为根节点“/”的子节点。

②、检查是否使用新的设备树启动的 Linux 内核。

        可以进入/sys/firmware/devicetree/base/dts_led 目录中,查看一下都有哪些属性文件,结果如下所示:

/ # ls /sys/firmware/devicetree/base/dts_led/
#address-cells  compatible      reg
#size-cells     name            status
/ # 

三、驱动程序编写

设备树准备好以后就可以编写驱动程序了,在 dts_led.c 里面输入如下内容:

/************************************************************
 * Copyright © toto Co., Ltd. 1998-2029. All rights reserved.
 * Description: 
 * Version: 1.0
 * Autor: toto
 * Date: Do not edit
 * LastEditors: Seven
 * LastEditTime: Do not edit
************************************************************/

#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>

#define DTS_LED_CNT  1          /* 设备号数量 */
#define DTS_LED_NAME "dts_led"  /* 设备名字 */
#define LED_ON          1       /* 开打 */
#define LED_OFF         0       /* 关灯 */

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *VM_CCM_CCGR1;
static void __iomem *VM_SW_MUX_GPIO1_IO03;
static void __iomem *VM_SW_PAD_GPIO1_IO03;
static void __iomem *VM_GPIO1_DR;
static void __iomem *VM_GPIO1_GDIR;

/* dts_led设备结构体 */
struct dts_led_dev
{
    dev_t devid;            /* 设备号 */
    struct cdev cdev;       /* cdev */
    struct class *class;    /* 类 */
    struct device *device;  /* 设备 */
    int major;              /* 主设备号 */
    int minor;              /* 次设备号 */
    struct device_node *nd; /* 设备节点 */
};

struct dts_led_dev dtsled;

/*
 * @Brief   LED打开/关闭
 * @Call    Internal or External
 * @Param   state: 1-打开 0-关闭
 * @Note    NOne
 * @RetVal  无
 */
void led_switch(u8 state)
{
    u32 val = 0;

    if(state == LED_ON)
    {
        val = readl(VM_GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, VM_GPIO1_DR);
    }
    else if(state == LED_OFF)
    {
        val = readl(VM_GPIO1_DR);
        val |= (1 << 3);
        writel(val, VM_GPIO1_DR);
    }
    else
    {
        printk("%s state:%d invalid\n", __func__, state);
    }
}

/*
 * @Brief   打开设备
 * @Call    Internal or External
 * @Param   inode:
 * @Param   filp:设备文件
 * @Note    NOne
 * @RetVal  0:成功 其他值:失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    /* 设置私有数据 */
    filp->private_data = &dtsled;

    return 0;
}

/*
 * @Brief   从设备读数据
 * @Call    Internal or External
 * @Param   filp:要打开的设备文件描述符
 * @Param   buf:返回给用户空间的数据地址
 * @Param   cnt:要读取的数据长度
 * @Param   offt:相对于文件首地址的偏移
 * @Note    NOne
 * @RetVal  读取的字节数,若为负值,表示读失败
 */
static ssize_t led_read(struct file *filp, char __user *buf,
                        size_t cnt, loff_t *offt)
{
    return 0;
}

/*
 * @Brief   写数据到设备
 * @Call    Internal or External
 * @Param   filp:要打开的设备文件描述符
 * @Param   buf:要写入设备的数据地址
 * @Param   cnt:要写入的数据长度
 * @Param   offt:相对于文件首地址的偏移
 * @Note    NOne
 * @RetVal  写入的字节数,若为负值,表示写失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf,
                        size_t cnt, loff_t *offt)
{
    int retval;
    unsigned char databuf[1];
    unsigned char ledstat;

    retval = copy_from_user(databuf, buf, cnt);
    if(retval < 0)
    {
        printk("%s copy_from_user failed\n", __func__);
        return -EFAULT;
    }

    ledstat = databuf[0];

    if(ledstat != LED_ON && ledstat != LED_OFF)
    {
        printk("%s ledstat:%d invalid\n", __func__, ledstat);
        return -1;
    }

    /*打开、关闭LED*/
    led_switch(ledstat);

    return 0;
}


/*
 * @Brief   关闭/释放设备
 * @Call    Internal or External
 * @Param   inode:
 * @Param   filp:要关闭的设备文件描述符
 * @Note    NOne
 * @RetVal  NOne
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 设备操作函数 */
static struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .open  = led_open,
    .read  = led_read,
    .write = led_write,
    .release = led_release,
};

/*
 * @Brief   驱动入口函数
 * @Call    Internal or External
 * @Param   None
 * @Note    NOne
 * @RetVal  NOne
 */
static int __init led_init(void)
{
    u32 val = 0, regdata[14];
    int ret;
    const char *str;
    struct property *proper;

    /* 1.获取设备树中的属性 */
    dtsled.nd = of_find_node_by_path("/dts_led");
    if(dtsled.nd == NULL)
    {
        printk("dts_led node can not found\n");
        return -EINVAL;
    }

    /* 2.获取 compatible 属性 */
    proper = of_find_property(dtsled.nd, "compatible", NULL);
    if(proper == NULL)
    {
        printk("compatible property not find\n");
        return -EINVAL;
    }

    /* 3.获取 status 属性 */
    ret = of_property_read_string(dtsled.nd, "status", &str);
    if(ret < 0)
    {
        printk("status property read failed\n");
        return -EINVAL;
    }

    /* 4.获取 reg 属性内容 */
    ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
    if(ret < 0)
    {
        printk("reg property read failed\n");
        return -EINVAL;
    }

    /* 5.物理地址映射为虚拟地址 */
    VM_CCM_CCGR1         = of_iomap(dtsled.nd, 0);
    VM_SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
    VM_SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
    VM_GPIO1_DR          = of_iomap(dtsled.nd, 3);
    VM_GPIO1_GDIR        = of_iomap(dtsled.nd, 4);

    /* 6.使能 GPIO1 时钟 */
    val = readl(VM_CCM_CCGR1);
    val |= (3 << 26);
    writel(val, VM_CCM_CCGR1);

    /* 设置 GPIO1_IO03 的复用功能, 将其复用为
     * GPIO1_IO03, 最后设置IO属性
     */
    writel(5, VM_SW_MUX_GPIO1_IO03);

    /* 寄存器 GPIO1_IO03 设置 IO 属性 */
    writel(0x10B0, VM_SW_PAD_GPIO1_IO03);

    /* 寄存器 GPIO1_IO03 为输出功能 */
    val = readl(VM_GPIO1_GDIR);
    val |= (1 << 3);
    writel(val, VM_GPIO1_GDIR);

    /* 默认关闭LED */
    val = readl(VM_GPIO1_DR);
    val |= (1 << 3);
    writel(val, VM_GPIO1_DR);

    /* 创建设备号 */
    if(dtsled.major) /* 定义了设备号 */
    {
        dtsled.devid = MKDEV(dtsled.major, 0);
        register_chrdev_region(dtsled.devid, DTS_LED_CNT,
                                DTS_LED_NAME);
    }
    else
    {
        /* 申请设备号 */
        alloc_chrdev_region(&dtsled.devid, 0, DTS_LED_CNT,
                            DTS_LED_NAME);
        /* 获取主设备号 */
        dtsled.major = MAJOR(dtsled.devid);
        /* 获取次设备号 */
        dtsled.minor = MINOR(dtsled.devid);
    }
    printk("%s new_chrdev major:%d minor:%d\n", __func__,
            dtsled.major, dtsled.minor);

    /* 初始化cdev */
    dtsled.cdev.owner = THIS_MODULE;
    cdev_init(&dtsled.cdev, &dtsled_fops);

    /* 添加一个cdev */
    cdev_add(&dtsled.cdev, dtsled.devid, DTS_LED_CNT);

    /* 创建类 */
    dtsled.class = class_create(THIS_MODULE, DTS_LED_NAME);
    if(IS_ERR(dtsled.class))
    {
        return PTR_ERR(dtsled.class);
    }

    /* 创建设备 */
    dtsled.device = device_create(dtsled.class, NULL,
                        dtsled.devid, NULL, DTS_LED_NAME);
    if(IS_ERR(dtsled.device))
    {
        return PTR_ERR(dtsled.device);
    }

    return 0;
}

/*
 * @Brief   驱动出口函数
 * @Call    Internal or External
 * @Param   None
 * @Note    NOne
 * @RetVal  NOne
 */
static void __exit led_exit(void)
{
    /* 取消寄存器物理地址映射为虚拟地址的转换 */
    iounmap(VM_CCM_CCGR1);
    iounmap(VM_SW_MUX_GPIO1_IO03);
    iounmap(VM_SW_PAD_GPIO1_IO03);
    iounmap(VM_GPIO1_DR);
    iounmap(VM_GPIO1_GDIR);

    /* 注销字符设备 */
    /* 删除cdev */
    cdev_del(&dtsled.cdev);
    /* 释放分配的设备号 */
    unregister_chrdev_region(dtsled.devid, DTS_LED_CNT);

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

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("toto");
  • 在设备结构体 dtsled_dev 中添加了成员变量 nd, nd 是 device_node 结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加 device_node 指针变量来存放这个节点。

  • 通过 of_find_node_by_path 函数得到 dts_led 节点,后续其他的 OF 函数要使用 device_node。

  • 通过 of_find_property 函数获取 dts_led 节点的 compatible 属性,返回值为property 结构体类型指针变量, property 的成员变量 value 表示属性值。

  • 通过 of_property_read_string 函数获取 dts_led 节点的 status 属性值。

  • 通过 of_property_read_u32_array 函数获取 dts_led 节点的 reg 属性所有值,并且将获取到的值都存放到 regdata 数组中。

  • 使用 of_iomap 函数一次性完成读取 reg 属性以及内存映射, of_iomap 函数是设备树推荐使用的 OF 函数。

四、测试程序编写

测试程序具体代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#define LEDON   1
#define LEDOFF  0

/*
 * @Brief   main 主程序
 * @Call    Internal or External
 * @Param   argc:
 * @Param   argv:
 * @Note    NOne
 * @RetVal  0-成功;其他-失败
 */
int main(int argc, char *argv[])
{
    int fd, retval;
    char *filename;
    unsigned char databuf[1];

    if(argc != 3)
    {
        printf("argc != 3\n");
        return -1;
    }

    filename = argv[1];

    /*打开驱动文件*/
    fd = open(filename, O_RDWR);
    if(fd < 0)
    {
        printf("open filename:%d failed\n", filename);
        return -1;
    }

    /* 要执行的操作:打开或关闭 */
    databuf[0] = atoi(argv[2]);

    retval = write(fd, databuf, sizeof(databuf));
    if(retval < 0)
    {
        printf("write file:%s failed\n", filename);
    }

    /*关闭文件*/
    close(fd);

    return 0;
}

五、运行验证

        开发板上电,将dts_led.kodts_led_app 这两个文件拷贝到 /lib/modules/5.19.0-g794a2f7be62d-dirty/ 目录中,输入如下命令加载 dts_led.ko 驱动模块:

/lib/modules/5.19.0-g794a2f7be62d-dirty # ls
chrdev_driver.ko  led_chrdev.ko     modules.dep
dts_led.ko        modules.alias     modules.symbols
/lib/modules/5.19.0-g794a2f7be62d-dirty # insmod dts_led.ko 
[  132.383363] dts_led: loading out-of-tree module taints kernel.
[  132.394631] led_init new_chrdev major:242 minor:0
/lib/modules/5.19.0-g794a2f7be62d-dirty # 

        驱动加载成功以后就可以使用 dts_led_app 软件来测试驱动是否工作正常,输入如下命令打开LED 灯:

# ./dts_led_app /dev/dts_led 1    //打开 LED 灯

        输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭 LED 灯:

# ./dts_led_app /dev/dts_led 0    //关闭 LED 灯

        输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否熄灭。如果要卸载驱动的话输入如下命令即可:

rmmod dts_led.ko

        关于更多嵌入式C语言、FreeRTOS、RT-Thread、Linux应用编程、linux驱动等相关知识,关注公众号【嵌入式Linux知识共享】,后续精彩内容及时收看了解。

猜你喜欢

转载自blog.csdn.net/Wang_XB_3434/article/details/132032988