中断和时钟机制是Linux驱动中重要的两项技术。使用这些技术,可以帮助驱动程序更高效地完成任务。在写设备驱动程序的过程中,为了使系统知道硬件在做什么,必须使用中断。如果没有中断,设备几乎什么都不能做。本章将详细讲解中断与时钟机制。
本节将对中断相关概念进行简要的分析,并对中断进行分类。根据不同的中断类型,写中断驱动程序的方法也不一样。下面将主要介绍中断的基本概念和常见分类。
中断
是计算机中一个十分重要的概念。如果没有中断,那么设备和程序就无法高效利用计算机的CPU资源。
1.什么是中断
这里以著名的数学家华罗庚老师的一篇科学小品文《统筹方法》来做一个比喻——泡壶茶。
当时的情况是:开水没有;水壶要洗,茶壶茶杯要洗;火生了;茶叶也有了。怎么办?最节约时间的方法是洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
在没有中断的情况下,计算机只能处理一个线性的过程,其要么只烧水,要么只洗茶壶,或者烧完水后再来处理洗茶壶这个事件,这显然是非常浪费时间的。不使用中断方式和使用中断方式泡茶喝水的过程如下图。
由于使用中断机制更为高效,所以计算机中引进了中断机制。在烧水的过程中处理洗茶壶、洗茶杯、拿茶叶,这些短时间的事情,其好处就是能使洗茶壶这个事件尽快得到执行,从而最快地完成泡茶喝这个任务。对应地,在计算机执行程序的过程中,由于出现某个特殊情况(或称为"事件"),使得暂时中止正在运行的程序,而转去执行这一特殊事件的处理,处理完毕之后再回到原来程序继续向下执行,这个过程就是中断。
2.中断在Linux中的实现
中断在Linux中仅仅是通过信号来实现的。当硬件需要通知处理器一个事件时,就可以发送一个信号给处理器。例如,当用户按下手机键盘的应答键时,就会向手机处理器发送一个信号。处理器接收到这个信号后,就会调用喇叭和话筒驱动程序,使用户可以进行通话。
通常情况下,一个驱动程序只要申请中断,并添加中断处理函数就可以了。中断的到达和中断处理函数的调用,都是由内核框架完成的。这样就减少了程序员的很多负担,程序员只要保证申请了正确的中断号及编写了正确的中断处理函数就可以了。
说明
:大多数手机使用的都是ARM处理器。对于驱动刚刚入门的读者,不知道应该选择什么处理器来学习。目前最为流行的处理器之一是ARM处理器。其广泛的应用于数字音频播放器、数字机顶盒、游戏机、数码相机和打印机等设备中。
在Linux操作系统中,中断的分类是非常复杂的。根据不同的角度,可以将中断分为不同的类型。各种类型之间的关系并非相互独立,往往是相互交叉的。从宏观上可以分为两类,分别是硬中断和软中断
。
1.硬中断
硬中断就是由系统硬件产生的中断。系统硬件通常引起外部事件。外部事件具有随机性和突发性,因此硬中断也具有随机性和突发性。例如当用户使用手机时,正常情况下处于待机状态,待机状态下CPU处理时钟和电源管理方面的问题。当手机的GSM模块接收到来电请求时,会通过连接到CPU的中断线向CPU发送一个硬件中断请求。CPU接收到该中断后,会立刻处理预先定义好的中断处理程序。该中断处理程序会调用铃声驱动程序或者电机驱动程序,使手机响起铃声或震动,等待用户接听电话。
硬件中断具有随机性和突发性的原因是手机根本无法预见电话什么时候到来。另外硬中断是可以屏蔽的。目前许多手机具有飞行模式,在飞机上可以自动屏蔽来电。
2.软中断
软中断是执行中断指令时产生的。软中断不用外设施加中断请求信号,因此中断的发生不是随机的而是由程序安排好的。在汇编程序设计中经常会使用软中断指令,比如int n
,n必须是中断向量。
处理器接收软中断有两个来源,一是处理器执行到错误的指令代码,如除零错误;二是由软件产生中断,如进程的调度就是使用的软中断方式。
从中断产生的位置,可以将中断分为外部中断和内部中断。
1.外部中断
外部中断一般是指由计算机外设发出的中断请求,键盘中断、打印机中断、定时器中断等。外部中断是可以通过编程方式给予屏蔽。
2.内部中断
内部中断是指因硬件出错(如突然掉电、奇偶校验等)或运算出错(除数为零,运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。通常情况下,大多数内部中断都由Linux内核进行处理,所以驱动程序员往往不需要关系这些问题。
从指令执行的角度,中断又可分为同步中断和异步中断
1.同步中断
同步中断是指令执行的过程中由CPU控制的,CPU在执行完一条指令后才发出中断。也就说,在指令执行过程中,即使有中断的到来,只要指令还没执行完,CPU就不会去执行该中断。同步中断一般是因为程序错误所引起的,例如内存管理中的缺页中断,被0除出错等。当CPU决定处理同步中断时,会调用异常处理函数,使系统从错误的状态恢复过来。当错误不可恢复时,就会出现死机和蓝屏等现象。Windows系统以前的版本经常出现蓝屏现象,就是因为无法从异常恢复的原因。
2.异步中断
异步中断是由硬件设备随机产生的,产生中断时并不考虑与处理器的时钟同步问题,该类型的中断是可以随时产生的。例如在网卡驱动程序中,当网卡接收到数据包后,会向CPU发送一个异步中断事件,表示数据到来,CPU并不知道何时将接收事件。异步中断的中断处理函数与内核的执行顺序是异步执行的,两者没有必然的联系,也不会相互影响。
以上4节从不同的角度对Linux中的中断进行了分类,但这不是严格的分类。例如,硬中断可以是外部中断也可以是异步中断,同时,软中断可以是内部中断也可以是同步中断,如下图:
中断的实现过程是一个比较复杂的过程。其涉及中断信号线、中断控制器等概念。首先介绍中断信号线的概念。
中断信号线
是对中断输入线和中断输出线的统称。中断输入线是指接收中断信号的引脚。中断输出线是指发送中断信号线的引脚。每一个能够产生中断的外设都有一条或者多条中断输出线(Interrupt ReQuest
,简称IRQ),用来通知处理器产生中断。相应地,处理器也有一组中断输入线,用来接收连接到它的外部设备发出的中断信号。
如下图,外设1、外设2、外设3都通过自己的中断输出线连接到ARM处理器上的不同中断输入线上。每一条IRQ线都是有编号的,一般从0开始编号,编号也可以叫做中断号。在写设备驱动程序的过程中,中断号往往需要驱动开发人员来指定。这时,可以查看硬件开发板的原理图,找到设备与ARM处理器的连接关系,如连接到0号中断线,那么中断号就是0。
中断控制器
位于ARM处理器核心和中断源之间。外部中断源将中断发送到中断控制器。中断控制器根据优先级进行判断,然后通过引脚将中断请求发送给ARM处理器核心。ARM处理器内部中断控制器如下图所示。
当外部设备同时产生中断时,中断优先级产生逻辑会判断哪一个中断将被执行。如上图中的中断屏蔽寄存器,当屏蔽位为1时,表示对应的中断被禁止;当屏蔽位为0时,表示对应的中断可以正常执行。不同的处理器屏蔽位0/1的意义可能有所不同。
I/O
端口A中,该端口直接连接到CPU的数据总线上。这样,CPU可以通过数据总线读出端口A中的中断号。当设备需要中断功能时,应该安装中断。如果驱动程序员没有通过安装中断的方式通知Linux内核需要使用中断,那么内核只会简单的应答并且忽略该中断。
1.申请中断线
申请中断线可以使内核知道外设应该使用哪一个中断号,哪一个中断处理函数。申请中断线在需要与外部设备交互时发生。Linux内核提供了request_irq()
函数申请中断线。在Linux2.6.29中,该函数由
实现。
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long irqflags,
const char *devname,
void *dev_id);
irq
表示要申请的中断号,中断号由开发板的硬件原理图决定。handler
表示要注册的中断处理函数指针。当中断发生时,内核会自动调用该函数来处理中断。irqflags
表示关于中断处理的属性。内核通过这个标志可以决定该中断应该如何处理,在中断上半部和下半部机制中,会详细讲解这部分知识。devname
表示设备名字,该名字会在/proc/interrupts
中显示。interrupts
记录了设备和中断号之间的对应关系。dev_id
这个指针是为共享中断线而设立的。如果不需要共享中断线,那么只要将该指针设为NULL即可。request_irq()
函数成功返回0,错误时返回-EINVAL
或者_NOMEM
。在头文件
中明确地定义了EINVAL和ENOMEM
宏。#define ENOMEM 12 /*Out of memory*/
#define EINVAL 22 /*Invalid argument */
ENOMEM
宏表示内存不足。嵌入式系统由于内存资源有限,经常发生这样的错误。EINVAL
宏表示无效的参数。如果出现这个返回值,那么就应该查看传递request_irq()
的参数是否正确。
说明:
如何知道一个函数的执行过程和返回值,最好的办法是使用前面介绍的Source Insight
工具来查看内核源代码。这样可以帮助读者对内容的实现机制有更深入的理解。
2.释放中断线
当设备不需要中断线时,需要释放中断线。中断信号是非常紧缺的,例如S3C2440处理器有24根外部中断线(EINT
)。可能有读者会疑问,24根外部中断线已经很多了,但其实远远不够的。例如,以不共享中断信号线的方式来设计手机键盘。数字键会占去10条中断线,应答和接听会占去两条中断线,其他功能键又会占去若干条中断线。这个例子中仅仅键盘就占去了十几条中断线,剩下十几条给手机的其他外部设备使用,就是说,中断信号线是远远不够的。
所以Linux内核设计者都建议当中断不再使用时,就应该释放该中断信号线
。但是,从应用的角度来思考,手机的键盘应该是手机开机时都是有效的,键盘设备的使用必须要借助中断线来实现,所以开机时不能释放中断线。关机时一般只有启动按键有效,关机任务不是通过操作系统来完成的,所以关机时,可以释放中断线。中断的有效期应该在手机的整个运行周期中。
释放中断线的实现函数是free_irq()
。
void free_irq(unsigned int irq, void *dev_id);
irq
表示释放申请的中断号。dev_id
这个指针是为共享中断线而设立的。该参数将“共享中断”一节中讲述。需要注意的是,只有中断线被释放了,该中断才能被其他设备使用。掌握了足够多的关于中断的知识后,下面将介绍一个安装驱动程序。该案件驱动程序当按键按下时,打印按键按下的提示信息。
作为一个驱动程序开发人员,要做的第一件事请,就是要读懂电路图。在实际的项目开发中,硬件设计有时非常复杂。这时驱动开发人员应该多和硬件开发人员沟通,掌握足够多的硬件知识,以避免写出错误的驱动程序。
首先应该仔细看懂按键设备的原理图。作为一名驱动开发人员这是最基本的素质。按键设备在实际项目中是一种非常简单的设备,硬件原理图也非常简单。本实例的原理图可以从mini2440
开发板官方网站免费下载(http://www.arm9.net
)。按键原理图如下图。
这里简单介绍一下该电路图的工作原理。K1到K6是6个按键,其一端接地,另一端分别连接到S3C2440处理器的EINT13、EINT14、EINT15、EINT19引脚上。EIN表示外部中断(External Interrupt
)的意思。其中EINT8和EINT19分别接了一个上拉电阻R17和R22。
说明
:上拉电阻就是起上拉作用。上拉就是将一个不确定的值的引脚通过一个电阻连接到高电平上,使该引脚呈现高电平。这个电阻就是上拉电阻,如上图的R17和R22所示。电阻同时起限流作用。下拉电阻同理。芯片的管脚加上拉电阻的作用是提高输出电平,从而提高芯片输入信号的噪声容限增强抗干扰能力。当按键K1、K2断开时EINT8和EINT19都处于高电平状态。当按键K1~K6的按键按下时,对应外部的中断线就接地,处于低电平状态。这是主要读取外部中断线对应的端口寄存器状态,就可以知道是否有按键按下。
从设备的角度来看,设备可以分为有寄存器和无寄存器的设备。按键设备就是一种没有寄存器的设备。按键设备内部没有寄存器并不能代表其没有相应的外部寄存器。为了节约成本,外部寄存器常常被集成到了处理器芯片的内部。这样,处理器可以通过内部寄存器控制外部设备的功能。所以目前的处理器已经不再像以前那样纯粹的处理器了,其更像一台简易的计算机。
与按键K1相关的寄存器是端口G控制寄存器,如下图所示。综合起来看可知,按键K1连接到EINT8引脚,该引脚对应GPG0端口的第0位。
端口是具有有限容量的高速存储部件(也叫寄存器),存储容量一般为8、16和32位。其可以用来存储指令,数据和地址。对硬件设备的操作一般是通过软件方法来读取相应寄存器的状态来实现的。下面介绍与按键设备相关的G端口寄存器,这些内容可以参考三星公司的S3C2440芯片用户手册,也叫datasheet
。
端口G有三个控制器,分别为GPGCON、GPGDAT、和GPGUP。该端口各寄存器的地址,读写要求等如下表所示。
1.GPGCON寄存器
GPGCON是配置寄存器(GPG Configure)。在S3C2440中,大多数引脚都是功能复用的。一个引脚可以配置成输入、输出或者其他功能。这里GPGCON就是用来为下面哪种功能分别是:数据输入、数据输出、中断和保留。GPGCON的每两位可以取值00、01、10、11表示不同的功能。
由上表可以看出,GPGCON总线的地址是0x56000060,其实就是一个4字节的寄存器。
2.GPGDAT寄存器
GPGDAT是数据寄存器。GPGDAT用于记录引脚的状态。寄存器的每一位表示一种状态。当引脚被GPGCON设置为输入时,读取该寄存器可以获得相应位的状态值;当引脚被GPGCON设置为输出时,写此寄存器的相应位可以令此引脚输出高电平或者低电平。当引脚被GPGCO设置为中断时,此引脚会被设置为中断信号源。
3.GPGUP寄存器
GPGUP寄存器是端口上拉寄存器。端口上拉寄存器控制着每一个端口的上拉寄存器的使能或禁止。当对应位为1时,表示相应的引脚没有内部上拉电阻;当为0时,相应的引脚使用上拉电阻。当需要上拉或下拉电阻时,外围电路没有加上拉或下拉电阻,那么就可以使用内部上拉或下拉电阻来代替。如下图所示为上拉电阻和下拉电阻。
一般GPIO引脚挂空时,即没有接芯片时,其电压是不稳定的,而且容易受到噪声信号的影响。如果该引脚接上上拉电阻,那么电平将处于高电平状态;接上下拉电阻,引脚电平被拉低。另外,上拉电阻可以增强I/O端口的驱动能力。由于硬件工程师一般会为电路设计外部上拉或下拉电阻,所以驱动开发人员在编写驱动时,一般禁用内部上拉或下拉电阻。
4.各寄存器的设置
GPGCON、GPGDAT和GPGUP这3个端口寄存器是相互联系的。它们的设置关系如下表。
现在开始对按键设备程序进行分析。按键驱动程序由初始化函数、退出函数和中断处理函数组成。
按键驱动程序初始化函数、退出函数和中断处理函数的关系如下图。
s3c2440_buttons_init()
。在该函数中会进一步调用request_irq()
函数来注册中断。request_irq()
函数会操作内核中的一个中断描述符数组结构irq_desc
。该数组结构比较复杂,主要的功能就是记录中断号对应的中断处理函数。isr_button
。s3c2440_buttons_exit()
。在该函数中,会调用free_irq()
释放设备所使用的中断号。free_irq()
函数也会操作中断描述符数组结构irq_desc
,将该设备所对应的中断处理函数删除。初始化函数s3c2440_buttons_init()
主要负责模块的初始化工作。模块初始化主要包括设置中断触发方式,注册中断号等。该函数的具体代码如下:
static int __init s3c2440_buttons_init(void)
{
int ret; //存储返回值
set_irq_type(K1_IRQ1, IRQ_TYPE_EDGE_FAILLING); //设置按键k为下降沿触发中断
/*注册中断处理函数*/
ret = request_irq(K1_IRQ1, isr_button, SA_INTERRUPT, DEVICE_NAME, NULL);
if(ret)
{
printk("K1_IRQ: could not register interrupt\n");
return ret;
}
printk(DEVICE_NAME"initialized\n");
return 0;
}
接下来逐行分析s3c2440_buttons_init()函数
set_irq_type()
设置中断触发条件。set_irq_type()
函数的原型如下:int set_irq_type(unsigned int irq, unsigned int type);
参数irq
表示中断号,参数type用来定义该中断的触发类型。中断触发类型有低电平触发、高电平触发、下降沿触发、上升沿触发、上升沿和下降沿联合触发。这里定义的中断类型为IRQ_TYPE_EDGE_FALLING
,表示该外部中断为下降沿触发。中断触发类型定义在
中。
#define IRQ_TYPE_NONE 0x00000000 /*未定义中断类型*/
#define IRQ_TYPE_EDGE_RISING 0x00000001 /*上升沿中断类型*/
#define IRQ_TYPE_EDGE_FALLING 0x00000002 /*下降沿中断类型*/
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING )
/*上升沿和下降沿联合触发类型*/
#define IRQ_TYPE_LEVEL_HIGH 0x00000004 /*高电平触发类型*/
#define IRQ_TYPE_LEVEL_LOW 0x00000008 /*低电平触发类型*/
6行,用来为按键K1申请中断。参数K1_IRQ1是要申请的中断号。参数isr_button
是中断回调函数,该回调函数由按键1触发。触发条件被设置为下降沿触发。下降沿触发就是在两个连续的时钟周期内,中断控制器检测到端口的相应引脚,第一个周期为高电平,第二个周期为低电平。如下图所示。
7~11行,当申请中断出错时,打印出错信息和返回。printk()
函数用法与printf()
函数的用法相同,只是前者用于驱动程序中,后者用于用户程序中。
当按键按下时,中断被触发,就会触发中断处理函数。该函数主要的功能是判断按键K1是否被按下。
中断处理函数由isr_button()
函数实现。该函数的参数由系统调用该函数时传递过来。参数irq
表示被触发的中断号。参数dev_id
是为共享中断线而设立的,因为按键驱动不使用共享中断,所以这里传进来的是NULL值。参数regs
是一个寄存器组的结构体指针。寄存器组保存了处理器进入中断代码之前处理器的上下文。这些信息一般只在调试时使用,其他时候很少使用。所以对于一般的驱动程序来说,该参数通常是没有用的。
static irqreturn_t isr_button(int irq, void *dev_id, struct pt_regs *regs)
{
unsigned long GPGDAT;
GPGDAT=(unsigned long)ioremap(0x56000064, 4); /*映射内核地址*/
if(irq == K1_IRQ1)
{
if((*(volatile unsigned long *)GPGDAT) & 1 == 0 ) //是否K1仍然被按下
{
printk("K1 is pressed\n");
}
}
return 0;
}
ioremap
将一个开发板上的物理端口地址转换为内核地址。ioremap
在内核中的实现如下:void *ioremap(unsigned long phys_addr, unsigned long size)
该函数的参数phys_addr
表示要映射的起始的I/O端口地址。参数size
表示要映射的空间大小。从上面GPGDAT寄存器表可知,大小属于32位寄存器。所以,它的参数分别是0x56000064和4字节。
当模块不再使用时,需要退出模块。按键的退出模块由s3c2440_buttons_exit()
函数实现,其主要功能是释放中断线。
static void __exit s3c2440_buttons_exit(void)
{
free_irq(K1_IRQ, NULL); /*释放中断线*/
printk(DEVICE_NAME "exit\n");
}
Linux驱动程序中经常会使用一些时钟机制,主要是用来延时一段时间。在这段时间中硬件设备可以完成相应的工作。本节将对Linux的时钟机制做一个简要的介绍。
Linux内核中一个重要的全局变量是HZ
,这个变量表示与时钟中断相关的一个值。时钟中断是由系统定时硬件以周期性的间隔产生,这个周期性的值由HZ来表示。根据不同的硬件平台,HZ的取值是不一样的。这个值一般被定义为1000,如下代码所示。
#define HZ 1000
这里HZ的意思是每一秒钟中断发生1000次。每当时钟中断发生时,内核内部计数器的值就会加上1。内部计数器由jiffies
变量来表示,当系统初始化时,这个变量被设置为0。每一个时钟到来,这个计数器的值加1,也就说这个变量记录了系统引导以来经历的时间。
比较jiffies
变量的值可以使用下面的几个宏来实现,这几个宏的原型如下:
#define time_after(a,b) \
(typecheck(unsigned long , a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
#define time_after_eq(a,b) \
(typecheck(unsigned long , a) && \
typecheck(unsigned long, b) && \
((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)
第1行的time_after
宏,只是简单比较a和b的大小,如果a>b则返回true。第5行的time_before
宏通过time_after
宏来实现。第6行的time_after_eq
宏用来比较a和b的大小及相等的情况,如果a>=b
则返回true。第10行的time_before_eq
宏通过time_after_eq
宏来实现。
在C语言中,经常使用sleep()
函数将程序延时一段时间,这个函数能够实现毫秒级的延时。在设备驱动程序中,很对对设备的操作也需要延时一段时间,使设备完成某些特定的任务。在Linux内核中,延时技术有很多种,这里也只讲解重要的两种。
1.短时延时
当设备驱动程序需要等待硬件处理的完成时,会主动地延时一段时间。这个时间一般是几十毫秒,甚至更短的时间。例如,驱动程序向设备的某个寄存器写入数据时,由于寄存器的写入数据较慢,所以需要驱动程序等待一定的时间,然后继续执行下面的工作。
Linux内核提供了3个函数来完成纳秒、微秒和毫秒级的延时,这3个函数的原型如下:
static inline void ndelay(unsigned long x)
static inline void udelay(unsigned long usecs)
static inline void msllep(unsigned int msecs)
这些函数的实现与具体的平台有关,有的平台根本不能实现纳秒级的等待。这种情况下,只能根据CPU频率信息计算执行一条代码的时间,然后通过一个忙等待来软件模拟这种软件模拟类似下面的代码:
static inline void ndelay(unsigned long x)
{
... /*由x计算出count的值*/
while(count)
{
count--; /*忙等待*/
}
}
除了使用msleep()
函数实现毫秒级的延时,另外还有一些函数也用来实现毫秒级的延时。这种函数会使等待的进程睡眠而不是忙等待,函数的原型如下:
void msleeep(unsigned int msecs)
unsigned long msleep_interruptible(unsigned int msecs)
static inline void ssleep(unsigned int seconds)
这3个函数不会忙等待,而是将等待的进程放入等待队列中,当延时的时间到达时,唤醒等待队列中的进程。其中msleep()、ssleep()
函数不能被打断,而msleep_interruptible()
函数可以被打断。
2.长时延时
长时延时表示驱动程序要延时一段相对较长的时间。实现这种延时,一般是比较当前jiffies
和目标jiffies
的值。长延时可以使用忙等待来实现,下面的代码给出了驱动程序延时3秒中的案例:
unsigned long timeout = jiffies + 3*Hz;
while(time_before(jiffies, timeout));
time_before
宏简单地比较两个时间的大小,如果参数1的值小于参数2的值,则返回true。
大多数设备以中断方式来驱动代码的执行。例如本章讲解的按键驱动程序,当用户按下键盘时,才会触发先前注册的中断处理程序。这种机制具有很多的优点,可以节约很多CPU时间。除了中断之外,本章还简要介绍了时钟机制,硬件工作的速度一般较慢,在操作硬件的某些寄存器,一般需要内核延时一段时间,在短时间时可以使用忙等待机制,但是对于长时间延时则对号使用等待延时机制。