本文隶属于AVR单片机教程系列。html
中断,是单片机的精华。git
当一个事件发生时,CPU会中止当前执行的代码,转而处理这个事件,这就是一个中断。触发中断的事件成为中断源,处理事件的函数称为中断服务程序(ISR)。ide
中断在单片机开发中有着举足轻重的地位——没有中断,不少功能就没法实现。好比,在程序干别的事时接受UART总线上的输入,而uart_scan_char
等函数只会接收调用该函数后的输入,先前的则会被忽略。利用中断,咱们能够在每次接受到一个字节输入时把数据存放到缓冲区中,程序能够从缓冲区中读取已经接收的数据。函数
AVR单片机支持多种中断,包括外部引脚中断、定时器中断、总线中断等。每个中断被触发时,经过中断向量表跳转到对应ISR。若是一个中断对应的ISR不存在,连接器会把复位地址放在那里,若是这个中断被响应程序就会复位(但单片机不会复位)。ui
那么,咱们之前从未写过ISR,但常常改变引脚电平,为何没有复位呢?由于中断默认是不开启的。要启用一个中断,须要让两个位于不一样寄存器中的位为1
,一个是中断对应的中断使能位,每一个中断都有各自的位,另外一个是全局中断使能位,位于寄存器SREG
中,不能直接存取,须要经过定义在<avr/interrupt.h>
头文件中的sei()
函数开全局中断,相对地,cli()
用于关全局中断。spa
先来写第一个带中断的程序吧。从原理图中能够看到,PB2
旁边标明了INT2
,表示PB2
引脚可用于外部中断2。把一个按键链接到PB2
引脚上,即开发板最下方的7P排母的最右边。利用中断,咱们实现每按一次按键就翻转LED状态的功能。操作系统
#include <avr/io.h> #include <avr/interrupt.h> int main() { PORTB |= 1 << PORTB2; EICRA |= 0b10 << ISC20; EIMSK |= 1 << INT2; DDRC |= 1 << DDC4; sei(); while (1) ; } ISR(INT2_vect) { PORTC ^= 1 << PORTC4; }
ISC21:0
两位指定外部中断的类型,这里设置为降低沿,即按键按下时触发;INT2
位使能外部中断2;所有初始化完成后,sei()
启用全局中断,而后单片机就会相应按键按下的事件了。code
ISR(INT2_vect)
指示这个函数是外部中断2的ISR。每一个中断ISR都有本身的名字,由数据手册12章Source
一栏的内容加上_vect
组成,这个名字能够当成函数名字来使用。htm
若是多个中断同时触发,单片机会先响应优先级高的。一些单片机支持自定义的优先级,但在AVR单片机中,只有简单的地址低的优先级高的规则。blog
中断能够被中断吗?在AVR单片机中,执行一个中断处理函数会自动地关闭全局中断,此时程序不会被中断,但能够手动地sei()
使中断能够被处理。程序是否相应中断仅取决于该中断是否被启用,与其优先级无关。
固然,中断不是完美的。其一,你也许已经发现上面的程序不能很好的工做,有时候明明按下了按键,灯却一闪就灭。这是由于,按键存在抖动,比单片机时钟周期长,能触发多个中断。之前把button_down()
放在main
函数的while
循环里时就没有这个问题,正是循环中的delay
滤除了这种抖动。
其二,进入和退出中断,除了须要CPU几个周期来改变PC(程序计数器,当前执行指令的地址)外,还须要保护和恢复现场,包括SREG
寄存器与ISR中用到的通用寄存器。下面这段汇编代码能够在Solution Explorer
中Output Files\xxx.lss
中找到。
00000094 <__vector_3>: #include <avr/io.h> #include <avr/interrupt.h> ISR(INT2_vect) { 94: 1f 92 push r1 96: 0f 92 push r0 98: 0f b6 in r0, 0x3f ; 63 9a: 0f 92 push r0 9c: 11 24 eor r1, r1 9e: 8f 93 push r24 a0: 9f 93 push r25 PORTC ^= 1 << PORTC4; a2: 98 b1 in r25, 0x08 ; 8 a4: 80 e1 ldi r24, 0x10 ; 16 a6: 89 27 eor r24, r25 a8: 88 b9 out 0x08, r24 ; 8 } aa: 9f 91 pop r25 ac: 8f 91 pop r24 ae: 0f 90 pop r0 b0: 0f be out 0x3f, r0 ; 63 b2: 0f 90 pop r0 b4: 1f 90 pop r1 b6: 18 95 reti
这段代码没必要理解,更不用会写。94
到a0
行是保护现场,依次将寄存器r1
、r0
、SREG
(即0x3f
)、r24
和r25
push进栈,把r1
清零,一共用了12个周期,还要加上响应中断的4个周期;a2
到a8
是恢复现场,把这些寄存器原来的值逆序地从栈上pop出来,用了15个周期;而只有中间aa
到b6
的语句是用于执行用户代码的,在总共35个周期中只占4个周期。
固然,这个比例很小是由于这个ISR过于简单。可是,ISR更复杂也意味着有更多寄存器须要push和pop,中断的响应时间更长。
这个例子并无中断效率低下的意思,而是代表不能过于频繁地依赖中断。好比接下来要讲的定时器中断,我一般设置为1ms间隔,只有一次到0.1ms,再快恐怕就起不到定时的做用了。
定时器,顾名思义,定时用的。以前咱们在main
函数的while (1)
循环中,每一个周期执行一些代码,而后延时一个固定的时长。我也曾见过根据该次周期的工做量来计算延时时长的操做,但毕竟写BASIC的人学得也basic吧,这种作法的定时仍不精确。利用定时器中断(其实没必要中断),咱们能够实现精确的定时,使每一周期的时间严格相同。
若是对操做系统有一点了解,就会知道操做系统须要进行任务调度。然而,任务在执行时,并不知道本身该什么时候被调度走。实际上,是操做系统在定时器中断中打断了任务的正常执行,而后进行调度。定时器中断是操做系统的基础。
在AVR单片机定时器的各类模式中,普通模式和CTC模式经常使用于产生定时器中断。咱们仍然以定时/计数器0为例。
在普通模式中,使用TIMER0_OVF
中断,频率为\(\frac {f_{CPU}} {256 \cdot N}\),\(N\)为分频系数。这样产生的定时器中断精确但不确切,由于N
的取值是很离散的。若是只须要在中断中进行外设轮询的话,普通模式就足够了。
若是在ISR的第一行就给TCNT0
赋值,或是使用TIMER0_COMPA
中断并在起始处写TCNT0 = 0
,那么能够改变中断频率,但因为有编译器插入的保护现场的代码的存在,这种定时不够精确,而CTC模式解决了这个问题。
在CTC模式中,使用TIMER0_COMPA
中断,频率精确地为\(\frac {f_{CPU}} {N \cdot (OCR0A + 1)}\)(注意没有蜂鸣器频率公式中的\(2\))。
还须要提醒一句,若是想要中断被响应,必须保证main
函数不退出,由于编译器会在退出处加上一句cli()
。最简单的方法是在main
函数的最后加上一句while (1);
。
数码管的动态扫描须要每隔一段时间就换一位点亮是一件很烦人的事,尤为是在操控其余外设的程序已经比较复杂的时候。我原本想把中断完美地拖到第二期再讲,没想到本身也受不了动态扫描的折磨,在某个版本的库中就放出了segment_auto
函数来接管这项工做。它正是使用了定时器中断。
实现思路很简单,把要显示的数据放在客户和库能够共同取用的变量中,在中断里逐位显示,只要中断够快,就能够实现动态扫描,使每一位看起来都在亮。
#include <avr/io.h> #include <avr/interrupt.h> #include <ee1/segment.h> void segment_int_init() { // other initializations, ex. pins TCCR0A = 0b10 << WGM00; // CTC mode TCCR0B = 0b0 << WGM02 | 0b100 << CS00; // divide by 256 OCR0A = 97; // ~1ms TIMSK0 = 1 << OCIE0A; // compare match A interrupt sei(); } static uint8_t segment_int_data[SEGMENT_DIGIT_COUNT]; void segment_int_display(/* ... */) { // store the display pattern in segment_int_data } ISR(TIMER0_COMPA_vect) { static uint8_t cur = 0; // display the cur-th digit according to segment_int_data if (++cur == SEGMENT_DIGIT_COUNT) cur = 0; }
若是你把以上代码放在可执行程序的项目中,那彻底没有问题,但若是是放在一个静态库项目中,而后在可执行程序项目中引用它,那么定时器中断的ISR是不会连接进程序的。这是由于,从连接器的角度来说,这个ISR历来没有被调用过,所以就被当成无用的函数扔掉了。为了让连接器把ISR连接进程序,咱们须要在main
会执行的代码中调用它,最简单地:
if (0) TIMER0_COMPA_vect();
放在初始化中,既达到了目的,又没有运行时的负担。
试着写一个库,管理开发板引出的16个引脚的外部中断。
研究定时器中断与PWM的关系。
改进ADC一讲中最后一个例程,把main
函数还给客户。