Bootstrap

pinctrl 和 gpio 子系统,设备树下LED驱动


  说明:此博客为我自己对正点原子提供的资料学习的记录,内容大部分来源于正点原子资料,大家也可以去下载正点原子的官方资料学习,内容丰富。侵权删。

1、pinctrl子系统

1.1pinctrl子系统简介

  Linux 驱动讲究驱动分离与分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。不管什么外设驱动,GPIO 驱动基本都是必须的,而 pinctrl 和 gpio 子系统又是 GPIO 驱动必须使用的。
  如果没有pinctrl子系统和GPIO子系统,我们写一个LED驱动的步骤如下:

  • 修改设备树,添加相应的节点,节点里面重点是设置 reg 属性,reg 属性包括了 GPIO相关寄存器。
  • 获 取 reg 属 性 中 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并且初始化这两个寄存器,这两个寄存器用于设置 GPIO1_IO03 这个 PIN 的复用功能、上下拉、速度等。
  • 在上一步里面将 GPIO1_IO03 这个 PIN 复用为了 GPIO 功能,因此需要设置 GPIO1_IO03这个 GPIO 相关的寄存器,也就是 GPIO1_DR 和GPIO1_GDIR 这两个寄存器。
  • 在驱动程序中还得使用相应函数初始化各个寄存器的值。

  可知,只是配置一个LED驱动就要查找很多个寄存器,然后将其写入设备树,对于新手和老手都是一个不友好的行为,新手不知道要配置哪些寄存器,老手配置复杂的设备时也很麻烦。所以诞生了GPIO子系统和pinctrl子系统。
  pinctrl子系统主要工作内容是:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
  对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,pinctrl 子系统源码目录为 drivers/pinctrl。

1.2pinctrl子系统驱动

1.2.1 pin配置信息详解

  要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息,毕竟 pinctrl 子系统要根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。iomuxc 节点就是 I.MX6ULL 的 IOMUXC 外设对应的节点。
在这里插入图片描述
  上图就是向 iomuxc 节点追加数据,不同的外设使用的 PIN 不同、其配置也不同,因此一个萝卜一个坑,将某个外设所使用的所有 PIN 都组织在一个子节点里面。
  第 317~320 行,pinctrl_hog_1 子节点所使用的 PIN 配置信息,我们就以第 9 行的 UART1_RTS_B这个 PIN 为例,讲解一下如何添加 PIN 的配置信息,UART1_RTS_B 的配置信息如下:
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
  首先说明一下,UART1_RTS_B 这个 PIN 是作为 SD 卡的检测引脚,也就是通过此 PIN 就可 以 检 测 到 SD 卡 是 否 有 插 入 。 UART1_RTS_B 的 配 置 信 息 分 为 两 部 分 :MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 和 0x17059。
  对于一个 PIN 的配置主要包括两方面,一个是设置这个 PIN 的复用功能,另一个就是设置这个 PIN 的电气特性。所以我们可以大胆的猜测 UART1_RTS_B 的这两部分配置信息一个是设置 UART1_RTS_B 的复用功能,一个是用来设置 UART1_RTS_B 的电气特性。
  首先来看一下 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19,这是一个宏定义,定义在文件arch/arm/boot/dts/imx6ul-pinfunc.h 中,imx6ull.dtsi 会引用 imx6ull-pinfunc.h 这个头文件,而imx6ull-pinfunc.h 又会引用 imx6ul-pinfunc.h 这个头文件(绕啊绕!)。从这里可以看出,可以在设备树中引用 C 语言中.h 文件中的内容。MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 的宏定义内容如下:
在这里插入图片描述
  在芯片手册里可以查到这个寄存器的说明:(复用为GPIO1_IO19的话,就得将MUX_MODE设置为0101,即0x5)
在这里插入图片描述
在MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 定义后面跟着5个数字
0x0090 0x031C 0x0000 0x5 0x0
这五个数字分别代表:
<mux_reg conf_reg input_reg mux_mode input_val>

  • 0x0090:mux_reg 寄存器偏移地址
  • 0x031C:conf_reg 寄存器偏移地址
  • 0x0000:input_reg 寄存器偏移地址
  • 0x5 : mux_reg 寄 存 器 值(就是刚才说的复用为GPIO1_IO19的值)
  • 0x0:input_reg 寄存器值,在这里无效。

  MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 我们上面已经分析了,就剩下了一个 0x17059,反应快的朋友应该已经猜出来了,0x17059 就是 conf_reg 寄存器值!此值由用户自行设置,通过此值来设置一个 IO 的上/下拉、驱动能力和速度等。

在这里插入图片描述
在这里插入图片描述
查看芯片手册对应的描述就能配置cong_reg的值了。
具体内容可以查看博客:链接: 设备树pinctrl子系统中第二个参数的配置

2、gpio子系统

  gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
  接着pinctrl 配置好以后就是设置 gpio 了,SD 卡驱动程序通过读取 GPIO1_IO19 的值来判断 SD卡有没有插入,但是 SD 卡驱动程序怎么知道 CD 引脚连接的 GPIO1_IO19 呢?肯定是需要设备树告诉驱动啊!在设备树中 SD 卡节点下添加一个属性来描述 SD 卡的 CD 引脚就行了,SD卡驱动直接读取这个属性值就知道 SD 卡的 CD 引脚使用的是哪个 GPIO 了。
在这里插入图片描述
  第 766 行,属性“cd-gpios”描述了 SD 卡的 CD 引脚使用的哪个 IO。属性值一共有三个,我们来看一下这三个属性值的含义,“&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,“19”表示 GPIO1 组的第 19 号 IO,通过这两个值 SD 卡驱动程序就知道 CD 引脚使用了 GPIO1_IO19这 GPIO。“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。

2.1gpio子系统API函数

  对于驱动开发人员,设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO,gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。gpio 子系统提供的常用的 API 函数有下面几个:

(1)gpio_request 函数

  gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request进行申请,函数原型如下:

int gpio_request(unsigned gpio, const char *label)

函数参数和返回值含义如下:
gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号。
label:给 gpio 设置个名字。
返回值:0,申请成功;其他值,申请失败。

(2)gpio_free 函数

  如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放。函数原型如下:

void gpio_free(unsigned gpio)

函数参数和返回值含义如下:
gpio:要释放的 gpio 标号。
返回值:无。

(3)gpio_direction_input 函数

此函数用于设置某个 GPIO 为输入,函数原型如下所示:

int gpio_direction_input(unsigned gpio)

函数参数和返回值含义如下:
gpio:要设置为输入的 GPIO 标号。
返回值:0,设置成功;负值,设置失败。

(4)gpio_direction_output 函数

此函数用于设置某个 GPIO 为输出,并且设置默认输出值,函数原型如下:

int gpio_direction_output(unsigned gpio, int value)

函数参数和返回值含义如下:
gpio:要设置为输出的 GPIO 标号。
value:GPIO 默认输出值。
返回值:0,设置成功;负值,设置失败。

(5)gpio_get_value 函数

此函数用于获取某个 GPIO 的值(0 或 1),此函数是个宏,定义所示:

#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)

函数参数和返回值含义如下:
gpio:要获取的 GPIO 标号。
返回值:非负值,得到的 GPIO 值;负值,获取失败。

(6)gpio_set_value 函数

此函数用于设置某个 GPIO 的值,此函数是个宏,定义如下

#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)

函数参数和返回值含义如下:
gpio:要设置的 GPIO 标号。
value:要设置的值。
返回值:无
关于 gpio 子系统常用的 API 函数就讲这些,这些是我们用的最多的。

3、实例测试

3.1修改设备树

在这里插入图片描述
  可以看到,我将系统自带的系统灯给注释掉,添加了自己的my-leds灯,但是在my-leds节点下,还有两个节点,其中节点led1就是连接的系统灯。节点led0是一个外接的led灯。
在这里插入图片描述
将pinctrl也配置好之后编译设备树,加载设备树。

3.2 编写驱动文件

其中sysled没有使用,我把它注释掉了

#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>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define GPIOLED_CNT     1           /*设备号个数*/
#define GPIOLED_NAME    "my-leds"   /*名字*/
#define SYSLEDOFF       0           /*关灯 sysled*/
#define SYSLEDON        1           /*开灯 sysled*/
#define LEDOFF          2           /*led open*/
#define LEDON           3           /*led close*/

struct gpioled_dev
{
    dev_t devid;            //设备号
    struct cdev cdev;       //cdev
    struct class *class;    //类
    struct device *device;  //设备
    int major;              //主设备号
    int minor;              //次设备号
    struct device_node *nd; //设备节点
   // int led_gpio;           //sysled所使用的GPIO编号
    int led1_gpio;          //外接的led gpio编号
};

struct gpioled_dev gpioled;

static int led_open(struct inode *inode,struct file *filp)
{
    filp->private_data = &gpioled;
    return 0;
}

static ssize_t led_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
    return 0;
}

static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)
{
    int rv;
    unsigned char databuf[1];
    unsigned char ledstate;
    struct gpioled_dev *dev = filp->private_data;

    rv = copy_from_user(databuf,buf,cnt);       //接收用户发来的信息
    if(rv<0)
    {
        printk("kernel write failure!\r\n");
        return -EFAULT;
    }

    ledstate = databuf[0];  //获取状态值

/*    if(ledstate == SYSLEDON)
    {
        gpio_set_value(dev->led_gpio,1);
    }
    else if(ledstate == SYSLEDOFF)
    {
        gpio_set_value(dev->led_gpio,0);
    }*/
    if(ledstate == LEDON)
    {
        gpio_set_value(dev->led1_gpio,1);
    }
    else if(ledstate == LEDOFF)
    {
        gpio_set_value(dev->led1_gpio,0);
    }
    return 0;
}

static int led_release(struct inode *inode,struct file *filp)
{
    return 0;
}

static struct file_operations gpioled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

static int __init led_init(void)
{
    int rv = 0;
    const char *str;

    /*设置LED所使用的GPIO
    1.获取设备节点:gpioled*/
    gpioled.nd = of_find_node_by_path("/my_leds");//位置是设备树中的位置
    if(gpioled.nd == NULL)
    {
        printk("gpioled node not find!\r\n");
        return -EINVAL;
    }

    /*2.读取status属性*/
    rv = of_property_read_string(gpioled.nd,"status",&str);
    if(rv<0)
        return -EINVAL;

    if(strcmp(str,"okay"))
        return -EINVAL;

    /*3.获取compatible属性值进行匹配*/
    rv = of_property_read_string(gpioled.nd,"compatible",&str);
    if(rv<0)
    {
        printk("gpioled :Failed to get compatible property\r\n");
        return -EINVAL;
    }
    if(strcmp(str,"my-leds"))
    {
        printk("gpioled:Compatible match failed %s\r\n",str);
        return -EINVAL;
    }

    /*4.获取设备树中的gpio属性,得到LED所使用的LED编号*/
 /*   gpioled.led_gpio = of_get_named_gpio(of_find_node_by_path("/my_leds/led1"),"gpios",0);
    if(gpioled.led_gpio<0)
    {
        printk("can't get sysled gpios");
        return -EINVAL;
    }
        */
    gpioled.led1_gpio = of_get_named_gpio(of_find_node_by_path("/my_leds/led0"),"gpios",0);
    if(gpioled.led1_gpio<0)
    {
        printk("can't get led0 gpios");
        return -EINVAL;
    }
 //   printk("led-gpio num = %d\r\n",gpioled.led_gpio);
    printk("led-gpio num = %d\r\n",gpioled.led1_gpio);

    /*5.向GPIO子系统申请使用GPIO*/
 /*   rv = gpio_request(gpioled.led_gpio,"LED-GPIO");
    if(rv)
    {
        printk(KERN_ERR "gpioled:Failed to request led-gpio\r\n");
        return rv;
    }*/
    rv = gpio_request(gpioled.led1_gpio,"LED1-GPIO");
    if(rv)
    {
        printk(KERN_ERR "gpioled1:Failed to request led-gpio\r\n");
        return rv;
    }

    /*6.设置GPIO输出,并且输出低电平,默认关闭LED灯*/
  /*  rv = gpio_direction_output(gpioled.led_gpio,0);
    if(rv<0)
    {
        printk("can't set gpio! \r\n");
    }
*/
    rv = gpio_direction_output(gpioled.led1_gpio,0);
    if(rv<0)
    {
        printk("can't set gpio! \r\n");
    }
        gpioled.major=0;
        gpioled.minor=0;

    /*注册字符设备驱动*/
    /*1.创建设备号*/
    if(gpioled.major)
    {
        gpioled.devid = MKDEV(gpioled.devid,0);
        rv = register_chrdev_region(gpioled.devid,GPIOLED_CNT,GPIOLED_NAME);
        if(rv<0)
        {
            pr_err("can't register %s char driver [ret=%d]\n",GPIOLED_NAME,GPIOLED_CNT);
            goto free_gpio;
        }
    }
    else
    {
        rv = alloc_chrdev_region(&gpioled.devid,0,GPIOLED_CNT,GPIOLED_NAME);
        if(rv < 0)
        {
            pr_err("%s Can't alloc_chrdev_region,ret = %d\r\n",GPIOLED_NAME,rv);
            goto free_gpio;
        }
        gpioled.major = MAJOR(gpioled.devid); //获取分配号的主设备号
        gpioled.minor = MINOR(gpioled.devid); //获取分配号的次设备号
    }
    printk("gpioled major = %d,minor = %d\r\n",gpioled.major,gpioled.minor);

    /*2初始化cdev*/
    gpioled.cdev.owner = THIS_MODULE;
    cdev_init(&gpioled.cdev,&gpioled_fops);

    /*3.添加一个cdev*/
    rv = cdev_add(&gpioled.cdev,gpioled.devid,GPIOLED_CNT);
    if(rv<0)
    {
        goto del_unregister;
    }

    /*4.创建类*/
    gpioled.class = class_create(THIS_MODULE,GPIOLED_NAME);
    if(IS_ERR(gpioled.class))
    {
        goto del_cdev;
    }

    /*创建设备*/
    gpioled.device = device_create(gpioled.class,NULL,gpioled.devid,NULL,GPIOLED_NAME);
    if(IS_ERR(gpioled.device))
    {
        goto destroy_class;
    }
    return 0;

destroy_class:
    class_destroy(gpioled.class);
del_cdev:
    cdev_del(&gpioled.cdev);
del_unregister:
    unregister_chrdev_region(gpioled.devid,GPIOLED_CNT);
free_gpio:
//    gpio_free(gpioled.led_gpio);
    gpio_free(gpioled.led1_gpio);
    return -EIO;
}

static void __exit led_exit(void)
{
    /*注销字符设备驱动*/
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid,GPIOLED_CNT);
    device_destroy(gpioled.class,gpioled.devid);
    class_destroy(gpioled.class);
  //  gpio_free(gpioled.led_gpio);
    gpio_free(gpioled.led1_gpio);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Dai rongan");

makefile
和之前的就改变了目标文件的名字,如果想添加完善一下makefile可以添加一个删除中间文件的clear功能,删除各种编译产生的中间文件。

KERNELDIR := /home/dra/imx6ull_file/bsp/kernel/linux-imx

CURRENT_PATH := $(shell pwd)

obj-m := chrdevled.o

build:kernel_modules

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

测试程序

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

#define SYSLEDOFF 0
#define SYSLEDON  1
#define LEDOFF    2
#define LEDON     3

int main(int argc,char *argv[])
{
    int fd,rv;
    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 failure!\r\n",argv[1]);
        return -1;
    }

    databuf[0] = atoi(argv[2]);

    rv = write(fd,databuf,sizeof(databuf));
    if(rv<0)
    {
        printf("LED control Failed !\r\n");
        close(fd);
        return -1;
    }

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

实测结果:
在这里插入图片描述
在这里插入图片描述

总之:pinctrl子系统为设备树配置pin引脚提供了很多方便,使用pinctrl之后再驱动程序里也不用再调用各种API来配置相应寄存器的值。GPIO子系统也为使得驱动程序更简洁,更方便,不需要再映射各种寄存器的地址。

;