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 早期),引脚配置存在几个典型问题:
- 各平台代码重复:每个 mach-* 目录都有自己的一套引脚配置代码,接口不统一,无法复用。
- 配置时机混乱:引脚配置写在板级文件(board file)里,与驱动代码割裂,驱动作者无法声明自己需要什么引脚状态。
- 静态配置导致功耗浪费:所有引脚在开机时一次性配置好,即使外设休眠了,引脚仍保持活动状态,无法配合 runtime PM 省电。
- GPIO 与复用功能冲突:引脚被复用为 I2C 后,又被其他驱动当 GPIO 请求,造成总线异常且难以定位。
1.3 pinctrl 的定位
pinctrl 子系统就是为解决上述问题而生的一个内核框架:
- 向上(客户端驱动) :提供统一接口,让驱动以"状态(state)"的抽象方式声明引脚需求,如
default、sleep; - 向下(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 驱动把成千上万的引脚组织成四个层次的概念:
- Pin(引脚) :最小单位,每个 pin 有全局唯一编号,如
pin 0 ~ pin 287。 - Group(引脚组) :实现某个功能的一组 pin 的集合。例如 I2C0 需要 SDA + SCL 两个引脚,驱动里就定义一个 group(如
i2c0_xfer)包含这两个 pin。功能是挂在 group 上而不是单个 pin 上------这是与裸机思维最大的不同。 - Function(功能) :一个可选的复用功能,如
i2c0、gpio、spi1。一个 function 可以对应多个 group(例如 I2C0 可以从两组不同引脚引出,就有i2c0_xfer和i2c0m1_xfer两个 group 供板级选择)。 - Map / State(映射/状态) :设备树里一个 pinctrl 节点描述的"某客户端设备在某个状态下使用哪些 group + 什么电气配置"。在客户端视角这叫 state ,如
default、sleep、idle。
记忆模型:

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

上图把 pinctrl 子系统从上到下分为四层:
- Client :各类外设驱动,只感知
default/sleep/idle等 state。 - pinctrl core :统一维护 device / map / state,通过
pinctrl_ops、pinmux_ops、pinconf_ops向驱动层派发。 - SoC pinctrl driver :各平台具体实现,用
pinctrl_desc描述全部引脚并操作寄存器。 - 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_count、get_group_name、get_group_pins、dt_node_to_map、dt_free_map |
struct pinmux_ops |
复用选择与 GPIO 互通 | get_functions_count、set_mux(核心)、gpio_request_enable、strict |
struct pinconf_ops |
电气配置读写 | pin_config_get、pin_config_set、pin_config_group_set、is_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-case (
i2c0-xfer),label 采用 snake_case (i2c0_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,电气属性单独写 | pinmux、bias-pull-up、drive-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.c 的 pinctrl_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));
}
要点:
dt_node_to_map是最花功夫的回调 :把板级 DTS 的引脚片段翻译成内核 map。很多平台用pinconf_generic_dt_node_to_map_group()辅助,或干脆用pinctrl-single这类通用驱动免除自写。.strict = true强烈推荐:启用复用保护,pin 已被 mux 为外设功能时拒绝 GPIO 申请,能提前暴露大量板级配置错误。- 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 硬件引脚 上生效。
三句话收束:
- pinctrl = pinmux(功能复用)+ pinconf(电气配置) 的统一资源管理框架;
- 对驱动开发者,90% 的工作是在 DTS 里写对 group、function、电气属性和 state 引用;
- 排障时 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