Linux驱动开发五.结合设备树的点灯试验


今天我们来试一下如何结合设备树文件来实现最基础的IO操作:点灯!

首先回忆一下前面我们在Linux系统下是如何实现点灯的:

  1. 完成驱动框架,通过modprobe命令加载驱动模块并完成/dev目录下的节点创建,这个过程比较复杂,包括创建dev结构体,获取设备号、创建class等一系列过程;
  2. 完成加载模块时对应的初始化函数,主要是通过ioremap获取GPIO对应寄存器的地址映射,将寄存器对应的地址写入相应初始化的值(裸机驱动里的点灯过程);
  3. 完成file_operations结构体里open、write、release等元素对应的函数,并通过cdev_init函数将其绑定至dev结构体。
  4. 完成模块注销时的流程

上面的过程是我们主要是在加载模块函数注册的初始化函数中实现地址映射的,在写驱动时候需要我们把所有要用到寄存器卸载驱动当中,实际使用过程是没有这样用的,太麻烦了也容易错。一般来说半导体厂商都给我们提供了设备树的支持,就是把需要用到的寄存器按照基地址和偏移量的方式写在设备树中了,这样便于做到驱动的分离、分层效果。下面会从简单的修改设备树开始,来实现点灯的效果。

修改设备树

理论上来说半导体厂商都给我们提供了设备树的支持,但是有些时候需要对其进行修改等操作,首先我们就先来自己创建一个led的节点,主要是给出操作gpio的对应物理地址。

我们现在使用的设备树的地址在内核路径下/linux/arch/arm/boot/dts/imx6ull-alientek-emmc.dts(修改以前确认下,设备树文件不是已经修改好的!可以运行查看一下(这里不截图了,下面有个加载新节点以后的截图)

没有led对应的设备,在设备树文件根节点下添加LED的设备树信息

//新添加LED信息
alphaLED {
    #address-cells=<1>;
    #size-cells= <1>;
    compatible = "alientek,alphaled";
    status = "okay";
    reg =  <0x020C406C 0x04
            0x020E0068 0x04
            0x020E02F4 0x04
            0x0209C000 0x04
            0x0209C004 0x04>;
};

主要的参数是reg,里面的参数是成对出现的,每组第一个是寄存器地址,第二个是长度,可以参考下前面led驱动里的内容

 

/**
 * @brief 寄存器物理地址
 * 
 */
#define CCM_CCGR1_BASE          (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE  (0X020E02F4)
#define GPIO1_GDIR_BASE         (0X0209C004)
#define GPIO1_DR_BASE           (0X0209C000)

 

一共5组寄存器,可以看出来reg里的内容和这里的地址是一样的,每个地址后面都是长度(4个32位寄存器)

 

注意:这里address-cells和size-cells的值可能有些问题,因为这两个cells指定的应该是子节点的地址长度(address-cells=1是用指用1个32数来描述地址,size-cell=1表示用1个32位的数值来描述寄存器的大小),而LED的节点我们是直接写在根节点下的,这里有点疑问,但是实际效果没什么问题,能成功点亮LED。

完成设备树信息修改以后可以直接用makefile编译设备树

make dtbs

如果我们是写一个新的设备树的话,一定要在dts文件夹下(设备树文件路径)的makefile里添加我们的新设备树文件信息,添加位置主要看我们CPU的类型

Linux驱动开发五.结合设备树的点灯试验

 

 

 我们所用的CPU是IMX6ULL的,找到该型号配置项下添加完成就行了(第419行),然后复制新编译的设备树文件(注意是dtb文件,别搞成dts文件来)到nfs目录下作为启动引导设备树文件。进入系统后查看下信息

Linux驱动开发五.结合设备树的点灯试验

 

 

注意那个alphaLED就是我们新建的设备树节点,如果是原始的设备树文件应该没有这个节点。在ls一下目标路径,可以发现有compatible、reg等各个属性,但是这里的属性值我们是无法直接打印出来的。就需要我们通过内核提供的of函数获取其信息。

驱动构成

首先我们先按照前面重构驱动框架的流程,编写一个驱动框架

完成驱动框架

这一版的框架比前面的补充了异常处理,处理的每一步都给了明确的注释。

/**
 * @file dtsled.c
 * @author your name (you@domain.com)
 * @brief led设备树点亮
 * @version 0.1
 * @date 2022-06-11
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>


#define DTSLED_CNT  1       //设备号个数
#define DTSLED_NAME    "DTSLED"   //设备名称

//cdev结构体
struct dtsled_dev
{
    dev_t   devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int     major;
    int     minor;
};

struct dtsled_dev dtsled;

/**
 * @brief 文件打开操作
 * 
 * @param inode 
 * @param filp 
 * @return int 
 */
static int dtsled_open(struct inode *inode , struct file *filp)
{
    filp->private_data = &dtsled;           //私有数据
    return 0;
}

/**
 * @brief 文件写操作
 * 
 * @param filp 
 * @param buf 
 * @param count 
 * @param ppos 
 * @return ssize_t 
 */
static ssize_t dtsled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;       //私有数据
    return 0;
}

/**
 * @brief 文件关闭操作
 * 
 * @param inode 
 * @param filp 
 * @return int 
 */
static int dtsled_release(struct inode *inode,struct file *filp)
{
    struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;        //私有数据
    return 0;
}



/**
 * @brief 文件操作集合
 * 
 */
static const struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .write = dtsled_write,
    .open =  dtsled_open,
    .release = dtsled_release,
};

/**
 * @brief 设备初始化
 * 
 * @return int 
 */
static int __init dtsled_init(void)
{
    int ret = 0; 

    //申请设备号
    dtsled.major = 0;
    if(dtsled.major){
        //手动指定设备号,使用指定的设备号
        dtsled.devid = MKDEV(dtsled.major,0);
        ret = register_chrdev_region(dtsled.devid,DTSLED_CNT,DTSLED_NAME);
    }
    else{
        //设备号未指定,申请设备号
        ret = alloc_chrdev_region(&dtsled.devid,0,DTSLED_CNT,DTSLED_NAME);
        dtsled.major = MAJOR(dtsled.devid);
        dtsled.minor = MINOR(dtsled.devid);
    }
    printk("dev id geted!/r/n");

    if(ret<0){
        //设备号申请异常,跳转至异常处理
        goto faile_devid;
    }

    //字符设备cdev初始化
    dtsled.cdev.owner = THIS_MODULE;

    cdev_init(&dtsled.cdev,&dtsled_fops);                 //文件操作集合映射

    ret = cdev_add(&dtsled.cdev,dtsled.devid,DTSLED_CNT);
    if(ret<0){
        //cdev初始化异常,跳转至异常处理
        goto fail_cdev;
    }

    printk("chr dev inited!/r/n");


    //自动创建设备节点
    dtsled.class = class_create(THIS_MODULE,DTSLED_NAME);
    if(IS_ERR(dtsled.class)){
        //class创建异常处理
        printk("class err!/r/n");
        ret = PTR_ERR(dtsled.class);
        goto fail_class;
    }
    printk("dev class created/r/n");
    dtsled.device = device_create(dtsled.class,NULL,dtsled.devid,NULL,DTSLED_NAME);
    if(IS_ERR(dtsled.device)){
        //设备创建异常处理
        printk("device err!/r/n");
        ret = PTR_ERR(dtsled.device);
        goto fail_device;
    }
    printk("device created!/r/n");

    return ret;
fail_device:
    //device创建失败,意味着class创建成功,应该将class销毁
    printk("device create err,class destroyed/r/n");
    class_destroy(dtsled.class);
fail_class:
    //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉
    printk("class create err,cdev del/r/n");
    cdev_del(&dtsled.cdev);
fail_cdev:
    //cdev初始化异常,意味着设备号已经申请完成,应将其释放
    printk("cdev init err,chrdev register/r/n");
    unregister_chrdev_region(dtsled.devid,DTSLED_CNT);
faile_devid:
    //设备号申请异常,由于是第一步操作,不需要进行其他处理
    printk("dev id err/r/n");
    return ret;
}

static void __exit dtsled_exit(void)
{

    //删除字符设备
    cdev_del(&dtsled.cdev);
    printk("chrdev del!/r/n");
    //释放设备号
    unregister_chrdev_region(dtsled.devid,DTSLED_CNT);
    printk("devID released/r/n");
    //销毁设备
    device_destroy(dtsled.class,dtsled.devid);
    printk("device destroyed/r/n");
    //销毁类
    class_destroy(dtsled.class);
    printk("class destroyed/r/n");
}

module_init(dtsled_init);
module_exit(dtsled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zzq");

框架完成以后,修改makefile,复制到rootfs路径下,加载试一下(加载前一定记得depmod一下!)

Linux驱动开发五.结合设备树的点灯试验

在/dev路径下能看到我们新创建的设备节点,说明框架是没有问题的。这里多说一句,框架完成以后要多加载和卸载几次,包括驱动完成以后也要把框架多加载和卸载几次,有可能卸载的时候哪个模块没清除干净可能导致某次模块加载干净已发bug!

设备树操作

模块能够顺利加载以后我们就要来从设备树搞数据了!这里不做兼容性等状态的判定,主要是从设备树节点信息中获取到reg里的数据。

这里获取reg数据的时候我们要用到of_property_read_u32_array函数。其中指针传的参数直接给一个数组就行,不再通过kmalloc申请内存了。这里设备树的操作我们新写一个函数,然后修改一下cdev结构体,新加一个成员device_node,然后在文件头部声明映射的地址

/**
 * @brief 寄存器映射
 * 
 */
static void __iomem *IMX6UL_CCM_CCGR1;
static void __iomem *IMX6UL_SW_MUX_GPIO1_IO03;
static void __iomem *IMX6UL_SW_PAD_GPIO1_IO03;
static void __iomem *IMX6UL_GPIO1_DR;
static void __iomem *IMX6UL_GPIO1_GDIR;

//cdev结构体
struct dtsled_dev
{
    dev_t   devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int    major;
    int    minor;
    struct device_node *dev_nd;
};

/**
 * @brief 设备树信息处理
 * 
 * @return int 
 */
static int DevTreeData(void){
    int ret = 0;
    u8  i=0;
    u32 regdata[10];
    const char *str;

    //获取节点
    dtsled.dev_nd = of_find_node_by_path("/alphaLED");
    if(dtsled.dev_nd == NULL){
    ret = -EINVAL;
    goto fail_findnd;
    }

    //获取status属性
    ret = of_property_read_string(dtsled.dev_nd,"status",&str);
    if(ret<0){
        printk("read string err/r/n");
        goto fail_readerr;
    }
    else{
        printk("status=%s/r/n",str);
    }

    //获取reg属性----最重要的地方
    ret = of_property_read_u32_array(dtsled.dev_nd,"reg",regdata,10);
    if(ret<0){
        printk("read reg err/r/n");
        goto fail_readerr;
    }
    else{
        printk("read reg data:/r/n");
        for(i=0;i<10;i++){
            printk("%#X ",regdata[i]);
        }
        printk("/r/n");
    }

    //获取地址映射
    IMX6UL_CCM_CCGR1 = ioremap(regdata[0],regdata[1]);
    IMX6UL_SW_MUX_GPIO1_IO03 = ioremap(regdata[2],regdata[3]);
    IMX6UL_SW_PAD_GPIO1_IO03 = ioremap(regdata[4],regdata[5]);
    IMX6UL_GPIO1_DR = ioremap(regdata[6],regdata[7]);
    IMX6UL_GPIO1_GDIR = ioremap(regdata[8],regdata[9]);

    return ret;
    
    //异常处理
    fail_readerr:
        printk("read data err/r/n");    
    fail_findnd:
        printk("find device node err/r/n");

    return ret;
}

然后我们把这个新函数在初始化的时候调用一下,重新make生成ko文件加载一下

Linux驱动开发五.结合设备树的点灯试验

可以看到读取到的寄存器的值了,和前面一章我们先申请了一段内存空间用来读取数组不同,由于我们要读取到数据长度时已知的,所以直接声明一个数组就行了(regdata[10])。

因为我们已经拿到了需要用到的地址的映射,其实到这里就可以回到我们前面led驱动的流程了。包括完善那些文件操作的对应函数,就不用重新再写了。最后把完整的驱动代码放出来(这个和教程给的有些区别,我把设备树相关的功能拿出来单独做了个函数,里面异常处理的部分没有相应的行为,但是教程里也是这样处理的)

Linux驱动开发五.结合设备树的点灯试验

/**
 * @file dtsled.c
 * @author your name (you@domain.com)
 * @brief led设备树点亮
 * @version 0.1
 * @date 2022-06-11
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>


#define DTSLED_CNT  1       //设备号个数
#define DTSLED_NAME    "DTSLED"   //设备名称

#define LEDOFF  0
#define LEDON   1
/**
 * @brief 寄存器映射
 * 
 */
static void __iomem *IMX6UL_CCM_CCGR1;
static void __iomem *IMX6UL_SW_MUX_GPIO1_IO03;
static void __iomem *IMX6UL_SW_PAD_GPIO1_IO03;
static void __iomem *IMX6UL_GPIO1_DR;
static void __iomem *IMX6UL_GPIO1_GDIR;

//cdev结构体
struct dtsled_dev
{
    dev_t   devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int    major;
    int    minor;
    struct device_node *dev_nd;
};

/**
 * @brief 切换LED状态
 * 
 * @param sta 
 */
static void led_switch(u8 sta)
{
    u32 val = 0;    
    if(sta == LEDON){
        val = readl(IMX6UL_GPIO1_DR);
        val &= ~(1<<3);
        writel(val,IMX6UL_GPIO1_DR);
    }
    else if(sta == LEDOFF){
        val = readl(IMX6UL_GPIO1_DR);
        val |= (1<<3);
        writel(val,IMX6UL_GPIO1_DR);
    }
}

struct dtsled_dev dtsled;

/**
 * @brief 文件打开操作
 * 
 * @param inode 
 * @param filp 
 * @return int 
 */
static int dtsled_open(struct inode *inode , struct file *filp)
{
    filp->private_data = &dtsled;           //私有数据
    return 0;
}

/**
 * @brief 文件写操作
 * 
 * @param filp 
 * @param buf 
 * @param count 
 * @param ppos 
 * @return ssize_t 
 */
static ssize_t dtsled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;
    int ret = 0;
    unsigned char databuf[1];                   //待写入的参数
    ret = copy_from_user(databuf,buf,count);    //获取从用户空间传递来的参数

    if (ret == 0){
        led_switch(databuf[0]);                 //根据参数改变LED状态
    }
    else{
        printk("kernelwrite err!/r/n");
        return -EFAULT;}
    return 0;
}

/**
 * @brief 文件关闭操作
 * 
 * @param inode 
 * @param filp 
 * @return int 
 */
static int dtsled_release(struct inode *inode,struct file *filp)
{
    struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;        //私有数据
    return 0;
}



/**
 * @brief 文件操作集合
 * 
 */
static const struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .write = dtsled_write,
    .open =  dtsled_open,
    .release = dtsled_release,
};


/**
 * @brief 设备树信息处理
 * 
 * @return int 
 */
static int DevTreeData(void){
    int ret = 0;
    u8  i=0;
    u32 regdata[10];
    const char *str;

    //获取节点
    dtsled.dev_nd = of_find_node_by_path("/alphaLED");
    if(dtsled.dev_nd == NULL){
    ret = -EINVAL;
    goto fail_findnd;
    }

    //获取status属性
    ret = of_property_read_string(dtsled.dev_nd,"status",&str);
    if(ret<0){
        printk("read string err/r/n");
        goto fail_readerr;
    }
    else{
        printk("status=%s/r/n",str);
    }

    //获取reg属性----最重要的地方
    ret = of_property_read_u32_array(dtsled.dev_nd,"reg",regdata,10);
    if(ret<0){
        printk("read reg err/r/n");
        goto fail_readerr;
    }
    else{
        printk("read reg data:/r/n");
        for(i=0;i<10;i++){
            printk("%#X ",regdata[i]);
        }
        printk("/r/n");
    }

    //获取地址映射
    IMX6UL_CCM_CCGR1 = ioremap(regdata[0],regdata[1]);
    IMX6UL_SW_MUX_GPIO1_IO03 = ioremap(regdata[2],regdata[3]);
    IMX6UL_SW_PAD_GPIO1_IO03 = ioremap(regdata[4],regdata[5]);
    IMX6UL_GPIO1_DR = ioremap(regdata[6],regdata[7]);
    IMX6UL_GPIO1_GDIR = ioremap(regdata[8],regdata[9]);

    return ret;

    //异常处理
    fail_readerr:
        printk("read data err/r/n");    
    fail_findnd:
        printk("find device node err/r/n");

    return ret;
}

/**
 * @brief 设备初始化
 * 
 * @return int 
 */
static int __init dtsled_init(void)
{
    int ret = 0; 
    unsigned int val = 0;

    //申请设备号
    dtsled.major = 0;
    if(dtsled.major){
        //手动指定设备号,使用指定的设备号
        dtsled.devid = MKDEV(dtsled.major,0);
        ret = register_chrdev_region(dtsled.devid,DTSLED_CNT,DTSLED_NAME);
    }
    else{
        //设备号未指定,申请设备号
        ret = alloc_chrdev_region(&dtsled.devid,0,DTSLED_CNT,DTSLED_NAME);
        dtsled.major = MAJOR(dtsled.devid);
        dtsled.minor = MINOR(dtsled.devid);
    }
    printk("dev id geted!/r/n");

    if(ret<0){
        //设备号申请异常,跳转至异常处理
        goto faile_devid;
    }

    //字符设备cdev初始化
    dtsled.cdev.owner = THIS_MODULE;

    cdev_init(&dtsled.cdev,&dtsled_fops);                 //文件操作集合映射

    ret = cdev_add(&dtsled.cdev,dtsled.devid,DTSLED_CNT);
    if(ret<0){
        //cdev初始化异常,跳转至异常处理
        goto fail_cdev;
    }

    printk("chr dev inited!/r/n");


    //自动创建设备节点
    dtsled.class = class_create(THIS_MODULE,DTSLED_NAME);
    if(IS_ERR(dtsled.class)){
        //class创建异常处理
        printk("class err!/r/n");
        ret = PTR_ERR(dtsled.class);
        goto fail_class;
    }
    printk("dev class created/r/n");
    dtsled.device = device_create(dtsled.class,NULL,dtsled.devid,NULL,DTSLED_NAME);
    if(IS_ERR(dtsled.device)){
        //设备创建异常处理
        printk("device err!/r/n");
        ret = PTR_ERR(dtsled.device);
        goto fail_device;
    }
    printk("device created!/r/n");


    ret = DevTreeData();

    val = readl(IMX6UL_CCM_CCGR1);  //读取CCM_CCGR1的值
    val &= ~(3<<26);                //清除bit26、27
    val |= (3<<26);                //bit26、27置1
    writel(val, IMX6UL_CCM_CCGR1);
    printk("CCM init finished!/r/n");

    /*GPIO初始化*/
    writel(0x5, IMX6UL_SW_MUX_GPIO1_IO03);
    writel(0x10B0, IMX6UL_SW_PAD_GPIO1_IO03);
    printk("GPIO SW init finished!/r/n");

    val = readl(IMX6UL_GPIO1_GDIR);
    val |= 1<<3;                        //bit3=1,设置为输出
    writel(val, IMX6UL_GPIO1_GDIR);
    printk("GPIO GDIR init finished!/r/n");

    val = readl(IMX6UL_GPIO1_DR);
    val &= ~(1<<3);
    writel(val,IMX6UL_GPIO1_DR);

    return ret;
fail_device:
    //device创建失败,意味着class创建成功,应该将class销毁
    printk("device create err,class destroyed/r/n");
    class_destroy(dtsled.class);
fail_class:
    //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉
    printk("class create err,cdev del/r/n");
    cdev_del(&dtsled.cdev);
fail_cdev:
    //cdev初始化异常,意味着设备号已经申请完成,应将其释放
    printk("cdev init err,chrdev register/r/n");
    unregister_chrdev_region(dtsled.devid,DTSLED_CNT);
faile_devid:
    //设备号申请异常,由于是第一步操作,不需要进行其他处理
    printk("dev id err/r/n");
    return ret;
}

static void __exit dtsled_exit(void)
{

    //删除字符设备
    cdev_del(&dtsled.cdev);
    printk("chrdev del!/r/n");
    //释放设备号
    unregister_chrdev_region(dtsled.devid,DTSLED_CNT);
    printk("devID released/r/n");
    //销毁设备
    device_destroy(dtsled.class,dtsled.devid);
    printk("device destroyed/r/n");
    //销毁类
    class_destroy(dtsled.class);
    printk("class destroyed/r/n");
}

module_init(dtsled_init);
module_exit(dtsled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zzq");

dtsled.c

编译以后放在rootfs下,还使用原来led的测试app就可以,加载模块以后可以直接操作LED的亮灭效果

Linux驱动开发五.结合设备树的点灯试验

 

这样就完成了设备树下LED的点亮效果。

总结一下

 这一章没什么难点,主要就是利用of函数获取了设备树中寄存器的地址。其他的部分和原先是一样的。

原创文章,作者:,如若转载,请注明出处:https://blog.ytso.com/267757.html

(0)
上一篇 2022年6月19日
下一篇 2022年6月19日

相关推荐

发表回复

登录后才能评论