本章介绍 Linux 内核寄存器映射抽象层,并展示如何简化 I/O 操作并将其委托给 regmap 子系统。 处理设备,无论它们是内置在 SoC(内存映射 I/O,也称为MMIO)中,还是位于 I2C/SPI 总线上,都包括访问(读取/修改/更新)寄存器。 Regmap 变得必要,因为很多设备驱动都是开放编码的,它们的寄存器访问例程都是开放的。 Regmap代表寄存器映射。 它主要针对ALSA SoC(ASOC)开发,以消除编解码器驱动中冗余的开放编码 SPI/I2C 寄存器访问例程。 最初,regmap 提供了一组用于读/写非内存映射 I/O 的 API(例如,I2C 和 SPI 读/写)。 从那以后,MMIO regmap 进行了升级,现在我们可以使用 regmap 访问 MMIO。
目前,该框架抽象了 I2C、SPI 和 MMIO 寄存器访问,不仅在必要时处理锁定,还管理寄存器缓存以及寄存器的可读写。 它还处理 IRQ 芯片和 IRQ。 本章将讨论 regmap,并解释如何使用它来抽象 I2C、SPI 和 MMIO 设备的寄存器访问。 我们还将介绍如何使用 regmap 来管理 IRQ 和 IRQ 控制器。
本章将介绍以下主题:
- Regmap 及其数据结构简介:I2C、SPI 和 MMIO
- Regmap 和 IRQ 管理
- Regmap IRQ API 和数据结构
为了在阅读本章时轻松自如,您需要以下内容:
- 良好的 C 编程技能
- 熟悉设备树的概念
- Linux 内核 v4.19.X 源代码,可从https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags获得
Regmap 是 Linux 内核提供的一种抽象寄存器访问机制,主要针对 SPI、I2C 和内存映射寄存器。
此框架中的 API 与总线无关,并在幕后处理底层配置。 也就是说,该框架中的主要数据结构是struct regmap_config
,在内核源码树的include/linux/regmap.h
中定义如下:
struct regmap_config {
const char *name;
int reg_bits;
int reg_stride;
int pad_bits;
int val_bits;
bool (*writeable_reg)(struct device *dev, unsigned int reg);
bool (*readable_reg)(struct device *dev, unsigned int reg);
bool (*volatile_reg)(struct device *dev, unsigned int reg);
bool (*precious_reg)(struct device *dev, unsigned int reg);
int (*reg_read)(void *context, unsigned int reg, unsigned int *val);
int (*reg_write)(void *context, unsigned int reg, unsigned int val);
bool disable_locking;
regmap_lock lock;
regmap_unlock unlock;
void *lock_arg;
bool fast_io;
unsigned int max_register;
const struct regmap_access_table *wr_table;
const struct regmap_access_table *rd_table;
const struct regmap_access_table *volatile_table;
const struct regmap_access_table *precious_table;
const struct reg_default *reg_defaults;
unsigned int num_reg_defaults;
unsigned long read_flag_mask;
unsigned long write_flag_mask;
enum regcache_type cache_type;
bool use_single_rw;
bool can_multi_write;
};
为简单起见,此结构中的某些字段已被删除,本章不对其进行讨论。 只要正确完成struct regmap_config
,用户就可以忽略底层总线机制。 让我们介绍一下这个数据结构中的字段:
-
reg_bits
以位为单位表示寄存器的大小。 换句话说,它是寄存器地址中的位数。 -
reg_stride
是寄存器地址的步长。 如果寄存器地址是该值的倍数,则该寄存器地址有效。 如果设置为0
,将使用值1
,这意味着任何地址都是有效的。 对不是该值倍数的地址的任何读/写操作都将返回-EINVAL
。 -
pad_bits
是寄存器和值之间的填充位数。 这是格式化时将寄存器的值向左移位的位数。 -
val_bits
:这表示用于存储寄存器值的位数。 这是必填字段。 -
writeable_reg
:如果提供,此可选回调将在每次 regmap 写操作时调用,以检查给定地址是否可写。 如果此函数在给定给 regmap 写入事务的地址上返回false
,则该事务将返回-EIO
。 以下摘录显示了如何实现此回调:static bool foo_writeable_register(struct device *dev, unsigned int reg) { switch (reg) { case 0x30 ... 0x38: case 0x40 ... 0x45: case 0x50 ... 0x57: case 0x60 ... 0x6e: case 0xb0 ... 0xb2: return true; default: return false; } }
-
readable_reg
:这与writeable_reg
相同,但用于寄存器读取操作。 -
volatile_reg
:这是一个可选的回调函数,如果提供,则每次需要通过 regmap 缓存读取或写入寄存器时都会调用该回调函数。 如果寄存器是易失性的(寄存器值无法缓存),则函数应返回true
。 然后对寄存器执行直接读/写。 如果返回false
,则表示寄存器是可缓存的。 在这种情况下,高速缓存将用于读取操作,而在写入操作的情况下将写入高速缓存。 以下是随机选择假寄存器地址的示例:static bool volatile_reg(struct device *dev, unsigned int reg) { switch (reg) { case 0x30: case 0x31: [...] case 0xb3: return false; case 0xb4: return true; default: if ((reg >= 0xb5) && (reg <= 0xcc)) return false; [...] break; } return true; }
-
reg_read
:如果您的设备需要个特殊的 hack来进行读取操作,您可以提供一个自定义的读取回调,并使该字段指向该回调,以便不使用标准的 regmap 读取函数,而使用此回调。 也就是说,大多数设备都不需要这个。 -
reg_write
:这与reg_read
相同,但用于写入操作。 -
disable_locking
:这表示是否应该使用lock
/unlock
回调。 如果为false
,则不使用锁定机构。 这意味着该 regmap 要么受到外部手段的保护,要么保证不会被多个线程访问。 -
lock
/unlock
:这些是可选的锁定/解锁回调,覆盖 regmap 的默认锁定/解锁函数。 它们基于自旋锁定或互斥,这取决于访问底层设备是否可以休眠。 -
lock_arg
:这是lock
/unlock
函数的唯一参数(如果未覆盖常规锁定/解锁函数,则将忽略该参数)。 -
fast_io
:这表示寄存器的 I/O 很快。 如果设置,regmap 将使用自旋锁而不是互斥锁来执行锁定。 如果使用自定义锁定/解锁(这里不讨论)函数(参见内核源代码中struct regmap_config
的lock
/unlock
字段),则忽略此字段。 它应仅用于“no bus
”情况(MMIO 设备),而不适用于访问可能休眠的 I2C、SPI 或类似总线等慢速总线。 -
wr_table
:这是regmap_access_table
类型的writeable_reg()
回调的替代方法,后者是一个包含yes_range
和no_range
字段的结构,这两个字段都是指向struct regmap_range
的指针。 属于yes_range
条目的任何寄存器都被认为是可写的,如果它属于no_range
或未在yes_range
中指定,则被认为是不可写的。 -
rd_table
:这与wr_table
相同,但适用于任何读取操作。 -
volatile_table
:可以提供volatile_table
,而不是volatile_reg
。 原理与wr_table
和rd_table
相同,只是用于缓存机制。 -
max_register
:这是可选的;它指定不允许操作的最大有效寄存器地址。 -
reg_defaults
是类型为reg_default
的元素数组,其中每个元素都是一对{reg, value}
,表示给定寄存器的上电复位值。 它与高速缓存一起使用,以便读取此阵列中存在且自上电重置以来未写入的地址将返回此阵列中的默认寄存器值,而不会在设备上执行任何读取事务。 IIO 设备驱动就是一个这样的例子,您可以在https://elixir.bootlin.com/linux/v4.19/source/drivers/iio/light/apds9960.c上找到有关它的更多信息。 -
use_single_rw
:这是一个布尔值,如果设置,将指示 regmap 将设备上的任何批量写入或读取操作转换为一系列单个写入或读取操作。 这对于不支持批量读取和/或写入操作的设备很有用。 -
can_multi_write
:这仅针对写入操作。 如果设置,则表示该设备支持批量写入操作的多写模式。 如果为空,则多写请求将被拆分为单独的写操作。 -
num_reg_defaults
:这是reg_defaults
中的元素数。 -
read_flag_mask
:这是执行读取时在寄存器的最高字节中设置的掩码。 通常,在 SPI 或 I2C 中,写入或读取将在最高字节中设置最高位,以区分写入和读取操作。 -
write_flag_mask
:这是执行写入时要在寄存器的最高字节中设置的掩码。 -
cache_type
:这是实际的缓存类型,可以是REGCACHE_NONE
、REGCACHE_RBTREE
、REGCACHE_COMPRESSED
或REGCACHE_FLAT
。
初始化 regmap 非常简单,只需根据设备所在的总线调用以下函数之一:
struct regmap * devm_regmap_init_i2c(
struct i2c_client *client,
struct regmap_config *config)
struct regmap * devm_regmap_init_spi(
struct spi_device *spi,
const struct regmap_config);
struct regmap * devm_regmap_init_mmio(
struct device *dev,
void __iomem *regs,
const struct regmap_config *config)
#define devm_regmap_init_spmi_base(dev, config) \
__regmap_lockdep_wrapper(__devm_regmap_init_spmi_base, \
#config, dev, config)
#define devm_regmap_init_w1(w1_dev, config) \
__regmap_lockdep_wrapper(__devm_regmap_init_w1, #config, \
w1_dev, config)
在前面的原型中,如果出现错误,返回值将是指向struct regmap
或ERR_PTR()
的有效指针。 设备管理代码将自动释放 regmap。 regs
是指向内存映射 IO 区域的指针(由devm_ioremap_resource()
或任何ioremap*
系列函数返回)。 dev
是将与交互的设备(类型为struct device
)。 以下示例摘录了内核源代码中的drivers/mfd/sun4i-gpadc.c
:
struct sun4i_gpadc_dev {
struct device *dev;
struct regmap *regmap;
struct regmap_irq_chip_data *regmap_irqc;
void __iomem *base;
};
static const struct regmap_config sun4i_gpadc_regmap_config = {
.reg_bits = 32,
.val_bits = 32,
.reg_stride = 4,
.fast_io = true,
};
static int sun4i_gpadc_probe(struct platform_device *pdev)
{
struct sun4i_gpadc_dev *dev;
struct resource *mem;
[...]
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
dev->base = devm_ioremap_resource(&pdev->dev, mem);
if (IS_ERR(dev->base))
return PTR_ERR(dev->base);
dev->dev = &pdev->dev;
dev_set_drvdata(dev->dev, dev);
dev->regmap = devm_regmap_init_mmio(dev->dev, dev->base,
&sun4i_gpadc_regmap_config);
if (IS_ERR(dev->regmap)) {
ret = PTR_ERR(dev->regmap);
dev_err(&pdev->dev, "failed to init regmap: %d\n", ret);
return ret;
}
[...]
此摘录显示了如何创建 regmap。 虽然这段摘录是面向 MMIO 的,但是的概念对于其他类型是相同的。 对于基于 SPI 或 I2C 的 regmap,我们将分别使用devm_regmap_init_spi()
或devm_regmap_init_i2c()
,而不是使用devm_regmap_init_MMIO()
。
访问设备寄存器有两个主要函数。 它们是regmap_write()
和regmap_read()
,它们负责锁定和抽象底层总线:
int regmap_write(struct regmap *map,
unsigned int reg,
unsigned int val);
int regmap_read(struct regmap *map,
unsigned int reg,
unsigned int *val);
在前面两个函数中,第一个参数map
是初始化期间返回的 regmap 结构。 reg
是写入/读取数据的寄存器地址。 val
是写入操作中要写入的数据,或者是读取操作中的读取值。 以下是这些接口的详细说明:
-
regmap_write
is used to write data to the device. The following are the steps performed by this function:1)首先,检查
reg
是否与regmap_config.reg_stride
对齐。 如果不是,则返回-EINVAL
,函数失败。2)然后根据
fast_io
、lock
和unlock
字段获取锁。 如果提供了lock
回调,它将用于获取锁。 否则,regmap 内核将使用其内部默认锁定函数,根据是否设置了fast_io
而使用自旋锁或互斥体。 接下来,regmap 内核对传递的寄存器地址执行一些健全性检查,如下所示:--如果设置了
max_register
,它将检查该寄存器的地址是否小于max_register
。 如果地址不小于max_register
,则regmap_write()
失败,返回-EIO
(无效 I/O)错误代码--然后,如果设置了
writeable_reg
回调,则使用寄存器作为参数调用该回调。 如果此回调返回false
,则regmap_write()
失败,返回-EIO
。 如果未设置writeable_reg
但设置了wr_table
,则 regmap 内核将检查寄存器地址是否位于no_range
内。 如果是,则regmap_write()
失败并返回-EIO
。 如果没有,regmap 内核将检查寄存器地址是否位于yes_range
中。 如果不存在,则regmap_write()
失败并返回-EIO
。3)如果设置了
cache_type
字段,则使用缓存。 要写入的值将缓存以供将来参考,而不是写入硬件。4)如果未设置
cache_type
,则立即调用写入例程将值写入硬件寄存器。 在将值写入该寄存器之前,该例程将首先将write_flag_mask
应用于寄存器地址的第一个字节。5)最后,使用适当的解锁功能解锁。
-
regmap_read
用于从设备读取数据。 此函数执行与regmap_write()
相同的安全和健全性检查,但将writable_reg
和wr_table
替换为readable_reg
和rd_table
。 在缓存方面,如果启用了缓存,则会从缓存中读取寄存器值。 如果未启用缓存,则调用读取例程从硬件寄存器读取值。 该例程将在读取操作之前将read_flag_mask
应用于寄存器地址的最高字节,并使用读取的新值更新*val
。 在此之后,使用适当的解锁功能释放锁。
虽然前面的访问器一次只针对一个寄存器,但其他访问器可以执行批量访问,我们将在下一节中看到这一点。
有时您可能希望同时对寄存器范围执行批量读/写数据操作。 即使在循环中使用regmap_read()
或regmap_write()
,最好的解决方案也是使用为此类情况提供的 regmap API。 这些函数是regmap_bulk_read()
和regmap_bulk_write()
:
int regmap_bulk_read(struct regmap *map, unsigned int reg,
void *val, size_tval_count);
int regmap_bulk_write(struct regmap *map, unsigned int reg,
const void *val, size_t val_count)
这些函数从器件读取多个寄存器/向器件写入多个寄存器。 map
是用于执行操作的 regmap。 对于读操作,reg
是应开始读取的第一个寄存器,val
是指向缓冲器的指针,读取值应存储在设备的本机寄存器大小中(这意味着如果设备寄存器大小为 4 字节,则读取值将以 4 字节为单位存储),val_count
是要读取的寄存器数。 对于写入操作,reg
是要写入的第一个寄存器,val
是指向要写入器件的本机寄存器大小的数据块的指针,val_count
是要写入的寄存器数。 对于这两个函数,成功时将返回值0
,如果出现错误,将返回负errno
。
给小费 / 翻倒 / 倾覆
该框架还提供了其他有趣的读/写函数。 有关更多信息,请查看内核头文件。 一个有趣的例子是regmap_multi_reg_write()
,它将一组{寄存器,值}对中的多个寄存器写入作为参数给出的设备,这些寄存器对以任何顺序提供,可能不是全部在一个范围内。
既然我们已经熟悉了寄存器访问,我们就可以通过在位级别管理寄存器内容来更进一步。
为了更新给定寄存器中的位,我们有一个三合一函数regmap_update_bits()
。 它的原型如下:
int regmap_update_bits(struct regmap *map, unsigned int reg,
unsigned int mask, unsigned int val)
它在寄存器映射上执行读/修改/写周期。 它是_regmap_update_bits()
的包装器,如下所示:
static int _regmap_update_bits(
struct regmap *map, unsigned int reg,
unsigned int mask, unsigned int val,
bool *change, bool force_write)
{
int ret;
unsigned int tmp, orig;
if (change)
*change = false;
if (regmap_volatile(map, reg) && map->reg_update_bits) {
ret = map->reg_update_bits(map->bus_context,
reg, mask, val);
if (ret == 0 && change)
*change = true;
} else {
ret = _regmap_read(map, reg, &orig);
if (ret != 0)
return ret;
tmp = orig & ~mask;
tmp |= val & mask;
if (force_write || (tmp != orig)) {
ret = _regmap_write(map, reg, tmp);
if (ret == 0 && change)
*change = true;
}
}
return ret;
}
需要更新的位应在mask
中设置为1
,相应的位将被赋予val
中相同位置的位的值。 例如,要将第一位(BIT(0)
)和第三位(BIT(2)
)设置为1
,mask
应为0b00000101
,值应为0bxxxxx1x1
。 要清除第七位(BIT(6)
),mask
必须为0b01000000
,值应为0bx0xxxxxx
,依此类推。
给小费 / 翻倒 / 倾覆
出于调试目的,您可以使用debugfs
文件系统转储 regmap 托管寄存器的内容,如以下摘录所示:
#mount-t debugfs none/sys/kernel/debug
#cat/sys/kernel/debug/regmap/1-0008/registers
这将以<addr:value>
格式转储寄存器地址及其值。
在本节中,我们已经看到了访问硬件寄存器是多么容易。 此外,我们还学习了在位级操作寄存器的一些花哨技巧,这些寄存器通常用于状态寄存器和配置寄存器。 接下来,我们来看看 IRQ 管理。
Regmap 和 IRQ 管理
Regmap 不仅抽象访问寄存器。 在这里,我们将看到该框架如何在较低级别抽象 IRQ 管理,例如 IRQ 芯片处理,从而隐藏样板操作。
IRQ 通过称为中断控制器的特殊设备暴露给设备。 从软件的角度来看,中断控制器设备驱动管理并使用虚拟 IRQ 概念(在 Linux 内核中称为 IRQ 域)公开这些行。 中断管理建立在以下结构之上:
-
struct irq_chip
:这个结构是 IRQ 控制器的 Linux 表示,它实现了一组方法来驱动中断控制器,这些方法由核心 IRQ 代码直接调用。 如有必要,此结构应由驱动填充,提供一组回调,允许我们管理 IRQ 芯片上的 IRQ,例如irq_startup
、irq_shutdown
、irq_enable
、irq_disable
、irq_ack
、irq_mask
、irq_unmask
、irq_eoi
和irq_set_affinity
。 哑巴 IRQ 芯片设备(例如,不允许 IRQ 管理的芯片)应该使用内核提供的dummy_irq_chip
。 -
struct irq_domain
: Each interrupt controller is given a domain, which is for the controller what the address space is for a process. Thestruct irq_domain
structure stores mappings between hardware IRQs and Linux IRQs (that is, virtual IRQs, or virq). It is the hardware interrupt number translation object. This structure provides the following:--指向给定中断控制器的固件节点的指针(
fwnode
)。--一种将 IRQ 的固件(设备树)描述转换为中断控制器本地 ID(硬件 IRQ号,称为hwirq)的方法。 对于同时充当 IRQ 控制器的 GPIO 芯片,给定 GPIO 线路的硬件 IRQ 号(Hwirq)大多数时候对应于该线路在芯片中的本地索引。
--从 hwirq 检索 IRQ 的 Linux 视图的方法。
-
struct irq_desc
:此结构是中断的 Linux 内核视图,包含所有核心内容以及到 Linux 中断号的一对一映射。 -
struct irq_action
:这是 Linux 用来描述 IRQ 处理程序的结构。 -
struct irq_data
: This structure is embedded in thestruct irq_desc
structure, and contains the following:--与管理该中断的
irq_chip
相关的数据--Linux IRQ 号和 hwirq
--指向
irq_chip
的指针--指向中断转换域的指针(
irq_domain
)
请始终记住,irq_domain 对于中断控制器而言就像地址空间对于进程一样,因为它存储 virqs 和 hwirqs之间的映射。
中断控制器驱动通过调用irq_domain_add_<mapping_method>()
函数之一来创建并注册irq_domain
。 这些函数实际上是irq_domain_add_linear()
、irq_domain_add_tree()
和irq_domain_add_nomap()
。 实际上,<mapping_method>
是将hwirqs
映射到virqs
的方法。
irq_domain_add_linear()
创建按 hwirq 编号索引的固定大小的空表。 struct irq_desc
被分配给每个被映射的 HWIRQ。 然后,所分配的 IRQ 描述符被存储在表中,其索引处等于其已被分配到的 HWIRQ。 该线性映射适用于固定和少量(小于 256)的 HWIRQ。
虽然这种映射的主要优点是 IRQ 号查找时间是固定的,并且irq_desc
仅分配给正在使用的 IRQ,但主要缺点在于表的大小,它可以尽可能大到最大的hwirq
号。 大多数司机应该使用线性地图。 此函数具有以下原型:
struct irq_domain *irq_domain_add_linear(
struct device_node *of_node,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
irq_domain_add_tree()
创建一个空的irq_domain
,用于维护 Linux IRQ 和基数树中的hwirq
数字之间的映射。 当映射 HWIRQ 时,分配struct irq_desc
,并且将 HWIRQ 用作基数树的查找关键字。 如果 hwirq 数非常大,则树映射是一个很好的选择,因为它不需要分配与最大 hwirq 数一样大的表。 缺点是hwirq-to-IRQ
号查找取决于表中有多少条目。 很少有驱动应该需要此映射。 它有以下原型:
struct irq_domain *irq_domain_add_tree(
struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
irq_domain_add_nomap()
是您可能永远不会用到的东西;但是,在内核源代码树中的Documentation/IRQ-domain.txt
中可以找到它的完整描述。 它的原型如下:
struct irq_domain *irq_domain_add_nomap(
struct device_node *of_node,
unsigned int max_irq,
const struct irq_domain_ops *ops,
void *host_data)
在所有这些原型中,of_node
是指向中断控制器的 DT 节点的指针。 size
表示线性映射情况下域中的中断数。 ops
表示映射/取消映射域回调,host_data
是控制器的私有数据指针。 由于这三个函数都创建空的irq
域,因此您应该使用irq_create_mapping()
函数,并将 hwirq 和指向irq
域的指针传递给它,以创建映射,并将此映射插入域中:
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
在前面的原型中,domain
是该硬件中断所属的域。 NULL
值表示默认域。 hwirq
是需要为其创建映射的硬件 IRQ 编号。 此函数将硬件中断映射到 Linux IRQ 空间,并返回 Linux IRQ 编号。 此外,请记住,每个硬件中断只允许一个映射。 以下是创建映射的示例:
unsigned int virq = 0;
virq = irq_create_mapping(irq_domain, hwirq);
if (!virq) {
ret = -EINVAL;
goto err_irq;
}
在前面的代码中,virq
是与映射对应的 Linux 内核 IRQ(虚拟 IRQ 号,virq)。
重要音符
为同时也是中断控制器的 GPIO 控制器编写驱动时,会从gpio_chip.to_irq()
回调中调用irq_create_mapping()
,并将 virq 返回为return irq_create_mapping(gpiochip->irq_domain, hwirq)
,其中hwirq
是 GPIO 芯片的 GPIO 偏移量。
一些驱动更喜欢在probe()
函数中预先创建映射并填充每个 hwirq 的域,如下所示:
for (j = 0; j < gpiochip->chip.ngpio; j++) {
irq = irq_create_mapping(gpiochip ->irq_domain, j);
}
在此之后,这样的驱动只需将irq_find_mapping()
(给定 hwirq)调用到to_irq()
回调函数中。 如果给定的hwirq
没有映射,irq_create_mapping()
将分配一个新的struct irq_desc
结构,将其与 hwirq 关联,并调用irq_domain_ops.map()
回调(通过使用irq_domain_associate()
函数),以便驱动可以执行任何所需的硬件设置。
此结构向 IRQ 域公开了一些特定于的回调。 当在给定的 IRQ 域中创建映射时,每个映射(实际上是每个irq_desc
)都应该被赋予一个 IRQ 配置、一些私有数据和一个转换函数(给定一个设备树节点和一个中断说明符,转换函数解码硬件 IRQ 编号和 Linux IRQ 类型值)。 此结构中的回调功能如下:
struct irq_domain_ops {
int (*map)(struct irq_domain *d, unsigned int virq,
irq_hw_number_t hw);
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
};
前面数据结构中元素的每个 Linux 内核 IRQ 管理都应该有一节来描述。
下面的是该回调的原型:
int (*map)(struct irq_domain *d, unsigned int virq,
irq_hw_number_t hw);
在描述此函数的功能之前,让我们先描述一下它的参数:
d
:此 IRQ 芯片使用的 IRQ 域virq
:此基于 GPIO 的 IRQ 芯片使用的全局 IRQ 号hw
:此 GPIO 芯片上的本地 IRQ/GPIO 线路偏移量
.map()
创建或更新 virq 和 hwirq 之间的映射。 此回调设置 IRQ 配置。 对于给定的映射,它只被调用一次(内部由 IRQ 内核调用)。 这是我们为给定 IRQ 设置irq
芯片数据的地方,这可以使用irq_set_chip_data()
来完成,它具有以下原型:
int irq_set_chip_data(unsigned int irq, void *data);
根据 IRQ 芯片的类型(嵌套或链接),可以执行其他操作。
给定一个 dt 节点和一个中断说明符,此回调解码硬件 IRQ 编号及其 Linux IRQ 类型值。 根据 DT 控制器节点中指定的#interrupt-cells
属性,内核提供通用转换函数:
irq_domain_xlate_twocell()
:此通用翻译函数用于直接两个单元格绑定。 DT IRQ 说明符使用两个单元格绑定,其中单元格值直接映射到hwirq
数字和 Linux IRQ 标志。irq_domain_xlate_onecell()
:这是用于直接单元格绑定的通用xlate
函数。irq_domain_xlate_onetwocell()
:这是一个用于一个或两个单元格绑定的泛型xlate
函数。
域操作的示例如下:
static struct irq_domain_ops mcp23016_irq_domain_ops = {
.map = my_irq_domain_map,
.xlate = irq_domain_xlate_twocell,
};
前述数据结构的独特的特征是分配给.xlate
元素的值,即irq_domain_xlate_twocell
。 这意味着我们在设备树中期待一个由两个单元格组成的irq
说明符,其中第一个单元格将指定irq
,第二个单元格将指定其标志。
当中断发生时,可以使用irq_find_mapping()
帮助器函数从 hwirq 号中查找 Linux IRQ 号。 例如,该 HWIRQ 号可以是一组 GPIO 控制器中的 GPIO 偏移量。 一旦找到并返回了有效的 virq,您就应该在这个virq
上调用handle_nested_irq()
或generic_handle_irq()
。 魔力来自前两个函数,它们管理irq
流处理程序,这意味着有两种方法可以处理中断处理程序。 硬中断处理程序或链式中断是原子的,在禁用 IRQ 的情况下运行,可以调度线程处理程序;还有称为嵌套中断的简单线程中断处理程序,它可能会被其他中断中断。
此方法用于可能不会休眠的控制器,例如 SoC 的内部 GPIO 控制器,该控制器是内存映射的,其访问不会休眠。 链式意味着这些中断只是函数调用链(例如,SoC 的 GPIO 控制器中断处理程序是从 GIC 中断处理程序内部调用的,就像函数调用一样)。 通过这种方法,可以在父 hwirq 处理程序内部调用子 IRQ 处理程序。 这里必须使用generic_handle_irq()
在父 hwirq 处理程序内链接子 IRQ 处理程序。 即使从子中断处理程序内部,我们仍然处于原子上下文(硬件中断)中。 您不能调用可能休眠的函数。
对于链式(且仅链式)IRQ 芯片,irq_domain_ops.map()
也是使用irq_set_chip_and_handler()
将高级irq-type
流处理程序分配给给定 IRQ 的合适位置,因此此高级代码将在调用相应的 IRQ 处理程序之前执行一些黑客操作,具体取决于它是什么。 多亏了irq_set_chip_and_handler()
函数,魔术在这里发挥作用:
void irq_set_chip_and_handler(unsigned int irq,
struct irq_chip *chip,
irq_flow_handler_t handle)
在前面的原型中,irq
表示 Linux IRQ(virq
),作为irq_domain_ops.map()
函数的参数给出;chip
是您的irq_chip
结构;handle
是您的高级中断流处理程序。
重要音符
有些控制器非常愚蠢,在它们的irq_chip
结构中几乎不需要任何东西。 在这种情况下,您应该将dummy_irq_chip
传递给irq_set_chip_and_handler()
。 dummy_irq_chip
在kernel/irq/dummychip.c
中定义。
下面的代码流总结了irq_set_chip_and_handler()
的功能:
void irq_set_chip_and_handler(unsigned int irq,
struct irq_chip *chip,
irq_flow_handler_t handle)
{
struct irq_desc *desc = irq_get_desc(irq);
desc->irq_data.chip = chip;
desc->handle_irq = handle;
}
以下是泛型层提供的一些可能的高级 IRQ 流处理程序:
/*
* Built-in IRQ handlers for various IRQ types,
* callable via desc->handle_irq()
*/
void handle_level_irq(struct irq_desc *desc);
void handle_fasteoi_irq(struct irq_desc *desc);
void handle_edge_irq(struct irq_desc *desc);
void handle_edge_eoi_irq(struct irq_desc *desc);
void handle_simple_irq(struct irq_desc *desc);
void handle_untracked_irq(struct irq_desc *desc);
void handle_percpu_irq(struct irq_desc *desc);
void handle_percpu_devid_irq(struct irq_desc *desc);
void handle_bad_irq(struct irq_desc *desc);
每个函数名都很好地描述了它处理的 IRQ 类型。 链式 IRQ 芯片的irq_domain_ops.map()
可能如下所示:
static int my_chained_irq_domain_map(struct irq_domain *d,
unsigned int virq,
irq_hw_number_t hw)
{
irq_set_chip_data(virq, d->host_data);
irq_set_chip_and_handler(virq, &dummy_irq_chip, handle_ edge_irq);
return 0;
}
在为链式 IRQ 芯片编写父 IRQ 处理程序时,代码应对每个子 IRQ 处理程序irq
调用generic_handle_irq()
。 此函数只调用irq_desc->handle_irq()
,它指向使用irq_set_chip_and_handler()
分配给给定子 IRQ 的高级中断处理程序。 底层的高级irq
事件处理程序(假设是handle_level_irq()
)将首先执行一些黑客操作,然后运行硬的irq-handler
(irq_desc->action->handler
),并根据返回值运行线程处理程序(irq_desc->action->thread_fn
)(如果提供)。
下面是一个链式 IRQ 芯片的父 IRQ 处理程序示例,其原始代码位于内核源代码的drivers/pinctrl/pinctrl-at91.c
中:
static void parent_hwirq_handler(struct irq_desc *desc)
{
struct irq_chip *chip = irq_desc_get_chip(desc);
struct gpio_chip *gpio_chip = irq_desc_get_handler_ data(desc);
struct at91_gpio_chip *at91_gpio = gpiochip_get_data (gpio_ chip);
void __iomem *pio = at91_gpio->regbase;
unsigned long isr;
int n;
chained_irq_enter(chip, desc);
for (;;) {
/* Reading ISR acks pending (edge triggered) GPIO
* interrupts. When there are none pending, we’re
* finished unless we need to process multiple banks
* (like ID_PIOCDE on sam9263).
*/
isr = readl_relaxed(pio + PIO_ISR) &
readl_relaxed(pio + PIO_IMR);
if (!isr) {
if (!at91_gpio->next)
break;
at91_gpio = at91_gpio->next;
pio = at91_gpio->regbase;
gpio_chip = &at91_gpio->chip;
continue;
}
for_each_set_bit(n, &isr, BITS_PER_LONG) {
generic_handle_irq(
irq_find_mapping(gpio_chip->irq.domain, n));
}
}
chained_irq_exit(chip, desc);
/* now it may re-trigger */
[...]
}
链式 IRQ 芯片驱动器不需要使用devm_request_threaded_irq()
或devm_request_irq()
注册父irq
处理程序。 当驱动在父 IRQ 上调用irq_set_chained_handler_and_data()
时,会自动注册此处理程序,给出关联的处理程序作为参数,以及一些私有数据:
void irq_set_chained_handler_and_data(unsigned int irq,
irq_flow_handler_t handle,
void *data)
该函数的参数非常简单明了。 您应该在probe
函数中调用此函数,如下所示:
static int my_probe(struct platform_device *pdev)
{
int parent_irq, i;
struct irq_domain *my_domain;
parent_irq = platform_get_irq(pdev, 0);
if (!parent_irq) {
pr_err("failed to map parent interrupt %d\n", parent_irq);
return -EINVAL;
}
my_domain =
irq_domain_add_linear(np, nr_irq, &my_irq_domain_ops,
my_private_data);
if (WARN_ON(!my_domain)) {
pr_warn("%s: irq domain init failed\n", __func__);
return;
}
/* This may be done elsewhere */
for(i = 0; i < nr_irq; i++) {
int virqno = irq_create_mapping(my_domain, i);
/*
* May need to mask and clear all IRQs before * registering a handler
*/
[...]
irq_set_chained_handler_and_data(parent_irq,
parent_hwirq_handler,
my_private_data);
/*
* May need to call irq_set_chip_data() on * the virqno too */
[...]
}
[...]
}
在前面的假probe
方法中,使用irq_domain_add_linear()
创建线性域,并且使用irq_create_mapping()
在该域中创建 IRQ 映射(虚拟 IRQ)。 最后,我们为主(或父)IRQ 设置高级链接流处理程序及其数据。
重要音符
请注意,irq_set_chained_handler_and_data()
自动启用中断(在第一个参数中指定),分配其处理程序(也作为参数给出),并将该中断标记为IRQ_NOREQUEST
、IRQ_NOPROBE
或IRQ_NOTHREAD
,这意味着该中断不能再通过request_irq()
请求,不能通过自动探测探测,也不能被线程处理(它被链接)。
嵌套流方法由可能休眠的 IRQ 芯片使用,例如那些位于 I2C(例如,I2C GPIO 扩展器)的慢速总线上的 IRQ 芯片。 “嵌套”是指那些不在硬件上下文中运行的中断处理程序(它们实际上不是 hwirq,也不在原子上下文中),而是线程化的,可以被抢占。 在这里,处理程序函数在调用线程上下文中调用。 对于嵌套(且仅嵌套)IRQ 芯片,irq_domain_ops.map()
回调也是设置irq
配置标志的正确位置。 最重要的配置标志如下:
IRQ_NESTED_THREAD
:这是一个标志,表示在devm_request_threaded_irq()
上,不应该为 irq 处理程序创建专用中断线程,因为它被称为嵌套在多路复用中断处理程序线程的上下文中(在内核源代码的kernel/irq/manage.c
中实现的__setup_irq()
函数中有更多关于这方面的信息)。 您可以使用void irq_set_nested_thread(unsigned int irq, int nest)
对该标志进行操作,其中irq
对应于全局中断号,nest
应为0
以清除或1
以设置IRQ_NESTED_THREAD
标志。IRQ_NOTHREAD
:可以使用void irq_set_nothread(unsigned int irq)
设置该标志。 它用于将给定的 IRQ 标记为不可线程。
嵌套 IRQ 芯片的irq_domain_ops.map()
可能如下所示:
static int my_nested_irq_domain_map(struct irq_domain *d,
unsigned int virq,
irq_hw_number_t hw)
{
irq_set_chip_data(virq, d->host_data);
irq_set_nested_thread(virq, 1);
irq_set_noprobe(virq);
return 0;
}
在为嵌套的 IRQ 芯片编写父 IRQ 处理程序时,代码应该调用handle_nested_irq()
以便处理子 IRQ 处理程序,以便它们从父 IRQ 线程运行。 handle_nested_irq()
不关心作为硬 irq 处理程序的irq_desc->action->handler
。 它只需运行irq_desc->action->thread_fn
:
static irqreturn_t mcp23016_irq(int irq, void *data)
{
struct mcp23016 *mcp = data;
unsigned int child_irq, i;
/* Do some stuff */
[...]
for (i = 0; i < mcp->chip.ngpio; i++) {
if (gpio_value_changed_and_raised_irq(i)) {
child_irq = irq_find_mapping(mcp->chip.irqdomain, i);
handle_nested_irq(child_irq);
}
}
[...]
}
嵌套 IRQ 芯片驱动器必须使用devm_request_threaded_irq()
注册父 IRQ 处理程序,因为此类 IRQ 芯片没有类似irq_set_chained_handler_and_data()
的函数。 将此 API 用于嵌套 IRQ 芯片是没有意义的。 大多数情况下,嵌套的 IRQ 芯片都是基于 GPIO 芯片的。 因此,我们最好使用基于 GPIO 芯片的 IRQ 芯片 API,或者使用基于 regmap 的 IRQ 芯片 API,如下一节所示。 不过,让我们看看这样的示例是什么样子的:
static int my_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int parent_irq, i;
struct irq_domain *my_domain;
[...]
int irq_nr = get_number_of_needed_irqs();
/* Do we have an interrupt line ? Enable the IRQ chip */
if (client->irq) {
domain = irq_domain_add_linear(
client->dev.of_node, irq_nr,
&my_irq_domain_ops, my_private_data);
if (!domain) {
dev_err(&client->dev,
"could not create irq domain\n");
return -ENODEV;
}
/*
* May be creating irq mapping in this domain using
* irq_create_mapping() or let the mfd core doing
* this if it is an MFD chip device
*/
[...]
ret =
devm_request_threaded_irq(
&client->dev, client->irq,
NULL, my_parent_irq_thread,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"my-parent-irq", my_private_data);
[...]
}
[...]
}
在前面的方法probe
中,与链式流有两个主要区别:
- 首先,注册主 IRQ 的方式:链式 IRQ 芯片使用自动注册处理程序的
irq_set_chained_handler_and_data()
,而嵌套流方法必须使用request_threaded_irq()
系列方法显式注册其处理程序。 - 其次,主 IRQ 处理程序调用底层 IRQ 处理程序的方式:在链接的流中,在主 IRQ 处理程序中调用
handle_nested_irq()
,主 IRQ 处理程序将每个底层 IRQ 的处理程序作为函数调用链调用,这些函数调用在与主处理程序相同的上下文中执行,即原子(原子性也称为hard-irq
)。 然而,嵌套的流处理程序必须调用handle_nested_irq()
,它在父级的线程上下文中执行底层 irq 的处理程序(thread_fn
)。
这些是链式流和嵌套流之间的主要区别。
由于每个irq-gpiochip
驱动都对自己的irqdomain
处理进行了开放编码,这导致了大量的冗余代码。 内核开发者决定将该代码转移到 gpiolib 框架,因此提供了GPIOLIB_IRQCHIP
Kconfig 符号,使用户能够对 GPIO 芯片使用统一的 Irq 域管理 API。 这部分代码有助于处理 GPIO IRQ 芯片和相关的irq_domain
和资源分配回调的管理,以及它们的设置,使用缩减的帮助器函数集。 这些是gpiochip_irqchip_add()
或gpiochip_irqchip_add_nested()
,以及gpiochip_set_chained_irqchip()
或gpiochip_set_nested_irqchip()
。 gpiochip_irqchip_add()
或gpiochip_irqchip_add_nested()
都将 IRQ 芯片添加到 GPIO 芯片。 以下是它们各自的原型:
static inline int gpiochip_irqchip_add( struct gpio_chip *gpiochip,
struct irq_chip *irqchip,
unsigned int first_irq,
irq_flow_handler_t handler,
unsigned int type)
static inline int gpiochip_irqchip_add_nested(
struct gpio_chip *gpiochip,
struct irq_chip *irqchip,
unsigned int first_irq,
irq_flow_handler_t handler,
unsigned int type)
在前面的原型中,gpiochip
参数是要添加irqchip
的 GPIO 芯片。 irqchip
是要添加到 GPIO 芯片中的 IRQ 芯片,以便扩展其功能,使其也可以充当 IRQ 控制器。 此 IRQ 芯片必须由驱动或 IRQ 核心代码正确配置(如果给出dummy_irq_chip
作为参数)。 如果它不是动态分配的,first_irq
将是从中分配 GPIO 芯片 IRQ 的基准(第一个)IRQ。 handler
是要使用的主要 IRQ 处理程序(通常是预定义的高级 IRQ 核心函数之一)。 type
是此IRQ chip
上 IRQ 的默认类型;在此处传递IRQ_TYPE_NONE
,让驱动根据请求进行配置。
这些功能操作的摘要如下:
-
第一个函数使用
irq_domain_add_simple()
函数将struct irq_domain
分配给 GPIO 芯片。 此 IRQ 域的 OPS 是使用名为gpiochip_domain_ops
的内核 IRQ 核心域 OPS 变量设置的。 该域 OPS 在drivers/gpio/gpiolib.c
中定义,其中irq_domain_ops.xlate
字段设置为irq_domain_xlate_twocell
,这意味着该 GPIO 芯片将处理双单元 IRQ。 -
将
gpiochip.to_irq
字段设置为gpiochip_to_irq
,这是一个返回irq_create_mapping(chip->irq.domain, offset)
的回调,创建与 GPIO 偏移量相对应的 IRQ 映射。 这是在我们对该 GPIO 调用gpiod_to_irq()
时执行的。 此函数假定gpiochip
上的每个引脚都可以生成唯一的 IRQ。 下面是gpiochip_domain_ops
IRQ 域的定义方式:static const struct irq_domain_ops gpiochip_domain_ops = { .map = gpiochip_irq_map, .unmap = gpiochip_irq_unmap, /* Virtually all GPIO-based IRQ chips are two-celled */ .xlate = irq_domain_xlate_twocell, };
gpiochip_irqchip_add_nested()
和gpiochip_irqchip_add()
之间的唯一区别是前者向 GPIO 芯片添加嵌套 IRQ 芯片(它将gpio_chip->irq.threaded
字段设置为true
),而后者将链式 IRQ 芯片添加到 GPIO 芯片,并将该字段设置为false
。 另一方面,gpiochip_set_chained_irqchip()
和gpiochip_set_nested_irqchip()
分别将链式或嵌套 IRQ 芯片分配/连接到 GPIO 芯片。 以下是这两个函数的原型:
void gpiochip_set_chained_irqchip( struct gpio_chip *gpiochip,
struct irq_chip *irqchip,
unsigned int parent_irq,
irq_flow_handler_t parent_handler)
void gpiochip_set_nested_irqchip(struct gpio_chip *gpiochip,
struct irq_chip *irqchip,
unsigned int parent_irq)
在前面的原型中,gpiochip
是要设置irqchip
链的 GPIO 芯片。 irqchip
表示要链接到 GPIO 芯片的 IRQ 芯片。 parent_irq
是对应于该链式 IRQ 芯片的父 IRQ 的 IRQ 编号。 换句话说,它是该芯片连接到的 IRQ 号。 parent_handler
是 GPIO 芯片输出的累积 IRQ 的父中断处理程序。 它实际上是 hwirq 处理程序。 这不用于嵌套 IRQ 芯片,因为父处理程序是线程化的。 链式变量将在内部调用parent_handler
上的irq_set_chained_handler_and_data()
。
gpiochip_irqchip_add()
和gpiochip_set_chained_irqchip()
用于基于链式 GPIO 芯片的 IRQ 芯片,而gpiochip_irqchip_add_nested()
和gpiochip_set_nested_irqchip()
仅用于基于嵌套 GPIO 芯片的 IRQ 芯片。 使用基于 GPIO 芯片的链式 IRQ 芯片,gpiochip_set_chained_irqchip()
将配置父 hwirq 的处理程序。 不需要调用任何devm_request_*``irq
族函数。 但是,父 hwirq 的处理程序必须对已提出的子irqs
调用generic_handle_irq()
,如下例所示(来自内核源代码中的drivers/pinctrl/pinctrl-at91.c
),这与标准的链式 IRQ 芯片有点类似:
static void gpio_irq_handler(struct irq_desc *desc)
{
unsigned long isr;
int n;
struct irq_chip *chip = irq_desc_get_chip(desc);
struct gpio_chip *gpio_chip = irq_desc_get_handler_data(desc);
struct at91_gpio_chip *at91_gpio =
gpiochip_get_data(gpio_chip);
void __iomem *pio = at91_gpio->regbase;
chained_irq_enter(chip, desc);
for (;;) {
isr = readl_relaxed(pio + PIO_ISR) &
readl_relaxed(pio + PIO_IMR);
[...]
for_each_set_bit(n, &isr, BITS_PER_LONG) {
generic_handle_irq(irq_find_mapping(
gpio_chip->irq.domain, n));
}
}
chained_irq_exit(chip, desc);
[...]
}
在前面的代码中,首先介绍了中断处理程序。 当 GPIO 芯片发出中断时,它的整个 GPIO 状态库被读取,以便检测设置在那里的每个位,这将意味着由相应 GPIO 线后面的器件触发的潜在 IRQ。
然后在域中的索引对应于 GPIO 状态库中设置的位的索引的每个 IRQ 描述符上调用generic_handle_irq()
。 此方法将依次调用在原子上下文(hard-irq
上下文)中为上一步中找到的每个描述符注册的每个处理程序,除非将 GPIO 用作 IRQ 行的设备的底层驱动请求线程化处理程序。
现在我们可以介绍probe
方法,其示例如下:
static int at91_gpio_probe(struct platform_device *pdev)
{
[...]
ret = gpiochip_irqchip_add(&at91_gpio->chip,
&gpio_irqchip,
0,
handle_edge_irq,
IRQ_TYPE_NONE);
if (ret) {
dev_err(
&pdev->dev,
"at91_gpio.%d: Couldn’t add irqchip to gpiochip.\n",
at91_gpio->pioc_idx);
return ret;
}
[...]
/* Then register the chain on the parent IRQ */
gpiochip_set_chained_irqchip(&at91_gpio->chip,
&gpio_irqchip,
at91_gpio->pioc_virq,
gpio_irq_handler);
return 0;
}
那里没有什么特别的东西。 这里的机制在某种程度上遵循了我们在通用 IRQ 芯片中看到的。 这里不使用任何request_irq()
系列方法请求父 IRQ,因为gpiochip_set_chained_irqchip()
将在幕后调用irq_set_chained_handler_and_data()
。
下面的节选显示了基于 GPIO 芯片的嵌套 IRQ 芯片是如何由其驱动注册的。 这有点类似于独立的嵌套 IRQ 芯片:
static irqreturn_t pcf857x_irq(int irq, void *data)
{
struct pcf857x *gpio = data;
unsigned long change, i, status;
status = gpio->read(gpio->client);
/*
* call the interrupt handler if gpio is used as
* interrupt source, just to avoid bad irqs
*/
mutex_lock(&gpio->lock);
change = (gpio->status ^ status) & gpio->irq_enabled;
gpio->status = status;
mutex_unlock(&gpio->lock);
for_each_set_bit(i, &change, gpio->chip.ngpio)
handle_nested_irq(
irq_find_mapping(gpio->chip.irq.domain, i));
return IRQ_HANDLED;
}
前面的代码是 IRQ 处理程序。 正如我们所看到的,它使用handle_nested_irq()
,这对我们来说并不是什么新鲜事。 现在让我们检查一下probe
方法:
static int pcf857x_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct pcf857x *gpio;
[...]
/* Enable irqchip only if we have an interrupt line */
if (client->irq) {
status = gpiochip_irqchip_add_nested(&gpio->chip,
&gpio->irqchip,
0, handle_level_irq,
IRQ_TYPE_NONE);
if (status) {
dev_err(&client->dev, "cannot add irqchip\n");
goto fail;
}
status = devm_request_threaded_irq(
&client->dev, client->irq,
NULL, pcf857x_irq,
IRQF_ONESHOT |IRQF_TRIGGER_FALLING | IRQF_SHARED,
dev_name(&client->dev), gpio);
if (status)
goto fail;
gpiochip_set_nested_irqchip(&gpio->chip, &gpio->irqchip,
client->irq);
}
[...]
}
在这里,父 IRQ 处理程序是线程化的,必须使用devm_request_threaded_irq()
注册。 这解释了为什么它的 IRQ 处理程序必须在子 IRQ 上调用handle_nested_irq()
才能调用它们的处理程序。 同样,这看起来与泛型嵌套irqchips
类似,只是 gpiolib 包装了一些底层嵌套的irqchip
API。 要确认这一点,您可以查看gpiochip_set_nested_irqchip()
和gpiochip_irqchip_add_nested()
方法的主体。
RegmapIRQ API 在drivers/base/regmap/regmap-irq.c
中实现。 它主要是在两个基本函数devm_regmap_add_irq_chip()
和regmap_irq_get_virq()
以及三个数据结构struct regmap_irq_chip
、struct regmap_irq_chip_data
和struct regmap_irq
之上构建的。
重要音符
Regmap 的irqchip
API 完全使用线程化 IRQ。 因此,只有我们在嵌套中断部分中看到的内容才适用于此。
正如前面提到的,我们需要介绍regmap irq api
的三种数据结构,以便理解它是如何抽象 IRQ 管理的。
struct regmap_irq_chip
结构描述泛型regmap irq_chip
。 在讨论这种结构之前,让我们先介绍一下struct regmap_irq
,它存储了寄存器和regmap irq_chip
的 IRQ 的掩码描述:
struct regmap_irq {
unsigned int reg_offset;
unsigned int mask;
unsigned int type_reg_offset;
unsigned int type_rising_mask;
unsigned int type_falling_mask;
};
以下是对上述结构中的字段的说明:
reg_offset
是存储体内状态/掩码寄存器的偏移量。 该存储体实际上可以是 IRQchip
的{status/mask/unmask/ack/wake}_base
寄存器。mask
是用于标记/控制此 IRQ 状态寄存器的掩码。 禁用 IRQ 时,屏蔽值将与 regmap 的irq_chip.status_base
寄存器中的实际内容reg_offset
进行或。 对于irq
启用,将对~mask
进行 AND 运算。type_reg_offset
是 IRQ 类型设置的偏移寄存器(来自irqchip
状态基址寄存器)。type_rising_mask
是配置上升型 IRQ 的屏蔽位。 将 IRQ 的类型设置为IRQ_TYPE_EDGE_RISING
时,该值将与type_reg_offset
的实际内容进行 OR 运算。type_falling_mask
是配置下降型 IRQ 的屏蔽位。 将 IRQ 的类型设置为IRQ_TYPE_EDGE_FALLING
时,该值将与type_reg_offset
的实际内容进行 OR 运算。 对于IRQ_TYPE_EDGE_BOTH
类型,将使用(type_falling_mask | irq_data->type_rising_mask)
作为掩码。
现在我们已经熟悉了struct regmap_irq
,让我们描述一下struct regmap_irq_chip
,它的结构如下所示:
struct regmap_irq_chip {
const char *name;
unsigned int status_base;
unsigned int mask_base;
unsigned int unmask_base;
unsigned int ack_base;
unsigned int wake_base;
unsigned int type_base;
unsigned int irq_reg_stride;
bool mask_writeonly:1;
bool init_ack_masked:1;
bool mask_invert:1;
bool use_ack:1;
bool ack_invert:1;
bool wake_invert:1;
bool type_invert:1;
int num_regs;
const struct regmap_irq *irqs;
int num_irqs;
int num_type_reg;
unsigned int type_reg_stride;
int (*handle_pre_irq)(void *irq_drv_data);
int (*handle_post_irq)(void *irq_drv_data);
void *irq_drv_data;
};
此结构描述了一个通用的regmap_irq_chip
,它可以处理大多数中断控制器(不是所有中断控制器,我们将在后面看到)。 下表介绍了此数据结构中的字段:
name
是 IRQ 控制器的描述性名称。status_base
是在获取给定regmap_irq
的最终状态寄存器之前,regmap IRQ 内核添加regmap_irq.reg_offset
的基址状态寄存器地址。mask_writeonly
说明基掩码寄存器是否为只写寄存器。 如果是,则使用regmap_write_bits()
写入寄存器,否则使用regmap_update_bits()
。unmask_base
是基址去屏蔽寄存器地址,必须为具有独立屏蔽和去屏蔽寄存器的芯片指定该地址。ack_base
是确认基址寄存器地址。 通过use_ack
位可以使用值0
。wake_base
是wake enable
的基地址,用于控制 IRQ 电源管理唤醒。 如果值为0
,则表示不支持此操作。type_base
是在获得给定regmap_irq
的最终类型寄存器之前,regmap IRQ 内核添加regmap_irq.type_reg_offset
到的 IRQ 类型的基地址。 如果为0
,则表示不支持此操作。irq_reg_stride
是用于寄存器不连续的芯片的步长。init_ack_masked
说明 regmap IRQ 内核是否应在初始化期间确认所有屏蔽中断一次。mask_invert
,如果为true
,则表示掩码寄存器反转。 这意味着清除的位索引对应于屏蔽中断。use_ack
,如果为true
,则表示即使是0
也应使用确认寄存器。ack_invert
,如果为true
,则表示确认寄存器反转:清除相应的位以进行确认。wake_invert
,如果为true
,则表示唤醒寄存器反转:清除位对应于唤醒启用。type_invert
,如果为true
,则表示使用反转类型标志。num_regs
是每个控制库中的寄存器数。 将给出使用regmap_bulk_read()
时要读取的寄存器数量。 有关更多信息,请查看regmap_irq_thread()
的定义。irqs
是单个 IRQ 的描述符数组,num_irqs
是数组中描述符的总数。 中断号是根据该数组中的索引分配的。num_type_reg
是类型寄存器的数量,而type_reg_stride
是用于类型寄存器不连续的芯片的步长。 Regmap IRQ 实现了通用中断服务例程,这在大多数设备中都很常见。- 某些设备,如
MAX77620
或MAX20024
,在服务中断之前和之后需要特殊的处理。 这就是handle_pre_irq
和handle_post_irq
的用武之地。 这些是驱动特定的回调,用于在regmap_irq_handler
处理中断之前处理来自设备的中断。irq_drv_data
然后是作为参数传递给这些中断前/中断后处理程序的数据。 例如,中断服务的MAX77620
编程指南如下所述:
--当 PMIC 发生中断时,通过设置 GLBLM 来屏蔽 PMIC 中断。
--读取 IRQTOP 并相应地服务中断。
--一旦检查并处理了所有中断,中断服务例程通过清除 GLBLM 来取消屏蔽硬件中断线。
回到regmap_irq_chip.irqs
字段,该字段属于前面介绍的regmap_irq
类型。
此结构是 regmap IRQ 控制器的运行时数据结构,分配在成功返回路径devm_regmap_add_irq_chip()
上。 它必须存储在大型私有数据结构中以备后用。 其定义如下:
struct regmap_irq_chip_data {
struct mutex lock;
struct irq_chip irq_chip;
struct regmap *map;
const struct regmap_irq_chip *chip;
int irq_base;
struct irq_domain *domain;
int irq;
[...]
};
为简单起见,结构中的一些字段已被删除。 以下是对此结构中的字段的说明:
-
lock
是用于保护对regmap_irq_chip_data
所属的irq_chip
的访问的锁。 由于 regmap IRQ 是完全线程化的,所以使用互斥是安全的。 -
irq_chip
是这个启用 regmap 的irqchip
的底层中断芯片描述符结构(提供与 IRQ 相关的操作),用regmap_irq_chip
设置,定义如下drivers/base/regmap/regmap-irq.c
:static const struct irq_chip regmap_irq_chip = { .irq_bus_lock = regmap_irq_lock, .irq_bus_sync_unlock = regmap_irq_sync_unlock, .irq_disable = regmap_irq_disable, .irq_enable = regmap_irq_enable, .irq_set_type = regmap_irq_set_type, .irq_set_wake = regmap_irq_set_wake, };
-
map
是上述irq_chip
的 regmap 结构。 -
chip
是指向应该在驱动中设置的通用 regmapirq_chip
的指针。 它作为参数提供给devm_regmap_add_irq_chip()
。 -
base
,如果大于零,则是它从中分配特定 IRQ 号的基数。 换句话说,IRQ 的编号从base
开始。 -
domain
是底层 IRQ 芯片的 IRQ 域,ops
设置为regmap_domain_ops
,定义如下:static const struct irq_domain_ops regmap_domain_ops = { .map = regmap_irq_map, .xlate = irq_domain_xlate_onetwocell, };
-
irq
是irq_chip
的父(基本)IRQ。 它对应于给devm_regmap_add_irq_chip()
的irq
参数。
在本章前面的中,我们介绍了作为 regmap IRQ API 组成的两个基本函数的devm_regmap_add_irq_chip()
和regmap_irq_get_virq()
。 这些实际上是 regmap IRQ 管理最重要的功能,下面是它们各自的原型:
int devm_regmap_add_irq_chip(struct device *dev, struct regmap *map,
int irq, int irq_flags, int irq_base,
const struct regmap_irq_chip *chip,
struct regmap_irq_chip_data **data)
int regmap_irq_get_virq(struct regmap_irq_chip_data *data, int irq)
在前面的代码中,dev
是irq_chip
所属的设备指针。 map
是设备的有效且已初始化的 regmap。 irq_base
,如果大于零,则为第一个分配的 IRQ 的编号。 chip
是中断控制器的配置。 在regmap_irq_get_virq()
的原型中,*data
是一个初始化的输入参数,必须由devm_regmap_add_irq_chip()
到**data
返回。
devm_regmap_add_irq_chip()
是应该用来在代码中添加基于 regmap 的 irqChip 支持的函数。 它的data
参数是一个输出参数,表示控制器的运行时数据结构,在此函数调用成功时分配。 它的irq
参数是 irqChip 的父 IRQ 和主 IRQ。 它是器件用来发出中断信号的 IRQ,而irq_flags
是用于该主中断的IRQF_
标志的掩码。 如果此函数成功(即返回0
),则输出数据将设置为类型为regmap_irq_chip_data
的新分配且配置良好的结构。 此函数在失败时返回errno
。 devm_regmap_add_irq_chip()
是以下各项的组合:
- 分配和初始化
struct regmap_irq_chip_data
。 irq_domain_add_linear()
(ifirq_base == 0
),它在给定域中需要的 IRQ 数量的情况下分配 IRQ 域。 如果成功,IRQ 域将被分配给先前分配的 IRQ 芯片数据的.domain
字段。 该域的ops.map
函数会将每个 IRQ 子函数配置为嵌套到父线程中,并且ops.xlate
将被设置为irq_domain_xlate_onetwocell
。 如果使用irq_base > 0
,则使用irq_domain_add_legacy()
而不是irq_domain_add_linear()
。request_threaded_irq()
,以便注册父 IRQ 线程处理程序。 Regmap 使用自己定义的线程处理程序regmap_irq_thread()
,它在对子对象irqs
调用handle_nested_irq()
之前执行一些黑客操作。
下面的摘录总结了前面的操作:
static int regmap_irq_map(struct irq_domain *h, unsigned int virq,
irq_hw_number_t hw)
{
struct regmap_irq_chip_data *data = h->host_data;
irq_set_chip_data(virq, data);
irq_set_chip(virq, &data->irq_chip);
irq_set_nested_thread(virq, 1);
irq_set_parent(virq, data->irq);
irq_set_noprobe(virq);
return 0;
}
static const struct irq_domain_ops regmap_domain_ops = {
.map = regmap_irq_map,
.xlate = irq_domain_xlate_onetwocell,
};
static irqreturn_t regmap_irq_thread(int irq, void *d)
{
[...]
for (i = 0; i < chip->num_irqs; i++) {
if (data->status_buf[chip->irqs[i].reg_offset /
map->reg_stride] & chip->irqs[i].mask) {
handle_nested_irq(irq_find_mapping(data->domain, i));
handled = true;
}
}
[...]
if (handled)
return IRQ_HANDLED;
else
return IRQ_NONE;
}
int regmap_add_irq_chip(struct regmap *map, int irq, int irq_ flags,
int irq_base, const struct regmap_irq_chip *chip,
struct regmap_irq_chip_data **data)
{
struct regmap_irq_chip_data *d;
[...]
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
/* The below is just for simplicity */
initialize_irq_chip_data(d);
if (irq_base)
d->domain = irq_domain_add_legacy(map->dev->of_node,
chip->num_irqs,
irq_base, 0,
®map_domain_ops, d);
else
d->domain = irq_domain_add_linear(map->dev->of_node,
chip->num_irqs,
®map_domain_ops, d);
ret = request_threaded_irq(irq, NULL, regmap_irq_thread,
irq_flags | IRQF_ONESHOT,
chip->name, d);
[...]
*data = d;
return 0;
}
regmap_irq_get_virq()
将芯片上的中断映射到虚拟 IRQ。 它只是在给定的irq
和域上返回irq_create_mapping(data->domain, irq)
,正如我们前面看到的。 其irq
参数是芯片 IRQ 中请求的中断的索引。
让我们使用max7760
GPIO 控制器的驱动来看看 regmap IRQ API 背后的概念是如何应用的。 此驱动位于内核源代码中的drivers/gpio/gpio-max77620.c
处,以下是此驱动使用 regmap 处理 IRQ 管理的简化方式摘录。
让我们从定义将在整个代码编写过程中使用的数据结构开始:
struct max77620_gpio {
struct gpio_chip gpio_chip;
struct regmap *rmap;
struct device *dev;
};
struct max77620_chip {
struct device *dev;
struct regmap *rmap;
int chip_irq;
int irq_base;
[...]
struct regmap_irq_chip_data *top_irq_data;
struct regmap_irq_chip_data *gpio_irq_data;
};
当您浏览代码时,前面数据结构的含义将变得清晰。 接下来,让我们定义 regmap IRQ 数组,如下所示:
static const struct regmap_irq max77620_gpio_irqs[] = {
[0] = {
.mask = MAX77620_IRQ_LVL2_GPIO_EDGE0,
.type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING,
.type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING,
.reg_offset = 0,
.type_reg_offset = 0,
},
[1] = {
.mask = MAX77620_IRQ_LVL2_GPIO_EDGE1,
.type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING,
.type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING,
.reg_offset = 0,
.type_reg_offset = 1,
},
[2] = {
.mask = MAX77620_IRQ_LVL2_GPIO_EDGE2,
.type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING,
.type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING,
.reg_offset = 0,
.type_reg_offset = 2,
},
[...]
[7] = {
.mask = MAX77620_IRQ_LVL2_GPIO_EDGE7,
.type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING,
.type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING,
.reg_offset = 0,
.type_reg_offset = 7,
},
};
您可能已经注意到,出于可读性的考虑,数组已被截断。 然后可以将该数组分配给regmap_irq_chip
数据结构,如下所示:
static const struct regmap_irq_chip max77620_gpio_irq_chip = {
.name = "max77620-gpio",
.irqs = max77620_gpio_irqs,
.num_irqs = ARRAY_SIZE(max77620_gpio_irqs),
.num_regs = 1,
.num_type_reg = 8,
.irq_reg_stride = 1,
.type_reg_stride = 1,
.status_base = MAX77620_REG_IRQ_LVL2_GPIO,
.type_base = MAX77620_REG_GPIO0,
};
总结前面的摘录,驱动填充一个regmap_irq
数组(max77620_gpio_irqs[]
),并使用它构建一个regmap_irq_chip
结构(max77620_gpio_irq_chip
)。 一旦regmap_irq_chip
数据结构准备就绪,我们就按照内核gpiochip
内核的要求开始编写一个irqchip
回调:
static int max77620_gpio_to_irq(struct gpio_chip *gc,
unsigned int offset)
{
struct max77620_gpio *mgpio = gpiochip_get_data(gc);
struct max77620_chip *chip = dev_get_drvdata(mgpio->dev- >parent);
return regmap_irq_get_virq(chip->gpio_irq_data, offset);
}
在前面的代码片段中,我们只定义了将分配给 GPIO 芯片的.to_irq
字段的回调。 其他回调可以在原始驱动中找到。 同样,代码在这里被截断。 在此阶段,我们可以讨论probe
方法,它将使用之前定义的所有函数:
static int max77620_gpio_probe(struct platform_device *pdev)
{
struct max77620_chip *chip = dev_get_drvdata(pdev->dev.parent);
struct max77620_gpio *mgpio;
int gpio_irq;
int ret;
gpio_irq = platform_get_irq(pdev, 0);
[...]
mgpio = devm_kzalloc(&pdev->dev, sizeof(*mgpio), GFP_KERNEL);
if (!mgpio)
return -ENOMEM;
mgpio->rmap = chip->rmap;
mgpio->dev = &pdev->dev;
/* setting gpiochip stuffs*/
mgpio->gpio_chip.direction_input = max77620_gpio_dir_input;
mgpio->gpio_chip.get = max77620_gpio_get;
mgpio->gpio_chip.direction_output = max77620_gpio_dir_output;
mgpio->gpio_chip.set = max77620_gpio_set;
mgpio->gpio_chip.set_config = max77620_gpio_set_config;
mgpio->gpio_chip.to_irq = max77620_gpio_to_irq;
mgpio->gpio_chip.ngpio = MAX77620_GPIO_NR;
mgpio->gpio_chip.can_sleep = 1;
mgpio->gpio_chip.base = -1;
#ifdef CONFIG_OF_GPIO
mgpio->gpio_chip.of_node = pdev->dev.parent->of_node;
#endif
ret = devm_gpiochip_add_data(&pdev->dev,
&mgpio->gpio_chip, mgpio);
[...]
ret = devm_regmap_add_irq_chip(&pdev->dev,
chip->rmap, gpio_irq,
IRQF_ONESHOT, -1,
&max77620_gpio_irq_chip,
&chip->gpio_irq_data);
[...]
return 0;
}
在此probe
方法摘录(没有错误检查)中,最后将max77620_gpio_irq_chip
赋予devm_regmap_add_irq_chip
,以便用 IRQ 填充 irq 芯片,然后将 IRQ 芯片添加到 regmap 核心。 此函数还将chip->gpio_irq_data
设置为有效的regmap_irq_chip_data
结构,chip
是允许我们存储此 IRQ 芯片数据以备后用的私有数据结构。 由于此 IRQ 控制器构建在 GPIO 控制器(gpiochip
)之上,因此必须设置gpio_chip.to_irq
字段,这里是max77620_gpio_to_irq
回调。 此回调只返回regmap_irq_get_virq()
返回的值,它根据作为参数给定的偏移量在regmap_irq_chip_data.domain
中创建并返回有效的irq
映射。 其他功能已经推出,对我们来说并不新鲜。
在本节中,我们将介绍使用 regmap 进行 IRQ 管理的全部内容。 您已经准备好将基于 MMIO 的 IRQ 管理转移到 regmap。
本章主要讨论 regmap 核心。 我们介绍了该框架,演练了它的 API,并描述了一些用例。 除了寄存器访问之外,我们还学习了如何使用 regmap 进行基于 MMIO 的 IRQ 管理。 下一章将讨论 MFD 设备和 syscon 框架,将深入使用本章中学到的概念。 在本章结束时,您应该能够开发支持 regmap 的 IRQ 控制器,并且您不会发现自己在重新发明轮子并利用此框架进行寄存器访问。