Pinctrl和GPIO子系统

目录

  1. 1. Pinctrl子系统
    1. 1.1. 示例代码
    2. 1.2. 1. 主要功能
      1. 1.2.1. Pin Controller 概念
      2. 1.2.2. Client Device 概念
    3. 1.3. 2. 关键组件
    4. 1.4. 3. 使用示例
    5. 1.5. 4. API 接口
    6. 1.6. 5. 适用场景
    7. 1.7. 6. 总结
  2. 2. GPIO子系统
    1. 2.1. GPIO 控制器在设备树中的定义
    2. 2.2. 在设备节点中引用 GPIO 引脚
    3. 2.3. 在驱动代码中调用 GPIO 子系统
    4. 2.4. 在sysfs中访问GPIO的方式

Pinctrl 子系统:负责管理引脚的复用与配置。它允许将引脚配置为不同的功能(如 GPIO、I2C、UART 等)并设置引脚的电气特性(如上拉、下拉等)。
GPIO 子系统: 主要负责对通用输入输出引脚的管理。它提供 API 来配置引脚为输入或输出,并控制引脚的电平状态。
层次结构: Pinctrl 子系统位于 GPIO 子系统之上。Pinctrl 负责引脚的初步配置和复用,而 GPIO 子系统则利用这些配置来实现输入输出操作,两者有密切的关系,先配置Pinctrl,在配置GPIO。

  • 引脚配置:首先,通过 Pinctrl 子系统配置引脚的功能。例如,将某个引脚设置为 GPIO 功能。
  • 状态管理:然后,GPIO 子系统使用这些配置来管理引脚的电平状态(读取或设置引脚电平)。

Pinctrl子系统

pinctrl 子系统是 Linux 内核中用于管理 GPIO(通用输入输出)引脚和其他引脚控制功能的一个重要组件。它的主要作用是提供一个统一的接口来配置和控制各种硬件引脚的功能,例如输入、输出、复用等。以下是对 pinctrl 子系统的详细介绍:

在有pinctrl之前,一般直接操作硬件寄存器以配置 GPIO 引脚的方式通常涉及到访问特定的内存地址。在Imx6u平台上,展示如何在 Linux 驱动中直接访问寄存器来设置 GPIO 引脚的模式和状态。

示例代码

假设我们有一个简单的硬件平台,其 GPIO 寄存器映射如下:

  • GPIO 控制寄存器(用于设置引脚模式)
  • GPIO 数据寄存器(用于读写引脚状态)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/io.h>

#define GPIO_BASE_ADDR 0x3F200000 // 假设的 GPIO 基址
#define GPIO_SET_OFFSET 0x1C // 设置 GPIO 引脚高电平的偏移量
#define GPIO_CLR_OFFSET 0x28 // 清除 GPIO 引脚高电平的偏移量
#define GPIO_DIR_OFFSET 0x00 // GPIO 方向寄存器偏移量

#define GPIO_PIN 17 // 假设我们操作 GPIO 17

static void __iomem *gpio_base;

static int __init gpio_init(void) {
// 映射 GPIO 控制寄存器
gpio_base = ioremap(GPIO_BASE_ADDR, 0x1000);
if (!gpio_base) {
printk(KERN_ERR "Failed to map GPIO base address\n");
return -ENOMEM;
}

// 设置 GPIO_PIN 为输出
unsigned int *gpio_dir = gpio_base + GPIO_DIR_OFFSET;
*gpio_dir |= (1 << GPIO_PIN); // 将 GPIO_PIN 设置为输出模式

// 设置 GPIO_PIN 为高电平
unsigned int *gpio_set = gpio_base + GPIO_SET_OFFSET;
*gpio_set = (1 << GPIO_PIN);

printk(KERN_INFO "GPIO %d is set to high\n", GPIO_PIN);
return 0;
}

static void __exit gpio_exit(void) {
// 清除 GPIO_PIN 的高电平
unsigned int *gpio_clr = gpio_base + GPIO_CLR_OFFSET;
*gpio_clr = (1 << GPIO_PIN);

// 解除映射
iounmap(gpio_base);
printk(KERN_INFO "GPIO %d is set to low\n", GPIO_PIN);
}

module_init(gpio_init);
module_exit(gpio_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("Direct GPIO Access Example");
  • 映射寄存器:

    • 使用 ioremap() 函数将 GPIO 基地址映射到内核虚拟地址空间。
  • 设置 GPIO 方向:

    • 通过对 GPIO 方向寄存器进行位操作,将指定的 GPIO 引脚设置为输出模式。
  • 设置引脚状态:

    • 通过操作 GPIO 设置寄存器,将引脚设为高电平;通过 GPIO 清除寄存器可以将引脚设为低电平。
  • 模块初始化与退出:

    • 在模块初始化时配置 GPIO 引脚,并在模块卸载时清除引脚状态并解除映射。

导致问题:

  • 硬件依赖性:直接寄存器访问依赖于特定硬件平台的寄存器映射,具有很强的硬件依赖性,不易移植。
  • 安全性:直接操作硬件寄存器时,必须确保不会引起冲突或不安全的状态。
  • 错误处理:实际代码中应包含更多的错误处理逻辑,以确保健壮性。

1. 主要功能

  • 引脚配置:允许用户设置每个引脚的模式(如输入、输出、复用功能等)。
  • 引脚状态管理:可以读写引脚的电平状态。
  • 引脚复用:支持将同一引脚配置为不同的功能,以适应不同的硬件需求。

在设备树(Device Tree)中,Pin Controller 节点的格式并没有统一的标准。这意味着不同的芯片制造商可能会采用不同的方式来定义这些节点。尽管所有芯片都具备类似的功能概念,如引脚复用和配置,但具体的实现和描述方式各不相同。

Pin Controller 概念

Pin Controller 是一个软件抽象概念,主要用于管理和配置芯片上的引脚功能。它并不是在芯片手册中可以直接找到的硬件模块,而是通过软件来实现引脚的复用和配置。

  • 引脚复用:类似于 IOMUX,Pin Controller 允许将同一引脚配置为多种功能(例如 GPIO、I2C、UART 等)。
  • 引脚配置:除了复用功能外,Pin Controller 还可以配置引脚的电气特性,例如上下拉电阻、驱动强度等。

Client Device 概念

Client Device(客户设备)是指在 pinctrl 系统中,使用 Pin Controller 控制引脚的外设或模块。简而言之,Client Device 是依赖于 Pin Controller 提供的接口和功能的设备。Client Device 是使用 pinctrl 系统的设备。它通过 Pin Controller 配置和管理所需的引脚功能,以实现与主控芯片或其他设备的通信。

在设备树(Device Tree)中,Client Device 通常被定义为一个节点,该节点包含以下信息:

my_device: my_device@0 {
compatible = "my_vendor,my_device";
pinctrl-names = "default";
pinctrl = <&pinctrl_0>; // 引用 Pin Controller 配置
// 其他设备属性
};

在设备树中,<&pinctrl_0>; 是对 Pin Controller 配置的引用,它通常在设备树的某个部分定义。具体来说,pinctrl_0 是一个节点标签(label),通常定义在 pinctrl 子系统部分,描述相关引脚的配置和功能。

&pinctrl {
pinctrl_0: pinctrl {
gpio1_pins: gpio1_pins {
fsl,pins = <MX6UL_PAD_GPIO1_IO16__GPIO1_IO16>;
};

gpio2_pins: gpio2_pins {
fsl,pins = <MX6UL_PAD_GPIO1_IO17__GPIO1_IO17>;
};

// 其他引脚配置
};
};

2. 关键组件

  • 设备树:pinctrl 使用设备树(Device Tree)来描述硬件的引脚功能和配置。设备树中的节点定义了硬件平台的引脚布局和特性。
  • pinctrl 驱动:每个硬件平台都有相应的 pinctrl 驱动,它实现了 pinctrl 子系统的接口,负责具体的硬件控制。
  • GPIO 线路:pinctrl 提供的接口通常与 GPIO 线路紧密结合,允许用户在需要时直接控制 GPIO 引脚。

驱动结构:每个硬件平台通常实现一个 pinctrl 驱动,负责解析设备树并配置引脚。引脚控制逻辑:驱动通过访问特定的寄存器来实现引脚的配置和状态管理。

由BSP工程师完成:驱动程序使用特定的输入/输出操作函数(如 readl 和 writel)来访问硬件寄存器。这些寄存器通常位于系统级芯片(SoC)的地址空间中,负责控制引脚的功能和状态。驱动程序通过写入相应的寄存器值,配置引脚为特定的功能,例如将引脚设置为 GPIO、I2C 或 UART 等。驱动程序可以设置引脚为输入或输出模式。在输出模式下,驱动程序控制引脚的电平状态;在输入模式下,驱动程序读取引脚的电平状态。

3. 使用示例

在设备树中,pinctrl 的定义通常如下:

pinctrl {
pinctrl0: pinctrl0 {
pins = "gpio1", "gpio2";
function = "gpio";
};
};

在 Linux 驱动中,使用 pinctrl API 进行引脚配置的一个简单示例:

struct pinctrl *pinctrl;
struct pinctrl_state *state;

pinctrl = devm_pinctrl_get(dev);
state = pinctrl_lookup_state(pinctrl, "pinctrl0");
pinctrl_select_state(pinctrl, state);

4. API 接口

pinctrl 提供了一系列的 API,主要包括:

  • pinctrl_get():获取 pinctrl 句柄。
  • pinctrl_lookup_state():查找特定的引脚状态。
  • pinctrl_select_state():选择并应用指定的引脚状态。

5. 适用场景

  • 嵌入式系统:广泛应用于嵌入式设备中,以控制各种外设。
  • 开发板:如 Raspberry Pi、BeagleBone 等,用户需要对 GPIO 进行灵活控制。
  • 自定义硬件:在需要对硬件引脚进行特定配置的场景中。

6. 总结

pinctrl 子系统是 Linux 内核中用于引脚管理的重要组成部分,它通过统一的接口简化了 GPIO 和引脚控制的复杂性。了解和使用 pinctrl 可以帮助开发者更有效地管理硬件资源,提高开发效率。

GPIO子系统

在早期的嵌入式系统中,直接通过寄存器操作 GPIO 引脚的方式存在许多问题,尤其是在不同硬件平台上的兼容性。每个板子的 GPIO 控制代码往往需要完全不同的实现,导致代码重复和维护困难。

驱动代码可以使用 GPIO 子系统提供的标准函数来操作 GPIO 引脚,而不需要关心底层的寄存器操作。这些标准函数包括:

  • 获取 GPIO:申请使用特定的 GPIO 引脚。
  • 设置 GPIO 方向:配置 GPIO 引脚为输入或输出。
  • 读取/设置 GPIO 值:读取引脚的电平状态或设置引脚的电平。

通过这种方式,驱动代码将变得与具体板子无关,具有更好的可移植性和可重用性。无论在何种硬件平台上,只需确保设备树正确配置和 GPIO 子系统实现得当,驱动程序就可以正常工作。

下面是一个使用 GPIO 子系统的简单示例:

#include <linux/gpio.h>

#define LED_GPIO_PIN 17 // 定义 LED 使用的 GPIO 引脚编号

void led_control(int state) {
// 申请 GPIO 引脚
gpio_request(LED_GPIO_PIN, "led_gpio");

// 设置引脚方向为输出
gpio_direction_output(LED_GPIO_PIN, state);

// 控制 LED 状态
gpio_set_value(LED_GPIO_PIN, state);

// 释放 GPIO 引脚
gpio_free(LED_GPIO_PIN);
}

GPIO 控制器在设备树中的定义

在许多 ARM 芯片中,GPIO 引脚通常被分为多个组,每组包含若干个引脚。在使用 GPIO 子系统之前,必须确定要使用的 GPIO 组和具体引脚。以下是如何在设备树中定义和引用 GPIO 控制器的详细说明。

gpio1: gpio@12340000 {
compatible = "vendor,gpio-controller"; // 兼容的设备类型
reg = <0x12340000 0x1000>; // 寄存器基地址及大小
interrupts = <GIC_SPI 32 0>; // 中断配置,GIC_SPI 表示中断类型,32 是中断号
gpio-cells = <2>; // GPIO 单元数:第一个为引脚编号,第二个为属性(如上下拉配置等)
gpio-controller; // 指示这是一个 GPIO 控制器
ngpios = <32>; // 可用的 GPIO 引脚数量

// 额外的属性,可以根据具体需要添加
vendor = "MyVendor"; // 厂商信息
model = "MyGPIOController"; // 控制器型号

// 可选的引脚配置
pinctrl-names = "default"; // 引脚控制器配置名称
pinctrl-0 = <&pinctrl_0>; // 引用引脚控制器配置
};

在设备树中,GPIO 控制器节点的定义中,有两个关键属性需要关注:

  1. gpio-controller
  • 意义:该属性标记该节点为一个 GPIO 控制器,表示它负责管理多个 GPIO 引脚。
  • 作用:它让内核识别该节点为 GPIO 控制器,并允许其他设备节点引用其管理的 GPIO 引脚。
  1. #gpio-cells = <2>
  • 意义:这个属性指定每个 GPIO 引脚的描述需要使用两个 32 位的数(cell)。
  • 具体含义:
    • 第一个值:表示引脚的编号(index),用于指定 GPIO 控制器下的具体引脚。
    • 第二个值:可以是可选属性,例如设置引脚的上下拉电阻或其他特性。

在设备节点中引用 GPIO 引脚

gpios 属性并不是将设备定义为 GPIO,而是指定该设备使用的 GPIO 引脚。具体来说,这一行代码指明了该设备将使用由 gpio1 控制器管理的第 0 号引脚。

my_device: my_device@0 {
compatible = "goodix,gt9xx";
reg = <0x5d>; // 设备寄存器地址
status = "okay"; // 状态
interrupt-parent = <&gpio1>; // 中断父节点
pinctrl-names = "default"; // 默认引脚控制
pinctrl-0 = <&pinctrl_tsc_reset &pinctrl_touchscreen>; // 引脚控制配置

name-gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>; //// 引用 gpio1 控制器的第 0 号引脚,高电平有效
reset-gpios = <&gpio2 2 GPIO_ACTIVE_LOW>; // 复位 GPIO 低电平有效
irq-gpios = <&gpio1 5 IRQ_TYPE_EDGE_FALLING>; // 中断 GPIO,下降沿触发
};
  • gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>; 是一个重要的属性,定义了该设备使用的 GPIO 引脚。
  • 上图中,可以使用 gpios 属性,也可以使用 name-gpios 属性。

pinctrl 负责引脚的配置和初始化,而 gpio 则负责在运行时对这些引脚的实际使用。分开指定能够提供更大的灵活性和清晰性,使得设备在不同环境下的引脚配置和控制更加清晰和可管理。

0 表示该引脚在 gpio1 控制器中的编号。GPIO_ACTIVE_HIGH 指定该 GPIO 引脚在高电平状态下有效。这意味着,当引脚电平为高时,设备的功能将被激活。以上dts定义指定了设备所使用的 GPIO 引脚及其状态。这是设备树中对设备与 GPIO 之间关系的描述,使得内核能够通过驱动程序正确控制该设备。

效果:

设备功能控制:通过此引用,驱动程序可以控制该引脚的状态,从而影响设备的行为。例如,如果这个设备是一个 LED,设置该引脚为高电平可能会使 LED 亮起。

硬件抽象:这使得设备树能够抽象出硬件细节,驱动程序可以通过统一的接口与硬件进行交互,而不必关心具体的硬件实现。

在驱动代码中调用 GPIO 子系统

在驱动程序中使用 GPIO 引脚时,可以通过 Linux GPIO 子系统提供的 API 来进行管理。以下是您提供的代码的详细解释,展示如何在驱动程序中使用 GPIO。

#include <linux/gpio.h>

#define GPIO_PIN 0 // 使用 gpio1 控制器中的第 0 号引脚

void control_gpio(void) {
// 申请 GPIO 引脚
gpio_request(GPIO_PIN, "my_gpio");

// 设置引脚方向为输出
gpio_direction_output(GPIO_PIN, 1); // 设置引脚为高电平

// 控制引脚状态
gpio_set_value(GPIO_PIN, 0); // 设置引脚为低电平

// 释放 GPIO 引脚
gpio_free(GPIO_PIN);
}

在sysfs中访问GPIO的方式

在 Linux 的 sysfs 中,访问方法主要涉及如何通过文件系统接口与内核对象进行交互。sysfs 是一个虚拟文件系统,它提供了用户空间与内核空间之间的交互机制,尤其用于设备和驱动程序的信息暴露。以下是访问 sysfs 的一般方法和步骤:

查看设备信息:

ls /sys/class/ | grep <device_class>

读取属性:

cat /sys/class/<device_class>/<device>/attribute_name

写入属性:

echo <value> > /sys/class/<device_class>/<device>/attribute_name

在 C 程序中,可以使用标准文件操作函数来读取和写入 sysfs 文件:

#include <stdio.h>
#include <stdlib.h>

void read_sysfs(const char *path) {
FILE *file = fopen(path, "r");
if (file) {
char buffer[256];
fgets(buffer, sizeof(buffer), file);
printf("Value: %s\n", buffer);
fclose(file);
} else {
perror("Failed to open file");
}
}

void write_sysfs(const char *path, const char *value) {
FILE *file = fopen(path, "w");
if (file) {
fprintf(file, "%s\n", value);
fclose(file);
} else {
perror("Failed to open file");
}
}

int main() {
// 示例:读取和写入 sysfs 属性
const char *path = "/sys/class/<device_class>/<device>/attribute_name";

read_sysfs(path); // 读取属性
write_sysfs(path, "1"); // 写入属性
return 0;
}
  • 权限:访问 sysfs 中的某些文件可能需要特定的权限,特别是写入操作。
  • 数据格式:确保读取和写入的数据格式正确,例如整数、布尔值等。
  • 设备状态:在对设备进行操作前,了解设备的当前状态,以避免不必要的错误。