【嵌入式linux学习】04_Pinctrl 和 GPIO子系统

Pinctrl 和 GPIO子系统

开始记录 Pinctrl 和 GPIO子系统~~ 参考自北京讯为电子 和 野火官网文档。最后一部分是实操部分,基于鲁班猫4 rk3588s

Pinctrl子系统管理所有的引脚,GPIO子系统是这些引脚的用途之一, GPIO子系统会向Pinctrl子系统申请引脚,并设置为GPIO功能。申请GPIO的时候会去检查该gpio所对应的pin是否已经被其他驱动申请作其他功能了,如果已经被申请则申请时会报错。


文章目录

Pinctrl子系统

传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引入的

1️⃣获取设备树中 pin 信息,管理系统中所有的可以控制的 pin,在系统初始化的时候,枚举所有可以控制的 pin,并标识这些 pin。

2️⃣根据获得到的 pin 信息来设置 pin 的复用功能,对于 SOC 而言,其引脚除了配置成普通的 GPIO 之外,若干个引脚还可以组成一个 pin group,形成特定的功能。

3️⃣根据获得到的 pin 信息来设置 pin 的电气特性,比如上下拉、速度、驱动能力

对于我们使用者来说,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由pinctrl 子系统来完成,pinctrl 子系统源码目录 drivers/pinctrl。

pinctrl核心层是内核抽象出来,向下为个SoC pin controler drvier提供底层通信接口的能力, 向上为其他驱动提供了控制pin的能力,比如pin复用、配置引脚的电气特性,同时也为GPIO子系统提供pin操作。而pin控制器驱动层,主要提供了操作pin的方法。

重要概念

pinctrl核心层提供struct pinctrl_desc,具体soc厂商根据这个抽象出一个pin controller实例, 把系统中所有的pin描述出来,并建立索引,实际是struct pinctrl_desc结构中pins和npins来完成。 相应的就有pin controller driver

Pinctrl驱动对引脚的管理,通过抽象出pin groups和function实现

配置

想要配置某个外设而需要配置某一个引脚为 GPIO 时,一般的配置流程是这样的:

1️⃣在 IOMUXC/pinctrl 中对某一个引脚进行配置,两个部分(复用、初始化),在外设节点中调用

2️⃣在驱动中获取设备节点以及 GPIO

3️⃣对 GPIO 进行配置

示例 1:单状态配置
c 复制代码
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;

说明

  • pinctrl-names = "default" :定义设备的状态,可以有多个状态,default 为状态 0
  • pinctrl-0 = <&pinctrl_hog_1> :第 0 个状态所对应的引脚配置,也就是 default 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_hog_1 里面的管脚配置

示例 2:多状态配置
c 复制代码
pinctrl-names = "default", "wake up";
pinctrl-0 = <&pinctrl_hog_1>;
pinctrl-1 = <&pinctrl_hog_2>;

说明

  • pinctrl-names = "default", "wake up" :设备的状态,可以有多个状态,default 为状态 0,wake up 为状态 1
  • pinctrl-0 = <&pinctrl_hog_1> :第 0 个状态所对应的引脚配置,也就是 default 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_hog_1 里面的管脚配置
  • pinctrl-1 = <&pinctrl_hog_2> :第 1 个状态所对应的引脚配置,也就是 wake up 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_hog_2 里面的管脚配置

要点pinctrl-x 中的数字对应 pinctrl-names 中定义的状态顺序,从 0 开始计数。

实例3
c 复制代码
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1 & pinctrl_hog_2>;

pinctrl-names = "default"; 设备的状态,可以有多个状态,default 为状态 0

pinctrl-0 = <&pinctrl_hog_1&pinctrl_hog_2>;第 0 个状态所对应的引脚配置,也就是 default 状态对应的引脚在 pin controller 里面定义好的节点,对应 pinctrl_hog_1 和 pinctrl_hog_2 这俩个节点的管脚配置。

主要的数据结构和接口

一般控制器驱动匹配设备,调用probe,最后会调用pinctrl_register函数,向内核注册pinctrl,产生pinctrl_dev

c 复制代码
struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
           struct device *dev, void *driver_data);

描述一个引脚的结构体 struct pinctrl_pin_desc:

c 复制代码
struct pinctrl_pin_desc {
   unsigned number;
   const char *name;
   void *drv_data;
};

很多pin组合在一起,实现特定功能,使用struct group_desc:

c 复制代码
struct group_desc {
   const char *name;
   int *pins;
   int num_pins;
   void *data;
};

GPIO子系统

在pinctrl子系统中把pin脚初始化成了普通GPIO后,就可以使用GPIO子系统的接口去操作IO口的电平、中断等。驱动开发者在设备树中添加gpio相关信息, 然后就可以在驱动程序中使用gpio子系统提供的API函数来操作GPIO,极大的方便了驱动开发者使用GPIO。gpio子系统的代码在内核源码/drivers/gpio/目录下。

GPIO的核心是gpiolib框架,向上提供一些gpio接口给其他驱动调用,向下提供用于gpio资源注册函数。 上层的其他驱动,比如LED的驱动,可以通过函数向gpiolib申请gpio,然后设置和使用gpio。下层的控制器驱动(一般是SOC厂商编写),启动时会注册gpio资源到gpiolib,比如引脚数量,操作函数等等

通过GPIO子系统要实现:

1️⃣引脚功能的配置包括:

  • 设置为 GPIO
  • 配置特殊功能
  • 设置 GPIO 方向(输入/输出)
  • 设置为中断等

2️⃣实现软硬件分离: 分离硬件差异,由厂商提供底层支持;软件分层,驱动只需调用接口 API 即可操作 GPIO

3️⃣IOMMU 内存管理: 直接调用宏即可操作 GPIO

GPIO 子系统常用api

c 复制代码
int of_get_named_gpio(struct device_node *np,const char *propname,int index)//此函数获取 GPIO 编号
gpio_request(leddev.ledo, "led0");//gpio_request 函数用于申请一个 GPIO 管脚
gpio_direction_output(leddev.ledo, 1);//此函数用于设置某个 GPIO 为输出,并且设置默认输出值
gpio_set_value(leddev.ledo, 0);//此函数用于获取某个 GPIO 的值(0 或 1)
gpio_direction_input(key.irqkeydesc[0].gpio);//此函数用于设置某个 GPIO 为输入
gpio_get_value(keydesc->gpio);//此函数用于设置某个 GPIO 的值
gpio_free(leddev.ledo);//要释放的 gpio 标号

重要结构体

GPIO子系统,主要的数据结构有gpio_device、gpio_chip、gpio_desc等,主要是为了描述gpio控制器,有引脚信息,中断信息,以及相关操作函数等。

LED实验------鲁班猫4 rk3588s

使用GPIO子系统实现LED驱动,GPIO子系统要用到pinctrl子系统。

我们仍然根据前面几节,用GPIO4_B5作为led引脚

pinctrl子系统主要用于管理芯片的引脚,比如引脚的复用,引脚上下拉,驱动能力等。rockchip芯片拥有众多的片上外设, 大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制,例如我们熟悉的I2C、SPI、LCD、USDHC等等。 而我们知道芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性, 一个芯片引脚往往可以做为多个片上外设的功能引脚

pinctrl子系统和GPIO子系统配置

在编程过程中,无论是裸机还是驱动, 一般首先要设置引脚的复用功能并且设置引脚的PAD属性(驱动能力、上下拉等等)。

pinctrl子系统是由芯片厂商来实现的,简单来说用于帮助我们管理芯片引脚并自动完成引脚的初始化, 而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可

以lubuncat4为例,在rk3588s-lubancat-4.dts添加以下内容。

c 复制代码
led_test{
		led_test_pin:led_test_pin{
			rockchip,pins = <4 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};

如下图所示:【记得注意分号】

新增的节点名为"led_test",名字任意选取(在同一节点下不要同名),长度不要超过32个字符,最好能表达出节点的信息。 "led_test_pin"节点标签,"rockchip,pins"是固定的格式,后面的内容自定义的,我们将通过这个标签引用这个节点。

在添加完pinctrl子节点后,系统会根据我们添加的配置信息将引脚初始化为GPIO功能

在没有使用GPIO子系统之前,如果我们想点亮一个LED,首先要得到led相关的配置寄存器,再手动地读、改、写这些配置寄存器实现 控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统帮我们完成,我们只需要调用GPIO子系统提供的API函数即可完成GPIO的 控制动作。

在rk3588.dtsi文件中的pinctrl子节点记录着GPIO控制器的寄存器地址,下面我们以GPIO4为例介绍GPIO4子节点相关内容

下面是rk3588.dtsi里面GPIO4节点的内容

c 复制代码
	pinctrl: pinctrl {
		compatible = "rockchip,rk3588-pinctrl";
		rockchip,grf = <&ioc>;
		#address-cells = <2>;
		#size-cells = <2>;
		ranges;

		/* 以上内容省略 */
		gpio4: gpio@fec50000 {
			compatible = "rockchip,gpio-bank";
			reg = <0x0 0xfec50000 0x0 0x100>;
			interrupts = <GIC_SPI 281 IRQ_TYPE_LEVEL_HIGH>;
			clocks = <&cru PCLK_GPIO4>, <&cru DBCLK_GPIO4>;

			gpio-controller;
			#gpio-cells = <2>;
			gpio-ranges = <&pinctrl 0 128 32>;
			interrupt-controller;
			#interrupt-cells = <2>;
		};
	};

gpio4这个节点对整个gpio4进行了描述。使用GPIO子系统时需要往设备树中添加设备节点,在驱动程序中使用GPIO子系统提供的API 实现控制GPIO的效果。

在设备树里面添加led的设备树节点

相比之前设备树led灯设备节点(没有使用GPIO子系统),以下只需要增加GPIO属性定义,基于GPIO子系统的led_test设备树节点。

以lubuncat4为例,添加到rk3588s-lubancat-4.dts设备树的 根节点内 ,添加完成后的设备树如下所示。

c 复制代码
/*添加led_test节点*/
led_test: led_test {
    status = "okay";
    compatible = "fire,led_test";
    default-state = "on";
    gpios = <&gpio4 RK_PB5 GPIO_ACTIVE_HIGH>;
    pinctrl-names = "default";
    pinctrl-0 = <&led_test_pin>;
};

系统led灯使用了内核自带的led驱动,由于会与本实验冲突,我们需要将其屏蔽掉。找到leds的节点信息,将status设置为disabled,从而关闭leds节点。

总结

rk3588s-lubancat-4.dts文件:

&pinctrl

编译、下载设备树验证修改结果

在内核目录执行编译的命令

c 复制代码
#加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

如果编译没有通过的话,大概率是语法的问题,比如是不是漏了分号

此时编译成功后生成的设备树文件(.dtb)位于源码目录下的=arch/arm64/boot/dts/rockchip/

然后这边具体怎么检查是否正确,可以参考我之前写的blog的编译设备树的部分

重启之后可以看到

也可以查看:"/proc/device-tree"目录下有"led_test"和"leds"节点,并且"led_test"节点的状态为"okay","leds"节点的状态为"disabled"

实验代码编写

程序包含两个C语言文件,一个是驱动程序,驱动程序在平台总线基础上编写。 另一个是一个简单的测试程序,用于测试驱动是否正常。

驱动程序大致分为三个部分,第一部分,编写平台设备驱动的入口和出口函数。第二部分,编写平台设备的.probe函数, 在probe函数中实现字符设备的注册和LED灯的初始化。第三部分,编写字符设备函数集,实现open和write函数。

led_driver.c 驱动程序编写
c 复制代码
#include<linux/init.h>
#include<linux/module.h>
#include<linux/platform_device.h>
#include<linux/mod_devicetable.h>
#include <linux/ioport.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>//包含了 copy_to_user、copy_from_user 等内核访问用户进程内存地址的函数定义。
#include <linux/io.h>//包含了 ioremap、iowrite 等内核访问 IO 内存等函数的定义。
#include <linux/kernel.h>
#include <linux/gpio/consumer.h> /* GPIO描述符*/
#include <linux/of_gpio.h>




static int major;
static struct class *led_class;
int led;


ssize_t led_write (struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t){
    char val;
    int ret;
    ret=copy_from_user(&val,ubuf,1);
    if(val=='0'){
         //假设输入0 为灭灯
        //*gpio4_dr=(1<<(GPIO_PIN_BIT+16))|(1<<GPIO_PIN_BIT);
        
        gpio_direction_output(led, 1);  // 引脚输出高电平,红灯灭
    }else if(val=='1'){
        //假设输入1 为亮灯
       gpio_direction_output(led, 0);  // 引脚输出高电平,红灯亮
    }
    return size;
}

static int led_open(struct inode *inode,struct file *file){
    //配置gpio
    printk("led open \n");
    return 0;
}

static struct file_operations led_fops={
    .owner=THIS_MODULE,
    .open=led_open,
    .write=led_write,
};


int led_remove(struct platform_device *pdev){
    printk("led_remove\n");
    return 0;
}


int led_probe(struct platform_device *pdev){
    struct device_node *led_device_node;
    
    printk("led_probe------match successed\n");

    //获取设备节点_在设备树的根节点 / 下寻找名字叫 led_test 的子节点。
    led_device_node = of_find_node_by_path("/led_test");
    if(led_device_node == NULL){
        printk(KERN_EMERG "get led_test failed!  \n");
    }
    
   //从刚才找到的 led_device_node 节点中,读取名为 "gpios" 的属性,并解析出内核使用的 全局 GPIO 编号 (int 类型)
    led = of_get_named_gpio(led_device_node, "gpios", 0);
    printk("led = %d \n",led);

    /*设置gpio输出高电平*/
    gpio_direction_output(led, 1);

    //注册设备
    major=register_chrdev(0,"myled",&led_fops);

    led_class=class_create(THIS_MODULE,"myled");

    //创建主设备号为major 次设备号为0 的class 创建/dev/myled 的节点
    device_create(led_class,NULL,MKDEV(major,0),NULL,"myled");

    return 0;
}



//================平台驱动框架=====================
static const struct of_device_id led_ids[] = {
    { .compatible = "fire,led_test"},
    { /* sentinel */ }
};

const struct platform_device_id led_idtable={
    .name="led_test",
};

struct platform_driver led_driver={
    .probe=led_probe,
    .remove=led_remove,
    .driver={
        .owner=THIS_MODULE,
        .name="leds-platform",
        .of_match_table = led_ids,
    },
    .id_table=&led_idtable
};

//平台设备注册
static int led_driver_init(void){
    int ret=0;
    ret=platform_driver_register(&led_driver);
    if(ret<0){
        printk("platform_driver_register error\n");
    }
    printk("platform_driver_register ok ssss\n");

    return 0;
}

//平台设备注销
static void __exit led_driver_exit(void){

    printk(KERN_EMERG "led_test exit!\n");
    platform_driver_unregister(&led_driver);
    device_destroy(led_class,MKDEV(major,0));
    class_destroy(led_class);

    unregister_chrdev(major,"myled");
    printk("goodbye!\n");
}


module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
app------应用程序编写
c 复制代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main(int argc, char const *argv[])
{
    // 1. 校验参数,防止用户没输入参数导致崩溃
    if (argc != 2) {
        printf("Usage: %s <1|0>\n", argv[0]);
        return -1;
    }

    int fd = open("/dev/myled", O_RDWR);
    if (fd < 0) {
        perror("open error");
        return -1;
    }

    // 2. 直接获取参数的第一个字符
    // 如果输入 ./app 1,argv[1]是字符串 "1",argv[1][0] 就是字符 '1'
    char mybuf = argv[1][0]; 

    // 3. 写入数据
   
    write(fd, &mybuf, 1);
    
    printf("Set LED to %c\n", mybuf);
    
    close(fd);
    return 0;
}
相关推荐
475.352 小时前
linux-journal日志清理
linux·运维·服务器
weixin_438732102 小时前
ChromeDriver谷歌驱动下载
linux·chrome·selenium·自动化·mac·chrome devtools·chromedriver
Black__Jacket2 小时前
Ubuntu下,/dev下,无法读取到CH340串口芯片的端口号
linux·运维·ubuntu
清泉影月3 小时前
Linux:Squid正向代理实现内网访问互联网
linux·运维·服务器
切糕师学AI3 小时前
ARM 中的 SVC 监管调用(Supervisor Call)
linux·c语言·汇编·arm开发
陌上花开缓缓归以3 小时前
linux jiffies 初始化不为0问题分析
linux·arm开发
霖霖总总3 小时前
[小技巧39]Linux 文件与命令查找工具(which、whereis、locate、find)全面解析
linux·运维
xlq223223 小时前
6.Linux权限
linux
ayaya_mana3 小时前
在 CentOS 7/RHEL 7 上安装并切换至新版内核
linux·运维·centos