自学内容网 自学内容网

GIC v3 详解

GIC 中断控制器学习笔记

GIC简介

GIC v3是目前使用最多的中断控制器,版本功能如下:
在这里插入图片描述

GIC v3定义了四种中断类别

SPI (Shared Peripheral Interrupt)
公用的外部设备中断,也定义为共享中断。可以多个Cpu或者说Core处理,不限定特定的Cpu。比如按键触发一个中断,手机触摸屏触发的中断。
PPI (Private Peripheral Interrupt)
私有外设中断。这是每个核心私有的中断。PPI会送达到指定的CPU上,应用场景有CPU本地时钟。
SGI (Software Generated Interrupt)
软件触发的中断。软件可以通过写GICD_SGIR寄存器来触发一个中断事件,一般用于核间通信。
LPI (Locality-specific Peripheral Interrupt)
LPI是GICv3中的新特性,它们在很多方面与其他类型的中断不同。LPI始终是基于消息的中断,它们的配置保存在表中而不是寄存器。比如PCIe的MSI/MSI-x中断。

中断号如下
类别硬件中断号
SGI0 - 15
PPI16 - 31
SPI32 - 1019
RESERVED1024 - 8191
LPI8192 - MAX

典型架构

在这里插入图片描述
其中distributor、redistributor和CPU interface是三个核心部件,distributor处理从硬件连接线上发过来的中断,将中断路由到某个redistributor上去,最后由redistributor发往CPU interface,CPU interface使用IRQ或者FIQ中断线触发CPU中断。

distributor与redistributor是什么关系?

答:distributor负责外设中断SPI处理,redistributor主要负责本地中断SGI、PPI处理。所有的中断SPI、LPI都要通过redistributor发往cpu interface,所以名字上面有个前缀re吧。

distributor的中断流程
  • 外设发起中断,发送给distributor
  • distributor将该中断,分发给合适的re-distributor
  • re-distributor将中断信息,发送给cpu interface。
  • cpu interface产生合适的中断异常给处理器
  • 处理器接收该异常,并且软件处理该中断

处理器接收该异常,并且软件处理该中断

寄存器

gicv3中,多了很多寄存器,这些寄存器分为了2种访问方式,一种是memory-mapped的访问,另一种是系统寄存器访问:

memory-mapped访问的寄存器:
GICC: cpu interface寄存器
GICD: distributor寄存器
GICH: virtual interface控制寄存器,在hypervisor模式访问
GICR: redistributor寄存器
GICV: virtual cpu interface寄存器
GITS: ITS寄存器
根据寄存器名称最后一个字母就很好分辨寄存器的用途。

系统寄存器访问的寄存器:
ICC: 物理 cpu interface 系统寄存器
ICV: 虚拟 cpu interface 系统寄存器
ICH: 虚拟 cpu interface 控制系统寄存器

GIC v3吧CPU需要高速访问的寄存器全部做在了核心内部,这样就不需要通过系统总线访问,提高了中断处理速度,降低了总线数据传输压力。这点从GIC内部架构上也能看出来,下面是v2和v3内部架构对比。系统寄存器也可以通过最后一个字母区分功能。
GIC v2
GIC V3
gic v2的CPU interface在CPU核外部,通过CPU的系统总线进行通信,在中断读取,清除中断等等操作上都要占用系统总线,并且有一定的延时,v3将其拆分,一部分放在CPU内部,一部分留在了外部,访问频繁的寄存器就变成了CPU内部的寄存器,可以直接操作,访问不频繁的就使用memory-mapped来访问。

gic分组

gic吧中断分成了两组:
group0:提供给EL3使用
group1:又分为2组,分别给安全中断和非安全中断使用,这里分组就不展开了。

GIC的初始化

先看一下设备树

gic: interrupt-controller@2c010000 {
                compatible = "arm,gic-v3";               
                #interrupt-cells = <4>;                   
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;
                interrupt-controller;
                redistributor-stride = <0x0 0x40000>;   // 256kB stride
                #redistributor-regions = <2>;
                reg = <0x0 0x2c010000 0 0x10000>,       // GICD
                      <0x0 0x2d000000 0 0x800000>,      // GICR 1: CPUs 0-31
                      <0x0 0x2e000000 0 0x800000>;      // GICR 2: CPUs 32-63
                      <0x0 0x2c040000 0 0x2000>,        // GICC
                      <0x0 0x2c060000 0 0x2000>,        // GICH
                      <0x0 0x2c080000 0 0x2000>;        // GICV
                interrupts = <1 9 4>;

                gic-its@2c200000 {
                        compatible = "arm,gic-v3-its";
                        msi-controller;
                        #msi-cells = <1>;
                        reg = <0x0 0x2c200000 0 0x20000>;
                };

                gic-its@2c400000 {
                        compatible = "arm,gic-v3-its";
                        msi-controller;
                        #msi-cells = <1>;
                        reg = <0x0 0x2c400000 0 0x20000>;
                };
        };
drivers/irqchip/irq-gic-v3.c

static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
/* map设备树中第一个mmio内存,也就是GICD */
dist_base = of_iomap(node, 0);
/* 获取GIC版本号:dist_base + GICD_PIDR2(0xFFE8)
(主要通过读GICD_PIDR2寄存器bit[7:4]. 0x1代表GICv1, 0x2代表GICv2…以此类推)*/
err = gic_validate_dist_version(dist_base);
/* 获取有多少个distributor */
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
nr_redist_regions = 1;

rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs),
     GFP_KERNEL);
/* 映射redistributor的地址进来 */
for (i = 0; i < nr_redist_regions; i++) {
struct resource res;
int ret;

ret = of_address_to_resource(node, 1 + i, &res);
rdist_regs[i].redist_base = of_iomap(node, 1 + i);
if (ret || !rdist_regs[i].redist_base) {
pr_err("%pOF: couldn't map region %d\n", node, i);
err = -ENODEV;
goto out_unmap_rdist;
}
rdist_regs[i].phys_base = res.start;
}
/* 获取redistributor的宽度 */
if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
redist_stride = 0;

gic_enable_of_quirks(node, gic_quirks, &gic_data);

err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,
     redist_stride, &node->fwnode);
if (err)
goto out_unmap_rdist;

gic_populate_ppi_partitions(node);

if (static_branch_likely(&supports_deactivate_key))
gic_of_setup_kvm_info(node);
return 0;
}

IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
static int __init gic_init_bases(void __iomem *dist_base,
 struct redist_region *rdist_regs,
 u32 nr_redist_regions,
 u64 redist_stride,
 struct fwnode_handle *handle)
{
u32 typer;
int err;

if (!is_hyp_mode_available())
static_branch_disable(&supports_deactivate_key);

if (static_branch_likely(&supports_deactivate_key))
pr_info("GIC: Using split EOI/Deactivate mode\n");

gic_data.fwnode = handle;
gic_data.dist_base = dist_base;
gic_data.redist_regions = rdist_regs;
gic_data.nr_redist_regions = nr_redist_regions;
gic_data.redist_stride = redist_stride;

/* 获取支持的最大中断号是多少 */
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
gic_data.rdists.gicd_typer = typer;

gic_enable_quirks(readl_relaxed(gic_data.dist_base + GICD_IIDR),
  gic_quirks, &gic_data);

pr_info("%d SPIs implemented\n", GIC_LINE_NR - 32);
pr_info("%d Extended SPIs implemented\n", GIC_ESPI_NR);

/*
 * ThunderX1 explodes on reading GICD_TYPER2, in violation of the
 * architecture spec (which says that reserved registers are RES0).
 */
if (!(gic_data.flags & FLAGS_WORKAROUND_CAVIUM_ERRATUM_38539))
gic_data.rdists.gicd_typer2 = readl_relaxed(gic_data.dist_base + GICD_TYPER2);

gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
 &gic_data);
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
gic_data.rdists.has_rvpeid = true;
gic_data.rdists.has_vlpis = true;
gic_data.rdists.has_direct_lpi = true;
gic_data.rdists.has_vpend_valid_dirty = true;

if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
err = -ENOMEM;
goto out_free;
}

irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);

gic_data.has_rss = !!(typer & GICD_TYPER_RSS);
pr_info("Distributor has %sRange Selector support\n",
gic_data.has_rss ? "" : "no ");

if (typer & GICD_TYPER_MBIS) {
err = mbi_init(handle, gic_data.domain);
if (err)
pr_err("Failed to initialize MBIs\n");
}
/* 注册中断上半部回调函数 */
set_handle_irq(gic_handle_irq);

gic_update_rdist_properties();
/* 初始化各个寄存器,下面讲 */
gic_dist_init();
gic_cpu_init();
gic_smp_init();
/* 电源管理相关,不展开 */
gic_cpu_pm_init();
gic_syscore_init();
/* 初始化LPI/ITS */
if (gic_dist_supports_lpis()) {
its_init(handle, &gic_data.rdists, gic_data.domain);
its_cpu_init();
} else {
if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_init(handle, gic_data.domain);
}

gic_enable_nmi_support();

return 0;
}
static void __init gic_dist_init(void)
{
/* 禁用 distributor */
writel_relaxed(0, base + GICD_CTLR);
/* 忙等禁用操作完成 */
gic_dist_wait_for_rwp();

/* 吧spi设置为非安全模式 */
for (i = 32; i < GIC_LINE_NR; i += 32)
writel_relaxed(~0, base + GICD_IGROUPR + i / 8);

/* Extended SPI range, not handled by the GICv2/GICv3 common code */
for (i = 0; i < GIC_ESPI_NR; i += 32) {
writel_relaxed(~0U, base + GICD_ICENABLERnE + i / 8);
writel_relaxed(~0U, base + GICD_ICACTIVERnE + i / 8);
}

for (i = 0; i < GIC_ESPI_NR; i += 32)
writel_relaxed(~0U, base + GICD_IGROUPRnE + i / 8);

for (i = 0; i < GIC_ESPI_NR; i += 16)
writel_relaxed(0, base + GICD_ICFGRnE + i / 4);

for (i = 0; i < GIC_ESPI_NR; i += 4)
writel_relaxed(GICD_INT_DEF_PRI_X4, base + GICD_IPRIORITYRnE + i);

/* 等待操作完成 */
gic_dist_config(base, GIC_LINE_NR, gic_dist_wait_for_rwp);

/* 使能distributor */
writel_relaxed(val, base + GICD_CTLR);

/* 初始化中断亲和性,全部绑定中断到当前cpu */
affinity = gic_mpidr_to_affinity(cpu_logical_map(smp_processor_id()));
for (i = 32; i < GIC_LINE_NR; i++)
gic_write_irouter(affinity, base + GICD_IROUTER + i * 8);

for (i = 0; i < GIC_ESPI_NR; i++)
gic_write_irouter(affinity, base + GICD_IROUTERnE + i * 8);
}
/* 填充redistributor */
static int __gic_populate_rdist(struct redist_region *region, void __iomem *ptr)
{
unsigned long mpidr = cpu_logical_map(smp_processor_id());
/*
 * 生成以一个当前核心的中断亲和值
 * #define MPIDR_AFFINITY_LEVEL(mpidr, level) ((mpidr >> (8 * level)) & 0b11111111)
 */
aff = (MPIDR_AFFINITY_LEVEL(mpidr, 3) << 24 |
       MPIDR_AFFINITY_LEVEL(mpidr, 2) << 16 |
       MPIDR_AFFINITY_LEVEL(mpidr, 1) << 8 |
       MPIDR_AFFINITY_LEVEL(mpidr, 0));
/* 读取GICR_TYPER,芯片手册中的偏移地址是0x8,跟这里吻合 */
typer = gic_read_typer(ptr + GICR_TYPER);
/* 如果redistributor绑定的就是当前核心 */
if ((typer >> 32) == aff) {
/* 填充结构体基地址 */
u64 offset = ptr - region->redist_base;
raw_spin_lock_init(&gic_data_rdist()->rd_lock);
gic_data_rdist_rd_base() = ptr;
gic_data_rdist()->phys_base = region->phys_base + offset;
return 0;
}
}

在这里插入图片描述
可以看到GICR_TYPER的偏移量是0x8,只读模式,其中记录了redistributor属于哪一个CPU核心和一些其他属性。
在这里插入图片描述
这里的GICR_TYPER值是64位的,其中高32位是CPU亲和性属性。一共四组,每组8位,分表表示不同安全级别下绑定的CPU核心。

static int gic_populate_rdist(void)
{
if (gic_iterate_rdists(__gic_populate_rdist) == 0)
return 0;
}
static void gic_cpu_init(void)
{
/* 填充redistributor结构体基地址 */
if (gic_populate_rdist())
return;
/* 使能redistributor 就是写GICR_WAKER,然后忙等结果,不展开了 */
gic_enable_redist(true);
/* 配置SGIs/PPIs为非安全模式 */
for (i = 0; i < gic_data.ppi_nr + 16; i += 32)
writel_relaxed(~0, rbase + GICR_IGROUPR0 + i / 8);
/* 清除PPI\SGI中断设置默认中断优先级 */
gic_cpu_config(rbase, gic_data.ppi_nr + 16, gic_redist_wait_for_rwp);
/* 初始化系统寄存器 */
gic_cpu_sys_reg_init();
}

void gic_cpu_config(void __iomem *base, int nr, void (*sync_access)(void))
{
/* 遍历每个redistributor清除他们的PPI和SGI中断 */
for (i = 0; i < nr; i += 32) {
writel_relaxed(GICD_INT_EN_CLR_X32,
       base + GIC_DIST_ACTIVE_CLEAR + i / 8);
writel_relaxed(GICD_INT_EN_CLR_X32,
       base + GIC_DIST_ENABLE_CLEAR + i / 8);
}

/* 初始化PPI和SGI中断优先级 */
for (i = 0; i < nr; i += 4)
writel_relaxed(GICD_INT_DEF_PRI_X4,
base + GIC_DIST_PRI + i * 4 / 4);
}

在这里插入图片描述
GIC_DIST_ACTIVE_CLEAR的地址位0x380,可读写,每个redistributor一个,长度为4字节,代表32个中断

在这里插入图片描述
GIC_DIST_PRI地址为0x400,4字节,每个redistributor一个,值越低优先级越高

static void gic_cpu_sys_reg_init(void)
{
int i, cpu = smp_processor_id();
u64 mpidr = cpu_logical_map(cpu);
u64 need_rss = MPIDR_RS(mpidr);
bool group0;
u32 pribits;

/*
 * Need to check that the SRE bit has actually been set. If
 * not, it means that SRE is disabled at EL2. We're going to
 * die painfully, and there is nothing we can do about it.
 *
 * Kindly inform the luser.
 */
if (!gic_enable_sre())
pr_err("GIC: unable to set SRE (disabled at EL2), panic ahead\n");

pribits = gic_get_pribits();

group0 = gic_has_group0();

/* 配置中断优先级过滤为默认优先级,高于这个优先级的中断才会发往CPU
通过ICC_PMR_EL1寄存器配置 */
if (!gic_prio_masking_enabled())
write_gicreg(DEFAULT_PMR_VALUE, ICC_PMR_EL1);

gic_write_bpr1(0);

if (static_branch_likely(&supports_deactivate_key)) {
gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop);
} else {
gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop_dir);
}

if (group0) {
switch(pribits) {
case 8:
case 7:
write_gicreg(0, ICC_AP0R3_EL1);
write_gicreg(0, ICC_AP0R2_EL1);
fallthrough;
case 6:
write_gicreg(0, ICC_AP0R1_EL1);
fallthrough;
case 5:
case 4:
write_gicreg(0, ICC_AP0R0_EL1);
}

isb();
}

switch(pribits) {
case 8:
case 7:
write_gicreg(0, ICC_AP1R3_EL1);
write_gicreg(0, ICC_AP1R2_EL1);
fallthrough;
case 6:
write_gicreg(0, ICC_AP1R1_EL1);
fallthrough;
case 5:
case 4:
write_gicreg(0, ICC_AP1R0_EL1);
}

isb();

gic_write_grpen1(1);

/* RSS */
per_cpu(has_rss, cpu) = !!(gic_read_ctlr() & ICC_CTLR_EL1_RSS);

/* 检查SGI能力 */
for_each_online_cpu(i) {
bool have_rss = per_cpu(has_rss, i) && per_cpu(has_rss, cpu);

need_rss |= MPIDR_RS(cpu_logical_map(i));
if (need_rss && (!have_rss))
pr_crit("CPU%d (%lx) can't SGI CPU%d (%lx), no RSS\n",
cpu, (unsigned long)mpidr,
i, (unsigned long)cpu_logical_map(i));
}
}

上面主要是操作寄存器对distributor做一些初始值的配置。

/* 初始化核间中断,主要是配置domain */
static void __init gic_smp_init(void)
{
struct irq_fwspec sgi_fwspec = {
.fwnode= gic_data.fwnode,
.param_count= 1,
};
int base_sgi;

cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
  "irqchip/arm/gicv3:starting",
  gic_starting_cpu, NULL);

/* 分配8个SGIs的desc */
base_sgi = __irq_domain_alloc_irqs(gic_data.domain, -1, 8,
   NUMA_NO_NODE, &sgi_fwspec,
   false, NULL);

set_smp_ipi_range(base_sgi, 8);
}

中断处理

见:arm64 中断处理流程

中断亲和性、中断绑定

中断的绑定实际上就是操作一下distributor的寄存器就可以了,我们看一下芯片手册:
在这里插入图片描述
GICD_IROUTER寄存器控制着SPI中断的路由配置,可以看到每一个SPI硬件中断号对应于一个寄存器,0-31是没有使用的,寄存器中使用8位来记录中断的路由,一共4组,每组对应一个异常级别。
在这里插入图片描述

gicv3使用hierarchy来标识一个具体的core, 上面是一个4层的结构(aarch64)
<affinity level 3>.<affinity level 2>.<affinity level 1>.<affinity level 0> 组成一个PE的路由。
每一个core的affnity值可以通过MPDIR_EL1寄存器获取, 每一个affinity占用8bit.
配置对应core的MPIDR值,可以将中断路由到该core上。

各个affinity的定义是根据SOC自己的定义
比如可能affinity3代表socketid,affinity2 代表clusterid, affnity1代表coreid, affnity0代表thread id.

下面跟着源码看一下。

static int gic_set_affinity(struct irq_data *d, const struct cpumask *mask_val,
    bool force)
{
unsigned int cpu;
u32 offset, index;
void __iomem *reg;
int enabled;
u64 val;
/* 获取绑定的cpu */
if (force)
cpu = cpumask_first(mask_val);
else
cpu = cpumask_any_and(mask_val, cpu_online_mask);

if (cpu >= nr_cpu_ids)
return -EINVAL;
/* 如果中断号属于SGI、PPI则不允许绑定 */
if (gic_irq_in_rdist(d))
return -EINVAL;

/* 禁用这个中断 */
enabled = gic_peek_irq(d, GICD_ISENABLER);
if (enabled)
gic_mask_irq(d);

/* 算出中断对应的偏移 */
offset = convert_offset_index(d, GICD_IROUTER, &index);
/* 根据偏移得到寄存器 */
reg = gic_dist_base(d) + offset + (index * 8);
/* 转换CPU为具体寄存器值 */
val = gic_mpidr_to_affinity(cpu_logical_map(cpu));
/* 写入寄存器 */
gic_write_irouter(val, reg);
return IRQ_SET_MASK_OK_DONE;
}

LPI消息中断

GIC v3支持消息中断LPI,只要往GIC某个寄存器写入一个值就能触发一个中断,消息中断有几个好处:

  • 避免了繁杂的中断线
  • 可以拥有相当多的中断号,避免了共用中断号的问题(优化PCIE INTX中断共用问题)
  • 可以保证数据先于中断到达内存(优化PCIE INTX中断比数据先到达内存问题)

LPI的触发可以通过两种方式:

  1. forwarding方式
  2. ITS

LPI的配置是保存在memory的表中,而不是保存在gic的寄存器中,LPI两个表的寄存器如下:

  • GICR_PROPBASER:LPI中断配置表基地址
  • GICR_PENDBASER:LPI中断状态表基地址
    为了加速查表,GIC内部也会缓存配置表,软件上修改了配置表的值需要invalid cache一下

GICR_PROPBASER寄存器:
在这里插入图片描述
中断配置表里面每个字节表示一个中断,其中高6位代表优先级,最低为代表使能:
在这里插入图片描述

GICR_PENDBASER寄存器:
在这里插入图片描述
中断pendding表里面每个位代表一个中断,0代表中断没有发生,1代表中断发生

forwarding LPI中断

forwarding LPI中断的中断流程:

  • 外设直接将中断号写入GICR_SETLPIR
  • re-distributor产生中断,发送给cpu interface
  • cpu interface产生合适的中断异常给处理器

forwarding方式的实现逻辑图如下:
在这里插入图片描述

forwarding方式的LPI主要是通过以下几个寄存器实现:

  • GICR_SETLPIR
  • GICR_CLRLPIR
  • GICR_INVLPIR
  • GICR_INVALLR
  • GICR_SYNCR

GICR_SETLPIR寄存器:
在这里插入图片描述
将指定的LPI中断设置为pending状态,由外设写入值,寄存器低32位代表LPI中断号,可以通过memory-mapped方式读取。每个redistributor都有一个相同的寄存器,地址相同

GICR_CLRLPIR寄存器:
在这里插入图片描述
清除pending状态,内核向这个寄存器写入中断号就能清除中断。可以通过memory-mapped方式写入,每个CPU核心都有一个复制。
GICR_INVLPIR寄存器:
在这里插入图片描述
invalid某个中断的cache状态,让GIC重新从内存中载入新的值

GICR_INVALLR寄存器:
无效所有LPI配置缓存

GICR_SYNCR寄存器:
在这里插入图片描述

等待GIC操作完成,比如写 GICR_CLRLPIR 、GICR_INVLPIR 、GICR_INVALLR

GIC ITS

forwarding方式需要外设知道中断号,并且直接写入GICR_SETLPIR,一般都不适用forwarding模式,比如pcie就是使用的ITS模式。

ITS中断的中断流程:

  • 外设发起中断,发送给ITS
  • ITS分析中断,决定将来发送的re-distributor
  • ITS将中断发送给合适的re-distributor
  • re-distributor将中断信息,发送给cpu interface。
  • cpu interface产生合适的中断异常给处理器
  • 处理器接收该异常,并且软件处理该中断

ITS跟distributor是什么关系,ITS负责消息中断的分发,可以变相理解ITS就是LPI的distributor,从下面这个架构图可以比较直观的看出他们的关系distributor与ITS都起到了路由的作用。
在这里插入图片描述
ITS模式逻辑图:
在这里插入图片描述
外设写GITS_TRANSLATER寄存器,发起LPI中断。写操作,给ITS提供2个信息:

  • EventID:值保存在GITS_TRANSLATER寄存器中,表示外设发送中断的事件类型
  • DeviceID:表示哪一个外设发起LPI中断。该值的传递,是实现自定义,例如,可以使用AXI的user信号来传递。

ITS将DeviceID和eventID,通过一系列查表,得到LPI中断号,再使用LPI中断号查表,得到该中断的目标cpu。
ITS将LPI中断号,LPI中断对应的目标cpu,发送给对应的redistributor。
redistributor再将该中断信息,发送给CPU。

ITS使用三类表格,实现LPI的转换和映射:

  • device table: 映射deviceID到中断转换表
  • interrupt translation table:映射EventID到INTID。以及INTID属于的collection组
  • collection table:映射collection到redistributor

在这里插入图片描述

  • 使用DeviceID,从设备表(device table)中选择索引为DeviceID的表项。从该表项中,得到中断映射表的位置
  • 使用EventID,从中断映射表中选择索引为EventID的表项。得到中断号,以及中断所属的collection号
  • 使用collection号,从collection表格中,选择索引为collection号的表项。得到redistributor的映射信息
  • 根据collection表项的映射信息,将中断信息,发送给对应的redistributor

理论上来说有Device Table表和Interrupt Translation Tables两个表就够了,为什么还要多弄一个Collection表?
答:

通过GITS_BASER寄存器指定Traslation表的基地址,每个CPU核心有一个复制,另外一些表的寄存器我们就不看了,后面直接看代码。
在这里插入图片描述
在这里插入图片描述

ITS命令消息

ITS使用命令消息来配置前面说的三个表,命令也是通过mmap内存写入,这里有三个关键的寄存器CBASER、CREADR、CWRITER
在这里插入图片描述

  • CBASER:指向一个内存空间,代表命令存放的环形队列,命令就放入这个环形队列中
  • CREADR:指示GIC目前读取环形队列的位置,读取GIC处理了多少数据
  • CWRITER:指示当前环形缓冲区写入位置,告诉GIC写入数据了
    在这里插入图片描述

ITS每个命令32字节。ITS 命令队列基地址是 64KB 对齐的,队列大小 4KB 对齐。
下面是ITS实际的命令队列:
在这里插入图片描述
大概就是这么一个队列,三个寄存器分别指向头,读取位置,写入位置,下面我们开始看代码:

我们从gic_init_bases开始看,在gic_init_bases的结尾初始化ITS,分别调用了its_initits_cpu_init两个关键函数

static int __init gic_init_bases(void __iomem *dist_base,
 struct redist_region *rdist_regs,
 u32 nr_redist_regions,
 u64 redist_stride,
 struct fwnode_handle *handle)
{
if (gic_dist_supports_lpis()) {
its_init(handle, &gic_data.rdists, gic_data.domain);
its_cpu_init();
} else {
if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_init(handle, gic_data.domain);
}

gic_enable_nmi_support();

return 0;
}

its_init里面主要调用了its_of_probeallocate_lpi_tables来初始化ITS和LPI

int __init its_init(struct fwnode_handle *handle, struct rdists *rdists,
    struct irq_domain *parent_domain)
{
of_node = to_of_node(handle);
if (of_node)
its_of_probe(of_node);
else
its_acpi_probe();

err = allocate_lpi_tables();
if (err)
return err;
return 0;
}

its的初始化就省略写了,感兴趣的可以结合代码去深入探索

static int __init its_probe_one(struct resource *res,
struct fwnode_handle *handle, int numa_node)
{
/* 分配CBASE表内存,也就是命令队列内存 */
page = alloc_pages_node(its->numa_node, gfp_flags,
get_order(ITS_CMD_QUEUE_SZ));

its->cmd_base = (void *)page_address(page);
its->cmd_write = its->cmd_base;
its->fwnode_handle = handle;
its->get_msi_base = its_irq_get_msi_base;
its->msi_domain_flags = IRQ_DOMAIN_FLAG_MSI_REMAP;
/* 分配设备表 */
err = its_alloc_tables(its);

/* 分配collection表 */
err = its_alloc_collections(its);

/* 将命令表基地址写入CBASE寄存器 */
baser = (virt_to_phys(its->cmd_base)|
 GITS_CBASER_RaWaWb|
 GITS_CBASER_InnerShareable|
 (ITS_CMD_QUEUE_SZ / SZ_4K - 1)|
 GITS_CBASER_VALID);

gits_write_cbaser(baser, its->base + GITS_CBASER);
tmp = gits_read_cbaser(its->base + GITS_CBASER);
/* 清除CWRITER寄存器 */
gits_write_cwriter(0, its->base + GITS_CWRITER);
ctlr = readl_relaxed(its->base + GITS_CTLR);
ctlr |= GITS_CTLR_ENABLE;
if (is_v4(its))
ctlr |= GITS_CTLR_ImDe;
writel_relaxed(ctlr, its->base + GITS_CTLR);

/* 生成一个domain */
err = its_init_domain(handle, its);
return 0;
}

static int __init its_of_probe(struct device_node *node)
{
for (np = of_find_matching_node(node, its_device_id); np;
its_probe_one(&res, &np->fwnode, of_node_to_nid(np));
}
return 0;
}

下面看一下lpi初始化

static int __init allocate_lpi_tables(void)
{
/* 分配lpi配置表 */
err = its_setup_lpi_prop_table();

for_each_possible_cpu(cpu) {
struct page *pend_page;
/* 分配lpi pedding表 */
pend_page = its_allocate_pending_table(GFP_NOWAIT);
if (!pend_page) {
pr_err("Failed to allocate PENDBASE for CPU%d\n", cpu);
return -ENOMEM;
}

gic_data_rdist_cpu(cpu)->pend_page = pend_page;
}

return 0;
}

下面看一下后续its_cpu_init的初始化流程

int its_cpu_init(void)
{
if (!list_empty(&its_nodes)) {
int ret;

ret = redist_disable_lpis();
if (ret)
return ret;

its_cpu_init_lpis();
its_cpu_init_collections();
}

return 0;
}
static void its_cpu_init_lpis(void)
{
/* 将配置表基地址写入寄存器 */
val = (gic_rdists->prop_table_pa |
       GICR_PROPBASER_InnerShareable |
       GICR_PROPBASER_RaWaWb |
       ((LPI_NRBITS - 1) & GICR_PROPBASER_IDBITS_MASK));
gicr_write_propbaser(val, rbase + GICR_PROPBASER);
tmp = gicr_read_propbaser(rbase + GICR_PROPBASER);

/* 将pendding表基地址写入寄存器 */
val = (page_to_phys(pend_page) |
       GICR_PENDBASER_InnerShareable |
       GICR_PENDBASER_RaWaWb);
gicr_write_pendbaser(val, rbase + GICR_PENDBASER);
tmp = gicr_read_pendbaser(rbase + GICR_PENDBASER);
}
static void its_cpu_init_collections(void)
{
struct its_node *its;

raw_spin_lock(&its_lock);

list_for_each_entry(its, &its_nodes, entry)
its_cpu_init_collection(its);

raw_spin_unlock(&its_lock);
}
static void its_cpu_init_collection(struct its_node *its)
{
int cpu = smp_processor_id();


its->collections[cpu].target_address = target;
its->collections[cpu].col_id = cpu;
/* 通过命令的方式配置collections表 */
its_send_mapc(its, &its->collections[cpu], 1);
its_send_invall(its, &its->collections[cpu]);
}

至此ITS的初始化就完成了,下面说一下ITS命令。

ITS使用命令来填他的三个转换表,基本的ITS命令如下:

命令命令参数描述
CLEARDeviceID, EventID将EventID和DeviceID定义的event转化为ICID和pINTID,并发送到合适的Redistributor移除pending状态
DISCARDDeviceID, EventID将EventID和DeviceID定义的event转化为ICID和pINTID,并发送到合适的Redistributor移除pending状态。它也保证与特定的EventID相关的Redistributor的任何缓存与内存中配置保持一致。DISCARD移除了ITT中DeviceID和EventID的映射,保证特定EventID的请求被丢弃。
INTDeviceID, EventID将EventID和DeviceID定义的event转化为ICID和pINTID,并发送到合适的Redistributor设置pending状态
INVDeviceID, EventID指定ITS必须保证与特定EventID相关的Redistributor的任何缓存与内存中LPI配置表保持一致
INVALLICID指定ITS必须保证由ICID定义的中断collection的任何缓存与所有Redistributor内存中LPI配置表保持一致
INVDB仅GICv4.1 vPEID仅GICv4.1。指定ITS必须保证vPEID的默认doorbell相关的任何缓存与所有Redistributor内存中LPI配置表保持一致
MAPCICID, RDbase将ICID定义的collection表映射到RDbase定义的Redistributor
MAPDDeviceID,ITT_addr,Size将DeviceID相关的设备表项映射到由ITT_addr和Size定义的ITT
MAPIDeviceID,EventID, ICID将EventID和DeviceID定义的event映射到ICID和pINTID=EventID的ITT项
MAPTIDeviceID,EventID, pINTID, ICID将EventID和DeviceID定义的event映射到它相关的ITE,ITE由DeviceID相关的ITT中ICID和pINTID定义
MOVALLRDbase1, RDbase2让RDbase1指定的Redistributor上所有的中断移到RDbase2指定的Redistributor上
MOVIDeviceID,EventID, ICID更新DeviceID和EventID定义的event的ITT表项的ICID域。它也将DeviceID和EventID定义的event转化为ICID和pINTID,并在合适的Redistributor移除pending状态,如果它被设置,将中断移到新的ICID定义的Redistributor,并使用新的ICID更新event相关的ITE
SYNCRDbase保证RDbase指定的Redistributor的物理中断相关的所有未完成ITS操作在执行任何更多ITS命令之前全局可见。跟随SYNC的执行,所有之前命令必须应用于后续写入GITS_TRANSLATE。
VINVALLvPEID保证与vPEID相关的任何缓存的Redistributor信息与内存中相关的LPI配置表保持一致
VMAPIDeviceID, EventID,Dbell_pINTID,vPEID将由DeviceID和EventID定义的event映射到ITT表项,该表项vPEID, vINTID=EventID, Dbell_pINTID,一个doorbell
VMAPPGICv4.0 vPEID,RDbase,VPT_addr,VPT_size将vPEID定义的vPE表项映射到目标RDbase,包括相关的虚拟LPI pending表(VPT_addr, VPT_size)
VMAPPGICv4.1 vPEID,RDbase, VCONF_addr,VPT_addr,VPT_size, PTZ, Alloc,Default_Doorbell_pINTID映射vPEID定义的vPE,包括相关的虚拟LPI配置和pending表。可选的指定默认doorbell。
VMAPTIDeviceID, EventID, vINTID, Dbell_pINTID,vPEID使用vPEID,vINTID,Dbell_pINTID和doorbell将DeviceID和EventID定义的event映射到ITT表项
VMOVIDeviceID, EventID, vPEID为DeviceID和EventID定义的event更新ITT表项的vPEID域。将DeviceID和EventID定义的event转化为vPEID和pINTID,让合适的Redistributor移除pending状态,如果有设置,将中断发送到新vPEID定义的Redistributor,使用新的vPEID更新event相关的ITE
VMOVPGICv4.0 vPEID, RDbase, SequenceNumber, ITSList更新vPEID定义的vPE表项到由RDbase定义的目标Redistributor。软件必须使用SequenceNumber和ITSList在超过一个ITS的情况下同步VMOVP命令的执行
VMOVPGICv4.1 vPEID, RDbase, SequenceNumber, ITSList, Default_Doorbell_pINTID将vPEID定义的vPE映射更新到RDbase定义的目标Redistributor
VSGI仅GICv4.1 vPEID, Priority, G, C, E, vPEID仅GICv4.1。对于vPEID定义的vPE,设置配置或更新由vINTID定义的中断状态
VSYNCvPEID保证在任何更多ITS命令被执行之前所有未完成的ITS操作被全局可见。VSYNC接下来的执行,所有之前的命令必须应用到后续写GITS_TRANSLATER。

最主要的命令有下面三个:

  • MAPD:填充Device Table
  • MAPI、MAPTI:填充Interrupt Traslation Table表(ITT)
  • MAPC :填充Collection Table

ITS表的配置原理

下面来看一下代码里面是怎么组织命令和发送命令的,结合mapd命令来一窥命令的发送过程。

/* Warning, macro hell follows */
#define BUILD_SINGLE_CMD_FUNC(name, buildtype, synctype, buildfn)\
void name(struct its_node *its,\
  buildtype builder,\
  struct its_cmd_desc *desc)\
{\
struct its_cmd_block *cmd, *sync_cmd, *next_cmd;\
synctype *sync_obj;\
unsigned long flags;\
u64 rd_idx;\
\
raw_spin_lock_irqsave(&its->lock, flags);\
\
cmd = its_allocate_entry(its);\
if (!cmd) {/* We're soooooo screewed... */\
raw_spin_unlock_irqrestore(&its->lock, flags);\
return;\
}\
sync_obj = builder(its, cmd, desc);\
its_flush_cmd(its, cmd);\
\
if (sync_obj) {\
sync_cmd = its_allocate_entry(its);\
if (!sync_cmd)\
goto post;\
\
buildfn(its, sync_cmd, sync_obj);\
its_flush_cmd(its, sync_cmd);\
}\
\
post:\
rd_idx = readl_relaxed(its->base + GITS_CREADR);\
next_cmd = its_post_commands(its);\
raw_spin_unlock_irqrestore(&its->lock, flags);\
\
if (its_wait_for_range_completion(its, rd_idx, next_cmd))\
pr_err_ratelimited("ITS cmd %ps failed\n", builder);\
}

上面是一个最关键的代码块,他定义命令怎么组织和发送,关键的流程如下:

  • its_allocate_entry
  • builder
  • its_flush_cmd
  • its_allocate_entry
  • buildfn
  • its_flush_cmd
  • its_post_commands
static int its_queue_full(struct its_node *its)
{
int widx;
int ridx;
/* 写指针位置,由代码更新 */
widx = its->cmd_write - its->cmd_base;
/* 读指针的位置,由ITS硬件更新 */
ridx = readl_relaxed(its->base + GITS_CREADR) / sizeof(struct its_cmd_block);

/* This is incredibly unlikely to happen, unless the ITS locks up. */
if (((widx + 1) % ITS_CMD_QUEUE_NR_ENTRIES) == ridx)
return 1;

return 0;
}

static struct its_cmd_block *its_allocate_entry(struct its_node *its)
{
struct its_cmd_block *cmd;
u32 count = 1000000;/* 1s! */
/* 如果队列是满的就等待ITS处理 */
while (its_queue_full(its)) {
count--;
if (!count) {
pr_err_ratelimited("ITS queue not draining\n");
return NULL;
}
cpu_relax();
udelay(1);
}
/* 写指针加一 */
cmd = its->cmd_write++;

/* 环形队列掉头 */
if (its->cmd_write == (its->cmd_base + ITS_CMD_QUEUE_NR_ENTRIES))
its->cmd_write = its->cmd_base;

/* 清空数据  */
cmd->raw_cmd[0] = 0;
cmd->raw_cmd[1] = 0;
cmd->raw_cmd[2] = 0;
cmd->raw_cmd[3] = 0;

return cmd;
}

上面对应宏定义代码块中得的its_allocate_entry流程,它从命令队列里面取出下一个空闲的命令空间并返回,如果没有剩余空间就等待

/* 定义命令刷新函数 */
static void its_build_sync_cmd(struct its_node *its,
       struct its_cmd_block *sync_cmd,
       struct its_collection *sync_col)
{
its_encode_cmd(sync_cmd, GITS_CMD_SYNC);
its_encode_target(sync_cmd, sync_col->target_address);
/* 大小端转换 */
its_fixup_cmd(sync_cmd);
}
/* 定义its_send_single_command函数 */
static BUILD_SINGLE_CMD_FUNC(its_send_single_command, its_cmd_builder_t,
     struct its_collection, its_build_sync_cmd)

上面定义了最关键的its_send_single_command函数用来发送命令,还有一个虚拟化用的发送命令,不展开,开新坑再说。

/* 由上面宏定义代码块回调来构建mapd命令 */
static struct its_collection *its_build_mapd_cmd(struct its_node *its,
 struct its_cmd_block *cmd,
 struct its_cmd_desc *desc)
{
unsigned long itt_addr;
u8 size = ilog2(desc->its_mapd_cmd.dev->nr_ites);

itt_addr = virt_to_phys(desc->its_mapd_cmd.dev->itt);
itt_addr = ALIGN(itt_addr, ITS_ITT_ALIGN);

its_encode_cmd(cmd, GITS_CMD_MAPD);
its_encode_devid(cmd, desc->its_mapd_cmd.dev->device_id);
its_encode_size(cmd, size - 1);
its_encode_itt(cmd, itt_addr);
its_encode_valid(cmd, desc->its_mapd_cmd.valid);

its_fixup_cmd(cmd);

return NULL;
}
/* 发送mapd命令 */
static void its_send_mapd(struct its_device *dev, int valid)
{
struct its_cmd_desc desc;

desc.its_mapd_cmd.dev = dev;
desc.its_mapd_cmd.valid = !!valid;

its_send_single_command(dev->its, its_build_mapd_cmd, &desc);
}

上面对应builder流程,是构造cmd的关键逻辑,具体构造流程下面讲解。

static void its_flush_cmd(struct its_node *its, struct its_cmd_block *cmd)
{
/*
 * Make sure the commands written to memory are observable by
 * the ITS.
 */
if (its->flags & ITS_FLAGS_CMDQ_NEEDS_FLUSHING)
gic_flush_dcache_to_poc(cmd, sizeof(*cmd));
else
dsb(ishst);
}

上面对应its_flush_cmd流程,原理就是吧cache数据刷到内存中,感兴趣的自己展开看看,具体实现在arch/arm64/include/asm/arch_gicv3.h中,如果builder需要做sync操作会返回值,然后根据宏定义代码块组织发送sync命令。最后的post操作就不展开了,原理就是更新CWRITER寄存器的值,告诉GIC有命令写入,然后根据CREADR寄存器的值等待GIC处理完成。

下面看看cmd的构造方法:

/*
 * The ITS command block, which is what the ITS actually parses.
 */
struct its_cmd_block {
union {
u64raw_cmd[4];
__le64raw_cmd_le[4];
};
};

static void its_mask_encode(u64 *raw_cmd, u64 val, int h, int l)
{
u64 mask = GENMASK_ULL(h, l);
*raw_cmd &= ~mask;
*raw_cmd |= (val << l) & mask;
}

static void its_encode_cmd(struct its_cmd_block *cmd, u8 cmd_nr)
{
its_mask_encode(&cmd->raw_cmd[0], cmd_nr, 7, 0);
}
static void its_encode_devid(struct its_cmd_block *cmd, u32 devid)
{
its_mask_encode(&cmd->raw_cmd[0], devid, 63, 32);
}
static void its_encode_event_id(struct its_cmd_block *cmd, u32 id)
{
its_mask_encode(&cmd->raw_cmd[1], id, 31, 0);
}
static void its_encode_phys_id(struct its_cmd_block *cmd, u32 phys_id)
{
its_mask_encode(&cmd->raw_cmd[1], phys_id, 63, 32);
}

其实就是往cmd特殊位置填充值,这里就不展开了,到此ITS整个流程就分析结束了。总结来说就是观察ITS队列有空闲位置就往空闲位置里面写入数据,数据写入之后将cache刷出内存,如果要等待ITS响应命令就用sync命令去等待ITS操作。

PCIE MSI中断注册流程

结合上面的所有分析,下面来看一下一个具体的PCIE设备是怎么注册一个ITS中断的
在这里插入图片描述
pcie控制器从中断角度看逻辑上是级联在了ITS下面,pcie设备使用msi消息向pcie控制器写入一个msi中断,然后pcie控制器将msi中断转换成LPI消息写到ITS中去,从而引发LPI中断。下面看一下代码

drivers/irqchip/irq-gic-v3-its-pci-msi.c

static struct of_device_id its_device_id[] = {
{.compatible= "arm,gic-v3-its",},
{},
};
static int __init its_pci_of_msi_init(void)
{
struct device_node *np;

for (np = of_find_matching_node(NULL, its_device_id); np;
     np = of_find_matching_node(np, its_device_id)) {
if (!of_device_is_available(np))
continue;
if (!of_property_read_bool(np, "msi-controller"))
continue;

if (its_pci_msi_init_one(of_node_to_fwnode(np), np->full_name))
continue;

pr_info("PCI/MSI: %pOF domain created\n", np);
}

return 0;
}

gic驱动中专门预留了pcie中断的处理模块,上面首先遍历gic设备树,如果设备树中有msi-controller属性的就调用its_pci_msi_init_one初始化一个pcie domian,代码比较简单,就不展开了。最终的irq domain将形成一个级联的结构,类似下面这样:

GIC domain
ITS domain
PCIE domain

下面看一下中断注册:

static int its_pci_msi_prepare(struct irq_domain *domain, struct device *dev,
       int nvec, msi_alloc_info_t *info)
{
struct pci_dev *pdev, *alias_dev;
struct msi_domain_info *msi_info;
int alias_count = 0, minnvec = 1;

if (!dev_is_pci(dev))
return -EINVAL;

msi_info = msi_get_domain_info(domain->parent);

pdev = to_pci_dev(dev);
/*
 * If pdev is downstream of any aliasing bridges, take an upper
 * bound of how many other vectors could map to the same DevID.
 */
pci_for_each_dma_alias(pdev, its_get_pci_alias, &alias_dev);
if (alias_dev != pdev && alias_dev->subordinate)
pci_walk_bus(alias_dev->subordinate, its_pci_msi_vec_count,
     &alias_count);

/* ITS specific DeviceID, as the core ITS ignores dev. */
info->scratchpad[0].ul = pci_msi_domain_get_msi_rid(domain, pdev);

/*
 * Always allocate a power of 2, and special case device 0 for
 * broken systems where the DevID is not wired (and all devices
 * appear as DevID 0). For that reason, we generously allocate a
 * minimum of 32 MSIs for DevID 0. If you want more because all
 * your devices are aliasing to DevID 0, consider fixing your HW.
 */
nvec = max(nvec, alias_count);
if (!info->scratchpad[0].ul)
minnvec = 32;
nvec = max_t(int, minnvec, roundup_pow_of_two(nvec));
return msi_info->ops->msi_prepare(domain->parent, dev, nvec, info);
}

static struct msi_domain_ops its_pci_msi_ops = {
.msi_prepare= its_pci_msi_prepare,
};

当一个PCIE设备初始化中断的时候会调用到domain的msi_prepare这里来,这里面主要填充的就是info->scratchpad[0].ul这个数据,他将交给上级domain:ITS去作为devices id继续注册ITS中断。
drivers/irqchip/irq-gic-v3-its.c

static int its_msi_prepare(struct irq_domain *domain, struct device *dev,
   int nvec, msi_alloc_info_t *info)
{
struct its_node *its;
struct its_device *its_dev;
struct msi_domain_info *msi_info;
u32 dev_id;
int err = 0;

/*
 * We ignore "dev" entirely, and rely on the dev_id that has
 * been passed via the scratchpad. This limits this domain's
 * usefulness to upper layers that definitely know that they
 * are built on top of the ITS.
 */
dev_id = info->scratchpad[0].ul;

msi_info = msi_get_domain_info(domain);
its = msi_info->data;

if (!gic_rdists->has_direct_lpi &&
    vpe_proxy.dev &&
    vpe_proxy.dev->its == its &&
    dev_id == vpe_proxy.dev->device_id) {
/* Bad luck. Get yourself a better implementation */
WARN_ONCE(1, "DevId %x clashes with GICv4 VPE proxy device\n",
  dev_id);
return -EINVAL;
}

mutex_lock(&its->dev_alloc_lock);
its_dev = its_find_device(its, dev_id);
if (its_dev) {
/*
 * We already have seen this ID, probably through
 * another alias (PCI bridge of some sort). No need to
 * create the device.
 */
its_dev->shared = true;
pr_debug("Reusing ITT for devID %x\n", dev_id);
goto out;
}

its_dev = its_create_device(its, dev_id, nvec, true);
if (!its_dev) {
err = -ENOMEM;
goto out;
}

pr_debug("ITT %d entries, %d bits\n", nvec, ilog2(nvec));
out:
mutex_unlock(&its->dev_alloc_lock);
info->scratchpad[0].ptr = its_dev;
return err;
}

static struct msi_domain_ops its_msi_domain_ops = {
.msi_prepare= its_msi_prepare,
};

可以看到在ITS里面将info->scratchpad[0].ul作为device id拿去注册ITS中断去了,到此所有流程结束。

参考

ARM GICv3中断控制器
ARM GIC(四) GIC V3 中断线映射&关键寄存器配置 分析笔记
GICv3和GICv4虚拟化
GIC/ITS代码分析(3)ITS驱动初始化


原文地址:https://blog.csdn.net/qq_16054639/article/details/142334293

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!