0、说明
对于嵌入式系统开发来说,中断使用尤为重要。linux中断用在很多方面,如最简单的按键触发的中断事件,网卡收包后的中断等等。
文章参考了韦东山老师中断讲解内容。重点偏向于中断自身的功能及gic驱动。对于设计的内核其他知识,如异常向量表具体的初始化等在另外章节。
1、环境
2、中断概念
2.1 异常
异常概念大于中断,中断也是异常的一种。
- 指令未定义
- 指令、数据访问有问题
- SWI(软中断)
- 快中断
- 中断
2.2 arm关于中断的处理流程
初始化:
a. 设置中断源,让它可以感知外部中断事件
b. 设置中断控制器(可以屏蔽某个中断,优先级)
c. 设置CPU总开关(使能中断)
CPU正常执行用户程序
CPU探测中断是否发生(硬件完成)
CPU 每执行完一条指令都会检查有无中断/异常产生
CPU处理中断事件
跳转到异常向量地址(硬件完成)
a. 保存现场(各种寄存器)
b. 处理异常(中断):分辨中断源,再调用不同的处理函数
c. 恢复现场
2.3 异常向量表
发生不同异常时的硬件跳转地址。
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq //发生中断时,CPU跳到这个地址执行该指令 **假设地址为0x18**
ldr pc, _fiq
向量表一般存储在0地址,但不是固定的地址,存在vector base寄存器,可以在该寄存器中设置中断向量表的基址。
中断向量表中,各子异常地址偏移是固定的。
ARM架构异常向量表在0x0地址和0xffff0000(虚拟地址)。
内核异常向量表的建立:
在内核前期汇编阶段,会将向量表拷贝到物理空间,并映射0xffff0000到该物理空间,当异常发生时在异常向量表中进行跳转。如发生中断,则通过gic驱动提供的irq_handle(handle_arch)_irq)进行中断类型判断与处理。
2.4 栈在中断中的核心作用
当发生异常时,如中断,会跳去中断向量表处理中断事件,那么被中断的APP,如何办,如当前CPU在计算a+b,此时CPU中r0开始的寄存器可能存储的是APP的参数,这个就需要记录r0-r15寄存器值到当前app的栈中,即为保护现场。
如CPU当前在计算a=a+b,首先C需要被转换为汇编语言,然后CPU取指执行。
CPU运行时,先去取得指令,再执行指令:
① 把内存a的值读入CPU寄存器R0
② 把内存b的值读入CPU寄存器R1
③ 把R0、R1累加,存入R0
④ 把R0的值写入内存a
对于程序A被程序B中断的情况,如下图。可以是中断或者进程调度触发:
2.5 进程与线程浅析
在Linux中:资源分配的单位是进程,调度的单位是线程。
同一进程中存在共享内容,且每个线程有自己的栈,多个线程参与调度。
2.6 中断处理函数-上下部
linux多进程在不断的进行调度,如果中断终止了进程APP的执行,进入中断处理函数,如果中断处理函数执行很多内容,会导致进程一直无法被调度,从而出现APP卡顿的情况出现,因此中断处理函数中不应该执行太复杂太耗时的操作。
下半部可以通过tasklet(小任务)软中断、work queue(工作队列)来实现。
tasklet
适合中断处理操作比较简单的场景。通过软中断实现,上半部执行后开中断,可以接收新中断,此时处理下半部,处理完成后APP进程开始调度。
因此tasklet只适合中断耗时不长的场景,因为软中断下半部执行过程中,虽然开了中断,但是进行是不会被调度,长时间处理下半部依然会导致app卡顿。
work queue
通过内核线程,在上半部完成后,唤醒内核线程处理下半部,内核线程与app共同参与调度。schedule_work会把当前work放入system_wq队列,且会唤醒内核线程,内核线程会处理system_wq中的任务。
因此可在内核上半部中调用schedule_work,将下半部加入工作队列,并唤醒处理下半部的内核线程参与调度,当抢到CPU资源时进行处理。
2.7 threaded irq技术
在SMP环境下,为每个中断都创建一个内核线程,且多个中断的线程可被创建在不同CPU上,从而充分利用多核的优势。
2.8 硬件中断与软中断
按键中断、网卡中断、系统时钟首先发生硬件中断,然后在中断处理函数中,使能软件中断,在每次硬件中断完成后会判断是够需要处理软中断。若系统时钟中断为10ms,则软中断在10ms内会被执行到。
软中断,如上面说的tasklet,用于中断下半部。
3、中断下半部处理机制
request_irq及相关数据结构
tasklet与work queue
4、GIC驱动分析
4.1 interrupt相关数据结构
4.1.1 irq_desc
一个硬件中断对应一个irq_desc结构体,描述该硬件中断信息。
类型 | 成员名 | 功能 |
irq_flow_handler_t | handle_irq | highlevel irq-events handler |
irq_data | irq_data | 数据 |
struct irqaction * | action | the irq action chain |
4.1.2 irq_data
中断数据信息
/**
* struct irq_data - per irq chip data passed down to chip functions
* @mask: precomputed bitmask for accessing the chip registers
* @irq: interrupt number
* @hwirq: hardware interrupt number, local to the interrupt domain
* @common: point to data shared by all irqchips
* @chip: low level interrupt hardware access
* @domain: Interrupt translation domain; responsible for mapping
* between hwirq number and linux irq number.
* @parent_data: pointer to parent struct irq_data to support hierarchy
* irq_domain
* @chip_data: platform-specific per-chip private data for the chip
* methods, to allow shared chip implementations
*/
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
4.1.3 irq_chip
提供中断操作函数,如屏蔽中断,开中断,使能,非使能等。
irq_mask等函数提供给handle_irq函数调用。
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
...
}
创建domain: irq_domain_add_legacy
gic->domain = irq_domain_add_legacy(NULL, gic_irqs, irq_base, 16, &gic_irq_domain_ops, gic);
domain = __irq_domain_add(of_node_to_fwnode(of_node), first_hwirq + size,first_hwirq + size, 0, ops, host_data);
domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size), GFP_KERNEL,of_node_to_nid(to_of_node(fwnode)));
domain->ops = ops;//gic_irq_domain_ops
4.2 gic中断处理逻辑
gic在用到某个中断时,分配irq_desc,并确定virq以及hwirq和virq的对应关系(gic irq_domain)。
一个hwirq对应一个中断源时:一个hwirq对应一个irq_desc
一个hwirq对应多中断源时:一个hwirq对应多个irq_desc,如gpio中断。多个irq_desc在 gpio的irq_domain中记录对应关系。gic读取gpio寄存器确认是哪个gpio发生的中断,然后在irq_domain中找到对应的irq_desc,调用处理函数。
每个中断控制器都有irq_domain。
中断发生-硬件自动跳转到异常向量表-执行irq_handle函数-查询gic控制器确认中断是哪个hwirq-在gic irq_domain中找到对应的irq_desc项-执行irq_desc中的handle-handle调用action-action执行用户中断函数
4.3 gic驱动分析
4.3.1 gic几大要素
- 提供irq_handle函数,异常向量表会跳转执行该函数
- 中断控制器通过irq_domain结构体记录hwirq和virq的对应关系
- 提供irq_desc的handle调用action函数
- 提供中断屏蔽和清除函数
4.3.2 设备树
gic中断控制器设备树中的描述
intc: interrupt-controller@f8f01000 {
compatpble = "arm,cortex-a9-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0xF8F01000 0x1000>,
<0xF8F00100 0x100>;
};
对应驱动程序:
kernel/drivers/irqchip/irq-gic.c
将IRQCHIP_DECLARE宏展开:
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
OF_DECLARE_2(irqchip, cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init)
_OF_DECLARE(irqchip, cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init, of_init_fn_2)
static const struct of_device_id __of_table_cortex_a9_gic
__used __section("__irqchip_of_table") = {
.compatible = "arm,cortex-a9-gic",
.data = gic_of_init
}
4.3.3 gic初始化流程-gic_of_init
根据__irqchip_of_table,可以找到of_irq_init中使用。进而分析整个gic的加载初始化过程。
start_kernel (init\main.c)
init_IRQ (arch\arm\kernel\irq.c)
irqchip_init (drivers\irqchip\irqchip.c)
of_irq_init (drivers\of\irq.c)
desc->irq_init_cb = match->data;
ret = desc->irq_init_cb(desc->dev, desc->interrupt_parent);
最终gic_of_init被调用。
gic_of_init
//映射设备树中的地址
gic_of_setup(gic, node);
__gic_init_bases
//提供中断向量表中处理函数
set_handle_irq(gic_handle_irq);
handle_arch_irq = handle_irq;
gic_init_chip(gic, NULL, name, false);
//初始化gic_chip,里面有屏蔽中断等函数
gic->chip = gic_chip;
gic_init_bases(gic, handle);
//初始化gic->domain,提供相关能力
gic->domain = irq_domain_add_legacy(NULL, gic_irqs, irq_base, \
16, &gic_irq_domain_ops, gic);
gic_irq_domain_ops
4.3.4 设备树的解析与中断节点的创建
内核启动阶段完成对设备书解析。并创建中断的映射关系,并将中断信息存储在设备树对应的platform_device的resoure中。
4.3.5 中断向量表函数handle_arch_irq
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
//读取寄存器,获取发生哪个硬件中断
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
//处理domain中的irqnr硬件中断对应的virq
handle_domain_irq(gic->domain, irqnr, regs);
//找到hwirq对应的virq
irq = irq_find_mapping(domain, hwirq);
//调用处理函数
generic_handle_irq(irq);
generic_handle_irq_desc(desc);
//以下函数由解析设备树时domain中的alloc函数设置->handle_fasteoi_irq
desc->handle_irq(desc);
}
调用handle_irq函数处理中断流程,最终调用用户注册的中断函数。
handle_fasteoi_irq
struct irq_chip *chip = desc->irq_data.chip;
mask_irq(desc);
handle_irq_event(desc);
//调用desc中的action函数,也就是request_irq时的函数
cond_unmask_eoi_irq(desc, chip);
5、虚拟二级中断控制器驱动
如果gic称之为一级中断控制器的话,这里将GPIO、gmac等称为二级中断控制器。
在UG585 Interrupts一节列出了ZYNQ 中断对应的硬件中断号:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/random.h>
#include <linux/irq.h>
#include <linux/irqdomain.h>
#include <linux/irqchip/chained_irq.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/gpio/driver.h>
/* FIXME: for gpio_get_value() replace this with direct register read */
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/bug.h>
#include <linux/random.h>
static struct irq_domain *virtual_intc_domain;
static int virtual_intc_get_hwirq(void)
{
return get_random_int() & 0x3;
}
static void virtual_intc_irq_handler(struct irq_desc *desc)
{
/* 它的功能时分辨是哪一个hwirq, 调用对应的irq_desc[].handle_irq */
int hwirq;
struct irq_chip *chip = irq_desc_get_chip(desc);
chained_irq_enter(chip, desc);
/* a. 分辨中断 */
hwirq = virtual_intc_get_hwirq();
/* b. 调用irq_desc[].handle_irq(handleC) */
generic_handle_irq(irq_find_mapping(virtual_intc_domain, hwirq));
chained_irq_exit(chip, desc);
}
static void virtual_intc_irq_ack(struct irq_data *data)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
}
static void virtual_intc_irq_mask(struct irq_data *data)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
}
static void virtual_intc_irq_mask_ack(struct irq_data *data)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
}
static void virtual_intc_irq_unmask(struct irq_data *data)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
}
static void virtual_intc_irq_eoi(struct irq_data *data)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
}
static struct irq_chip virtual_intc_irq_chip = {
.name = "100ask_virtual_intc",
.irq_ack = virtual_intc_irq_ack ,
.irq_mask = virtual_intc_irq_mask ,
.irq_mask_ack = virtual_intc_irq_mask_ack ,
.irq_unmask = virtual_intc_irq_unmask ,
.irq_eoi = virtual_intc_irq_eoi ,
};
static int virtual_intc_irq_map(struct irq_domain *h, unsigned int virq,
irq_hw_number_t hw)
{
/* 1. 给virq提供处理函数
* 2. 提供irq_chip用来mask/unmask中断
*/
irq_set_chip_data(virq, h->host_data);
//irq_set_chip_and_handler(virq, &virtual_intc_irq_chip, handle_edge_irq); /* handle_edge_irq就是handleC */
irq_set_chip_and_handler(virq, &virtual_intc_irq_chip, handle_level_irq); /* handle_level_irq就是handleC */
//irq_set_nested_thread(virq, 1);
//irq_set_noprobe(virq);
return 0;
}
static const struct irq_domain_ops virtual_intc_domain_ops = {
.xlate = irq_domain_xlate_onetwocell,
.map = virtual_intc_irq_map,
};
static int virtual_intc_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
int irq_to_parent;
int irq_base;
/* 1. virutal intc 会向GIC发出n号中断 */
/* 1.1 从设备树里获得virq_n */
irq_to_parent = platform_get_irq(pdev, 0);
printk("virtual_intc_probe irq_to_parent = %d\n", irq_to_parent);
/* 1.2 设置它的irq_desc[].handle_irq, 它的功能时分辨是哪一个hwirq, 调用对应的irq_desc[].handle_irq */
irq_set_chained_handler_and_data(irq_to_parent, virtual_intc_irq_handler, NULL);
/* 2. 分配/设置/注册一个irq_domain */
irq_base = irq_alloc_descs(-1, 0, 4, numa_node_id());
printk("virtual_intc_probe irq_base = %d\n", irq_base);
virtual_intc_domain = irq_domain_add_legacy(np, 4, irq_base, 0,
&virtual_intc_domain_ops, NULL);
return 0;
}
static int virtual_intc_remove(struct platform_device *pdev)
{
return 0;
}
static const struct of_device_id virtual_intc_of_match[] = {
{ .compatible = "100ask,virtual_intc", },
{ },
};
static struct platform_driver virtual_intc_driver = {
.probe = virtual_intc_probe,
.remove = virtual_intc_remove,
.driver = {
.name = "100ask_virtual_intc",
.of_match_table = of_match_ptr(virtual_intc_of_match),
}
};
/* 1. 入口函数 */
static int __init virtual_intc_init(void)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 1.1 注册一个platform_driver */
return platform_driver_register(&virtual_intc_driver);
}
/* 2. 出口函数 */
static void __exit virtual_intc_exit(void)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 2.1 反注册platform_driver */
platform_driver_unregister(&virtual_intc_driver);
}
module_init(virtual_intc_init);
module_exit(virtual_intc_exit);
MODULE_LICENSE("GPL");
6、二级中断控制器驱动-GPIO
6.1 GPIO中断控制器设备树
gpio产生中断后,向上一级gic发出52号SPI中断。
//gic一级中断控制器
intc: interrupt-controller@f8f01000 {
compatible = "arm,cortex-a9-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0xF8F01000 0x1000>,
<0xF8F00100 0x100>;
};
//gpio二级中断控制器
gpio0: gpio@e000a000 {
compatible = "xlnx,zynq-gpio-1.0";
#gpio-cells = <2>;
clocks = <&clkc 42>;
gpio-controller;
interrupt-controller;
#interrupt-cells = <2>;
interrupt-parent = <&intc>;
interrupts = <0 20 4>;
reg = <0xe000a000 0x1000>;
};
interrupts = <0 20 4>;即SPI类型的第20个中断类型,对应手册中的spi_status_0[20] 。
6.2 GPIO中断控制器驱动
可以看出ZYNQ支持的几十个GPIO,只会向GIC发出一个SPI-20中断,那么如何知道是哪个GPIO发生的中断,需要读取GPIO寄存器来判断。因此二级GPIO中断控制器驱动中需要自己读取硬件完成中断源判断。
一个硬件中断会对应一个irq_desc。需要设置52号中断中的irq_desc的handle函数去找到每个gpio中断对应的irq_desc。
上图来自韦东山老师笔记
6.2.1 GPIO控制器驱动中的IRQ数据结构
一个zynq_gpio结构体对应一个GPIO控制器,zynq_gpio中包含一个gpio_chip,gpio_chip中包含一个gpio_irq_chip irq。gpio_irq_chip中包含irq_chip,irq_domain。
//GPIO控制器中的中断配置
zynq_gpio_probe
//提供irqchip
girq->chip = &zynq_gpio_edge_irqchip;
//提供handler
girq->parent_handler = zynq_gpio_irqhandler;
girq->handler = handle_level_irq;
gpiochip_add_data(chip, gpio);
gpiochip_add_data_with_key
gdev = kzalloc(sizeof(*gdev), GFP_KERNEL);
...
gpiochip_irqchip_init_valid_mask(gc);
gpiochip_irqchip_init_hw(gc);
gpiochip_add_irqchip(gc, lock_key, request_key);
//配置domain
gc->irq.domain = irq_domain_add_simple(np, gc->ngpio, gc->irq.first, ops, gc);
//提供handler函数
irq_set_chained_handler_and_data(gc->irq.parents[i], gc->irq.parent_handler, data);
待确认:
系统启动初始化阶段创建了部分irq_desc。在内核解析设备树找到gpio控制器的时候,在gic的domain中找不到对应关系,则创建gpio的irq_desc,并插入到树种,然后创建对应关系。
关于irq_desc的初始化
http://www.wowotech.net/linux_kenrel/interrupt_descriptor.html