CAN 协议即控制器局域网络 (Controller Area Network)简称,由研发和生产汽车电子产品著称的德国 BOSCH 公司开发,成为国际标准ISO11519以及ISO11898。
一、中断子系统原理 文中的例子基本都是基于ARM这一体系架构,其他架构的原理其实也差不多,区别只是其中的硬件抽象层。内核版本基于3.3。 1. 设备、中断控制器和CPU 一个完整的设备中,与中断相关的硬件可以划分为3类,它们分别是:设备、中断控制器和CPU本身,下图展示了一个smp系统中的中断硬件的组成结构: 设备 设备是发起中断的源,当设备需要请求某种服务的时候,它会发起一个硬件中断信号,通常,该信号会连接至中断控制器,由中断控制器做进一步的处理。在现代的移动设备中,发起中断的设备可以位于soc(system-on-chip)芯片的外部,也可以位于soc的内部,因为目前大多数soc都集成了大量的硬件IP,例如I2C、SPI、Display Controller等等。 中断控制器 中断控制器负责收集所有中断源发起的中断,现有的中断控制器几乎都是可编程的,通过对中断控制器的编程,我们可以控制每个中断源的优先级、中断的电器类型,还可以打开和关闭某一个中断源,在smp系统中,甚至可以控制某个中断源发往哪一个CPU进行处理。对于ARM架构的soc,使用较多的中断控制器是VIC(Vector Interrupt Controller),进入多核时代以后,GIC(General Interrupt Controller)的应用也开始逐渐变多。 CPU cpu是最终响应中断的部件,它通过对可编程中断控制器的编程操作,控制和管理者系统中的每个中断,当中断控制器最终判定一个中断可以被处理时,他会根据事先的设定,通知其中一个或者是某几个cpu对该中断进行处理,虽然中断控制器可以同时通知数个cpu对某一个中断进行处理,实际上,最后只会有一个cpu相应这个中断请求,但具体是哪个cpu进行响应是可能是随机的,中断控制器在硬件上对这一特性进行了保证,不过这也依赖于操作系统对中断系统的软件实现。在smp系统中,cpu之间也通过IPI(interprocessor interrupt)中断进行通信。 2. IRQ编号 系统中每一个注册的中断源,都会分配一个唯一的编号用于识别该中断,我们称之为IRQ编号。IRQ编号贯穿在整个Linux的通用中断子系统中。在移动设备中,每个中断源的IRQ编号都会在arch相关的一些头文件中,例如arch/xxx/mach-xxx/include/irqs.h。驱动程序在请求中断服务时,它会使用IRQ编号注册该中断,中断发生时,cpu通常会从中断控制器中获取相关信息,然后计算出相应的IRQ编号,然后把该IRQ编号传递到相应的驱动程序中。 3. 在驱动程序中申请中断 Linux中断子系统向驱动程序提供了一系列的API,其中的一个用于向系统申请中断: int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) 其中, irq是要申请的IRQ编号, handler是中断处理服务函数,该函数工作在中断上下文中,如果不需要,可以传入NULL,但是不可以和thread_fn同时为NULL; thread_fn是中断线程的回调函数,工作在内核进程上下文中,如果不需要,可以传入NULL,但是不可以和handler同时为NULL; irqflags是该中断的一些标志,可以指定该中断的电气类型,是否共享等信息; devname指定该中断的名称; dev_id用于共享中断时的cookie data,通常用于区分共享中断具体由哪个设备发起; 关于该API的详细工作机理我们后面再讨论。 4. 通用中断子系统(Generic irq)的软件抽象 在通用中断子系统(generic irq)出现之前,内核使用__do_IRQ处理所有的中断,这意味着__do_IRQ中要处理各种类型的中断,这会导致软件的复杂性增加,层次不分明,而且代码的可重用性也不好。事实上,到了内核版本2.6.38,__do_IRQ这种方式已经彻底在内核的代码中消失了。通用中断子系统的原型最初出现于ARM体系中,一开始内核的开发者们把3种中断类型区分出来,他们是: 电平触发中断(level type) 边缘触发中断(edge type) 简易的中断(simple type) 后来又针对某些需要回应eoi(end of interrupt)的中断控制器,加入了fast eoi type,针对smp加入了per cpu type。把这些不同的中断类型抽象出来后,成为了中断子系统的流控层。要使所有的体系架构都可以重用这部分的代码,中断控制器也被进一步地封装起来,形成了中断子系统中的硬件封装层。我们可以用下面的图示表示通用中断子系统的层次结构: 硬件封装层 它包含了体系架构相关的所有代码,包括中断控制器的抽象封装,arch相关的中断初始化,以及各个IRQ的相关数据结构的初始化工作,cpu的中断入口也会在arch相关的代码中实现。中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器的行为,体系相关的中断入口函数在获取IRQ编号后,通过中断通用逻辑层提供的标准函数,把中断调用传递到中断流控层中。我们看看irq_chip的部分定义: struct irq_chip { 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); int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); int (*irq_retrigger)(struct irq_data *data); int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); int (*irq_set_wake)(struct irq_data *data, unsigned int on); ......}; 看到上面的结构定义,很明显,它实际上就是对中断控制器的接口抽象,我们只要对每个中断控制器实现以上接口(不必全部),并把它和相应的irq关联起来,上层的实现即可通过这些接口访问中断控制器。而且,同一个中断控制器的代码可以方便地被不同的平台所重用。 中断流控层 所谓中断流控是指合理并正确地处理连续发生的中断,比如一个中断在处理中,同一个中断再次到达时如何处理,何时应该屏蔽中断,何时打开中断,何时回应中断控制器等一系列的操作。该层实现了与体系和硬件无关的中断流控处理操作,它针对不同的中断电气类型(level,edge......),实现了对应的标准中断流控处理函数,在这些处理函数中,最终会把中断控制权传递到驱动程序注册中断时传入的处理函数或者是中断线程中。目前内核提供了以下几个主要的中断流控函数的实现(只列出部分): handle_simple_irq(); handle_level_irq(); 电平中断流控处理程序 handle_edge_irq(); 边沿触发中断流控处理程序 handle_fasteoi_irq(); 需要eoi的中断处理器使用的中断流控处理程序 handle_percpu_irq(); 该irq只有单个cpu响应时使用的流控处理程序 中断通用逻辑层 该层实现了对中断系统几个重要数据的管理,并提供了一系列的辅助管理函数。同时,该层还实现了中断线程的实现和管理,共享中断和嵌套中断的实现和管理,另外它还提供了一些接口函数,它们将作为硬件封装层和中断流控层以及驱动程序API层之间的桥梁,例如以下API: generic_handle_irq(); irq_to_desc(); irq_set_chip(); irq_set_chained_handler(); 驱动程序API 该部分向驱动程序提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作。驱动程序的开发者通常只会使用到这一层提供的这些API即可完成驱动程序的开发工作,其他的细节都由另外几个软件层较好地“隐藏”起来了,驱动程序开发者无需再关注底层的实现,这看起来确实是一件美妙的事情,不过我认为,要想写出好的中断代码,还是花点时间了解一下其他几层的实现吧。其中的一些API如下: enable_irq(); disable_irq(); disable_irq_nosync(); request_threaded_irq(); irq_set_affinity(); 这里不再对每一层做详细的介绍,我会在后面做深入的探讨。 5. irq描述结构:struct irq_desc 整个通用中断子系统几乎都是围绕着irq_desc结构进行,系统中每一个irq都对应着一个irq_desc结构,所有的irq_desc结构的组织方式有两种: 基于数组方式 平台相关板级代码事先根据系统中的IRQ数量,定义常量:NR_IRQS,在kernel/irq/irqdesc.c中使用该常量定义irq_desc结构数组: 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), }}; 基于基数树方式 当内核的配置项CONFIG_SPARSE_IRQ被选中时,内核使用基数树(radix tree)来管理irq_desc结构,这一方式可以动态地分配irq_desc结构,对于那些具备大量IRQ数量或者IRQ编号不连续的系统,使用该方式管理irq_desc对内存的节省有好处,而且对那些自带中断控制器管理设备自身多个中断源的外部设备,它们可以在驱动程序中动态地申请这些中断源所对应的irq_desc结构,而不必在系统的编译阶段保留irq_desc结构所需的内存。 下面我们看一看irq_desc的部分定义: struct irq_data { unsigned int irq; unsigned long hwirq; unsigned int node; unsigned int state_use_accessors; struct irq_chip *chip; struct irq_domain *domain; void *handler_data; void *chip_data; struct msi_desc *msi_desc;#ifdef CONFIG_SMP cpumask_var_t affinity;#endif}; struct irq_desc { struct irq_data irq_data; unsigned int __percpu *kstat_irqs; irq_flow_handler_t handle_irq;#ifdef CONFIG_IRQ_PREFLOW_FASTEOI irq_preflow_handler_t preflow_handler;#endif struct irqaction *action; /* IRQ action list */ unsigned int status_use_accessors; unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ unsigned int irq_count; /* For detecting broken IRQs */ raw_spinlock_t lock; struct cpumask *percpu_enabled;#ifdef CONFIG_SMP const struct cpumask *affinity_hint; struct irq_affinity_notify *affinity_notify;#ifdef CONFIG_GENERIC_PENDING_IRQ cpumask_var_t pending_mask;#endif#endif wait_queue_head_t wait_for_threads; const char *name;} ____cacheline_internodealigned_in_smp; 对于irq_desc中的主要字段做一个解释: irq_data 这个内嵌结构在2.6.37版本引入,之前的内核版本的做法是直接把这个结构中的字段直接放置在irq_desc结构体中,然后在调用硬件封装层的chip->xxx()回调中传入IRQ编号作为参数,但是底层的函数经常需要访问->handler_data,->chip_data,->msi_desc等字段,这需要利用irq_to_desc(irq)来获得irq_desc结构的指针,然后才能访问上述字段,者带来了性能的降低,尤其在配置为sparse irq的系统中更是如此,因为这意味着基数树的搜索操作。为了解决这一问题,内核开发者把几个低层函数需要使用的字段单独封装为一个结构,调用时的参数则改为传入该结构的指针。实现同样的目的,那为什么不直接传入irq_desc结构指针?因为这会破坏层次的封装性,我们不希望低层代码可以看到不应该看到的部分,仅此而已。 kstat_irqs 用于irq的一些统计信息,这些统计信息可以从proc文件系统中查询。 action 中断响应链表,当一个irq被触发时,内核会遍历该链表,调用action结构中的回调handler或者激活其中的中断线程,之所以实现为一个链表,是为了实现中断的共享,多个设备共享同一个irq,这在外围设备中是普遍存在的。 status_use_accessors 记录该irq的状态信息,内核提供了一系列irq_settings_xxx的辅助函数访问该字段,详细请查看kernel/irq/settings.h depth 用于管理enable_irq()/disable_irq()这两个API的嵌套深度管理,每次enable_irq时该值减去1,每次disable_irq时该值加1,只有depth==0时才真正向硬件封装层发出关闭irq的调用,只有depth==1时才会向硬件封装层发出打开irq的调用。disable的嵌套次数可以比enable的次数多,此时depth的值大于1,随着enable的不断调用,当depth的值为1时,在向硬件封装层发出打开irq的调用后,depth减去1后,此时depth为0,此时处于一个平衡状态,我们只能调用disable_irq,如果此时enable_irq被调用,内核会报告一个irq失衡的警告,提醒驱动程序的开发人员检查自己的代码。 lock 用于保护irq_desc结构本身的自旋锁。 affinity_hit 用于提示用户空间,作为优化irq和cpu之间的亲缘关系的依据。 pending_mask 用于调整irq在各个cpu之间的平衡。 wait_for_threads 用于synchronize_irq(),等待该irq所有线程完成。 irq_data结构中的各字段: irq 该结构所对应的IRQ编号。 hwirq 硬件irq编号,它不同于上面的irq; node 通常用于hwirq和irq之间的映射操作; state_use_accessors 硬件封装层需要使用的状态信息,不要直接访问该字段,内核定义了一组函数用于访问该字段:irqd_xxxx(),参见include/linux/irq.h。 chip 指向该irq所属的中断控制器的irq_chip结构指针 handler_data 每个irq的私有数据指针,该字段由硬件封转层使用,例如用作底层硬件的多路复用中断。 chip_data 中断控制器的私有数据,该字段由硬件封转层使用。 msi_desc 用于PCIe总线的MSI或MSI-X中断机制。 affinity 记录该irq与cpu之间的亲缘关系,它其实是一个bit-mask,每一个bit代表一个cpu,置位后代表该cpu可能处理该irq。 这是通用中断子系统系列文章的第一篇,这里不会详细介绍各个软件层次的实现原理,但是有必要对整个架构做简要的介绍: 系统启动阶段,取决于内核的配置,内核会通过数组或基数树分配好足够多的irq_desc结构; 根据不同的体系结构,初始化中断相关的硬件,尤其是中断控制器; 为每个必要irq的irq_desc结构填充默认的字段,例如irq编号,irq_chip指针,根据不同的中断类型配置流控handler; 设备驱动程序在初始化阶段,利用request_threaded_irq() api申请中断服务,两个重要的参数是handler和thread_fn; 当设备触发一个中断后,cpu会进入事先设定好的中断入口,它属于底层体系相关的代码,它通过中断控制器获得irq编号,在对irq_data结构中的某些字段进行处理后,会将控制权传递到中断流控层(通过irq_desc->handle_irq); 中断流控处理代码在作出必要的流控处理后,通过irq_desc->action链表,取出驱动程序申请中断时注册的handler和thread_fn,根据它们的赋值情况,或者只是调用handler回调,或者启动一个线程执行thread_fn,又或者两者都执行; 至此,中断最终由驱动程序进行了响应和处理。 6. 中断子系统的proc文件接口 在/proc目录下面,有两个与中断子系统相关的文件和子目录,它们是: /proc/interrupts:文件 /proc/irq:子目录 读取interrupts会依次显示irq编号,每个cpu对该irq的处理次数,中断控制器的名字,irq的名字,以及驱动程序注册该irq时使用的名字,以下是一个例子: 根据irq的不同,以上条目不一定会全部都出现,以下是某个设备的例子: # cd /proc/irq # ls ls 332 248 ...... ...... 12 11 default_smp_affinity # ls 332 bcmsdh_sdmmc spurious node affinity_hint smp_affinity # cat 332/smp_affinity 3 可见,以上设备是一个使用双核cpu的设备,因为smp_affinity的值是3,系统默认每个中断可以由两个cpu进行处理。 二、arch相关的硬件封装层 Linux的通用中断子系统的一个设计原则就是把底层的硬件实现尽可能地隐藏起来,使得驱动程序的开发人员不用关注底层的实现,要实现这个目标,内核的开发者们必须把硬件相关的内容剥离出来,然后定义一些列标准的接口供上层访问,上层的开发人员只要知道这些接口即可完成对中断的进一步处理和控制。对底层的封装主要包括两部分: 实现不同体系结构中断入口,这部分代码通常用asm实现; 中断控制器进行封装和实现; 本节的内容是讨论硬件封装层的实现细节。以ARM体系进行介绍,大部分的代码位于内核代码树的arch/arm/目录内。 1. CPU的中断入口 我们知道,arm的异常和复位向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端向量,向量地址位于0xffff0000,Linux选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上: ARM的异常向量表 地址 异常种类 FFFF0000 复位 FFFF0004 未定义指令 FFFF0008 软中断(swi) FFFF000C Prefetch abort FFFF0010 Data abort FFFF0014 保留 FFFF0018 IRQ FFFF001C FIQ 中断向量表在arch/arm/kernel/entry_armv.S中定义,为了方便讨论,下面只列出部分关键的代码: .globl __stubs_start__stubs_start: vector_stub irq, IRQ_MODE, 4 .long __irq_usr @ 0 (USR_26 / USR_32) .long __irq_invalid @ 1 (FIQ_26 / FIQ_32) .long __irq_invalid @ 2 (IRQ_26 / IRQ_32) .long __irq_svc @ 3 (SVC_26 / SVC_32) vector_stub dabt, ABT_MODE, 8 .long __dabt_usr @ 0 (USR_26 / USR_32) .long __dabt_invalid @ 1 (FIQ_26 / FIQ_32) .long __dabt_invalid @ 2 (IRQ_26 / IRQ_32) .long __dabt_svc @ 3 (SVC_26 / SVC_32) vector_fiq: disable_fiq subs pc, lr, #4 ...... .globl __stubs_end__stubs_end: .equ stubs_offset, __vectors_start + 0x200 - __stubs_start .globl __vectors_start__vectors_start: ARM( swi SYS_ERROR0 ) THUMB( svc #0 ) THUMB( nop ) W(b) vector_und + stubs_offset W(ldr) pc, .LCvswi + stubs_offset W(b) vector_pabt + stubs_offset W(b) vector_dabt + stubs_offset W(b) vector_addrexcptn + stubs_offset W(b) vector_irq + stubs_offset W(b) vector_fiq + stubs_offset .globl __vectors_end__vectors_end: 代码被分为两部分: 第一部分是真正的向量跳转表,位于__vectors_start和__vectors_end之间; 第二部分是处理跳转的部分,位于__stubs_start和__stubs_end之间; vector_stub irq, IRQ_MODE, 4 以上这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。 vector_stub dabt, ABT_MODE, 8 以上这一句把宏展开后实际上就是定义了vector_dabt,根据进入中断前的cpu模式,分别跳转到__dabt_usr或__dabt_svc。 系统启动阶段,位于arch/arm/kernel/traps.c中的early_trap_init()被调用: void __init early_trap_init(void){ ...... /* * Copy the vectors, stubs and kuser helpers (in entry-armv.S) * into the vector page, mapped at 0xffff0000, and ensure these * are visible to the instruction stream. */ memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start); ......} 以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x200处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。 对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两者的区别是进入和退出中断时是否进行用户栈和内核栈之间的切换,还有进程调度和抢占的处理等,这些细节不在这里讨论。两个函数最终都会进入irq_handler这个宏: .macro irq_handler#ifdef CONFIG_MULTI_IRQ_HANDLER ldr r1, =handle_arch_irq mov r0, sp adr lr, BSYM(9997f) ldr pc, [r1]#else arch_irq_handler_default#endif9997: .endm 如果选择了MULTI_IRQ_HANDLER配置项,则意味着允许平台的代码可以动态设置irq处理程序,平台代码可以修改全局变量:handle_arch_irq,从而可以修改irq的处理程序。这里我们讨论默认的实现:arch_irq_handler_default,它位于arch/arm/include/asm/entry_macro_multi.S中: .macro arch_irq_handler_default get_irqnr_preamble r6, lr1: get_irqnr_and_base r0, r2, r6, lr movne r1, sp @ @ routine called with r0 = irq number, r1 = struct pt_regs * @ adrne lr, BSYM(1b) bne asm_do_IRQ ...... get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是IRQ编号和寄存器结构指针,这个函数在arch/arm/kernel/irq.c中实现: /* * asm_do_IRQ is the interface to be used from assembly code. */asmlinkage void __exception_irq_entryasm_do_IRQ(unsigned int irq, struct pt_regs *regs){ handle_IRQ(irq, regs);} 到这里,中断程序完成了从asm代码到C代码的传递,并且获得了引起中断的IRQ编号。 2. 初始化 与通用中断子系统相关的初始化由start_kernel()函数发起,调用流程如下图所视: 首先,在setup_arch函数中,early_trap_init被调用,其中完成了第1节所说的中断向量的拷贝和重定位工作。 然后,start_kernel发出early_irq_init调用,early_irq_init属于与硬件和平台无关的通用逻辑层,它完成irq_desc结构的内存申请,为它们其中某些字段填充默认值,完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM体系没有实现arch_early_irq_init。 接着,start_kernel发出init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义。 machine_desc->init_irq()完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联。 3. 中断控制器的软件抽象:struct irq_chip 正如第一节所述,所有的硬件中断在到达CPU之前,都要先经过中断控制器进行汇集,合乎要求的中断请求才会通知cpu进行处理,中断控制器主要完成以下这些功能: 对各个irq的优先级进行控制; 向CPU发出中断请求后,提供某种机制让CPU获得实际的中断源(irq编号); 控制各个irq的电气触发条件,例如边缘触发或者是电平触发; 使能(enable)或者屏蔽(mask)某一个irq; 提供嵌套中断请求的能力; 提供清除中断请求的机制(ack); 有些控制器还需要CPU在处理完irq后对控制器发出eoi指令(end of interrupt); 在smp系统中,控制各个irq与cpu之间的亲缘关系(affinity); 通用中断子系统把中断控制器抽象为一个数据结构:struct irq_chip,其中定义了一系列的操作函数,大部分多对应于上面所列的某个功能: struct irq_chip { 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); int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); int (*irq_retrigger)(struct irq_data *data); int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); int (*irq_set_wake)(struct irq_data *data, unsigned int on); void (*irq_bus_lock)(struct irq_data *data); void (*irq_bus_sync_unlock)(struct irq_data *data); void (*irq_cpu_online)(struct irq_data *data); void (*irq_cpu_offline)(struct irq_data *data); void (*irq_suspend)(struct irq_data *data); void (*irq_resume)(struct irq_data *data); void (*irq_pm_shutdown)(struct irq_data *data); void (*irq_print_chip)(struct irq_data *data, struct seq_file *p); unsigned long flags; /* Currently used only by UML, might disappear one day.*/#ifdef CONFIG_IRQ_RELEASE_METHOD void (*release)(unsigned int irq, void *dev_id);#endif}; 各个字段解释如下: name 中断控制器的名字,会出现在 /proc/interrupts中。 irq_startup 第一次开启一个irq时使用。 irq_shutdown 与irq_starup相对应。 irq_enable 使能该irq,通常是直接调用irq_unmask()。 irq_disable 禁止该irq,通常是直接调用irq_mask,严格意义上,他俩其实代表不同的意义,disable表示中断控制器根本就不响应该irq,而mask时,中断控制器可能响应该irq,只是不通知CPU,这时,该irq处于pending状态。类似的区别也适用于enable和unmask。 irq_ack 用于CPU对该irq的回应,通常表示cpu希望要清除该irq的pending状态,准备接受下一个irq请求。 irq_mask 屏蔽该irq。 irq_unmask 取消屏蔽该irq。 irq_mask_ack 相当于irq_mask + irq_ack。 irq_eoi 有些中断控制器需要在cpu处理完该irq后发出eoi信号,该回调就是用于这个目的。 irq_set_affinity 用于设置该irq和cpu之间的亲缘关系,就是通知中断控制器,该irq发生时,那些cpu有权响应该irq。当然,中断控制器会在软件的配合下,最终只会让一个cpu处理本次请求。 irq_set_type 设置irq的电气触发条件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。 irq_set_wake 通知电源管理子系统,该irq是否可以用作系统的唤醒源。 以上大部分的函数接口的参数都是irq_data结构指针,irq_data结构的由来在上一篇文章已经说过,这里仅贴出它的定义,各字段的意义请参考注释: /** * struct irq_data - per irq and irq chip data passed down to chip functions * @irq: interrupt number * @hwirq: hardware interrupt number, local to the interrupt domain * @node: node index useful for balancing * @state_use_accessors: status information for irq chip functions. * Use accessor functions to deal with it * @chip: low level interrupt hardware access * @domain: Interrupt translation domain; responsible for mapping * between hwirq number and linux irq number. * @handler_data: per-IRQ data for the irq_chip methods * @chip_data: platform-specific per-chip private data for the chip * methods, to allow shared chip implementations * @msi_desc: MSI descriptor * @affinity: IRQ affinity on SMP * * The fields here need to overlay the ones in irq_desc until we * cleaned up the direct references and switched everything over to * irq_data. */struct irq_data { unsigned int irq; unsigned long hwirq; unsigned int node; unsigned int state_use_accessors; struct irq_chip *chip; struct irq_domain *domain; void *handler_data; void *chip_data; struct msi_desc *msi_desc;#ifdef CONFIG_SMP cpumask_var_t affinity;#endif}; 根据设备使用的中断控制器的类型,体系架构的底层的开发只要实现上述接口中的各个回调函数,然后把它们填充到irq_chip结构的实例中,最终把该irq_chip实例注册到irq_desc.irq_data.chip字段中,这样各个irq和中断控制器就进行了关联,只要知道irq编号,即可得到对应到irq_desc结构,进而可以通过chip指针访问中断控制器。 4. 进入流控处理层 进入C代码的第一个函数是asm_do_IRQ,在ARM体系中,这个函数只是简单地调用handle_IRQ: /* * asm_do_IRQ is the interface to be used from assembly code. */asmlinkage void __exception_irq_entryasm_do_IRQ(unsigned int irq, struct pt_regs *regs){ handle_IRQ(irq, regs);}handle_IRQ本身也不是很复杂:void handle_IRQ(unsigned int irq, struct pt_regs *regs){ struct pt_regs *old_regs = set_irq_regs(regs); irq_enter(); /* * Some hardware gives randomly wrong interrupts. Rather * than crashing, do something sensible. */ if (unlikely(irq >= nr_irqs)) { if (printk_ratelimit()) printk(KERN_WARNING "Bad IRQ%u\n", irq); ack_bad_irq(irq); } else { generic_handle_irq(irq); } /* AT91 specific workaround */ irq_finish(irq); irq_exit(); set_irq_regs(old_regs);} irq_enter主要是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占: #define __irq_enter() \ do { \ account_system_vtime(current); \ add_preempt_count(HARDIRQ_OFFSET); \ trace_hardirq_enter(); \ } while (0) CPU一旦响应IRQ中断后,ARM会自动把CPSR中的I位置位,表明禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。你可能会奇怪,既然此时的irq中断都是都是被禁止的,为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。 下一步,generic_handle_irq被调用,generic_handle_irq是通用逻辑层提供的API,通过该API,中断的控制被传递到了与体系结构无关的中断流控层: int generic_handle_irq(unsigned int irq){ struct irq_desc *desc = irq_to_desc(irq); if (!desc) return -EINVAL; generic_handle_irq_desc(irq, desc); return 0;} 最终会进入该irq注册的流控处理回调中: static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc){ desc->handle_irq(irq, desc);} 5. 中断控制器的级联 在实际的设备中,经常存在多个中断控制器,有时多个中断控制器还会进行所谓的级联。为了方便讨论,我们把直接和CPU相连的中断控制器叫做根控制器,另外一些和跟控制器相连的叫子控制器。根据子控制器的位置,我们把它们分为两种类型: 机器级别的级联 子控制器位于SOC内部,或者子控制器在SOC的外部,但是是某个板子系列的标准配置,如图5.1的左边所示; 设备级别的级联 子控制器位于某个外部设备中,用于汇集该设备发出的多个中断,如图5.1的右边所示; 对于机器级别的级联,级联的初始化代码理所当然地位于板子的初始化代码中(arch/xxx/mach-xxx),因为只要是使用这个板子或SOC的设备,必然要使用这个子控制器。而对于设备级别的级联,因为该设备并不一定是系统的标配设备,所以中断控制器的级联操作应该在该设备的驱动程序中实现。机器设备的级联,因为得益于事先已经知道子控制器的硬件连接信息,内核可以方便地为子控制器保留相应的irq_desc结构和irq编号,处理起来相对简单。设备级别的级联则不一样,驱动程序必须动态地决定组合设备中各个子设备的irq编号和irq_desc结构。这里只讨论机器级别的级联,设备级别的关联可以使用同样的原理,也可以实现为共享中断,会在下文讨论。 要实现中断控制器的级联,要使用以下几个的关键数据结构字段和通用中断逻辑层的API: irq_desc.handle_irq irq的流控处理回调函数,子控制器在把多个irq汇集起来后,输出端连接到根控制器的其中一个irq中断线输入脚,这意味着,每个子控制器的中断发生时,CPU一开始只会得到根控制器的irq编号,然后进入该irq编号对应的irq_desc.handle_irq回调,该回调我们不能使用流控层定义好的几个流控函数,而是要自己实现一个函数,该函数负责从子控制器中获得irq的中断源,并计算出对应新的irq编号,然后调用新irq所对应的irq_desc.handle_irq回调,这个回调使用流控层的标准实现。 irq_set_chained_handler() 该API用于设置根控制器与子控制器相连的irq所对应的irq_desc.handle_irq回调函数,并且设置IRQ_NOPROBE和IRQ_NOTHREAD以及IRQ_NOREQUEST标志,这几个标志保证驱动程序不会错误地申请该irq,因为该irq已经被作为级联irq使用。 irq_set_chip_and_handler() 该API同时设置irq_desc中的handle_irq回调和irq_chip指针。 以下例子代码位于:/arch/arm/plat-s5p/irq-eint.c: int __init s5p_init_irq_eint(void){ int irq; for (irq = IRQ_EINT(0); irq <= IRQ_EINT(15); irq++) irq_set_chip(irq, &s5p_irq_vic_eint); for (irq = IRQ_EINT(16); irq <= IRQ_EINT(31); irq++) { irq_set_chip_and_handler(irq, &s5p_irq_eint, handle_level_irq); set_irq_flags(irq, IRQF_VALID); } irq_set_chained_handler(IRQ_EINT16_31, s5p_irq_demux_eint16_31); return 0;} 该SOC芯片的外部中断:IRQ_EINT(0)到IRQ_EINT(15),每个引脚对应一个根控制器的irq中断线,它们是正常的irq,无需级联。IRQ_EINT(16)到IRQ_EINT(31)经过子控制器汇集后,统一连接到根控制器编号为IRQ_EINT16_31这个中断线上。可以看到,子控制器对应的irq_chip是s5p_irq_eint,子控制器的irq默认设置为电平中断的流控处理函数handle_level_irq,它们通过API:irq_set_chained_handler进行设置。如果根控制器有128个中断线,IRQ_EINT0--IRQ_EINT15通常占据128内的某段连续范围,这取决于实际的物理连接。IRQ_EINT16_31因为也属于跟控制器,所以它的值也会位于128以内,但是IRQ_EINT16--IRQ_EINT31通常会在128以外的某段范围,这时,代表irq数量的常量NR_IRQS,必须考虑这种情况,定义出超过128的某个足够的数值。级联的实现主要依靠编号为IRQ_EINT16_31的流控处理程序:s5p_irq_demux_eint16_31,它的最终实现类似于以下代码: static inline void s5p_irq_demux_eint(unsigned int start){ u32 status = __raw_readl(S5P_EINT_PEND(EINT_REG_NR(start))); u32 mask = __raw_readl(S5P_EINT_MASK(EINT_REG_NR(start))); unsigned int irq; status &= ~mask; status &= 0xff; while (status) { irq = fls(status) - 1; generic_handle_irq(irq + start); status &= ~(1 << irq); }} 在获得新的irq编号后,它的最关键的一句是调用了通用中断逻辑层的API:generic_handle_irq,这时它才真正地把中断控制权传递到中断流控层中来。 三、中断流控处理层 1. 中断流控层简介 早期的内核版本中,几乎所有的中断都是由__do_IRQ函数进行处理,但是,因为各种中断请求的电气特性会有所不同,又或者中断控制器的特性也不同,这会导致以下这些处理也会有所不同: 何时对中断控制器发出ack回应; mask_irq和unmask_irq的处理; 中断控制器是否需要eoi回应? 何时打开cpu的本地irq中断?以便允许irq的嵌套; 中断数据结构的同步和保护; 为此,通用中断子系统把几种常用的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。这些标准的回调函数都是irq_flow_handler_t类型: typedef void (*irq_flow_handler_t)(unsigned int irq, struct irq_desc *desc); 目前的通用中断子系统实现了以下这些标准流控回调函数,这些函数都定义在:kernel/irq/chip.c中, handle_simple_irq 用于简易流控处理; handle_level_irq 用于电平触发中断的流控处理; handle_edge_irq 用于边沿触发中断的流控处理; handle_fasteoi_irq 用于需要响应eoi的中断控制器; handle_percpu_irq 用于只在单一cpu响应的中断; handle_nested_irq 用于处理使用线程的嵌套中断; 驱动程序和板级代码可以通过以下几个API设置irq的流控函数: irq_set_handler(); irq_set_chip_and_handler(); irq_set_chip_and_handler_name(); 以下这个序列图展示了整个通用中断子系统的中断响应过程,flow_handle一栏就是中断流控层的生命周期: 2. handle_simple_irq 该函数没有实现任何实质性的流控操作,在把irq_desc结构锁住后,直接调用handle_irq_event处理irq_desc中的action链表,它通常用于多路复用(类似于中断控制器级联)中的子中断,由父中断的流控回调中调用。或者用于无需进行硬件控制的中断中。以下是它的经过简化的代码: voidhandle_simple_irq(unsigned int irq, struct irq_desc *desc){ raw_spin_lock(&desc->lock); ...... handle_irq_event(desc); out_unlock: raw_spin_unlock(&desc->lock);} 3. handle_level_irq 该函数用于处理电平中断的流控操作。电平中断的特点是,只要设备的中断请求引脚(中断线)保持在预设的触发电平,中断就会一直被请求,所以,为了避免同一中断被重复响应,必须在处理中断前先把mask irq,然后ack irq,以便复位设备的中断请求引脚,响应完成后再unmask irq。实际的情况稍稍复杂一点,在mask和ack之后,还要判断IRQ_INPROGRESS标志位,如果该标志已经置位,则直接退出,不再做实质性的处理,IRQ_INPROGRESS标志在handle_irq_event的开始设置,在handle_irq_event结束时清除,如果监测到IRQ_INPROGRESS被置位,表明该irq正在被另一个CPU处理中,所以直接退出,对电平中断来说是正确的处理方法。但是我觉得在ARM系统中,这种情况根本就不会发生,因为在没有进入handle_level_irq之前,中断控制器没有收到ack通知,它不会向第二个CPU再次发出中断请求,而当程序进入handle_level_irq之后,第一个动作就是mask irq,然后ack irq(通常是联合起来的:mask_ack_irq),这时候就算设备再次发出中断请求,也是在handle_irq_event结束,unmask irq之后,这时IRQ_INPROGRESS标志已经被清除。我不知道其他像X86之类的体系是否有不同的行为,有知道的朋友请告知我一下。以下是handle_level_irq经过简化之后的代码: voidhandle_level_irq(unsigned int irq, struct irq_desc *desc){ raw_spin_lock(&desc->lock); mask_ack_irq(desc); if (unlikely(irqd_irq_inprogress(&desc->irq_data))) goto out_unlock; ...... if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) goto out_unlock; handle_irq_event(desc); if (!irqd_irq_disabled(&desc->irq_data) && !(desc->istate & IRQS_ONESHOT)) unmask_irq(desc);out_unlock: raw_spin_unlock(&desc->lock);} 虽然handle_level_irq对电平中断的流控进行了必要的处理,因为电平中断的特性:只要没有ack irq,中断线会一直有效,所以我们不会错过某次中断请求,但是驱动程序的开发人员如果对该过程理解不透彻,特别容易发生某次中断被多次处理的情况。特别是使用了中断线程(action->thread_fn)来响应中断的时候:通常mask_ack_irq只会清除中断控制器的pending状态,很多慢速设备(例如通过i2c或spi控制的设备)需要在中断线程中清除中断线的pending状态,但是未等到中断线程被调度执行的时候,handle_level_irq早就返回了,这时已经执行过unmask_irq,设备的中断线pending处于有效状态,中断控制器会再次发出中断请求,结果是设备的一次中断请求,产生了两次中断响应。要避免这种情况,最好的办法就是不要单独使用中断线程处理中断,而是要实现request_threaded_irq()的第二个参数irq_handler_t:handler,在handle回调中使用disable_irq()关闭该irq,然后在退出中断线程回调前再enable_irq()。假设action->handler没有屏蔽irq,以下这幅图展示了电平中断期间IRQ_PROGRESS标志、本地中断状态和触发其他CPU的状态: 上图中颜色分别代表不同的状态: 状态 红色 绿色 IRQ_PROGRESS TRUE FALSE 是否允许本地cpu中断 禁止 允许 是否允许该设备再次触发中断(可能由其它cpu响应) 禁止 允许 4. handle_edge_irq 该函数用于处理边沿触发中断的流控操作。边沿触发中断的特点是,只有设备的中断请求引脚(中断线)的电平发生跳变时(由高变低或者有低变高),才会发出中断请求,因为跳变是一瞬间,而且不会像电平中断能保持住电平,所以处理不当就特别容易漏掉一次中断请求,为了避免这种情况,屏蔽中断的时间必须越短越好。内核的开发者们显然意识到这一点,在正是处理中断前,判断IRQ_PROGRESS标志没有被设置的情况下,只是ack irq,并没有mask irq,以便复位设备的中断请求引脚,在这之后的中断处理期间,另外的cpu可以再次响应同一个irq请求,如果IRQ_PROGRESS已经置位,表明另一个CPU正在处理该irq的上一次请求,这种情况下,他只是简单地设置IRQS_PENDING标志,然后mask_ack_irq后退出,中断请求交由原来的CPU继续处理。因为是mask_ack_irq,所以系统实际上只允许挂起一次中断。 if (unlikely(irqd_irq_disabled(&desc->irq_data) || irqd_irq_inprogress(&desc->irq_data) || !desc->action)) { if (!irq_check_poll(desc)) { desc->istate |= IRQS_PENDING; mask_ack_irq(desc); goto out_unlock; } } desc->irq_data.chip->irq_ack(&desc->irq_data); 从上面的分析可以知道,处理中断期间,另一次请求可能由另一个cpu响应后挂起,所以在处理完本次请求后还要判断IRQS_PENDING标志,如果被置位,当前cpu要接着处理被另一个cpu“委托”的请求。内核在这里设置了一个循环来处理这种情况,直到IRQS_PENDING标志无效为止,而且因为另一个cpu在响应并挂起irq时,会mask irq,所以在循环中要再次unmask irq,以便另一个cpu可以再次响应并挂起irq: do { ...... if (unlikely(desc->istate & IRQS_PENDING)) { if (!irqd_irq_disabled(&desc->irq_data) && irqd_irq_masked(&desc->irq_data)) unmask_irq(desc); } handle_irq_event(desc); } while ((desc->istate & IRQS_PENDING) && !irqd_irq_disabled(&desc->irq_data)); IRQS_PENDING标志会在handle_irq_event中清除。 上图中颜色分别代表不同的状态: 状态 红色 绿色 IRQ_PROGRESS TRUE FALSE 是否允许本地cpu中断 禁止 允许 是否允许该设备再次触发中断(可能由其它cpu响应) 禁止 允许 是否处于中断上下文 处于中断上下文 处于进程上下文 由上图也可以看出,在处理软件中断(softirq)期间,此时仍然处于中断上下文中,但是cpu的本地中断是处于打开状态的,这表明此时嵌套中断允许发生,不过这不要紧,因为重要的处理已经完成,被嵌套的也只是软件中断部分而已。这个也就是内核区分top和bottom两个部分的初衷吧。 5. handle_fasteoi_irq 现代的中断控制器通常会在硬件上实现了中断流控功能,例如ARM体系中的GIC通用中断控制器。对于这种中断控制器,CPU只需要在每次处理完中断后发出一个end of interrupt(eoi),我们无需关注何时mask,何时unmask。不过虽然想着很完美,事情总有特殊的时候,所以内核还是给了我们插手的机会,它利用irq_desc结构中的preflow_handler字段,在正式处理中断前会通过preflow_handler函数调用该回调。 voidhandle_fasteoi_irq(unsigned int irq, struct irq_desc *desc){ raw_spin_lock(&desc->lock); if (unlikely(irqd_irq_inprogress(&desc->irq_data))) if (!irq_check_poll(desc)) goto out; ...... if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) { desc->istate |= IRQS_PENDING; mask_irq(desc); goto out; } if (desc->istate & IRQS_ONESHOT) mask_irq(desc); preflow_handler(desc); handle_irq_event(desc); out_eoi: desc->irq_data.chip->irq_eoi(&desc->irq_data);out_unlock: raw_spin_unlock(&desc->lock); return; ......} 此外,内核还提供了另外一个eoi版的函数: handle_edge_eoi_irq,它的处理类似于handle_edge_irq,只是无需实现mask和unmask的逻辑。 6. handle_percpu_irq 该函数用于smp系统,当某个irq只在一个cpu上处理时,我们可以无需用自旋锁对数据进行保护,也无需处理cpu之间的中断嵌套重入,所以函数很简单: voidhandle_percpu_irq(unsigned int irq, struct irq_desc *desc){ struct irq_chip *chip = irq_desc_get_chip(desc); kstat_incr_irqs_this_cpu(irq, desc); if (chip->irq_ack) chip->irq_ack(&desc->irq_data); handle_irq_event_percpu(desc, desc->action); if (chip->irq_eoi) chip->irq_eoi(&desc->irq_data);} 7. handle_nested_irq 该函数用于实现其中一种中断共享机制,当多个中断共享某一根中断线时,我们可以把这个中断线作为父中断,共享该中断的各个设备作为子中断,在父中断的中断线程中决定和分发响应哪个设备的请求,在得出真正发出请求的子设备后,调用handle_nested_irq来响应中断。所以,该函数是在进程上下文执行的,我们也无需扫描和执行irq_desc结构中的action链表。父中断在初始化时必须通过irq_set_nested_thread函数明确告知中断子系统:这些子中断属于线程嵌套中断类型,这样驱动程序在申请这些子中断时,内核不会为它们建立自己的中断线程,所有的子中断共享父中断的中断线程。 void handle_nested_irq(unsigned int irq){ ...... might_sleep(); raw_spin_lock_irq(&desc->lock); ...... action = desc->action; if (unlikely(!action || irqd_irq_disabled(&desc->irq_data))) goto out_unlock; irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); raw_spin_unlock_irq(&desc->lock); action_ret = action->thread_fn(action->irq, action->dev_id); raw_spin_lock_irq(&desc->lock); irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); out_unlock: raw_spin_unlock_irq(&desc->lock);} 四、驱动程序接口层 & 中断通用逻辑层 本节讨论驱动程序接口层和中断通用逻辑层对外提供的标准接口和内部实现机制,几乎所有的接口都是围绕着irq_desc和irq_chip这两个结构体进行的 1. irq的打开和关闭 中断子系统为我们提供了一系列用于irq的打开和关闭的函数接口,其中最基本的一对是: disable_irq(unsigned int irq); enable_irq(unsigned int irq); 这两个API应该配对使用,disable_irq可以被多次嵌套调用,要想重新打开irq,enable_irq必须也要被调用同样的次数,为此,irq_desc结构中的depth字段专门用于这两个API嵌套深度的管理。当某个irq首次被驱动程序申请时,默认情况下,设置depth的初始值是0,对应的irq处于打开状态。我们看看disable_irq的调用过程: 函数的开始使用异步方式的内部函数__disable_irq_nosync(),所谓异步方式就是不理会当前该irq是否正在被处理(有handler在运行或者有中断线程尚未结束)。有些中断控制器可能挂在某个慢速的总线上,所以在进一步处理前,先通过irq_get_desc_buslock获得总线锁(最终会调用chip->irq_bus_lock),然后进入内部函数__disable_irq: void __disable_irq(struct irq_desc *desc, unsigned int irq, bool suspend){ if (suspend) { if (!desc->action || (desc->action->flags & IRQF_NO_SUSPEND)) return; desc->istate |= IRQS_SUSPENDED; } if (!desc->depth++) irq_disable(desc);} 前面几句是对suspend的处理,最后两句,只有之前的depth为0,才会通过irq_disable函数,调用中断控制器的回调chip->irq_mask,否则只是简单地把depth的值加1。irq_disable函数还会通过irq_state_set_disabled和irq_state_set_masked,设置irq_data.flag的IRQD_IRQ_DISABLED和IRQD_IRQ_MASK标志。 disable_irq的最后,调用了synchronize_irq,该函数通过IRQ_INPROGRESS标志,确保action链表中所有的handler都已经处理完毕,然后还要通过wait_event等待该irq所有的中断线程退出。正因为这样,在中断上下文中,不应该使用该API来关闭irq,同时要确保调用该API的函数不能拥有该irq处理函数或线程的资源,否则就会发生死锁!!如果一定要在这两种情况下关闭irq,中断子系统为我们提供了另外一个API,它不会做出任何等待动作: disable_irq_nosync(); 中断子系统打开irq的的API是: enable_irq(); 打开irq无需提供同步的版本,因为irq打开前,没有handler和线程在运行,我们关注一下他对depth的处理,他在内部函数__enable_irq中处理: void __enable_irq(struct irq_desc *desc, unsigned int irq, bool resume){ if (resume) { ...... } switch (desc->depth) { case 0: err_out: WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n", irq); break; case 1: { ...... irq_enable(desc); ...... } default: desc->depth--; }} 当depth的值为1时,才真正地调用irq_enable(),它最终通过chip->unmask或chip->enable回调开启中断控制器中相应的中断线,如果depth不是1,只是简单地减去1。如果已经是0,驱动还要调用enable_irq,说明驱动程序处理不当,造成enable与disable不平衡,内核会打印一句警告信息:Unbalanced enable for IRQ xxx。 2. 中断子系统内部数据结构访问接口 我们知道,中断子系统内部定义了几个重要的数据结构,例如:irq_desc,irq_chip,irq_data等等,这些数据结构的各个字段控制或影响着中断子系统和各个irq的行为和实现方式。通常,驱动程序不应该直接访问这些数据结构,直接访问会破会中断子系统的封装性,为此,中断子系统为我们提供了一系列的访问接口函数,用于访问这些数据结构。 存取irq_data结构相关字段的API: irq_set_chip(irq, *chip) / irq_get_chip(irq) 通过irq编号,设置、获取irq_cip结构指针; irq_set_handler_data(irq, *data) / irq_get_handler_data(irq) 通过irq编号,设置、获取irq_desc.irq_data.handler_data字段,该字段是每个irq的私有数据,通常用于硬件封装层,例如中断控制器级联时,父irq用该字段保存子irq的起始编号。 irq_set_chip_data(irq, *data) / irq_get_chip_data(irq) 通过irq编号,设置、获取irq_desc.irq_data.chip_data字段,该字段是每个中断控制器的私有数据,通常用于硬件封装层。 irq_set_irq_type(irq, type) 用于设置中断的电气类型,可选的类型有: IRQ_TYPE_EDGE_RISING IRQ_TYPE_EDGE_FALLING IRQ_TYPE_EDGE_BOTH IRQ_TYPE_LEVEL_HIGH IRQ_TYPE_LEVEL_LOW irq_get_irq_data(irq) 通过irq编号,获取irq_data结构指针; irq_data_get_irq_chip(irq_data *d) 通过irq_data指针,获取irq_chip字段; irq_data_get_irq_chip_data(irq_data *d) 通过irq_data指针,获取chip_data字段; irq_data_get_irq_handler_data(irq_data *d) 通过irq_data指针,获取handler_data字段; 设置中断流控处理回调API: irq_set_handler(irq, handle) 设置中断流控回调字段:irq_desc.handle_irq,参数handle的类型是irq_flow_handler_t。 irq_set_chip_and_handler(irq, *chip, handle) 同时设置中断流控回调字段和irq_chip指针:irq_desc.handle_irq和irq_desc.irq_data.chip。 irq_set_chip_and_handler_name(irq, *chip, handle, *name) 同时设置中断流控回调字段和irq_chip指针以及irq名字:irq_desc.handle_irq、irq_desc.irq_data.chip、irq_desc.name。 irq_set_chained_handler(irq, *chip, handle) 设置中断流控回调字段:irq_desc.handle_irq,同时设置标志:IRQ_NOREQUEST、IRQ_NOPROBE、IRQ_NOTHREAD,该api通常用于中断控制器的级联,父控制器通过该api设置流控回调后,同时设置上述三个标志位,使得父控制器的中断线不允许被驱动程序申请。 3. 在驱动程序中申请中断 系统启动阶段,中断子系统完成了必要的初始化工作,为驱动程序申请中断服务做好了准备,通常,我们用一下API申请中断服务: request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev); irq 需要申请的irq编号,对于ARM体系,irq编号通常在平台级的代码中事先定义好,有时候也可以动态申请。 handler 中断服务回调函数,该回调运行在中断上下文中,并且cpu的本地中断处于关闭状态,所以该回调函数应该只是执行需要快速响应的操作,执行时间应该尽可能短小,耗时的工作最好留给下面的thread_fn回调处理。 thread_fn 如果该参数不为NULL,内核会为该irq创建一个内核线程,当中断发生时,如果handler回调返回值是IRQ_WAKE_THREAD,内核将会激活中断线程,在中断线程中,该回调函数将被调用,所以,该回调函数运行在进程上下文中,允许进行阻塞操作。 flags 控制中断行为的位标志,IRQF_XXXX,例如:IRQF_TRIGGER_RISING,IRQF_TRIGGER_LOW,IRQF_SHARED等,在include/linux/interrupt.h中定义。 name 申请本中断服务的设备名称,同时也作为中断线程的名称,该名称可以在/proc/interrupts文件中显示。 dev 当多个设备的中断线共享同一个irq时,它会作为handler的参数,用于区分不同的设备。 下面我们分析一下request_threaded_irq的工作流程。函数先是根据irq编号取出对应的irq_desc实例的指针,然后分配了一个irqaction结构,用参数handler,thread_fn,irqflags,devname,dev_id初始化irqaction结构的各字段,同时做了一些必要的条件判断:该irq是否禁止申请?handler和thread_fn不允许同时为NULL,最后把大部分工作委托给__setup_irq函数: desc = irq_to_desc(irq); if (!desc) return -EINVAL; if (!irq_settings_can_request(desc) || WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL; if (!handler) { if (!thread_fn) return -EINVAL; handler = irq_default_primary_handler; } action = kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM; action->handler = handler; action->thread_fn = thread_fn; action->flags = irqflags; action->name = devname; action->dev_id = dev_id; chip_bus_lock(desc); retval = __setup_irq(irq, desc, action); chip_bus_sync_unlock(desc); 进入__setup_irq函数,如果参数flag中设置了IRQF_SAMPLE_RANDOM标志,它会调用rand_initialize_irq,以便对随机数的生成产生影响。如果申请的不是一个线程嵌套中断,而且提供了thread_fn参数,它将创建一个内核线程: if (new->thread_fn && !nested) { struct task_struct *t; t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name); if (IS_ERR(t)) { ret = PTR_ERR(t); goto out_mput; } /* * We keep the reference to the task struct even if * the thread dies to avoid that the interrupt code * references an already freed task_struct. */ get_task_struct(t); new->thread = t; } 如果irq_desc结构中断action链表不为空,说明这个irq已经被其它设备申请过,也就是说,这是一个共享中断,所以接下来会判断这个新申请的中断与已经申请的旧中断的以下几个标志是否一致: 一定要设置了IRQF_SHARED标志 电气触发方式要完全一样(IRQF_TRIGGER_XXXX) IRQF_PERCPU要一致 IRQF_ONESHOT要一致 检查这些条件都是因为多个设备试图共享一根中断线,试想一下,如果一个设备要求上升沿中断,一个设备要求电平中断,当中断到达时,内核将不知如何选择合适的流控操作。完成检查后,函数找出action链表中最后一个irqaction实例的指针。 /* add new interrupt at end of irq queue */ do { thread_mask |= old->thread_mask; old_ptr = &old->next; old = *old_ptr; } while (old); shared = 1; 如果这不是一个共享中断,或者是共享中断的第一次申请,函数将初始化irq_desc结构中断线程等待结构:wait_for_threads,disable_irq函数会使用该字段等待所有irq线程的结束。接下来设置中断控制器的电气触发类型,然后处理一些必要的IRQF_XXXX标志位。如果没有设置IRQF_NOAUTOEN标志,则调用irq_startup()打开该irq,在irq_startup()函数中irq_desc中的enable_irq/disable_irq嵌套深度字段depth设置为0,代表该irq已经打开,如果在没有任何disable_irq被调用的情况下,enable_irq将会打印一个警告信息。 if (irq_settings_can_autoenable(desc)) irq_startup(desc); else /* Undo nested disables: */ desc->depth = 1; 接着,设置cpu和irq的亲缘关系: /* Set default affinity mask once everything is setup */ setup_affinity(irq, desc, mask); 然后,把新的irqaction实例链接到action链表的最后: new->irq = irq;*old_ptr = new; 最后,唤醒中断线程,注册相关的/proc文件节点: if (new->thread) wake_up_process(new->thread); register_irq_proc(irq, desc); new->dir = NULL; register_handler_proc(irq, new); 至此,irq的申请宣告完毕,当中断发生时,处理的路径将会沿着:irq_desc.handle_irq,irqaction.handler,irqaction.thread_fn(irqaction.handler的返回值是IRQ_WAKE_THREAD)这个过程进行处理。下图表明了某个irq被申请后,各个数据结构之间的关系: 4. 动态扩展irq编号 在ARM体系的移动设备中,irq的编号通常在平台级或板级代码中事先根据硬件的连接定义好,最大的irq数目也用NR_IRQS常量指定。几种情况下,我们希望能够动态地增加系统中irq的数量: 配置了CONFIG_SPARSE_IRQ内核配置项,使用基数树动态管理irq_desc结构。 针对多功能复合设备,内部具备多个中断源,但中断触发引脚只有一个,为了实现驱动程序的跨平台,不希望这些中断源的irq被硬编码在板级代码中。 中断子系统为我们提供了以下几个api,用于动态申请/扩展irq编号: irq_alloc_desc(node) 申请一个irq,node是对应内存节点的编号; irq_alloc_desc_at(at, node) 在指定位置申请一个irq,如果指定位置已经被占用,则申请失败; irq_alloc_desc_from(from, node) 从指定位置开始搜索,申请一个irq; irq_alloc_descs(irq, from, cnt, node) 申请多个连续的irq编号,从from位置开始搜索; irq_free_descs(irq, cnt) 释放irq资源; 以上这些申请函数(宏),会为我们申请相应的irq_desc结构并初始化为默认状态,要想这些irq能够正常工作,我们还要使用第二节提到的api,对必要的字段进行设置,例如: irq_set_chip_and_handler_name irq_set_handler_data irq_set_chip_data 对于没有配置CONFIG_SPARSE_IRQ内核配置项的内核,irq_desc是一个数组,根本不可能做到动态扩展,但是很多驱动又确实使用到了上述api,尤其是mfd驱动,这些驱动并没有我们一定要配置CONFIG_SPARSE_IRQ选项,要想不对这些驱动做出修改,你只能妥协一下,在你的板级代码中把NR_IRQS定义得大一些,留出足够的保留数量 5. 多功能复合设备的中断处理 在移动设备系统中,存在着大量的多功能复合设备,最常见的是一个芯片中,内部集成了多个功能部件,或者是一个模块单元内部集成了功能部件,这些内部功能部件可以各自产生中断请求,但是芯片或者硬件模块对外只有一个中断请求引脚,我们可以使用多种方式处理这些设备的中断请求,以下我们逐一讨论这些方法。 5.1 单一中断模式 对于这种复合设备,通常设备中会提供某种方式,以便让CPU获取真正的中断来源, 方式可以是一个内部寄存器,gpio的状态等等。单一中断模式是指驱动程序只申请一个irq,然后在中断处理程序中通过读取设备的内部寄存器,获取中断源,然后根据不同的中断源做出不同的处理,以下是一个简化后的代码: static int xxx_probe(device *dev){ ...... irq = get_irq_from_dev(dev); ret = request_threaded_irq(irq, NULL, xxx_irq_thread, IRQF_TRIGGER_RISING, "xxx_dev", NULL); ...... return 0;} static irqreturn_t xxx_irq_thread(int irq, void *data){ ...... irq_src = read_device_irq(); switch (irq_src) { case IRQ_SUB_DEV0: ret = handle_sub_dev0_irq(); break; case IRQ_SUB_DEV1: ret = handle_sub_dev1_irq(); break; ...... default: ret = IRQ_NONE; break; } ...... return ret;} 5.2 共享中断模式 共享中断模式充分利用了通用中断子系统的特性,经过前面的讨论,我们知道,irq对应的irq_desc结构中的action字段,本质上是一个链表,这给我们实现中断共享提供了必要的基础,只要我们以相同的irq编号多次申请中断服务,那么,action链表上就会有多个irqaction实例,当中断发生时,中断子系统会遍历action链表,逐个执行irqaction实例中的handler回调,根据handler回调的返回值不同,决定是否唤醒中断线程。需要注意到是,申请多个中断时,irq编号要保持一致,flag参数最好也能保持一致,并且都要设上IRQF_SHARED标志。在使用共享中断时,最好handler和thread_fn都要提供,在各自的中断处理回调handler中,做出以下处理: 判断中断是否来自本设备; 如果不是来自本设备:直接返回IRQ_NONE; 如果是来自本设备: 关闭irq; 返回IRQ_WAKE_THREAD,唤醒中断线程,thread_fn将会被执行; 5.3 中断控制器级联模式 多数多功能复合设备内部提供了基本的中断控制器功能,例如可以单独地控制某个子中断的打开和关闭,并且可以方便地获得子中断源,对于这种设备,我们可以把设备内的中断控制器实现为一个子控制器,然后使用中断控制器级联模式。这种模式下,各个子设备拥有各自独立的irq编号,中断服务通过父中断进行分发。 对于父中断,具体的实现步骤如下: 首先,父中断的irq编号可以从板级代码的预定义中获得,或者通过device的platform_data字段获得; 使用父中断的irq编号,利用irq_set_chained_handler函数修改父中断的流控函数; 使用父中断的irq编号,利用irq_set_handler_data设置流控函数的参数,该参数要能够用于判别子控制器的中断来源; 实现父中断的流控函数,其中只需获得并计算子设备的irq编号,然后调用generic_handle_irq即可; 对于子设备,具体的实现步骤如下 为设备内的中断控制器实现一个irq_chip结构,实现其中必要的回调,例如irq_mask,irq_unmask,irq_ack等; 循环每一个子设备,做以下动作: 为每个子设备,使用irq_alloc_descs函数申请irq编号; 使用irq_set_chip_data设置必要的cookie数据; 使用irq_set_chip_and_handler设置子控制器的irq_chip实例和子irq的流控处理程序,通常使用标准的流控函数,例如handle_edge_irq; 子设备的驱动程序使用自身申请到的irq编号,按照正常流程申请中断服务即可。 5.4 中断线程嵌套模式 该模式与中断控制器级联模式大体相似,只不过级联模式时,父中断无需通过request_threaded_irq申请中断服务,而是直接更换了父中断的流控回调,在父中断的流控回调中实现子中断的二次分发。但是这在有些情况下会给我们带来不便,因为流控回调要获取子控制器的中断源,而流控回调运行在中断上下文中,对于那些子控制器需要通过慢速总线访问的设备,在中断上下文中访问显然不太合适,这时我们可以把子中断分发放在父中断的中断线程中进行,这就是我所说的所谓中断线程嵌套模式。下面是大概的实现过程: 对于父中断,具体的实现步骤如下: 首先,父中断的irq编号可以从板级代码的预定义中获得,或者通过device的platform_data字段获得; 使用父中断的irq编号,利用request_threaded_irq函数申请中断服务,需要提供thread_fn参数和dev_id参数; dev_id参数要能够用于判别子控制器的中断来源; 实现父中断的thread_fn函数,其中只需获得并计算子设备的irq编号,然后调用handle_nested_irq即可; 对于子设备,具体的实现步骤如下 为设备内的中断控制器实现一个irq_chip结构,实现其中必要的回调,例如irq_mask,irq_unmask,irq_ack等; 循环每一个子设备,做以下动作: 为每个子设备,使用irq_alloc_descs函数申请irq编号; 使用irq_set_chip_data设置必要的cookie数据; 使用irq_set_chip_and_handler设置子控制器的irq_chip实例和子irq的流控处理程序,通常使用标准的流控函数,例如handle_edge_irq; 使用irq_set_nested_thread函数,把子设备irq的线程嵌套特性打开; 子设备的驱动程序使用自身申请到的irq编号,按照正常流程申请中断服务即可。 应为子设备irq的线程嵌套特性被打开,使用request_threaded_irq申请子设备的中断服务时,即是是提供了handler参数,中断子系统也不会使用它,同时也不会为它创建中断线程,子设备的thread_fn回调是在父中断的中断线程中,通过handle_nested_irq调用的,也就是说,尽管子中断有自己独立的irq编号,但是它们没有独立的中断线程,只是共享了父中断的中断服务线程。 五、软件中断(softIRQ) 软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。软中断也是其他一些内核机制的基础,比如tasklet,高分辨率timer等。 1. 软件中断的数据结构 1.1 struct softirq_action 内核用softirq_action结构管理软件中断的注册和激活等操作,它的定义如下: struct softirq_action{ void (*action)(struct softirq_action *);}; 非常简单,只有一个用于回调的函数指针。软件中断的资源是有限的,内核目前只实现了10种类型的软件中断,它们是: enum{ HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS}; 内核的开发者们不建议我们擅自增加软件中断的数量,如果需要新的软件中断,尽可能把它们实现为基于软件中断的tasklet形式。与上面的枚举值相对应,内核定义了一个softirq_action的结构数组,每种软中断对应数组中的一项: static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; 1.2 irq_cpustat_t 多个软中断可以同时在多个cpu运行,就算是同一种软中断,也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t: typedef struct { unsigned int __softirq_pending;} ____cacheline_aligned irq_cpustat_t;irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned; __softirq_pending字段中的每一个bit,对应着某一个软中断,某个bit被置位,说明有相应的软中断等待处理。 1.3 软中断的守护进程ksoftirqd 在cpu的热插拔阶段,内核为每个cpu创建了一个用于执行软件中断的守护进程ksoftirqd,同时定义了一个per_cpu变量用于保存每个守护进程的task_struct结构指针: DEFINE_PER_CPU(struct task_struct *, ksoftirqd); 大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。 2. 触发软中断 要触发一个软中断,只要调用api:raise_softirq即可,它的实现很简单,先是关闭本地cpu中断,然后调用:raise_softirq_irqoff void raise_softirq(unsigned int nr){ unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags);} 再看看raise_softirq_irqoff: inline void raise_softirq_irqoff(unsigned int nr){ __raise_softirq_irqoff(nr); ...... if (!in_interrupt()) wakeup_softirqd();} 先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位(irq_stat[NR_CPUS] ),然后通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行。 3. 软中断的执行 基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。 3.1 在irq_exit中执行 看看irq_exit的部分: void irq_exit(void){ ...... sub_preempt_count(IRQ_EXIT_OFFSET); if (!in_interrupt() && local_softirq_pending()) invoke_softirq(); ......} 如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq: asmlinkage void __do_softirq(void){ ...... pending = local_softirq_pending(); __local_bh_disable((unsigned long)__builtin_return_address(0), SOFTIRQ_OFFSET);restart: /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0); local_irq_enable(); h = softirq_vec; do { if (pending & 1) { ...... trace_softirq_entry(vec_nr); h->action(h); trace_softirq_exit(vec_nr); ...... } h++; pending >>= 1; } while (pending); local_irq_disable(); pending = local_softirq_pending(); if (pending && --max_restart) goto restart; if (pending) wakeup_softirqd(); lockdep_softirq_exit(); __local_bh_enable(SOFTIRQ_OFFSET);} 首先取出pending的状态; 禁止软中断,主要是为了防止和软中断守护进程发生竞争; 清除所有的软中断待决标志; 打开本地cpu中断; 循环执行待决软中断的回调函数; 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出: 没有新的软中断等待执行; 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次; 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断; 推出前恢复软中断; 3.2 在ksoftirqd进程中执行 从前面几节的讨论我们可以看出,软中断也可能由ksoftirqd守护进程执行,这要发生在以下两种情况下: 在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况虽然极少发生,但毕竟有可能; 内核的其它代码主动调用raise_softirq,而这时正好不是在中断上下文中,守护进程将被唤醒; 守护进程最终也会调用__do_softirq执行软中断的回调,具体的代码位于run_ksoftirqd函数中,内核会关闭抢占的情况下执行__do_softirq,具体的过程这里不做讨论。 4. tasklet 因为内核已经定义好了10种软中断类型,并且不建议我们自行添加额外的软中断,所以对软中断的实现方式,我们主要是做一个简单的了解,对于驱动程序的开发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处理,这就需要使用tasklet机制。 tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。 4.1 tasklet_struct 在软中断的初始化函数softirq_init的最后,内核注册了TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断: void __init softirq_init(void){ ...... open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action);} 内核用一个tasklet_struct来表示一个tasklet,它的定义如下: struct tasklet_struct{ struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data;}; next用于把同一个cpu的tasklet链接成一个链表,state用于表示该tasklet的当前状态,目前只是用了最低的两个bit,分别用于表示已经准备被调度执行和已经在另一个cpu上执行: enum{ TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */ TASKLET_STATE_RUN /* Tasklet is running (SMP only) */}; 原子变量count用于tasklet对tasklet_disable和tasklet_enable的计数,count为0时表示允许tasklet执行,否则不允许执行,每次tasklet_disable时,该值加1,tasklet_enable时该值减1。func是tasklet被执行时的回调函数指针,data则用作回调函数func的参数。 4.2 初始化一个tasklet 有两种办法初始化一个tasklet,第一种是静态初始化,使用以下两个宏,这两个宏定义一个tasklet_struct结构,并用相应的参数对结构中的字段进行初始化: DECLARE_TASKLET(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于0。 DECLARE_TASKLET_DISABLED(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于1。 第二个是动态初始化方法:先定义一个tasklet_struct,然后用tasklet_init函数进行初始化,该方法默认tasklet处于enable状态: struct tasklet_struct tasklet_xxx;......tasklet_init(&tasklet_xxx, func, data); 4.3 tasklet的使用方法 使能和禁止tasklet,使用以下函数: tasklet_disable() 通过给count字段加1来禁止一个tasklet,如果tasklet正在运行中,则等待运行完毕才返回(通过TASKLET_STATE_RUN标志)。 tasklet_disable_nosync() tasklet_disable的异步版本,它不会等待tasklet运行完毕。 tasklet_enable() 使能tasklet,只是简单地给count字段减1。 调度tasklet的执行,使用以下函数: tasklet_schedule(struct tasklet_struct *t) 如果TASKLET_STATE_SCHED标志为0,则置位TASKLET_STATE_SCHED,然后把tasklet挂到该cpu等待执行的tasklet链表上,接着发出TASKLET_SOFTIRQ软件中断请求。 tasklet_hi_schedule(struct tasklet_struct *t) 效果同上,区别是它发出的是HI_SOFTIRQ软件中断请求。 销毁tasklet,使用以下函数: tasklet_kill(struct tasklet_struct *t) 如果tasklet处于TASKLET_STATE_SCHED状态,或者tasklet正在执行,则会等待tasklet执行完毕,然后清除TASKLET_STATE_SCHED状态。 4.4 tasklet的内部执行机制 内核为每个cpu用定义了一个tasklet_head结构,用于管理每个cpu上的tasklet的调度和执行: struct tasklet_head{ struct tasklet_struct *head; struct tasklet_struct **tail;}; static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec); 上面说过,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的,两个软中断只是有优先级的差别,所以我们只讨论TASKLET_SOFTIRQ的实现,TASKLET_SOFTIRQ的中断回调函数是tasklet_action,我们看看它的代码: static void tasklet_action(struct softirq_action *a){ struct tasklet_struct *list; local_irq_disable(); list = __this_cpu_read(tasklet_vec.head); __this_cpu_write(tasklet_vec.head, NULL); __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head); local_irq_enable(); while (list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); __raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_enable(); }} 解析如下: 关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。 遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止: 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。 分析到这了我有个疑问,看了上面的代码,如果一个tasklet被tasklet_schedule后,在没有被执行前被tasklet_disable了,岂不是会无穷无尽地引发TASKLET_SOFTIRQ软中断? 通过以上的分析,我们需要注意的是,tasklet有以下几个特征: 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行; 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次; 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次; 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行; 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护;
PLC与变频器两者是一种包含与被包含的关系,PLC与变频器都可以完成一些特定的指令,用来控制电机马达,PLC是一种程序输入执行硬件,变频器则是其中之一。 但是PLC的涵盖范围又比变频器大,还可以用来控制更多的东西,应用领域更广,性能更强大,当然PLC的控制精度也更大。变频器无法进行编程,改变电源的频率、电压等参数,它的输出频率可以设为固定值,也可以由PLC动态控制。 PLC是可以编程序的,用来控制电气元件或完成功能、通信等任务。 PLC与变频器之间通信需要遵循通用的串行接口协议(USS),按照串行总线的主从通信原理来确定访问的方法。总线上可以连接一个主站和最多31个从站,主站根据通信报文中的地址字符来选择要传输数据的从站,在主站没有要求它进行通信时,从站本身不能首先发送数据,各个从站之间也不能直接进行信息的传输。 PLC基本结构图 PLC可编程控制器的存储器可以分为系统程序存储器、用户程序存储器及工作数据存储器等三种。 1、系统程序存储器 系统程序存储器用来存放由可编程控制器生产厂家编写的系统程序,并固化在ROM内,用户不能直接更改。系统程序质量的好坏,很大程度上决定了PLC的性能。 其内容主要包括三部分:第一部分为系统管理程序,它主要控制可编程控制器的运行,使整个可编程控制器按部就班地工作,第二部分为用户指令解释程序,通过用户指令解释程序,将可编程控制器的编程语言变为机器语言指令,再由CPU执行这些指令;第三部分为标准程序模块与系统调用程序。 2、用户程序存储器 根据控制要求而编制的应用程序称为用户程序。用户程序存储器用来存放用户针对具体控制任务,用规定的可编程控制器编程语言编写的各种用户程序。 目前较先进的可编程控制器采用可随时读写的快闪存储器作为用户程序存储器,快闪存储器不需后备电池,断电时数据也不会丢失。 3、工作数据存储器 工作数据存储器用来存储工作数据,即用户程序中使用的ON/OFF状态、数值数据等。在工作数据区中开辟有元件映像寄存器和数据表。其中元件映像寄存器用来存储开关量、输出状态以及定时器、计数器、辅助继电器等内部器件的ON/OFF状态。数据表用来存放各种数据,它存储用户程序执行时的某些可变参数值及A/D转换得到的数字量和数字运算的结果等。 变频器基本结构图 变频器是把工频电源(50Hz或60Hz)变换成各种频率的交流电源,以实现电机的变速运行的设备,其中控制电路完成对主电路的控制,整流电路将交流电变换成直流电,直流中间电路对整流电路的输出进行平滑滤波,逆变电路将直流电再逆变成交流电。对于如矢量控制变频器这种需要大量运算的变频器来说,有时还需要一个进行转矩计算的CPU以及一些相应的电路。 PLC与变频器一般有三种连接方法 ①利用PLC的模拟量输出模块控制变频器PLC的模拟量输出模块输出0~5V电压信号或4~20mA电流信号,作为变频器的模拟量输入信号,控制变频器的输出频率。这种控制方式接线简单,但需要选择与变频器输入阻抗匹配的PLC输出模块,且PLC的模拟量输出模块价格较为昂贵,此外还需采取分压措施使变频器适应PLC的电压信号范围,在连接时注意将布线分开,保证主电路一侧的噪声不传至控制电路。 ②利用PLC的开关量输出控制变频器。PLC的开关输出量一般可以与变频器的开关量输入端直接相连。这种控制方式的接线简单,抗干扰能力强。利用PLC的开关量输出可以控制变频器的启动/停止、正/反转、点动、转速和加减时间等,能实现较为复杂的控制要求,但只能有级调速。 使用继电器触点进行连接时,有时存在因接触不良而误操作现象。使用晶体管进行连接时,则需要考虑晶体管自身的电压、电流容量等因素,保证系统的可靠性。另外,在设计变频器的输入信号电路时,还应该注意到输入信号电路连接不当,有时也会造成变频器的误动作。例如,当输入信号电路采用继电器等感性负载,继电器开闭时,产生的浪涌电流带来的噪声有可能引起变频器的误动作,应尽量避免。 ③PLC与RS-485通信接口的连接。所有的标准西门子变频器都有一个RS-485串行接口(有的也提供RS-232接口),采用双线连接,其设计标准适用于工业环境的应用对象。单一的RS-485链路最多可以连接30台变频器,而且根据各变频器的地址或采用广播信息,都可以找到需要通信的变频器。链路中需要有一个主控制器(主站),而各个变频器则是从属的控制对象(从站)。 PLC的变频器控制电机正反转接线图 1.按接线图将线连好后,启动电源,准备设置变频器各参数。 2.按“MODE”键进入参数设置模式,将Pr.79设置为“2”:外部操作模式,启动信号由外部端子(STF、STR)输入,转速调节由外部端子(2、5之间、4、5之间、多端速)输入。 3.连续按“MODE”按钮,退出参数设置模式。 4.按下正转按钮,电动机正转启动运行。 5.按下停止按钮,电动机停止。 6.按下反转按钮,电动机反转启动运行。 7.按下停止按钮,电动机停止。 8. 若在电动正转时按下反转按钮,电动机先停止后反转;反之,若在电动机反转时按下正转按钮,电动机先停止后正转。 PLC与变频器的接线图 PLC和变频器通讯方式 1、PLC的开关量信号控制变频器 PLC(MR型或MT型)的输出点、COM点直接与变频器的STF(正转启动)、RH(高速)、RM(中速)、RL(低速)、输入端SG等端口分别相连。PLC可以通过程序控制变频器的启动、停止、复位;也可以控制变频器高速、中速、低速端子的不同组合实现多段速度运行。但是,因为它是采用开关量来实施控制的,其调速曲线不是一条连续平滑的曲线,也无法实现精细的速度调节。 2、PLC的模拟量信号控制变频器 硬件:FX1N型、FX2N型PLC主机,配置1路简易型的FX1N-1DA-BD扩展模拟量输出板;或模拟量输入输出混合模块FX0N-3A;或两路输出的FX2N-2DA;或四路输出的FX2N-4DA模块等。优点:PLC程序编制简单方便,调速曲线平滑连续、工作稳定。 缺点:在大规模生产线中,控制电缆较长,尤其是DA模块采用电压信号输出时,线路有较大的电压降,影响了系统的稳定性和可靠性。 3、 PLC采用RS-485通讯方法控制变频器 这是使用得最为普遍的一种方法,PLC采用RS串行通讯指令编程。优点:硬件简单、造价最低,可控制32台变频器。缺点:编程工作量较大。 4、 PLC采用RS-485的Modbus-RTU通讯方法控制变频器 三菱新型F700系列变频器使用RS-485端子利用Modbus-RTU协议与PLC进行通讯。优点:Modbus通讯方式的PLC编程比RS-485无协议方式要简单便捷。缺点:PLC编程工作量仍然较大。 5、 PLC采用现场总线方式控制变频器 三菱变频器可内置各种类型的通讯选件,如用于CC-Link现场总线的FR-A5NC选件;用于Profibus DP现场总线的FR-A5AP(A)选件;用于DeviceNet现场总线的FR-A5ND选件等等。三菱FX系列PLC有对应的通讯接口模块与之对接。 优点:速度快、距离远、效率高、工作稳定、编程简单、可连接变频器数量多。缺点:造价较高。 6、采用扩展存储器 优点:造价低廉、易学易用、性能可靠 缺点:只能用于不多于8台变频器的系统。 PLC和变频器通讯接线图 三菱PLC控制台达变频器案例分析 在不外接控制器(如PLC)的情况下,直接操作变频器有三种方式: ①操作面板上的按键; ②操作接线端子连接的部件(如按钮和电位器); ③复合操作(如操作面板设置频率,操作接线端子连接的按钮进行启/停控制)。为了操作方便和充分利用变频器,也可以采用PLC来控制变频器。 PLC控制变频器有三种基本方式: ①以开关量方式控制; ②以模拟量方式控制; ③以RS485通信方式控制。 PLC以开关量方式控制变频器的硬件连接 变频器有很多开关量端子,如正转、反转和多档转速控制端子等,不使用PLC时,只要给这些端子接上开关就能对变频器进行正转、反转和多档转速控制。当使用PLC控制变频器时,若PLC是以开关量方式对变频进行控制,需要将PLC的开关量输出端子与变频器的开关量输入端子连接起来,为了检测变频器某些状态,同时可以将变频器的开关量输出端子与PLC的开关量输入端子连接起来。 PLC以开关量方式控制变频器的硬件连接如下图所示。当PLC内部程序运行使Y001端子内部硬触点闭合时,相当于变频器的STF端子外部开关闭合,STF端子输入为ON,变频器启动电动机正转,调节10、2、5端子所接电位器可以改变端子2的输入电压,从而改变变频器输出电源的频率,进而改变电动机的转速。如果变频器内部出现异常时,A、C端子之间的内部触点闭合,相当于PLC的X001端子外部开关闭合,X001端子输入为ON。 PLC以模拟量方式控制变频器的硬件连接 变频器有一些电压和电流模拟量输入端子,改变这些端子的电压或电流输入值可以改变电动机的转速,如果将这些端子与PLC的模拟量输出端子连接,就可以利用PLC控制变频器来调节电动机的转速。模拟量是一种连续变化的量,利用模拟量控制功能可以使电动机的转速连续变化(无级变速)。 PLC以模拟量方式控制变频器的硬件连接如下图所示,由于三菱FX2N-32MR型PLC无模拟量输出功能,需要给它连接模拟量输出模块(如FX2N-4DA),再将模拟量输出模块的输出端子与变频器的模拟量输入端子连接。当变频器的STF端子外部开关闭合时,该端子输入为ON,变频器启动电动机正转,PLC内部程序运行时产生的数字量数据通过连接电缆送到模拟量输出模块(DA模块),由其转换成0~5V或0~10V范围内的电压(模拟量)送到变频器2、5端子,控制变频器输出电源的频率,进而控制电动机的转速,如果DA模块输出到变频器2、5端子的电压发生变化,变频器输出电源频率也会变化,电动机转速就会变化。 PLC在以模拟量方式控制变频器的模拟量输入端子时,也可同时用开关量方式控制变频器的开关量输入端子。 PLC以RS485通信方式控制变频器的硬件连接 PLC以开关量方式控制变频器时,需要占用较多的输出端子去连接变频器相应功能的输入端子,才能对变频器进行正转、反转和停止等控制;PLC以模拟量方式控制变频器时,需要使用DA模块才能对变频器进行频率调速控制。如果PLC以RS485通信方式控制变频器,只需一根RS485通信电缆(内含5根芯线),直接将各种控制和调频命令送给变频器,变频器根据PLC通过RS485通信电缆送来的指令就能执行相应的功能控制。 RS485通信是目前工业控制广泛采用的一种通信方式,具有较强的抗干扰能力,其通信距离可达几十米至上千米。采用RS485通信不但可以将两台设备连接起来进行通信,还可以将多台设备(最多可并联32台设备)连接起来构成分布式系统,进行相互通信。 1.变频器的RS485通信口 三菱FR500系列变频器有一个用于连接操作面板的PU口,该接口可用作RS485通信口,在使用RS485方式与其他设备通信时,需要将操作面板插头(RJ45插头)从PU口拔出,再将RS485通信电缆的一端插入PU口,通信电缆另一端连接PLC或其他设备。三菱FR500系列变频器PU口外形及各引脚功能说明如下图所示。 三菱FR500系列变频器只有一个RS485通信口(PU口),面板操作和RS485通信不能同时进行,而三菱FR700系列变频器除了有一个PU接口外,还单独配备了一个RS485通信口(接线排),专用于进行RS485通信。三菱FR700系列变频器RS485通信口外形及各功能说明如下图所示,通信口的每个功能端子都有2个,一个接上一台RS485通信设备,另一个端子接下一台RS485通信设备,若无下一台设备,应将终端电阻开关拨至“100Ω”侧。 2.PLC的RS485通信口 三菱FX PLC一般不带RS485通信口,如果要与变频器进行RS485通信,须给PLC安装FX2N-485BD通信板。485BD通信板的外形和端子如下图(a)所示,通信板的安装方法如下图(b)所示。 (a)外形 (b)安装方法 3.变频器与PLC的RS485通信连接 (1)单台变频器与PLC的RS485通信连接 单台变频器与PLC的RS485通信连接如下图所示,两者在连接时,一台设备的发送端子(+\-)应分别与另一台设备的接收端子(+\-)连接,接收端子(+\-)应分别与另一台设备的发送端子(+\-)连接。 (2)多台变频器与PLC的RS485通信连接 多台变频器与PLC的RS485通信连接如下图所示,它可以实现一台PLC控制多台变频器的运行。 PLC控制变频器驱动电动机正反转的电路、程序及参数设置 1.PLC与变频器的硬件连接线路图 PLC以开关量方式控制变频器驱动电动机正反转的线路图如下图所示。 2.变频器的参数设置 在使用PLC控制变频器时,需要对变频器进行有关参数设置,具体见下表。 3.编写PLC控制程序 变频器有关参数设置好后,还要用编程软件编写相应的PLC控制程序并下载给PLC。PLC控制变频器驱动电动机正反转的PLC程序如下图所示。 PLC控制变频器驱动电动机多档转速运行的电路、程序及参数设置 变频器可以连续调速,也可以分档调速,FR-500系列变频器有RH(高速)、RM(中速)和RL(低速)三个控制端子,通过这三个端子的组合输入,可以实现7档转速控制。如果将PLC的输出端子与变频器这些端子连接,就可以用PLC控制变频器来驱动电动机多档转速运行。 1.PLC与变频器的硬件连接线路图 PLC以开关量方式控制变频器驱动电动机多档转速运行的线路图如下图所示。 2.编写PLC控制程序 PLC以开关量方式控制变频器驱动电动机多档转速运行的PLC程序如下图。
移动机器人是一种能够感知外部环境,在有障碍物的环境中能够实现动态决策与规划,从而完成避障等多种功能的综合系统。机器人系统通常分为机构本体和控制系统两部分,控制系统的作用是根据用户的指令对机构本体进行...
在I2C、SPI等总线的设计中,由于平台SOC总线接口数量的限制,一组总线接口通常会连接多个外设器件。 但有些时候,由于某些场景下,总线上挂载的器件会处于不同的状态,此时可能会因为某些器件内部架构问题,导致总线异常。 以之前碰到的一个问题为例,同一组I2C总线接口上挂载SmartPA、背光IC、显示驱动IC三颗器件。 由于早期BSP bringup的时候,分开对音频和显示模块进行调试,此时两个模块均能正常打通,功能正常。 但是硬件整合模块测试的时候发现,系统无法正常开机,log上报一直卡在Audio初始化。只要拔掉显示屏,Audio就能初始化成功,系统就能正常跑下去。 通过测量I2C信号波形发现,此时总线被一直被拉低到0.3V。因为Audio的初始化是在显示屏之前的,且显示屏的VDDIO电源是独立的,因此猜测是显示屏内部与上拉电阻一起产生了回路,导致总线被拉死。 最终和供应商确认到,显示驱动IC的所有IO接口都留有单向TVS管,如下图所示。 当VDDIO还未上电时,红色箭头的回路得以产生,最终总线被TVS的正向导通电压所钳位。 初步解决对策,就是先对显示模块做初始化,即让VDDIO先上电,此时总线恢复正常。但由于存在灭屏播放音乐的场景,所以Audio和显示模块的I2C必须加以隔离。 隔离的方案一种是选择集成的level-shift,这种方案设计简单、布局面积小,只需要注意VCCA和VCCB的电压关系就行。(截图未展示上拉电阻) 如果成本是关键,布局面积乐观的话,可以采用分立器件去搭建,具体分析步骤这里不再赘述,需要注意的点是肖特基二极管的选择。由于MOS管内部的体二极管正向导通压降通常较大,因此需要外部并联一颗导通压降更低的二极管,保证I2C总线上的低电平尽可能逼近0V。 以上是对于总线并联设计过程中碰到的一点小问题,希望对大家后续的设计和调试起到一点帮助。