> 技术文档 > 【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统

【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统

文章偏向应用案例讲解
本篇文章将会实现下面案例,讲解PinCtrl子系统

把一对引脚复用作为IIC引脚

一、基础知识点补充

1.1 设备树知识点补充

设备树很多人只知道他是个描述硬件的文件
其他一无所知
在讲解之前先花费一些篇幅进行科普
1.作用
我这块板子上有哪些硬件
每个硬件的地址、中断号、引脚、时钟等信息
2.内核怎么使用

1)我们写 .dts 文件(描述硬件)
2)用 dtc 工具编译成 .dtb 文件(二进制格式)
3)Bootloader加载 .dtb, 并传给内核
4)内核解析 .dtb 并初始化相应驱动

1.2 搞清楚嵌入式厂商的区别:

首先我们要知道一个芯片,和一个开发板的区别【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
他们设备厂商不同

2 定位不同

1)SOC厂商:卖芯片的
a.瑞芯微:RK3588芯片
b.恩智浦半导体:imx6ull线片

2)板级厂商:买开发板的
a.迅龙软件有限公司:香橙派(用的RK3588芯片)
b.百问网:韦东山的imx6ull开发板

2 产出资料不同

1)SOC厂商:卖芯片的:
他们会产出DTSI文件、驱动代码
芯片厂商产出的设备树文件叫做DTSI,一般包如下信息

1.CPU 核、总线、片上外设的寄存器地址范围
2.芯片内部时钟、复位、电源控制器的名字和编号
3.中断控制器的连线关系、中断号分配
4.内部 SRAM、ROM、Cache 的大小和地址
5.芯片固定管脚的功能/复用默认值
6.芯片级电源域、时钟树、复位树的拓扑

2)板级厂商
产出DTS设备树文件,包含如下信息

1.板载 DDR、Flash 的实际大小和物理地址
2.外接 PHY、USB Hub、Codec、Wi-Fi 模块的节点
3.GPIO/IOMUX 在板子上的具体复用配置
4.LED、按键、风扇等板级设备的 GPIO 引脚号
5.设备在板子上的使能/禁用状态(status = “okay”/“disabled”)
6.板子特有的电源、复位、使能脚连接方式

二、快速理解PinCtrl子系统的功能

稍微用过STM32知道我们想要让一个引脚输出为高电平
需要以下几个步骤:
1.使能GPIO时钟
2.配置引脚为输出模式
3.输出类型:配置上拉/下拉电阻
4.引脚输出高电平
其中2、3两步骤是pinctrl子系统要实现的功能

三、PinCtrl子系统设备树讲解

3.1 PinCtrl子系统对应的硬件信息

学过STM32都知道,配置外设的功能本质都是对寄存器的操作,PinCtrl子系统也不例外,它实现的复用功能是靠着IOMUX寄存器来实现的

以imx6ull设备为例,该功能是由专门的一个硬件负责
IOMUXC控制器:负责IO管脚功能复用
我们希望操作该控制器,需要知道该芯片的寄存器地址
芯片手册显示其及基地址是202E 4000,长度:16KB

IOMUXC:Input/Output Multiplexer Controller
GPR:General Purpose Register(通用寄存器)

【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
于是,该硬件在设备树的描述如下所示
(这个是SOC厂商提供的DTSI文件)

iomuxc: iomuxc@020e0000 {compatible = \"fsl,imx6ul-iomuxc\";reg = <0x020e0000 0x4000>;};

很显然这个设备树信息只是指出了这个硬件设备的地址,我们真正开始想用还需要至少知道我们希望在哪个管脚,复用成I2C1
这时候还需要查阅芯片手册,关键词搜索i2c1,点击去如图
【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
这个图片内容就是I2C1的SCL功能具体要复用到下面红框的哪个引脚上
仔细想之下至少又出现一个问题,平时开发STM32习惯了的时候我们通常都是用PA1这种方式命名引脚

SOC厂商是如何进行命名引脚的?
为此我专门把对应的引脚命名从源数据手册裁剪下来供大家观看
IMX芯片引脚信息
【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
经过查看发现红框中的三个不是具体的功能,是管脚名称
我们希望把他用到UART4_TX_DATA引脚上
那么在设备树中如何去写?
如果接着找UART4_TX_DATA寄存器地址那就太麻烦了
SOC芯片厂商给了一个比较方便的功能就是宏定义:

MX6UL_PAD_<PAD_NAME>__<FUNC_NAME>MX6UL_PAD_UART4_TX_DATA__I2C1_SCL arch/arm/boot/dts/imx6ul-pinfunc.h#define MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x0090 0x031C 0x0588 0x2 0x0

关于宏定义的具体的含义如下

<mux_reg conf_reg input_reg mux_mode input_val>mux_reg:复用寄存器地址偏移conf_reg:配置寄存器地址偏移(上下拉、驱动强度等)input_reg:输入选择寄存器偏移(某些功能需要)mux_mode:复用功能编号(0~7)input_val:输入选择值(某些功能需要)

然后我们还需要实现第二个功能,就是实现引脚的电气属性
比如电阻上拉下拉之类,这个是直接用一个值来实现的

0x4001b8b0

【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统

3.2 设备树语法

最后我们在设备树中写下如下信息
✅ 第一部分:定义引脚配置(pinctrl)

 pinctrl_i2c1: i2c1grp { fsl,pins = < MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0 MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0 >; };};

✅ 第二部分:启用 I2C1 控制器并绑定引脚

&i2c1 { clock-frequency = <100000>; pinctrl-names = \"default\"; pinctrl-0 = <&pinctrl_i2c1>; status = \"okay\";};

关于命名规则,第二部分的i2c1是驱动程序用来匹配时,使用的名称。
如果我们设备比如说有正常工作模式和睡眠模式两种状态,我们希望正常工作时保持IIC,休眠时恢复GPIO。
我们可以在进行修改

&i2c1 { clock-frequency = <100000>; pinctrl-names = \"default\"\"sleep\"; pinctrl-0 = <&pinctrl_i2c1>; pinctrl-1 = <&pinctrl_gpio1>; status = \"okay\";};pinctrl_gpio1: gpio1grp { fsl,pins = < MX6UL_PAD_UART4_TX_DATA__GPIO1_IO28 0x1b0b0 /* SCL → input-pull-up */ MX6UL_PAD_UART4_RX_DATA__GPIO1_IO29 0x1b0b0 /* SDA → input-pull-up */ >;};

3.3 稍微高级的用法pinctrl-hog

几乎每一块开发板或嵌入式产品上,都有一个或多个LED灯。其中最基本的就是电源指示灯,它的作用是在设备上电并正常工作后立即亮起,给用户一个明确的视觉反馈:“设备已开机,系统正在运行”
问题
如果按照标准的驱动程序模型来处理:

leds-gpio 驱动会在GPIO子系统、平台总线等一系列更基础的驱动初始化完毕后,才会被内核探测(probe)并加载

结果就是:从系统上电到leds-gpio驱动最终被加载,中间会有几秒钟甚至更长的时间。在这段时间里,电源指示灯是熄灭的。这会给用户带来困惑,他们可能会以为设备没有启动成功或者坏了。
pinctrl-hog 完美地解决了这个问题。我们不需要为这个简单的LED编写任何专门的驱动程序,只需要在设备树中“霸占”这个引脚,并直接设置其状态。

使用方法:

/* /include/dt-bindings/gpio/gpio.h */#include &iomuxc { /* ... 其他引脚配置 ... */ pinctrl_hog_led: ledhoggrp { fsl,pins = < /*  * 1. MX6UL_PAD_GPIO1_IO02__GPIO1_IO02:  * 将 GPIO1_IO02 这个物理引脚复用为 GPIO1_IO02 功能。 * 2. 0x19:  * 配置引脚的电气特性,比如驱动强度、速度等。 * 这里只是一个示例值。 */ MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0x19 >; };};/* 在对应的GPIO控制器节点中声明 \"hog\" */&gpio1 { /* * 定义一个GPIO Hog节点来控制电源LED */ power-led-hog { gpio-hog;  // 声明这是一个GPIO Hog gpios = <2 GPIO_ACTIVE_HIGH>; // 指定是gpio1的第2个引脚,高电平有效 output-high;  // **关键!** 在占用时,立即将此引脚配置为输出,并设置为高电平 line-name = \"power-led\"; // 为这个GPIO行起一个名字,方便调试 };};

其实到这里基本我们就可以进行开发了 但是对于系统内核如何识匹配,如何保存信息这些深度信息,我觉得还是有必要了解的

四、深入:内核是如何识别设备树


4.1 内核如何“看懂”Pin Controller

1. 入口:probe函数的执行**

首先,内核启动,设备树被解析。内核发现一个compatible = \"fsl,imx6ul-iomuxc\"的设备节点

iomuxc: iomuxc@020e0000 {compatible = \"fsl,imx6ul-iomuxc\";reg = <0x020e0000 0x4000>;};

内核在驱动列表中,找到了一个能处理此compatible字符串的驱动程序——pinctrl-imx6ul.c。于是,该驱动的probe函数(imx6ul_pinctrl_probe)被执行。

Pinctrl核心框架是一个通用的框架,它不认识任何特定的芯片。它需要一种标准化的方式来了解这个新来的iomuxc设备

pinctrl-imx6ul.c驱动程序必须回答内核的这个问题。它用来回答的“标准格式答卷”,就是struct pinctrl_desc

所以,probe函数的核心任务,就是填写这份pinctrl_desc答卷。

struct pinctrl_desc {const char*name; // 控制器的名字const struct pinctrl_pin_desc*pins; // 控制器管理的所有引脚的列表 (一个数组)unsigned intnpins; // 上面引脚列表的长度const struct pinctrl_ops*pctlops; // \"引脚组\" 操作函数集const struct pinmux_ops*pmxops; // \"引脚复用\" 操作函数集const struct pinconf_ops*confops; // \"引脚配置\" 操作函数集struct module*owner; // 指向驱动模块本身 (THIS_MODULE)// ... 其他成员};

2 核心:填写pinctrl_desc以描述能力

这份答卷包含几个关键问题,驱动必须一一作答:

  1. “你管着哪些引脚?”
    回答: 驱动程序指向一个静态数组imx6ul_pins,并将pinctrl_desc->pins指向它,同时在pinctrl_desc->npins里填上引脚总数。这个数组里定义了所有引脚的编号和名字。
    数组单个元素的结构体定义如下:

    struct pinctrl_pin_desc {unsigned intnumber; // 引脚的全局唯一编号const char*name; // 引脚的名字 (例如 \"GPIO1_IO00\")void*drv_data; // 驱动私有数据,pinctrl核心不使用};
  2. “如果要复用你的引脚,我该调用你的哪个函数?”
    回答: 驱动程序将pinctrl_desc->pmxops指向一个叫imx_pmx_ops的结构体。这个结构体里包含了set_mux等函数指针。(pm—pinmux)

    struct pinmux_ops {// 获取该控制器支持的所有“功能”(function)的数量int (*get_functions_count) (struct pinctrl_dev *pctldev);// 根据索引号,获取“功能”的名字 (例如 \"i2c1\", \"uart2\")const char *(*get_function_name) (struct pinctrl_dev *pctldev, unsigned selector);// 获取一个“功能”可以被哪些“引脚组”(group)实现int (*get_function_groups) (struct pinctrl_dev *pctldev, unsigned selector, const char *const **groups, unsigned *const num_groups);// 【核心】将指定的“引脚组”复用为指定的“功能”int (*set_mux) (struct pinctrl_dev *pctldev, unsigned func_selector,unsigned group_selector);// 当一个引脚要用作GPIO时,调用的使能函数int (*gpio_request_enable) (struct pinctrl_dev *pctldev, struct pinctrl_gpio_range *range, unsigned offset);// 释放GPIO时调用的函数void (*gpio_disable_free) (struct pinctrl_dev *pctldev, struct pinctrl_gpio_range *range, unsigned offset);// ... 其他};
  3. “如果要配置引脚的电气特性,又该调用哪个函数?”
    回答: 驱动程序将pinctrl_desc->confops指向imx_pinconf_ops结构体,里面有pin_config_set等函数指针。

    struct pinconf_ops {// 检查一个配置参数是否受支持bool (*is_generic) (void); // 在新版本内核中已不常用// 获取某个引脚的当前电气配置int (*pin_config_get) (struct pinctrl_dev *pctldev, unsigned pin, unsigned long *config);// 【核心】为一个引脚设置电气配置int (*pin_config_set) (struct pinctrl_dev *pctldev, unsigned pin, unsigned long *configs, unsigned num_configs);// 获取某个引脚组的当前电气配置int (*pin_config_group_get) (struct pinctrl_dev *pctldev, unsigned selector, unsigned long *config);// 【核心】为一个引脚组设置电气配置int (*pin_config_group_set) (struct pinctrl_dev *pctldev, unsigned selector, unsigned long *configs, unsigned num_configs);// ... 其他用于调试的函数};
  4. “如果要按‘组’来操作引脚,该调用哪个函数?”
    回答: 驱动程序将pinctrl_desc->pctlops指向imx_pctrl_ops结构体。

    struct pinctrl_ops {// 获取该控制器中所有“引脚组”(group)的数量int (*get_groups_count) (struct pinctrl_dev *pctldev);// 根据索引号,获取“引脚组”的名字 (例如 \"i2c1grp\", \"uart2grp\")const char *(*get_group_name) (struct pinctrl_dev *pctldev, unsigned selector);// 获取一个“引脚组”中包含了哪些引脚int (*get_group_pins) (struct pinctrl_dev *pctldev, unsigned selector, const unsigned **pins, unsigned *num_pins);// 【核心】将一个设备树节点,翻译成内核标准的pinctrl_map格式int (*dt_node_to_map) (struct pinctrl_dev *pctldev, struct device_node *np, struct pinctrl_map **map, unsigned *num_maps);// 释放由dt_node_to_map分配的内存void (*dt_free_map) (struct pinctrl_dev *pctldev, struct pinctrl_map *map, unsigned num_maps);// ... 其他用于调试的函数};

上面的结构体定义看看就行,也被背不下来,但需要记住大概功能


3. 终点:注册到内核**

根据上面我们知道,pinctrl_desc这份“答卷”已经在内存里填写完毕

驱动程序必须把这份答卷提交给Pinctrl核心框架审批。
提交动作由devm_pinctrl_register()函数完成。
devm_pinctrl_register()函数做了两件重要的事:

  1. 审批“答卷” (创建 pinctrl_dev):

    • 它接收驱动递交的pinctrl_desc
    • 它在内核中创建一个新的、代表这个iomuxc硬件的活动实例——struct pinctrl_dev。这个pinctrl_dev就像是内核颁发给iomuxc的“工作许可证”。它内部保存了指向pinctrl_desc的指针,以便随时查阅其能力。
  2. 登记备案:

    • 它将新创建的pinctrl_dev添加到一个全局的链表中。这样,当系统中的其他设备(如I2C1)需要pinctrl服务时,就能通过这个全局链表找到iomuxc这个服务提供商。
      这个全局链表是pinctrldev_list。
    //drivers/pinctrl/core.cstatic LIST_HEAD(pinctrldev_list);//LIST_HEAD是内核链表宏,//它定义并初始化一个名为pinctrldev_list的空链表头。

    内核中所有已经成功注册的Pin Controller,它们的pinctrl_dev结构体都会被添加到这个pinctrldev_list链表中。

    提供查找: 当一个Client设备(如I2C1)需要Pinctrl服务时,Pinctrl核心框架会遍历这个pinctrldev_list链表,从中查找并匹配能够满足其需求的Pin Controller。

    struct pinctrl_dev *pinctrl_register(...){ // ... 分配和初始化 pctldev ... list_add_tail(&pctldev->node, &pinctrldev_list); // 关键:将新的pinctrl_dev实例添加到全局链表尾部 // ... 其他代码 ... return pctldev;}

4. 伏笔:注册“翻译器”

注册一个“翻译器”以备后用

  • 前因: 在第2.2步中,驱动程序填写了pinctrl_descpctlops成员,使其指向imx_pctrl_ops这个函数集。在这个函数集里,有一个非常关键的函数指针dt_node_to_map,它被设置为指向imx_dt_node_to_map这个具体函数。

  • 后果: 这个dt_node_to_map函数指针的注册,相当于Pin Controller驱动向Pinctrl核心框架做出了一个承诺:

    “我提供一个‘翻译服务’。
    以后,当你拿到一个我设备树节点下的子节点(比如i2c1grp),而你又看不懂里面私有的fsl,pins语法
    就把那个节点交给我这个函数,我负责把它翻译成你能看懂的、内核标准格式的pinctrl_map。”

  • 当前状态: 在Pin Controller驱动的probe阶段,这个翻译服务只是被声明和注册了,但并不会被调用。它处于待命状态,等待真正的“翻译任务”到来。这个任务将由Client设备(如i2c1)在初始化时触发。

现在,Pin Controller驱动已经初始化完毕,并处于“待命”状态。接下来,轮到Client设备登场了。

4.2 i2c1节点到pinctrl的过程

目标: 跟踪内核处理i2c1设备pinctrl请求的全过程。
重点是理解设备树信息如何被翻译成内核内部使用的数据结构。

1.i2c1设备树节点和驱动代码匹配过程

1.设备(Device)的注册
内核启动时,会解析imx6ull.dtsi等设备树文件
发现i2c1的节点如下

i2c1: i2c@021a0000 {#address-cells = <1>;#size-cells = <0>;compatible = \"fsl,imx6ul-i2c\", \"fsl,imx21-i2c\";reg = <0x021a0000 0x4000>;interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_I2C1>;status = \"disabled\";};

在板级厂商出品的xxxx-imx6ull-xxxxx.dts文件中会有如下定义:

&i2c1 { clock-frequency = <100000>; pinctrl-names = \"default\"; pinctrl-0 = <&pinctrl_i2c1>; status = \"okay\";};

当它看到**&i2c1**这个节点时,它会读取节点内的属性。其中最重要的属性是compatible。

创建platform_device: 内核会为这个i2c1设备树节点,在内存中创建一个struct platform_device对象

内核将这个新创建的platform_device对象,注册到platform bus设备链表device list)上。

2.驱动(Driver)的注册
在内核启动的某个阶段,drivers/i2c/busses/i2c-imx.c这个文件会被编译进内核。文件末尾通常有一个module_init宏,它会调用一个初始化函数,比如i2c_imx_init

创建platform_driver: 在i2c_imx_init函数中,驱动的作者会定义一个struct platform_driver对象。

// 在 \\Linux-4.9.88\\drivers\\i2c\\busses\\i2c-imx.c 中static const struct of_device_id i2c_imx_dt_ids[] = {{ .compatible = \"fsl,imx1-i2c\", .data = &imx1_i2c_hwdata, },{ .compatible = \"fsl,imx21-i2c\", .data = &imx21_i2c_hwdata, },//事实上匹配到了这一项,和我们预想的匹配fsl,imx6ul-i2c是不一样的!{ .compatible = \"fsl,vf610-i2c\", .data = &vf610_i2c_hwdata, },{ /* sentinel */ }};static struct platform_driver i2c_imx_driver = {.probe = i2c_imx_probe,.remove = i2c_imx_remove,.driver = {.name = DRIVER_NAME,.pm = I2C_IMX_PM_OPS,.of_match_table = i2c_imx_dt_ids,},.id_table = imx_i2c_devtype,};

这里很多人可能会疑惑为甚么没匹配到fsl,imx6ul-i2c

为什么会这样设计?
这种设计实现了驱动复用:i.MX6ULL的I2C与i.MX21的I2C硬件高度相似,因此它们可以共用同一个驱动程序,避免了代码冗余。

这个细节不查阅源码是不会发现的!
这样子较真不容易,看到这里求个赞!

“懂!都懂!咱家宝藏读者们怎么可能不想点赞呢?肯定是内容太精彩,看得太入迷,回过神来已经刷到下一个啦!或者就是忙着收藏、把点赞这个小可爱给‘冷落’了。嘿嘿,不过悄悄说,你们留下的每一个赞,对我们创作者来说都是超大的能量包!要是觉得‘这波不亏’、‘学到了’或者‘笑出声’,手指头稍微动一动点个赞~让我们一起让好内容被更多人看见!”

回归主题,下一步是:

注册到总线: i2c_imx_init函数会调用platform_driver_register(&i2c_imx_driver)。这个函数将i2c_imx_driver对象,注册到platform bus的**驱动链表(driver list)**上。

3.总线(Bus)的匹配工作**

匹配的动作,在注册的瞬间就会发生。

  • 当驱动注册时:
    1. platform_driver_register函数被调用。
    2. 它会遍历platform bus设备链表(上面挂着i2c1等设备)。
    3. 对于每一个设备,它都会调用一个bus_match函数来检查是否匹配。
  • 当设备注册时(反之亦然):
    1. platform_device_register被调用。
    2. 它会遍历platform bus驱动链表
    3. 对于每一个驱动,也调用bus_match

一旦bus_match返回成功,总线就会立刻做一件事:

  • 调用驱动的probe函数: 它会调用i2c_imx_driver中注册的.probe函数指针,也就是i2c_imx_probe函数。并且,它会把匹配上的那个platform_device对象(代表i2c1)作为参数,传递给i2c_imx_probe

这就是i2c_imx_probe(struct platform_device *pdev)pdev参数的由来。

2.解析 i2c1 设备树节点

在调用probe之前,内核框架代码(位于drivers/base/dd.c的really_probe函数)会做一个检查:
1)内核的第一个问题:“这个设备需要Pinctrl服务吗??”
它通过检查设备树节点里是否存在pinctrl-0等属性来判断。因为i2c1节点有这些属性,所以Pinctrl相关的处理流程被触发。
2)内核的第二个问题:“这个Client需要什么?”
前因: 内核知道i2c1需要Pinctrl服务,但具体需要什么,需要多少种状态,它还不清楚。
后果: 内核开始解析i2c1设备树节点里的pinctrl-*属性,并将这些信息组织成C语言结构体。

a.解析引脚配置:pinctrl_bind_pins

在启动i2c1驱动之前,必须先处理好它的引脚配置
内核需要一个地方来存放i2c1的所有pinctrl相关信息
解决方案: 创建一个struct pinctrl实例。这是i2c1设备的“Pinctrl信息总管”。

struct pinctrl 的定义 (简化后`):

struct pinctrl { struct device *dev; // 指回它所属的设备 (i2c1) struct list_head states; // 一个链表头,用来挂载该设备的所有状态 struct pinctrl_state *state; // 指向当前正被应用的那个状态 struct list_head dt_maps; // 临时存放从设备树翻译来的 pinctrl_map};

如何关联: 这个新创建的pinctrl实例的地址,被存放在i2c1设备自身的dev->pins->p指针里

b.解析pinctrl-names:为档案分出不同的“状态”

任务: i2c1可能有多种引脚状态(如工作、休眠)。需要为每种状态创建一个容器。
信息来源: 读取i2c1设备树节点里的`pinctrl-names = “default”,“sleep”

解决方案:\"default\"\"sleep\"这两个名字,分别创建两个struct pinctrl_state实例。
struct pinctrl_state 的定义 :

struct pinctrl_state { const char *name;  // 状态的名字,比如 \"default\" struct list_head settings; // 一个链表头,用来挂载这个状态下的所有具体配置指令 struct list_head node; // 用于把自己挂载到 pinctrl->states 链表上};

如何组织: 这两个新创建的pinctrl_state实例,通过它们的node成员,被挂载到struct pinctrlstates链表上。
具体我发了一篇专门的博客文章去讲解这个,点击即可查看

c.如何翻译设备树每个状态下的引脚信息:dt_node_to_map函数+ struct pinctrl_map数组

当前状态: 我们有了名为\"default\"pinctrl_state,但它的settings链表是空的。内核需要知道这个状态下到底要做哪些具体的引脚配置。
信息来源: 读取pinctrl-0 = ;。这行告诉内核:\"default\"状态的配置,定义在pinctrl_i2c1标签指向的节点里。

问题: 内核找到了iomuxc节点下的i2c1grp子节点,但它看不懂里面的fsl,pins = 这种厂商私有格式。

必然推论: 看不懂私有格式,就必须让懂的人来翻译。这个“懂行”的人就是iomuxc的驱动程序。
内核的动作: 内核调用iomuxc驱动在第二章注册的dt_node_to_map函数。

  • 输入: i2c1grp设备树节点。
  • 输出: 一个标准的“翻译稿”—— struct pinctrl_map数组。

任务: 将翻译稿转化为最终可执行的硬件指令。
解决方案: 内核遍历dt_node_to_map函数返回的pinctrl_map数组,并将每一个map条目转换成一个struct pinctrl_setting

struct pinctrl_setting 的定义 :

struct pinctrl_setting { struct list_head node; // 用于把自己挂载到 pinctrl_state->settings 链表上 enum pinctrl_map_type type; // 操作类型: MUX (复用) 还是 CONFIG (配置) struct pinctrl_dev *pctldev; // 要操作哪个Pin Controller (iomuxc的句柄) // data联合体,根据type不同,包含不同的内容 union { // 如果type是MUX,这里存放功能ID和引脚组ID struct pinctrl_setting_mux mux; // 如果type是CONFIG,这里存放配置参数数组 struct pinctrl_setting_configs configs; } data;};

最后给一个整体关系图

1. Client 侧(每个被 pinctrl 管理的设备,比如 i2c1)┌─────────────────────────────────────┐│ struct device (e.g. “i2c1”) │└─────────────────────────────────────┘ ├─ .pins ──▶ struct dev_pin_info │ └─ .p ──▶ struct pinctrl ←—— 每个 device 对应唯一一个 pinctrl 实例 │ │ struct pinctrl │ ├─ .dev ──▶ back to struct device │ ├─ .dt_maps ──▶ temporary list of struct pinctrl_map │ │ (dt_node_to_map() 从 DTS 私有节点翻译而来) │ └─ .states ──▶ list of struct pinctrl_state │  ├─ [state 0] struct pinctrl_state (default) │  │ ├─ .name = \"default\" │  │ └─ .settings ──▶ list of struct pinctrl_setting │  │ ├─ [setting A] (MUX) │  │ │ ├─ .type = PIN_MAP_TYPE_MUX_GROUP │  │ │ ├─ .data.mux.group = \"i2c1grp\" │  │ │ └─ .data.mux.function = \"i2c1\" │  │ └─ [setting B] (CONFIG) │  │  ├─ .type = PIN_MAP_TYPE_CONFIGS_GROUP │  │  └─ .data.configs = {0x4001b8b0} │  └─ [state 1] struct pinctrl_state (“sleep”) │  ├─ .name = \"sleep\" │  └─ .settings ──▶ list of struct pinctrl_setting │ └─ [setting C] (只属于 sleep 状态的配置) └─ (device 其它字段…)2. 中间翻译稿┌─────────────────────────────────────┐│ struct pinctrl_map[] │└─────────────────────────────────────┘ ├─ map[0]: 描述 “i2c1grp” ↔ MUX “i2c1” └─ map[1]: 描述 “i2c1grp” ↔ CONFIG 0x4001b8b0↓ 核心框架遍历所有 map[i], 为每个相关 state 创建一个独立的 struct pinctrl_setting, 并插入到对应的 state.settings 链表中。3. 最终执行单元┌─────────────────────────────────────┐│ struct pinctrl_setting  │└─────────────────────────────────────┘ ├─ .type (MUX_GROUP 或 CONFIGS_GROUP) ├─ .data (mux 或 configs) └─ .pctldev ──▶ struct pinctrl_dev ←—— 实际调用硬件操作时要经由它4. Controller Driver 侧┌─────────────────────────────────────┐│ struct pinctrl_desc │└─────────────────────────────────────┘ ├─ name : 控制器名字 ├─ pins :struct pinctrl_pin_desc[] ├─ pmxops :struct pinmux_ops ├─ confops :struct pinconf_ops └─ pctlops :struct pinctrl_ops ┌─────────────────────────────────────┐│ devm_pinctrl_register(&desc) │└─────────────────────────────────────┘ └─ creates struct pinctrl_dev  ├─ .desc ──▶ &pinctrl_desc  └─ adds to global pinctrldev_list5. 运行时调用路径struct pinctrl_setting.pctldev → struct pinctrl_dev.desc → { pmxops / confops } → 最终写寄存器

4.3 从内存中pinctrl_setting指令,到IOMUXC寄存器的写操作

内核已经为i2c1设备准备好了所有Pinctrl信息,但还没有实际操作硬件。现在,它仍然在i2c1驱动的probe函数被调用之前。
内核的动作: 内核框架自动调用pinctrl_select_state()函数,来激活i2c1的默认状态。
输入: i2c1pinctrl句柄,以及要激活的状态名(\"default\")。
核心任务: 找到\"default\"状态对应的pinctrl_state实例,然后遍历它下面的settings链表,执行每一条pinctrl_setting指令。

pinctrl_select_state内部的逻辑: 函数内部有一个巨大的switch-case语句,它检查每一条pinctrl_setting指令的type成员。

// 简化后的伪代码list_for_each_entry(setting, &state->settings, node) { switch (setting->type) { case PIN_MAP_TYPE_MUX_GROUP: // 这是条“复用”指令 pinmux_enable_setting(setting); break; case PIN_MAP_TYPE_CONFIGS_GROUP: // 这是条“电气配置”指令 pinconf_apply_setting(setting); break; // ... 其他case }}

执行“复用”指令 (PIN_MAP_TYPE_MUX_GROUP)
当前指令: 假设遍历到的setting是“复用”类型的。
内核的动作: 调用pinmux_enable_setting(setting)。这个函数会进一步调用iomuxc的Pin Controller驱动在第二章注册的pmxops操作集里的函数。
struct pinmux_ops 的关键成员 :

struct pinmux_ops { // ... int (*set_mux) (struct pinctrl_dev *pctldev,  unsigned func_selector,  unsigned group_selector); // ...};

最终调用链:

  1. pinctrl_select_state()
  2. -> pinmux_enable_setting(setting)
  3. -> ops->set_mux(...) (这里的ops就是iomuxc驱动pmxops)

硬件操作: iomuxc驱动的set_mux函数被执行。它从setting参数中提取出功能ID和引脚组ID,然后计算出正确的寄存器地址和要写入的值,最后通过writel()之类的函数,向IOMUXC的硬件复用寄存器写入数据

执行“电气配置”指令 (PIN_MAP_TYPE_CONFIGS_GROUP)

当前指令: 假设遍历到的setting是“电气配置”类型的。
内核的动作: 调用pinconf_apply_setting(setting)。这个函数会进一步调用iomuxc的Pin Controller驱动注册的confops操作集里的函数。

struct pinconf_ops 的关键成员:

struct pinconf_ops { // ... int (*pin_config_group_set) (struct pinctrl_dev *pctldev, unsigned group_selector, unsigned long *configs, unsigned num_configs); // ...};

最终调用链:

  1. pinctrl_select_state()
  2. -> pinconf_apply_setting(setting)
  3. -> ops->pin_config_group_set(...) (这里的opsiomuxc驱动的confops)

硬件操作: iomuxc驱动的pin_config_group_set函数被执行。它从setting参数中提取出引脚组ID和配置参数数组(里面存放着0x4001b8b0这样的值),计算出对应的PAD control寄存器地址,向该寄存器写入配置值

故事的结局:调用Client驱动的probe

当前状态: pinctrl_select_state执行完毕。settings链表里的所有指令都已被执行,IOMUXC硬件寄存器已被正确配置。i2c1的引脚现在物理上已经连接到了I2C控制器,并具备了正确的电气特性。
最后的动作: 内核框架终于可以放心地调用i2c1设备驱动自己的probe函数了。
Client驱动视角: I2C驱动的开发者在probe函数里,完全不需要关心任何引脚配置的细节。他可以假设一切就绪,直接开始操作I2C控制器的寄存器,收发数据。