3、PinMux控制器驱动框架:pinctrl子系统的架构、pinctrl核心层与驱动层
好,咱们今天聊聊pinctrl子系统的驱动框架。说实话,这个框架刚出来的时候,我也觉得挺绕的。但用熟了以后,你会发现它其实很优雅——把硬件细节和上层逻辑分得清清楚楚。
我个人习惯把pinctrl子系统想象成一个“接线员”。设备要什么功能,告诉接线员,接线员去查表、拨开关。这个接线员就是pinctrl核心层,而真正动手拨开关的,是底层的驱动层。
3.1 整体架构:三层分工
pinctrl子系统分三层,我画了张图,你一看就明白:
你看,设备驱动在最上面,它只管说“我要用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_countget_group_pinsdt_node_to_map |
管理引脚组,将设备树节点映射为引脚配置 |
pinmux_ops |
requestset_muxgpio_request_enable |
引脚复用选择,比如把引脚设为UART功能 |
pinconf_ops |
pin_config_setpin_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驱动,大致流程是这样的:
- 分配并初始化
struct pinctrl_desc,填充三个操作集 - 调用
devm_pinctrl_register()注册到核心层 - 核心层会遍历设备树,找到匹配的节点
- 当其他驱动请求引脚时,核心层回调你的操作集
代码骨架大概长这样:
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_mux或pin_config_set - 驱动层去写硬件寄存器
这种设计的好处是,核心层可以做一些通用的事情,比如检查引脚是否已被占用、管理引用计数等。驱动层只需要关心怎么操作硬件。
嗯,这里要注意一点:引脚冲突检测。核心层会记录每个引脚当前被谁使用。如果你在设备树里把同一个引脚配给了两个设备,核心层会报错。我在项目中遇到过这种情况,当时两个外设驱动都请求了同一个引脚,系统启动时直接打印了一堆backtrace。
好了,pinctrl的框架就聊到这儿。三层架构、核心层的调度逻辑、驱动层的回调实现,这些是理解pinctrl的基础。下一节我们会深入设备树中的pinctrl属性,看看那些 pinctrl-0、pins、function 到底是怎么被解析的。