3、PinMux控制器驱动框架:pinctrl子系统的架构、pinctrl核心层与驱动层

好,咱们今天聊聊pinctrl子系统的驱动框架。说实话,这个框架刚出来的时候,我也觉得挺绕的。但用熟了以后,你会发现它其实很优雅——把硬件细节和上层逻辑分得清清楚楚。

我个人习惯把pinctrl子系统想象成一个“接线员”。设备要什么功能,告诉接线员,接线员去查表、拨开关。这个接线员就是pinctrl核心层,而真正动手拨开关的,是底层的驱动层。

3.1 整体架构:三层分工

pinctrl子系统分三层,我画了张图,你一看就明白:

pinctrl子系统三层架构 设备驱动层 UART、I2C、SPI、MMC等外设驱动 通过pinctrl API请求引脚配置 pinctrl核心层 (drivers/pinctrl/core.c) 引脚状态管理、复用选择、配置解析 维护全局引脚描述符、提供标准API接口 处理设备树中的pinctrl-*属性 pinctrl驱动层 (pinmux/pinconf控制器驱动) 操作硬件寄存器:设置复用、上下拉、驱动能力 实现pinctrl_ops、pinmux_ops、pinconf_ops 调用者 调度者 执行者

你看,设备驱动在最上面,它只管说“我要用UART2”,剩下的交给核心层。核心层去查设备树,找到对应的引脚和配置,然后叫驱动层去写寄存器。驱动层是唯一碰硬件的人。

3.2 pinctrl核心层:大脑与调度中心

核心层在 drivers/pinctrl/core.c 里。它维护了一个全局的引脚描述符链表,每个引脚对应一个 struct pinctrl_pin_desc

我记得刚接触这个框架时,最让我困惑的是“引脚状态”这个概念。其实说白了,一个引脚在不同场景下有不同的配置——比如休眠时拉低、工作时拉高。核心层通过 struct pinctrl_state 来管理这些状态。

核心层的关键数据结构:

  • struct pinctrl_dev:代表一个pinctrl控制器实例
  • struct pinctrl_pin_desc:描述单个引脚的信息
  • struct pinctrl_state:引脚状态(如default、sleep、idle)
  • struct pinctrl_setting:状态中的具体配置项

核心层还负责解析设备树。当驱动调用 devm_pinctrl_get_select_default() 时,核心层会去设备树里找 pinctrl-0 属性,然后解析出引脚号和配置参数。

3.3 pinctrl驱动层:真正干活的

驱动层需要实现三个操作集,我称之为“三板斧”:

操作集 主要回调 职责
pinctrl_ops get_groups_count
get_group_pins
dt_node_to_map
管理引脚组,将设备树节点映射为引脚配置
pinmux_ops request
set_mux
gpio_request_enable
引脚复用选择,比如把引脚设为UART功能
pinconf_ops pin_config_set
pin_config_get
引脚电气属性配置,如上下拉、驱动强度

写驱动层代码时,我最常踩的坑是 dt_node_to_map 的实现。这个函数要把设备树里的 pinctrl-0 节点解析成内核能理解的配置。如果你解析错了,设备树写得再漂亮也没用。

避坑指南:我曾经在一个项目里,设备树里明明配了上拉,但实际测量引脚却是浮空。查了两天才发现,是 pin_config_set 里寄存器位写反了。所以写驱动层时,一定要对照芯片手册,逐位确认寄存器操作。

3.4 设备树与pinctrl的绑定

设备树里怎么用pinctrl?我给你看个典型例子:

/* 设备树中的pinctrl配置 */
&uart2 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = &uart2_default;
    pinctrl-1 = &uart2_sleep;
    status = "okay";
};

/* 引脚复用配置节点 */
uart2_default: uart2_default {
    function = "uart2";
    groups = "uart2_tx", "uart2_rx";
    pins = "GPIO1_0", "GPIO1_1";
    bias-pull-up;
    drive-strength = <2>;
};

这里 pinctrl-names 定义了两种状态:default 和 sleep。核心层会在设备初始化时自动应用 default 状态,在系统休眠时切换到 sleep 状态。你想想看,这比在驱动里手动写寄存器省了多少事?

3.5 驱动注册流程

写一个pinctrl驱动,大致流程是这样的:

  1. 分配并初始化 struct pinctrl_desc,填充三个操作集
  2. 调用 devm_pinctrl_register() 注册到核心层
  3. 核心层会遍历设备树,找到匹配的节点
  4. 当其他驱动请求引脚时,核心层回调你的操作集

代码骨架大概长这样:

static const struct pinctrl_ops my_pinctrl_ops = {
    .get_groups_count = my_get_groups_count,
    .get_group_pins   = my_get_group_pins,
    .dt_node_to_map   = my_dt_node_to_map,
};

static const struct pinmux_ops my_pinmux_ops = {
    .request  = my_pinmux_request,
    .set_mux  = my_pinmux_set_mux,
};

static const struct pinconf_ops my_pinconf_ops = {
    .pin_config_set = my_pinconf_set,
};

static struct pinctrl_desc my_pinctrl_desc = {
    .name      = "my-pinctrl",
    .pins      = my_pins,
    .npins     = ARRAY_SIZE(my_pins),
    .pctlops   = &my_pinctrl_ops,
    .pmxops    = &my_pinmux_ops,
    .confops   = &my_pinconf_ops,
    .owner     = THIS_MODULE,
};

static int my_pinctrl_probe(struct platform_device *pdev)
{
    struct pinctrl_dev *pctl;

    pctl = devm_pinctrl_register(&pdev->dev, &my_pinctrl_desc, NULL);
    if (IS_ERR(pctl))
        return PTR_ERR(pctl);

    return 0;
}

个人经验:我建议你在 set_mux 回调里加一些调试打印,把当前设置的引脚号和功能号打出来。这样调试设备树时,能快速确认解析是否正确。别问我怎么知道的——都是血泪教训。

3.6 核心层与驱动层的交互细节

核心层和驱动层之间通过回调函数交互。核心层不直接操作寄存器,它只负责调度。举个例子:

  • 设备驱动调用 pinctrl_select_state()
  • 核心层遍历该状态下的所有 pinctrl_setting
  • 对每个setting,核心层调用驱动层的 set_muxpin_config_set
  • 驱动层去写硬件寄存器

这种设计的好处是,核心层可以做一些通用的事情,比如检查引脚是否已被占用、管理引用计数等。驱动层只需要关心怎么操作硬件。

嗯,这里要注意一点:引脚冲突检测。核心层会记录每个引脚当前被谁使用。如果你在设备树里把同一个引脚配给了两个设备,核心层会报错。我在项目中遇到过这种情况,当时两个外设驱动都请求了同一个引脚,系统启动时直接打印了一堆backtrace。

好了,pinctrl的框架就聊到这儿。三层架构、核心层的调度逻辑、驱动层的回调实现,这些是理解pinctrl的基础。下一节我们会深入设备树中的pinctrl属性,看看那些 pinctrl-0pinsfunction 到底是怎么被解析的。