linux 中的 pinctrl 子系统

linux 中的 pinctrl 子系统

前置知识:Linux 设备模型、设备树(Device Tree)、GPIO 基本概念


1. 为什么需要 pinctrl?

1.1 SoC 引脚复用的现实问题

现代 SoC 的引脚(pin)数量远小于内部外设数量,因此绝大多数引脚都是多功能复用的

  • 同一个物理引脚,既可以配置为 GPIO,也可以作为 I2C_SDA、SPI_CLK、UART_TX、PWM 输出等;
  • 每个引脚往往还带有电气属性配置:上拉/下拉(pull-up/down)、驱动强度(drive strength)、施密特触发、斜率控制、开漏(open-drain)等。

以一颗典型的 Cortex-A 级 SoC 为例,一个引脚可能有 4~8 种功能(mux function),而复用关系散布在数十个引脚上。

1.2 没有 pinctrl 之前的混乱

在 pinctrl 子系统出现之前(ARM Linux 早期),引脚配置存在几个典型问题:

  1. 各平台代码重复:每个 mach-* 目录都有自己的一套引脚配置代码,接口不统一,无法复用。
  2. 配置时机混乱:引脚配置写在板级文件(board file)里,与驱动代码割裂,驱动作者无法声明自己需要什么引脚状态。
  3. 静态配置导致功耗浪费:所有引脚在开机时一次性配置好,即使外设休眠了,引脚仍保持活动状态,无法配合 runtime PM 省电。
  4. GPIO 与复用功能冲突:引脚被复用为 I2C 后,又被其他驱动当 GPIO 请求,造成总线异常且难以定位。

1.3 pinctrl 的定位

pinctrl 子系统就是为解决上述问题而生的一个内核框架

  • 向上(客户端驱动) :提供统一接口,让驱动以"状态(state)"的抽象方式声明引脚需求,如 defaultsleep
  • 向下(SoC 驱动):定义 pinctrl 驱动的编写框架(pinctrl_desc、三组操作函数集);
  • 横向:与设备树绑定(引脚配置写在 DTS 里)、与 GPIO 子系统互通(gpio-ranges)、与 runtime PM 配合(引脚状态随设备电源切换)。

一句话总结:pinctrl 负责"引脚复用(pinmux)+ 引脚电气配置(pinconf)"的统一管理,让引脚资源像时钟、电源一样成为可被设备树描述、被驱动申请的内核资源。


2. 核心概念体系

理解 pinctrl 的关键是把下面几个抽象层次分清:

2.1 引脚控制器的功能划分

pinctrl 子系统管理的能力分为两块:

功能块 英文 职责 典型配置项
引脚复用 Pinmux 选择引脚连接到外设还是 GPIO function = i2c0 / gpio / spi1 ...
引脚配置 Pinconf 配置引脚的电气特性 bias-pull-up、drive-strength、slew-rate ...

有些 SoC 这两块在同一组寄存器里,有些则分开;pinctrl 框架允许驱动只实现其中一块(但大多数驱动两块都实现)。

2.2 四个基本组织单位

pinctrl 驱动把成千上万的引脚组织成四个层次的概念:

  1. Pin(引脚) :最小单位,每个 pin 有全局唯一编号,如 pin 0 ~ pin 287
  2. Group(引脚组) :实现某个功能的一组 pin 的集合。例如 I2C0 需要 SDA + SCL 两个引脚,驱动里就定义一个 group(如 i2c0_xfer)包含这两个 pin。功能是挂在 group 上而不是单个 pin 上------这是与裸机思维最大的不同。
  3. Function(功能) :一个可选的复用功能,如 i2c0gpiospi1。一个 function 可以对应多个 group(例如 I2C0 可以从两组不同引脚引出,就有 i2c0_xferi2c0m1_xfer 两个 group 供板级选择)。
  4. Map / State(映射/状态) :设备树里一个 pinctrl 节点描述的"某客户端设备在某个状态下使用哪些 group + 什么电气配置"。在客户端视角这叫 state ,如 defaultsleepidle

记忆模型:

上图要点:客户端设备以 state 名义引用若干 Group ;每个 Group 对应一个 Function ,并包含若干 Pin ;每个 Pin 再附带 pinconf 电气配置。


3. 整体架构

3.1 分层视图

上图把 pinctrl 子系统从上到下分为四层:

  1. Client :各类外设驱动,只感知 default / sleep / idle 等 state。
  2. pinctrl core :统一维护 device / map / state,通过 pinctrl_opspinmux_opspinconf_ops 向驱动层派发。
  3. SoC pinctrl driver :各平台具体实现,用 pinctrl_desc 描述全部引脚并操作寄存器。
  4. Hardware:SoC 引脚控制器寄存器。

此外,pinctrl 还与 GPIO 子系统Runtime PM 存在横向关联。

3.2 源码位置

路径 内容
drivers/pinctrl/core.c pinctrl 核心:注册、map 解析、state 管理
drivers/pinctrl/pinmux.c function/group 管理,复用冲突检查
drivers/pinctrl/pinconf.c pinconf 配置分发与 debugfs
drivers/pinctrl/devicetree.c 设备树 pinctrl-x 属性解析
include/linux/pinctrl/ 核心头文件(consumer.h、machine.h、pinmux.h、pinconf.h 等)
drivers/pinctrl/pinctrl-*.c 各 SoC 的 pinctrl 驱动
Documentation/devicetree/bindings/pinctrl/ 各平台 pinctrl bindings 文档

3.3 与相关子系统的关系

  • 与 GPIO 子系统 :GPIO 引脚本质上也是"复用功能的一种"(function = gpio)。pinctrl 驱动通过 gpio-ranges 声明哪些 pin 对应哪些 GPIO 编号,gpiolib 在 gpio_request() 时回调 pinctrl_gpio_request(),防止"引脚已被复用为 I2C 却被当 GPIO 申请"的冲突。
  • 与设备模型 :客户端驱动的 struct device 内部挂着 pins 指针;驱动核心在 probe 前会自动把设备切到 default 状态(自动调用,不需要驱动手写)。
  • 与 runtime PM :设备进入休眠时驱动把 state 切到 sleep,引脚进入低功耗配置(如下拉、输入高阻),唤醒时切回 default

4. 核心数据结构(内核视角)

4.1 驱动侧:描述 SoC

c 复制代码
/* include/linux/pinctrl/pinctrl.h */
struct pinctrl_desc {
    const char *name;
    const struct pinctrl_pin_desc *pins;   /* 本 SoC 全部 pin 的表 */
    unsigned int npins;
    const struct pinctrl_ops *pctlops;     /* 组/功能枚举 */
    const struct pinmux_ops *pmxops;       /* 复用选择 */
    const struct pinconf_ops *confops;     /* 电气配置 */
    struct module *owner;
    /* ... */
};

注册入口:

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

4.2 三组操作函数集

操作集 职责 关键回调
struct pinctrl_ops 枚举 group/function,处理 DT 节点 get_groups_countget_group_nameget_group_pinsdt_node_to_mapdt_free_map
struct pinmux_ops 复用选择与 GPIO 互通 get_functions_countset_mux(核心)、gpio_request_enablestrict
struct pinconf_ops 电气配置读写 pin_config_getpin_config_setpin_config_group_setis_generic

其中 set_mux 是 pinctrl 驱动真正的"干活"函数:给定 function + group,写复用寄存器把引脚切到对应功能。

4.3 客户端侧:抽象状态

c 复制代码
struct pinctrl;              /* 一个客户端设备的 pinctrl 句柄 */
struct pinctrl_state;        /* 一个状态(如 default / sleep) */

客户端通过 devm_pinctrl_get(dev) 拿到句柄,pinctrl_lookup_state(p, "sleep") 找到状态,pinctrl_select_state(p, s) 应用。设备树里 pinctrl-names 的每个名字对应一个 pinctrl_state,每个 state 内部是一组 map(功能映射 + 配置列表)。


5. 设备树绑定:驱动开发者最常接触的部分

5.1 服务端(pinctrl 控制器节点)

dts 复制代码
/* 控制器本身 */
pinctrl: pinctrl {
    compatible = "rockchip,rk3568-pinctrl";
    rockchip,grf = <&grf>;
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    /* 各 bank 子节点,同时是 GPIO 控制器 */
    gpio0: gpio@fdd60000 {
        compatible = "rockchip,gpio-bank";
        reg = <0x0 0xfdd60000 0x0 0x100>;
        gpio-controller;
        #gpio-cells = <2>;
        /* ... */
    };
};

5.2 引脚配置子节点(板级可复用的"引脚片段")

dts 复制代码
&pinctrl {
    i2c0 {
        /* 一个引脚配置片段:group + 电气属性 */
        i2c0_xfer: i2c0-xfer {
            rockchip,pins =
                <0 RK_PB1 RK_FUNC_I2C0_SCL &pcfg_pull_up>,
                <0 RK_PB0 RK_FUNC_I2C0_SDA &pcfg_pull_up>;
        };
    };

    uart2 {
        uart2m0_xfer: uart2m0-xfer {
            rockchip,pins =
                <0 RK_PC7 RK_FUNC_UART2_TX_M0 &pcfg_pull_up>,
                <0 RK_PC6 RK_FUNC_UART2_RX_M0 &pcfg_pull_up>;
        };
    };

    /* 通用电气配置片段 */
    pcfg_pull_up: pcfg-pull-up {
        bias-pull-up;
    };
    pcfg_pull_none: pcfg-pull-none {
        bias-disable;
    };
};

注意两点:

  • 节点名采用 kebab-casei2c0-xfer),label 采用 snake_casei2c0_xfer)------这是 bindings 的通用约定;
  • 电气属性可以做成公共片段(pcfg_pull_up),被多个引脚配置引用复用。

5.3 客户端引用方式

dts 复制代码
&i2c0 {
    pinctrl-names = "default";          /* state 名字列表 */
    pinctrl-0 = <&i2c0_xfer>;           /* 名字对应的 phandle 列表 */
    status = "okay";
};

&uart2 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&uart2m0_xfer>;
    pinctrl-1 = <&uart2m0_sleep>;       /* 休眠时的引脚配置 */
    status = "okay";
};

解析规则(drivers/pinctrl/devicetree.c):

  • pinctrl-names 第 N 个名字 ↔ pinctrl-N 属性,一一对应;
  • 名字 default 是特殊的:设备模型在驱动 probe 之前会自动 select 它;
  • pinctrl-N 里可以挂多个 phandle(多个配置片段合并成一个 state)。

5.4 常见 state 命名约定

state 名 语义 内核自动处理?
default 正常工作 是,probe 前自动 select
sleep 休眠低功耗 否,驱动手动切换(配合 runtime/system PM)
idle 空闲 否,较少用
init probe 期间使用,之后释放 半自动(pinctrl_bind_pins 处理)

5.5 各平台风格差异

平台 配置风格 例子
Rockchip 一个属性打包:<bank pin func &cfg-ref> rockchip,pins
STM32 分离:pinmux 声明 mux + bias,电气属性单独写 pinmuxbias-pull-updrive-push-pull
i.MX (fsl) 紧凑数组:<mux_reg conf_reg input_reg mux_val input_val pad_val> fsl,pins
通用 pinctrl-single 寄存器偏移 + 值的原始数组 pinctrl-single,pins

虽然风格不同,但概念模型完全一致:都是"把一组 pin 设置成某 function + 某电气配置"


6. 客户端驱动如何使用 pinctrl

6.1 最简单的情况:什么都不用写

绝大多数驱动只需在 DTS 里写好 pinctrl-names = "default"; pinctrl-0 = <&xxx>; ------ 设备模型(drivers/base/pinctrl.cpinctrl_bind_pins())在 probe 前自动完成 get + select。这就是 i2c/spi/serial 驱动里看不到 pinctrl 代码的原因。

6.2 需要动态切换的驱动

c 复制代码
#include <linux/pinctrl/consumer.h>

struct my_dev {
    struct pinctrl *pctl;
    struct pinctrl_state *st_active;
    struct pinctrl_state *st_sleep;
};

static int my_probe(struct platform_device *pdev)
{
    struct my_dev *m = /* ... */;

    m->pctl = devm_pinctrl_get(&pdev->dev);          /* 获取句柄 */
    if (IS_ERR(m->pctl))
        return PTR_ERR(m->pctl);

    m->st_active = pinctrl_lookup_state(m->pctl, "default");
    m->st_sleep  = pinctrl_lookup_state(m->pctl, "sleep");
    /* ... */
}

static int my_suspend(struct device *dev)
{
    struct my_dev *m = dev_get_drvdata(dev);
    pinctrl_select_state(m->pctl, m->st_sleep);      /* 引脚进入低功耗 */
    return 0;
}

static int my_resume(struct device *dev)
{
    struct my_dev *m = dev_get_drvdata(dev);
    pinctrl_select_state(m->pctl, m->st_active);
    return 0;
}

6.3 客户端 API 一览

API 用途
devm_pinctrl_get() 获取设备的 pinctrl 句柄(资源托管,推荐)
pinctrl_lookup_state() 按名字查找 state
pinctrl_select_state() 应用一个 state(做 mux + conf)
devm_pinctrl_get_select_default() 一步到位:get + select "default"
pinctrl_pm_select_default_state() / _sleep_state() / _idle_state() PM 语义封装,没有对应 state 时不报错
pinctrl_gpio_request() / pinctrl_gpio_free() 把某个 GPIO 对应的 pin 切到 gpio 功能

7. 编写一个 SoC 的 pinctrl 驱动(框架视角)

新平台移植时的最小骨架:

c 复制代码
static const struct pinctrl_pin_desc foo_pins[] = {
    PINCTRL_PIN(0, "P0"),  PINCTRL_PIN(1, "P1"),  /* ... 全部 pin */
};

static int foo_get_groups_count(struct pinctrl_dev *pctldev) { ... }
static const char *foo_get_group_name(struct pinctrl_dev *pctldev, unsigned sel) { ... }
static int foo_get_group_pins(struct pinctrl_dev *pctldev, unsigned sel,
                              const unsigned **pins, unsigned *npins) { ... }
static int foo_dt_node_to_map(struct pinctrl_dev *pctldev,
                              struct device_node *np,
                              struct pinctrl_map **map, unsigned *num_maps) { ... }

static const struct pinctrl_ops foo_pctrl_ops = {
    .get_groups_count = foo_get_groups_count,
    .get_group_name   = foo_get_group_name,
    .get_group_pins   = foo_get_group_pins,
    .dt_node_to_map   = foo_dt_node_to_map,
    .dt_free_map      = pinctrl_utils_free_map,
};

static int foo_set_mux(struct pinctrl_dev *pctldev, unsigned func, unsigned group)
{
    /* 查表找到 group 的 pins + 目标 function 编号,写复用寄存器 */
    return 0;
}

static const struct pinmux_ops foo_pmxops = {
    .get_functions_count = ... ,
    .get_function_name   = ... ,
    .get_function_groups = ... ,
    .set_mux             = foo_set_mux,
    .gpio_request_enable = ... ,
    .strict              = true,        /* 禁止对已复用 pin 再申请 GPIO */
};

static const struct pinconf_ops foo_confops = {
    .is_generic            = true,       /* 使用通用配置参数 */
    .pin_config_get        = foo_pinconf_get,
    .pin_config_set        = foo_pinconf_set,
    .pin_config_group_set  = foo_pinconf_group_set,
};

static struct pinctrl_desc foo_desc = {
    .name    = "foo-pinctrl",
    .pins    = foo_pins,
    .npins   = ARRAY_SIZE(foo_pins),
    .pctlops = &foo_pctrl_ops,
    .pmxops  = &foo_pmxops,
    .confops = &foo_confops,
    .owner   = THIS_MODULE,
};

static int foo_pinctrl_probe(struct platform_device *pdev)
{
    /* 映射寄存器、初始化私有数据,然后: */
    return PTR_ERR_OR_ZERO(devm_pinctrl_register(&pdev->dev, &foo_desc, priv));
}

要点:

  1. dt_node_to_map 是最花功夫的回调 :把板级 DTS 的引脚片段翻译成内核 map。很多平台用 pinconf_generic_dt_node_to_map_group() 辅助,或干脆用 pinctrl-single 这类通用驱动免除自写。
  2. .strict = true 强烈推荐:启用复用保护,pin 已被 mux 为外设功能时拒绝 GPIO 申请,能提前暴露大量板级配置错误。
  3. GPIO 互通 :pinctrl 驱动常和 GPIO 驱动是一对(一个 bank 一个 gpio_chip),通过 gpio-ranges 把两个子系统连起来。

7.1 pinctrl-single:不想写驱动的替代方案

对于"引脚配置就是往某偏移寄存器写值"的简单控制器,可直接用通用驱动 pinctrl-single

dts 复制代码
&pinmux {
    pinctrl-single,pins = <
        0x1a0 (PIN_INPUT_PULLUP | MUX_MODE2)   /* uart0_rxd */
        0x1a4 (PIN_OUTPUT      | MUX_MODE2)    /* uart0_txd */
    >;
};

DTS 直接描述"偏移 + 值",内核里不用新增任何 C 代码------AM335x、OMAP、部分 RISC-V SoC 都是这种模式。


8. 调试手段

8.1 debugfs(首选)

挂载 debugfs 后,/sys/kernel/debug/pinctrl/ 下每个 pinctrl 控制器一个目录:

sh 复制代码
# 列出所有 pin 及其当前 owner/mux 状态
cat /sys/kernel/debug/pinctrl/*/pins

# 查看当前活跃的 mux 映射
cat /sys/kernel/debug/pinctrl/*/pinmux-pins

# 查看每个 function ↔ group 映射
cat /sys/kernel/debug/pinctrl/*/pinmux-functions

# 查看每个 pin 的电气配置(需要 pinconf debug 支持)
cat /sys/kernel/debug/pinctrl/*/pinconf-pins

pins 输出的典型形态:

复制代码
pin 32 (gpio1-0): device 3ff20000.i2c function i2c0 group i2c0-xfer
pin 33 (gpio1-1): GPIO gpio1

一行就能看到:引脚被谁占用、复用成什么功能、属于哪个 group------排查"引脚没反应"类问题第一刀就砍这里。

8.2 动态调试

sh 复制代码
echo 'file drivers/pinctrl/* +p' > /sys/kernel/debug/dynamic_debug/control

8.3 常见故障速查表

现象 大概率原因 排查动作
probe 报 -EBUSY 申请 pinctrl 失败 同一组 pin 被两个设备同时引用 看 debugfs pins 里 owner 是谁
外设无响应但无报错 group 选错(如用了 m0 引脚组,实际板子走 m1) 对照原理图核对 bank/pin 编号
信号有但幅度/速率异常 drive-strength / slew-rate 不对 查 pinconf,调电气属性
休眠唤醒后外设死掉 sleep state 切过去没切回来,或 sleep 配置写错 检查 PM 回调里的 select_state
GPIO 操作影响外设 没开 .strict,GPIO 与 mux 冲突未被发现 pinctrl 驱动加 strict
DTS 改了不生效 客户端节点没写 pinctrl-0 引用,或 label 拼错 检查编译出的 dtb(fdtdump

9. 与其他系统的横向对比

维度 裸机/RTOS 常见做法 Linux pinctrl
配置位置 散落在各驱动 init 函数里直接写寄存器 集中在设备树,驱动只声明"要什么状态"
复用冲突 靠人工 review,运行期才发现 内核自动检查,申请时即报错
功耗联动 手写 suspend 里重新配引脚 state 抽象 + PM 框架联动
可移植性 换板子改一堆驱动代码 只改 DTS,驱动零改动

对 RT-Thread 等 RTOS 而言,引脚配置通常是各 BSP 的 pinmux.c + 一个 rt_pin_* GPIO 框架,复用管理远弱于 Linux pinctrl------这也是 Linux 在复杂 SoC 上的结构性优势之一。


10. 总结:一张图记住 pinctrl

整个流程可以概括为:设备树里的 控制器节点 定义引脚配置片段,客户端设备节点 通过 pinctrl-N = <&片段> 引用这些片段;内核解析后交给 pinctrl core 管理,再经 SoC pinctrl 驱动 的 ops 回调写入寄存器,最终在 SoC 硬件引脚 上生效。

三句话收束:

  1. pinctrl = pinmux(功能复用)+ pinconf(电气配置) 的统一资源管理框架;
  2. 对驱动开发者,90% 的工作是在 DTS 里写对 group、function、电气属性和 state 引用
  3. 排障时 debugfs 的 pins / pinmux-pins 是最直接的事实来源。

附录:常用配置属性速查(generic pinconf)

DTS 属性 含义
bias-disable 关闭上下拉
bias-pull-up / bias-pull-down 上拉 / 下拉
bias-pull-pin-default 使用引脚默认上下拉
drive-push-pull / drive-open-drain / drive-open-source 推挽 / 开漏 / 开源输出
drive-strength = <mA>; 驱动电流强度
input-enable / input-disable 输入使能
input-schmitt-enable 施密特触发输入
slew-rate = <n>; 边沿速率档位
output-high / output-low 输出电平

绑定文档:Documentation/devicetree/bindings/pinctrl/pincfg-node.yaml

相关推荐
忧虑的乌龟蛋1 年前
嵌入式 Linux:使用设备树驱动GPIO全流程
linux·服务器·嵌入式·imx6ull·gpio·点灯·pinctrl