Arduino菜鸟通俗版解读系列(8)中断和伪中断

您所在的位置:网站首页 arduinofor循环跳不出去 Arduino菜鸟通俗版解读系列(8)中断和伪中断

Arduino菜鸟通俗版解读系列(8)中断和伪中断

2024-04-18 14:09| 来源: 网络整理| 查看: 265

这一篇我们来讲一讲“中断”的概念。之所以会想讲“中断”这个内容,是因为在第6篇中我们遇到了一个新的主函数: serialEvent()。 我们在以往的篇幅中讲过Arduino有两个主函数:setup() 和loop() 。(Arduino中所谓的主函数意思可以理解为这个函数不需要你自己定义,本身就存在,并且你写的任何Arduino程序在运行过程中一定会运行它们,即默认存在的函数;同时我们在本篇后文中会讲到,setup(),loop(),serialEvent()这几个函数其实是包含在一个叫main()的更大的主函数内部的,也就是C语言中我们常常见到的那个main()主体函数)第6篇文章中忽然冒出一个serialEvent()函数大家可能会觉得很唐突,所以为了向大家解释:为什么说好的只有两个主函数setup() {...}和loop() {...},但是现在又冒出一个新的主函数serialEvent() {..}这个问题,我打算在这一篇专门讲一讲其中的来龙去脉。

图1

关于serialEvent()函数的诞生,要从一个计算机常用概念讲起------“中断”。先点个题:serialEvent()函数本身是一个“伪中断”,但是我们要从什么是“中断”开始讲起。“中断“这个词相信有的朋友听过,它的大概意思是什么呢?见图1。图1中就是中断的一个逻辑流程图,它讲了这么一个故事:假设我们有一段程序正在运行,这个时候忽然CPU接收到了一个中断信号,这个中断信号告诉CPU:“你停一停我这边有个急事儿,你支持一下!”,于是CPU暂时停止运行主程序,开了一个小差跑去支持这个急事儿即中断程序,在运行完中断程序以后CPU又跑回来继续运行刚才的主程序。这就是“中断”的意思,做过项目管理的朋友看到这里一定会很有感触地笑了一下,对这就是我们日常工作的样子,电脑也一样往往有急事儿需要支持。可能有的朋友会问:“这和之前我们遇到过的那些按部就班运行的程序有什么区别吗?”,好吧,其实我想说的是:中断的特点就是随叫随停,CPU什么时候收到中断信号,什么时候就能立刻马上跳出主程序,运行中断程序,这就是“中断”的特点。那这个特点有什么好呢?很简单,就好比工作中,你有两个同事A和B,你经常他们帮忙,但是A同事往往会说:“等一下,等我忙完手上这件事在帮你处理。”B同事会说:“OK,我立刻帮你处理。等处理完你的事儿我再回来继续干自己的活。“你说哪个同事是你喜欢的呢?显然是B同事对不对?B同事就是”中断“处理模式而A同事就是传统的按部就班处理模式。

知道了中断的大致含义,下面我们来看一下实际的流程图,可能会有点抽象,不过我们现在要慢慢培养”编程思维“了,毕竟Arduino 是一款微处理器,没有很好的编程思维那是不可以的。见图2。

图2

图2中绘制了两个流程图,虚线左侧的是一个,虚线右侧是另一个。

先看左侧的流程图,左侧的流程图讲了一件什么事呢?很简单,就是输出1~5这几个数字,打印完了以后判断一下2号引脚的状态,如果2号引脚此时是高电平就输出一个“Yes",如果是低电平就输出一个"No"。接着再循环到开头,重新走一遍前面的流程,如此一直循环下去。这个流程图本身没什么意义,大家不要过度解读,就是无脑地输出数字1~5,然后再基于2号引脚的状态输出“Yes"或者"No"。但是大家可以发现一个问题,那就是我们必须等到1~5这几个数字全部输出完毕以后,才能够判断2号引脚状态,然后再输出完一轮1~5这几个数字,才能够再进行一次2号引脚判断;那如果我想在任一我希望的时刻判断2号引脚状态的话该怎么办呢?这个问题就要是人们发明“中断”的初衷。OK 可能有的编程思维不是太强的朋友还不能很好地理解上面这个例子想说明的问题,那么我按照惯例给出一个生活中的实际例子来加以比喻。如:我们把上面输出数字1~5这几步比做你在做作业,作业就是不断地默写1,2,3,4,5这几个数字(很无脑);然后每次你默写完一轮5个数字后,你就要去门口看看爸妈回家没,因为你爸妈没带钥匙你得帮着开门,如果发现爸妈回来了你就要开门,如果爸妈没回来你就继续回去写作业。这个例子中的一个痛点就是:你必须时刻注意你爸妈是否回家,所以你要定期去门口看一看,这就很影响你写作业是不是?我们希望什么呢?我们希望爸妈回家的时候可以主动按门铃,我们听到门铃后才去开门,这样就不需要写一会儿作业就跑去门口看看了。

了解了上面这个痛点后,我们可以开始理解“中断”了。我们在门口安装了一个门铃,一旦门铃响了,我们就会停止写作业,跑去给爸妈开门;而这个门铃就是“中断信号“,我们去开门这个行为就是“中断程序”。中断的优势就在于,只要门铃不响,我们就能安心写作业不需要定期停下来去门口确认爸妈回家与否。所以对于CPU而言,采用”中断“这种方式,可以节省CPU计算资源,CPU就相当于上面例子中的”你“,你专心写作业的时候是不是会比你总当心爸妈回家与否来的效率高呢?当然是!所以”中断“可以让CPU专心地做一件事,所以CPU的效率会因此而变高。

那么好,”中断“是具体事怎么实现的呢?看图2右边的流程图。事实上,右边的流程图相对于左边的流程图只是改变了两件事:1.把“输出1~5这几个数字”和“判断引脚2的状态”这两块流程分开;2.在整个流程图的最开始设置了一个“中断信号”引脚,即将引脚3作为中断信号引脚。(在Arduino中是这么做的,在其他电子平台上是不是这么简单我不清楚,反正Arduino很简单很容易操作)在完成上述两种改变后,右侧的流程图的执行逻辑是这样的:当引脚3没有“中断信号”传入的时候,不断地循环输出1~5这五个数字,一旦3号引脚发现“中断信号”则立刻跳转到“查看2号引脚状态,并输出对应结果”的流程中。讲到这里可能有的朋友会问:“OK我理解你说的意思了,可是我还是不明白怎么在Arduino中通过程序来实现这个过程?”那么下面请看图3和图4,图3是对应于图2右侧流程图的程序,图4则是图3对应的硬件连接图。

图3图4

先讲图4中的硬件连接,3号引脚和2号引脚分别接一个单刀双掷开关;以2号引脚为例,假如2号引脚的开关A往左打,则接通5V高电平,假如往右打则接通GND,即接地成为低电平。3号引脚与2号引脚运行模式一样。硬件结构就这么简单。

再看图3,图3中程序分为三大部分:check程序,setup主程序,loop主程序。下面一一做介绍:

check程序---check程序对应的就是图2中右侧流程图上判断2号引脚的那部分逻辑流程,check程序本身很容易理解,就是一个if,else判断语句来判断2号引脚的状态,假如图4中A开关打到5V位置,则2号引脚状态为高电平HIGH,此时根据图3中程序所示Arduino将输出一个“yes";假如图4中A开关打到GND位置,则2号引脚状态为低电平LOW,此时根据图3中程序所示Arduino将输出一个“no",这就是check程序的逻辑。不过没有结束!还有一点很重要:check程序必须放在setup程序的前面,否则整个图3中的程序将运行出错。为什么?这个就要牵扯到下面的setup主程序的内容了,不过在这里提前讲解一下:setup主程序中最后一句语句:

attachInterrupt(digitalPinToInterrupt(3), check, CHANGE);

我们可以看见有调用到check这个程序,因为在setup中需要调用check程序,所以我们就必须在此之前定义好check程序的内容,这就是为什么我们必须将check程序放在setup主程序之前的原因。

setup主程序---这个主程序我们在前面几篇文章中讲过好多次了,所以关于波特率的设置和引脚状态设置就不重复讲解了,唯一要讲的是

attachInterrupt(digitalPinToInterrupt(3), check, CHANGE);

它的原型是这样的:

attachInterrupt(digitalPinToInterrupt(X) , 中断函数 , 状态 );

这个语句是整个图3中程序的核心语句,它的作用就是建立起一个中断。具体来讲这个语句的意思是这样的:digitalPinToInterrupt(X)是将X引脚设定为中断信号引脚,换句话讲就是X引脚就相当于前面说的门铃,我们需要在建立中断的时候先定义一个门铃,门铃一响就运行中断程序,在图3的例子中我们把3号引脚作为这个门铃,所以X就填3;第二个参数是设定中断函数,也就是当门铃响的时候,你需要Arduino运行什么程序,你需要Arduino运行什么程序就把对应的程序名写入第二个参数中,在图3的例子中“中断函数”的函数名是check;第三个参数是设定中断引脚X的状态,这个是什么意思呢?意思是我们希望把中断引脚的什么状态作为中断信号。好比我们通常是听到门铃响了,才会去开门,但是我们也可以设定门铃一般情况下一直响,有人来按了一下的时候门铃就不响了,于是当门铃不响的时候我们去开门,这听起来很愚蠢,但是确实说明了所谓的“中断信号”其实是需要我们自己定义的。回到Arduino上,我们知道任何引脚都有高电平,低电平两个状态,那么是在高电平状态作为中断信号还是低电平状态作为中断信号呢?其实都可以,下面是第三个参数不同的取值对应的定义:

HIGH当引脚为高电平时,触发中断;

LOW 当引脚为低电平时,触发中断;

CHANGE 当引脚电平发生改变时,触发中断;

RISING 当引脚由低电平变为高电平时,触发中断;

FALLING 当引脚由高电平变为低电平时,触发中断;

那么在图4的程序中我们选用的是CHANGE,即只要引脚3的状态发生改变,我们就触发中断,也就是说当引脚3从高电平变为低电平时我们会触发中断,从低电平变为高电平时也会触发中断。

Loop主程序---这个就没什么可说的了,纯粹是循环输出数字1~5而已。

好了上面给出了一个完整的带有中断的程序,也对其进行了讲解,那么这个中断程序运行起来会是一个什么效果呢?对照图4来讲就是这样的效果:假设初始状态如图4所示A开关接在5V上,B开关也接在5V上;当我们不去改变A,B开关状态时Arduino将一直输出1,2,3,4,5,1,2,3,4,5,如此一直循环下去;一旦我们改变B开关的状态,此时Arduino将会迅速地判断2号引脚状态,假如为高电平就输出一个“yes",否则就输出一个“no",而2号引脚的状态取决于A开关的位置,按照图4中所示此时A开关处于5V位置,为高电平,所以会输出“yes"。

以上就是中断的概念,不过还没结束,接下去要引出伪中断的概念了,也就是serialEvent()这个函数,这个函数在Arduino中往往被很多人看作是一个“伪中断”函数,那么下面就来讲为什么serialEvent() 被称为伪中断。见图5和图6。

图5图6

图5所示的其实是Arduino背后的源代码,也就是藏在setup()和loop()这两个函数背后的底层代码。从图5中可以看到setup()和loop()其实隶属于一个main()函数,就是图5中一开头出现的int main()这个函数,看到这个函数有的朋友肯定恍然大悟了:“这不就是C语言的main 函数吗?“对!!这就是C语言的代码,我们在这个系列的最开始讲到过,整个Arduino就是基于C语言开发的,那么当我们今天讲到这里的时候也就印证了这一说法。继续往下看图5中的程序,第一个红框中有一堆看不懂的代码,其实它们是一堆自定义函数,比如init(),这个函数是一个自定义的初始化函数,什么意思呢?我们说过Arduino本身是一个集成度很高的电子平台,它帮我们做好了所有的前期初始化工作,比如时钟定义,串口的定义等等,而这些初始化工作其实就是init() 这个自定义函数完成的,而所谓自定义就是说是Arduino开发者帮我们写了这一个init()函数,它是Arduino自带的,这个函数其实背后还有一个脚本文件,在那个脚本文件中放着所有初始化语句的具体内容,init只是这个函数的函数名,其实就相当于图3中的check函数一样。好了,这是第一个红框的内容介绍,不过这不是重点,重点是第二个红框。第二个红框才是体现了Arduino运行逻辑的核心代码,可以看到在完成所有初始化以后,Arduino会首先运行setup函数,这就和我们前面一直以来讲的内容对应上了,在setup函数运行完毕后我们看到一个for循环语句,在这个for循环语句中放着loop函数。这正好印证了我们说过的loop函数会不断循环执行,因为我们可以看见for循环的设定是一个无限循环,即for( ;;){...} ,在它的第一个小括号内并没有设置任何循环数的限制。在loop函数后面还跟着一个语句:

if (serialEventRun) serialEventRun();

这个语句说了一个什么意思呢?它说:“如果serialEventRun为“真”,则运行serialEventRun这个函数。“注意,这个serialEventRun函数不是serialEvent函数,请看图6,其实serialEvent函数是serialEventRun函数里面的一个函数。在图6中我们看到serialEventRun函数内部有4个if...endif判断语句,这4个判断语句做了些什么事情呢?其实它们做的事情是类似的,所以我们就拿第一个if...endif语句来讲即可:

#if defined(HAVE_HWSERIAL0)

if (Serial0_available && serialEvent && Serial0_available()) serialEvent();

#endif

这段语句首先判断0号硬串口存不存在,假如0号硬串口存在那么后面的第二行程序就会被保留,假如0号硬串口不存在那么后面的第二行程序会被Arduino自动删除。这就是#if defined(HAVE_HWSERIAL0)这句话的功能。可是为啥要先有这么一个判断硬串口是否存在的语句呢?我们在之前的文章中讲到过一个事实:Arduino有很多系列的板子,不仅仅是Arduino Uno,还有Arduino Mega,Arduino Leonardo......等等,它们的性能参数是不一样的,其中就包括硬串口数量不一样。当我们用的是一款Arduino Uno时,它最多只有一个硬串口可以存在,所以这个时候由于defined(HAVE_HWSERIAL0)这句话的存在,后面的HWSERIAL1,HWSERIAL2,HWSERIAL3相关语句就会被自动删除;而假如我们用的是一款Arduino Mega的话,由于它最多支持4个硬串口,这个时候通过defined(HAVE_HWSERIAL0)这句话的作用,就可以最多打开四个硬串口。

好了,在判断完Arduino板子到底有几个串口后,程序进入第二行:

if (Serial0_available && serialEvent && Serial0_available()) serialEvent();

这句话讲的是:“假如我们定义了serialEvent函数,同时0号硬串口的串口寄存器中有数据传入的话,就开始运行serialEvent函数。”

最后的endif则是呼应第一句的if,形成一个完整的语法,无实际意思。

所以总结一下图5和图6中所讲的意思,见图7。图7是一个针对图5和图6的程序所绘制的流程图,从图7中可以很清晰地看到:loop函数后面跟着4个串口的判断语句,假如所有串口的serialEvent函数都不存在或者寄存器中没有数据传入,那么loop函数运行完毕以后不会执行后面的串口程序,此时for 循环只会不断循环执行loop函数;假如任意一个serialEvent函数存在且有数据传入串口寄存器,那么loop函数运行完以后就会运行serialEvent函数。所以serialEvent函数被称为“伪中断函数”是因为它虽然具有“中断”函数的特点但是它无法做到任意时刻立即运行,它一定是在loop函数运行完一次以后才能运行。

关于serialEvent函数的运行条件总结如下:

1.每次loop函数运行完毕,下一次loop函数还未开始之前,即两次loop函数运行之间的时刻;

2.用户定义了serialEvent()函数,注意这里讲的定义和自定义函数有点不同,自定义函数是不管函数名还是函数内容都由用户自己来定义;而serialEvent这个函数名是默认的,你不能改动,你能做的是自己来写serialEvent()里面的执行语句。简单地讲就是你只能决定serialEvent()函数做什么,但是你不能改它的名字。

2.串口寄存器中有数据传入;

上面三个条件同时满足时,serialEvent函数会被周期性调用!基于这三个条件的限制,serialEvent函数被认为是一种“伪中断函数”。

图7



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3