2019-5-16 11:12:59 [显示全部楼层]
21971浏览
查看: 21971|回复: 3

[ESP32系列教程] ESP32 Arduino教程:定时器中断

[复制链接]
本文的目的是解释如何使用Arduino内核在ESP32上配置计时器中断。
测试是在DFRobot的ESP-WROOM-32设备上进行的,该设备集成在ESP32 FireBeetle板上。

引言
本文主要解释在使用Arduino内核的ESP32平台上如何配置定时器中断。下文代码基于Arduino核心库里面的这个例子,非常建议你好好看看这个例子(https://github.com/espressif/ard ... mer/RepeatTimer.ino )。
本教程将会介绍如何对定时器进行配置以周期性地产生中断,以及如何对中断进行处理。
有关外部中断的教程:ESP32 Arduino教程:外部中断 中已经见过这种计数器的用法,因为ISR应该尽可能快地运行,所以不应执行过长的操作(比如向串口写入数据)。因此,在实现中断处理代码时,最好让ISR仅对中断进行响应,然后把实际的处理(可能包含时间较长的操作)交给主循环来做。

  1. ]portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
复制代码

设置函数
在设置函数中,首先需要打开一个串行连接,以便后面将程序结果输出到Arduino IDE串口监测器。

  1. Serial.begin(115200);
复制代码

然后,调用timerBegin函数以对定时器进行初始化,这个函数会返回一个指向hw_timer_t结构类型的指针,这个指针正是我们在上一节声明的定时器全局变量之一。
该函数有三个输入参数,分别是我们要使用的定时器编号(0到3,对应全部4个硬件定时器)、预分频器数值以及一个用于表示计数器向上(真)或向下(假)计数的标志。
在本例中,我们将使用第一个定时器,最后一个参数设为真(表示计数器向上计数)。
关于预分频器,我们在引言部分说过,ESP32计数器使用的基频信号通常是80 MHz(仅就FireBeetle开发板而言)。这个数等于80 000 000 Hz,这就意味着基频信号将使定时器计数器每秒递增80 000 000次。
尽管我们可以基于这个数值进行计算,进而对计数器值进行设置以产生中断,但是我们将利用预分频器来简化设置。如果我们将这个数值除以80(也就是说使用80作为预分频器的数值),那么就能得到一个1 MHz的频率,这就意味着定时器计数器将会每秒递增1 000 000次。
将上述数值取倒数,可知计数器将会每一微秒递增一次。所以,如果预分频器值为80,那么当我们调用函数对计数器值进行设置以产生中断时,我们所指定的数值就是以微秒为单位。

  1. timer = timerBegin(0, 80, true);
复制代码

但是在启用定时器之前,我们还需要将其绑定到一个处理函数,以便对产生的中断做出响应。该操作可通过调用timerAttachInterrupt函数来完成。
该函数共有三个参数,分别是一个指向已初始化定时器的指针(保存在我们的全局变量中)、中断处理函数的地址以及一个表示中断触发类型是边沿(真)还是电平(假)的标志。有关边沿和电平触发中断的区别可参见此处https://electronics.stackexchange.com/questions/21886/what-does-edge-triggered-and-level-triggered-mean
在调用函数时,我们将把定时器全局变量作为第一个输入参数,一个叫做onTimer的函数(稍后介绍)地址作为第二个参数,第三个参数赋值为真(表示中断是边沿触发类型)。

  1. timerAttachInterrupt(timer, &onTimer, true);
复制代码

接下来,使用timerAlarmWrite函数指定触发定时器中断的计数器值。该函数的第一个参数是定时器指针,第二个参数是触发中断的计数器值,第三个参数是一个表示定时器在产生中断时是否重新加载的标志。
所以,在调用函数时,我们同样把定时器全局变量作为第一个输入参数,第三个参数设置为真(表示计数器将自动重新加载),这样就能周期性地产生中断。
关于第二个参数,不要忘了我们之前对预分频器的设置,中断应在数微秒后产生。因此,对本例而言,假如我们想要每秒产生一个中断,那么该值就是1 000 000微秒(等于1秒)。

重要说明:请注意,只有在将预分频器值设为80时,此处指定的时间单位才是微秒。如果使用其他预分频器数值,那就需要重新计算计数值。

  1. timerAlarmWrite(timer, 1000000, true);
复制代码

在设置函数最后,通过调用timerAlarmEnable函数即可启用定时器,函数调用时的输入参数就是我们的定时器变量。

  1. timerAlarmEnable(timer);
复制代码

设置函数的最终代码如下所示。

  1. void setup() {
  2.   Serial.begin(115200);
  3.   timer = timerBegin(0, 80, true);
  4.   timerAttachInterrupt(timer, &onTimer, true);
  5.   timerAlarmWrite(timer, 1000000, true);
  6.   timerAlarmEnable(timer);
  7. }
复制代码

主循环
如前文所述,在ISR对中断做出响应之后,真正的定时器中断处理操作其实是在主循环中。为简单起见,我们仅通过轮询对中断计数器的数值进行检验。但是更有效的处理方式是,使用一个信号量将主循环锁定,然后在ISR中将其解锁。这也是原始示例中所使用的方法:https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Timer/RepeatTimer/RepeatTimer.ino
首先,我们会检查interruptCounter变量是不是大于零,如果大于零,那么就进入中断处理代码。在中断处理代码中,首先会递减计数器的数值,表示已经对中断进行了响应和处理。
由于该变量由主循环和ISR所共享,所以必须在portENTER_CRITICAL和portEXIT_CRITICAL宏指定的关键代码段内处理。两次函数调用所使用的输入参数都是portMUX_TYPE全局变量的地址。
  1. if (interruptCounter > 0) {
  2.     portENTER_CRITICAL(&timerMux);
  3.     interruptCounter--;
  4.     portEXIT_CRITICAL(&timerMux);
  5.     // Interrupt handling code
  6.   }
复制代码

实际的中断处理操作只是对计数器(计数器数值表示自程序运行以来所发生的中断总数)进行递减,并将计数器值输出到串口。完整的主循环代码如下所示,其中已经包含了该函数调用。
  1. void loop() {
  2.   if (interruptCounter > 0) {
  3.     portENTER_CRITICAL(&timerMux);
  4.     interruptCounter--;
  5.     portEXIT_CRITICAL(&timerMux);
  6.     totalInterruptCounter++;
  7.     Serial.print("An interrupt as occurred. Total number: ");
  8.     Serial.println(totalInterruptCounter);
  9.   }
  10. }
复制代码

ISR代码
中断服务程序必须是一个返回void(空)且没有输入参数的函数。
本例中,中断服务程序非常简单,仅对中断计数器进行递增,以告知主循环发生了一次中断。代码位于portENTER_CRITICAL和portEXIT_CRITICAL宏指定的关键代码段内。两次函数调用所使用的输入参数都是先前声明的portMUX_TYPE全局变量的地址。
更新:为使编译器将代码分配到IRAM内,中断处理程序应该具有 IRAM_ATTR属性。而且,根据IDF文档的说明(参见此处),中断处理程序只能调用同样位于IRAM内的函数。感谢Manuato指出这一点。
该函数的完整代码如下所示。

  1. void IRAM_ATTR onTimer() {
  2.   portENTER_CRITICAL_ISR(&timerMux);
  3.   interruptCounter++;
  4.   portEXIT_CRITICAL_ISR(&timerMux);
  5. }
复制代码

最终代码
周期计数器中断程序的最终源代码如下所示。
  1. volatile int interruptCounter;
  2. int totalInterruptCounter;
  3. hw_timer_t * timer = NULL;
  4. portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
  5. void IRAM_ATTR onTimer() {
  6.   portENTER_CRITICAL_ISR(&timerMux);
  7.   interruptCounter++;
  8.   portEXIT_CRITICAL_ISR(&timerMux);
  9. }
  10. void setup() {
  11.   Serial.begin(115200);
  12.   timer = timerBegin(0, 80, true);
  13.   timerAttachInterrupt(timer, &onTimer, true);
  14.   timerAlarmWrite(timer, 1000000, true);
  15.   timerAlarmEnable(timer);
  16. }
  17. void loop() {
  18.   if (interruptCounter > 0) {
  19.     portENTER_CRITICAL(&timerMux);
  20.     interruptCounter--;
  21.     portEXIT_CRITICAL(&timerMux);
  22.     totalInterruptCounter++;
  23.     Serial.print("An interrupt as occurred. Total number: ");
  24.     Serial.println(totalInterruptCounter);
  25.   }
  26. }
复制代码

测试代码
将程序上传到您的ESP32开发板并打开Arduino IDE串口监测器,即可对代码进行测试。输出结果如图1所示,相关消息会周期性地显示出来(每秒显示一条消息)。

ESP32 Arduino教程:定时器中断图1
图1 - 定时器中断程序的输出。

注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。
他写了200多篇有关ESP32、ESP8266的有用的教程和项目。

查看更多ESP32/ESP8266教程和项目,请点击 : ESP32教程汇总贴
英文版教程 : ESP32 tutorial

gada888  版主

发表于 2019-5-16 19:39:37

中断不容易理解。
回复

使用道具 举报

luna  初级技神
 楼主|

发表于 2019-5-30 23:40:59

gada888 发表于 2019-5-16 19:39
中断不容易理解。

哪个部分不理解?可以留言
回复

使用道具 举报

杨昌平  学徒

发表于 2021-9-15 10:43:44

这个IRAM 是什么东西?
在这IRAM 函数中还能不能调用正常的函数?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail