Linux内核中断号映射过程分析(三)

1.概述

早期的CPU中断数量较少,中断系统简单,Linux内核可以将硬件中断号直接映射为软件中断号。但随着CPU支持的中断数量越来越多,中断系统也被设计的越来越复杂,一个CPU内部可能包含多个中断控制器,某些中断控制器还存在级联的可能。因此,Linux内核引入了虚拟中断号的概念,使用irq_domain进行管理,支持多个中断控制器及中断控制器级联的情况。Linux内核的虚拟中断号与中断控制器的硬件中断号一一对应,但对应关系不固定,在中断映射时才能确定。

2.zynq7k串口设备树节点

下面是zynq7k串口0的设备树节点,是amba总线的外设。uart0的兼容属性为"xlnx,xuartps", "cdns,uart-r1p8",父中断为intc,中断类型为SPI中断,使用第27个中断引脚,中断触发方式为高电平。下面将以uart0为例,说明uart0的中断号映射过程。
需要注意是,interrupts属性的第2个值,并不是硬件中断号,是中断引脚序号。由于SGI和PPI中断为CPU内部中断,并不占用中断引脚,只有SPI(外部中断)占用中断引脚。中断引脚从0开始编号,但硬件中断号需要包含所有中断,编号的顺序一般为SGI中断、PPI中断、SPI中断,一般中断引脚号需要加上一个偏移值,才能得到硬件中断号。如zynq7k uart0的中断引脚为27,但硬件中断号为59,这是因为0-31的硬件中断号被分配给了SGI和PPI中断,而SGI和PPI中断属于内部中断,不使用中断引脚。

[arch/arm/boot/dts/zynq-7000.dtsi]
/ {  // 根节点
    compatible = "xlnx,zynq-7000";  // cpu的兼容属性
    ......
    amba: amba {  // arm片上总线
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        interrupt-parent = <&intc>;  // 指定父中断控制器为intc
        ranges;
        .....
        uart0: serial@e0000000 {  // 串口0设备节点
            compatible = "xlnx,xuartps", "cdns,uart-r1p8";  // 串口0的兼容属性
            status = "disabled";
            clocks = <&clkc 23>, <&clkc 40>;
            clock-names = "uart_clk", "pclk";
            reg = <0xE0000000 0x1000>;
            interrupts = <0 27 4>;  // 中断属性,0表示SPI中断,27表示使用第27个中断引脚,
                                    // 4表示中断触发方式为高电平
        };
        ......
    }
    ......
}

3.串口初始化过程分析

zynq7k的串口驱动在xilinx_uartps.c文件中,定义的设备树匹配表为cdns_uart_of_match。设备驱动注册时会用此匹配表与设备树中的属性进行匹配。可以看出设备驱动和设备树中都有"xlnx,xuartps"属性。

    [drivers/tty/serial/xilinx_uartps.c]
    static const struct of_device_id cdns_uart_of_match[] = {
        { .compatible = "xlnx,xuartps", },
        { .compatible = "cdns,uart-r1p8", },
        { .compatible = "cdns,uart-r1p12", .data = &zynqmp_uart_def },
        {}
    };

当驱动被静态编译进内核时,通过module_init定义函数的函数指针将会被放在".initcall6.init"段中。系统启动时,在do_initcall_level函数中调用初始化函数cdns_uart_init

    [drivers/tty/serial/xilinx_uartps.c]
    module_init(cdns_uart_init)
    module_exit(cdns_uart_exit)

    // 平台总线类型定义的结构体,设备和驱动匹配的时候需要用到platform_match函数
    [drivers/base/platform.c]
    struct bus_type platform_bus_type = {
        .name		= "platform",
        .dev_groups	= platform_dev_groups,
        .match		= platform_match,
        .uevent		= platform_uevent,
        .pm		= &platform_dev_pm_ops,
    };

cdns_uart_init函数完成设备驱动的注册、设备和驱动的匹配及匹配成功后调用cdns_uart_probe函数。调用平台总线提供的platform_match完成设备和驱动的匹配,匹配成功后在platform_drv_probe函数中调用驱动的cdns_uart_probe函数。

    cdns_uart_init
      ->uart_register_driver(&cdns_uart_uart_driver)  // 注册串口驱动到串口核心层
      platform_driver_register(&cdns_uart_platform_driver)  // 注册平台驱动
      ->__platform_driver_register
        drv->driver.bus = &platform_bus_type  // 总线类型为平台总线类型
        drv->driver.probe = platform_drv_probe  // 平台设备驱动的probe函数
        drv->driver.remove = platform_drv_remove  // 平台设备驱动的remove函数
        driver_register(&drv->driver)  // 注册设备驱动
          ->driver_find  // 根据设备名字在总线中查找驱动,以确定此设备驱动是否已经注册,已注册返回-EBUSY
            ->kset_find_obj  // 根据设备名称,遍历kset,查找kobject
            ->kobject_put  // kobject引用计数减1,如等于0,则清除该kobject
            ->to_driver  // 获取嵌入kobject的结构体指针
            return priv->driver  // 返回device_driver结构体
          ->bus_add_driver
            ->bus_get  // 获取总线类型
            ->kzalloc  // 分配driver_private结构体
            ->klist_init  // 初始化klist_devices结构体
            ->kobject_init_and_add  // 初始化并添加kobject
            ->driver_attach  // 绑定设备和驱动
              ->bus_for_each_dev  // 遍历总线上的所有设备
                ->__driver_attach
                  ->driver_match_device
                    // 调用匹配函数,匹配函数为platform_bus_type的platform_match函数
                    ->drv->bus->match(dev, drv)
                      ->to_platform_device  // 获取platform_device结构体
                      ->platform_driver  // 获取platform_driver结构体
                      ->of_driver_match_device  // 设备树的匹配方法
                        ->of_match_device
                          ->of_match_node
                            ->__of_match_node
                              ->__of_device_is_compatible
                                ->of_compat_cmp
                                strcasecmp  // 比较兼容属性字符串,返回值大于0则匹配成功
                  ->driver_probe_device  // 匹配成功,调用probe函数
                    ->really_probe
                      ->drv->probe(dev)  // 调用platform_drv_probe函数
                        ->to_platform_driver  // 获取platform_device结构体
                        ->to_platform_device  // 获取platform_driver结构体
                        // 调用设备驱动中的probe函数,实质调用的是cdns_uart_probe函数
                        ->drv->probe(dev)

4.串口中断号的的处理

uart0设备树节点兼容性属性和驱动的兼容性属性匹配成功后,串口驱动的cdns_uart_probe将被调用。

    [drivers/tty/serial/xilinx_uartps.c]
    // 平台驱动结构体    
    static struct platform_driver cdns_uart_platform_driver = {
        .probe   = cdns_uart_probe,   // 匹配成功调用的probe函数
        .remove  = cdns_uart_remove,  // 卸载设备驱动时调用的函数 
        .driver  = {
            .name = CDNS_UART_NAME,  // 设备名称
            .of_match_table = cdns_uart_of_match,  // 与设备树匹配的匹配表
            .pm = &cdns_uart_dev_pm_ops,  // uart操作函数集合
        },
    };

cdns_uart_probe函数完成串口设备的初始化,如设备结构体内存的分配、资源的获取、信息的注册等。我们重点关注对中断资源的处理。cdns_uart_probe函数使用platform_get_irq函数处理串口设备的中断资源,主要完成以下3步:
(1)从设备树中获取中断信息。这里获取的是中断引脚号,需要使用gic_irq_domain_translate函数将中断引脚号转换为硬件中断号,实质是中断引脚号加上32得到硬件中断号。
(2)分配软件中断号。内核使用位图的方式分配软件中断号,从位图的第一位开始查找,直到查找到连续n个为0的bit区域,将此区域内的bit位设置为1,返回第一个为0的bit的索引号,这个索引号就是软件中断号,n为需要分配的软件中断号的数量。
(3)然后分配中断描述符irq_desc,若定义CONFIG_SPARSE_IRQ,则动态分配irq_desc并插入到irq_desc_tree中,若没有定义CONFIG_SPARSE_IRQ,则使用静态定义的irq_desc;最后使用gic_irq_domain_alloc将软件中断号映射为硬件中断号,实质是将软件中断号和硬件中断号设置到同一个irq_data中,若是线性映射,则将虚拟中断号设置到linear_revmap[hwirq]数组中,hwirq为硬件中断号,若为非线性映射,则将硬件中断号hwirq和关联的irq_data插入到revmap_tree中,其中还设置了通用的中断处理函数,即设置irq_deschandle_irq变量,使其指向handle_fasteoi_irqhandle_fasteoi_irq在分析具体的中断处理流程时引入。

    cdns_uart_probe
      ->devm_kzalloc  // 分配struct cdns_uart设备结构体
      ->devm_clk_get  // 获取时钟pclk
      ->devm_clk_get  // 获取uart_clk
      ->clk_prepare   // 使能pclk时钟,可睡眠
      ->clk_prepare   // 使能uart_clk时钟,可睡眠
      ->platform_get_resource  // 获取内存资源
      ->platform_get_irq  // 获取中断资源,这里详细分析其获取过程
        ->of_irq_get  // 如果定义设备树,调用此函数
          ->of_irq_parse_one
            **********************从设备树中获取中断信息*************************
            ->of_get_property  // 获取interrupts属性的值
            ->of_irq_find_parent  // 寻找父中断控制器节点
            ->of_get_property  // 获取父中断控制器#interrupt-cells属性的值
            out_irq->np = p  // 保存父中断控制器设备树节点
            out_irq->args_count = intsize  // 保存interrupts属性有几个32位的值
            out_irq->args[i] = be32_to_cpup(intspec++)  // 保存interrupts属性值
            ->of_irq_parse_raw  // 检查此中断是否被处理
          ->irq_find_host  // 获取父中断控制器的irq_domain结构体,找不到返回-EPROBE_DEFER
            ->irq_find_matching_host
              ->irq_find_matching_fwnode
              list_for_each_entry  // 遍历irq_domain_list链表,查找irq_domain结构体
          ->irq_create_of_mapping
            ->of_phandle_args_to_fwspec  // 将of_phandle_args转化为irq_fwspec
            ->irq_create_fwspec_mapping
              ->irq_find_matching_fwnode  // 获取父中断控制器的irq_domain结构体
              ->irq_domain_translate  // 获取真实的硬件中断号和中断触发类型
                // 调用中断控制提供的translate函数,即gic_irq_domain_hierarchy_ops中的
                // gic_irq_domain_translate函数
                ->d->ops->translate()
                **********************将中断引脚号转换为硬件中断号*************************
                // 调用irq-gic中的函数gic_irq_domain_translate
                ->gic_irq_domain_translate
                  *hwirq = fwspec->param[1] + 16  // 加16跳过SGI中断
                  if (!fwspec->param[0])  // 对于SPI中断,再加16得到gic的中断号
                    *hwirq += 16;  // 则串口最终的硬件中断号为27+16+16=59,和手册中一致
                  *type = fwspec->param[2] & IRQ_TYPE_SENSE_MASK  // 获取中断类型
              ->irq_domain_is_hierarchy  // zynq7k的gic中断控制器支持级联
              // 检查此硬件中断号是否已经被映射,若映射,则直接返回映射后的软件中断号
              ->irq_find_mapping
              **********************分配软件中断号*************************
              ->irq_domain_alloc_irqs  // 分配中断号
                ->__irq_domain_alloc_irqs
                  ->irq_domain_alloc_descs
                    ->irq_alloc_descs_from
                    irq_alloc_descs
                    __irq_alloc_descs
                      // 从位图allocated_irqs中查找为0的区域,返回第一个为0的bit的索引号
                      // 索引号即为软件中断号
                      ->bitmap_find_next_zero_area
                        ->bitmap_find_next_zero_area_off
                      ->irq_expand_nr_irqs  // 如果超过nr_irqs,则扩大nr_irqs
                      ->bitmap_set  // 在allocated_irqs中设置已分配的bit位
                      ->alloc_descs  // 分配irq_desc
                        ->alloc_descs  // 如果定义CONFIG_SPARSE_IRQ,则动态分配irq_desc
                          ->irq_insert_desc  // 插入到irq_desc_tree中
                            ->radix_tree_insert
                        ->alloc_descs  // 如果没有定义CONFIG_SPARSE_IRQ,则使用静态定义的irq_desc
                          ->irq_to_desc
                            // irq_desc数组使用软件中断号作为索引
                            return (irq < NR_IRQS) ? irq_desc + irq : NULL;
                          desc->owner = owner
                        desc->owner = owner  // 设置owner成员
                  ->irq_domain_alloc_irq_data  // 设置
                    ->irq_get_irq_data  // 通过软件中断号获取irq_desc中的irq_data指针
                    irq_data->domain = domain  // 设置irq_data中的domain成员
                  ->irq_domain_alloc_irqs_recursive
                    **********************将硬件中断号映射为软件中断号*************************
                    ->domain->ops->alloc()  // 调用gic_irq_domain_alloc函数进行中断号的映射
                    ->gic_irq_domain_alloc
                      // 将设备树中的中断引脚号转换为硬件中断号,获取中断触发类型
                      ->gic_irq_domain_translate  
                      ->gic_irq_domain_map  // 进行中断映射
                        gic_chip_data *gic = d->host_data  // 获取gic设备结构体

                        // 如果硬件中断号小于32,调用下面3个函数
                        ->irq_set_percpu_devid
                        ->irq_domain_set_info  // 设置的通用中断处理函数为handle_percpu_devid_irq
                        ->irq_set_status_flags
                        // 如果硬件中断号大于32,调用此函数
                        ->irq_domain_set_info 
                          ->irq_domain_set_hwirq_and_chip
                            ->irq_domain_get_irq_data  // 获取软件中断号对应的irq_data
                              irq_data->hwirq = hwirq  // 设置硬件中断号
                              irq_data->chip = chip  设置chip,chip为gic设备结构体中的chip成员
                              irq_data->chip_data = chip_data  // 设置gic设备结构体
                          ->__irq_set_handler
                            ->__irq_do_set_handler
                            **********************设置通用的中断处理函数*************************
                              desc->handle_irq = handle // 设置中断处理函数为handle_fasteoi_irq
                          ->irq_set_handler_data
                            desc->irq_common_data.handler_data = data
                        ->irq_set_probe  // 设置一些标志
                  ->irq_domain_insert_irq  // 将映射好的硬件中断号插入到
                    domain->linear_revmap[hwirq] = virq  // 如果使用线性映射
                    radix_tree_insert(&domain->revmap_tree, hwirq, data)  // 如果使用树映射
              ->irq_set_irq_type  // 设置中断触发类型

5.串口中断号映射过程总结

中断号映射过程

参考资料

  1. Linux kernel V4.6版本源码
  2. https://www.cnblogs.com/LoyenWang/p/12996812.html
  3. 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
  4. 《Zynq-7000 SoC Technical Reference Manual》
  5. 《ARM® Generic Interrupt Controller Architecture version 2.0》
  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值