Linux 驱动之 Pinctrl 子系统

Pinctrl 概述

关于 pinctrl 主要可以归结为两类设置,其中一类是功能选择,即一组 gpio 是用于 iic 还是 uart 还是就作为普通 gpio 来用,另一类是 gpio 的特性配置,即上拉、下拉、驱动能力和速率的配置。而 pinctrl 主要负责这两类配置的管理工作。总结起来 pinctrl 主要完成以下三种功能:

  • 引脚枚举与命名 (Enumerating and naming)
  • 引脚复用 (Multiplexing):比如用作 GPIO、I2C 或其他功能
  • 引脚配置 (Configuration):比如上拉、下来、open drain、驱动强度等

dts 配置

以全志平台的写法如下(虚构的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uart0_pins: uart0-pins {
pins = "PA4", "PA5";
function = "uart0";
};
uart0_sleep_pins: uart0-pins {
pins = "PA4", "PA5";
function = "gpio";
};
...
&uart0 {
pinctrl-names = "default","sleep";
pinctrl-0 = <&uart0_pins>;
pinctrl-1 = <&uart0_sleep_pins>;
status = "okay";
};

pinctrl-0 对应的 pinctrl-names 的第 0 个名称 default,默认配置,也就是 uart0 在使用 pinctrl 的时候的默认配置,pinctrl-1 对应的是 pinctrl-names 的第一个名称 sleep,在系统进入休眠的时候会通过 pinctrl 将 uart0 的 pin 配置成 gpio 功能。

Pinctrl 重要概念

在软件上将他们分为 pin controller 和 client device 两个设备,但是 pinctrl 一般是没有具体硬件的控制器的,就是 gpio 里的一些 config 和 data 寄存器,control 是软件抽象的概念。

Pin Controller

pin controller 和 GPIO Controller 不是一回事,前者控制的引脚可用于 GPIO 功能、I2C 功能;后者只是把引脚配置为输入、输出等简单的功能。即先用 pin controller 把引脚配置为 GPIO,再用 GPIO Controler 把引脚配置为输入或输出。

  • Pin State
    对于一个“client device”来说,比如对于一个 UART 设备,它有多个“状态”:default、sleep 等,那对应的引脚也有这些状态。比如默认状态下,UART 设备是工作的,那么所用的引脚就要复用为 UART 功能。在休眠状态下,为了省电,可以把这些引脚复用为 GPIO 功能;或者直接把它们配置输出高电平

  • Groups 和 Function
    一个设备会用到一个或多个引脚,这些引脚就可以归为一组 (group)。这些引脚可以复用为某个功能:function

  • Multiplexing node 和 Configuration node
    可以用来描述复用信息:哪组 (group) 引脚复用为哪个功能 (function) 可以用来描述配置信息:哪组 (group) 引脚配置为哪个设置功能 (setting),比如上拉、下拉等

Client Device

具体到每个控制器一般都是要通过 pin 脚与外部连接的,这就需要使用 soc 的 pin 脚,那么这个控制器就是 pinctrl 的 client device,具体的 client device 会通过 pinctrl 来将 pin 设置为相应的功能及配置。

数据结构

pinctrl 的数据结构是比较复杂的,整体数据结构之间的关系如下:原图

上图是数据结构的整体结构图,主要分为三部分,下面我将分别介绍每一部分:

  • pin controller:
    pin controller 主要是描述该 soc 的所有引脚信息以及一系列操作函数集。该结构通过 pinctrl_dev 数据结构来描述,该结构体主要提供的功能是:将此引脚控制器的每个引脚描述符保存在 radix_tree 树中,通过 struct pin_desc 来描述该引脚控制器,pin_desc 里有控制器的每个引脚配置信息以及访问及设置该引脚控制器的方法,也就是 pctlops、pmxops 和 confops,分别是访问控制器下的 pin 和 group 的方法、设置复用功能和设置配置引脚的操作方法,这个一般由 soc 原厂来提供驱动

  • pinctrl_maps:
    pinctrl_maps 主要是描述板级配置信息,对于一块单板会使用的 soc 的不同控制器及不同配置,对于同一 soc 的 iic0 控制器在不同单板上可以接在不同的 pin group 上,这就是 pin 的管脚复用功能。在 dts 中如下:

    1
    2
    3
    4
    5
    6
    &uart0 {
    pinctrl-names = "default","sleep";
    pinctrl-0 = <&uart0_pins>;
    pinctrl-1 = <&uart0_sleep_pins>;
    status = "okay";
    };

    pinctrl-0 和 pinctrl-1 等会被解析成一个个的 pinctrl_maps 然后通过 pinctrl_register_mappings 函数将每一个控制器用到的引脚信息这册到内核,实际上就是通过一个链表组织整个单板上控制器的引脚状态信息

  • pinctrl:
    pinctrl 主要是描述 client 设备管脚的状态和使用情况。例如对于 uart、iic、spi 等设备都需要通过 pin 与其他设备进行通信,那么就需要配置 pin 连接到对应的具体控制器以及配置 pin 的一些电器特性,例如上下拉,驱动能力速率等信息。该数据结构既然是 client 端的数据那么就需要挂入到设备驱动模型结构中,在上图可以看到该结构先是挂入到 dev_pin_info 结构,然后 dev_pin_info 又是 struct device 结构的成员,这是该结构的上半部分;下半部分是状态信息的描述了首先是挂接所有的状态例如 default、sleep 状态。然后每个状态下就是配置信息,这些配置信息来自上面介绍的 pinctrl_maps 结构体中的数据

实现

Pin Controller 的注册

pin controller 的注册主要调用吧如下函数:

1
2
struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
struct device *dev, void *driver_data)

该函数需要提供一个 pinctrl_desc 结构体,并返回一个 pinctrl_dev 结构体。由于函数调用关系比较简单这里就不画流程图了。这里最关键的是 pinctrl_desc 结构体:该结构体用于描述一个 soc pin controller 的信息,包含所支持的 pin 引脚的描述(每一个引脚的名称与 index)、pin 复用操作的接口(主要是 struct pinmux_ops 类型的变量,包含引脚的申请与释放、gpio 引脚复用配置以及方向设置接口等)、pin 引脚或 group 相关的引脚配置接口(包含引脚配置接口、引脚当前配置参数获取接口、group 相关引脚的配置接口、debug 接口等)、group 相关的操作接口(获取 group 的个数、获取 group 的名称、获取 group 对应的引脚内容、从设备树设备节点中解析 board pin 描述信息并进行 pinctrl map 注册的接口 dt_node_to_map)。然后 pinctrl_register 函数就是创建一个 pinctrl_dev 结构体然后设置相关成员变量并对 pinctrl_desc 结构体中提供的一些函数操作做检查(必须提供函数实现),再将 pinctrl_dev 链入 pinctrldev_list 全局链表中即可。

devm_pinctrl_register 是 pinctrl_register 的资源管理版本,也是调用 pinctrl_register 实现的:

Pinctrl Map 注册

pinctrl map 的注册函数是 pinctrl_register_mappings,如下:

1
2
int pinctrl_register_mappings(const struct pinctrl_map *maps,
unsigned num_maps)

该函数也比较简单,需要提供一个需要注册到内核的 pinctrl_map 结构体,然后 pinctrl_register_mappings 函数就对该结构体进行 check,然后将其挂在 pinctrl_maps 链表上。

Bing 过程

在前面提到了 client 里会通过设备树获得一个个的 pinctrl_maps 结构,而该结构是要根 pin controller 里的 pin 之间建立联系的,因为不同的 soc pin 脚的设计都是不一样的,其配置方法也不一样,而在控制器初始化阶段需要解析出该控制器所使用的 pin 脚然后与 controller 提供的 pin 脚建立联系通过 controller 的 pin 或 group 操作函数集来操作具体的 pin 配置为想要的功能。

在前面介绍设备驱动模型的时候有一张 device 注册过程的流程图,其中有一个细节就是在 driver_probe_device 的时候会调用 really_probe 函数,而 really_probe 函数又会调用 pinctrl_bind_pins 函数用来处理 pinctrl 信息,如下:原图

下面我们将其中的 pinctrl 处理过程补充完,pinctrl_bind_pins 函数如下:原图

该函数主要完成以下工作:

  • 解析设备树,解析 client 设备的 pinctrl 状态信息,并创建相关结构
  • 通过设备树中的 pinctrl 的 phandle 找到 controller 配置中的 pin,并将设备树中的每一个状态转换为 map 结构,这里由于不同的 soc 关于 pinctrl 的设备树写法不一致因此这里通过 controller 提供的回调函数 dt_node_to_map 转换为 map 结构(具体的 soc 有自己的实现自然知道转换方法)并将一系列的 map 转换为一系列的与 map 对应的 setting 结构
  • 对每一个 map 结构进行 pinmux 和 pinconf 的设置,同样设置函数也是由具体的 controller 提供实现,然后通过函数指针调用具体的实现

与 gpio 的关系

原则上 gpio 子系统是 pinctrl 的 client 用户,gpio 和 iic、spi 这些一样会通过 pinctrl 来配置,但是实际上 gpio 和 pinctrl 之间有更为紧密的联系,如果通过 soc 的 datasheet 我们也可以了解到实际的 soc 中并没有具体的 pin controller 硬件,这个一般都在 gpio 的章节中介绍,与 gpio 的寄存器之间是密不可分的。这里关于 gpio 和 pinctrl 之间的关系可以参考 窝窝科技 的文章。

pinctrl 和 gpio 之间的强耦合实现

首先还是看下相关数据结构,如下:原图

gpio 和 pinctrl 通过 gpio_pin_range 建立映射关系,将 gpio 中的 pin 描述转换到 pinctrl 中的 pin 描述空间,从而将两者建立关联,这样 gpio 就可以不通过 pinctrl 提供的标准接口来设置 pin 的复用和配置了。gpio_pin_range 这个描述关系通过 dts 文件来描述,这样就可以不通过代码将他们建立联系,写法如下:

1
2
3
4
5
6
qe_pio_e: gpio-controller@1460 {
#gpio-cells = <2>;
compatible = "fsl,qe-pario-bank-e", "fsl,qe-pario-bank";
gpio-controller;
gpio-ranges = <&pinctrl1 0 20 10>, <&pinctrl2 10 50 20>;
};

上面 dts 节点中的 gpio-ranges 关键字指定了两个 gpio range:gpio controller(qe_pio_e) 中的 gpio0 ~ 9 和 pinctrl1 中的 pin20 ~ 29 对应;gpio controller(qe_pio_e) 中的 gpio10 ~ 29 和 pinctrl2 中的 pin50 ~ 69 对应。 而 dts 文件的解析过程如下:原图

以上过程将生成 pinctrl_dev 下由 gpio_ranges 组织成的链表。

下面以 gpiod_get 函数为例看一下 gpio 中具体是如何使用 pinctrl 中的接口的,gpiod_get 函数流程如下:原图

该函数就是将 gpio 中的 pin 描述转换到 pinctrl 中的描述,进而找到该管脚在 pinctrl 中的描述然后调用 pin controller 提供的函数来设置引脚复用功能和配置引脚。

参考文献

https://blog.csdn.net/lickylin/article/details/106677781
https://blog.csdn.net/lickylin/article/details/106677851?spm=1001.2014.3001.5501
窝窝科技 gpio 子系统相关文章
《韦东山老师的书籍和课程》