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_desc
的handle_irq
变量,使其指向handle_fasteoi_irq
。handle_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.串口中断号映射过程总结
参考资料
- Linux kernel V4.6版本源码
- https://www.cnblogs.com/LoyenWang/p/12996812.html
- 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
- 《Zynq-7000 SoC Technical Reference Manual》
- 《ARM® Generic Interrupt Controller Architecture version 2.0》