ATmega328P定时器详解

发布者:睿智之光最新更新时间:2020-09-08 来源: eefocus关键字:ATmega328P  定时器  组件 手机看文章 扫描二维码
随时随地手机看文章

写这篇文章,纯粹是想为博客拉点点击量。在博客园,游客访问好像是不计入阅读量的,而作为一个十八线博主,注册用户的访问应该以搜索引擎为主,博客园首页为次,个位数的粉丝就别谈了。


所以,希望各位从搜索引擎点进来的朋友,多多评论,有问题咱们一起讨论。


我写过AVR单片机教程,设计过自己的Arduino板,希望你相信我能给你带来收获。

我不想听你放那么多屁,我只想知道周期为1ms的定时器中断怎么写!

什么是定时器

在ATmega328P单片机中,定时/计数器(Timer/Counter)是这样的组件:它需要一个时钟源,驱动一个8或16位的计数器递增或递减,当计数器等于一个值时,会触发一些操作,如产生中断、翻转引脚电平等。由于定时器的时钟源是系统时钟或外接晶振(一种产生频率精准的波的器件)分频得到的,一旦设置好定时器的工作参数,直到下次调整参数,定时器都会按照预期工作,与CPU执行的代码无关。


为什么要用定时器

之前有过这样的经历:

跟一个优秀作品设计者聊了几句,他说同时控制舵机和扬声器很难控制好延时,扬声器输出的音乐节奏会乱。我第一反应当然是他没有用定时器中断,一问果然如此,并且他不知道中断也不知道定时器。

还有一位同学,写TI计算器的程序。在他的一个作品中,每次循环的计算量不定,循环间隔也不定,导致游戏效果不好。他的解决方法是根据计算量计算出循环最后需要的延时,使得循环间隔基本保持不变。


这种思路是相当优秀的。但是如果有定时器可用的话,编程难度会降低,循环间隔的一致性也会更好,是更加优秀的解决方案。


其实你一直在用定时器

Arduino Uno Rev3的3、5、6、9、10、11号端口可以使用analogWrite和tone函数,它们的功能都是利用定时器实现的。用函数确实方便,但是只知使用而不知其原理就只能停留在技术的表面——Arduino的强大封装对开发者的学习有两面性。


定时器其实不知道什么3号端口,它只知道OC2B。两种表示之间的对应关系如下表:

端口编号硬件符号
3PD3(PCINT19/OC2B/INT1)
5PD5(PCINT21/OC0B/T1)
6PD6(PCINT22/OC0A/AIN0)
9PB1(PCINT1/OC1A)
10PB2(PCINT2/SS/OC1B)
11PB3(PCINT3/OC2A/MOSI)

寄存器

寄存器是开发者与硬件打交道的方式。从编程的语法上,可以把寄存器当作是变量,可以对它赋值,也可以读取它的数值。


寄存器中的位有几种不同的组织结构,它们的存取方式也不尽相同:

TCCR1B寄存器中有4组参数:ICNC1、ICES1、WGM1[3:2]、CS1[2:0]。现在你完全无需理解这些字母的含义,但是得对这些数字有个概念:WGM1[3:2]表示从WGM13到WGM12;TCCR1B中的1表示该寄存器属于定时器1,ICNC1和WGM13等名字中的1也是;CS12中的2表示该位为CS1[2:0]位域(bitfield)中的第2位(最低位为第0位)。


ICNC1和ICES1都是1位的位域,它们的值可以是0或1;WGM1[3:2]是2位的位域,它的值可以是00、01、10、11;CS1[2:0]同理。


你也许一眼就能看出二进制的11在十进制中是3,但是你很可能看不出23对应10111。在Arduino编程中(语言为C++),二进制数可以直接写,无需与十进制或十六进制转换。Arduino提供的方法是B10111,GCC提供的是0b10111(0b前缀字面量是C++14标准才规定的)。后者是我一直以来的习惯。


假如我要把这4个参数分别写为1、0、0b00、0b101,就要写:

TCCR1B =     1 << ICNC1

       |     0 << ICES1

       |  0b00 << WGM12

       | 0b101 << CS10

       ;

全是0的可以不写,写是为了可读性。ICNC1是寄存器的第7位,所以代码中它的值就是7,其他位同理。


如果要判断ICNC1位是否为1:


if (TCCR1B & 1 << ICNC1)

    // ...

如果要读取WGM1[3:2]位:


uint8_t wgm = (TCCR1B & 0b11 << WGM12) >> WGM12;

有的位因为不存在而不能写,如TCCR1B的第5位;有的位即使存在但是只读所以也不能写;有的位域分布于多个寄存器中,如WGM1[3:0],低两位在TCCR1A,高两位在TCCR1B。


除了一个或多个位的位域以外,有些寄存器是整体使用的:

可以直接当变量读写:


OCR0A = 233;

uint8_t ocr0a = OCR0A;

还有16位寄存器,虽然读写不能用一句汇编搞定,但是高级语言层面上可以:

TCNT1 = 10086;

uint16_t tcnt1 = TCNT1;

不超过255的话可以只写低字节TCNT1L。


定时器相关寄存器总览:

定时器的工作模式

读数据手册无疑是深入了解单片机的最好方法,可惜很多人没这个耐心,几十页的英语也不是每个人都吃得消的。有些中文书打着介绍AVR单片机的幌子翻译数据手册,不仅没有营养还漏洞百出,我不也推荐。写这篇文章,也有避免后人重蹈覆辙的目的。


当然,除了有代码示例以外,本文再“详解”也详细不过数据手册,不过至少可以让你对定时器有个大致的印象,不致于让你读的时候一头雾水。


ATmega328P有3个定时器:定时器0、定时器1和定时器2(简单粗暴)。0和2都是8位的,2支持异步工作;1是16位的,精度更高,支持更多工作模式。我接触过其他型号的单片机,AVR的定时器是相对简单的。


定时器有3种工作模式:普通模式、CTC模式、PWM模式,其中PWM还分快速PWM、相位矫正(波形居中)PWM、相位与频率矫正PWM(频率可以任取,仅限定时器1)。

先讲各种模式中共通的部分。定时器需要一个时钟源,它可以是:

时钟源适用范围
所有
clkI/O/N,N=1,8,64,256,1024clkI/O/N,N=1,8,64,256,1024
(clkI/OclkI/O为系统时钟,16MHz)
定时器0/1
T0(4)引脚上升/下降沿定时器0
T1(5)引脚上升/下降沿定时器1
clkT2S/N,N=1,8,32,64,128,256,1024clkT2S/N,N=1,8,32,64,128,256,1024
(clkT2SclkT2S为系统时钟或外置32kHz晶振)
定时器2

工作模式之间的区别在于计数器的变化方向与范围,介绍之前需要先下3个定义:

名称描述
BOTTOM0,计数器的最小值
MAX对8位定时器为0xFF,对16位定时器为0xFFFF,计数器的最大可能值
TOP计数器达到这个值时,可能会被清零,或变化方向改变
对定时器0和2,可以为MAX或OCRnA
对定时器1,可以为0x00FF、0x01FF、0x03FF、OCR1A或ICR1
  • 普通模式中,计数器从0开始增长到MAX,然后溢出回到0,周而复始。频率为(clkclk为定时器时钟频率)


    clkMAX+1clkMAX+1


  • CTC模式和快速PWM模式中,计数器从0开始增长到TOP,然后不再继续增长而是直接回到0,重新开始增长。频率为


    clkTOP+1clkTOP+1


  • 两种相位矫正PWM模式中,计数器从0到TOP,再从TOP回到0,如此循环。频率为


    clk2TOPclk2TOP


计数器比较

当计数器的值与OCRnA或OCRnB相等时,可以对OCnx的电平进行一些操作。

  • 所有模式下,OCnx都可以不连接定时器。

  • 非PWM模式下,可以把OCnx置为低电平、高电平或翻转电平,tone就是这样实现的;

  • PWM模式下,有正相和反相两种模式,正相为OCRnx越大占空比越高,analogWrite就是这样实现的;反相反之;有些配置下OCnA可以被翻转,请参考数据手册。

由于引脚电平可以有宏观表现,我们终于可以开始写代码了。


先试试tone。在9号端口上连接一个蜂鸣器,使用定时器1的CTC模式,产生440Hz方波

void setup() {

  pinMode(9, OUTPUT);

  TCCR1A = 0b01 << COM1A0 | 0b00 << WGM10;

  TCCR1B = 0b01 << WGM12 | 0b001 << CS10;

  OCR1A = 18181;

}


void loop() {

  

}


OCR1A = 18181是怎么来的呢?每次计数器与OCR1A相等电平翻转,两次为一周期,频率为clk2(OCR1A+1)clk2(OCR1A+1)。先取clkclk为不分频试试,算出OCR1A为18181,没有超过最大值65535,因此就取这个。如果超过了,就要把定时器频率下调,直到OCRnx合理为止。

void setup() {

  pinMode(3, OUTPUT);

  TCCR2A = 0b10 << COM2B0 | 0b11 << WGM20;

  TCCR2B = 0 << WGM22 | 0b100 << CS20;

}


int brightness = 0;

int fadeAmount = 5;


void loop() {

  OCR2B = brightness;

  brightness = brightness + fadeAmount;

  if (brightness <= 0 || brightness >= 255)

    fadeAmount = -fadeAmount;

  delay(30);

}


如果要让程序以频率为参数计算出合适的分频系数与OCRnx值,可以参考tone的实现。

再试试analogWrite。在3号端口上连接一个LED,使用定时器2的快速PWM模式,实现呼吸灯的效果:


在快速PWM模式中,正相输出占空比不能为0,反相输出占空比不能为1,如果要达到这两个值,需要断开引脚与定时器的连接,用digitalWrite等方法输出。

定时器中断

懒得写了,我抄我自己:

中断,是单片机的精华。

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

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

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

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

定时器中断同样有着举足轻重的地位——操作系统的任务调度就是在定时器中断中进行的。如果没有中断,CPU就在那自顾自地执行代码,它哪知道什么时候要调度呢?正因为定时器是独立于CPU运行的,时间控制非常精准且不受影响,因而能解决前面优秀作品和计算器游戏中的问题。


什么时候需要定时器中断呢?当你发现没有中断的程序结构不能胜任你的需求时,或者……把所有代码都放进ISR。比如,每1ms产生一次中断,先检测按键是否被按下,根据其情况执行相应操作。


每个定时器都有3个中断源:OVF、COMPA和COMPB(定时器1还有CAPT),分别在计数器溢出、与OCRnA、OCRnB相等时触发。

产生精准的定时器中断,一般使用CTC模式和COMPA中断,分频系数与TOP值的计算方法与上面相同。


void setup() {

  pinMode(13, OUTPUT);

  TCCR1A = 0b00 << WGM10;

  TCCR1B = 0b01 << WGM12 | 0b100 << CS10;

  OCR1A = 31249;

  TIMSK1 = 1 << OCIE1A;

  sei();

}


ISR(TIMER1_COMPA_vect)

{

  static bool light = true;

  digitalWrite(13, light = !light);

}


void loop() {

  

}

在这个程序中:


WGM1[3:0] = 0b0100,定时器1工作于CTC模式,TOP为OCR1A;


CS1[2:0] = 0b100,时钟为clkI/O/256,分频系数N=256;


OCR1A为31249;


TIMSK1中OCIE1A置位,sei()开全局中断,COMPA中断启用;


ISR(TIMER1_COMPA_vect)为定时器1COMPA中断的函数头,TIMER1_COMPA_vect这个名字可以当成函数来用;


定时器中断频率为f=clkTOP+1=F_CPUN⋅(OCR1A+1)=16×106256×(31249+1)=2Hz。


一般而言,定时器中断的频率不要超过10kHz,1kHz已经能够应付旋转编码器了。


进入中断后,全局中断会自动禁用,如果中断代码执行期间发生了定时器事件,对应的中断不会触发,而是等到当前中断返回后再处理。可以用sei()开中断,但是要小心代码执行时间接近或超过周期的情况,虽然定时准了,但中断嵌套导致内存耗尽,程序跑飞了,得不偿失。可以考虑另一种时间同步的方法,在loop的最后轮询OCFnA直到它置位:


void setup() {

  pinMode(13, OUTPUT);

  TCCR1A = 0b00 << WGM10;

  TCCR1B = 0b01 << WGM12 | 0b100 << CS10;

  OCR1A = 31249;

}


void loop() {

  static bool light = true;

  digitalWrite(13, light = !light);

  while (!(TIFR1 & 1 << OCF1A))

    ;

  TIFR1 |= 1 << OCF1A;

}

这种程序结构有定时作用,但不能中断。频率较高的时候,推荐使用后一种,顶多定时不准,程序还是能运行的。


照顾一下Arduino

Arduino库非常贪心,在setup之前就把所有定时器都开启了(也许你不同意,但我想把这种行为称为“流氓”——想想百度网盘偷了你多少带宽和流量!)。定时器0是时间相关函数的命根,除非你想把它割掉,否则不要动定时器0。如果你不动定时器0,5和6的analogWrite和tone可以照常使用。


如果你要用定时器1,如用以下代码配置周期为1ms的定时器中断:


void init_timer1()

{

  TCCR1A = 0b00 << WGM10;

  TCCR1B = 0b01 << WGM12 | 0b011 << CS10;

  OCR1AL = 249;

  TIMSK1 = 1 << OCIE1A;

  sei();

}


void setup() {

  init_timer1();

  // ...

}


ISR(TIMER1_COMPA_vect)

{

  // ...

}


void loop() {

  // ...

}

需要注意:


由于Arduino库在setup之前动过TCCR1A,不能认为在执行我们的代码时TCCR1A为默认值,因此即使我们想要的是默认值也不能省略。


9和10端口不仅analogWrite和tone不能用,digitalWrite也不能用!请直接使用寄存器写引脚电平,参见:AVR单片机教程——数字IO寄存器。


定时器2同理,3和11不能用。


void init_timer2()

{

  TCCR2A = 0b10 << WGM20;

  TCCR2B = 0 << WGM22 | 0b100 << CS20;

  OCR2A = 249;

  TIMSK2 = 1 << OCIE2A;

  sei();

}


void setup() {

  init_timer2();

  // ...

}


ISR(TIMER2_COMPA_vect)

{

  // ...

}


void loop() {

  // ...

}

其他功能

有些工作模式下,向OCRnx写值并不会立即更新它,而是会在计数器达到BOTTOM或TOP时更新,这保证了PWM占空比的正确性,但是CTC模式中OCRnx是立即更新的,可能会错过匹配。


定时器1有输入捕获单元,可以对信号进行计数,计数达到一定值时触发中断。外部中断同样可以捕获引脚电平变化,但是中断是有成本的,信号频率不能太高,而定时器的捕获功能更加强大。


定时器1有额外的ICR1寄存器,作为TOP值可以实现许多特殊的功能,并且由于定时器1是16位的,即使是复用时的精度也比定时器0和2高,见思考题1。


定时器2可以用外置晶振驱动,比较适合实现实时时钟,可以在系统时钟停止的省电状态下工作。


关键字:ATmega328P  定时器  组件 引用地址:ATmega328P定时器详解

上一篇:AVR单片机教程——ADC
下一篇:AVR单片机教程——走向高层

推荐阅读最新更新时间:2024-11-17 13:23

单片机 中断( 定时器/计数器详说 )
方式0应用     通过设置TMOD寄存器中的M1M0位00选择定时器方式0,方式0的计数位数是13位,对T0来说,TL0寄存器的低5位(高3位未用)和TH0寄存器的8位组成。TL0的低5位溢出时向TH0进位,TH0溢出时,置位TCON中的TF0标志,向cpu发出中断请求。其逻辑图如下 定时器方式0位13位计数器,最多能装载的个数 2的13次方=8192个,当TL0和TH0的初始值为0时,最多经过8192个机器周期该计数器就会溢出一次,向cpu申请中断。 THX=(8192-N)/32 TLX=(8192-N)%32。机器周期=12 X (1/时钟频率) 单片机定时器程序的步骤: 对TMOD赋值、计算初值、中断方式,对IE赋值
[单片机]
单片机 中断( <font color='red'>定时器</font>/计数器详说 )
组件持续短缺 小米手机第三季出货量季减少15%
行业分析公司Counterpoint公布2021年第三季全球智能手机市场报告指出,小米第三季出货量按年及按季分别倒退4.5%及15.4%,至4,440万台,原因是品牌严重受到零组件持续短缺的情况冲击。 香港经济日报1日报导,据Counterpoint数据显示,全球智能手机市场第三季的出货量为3.42亿台,季增6%,按年却减少6%。三星仍是全球智能手机出货量最多的品牌,期内出货量季增达20.3%,至6,930万台。 在三星、苹果、小米、Vivo及OPPO合共五大品牌中,仅苹果及小米期内按季出货量为下跌,前者微跌1.8%,后者则大减15.4%。按年比较,三星及小米第三季出货量分别下跌13.8%及4.5%,苹果、Vivo及OPPO则取得
[手机便携]
雷迪埃推出医疗电缆组件和互连解决方案
  雷迪埃为全球市场提供病患监护设备的OEM商推出了新的经济型 ECG电缆组件和SpO2接口电缆。除标准种类外,这些电缆和连接器还可以设计定制,贴合各种电缆、连接器和接触件以及轭架装置,从而满足客户需求。   ECG电缆拥有全屏蔽主干电缆和病患导线,确保在高EMI和高噪音环境下也能实现信号完整性。可单独供应带3、5或10个导线的主干电缆或将它们集成为一个整体。这些导线含有不可拆卸香蕉插头、推入紧锁式或线夹式末端。该一体式设备主要用于紧急状况,以便在急救状态放置病患传感器并实现连接时节省时间。 SpO2接口电缆具备主要OEM制造商目前使用的所有电缆类型。另外,雷迪埃的专业技能还包括,能够混合配置连接器种类包括组合光纤通道为
[医疗电子]
Bourns携最新电子保护组件应用方案亮相2018上海慕尼黑电子展
2018上海慕尼黑电子展将于2018年3月14~16日在上海新国际博览中心隆重举行。素以提供最高质量的电路保护装置、工程师所需保护的电路调节与运动控制设备、控制与管理其最新系统与产品设计闻名的电子组件领导制造供货商美商柏恩Bourns也将亮相2018上海慕尼黑电子展。 届时,Bourns将针对工业、通讯、消费者与汽车应用电子组件展示最新最完整的解决方案。展出重点包括目前最夯的电动车电池管理系统解决方案、RS485接口保护方案、Bourns 小型断路器在电池管理中的应用介绍。 此外,3月15日,Bourns 将在展位上举办整点论坛活动,主题包括:工业接口的高等级防护、Bourns在通信领域的应用、Bourns 小型断路
[半导体设计/制造]
51单片机中断系统详解(定时器、计数器)
51单片机中断级别 中断源 默认中断级别 序号(C语言用) INT0---外部中断0 最高 0 T0---定时器/计数器0中断 第2 1 INT1---外部中断1 第3 2 T1----定时器/计数器1中断 第4 3 TX/RX---串行口中断 第5 4 T2---定时器/计数器2中断 最低 5 中断允许寄存器IE 位序号 DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 符号位 EA ------- ET2 ES ET1 EX1 ET0 EX0 EA---全局中允许位。 EA=1,打开全局中断控制,在此条件下,由
[单片机]
G20周刊|埃斯顿与北自所达成战略合作、海康机器人发布光伏组件汇流带视觉检测解决方案
埃斯顿与北自所达成战略合作 3月11日,南京埃斯顿自动化股份有限公司(以下简称:埃斯顿)与北京机械工业自动化研究所有限公司(以下简称:北自所)强强联合,达成战略合作协议。北自所党委书记、董事长王振林,埃斯顿自动化董事长吴波等双方领导出席签约仪式。(详情请点击) 海康机器人发布光伏组件汇流带视觉检测解决方案 3月 15日,海康机器人发布光伏组件汇流带视觉检测解决方案。据悉,再光伏领域,海康机器人深入研究行业需求,结合机器视觉软硬件产品优势,不断优化系统能力,可提供串EL外观检、排版定位、接线盒焊后检测等视觉解决方案。(详情请点击) 灵动科技携手高通 日前,高通技术
[机器人]
STM32 定时器用于外部脉冲计数
因为用stm32f103c8作主控制器,来控制小车,小车的转速由两路光电编码盘输入(左右各一路).因此想到外部时钟触发模式(TIM ETRClockMode2Config)。 可以试好好久,发现TIM1不能计数,到网上查了很久,也没有找到相关的文章,开始怀疑TIM1是不是需要特殊设置。经过很久的纠结,终于找到了问题 其实是我自己在GPIO设置的时候,后面的不小心覆盖了前面的了 没想到自己也会犯这么SB的事情。 现总结程序如下: 第一步,设置GPIO GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|
[单片机]
内置轮胎受力传感器及轮毂组件亮相
      在“第40届东京车展”上很多汽车部件厂商展出了内置轮胎受力传感器的轮毂组件。该组件能够迅速检测出路面对汽车的作用力,可用于控制汽车车体的晃动。过去使用陀螺传感器和加速度传感器等来间接检测该作用力,因此无法在该作用力造成车体摇晃之前将其检测出来。而使用该组件后,可通过轮胎受力时产生的轮毂组件内轴承内、外轮的位移变化,以及轴承的变形来检测轮胎受力,因此灵敏度高,可对轻微的车体摇晃也可进行控制。   使用霍尔(HALL)元件和磁编码器来检测轴承内位移的是日本精工(NSK)(图1)。该编码器为环形,沿圆周将V字形的磁化部分像豁口一样形成数层(V字形的底部重叠配置)。相关元件配置在内轮上,外轮每隔120°配置3组(每组2个)
[汽车电子]
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved