Arduino基础篇(三)-- 带你了解Arduino中断的秘密

1 中断的基本概念

        中断装置和中断处理中断处理程序统称为中断系统。中断(Interrupt)是计算机的一个重要概念,现代计算机普遍采用中断技术。

        当计算机执行正常程序时,系统中会出现某些急需处理的异常情况和特殊请求,此时 CPU 会暂时中止现行程序,转去对随机发生的更为紧迫的事件进行处理,处理完毕后,CPU 自动返回原来的程序继续执行,此过程就叫做中断。实现中断功能的硬件和软件统称中断系统。一个完整的中断处理过程包括中断请求、中断响应、中断处理和中断返回

  1. 中断请求 中断过程是从中断源向 CPU 发出中断请求而开始的,其中断请求信号应该至少保持到 CPU 做出响应为止。
  2. 中断响应 CPU检测到中断请求后,在一定的条件和情况下进行响应
  3. 中断处理 CPU响应中断结束后,返回原先被中断的程序并继续执行。
  4. 中断返回 中断返回是指把运行程序从中断服务程序转回到被中断的主程序中。

中断结构如下图所示:

        我们从一个生活中的例子引入。你正在家中看书,突然电话铃响了,你放下书本,去接电话,和来电话的人交谈,然后放下电话,回来继续看你的书。这就是生活中的“中断”的现象,就是正常的工作过程被外部的事件打断了。

        常见的中断有外部中断和时钟中断:

  1. 时钟中断,是指设定CPU内部定时器后,当到达指定时间,将产生中断请求。常用于定时

  2. 外部中断,就是当CPU的外部中断管脚电平变动时,将产生中断请求。 常用于键盘输入、串口通信等

如果没有中断的话,arduino 是一直运行 loop 内的代码,一遍一遍重复运行。当有中断产生时候,单片机会停止 loop 的代码,开始运行中断服务函数的代码,运行一遍中断服务函数后,继续回到 loop 内接着刚才运行的代码运行。


2 外部中断

        外部中断是由外部设备发起请求的中断。要想使用外部中断,就需了解中断引脚的位置,根据外部设备选择中断模式,以及编写一个中断被触发后需执行的中断函数。

2.1 中断引脚与中断编号

        在不同型号的 Arduino 控制器上,中断引脚的位置也不相同,只有中断信号发生在带有外部中断功能的引脚上,Arduino才能捕获到该中断信号并做出响应,下表列举了 Arduino 常见型号控制器的中断引脚所对应的外部中断编号。

Arduino 型号int0 int1 int2 int3 int4 int5
UNO
2
3
MEGA
2
3
21
20
19
18
Lernardo
3
2
0
1
DUE
所有引脚均可使用外部中断
        注:表格中int0、int1等都为外部中断编号。

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 函数时要注意以下几点:

  1. 在中断函数中 delay 函数不能使用。
  2. 使用 millis 函数始终返回进入中断前的值。
  3. 读取串口数据的话,可能会丢失。
  4. 中断函数中使用的变量需要定义为 volatile 型。
    attachInterrupt 函数的函数原型可在文件 WInterrupts.c 中找到,如下所示:

(2)detachInterrupt( interrupt)
        功能:禁用外部中断。
        参数:
        interrupt,需要禁用的中断编号。

(3)interrupts()和nolnterrupts()
        interruptsnoInterrupts 函数在 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
位数
8bit
16bit
8bit
对应引脚
5,6
9,10
3,11
封装函数
delay(),millis()和micors()等
servo库
tone()等
MEGA
位数
8bit
16bit
8bit
16bit
16bit
16bit
对应引脚
4,13
11,12
9,10
2,3,5
6,7,8
46,45,44
封装函数
delay(),millis()和micors()等
tone()等
servo库

        注意:对于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 的寄存器,更多请参考技术文档


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 的预分频系数配置如表:


        现在您可以用以下公式计算中断频率:

中断频率(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 的模式选择配置如表:

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会给你编译报错。


参考

  1. 官方教程:https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
  2. 定时器和中断:https://www.robotshop.com/community/forum/t/arduino-101-timers-and-interrupts/13072
  3. Arduino定时器和中断的使用:https://www.pipipi.net/1030.html
  4. Timer1&Timer3库文件:https://playground.arduino.cc/Code/Timer1/
  5. Timer2库文件:https://playground.arduino.cc/Main/FlexiTimer2/
  • 37
    点赞
  • 222
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

长路漫漫2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值