越来越密集的设备集成导致了一种由几个其他设备或 IP 组成的设备,可以实现特定的功能。 随着该设备的出现,Linux 内核中出现了一个新的子系统。 这些是MFD,代表多功能设备。 这些设备在物理上被视为独立设备,但从软件的角度来看,这些设备是以父子关系表示的,其中子设备就是子设备。
虽然一些基于 I2C 和 SPI 的设备/子设备在添加到系统之前可能需要一些攻击或配置,但也有一些基于 MMIO 的设备/子设备不需要任何配置/攻击,因为它们只需要在子设备之间共享主设备的寄存器区域。 然后引入了 Simple-mfd helper 来处理零 conf/hacks 子设备注册,并引入了 syscon 来与其他设备共享设备的内存区。 由于 regmap 负责处理 MMIO 寄存器和对内存的托管锁定(也称为同步)访问,因此在 regmap 之上构建 syscon 是自然而然的选择。 为了熟悉 MFD 子系统,在本章中,我们将从 MFD 的介绍开始,在这里您将了解其数据结构和 API,然后我们将查看设备树绑定,以便向内核描述这些设备。 最后,我们将讨论 syscon 并介绍用于零 conf/hacks 子设备的 Simple-mfd 驱动。
本章将介绍以下主题:
- 介绍 MFD 和 syscon API 和数据结构
- MFD 设备的设备树绑定
- 了解 syscon 和 Simple-mfd
为了充分利用本章,您需要以下内容:
- C 语言编程技巧
- 熟悉 Linux 设备驱动模型
- Linux 内核 v4.19.X 源代码,可从https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags获得
在深入研究 syscon 框架及其 API 之前,我们将介绍 MFD。 有个外围设备或硬件块通过它们嵌入其中的子设备公开多个功能,并由内核中的单独子系统处理。 也就是说,子设备是所谓多功能设备中的专用实体,负责特定的任务,并通过芯片寄存器映射中的一组减少的寄存器进行管理。 ADP5520
是 MFD 设备的典型示例,因为它包含背光、键盘、LED 和 GPIO 控制器。 然后,其中的每一个都被视为一个子设备,正如您所看到的,每个子设备都属于不同的子系统。 在include/linux/mfd/core.h
中定义并在drivers/mfd/mfd-core.c
中实现的 MFD 子系统是为处理这些设备而创建的,允许以下功能:
- 向多个子系统注册同一设备
- 多路复用总线和寄存器访问,因为子设备之间可能共享一些寄存器
- 处理 IRQ 和时钟
在本节中,我们将研究 DIALOG-Semiconductor 中的da9055
设备的驱动,该驱动位于内核源代码树中的drivers/mfd/da9055-core.c
中。 该设备的数据表可在https://www.dialog-semiconductor.com/sites/default/files/da9055-00-ids3a_20120710.pdf上找到。
在大多数情况下,MFD 设备驱动由两部分组成:
-
应在
drivers/mfd
中托管的核心驱动,负责主要初始化并将每个子设备注册为系统上的平台设备(及其平台数据)。 该驱动应该为子设备驱动提供公共服务。 这些服务包括寄存器访问、控制和共享中断管理。 当一个子系统的平台驱动器被实例化时,内核初始化芯片(其可以由平台数据指定)。 可以支持内置于单个内核映像中的多个相同类型的块设备。 这要归功于平台数据机制。 内核中特定于平台的数据抽象机制用于将配置传递给内核,而辅助驱动使其能够支持多个相同类型的块设备。 -
The subdevice driver, which is responsible for handling a specific subdevice registered earlier by the core driver. These drivers are located in their respective subsystem directories. Each peripheral (subsystem device) has a limited view of the device, which is implicitly reduced to the specific set of resources that the peripheral requires in order to function correctly.
重要音符
本章中的子设备概念不应与第 7 章、解密 V4L2 和视频捕获设备驱动中的同名概念混淆,后者略有不同,其中子设备也代表视频管道中的实体。
子设备在 MFD 子系统中由struct mfd_cell
结构的实例表示,您可以将其称为单元。 单元格用于描述子设备。 核心驱动器必须提供与给定外围设备中的子器件一样多的单元阵列。 MFD 子系统将使用阵列中每个结构中注册的信息为每个子设备创建平台设备,以及与每个子设备相关联的平台数据。 在struct mfd_cell
结构中,您可以指定更高级的内容,比如子设备使用的资源和挂起-恢复操作(从子设备的驱动调用)。 此结构如下所示,出于简单原因删除了一些字段:
/*
* This struct describes the MFD part ("cell").
* After registration the copy of this structure will
* become the platform data of the resulting platform_device
*/
struct mfd_cell {
const char *name;
int id;
[...]
int (*suspend)(struct platform_device *dev);
int (*resume)(struct platform_device *dev);
/* platform data passed to the sub devices drivers */
void *platform_data;
size_t pdata_size;
/* Device Tree compatible string */
const char *of_compatible;
/* Matches ACPI */
const struct mfd_cell_acpi_match *acpi_match;
/*
* These resources can be specified relative to the
* parent device. For accessing hardware, you should
* use resources from the platform dev
*/
int num_resources;
const struct resource *resources;
[...]
};
重要音符
创建的新平台设备将具有作为其平台数据的单元结构。 然后可以通过pdev->mfd_cell->platform_data
访问实际平台数据。 驱动还可以使用mfd_get_cell()
来检索与平台设备const struct mfd_cell *cell = mfd_get_cell(pdev);
相对应的 MFD 单元。
此结构的每个成员的功能不言而喻。 不过,以下内容提供了更多详细信息。
元素.resources
是一个数组,表示特定于子设备(也是平台设备)的资源,以及数组中的条目数.num_resources
。 这些都是使用platform_data
定义的,您可能希望为它们命名以便于检索。 以下是一个 MFD 驱动的示例,该驱动的原始核心源文件为drivers/mfd/da9055-core.c
:
static struct resource da9055_rtc_resource[] = {
{
.name = „ALM",
.start = DA9055_IRQ_ALARM,
.end = DA9055_IRQ_ALARM,
.flags = IORESOURCE_IRQ,
},
{
.name = "TICK",
.start = DA9055_IRQ_TICK,
.end = DA9055_IRQ_TICK,
.flags = IORESOURCE_IRQ,
},
};
static const struct mfd_cell da9055_devs[] = {
...
{
.of_compatible = "dlg,da9055-rtc",
.name = "da9055-rtc",
.resources = da9055_rtc_resource,
.num_resources = ARRAY_SIZE(da9055_rtc_resource),
},
...
};
以下示例显示如何从子设备驱动检索资源,在本例中,该子设备驱动在drivers/rtc/rtc-da9055.c
中实现:
static int da9055_rtc_probe(struct platform_device *pdev)
{
[...]
alm_irq = platform_get_irq_byname(pdev, "ALM");
if (alm_irq < 0)
return alm_irq;
ret = devm_request_threaded_irq(&pdev->dev, alm_irq, NULL,
da9055_rtc_alm_irq,
IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
"ALM", rtc);
if (ret != 0)
dev_err(rtc->da9055->dev,
"irq registration failed: %d\n", ret);
[...]
}
实际上,您应该使用platform_get_resource()
、platform_get_resource_byname()
、platform_get_irq()
和platform_get_irq_byname()
来检索资源。
使用.of_compatible
时,该函数必须是 MFD 的子级(参见 MFD 设备的设备树绑定部分)。 您应该静态填充此结构的数组,其中包含与设备上的子设备一样多的条目:
static struct resource da9055_rtc_resource[] = {
{
.name = „ALM",
.start = DA9055_IRQ_ALARM,
.end = DA9055_IRQ_ALARM,
.flags = IORESOURCE_IRQ,
},
[...]
};
[...]
static const struct mfd_cell da9055_devs[] = {
{
.of_compatible = "dlg,da9055-gpio",
.name = "da9055-gpio",
},
{
.of_compatible = "dlg,da9055-regulator",
.name = "da9055-regulator",
.id = 1,
},
[...]
{
.of_compatible = "dlg,da9055-rtc",
.name = "da9055-rtc",
.resources = da9055_rtc_resource,
.num_resources = ARRAY_SIZE(da9055_rtc_resource),
},
{
.of_compatible = "dlg,da9055-watchdog",
.name = "da9055-watchdog",
},
};
填充struct mfd_cell
数组后,必须将其传递给devm_mfd_add_devices()
函数,如下所示:
int devm_mfd_add_devices(
struct device *dev,
int id,
const struct mfd_cell *cells,
int n_devs,
struct resource *mem_base,
int irq_base,
struct irq_domain *domain)
此方法的参数解释如下:
-
dev
是 MFD 芯片的通用结构器件结构。 它将用于设置子设备的父设备。 -
id
:因为子设备是作为平台设备创建的,所以应该给它们一个 ID,这个字段应该设置为PLATFORM_DEVID_AUTO
,以便自动分配 ID,在这种情况下,忽略相应小区的mfd_cell.id
。 否则,您应该使用PLATFORM_DEVID_NONE
。 -
cells
是指向描述子设备的struct mfd_cell
结构列表(实际上是一个数组)的指针。 -
n_dev
是阵列中用于创建平台设备的struct mfd_cell
条目数。 要创建与阵列中的单元格一样多的平台设备,应使用ARRAY_SIZE()
宏。 -
mem_base
:如果不是NULL
,则其.start
字段将被用作前面提到的数组中每个 MFD 单元的IORESOURCE_MEM
类型的每个资源的基础。 下面是显示这一点的mfd_add_device()
函数的摘录:for (r = 0; r < cell->num_resources; r++) { res[r].name = cell->resources[r].name; res[r].flags = cell->resources[r].flags; /* Find out base to use */ if ((cell->resources[r].flags & IORESOURCE_MEM) && mem_base) { res[r].parent = mem_base; res[r].start = mem_base->start + cell->resources[r].start; res[r].end = mem_base->start + cell->resources[r].end; } else if (cell->resources[r].flags & IORESOURCE_IRQ) { [...]
-
irq_base
:如果设置了域,则忽略此参数。 否则,它的行为与mem_base
类似,但对于类型为IORESOURCE_IRQ
的每个资源。 下面是显示这一点的mfd_add_device()
函数的摘录:} else if (cell->resources[r].flags & IORESOURCE_IRQ) { if (domain) { /* Unable to create mappings for IRQ ranges. */ WARN_ON(cell->resources[r].start != cell->resources[r].end); res[r].start = res[r].end = irq_create_mapping( domain,cell->resources[r].start); } else { res[r].start = irq_base + cell->resources[r].start; res[r].end = irq_base + cell->resources[r].end; } } else { [...]
-
domain
:对于同时充当其子设备的 IRQ 控制器的 MFD 芯片,此参数将用作 IRQ 域,以创建这些子设备的 IRQ 映射。 它是这样工作的:对于每个单元中类型为IORESOURCE_IRQ
的每个资源r
,MFD 核心将创建一个相同类型的新资源res
(实际上是一个 IRQ 资源,其res.start
和res.end
字段被设置为该域中对应于初始资源的.start
字段的 IRQ 映射:res[r].start = res[r].end = irq_create_mapping(domain, cell->resources[r].start);
)。 然后,新的 IRQ 资源被分配给当前小区的平台设备,并对应于其 virQ。 请看前面的参数描述中的摘录。 请注意,此参数可以是NULL
。
现在让我们看看如何将这些内容与da9055
MFD 驱动的摘录结合在一起:
#define DA9055_IRQ_NONKEY_MASK 0x01
#define DA9055_IRQ_ALM_MASK 0x02
#define DA9055_IRQ_TICK_MASK 0x04
#define DA9055_IRQ_ADC_MASK 0x08
#define DA9055_IRQ_BUCK_ILIM_MASK 0x08
/*
* PMIC IRQ
*/
#define DA9055_IRQ_ALARM 0x01
#define DA9055_IRQ_TICK 0x02
#define DA9055_IRQ_NONKEY 0x00
#define DA9055_IRQ_REGULATOR 0x0B
#define DA9055_IRQ_HWMON 0x03
struct da9055 {
struct regmap *regmap;
struct regmap_irq_chip_data *irq_data;
struct device *dev;
struct i2c_client *i2c_client;
int irq_base;
int chip_irq;
};
在前面的摘录中,驱动定义了一些常量以及私有数据结构,当您阅读代码时,它们的含义将变得清晰。 之后,为寄存器映射内核定义 IRQ,如下所示:
static const struct regmap_irq da9055_irqs[] = {
[DA9055_IRQ_NONKEY] = {
.reg_offset = 0,
.mask = DA9055_IRQ_NONKEY_MASK,
},
[DA9055_IRQ_ALARM] = {
.reg_offset = 0,
.mask = DA9055_IRQ_ALM_MASK,
},
[DA9055_IRQ_TICK] = {
.reg_offset = 0,
.mask = DA9055_IRQ_TICK_MASK,
},
[DA9055_IRQ_HWMON] = {
.reg_offset = 0,
.mask = DA9055_IRQ_ADC_MASK,
},
[DA9055_IRQ_REGULATOR] = {
.reg_offset = 1,
.mask = DA9055_IRQ_BUCK_ILIM_MASK,
},
};
static const struct regmap_irq_chip da9055_regmap_irq_chip = {
.name = "da9055_irq",
.status_base = DA9055_REG_EVENT_A,
.mask_base = DA9055_REG_IRQ_MASK_A,
.ack_base = DA9055_REG_EVENT_A,
.num_regs = 3,
.irqs = da9055_irqs,
.num_irqs = ARRAY_SIZE(da9055_irqs),
};
在前面的摘录中,da9055_irqs
是类型为regmap_irq
的元素数组,它描述了一个泛型 regmap IRQ。 它被分配给类型为regmap_irq_chip
的da9055_regmap_irq_chip
,表示 regmap IRQ 芯片。 两者都是 regmap IRQ 数据结构集的一部分。 最后,实现了probe
方法,如下所示:
static int da9055_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret;
struct da9055_pdata *pdata = dev_get_platdata(da9055->dev);
uint8_t clear_events[3] = {0xFF, 0xFF, 0xFF};
[...]
ret =
devm_regmap_add_irq_chip(
&client->dev, da9055->regmap,
da9055->chip_irq, IRQF_TRIGGER_LOW | IRQF_ONESHOT,
da9055->irq_base, &da9055_regmap_irq_chip,
&da9055->irq_data);
if (ret < 0)
return ret;
da9055->irq_base = regmap_irq_chip_get_base( da9055->irq_data);
ret = devm_mfd_add_devices(
da9055->dev, -1,
da9055_devs, ARRAY_SIZE(da9055_devs),
NULL, da9055->irq_base,
regmap_irq_get_domain(da9055->irq_data));
if (ret)
goto err;
[...]
}
在前面的探测方法中,将da9055_regmap_irq_chip
(前面定义的)作为参数提供给regmap_add_irq_chip()
,以便将有效的 regmap IRQ 控制器添加到 IRQ 核心。 此函数在成功时返回O
。 此外,它还通过最后一个参数返回一个完全配置的regmap_irq_chip_data
结构,稍后可以将其用作控制器的运行时数据结构。 此regmap_irq_chip_data
结构将包含与先前添加的 IRQ 控制器相关联的 IRQ 域。 该 IRQ 域最终作为参数与 MFD 单元阵列及其根据单元数量的大小一起被给出给devm_mfd_add_devices()
。
重要音符
请注意,devm_mfd_add_devices()
实际上是mfd_add_devices()
的资源管理版本,它具有以下函数调用序列:
mfd_add_devices()-> mfd_add_device()-> platform_device_alloc() -> platform_device_add_data() -> platform_device_add_resources() -> platform_device_add()
有些 I2C 芯片本身和内部子器件具有不同的 I2C 地址。 这样的 I2C 子设备不能作为 I2C 客户端探测,因为 MFD 内核仅在给定 MFD 单元的情况下实例化平台设备。 此问题可通过以下方式解决:
- 给定子设备的 I2C 地址和 MFD 芯片的适配器,创建一个虚拟 I2C 客户端。 这实际上对应于管理 MFD 设备的适配器(总线)。 这可以使用
i2c_new_dummy()
来实现。 应保存返回的 I2C 客户端以供以后使用-例如,使用i2c_unregister_device()
,它应在卸载模块时调用。 - 如果子设备需要自己的 regmap,则此 regmap 必须构建在其虚拟 I2C 客户端之上。
- 仅存储 I2C 客户端(用于以后删除)或将 regmap 存储在可分配给底层平台设备的私有数据结构中。
为了总结前面的步骤,让我们来介绍一个真正的 MFD 设备 max8925 的驱动(它主要是一个电源管理 IC,但也由一大组子设备组成)。 我们的代码是原始代码的摘要(仅处理两个子设备),为了可读性而修改了函数名。 也就是说,可以在内核源代码树的drivers/mfd/max8925-i2c.c
中找到原始驱动。
让我们跳到摘录,从上下文数据结构定义开始,如下所示:
struct priv_chip {
struct device *dev;
struct regmap *regmap;
/* chip client for the parent chip, let's say the PMIC */
struct i2c_client *client;
/* chip client for subdevice 1, let's say an rtc */
struct i2c_client *subdev1_client;
/* chip client for subdevice 2 let's say a gpio controller */
struct i2c_client *subdev2_client;
struct regmap *subdev1_regmap;
struct regmap *subdev2_regmap;
unsigned short subdev1_addr; /* subdevice 1 I2C address */
unsigned short subdev2_addr; /* subdevice 2 I2C address */
};
const struct regmap_config chip_regmap_config = {
[...]
};
const struct regmap_config subdev_rtc_regmap_config = {
[...]
};
const struct regmap_config subdev_gpiochip_regmap_config = {
[...]
};
在前面的摘录中,驱动定义了上下文数据结构struct priv_chip
,其中包含子设备 regmap,然后初始化 MFD 设备 regmap 配置以及子设备自己的配置。 然后,定义probe
方法,如下所示:
static int my_mfd_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct priv_chip *chip;
struct regmap *map;
chip = devm_kzalloc(&client->dev,
sizeof(struct priv_chip), GFP_KERNEL);
map = devm_regmap_init_i2c(client, &chip_regmap_config);
chip->client = client;
chip->regmap = map;
chip->dev = &client->dev;
dev_set_drvdata(chip->dev, chip);
i2c_set_clientdata(chip->client, chip);
chip->subdev1_addr = client->addr + 1;
chip->subdev2_addr = client->addr + 2;
/* subdevice 1, let's say an RTC */
chip->subdev1_client = i2c_new_dummy(client->adapter,
chip->subdev1_addr);
chip->subdev1_regmap =
devm_regmap_init_i2c(chip->subdev1_client,
&subdev_rtc_regmap_config);
i2c_set_clientdata(chip->subdev1_client, chip);
/* subdevice 2, let's say a gpio controller */
chip->subdev2_client = i2c_new_dummy(client->adapter,
chip->subdev2_addr);
chip->subdev2_regmap =
devm_regmap_init_i2c(chip->subdev2_client,
&subdev_gpiochip_regmap_config);
i2c_set_clientdata(chip->subdev2_client, chip);
/* mfd_add_devices() is called somewhere */
[...]
}
出于可读性的考虑,前面的摘录省略了错误检查。 此外,以下代码显示如何删除虚拟 I2C 客户端:
static int my_mfd_remove(struct i2c_client *client)
{
struct priv_chip *chip = i2c_get_clientdata(client);
mfd_remove_devices(chip->dev);
i2c_unregister_device(chip->subdev1_client);
i2c_unregister_device(chip->subdev2_client);
return 0;
}
最后,下面的简化代码展示了子设备驱动如何获取指向 MFD 驱动中设置的任一 regmap 数据结构的指针:
static int subdev_rtc_probe(struct platform_device *pdev)
{
struct priv_chip *chip = dev_get_drvdata(pdev->dev.parent);
struct regmap *rtc_regmap = chip->subdev1_regmap;
int ret;
[...]
if (!rtc_regmap) {
dev_err(&pdev->dev, "no regmap!\n");
ret = -EINVAL;
goto out;
}
[...]
}
虽然我们已经掌握了开发 MFD 设备驱动所需的大部分知识,但为了对 MFD 设备有更好的(即不是硬编码的)描述,有必要将其与设备树集成。 这就是我们将在下一节中讨论的内容。
尽管我们拥有编写自己的 MFD 驱动所需的工具和输入,但在设备树中定义底层 MFD 设备的描述是很重要的,因为这会让 MFD 内核知道我们的 MFD 设备是由什么组成的,以及如何处理它。 此外,设备树仍然是声明设备的正确位置,无论它们是否为 MFD。 请记住,其目的仅用于描述系统上的设备。 由于子设备是其内置到的 MFD 设备的子设备(存在父子绑定),因此最好将这些子设备节点声明在其父节点下,如下例所示。 此外,子设备使用的资源有时是父设备资源的一部分。 因此,它强化了将子设备节点放在主设备节点下面的想法。 在每个子设备节点中,Compatible 属性应该同时匹配子设备的cell.of_compatible
字段和子设备的platform_driver.of_match_table
数组中的一个.compatible
字符串条目,或者同时匹配子设备的cell.name
字段和子设备的platform_driver.name
字段:
重要音符
子设备的cell.of_compatible
和cell.name
字段是在 MFD 核心驱动中的子设备的mfd_cell
结构中声明的字段。
&i2c3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c3>;
clock-frequency = <400000>;
status = "okay";
pmic0: da9062@58 {
compatible = "dlg,da9062";
reg = <0x58>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pmic>;
interrupt-parent = <&gpio6>;
interrupts = <11 IRQ_TYPE_LEVEL_LOW>;
interrupt-controller;
regulators {
DA9062_BUCK1: buck1 {
regulator-name = "BUCK1";
regulator-min-microvolt = <300000>;
regulator-max-microvolt = <1570000>;
regulator-min-microamp = <500000>;
regulator-max-microamp = <2000000>;
regulator-boot-on;
};
DA9062_LDO1: ldo1 {
regulator-name = "LDO_1";
regulator-min-microvolt = <900000>;
regulator-max-microvolt = <3600000>;
regulator-boot-on;
};
};
da9062_rtc: rtc {
compatible = "dlg,da9062-rtc";
};
watchdog {
compatible = "dlg,da9062-watchdog";
};
onkey {
compatible = "dlg,da9062-onkey";
dlg,disable-key-power;
};
};
};
在前面的设备树示例中,父节点(da9062
,一个PMIC,电源管理集成电路)在其总线节点下声明。 此 PMIC 的调节输出被声明为 PMIC 节点的子节点。 在这里,再一次,每件事都是正常的。 现在,每个子设备都被声明为其父(实际上是da9092
)节点下的独立设备节点。 让我们将重点放在子设备的compatible
属性上,并以onkey
为例。 此节点的 MFD 信元在 MFD 核心驱动(源文件为drivers/mfd/da9063-core.c
)中声明,如下所示:
static struct resource da9063_onkey_resources[] = {
{
.name = "ONKEY",
.start = DA9063_IRQ_ONKEY,
.end = DA9063_IRQ_ONKEY,
.flags = IORESOURCE_IRQ,d
},
};
static const struct mfd_cell da9062_devs[] = {
[...]
{
.name = "da9062-onkey",
.num_resources = ARRAY_SIZE(da9062_onkey_resources),
.resources = da9062_onkey_resources,
.of_compatible = "dlg,da9062-onkey",
},
};
现在,此onekey
平台驱动结构在驱动(其源文件为drivers/input/misc/da9063_onkey.c
)中声明(及其.of_match_table
条目),如下所示:
static const struct of_device_id da9063_compatible_reg_id_table[] = {
{ .compatible = "dlg,da9063-onkey", .data = &da9063_regs },
{ .compatible = "dlg,da9062-onkey", .data = &da9062_regs },
{ },
};
MODULE_DEVICE_TABLE(of, da9063_compatible_reg_id_table);
[...]
static struct platform_driver da9063_onkey_driver = {
.probe = da9063_onkey_probe,
.driver = {
.name = DA9063_DRVNAME_ONKEY,
.of_match_table = da9063_compatible_reg_id_table,
},
};
您可以看到两个compatible
字符串都与设备节点中节点的compatible
字符串匹配。 另一方面,我们可以看到,相同的平台驱动可以用于两个或多个(子)设备。 那么,使用名称匹配将会令人困惑。 这就是为什么要使用设备树进行声明,使用compatible
字符串进行匹配的原因。 到目前为止,我们已经了解了 MFD 子系统如何处理设备,反之亦然。 在下一节中,我们将把这些概念扩展到 syscon 和 Simple-mfd,这两个框架可以帮助开发 MFD 驱动。
Syscon代表代表系统控制器。 SoC 有时有一组 MMIO 寄存器,专门用于与特定 IP 无关的各种功能。 显然,这不可能有一个功能驱动,因为这些寄存器既不具有代表性,也没有足够的凝聚力来表示特定类型的设备。 Syscon 驱动处理这种情况。 Syscon 允许其他节点通过 regmap 机制访问此寄存器空间。 它实际上只是一组 regmap 的包装器 API。 当您请求访问 syscon 时,如果 regmap 尚不存在,则会创建该 regmap。
使用 syscon API 所需的标头为<linux/mfd/syscon.h>
。 由于此 API 基于 regmap,因此还必须包含<linux/regmap.h>
。 Syscon API 在内核源代码树的drivers/mfd/syscon.c
中实现。 它的主要数据结构是struct syscon
,尽管此结构不能直接使用:
struct syscon {
struct device_node *np;
struct regmap *regmap;
struct list_head list;
};
在前面的结构中,np
是指向充当 syscon 的节点的指针。 它还用于设备节点的 syscon 查找。 regmap
是与该 syscon 关联的 regmap,list
用于实现内核链表机制,用于将系统中的所有 syscon 链接到drivers/mfd/syscon.c
中定义的系统范围列表syscon_list
。 这种链表机制允许遍历整个 syscon 列表,要么按节点匹配,要么按 regmap 匹配。
通过将"syscon"
添加到应充当 Syscon 的设备节点的兼容字符串列表中,以独占方式从设备树中声明 Syscons。 在早期引导期间,根据默认 regmap 配置syscon_regmap_config
,其兼容字符串列表中包含syscon
的每个节点将其reg
内存区域 IO 映射并绑定到 MMIO regmap,如下所示:
static const struct regmap_config syscon_regmap_config = {
.reg_bits = 32,
.val_bits = 32,
.reg_stride = 4,
};
然后,创建的 syscon 被添加到 syscon 框架范围的syscon_list
,受syscon_list_slock
自旋锁保护,如下所示:
static DEFINE_SPINLOCK(syscon_list_slock);
static LIST_HEAD(syscon_list);
static struct syscon *of_syscon_register(struct device_node *np)
{
struct syscon *syscon;
struct regmap *regmap;
void __iomem *base;
[...]
if (!of_device_is_compatible(np, "syscon"))
return ERR_PTR(-EINVAL);
[...]
spin_lock(&syscon_list_slock);
list_add_tail(&syscon->list, &syscon_list);
spin_unlock(&syscon_list_slock);
return syscon;
}
Syscon 绑定需要以下强制属性:
compatible
:此属性值应为"syscon"
。reg
:这是可以从 syscon 访问的寄存器区域。
以下是可选属性,用于破坏默认的syscon_regmap_config
regmap 配置:
reg-io-width
:应在设备上执行的 IO 访问的大小(或宽度,以字节为单位hwlocks
:对硬件自旋锁提供程序节点的 phandle 的引用
下面的中显示了一个示例,摘录自内核文档,其完整版本可在内核源代码的Documentation/devicetree/bindings/mfd/syscon.txt
中找到:
gpr: iomuxc-gpr@20e0000 {
compatible = "fsl,imx6q-iomuxc-gpr", "syscon";
reg = <0x020e0000 0x38>;
hwlocks = <&hwlock1 1>;
};
hwlock1: hwspinlock@40500000 {
...
reg = <0x40500000 0x1000>;
#hwlock-cells = <1>;
};
在设备树中,您可以通过三种不同的方式引用 syscon 节点:通过 phandle(在此驱动的设备节点中指定)、通过其路径,或者通过使用特定的兼容值搜索它,之后驱动可以询问节点(或此 regmap 的相关 OS 驱动)以确定寄存器的位置,最后直接访问寄存器。 您可以使用以下 syscon API 之一来获取指向与给定 syscon 节点关联的 regmap 的指针:
struct regmap * syscon_node_to_regmap (struct device_node *np);
struct regmap * syscon_regmap_lookup_by_compatible(const char *s);
struct regmap * syscon_regmap_lookup_by_pdevname(const char *s);
struct regmap * syscon_regmap_lookup_by_phandle(
struct device_node *np,
const char *property);
前面的接口有如下说明:
syscon_regmap_lookup_by_compatible()
:在给定 syscon 设备节点的一个兼容字符串的情况下,此函数返回关联的 regmap,如果该 regmap 尚不存在,则在返回它之前创建一个 regmap。syscon_node_to_regmap()
:给定 syscon 设备节点作为参数,此函数返回关联的 regmap,如果该 regmap 尚不存在,则在返回它之前创建一个 regmap。syscon_regmap_lookup_by_phandle()
:给定一个包含 syscon 节点标识符的 phandle 属性,此函数返回与此 syscon 节点对应的 regmap。
在展示使用上述 API 的示例之前,我们先介绍一下以下平台设备节点,我们将为其编写probe
函数。 为了更好地理解syscon_node_to_regmap()
,让我们将此节点声明为前一个gpr
节点的子节点:
gpr: iomuxc-gpr@20e0000 {
compatible = "fsl,imx6q-iomuxc-gpr", "syscon";
reg = <0x020e0000 0x38>;
my_pdev: my_pdev {
compatible = "company,regmap-sample";
regmap-phandle = <&gpr>;
[...]
};
};
现在已经定义了设备树节点,我们可以关注驱动的代码,实现方式如下,并使用前面列举的函数:
static struct regmap *by_node_regmap;
static struct regmap *by_compat_regmap;
static struct regmap *by_pdevname_regmap;
static struct regmap *by_phandle_regmap;
static int my_pdev_regmap_sample(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct device_node *syscon_node;
[...]
syscon_node = of_get_parent(np);
if (!syscon_node)
return -ENODEV;
/* If we have a pointer to the syscon device node, we use it */
by_node_regmap = syscon_node_to_regmap(syscon_node);
of_node_put(syscon_node);
if (IS_ERR(by_node_regmap)) {
pr_err("%s: could not find regmap by node\n", __func__);
return PTR_ERR(by_node_regmap);
}
/* or we have one of the compatible string of the syscon node */
by_compat_regmap =
syscon_regmap_lookup_by_compatible("fsl, imx6q-iomuxc-gpr");
if (IS_ERR(by_compat_regmap)) {
pr_err("%s: could not find regmap by compatible\n", __func__);
return PTR_ERR(by_compat_regmap);
}
/* Or a phandle property pointing to the syscon device node
*/
by_phandle_regmap =
syscon_regmap_lookup_by_phandle(np, "fsl,tempmon");
if (IS_ERR(map)) {
pr_err("%s: could not find regmap by phandle\n", __func__);
return PTR_ERR(by_phandle_regmap);
}
/*
* It is the extrem and rare case fallback
* As of Linux kernel v4.18, there is only one driver
* using this, drivers/tty/serial/clps711x.c
*/
char pdev_syscon_name[9];
int index = pdev->id;
sprintf(syscon_name, "syscon.%i", index + 1);
by_pdevname_regmap =
syscon_regmap_lookup_by_pdevname(syscon_name);
if (IS_ERR(by_pdevname_regmap)) {
pr_err("%s: could not find regmap by pdevname\n", __func__);
return PTR_ERR(by_pdevname_regmap);
}
[...]
return 0;
}
在前面的示例中,如果我们认为syscon_name
包含gpr
设备的平台设备名称,那么by_node_regmap
、by_compat_regmap
、by_pdevname_regmap
和by_phandle_regmap
变量都将指向相同的 syscon regmap。 然而,这里的目的只是解释概念。 my_pdev
可能是gpr
的同级(或任何关系)节点。 在这里使用它作为它的子级是为了理解概念和代码,并根据具体情况说明这两个 API 都有自己的位置。 现在我们已经熟悉了 syscon 框架,让我们看看如何将其与 simple-mfd 一起使用。
对于基于 MMIO 的 MFD 设备,在将其添加到系统之前可能不需要配置子设备。 由于此配置是从 MFD 核心驱动内部完成的,因此此 MFD 核心驱动的唯一目标将是使用平台子设备填充系统。 由于存在大量基于 MMIO 的 MFD 设备,因此会有大量冗余代码。 简单的 MFD 是一个简单的 DT 绑定,它解决了这个问题。
当simple-mfd
字符串被添加到给定设备节点(这里被视为 MFD 设备)的兼容字符串列表中时,它将使用for_each_child_of_node()
迭代器使成为该 MFD 设备的所有子节点的(开放固件)核心派生子设备(实际上是子设备)的**。 Simple-mfd 作为 Simple-bus 的别名在drivers/of/platform.c
中实现,其文档位于内核源码树的Documentation/devicetree/bindings/mfd/mfd.txt
中。**
与 syscon 结合使用以创建 regmap,它有助于避免编写 MFD 驱动,并且开发人员可以将精力放在编写子设备驱动上。 以下是一个示例:
snvs: snvs@20cc000 {
compatible = "fsl,sec-v4.0-mon", "syscon", "simple-mfd";
reg = <0x020cc000 0x4000>;
snvs_rtc: snvs-rtc-lp {
compatible = "fsl,sec-v4.0-mon-rtc-lp";
regmap = <&snvs>;
offset = <0x34>;
interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;
};
snvs_poweroff: snvs-poweroff {
compatible = "syscon-poweroff";
regmap = <&snvs>;
offset = <0x38>;
value = <0x60>;
mask = <0x60>;
status = "disabled";
};
snvs_pwrkey: snvs-powerkey {
compatible = "fsl,sec-v4.0-pwrkey";
regmap = <&snvs>;
interrupts = <GIC_SPI 4 IRQ_TYPE_LEVEL_HIGH>;
linux,keycode = <KEY_POWER>;
wakeup-source;
};
[...]
};
在前面的设备树摘录中,snvs
是主要设备。 它由电源控制子设备(由主设备寄存器区域中的寄存器子设备表示)、rtc
子设备以及电源键等组成。 完整的定义可以在arch/arm/boot/dts/imx6qdl.dtsi
中找到,它是 i.MX6 芯片系列的 SoC 供应商dtsi
。 通过抓取(搜索)它们的compatible
属性的内容,可以在内核源代码中找到相应的驱动。 总而言之,对于snvs
节点中的每个子节点,MFD 核心将创建一个相应的设备及其 regmap,该 regmap 将对应于主设备的内存区中的它们的内存区。
本节介绍了在 MMIO 设备上轻松进行 MFD 驱动开发的方法。 虽然 SPI/I2C 设备不属于这一类别,但它覆盖了几乎 95%的基于 MMIO 的 MFD 设备。
本章介绍 MFD 设备,以及 syscon 和 regmap API。 在这里,我们讨论了 MFD 设备如何工作,以及如何将 regmap 深度嵌入到 syscon 中。 读完本章后,我们可以假设您能够开发支持 regmap 的 IRQ 控制器,以及设计和使用 syscon 在设备之间共享寄存器区域。 下一章将讨论通用时钟框架,以及该框架是如何组织的、它的实现、如何使用它,以及如何添加您自己的时钟。