Answers:
编写中断服务程序(ISR)时:
delay ()
大多数处理器都有中断。中断使您可以在执行其他操作时响应“外部”事件。例如,如果您正在做晚餐,则可以将土豆煮20分钟。您可以设置一个计时器,然后再看电视,而不是盯着时钟看20分钟。当计时器响起时,您可以“中断”电视观看,以进行土豆处理。
const byte LED = 13;
const byte SWITCH = 2;
// Interrupt Service Routine (ISR)
void switchPressed ()
{
if (digitalRead (SWITCH) == HIGH)
digitalWrite (LED, HIGH);
else
digitalWrite (LED, LOW);
} // end of switchPressed
void setup ()
{
pinMode (LED, OUTPUT); // so we can update the LED
pinMode (SWITCH, INPUT_PULLUP);
attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE); // attach interrupt handler
} // end of setup
void loop ()
{
// loop doing nothing
}
此示例说明,即使主循环无所事事,如果按了D2引脚上的开关,也可以打开或关闭13引脚上的LED。
要对此进行测试,只需在D2与地面之间连接电线(或开关)即可。内部上拉电阻(在设置中启用)通常将引脚强制为高电平。接地时,它变为低电平。引脚变化由CHANGE中断检测,该中断导致调用中断服务程序(ISR)。
在一个更复杂的示例中,主循环可能正在做一些有用的事情,例如获取温度读数,并允许中断处理程序检测按下的按钮。
为了简化将中断向量号转换为引脚号,您可以调用函数digitalPinToInterrupt()
,并传递一个引脚号。它返回适当的中断号或NOT_AN_INTERRUPT
(-1)。
例如,在Uno上,板上的D2引脚是中断0(下表中的INT0_vect)。
因此,这两行具有相同的效果:
attachInterrupt (0, switchPressed, CHANGE); // that is, for pin D2
attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);
但是第二个更易于阅读,并且可以移植到不同的Arduino类型。
以下是按优先级排列的Atmega328中断列表:
1 Reset
2 External Interrupt Request 0 (pin D2) (INT0_vect)
3 External Interrupt Request 1 (pin D3) (INT1_vect)
4 Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
5 Pin Change Interrupt Request 1 (pins A0 to A5) (PCINT1_vect)
6 Pin Change Interrupt Request 2 (pins D0 to D7) (PCINT2_vect)
7 Watchdog Time-out Interrupt (WDT_vect)
8 Timer/Counter2 Compare Match A (TIMER2_COMPA_vect)
9 Timer/Counter2 Compare Match B (TIMER2_COMPB_vect)
10 Timer/Counter2 Overflow (TIMER2_OVF_vect)
11 Timer/Counter1 Capture Event (TIMER1_CAPT_vect)
12 Timer/Counter1 Compare Match A (TIMER1_COMPA_vect)
13 Timer/Counter1 Compare Match B (TIMER1_COMPB_vect)
14 Timer/Counter1 Overflow (TIMER1_OVF_vect)
15 Timer/Counter0 Compare Match A (TIMER0_COMPA_vect)
16 Timer/Counter0 Compare Match B (TIMER0_COMPB_vect)
17 Timer/Counter0 Overflow (TIMER0_OVF_vect)
18 SPI Serial Transfer Complete (SPI_STC_vect)
19 USART Rx Complete (USART_RX_vect)
20 USART, Data Register Empty (USART_UDRE_vect)
21 USART, Tx Complete (USART_TX_vect)
22 ADC Conversion Complete (ADC_vect)
23 EEPROM Ready (EE_READY_vect)
24 Analog Comparator (ANALOG_COMP_vect)
25 2-wire Serial Interface (I2C) (TWI_vect)
26 Store Program Memory Ready (SPM_READY_vect)
内部名称(可用于设置ISR回调)在方括号中。
警告:如果您拼错了中断向量的名称,即使只是弄错了大写(很容易做到),中断例程也不会被调用,也不会得到编译器错误。
您可能使用中断的主要原因是:
在串行端口,SPI端口或I2C端口上发送或接收数据时,“数据传输”可用于让程序执行其他操作。
外部中断,引脚更改中断和看门狗定时器中断也可以用来唤醒处理器。这可能非常方便,因为在睡眠模式下,处理器可以配置为使用更少的功率(例如,大约10微安)。上升,下降或低电平中断可用于唤醒小工具(例如,如果您按了它的按钮),或者“看门狗定时器”中断可能会定期唤醒它(例如,检查时间或温度)。
如果按键盘上的某个键或类似键,则可以使用引脚更改中断来唤醒处理器。
计时器中断(例如,计时器达到某个值或溢出)和某些其他事件(例如传入的I2C消息)也可以唤醒处理器。
无法禁用“复位”中断。但是,其他中断可以通过清除全局中断标志来暂时禁用。
您可以使用函数调用“ interrupts”或“ sei”来启用中断,如下所示:
interrupts (); // or ...
sei (); // set interrupts flag
如果需要禁用中断,则可以像这样“清除”全局中断标志:
noInterrupts (); // or ...
cli (); // clear interrupts flag
两种方法都具有相同的效果,使用interrupts
/ noInterrupts
可以更容易记住它们的位置。
Arduino中的默认设置是启用中断。不要长时间禁用它们,否则定时器将无法正常工作。
您可能不希望中断时间紧迫的代码段,例如通过计时器中断。
同样,如果ISR正在更新多字节字段,则可能需要禁用中断,以便“原子地”获取数据。否则,在读取另一个字节时,ISR可能会更新一个字节。
例如:
noInterrupts ();
long myCounter = isrCounter; // get value set by ISR
interrupts ();
临时关闭中断可确保isrCounter(在ISR中设置的计数器)在我们获取其值时不会发生变化。
警告:如果不确定中断是否已经打开,则需要保存当前状态,然后再将其恢复。例如,来自millis()函数的代码执行以下操作:
unsigned long millis()
{
unsigned long m;
uint8_t oldSREG = SREG; // <--------- save status register
// disable interrupts while we read timer0_millis or we might get an
// inconsistent value (e.g. in the middle of a write to timer0_millis)
cli();
m = timer0_millis;
SREG = oldSREG; // <---------- restore status register including interrupt flag
return m;
}
请注意,所指示的行会保存当前的SREG(状态寄存器),其中包括中断标志。获得定时器值(4个字节长)后,我们将状态寄存器放回原来的状态。
功能cli
/ sei
和寄存器SREG特定于AVR处理器。如果您正在使用其他处理器(例如ARM处理器),则功能可能会略有不同。
如果使用cli()
,则禁用所有中断(包括定时器中断,串行中断等)。
但是,如果只想禁用特定的中断,则应清除该特定中断源的中断使能标志。例如,对于外部中断,请致电detachInterrupt()
。
由于存在25个中断(复位除外),因此可能一次或至少在处理前一个中断之前发生多个中断事件。在禁用中断时,也可能发生中断事件。
优先级顺序是处理器检查中断事件的顺序。列表越高,优先级越高。因此,例如,将在外部中断请求1(引脚D3)之前为外部中断请求0(引脚D2)提供服务。
中断事件(即注意到该事件)可以随时发生,并且大多数事件可以通过在处理器内部设置“中断事件”标志来记住。如果禁用了中断,则当再次启用它们时,将按优先级顺序处理该中断。
中断服务程序是不带参数的函数。一些Arduino库旨在调用您自己的函数,因此,您仅提供了一个普通函数(如上述示例中所示)。
// Interrupt Service Routine (ISR)
void switchPressed ()
{
flag = true;
} // end of switchPressed
但是,如果库尚未为ISR提供“挂钩”,则可以自己创建,例如:
volatile char buf [100];
volatile byte pos;
// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR; // grab byte from SPI Data Register
// add to buffer if room
if (pos < sizeof buf)
{
buf [pos++] = c;
} // end of room available
} // end of interrupt routine SPI_STC_vect
在这种情况下,您将使用“ ISR”宏,并提供相关中断向量的名称(从之前的表开始)。在这种情况下,ISR正在处理SPI传输完成。(请注意,一些旧代码使用SIGNAL而不是ISR,但是不建议使用SIGNAL)。
对于已经由库处理的中断,您只需使用记录的接口。例如:
void receiveEvent (int howMany)
{
while (Wire.available () > 0)
{
char c = Wire.receive ();
// do something with the incoming byte
}
} // end of receiveEvent
void setup ()
{
Wire.onReceive(receiveEvent);
}
在这种情况下,I2C库旨在内部处理传入的I2C字节,然后在传入数据流的末尾调用提供的函数。在这种情况下,receiveEvent严格来说不是ISR(它具有一个参数),但是由内置ISR调用。
另一个例子是“外部引脚”中断。
// Interrupt Service Routine (ISR)
void switchPressed ()
{
// handle pin change here
} // end of switchPressed
void setup ()
{
attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE); // attach interrupt handler for D2
} // end of setup
在这种情况下,attachInterrupt函数会将函数switchPressed添加到内部表中,并另外在处理器中配置适当的中断标志。
有了ISR后,下一步就是告诉处理器您希望此特定条件引发中断。
例如,对于外部中断0(D2中断),您可以执行以下操作:
EICRA &= ~3; // clear existing flags
EICRA |= 2; // set wanted flags (falling level interrupt)
EIMSK |= 1; // enable it
更具可读性的是使用定义的名称,如下所示:
EICRA &= ~(bit(ISC00) | bit (ISC01)); // clear existing flags
EICRA |= bit (ISC01); // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0); // enable it
EICRA(外部中断控制寄存器A)将根据Atmega328数据表中的此表进行设置。定义所需的确切中断类型:
EIMSK(外部中断屏蔽寄存器)实际上使能了中断。
幸运的是,您无需记住这些数字,因为attachInterrupt会为您完成这些任务。但是,这实际上是在发生这种情况,对于其他中断,您可能必须“手动”设置中断标志。
为了简化您的生活,一些常见的中断处理程序实际上位于库代码中(例如INT0_vect和INT1_vect),然后提供了更加用户友好的界面(例如,attachInterrupt)。attachInterrupt实际执行的操作是将所需中断处理程序的地址保存到变量中,然后在需要时从INT0_vect / INT1_vect调用该地址。它还设置适当的寄存器标志,以在需要时调用处理程序。
简而言之,不,除非您希望他们成为。
输入ISR后,将禁用中断。当然,必须首先启用它们,否则将不会输入ISR。但是,为避免ISR本身被中断,处理器会关闭中断。
当ISR退出时,则再次启用中断。编译器还在ISR内部生成代码以保存寄存器和状态标志,因此中断发生时所做的任何操作都不会受到影响。
但是,如果绝对必要,您可以在ISR内部打开中断,例如。
// Interrupt Service Routine (ISR)
void switchPressed ()
{
// handle pin change here
interrupts (); // allow more interrupts
} // end of switchPressed
通常,您需要一个很好的理由来执行此操作,因为另一个中断现在可能会导致对pinChange的递归调用,可能会产生不良结果。
根据数据手册,为中断服务的最短时间为4个时钟周期(将当前程序计数器压入堆栈),然后是现在在中断向量位置执行的代码。这通常包含一个到中断例程真正位置的跳转,这是另外3个周期。对编译器生成的代码进行的检查表明,使用“ ISR”声明进行的ISR执行可能需要大约2.625 µs的时间,加上代码本身所做的任何事情。确切的数量取决于需要保存和恢复多少个寄存器。最小数量为1.1875 µs。
外部中断(在其中使用attachInterrupt的中断)的功能要多一些,总共花费约5.125 µs(以16 MHz时钟运行)。
这有所不同。上面引用的数字是理想的数字,可以立即处理中断。一些因素可能会延迟:
如果处理器处于睡眠状态,则将指定“唤醒”时间,这可能会花费几毫秒的时间,同时时钟会回滚以加快速度。该时间取决于保险丝的设置以及睡眠的深度。
如果已经在执行中断服务程序,则在完成或启用中断本身之前,无法输入其他中断。这就是为什么您应该使每个中断服务例程的时间都较短的原因,因为每花费一个微秒,您就有可能延迟执行另一个中断服务例程。
一些代码关闭了中断。例如,调用millis()短暂关闭中断。因此,将要处理一个中断的时间将因中断被关闭的时间而延长。
中断只能在一条指令的末尾得到服务,因此,如果一条特定指令需要三个时钟周期并且刚刚开始执行,则该中断将至少延迟几个时钟周期。
保证将中断重新打开的事件(例如,从中断服务程序返回)将至少执行一条指令。因此,即使一个ISR结束并且您的中断处于挂起状态,它仍必须等待另一条指令才能被服务。
由于中断具有优先级,因此可能会在您感兴趣的中断之前处理优先级更高的中断。
在许多情况下,中断可以提高性能,因为您可以继续进行程序的“主要工作”,而不必不断进行测试以查看是否已按下开关。话虽如此,如上所述,服务中断的开销实际上比轮询单个输入端口的“紧密循环”要多。您几乎无法在一个微秒内响应一个事件。在这种情况下,您可以禁用中断(例如定时器),然后循环查找该引脚以进行更改。
有两种中断:
有些会设置一个标志,并且即使导致它们的事件已停止,也会按优先级顺序对其进行处理。例如,引脚D2的上升,下降或改变电平中断。
仅在其他人“现在”发生时才进行测试。例如,引脚D2上的低电平中断。
设置标志的程序可以被视为正在排队,因为中断标志将保持置位状态,直到进入中断例程为止,此时处理器清除该标志。当然,由于只有一个标志,因此如果在处理第一个标志之前再次发生相同的中断条件,则不会对其进行两次服务。
需要注意的是,可以在附加中断处理程序之前设置这些标志。例如,引脚“ D2”上的上升或下降电平中断有可能被“标记”,然后,一旦您执行attachInterrupt,即使该事件在一小时前发生,该中断也会立即触发。为避免这种情况,您可以手动清除该标志。例如:
EIFR = bit (INTF0); // clear flag for interrupt 0
EIFR = bit (INTF1); // clear flag for interrupt 1
但是,将连续检查“低级”中断,因此,即使您不小心,即使调用了该中断,它们也将继续触发。也就是说,ISR将退出,然后中断将立即再次触发。为了避免这种情况,您应该在知道中断触发后立即执行detachInterrupt。
简而言之,请保持简短!在执行ISR时,无法处理其他中断。因此,如果您尝试执行过多操作,则很容易错过按钮按下操作或传入的串行通信。特别是,您不应尝试在ISR中调试“打印”。完成这些工作所花费的时间可能会导致问题超出解决的范围。
合理的做法是设置一个单字节标志,然后在主循环功能中测试该标志。或者,将来自串行端口的传入字节存储到缓冲区中。内置的计时器中断通过在每次内部计时器溢出时触发来跟踪经过的时间,因此您可以通过了解计时器溢出的次数来计算经过的时间。
请记住,ISR内部的中断被禁用。因此,希望由millis()函数调用返回的时间会改变,将导致失望。以这种方式获取时间是有效的,只是要注意计时器不会递增。而且,如果您在ISR中花费的时间过长,则计时器可能会错过溢出事件,从而导致millis()返回的时间变得不正确。
测试表明,在16 MHz Atmega328处理器上,对micros()的调用耗时3.5625 µs。调用millis()耗时1.9375 µs。在ISR中,记录(保存)当前计时器值是一项合理的事情。查找经过的毫秒比经过的微秒更快(毫秒数仅从变量中检索)。但是,通过将计时器0计时器的当前值(将保持递增)与已保存的“计时器0溢出计数”相加来获得微秒计数。
警告:由于中断是在ISR内禁用的,并且由于最新版本的Arduino IDE使用中断进行串行读取和写入,并且还会增加“ millis”和“ delay”使用的计数器,因此您不应尝试使用这些功能在ISR中。换一种方式:
delay (100);
Serial.println ("ISR entered");
)您可以通过两种方法来检测引脚上的外部事件。第一个是特殊的“外部中断”引脚D2和D3。这些一般的离散中断事件,每个引脚一个。您可以通过对每个引脚使用attachInterrupt来达到这些目的。您可以为中断指定上升,下降,改变或低电平条件。
但是,所有引脚也有“引脚更改”中断(在Atmega328上,不一定是其他处理器上的所有引脚)。它们作用于引脚组(D0至D7,D8至D13和A0至A5)。它们的优先级也比外部事件中断低。但是,它们比外部中断更容易使用,因为它们被分为几批。因此,如果触发了中断,则您必须使用自己的代码来准确找出导致中断的引脚。
示例代码:
ISR (PCINT0_vect)
{
// handle pin change interrupt for D8 to D13 here
} // end of PCINT0_vect
ISR (PCINT1_vect)
{
// handle pin change interrupt for A0 to A5 here
} // end of PCINT1_vect
ISR (PCINT2_vect)
{
// handle pin change interrupt for D0 to D7 here
} // end of PCINT2_vect
void setup ()
{
// pin change interrupt (example for D9)
PCMSK0 |= bit (PCINT1); // want pin 9
PCIFR |= bit (PCIF0); // clear any outstanding interrupts
PCICR |= bit (PCIE0); // enable pin change interrupts for D8 to D13
}
要处理引脚更改中断,您需要:
D0 PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1 PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2 PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3 PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4 PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5 PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6 PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7 PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8 PCINT0 (PCMSK0 / PCIF0 / PCIE0)
D9 PCINT1 (PCMSK0 / PCIF0 / PCIE0)
D10 PCINT2 (PCMSK0 / PCIF0 / PCIE0)
D11 PCINT3 (PCMSK0 / PCIF0 / PCIE0)
D12 PCINT4 (PCMSK0 / PCIF0 / PCIE0)
D13 PCINT5 (PCMSK0 / PCIF0 / PCIE0)
A0 PCINT8 (PCMSK1 / PCIF1 / PCIE1)
A1 PCINT9 (PCMSK1 / PCIF1 / PCIE1)
A2 PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3 PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4 PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5 PCINT13 (PCMSK1 / PCIF1 / PCIE1)
如果掩码指定多个(例如,如果您要在D8 / D9 / D10上中断),则中断处理程序将需要找出导致中断的引脚。为此,您需要存储该引脚的先前状态,并在此特定引脚发生更改的情况下进行处理(通过执行digitalRead或类似操作)。
即使您不亲自尝试,“正常”的Arduino环境也已经在使用中断。millis()和micros()函数调用利用“定时器溢出”功能。内部定时器之一(定时器0)设置为大约每秒中断1000次,并递增一个内部计数器,该计数器实际上成为millis()计数器。除此之外,还有更多内容,因为可以对精确的时钟速度进行调整。
硬件串行库也使用中断来处理传入和传出的串行数据。这非常有用,因为您的程序可以在触发中断时执行其他操作,并填充内部缓冲区。然后,当您检查Serial.available()时,您可以找出已放入该缓冲区的内容(如果有的话)。
在Arduino论坛上进行了一些讨论和研究之后,我们明确了启用中断后的确切情况。我可以通过三种主要方式来考虑启用中断,而以前并未启用这些中断:
sei (); // set interrupt enable flag
SREG |= 0x80; // set the high-order bit in the status register
reti ; // assembler instruction "return from interrupt"
在所有情况下,即使中断事件未决,处理器也保证将始终执行允许中断后的下一条指令(如果先前已将其禁用)。(“下一个”是指程序序列中的下一个,不一定是物理上的下一个。例如,一条RETI指令跳回到发生中断的位置,然后再执行一条指令)。
这使您可以编写如下代码:
sei ();
sleep_cpu ();
如果不是出于这种保证,则中断可能在处理器休眠之前发生,然后可能永远不会被唤醒。
如果您只希望中断来唤醒处理器,而又不做任何特别的事情,则可以使用EMPTY_INTERRUPT定义,例如。
EMPTY_INTERRUPT (PCINT1_vect);
这只是生成一条“ reti”(从中断返回)指令。由于它不尝试保存或恢复寄存器,因此这是获取中断以唤醒它的最快方法。
关于在中断服务程序(ISR)和主代码(即,不在ISR中的代码)之间共享的变量,存在一些细微的问题。
由于启用中断后,ISR可以随时触发,因此您在访问此类共享变量时需要谨慎,因为它们在访问它们的那一刻可能会被更新。
如果一个变量同时在ISR内部和外部使用,则应仅将其标记为volatile。
例如。
volatile int counter;
将变量标记为易失性告诉编译器不要将变量内容“缓存”到处理器寄存器中,而是在需要时始终从内存中读取变量。这可能会减慢处理速度,这就是为什么在不需要时,不只是使每个变量都变得易变。
例如,要与count
某个数字进行比较,如果count
ISR已更新一个字节而不是另一个字节,则在比较期间关闭中断。
volatile unsigned int count;
ISR (TIMER1_OVF_vect)
{
count++;
} // end of TIMER1_OVF_vect
void setup ()
{
pinMode (13, OUTPUT);
} // end of setup
void loop ()
{
noInterrupts (); // <------ critical section
if (count > 20)
digitalWrite (13, HIGH);
interrupts (); // <------ end critical section
} // end of loop
有关中断,定时器等的更多信息,可以从处理器的数据手册中获得。
空间方面的考虑(帖子大小限制)使我无法列出更多示例代码。有关更多示例代码,请参见我的有关中断的页面。