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之间如果有资源冲突,应该要用自旋锁进行同步保护;
今天就来聊一聊常用常见的通讯方式I2C,网上对于I2C的讲解也非常的多,在此呢核桃也不想再重复一些概念性的知识点了,着重对通讯的过程做一个小的总结。 看下图1所示: 图1 图1是完整的写与读操作过程,看不懂图1是什么意思也没关系,我们接下来拆开了讲。 先看一下SDA和SCL如何表示逻辑状态的,如下: SCL在高电平期间,SDA也为高电平时,表示逻辑1。 SCL在高电平期间,SDA为低电平时,表示逻辑0。 SCL在高电平期间,SDA出现下降沿时,表示起始信号。 SCL在高电平期间,SDA出现上升沿时,表示停止信号。 SCL和SDA同时为高电平时,表示总线处于空闲状态。 要想看明白I2C的波形,上面的这些波形图表示的状态得先记住。 我们再看回图2 图2 假如要写入地址1010000,波形是怎么样的? 注意:要进行写操作的话,读/写位必须是0才能有效,要读数据的话需要置1(也就是0表示写,1表示读) 波形如下: 图3 可以看的出来图2和图3的写操作就对应上了。 那是不是这样就通讯成功了呢? 当然不是,主机发地址过去之后,还需要丛机应答,也就是应答位必须为0,如果为1就表示没有收到或者读取完成了。 接着再把8位寄存器地址传过去,同样也需要丛机应答 丛机应答之后,最后再需要写入对应寄存器的数据传过去,然后再收到丛机应答,即可收到停止位了。 以上就是写操作的整个完整的过程,其实理解起来也相对简单,那读操作也是类似,主要和写操作的区别在于: (1)需要核实两次的地址 (2)读/写位必须置1
一、什么是串口通讯? 串行通讯是指仅用一根接收线和一根发送线就能将数据以位进行传输的一种通讯方式。尽管串行通讯的比按字节传输的并行通信慢,但是串口可以在仅仅使用两根线的情况下就能实现数据的传输。 典型的串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,所以端口能够在一根线上发送数据同时在另一根线上接收数据。串口通信最重要的参数是波特率、数据位、停止位和奇偶的校验。对于两个需要进行串口通信的端口,这些参数必须匹配,这也是能够实现串口通讯的前提。 图1:串行通讯示数据传输意图 二、串口通讯的通讯协议? 最初数据是模拟信号输出简单过程量,后来仪表接口出现了RS232接口,这种接口可以实现点对点的通信方式,但这种方式不能实现联网功能,这就促生了RS485。 我们知道串口通信的数据传输都是0和1,在单总线、I2C、UART中都是通过一根线的高低电平来判断逻辑1或者逻辑0,但这种信号线的GND再与其他设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且抗干扰性能也比较弱。所以差分通信、支持多机通信、抗干扰强的RS485就被广泛的使用了。 RS485通信最大特点就是传输速度可以达到10Mb/s以上,传输距离可以达到3000米左右。大家需要注意的是虽然485最大速度和最大传输距离都很大,但是传输的速度是会随距离的增加而变慢的,所以两者是不可以兼得的。 三、串口通讯的物理层 串口通讯的物理层有很多标准,例如上面提到的,我们主要讲解RS-232标准,RS-232标准主要规定了信号的用途、通讯接口以及信号的电平标准。 在上面的通讯方式中,两个通讯设备的"DB9接口"之间通过串口信号线建立起连接,串口信号线中使用"RS-232标准"传输数据信号。由于RS-232电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个"电平转换芯片"转换成控制器能识别的"TTL校准"的电平信号,才能实现通讯。 下图为DB9标准串口通讯接口: DB9引脚说明: 上表中的是计算机端的DB9公头标准接法,由于两个通讯设备之间的收发信号(RXD与TXD)应交叉相连,所以调制调解器端的DB9母头的收发信号接法一般与公头的相反,两个设备之间连接时,只要使用"直通型"的串口线连接起来即可。 串口线中的RTS、CTS、DSR、DTR及DCD信号,使用逻辑 1表示信号有效,逻辑0表示信号无效。例如,当计算机端控制DTR信号线表示为逻辑1时,它是为了告知远端的调制调解器,本机已准备好接收数据,0则表示还没准备就绪。 四、波特率 波特率是指数据信号对载波的调制速率,它用单位时间内载波调制状态改变的次数来表示; 比如波特率为9600bps;代表的就是每秒中传输9600bit,也就是相当于每一秒中划分成了9600等份。 因此,那么每1bit的时间就是1/9600秒=104.1666...us。约0.1ms。既然是9600等份,即每1bit紧接着下一个比特,不存在额外的间隔。两台设备要想实现串口通讯,这收发端设置的波特率必须相同,否则是没办法实现通讯的。 收发波特率一致可以实现通讯: 收发波特率不一致,导致RX端不能正常接收: 五、串口通讯的数据结构 起始位: 起始位必须是持续一个比特时间的逻辑0电平,标志传输一个字符的开始,接收方可用起始位使自己的接收时钟与发送方的数据同步。 数据位: 数据位紧跟在起始位之后,是通信中的真正有效信息。数据位的位数可以由通信双方共同约定。传输数据时先传送字符的低位,后传送字符的高位。 奇偶校验位: 奇偶校验位仅占一位,用于进行奇校验或偶校验,奇偶检验位不是必须有的。如果是奇校验,需要保证传输的数据总共有奇数个逻辑高位;如果是偶校验,需要保证传输的数据总共有偶数个逻辑高位。 停止位: 停止位可以是是1位、1.5位或2位,可以由软件设定。它一定是逻辑1电平,标志着传输一个字符的结束。 空闲位: 空闲位是指从一个字符的停止位结束到下一个字符的起始位开始,表示线路处于空闲状态,必须由高电平来填充。 六、单双工通讯 单工: 数据传输只支持数据在一个方向上传输; 半双工: 允许数据在两个方向上传输,但某一时刻只允许数据在一个方向上传输,实际上是一种切换方向的单工通信,不需要独立的接收端和发送端,两者可合并为一个端口; 全双工: 允许数据同时在两个方向上传输,因此全双工通信是两个单工方式的结合,需要独立的接收端和发送端。 七、STM32中的串口通讯 STM32串口通信接口有两种,分别是:UART(通用异步收发器)、USART(通用同步异步收发器),对于大容量STM32F10x系列芯片,分别由3个USART和两个UART。 TXD:数据发送引脚;RXD:数据输入引脚 对于两芯片的间的连接,两个芯片GND共地,同时TXD和RXD交叉连接,这样两个芯片间可进行TTL电平通信。 但如果对于芯片和PC机相连,除了共地条件外,不能使用如上的直接交叉连接,虽然两者都有TXD和RXD引脚,但通常PC机使用的是RS232接口(9针),通常是TXC和RXD经过电平转换得到,故如果要使芯片与PC机的RS232接口直接通信,需要将芯片的输入输出端口也电平转换为RS232类型,再交叉连接,二者的电平标准不同: 单片机的点评标准(TTL电平):+5V表示1,0V表示0; RS232电平标准:+15/+13V表示0,-15/-13表示1。 因此单片机与PC机进行串口通信应该遵循:在单片机串口与上位机给出的RS232口之间,通过电平转换电路实现TTL电平与RS232电平间的转换. 如果使用USB转串口也可以实现串口通讯,USB转串口电路图如下所示 STM32串口通讯代码 STM32中串口通讯已经给大家建好了相应的库函数,大家在使用和配置串口的时候直接进行调用库函数和配置就行了,请大家参照一下代码: 1、初始化结构体代码 typedef struct { uint32_t USART_BaudRate; // 波特率 uint16_t USART_WordLength; // 字长 uint16_t USART_StopBits; // 停止位 uint16_t USART_Parity; // 校验位 uint16_t USART_Mode; // USART 模式 uint16_t USART_HardwareFlowControl; // 硬件流控制 } USART_InitTypeDef; 2、NVIC配置中断优先级 NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 嵌套向量中断控制器组选择 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 配置USART为中断源 */ NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ; /* 抢断优先级*/ NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; /* 子优先级 */ NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; /* 使能中断 */ NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; /* 初始化配置NVIC */ NVIC_Init(&NVIC_InitStructure); } 3、USART配置函数 void DEBUG_USART_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; /* 第一步:初始化GPIO */ // 打开串口GPIO的时钟 DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE); // 将USART Tx的GPIO配置为推挽复用模式 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure); // 将USART Rx的GPIO配置为浮空输入模式 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure); /* 第二步:配置串口的初始化结构体 */ // 打开串口外设的时钟 DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE); // 配置串口的工作参数 // 配置波特率 USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; // 配置 针数据字长 USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 配置停止位 USART_InitStructure.USART_StopBits = USART_StopBits_1; // 配置校验位 USART_InitStructure.USART_Parity = USART_Parity_No ; // 配置硬件流控制 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 配置工作模式,收发一起 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 完成串口的初始化配置 USART_Init(DEBUG_USARTx, &USART_InitStructure); /*--------------------------------------------------------*/ // 串口中断优先级配置 NVIC_Configuration(); // 使能串口接收中断 USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE); /*--------------------------------------------------------*/ /* 第三步:使能串口 */ // 使能串口 USART_Cmd(DEBUG_USARTx, ENABLE); } 声明: 本号对所有原创、转载文章的陈述与观点均保持中立,推送文章仅供读者学习和交流。文章、图片等版权归原作者享有,如有侵权,联系删除。
在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。 以上是对于总线并联设计过程中碰到的一点小问题,希望对大家后续的设计和调试起到一点帮助。
电子设备之间的通信就像人类之间的交流,双方都需要说相同的语言。在电子产品中,这些语言称为通信协议。 之前有单独地分享了SPI、UART、I2C通信的文章,这篇对它们做一些对比。 串行 VS 并行 电子设备通过发送数据位从而实现相互交谈。位是二进制的,只能是1或0。通过电压的快速变化,位从一个设备传输到另一个设备。在以5V工作的系统中,“0”通过0V的短脉冲进行通信,而“1”通过5V的短脉冲进行通信。 数据位可以通过并行或串行的形式进行传输。 在并行通信中,数据位在导线上同时传输。下图显示了二进制(01000011)中字母“C”的并行传输: 在串行通信中,位通过单根线一一发送。下图显示了二进制(01000011)中字母“C”的串行传输: SPI通信 SPI是一种常见的设备通用通信协议。它有一个独特优势就是可以无中断传输数据,可以连续地发送或接收任意数量的位。而在I2C和UART中,数据以数据包的形式发送,有着限定位数。 在SPI设备中,设备分为主机与从机系统。主机是控制设备(通常是微控制器),而从机(通常是传感器,显示器或存储芯片)从主机那获取指令。 一套SPI通讯共包含四种信号线:MOSI (Master Output/Slave Input) – 信号线,主机输出,从机输入。MISO (Master Input/Slave Output) – 信号线,主机输入,从机输出。SCLK (Clock) – 时钟信号。SS/CS (Slave Select/Chip Select) – 片选信号。 SPI协议特点 实际上,从机的数量受系统负载电容的限制,它会降低主机在电压电平之间准确切换的能力。 工作原理 时钟信号 每个时钟周期传输一位数据,因此数据传输的速度取决于时钟信号的频率。 时钟信号由于是主机配置生成的,因此SPI通信始终由主机启动。 设备共享时钟信号的任何通信协议都称为同步。SPI是一种同步通信协议,还有一些异步通信不使用时钟信号。 例如在UART通信中,双方都设置为预先配置的波特率,该波特率决定了数据传输的速度和时序。 片选信号 主机通过拉低从机的CS/SS来使能通信。 在空闲/非传输状态下,片选线保持高电平。在主机上可以存在多个CS/SS引脚,允许主机与多个不同的从机进行通讯。 如果主机只有一个片选引脚可用,则可以通过以下方式连接这些从器件: MOSI和MISO 主机通过MOSI以串行方式将数据发送给从机,从机也可以通过MISO将数据发送给主机,两者可以同时进行。所以理论上,SPI是一种全双工的通讯协议。 传输步骤 1. 主机输出时钟信号 2. 主机拉低SS / CS引脚,激活从机 3. 主机通过MOSI将数据发送给从机 4. 如果需要响应,则从机通过MISO将数据返回给主机 使用SPI有一些优点和缺点,如果在不同的通信协议之间进行选择,则应根据项目要求进行充分考量。 优劣 优点 SPI通讯无起始位和停止位,因此数据可以连续流传输而不会中断;没有像I2C这样的复杂的从站寻址系统,数据传输速率比I2C更高(几乎快两倍)。独立的MISO和MOSI线路,可以同时发送和接收数据。 缺点 SPI使用四根线(I2C和UART使用两根线),没有信号接收成功的确认(I2C拥有此功能),没有任何形式的错误检查(如UART中的奇偶校验位等)。 UART代表通用异步接收器/发送器也称为串口通讯,它不像SPI和I2C这样的通信协议,而是微控制器中的物理电路或独立的IC。 UART的主要目的是发送和接收串行数据,其最好的优点是它仅使用两条线在设备之间传输数据。UART的原理很容易理解,但是如果您还没有阅读SPI 通讯协议,那可能是一个不错的起点。 UART通信 在UART通信中,两个UART直接相互通信。 发送UART将控制设备(如CPU)的并行数据转换为串行形式,以串行方式将其发送到接收UART。只需要两条线即可在两个UART之间传输数据,数据从发送UART的Tx引脚流到接收UART的Rx引脚: UART属于异步通讯,这意味着没有时钟信号,取而代之的是在数据包中添加开始和停止位。这些位定义了数据包的开始和结束,因此接收UART知道何时读取这些数据。 当接收UART检测到起始位时,它将以特定波特率的频率读取。波特率是数据传输速度的度量,以每秒比特数(bps)表示。两个UART必须以大约相同的波特率工作,发送和接收UART之间的波特率只能相差约10%。 工作原理 发送UART从数据总线获取并行数据后,它会添加一个起始位,一个奇偶校验位和一个停止位来组成数据包并从Tx引脚上逐位串行输出,接收UART在其Rx引脚上逐位读取数据包。 UART数据包含有1个起始位,5至9个数据位(取决于UART),一个可选的奇偶校验位以及1个或2个停止位: 起始位: UART数据传输线通常在不传输数据时保持在高电压电平。开始传输时发送UART在一个时钟周期内将传输线从高电平拉低到低电平,当接收UART检测到高电压到低电压转换时,它开始以波特率的频率读取数据帧中的位。 数据帧: 数据帧内包含正在传输的实际数据。如果使用奇偶校验位,则可以是5位,最多8位。如果不使用奇偶校验位,则数据帧的长度可以为9位。 校验位: 奇偶校验位是接收UART判断传输期间是否有任何数据更改的方式。接收UART读取数据帧后,它将对值为1的位数进行计数,并检查总数是偶数还是奇数,是否与数据相匹配。 停止位: 为了向数据包的结尾发出信号,发送UART将数据传输线从低电压驱动到高电压至少持续两位时间。 传输步骤 发送UART从数据总线并行接收数据: 2.发送UART将起始位,奇偶校验位和停止位添加到数据帧: 3.整个数据包从发送UART串行发送到接收UART。接收UART以预先配置的波特率对数据线进行采样: 4.接收UART丢弃数据帧中的起始位,奇偶校验位和停止位: 5.接收UART将串行数据转换回并行数据,并将其传输到接收端的数据总线: 优劣 没有任何通信协议是完美的,但是UART非常擅长于其工作。以下是一些利弊,可帮助您确定它们是否适合您的项目需求: 优点 仅使用两根电线 无需时钟信号 具有奇偶校验位以允许进行错误检查 只要双方都设置好数据包的结构 有据可查并得到广泛使用的方法 缺点 数据帧的大小最大为9位 不支持多个从属系统或多个主系统 每个UART的波特率必须在彼此的10%之内 I2C通信 I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可传送信息。它结合了 SPI 和 UART 的优点,您可以将多个从机连接到单个主机(如SPI那样),也可以使用多个主机控制一个或多个从机。当您想让多个微控制器将数据记录到单个存储卡或将文本显示到单个LCD时,这将非常有用。 SDA (Serial Data) – 数据线。 SCL (Serial Clock) – 时钟线。 I2C是串行通信协议,因此数据沿着SDA一点一点地传输。与SPI一样,I2C也需要时钟同步信号且时钟始终由主机控制。 工作原理 I2C的数据传输是以多个msg的形式进行,每个msg都包含从机的二进制地址帧,以及一个或多个数据帧,还包括开始条件和停止条件,读/写位和数据帧之间的ACK / NACK位: 启动条件:当SCL是高电平时,SDA从高电平向低电平切换。 停止条件:当SCL是高电平时,SDA由低电平向高电平切换。 地址帧:每个从属设备唯一的7位或10位序列,用于主从设备之间的地址识别。 读/写位:一位,如果主机是向从机发送数据则为低电平,请求数据则为高电平。 ACK/NACK:消息中的每个帧后均带有一个ACK/NACK位。如果成功接收到地址帧或数据帧,接收设备会返回一个ACK位用于表示确认。 寻址 由于I2C没有像SPI那样的片选线,因此它需要使用另一种方式来确认某一个从设备,而这个方式就是 —— 寻址 。 主机将要通信的从机地址发送给每个从机,然后每个从机将其与自己的地址进行比较。如果地址匹配,它将向主机发送一个低电平ACK位。如果不匹配,则不执行任何操作,SDA线保持高电平。 读/写位 地址帧的末尾包含一个读/写位。如果主机要向从机发送数据,则为低电平。如果是主机向从机请求数据,则为高电平。 数据帧 当主机检测到从机的ACK位后,就可以发送第一个数据帧了。数据帧始终为8位,每个数据帧后紧跟一个ACK / NACK位,来验证接收状态。当发送完所有数据帧后,主机可以向从机发送停止条件来终止通信。 传输步骤 1. 在SCL线为高电平时,主机通过将SDA线从高电平切换到低电平来启动总线通信。 2. 主机向总线发送要与之通信的从机的7位或10位地址,以及读/写位: 3. 每个从机将主机发送的地址与其自己的地址进行比较。如果地址匹配,则从机通过将SDA线拉低一位返回一个ACK位。如果主机的地址与从机的地址不匹配,则从机将SDA线拉高。 4. 主机发送或接收数据帧: 5. 传输完每个数据帧后,接收设备将另一个ACK位返回给发送方,以确认已成功接收到该帧: 6. 随后主机将SCL切换为高电平,然后再将SDA切换为高电平,从而向从机发送停止条件。 单个主机VS多个从机 由于I2C使用寻址功能,可以通过一个主机控制多个从机。使用7位地址时,最多可以使用128(27)个唯一地址。使用10位地址并不常见,但可以提供1,024(210)个唯一地址。如果要将多个从机连接到单个主机时,请使用4.7K欧的上拉电阻将它们连接,例如将SDA和SCL线连接到Vcc: 多个主机VS多个从机 I2C支持多个主机同时与多个从机相连,当两个主机试图通过SDA线路同时发送或接收数据时,就会出现问题。因此每个主机都需要在发送消息之前检测SDA线是低电平还是高电平。如果SDA线为低电平,则意味着另一个主机正在控制总线。如果SDA线高,则可以安全地发送数据。如果要将多个主机连接到多个从机,请使用4.7K欧的上拉电阻将SDA和SCL线连接到Vcc: 优劣 与其他协议相比,I2C可能听起来很复杂。以下是一些利弊,可帮助您确定它们是否适合您的项目需求: 优点 仅使用两根电线 支持多个主机和多个从机 每个UART的波特率必须在彼此的10%之内 硬件比UART更简单 众所周知且被广泛使用的协议 缺点 数据传输速率比SPI慢 数据帧的大小限制为8位
显示屏接口一般有I2C、SPI、UART、RGB、LVDS、MIPI、EDP和DP等。下面简要总结一下。 01 中小屏接口I2C、SPI、UART 一般3.5寸以下的小尺寸LCD屏,显示数据量比较少,普遍采用低速串口,如I2C、SPI、UART。 I2C: I2C总线是半双工,两线。 I2C总线的工作速度分为3种: S(标准模式):100Kbps,即 100/8 = 12.5KB/s F(快速模式):400Kbps,即400/8 = 50KB/s HS(高速模式):3.4Mbps,即3.4M/8 = 435KB/s 超高速模式:5Mbit/s,即5M/8 = 525KB/s SPI: SPI总线是全双工,三线或四线制。 SPI没有官方化,速率不统一,根据器件不同传输速率不一,有几M,十几M的,也有几十M的,比I2C速度快。 UART: 无限制,速度取决于波特率,常用9600bps(1.2KB/s)和115200bps(14.4KB/s)。 02 大屏接口RGB、LVDS、MIPI、EDP和DP 高分辨率屏,几乎都是高速串口的接口。主要是LVDS、MIPI-DSI、EDP和DP接口。手机上都是MIPI接口的屏,车载和数码产品上有大量的LVDS接口的屏。 2.1、RGB接口 RGB一般是指RGB色彩模型(RGB color model),是工业界的一种颜色标准。通过对三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色。 a. Parallel RGB 分辨率:1920 * 1080 时钟频率:1920*1080*60*1.2 = 149MHz b. Serial RGB 分辨率:800 * 480 时钟频率:800*3*480*60*1.2 = 83MHz 特点: 1、RGB接口占用的资源较多,所以这个接口的LCD刷新率非常快,软件控制也比较简单; 2、RGB接口的显示数据不需要写入内存进行处理,可以直接写入LCD进行显示,所以响应速度和刷新速度都比MCU接口快很多; 3、缺点是控制需要增加电路,软件初始化需要增加程序,占用资源较多; 4、时钟频率要设置合适。太快,LCD反应不过来,显示不了,太慢也不合适,这个范围可以根据你的刷新率需求和lcd 的规格书(一般会有一个最少响应周期)来确定。 2.2、LVDS接口 LVDS(Low Voltage Differential Signal)即低电压差分信号。1994年由美国国家半导体(NS)公司为克服以TTL电平方式传输宽带高码率数据时功耗大、电磁干扰大等缺点而研制的一种数字视频信号传输方式。它是一种电平标准,广泛应用于液晶屏接口。 其中发送端是一个3.5mA的电流源,产生的3.5mA的电流通过差分线中的一路到接收端。由于接收端对于直流表现为高阻,电流通过接收端的100Ω的匹配电阻产生350mV的电压,同时电流经过差分线的另一路流回发送端。当发送端进行状态变化时,通过改变流经100Ω电阻的电流方向产生有效的'0'和'1' 态。 它是电流驱动的,通过在接收端放置一个负载而得到电压,当电流正向流动,接收端输出为1,反之为0。 特点: 1、LVDS是电流驱动模式 电压摆幅350mV,加载在100Ω电阻上; 2、传输速度快,推荐最大速率为655Mbps ,理论极限速率为1.923Gbps; 3、LVDS不太适合较长距离的信号传送; 4、LVDS接口只用于传输视频数据; 5、LVDS接口主要将RGB TTL非平衡传输信号转换成LVDS平衡传输信号进行传输。 6、LVDS不支持热插拔。 2.3、MIPI接口 MIPI (Mobile Industry Processor Interface) 是2003年由ARM, Nokia, ST ,TI等公司成立的一个联盟,目的是把手机内部的接口如摄像头、显示屏接口、射频/基带接口等标准化,从而减少手机设计的复杂程度和增加设计灵活性。 MIPI联盟下面有不同的WorkGroup,分别定义了一系列的手机内部接口标准,比如 摄像头接口CSI(Camera Serial Interface) 显示接口DSI(Display Serial Interface) 射频接口DigRF 麦克风 /喇叭接口SLIMbus 接口示意图 特点: 1、MIPI不仅能够传输视频数据,还能传输控制指令; 2、MIPI DSI接口是按照特定的握手顺序和指令规则传输屏幕控制所需的视频数据和控制数据; 3、MIPI接口的模组,相较于并口具有速度快,传输数据量大,功耗低,抗干扰好。并且专门为移动设备进行的优化,因而更适合手机和智能平板的连接。 2.4、EDP接口 全称为Embedded DisplayPort,用于笔记本、平板电脑的一种数字接口。是视讯电子标准协会(VESA)针对行动装置应用。 特点: 1、eDP协议是针对DP应用在嵌入式方向架构和协议的拓展,所以eDP协议完全兼容DP协议; 2、eDP接口属内部接口,可以用做芯片与芯片之间的传输,也可用显示屏与驱动板之间的传输; 3、由于该类接口能够实现多数据高速同时传输,且电磁干扰小,目前正在逐渐取代传统的低电压差动讯号(LVDS)接口。 2.5、DP接口 DisplayPort(简称DP)是第一个依赖数据包化数据传输技术的显示通信端口。是一个由PC及芯片制造商联盟开发,视频电子标准协会标准化的数字式视频接口标准。主要用于视频源与显示器等设备的连接,它既可以用于内部显示连接,也可以用于外部的显示连接。 速度: DP1.0:2006年5月发布。带宽10.8Gbps。DP1.0的最大传输速度是8.64Gbit/s,长度是2米。已经废弃。 DP1.2:2009年12月22日发布。它最大的改变是传输速度增加两倍到21.6Gbit/s,支持4K(4096X2160)60Hz,因此支持更高的分辨率、帧速率及色深。 DP1.3:2014年9月15日发布。带宽速度最高32.4Gbps(HBR3),编码后有效带宽为25.92Gbps,可支持4K(3840X2160)120hz、5K(5120X2880)60hz、8K(7680X4320)30hz。 DP1.4:2016年2月份最终版的DP1.4通信端口规范,声道也提升到32声道1536KHz采样率,将为笔记本电脑、智能手机及AIO一体机带来8K级别(7680x4320)的60Hz输出,4K的话则可以上到120Hz。 推荐游戏玩家用DP,因为DP传输机制可以只传输画面变化部分,理论DP延时更低,带宽更大。 特点: 1、DP将在传输视频信号的同时加入对高清音频信号传输的支持,同时支持更高的分辨率和刷新率。 2、DP接口属外部接口,只能用显示器与驱动板之间的传输,不可用做芯片与芯片之间的传输。 3、DP接口常见设备:智能电视,笔记本电脑,独立显卡,显示器。 4、AMD多屏拼接技术必须要DisplayPort接口。 5、带宽更高,成本更低。DP接口可以很轻松的支持2560×1600这样的超高分辨率的显示。 文章来源公众号:硬件笔记本