AVR单片机教程——定时器中断

摘要:
本文属于AVR SCM教程系列。AVR MCU支持多个中断,包括外部引脚中断、定时器中断、总线中断等。在AVR微控制器中,执行中断处理功能将自动关闭全局中断,此时,程序不会中断,但您可以手动sei()使中断可以处理。这是因为按键抖动,这比微控制器的时钟周期长,并可能触发多次中断。实际上,是操作系统在定时器中断中中断任务的正常执行,然后对其进行调度。在AVR微控制器定时器的各种模式中,常用共模和CTC模式来生成定时器中断。

本文隶属于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 Filesxxx.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单片机教程——定时器中断》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇全网NFS存储架构详解与分析nohup下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

一个操作系统的实现(1)

一个操作系统的实现 说明:本文是一个简单的学习记录,不是全面给大家提供学习的文章,文章内容均代表作者的个人观点,难免会有错误。转载请保留作者信息。                                                                                                         ...

js实现之--防抖节流【理解+代码】

防抖:     理解:在车站上车,人员上满了车才发走重点是人员上满触发一次。     场景:实时搜索,拖拽。     实现:         //每一次都要清空定时器,重新设置上计时器值,使得计时器每一次都重新开始,直到最后满足条件并且等待delay时间后,才开始执行handler函数。 // func是用户传入需要防抖的函数 // wait是等待时间 c...

使用boost的deadline_timer实现一个异步定时器

概述 最近在工作上需要用到定时器,然后看到boost里面的deadline_timer可以实现一个定时器,所以就直接将其封装成了ATimer类,方便使用,ATimer有以下优点: 可以支持纳秒、毫秒、秒、分、小时定时。 可以随时停止定时器。 支持单次调用。 因为使用了deadline_timer,所以定时比较准确。 ATimer和Qt的QTimer使用...

用Visual C++制作微秒级精度定时器

在工业生产控制系统中,有许多需要定时完成的操作,如:定时显示当前时间,定时刷新屏幕上的进度条,上位机定时向下位机发送命令和传送数据等。特别是在对控制性能要求较高的控制系统和数据采集系统中,就更需要精确定时操作。众所周知,Windows是基于消息机制的系统,任何事件的执行都是通过发送和接收消息来完成的。这样就带来了一些问题,如一旦计算机的CPU被某个进程占用...

Arduino 101/Genuino101使用-第2篇

1. Arduino 101编程只是在ARC的核心上进行,其具体架构为ARCv2EM。、 2. 而Quark核心,从目前可知的信息来看,其应该运行着名为Zephyr的RTOS 3.101并没有EEPROM存储单元,其提供的EEPROM库,实际上是在操纵其上的Flash空间。 4. 跑个定时器例程测试一下 1 #include "CurieTimerOne....

自己用C语言写单片机PIC18 serial bootloader

了解更多关于bootloader 的C语言实现,请加我QQ: 1273623966 (验证信息请填 bootloader),欢迎咨询或定制bootloader(在线升级程序)。 HyperBootloader_PIC18_J 和 HyperBootloader_PIC18_None_J 完成PIC16 bootloader (详细情况请阅读我的上一篇随笔《...