linux pinctrl and gpio driver

  • Pinctrl: 负责引脚的功能复用(MUX)和电气属性配置(上下拉,驱动能力等).
  • GPIO: 负责管理GPIO的输入输出,高低电平和中断.

芯片刚通电、Linux 还没启动时,几乎所有引脚的默认状态都是 GPIO 输入模式(且内部电阻断开,处于高阻态 High-Z)。
为什么?因为如果默认是输出高电平,而外部电路刚好接地,一通电芯片就烧了。默认设为“毫无防备的输入状态”是最安全的。只有少数几个用于调试的引脚(比如 UART 串口、JTAG),硬件工程师会设计成一上电就默认连通,方便开发者抓开机日志。

Pinctrl

只负责搭建好物理通路和电气环境

  1. Pin Enumeration (引脚枚举):
  • 内核启动时,Pinctrl 驱动要做的第一件事就是向内核的 Pinctrl Core 注册:“我这个芯片一共有 128 个引脚,编号从 0 到 127,它们的名字分别叫 PA0, PA1…”。这通过 struct pinctrl_pin_desc 结构体数组实现。
  1. Pin Muxing (引脚复用 - struct pinmux_ops):
  • 将引脚组合成 Group(组),并将 Group 映射到 Function(功能)。
  • Group:比如引脚 10 和 11 组成一个叫 i2c0_pins 的组。
  • Function:比如这个组可以提供 i2c0 功能。
  • 底层动作:当设备树请求这个功能时,内核调用你写的 set_mux 回调函数,你的代码去写底层的物理寄存器(比如向 0xd401xxxx 写入特定掩码),拨动物理开关。
  1. Pin Configuration (引脚配置 - struct pinconf_ops):
  • 设置引脚的电气属性。当设备树里写了 bias-pull-up; 时,内核会解析这个属性,并调用你驱动里的 pin_config_set 回调函数,去写芯片的上下拉寄存器。

  • K1 pinctrl & gpio 节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pinctrl: pinctrl@d401e000 {
compatible = "spacemit,k1-pinctrl";
reg = <0x0 0xd401e000 0x0 0x1000>; // 物理基地址 0xd401e000,长度 4KB
clocks = <&syscon_apbc CLK_AIB>, // 该模块的工作时钟
<&syscon_apbc CLK_AIB_BUS>; // 该模块的总线访问时钟
clock-names = "func", "bus"; // 给时钟起的名字,驱动里用这个名字申请
spacemit,apbc = <&syscon_apbc>; // 指向 APBC 控制器的句柄(后面详细)
};

gpio: gpio@d4019000 {
compatible = "spacemit,k1-gpio";
reg = <0x0 0xd4019000 0x0 0x100>; // GPIO 寄存器区域(只有 256 字节,比 pinctrl 小得多)
clocks = <&syscon_apbc CLK_GPIO>,
<&syscon_apbc CLK_GPIO_BUS>;
clock-names = "core", "bus";
gpio-controller; // 声明:我是一个 GPIO 管理器
#gpio-cells = <3>; // 规定:别人引用我时要填 3 个数字(通常是:组、组内编号、极性)
interrupts = <58>; // 整个 GPIO 模块共用 58 号中断(当中断发生,内核来这里查是谁跳变了)
interrupt-parent = <&plic>;// 中断上报给 RISC-V 的 PLIC 中断控制器
interrupt-controller; // gpio也是一个中断控制器,可以接收控制中断
#interrupt-cells = <3>;

gpio-ranges = <&pinctrl 0 0 0 32>, // 把 GPIO 第 0 组(0-31号)映射到 pinctrl 的物理引脚 0-31
<&pinctrl 1 0 32 32>, // 把 GPIO 第 1 组映射到 pinctrl 的物理引脚 32-63
<&pinctrl 2 0 64 32>, // 以此类推...
<&pinctrl 3 0 96 32>;
};

pinctrl 驱动操作集关键函数:

  • dt_node_to_map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 看看设备树里这个节点写了几个引脚?(比如 uart0 里面写了 104 和 105 两个引脚)
npins = of_property_count_u32_elems(child, "pinmux");

for (i = 0; i < npins; i++) {
// 2. 把 <K1_PADCONF(104, 3)> 这个 32 位的数字读出来放到 config 里
of_property_read_u32_index(child, "pinmux", i, &config);

// 3. 提取引脚号 (高16位):104
pins[i] = spacemit_dt_get_pin(config);
// 4. 把完整的配置存起来
pinmuxs[i].config = config;
}

// 5. 现代魔法来了!动态建组:告诉内核,把这 2 个引脚打包成一个叫 uart0_0_cfg 的 Group
pinctrl_generic_add_group(pctldev, grpname, pins, npins, pinmuxs);

// 6. 现代魔法 2!解析电气属性:比如 drive-strength = <19>;
// 把复杂的字符串解析工作直接甩给内核的通用函数,它会自动帮你填好 map!
pinconf_generic_parse_dt_config(child, pctldev, &map[nmaps].data.configs.configs, ...);
  • set_mux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 获取刚才动态建立的那个 Group 的信息
group = pinctrl_generic_get_group(pctldev, gsel);
configs = group->data;

for (i = 0; i < group->grp.npins; i++) {
// 2. 算物理账:找到这个引脚对应的寄存器虚拟地址
reg = spacemit_pin_to_reg(pctrl, spin->pin);

// 3. 算逻辑账:提取出复用功能号(比如 3)
mux = spacemit_dt_get_pin_mux(configs[i].config);

// ============ 经典 Read-Modify-Write (读-改-写) + 锁 ============
guard(raw_spinlock_irqsave)(&pctrl->lock); // 上锁:别人别碰!

value = readl_relaxed(reg) & ~PAD_MUX; // 读出旧值,并把旧的复用位清零
writel_relaxed(mux | value, reg); // 填入新的复用位,写回寄存器

// 离开大括号,guard 机制会自动解锁
}
  • pin_config_set

之前内核把设备树里的 bias-pull-up = <0> 解析成了宏 PIN_CONFIG_BIAS_PULL_UP。现在,轮到把宏变成具体的寄存器位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for (i = 0; i < num_configs; i++) {
param = pinconf_to_config_param(configs[i]); // 提取宏,比如 PIN_CONFIG_BIAS_PULL_UP
arg = pinconf_to_config_argument(configs[i]); // 提取参数,比如 <0> 里的 0

switch (param) {
case PIN_CONFIG_BIAS_DISABLE:
// 如果设备树写了 bias-disable,就把上下拉使能位全部清零
v &= ~(PAD_PULL_EN | PAD_PULLDOWN | PAD_PULLUP);
break;

case PIN_CONFIG_BIAS_PULL_UP:
// 如果是上拉:清除下拉位,置位使能位和上拉位
v &= ~PAD_PULLDOWN;
v |= (PAD_PULL_EN | PAD_PULLUP);
if (arg == 1) v |= PAD_STRONG_PULL; // 如果参数是 1,还开启强上拉
break;

case PIN_CONFIG_DRIVE_STRENGTH:
// 如果是驱动能力,就把参数存下来,后面去查表换算成寄存器的真实值
flag |= ENABLE_DRV_STRENGTH;
drv_strength = arg;
break;

// ...
}
}
  • spacemit_pinconf_dbg_show

输入这行命令时:cat /sys/kernel/debug/pinctrl/d401e000.pinctrl-spacemit,k1-pinctrl/pinconf-pins
内核回调 dbg_show 函数

1
2
3
4
5
value = readl(reg); // 读出寄存器的真实物理值

// 然后用 seq_printf 把这个值格式化成人类能看懂的字符串,打印到屏幕上
spacemit_pinconf_dbg_pull(seq, value);
seq_printf(seq, ", register (0x%04x)", value);
  • gpio_request_enable: spacemit_request_gpio
1
2
3
4
5
static int spacemit_request_gpio(struct pinctrl_dev *pctldev, ...)
{
reg = spacemit_pin_to_reg(pctrl, pin); // 找到寄存器
writel_relaxed(spin->gpiofunc, reg); // 强行把这个引脚的复用,切到 GPIO 模式!
}

当在外部的 GPIO 驱动里调用申请引脚时,内核底层就会间接触发 Pinctrl 里的这个 gpio_request_enable 函数,自动完成硬件级别的 Mux 切换.

GPIO

引脚的配置主要分为 “自动档” 和 “手动档”。

  1. really_probe() 托管 (常见)
    触发条件:只要在设备树节点里写了 pinctrl-names = “default”; 和 pinctrl-0 = <&xxx>;。在执行你的 probe 之前,内核调用 really_probe() -> pinctrl_bind_pins()。引脚自动切换到正确模式。直接在驱动里操作 UART 寄存器就行了。

  2. 驱动内部申请 (GPIO)

  • 2.1 这主要发生在 GPIO 子系统 介入的时候。
    如果不是在写一个标准的 UART 驱动,而是在写一个点灯程序或者按键驱动(使用 GPIO 模式)
    操作函数:你会调用 devm_gpiod_get() 或 gpio_request()。
    申请 GPIO 44。GPIO 子系统发现这个引脚属于某个 Pinctrl 范围。GPIO 子系统会背地里调用 Pinctrl 框架的 pinctrl_gpio_request()。Pinctrl 框架再调用你 Pinctrl 驱动里的 set_mux,把这个引脚物理上切到“GPIO 功能”。
    这种方式下,是 GPIO 驱动在指挥 Pinctrl 驱动。
  • 2.2 需要动态切换引脚状态
    比如一个 SD 卡驱动:
    平时:工作在 default 状态(高速模式)。
    休眠时:为了省电,需要切换到 sleep 状态(把引脚设为输入,防止漏电)。
    pinctrl_select_state(p->p, p->pins_sleep);
    这虽然是“手动”切换,但它操作的是 Pinctrl 中预设好的状态,而不是去申请单个引脚。
作者

GoKo Mell

发布于

2025-11-05

更新于

2026-04-14

许可协议

# 相关文章
  1.linux clock driver framework
评论

:D 一言句子获取中...