linux 中的 pinctrl 子系统
2026/7/22 3:04:20 网站建设 项目流程

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选择引脚连接到外设还是 GPIOfunction = 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.cpinctrl 核心:注册、map 解析、state 管理
drivers/pinctrl/pinmux.cfunction/group 管理,复用冲突检查
drivers/pinctrl/pinconf.cpinconf 配置分发与 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

/* include/linux/pinctrl/pinctrl.h */structpinctrl_desc{constchar*name;conststructpinctrl_pin_desc*pins;/* 本 SoC 全部 pin 的表 */unsignedintnpins;conststructpinctrl_ops*pctlops;/* 组/功能枚举 */conststructpinmux_ops*pmxops;/* 复用选择 */conststructpinconf_ops*confops;/* 电气配置 */structmodule*owner;/* ... */};

注册入口:

structpinctrl_dev*devm_pinctrl_register(structdevice*dev,structpinctrl_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 客户端侧:抽象状态

structpinctrl;/* 一个客户端设备的 pinctrl 句柄 */structpinctrl_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 控制器节点)

/* 控制器本身 */ 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 引脚配置子节点(板级可复用的"引脚片段")

&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 客户端引用方式

&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空闲否,较少用
initprobe 期间使用,之后释放半自动(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 需要动态切换的驱动

#include<linux/pinctrl/consumer.h>structmy_dev{structpinctrl*pctl;structpinctrl_state*st_active;structpinctrl_state*st_sleep;};staticintmy_probe(structplatform_device*pdev){structmy_dev*m=/* ... */;m->pctl=devm_pinctrl_get(&pdev->dev);/* 获取句柄 */if(IS_ERR(m->pctl))returnPTR_ERR(m->pctl);m->st_active=pinctrl_lookup_state(m->pctl,"default");m->st_sleep=pinctrl_lookup_state(m->pctl,"sleep");/* ... */}staticintmy_suspend(structdevice*dev){structmy_dev*m=dev_get_drvdata(dev);pinctrl_select_state(m->pctl,m->st_sleep);/* 引脚进入低功耗 */return0;}staticintmy_resume(structdevice*dev){structmy_dev*m=dev_get_drvdata(dev);pinctrl_select_state(m->pctl,m->st_active);return0;}

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 驱动(框架视角)

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

staticconststructpinctrl_pin_descfoo_pins[]={PINCTRL_PIN(0,"P0"),PINCTRL_PIN(1,"P1"),/* ... 全部 pin */};staticintfoo_get_groups_count(structpinctrl_dev*pctldev){...}staticconstchar*foo_get_group_name(structpinctrl_dev*pctldev,unsignedsel){...}staticintfoo_get_group_pins(structpinctrl_dev*pctldev,unsignedsel,constunsigned**pins,unsigned*npins){...}staticintfoo_dt_node_to_map(structpinctrl_dev*pctldev,structdevice_node*np,structpinctrl_map**map,unsigned*num_maps){...}staticconststructpinctrl_opsfoo_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,};staticintfoo_set_mux(structpinctrl_dev*pctldev,unsignedfunc,unsignedgroup){/* 查表找到 group 的 pins + 目标 function 编号,写复用寄存器 */return0;}staticconststructpinmux_opsfoo_pmxops={.get_functions_count=...,.get_function_name=...,.get_function_groups=...,.set_mux=foo_set_mux,.gpio_request_enable=...,.strict=true,/* 禁止对已复用 pin 再申请 GPIO */};staticconststructpinconf_opsfoo_confops={.is_generic=true,/* 使用通用配置参数 */.pin_config_get=foo_pinconf_get,.pin_config_set=foo_pinconf_set,.pin_config_group_set=foo_pinconf_group_set,};staticstructpinctrl_descfoo_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,};staticintfoo_pinctrl_probe(structplatform_device*pdev){/* 映射寄存器、初始化私有数据,然后: */returnPTR_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

&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 控制器一个目录:

# 列出所有 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 动态调试

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询