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

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

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

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

什么是定时器

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

为什么要用定时器

之前有过这样的经历:

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

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

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

其实你一直在用定时器

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

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

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

寄存器

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

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

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

ICNC1ICES1都是1位的位域,它们的值可以是01WGM1[3:2]是2位的位域,它的值可以是00011011CS1[2:0]同理。

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

假如我要把这4个参数分别写为100b000b101,就要写:

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)。

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

时钟源 适用范围
所有
\(clk_{I/O} / N, N = 1, 8, 64, 256, 1024\)
(\(clk_{I/O}\)为系统时钟,16MHz)
定时器0/1
T04)引脚上升/下降沿 定时器0
T15)引脚上升/下降沿 定时器1
\(clk_{T2S} / N, N = 1, 8, 32, 64, 128, 256, 1024\)
(\(clk_{T2S}\)为系统时钟或外置32kHz晶振)
定时器2

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

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

    \[\frac {clk} {MAX + 1}
    \]

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

    \[\frac {clk} {TOP + 1}
    \]

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

    \[\frac {clk} {2 TOP}
    \]

计数器比较

当计数器的值与OCRnAOCRnB相等时,可以对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相等电平翻转,两次为一周期,频率为\(\frac {clk} {2(OCR1A + 1)}\)。先取\(clk\)为不分频试试,算出OCR1A为18181,没有超过最大值65535,因此就取这个。如果超过了,就要把定时器频率下调,直到OCRnx合理为止。

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

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

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);
}

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

定时器中断

懒得写了,我抄我自己:

中断,是单片机的精华。

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

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

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

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

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

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

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

产生精准的定时器中断,一般使用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模式,TOPOCR1A

  • CS1[2:0] = 0b100,时钟为\(clk_{I/O} / 256\),分频系数\(N = 256\);

  • OCR1A31249

  • TIMSK1OCIE1A置位,sei()开全局中断,COMPA中断启用;

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

  • 定时器中断频率为\(f = \frac {clk} {TOP + 1} = \frac {F\_CPU} {N \cdot (OCR1A + 1)} = \frac {16 \times 10^6} {256 \times (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,56analogWritetone可以照常使用。

如果你要用定时器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() {
// ...
}

需要注意:

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

  2. 910端口不仅analogWritetone不能用,digitalWrite也不能用!请直接使用寄存器写引脚电平,参见:AVR单片机教程——数字IO寄存器

定时器2同理,311不能用。

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写值并不会立即更新它,而是会在计数器达到BOTTOMTOP时更新,这保证了PWM占空比的正确性,但是CTC模式中OCRnx是立即更新的,可能会错过匹配。

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

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

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

思考题

  1. 对于同一个定时器,中断与PWM能否同时使用?方波与PWM波能否同时输出?

  2. 尝试用定时器中断来创造新的PWM通道,频率和精度能实现呼吸灯即可。

  3. 在STM8单片机中,定时器TIM1TIM5TIM6可以相互控制。ATmega328P能否实现类似的功能?

ATmega328P定时器详解的更多相关文章

  1. STM32F103的11个定时器详解(转)

    源:STM32F103的11个定时器详解 STM32F103系列的单片机一共有11个定时器,其中:2个高级定时器4个普通定时器2个基本定时器2个看门狗定时器1个系统嘀嗒定时器 出去看门狗定时器和系统滴 ...

  2. 定时器详解和应用、js加载阻塞、css加载阻塞

    1.setTimeout().setInterval()详解和应用 1.1 详解: setTimeout.setInterval执行时机 1.2 存在问题: setInterval重复定时器可能存在的 ...

  3. oracle创建定时器详解|interval属性

    定时任务首先先创建定时任务中的存储过程 create or replace procedure pro_jggl as                                          ...

  4. Node中的定时器详解

    在大多数的业务中,我们都会有一些需求,例如几秒钟实现网页的跳转,几分钟对于后台数据进行清理,node与javascript都具有将代码延迟一段时间的能力.在node中可以使用三种方式实现定时功能:超时 ...

  5. JavaScript定时器详解及实例

    JS里设定延时: 使用SetInterval和设定延时函数setTimeout 很类似.setTimeout 运用在延迟一段时间,再进行某项操作. setTimeout("function& ...

  6. IOS中的NSTimer定时器详解

    /* 在IOS中有多种定时器,这里我对NSTimer定时器做了一个简单的介绍.如果你是小白,你可能会从这篇文章中学习到一些知识,如果你是大牛,请别吝啬你的评论,指出我的不足,你的质疑是对我最大的帮助. ...

  7. iOS中的几种定时器详解

    在软件开发过程中,我们常常需要在某个时间后执行某个方法,或者是按照某个周期一直执行某个方法.在这个时候,我们就需要用到定时器. 然而,在iOS中有很多方法完成以上的任务,经过查阅资料,大概有三种方法: ...

  8. S3C2440的定时器详解

    还包含用于大电流驱动的死区发生器 位预分频器是可编程的,并且按存储在TCFG0和TCFG1寄存器中的加载值来分频PCLK 位递减计数器.当递减计数器到达零时,产生定时器中断请求通知CPU定时器操作已经 ...

  9. JavaEE开发之Spring中的多线程编程以及任务定时器详解

    上篇博客我们详细的聊了Spring中的事件的发送和监听,也就是常说的广播或者通知一类的东西,详情请移步于<JavaEE开发之Spring中的事件发送与监听以及使用@Profile进行环境切换&g ...

随机推荐

  1. Django-rest-framework源码分析(二)

    四.Serializer对象的data属性 在<Django-rest-framework源码分析(一)>中我分析了Serializer对象实例化的过程,而Serializer类的其他方法 ...

  2. OpenCV-Python 用于角点检测的FAST算法 | 四十一

    目标 在本章中, 我们将了解FAST算法的基础知识. 我们将使用OpenCV功能对FAST算法进行探索. 理论 我们看到了几个特征检测器,其中很多真的很棒.但是,从实时应用程序的角度来看,它们不够快. ...

  3. OpenCV-Python 哈里斯角检测 | 三十七

    目标 在本章中, 我们将了解"Harris Corner Detection"背后的概念. 我们将看到以下函数:cv.cornerHarris(),cv.cornerSubPix( ...

  4. 知识图谱与机器学习 | KG入门 -- Part1-b 图深度学习

    介绍 我们正在定义一种新的机器学习方法,专注于一种新的范式 -- Data Fabric. 在上一篇文章中,我们对机器学习给出了新的定义: 机器学习是一种自动发现Data Fabric中隐藏的&quo ...

  5. 一文总结数据科学家常用的Python库(下)

    用于建模的Python库 我们已经到达了本文最受期待的部分 - 构建模型!这就是我们大多数人首先进入数据科学领域的原因,不是吗? 让我们通过这三个Python库探索模型构建. Scikit-learn ...

  6. 2-SAT(HDU-3062 party)

    2-SAT(HDU-3062 party) 解决问题类型: 书本定义:给一个布尔方程,判断是否存在一组解使整个方程为真,被称为布尔方程可满足性问题(SAT) 因为本题只有0,1(丈夫 妻子只能去一个人 ...

  7. iOS 图片加载和处理

    一.图片显示 图片的显示分为三步:加载.解码.渲染.解码和渲染是由 UIKit 进行,通常我们操作的只有加载. 以 UIImageView 为例.当其显示在屏幕上时,需要 UIImage 作为数据源. ...

  8. mongodb的增加和删除

    一  mongodb中使用insert()方法来增加集合中的文档: db.myTable.insert({name:'arvin',age:12}) //对名为myTable的集合插入数据 插入数据 ...

  9. java 下载与配置环境变量

    第一​:JDK下载 ​地址:https://www.java.com/zh_CN/download/win10.jsp 注意:自己载点击安装jdk的时候留意一下自己的安装地址,下一步要用: 第二​:环 ...

  10. PTA数据结构与算法题目集(中文) 7-10

    PTA数据结构与算法题目集(中文)  7-10 7-10 公路村村通 (30 分)   现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低 ...