中断是在程序运行中经常用到的功能,用于处理一下实时性比较高的事件,首先来了解一下中断的概念。
中断
当出现需要及时处理的事件(中断请求)时,CPU暂停当前工作的执行转而处理应急事件(中断)的过程。即在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂停当前程序去处理这个新事件的过程就就是中断。
举个栗子:你正在看着一本Arduino程序设计的书,看到95页时肚子饿了,你记下来页码跑去吃东西,吃完东西后继续从书的95页往下看;这里面看书就类似CPU在正常工作,肚子饿了吃东西就像处理中断,中断事件处理(吃东西)结束后(CPU)又从停下来的地方继续运行(看书)。
Arduino外部中断
外部中断是单片机实时地处理外部事件的一种内部机制。当某种外部事件发生时,单片机的中断系统将迫使CPU暂停正在执行的程序,转而去进行中断事件的处理;中断处理完毕后.又返回被中断的程序处,继续执行下去。
大多数的Arduino板至少拥有两个外部中断引脚:0号中断(引脚2)和1号中断(引脚3)。不同的Arduino控制板的中断引脚还有所区别,如下表所示。
Arduino控制板 | int0 | int1 | int2 | int3 | int4 | int5 |
---|---|---|---|---|---|---|
Uno,Ethernet | 2 | 3 | X | X | X | X |
Mega | 2 | 3 | 21 | 20 | 19 | 18 |
Leonardo | 3 | 2 | 0 | 1 | X | X |
Due | 所 | 有 | IO | 口 | 均 | 可 |
表中int表示Arduino板的中断号对应的引脚,X
则表示该板没有对应的中断引脚, 在所有的Arduino控制板中,Arduino Due比较强大的中断功能,允许在所有IO脚触发外部中断。
Arduino编程中的中断均已函数调用的形式来配置及使用,相对来说比较简单,下面是外部中断的常用函数。
1、中断函数
attachInterrupt(interrupt,function,mode)
描述:当发生外部中断时,调用一个指定的函数。在程序中再次调用时可以指定新的中断调用函数;如在setup函数中初始化函数A为中断0的中断调用函数,再次调用 attachInterrupt
函数时,可以指定函数B为中断0的中断调用函数,在程序中有需要可多次调用更新中断函数。
语法:attachInterrupt(interrupt,function,mode)
attachInterrupt(pin,function,mode)(Due专用)
参数:
interrupt,中断编号;pin,引脚编号(Due专用);
function:中断发生时调用的函数,此函数必须不带任何参数,不返回任何值。
mode:定义什么情况下触发中断,以下四个常数为mode的有效值:
- LOW: 当引脚为低电平时,触发中断;
- CHANGE:当引脚电平发生变化时,触发中断;
- RISING:当引脚电平由低变高时,触发中断(上升沿中断);
- FALLING:当引脚电平由高变低时,触发中断(下降沿中断);
对于Due而言,多了一个专用参数 – HIGH,即当引脚为高电平时,触发中断;
注意:不要妄想在中断函数中加延时,在Arduino中断函数中,delay()不会生效,millis()不会持续累加;当中断发生时,串口数据可能会出现丢包;在中断函数里面使用到的全局变量应该声明为volatile变量。
示例:
const byte ledPin = 13;
const byte interruptpin = 2;
volatile byte state = LOW;
void setup(void)
{
pinMode(ledPin,OUTPUT);
pinMode(interruptPin,INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(interruptPin),blink,CHANGE);
}
void loop()
{
digitalWrite(ledPin,state);
}
void blink()
{
state = !state;
}
detachInterrupt(interrupt)
描述:关闭对应的中断。
参数:interrupt,禁用中断的编号(0或1)。
2、中断使能函数
interrupts()
描述:启用中断/重新启用中断(在被禁用中断过后)。
noInterrupts()
描述:禁用中断。在程序中如果有一些函数的运行不希望被中断打断,可以调用noInterrupts函数来禁用中断的发生,再配合interrupts函数恢复中断使能。
示例:
void setup(){
}
void loop(){
...
noInterrupts();
//放置关键函数(不想被打断)
interrupts();
//放置其他函数
...
}
Arduino定时器中断
外部中断是通过检测输入电平的变化,而产生中断信号。除了外部中断方式外,Arduino控制板还可以按时间变化产生中断,这里使用到定时器(Timer),而对应产生的中断被称为定时器中断。
定时器是嵌入式系统中的一个特殊的计数器。它可以对分频后时钟信号的进行计数,当计数值达到设定值,即会产生定时器中断。且通过时钟频率和计数值可以计算出时间,所以可以达到以时间触发中断的效果。
即当需要按一定的时间间隔执行某个操作时,就需要用到定时器中断了。
以UNO为例,一共有3个定时器可以使用:Timer0,Timer1,Timer2。
每个定时器都有自己的函数库,通过使用硬件内部计时器中断来实现中断效果,下面以Timer1库的程序为例说明~
首先需要安装TimeOne库,也就是Timer1的对应库。下面提供两种安装方式,可以点击链接跳到对应的安装教程:
第一种:Arduino IDE安装TimerOne库
第二种:Sublime Text3环境安装TimerOne库
安装完成后,大概了解一下Timer1的库函数,打开.h文件TimerOne.h,除了包含 Arduino.h
外,还有一个宏定义声明了Timer1是一个16位定时器。
#define TIMER1_RESOLUTION 65536UL // Timer1 is 16 bit
随后在TimerOne类中定义了定时器相关函数,在配置函数中包含了初始化函数initialize()
函数,参数默认为1000000毫秒,也就是1秒;setPeriod()
函数较长就不发出来了,主要作用是设置定时器的触发周期,在initialize()
函数中调用。
//****************************
// Configuration
//****************************
void initialize(unsigned long microseconds=1000000) __attribute__((always_inline)) {
TCCR1B = _BV(WGM13); // set mode as phase and frequency correct pwm, stop the timer
TCCR1A = 0; // clear control register A
setPeriod(microseconds);
}
void setPeriod(unsigned long microseconds) __attribute__((always_inline)) {
...
}
定时器状态控制函数
//****************************
// Run Control
//****************************
void start() __attribute__((always_inline)) {
TCCR1B = 0;
TCNT1 = 0; // TODO: does this cause an undesired interrupt?
resume();
}
void stop() __attribute__((always_inline)) {
TCCR1B = _BV(WGM13);
}
void restart() __attribute__((always_inline)) {
start();
}
void resume() __attribute__((always_inline)) {
TCCR1B = _BV(WGM13) | clockSelectBits;
}
PWM控制相关函数(函数体省略)
//****************************
// PWM outputs
//****************************
void setPwmDuty(char pin, unsigned int duty) __attribute__((always_inline)) {
...
}
void pwm(char pin, unsigned int duty) __attribute__((always_inline)) {
...
}
void pwm(char pin, unsigned int duty, unsigned long microseconds) __attribute__((always_inline)) {
...
}
void disablePwm(char pin) __attribute__((always_inline)) {
...
}
最重要的来了,定时器中断相关函数
//****************************
// Interrupt Function
//****************************
void attachInterrupt(void (*isr)()) __attribute__((always_inline)) {
isrCallback = isr;
TIMSK1 = _BV(TOIE1);
}
void attachInterrupt(void (*isr)(), unsigned long microseconds) __attribute__((always_inline)) {
if(microseconds > 0) setPeriod(microseconds);
attachInterrupt(isr);
}
void detachInterrupt() __attribute__((always_inline)) {
TIMSK1 = 0;
}
大概了解过定时器相关函数后,可以开始编写定时器中断程序了。
1、添加TimerOne头文件(安装库之后);
2、初始化定时器,开启定时器中断;
3、编写定时器中断操作;
简单程序如下
#include "TimerOne.h"
//定时器中断函数
void Timer1Interrupt()
{
digitalWrite(13,digitalRead(13)^1); //控制LED闪烁
}
void setup()
{
pinMode(13,OUTPUT); //设置LED引脚为输出
Timer1.initialize(500000); //Timer1每500毫秒触发一次
Timer1.attachInterrupt(Timer1Interrupt); //配置定时器中断函数,开启中断
}
void loop(){}
使用定时器中断可以很好的解决Arduino的单任务机制,但是UNO的3个定时器资源同样跟引脚PWM会有冲突,比如Timer0定时器负责 millis()函数
和 delay()函数
的计时工作,同时控制着数字引脚 pin5
,pin6
的PWM功能;Timer1定时器负责数字引脚 pin9
、pin10
的PWM功能,也影响 Servo.h库
的使用;Timer2定时器负责数字引脚 pin11
,pin3
的PWM功能,也和蜂鸣器的 tone()
函数有关。
综上所述,定时器的使用会影响对应引脚的PWM功能,和某些时序函数的运行,比如Timer0负责delay()函数延时操作的,使用pin5、pin6的PWM输出可能会有些微误差,像analogWrite(5,0)和analogWrite(5,6)可能是一样的效果
除了对应的硬件定时器,还可以用Timer库和SimpleTimer库来实现定时效果,这两个库不用到硬件定时器,也是用到 millis()函数
来实现延时,所以对 delay函数
也是会有点影响。
串口中断(没这回事儿)
关于Arduino串口中断的也是找了一段时间没结果,后来发现了Arduino没有串口中断这回事,但是却机缘巧合的在main函数中找到了串口的端倪。
以下是Arduino中main函数的内容~
int main(void)
{
init();
initVariant();
#if defined(USBCON)
USBDevice.attach();
#endif
setup();
for (;;) {
loop();
if (serialEventRun) serialEventRun();
}
return 0;
}
即是Arduino没有串口中断,但是却用for循环来检测串口事件,与loop函数在同个循环中,相当于loop函数运行一次顺带检测一次串口事件。