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中断。
中断号如下
类别 | 硬件中断号 |
---|---|
SGI | 0 - 15 |
PPI | 16 - 31 |
SPI | 32 - 1019 |
RESERVED | 1024 - 8191 |
LPI | 8192 - 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的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>; // GICVinterrupts = <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.cstatic 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);elsecpu = 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的触发可以通过两种方式:
- forwarding方式
- 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_init
和its_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_probe
和allocate_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);elseits_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命令如下:
命令 | 命令参数 | 描述 |
---|---|---|
CLEAR | DeviceID, EventID | 将EventID和DeviceID定义的event转化为ICID和pINTID,并发送到合适的Redistributor移除pending状态 |
DISCARD | DeviceID, EventID | 将EventID和DeviceID定义的event转化为ICID和pINTID,并发送到合适的Redistributor移除pending状态。它也保证与特定的EventID相关的Redistributor的任何缓存与内存中配置保持一致。DISCARD移除了ITT中DeviceID和EventID的映射,保证特定EventID的请求被丢弃。 |
INT | DeviceID, EventID | 将EventID和DeviceID定义的event转化为ICID和pINTID,并发送到合适的Redistributor设置pending状态 |
INV | DeviceID, EventID | 指定ITS必须保证与特定EventID相关的Redistributor的任何缓存与内存中LPI配置表保持一致 |
INVALL | ICID | 指定ITS必须保证由ICID定义的中断collection的任何缓存与所有Redistributor内存中LPI配置表保持一致 |
INVDB | 仅GICv4.1 vPEID | 仅GICv4.1。指定ITS必须保证vPEID的默认doorbell相关的任何缓存与所有Redistributor内存中LPI配置表保持一致 |
MAPC | ICID, RDbase | 将ICID定义的collection表映射到RDbase定义的Redistributor |
MAPD | DeviceID,ITT_addr,Size | 将DeviceID相关的设备表项映射到由ITT_addr和Size定义的ITT |
MAPI | DeviceID,EventID, ICID | 将EventID和DeviceID定义的event映射到ICID和pINTID=EventID的ITT项 |
MAPTI | DeviceID,EventID, pINTID, ICID | 将EventID和DeviceID定义的event映射到它相关的ITE,ITE由DeviceID相关的ITT中ICID和pINTID定义 |
MOVALL | RDbase1, RDbase2 | 让RDbase1指定的Redistributor上所有的中断移到RDbase2指定的Redistributor上 |
MOVI | DeviceID,EventID, ICID | 更新DeviceID和EventID定义的event的ITT表项的ICID域。它也将DeviceID和EventID定义的event转化为ICID和pINTID,并在合适的Redistributor移除pending状态,如果它被设置,将中断移到新的ICID定义的Redistributor,并使用新的ICID更新event相关的ITE |
SYNC | RDbase | 保证RDbase指定的Redistributor的物理中断相关的所有未完成ITS操作在执行任何更多ITS命令之前全局可见。跟随SYNC的执行,所有之前命令必须应用于后续写入GITS_TRANSLATE。 |
VINVALL | vPEID | 保证与vPEID相关的任何缓存的Redistributor信息与内存中相关的LPI配置表保持一致 |
VMAPI | DeviceID, EventID,Dbell_pINTID,vPEID | 将由DeviceID和EventID定义的event映射到ITT表项,该表项vPEID, vINTID=EventID, Dbell_pINTID,一个doorbell |
VMAPP | GICv4.0 vPEID,RDbase,VPT_addr,VPT_size | 将vPEID定义的vPE表项映射到目标RDbase,包括相关的虚拟LPI pending表(VPT_addr, VPT_size) |
VMAPP | GICv4.1 vPEID,RDbase, VCONF_addr,VPT_addr,VPT_size, PTZ, Alloc,Default_Doorbell_pINTID | 映射vPEID定义的vPE,包括相关的虚拟LPI配置和pending表。可选的指定默认doorbell。 |
VMAPTI | DeviceID, EventID, vINTID, Dbell_pINTID,vPEID | 使用vPEID,vINTID,Dbell_pINTID和doorbell将DeviceID和EventID定义的event映射到ITT表项 |
VMOVI | DeviceID, EventID, vPEID | 为DeviceID和EventID定义的event更新ITT表项的vPEID域。将DeviceID和EventID定义的event转化为vPEID和pINTID,让合适的Redistributor移除pending状态,如果有设置,将中断发送到新vPEID定义的Redistributor,使用新的vPEID更新event相关的ITE |
VMOVP | GICv4.0 vPEID, RDbase, SequenceNumber, ITSList | 更新vPEID定义的vPE表项到由RDbase定义的目标Redistributor。软件必须使用SequenceNumber和ITSList在超过一个ITS的情况下同步VMOVP命令的执行 |
VMOVP | GICv4.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定义的中断状态 |
VSYNC | vPEID | 保证在任何更多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));elsedsb(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 {u64 raw_cmd[4];__le64 raw_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将形成一个级联的结构,类似下面这样:
下面看一下中断注册:
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驱动初始化