【Linux驱动-快速回顾】一文彻底搞懂pinctrl子系统
文章偏向应用案例讲解
本篇文章将会实现下面案例,讲解PinCtrl子系统
把一对引脚复用作为IIC引脚
一、基础知识点补充
1.1 设备树知识点补充
设备树很多人只知道他是个描述硬件的文件
其他一无所知
在讲解之前先花费一些篇幅进行科普
1.作用
我这块板子上有哪些硬件
每个硬件的地址、中断号、引脚、时钟等信息
2.内核怎么使用
1)我们写 .dts 文件(描述硬件)
2)用 dtc 工具编译成 .dtb 文件(二进制格式)
3)Bootloader加载 .dtb, 并传给内核
4)内核解析 .dtb 并初始化相应驱动
1.2 搞清楚嵌入式厂商的区别:
首先我们要知道一个芯片,和一个开发板的区别
他们设备厂商不同
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(通用寄存器)
于是,该硬件在设备树的描述如下所示
(这个是SOC厂商提供的DTSI文件)
iomuxc: iomuxc@020e0000 {compatible = \"fsl,imx6ul-iomuxc\";reg = <0x020e0000 0x4000>;};
很显然这个设备树信息只是指出了这个硬件设备的地址,我们真正开始想用还需要至少知道我们希望在哪个管脚,复用成I2C1
这时候还需要查阅芯片手册,关键词搜索i2c1,点击去如图
这个图片内容就是I2C1的SCL功能具体要复用到下面红框的哪个引脚上
仔细想之下至少又出现一个问题,平时开发STM32习惯了的时候我们通常都是用PA1这种方式命名引脚
SOC厂商是如何进行命名引脚的?
为此我专门把对应的引脚命名从源数据手册裁剪下来供大家观看
IMX芯片引脚信息
经过查看发现红框中的三个不是具体的功能,是管脚名称
我们希望把他用到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
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以描述能力
这份答卷包含几个关键问题,驱动必须一一作答:
-
“你管着哪些引脚?”
回答: 驱动程序指向一个静态数组imx6ul_pins,并将pinctrl_desc->pins指向它,同时在pinctrl_desc->npins里填上引脚总数。这个数组里定义了所有引脚的编号和名字。
数组单个元素的结构体定义如下:struct pinctrl_pin_desc {unsigned intnumber; // 引脚的全局唯一编号const char*name; // 引脚的名字 (例如 \"GPIO1_IO00\")void*drv_data; // 驱动私有数据,pinctrl核心不使用};
-
“如果要复用你的引脚,我该调用你的哪个函数?”
回答: 驱动程序将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);// ... 其他};
-
“如果要配置引脚的电气特性,又该调用哪个函数?”
回答: 驱动程序将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);// ... 其他用于调试的函数};
-
“如果要按‘组’来操作引脚,该调用哪个函数?”
回答: 驱动程序将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()函数做了两件重要的事:
-
审批“答卷” (创建
pinctrl_dev
):- 它接收驱动递交的
pinctrl_desc
。 - 它在内核中创建一个新的、代表这个
iomuxc
硬件的活动实例——struct pinctrl_dev
。这个pinctrl_dev
就像是内核颁发给iomuxc
的“工作许可证”。它内部保存了指向pinctrl_desc
的指针,以便随时查阅其能力。
- 它接收驱动递交的
-
登记备案:
- 它将新创建的
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_desc
的pctlops
成员,使其指向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)的匹配工作**
匹配的动作,在注册的瞬间就会发生。
- 当驱动注册时:
platform_driver_register
函数被调用。- 它会遍历
platform bus
的设备链表(上面挂着i2c1
等设备)。 - 对于每一个设备,它都会调用一个
bus_match
函数来检查是否匹配。
- 当设备注册时(反之亦然):
platform_device_register
被调用。- 它会遍历
platform bus
的驱动链表。 - 对于每一个驱动,也调用
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 pinctrl
的states
链表上。
具体我发了一篇专门的博客文章去讲解这个,点击即可查看
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
的默认状态。
输入: i2c1
的pinctrl
句柄,以及要激活的状态名(\"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); // ...};
最终调用链:
pinctrl_select_state()
-> pinmux_enable_setting(setting)
-> 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); // ...};
最终调用链:
pinctrl_select_state()
-> pinconf_apply_setting(setting)
-> ops->pin_config_group_set(...)
(这里的ops
就iomuxc
驱动的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控制器的寄存器,收发数据。