4.5 中断函数练习
下面我们以P1端口的中断为例,练习一下如何编写一个完整的带有中断的程序。练习中我们要实现的功能是使用I/O口中断来读取按键状态,并根据按键状态来控制LED灯。在这个练习中涉及到的中断相关知识点包括:
下面我们来进入具体的练习。
练习目标:利用中断读取MSP430FR698X LaunchPad上按键S2的状态,并根据该状态控制红色LED灯。每按下S1一次,LED改变一次亮灭状态。
在第二章中,我们曾经介绍过MSP430FR698x LaunchPad的引脚定义。在本练习中,我们需要检测按键S1的状态,S1与MSP430FR698x 的P1.1端口相连;另外我们还要控制绿色的LED1,它与P1.0端口相连。
MSP430FR698x 上的按键S1是低电平有效,也就是说当按键被按下时,P1.1为低电平;而当按键没有被按下时,P1.1为高电平。我们要检测按键按下的动作,实际上就是检测P1.1端口上的电平变化。当P1.1端口上出现一个下降沿(即电平从高变低)时,就可以认为出现了按键被按下的动作。
本练习的程序可以分为3个部分,首先需要对GPIO模块做初始化,以配置相应的输入输出功能,并打开中断使能位;其次在主程序中还是要写一个无限循环,不过由于现在按键的检测是通过中断完成的,因此循环中CPU其实什么事情也不用做;最后还要写一个中断服 务函数,在里面设定中断事件发生后要执行的任务。
1) 初始化
初始化时,我们需要完成下列步骤:
i) 将P1.0设为输出,P1.1设为输入;
ii) 使能P1.1的内部上拉电阻;
iii) 使能P1.1引脚中断;
iv) 将P1.1中断设为下降沿中断;
v) 复位P1.1的中断标志位
其中第ii步是为了实现按键S1的低电平有效,GPIO的初始化和上拉电阻设置在第2章都有介绍过,可以复习第二章的对应部分。
完成GPIO初始化后,请不要忘记使用_BIS_SR(GIE)指令来使能全局中断,这样才能使CPU对中断信号做出反应。
初始化的具体代码如下:
WDTCTL = WDTPW | WDTHOLD; // Stop WDT
// Configure GPIO
P1DIR |= BIT0; // Clear P1.0 output latch for a defined power-on state
P1DIR &= ~BIT1; //配置BIT1为输入
P1OUT|=BIT1; //配置 BIT1为1
/*当GPIO作为输入时,PxREN和PxOUT一起配合设置内部的上/下拉电阻,
*PxREN置1时使能上/下拉电阻,PxOUT置1时上拉,置0时下拉*******/
P1REN |= BIT1; //配置BIT1上拉
P1IE |= BIT1; // P1.1 interrupt enabled
P1IES |= BIT1; // P1.1 Hi-low edge
P1IFG &= ~BIT1; // P1.1 IFG cleared
_BIS_SR(GIE); // Enable global interrupt
PM5CTL0 &= ~LOCKLPM5; // Disable the GPIO power-on default high-impedance mode
2) while循环
初始化完成后,需要写一个while循环,由于现在按键检测是通过中断功能自动完成的,因此在这个循环中CPU其实没有做任何事情,只是简单地等待中断事件的发生。当按键按下的一刹那(即P1.3的下降沿),CPU将检测到中断事件,并自动进入对应的中断服 务函数。
复制代码
3) 中断服 务函数
下面我们要编写P1.1对应的中断服 务函数,来告诉CPU中断事件(即按键被按下)发生后需要执行的任务。
编写中断服 务函数时,首先要找到对应的中断向量。本练习中我们使用的是P1端口的中断,其中断向量名为PORT1_VECTOR。接下来按照4.4节中的说明编写中断服 务函数。
中断服 务函数里具体要做的事情包括:
i) 翻转P1.0的电平;
ii) 清除P1.1中断标志位。
这样,当每一次按下S1按键时,LaunchPad上的红色色LED灯就会改变状态。
复制代码
完整代码
把上述步骤连起来,就完成了一个完整的GPIO中断程序。完整代码如下:
int main(void)
{
WDTCTL = WDTPW | WDTHOLD; // Stop WDT
// Configure GPIO
P1DIR |= BIT0; // Clear P1.0 output latch for a defined power-on state
P1DIR &= ~BIT1; //配置BIT1为输入
P1OUT|=BIT1; //配置 BIT1为1
/*当GPIO作为输入时,PxREN和PxOUT一起配合设置内部的上/下拉电阻,
*PxREN置1时使能上/下拉电阻,PxOUT置1时上拉,置0时下拉*******/
P1REN |= BIT1; //配置BIT1上拉
P1IE |= BIT1; // P1.1 interrupt enabled
P1IES |= BIT1; // P1.1 Hi-low edge
P1IFG &= ~BIT1; // P1.1 IFG cleared
_BIS_SR(GIE); // Enable global interrupt
PM5CTL0 &= ~LOCKLPM5; // Disable the GPIO power-on default high-impedance mode
while (1) {
__no_operation(); // Do nothing
}
}
#pragma vector=PORT1_VECTOR
__interrupt void Port_1(void)//中断服务函数名在fr6989.h里面
{
P1OUT ^= BIT0; // P1.6 toggled
P1IFG &= ~BIT1; // P1.3 IFG cleared
}
复制代码
4.1 什么是中断
中断是单片机内非常重要的一个功能。在单片机内部,CPU就像是主心骨,能力强责任大,最重要的任务都需要它来完成。同时正是因为CPU性能较强,因此CPU运行时要消耗的功耗也是很大的。而中断就像是CPU的一个助理,当出现一些特定事件时助理会主动提醒CPU,让CPU及时知道并做出反应。有了中断,CPU不仅可以一心多用,同时处理多个任务,而且在不是必须工作时CPU可以解放出来,这样可以大大节省单片机的功耗。
以下是几个常见的中断应用场景:
总之中断起到的作用就是提醒,就像接电话的过程一样,正常情况下CPU按照程序一步一步的执行,当有电话进来时(中断事件发生时),CPU会暂时放下手里的工作,处理电话中的事情,处理完之后再继续投入之前的工作。
轮询 vs 中断
要让CPU检测到一件事情发生有2种方法,一种叫“轮询”,一种叫“中断”。就好像收快递,如果没有短信或电话提醒,那只能不断的到快递点去询问“快递到了吗?”,这种情况就是轮询。而中断就是提醒短信,只要收到短信后再到快递点去取件就可以了。
这样一对比就发现,中断比轮询要省力得多。对于单片机也是一样,有了中断之后平时单片机可以去处理其他任务,甚至进入休眠状态,只要等中断事件发生时再醒来处理就可以了,处理完了还可以继续睡大觉。
一个例子就是我们第二章学过的按键状态检测,当时我们用的就是轮询的方式,CPU反复的读取I/O口的状态,判断按键是否被按下。这种方式下CPU一直被占用。而本章中我们就要学会使用中断来处理按键,CPU占用率会大大降低。
上图左边就是轮询方式检测按键,右边则是中断方式来实现。请注意右边的中断服 务函数,中断服 务函数里面就是单片机收到中断信号后具体要做的事情。MSP430的中断服 务函数有固定的格式。
另外,要想使用中断,必须开启中断使能位,同时在对应外设中设置好中断功能(例如将GPIO引脚设为中断输入引脚)。
4.2 中断的工作过程
要想学会使用中断,需要先了解一下中断的工作过程。一个中断过程可以分为4个阶段。
1) 中断事件发生
首先,中断事件的发生是一个中断的起点。中断事件指的是可以导致CPU进入中断的外部事件,MSP430中有许多外设可以产生中断事件,例如串口、GPIO、定时器、ADC等等。
以GPIO为例,I/O口上的电平变化就可以是一个中断事件,如果I/O口外接按键,那么按键按下或抬起所造成的电平变化就可以导致中断事件发生。MSP430系列大部分型号允许两组I/O口(P1和P2)作为中断输入。
2) 中断标志位被置位
当中断事件被单片机探测到之后,对应的中断标志位(interrupt flag,简称IFG)就会被自动置位(置为1)。置位之后除非进行复位,否则中断标志位会一直保持置位状态。这样做的好处是当有些中断事件发生的时间很短时,中断信号不会被CPU所忽略。MSP430的外设中有该外设中断所对应的IFG寄存器,中断事件发生时这些寄存器就会被改写。
中断信号是如何到达CPU的呢?中断事件发生以后,对应的外设IFG寄存器被置位,但这并不意味着CPU能够接收到中断信号,要想CPU能接收到中断信号,必须将中断使能位(Interruptenable bits,简称IE)置为1。
中断使能位的存在是使得CPU能够自由选择对哪个中断事件作出响应。在单片机工作过程中,可能会产生多个不同外设的中断事件,例如定时器溢出、串口接到数据、I/O口电平变化等,但单片机CPU并不一定想对所有这些中断事件作出响应。有了中断使能位,就像有了开关,CPU就可选择只响应那些需要响应的中断事件。
默认情况下,所有中断使能位都是关闭的(除了看门狗以外)。我们需要通过编程来打开需要的中断使能位。例如想要检测P1.3口所连接的按键,就要将P1端口的中断使能寄存器(P1IE)中的BIT3置为1。
除了上述中断使能位以外,CPU中还有一个全局中断使能位,它相当于一个总开关。MSP430的全局中断使能位称为GIE,它在SR寄存器中。
3) CPU响应中断
现在我们假设一个中断事件已经发生,IFG也已被置位,对应的中断使能位也已经打开,这样我们确保中断信号能够顺利到达CPU。那么CPU接收到中断信号之后如何做出反应呢?
CPU接收到中断信号之后,会暂停正在执行的工作,并执行中断服 务函数(interrupt ser-vice routine,简称ISR)。这句话说起来简单,但实际上CPU为了保证执行完中断服 务函数之后能够顺利的回到之前的工作,需要做一系列的准备工作,如下图中第3项所述。
在执行完当前一条指令后,CPU会保存当前程序计数器(PC)的值,这样就记录下现在程序执行到的位置,以便中断结束后能返回主程序继续执行。同时状态寄存器SR的值也会被保存起来。接下来CPU会将SR寄存器清零,这将使单片机退出所有低功耗状态以便执行中断程序。另外SR寄存器中的全局中断使能位GIE也将被禁用,这样就避免了在执行中断服 务函数时CPU再响应其他中断。退出中断后CPU会重新取回SR寄存器的值,程序回到进入中断之前的位置继续执行。
接下来中断向量的地址会被加载到程序计数器(PC)中,这样就会进入到中断服 务函数的入口开始执行中断程序。CPU之所以能识别出中断的源头,是因为每一个中断标志位(IFG)是与一个中断服 务函数(ISR)一一对应的,这个对应关系存在一个表里面,这个表叫做中断向量表。例如P1端口的中断就与中断向量PORT1_VECTOR对应,因此检测到P1口的中断信号后就会自动进入PORT1_VECTOR对应的中断服 务函数。更多关于中断向量表的知识将在后续章节中详细介绍。
4) 执行中断服 务函数
中断服 务函数(ISR)里的内容是由用户编程的,想要单片机在中断时执行什么任务,就在中断服 务函数中编写相应指令。例如想要实现按键按下时LED灯亮起,那么就在GPIO中断函数中加入LED控制的语句。
中断服 务函数所做的事情由上图第4项描述,其中最关键的就是红色的“Runyour interrupt’s code”。不过需要注意在此之前,还需要判断是否为“grouped interrupt”。由于MSP430的中断资源是很宝贵的,很多中断会共用一个中断向量入口。例如前面提到的P1端口中断,当P1口的8个I/O口中的任何一个检测到中断事件时,都会进入中断服 务函数。这时就需要在中断服 务函数中先查询中断标志位,以判断究竟是哪个I/O口“出事”了。
|
4.4 如何写一个中断函数
要写一个带有中断的程序,需要做下面几件事情:
1) 配置外设中和中断有关的寄存器,例如I/O口中断是上升沿触发还是下降沿触发,定时器中断的计数方式和定时值等等。
2) 依照中断服 务函数的模板写中断服 务函数,添加中断后要干什么的代码。
3) 使能外设的中断,使能全局中断(GIE)
4) 一旦中断发生,CPU停下主函数中的任务,并标记位置,进入中断服 务函数,执行完中断服 务函数之后回到主函数标记位置处继续运行。
中断服 务函数
我们想要单片机在中断里执行什么任务,都是由中断服 务函数决定的。中断服 务函数的编写和一般的函数略有不同,我们以P1端口的中断服 务函数来做一个说明:
复制代码
1) #pragma vector=PORT1_VECTOR
#Pragma是编译器指令,是告诉编译器将函数与中断向量连接起来。“vector=”后面是中断向量地址的宏定义,例如P1口中断就是PORT1_VECTOR,定时器中断就是TIMER0_A1_VECTOR。
不同外设的中断向量名在哪里找呢?打开CCS可以跳转到一个名为msp430.h的头文件。在里面在找到"msp430fr6989.h"这个头文件,搜索Interrupt Vectors, 这个下面包含了所有寄存器位的宏定义,包括中断向量的宏定义(如下图)。其中包含了所有中断向量的名称。例如P1端口的中断向量名就是PORT1_VECTOR。
2) __interrupt void Port_1(void)
__interrupt关键字表明这是一个中断服 务函数,CPU见到这个关键字以后就会去做中断之前的准备工作。Port_1是用户自己取的函数名称,这个名称可以任意命名。
3) 中断服 务函数的具体内容
中断服 务函数的内容依据中断的不同种类有所差别。退出中断前一定不要忘记将中断标志位复位。
本文参考:https://e2echina.ti.com/group/universityprogram/students/f/11/p/149721/424157#424157