文章目录
1 中断的基本概念
中断装置和中断处理中断处理程序统称为中断系统。中断(Interrupt)是计算机的一个重要概念,现代计算机普遍采用中断技术。
当计算机执行正常程序时,系统中会出现某些急需处理的异常情况和特殊请求,此时 CPU 会暂时中止现行程序,转去对随机发生的更为紧迫的事件进行处理,处理完毕后,CPU 自动返回原来的程序继续执行,此过程就叫做中断。实现中断功能的硬件和软件统称中断系统。一个完整的中断处理过程包括中断请求、中断响应、中断处理和中断返回。
- 中断请求 中断过程是从中断源向 CPU 发出中断请求而开始的,其中断请求信号应该至少保持到 CPU 做出响应为止。
- 中断响应 CPU检测到中断请求后,在一定的条件和情况下进行响应。
- 中断处理 CPU响应中断结束后,返回原先被中断的程序并继续执行。
- 中断返回 中断返回是指把运行程序从中断服务程序转回到被中断的主程序中。
中断结构如下图所示:
我们从一个生活中的例子引入。你正在家中看书,突然电话铃响了,你放下书本,去接电话,和来电话的人交谈,然后放下电话,回来继续看你的书。这就是生活中的“中断”的现象,就是正常的工作过程被外部的事件打断了。
常见的中断有外部中断和时钟中断:
-
时钟中断,是指设定CPU内部定时器后,当到达指定时间,将产生中断请求。常用于定时。
-
外部中断,就是当CPU的外部中断管脚电平变动时,将产生中断请求。 常用于键盘输入、串口通信等。
如果没有中断的话,arduino 是一直运行 loop 内的代码,一遍一遍重复运行。当有中断产生时候,单片机会停止 loop 的代码,开始运行中断服务函数的代码,运行一遍中断服务函数后,继续回到 loop 内接着刚才运行的代码运行。
2 外部中断
外部中断是由外部设备发起请求的中断。要想使用外部中断,就需了解中断引脚的位置,根据外部设备选择中断模式,以及编写一个中断被触发后需执行的中断函数。
2.1 中断引脚与中断编号
在不同型号的 Arduino 控制器上,中断引脚的位置也不相同,只有中断信号发生在带有外部中断功能的引脚上,Arduino才能捕获到该中断信号并做出响应,下表列举了 Arduino 常见型号控制器的中断引脚所对应的外部中断编号。
Arduino 型号 | int0 | int1 | int2 | int3 | int4 | int5 |
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.2 中断模式
为了设置中断模式,还需要了解设备触发外部中断的输入信号类型。中断模式也就是中断触发的方式,在大多数 Arduino 上支持下表中的四种中断触发方式。
模式名称 | 说明 |
---|---|
LOW | 低电平出发 |
CHANGE | 电平变化出发,即由高变低、由低变高 |
RISING | 上升沿触发,即低电平变高电平 |
FALLING | 下降沿触发,即高电平变低电平 |
在 Arduino Due 中,还可以使用高电平(HIGH)来触发中断,另外 Arduino Due 上的每一个 I/O 口都可以触发中断,其中断编号便是引脚编号。
2.3 中断函数
除了设置中断模式外,还需要编写一个响应中断的处理程序——中断函数,当中断被触发后,便可以让Arduino运行该中断函数。中断函数就是当中断被触发后要去执行的函数,该函数不能带有任何参数,且返回类型为空,如:
void counter()
{
count++;
}
当中断被触发后,Arduino 便会执行该函数中的语句。
这些准备工作完成后,还需要在 setup()
中使用 attachInterrupt()
函数对中断引脚进行初始化配置,以开启 Arduino 的外部中断功能,其用法如下:
(1)attachInterrupt( interrupt,function,mode)
功能:对中断引脚进行初始化配置。
参数:
interrupt,中断编号,注意,这里的中断编号并不是引脚编号。
function,中断函数名,当中断被触发后即会运行此函数名称所代表的中断函数。
mode,中断模式。
attachInterrupt(0,counter,RISING);
如果使用的是 Arduino UNO 或者 MEGA 控制器,则该语句即会开启2号引脚(中断编号0)上的外部中断功能,并指定上升沿时触发该中断。当2号引脚上的电平由低变高后,该中断会被触发,而 Arduino 即会运行 counter()
函数中的语句。
如果不需要使用外部中断了,则可以使用中断分离函数 detachInterrupt()
来关闭中断功能。
在使用 attachInterrupt
函数时要注意以下几点:
- 在中断函数中
delay
函数不能使用。 - 使用
millis
函数始终返回进入中断前的值。 - 读取串口数据的话,可能会丢失。
- 中断函数中使用的变量需要定义为
volatile
型。
attachInterrupt
函数的函数原型可在文件WInterrupts.c
中找到,如下所示:
(2)detachInterrupt( interrupt)
功能:禁用外部中断。
参数:
interrupt,需要禁用的中断编号。
(3)interrupts()和nolnterrupts()
interrupts
和 noInterrupts
函数在 Arduino 中负责打开和关闭总中断,函数无返回值,无参数,可以在文件 wiring.h
中查看函数原型,如下:
#define interrupts() sei()
#define noInterrupts() cli()
3 定时器中断
3.1 定时器的作用
定时器对于单片机来说就类似我们现实生活中的时钟,记录很多和时间相关的事件。在我们平时经常使用的 delay()
millis()
,micros()
,delayMicroseconds()
,PWM 波生成的 analogWrite()
和 tone()
函数都是通过定时器实现的,不过这些都被 Arduino 的封装库隐藏起来了,为了让使用者更快更便捷地开发项目。
我们平常使用的 Arduino 单片机为 UNO,NANO和MEGA 2560。UNO 和 NANO 都使用的是 ATmega328 芯片,这款芯片有3个定时器,Timer0,Timer1,Timer2,其中Timer0和Timer2都是8位寄存器(256),Timer1是16位寄存器(65536),意味着更高的分辨率。mege2560 使用的是 ATmege2560 芯片,这款芯片有 6 个定时器,在328 的基础上,增加了 Timer3,Timer4,Timer5。这三个定时器都是16位的寄存器。
Arduino 型号 | 参数 | Timer0 | Timer1 | Timer2 | Timer3 | Timer4 | Timer5 |
---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
|
注意:对于UNO开发板,引脚5和6的 PWM 功能输出时将产生高于预期的占空比,这是因为 millis()
和 delay()
函数共用一个内部定时器,使得内部计时器在处理PWM 时候分心,这种情况一般出现在低占空比时。MEGA 2560以此类比。
3.2 定时器的基本概念
1、寄存器
寄存器列表如下,x代表0,1,2,3,4,5这6种定时器。
寄存器 | 作用 |
---|---|
TCCRx | 定时器/计数器控制寄存器;预分频器可以在这里配置 |
TCNTx | 定时器/计数器寄存器 |
OCRx | 输出比较寄存器 |
ICRx | 输入捕捉寄存器(仅适用于16位定时器) |
TIMSKx | 定时器/计数器中断屏蔽寄存器;启用/禁用定时器中断 |
TIFRx | 定时器/计数器中断标志寄存器;表示挂起的定时器中断 |
这里给个 Timer0 的寄存器,更多请参考技术文档。
![](https://i-blog.csdnimg.cn/blog_migrate/e4b640ee0c4fe559ce11672476515cf0.png#pic_center)
![](https://i-blog.csdnimg.cn/blog_migrate/9733daf4698bc8d2c3e7226a8bd5c259.png#pic_center)
2、预分频系数与比较匹配器
Arduino UNO 时钟以16MHz运行。计数器的一个刻度值表示 1 / 16,000,000秒(~63ns),跑完1s需要计数值16,000,000。
(1)Timer0 和 Timer2 是8位定时器,可以存储最大计数器值255。
(2)Timer1 是一个16位定时器,可以存储最大计数器值65535。
一旦计数器达到其最大值,它将回到零(这称为溢出)。因此,需要对时钟频率进行分频处理,即预分频器。通过预分频器控制定时计数器的增量速度。预分频器与定时器的计数速度如下:
定时器速度(HZ) = Arduino UNO时钟速度(16MHz) / 预分频器系数
因此,1预分频器将以16MHz递增计数器,8预分频器将在2MHz递增,64预分频器= 250kHz,依此类推。
定时器 Timer0 的预分频系数配置如表:
![](https://i-blog.csdnimg.cn/blog_migrate/df3828c46193cc4cbdd6ca6c48ed3205.png#pic_center)
现在您可以用以下公式计算中断频率:
中断频率(Hz)=(Arduino时钟速度16MHz)/(预分频器*(比较匹配寄存器+ 1))
重新排列上面的等式,给出你想要的中断频率,你可以求解比较匹配寄存器值:
比较匹配寄存器= [16,000,000Hz /(预分频器*所需的中断频率)] - 1
当你使用定时器0和2时,这个数字必须小于256,对于timer1小于65536。
所以如果你想每秒一次中断(频率为1Hz):
比较匹配寄存器= [16,000,000 /(预分频器 * 1)] -1
预分频器为1024,你得到:
比较匹配寄存器= [16,000,000 /(1024 * 1)] -1 = 15,624
因为256 <15,624 <65,536,你必须使用timer1来实现这个中断。
3、定时器模式
定时器可以配置为不同的模式。
(1)PWM模式。 纸浆宽度调制模式。 OCxy输出用于生成PWM信号
(2)CTC模式。 比较匹配时清除计时器。 当定时器计数器到达比较匹配寄存器时,定时器将被清除。
定时器 Timer0 的模式选择配置如表:
![](https://i-blog.csdnimg.cn/blog_migrate/c859cd69c19e6add4492141e7cc4b421.png#pic_center)
3.3 定时器的配置
请注意不同型号板子对应配置不同,详细配置请看技术文档。
int toggle0,toggle1,toggle2;
void setup(){
cli();关闭全局中断
//设置定时器0为10kHz(100us)
TCCR0A = 0;//将整个TCCR0A寄存器设置为0
TCCR0B = 0;//将整个TCCR0B寄存器设置为0
TCNT0 = 0;//将计数器值初始化为0
//设置计数器为10kHZ,即100us
OCR0A = 24;//比较匹配寄存器= [16,000,000Hz /(预分频器*所需中断频率)] - 1
//比较匹配寄存器=24,中断间隔=100us即中断频率10khz
TCCR0A |= (1 << WGM01);//打开CTC模式
TCCR0B |= (1 << CS01) | (1 << CS00); //设置CS01位为1,CS00位为1(64倍预分频)
TIMSK0 |= (1 << OCIE0A);//启用定时器比较中断
//设置定时器1为1kHz
TCCR1A = 0;//将整个TCCR1A寄存器设置为0
TCCR1B = 0;//将整个TCCR1B寄存器设置为0
TCNT1 = 0;//将计数器值初始化为0
//设置计数器为1kHZ,即1ms
OCR1A = 1999;// = (16*10^6)/(1000*8) - 1 (must be <65536)
TCCR1B |= (1 << WGM12);//打开CTC模式
TCCR1B |= (1 << CS11);//设置CS11位为1(8倍预分频)
TIMSK1 |= (1 << OCIE1A);
//设置定时器2为8kHz
TCCR2A = 0;// set entire TCCR2A register to 0
TCCR2B = 0;// same for TCCR2B
TCNT2 = 0;//initialize counter value to 0
// set compare match register for 8khz increments
OCR2A = 249;// = (16*10^6) / (8000*8) - 1 (must be <256)
// turn on CTC mode
TCCR2A |= (1 << WGM21);//打开CTC模式
// Set CS21 bit for 8 prescaler
TCCR2B |= (1 << CS21);
// enable timer compare interrupt
TIMSK2 |= (1 << OCIE2A);
sei();//打开全局中断
}
//中断0服务函数
ISR(TIMER0_COMPA_vect){// timer0中断2Hz切换引脚13(LED)
//产生频率为10kHz / 2 = 5kHz的脉冲波
if(toggle0){
digitalWrite(8,HIGH);
toggle0 = 0;
}
else{
digitalWrite(8,LOW);
toggle0 = 1;
}
}
ISR(TIMER1_COMPA_vect){// timer1中断2Hz切换引脚13(LED)
//产生频率为2Hz / 2 = 1Hz的脉冲波
if(toggle1>=500)
digitalWrite(13,HIGH);
if(toggle1<=500)
digitalWrite(13,LOW);
toggle1 += 1;
if(toggle1 >= 1000)
toggle1 = 0;
}
ISR(TIMER2_COMPA_vect){// timer2中断8kHz切换引脚9
//产生频率为8kHz / 2 = 4kHz的脉冲波
if(toggle2){
digitalWrite(9,HIGH);
toggle2 = 0;
}
else{
digitalWrite(9,LOW);
toggle2 = 1;
}
}
//loop function
void loop(){
}
3.4 定时器中断的使用
使用定时器中断前,必须先安装对应的 Timer
库,然后导入到 Arduino 的库文件里,并在程序中引用头文件 Timer.h
。
实例:
// 秒切换一次引脚13的电平
// 包含定时器库的头文件
#include <FlexiTimer2.h>
// 中断服务程序
void flash() {
static boolean output = HIGH;
digitalWrite(13, output);
output = !output;
}
void setup() {
pinMode(13, OUTPUT);
FlexiTimer2::set(500, flash); // 中断设置函数,每 500ms 进入一次中断
FlexiTimer2::start(); // 开始计时
}
void loop() {
}
其他 Timer 的使用基本一样,只要找到合适的库函数就能够使用,注意使用的定时器不要和你已使用的封装函数冲突,比如对于 UNO 来说,你在使用 Servo.h
的时候,就不能再使用 timer1 了,此时IDE会给你编译报错。
参考
- 官方教程:https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
- 定时器和中断:https://www.robotshop.com/community/forum/t/arduino-101-timers-and-interrupts/13072
- Arduino定时器和中断的使用:https://www.pipipi.net/1030.html
- Timer1&Timer3库文件:https://playground.arduino.cc/Code/Timer1/
- Timer2库文件:https://playground.arduino.cc/Main/FlexiTimer2/