关于中断(Interrupt)的详细介绍以及IIC、软串口、PinChangeInt库


https://www.arduino.cn/thread-13205-1-1.html
注: 五四三就是英文的 etc.

参考文档

1. 关于中断处理的一些常见问题 . . .

1.1 常常看到有人问到:

  1. 我在中断的子程序内加进IIC通信后就进不了中断了…求指点。
  2. 我在中断程序内加入 Lcd_IIC 的程序后就死机…求指点。

其实, 不论是 IIC/TWI, 或是 SPI, 以及硬串口、软串口甚至 Serial.print 都是要靠中断来帮忙处理, 如果你把中断禁止了, 那 IIC/TWI, SPI 都无法动作了 !

啥?
你说你没有禁止中断?
Arduino 一旦进入中断程序 就会自动禁止中断, 因此, 在中断程序内(包括它的子程序内)原则上无法做 IIC 与 SPI 以及软硬串口的通信!
为何说原则上呢?
因为你还是可以在中断程序内把中断打开, 只要这样:
sei( ); //打开中断
cli(); //关闭中断
可是, 那是否有其他问题就要看看你的中断处理到底做何事情, 以及中断来的时间是否太短 ?
就是,如果两次中断的时间间隔足够长,只要来得及处理或是中断重进入(reentrant)不会有问题, 那就可以把中断打开 !

1.2 关于中断的重进入(reentrant), 请参考维基百科:

http://en.wikipedia.org/wiki/Reentrancy_(computing)
当然, 如果你测试结果没问题那就放心的打开中断 !

1.3 关于中断的概念可以看看奈何大神写的这篇有趣文章:

http://www.arduino.cn/thread-2421-1-1.html

该篇主要是介绍Arduino外部中断INT0, INT1的使用, 也就是外部 0 号和 1 号中断(pin2, pin 3)的介绍, 使用 attachInterrupt(INT_number, function, mode);
也可以参考:
http://arduino.cc/en/Reference/attachInterrupt

1.4 如果你是要使用内部定时器(timer0, timer1, timer2)定时中断, 请看我写的这些贴文:

  1. 使用 MsTimer2 库定时做很多事(教程):
    http://www.arduino.cn/thread-12435-1-1.html

  2. 使用TimerOne库(Timer1)定时做多件事(教程):
    http://www.arduino.cn/thread-12441-1-4.html

  3. 自己控制 timer1 定时器定时做多件事(教程):
    http://www.arduino.cn/thread-12445-1-1.html

  4. 自己控制 timer2 定时器定时做多件事(教程)":
    http://www.arduino.cn/thread-12448-1-1.html

  5. 补充设定 timer1 定时器和 timer2 定时器定时做多件事(教程)
    http://www.arduino.cn/thread-12452-1-2.html

    不论是 SPI, IIC 与软串口都是大量使用中断处理(Interrupt), 在中断处理程序内工作没处理完之前是在禁止其它中断的状态, 如果中断处理程序做太多事, 本来就会影响其他中断的进行! !

IIC 使用 ISR(TWI_vect) 中断处理, 软串口SoftwareSerial 使用 ISR(PCINT0_vect) 或类似的(PCINT1/2)中断, 虽然 ISR(PCINT0_vect) 的优先权高于 ISR(TWI_vect), 但一旦进入 ISR(TWI_vect) 内由于中断请求被禁止, 此时即使软串口所用的 ISR(PCINT0_vect) 中断来到, 一样无法处理, 于是导致软串口的通信失常或数据遗失 !

1.5 Arduino的 CPU 之25种中断之优先级

关于Arduino的 CPU 之25种中断之优先级可看:
http://gammon.com.au/interrupts
就是说, 除了 Reset 之外, 还有 25种中断可以使用:
(依照优先级排列, 所以外部 pin 2 的 INT0 是最优先的! , 其次是 pin 3 的 INT1 中断)

优先级名称功能
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)

2. 关于IIC与软串口等的源代码可看:

https://github.com/arduino/Arduino/tree/master/hardware/arduino/avr/libraries
( 软串口是在 SoftwareSerial.cpp; IIC 要看 Wire.cpp 和其 utility 目录内的 twi.c )

接下来,
我们来讨论可以用在全部 pin 的 Pin Change Interrupt 针脚状态改变中断 !
http://playground.arduino.cc/Main/PinChangeInterrupt
这个在官网已经有大神帮忙写了Library库可用:
http://playground.arduino.cc/Main/PinChangeInt

3. Pin Change Interrupt

请注意, 如果你使用了软串口(SoftwareSerial), 就是说你用了一下:
#include <SoftwareSerial.h>
那接着我们要讲的 PCI (Pin Change Interrupt) 就不能用了, 因为软串口的库已经写个该三个中断处理 ISR( ) 如下:

#if defined(PCINT0_vect)
ISR(PCINT0_vect)
{
  SoftwareSerial::handle_interrupt();
}
#endif

#if defined(PCINT1_vect)
ISR(PCINT1_vect)
{
  SoftwareSerial::handle_interrupt();
}
#endif

#if defined(PCINT2_vect)
ISR(PCINT2_vect)
{
  SoftwareSerial::handle_interrupt();
}
#endif

这可以在你 Arduino IDE 目录下的以下档案找到:
libraries\SoftwareSerial\SoftwareSerial.cpp

在Arduino UNO 以及大部分的板子有 Digital pin 0 到 pin 13, 以及 analog pin A0 到 A5 (又称 pin 14 到 pin 19);
这 20支 pin 分为三组, 对应到 ISR(PCINT2_vect), ISR(PCINT0_vect), 以及 ISR(PCINT1_vect) 这三个 ISR( ) 中断程序:
ISR (PCINT2_vect) 处理 Pin D0 to D7
ISR (PCINT0_vect) 处理 Pin D8 to D13
ISR (PCINT1_vect) 处理 Pin A0 to A5

3.1 Pin Change Interrupts 范例:


ISR (PCINT0_vect)
{
// handle pin change interrupt for pin D8 to D13 here
} // end of PCINT0_vect
 
ISR (PCINT1_vect)
{
// handle pin change interrupt for pin A0 to A5 here
} // end of PCINT1_vect
 
ISR (PCINT2_vect)
{
// handle pin change interrupt for pin D0 to D7 here
} // end of PCINT2_vect
 
void setup ()
{ 
// pin change interrupt (example for pin 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
}

Table of pins -> pin change names / masks

pinpin change names / masks
D0PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8PCINT0 (PCMSK0 / PCIF0 / PCIE0)
D9PCINT1 (PCMSK0 / PCIF0 / PCIE0) <===== PCINT1 代表 pin D9
D10PCINT2 (PCMSK0 / PCIF0 / PCIE0)
D11PCINT3 (PCMSK0 / PCIF0 / PCIE0)
D12PCINT4 (PCMSK0 / PCIF0 / PCIE0)
D13PCINT5 (PCMSK0 / PCIF0 / PCIE0)
A0PCINT8 (PCMSK1 / PCIF1 / PCIE1)
A1PCINT9 (PCMSK1 / PCIF1 / PCIE1)
A2PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Reference: http://gammon.com.au/interrupts

3.2 中断测试

接下来我们来测试两个使用中断处理的按钮 ! (两个按钮按下与放掉都会产生中断),但也可以不必使用真的按钮, 拿一条杜邦线或是一条电导线即可测试了 , (例如把没用的网络线剥开里面有八条电导线可用)
第一个按钮接 pin2 以便使用外部中断 INT0, 因为我们设定 pinMode(2, INPUT_PULLUP); 所以按钮另一端接 GND 即可 !
另一个按钮接 pin 8, (也是用 PULLUP, 且代码已经写成可把 pinPrint 可改为 9, 或 10 都可),

3.2.1代码如下:

// 按钮接 pin 2
// 另一个测试按钮接  pin 8 (pinPrint),  或改 9,  或 10 都可,  pinPrint:
int pinPrint = 8;  // interrupt to print value of cnt; 8/9/10 OK
const byte led = 13;
const byte button = 2;  // pin2 是 INT0 外部中断
volatile int cnt = 0;  // 纪录 button  interrupt 次数
volatile int doPrint = 0;
// Interrupt Service Routine (ISR)
void btnChange ( ) {  // for INT0 to attach
  ++cnt;
  if (digitalRead (button) == HIGH) digitalWrite (led,  HIGH);
  else digitalWrite (led,  LOW);
}  // end of btnChange

void setup ()
{
  pinMode(led,  OUTPUT); digitalWrite(13,  0);
  Serial.begin(9600);
  pinMode(button,  INPUT_PULLUP); //internal pull-up resistor
  setup555( pinPrint );  // set interrupt for pin 8
  attachInterrupt (0,  btnChange,  CHANGE);  //  INT0 == pin 2
  Serial.print("Started... cnt ="); Serial.println(cnt);
}  // end of setup
void loop ()
{
  // loop doing nothing 
  if(doPrint) {
    doPrint = 0;
    Serial.println(String("Interrupt cnt=") +cnt+",  time=" + millis( ) );
  }// if(
  // .. .. ..
} // loop(
void setup555(int pin ) {
  pinMode(pin,  INPUT_PULLUP);  // 启动内部上拉电阻
  cli( );
  switch(pin) {
    case 8: PCMSK0 |= bit (PCINT0);  break; // the pin 8
    case 9: PCMSK0 |= bit (PCINT1);  break; // the pin 9
    case 10: PCMSK0 |= bit (PCINT2);  break; // the pin 10
  }
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  sei( );
}
void haha( ) {
  digitalWrite(led,  0 );
  doPrint = 1;   // 要求 loop ( ) 内要做一次 print
}
ISR (PCINT0_vect) {
  // one of pins D8 to D13 has changed
  haha( );
}

3.2.2 如何测试呢?

(1)把串口监视器 Serial Monitor 开启
(2)拿一根杜邦线或任意电导线, 一端接 GND,
(a)另一端轻轻触一下 pin 8 (由 pinPrint 决定, 目前写成可以 8, 或 9, 或 10, 改 pinPrint 即可 ! )
(b)改轻轻触一下 pin 2 (或按下这按钮)
你会发现按下这按钮或插入 pin 2 时 Led 13 熄灭, 拉出时或放掉按钮时 Led 13 灯亮 !
©再改回轻轻触一下 pin 8
注意串口监视器输出的答案!
发现了没, 只插入一下 pin 2 又拔掉,
结果 cnt 的值就跳好多, 理论上应该是多 2 (按下放掉各加 1),
但是实际却好像发生了很多次甚至一二十次中断 ! 这就是所谓抖动(Bouncing)的问题 !
还有, 插一下 pin 8 却印好几次, 这也是抖动的问题 !
(3)重复刚刚 (2)全部步骤, 注意 LED 13 的亮灭, 以及串口监视器输出的值 !

再来把以上范例改为方便针对监看 pin 2, 3, 4, 5, 6, 7 这六支 pin
你只要对照面范例与对照表就会改为可监看 pin A0 到 A5 了 !
以下我故意把 pinPrint 设 2, 看看会怎样 ? !
注意, pin 2 同时也是 INT0 的外部中断,
就是说以下程序码中 pinPrint 与 button 都是 2, 这样用接 GND 线轻触一下 pin 2,
势必两种中断都会产生, 那先做哪个呢?
其实这之前就说过了 , 在 25 种中断之中, INT0 的优先权是最高的 !


// 按钮接 pin 2
// 另一个测试按钮接  pin 7 (pinPrint),  或 6, 5, 4, 3, 2 都可,  pinPrint:
int pinPrint = 2;  // 可故意改为 2 测试看看
const byte led = 13;
const byte button = 2;  // pin2 是 INT0 外部中断
volatile int cnt = 0;  // 纪录 button  interrupt 次数
volatile int cnt88 = 0; // by haha
volatile int doPrint = 0;
// Interrupt Service Routine (ISR)
void btnChange ( ) {  // for INT0 to attach
  ++cnt;
  //if (digitalRead (button) == HIGH) 
  digitalWrite (led,  HIGH);
  //else digitalWrite (led,  LOW);
}  // end of btnChange

void setup ()
{
  pinMode(led,  OUTPUT); digitalWrite(13,  0);
  Serial.begin(9600);
  pinMode(button,  INPUT_PULLUP); //internal pull-up resistor
  setup555( pinPrint );  // set interrupt for pin 8
  attachInterrupt (0,  btnChange,  FALLING);  //  INT0 == pin 2
  Serial.print("Started... cnt ="); Serial.println(cnt);
}  // end of setup
void loop ()
{
  // loop doing nothing 
  if(doPrint) {
    doPrint = 0;
    Serial.println(String("Interrupt cnt=") +cnt+
    ",  cnt88=" + cnt88 +
    ",  time=" + millis( ) );
  }// if(
  // .. .. ..
} // loop(
void setup555(int pin ) {
  pinMode(pin,  INPUT_PULLUP);  // 启动内部上拉电阻
  cli( );
  switch(pin) {
    case 2: PCMSK2 |= bit (PCINT18);  break; // the pin 2
    case 3: PCMSK2 |= bit (PCINT19);  break; // the pin 3
    case 4: PCMSK2 |= bit (PCINT20);  break; // the pin 4
    case 5: PCMSK2 |= bit (PCINT21);  break; // the pin 5
    case 6: PCMSK2 |= bit (PCINT22);  break; // the pin 6
    case 7: PCMSK2 |= bit (PCINT23);  break; // the pin 7
  }
  PCIFR  |= bit (PCIF2);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE2);   // enable pin change interrupts for D8 to D13
  sei( );
}
void haha( ) {
  cnt88++;
  digitalWrite(led,  0 );
  doPrint = 1;   // 要求 loop ( ) 内要做一次 print
}
ISR (PCINT2_vect) {
  // one of pins D8 to D13 has changed
  haha( );
 }

以上这范例的测试结果也夹档在后面 !
你可以看出 INT0 的中断优先处理, 否则在 loop( )内第一次印出的 cnt 就应该是 0 才对 !
http://www.arduino.cn/forum.php? mod=attachment&aid=MTA0NDh8ZDRlZDg0NTF8MTQyNzQ4MTY3OHwzMDYwMXwxMzIwNQ%3D%3D%C2%AChumb=yes

请注意, 目前为止我们还没用到国外某大神写的 Pin Change Interrupt 的库喔 !

4. 关于 P-C-I 库 : Pin Change Interrupt Library for the Arduino

4.1 安装PinChangeInt库

要使用 Pin Change Interrupt 库, 必须先下载该 P-C-I 库来安装:
抓回 .zip 档案之后, 从你的 Arduino IDE
Sketch > Import Library… > Add Library…
然后选到该 .zip 档案, 把 PCI 库加进去你的 Arduino IDE.
(不必手动解压缩再把PinChangeInt库目录复制到 libraris 目录, 当然你要那么做也可以! )
该 P-C-I 库可到这抓:
https://github.com/GreyGnome/PinChangeInt

或直接点以下连结: (PinChangeInt-master.zip)
https://github.com/GreyGnome/PinChangeInt/archive/master.zip
这个库从以前 Version 1.0 的 4.5KB,
到现在 2.40版已经有 49KB.

如果你只是要在Arduino UNO 上用, 其实抓P-C-I 库的 1.0版的就可以了 ?
不过要注意 1.0版和 1.72版只可用旧的写法:
PCattachInterrupt(pin, yourISRFunction, FALLING); // FALLING, RISING, CHANGE
PCdetachInterrupt(pin);

(在这篇最后夹档 1.0 版, 1.72版, 以及 2.402 版本)

其实, 这个 P-C-I 库所用的方法就是类似上面我写的范例,
只是P-C-I库已经考虑所有的 pin, 但上述我写的范例只有考虑 pin 8, 9, 10 这三支脚位,
且 P-C-I 库设计成仿照外部中断INT0, INT1的使用方法,
可以用类似写法指定监看某 pin 的变化产生中断, 例如:
attachPinChangeInterrupt(pin, ggyy, FALLING);
表示只要 pin 由高电平往低电平下降(FALLING), 就产生中断跳入函数 ggyy( ) 内 !
再次提醒, 关于外部中断INT0, INT1的使用, 以及中断的概念,
还是请看看奈何大神写的这篇有趣文章:
http://www.arduino.cn/thread-2421-1-1.html

接着我们来看看使用该 P-C-I 库的简单范例,
这是我把范例拿来稍微改过,
测试 pin 8 产生中断的次数, 你可以找一个按钮连接 pin 8 (另一端接 GND),
或是拿一条杜邦线或任意电导线, 一端接 GND, 另一端轻触 pin 8
记得要把串口监视器 Serial Monitor 打开观看!


#include <inChangeInt.h>
//  以下用 pin 8,  你可以改为其他 pin
#define testPIN 8
 
volatile unsigned int interruptCount=0; // 最大到 65535,  接着会归零从头算起
 
// 尽量不要在中断处理程序内用 Serial.print()  
// 中断程序不要做太多事!  这里我们只是把中断次数 + 1
void ggyy() {   // 中断处理程序
  interruptCount++;
}
 
// Attach the interrupt in setup()
void setup() {
  pinMode(testPIN,  INPUT_PULLUP);  // 启动内部上拉电阻 the pullup resistor.
  attachPinChangeInterrupt(testPIN,  ggyy,  FALLING);
  Serial.begin(9600);
  Serial.println("start ------");
}
 
void loop() {
  delay(3388);    // Every  3.388 second, 
  Serial.print("Total: ");
  Serial.print(interruptCount,  DEC);   // print the interrupt count.
  Serial.println(" interrupt times so far.");
}

看到了, 很简单吧 ?
就是先写好中断处理函数例如 ggyy( );
然后用类似使用外部中断 INT0 和 INT1 的方法:
attachPinChangeInterrupt(testPIN, ggyy, FALLING);
表示监督 testPIN 的状态,
当 testPIN 由高电平往低电平下降(FALLING)时产生中断, 跳入函数 ggyy( );
你可以把上述例子复制并改变 testPIN 的值为其它 pin 多多测试看看 !

如果你是使用 1.0版本或 1.72版本,
则改用:
PCattachInterrupt(testPIN, ggyy, FALLING);

如何停止某 pin 的中断处理 ?
很简单, 与使用外部中断 INT0, INT1 类似:
detachPinChangeInterrupt(pin);
这样就可以了 !

如果你是使用 1.0版本或 1.72版本,
对某 pin 启动中断用:
PCattachInterrupt(pin, yourFunction, mode);
停止中断用:
PCdetachInterrupt(pin);

更复杂的用法可自己看抓回 P-C-I 库内的范例 {:soso_e100:}

参考:
http://playground.arduino.cc/Main/PinChangeInt
http://playground.arduino.cc/Main/PinChangeIntExample
http://playground.arduino.cc/Code/ReadReceiver

故意测 pin2 的 pin change interrupt 与 INT0 (也是 pin2)
已经更新加入使用 P-C-I Library 的简单范例,

关于Chris J. Kiick和Michael Schwager写的 P-C-I Library:
https://github.com/GreyGnome/PinChangeInt
Alse see:
http://playground.arduino.cc/Main/PinChangeInt
http://playground.arduino.cc/Main/PinChangeIntExample
http://playground.arduino.cc/Code/ReadReceiver

已经顺便把 ver. 1.0, ver 1.72, ver 2.402 夹档案在原贴文内.

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页