4.2_物理中断号如何转换成Linux中断号

4.2 物理中断号如何转换成Linux中断号

4.2.1 通过DTS传递物理中断号给Linux

参考《GICv3_Software_Overview_Official_Release_B》,下表描述了GIC V3支持的INTID(硬件中断号)的范围。

在这里插入图片描述

  • SGI (Software Generated Interrupt):软件触发的中断。Linux内核可以通过写GICD_SGIR寄存器来触发一个中断事件,用于CPU core之间的通信。

  • PPI (Private Peripheral Interrupt):私有外设中断。这是每个核心私有的中断。PPI会送达到指定的CPU上,应用场景有CPU本地时钟。

  • SPI (Shared Peripheral Interrupt):软件触发的中断。软件可以通过写GICD_SGIR寄存器来触发一个中断事件,一般用于核间通信。

  • LPI (Locality-specific Peripheral Interrupt):LPI是GICv3中的新特性,是基于消息的中断。当前GIC V3驱动irq-gic-v3.c的参数gicv3_nolpi默认为0,所以默认是默认支持LPI的。

SOC芯片在设计的时候,不同设备会分配到自己固定的硬件中断号,一般在手册里都会说明。这些硬件中断号如何告知内核呢?通过open firmware device tree,经常简称device tree。设备树通常以 .dts(Device Tree Source)文件的形式编写,这是一种人类可读的文本格式。.dts 文件会被编译成二进制的 .dtb(Device Tree Blob)文件,供内核在启动时使用。

以dts中的timer设备为例,说明一下硬件中断号是如何传入Linux中的。

timer {
    compatible = "arm,armv8-timer";
    interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(1) | IRQ_TYPE_LEVEL_HIGH)>,
                 <GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(1) | IRQ_TYPE_LEVEL_HIGH)>,
                 <GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(1) | IRQ_TYPE_LEVEL_HIGH)>,
                 <GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(1) | IRQ_TYPE_LEVEL_HIGH)>;
};

interrupts 属性定义了定时器产生的中断。每个中断由三个字段组成:

  • 第一个字段是中断类型,0代表GIC_SPI,1代表GIC_PPI

在这里插入图片描述

  • 第二个字段是中断号。这个中断号并不是GIC中的硬件中断号,而是PPI或SPI各自范围的逻辑编号。对于PPI来说,范围是015;对于SPI来说,范围是0987。例子中有四个PPI中断号:中断号13 (0x0d)、中断号14 (0x0e)、中断号11 (0x0b)、中断号10 (0x0a)。
  • 第三个字段是中断的属性,包括2个字节。低字节是中断的触发类型,在这个例子中,中断是高电平触发。高字节表示中断的目标 CPU 掩码,例如GIC_CPU_MASK_SIMPLE(8)可以表示所有 8 个 CPU 都可以接收这个中断。

dts里面的中断号,是如何传递到Linux中呢?

Linux根据dts中的timer节点来创建设备时,需要计算出真实的hwirq。以GIC IRQ V3来说,使用gic_irq_domain_translate函数,根据中断类型进行转换。对于PPI,要加上16;对于SPI,要加上32.

在这里插入图片描述

最后一个问题,对于timer设备来说,四个中断最终使用哪个中断?

根据compatible = “arm,armv8-timer”,确认匹配的驱动程序为drivers/clocksource/arm_arch_timer.c。四个中断的定义在include/clocksource/arm_arch_timer.h。

drivers/clocksource/arm_arch_timer.c
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);

include/clocksource/arm_arch_timer.h
enum arch_timer_ppi_nr {
    ARCH_TIMER_PHYS_SECURE_PPI,   // 对应timer中的PPI 13
    ARCH_TIMER_PHYS_NONSECURE_PPI, //对应timer中的PPI 14
    ARCH_TIMER_VIRT_PPI,          //对应timer中的PPI 11    ARCH_TIMER_HYP_PPI,           //对应timer中的PPI 10    ARCH_TIMER_MAX_TIMER_PPI      // 用于边界检查
};

以我用的QEMU virt为例,arch_timer_of_init调用arch_timer_select_ppi()选择了ARCH_TIMER_VIRT_PPI,对应timer中的PPI 11。根据上面的分析,针对PPI中断,Linux会加上偏移量16,最终得到hwirq 27!

在这里插入图片描述

最后,通过cat /proc/interrupts来验证一下,arch_timer的硬件中断号hwirq确实是27!

在这里插入图片描述

注意看图上的第一列,显示的是Linux中断号。arch_timer的硬件中断号hwirq 27对应Linux的中断号为3,二者是怎么映射的,下一章继续分析。

4.2.2 Linux解析DTS/DTB流程

为什么要分析Linux解析DTS/DTB流程?因为物理中断号的解析和DTS息息相关。

DTS(Device Tree Source)是一种人类可读的文本文件,但是对内核解析和使用并不友好。DTB(Device Tree Blob)是DTS文件经过编译后的二进制形式,是内核可以直接解析的紧凑数据结构。内核在启动时读取并解析DTB文件,构建内存中的设备树结构。内核对DTB的处理,归纳为两个阶段:发现与展开。

在这里插入图片描述

1. 发现

根据Documentation/arm64/booting.txt,在跳转到内核之前,必须把DTB在内存中的物理地址存入X0寄存器。

在这里插入图片描述

经过如下preserve_boot_args和__primary_switched两个过程后,DTB的物理地址存入了变量__fdt_pointer。

arch/arm64/kernel/head.S:

preserve_boot_args:
	mov x21, x0				// x21=FDT

__primary_switched:
	str_l	x21, __fdt_pointer, x5		// Save FDT pointer

setup_machine_fdt(__fdt_pointer)函数的入参就是上面得到的__fdt_pointer即DTB物理地址。setup_machine_fdt的主要任务是检查和验证设备树,确保传递给内核的DTB是有效的。

在这里插入图片描述

必须指出的是,上图中的设备树的虚拟地址dt_virt,在setup_machine_fdt->early_init_dt_scan->early_init_dt_verify调用过程中,会赋值给全局指针initial_boot_params。后续对DTB进行访问时,总是直接访问DTB虚拟地址initial_boot_params而不是DTB物理地址__fdt_pointer。

2. 展开

展开的要点就是扁平化的二进制形式的DTB,展开成用struct device_node描述的树形结构。在Linux内核中,struct device_node代表设备树中的一个具体节点,这些节点可以是根节点、总线、设备或者其他类型的节点。

include/linux/of.h:
struct device_node {
    const char *name;  // 节点名
    const char *type;  // 节点类型
    phandle phandle;   // 该节点的唯一标识符
    struct property *properties;  // 属性列表
    struct property *deadprops;   // 已删除的属性列表
    struct device_node *parent;   // 父节点
    struct device_node *child;    // 第一个子节点
    struct device_node *sibling;  // 下一个兄弟节点
    /* 这里省略了一些字段 */
};

unflatten_device_tree调用__unflatten_device_tree,传入的是DTB虚拟地址initial_boot_prarms,最终得到一颗用struct device_node描述的树,of_root指向树的头节点。

在这里插入图片描述

__unflatten_device_tree调用unflatten_dt_nodes,unflatten_dt_nodes遍历DTB,循环调用 populate_node 来创建和初始化 struct device_node 结构体,对成员name和type进行填充,并利用其成员parent/child/sibling形成树形结构。

小结一下,DTB物理地址通过X0传入内核,存入变量__fdt_pointer,而后完成地址映射并把DTB虚拟地址存入变量initial_boot_prarms,最后DTB展开为struct device_node设备树,头指针为of_root。通过访问of_root,即可遍历整个设备树,找到某个struct device_node节点,获取节点的属性。内核定义了很多API来方便的访问设备树,例如of_find_node_by_path(“/chosen”)可以根据路径来找到对应的struct device_node节点。

在这里插入图片描述

4.2.3 根据DTS完成中断控制器初始化

在上一章节,分析了Linux解析DTS的流程,device tree已经解析完成,根据需要可以随时访问。这一章节来分析中断控制器的初始化,它依赖device tree。

在这里插入图片描述

GIC V3中断控制器驱动irq-gic-v3.c中,IRQCHIP_DECLARE(gic_v3, “arm,gic-v3”, gic_of_init)其实就是初始化了一个静态常量:struct of_device_id __of_table_gic_v3。

//定义了 struct of_device_id __of_table_gic_v3
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);

#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
 
 
#define OF_DECLARE_2(table, name, compat, fn) \ 
        _OF_DECLARE(table, name, compat, fn, of_init_fn_2)
 
 
#define _OF_DECLARE(table, name, compat, fn, fn_type)            \ 
    static const struct of_device_id __of_table_##name        \ 
        __used __section(__##table##_of_table)            \ 
         = { .compatible = compat,                \ 
             .data = (fn == (fn_type)NULL) ? fn : fn  }

根据_OF_DECLARE,struct of_device_id __of_table_gic_v3被指定放在了__irqchip_of_table段(section)中。这个段定义arch/arm64/kernel/vmlinux.lds中,在编译Linux的过程中,变量__of_table_gic_v3会被放在__irqchip_of_table段(section)中。

在这里插入图片描述

IRQCHIP_DECLARE(gic_v3, “arm,gic-v3”, gic_of_init)最终展开为:

struct of_device_id __of_table_gic_v3 __used __section(__irqchip_of_table) = {
.compatible = "arm,gic-v3",
.data = gic_of_init
};

of_irq_init(__irqchip_of_table)从__irqchip_of_table中找出变量__of_table_gic_v3,根据.compatible = "arm,gic-v3"成员,遍历struct device_node设备树,找到匹配的struct device_node节点。如果这个struct device_node节点带有"interrupt-controller"属性,调用gic_of_init(.data = gic_of_init)进行GIC V3中断控制器的初始化。

在这里插入图片描述

以QEMU virt为例,上图中的DTS中的intc@8000000节点,完全符合上述分析。下图是实际的调用堆栈,gic_of_init传入的参数struct device_node *node就指向intc@8000000节点。

在这里插入图片描述

gic_of_init函数的内部细节就不一一分析了,这里只关注重点irq_domain。如下图所示的调用堆栈,分配了一个 irq_domain数据结构实例来表示GIC v3,最后把这个irq_domain加入到了全局链表irq_domain_list。

在这里插入图片描述

struct irq_domain 是 Linux 内核中用于硬件中断号转换的数据结构,将硬件中断号(hwirq)映射到内核中的虚拟中断号(virq)。关注struct irq_domain如下几个成员,在后面分析中断号映射时会用到。

  1. 映射操作:struct irq_domain_ops

irq_domain_ops 是 Linux 内核中用于定义中断域操作方法的结构体。这个结构体包含了一系列函数指针,这些函数指针指向了处理特定中断域操作的函数。通过这些操作,内核可以与中断控制器进行交互,并实现硬件中断号(hwirq)到虚拟中断号(virq)的映射。

static const struct irq_domain_ops gic_irq_domain_ops = {
    //从设备树中的中断规范(irq_fwspec)翻译出硬件中断号(hwirq)和中断类型(type)
	.translate = gic_irq_domain_translate,
    //完成虚拟中断号到硬件中断号的映射
	.alloc = gic_irq_domain_alloc,
    //释放之前通过 alloc 函数分配的资源
	.free = gic_irq_domain_free,
    //选择合适的中断控制器
	.select = gic_irq_domain_select,
};
  1. 反向映射相关数据结构

反向映射是指从hwirq找到对应的virq;而映射(正向映射)是指从virq找到对应的hwirq。irq_domain支持两种反向映射的方法:数组linear_revmap和基数树revmap_tree。

数组linear_revmap,其中每个数组下标对应一个硬件中断号,数值为相应的虚拟中断号。优点是访问速度快,适用于连续且范围较小的硬件中断号。缺点是如果硬件中断号范围很大,会占用大量内存。

基数树revmap_tree,用于存储非线性的、稀疏的反向映射关系,基数树中的每个节点存储了一个硬件中断号和相应的虚拟中断号。基数树是一种高效的数据结构,特别适合处理不连续或范围较大的硬件中断号。优点是内存使用效率高,适用于稀疏的中断号映射。缺点是访问速度比线性数组稍慢,但仍然非常高效。

hwirq_max: 最大的硬件中断号。
revmap_direct_max_irq: 支持直接映射的最大硬件中断号。
revmap_size: 线性映射表 linear_revmap 的大小。
revmap_tree: 基数树,用于存储硬件中断号到虚拟中断号的反向映射。
revmap_tree_mutex: 保护基数树的互斥锁。
linear_revmap: 线性表,用于存储硬件中断号到虚拟中断号的反向映射。

对于GIC V3来说,创建domain的调用过程:irq_domain_create_tree->__irq_domain_add(fwnode, 0, ~0, 0, ops, host_data),其中第2个参数为domain->revmap_size赋值0;第四个参数为domain->revmap_direct_max_irq赋值0。所以,总是使用基数树revmap_tree。

小结一下,根据DTS树找到中断控制器后,创建irq_domain。irq_domain提供struct irq_domain_ops来完成virq到hwirq的映射,提供数组linear_revmap或基数树revmap_tree用于从hwirq反向查找virq。

具体的映射过程和反向查找,下一章节举例说明。

4.2.4 根据DTS完成timer初始化

4.2.4.1 初始化入口arch_timer_of_init

这一章节来分析timer设备的初始化,它依赖于device tree,也可以说它“消费”device tree。device tree的初始化,参考《4.2.2 Linux解析DTS/DTB流程》。

在这里插入图片描述

内核实现的套路和《4.2.3 根据DTS完成中断控制器初始化》基本一样,那就仿写一下。

Arch timer时钟源驱动drivers/clocksource/arm_arch_timer.c中,定义了时钟初始化入口:TIMER_OF_DECLARE(armv8_arch_timer, “arm,armv8-timer”, arch_timer_of_init)。

这个宏其实就是初始化了一个静态常量:struct of_device_id __of_table_armv8_arch_timer。

//本质是定义struct of_device_id __of_table_armv8_arch_timer
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);

#define TIMER_OF_DECLARE(name, compat, fn) \
    OF_DECLARE_1_RET(timer, name, compat, fn)

#define OF_DECLARE_1_RET(table, name, compat, fn) \
        _OF_DECLARE(table, name, compat, fn, of_init_fn_1_ret)

#define _OF_DECLARE(table, name, compat, fn, fn_type)           \
    static const struct of_device_id __of_table_##name      \
        __used __section(__##table##_of_table)          \
        __aligned(__alignof__(struct of_device_id))     \
         = { .compatible = compat,              \
             .data = (fn == (fn_type)NULL) ? fn : fn  }

根据_OF_DECLARE,struct of_device_id __of_table_armv8_arch_timer被指定放在了__timer_of_table段(section)中。这个段定义arch/arm64/kernel/vmlinux.lds中,在编译Linux的过程中,变量__of_table_armv8_arch_timer会被放在___timer_of_table段(section)中。

在这里插入图片描述

TIMER_OF_DECLARE(armv8_arch_timer, “arm,armv8-timer”, arch_timer_of_init)最终展开为:

static const struct of_device_id __of_table_armv8_arch_timer __used __section(___timer_of_table) = { 
.compatible = "arm,armv8-timer", 
.data = arch_timer_of_init 
}

timer_probe从__timer_of_table中找出变量__of_table_armv8_arch_timer,根据.compatible = "arm,armv8-timer"成员,遍历struct device_node设备树,找到匹配的struct device_node节点。调用arch_timer_of_init (.data = arch_timer_of_init)进行arch timer的初始化。
在这里插入图片描述

device tree中是否有匹配的节点?以QEMU virt为例,图中的DTS中的timer节点,完全符合上述分析。

在这里插入图片描述

这是arch timer初始化运行的调用堆栈,与上述分析一致。

在这里插入图片描述

找到了初始化的入口,接下来分析arch timer的Linux virq与hwirq的映射关系是如何建立的。

4.2.4.2 建立Linux virq与hwirq的映射关系-上半部分

arch_timer_of_init循环调用irq_of_parse_and_map,依次把DTS中timer节点定义的4个物理中断号,都转换成Linux virq存到数组arch_timer_ppi中。

在这里插入图片描述

irq_of_parse_and_map首先调用of_irq_parse_one从DTS timer节点得到中断的属性值存入struct of_handle_args oirq结构体变量中。例如对于ARCH_TIMER_VIRT_PPI,解析出来的3个参数如下图,与DTS timer节点中的定义完全相同。其中oriq.args[0]是1,代表是PPI中断;oriq.args[1]等于11,就是0xb,代表PPI中断号;oriq.args[2]等于4,代表中断属性。注意oriq.args[1]等于11,但是11只是PPI中断号,并不是GIC V3的硬件中断号,后面irq_of_parse_and_map会调用irq_create_of_mapping(&oirq)继续转换。

在这里插入图片描述

irq_create_of_mapping(&oirq)调用of_phandle_args_to_fwspec函数,把irq_data(传入struct of_handle_args oirq)转换为中断专用的数据结构变量struct irq_fwspec fwspec,3个数值保持一致。irq_create_of_mapping(&oirq)继续调用irq_create_fwspec_mapping(&fwspec)完成映射。

在这里插入图片描述

irq_create_fwspec_mapping(&fwspec)首先通过fwspec找到irq_domain,然后调用irq_domain_translate(domain, fwspec, &hwirq, &type)。irq_domain_translate通过调用domain->ops->translate回调函数,实际调用了GIC V3中断的gic_irq_domain_translate函数。根据fwspec->param[0],判断是PPI中断,把fwspec->param[1] + 16得到27存入*hwirq。

在这里插入图片描述

小结一下,irq_create_of_mapping-> irq_create_fwspec_mapping->irq_domain_translate返回后,变量hwirq里面存储的就是DTS PPI 11对应的硬件中断号27。既然确定了hwirq,那它对应的Linux virq是多少?接下来调用irq_find_mapping(domain, hwirq),在irq_domain中寻找此hwirq(27)是否已经分配过对应的Linux irq。显然是没有的,所以irq_find_mapping(domain, hwirq)一定返回0。

在这里插入图片描述

接下来irq_create_fwspec_mapping中,if (irq_domain_is_hierarchy(domain))判断为真,走到irq_domain_alloc_irqs->__irq_domain_alloc_irqs,分配Linux virq并且与hwirq建立映射关系。注意,irq_domain_alloc_irqs传入的第4个参数是fwspec,而不是已经算出的hwirq。估计后面还会再重新计算一次,拭目以待。

4.2.4.3 建立Linux virq与hwirq的映射关系-下半部分

在这里插入图片描述

__irq_domain_alloc_irqs分为如下几步,且出现了几个新的和中断相关的数据结构,这里一一分析。

第一步,irq_domain_alloc_descs调用__irq_alloc_descs分配virq及对应的irq_desc数据结构。保留其中最核心的两个函数调用,来展开分析。

int __ref
__irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node,
		  struct module *owner, const struct cpumask *affinity)
{
	start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS,
					   from, cnt, 0);

	ret = alloc_descs(start, cnt, node, affinity, owner);
}
  1. bitmap_find_next_zero_area

这个地方,清晰的说明逻辑中断号virq是通过位图变量allocated_irqs按照先申请先得的规则分配的。allocated_irqs和IRQ_BITMAP_BITS的分析,详见《3.4.1.2 IPIPE对Linux中断号的改造》。

  1. alloc_descs

说到irq_desc数据结构,内核提供了两种管理方式。一种是静态数组struct irq_desc irq_desc[NR_IRQS],一种是基数树static RADIX_TREE(irq_desc_tree, GFP_KERNEL)。

static RADIX_TREE(irq_desc_tree, GFP_KERNEL);

#define RADIX_TREE(name, mask) \
    struct radix_tree_root name = RADIX_TREE_INIT(name, mask)

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
	[0 ... NR_IRQS-1] = {
		.handle_irq	= handle_bad_irq,
		.depth		= 1,
		.lock		= __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
	}
};

对于静态数组,struct irq_desc相当于编译的时候就已经分配了,逻辑比较简单,只要根据virq来索引即可。对于基数树,需要针对virq进行对应struct irq_desc进行申请和释放。当前默认是CONFIG_SPARSE_IRQ=y,使用基数树。alloc_descs的最重要的主体为:

static int alloc_descs(unsigned int start, unsigned int cnt, int node,
               const struct cpumask *affinity, struct module *owner)
{
        desc = alloc_desc(start + i, node, flags, mask, owner);

        irq_insert_desc(start + i, desc);
}
  • 调用alloc_desc申请struct irq_desc。

每个virq都拥有一个struct irq_desc与其对应,且struct irq_desc包含一个struct irq_data结构体。

alloc_descs->alloc_desc-> desc_set_defaults会把virq保存起来:desc->irq_data.irq = irq。注意,irq_data.hwirq此时还没有初始化哦,所以已知virq,是找不到hwirq的。

  • 以virq为索引值,调用irq_insert_desc插入基数树irq_desc_tree。

第二步,irq_domain_alloc_irq_data初始化irq_data

特别注意,struct irq_data实体是直接定义在struct irq_desc中,所以在上一步分配struct irq_desc时,struct irq_data实际上也分配了。这里最重要的是给irq_data->domain赋值,让struct irq_desc通过irq_data与domain关联起来。

在这里插入图片描述

第三步,irq_domain_alloc_irqs_hierarchy确定virq和hwirq的关系。

irq_domain_alloc_irqs_hierarchy通过钩子domain->ops->alloc调用了gic_irq_domain_alloc,注意传入的第4个参数不是上面已经计算出来的hwirq,反而是fwspec。所以,在最终到达gic_irq_domain_alloc时。

  1. 调用gic_irq_domain_translate重新计算hwirq。

在这里插入图片描述

  1. 调用gic_irq_domain_map完成virq到hwirq的映射关系:irq_data->hwirq = hwirq。至此之后,通过virq可以找到irq_desc.irq_data.hwirq。函数irq_get_irq_data可以方便的从virq找到irq_data。

在这里插入图片描述

第四步,调用irq_domain_insert_irq->irq_domain_set_mapping,完成hwirq到virq的反向映射。

对于GIC V3来说,创建domain的调用过程:irq_domain_create_tree->__irq_domain_add(fwnode, 0, ~0, 0, ops, host_data),其中第2个参数为domain->revmap_size赋值0;第四个参数为domain->revmap_direct_max_irq赋值0。

所以,在irq_domain_set_mapping中,通过radix_tree_insert,以hwirq为索引,向domain->revmap_tree插入irq_data。至此之后,通过hwirq可以找到virq即irq_data.irq。内核封装了irq_find_mapping函数来找出hwirq对应的virq。

在这里插入图片描述

总结一下:

Linux virq是通过位图变量allocated_irqs按照先申请先得的规则分配的。

每个virq都拥有一个struct irq_desc与其对应,且struct irq_desc包含一个struct irq_data结构体。struct irq_desc通过静态数组或基数树组织起来,索引值为virq。

struct irq_data结构体中的irq代表virq,在alloc_desc时初始化为virq。

struct irq_data结构体中的hwirq,会存储硬件中断号。

已知hwirq,通过irq_domain的revmap_tree或linear_revmap,可以找到irq_data.irq(virq),可以使用API irq_find_mapping函数。

已知virq,通过irq_desc.irq_data.hwirq快速找到hwirq,可以使用API irq_get_irq_data.

4.2.5 根据DTS完成platform设备创建

4.2.5.1 初始化入口及概述

上一章节所述timer设备,为系统提供时钟,所以需要提前初始化。接下来展开DTS中定义的其它设备的初始化过程。默认都会创建struct platform_device,但是也有特例会创建struct amba_device。承接之前的章节,增加第(4)~(6)步,依次展开。

在这里插入图片描述

在 Linux 设备驱动开发中,许多外设并没有特定的总线(如 PCI、USB 、SPI、I2C等)支持。为了统一管理和简化这些设备的驱动开发,Linux 内核引入了 platform 虚拟总线。platform 总线是一种虚拟的总线模型,用于管理那些没有专用总线的设备。

平台总线:platform 总线是一个虚拟的总线,用于管理那些没有特定总线支持的设备。它提供了一种统一的机制来注册和管理这些设备及其驱动程序。

平台设备:挂接在 platform 总线上的设备称为 platform 设备,由 struct platform_device 结构体描述。platform 设备通常通过设备树(Device Tree)节点或板级支持包(BSP)定义,并在内核启动时被创建和注册到 platform 总线上。

平台驱动:挂接在 platform 总线上的驱动程序称为 platform 驱动,由 struct platform_driver 结构体描述。platform 驱动程序通常包含设备的探测(probe)和移除(remove)函数,以及一个匹配表(of_match_table 或 platform_device_id),用于匹配设备树节点或平台设备。

通过 platform 总线模型,Linux 内核能够以一种统一和灵活的方式管理各种无特定总线支持的设备,从而简化了设备驱动的开发和维护。Platform总线的定义和注册如下图所示:

在这里插入图片描述

AMBA(Advanced Microcontroller Bus Architecture)是由 ARM 公司设计的一种标准,用于连接和管理片上系统(SoC)中的各个组件。AMBA 总线提供了一种高效、灵活的通信机制,使得 SoC 中的处理器、外设和其他组件能够相互通信。

在 Linux 内核中,AMBA 总线同样采用了一种虚拟总线模型,用于管理和配置 AMBA 设备。

AMBA 设备:挂接在 AMBA 总线上的设备称为 AMBA 设备,由 struct amba_device 结构体描述。AMBA 设备通常通过设备树节点定义,并在内核启动时被创建和注册到 AMBA 总线上。“arm,primecell” 是一个在设备树(Device Tree, DTS/DTSI)中常用的兼容属性(compatible property),用于标识 ARM PrimeCell 设备。PrimeCell 是 ARM 公司设计的一系列外设 IP 核,这些外设通过 AMBA 总线连接到片上系统(SoC)中。

AMBA 驱动:挂接在 AMBA 总线上的驱动程序称为 AMBA 驱动,由 struct amba_driver 结构体描述。AMBA 驱动程序通常包含设备的探测(probe)和移除(remove)函数,以及一个匹配表(amba_id),用于匹配设备树节点或 AMBA 设备。AMBA 驱动程序通过 amba_driver_register 和 amba_driver_unregister 函数注册和卸载。

Amba总线的定义和注册如下图所示:

在这里插入图片描述

4.2.5.2 哪些DTS节点创建设备

扫描的入口函数是of_platform_default_populate_init,它是怎么被调用的呢?利用arch_initcall_sync宏定义。

在这里插入图片描述

在 Linux 内核中,initcall 宏用于在启动过程中注册不同的初始化函数。这些宏按优先级分类,确保在内核启动的不同阶段执行特定的初始化任务。数字越小,优先级越高,越早执行。异步的初始化函数会尽早执行,而同步(数字后面加后缀s)的初始化函数会等待所有同级别的异步初始化函数完成后才执行。arch_initcall_sync的优先级是3s,即优先级为3,属于同步初始化。

在这里插入图片描述

沿着如下调用关系,走到of_platform_populate,遍历DTS根节点的所有子节点,调用 of_platform_bus_create 创建并注册平台设备:of_platform_default_populate_init->of_platform_default_populate->of_platform_populate

int of_platform_populate(
    struct device_node *root,          // 设备树的根节点
    const struct of_device_id *matches, // 匹配表,用于过滤设备树节点
    const struct of_dev_auxdata *lookup, // 辅助数据,用于查找额外的设备信息
    struct device *parent              // 父设备,用于创建新的平台设备
)
{
    struct device_node *child;
    int rc = 0;

    // 获取或查找根节点
    root = root ? of_node_get(root) : of_find_node_by_path("/");
    if (!root)
        return -EINVAL;

    // 打印调试信息
    pr_debug("%s()\n", __func__);
    pr_debug(" starting at: %pOF\n", root);

    // 遍历根节点的所有子节点
    for_each_child_of_node(root, child) {
        // 创建并注册平台设备
        rc = of_platform_bus_create(child, matches, lookup, parent, true);
        if (rc) {
            of_node_put(child); // 释放子节点的引用计数
            break; // 如果创建失败,跳出循环
        }
    }

    // 设置根节点的填充标志
    of_node_set_flag(root, OF_POPULATED_BUS);

    // 释放根节点的引用计数
    of_node_put(root);

    // 返回最终的错误码
    return rc;
}

of_platform_bus_create函数用于从设备树中创建并注册一个平台设备,并递归地处理其子节点。它的逻辑包括:检查节点属性、跳过特定节点、查找辅助数据、处理 ARM PrimeCell 设备、创建平台设备、递归处理子节点、设置填充标志。

static int of_platform_bus_create(
    struct device_node *bus,          // 当前处理的设备树节点
    const struct of_device_id *matches, // 匹配表,用于过滤设备树节点
    const struct of_dev_auxdata *lookup, // 辅助数据,用于查找额外的设备信息
    struct device *parent,            // 父设备,用于创建新的平台设备
    bool strict                       // 是否严格模式,要求必须有 `compatible` 属性
)
{
    const struct of_dev_auxdata *auxdata;
    struct device_node *child;
    struct platform_device *dev;
    const char *bus_id = NULL;
    void *platform_data = NULL;
    int rc = 0;

    // 检查是否有 compatible 属性(严格模式)
    if (strict && (!of_get_property(bus, "compatible", NULL))) {
        pr_debug("%s() - skipping %pOF, no compatible prop\n", __func__, bus);
        return 0;
    }

    // 跳过特定节点
    if (unlikely(of_match_node(of_skipped_node_table, bus))) {
        pr_debug("%s() - skipping %pOF node\n", __func__, bus);
        return 0;
    }

    // 检查是否已填充
    if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
        pr_debug("%s() - skipping %pOF, already populated\n", __func__, bus);
        return 0;
    }

    // 查找辅助数据
    auxdata = of_dev_lookup(lookup, bus);
    if (auxdata) {
        bus_id = auxdata->name;
        platform_data = auxdata->platform_data;
    }

    // 处理 ARM PrimeCell 设备
    if (of_device_is_compatible(bus, "arm,primecell")) {
        of_amba_device_create(bus, bus_id, platform_data, parent);
        return 0;
    }

    // 创建平台设备
    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
    if (!dev || !of_match_node(matches, bus))
        return 0;

    // 递归处理子节点
    for_each_child_of_node(bus, child) {
        pr_debug("   create child: %pOF\n", child);
        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
        if (rc) {
            of_node_put(child); // 释放子节点的引用计数
            break; // 如果创建失败,跳出循环
        }
    }

    // 设置填充标志
    of_node_set_flag(bus, OF_POPULATED_BUS);

    // 返回最终的错误码
    return rc;
}
  1. 检查节点属性

检查是否有 compatible 属性,如果没有,则不创建设备节点。

  1. 处理 ARM PrimeCell 设备

在上一章节已经解释了AMBA总线及AMBA设备。此处遇到了“arm,primecell”,做特殊处理,调用of_amba_device_create创建amba_device并加入amba总线。

  1. 创建平台设备

调用of_platform_device_create_pdata,创建platfrom device,以及相应的sys下的节点,例如下图的例子。同时,也会把每个platform device的virq和hwirq完成映射,在下一小节展开。

在这里插入图片描述

  1. 递归处理子节点

DTS根节点下的子节点,如果compatible属性与of_default_bus_match_table中的定义匹配,那么就递归调用of_platform_bus_create,扫描下一级子节点,并建立对应的platform设备。

在这里插入图片描述

上述of_default_bus_match_table定义的{ .compatible = “arm,amba-bus”, }容易让人困惑,因为已经对“arm,primecell”做了特殊处理。查看了一个ARM64的dts,发现有定义“arm,primecell”的节点,但是直接放在了DTS的根节点下,而不是包含在"arm,amba-bus"节点中。所以,因为DTS不规范,导致了内核需要特殊处理。

在这里插入图片描述

在/sys/bus/amba中确实有这两个设备:

在这里插入图片描述

针对这个递归反向思考一下,对于DTS根节点下定义的PCIE总线控制器、USB总线控制器、SPI总线控制器、I2C总线控制器等,只会创建出总线控制器对应的platform device,并不会递归扫描其子节点。这些总线控制器的slave设备,由各自的总线控制器子系统自行处理。

4.2.5.3 建立hwirq与irq的映射

of_platform_device_create_pdata会调用of_device_alloc创建platform device,并设置其总线为dev.bus = &platform_bus_type。

static struct platform_device *of_platform_device_create_pdata(
    struct device_node *np,           // 设备树节点
    const char *bus_id,               // 设备的名称
    void *platform_data,              // 平台数据
    struct device *parent             // 父设备
)
{
    struct platform_device *dev;

    // 检查设备是否可用和是否已填充
    if (!of_device_is_available(np) ||
        of_node_test_and_set_flag(np, OF_POPULATED))
        return NULL;

    // 分配平台设备结构体
    dev = of_device_alloc(np, bus_id, parent);
    if (!dev)
        goto err_clear_flag;

    // 设置 DMA 掩码
    dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
    if (!dev->dev.dma_mask)
        dev->dev.dma_mask = &dev->dev.coherent_dma_mask;

    // 设置总线类型和平台数据
    dev->dev.bus = &platform_bus_type;
    dev->dev.platform_data = platform_data;
    of_msi_configure(&dev->dev, dev->dev.of_node);

    // 添加设备
    if (of_device_add(dev) != 0) {
        platform_device_put(dev);
        goto err_clear_flag;
    }

    return dev;

err_clear_flag:
    // 清除填充标志
    of_node_clear_flag(np, OF_POPULATED);
    return NULL;
}

沿着of_device_alloc,会发现它会层层调用走到__irq_domain_alloc_irqs。走到这里,逻辑就和4.2.4.3的逻辑一模一样了,不需要重复描述了。

在这里插入图片描述

小节一下,到此为止,已经完全搞通搞透了物理中断号hwirq如何转换成Linux中断号virq。

接下来将思路转向中断处理函数的注册和处理过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值