LV005-pinctrl子系统简介
一、pinctrl 子系统
1. GPIO 的复用
以 imx6ull 为例,这个芯片拥有众多的片上外设, 大多数外设需要通过芯片的引脚与外部设备(器件)相连实现相对应的控制,例如我们熟悉的 I2C、SPI、LCD、USDHC 等等。
我们知道芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性, 一个芯片引脚往往可以做为多个片上外设的功能引脚,以 GPIO1_IO03 所对应的引脚为例,如下图所示。

这个引脚不仅仅可以作为 I2C1 的 SDA,也可以作为多个外设的功能引脚,如普通的 GPIO 引脚,USB_OTG2_OC 等, 在设计硬件时我们可以根据需要灵活的选择其中的一个。这个就是之前经常多说 GPIO 的复用。
复用(Multiplexing):就是将单一物理引脚通过配置,动态分配给多个功能模块使用。这样做的目的就是在有限的引脚资源下,支持更多外设功能(如 UART、SPI、I2C、PWM 等),避免芯片引脚数量膨胀。所以其实无论是哪种芯片,基本都有类似下图的结构:
要想让 pinA、 B 用于 GPIO,需要设置 IOMUX 让它们连接到 GPIO 模块;
要想让 pinA、 B 用于 I2C,需要设置 IOMUX 让它们连接到 I2C 模块。
以 GPIO、 I2C 应该是并列的关系,它们能够使用之前,需要设置 IOMUX。有时候并不仅仅是设置 IOMUX,还要配置引脚,比如上拉、下拉、开漏等等。
在编程过程中,无论是裸机还是驱动, 一般 首先要设置引脚的复用功能并且设置引脚的 PAD 属性(驱动能力、上下拉等等)。
2. pinctrl 子系统简介
在驱动程序中我们需要手动设置每个引脚的复用功能,不仅增加了工作量,编写的驱动程序不方便移植, 可重用性差等。更糟糕的是缺乏对引脚的统一管理,容易出现引脚的重复定义。
假设我们在 I2C1 的驱动中将 UART4_RX_DATA 引脚和 UART4_TX_DATA 引脚复用为 SCL 和 SDA, 而这个时候恰好在编写 UART4 驱动驱动时没有注意到 UART4_RX_DATA 引脚和 UART4_TX_DATA 引脚已经被使用, 在驱动中又将其初始化为 UART4_RX 和 UART4_TX,这样 I2C1 驱动将不能正常工作,并且这种错误很难被发现。pinctrl 子系统就是为了解决这个问题而引入的, pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能。
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
pinctrl 子系统是由芯片厂商来实现的, 简单来说用于帮助我们管理芯片引脚并自动完成引脚的初始化, 而我们要做的只是在设备树中按照规定的格式写出想要的配置参数即可。
总的来说,Linux 中的 pinctrl 子系统(Pin Control Subsystem) 是一个用于管理和配置通用输入/输出(GPIO) 引脚的框架。它提供了一种标准化的方法, 以在 Linux 内核中对 GPIO 引脚进行配置、分配和控制, 从而适应不同的硬件平台和设备。
3. 一些基本概念
linux 内核中提供了相关的文档,如:pinctrl-bindings.txt - Documentation/devicetree/bindings/pinctrl/pinctrl-bindings.txt - Linux source code v4.19.71
这会涉及 2 个对象: pin controller、 client device。前者提供服务:可以用它来复用引脚、配置引脚。后者使用服务:声明自己要使用哪些引脚的哪些功能,怎么配置它们。
3.1 pin controller
它是一个软件上的概念,可以认为它对应 IOMUX——用来复用引脚,还可以配置引脚(比如上下拉电阻等)。
注意, pin controller 和 GPIO Controller 不是一回事,前者控制的引脚可用于 GPIO 功能、 I2C 功能;后者只是把引脚配置为输入、输出等简单的功能。即先用 pin controller 把引脚配置为 GPIO,再用 GPIO Controler 把引 脚配置为输入或输出。
3.2 client device
“客户设备”,谁的客户? pinctrl 系统的客户,那就是使用 pinctrl 系统的设备,使用引脚的设备。它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。我们可以看一下这个 Example:

上图中,左边是 pin controller 节点,右边是 client device 节点:
3.2.1 pin state
对于一个“client device”来说,比如对于一个 UART 设备,它有多个“状态”: default、 sleep 等,那对应的引脚也有这些状态。怎么理解?
比如默认状态下, UART 设备是工作的,那么所用的引脚就要复用为 UART 功能。在休眠状态下,为了省电,可以把这些引脚复用为 GPIO 功能;或者直接把它们配置输出高电平。
上图中, pinctrl-names 里定义了 2 种状态: default、 sleep。第 0 种状态用到的引脚在 pinctrl-0 中定义,它是 state_0_node_a,位于 pincontroller 节点中。第 1 种状态用到的引脚在 pinctrl-1 中定义,它是 state_1_node_a,位于 pincontroller 节点中。当这个设备处于 default 状态时, pinctrl 子系统会自动根据上述信息把所用引脚复用为 uart0 功能。当这这个设备处于 sleep 状态时, pinctrl 子系统会自动根据上述信息把所用引脚配置为高电平。
3.2.2 groups 和 function
引脚组 groups 是一组具有相似功能、 约束条件或共同工作的引脚的集合。 每个引脚组通常与特定的硬件功能或外设相关联。 例如, 一个引脚组可以用于控制串行通信接口(如 UART 或 SPI) ,另一个引脚组可以用于驱动 GPIO。
功能(function)定义了芯片上具有外设功能的功能。 每个功能节点对应于一个或多个 IO 组(group) 的配置信息。 这些功能可以是串口、 SPI、 I2C 等外设功能。
举个例子:rk3568-pinctrl.dtsi - arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi - Linux source code v5.19.17,这里会有 can0 和 can1 对应两个不同的 function, 分别为 CAN0 控制器和 CAN 1 控制器。 每个控制器中又都有两个不同的 groups 引脚组。
- CAN0 控制器 : rk3568-pinctrl.dtsi - can0
can0 {
/omit-if-no-ref/
can0m0_pins: can0m0-pins {
rockchip,pins =
/* can0_rxm0 */
<0 RK_PB4 2 &pcfg_pull_none>,
/* can0_txm0 */
<0 RK_PB3 2 &pcfg_pull_none>;
};
/omit-if-no-ref/
can0m1_pins: can0m1-pins {
rockchip,pins =
/* can0_rxm1 */
<2 RK_PA2 4 &pcfg_pull_none>,
/* can0_txm1 */
<2 RK_PA1 4 &pcfg_pull_none>;
};
};引脚组 can0m0-pins: 这是 CAN0 控制器的第一个引脚组, 用于配置 CAN0 的引脚。 它定义了两个引脚: RK_PB4 和 RK_PB3。 其中, RK_PB4 用于 CAN0 的接收引脚(can0_rxm0) , RK_PB3 用于 CAN0 的发送引脚(can0_txm0) 。
引脚组 can0m1-pins: 这是 CAN0 控制器的第二个引脚组, 也用于配置 CAN0 的引脚。 它定义了两个引脚: RK_PA2 和 RK_PA1。 其中, RK_PA2 用于 CAN0 的接收引脚(can0_rxm1) , RK_PA1 用于 CAN0 的发送引脚(can0_txm1) 。
can1 {
/omit-if-no-ref/
can1m0_pins: can1m0-pins {
rockchip,pins =
/* can1_rxm0 */
<1 RK_PA0 3 &pcfg_pull_none>,
/* can1_txm0 */
<1 RK_PA1 3 &pcfg_pull_none>;
};
/omit-if-no-ref/
can1m1_pins: can1m1-pins {
rockchip,pins =
/* can1_rxm1 */
<4 RK_PC2 3 &pcfg_pull_none>,
/* can1_txm1 */
<4 RK_PC3 3 &pcfg_pull_none>;
};
};引脚组 can1m0-pins: 这是 CAN1 控制器的第一个引脚组, 用于配置 CAN1 的引脚。 它定义了两个引脚: RK_PA0 和 RK_PA1。 其中, RK_PA0 用于 CAN1 的接收引脚(can1_rxm0) , RK_PA1 用于 CAN1 的发送引脚(can1_txm0) 。
引脚组 can1m1-pins: 这是 CAN1 控制器的第二个引脚组, 也用于配置 CAN1 的引脚。 它定义了两个引脚: RK_PC2 和 RK_PC3。 其中, RK_PC2 用于 CAN1 的接收引脚(can1_rxm1) , RK_PC3 用于 CAN1 的发送引脚(can1_txm1) 。
3.2.3 Generic pin multiplexing node 和 Generic pin configuration node

在上图左边的 pin controller 节点中,有子节点或孙节点,它们是给 client device 使用的。可以用来描述复用信息:哪组 (group) 引脚复用为哪个功能(function);可以用来描述配置信息:哪组(group)引脚配置为哪个设置功能(setting),比如上拉、下拉等。
注意: pin controller 节点的格式, 没有统一的标准!!!每家芯片都不一样。甚至上面的 group、 function 关键字也不一定有,但是概念是有的。
3.3 示例
我们看一个示例,打开 imx6ul-14x14-evk.dtsi,有如下内容:
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};这个就相当于 client device。它对应的 pin controller 是什么?我们打开 imx6ul-14x14-evk.dtsi,会有如下内容:
pinctrl_uart1: uart1grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
>;
};二、PIN 配置信息
上面大概了解了一些基本概念,我们以 I.MX6ULL 为例,看一下设备树中的 pin 配置信息。
要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息,毕竟 pinctrl 子系统要根据我们提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。
1. iomuxc 节点
打开 imx6ul.dtsi 文件,找到一个叫做 iomuxc 的节点:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
//......
iomuxc: iomuxc@20e0000 {
compatible = "fsl,imx6ul-iomuxc";
reg = <0x020e0000 0x4000>;
};
//......
};iomuxc 节点就是 I.MX6ULL 的 IOMUXC 外设对应的节点,看起来内容很少,没看出什么跟 PIN 的配置有关的内容啊,这里其实只是指定了这个 IOMUXC 外设,我们可以看一下 reg 属性,往前翻一下,就会发现,这个节点的父节点配置了:
#address-cells = <1>; // 它指定了设备树中地址单元的位数。告诉解析设备树的软件在解释设备地址时应该使用多少位来表示一个地址单元。
#size-cells = <1>; // 告诉解析设备树的软件在解释设备大小时应该使用多少位来表示一个大小单元。所以这里的 reg 属性中,它的地址是 0x020e0000,大小是 0x4000。我们看一下《 i.MX 6ULL Applications Processor Reference Manual》—— Table 2-2. AIPS-1 memory map :

会发现,这个外设的起始地址就是 0x020e0000,大小是 16KB,就是 0x4000。
2. &iomuxc
我们打开 imx6ul-14x14-evk.dtsi 找到以下内容:
&iomuxc {
pinctrl-names = "default";
//......
pinctrl_flexcan1: flexcan1grp{
fsl,pins = <
MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020
MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020
>;
};
//......
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
//......
pinctrl_wdog: wdoggrp {
fsl,pins = <
MX6UL_PAD_LCD_RESET__WDOG1_WDOG_ANY 0x30b0
>;
};
};这个部分就是向 iomuxc 节点追加数据,不同的外设使用的 PIN 不同、其配置也不同,因此一个萝卜一个坑,将某个外设所使用的所有 PIN 都组织在一个子节点里面。例如上面的 pinctrl_i2c1 就是 I2C1 外设所用的 pin 引脚信息。和前面的结合起来就是:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
//......
iomuxc: iomuxc@20e0000 {
compatible = "fsl,imx6ul-iomuxc";
reg = <0x020e0000 0x4000>;
pinctrl-names = "default";
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
};
//......
};第 9 行:compatible 属性值为“fsl, imx6ul-iomuxc”,前面学习设备树的时候说过, Linux 内核会根据 compatbile 属性值来查找对应的驱动文件,所以我们在 Linux 内核源码中全局搜索字符串“fsl, imx6ul-iomuxc”就会找到 I.MX6ULL 这颗 SOC 的 pinctrl 驱动文件。稍后我们会学习这个 pinctrl 驱动文件。
2.1 fsl, pins
第 14 - 18 行:pinctrl_i2c1 子节点所使用的 PIN 配置信息 。我们可以看一下《 i.MX 6ULL Applications Processor Reference Manual》——Table 31-1. I2C External Signals :

会发现,I2C1 的 SCL 和 SDA 可以通过 UART4 的两个引脚复用而来。可以看到这个 pin 配置信息分为俩个部分,接下来来看一下这两个部分是什么含义:
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0对于一个 PIN 的配置主要包括两方面,一个是设置这个 PIN 的复用功能,另一个就是设置这个 PIN 的电气特性。所以我们可以大胆的猜测 MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 的这两部分配置信息一个是设置 UART4_TX_DATA 的复用功能,一个是用来设置 UART4_TX_DATA 的电气特性。
2.1.1 MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 的含义
我们接下来就来看一下这个宏是什么意思,MX6UL_PAD_UART4_TX_DATA__I2C1_SCL定义在imx6ul-pinfunc.h,其实关于这个 UART4_TX_DATA 引脚一共有 8 个相关的宏定义:
#define MX6UL_PAD_UART4_TX_DATA__UART4_DCE_TX 0x00b4 0x0340 0x0000 0 0
#define MX6UL_PAD_UART4_TX_DATA__UART4_DTE_RX 0x00b4 0x0340 0x063c 0 0
#define MX6UL_PAD_UART4_TX_DATA__ENET2_TDATA02 0x00b4 0x0340 0x0000 1 0
#define MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x00b4 0x0340 0x05a4 2 1
#define MX6UL_PAD_UART4_TX_DATA__CSI_DATA12 0x00b4 0x0340 0x04f4 3 0
#define MX6UL_PAD_UART4_TX_DATA__CSU_CSU_ALARM_AUT02 0x00b4 0x0340 0x0000 4 0
#define MX6UL_PAD_UART4_TX_DATA__GPIO1_IO28 0x00b4 0x0340 0x0000 5 0
#define MX6UL_PAD_UART4_TX_DATA__ECSPI2_SCLK 0x00b4 0x0340 0x0544 8 1我们看一下《 i.MX 6ULL Applications Processor Reference Manual》—— 32.6.29 这一部分:

观察一下就会发现,上面的宏以及展开后对应的第四个值和下面图片中的 ALTx 刚好对应。这个时候我们再来看这个:
#define MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x00b4 0x0340 0x05a4 2 1这 5 个值的含义如下所示:
#define MX6UL_PAD_UART4_TX_DATA__I2C1_SCL \
0x00b4 0x0340 0x05a4 2 1
// <mux_reg conf_reg input_reg mux_mode input_val>- 0x00b4: mux_reg 寄存器 偏移地址,在这里就是 IOMUXC_SW_MUX_CTL_PAD_UART4_TX_DATA
前面分析过了,设备树中的 iomuxc 节点就是 IOMUXC 外设对应的节点 根据其 reg 属性可知 IOMUXC 外设寄存器起始地址为 0x020e0000 。 因 此 0x020e0000+0x00b4 = 0x020e00b4,因此可知, 0x020e0000+mux_reg 就是 PIN 的复用寄存器 IOMUXC_SW_MUX_CTL_PAD_UART4_TX_DATA 的地址:

- 0x0340:conf_reg 寄存器 偏移地址,在这里就是 IOMUXC_SW_PAD_CTL_PAD_UART4_TX_DATA
0x020e0000+0x0340 = 0x020e0340,这个就是寄存器 IOMUXC_SW_PAD_CTL_PAD_UART4_TX_DATA 的地址。

- 0x05a4:input_reg 寄存器 偏移地址,在这里就是 IOMUXC_I2C1_SCL_SELECT_INPUT
有些外设有 input_reg 寄存器,有 input_reg 寄存器的外设需要配置 input_reg 寄存器。这个引脚用于 I2C1,这个有 input_reg,对应了 I2C1_SCL_SELECT_INPUT DAISY Register:IOMUXC_I2C1_SCL_SELECT_INPUT:

- 2:mux_reg 寄存器值 , 在这里就相当于设置 IOMUXC_SW_MUX_CTL_PAD_UART4_TX_DATA 寄存器为 0x2,也即是设置 UART4_TX_DATA 这个 PIN 复用为 ALT2 模式,即 I2C1_SCL。

- 1: input_reg 寄存器值, 这里就是设置 IOMUXC_I2C1_SCL_SELECT_INPUT 寄存器(参考手册的 32.6.328 I2C1_SCL_SELECT_INPUT DAISY Register)低 2 位为 1:

2.1.2 0x4001b8b0
上面已经设置了 IOMUXC_SW_MUX_CTL_PAD_UART4_TX_DATA 复用寄存器的值,那么还有电气属性没有设置,那么 IOMUXC_SW_PAD_CTL_PAD_UART4_TX_DATA 寄存器的值是怎么设置的?前面的设备树中不是还有个值吗?
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;接下来看 0x4001b8b0 就是 conf_reg 寄存器值!此值由用户自行设置,通过此值来设置一个 IO 的上/下拉、驱动能力和速度等。这里 0x4001b8b0 换算成二进制就是 0100 0000 0000 0001 1011 1000 1011 0000:

具体含义可以看参考手册对应寄存器的说明。
2.2 &i2c1
怎么使用上面的 pinctrl 信息?我们来看一下 i2c1 这个节点,这个就相当于上面说的 client device,我们打开 imx6ul-14x14-evk.dtsi
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
mag3110@e {
compatible = "fsl,mag3110";
reg = <0x0e>;
};
};3. 设备树添加 pinctrl 节点
关于 I.MX 系列 SOC 的 pinctrl 设备树绑定信息可以参考文档:
这里我们虚拟一个名为“test”的设备, test 使用了 GPIO1_IO00 这个 PIN 的 GPIO 功能, pinctrl 节点添加过程如下:
3.1 创建对应的节点
同一个外设的 PIN 都放到一个节点里面,打开 imx6ul-14x14-evk.dtsi ,在 &iomuxc 节点中添加“pinctrl_test”节点,注意!节点前缀一定要为“pinctrl_”。添加完成以后如下所示:
&iomuxc {
pinctrl-names = "default";
//......
pinctrl_wdog: wdoggrp {
fsl,pins = <
MX6UL_PAD_LCD_RESET__WDOG1_WDOG_ANY 0x30b0
>;
};
// pinctrl_test 节点
pinctrl_test: testgrp {
/* 具体的 PIN 信息 */
};
};3.2 添加“fsl, pins”属性
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为“fsl, pins”,因为对于 I.MX 系列 SOC 而言, pinctrl 驱动程序是通过读取“fsl, pins”属性值来获取 PIN 的配置信息,完成以后如下所示:
// pinctrl_test 节点
pinctrl_test: testgrp {
fsl,pins = <
/* 设备所使用的 PIN 配置信息 */
>;
};3.3 在“fsl, pins”属性中添加 PIN 配置信息
最后在“fsl, pins”属性中添加具体的 PIN 配置信息,完成以后如下所示:
// pinctrl_test 节点
pinctrl_test: testgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /* config 是具体设置值 */
>;
};4. 生成 pincontroller 设备树信息
pincontroller 设备树的信息是怎么生成的?生成 pincontroller 设备树信息,有 3 种方法:
- 有些芯片有图形化的工具,可以点点鼠标就可以配置引脚信息,得到 pincontroller 中的信息。例如 imx6ull,NXP 官方就提供了这样的工具:Search | NXP Semiconductors,或者直接点这里下载 i.MX Pins Tool, Windows 64bit package

- 有些芯片,只能看厂家给的设备树文档或者参考设备树的例子
- 最差的就是需要阅读驱动代码才能构造设备树信息。
参考资料:
linux 内核驱动-pinctrl 子系统 - fuzidage - 博客园
NXP(恩智浦)官方提供的配置 i.MX 系列处理器引脚复用和功能的工具“i.MX Pins Tool”的安装及使用说明【我主要用于生成设备树节点描述语句】_imx pin tool-CSDN 博客