(六)pinctrl 和 gpio 子系统--LED

Linux 是一个庞大而完善的系统,尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班。Linux 内核提供了 pinctrl 和 gpio 子系统用于GPIO 驱动,现在我们就来学习一下如何借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。

pinctrl 子系统

pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
化 LED 灯所使用的 GPIO,步骤如下:
①、修改设备树,添加相应的节点,节点里面重点是设置 reg 属性,reg 属性包括了 GPIO相关寄存器。
②、获取 reg 属性中 GPIOI_MODER、GPIOI_OTYPER、GPIOI_OSPEEDR、GPIOI_PUPDR和 GPIOI_BSRR 这些寄存器的地址,并且初始化它们,这些寄存器用于设置 PI0 这个 PIN 的复用功能、上下拉、速度等。
③、 在②里面将 PI0 这个 PIN 设置为通用输出功能,因此需要设置 PI0 这个 GPIO 相关的寄存器,也就是设置 GPIOI_MODER 寄存器。
④、在②里面将 PI0 这个 PIN 设置为高速、上拉和推挽模式,就要需要设置 PI0 的GPIOI_OTYPER、GPIOI_OSPEEDR 和 GPIOI_PUPDR 这些寄存器。

Linux 内核针对 PIN的复用配置推出了 pinctrl 子系统,对于 GPIO 的电气属性配置推出了 gpio 子系统。大多数 SOC 的 pin 都是支持复用的,比如 STM32MP1 的 PI0 既可以作为普通的 GPIO 使用,也可以作为 TIM5 的 CH4 引脚等等。此外我们还需要配置 pin 的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引入的,pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。

STM32MP1 的 pinctrl 子系统驱动

1.PIN 配置信息详解
要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息,毕竟 pinctrl 子系统要根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。打开 stm32mp151.dtsi 文件,找到一个叫做 pinctrl 的节点,如下所示

 pinctrl: pin-controller@50002000 {
   #address-cells = <1>;
   #size-cells = <1>;
   compatible = "st,stm32mp157-pinctrl";
   ranges = <0 0x50002000 0xa400>;
   interrupt-parent = <&exti>;
   st,syscfg = <&exti 0x60 0xff>;
   hwlocks = <&hsem 0 1>;
   pins-are-numbered;
  ......
 };
 &pinctrl {
......
 m_can1_pins_a: m-can1-0 {
 pins1 {
 pinmux = <STM32_PINMUX('H', 13, AF9)>; /* CAN1_TX */
 slew-rate = <1>;
 drive-push-pull;
 bias-disable;
 };
 pins2 {
 pinmux = <STM32_PINMUX('I', 9, AF9)>; /* CAN1_RX */
 bias-disable;
 };
 };
 pwm1_pins_a: pwm1-0 {
 pins {
 pinmux = <STM32_PINMUX('E', 9, AF1)>, /* TIM1_CH1 */
 <STM32_PINMUX('E', 11, AF1)>, /* TIM1_CH2 */
 <STM32_PINMUX('E', 14, AF1)>; /* TIM1_CH4 */
 bias-pull-down;
 drive-push-pull;
 slew-rate = <0>;
 };
};
......
};

stm32-pinfunc.h 文件里面定义了一个 PIN 的所有配置项 AF0~AF9:

 /* define PIN modes */
 #define GPIO 0x0
 #define AF0 0x1
 #define AF1 0x2
 #define AF2 0x3
 #define AF3 0x4
 #define AF4 0x5
 #define AF5 0x6
 #define AF6 0x7
 #define AF7 0x8
 #define AF8 0x9
 #define AF9 0xa
 #define AF10 0xb
 #define AF11 0xc
 #define AF12 0xd
 #define AF13 0xe
 #define AF14 0xf
 #define AF15 0x10
 #define ANALOG 0x11
 #define RSVD 0x12

可以看出,除了 A0~AF15,还有 GPIO、ANALOG 这两个,如果一个 PIN 只是作为最基本的 GPIO 功能,那么就是用“GPIO”;如果这个引脚要用作模拟功能,比如 ADC 采集引脚,那么就设置为“ANALOG”。

2.电气属性配置
在这里插入图片描述

&pinctrl { 
 myuart4_pins_a: myuart4-0 {
 pins1{
 pinmux = <STM32_PINMUX('H', 13, AF8)>;
 bias-pull-up;
 drive-push-pull;
 };
 };

设备树中添加 pinctrl 节点模板
在设备树中添加某个外设的 PIN 信息。比如我们需要将 PG11 这个 PIN 复用为 UART4_TX 引脚,pinctrl 节点添加过程如下:

1、创建对应的节点
在 pinctrl 节点下添加一个“uart4_pins”节点:

 &pinctrl {
  uart4_pins: uart4-0 {
  /* 具体的 PIN 信息 */
 };
 };

2、添加“pins”属性
添加一个“pins”子节点,这个子节点是真正用来描述 PIN 配置信,要注意,同一个 pins子节点下的所有 PIN 电气属性要一样。如果某个外设所用的 PIN 可能有不同的配置,那么就需要多个 pins 子节点,例如 UART4 的 TX 和 RX 引脚配置不同,因此就有 pins1 和 pins2 两个子节点。这里我们只添加 UART4 的 TX 引脚,所以添加完 pins1 子节点以后如下所示:

&pinctrl {
 uart4_pins: uart4-0 {
 pins1{
 /* UART4 TX 引脚的 PIN 配置信息 */
 };
 };
 };

3、在“pins”节点中添加 PIN 配置信息
最后在“pins”节点中添加具体的 PIN 配置信息,完成以后如下所示:

&pinctrl {
  uart4_pins: uart4-0 {
  pins1{
  pinmux = <STM32_PINMUX('G', 11, AF6)>; /* UART4_TX */ 
  bias-disable;
 drive-push-pull;
 };
 };
 };

按道理来讲,当我们将一个 IO 用作 GPIO 功能的时候也需要创建对应的 pinctrl 节点,并且将所用的 IO 复用为 GPIO 功能,比如将 PI0 复用为 GPIO 的时候就需要设置 pinmux 属性为:<STM32_PINMUX(‘I’, 0, GPIO)>,但是!对于 STM32MP1 而言,如果一个 IO 用作 GPIO 功能的时候不需要创建对应的 pinctrl 节点!

gpio 子系统

如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。

STM32MP1 的 gpio 子系统驱动
1、设备树中的 gpio 信息
首先肯定是 GPIO 控制器的节点信息,以 PI0 这个引脚所在的 GPIOI 为例,打开stm32mp151.dtsi,在里面找到如下所示内容:

 pinctrl: pin-controller@50002000 {
 #address-cells = <1>;
 #size-cells = <1>;
 compatible = "st,stm32mp157-pinctrl";
......
 gpioi: gpio@5000a000 {
 gpio-controller;
 #gpio-cells = <2>;
 interrupt-controller;
 #interrupt-cells = <2>;
 reg = <0x8000 0x400>;
 clocks = <&rcc GPIOI>;
 st,bank-name = "GPIOI";
 status = "disabled";
 };
 };

设备树中 SD 卡节点

 enum gpio_lookup_flags {
 GPIO_ACTIVE_HIGH = (0 << 0),
 GPIO_ACTIVE_LOW = (1 << 0),
 GPIO_OPEN_DRAIN = (1 << 1),
 GPIO_OPEN_SOURCE = (1 << 2),
 GPIO_PERSISTENT = (0 << 3),
 GPIO_TRANSITORY = (1 << 3),
 GPIO_PULL_UP = (1 << 4),
 GPIO_PULL_DOWN = (1 << 5),
 GPIO_LOOKUP_FLAGS_DEFAULT = GPIO_ACTIVE_HIGH | GPIO_PERSISTENT,
 };

gpio 子系统 API 函数
对于驱动开发人员,设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO,gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。

int gpio_request(unsigned gpio, const char *label)
void gpio_free(unsigned gpio)
int gpio_direction_input(unsigned gpio)
int gpio_direction_output(unsigned gpio, int value)
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)

以下是关于 GPIO 函数及其作用的表格:

函数名称作用描述
gpio_request用于申请一个 GPIO 管脚。在使用任何 GPIO 之前,必须先调用此函数进行申请,以确保该管脚未被其他设备或驱动占用。
gpio_free用于释放不再使用的 GPIO 管脚。调用此函数可以释放之前通过 gpio_request 申请的 GPIO 资源。
gpio_direction_input将指定的 GPIO 设置为输入模式。配置该管脚为输入状态,以便读取外部信号。
gpio_direction_output将指定的 GPIO 设置为输出模式,并设置其默认输出值。配置该管脚为输出状态,并指定初始输出电平(高或低)。
gpio_get_value获取指定 GPIO 的当前值(0 或 1)。这是一个宏,用于读取 GPIO 的当前电平状态。
gpio_set_value设置指定 GPIO 的值(0 或 1)。这是一个宏,用于将 GPIO 设置为高电平(1)或低电平(0)。

设备树中添加 gpio 节点模板

led {
 compatible = "atk,led";
 gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
 status = "okay";
 };

与 gpio 相关的 OF 函数

int of_gpio_named_count(struct device_node *np, const char *propname)
int of_gpio_count(struct device_node *np)
int of_get_named_gpio(struct device_node *np,const char *propname,int index)

1、of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信息。
2、of_gpio_count 函数
函数统计的是“gpios”这个属性的 GPIO 数量,而of_gpio_named_count 函数可以统计任意属性的 GPIO 信息。
3、of_get_named_gpio 函数
此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpioi 0(GPIO_ACTIVE_LOW | GPIO_PULL_UP)>的属性信息转换
为对应的 GPIO 编号,此函数在驱动中使用很频繁!

LED 灯驱动程序编写

#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>

/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : gpioled.c
作者 : 正点原子 Linux 团队
版本 : V1.0
描述 : gpio 子系统驱动 LED 灯。
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2020/12/30 正点原子 Linux 团队创建
***************************************************************/
#define GPIOLED_CNT 1 /* 设备号个数 */
#define GPIOLED_NAME "gpioled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */

/* gpioled 设备结构体 */
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; /* led 所使用的 GPIO 编号 */
};

struct gpioled_dev gpioled; /* led 设备 */

/*
 * @description : 打开设备
 * @param – inode : 传递给驱动的 inode
 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
 * 一般在 open 的时候将 private_data 指向设备结构体。
 * @return : 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpioled; /* 设置私有数据 */
    return 0;
}

/*
 * @description : 从设备读取数据
 * @param - filp : 要打开的设备文件(文件描述符)
 * @param - buf : 返回给用户空间的数据缓冲区
 * @param - cnt : 要读取的数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
    return 0;
}

/*
 * @description : 向设备写数据
 * @param – filp : 设备文件,表示打开的文件描述符
 * @param - buf : 要写给设备写入的数据
 * @param - cnt : 要写入的数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 写入的字节数,如果为负值,表示写入失败
 */
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;
    struct gpioled_dev *dev = filp->private_data;

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

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

    if(ledstat == LEDON) {
        gpio_set_value(dev->led_gpio, 0); /* 打开 LED 灯 */
    } else if(ledstat == LEDOFF) {
        gpio_set_value(dev->led_gpio, 1); /* 关闭 LED 灯 */
    }
    return 0;
}

/*
 * @description : 关闭/释放设备
 * @param – filp : 要关闭的设备文件(文件描述符)
 * @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,
};

/*
 * @description : 驱动入口函数
 * @param : 无
 * @return : 无
 */
static int __init led_init(void)
{
    int ret = 0;
    const char *str;

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

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

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

    /* 3、获取 compatible 属性值并进行匹配 */
    ret = of_property_read_string(gpioled.nd, "compatible", &str);
    if(ret < 0) {
        printk("gpioled: Failed to get compatible property\n");
        return -EINVAL;
    }

    if (strcmp(str, "alientek,led")) {
        printk("gpioled: Compatible match failed\n");
        return -EINVAL;
    }

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

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

    /* 6、设置 PI0 为输出,并且输出高电平,默认关闭 LED 灯 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if(ret < 0) {
        printk("can't set gpio!\r\n");
    }

    /* 注册字符设备驱动 */
    /* 1、创建设备号 */
    if (gpioled.major) { /* 定义了设备号 */
        gpioled.devid = MKDEV(gpioled.major, 0);
        ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT,
GPIOLED_NAME);
        if(ret < 0) {
            pr_err("cannot register %s char driver [ret=%d]\n",
GPIOLED_NAME, GPIOLED_CNT);
            goto free_gpio;
        }
    } else { /* 没有定义设备号 */
        ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT,
GPIOLED_NAME); /* 申请设备号 */
        if(ret < 0) {
            pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n",
GPIOLED_NAME, ret);
            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 */
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
    if(ret < 0)
        goto del_unregister;

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

    /* 5、创建设备 */
    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);
    return -EIO;
}

/*
 * @description : 驱动出口函数
 * @param : 无
 * @return : 无
 */
static void __exit led_exit(void)
{
    /* 注销字符设备驱动 */
    cdev_del(&gpioled.cdev); /* 删除 cdev */
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class); /* 注销类 */
    gpio_free(gpioled.led_gpio); /* 释放 GPIO */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

编写测试 APP
运行测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值