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仅对中断进行响应,然后把实际的处理(可能包含时间较长的操作)交给主循环来做。

]portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

设置函数

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

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,那么当我们调用函数对计数器值进行设置以产生中断时,我们所指定的数值就是以微秒为单位。

timer = timerBegin(0, 80, true);

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

timerAttachInterrupt(timer, &onTimer, true);

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

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

timerAlarmWrite(timer, 1000000, true);

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

timerAlarmEnable(timer); 

void setup() {
  Serial.begin(115200);
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);
}

主循环

如前文所述,在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全局变量的地址。

if (interruptCounter > 0) {
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
    // Interrupt handling code
  }

实际的中断处理操作只是对计数器(计数器数值表示自程序运行以来所发生的中断总数)进行递减,并将计数器值输出到串口。完整的主循环代码如下所示,其中已经包含了该函数调用。

void loop() {
  if (interruptCounter > 0) {
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
    totalInterruptCounter++;
    Serial.print("An interrupt as occurred. Total number: ");
    Serial.println(totalInterruptCounter);
  }
}

ISR代码

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

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

最终代码

周期计数器中断程序的最终源代码如下所示。

volatile int interruptCounter;
int totalInterruptCounter;
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
}
void setup() {
  Serial.begin(115200);
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);
}
void loop() {
  if (interruptCounter > 0) {
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
    totalInterruptCounter++;
    Serial.print("An interrupt as occurred. Total number: ");
    Serial.println(totalInterruptCounter);
  }
}

测试代码

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

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

注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。
他写了200多篇有关ESP32、ESP8266的有用的教程和项目。涉及arduino、micropython、 Picoweb、Espruino、Bluetooth、RFID、IDF……等等非常广泛,说是最全的完全不为过。

精华教程:

ESP32 MicroPython教程:uPyCraft IDE入门
ESP32 MicroPython教程:解析JSON
ESP32 MicroPython教程:MicroPython支持
ESP32 MicroPython教程:连接Wi-Fi网络
ESP32 / ESP8266 MicroPython教程:自动连接WiFi
ESP32 / ESP8266 MicroPython教程:从文件系统运行脚本
ESP32 / ESP8266 MicroPython教程:HTTP GET请求
ESP32 Arduino教程:用于构建ESP32编译环境的Arduino IDE软件
ESP32 Arduino教程:FreeRTOS队列性能测试
ESP32 RFID教程:打印MFRC522固件版本
ESP32 Picoweb教程:获取请求的HTTP方法
……

还有更多教程: ESP32教程 合集

英文版 :ESP32 tutorial合集

你可能感兴趣的:(ESP32,arduino)