说明
说起单片机或者嵌入式系统,定时器应该可以说是不可不提的内容,下面以具体的代码来介绍ESP32的定时器如何使用,一般而言在实现中断处理代码时,最好让ISR仅对中断进行响应,然后把实际的处理(可能包含时间较长的操作)交给主循环来做。按照惯例,先介绍一下环境吧:
硬件:TTGO T-Display ESP32带1.14LCD的小开发板 + 编码器
软件:VSCode + PlatformIO IDE(其实就是Arduino环境)
程序详解
关于定时器的初始化大概只有四五个函数,下面直接每一段单独放出来,有详细的注释。
timerBegin()
1 2 3 4 5 6 7
| // 函数名称:timerBegin() // 函数功能:Timer初始化,分别有三个参数 // 函数输入:1. 定时器编号(0到3,对应全部4个硬件定时器) // 2. 预分频器数值(ESP32计数器基频为80M,80分频单位是微秒) // 3. 计数器向上(true)或向下(false)计数的标志 // 函数返回:一个指向 hw_timer_t 结构类型的指针 timer = timerBegin(0, 80, true);
|
timerAttachInterrupt()
1 2 3 4 5 6 7
| // 函数名称:timerAttachInterrupt() // 函数功能:绑定定时器的中断处理函数,分别有三个参数 // 函数输入:1. 指向已初始化定时器的指针(本例子:timer) // 2. 中断处理函数的地址 // 3. 表示中断触发类型是边沿(true)还是电平(false)的标志 // 函数返回:无 timerAttachInterrupt(timer, &onTimer, true);
|
timerAlarmWrite()
1 2 3 4 5 6 7
| // 函数名称:timerAlarmWrite() // 函数功能:指定触发定时器中断的计数器值,分别有三个参数 // 函数输入:1. 指向已初始化定时器的指针(本例子:timer) // 2. 第二个参数是触发中断的计数器值(1000000 us -> 1s) // 3. 定时器在产生中断时是否重新加载的标志 // 函数返回:无 timerAlarmWrite(timer, 1000000, true);
|
timerAlarmEnable()
1
| timerAlarmEnable(timer); // 使能定时器
|
然后是中断服务函数ISR:
1 2 3 4 5 6
| // 函数名称:onTimer() // 函数功能:中断服务的功能,它必须是一个返回void(空)且没有输入参数的函数, // 为使编译器将代码分配到IRAM内,中断处理程序应该具有 IRAM_ATTR 属性 void IRAM_ATTR onTimer() { Serial.println(interruptCounter++); }
|
最后是主函数,啥都不做:
函数的结果:每秒输出一个自加的 interruptCounter,具体如下图所示:
结构优化
如前文所述,在ISR对中断做出响应之后,真正的定时器中断处理操作其实是在主循环中,更有效的处理方式是,使用一个信号量将主循环锁定,然后在ISR中将其解锁。具体操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
volatile int interruptCounter; int totalInterruptCounter; hw_timer_t * timer = NULL; portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// 函数名称:onTimer() // 函数功能:中断服务的功能,它必须是一个返回void(空)且没有输入参数的函数, // 为使编译器将代码分配到IRAM内,中断处理程序应该具有 IRAM_ATTR 属性 void IRAM_ATTR onTimer() { // Serial.println(interruptCounter++); portENTER_CRITICAL_ISR(&timerMux); interruptCounter++; portEXIT_CRITICAL_ISR(&timerMux); } void setup() { Serial.begin(115200);
// 函数名称:timerBegin() // 函数功能:Timer初始化,分别有三个参数 // 函数输入:1. 定时器编号(0到3,对应全部4个硬件定时器) // 2. 预分频器数值(ESP32计数器基频为80M,80分频单位是微秒) // 3. 计数器向上(true)或向下(false)计数的标志 // 函数返回:一个指向 hw_timer_t 结构类型的指针 timer = timerBegin(0, 80, true);
// 函数名称:timerAttachInterrupt() // 函数功能:绑定定时器的中断处理函数,分别有三个参数 // 函数输入:1. 指向已初始化定时器的指针(本例子:timer) // 2. 中断处理函数的地址 // 3. 表示中断触发类型是边沿(true)还是电平(false)的标志 // 函数返回:无 timerAttachInterrupt(timer, &onTimer, true);
// 函数名称:timerAlarmWrite() // 函数功能:指定触发定时器中断的计数器值,分别有三个参数 // 函数输入:1. 指向已初始化定时器的指针(本例子:timer) // 2. 第二个参数是触发中断的计数器值(1000000 us -> 1s) // 3. 定时器在产生中断时是否重新加载的标志 // 函数返回:无 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); } }
|
总结
关于定时器,我个人觉得使用环境真的是非常多,也希望大家能够多看看,下面贴大佬的链接,可以多多参考。