本文隶属于AVR单片机教程系列。

 

中断,是单片机的精华。

中断基础

当一个事件发生时,CPU会停止当前执行的代码,转而处理这个事件,这就是一个中断。触发中断的事件成为中断源,处理事件的函数称为中断服务程序(ISR)。

中断在单片机开发中有着举足轻重的地位——没有中断,很多功能就无法实现。比如,在程序干别的事时接受UART总线上的输入,而uart_scan_char等函数只会接收调用该函数后的输入,先前的则会被忽略。利用中断,我们可以在每次接受到一个字节输入时把数据存放到缓冲区中,程序可以从缓冲区中读取已经接收的数据。

AVR单片机支持多种中断,包括外部引脚中断、定时器中断、总线中断等。每一个中断被触发时,通过中断向量表跳转到对应ISR。如果一个中断对应的ISR不存在,链接器会把复位地址放在那里,如果这个中断被响应程序就会复位(但单片机不会复位)。

那么,我们以前从未写过ISR,但经常改变引脚电平,为什么没有复位呢?因为中断默认是不开启的。要启用一个中断,需要让两个位于不同寄存器中的位为1,一个是中断对应的中断使能位,每个中断都有各自的位,另一个是全局中断使能位,位于寄存器SREG中,不能直接存取,需要通过定义在<avr/interrupt.h>头文件中的sei()函数开全局中断,相对地,cli()用于关全局中断。

先来写第一个带中断的程序吧。从原理图中可以看到,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()启用全局中断,然后单片机就会相应按键按下的事件了。

ISR(INT2_vect)指示这个函数是外部中断2的ISR。每个中断ISR都有自己的名字,由数据手册12章Source一栏的内容加上_vect组成,这个名字可以当成函数名字来使用。

如果多个中断同时触发,单片机会先响应优先级高的。一些单片机支持自定义的优先级,但在AVR单片机中,只有简单的地址低的优先级高的规则。

中断可以被中断吗?在AVR单片机中,执行一个中断处理函数会自动地关闭全局中断,此时程序不会被中断,但可以手动地sei()使中断可以被处理。程序是否相应中断仅取决于该中断是否被启用,与其优先级无关。

当然,中断不是完美的。其一,你也许已经发现上面的程序不能很好的工作,有时候明明按下了按键,灯却一闪就灭。这是因为,按键存在抖动,比单片机时钟周期长,能触发多个中断。以前把button_down()放在main函数的while循环里时就没有这个问题,正是循环中的delay滤除了这种抖动。

其二,进入和退出中断,除了需要CPU几个周期来改变PC(程序计数器,当前执行指令的地址)外,还需要保护和恢复现场,包括SREG寄存器与ISR中用到的通用寄存器。下面这段汇编代码可以在Solution ExplorerOutput 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

这段代码不必理解,更不用会写。94a0行是保护现场,依次将寄存器r1r0SREG(即0x3f)、r24r25push进栈,把r1清零,一共用了12个周期,还要加上响应中断的4个周期;a2a8是恢复现场,把这些寄存器原来的值逆序地从栈上pop出来,用了15个周期;而只有中间aab6的语句是用于执行用户代码的,在总共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();

放在初始化中,既达到了目的,又没有运行时的负担。

作业

  1. 试着写一个库,管理开发板引出的16个引脚的外部中断。

  2. 研究定时器中断与PWM的关系。

  3. 改进ADC一讲中最后一个例程,把main函数还给客户。

AVR单片机教程——定时器中断的更多相关文章

  1. 打打基础,回头看看avr单片机的定时器、中断和PWM(转)

    以前小看了定时器,发现这东西还真的很讲究,那先复习复习吧. 先提提中断:我的理解就是cpu执行时,遇到中断——根据对应的中断源(硬件或软件)——pc定位中断入口地址,然后根据这里的函数指针——跳转到相 ...

  2. AVR单片机教程——ADC

    ADC 计算机的世界是0和1的.单片机可以通过读取0和1来确定按键状态,也可以输出0和1来控制LED.即使是看起来不太0和1的PWM,好像可以输出0到5V之间的电压一样,达到0和1之间的效果,但本质上 ...

  3. AVR单片机教程——蜂鸣器

    本文隶属于AVR单片机教程系列.   引子 定时/计数器(简称定时器)是单片机编程中至关重要的一部分,再简单的单片机也会带有定时器. 也许你会觉得我们已经在delay函数中接触过定时器了,然而并不是, ...

  4. AVR单片机教程——小结

    本文隶属于AVR单片机教程系列.   第一期挺让我失望的,是我太菜,没有把想讲的都讲出来.经常写了很多,然后一点一点删掉,最后就没多少了. 而且感觉难度不合适,处于很尴尬的位置.讲得简单,难的丢给库, ...

  5. AVR单片机教程——UART进阶

    本文隶属于AVR单片机教程系列.   在第一期中,我们已经开始使用UART来实现单片机开发板与计算机之间的通信,但只是简单地讲了讲一些概念和库函数的使用.在这一篇教程中,我们将从硬件与软件等各方面更深 ...

  6. AVR单片机教程——矩阵键盘

    本文隶属于AVR单片机教程系列.   开发板上有4个按键,我们可以把每一个按键连接到一个单片机引脚上,来实现按键状态的检测.但是常见的键盘有104键,是每一个键分别连接到一个引脚上的吗?我没有考证过, ...

  7. AVR单片机教程——示波器

    本文隶属于AVR单片机教程系列.   在用DAC做了一个稍大的项目之后,我们来拿ADC开开刀.在本讲中,我们将了解0.96寸OLED屏,移植著名的U8g2库到我们的开发板上,学习在屏幕上画直线的算法, ...

  8. AVR单片机教程——DAC

    本文隶属于AVR单片机教程系列.   单片机的应用场景时常涉及到模拟信号.我们已经会使用ADC把模拟信号转换成数字信号,本讲中我们要学习使用DAC把数字信号转换成模拟信号.我们还将搭建一个简单的功率放 ...

  9. AVR单片机教程——走向高层

    本文隶属于AVR单片机教程系列.   在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS.C++.事件驱动.掌握这些技术可以帮助你更快.更好地开发更大的项目. 本文涉及到许多概念性的内容 ...

随机推荐

  1. CCPC 2018 吉林 H "LOVERS" (线段树)

    ---恢复内容开始--- 传送门 参考资料: [1]:https://blog.csdn.net/mmk27_word/article/details/89788448 题目描述: The Fool ...

  2. C#面试题整理(带答案)

    1.维护数据库的完整性.一致性.你喜欢用触发器还是自写业务逻辑?为什么? 答:尽可能用约束(包括CHECK.主键.唯一键.外键.非空字段)实现,这种方式的效率最好:其次用触发器,这种方式可以保证无论何 ...

  3. CITRIX ADC配置SSL卸载

    如上图,将ssl的加密解密放在前端的负载均衡设备上,客户端到VPX的访问都是加密的,VPX到后端的服务器都是http的 Step1:上传证书到VPX,如下图: Step2:创建SSL的虚拟服务器并且绑 ...

  4. 闲着没事,做个chrome浏览器插件,适合初学者

    时光偷走的,永远都是我们眼皮底下看不见的珍贵. 本插件功能:替换掉网页中的指定图片的src地址. 使用插件前: 使用插件后: 鲜花(闲话):这个网站的不加水印的图片连接被保存在,图片的data-ima ...

  5. HBase 原理

    遗留问题: 数据在更新时首先写入Log(WAL log)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的 ...

  6. 【译】PEP 318--函数和方法的装饰器

    PEP原文 : https://www.python.org/dev/peps/pep-0318 PEP标题: Decorators for Functions and Methods PEP作者: ...

  7. MongoDB not authorized for query - code 13 错误解决办法

    跟着教程走完到了鉴权阶段,不加 --auth 登陆正常,但会出现warning :没有鉴权,修改不会生效,此时登陆正常. 但是加上了--auth 启动之后加上密码登陆则无法登陆. 添加用户和鉴权: 先 ...

  8. 金蝶handler中 collection 代码片段理解

    1,AtsOverTimeBillBatchEditHandler中collection的理解 SelectorItemCollection selectors = new SelectorItemC ...

  9. 通过9个Linux-0.11实验学习操作系统

    简介 2019年秋,我自学了一下哈工大的操作系统课程,感觉其设计的教程和实验作为操作系统入门是个不错的选择(虽然是基于较老的Linux-0.11写的).实验大致覆盖了操作系统中的核心概念,例如启动.中 ...

  10. context:component-scan 和 mvc:annotation-driven

    前言 Spring MVC 框架提供了几种不同的配置元素来帮助和指示 Spring 容器管理以及注入 bean . 常用的几个 XML 配置是 context:component-scan mvc:a ...