[linux驱动开发] 基于gpiod API的platform总线多个led驱动开发

本人使用i.MX6ul开发板,不是市面主流开发板,不过和主流正点原子等厂家的cpu是一样的,设备树修改方式也很类似,知道设备树基本概念就知道第一部分增加的地方。

修改设备树源码

在官方提供的设备树源码,arch\arm\boot\dts\imx6ul-14x14-evk.dtsi 中的根节点下增加 led的节点内容,如果使用自己创建的设备树文件,那就在自己文件的根节点下添加

    my_gpio_led {
        compatible = "my_gpio_led";
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_my_gpio_led>;
        led0{
            gpios = <&gpio5 9 GPIO_ACTIVE_HIGH>;
            default-state = "off";
        };  

        led1{
            gpios = <&gpio4 16 GPIO_ACTIVE_HIGH>;
            default-state = "off";
        };  
    };  

在iomuxc中添加pinctrl对应的参数

    pinctrl_my_gpio_led: my_gpio_led{
        fdl,pins = <
           MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09   0x17059   /* my_gpio_led */
           MX6UL_PAD_NAND_DQS__GPIO4_IO16       0x17059  /*board led*/
        >;
    };

配置管脚为普通gpio管脚,如何去配置pinctrl第二个参数,如何计算,我单独开一个博客讲。
对于gpio配置理解较为容易,<> 中的三个参数,前两个决定哪一个管脚,第三个决定该gpio的激活状态,是高电平有效还是低电平有效,根据原理图与实际应用得出。
 

如何在驱动中获取设备树节点信息

本人没有使用单节点,很多教程使用单节点,通过常用的of_函数可以直接获取节点信息,多节点的设计也使得代码更加通用,在增加led时候,只需要在my_gpio_led节点下创建新的led子节点。

计算设备子节点数量

linux设备驱动提供了计算设备子节点数量的API,子节点数量即设备led数量

num_leds = device_get_child_node_count(dev);
if(num_leds <= 0)
{
    dev_err(dev, "No leds gpio assigned.\n");
    return -EINVAL;
}

dev即设备指针,类型struct device *的

给私有属性分配内存

常用教程使用全局变量保存私有属性,其实好处在于可以随处使用该变量,但坏处很多,并不建议这么做,官方代码也是不建议这么做的,坏处所体现的地方与临界资源相关,容易导致程序出问题,后面单独深究。

priv = devm_kzalloc(dev, sizeof_gpio_leds_priv(num_leds), GFP_KERNEL);//最后一个参数查看linux/gfp.h
if(!priv)
{
    return -ENOMEM;//错误码,errno-base.h中,内存不足
}

函数devm_kzalloc和kzalloc一样都是内核内存分配函数,但是devm_kzalloc是跟设备(装置)有关的,当设备(装置)被拆卸或者驱动(驱动程序)卸载(空载)时,内存会被自动释放。另外,当内存不再使用时,可以使用函数devm_kfree()释放。而kzalloc没有自动释放的功能,用的时候需要小心使用,如果忘记释放,会造成内存泄漏。

对子节点进行遍历

无需手动去一个一个获取,kernel提供了遍历字节点的一个宏进行遍历

device_for_each_child_node(dev, child) { //一个宏,遍历每个子节点

}

child 是固件子节点指针 即 struct fwnode_handle *

gpiod的获取

本文最重要的地方,将会查看kernel文档,看到最新的gpio文档建议放弃使用原本的gpio的API,建议使用gpiod库
在这里插入图片描述

  • 既然官方都这么说,那有何道理不使用呢,尽管原本的gpio子系统提供的接口方便简单,但是有他的弊端,pin管脚都是int型的返回,管理起来会出现问题,并且无法自动回收。
  • 至于gpiod,那个d就是描述(describe)的英文单词第一个字母,这个新接口就是使用描述符的方式进行封装,更加安全,只比原本的接口麻烦一丢丢。
    代码如下
gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child,   //获取子节点的gpio描述符,并且获取gpio
                GPIOD_ASIS,  //不初始化gpio方向状态
                NULL);

该函数是结合了上文的子节点进行获取gpiod的方法,主要关心第四个参数,是一个enum gpiod_flags类型的枚举,用于选择性地指定 GPIO 的方向和初始值。可以是

  • GPIOD_ASIS 或 0 根本不初始化 GPIO。必须稍后使用专用功能之一设置方向。
  • GPIOD_IN 将 GPIO 初始化为输入。
  • GPIOD_OUT_LOW 将 GPIO 初始化为值为 0 的输出。
  • GPIOD_OUT_HIGH 将 GPIO 初始化为值为 1 的输出。
  • GPIOD_OUT_LOW_OPEN_DRAIN 与 GPIOD_OUT_LOW 相同,但也强制线路在电气上使用开漏。
  • GPIOD_OUT_HIGH_OPEN_DRAIN 与 GPIOD_OUT_HIGH 相同,但也强制线路在电气上使用开漏。

根据设备树字节给的默认状态给gpio设置初始值

设备树默认状态属性总算用上了,并且通过代码方式获取到,通过代码自动配置它

     if(!fwnode_property_read_string(child, "default-state", &state))
     {
         if(!strncmp(state, "off", 3))
         {
             level = OFF; //默认状态为off时候
         }
         else
         {
             level = ON;
         }
     }

     if((rv = gpiod_direction_output(priv->leds[i].led_gpiod, level)) < 0) //设置gpio的输出初始电平
     {
         dev_err(dev, "Can't set gpio output for %s\n", priv->leds[i].name);
         break;
     }
  • 第一个函数是读取固件节点中"default-state"属性的字符值,字符串给到state
  • 第二个函数是gpiod设置gpio为输出方向和配置初始电平的接口函数

其实gpiod没有想象那么难,很多接口只是比原本的gpio接口多了一个d而已,原本int gpio参数更改为gpiod描述符的指针

把私有数据保存起来

我们在自定义的函数里面把数据提取出来了,去放哪儿呢?

  1. 通过函数返回值方式。
  2. 保存至设备区中的私有属性区。

当然第一种方式很多人会,使用分配的堆区进行保存私有数据,去使用它不过是一个地址而已,可以直接通过函数返回出去,让外部函数获取,外部函数即probe函数,但是有个问题,probe中可以用,那remove接口函数中咋用呢?没有了全局变量,感觉干啥都变得困难了。

其实linux内核都已经安排好了,给设备结构体分配了一块私有数据的保存的地方。

本人使用platform总线开发,kernel提供了保存的接口

platform_set_drvdata(pdev, priv);

函数原型,其实调用的是dev_set_drvdata函数

static inline void platform_set_drvdata(struct platform_device *pdev, void *data)
{
	dev_set_drvdata(&pdev->dev, data);
}

继续追下去,找到数据的流向,确实很友好,数据位置清晰可见。
在这里插入图片描述
有set函数当然就有get函数,没办法,它俩天生一对。

static inline void *platform_get_drvdata(const struct platform_device *pdev)
{
	return dev_get_drvdata(&pdev->dev);
}

使用起来也是很简单,只需要platform设备指针即可。

probe函数

解析完设备树,gpiod也配好了,那probe中只需要做一些常规事情,我直接贴代码,代码有注释,应该很好看懂。

	//1.初始化io状态
    rv = paser_dt_init_led(pdev);
    if(rv < 0)
    {
        return rv;
    }
    priv = platform_get_drvdata(pdev); //获取私有数据

    //2. 创建设备号
    if (0 != dev_major)
    {
        devno = MKDEV(dev_major, 0);
        rv = register_chrdev_region(devno, 1, DEV_NAME);//静态申请字符设备号
    }
    else
    {
        rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);//动态申请字符设备号
        dev_major = MAJOR(devno);//获取其主设备编号
    }

    if (rv < 0)
    {
        dev_err(&pdev->dev, "%s driver can't get major %d\n", DEV_NAME, dev_major);
        return rv;
    }

    //3.注册字符设备,将注册设备好和cdev绑定,交给内核
    cdev_init(&priv->cdev, &led_fops);  //初始化cdev,把fops添加进去
    priv->cdev.owner = THIS_MODULE;

    rv = cdev_add(&priv->cdev, devno, priv->num_leds);//注册给内核,设备数量1个
    if (0 != rv)
    {
        dev_err(&pdev->dev, "error %d add %s device failure.\n", rv, DEV_NAME);
        goto undo_major;  //撤销设备号
    }
	
    //4.创建类,驱动中进行节点创建
    priv->dev_class = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(priv->dev_class))
    {
        dev_err(&pdev->dev,"%s driver create class failure\n", DEV_NAME);
        rv = -ENOMEM;
        goto undo_cdev;
    }

    //5.创建设备
    for(i=0; i<priv->num_leds; i++)
    {
        devno = MKDEV(dev_major, i); //给每一个led设置设备号
        dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME"%d", i);
        if(IS_ERR(dev)) 
        {
            rv = -ENOMEM;   //返回错误码
            goto undo_class;
        }
    }

remove函数

对设备进行注销,记得释放gpio,虽然它可以自己释放,但这个好习惯,就像应用空间文件IO,有open就要记得不用时候close掉,系统给你擦屁股可不是好习惯。

    for (i=0; i<priv->num_leds; i++)
    {
        devno = MKDEV(dev_major, i);
        device_destroy(priv->dev_class, devno);//注销设备
    }
    class_destroy(priv->dev_class); //注销类

    cdev_del(&priv->cdev);//删除cdev
    unregister_chrdev_region(MKDEV(dev_major, 0), priv->num_leds); //释放设备号

    for(i=0; i<priv->num_leds; i++)
    {
        gpiod_set_value(priv->leds[i].led_gpiod, OFF);
        devm_gpiod_put(&pdev->dev, priv->leds[i].led_gpiod); //释放gpiod
    }

fops的操作函数

这之中我没有进行太复杂的操作,对io的控制没有使用读写操作,使用较为简单的ioctl。

open函数

通过inode的cdev属性找到私有数据的地址

    priv = container_of(inode->i_cdev, struct gpio_leds_priv, cdev);//找到私有数据的地址
    filp->private_data = priv;

利用强大的container_of函数找到一个结构体的首地址,然后把该地址放到file的私有数据区中

  • 函数宏 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.
 *
 */
#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

ioctl函数

使用魔术字,保证命令的唯一性。

#define PLATDRV_MAGIC       0x60     //魔术字
#define LED_OFF             _IO (PLATDRV_MAGIC, 0x18)
#define LED_ON              _IO (PLATDRV_MAGIC, 0x19)

然后利用ioctl的第三个参数,指定led设备

    switch (cmd) {
        case LED_OFF:/* variable case */
            if(priv->num_leds <= arg)
            {
                printk("led%ld doesn't exist\n", arg);
                return -ENOTTY;
            }
            gpiod_set_value(priv->leds[arg].led_gpiod, OFF);
            break;
        case LED_ON:
            if(priv->num_leds <= arg)
            {
                printk("led%ld doesn't exist\n", arg);
                return -ENOTTY;
            }
            gpiod_set_value(priv->leds[arg].led_gpiod, ON);
            break;
        default:
            printk("%s driver don't support ioctl command=%d\n", DEV_NAME, cmd);
            print_led_help();
    }   

使用gpiod的设置电平的api,进行电平的控制

platform设备init和exit

使用platform设备的注册函数和注销函数即可。

static int __init platdrv_led_init(void)
{
    int     rv = 0;

    rv = platform_driver_register(&gpio_led_driver);  //注册platform的led驱动
    if(rv)
    {   
        printk(KERN_ERR "%s:%d: Can't register platform driver %d\n", __FUNCTION__, __LINE__, rv);
        return rv; 
    }   
    printk("Regist imx 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);    //卸载驱动
}

模块init和exit

module_init(platdrv_led_init);
module_exit(platdrv_led_exit);

应用代码

简易的测试led闪烁,只需要open设备,利用ioctl系统调用即可,第二个参数为魔术字。

    ioctl(fd0, LED_ON, 0);
    ioctl(fd1, LED_OFF, 1);
    sleep(1);
    ioctl(fd0, LED_OFF, 0);
    ioctl(fd1, LED_ON, 1);
    sleep(1);

便可以看到led的交替闪烁。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值