[TOC]

drivers/mfd 多功能设备驱动核心(Multi-Function Devices Core) 管理集成多种功能的复合芯片

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了解决现代集成电路(IC)日益增长的功能集成度所带来的驱动程序开发和管理难题。许多现代芯片,特别是电源管理集成电路(PMIC)、音频编解码器(Audio Codec)、系统控制器等,不再是单一功能的设备。它们在一个物理芯片上集成了多个可以独立工作的硬件模块。
例如,一个PMIC可能同时包含:

  • 多个直流-直流(DC-DC)转换器和低压差线性稳压器(LDO)
  • 一个实时时钟(RTC)
  • 一个GPIO控制器
  • 一个中断控制器
  • 一个看门狗定时器(Watchdog)
  • 一个ADC(模数转换器)

如果没有MFD框架,开发者可能会编写一个巨大的、单一的“巨石型”(Monolithic)驱动程序来管理所有这些功能。这种做法会导致以下问题:

  • 代码臃肿且难以维护:一个驱动文件包含所有功能的逻辑,违反了单一职责原则,代码耦合度高。
  • 缺乏模块化和重用性:RTC功能部分的代码无法被内核标准的RTC子系统重用,GPIO部分也无法与标准的GPIO子系统集成。
  • 配置困难:内核的配置选项(Kconfig)会变得非常复杂,无法独立地启用或禁用芯片上的某个特定功能。
  • 所有权混乱:巨石型驱动使得内核的各个子系统(如RTC, GPIO, Watchdog)无法正确地“拥有”和管理它们对应的硬件部分。

MFD框架的诞生就是为了解决这个问题,它提供了一种**“分而治之”**的策略,允许一个核心的父驱动程序将一个物理上的多功能芯片,在软件层面分解成多个独立的、虚拟的子设备。

它的发展经历了哪些重要的里程碑或版本迭代?

MFD框架是随着Linux内核设备模型的演进而自然产生的需求。它不是一次性被设计出来的,而是从实践中总结出的最佳模式。

  • 早期尝试:在MFD框架正式出现之前,一些驱动开发者会通过自定义的方式在自己的驱动内部注册其他设备,但这缺乏统一的标准。
  • MFD核心API的出现:内核社区认识到这种模式的普遍性,于是创建了drivers/mfd目录,并提供了核心的辅助函数,最重要的是 mfd_add_devices()。这个API标准化了从一个父设备注册多个子设备的过程。
  • 与Device Tree的集成:随着设备树(Device Tree)在ARM等平台上的普及,MFD框架与DT紧密集成。在DT中,一个多功能芯片被描述为一个父节点,其内部的各个功能块则被描述为子节点。MFD父驱动在解析自己的DT节点时,会遍历其子节点,并为每个子节点注册一个对应的平台设备(platform_device)。
  • Cell-based驱动mfd_add_devices() 使用 struct mfd_cell 结构来描述每个子设备,包括其名称、平台数据、资源等,这成为了一种标准的数据驱动注册方式。

目前该技术的社区活跃度和主流应用情况如何?

MFD是一种非常成熟、稳定且被广泛使用的内核驱动设计模式。它不是一个可选的附加功能,而是编写复杂芯片驱动的标准和推荐方法

  • 应用广泛:几乎所有现代嵌入式系统、手机、平板电脑和许多PC主板上的复杂芯片都使用MFD模式进行驱动。PMIC、音频Codec、多功能传感器集线器(Sensor Hub)等驱动程序绝大多数都位于 drivers/mfd/ 目录下或遵循MFD的设计思想。
  • 社区规范:向内核提交新的复杂芯片驱动时,社区审查者会期望它遵循MFD模式,将共享逻辑放在一个核心驱动中,并将独立的功能分解为子设备驱动。

核心原理与设计

它的核心工作原理是什么?

MFD框架的核心原理是**“分解”与“代理”**。

  1. 核心父驱动(MFD Core Driver):首先,会有一个核心的驱动程序,它负责与这个多功能物理芯片本身进行通信(例如,通过I2C或SPI总线)。这个父驱动通常被称为“MFD核心驱动”或“总线驱动”。它负责:

    • 初始化与芯片的通信。
    • 管理芯片级别的共享资源,如共享的中断线、复位引脚、核心寄存器映射和时钟。
    • 读取芯片ID和版本信息。
  2. 子设备描述:父驱动内部会定义一个子设备列表,通常是一个 struct mfd_cell 数组。每个 mfd_cell 描述了一个子功能,包括:

    • 子设备的名称,这将成为子平台设备的名称。
    • 子设备所需的硬件资源,如寄存器地址范围、中断号等。这些资源通常是父设备资源的子集或偏移。
    • 特定于子设备的平台数据(platform_data)或固件节点(of_node)。
  3. 注册子设备:父驱动在其 .probe() 函数的最后,会调用 mfd_add_devices()。这个函数会遍历子设备列表,为列表中的每一项创建一个新的 struct platform_device 实例,并将其注册到内核中。

  4. 子驱动匹配与探测:一旦这些新的平台设备被注册,它们就会像普通的平台设备一样,被内核的驱动核心用于与对应的子驱动程序进行匹配。例如,被注册的名为 “mychip-rtc” 的子设备,会与一个专门的 “mychip-rtc” 平台驱动进行匹配。

  5. 资源共享:子驱动在工作时,如果需要访问芯片的共享寄存器或处理共享中断,它不会直接操作硬件。相反,它会通过父驱动提供的API(通常通过 dev_get_drvdata() 获取父驱动实例)来进行。父驱动在这里扮演了一个**“硬件访问代理”**的角色,它可能会使用锁来仲裁对共享资源的访问。

它的主要优势体现在哪些方面?

  • 代码模块化:将一个复杂的驱动分解为多个小的、功能单一的驱动,使得代码更清晰、更易于维护。
  • 驱动重用:如果一个芯片的RTC模块是标准兼容的,那么分解出的RTC子驱动就可以直接使用内核通用的rtc-platdev.c驱动,而无需重写。这极大地促进了代码重用。
  • -Kconfig的灵活性:每个子驱动都可以有自己的Kconfig选项,允许用户根据需要独立地启用或禁用芯片上的某个功能,从而精简内核大小。
  • 职责清晰:符合Linux内核的子系统划分。RTC子设备由RTC子系统管理,GPIO子设备由GPIO子系统管理,权责分明。

它存在哪些已知的劣劣势、局限性或在特定场景下的不适用性?

  • 通信开销:子驱动与硬件之间的通信需要通过父驱动进行中转,这相比于直接访问硬件增加了一层函数调用的开销。但在MFD设备通常连接的低速总线(I2C/SPI)上,这种软件开销通常可以忽略不计。
  • 设计复杂性:对于开发者来说,需要设计父驱动和子驱动之间的API,这比编写一个单一的驱动要稍微复杂一些。
  • 不适用于紧耦合功能:如果芯片上的多个功能块之间存在非常紧密的、实时的交互和依赖关系,将它们强行拆分为独立的子驱动可能会导致设计困难或性能问题。在这种罕见的情况下,将它们保留在一个驱动中可能是更合适的选择。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

MFD是处理任何在一个物理IC上集成了多个独立或半独立硬件功能块的唯一标准和首选方案

  • 电源管理集成电路(PMIC):这是最典型的例子。一个PMIC驱动(如drivers/mfd/da9063.c)会作为父驱动,负责I2C通信和中断管理。然后它会注册多个子设备,如:
    • da9063-regulator:由内核的Regulator子系统处理。
    • da9063-rtc:由RTC子系统处理。
    • da9063-onkey:由Input子系统处理(电源键)。
  • 音频编解码器(Audio Codec):一个复杂的音频Codec芯片(如drivers/mfd/wm8994-core.c)可能包含音频接口(AIF)、DAC/ADC、耳机/麦克风插孔检测、GPIO和时钟控制。MFD父驱动处理与芯片的通信,然后注册不同的子设备,由ALSA、GPIO等子系统分别处理。
  • 系统控制器芯片:一些主板上的嵌入式控制器(EC)或系统级芯片(SoC)内部集成了多种外设,如I2C控制器、SPI控制器、看门狗等。一个MFD驱动可以作为这些内部外设的“总线管理器”。

是否有不推荐使用该技术的场景?为什么?

  • 单一功能设备:对于只实现一个功能的简单芯片(例如,一个单纯的I2C温度传感器),完全没有必要使用MFD框架。一个简单的I2C客户端驱动就足够了。
  • 纯软件设备:MFD是为物理硬件设备设计的。对于纯软件或虚拟设备,有更合适的抽象层。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比 “巨石型” (Monolithic) 驱动

特性 MFD (Multi-Function Device) 模式 “巨石型” (Monolithic) 驱动
设计思想 分而治之,分解与代理。 将所有功能实现在一个单一的大驱动中。
代码结构 一个核心父驱动 + 多个小型、独立的子驱动。 一个庞大、复杂、高耦合的驱动文件。
模块化与重用 。子驱动可以被内核通用框架重用。 极低。代码难以复用,需要为每个芯片重写相似逻辑。
可维护性 。职责清晰,修改一个功能不影响其他。 。代码逻辑交织,牵一发而动全身。
内核集成 。每个子设备能被正确的内核子系统管理。 。驱动游离于标准子系统之外,形成“孤岛”。

对比 普通总线驱动 (如 I2C, SPI)

MFD本身不是一种新的总线,而是一种设计模式,它建立在现有的总线(如I2C, SPI, Platform Bus)之上。

  • 关系:一个MFD的核心父驱动本身通常是一个I2C客户端驱动或SPI设备驱动。它使用I2C/SPI总线与物理芯片通信。
  • 区别:普通I2C/SPI驱动只关心它所驱动的那个设备本身的功能。而MFD父驱动的主要职责不是实现具体功能,而是作为**“设备发现者”和“资源代理”,它在软件层面创建出多个新的平台设备(Platform Devices)**,这些新创建的平台设备再由它们各自的驱动来处理。

可以这样理解:MFD父驱动是一个将一个物理设备地址(如一个I2E从设备地址)映射为多个逻辑平台设备地址的转换器。

drivers/mfd/syscon.c 系统控制器驱动程序 (Syscon) 用于管理共享寄存器映射的通用框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了解决在片上系统(SoC)设计中一个极为普遍的模式所带来的驱动开发问题:多个独立的外设驱动需要访问一个共享的、单一的、连续的物理寄存器区域。这个共享的区域通常被称为“系统控制器”(System Controller, or Syscon)、“顶层寄存器”或“配置寄存器”等。

syscon框架出现之前,处理这种情况的方式通常是:

  1. 编写一个定制的MFD驱动:为每个特定的SoC编写一个MFD父驱动,由它来映射整个共享寄存器区,然后向子驱动提供一个自定义的API来访问这些寄存器。这导致了大量功能相似但代码无法重用的驱动程序。
  2. 在每个驱动中单独ioremap:不同的驱动各自去映射(ioremap)同一个物理内存区域。这违反了内核中一个物理资源只应被一个驱动管理的原则,并且效率低下,容易出错。

syscon的诞生就是为了提供一个**通用的、数据驱动的(Data-driven)**解决方案,来替代上述的重复性工作。它本身是一个简单的、通用的MFD驱动,其唯一目标就是映射一个寄存器区域,并为其他驱动提供一个标准的、安全的访问接口。

它的发展经历了哪些重要的里程碑或版本迭代?

syscon的演进与Linux内核中另外两个关键组件紧密相连:Regmap API设备树(Device Tree)

  • Regmap的出现regmap API提供了一个硬件访问的抽象层,它可以处理寄存器访问的细节(如I/O是内存映射还是通过I2C/SPI总线,寄存器位宽,是否需要锁等)。syscon 正是构建在 regmap 之上的。syscon的核心就是为一个内存映射的区域创建一个regmap实例。
  • 设备树的普及:设备树成为syscon机制的完美搭档。开发者可以在设备树中简单地描述系统控制器的物理地址和大小,并给它一个compatible = "syscon";属性。然后,其他需要访问这个区域的设备节点,可以通过一个句柄(phandle)指向这个syscon节点。这使得整个关系可以被静态描述,而无需编写任何平台相关的代码。
  • 成为标准实践:由于其极大的便利性,syscon迅速成为ARM、MIPS、RISC-V等几乎所有使用设备树的SoC平台的标准实践,用于管理如引脚复用(Pinmux)、复位控制、时钟控制等功能的寄存器。

目前该技术的社区活跃度和主流应用情况如何?

syscon是一个非常成熟、稳定且在嵌入式Linux内核中被极度广泛使用的框架。它不是一个边缘功能,而是现代SoC平台驱动开发的基石之一。几乎所有新的SoC支持包(BSP)都会大量使用syscon来简化驱动开发。

核心原理与设计

它的核心工作原理是什么?

syscon的核心原理是**“注册-查找-访问”**模式,并以regmap为媒介。

  1. 注册(Registration)

    • 在设备树(Device Tree)中,一个节点被定义用来描述共享寄存器块,它包含物理基地址、大小,并被赋予一个特殊的兼容性字符串:compatible = "syscon";
    • 内核启动时,通用的syscon平台驱动(drivers/mfd/syscon.c)会匹配到这个compatible字符串并进行探测(probe)。
    • 在探测函数中,syscon驱动会映射(ioremap)这块物理内存,然后利用这块内存创建一个regmap实例。这个regmap实例包含了访问这块区域所需的所有信息(如内存指针、锁、访问函数等)。
    • syscon驱动把自己和创建好的regmap实例关联起来,并注册到系统中,作为一个可供查找的服务提供者。
  2. 查找(Lookup)

    • 另一个需要访问这块共享区域的“客户端”驱动(例如,一个Pinmux驱动)进行探测。
    • 在其设备树节点中,会有一个指向syscon节点的引用(phandle),例如 pinctrl-single,pins = <&syscon 0x24 0xf>;
    • 客户端驱动在自己的探测函数中,会调用 syscon_regmap_lookup_by_phandle() 或类似的API。这个API会根据phandle找到对应的syscon设备,并返回其在第一步中创建的那个regmap实例的句柄。
  3. 访问(Access)

    • 一旦客户端驱动获取到了regmap句柄,它就可以使用标准的regmap API(如 regmap_read, regmap_write, regmap_update_bits)来读写它所关心的寄存器。
    • 所有regmap函数的第一个参数都是这个获取到的句柄,第二个参数是寄存器的偏移地址(相对于syscon区域的基地址)。
    • 实际的内存读写操作由syscon持有的regmap实例来完成,包括必要的加锁和解锁,确保了多客户端并发访问的安全性。

它的主要优势体现在哪些方面?

  • 消除代码冗余:一个通用的syscon驱动取代了为每个SoC编写定制化MFD驱动的需要。
  • 驱动解耦:客户端驱动完全不需要知道共享寄存器的物理基地址,只需要知道自己要访问的寄存器相对于该区域的偏移即可。这使得客户端驱动(如通用的pinctrl驱动)更具可移植性。
  • 集中式资源管理:物理内存资源只被syscon驱动一次性申请和映射,符合内核资源管理原则。
  • 并发安全regmap层内置的锁机制自动处理了来自不同客户端驱动的并发访问,客户端驱动开发者无需关心加锁问题。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 微小的性能开销:通过regmap进行访问比直接调用readl/writel多了一层函数调用和可能的锁开销。因此,syscon非常适合用于访问控制寄存器,但不适合用于访问需要高吞吐量的数据寄存器(如DMA或FIFO)。
  • 设备树强依赖syscon的查找机制高度依赖设备树的phandle。在不使用设备树的旧式平台数据(platform_data)系统中,使用起来较为不便。
  • 不适用于非MMIOsyscon是为内存映射I/O(MMIO)设计的。如果共享寄存器是通过I2C或SPI总线访问的,那么应该直接使用regmap的I2C/SPI后端,而不是syscon

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

syscon是处理SoC中分散在单一寄存器文件中的各种小型硬件功能的事实标准和首选方案

  • 引脚复用(Pinmux/Pinctrl):SoC的引脚功能(如配置为GPIO、UART、I2C等)通常由一个集中的Pinctrl模块控制,其寄存器位于一个大的syscon区域内。Pinctrl驱动通过syscon获取regmap句柄来配置这些引脚。
  • 复位控制器(Reset Controller):SoC中各个外设的复位信号通常由一个全局的复位控制器管理,其寄存器也位于syscon区域。Reset驱动通过syscon来操作这些寄存器位以控制外设复位。
  • 时钟控制器(Clock Controller):与复位类似,部分简单的时钟使能和选择逻辑也可能在syscon区域中。
  • 获取系统ID或版本号:SoC的版本和ID寄存器通常放在syscon区域的某个固定偏移处,任何需要识别芯片型号的驱动都可以通过syscon来读取。

是否有不推荐使用该技术的场景?为什么?

  • 独立的、功能完整的外设:对于一个拥有自己独立、完整寄存器空间的设备(如一个UART控制器、一个I2C控制器),应该为其编写一个独立的平台驱动,并在驱动中直接ioremap它自己的资源。使用syscon是完全没有必要的。
  • 高性能数据路径:如上所述,不要用syscon来访问需要频繁、高速读写的数据FIFO或DMA描述符。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比 定制的MFD驱动

特性 syscon 框架 定制的MFD驱动
通用性 。一个驱动适用于所有SoC。 。每个SoC都需要一个特定的MFD驱动。
实现方式 数据驱动,通过设备树描述。 代码驱动,需要在C代码中定义子设备和API。
代码量 极少。开发者几乎只需修改设备树。 较多。需要编写父驱动和子驱动之间的API。
标准化 。使用标准的regmap API。 。父子驱动间的API是自定义的。

对比 regmap API

sysconregmap不是竞争关系,而是分层合作关系。

  • regmap 是一个库/API,它提供了对寄存器区域(无论是MMIO、I2C还是SPI)的抽象访问方法。它本身不关心自己是如何被发现或实例化的。
  • syscon 是一个驱动,它是一个**regmap的提供者(Provider)。它的核心职责就是实例化一个用于MMIO的regmap,并使这个实例可以被系统中的其他驱动通过设备树方便地查找到**。

可以这样理解:regmap是工具箱(提供了螺丝刀、扳手),而syscon是那个把工具箱放在一个公共、易于找到的地方(如车间入口)并告诉大家“工具在这里”的管理员。客户端驱动就是来这个公共地方拿工具去干活的工人。

syscon_register_regmap 注册 syscon 设备节点的 regmap

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
static const struct regmap_config syscon_regmap_config = {
.reg_bits = 32,
.val_bits = 32,
.reg_stride = 4,
};

static struct syscon *of_syscon_register(struct device_node *np, bool check_res)
{
struct clk *clk;
struct regmap *regmap;
void __iomem *base;
u32 reg_io_width;
int ret;
struct regmap_config syscon_config = syscon_regmap_config;
struct resource res;
struct reset_control *reset;
resource_size_t res_size;

WARN_ON(!mutex_is_locked(&syscon_list_lock));

struct syscon *syscon __free(kfree) = kzalloc(sizeof(*syscon), GFP_KERNEL);
if (!syscon)
return ERR_PTR(-ENOMEM);

if (of_address_to_resource(np, 0, &res))
return ERR_PTR(-ENOMEM);

base = of_iomap(np, 0);
if (!base)
return ERR_PTR(-ENOMEM);

/* 解析设备的 DT 节点以获取字节序规范 */
if (of_property_read_bool(np, "big-endian"))
syscon_config.val_format_endian = REGMAP_ENDIAN_BIG;
else if (of_property_read_bool(np, "little-endian"))
syscon_config.val_format_endian = REGMAP_ENDIAN_LITTLE;
else if (of_property_read_bool(np, "native-endian"))
syscon_config.val_format_endian = REGMAP_ENDIAN_NATIVE;

/*
* 在 DT 中搜索 reg-io-width 属性。如果未提供,则默认为 4 字节。如果值无效,regmap_init_mmio将返回错误,因此无需在此处检查它们。
*/
ret = of_property_read_u32(np, "reg-io-width", &reg_io_width);
if (ret)
reg_io_width = 4;

ret = of_hwspin_lock_get_id(np, 0);
if (ret > 0 || (IS_ENABLED(CONFIG_HWSPINLOCK) && ret == 0)) {
syscon_config.use_hwlock = true;
syscon_config.hwlock_id = ret;
syscon_config.hwlock_mode = HWLOCK_IRQSTATE;
} else if (ret < 0) {
switch (ret) {
case -ENOENT:
/* Ignore missing hwlock, it's optional. */
break;
default:
pr_err("Failed to retrieve valid hwlock: %d\n", ret);
fallthrough;
case -EPROBE_DEFER:
goto err_regmap;
}
}

res_size = resource_size(&res);
if (res_size < reg_io_width) {
ret = -EFAULT;
goto err_regmap;
}

syscon_config.name = kasprintf(GFP_KERNEL, "%pOFn@%pa", np, &res.start);
if (!syscon_config.name) {
ret = -ENOMEM;
goto err_regmap;
}
syscon_config.reg_stride = reg_io_width;
syscon_config.val_bits = reg_io_width * 8;
syscon_config.max_register = res_size - reg_io_width;
if (!syscon_config.max_register)
syscon_config.max_register_is_0 = true;

regmap = regmap_init_mmio(NULL, base, &syscon_config);
kfree(syscon_config.name);
if (IS_ERR(regmap)) {
pr_err("regmap init failed\n");
ret = PTR_ERR(regmap);
goto err_regmap;
}

if (check_res) {
clk = of_clk_get(np, 0);
if (IS_ERR(clk)) {
ret = PTR_ERR(clk);
/* clock is optional */
if (ret != -ENOENT)
goto err_clk;
} else {
ret = regmap_mmio_attach_clk(regmap, clk);
if (ret)
goto err_attach_clk;
}
/* 可以没有也可以不是错误 */
reset = of_reset_control_get_optional_exclusive(np, NULL);
if (IS_ERR(reset)) {
ret = PTR_ERR(reset);
goto err_attach_clk;
}

ret = reset_control_deassert(reset);
if (ret)
goto err_reset;
}

syscon->regmap = regmap;
syscon->np = np;

list_add_tail(&syscon->list, &syscon_list);

return_ptr(syscon);

err_reset:
reset_control_put(reset);
err_attach_clk:
if (!IS_ERR(clk))
clk_put(clk);
err_clk:
regmap_exit(regmap);
err_regmap:
iounmap(base);
return ERR_PTR(ret);
}

device_node_get_regmap 从设备树节点获取或创建regmap

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
static struct regmap *device_node_get_regmap(struct device_node *np,
bool create_regmap,
bool check_res)
{
struct syscon *entry, *syscon = NULL;

mutex_lock(&syscon_list_lock);

list_for_each_entry(entry, &syscon_list, list)
if (entry->np == np) {
syscon = entry;
break;
}

if (!syscon) {
if (create_regmap)
syscon = of_syscon_register(np, check_res);
else
syscon = ERR_PTR(-EINVAL);
}
mutex_unlock(&syscon_list_lock);

if (IS_ERR(syscon))
return ERR_CAST(syscon);

return syscon->regmap;
}

syscon_node_to_regmap 获取或创建指定 syscon 设备节点的 regmap

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* syscon_node_to_regmap() - 获取或创建指定 syscon 设备节点的 regmap
* @np:设备树节点
*
* 获取指定设备节点的 regmap。如果没有现有的 regmap,则如果节点是通用的 “syscon” ,则会实例化一个 regmap。此函数可以安全地用于向 of_syscon_register_regmap() 注册的 syscon。
*
* 返回:成功时 regmap ptr,失败时为负错误代码。
*/
struct regmap *syscon_node_to_regmap(struct device_node *np)
{
return device_node_get_regmap(np, of_device_is_compatible(np, "syscon"), true);
}
EXPORT_SYMBOL_GPL(syscon_node_to_regmap);

syscon_regmap_lookup_by_phandle 通过phandle查找Syscon regmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct regmap *syscon_regmap_lookup_by_phandle(struct device_node *np,
const char *property)
{
struct device_node *syscon_np;
struct regmap *regmap;

if (property)
syscon_np = of_parse_phandle(np, property, 0);
else
syscon_np = np;

if (!syscon_np)
return ERR_PTR(-ENODEV);

regmap = syscon_node_to_regmap(syscon_np);

if (property)
of_node_put(syscon_np);

return regmap;
}
EXPORT_SYMBOL_GPL(syscon_regmap_lookup_by_phandle);

drivers/mfd/stmpe.c 多功能外设驱动(Multi-Function Peripheral Driver) STMicro STMPE系列芯片的核心支持

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术,以及它所属的MFD(Multi-Function Device,多功能设备)子系统,是为了解决现代集成电路中一个常见的设计模式:将多个独立的外设功能集成到单个物理芯片上

  • 硬件集成趋势:为了降低成本、缩小电路板尺寸和功耗,芯片制造商(如STMicroelectronics)经常将多种功能(如GPIO扩展器、触摸屏控制器、键盘矩阵扫描器、ADC、PWM控制器等)集成到一个芯片中。这些功能共享同一个物理接口(如I2C或SPI总线)、同一个中断引脚和相同的电源管理。
  • 软件驱动的挑战:如果没有MFD框架,开发者将面临两种糟糕的选择:
    1. 编写一个庞大的单体驱动(Monolithic Driver):一个驱动文件实现所有功能。这会导致代码极度臃肿、逻辑混乱、难以维护,并且与Linux内核现有的各个子系统(GPIO, Input, IIO等)的解耦设计理念背道而驰。
    2. 为每个功能编写独立驱动:这会导致严重的资源冲突。所有驱动都会尝试去访问同一个I2C地址,请求同一个中断号,这在内核中是不被允许的。

stmpe.c 作为MFD驱动,完美地解决了这个问题。它充当一个“看门人”或“分发器”,自己处理与物理芯片的底层通信和资源共享,然后将芯片上的各个独立功能“暴露”为标准的、独立的虚拟设备,供其他标准的内核驱动程序使用。

它的发展经历了哪些重要的里程碑或版本迭代?

stmpe.c 驱动的发展历程是逐步支持更多STMPE系列芯片和完善框架的过程。

  • 初期支持:驱动最初可能是为支持某一个流行的芯片型号(如STMPE811)而创建的。
  • 家族化支持:随着STMPE系列芯片的扩展(如STMPE610, STMPE1601, STMPE24xx等),该驱动被重构为一个更通用的框架。它通过在探测时读取芯片的ID寄存器来识别具体的芯片型号,并根据型号加载不同的配置(如可用的功能块、寄存器地址等)。
  • 功能抽象:驱动内部对寄存器访问、中断处理等通用逻辑进行了抽象,使得为新的芯片变体添加支持变得更加容易,通常只需要定义一个新的芯片配置结构体即可。
  • 与Device Tree的集成:随着Device Tree在嵌入式Linux中的普及,该驱动也完全支持通过Device Tree来描述芯片及其子功能,使得硬件配置更加灵活。

目前该技术的社区活跃度和主流应用情况如何?

stmpe.c 和它所代表的MFD模式是非常成熟和稳定的内核技术。

  • 社区活跃度:由于该驱动支持的芯片系列已经非常成熟,因此不会有频繁的大规模功能更新。目前的维护工作主要集中在修复bug、支持新的芯片变体或进行一些代码清理和优化。
  • 主流应用:STMPE系列芯片被广泛应用于各种嵌入式系统中,特别是在空间和成本受限的场景下,例如:
    • 手持工业控制设备
    • 医疗仪器
    • POS机
    • 一些较早期的智能手机或功能手机
    • 单板计算机的扩展板

核心原理与设计

它的核心工作原理是什么?

stmpe.c 的工作原理是典型的MFD核心驱动模式:

  1. 探测与识别:当内核的I2C或SPI总线驱动发现一个与stmpe驱动匹配的设备时(通过Device Tree的compatible属性或ACPI ID),stmpe.c中的.probe()函数被调用。该函数通过总线(I2C/SPI)读取芯片的ID寄存器,以确定具体的芯片型号。
  2. 核心初始化:驱动程序初始化芯片的核心部分,例如启用时钟、配置中断控制器等。它还会创建一个共享的数据结构(struct stmpe),其中包含了访问芯片所需的所有信息,如I/O函数、锁(mutex用于保护共享的总线访问)等。
  3. 子设备实例化:这是MFD驱动最关键的一步。根据识别出的芯片型号和Device Tree中的配置,stmpe.c会动态地创建并注册多个平台设备(Platform Devices)。每个平台设备代表芯片上的一个独立功能(例如,一个名为 stmpe-gpio 的平台设备和一个名为 stmpe-touchscreen 的平台设备)。
  4. 资源注册与传递:在注册这些子设备时,stmpe.c会将共享的数据结构指针(struct stmpe)作为平台数据(platform data)传递给它们。这样,子设备的驱动程序就能通过这个指针访问到父驱动提供的I/O函数和锁。
  5. 中断处理与分发stmpe.c 会为芯片的物理中断线注册一个顶层中断处理函数。当硬件中断发生时,这个函数被触发。它会读取芯片的中断状态寄存器,以确定是哪个内部功能块(GPIO, ADC等)触发了中断。然后,它调用对应子设备驱动程序注册的中断处理函数。这个过程被称为中断多路分发(Interrupt Demultiplexing)
  6. 子驱动绑定:内核在stmpe.c注册了新的平台设备后,会去寻找能够处理这些设备的驱动。例如,drivers/gpio/gpio-stmpe.c 会绑定到stmpe-gpio设备上,drivers/input/touchscreen/stmpe-ts.c会绑定到stmpe-touchscreen设备上。这些子驱动(也称客户端驱动)的实现就变得非常纯粹,它们只需要关心自己的逻辑,通过父驱动提供的接口与硬件交互即可。

它的主要优势体现在哪些方面?

  • 模块化与代码重用:完美地将底层总线通信与上层功能逻辑分离。GPIO驱动、触摸屏驱动等都可以作为独立的、可复用的模块存在。
  • 符合Linux驱动模型:让多功能芯片与Linux的设备驱动模型无缝集成。每个功能都表现为一个标准的设备,可以独立进行电源管理、绑定和解绑。
    • 简化客户端驱动:客户端驱动(如GPIO驱动)无需关心芯片是在I2C总线上还是SPI总线上,也无需实现复杂的寄存器访问锁,这些都由stmpe.c核心驱动统一处理。
  • 集中式资源管理:中断、时钟和电源等共享资源由核心驱动集中管理,避免了冲突,并能进行更有效的控制。

它存在哪些已知的劣劣势、局限性或在特定场景下的不适用性?

  • 性能瓶颈:由于所有功能共享一条I2C/SPI总线,并且寄存器访问需要通过互斥锁进行序列化,如果多个功能(如触摸屏和GPIO)同时高频率地工作,可能会遇到性能瓶颈。
  • 单点故障:整个物理芯片是一个单点故障。如果芯片本身或stmpe.c核心驱动出现问题,所有相关功能都会失效。
  • 紧耦合:虽然软件上是模块化的,但硬件上是紧耦合的。例如,对芯片进行一次软复位会影响所有功能。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

stmpe.c 是使用STMicroelectronics STMPE系列芯片时的唯一且标准的解决方案。更广泛地说,MFD驱动模式是任何多功能集成电路的首选驱动架构。

  • 场景举例:嵌入式人机交互界面
    一个设备使用了一块STMPE811芯片来驱动一个电阻式触摸屏,并连接了几个物理按键。
    • 硬件连接:STMPE811通过I2C总线连接到主处理器。触摸屏的4根线连接到STMPE811的触摸屏输入引脚。几个按键连接到STMPE811的GPIO引脚上。
    • 软件工作流程
      1. 系统启动,stmpe.c 驱动探测到STMPE811。
      2. stmpe.c 注册两个平台设备:stmpe-tsstmpe-gpio
      3. stmpe-ts.c 驱动绑定到 stmpe-ts 设备,并注册一个输入设备(input device)来上报触摸坐标。
      4. gpio-stmpe.c 驱动绑定到 stmpe-gpio 设备,并注册一个gpio_chip,将按键配置为输入和中断。
      5. 当用户触摸屏幕或按下按键,芯片的物理中断线被触发。stmpe.c 的顶层中断处理函数被调用,它读取状态寄存器,发现是触摸屏或GPIO中断,然后调用stmpe-ts.cgpio-stmpe.c的相应处理函数,最终将事件上报给用户空间。

是否有不推荐使用该技术的场景?为什么?

该驱动本身是针对特定硬件系列的,所以不会用于其他厂商的芯片。MFD驱动模式不适用于由多个独立、分立的芯片组成的系统。如果你的系统中有一个独立的I2C GPIO扩展芯片和一个独立的I2C触摸屏控制器芯片(它们有不同的I2C地址),那么应该为它们分别编写两个独立的驱动,而不是使用MFD模式。

对比分析

请将其 与 其他相似技术 进行详细对比。

stmpe.c 所代表的MFD驱动方法,其主要对比对象是已经被淘汰的**单体驱动(Monolithic Driver)**方法。

特性 MFD驱动模式 (例如 stmpe.c) 单体驱动模式 (Monolithic)
实现方式 一个核心驱动 (mfd-core.c) + 多个客户端驱动 (client.c)。核心驱动通过mfd_add_devices()注册子设备。 一个巨大的驱动文件 (big-driver.c) 包含所有功能的实现逻辑。
内核集成 优秀。每个子功能都表现为一个标准的平台设备,完美融入内核的GPIO、Input、IIO等子系统。 。驱动内部直接调用各子系统的注册函数(如gpiochip_add_data, input_register_device),逻辑混乱,耦合度高。
代码结构 清晰、模块化。职责分明,易于阅读和维护。 复杂、混乱。所有逻辑交织在一起,难以维护和调试。
灵活性与复用 。可以很容易地通过Device Tree或Kconfig来启用/禁用某个子功能。客户端驱动(如GPIO驱动)也可以被其他MFD核心驱动复用。 。禁用某个功能可能需要大量的#ifdef宏,代码可读性差。驱动基本无法复用。
资源管理 健壮。核心驱动集中管理共享资源(锁、中断、电源),客户端驱动通过标准API访问。 易出错。共享资源的访问控制逻辑散布在整个大文件中,容易引入竞态条件等bug。
典型代表 内核drivers/mfd/目录下的所有驱动,如 stmpe.c, wm8350.c等。 在现代内核中已基本绝迹,是早期内核或一些质量较差的板级支持包(BSP)中的常见做法。

STMPE 低层硬件访问接口

此代码片段是核心stmpe驱动程序的”硬件抽象层”(Hardware Abstraction Layer, HAL)。它定义了一系列函数, 用于实现对STMPE芯片所有寄存器的底层读写操作。其核心原理是将高层的、有特定意图的请求(如”使能GPIO模块”、”读取寄存器0x10”)转换成底层的、与物理总线(I2C或SPI)相关的具体通信操作

这个HAL的设计体现了Linux内核驱动中两个非常重要的设计模式:

  1. 锁保护的API封装: 代码中存在两套函数: 一套是公开的API(如 stmpe_reg_read), 另一套是内部使用的、以__开头的版本(如 __stmpe_reg_read)。公开API的唯一职责就是获取一个互斥锁(mutex), 调用内部函数, 然后释放锁。这种”锁包装”模式确保了所有对芯片的访问都是线程安全的。即使在STM32H750这样的单核系统上, 由于Linux内核是抢占式的, 一个任务在执行多步操作(如读-修改-写)的过程中, 可能会被另一个任务或中断抢占, 锁可以防止这种并发访问导致的数据损坏。

  2. 通过函数指针实现硬件无关性: 内部函数并不直接操作I2C或SPI总线。__stmpe_reg_read调用stmpe->ci->read_byte(...), __stmpe_enable调用stmpe->variant->enable(...)。这里的ci(Communication Interface)和variant都是在驱动探测时根据具体的总线类型和芯片型号初始化的函数指针表。这种设计使得核心驱动逻辑与具体的总线协议(I2C/SPI)和芯片型号(STMPE811, STMPE2401等)完全解耦, 具有极高的可移植性和可扩展性。


模块使能/禁能函数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 内部函数: 实际执行使能/禁能操作, 不带锁 */
static int __stmpe_enable(struct stmpe *stmpe, unsigned int blocks)
{
/*
* 调用 stmpe->variant->enable 指针. 这是一个多态调用.
* 具体的使能寄存器和位定义, 由特定芯片型号的 variant 结构体提供.
* 'true' 表示使能.
*/
return stmpe->variant->enable(stmpe, blocks, true);
}

/* 内部函数: 实际执行禁能操作, 不带锁 */
static int __stmpe_disable(struct stmpe *stmpe, unsigned int blocks)
{
/* 调用特定型号的 enable 函数, 'false' 表示禁能. */
return stmpe->variant->enable(stmpe, blocks, false);
}

/**
* stmpe_enable - 使能一个STMPE设备上的功能块
* @stmpe: 要操作的设备
* @blocks: 要使能的功能块的掩码 (例如 STMPE_BLOCK_GPIO)
*/
int stmpe_enable(struct stmpe *stmpe, unsigned int blocks)
{
int ret;

mutex_lock(&stmpe->lock); // 获取锁, 保护对硬件的访问
ret = __stmpe_enable(stmpe, blocks); // 调用内部函数执行实际操作
mutex_unlock(&stmpe->lock); // 释放锁

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_enable); // 导出符号, 供其他模块(如stmpe-gpio)使用

/**
* stmpe_disable - 禁能一个STMPE设备上的功能块
* @stmpe: 要操作的设备
* @blocks: 要禁能的功能块的掩码
*/
int stmpe_disable(struct stmpe *stmpe, unsigned int blocks)
{
int ret;

mutex_lock(&stmpe->lock);
ret = __stmpe_disable(stmpe, blocks);
mutex_unlock(&stmpe->lock);

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_disable);

单字节寄存器访问函数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/* 内部函数: 实际执行单字节读操作, 不带锁 */
static int __stmpe_reg_read(struct stmpe *stmpe, u8 reg)
{
int ret;

/*
* 调用 stmpe->ci->read_byte 指针.
* 'ci' (Communication Interface) 会根据探测时是I2C还是SPI,
* 指向对应的总线读函数.
*/
ret = stmpe->ci->read_byte(stmpe, reg);
if (ret < 0)
dev_err(stmpe->dev, "failed to read reg %#x: %d\n", reg, ret);

/* 打印详细的调试日志, 'vdbg'表示非常详细, 默认不显示 */
dev_vdbg(stmpe->dev, "rd: reg %#x => data %#x\n", reg, ret);

return ret;
}

/* 内部函数: 实际执行单字节写操作, 不带锁 */
static int __stmpe_reg_write(struct stmpe *stmpe, u8 reg, u8 val)
{
int ret;

dev_vdbg(stmpe->dev, "wr: reg %#x <= %#x\n", reg, val);

/* 调用通信接口的写函数 */
ret = stmpe->ci->write_byte(stmpe, reg, val);
if (ret < 0)
dev_err(stmpe->dev, "failed to write reg %#x: %d\n", reg, ret);

return ret;
}

/**
* stmpe_reg_read() - 读取一个STMPE寄存器
* @stmpe: 要读取的设备
* @reg: 要读取的寄存器地址
*/
int stmpe_reg_read(struct stmpe *stmpe, u8 reg)
{
int ret;

mutex_lock(&stmpe->lock);
ret = __stmpe_reg_read(stmpe, reg);
mutex_unlock(&stmpe->lock);

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_reg_read);

/**
* stmpe_reg_write() - 写入一个STMPE寄存器
* @stmpe: 要写入的设备
* @reg: 要写入的寄存器地址
* @val: 要写入的值
*/
int stmpe_reg_write(struct stmpe *stmpe, u8 reg, u8 val)
{
int ret;

mutex_lock(&stmpe->lock);
ret = __stmpe_reg_write(stmpe, reg, val);
mutex_unlock(&stmpe->lock);

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_reg_write);

位域操作与多字节访问函数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/* 内部函数: 安全的读-修改-写操作, 不带锁 */
static int __stmpe_set_bits(struct stmpe *stmpe, u8 reg, u8 mask, u8 val)
{
int ret;

/* 步骤1: 读 (Read) */
ret = __stmpe_reg_read(stmpe, reg);
if (ret < 0)
return ret;

/* 步骤2: 修改 (Modify) */
ret &= ~mask; /* 使用掩码的反码, 将需要修改的位清零 */
ret |= val; /* 使用按位或, 设置新的值 */

/* 步骤3: 写 (Write) */
return __stmpe_reg_write(stmpe, reg, ret);
}

/* 内部函数: 实际执行多字节读操作, 不带锁 */
static int __stmpe_block_read(struct stmpe *stmpe, u8 reg, u8 length,
u8 *values)
{
int ret = stmpe->ci->read_block(stmpe, reg, length, values);
if (ret < 0)
dev_err(stmpe->dev, "failed to read regs %#x: %d\n", reg, ret);

/* 打印调试信息, 包括dump内存内容 */
dev_vdbg(stmpe->dev, "rd: reg %#x (%d) => ret %#x\n", reg, length, ret);
stmpe_dump_bytes("stmpe rd: ", values, length);

return ret;
}

/* 内部函数: 实际执行多字节写操作, 不带锁 */
static int __stmpe_block_write(struct stmpe *stmpe, u8 reg, u8 length,
const u8 *values)
{
int ret;

dev_vdbg(stmpe->dev, "wr: regs %#x (%d)\n", reg, length);
stmpe_dump_bytes("stmpe wr: ", values, length);

ret = stmpe->ci->write_block(stmpe, reg, length, values);
if (ret < 0)
dev_err(stmpe->dev, "failed to write regs %#x: %d\n", reg, ret);

return ret;
}

/**
* stmpe_set_bits() - 设置一个STMPE寄存器中某个位域的值
* @stmpe: 要写入的设备
* @reg: 寄存器地址
* @mask: 要设置的位的掩码
* @val: 要设置的值
*/
int stmpe_set_bits(struct stmpe *stmpe, u8 reg, u8 mask, u8 val)
{
int ret;

mutex_lock(&stmpe->lock);
ret = __stmpe_set_bits(stmpe, reg, mask, val);
mutex_unlock(&stmpe->lock);

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_set_bits);

/**
* stmpe_block_read() - 读取多个STMPE寄存器
* @stmpe: 要读取的设备
* @reg: 起始寄存器地址
* @length: 要读取的寄存器数量
* @values: 存放读取结果的缓冲区
*/
int stmpe_block_read(struct stmpe *stmpe, u8 reg, u8 length, u8 *values)
{
int ret;

mutex_lock(&stmpe->lock);
ret = __stmpe_block_read(stmpe, reg, length, values);
mutex_unlock(&stmpe->lock);

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_block_read);

/**
* stmpe_block_write() - 写入多个STMPE寄存器
* @stmpe: 要写入的设备
* @reg: 起始寄存器地址
* @length: 要写入的寄存器数量
* @values: 要写入的数据
*/
int stmpe_block_write(struct stmpe *stmpe, u8 reg, u8 length,
const u8 *values)
{
int ret;

mutex_lock(&stmpe->lock);
ret = __stmpe_block_write(stmpe, reg, length, values);
mutex_unlock(&stmpe->lock);

return ret;
}
EXPORT_SYMBOL_GPL(stmpe_block_write);

stmpe_set_altfunc: 为STMPE引脚设置复用功能

此函数是核心stmpe驱动(负责I2C/SPI通信的父驱动)提供的一个关键API。它的核心作用是将一个或多个指定的GPIO引脚从其默认的IO功能, 切换到一个特定的”复用功能”(Alternate Function, AF), 例如SPI, I2C, PWM等。

这个函数是典型的嵌入式驱动对硬件寄存器进行”读-修改-写”(Read-Modify-Write)操作的范例, 其原理如下:

  1. 适应硬件差异: STMPE芯片家族有多种型号(variant), 不同型号配置复用功能的方式(需要写几个比特位、寄存器地址等)可能不同。函数首先获取特定型号的信息, 如af_bits(配置一个引脚需要几个比特), 并计算出需要读写的寄存器总数(numregs)。这使得代码具有良好的可移植性。
  2. 保证操作原子性: 整个过程被一个互斥锁(mutex_lock)保护。这至关重要, 因为配置复用功能涉及多个步骤, 必须作为一个不可分割的原子操作来执行, 以防止被其他线程(例如, 另一个正在配置不同引脚的线程)中断, 从而避免数据损坏。
  3. 读(Read): 它不是一次只读一个寄存器, 而是通过__stmpe_block_read一次性将所有相关的复用功能寄存器(AFR)的当前值读取到一个本地的内存缓存(regs数组)中。这样做效率更高, 减少了总线通信次数。
  4. 修改(Modify): 这是最核心的逻辑。
    • 它进入一个while循环, 利用__ffs(find first set bit)高效地遍历pins位掩码中每一个需要被修改的引脚。
    • 对于每一个引脚, 它会进行精确的计算, 以确定该引脚的配置位存在于本地缓存regs数组的哪个字节(regoffset)以及该字节中的哪个比特位置(pos)。
    • 它使用位操作: 首先用一个掩码(mask)将该引脚原来的配置位清零, 然后再将代表新功能block的数值(af)设置到清空的位置上。这个过程确保了只修改目标引脚的配置, 而不会影响到同一寄存器中其他引脚的配置。
  5. 写(Write): 当本地缓存regs中所有需要修改的位都更新完毕后, 函数通过__stmpe_block_write一次性将整个修改后的缓存块写回到芯片的硬件寄存器中。
  6. 资源管理: 在操作之前, 它会确保GPIO功能块是使能的(__stmpe_enable)。操作结束后, 无论成功与否, 都会通过goto outmutex_unlock来确保锁被释放。
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* stmpe_set_altfunc()- 为STMPE引脚设置复用功能
* @stmpe: 要配置的设备
* @pins: 受影响的引脚的位掩码
* @block: 要为其使能复用功能的模块 (如SPI, I2C等)
*
* @pins 中, 需要改变复用功能的引脚所对应的比特位应被设置为1.
*
* 如果GPIO模块未使能, 此函数会自动使能它以完成更改.
*/
int stmpe_set_altfunc(struct stmpe *stmpe, u32 pins, enum stmpe_block block)
{
/* 获取芯片型号特定的信息 */
struct stmpe_variant_info *variant = stmpe->variant;
/* 获取复用功能寄存器块的起始地址 */
u8 regaddr = stmpe->regs[STMPE_IDX_GPAFR_U_MSB];
/* 获取配置单个引脚所需的比特数 */
int af_bits = variant->af_bits;
/* 计算配置所有引脚总共需要多少个8位的寄存器 */
int numregs = DIV_ROUND_UP(stmpe->num_gpios * af_bits, 8);
/* 创建一个位掩码, 用于操作单个引脚的配置位 (例如, af_bits=2, mask=0b11) */
int mask = (1 << af_bits) - 1;
/* 本地缓存, 用于存放从硬件读出的寄存器值 */
u8 regs[8];
/* af: 新功能对应的数值; afperreg: 每个8位寄存器能配置几个引脚; ret: 返回值 */
int af, afperreg, ret;

/* 如果当前芯片型号不支持复用功能配置, 则什么都不做, 直接成功返回 */
if (!variant->get_altfunc)
return 0;

/* 计算每个8位寄存器能配置多少个引脚 */
afperreg = 8 / af_bits;
/* --- 原子操作开始 --- */
mutex_lock(&stmpe->lock);

/* 确保GPIO模块本身是使能的 */
ret = __stmpe_enable(stmpe, STMPE_BLOCK_GPIO);
if (ret < 0)
goto out; /* 如果失败, 跳转到结尾释放锁 */

/* --- 读-修改-写: 读阶段 --- */
/* 将整个复用功能寄存器块一次性读入本地缓存 regs */
ret = __stmpe_block_read(stmpe, regaddr, numregs, regs);
if (ret < 0)
goto out;

/* --- 读-修改-写: 修改阶段 --- */
/* 获取'block' (如SPI) 对应的需要写入寄存器的具体数值 */
af = variant->get_altfunc(stmpe, block);

/* 循环处理 pins 位掩码中所有被置位的引脚 */
while (pins) {
/* 找到需要处理的编号最小的引脚 */
int pin = __ffs(pins);
/* 计算该引脚的配置位在哪个寄存器中 (注意这里从尾部开始计算, 因为寄存器是MSB在前的) */
int regoffset = numregs - (pin / afperreg) - 1;
/* 计算该引脚的配置位在该寄存器中的起始位置 */
int pos = (pin % afperreg) * af_bits;

/* 步骤1: 清除该引脚原来的配置位 (将对应位置零) */
regs[regoffset] &= ~(mask << pos);
/* 步骤2: 设置该引脚新的配置位 */
regs[regoffset] |= af << pos;

/* 从 pins 位掩码中清除已处理完的引脚位 */
pins &= ~(1 << pin);
}

/* --- 读-修改-写: 写阶段 --- */
/* 将修改后的整个缓存块一次性写回硬件寄存器 */
ret = __stmpe_block_write(stmpe, regaddr, numregs, regs);

out:
/* --- 原子操作结束 --- */
mutex_unlock(&stmpe->lock);
return ret;
}
/* 将此函数导出, 使其对 stmpe-gpio 等其他内核模块可用 */
EXPORT_SYMBOL_GPL(stmpe_set_altfunc);

STMPE MFD核心探测:总线无关的设备初始化与子功能注册

本代码片段展示了stmpe_probe函数,它是STMPE MFD(Multi-Function Device)驱动框架的核心。此函数是总线无关的,意味着它可以被不同的总线接口驱动(如stmpe-i2c.cstmpe-spi.c)调用。其核心功能是:接收总线层传递过来的设备信息,分配并初始化代表整个STMPE芯片的主设备结构(struct stmpe),完成对芯片的硬件初始化,并最终注册该芯片所提供的所有独立功能(如GPIO、触摸屏、ADC)作为独立的“子”设备,从而将一个物理芯片暴露为系统中的多个逻辑设备。

实现原理分析

stmpe_probe函数遵循一个标准的设备驱动初始化流程,并加入了MFD框架特有的逻辑。

  1. 资源分配与配置加载:

    • 函数首先使用devm_kzalloc来分配平台数据(pdata)和核心设备结构(stmpe)的内存。devm_前缀确保了这些内存在驱动卸载时会自动释放,简化了错误处理。
    • 通过调用stmpe_of_probe和一系列of_property_read_u32函数,从设备树(Device Tree)中解析并加载平台特定的配置数据,如ADC采样参数、中断配置等。
  2. 核心结构体组装:

    • 它将来自不同来源的信息聚合到stmpe结构体中:
      • 总线信息(ci):包含设备指针、I2C/SPI客户端句柄、总线层提供的中断号以及总线读写函数指针。
      • 平台数据(pdata):主要来自设备树的配置。
      • 静态变体信息(stmpe_variant_info):根据芯片型号(partnum),从一个静态数组中查找芯片的能力、寄存器布局、GPIO数量等固定信息。
  3. 硬件资源初始化:

    • 电源管理: 通过devm_regulator_get_optional获取并使能VCC和VIO电源。这是确保芯片正常工作的第一步。
    • 中断处理: 实现了一套灵活的中断发现逻辑。它优先尝试从设备树中获取一个专用的”irq” GPIO,并将其转换为IRQ号。如果失败,则回退到使用总线层传递过来的IRQ号。它还处理了无中断(轮询)模式,并能自动检测中断触发类型。
  4. 芯片硬件初始化:

    • 在所有软件结构和资源都准备就绪后,调用stmpe_chip_init(stmpe)。这个函数会首次通过ci中提供的总线读写函数与STMPE芯片进行通信,读取芯片ID,并对其进行复位和基本配置,使其进入一个已知的可用状态。
  5. 中断服务注册:

    • 如果确定了有效的中断号,它会调用stmpe_irq_init来配置芯片的中断控制器,并使用devm_request_threaded_irq将核心中断处理函数stmpe_irq注册到内核的中断管理系统中。
  6. MFD子设备注册:

    • 这是MFD驱动的标志性步骤。调用stmpe_devices_init(stmpe),该函数会遍历stmpe->variant中定义的该芯片型号所支持的所有功能单元(cells)。对于每个功能单元,它会创建一个mfd_cell结构,并调用mfd_add_devices。内核的MFD核心层会根据这些mfd_cell自动创建和注册新的平台设备(platform_device)。例如,一个STMPE811芯片会因此注册一个stmpe-gpio平台设备和一个stmpe-ts(触摸屏)平台设备。这些新注册的平台设备随后会被它们各自的驱动(如gpio-stmpe.c)所探测和接管。

代码分析

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/* 从特定于客户端的probe例程中调用 */
int stmpe_probe(struct stmpe_client_info *ci, enum stmpe_partnum partnum)
{
struct stmpe_platform_data *pdata;
struct device_node *np = ci->dev->of_node;
struct stmpe *stmpe;
struct gpio_desc *irq_gpio;
int ret;
u32 val;

// 分配平台数据结构体内存,使用devm_系列函数以实现资源托管。
pdata = devm_kzalloc(ci->dev, sizeof(*pdata), GFP_KERNEL);
if (!pdata)
return -ENOMEM;

// 从设备树节点(np)解析平台数据。
stmpe_of_probe(pdata, np);

// 如果设备树中没有 "interrupts" 属性,则将中断号设为无效。
if (!of_property_present(np, "interrupts"))
ci->irq = -1;

// 分配STMPE核心设备结构体内存。
stmpe = devm_kzalloc(ci->dev, sizeof(struct stmpe), GFP_KERNEL);
if (!stmpe)
return -ENOMEM;

// 初始化用于保护中断和设备访问的互斥锁。
mutex_init(&stmpe->irq_lock);
mutex_init(&stmpe->lock);

// 从设备树读取可选的ADC配置参数。
if (!of_property_read_u32(np, "st,sample-time", &val))
stmpe->sample_time = val;
if (!of_property_read_u32(np, "st,mod-12b", &val))
stmpe->mod_12b = val;
if (!of_property_read_u32(np, "st,ref-sel", &val))
stmpe->ref_sel = val;
if (!of_property_read_u32(np, "st,adc-freq", &val))
stmpe->adc_freq = val;

// 填充stmpe核心结构体。
stmpe->dev = ci->dev;
stmpe->client = ci->client; // I2C/SPI 客户端句柄
stmpe->pdata = pdata;
stmpe->ci = ci; // 总线客户端信息 (包含读写函数)
stmpe->partnum = partnum; // 芯片型号
stmpe->variant = stmpe_variant_info[partnum]; // 获取芯片的静态变体信息
stmpe->regs = stmpe->variant->regs; // 寄存器地址表
stmpe->num_gpios = stmpe->variant->num_gpios; // GPIO数量

// 获取并使能VCC和VIO电源。
stmpe->vcc = devm_regulator_get_optional(ci->dev, "vcc");
if (!IS_ERR(stmpe->vcc)) {
ret = regulator_enable(stmpe->vcc);
if (ret)
dev_warn(ci->dev, "failed to enable VCC supply\n");
}
stmpe->vio = devm_regulator_get_optional(ci->dev, "vio");
if (!IS_ERR(stmpe->vio)) {
ret = regulator_enable(stmpe->vio);
if (ret)
dev_warn(ci->dev, "failed to enable VIO supply\n");
}
// 将stmpe结构体指针保存为设备的私有数据,方便后续通过dev_get_drvdata获取。
dev_set_drvdata(stmpe->dev, stmpe);

// 如果总线驱动提供了初始化回调,则调用它。
if (ci->init)
ci->init(stmpe);

// 优先尝试从设备树获取 "irq" GPIO作为中断源。
irq_gpio = devm_gpiod_get_optional(ci->dev, "irq", GPIOD_ASIS);
ret = PTR_ERR_OR_ZERO(irq_gpio);
if (ret) {
dev_err(stmpe->dev, "failed to request IRQ GPIO: %d\n", ret);
return ret;
}

if (irq_gpio) { // 如果成功获取到GPIO
stmpe->irq = gpiod_to_irq(irq_gpio); // 将GPIO描述符转换为IRQ号
// 根据GPIO的有效电平设置中断触发类型。
pdata->irq_trigger = gpiod_is_active_low(irq_gpio) ?
IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH;
} else { // 如果没有提供irq-gpio
stmpe->irq = ci->irq; // 回退到使用总线层提供的IRQ号
pdata->irq_trigger = IRQF_TRIGGER_NONE; // 触发类型未知
}

if (stmpe->irq < 0) { // 如果最终没有有效的中断号
// 切换到无中断模式的变体信息(如果支持的话)。
dev_info(stmpe->dev,
"%s configured in no-irq mode by platform data\n",
stmpe->variant->name);
if (!stmpe_noirq_variant_info[stmpe->partnum]) {
dev_err(stmpe->dev,
"%s does not support no-irq mode!\n",
stmpe->variant->name);
return -ENODEV;
}
stmpe->variant = stmpe_noirq_variant_info[stmpe->partnum];
} else if (pdata->irq_trigger == IRQF_TRIGGER_NONE) {
// 如果中断触发类型未知,则尝试从内核中断子系统查询。
pdata->irq_trigger = irq_get_trigger_type(stmpe->irq);
}

// 初始化STMPE芯片硬件。
ret = stmpe_chip_init(stmpe);
if (ret)
return ret;

if (stmpe->irq >= 0) { // 如果有有效的中断
// 初始化芯片的中断控制器。
ret = stmpe_irq_init(stmpe, np);
if (ret)
return ret;

// 向内核请求一个线程化的中断处理程序。
ret = devm_request_threaded_irq(ci->dev, stmpe->irq, NULL,
stmpe_irq, pdata->irq_trigger | IRQF_ONESHOT,
"stmpe", stmpe);
if (ret) {
dev_err(stmpe->dev, "failed to request IRQ: %d\n",
ret);
return ret;
}
}

// 初始化并注册所有子设备 (MFD核心步骤)。
ret = stmpe_devices_init(stmpe);
if (!ret)
return 0; // 成功

// 如果注册子设备失败,则进行清理。
dev_err(stmpe->dev, "failed to add children\n");
mfd_remove_devices(stmpe->dev);

return ret;
}

drivers/mfd/stmpe-i2c.c MFD I2C客户端(MFD I2C Client) STMPE多功能设备I2C总线接口

本代码文件是一个I2C客户端驱动,它充当了Linux I2C总线核心与通用的STMPE MFD(Multi-Function Device,多功能设备)核心驱动之间的“粘合层”。其核心功能并非直接控制STMPE芯片(如触摸屏、GPIO等),而是为STMPE核心驱动提供通过I2C总线进行数据读写的能力。当系统中一个STMPE设备通过I2C总线被探测到时,此驱动负责实例化设备,并移交控制权给STMPE核心驱动进行后续的功能初始化。

实现原理分析

该驱动遵循标准的Linux I2C驱动模型。其实现原理可以概括为以下几点:

  1. 通信接口封装:驱动首先定义了一组静态函数(i2c_reg_read, i2c_reg_write, i2c_block_read, i2c_block_write)。这些函数将通用的I2C SMBus(System Management Bus)调用进行了一层简单的封装,目的是为了匹配STMPE核心驱动所要求的函数指针接口格式。
  2. 接口结构体填充:这些封装好的函数指针被统一填充到一个stmpe_client_info结构体实例i2c_ci中。这个结构体是本驱动与STMPE核心驱动进行交互的契约,它将总线相关的操作(如I2C读写)与设备特定的、总线无关的逻辑解耦。
  3. 设备探测与匹配:驱动定义了stmpe_i2c_driver结构体。当I2C总线控制器驱动探测到物理设备时,I2C核心会使用此结构中定义的设备ID表(stmpe_i2c_id)或设备树(Device Tree)匹配表(stmpe_of_match)来寻找匹配的驱动。
  4. Probe函数执行:一旦匹配成功,I2C核心就会调用本驱动的stmpe_i2c_probe函数。此函数是驱动的核心入口点,其主要工作是:
    • i2c_client结构体中获取设备信息,如中断号(IRQ)、设备节点等。
    • 将这些信息连同预先准备好的I2C通信函数一起,填充到stmpe_client_info结构体中。
    • 根据设备ID或设备树的compatible属性确定STMPE芯片的具体型号(partnum)。
    • 调用通用的stmpe_probe函数,将填充好的stmpe_client_info作为参数传入。至此,控制权正式移交给STMPE MFD核心驱动,由它来完成芯片的识别、初始化以及子设备(如GPIO控制器、触摸屏控制器)的注册。
  5. 移除与清理:当设备被移除时,stmpe_i2c_remove函数被调用,它会通过dev_get_drvdata获取到核心驱动分配的stmpe结构体,并调用stmpe_remove来执行通用的清理流程。

I2C总线访问函数封装

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// i2c_reg_read: 通过I2C读取STMPE单个寄存器。
// @stmpe: 指向STMPE核心设备结构体的指针。
// @reg: 要读取的寄存器地址。
// 返回值: 成功则为读取到的8位数据,失败则为负数错误码。
static int i2c_reg_read(struct stmpe *stmpe, u8 reg)
{
struct i2c_client *i2c = stmpe->client;

// 调用I2C核心提供的SMBus单字节数据读取函数。
return i2c_smbus_read_byte_data(i2c, reg);
}

// i2c_reg_write: 通过I2C向STMPE单个寄存器写入一个字节。
// @stmpe: 指向STMPE核心设备结构体的指针。
// @reg: 要写入的寄存器地址。
// @val: 要写入的8位数据。
// 返回值: 成功则为0,失败则为负数错误码。
static int i2c_reg_write(struct stmpe *stmpe, u8 reg, u8 val)
{
struct i2c_client *i2c = stmpe->client;

// 调用I2C核心提供的SMBus单字节数据写入函数。
return i2c_smbus_write_byte_data(i2c, reg, val);
}

// i2c_block_read: 通过I2C从STMPE读取一个数据块。
// @stmpe: 指向STMPE核心设备结构体的指针。
// @reg: 起始寄存器地址。
// @length: 要读取的字节数。
// @values: 存放读取数据的缓冲区指针。
// 返回值: 成功则为读取的字节数,失败则为负数错误码。
static int i2c_block_read(struct stmpe *stmpe, u8 reg, u8 length, u8 *values)
{
struct i2c_client *i2c = stmpe->client;

// 调用I2C核心提供的SMBus块数据读取函数。
return i2c_smbus_read_i2c_block_data(i2c, reg, length, values);
}

// i2c_block_write: 通过I2C向STMPE写入一个数据块。
// @stmpe: 指向STMPE核心设备结构体的指针。
// @reg: 起始寄存器地址。
// @length: 要写入的字节数。
// @values: 存放待写入数据的缓冲区指针。
// 返回值: 成功则为0,失败则为负数错误码。
static int i2c_block_write(struct stmpe *stmpe, u8 reg, u8 length,
const u8 *values)
{
struct i2c_client *i2c = stmpe->client;

// 调用I2C核心提供的SMBus块数据写入函数。
return i2c_smbus_write_i2c_block_data(i2c, reg, length, values);
}

// i2c_ci: 全局的STMPE客户端信息结构体。
// 用于向STMPE核心驱动传递I2C总线相关的操作函数和数据。
static struct stmpe_client_info i2c_ci = {
.read_byte = i2c_reg_read, // 注册读字节函数
.write_byte = i2c_reg_write, // 注册写字节函数
.read_block = i2c_block_read, // 注册读数据块函数
.write_block = i2c_block_write, // 注册写数据块函数
};

设备匹配表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// stmpe_of_match: 用于设备树的设备匹配表。
// 当设备树节点的 "compatible" 属性与此表中的条目匹配时,该驱动将被加载。
static const struct of_device_id stmpe_of_match[] = {
{ .compatible = "st,stmpe610", .data = (void *)STMPE610, },
{ .compatible = "st,stmpe801", .data = (void *)STMPE801, },
{ .compatible = "st,stmpe811", .data = (void *)STMPE811, },
{ .compatible = "st,stmpe1600", .data = (void *)STMPE1600, },
{ .compatible = "st,stmpe1601", .data = (void *)STMPE1601, },
{ .compatible = "st,stmpe1801", .data = (void *)STMPE1801, },
{ .compatible = "st,stmpe2401", .data = (void *)STMPE2401, },
{ .compatible = "st,stmpe2403", .data = (void *)STMPE2403, },
{}, // 数组结束标志
};
// MODULE_DEVICE_TABLE: 将设备树匹配表导出到用户空间,工具(如depmod)可用它来分析模块依赖。
MODULE_DEVICE_TABLE(of, stmpe_of_match);

核心Probe与Remove函数

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
28
29
30
31
32
33
34
35
36
37
38
39
// stmpe_i2c_probe: I2C驱动的探测函数,当I2C核心匹配到设备时调用。
// @i2c: 指向本次探测到的I2C客户端设备的指针。
// 返回值: 成功则为0,失败则为负数错误码。
static int
stmpe_i2c_probe(struct i2c_client *i2c)
{
const struct i2c_device_id *id = i2c_client_get_device_id(i2c);
enum stmpe_partnum partnum;
const struct of_device_id *of_id;

// 填充传递给核心驱动的客户端信息结构体
i2c_ci.data = (void *)id; // 传递I2C设备ID
i2c_ci.irq = i2c->irq; // 传递中断号
i2c_ci.client = i2c; // 传递I2C客户端指针
i2c_ci.dev = &i2c->dev; // 传递设备结构体指针

// 尝试通过设备树进行匹配
of_id = of_match_device(stmpe_of_match, &i2c->dev);
if (!of_id) {
// 如果设备树匹配失败(例如在没有设备树的旧系统上),则回退到使用I2C ID。
dev_info(&i2c->dev, "matching on node name, compatible is preferred\n");
partnum = id->driver_data; // 从I2C ID表中获取芯片型号
} else
partnum = (uintptr_t)of_id->data; // 从设备树匹配项中获取芯片型号

// 调用通用的STMPE核心探测函数,将I2C相关信息和芯片型号传递过去。
return stmpe_probe(&i2c_ci, partnum);
}

// stmpe_i2c_remove: I2C驱动的移除函数,当设备被移除时调用。
// @i2c: 指向被移除的I2C客户端设备的指针。
static void stmpe_i2c_remove(struct i2c_client *i2c)
{
// 从设备私有数据中获取由核心驱动分配的stmpe结构体指针。
struct stmpe *stmpe = dev_get_drvdata(&i2c->dev);

// 调用通用的STMPE核心移除函数以执行清理工作。
stmpe_remove(stmpe);
}

I2C驱动结构体定义

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
// stmpe_i2c_id: 用于非设备树系统的I2C设备ID表。
static const struct i2c_device_id stmpe_i2c_id[] = {
{ "stmpe610", STMPE610 },
{ "stmpe801", STMPE801 },
{ "stmpe811", STMPE811 },
{ "stmpe1600", STMPE1600 },
{ "stmpe1601", STMPE1601 },
{ "stmpe1801", STMPE1801 },
{ "stmpe2401", STMPE2401 },
{ "stmpe2403", STMPE2403 },
{ } // 数组结束标志
};
// MODULE_DEVICE_TABLE: 导出I2C设备ID表。
MODULE_DEVICE_TABLE(i2c, stmpe_i2c_id);

// stmpe_i2c_driver: I2C驱动的核心结构体。
// 它将驱动的名称、回调函数、设备匹配表等信息注册到Linux I2C子系统。
static struct i2c_driver stmpe_i2c_driver = {
.driver = {
.name = "stmpe-i2c", // 驱动名称
.pm = pm_sleep_ptr(&stmpe_dev_pm_ops), // 电源管理回调函数
.of_match_table = stmpe_of_match, // 设备树匹配表
},
.probe = stmpe_i2c_probe, // 探测函数
.remove = stmpe_i2c_remove, // 移除函数
.id_table = stmpe_i2c_id, // I2C设备ID表
};

模块初始化与退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// stmpe_init: 模块加载时的初始化函数。
static int __init stmpe_init(void)
{
// 向I2C核心注册本驱动。
return i2c_add_driver(&stmpe_i2c_driver);
}
// subsys_initcall: 宏,用于指定本初始化函数在子系统初始化阶段被调用。
subsys_initcall(stmpe_init);

// stmpe_exit: 模块卸载时的退出函数。
static void __exit stmpe_exit(void)
{
// 从I2C核心注销本驱动。
i2c_del_driver(&stmpe_i2c_driver);
}
module_exit(stmpe_exit);

MODULE_DESCRIPTION("STMPE MFD I2C Interface Driver");
MODULE_AUTHOR("Rabin Vincent <rabin.vincent@stericsson.com>");