ESP32S3-Arduino外部中断与定时器

外部中断与定时器

1 外部中断

1.1 什么是中断

  • 在单片机系统中,中断是一种机制。它允许单片机在执行正常程序的过程中,暂停当前的任务,转而去处理其它更为紧急或者有更高优先级的事件。当这个紧急事件处理完毕后,单片机又能够回到原来被中断的地方,继续执行之前的程序,就好像被打断的工作又重新开始一样。
  • 例如,想象你正在房间里看书(这就相当于单片机在执行主程序),突然电话铃响了(这就是一个中断事件)。你放下书本去接电话(处理中断),接完电话后,你又能回到刚才看书的地方继续阅读(返回主程序)。

1.2 外部中断介绍

  • 外部中断是指由单片机外部设备或信号触发的中断。它提供了一种让单片机能够及时响应外部事件的方式,使单片机可以和外部世界进行有效的交互。

  • 外部中断是硬件中断的一种,它由微控制器外部的事件引发。微控制器的某些引脚被设计为对特定事件的发生做出响应,例如按钮的按压、传感器的信号改变等。这些指定的引脚通常被称为“外部中断引脚”。

  • 之前做按键实验时,现有代码靠loop循环持续检测 IO 口来读取 GPIO 口输入。若后续添加大量代码,轮询到按键检测部分耗时久、效率低。比如特定场合下,按键可能 1 天才按一次,持续检测浪费大量时间。为解决该问题,引入外部中断概念,即仅在按键按下产生中断时,才执行相关功能。这大幅节省 CPU 资源,所以中断在实际项目中应用广泛 。

  • ESP32S3 的外部中断有上升沿触发、下降沿触发、低电平触发、高电平触发模式(可以在Arduino.h中查找到)。上升沿和下降沿触发如下:

111

1.3 外部中断的作用和优势

实时响应外部事件 提高系统的效率 实现多任务处理的协同 灵活的触发方式适应多种设备

1.4 使用外部中断流程

1.4.1配置引脚

因为外部中断实际上还是读取引脚的状态,所以需要将引脚设置为输入或启用上拉电阻输入模式;在 setup() 函数中,使用 pinMode() 函数将引脚配置为输入引脚:

pinMode(pin, INPUT);pinMode(pin, INPUT_PULLUP);

1.4.2将引脚绑定中断服务程序并设置触发中断方式

int IntPin = 0;
void ISR()
{
//省略代码...
}
void setup()
{
// 配置中断引脚为GPIO5,假设外部中断回调函数为 ISR(), 中断下降沿触发
attachInterrupt(digitalPinToInterrupt(IntPin), ISR, FALLING);
// 或者
attachInterrupt(IntPin, ISR, FALLING);
}

注意attachInterrupt()函数只适用于 digitalPinToInterrupt() 所支持的GPIO引脚,而不是所有的GPIO引脚都能用于外部中断。此外,在中断服务函数进行中断处理时,一定要避免使用占用 CPU 大量时间的操作(例如延时函数),以确保中断响应速度和精度。

attachInterrupt() 函数有两种参数传递方式,可以传递函数指针或者函数名。如果是函数名,可以直接传递函数名,不需要加上括号。如果函数名被加上了括号,那就相当于调用该函数,传递的则是函数的返回值。如果使用函数指针,那么需要在函数名前加上&符号。

1.4.3处理中断事件

a. 尽量保证中断程序内容少

b. 避免在中断处理函数中使用阻塞函数(如 delay()),使用非阻塞的延迟方法来处理需要延迟的操作(micros() 函数),以保证中断的正常执行和系统的稳定性。这是因为 delay() 函数会阻塞整个系统,包括中断的正常执行。当中断触发时,处理函数应该尽快执行完毕,以确保及时响应**并避免中断积压;

c. 与主程序共享的变量要加上 volatile 关键字;

d. 在 Arduino 中使用中断时,应尽量避免在中断处理函数中使用 Serial 串口对象的打印函数。当在中断处理函数中使用 Serial 打印函数时,会导致以下问题:

  • 时间延迟:Serial 打印函数通常是比较耗时的操作,它会阻塞中断的执行时间,导致中断响应的延迟。这可能会导致在中断期间丢失其他重要的中断事件或导致系统不稳定。
  • 缓冲区溢出:Serial 对象在内部使用一个缓冲区来存储要发送的数据。如果在中断处理函数中频繁调用 Serial 打印函数,可能会导致缓冲区溢出,造成数据丢失或不可预测的行为。

为了避免这些问题,建议在中断处理函数中尽量避免使用 Serial 打印函数。如果需要在中断处理函数中输出调试信息,可以使用其他方式,如设置标志位,在主循环中检查标志位并进行打印。

1.4.4示例代码与现象

按钮控制LED灯的状态,但是外部中断

示例程序1:

#include <Arduino.h>
// 将按钮一端连接到GPIO4引脚,另一端连接到GND,和板子共地
// 将LED正极接GPIO15,负极接地
#define Button 4
#define LED 15
void buttonISR()
{
static bool state = false;
digitalWrite(LED, state = !state);
}
void setup()
{
Serial.begin(115200);
pinMode(Button, INPUT_PULLUP);
pinMode(LED, OUTPUT);
attachInterrupt(Button, buttonISR, FALLING);
delay(2000);
}
void loop() {
delay(1000);
}

示例程序2:

#include <Arduino.h>
#define BUTTON 4
#define LED 15
//LED状态
bool led_flag = false;
//按键状态(中断回调函数与主程序共享的变量要加上 volatile 关键字)
volatile bool flag = false;
//中断回调函数
void ISR()
{
flag = true;
}
void setup()
{
//设置KEY引脚(0) 为上拉输入模式
pinMode(BUTTON, INPUT_PULLUP);
//设置LED引脚(48) 为输出模式
pinMode(LED, OUTPUT);
// 配置中断引脚为GPIO0,外部中断回调函数为 ISR(), 中断下降沿触发
attachInterrupt(digitalPinToInterrupt(BUTTON), ISR, FALLING);
}
void loop()
{
//当按键按下时会产生一个下降沿,进入回调函数 ISR(),而ISR()中只是将flag = true
//故当flag == true 时,说明按键按下
if ( flag == true )
{
//延时200毫秒
delay(200);
//LED状态取反
digitalWrite(LED, led_flag=!led_flag);
// 重置中断标志位
flag = false;
}
}

2. 定时器

2.1 定时器介绍

定时器是单片机内部集成,可以通过编程控制。单片机的定时功能是通过计数来实现的,当单片机每一个机器周期产生一个脉冲时,计数器就加一。定时器的主要功能是用来计时,时间到达之后可以产生中断,提醒计时时间到,然后可以在中断函数中去执行功能。比如我们想让一个 led 灯 1 秒钟翻转一次,就可以使用定时器配置为 1 秒钟触发中断,然后在中断函数中执行 led 翻转的程序。

  • 硬件定时器:
    硬件定时器依托微控制器硬件,通过专门的计时 / 计数器电路,为系统提供定时功能。其最大的亮点就是高精度与高可靠性,这源于它独立于软件任务以及操作系统调度机制。在对定时精度要求极高的场景,比如生成 PWM 信号,或是进行精准时间测量,硬件定时器无疑是最佳之选。定时工作由硬件直接把控,即便主 CPU 被其他任务占满,定时器也能在预设时间一到,丝毫不差地触发回调操作 。
  • 软件定时器:
    软件定时器是借助操作系统或软件库达成的定时器。它借助操作系统所提供的相关机制,对定时器功能加以模拟。由于软件定时器的运行依赖于系统当前的负载状况以及任务调度策略,所以与硬件定时器相比,其精准度稍显逊色。然而,软件定时器往往具备更高的灵活性,能够同时创建多个定时器,在那些对时间控制精度要求不高的场景中,软件定时器的适用性更强。

2.2 定时器基本参数

ESP32S3芯片集成了丰富的硬件定时器资源,诸如Timer0、Timer1、Timer2、Timer3 等。每个定时器均配备多个通道,用户可通过指定具体的定时器号与通道号,灵活选择所需使用的定时器及其通道。 在操作硬件定时器时,需关注一些基本的定时器参数,其中包括定时器号、通道号、预分频器、自动重新加载值以及定时器中断使能等。下面是一些相关的基本概念及定时器的共有属性:

  • 计时器(Counter):作为定时器的核心构成部分,其作用是持续不断地进行计数操作,为定时器的各项功能提供基础数据支持。
  • 定时器溢出(Overflow):当计数器累加到其所能表示的最大值后,会自动归零,这一现象即被称为定时器溢出。该事件通常会触发一系列后续动作,比如标志位的变更等。
  • 预置值(Preset Value):当计数器的计数值达到此预置值时,便会引发特定事件,如产生中断信号,从而通知系统执行相应的处理流程。
  • 分频器(Prescaler):其主要功能是降低计数器所接收的时钟信号频率。通过分频操作,能够有效延长定时器的最大计时范围,满足不同场景下对定时时长的多样化需求。
  • 中断(Interrupt):用户可对定时器进行配置,使其在计数值达到预置值时产生中断信号。一旦触发中断,系统会立即暂停当前正在执行的任务,转而执行预先设定好的中断处理程序,在该程序中完成特定的任务。

2.3 定时器的使用流程

2.3.1 创建定时器对象

在程序的顶部声明一个hw_timer_t 类型的全局变量,用于表示定时器对象。

hw_timer_t *timer = NULL;

2.3.2 初始化定时器

使用示例:

timer = timerBegin(0,80,true);

函数讲解:

hw_timer_t * timerBegin(uint8_t num, uint16_t divider, bool countUp)
// uint8_t num: 定时器编号,可选值为 0-3 等。
// uint16_t divider: 定时器的分频系数,用于设置定时器的时钟频率。较大的分频系数将降低定时器的时钟频
// 率。可以根据需要选择合适的值,一般设置为 80 即可;
// bool countUp: 指定定时器是否为向上计数模式。设置为 true 表示向上计数,设置为 false 表示向下计数。

2.3.3 注册中断处理函数

void timer_INT()
{
//命名符合C/C++标准即可
}

2.3.4 配置定时器

使用示例:

timerAttachInterrupt(timer,timer_INT,true);

函数讲解:

void timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge)
// hw_timer_t *timer: 定时器指针
// void (*fn)(void): 中断处理函数
// bool edge: 用于指定中断触发的边缘类型,可选值为 ture(边沿触发)或 false(电平触发)

2.3.5 设置定时模式并启动定时器

使用示例:

// 定时模式,单位us,只触发一次
timerAlarmWrite(timer,1000000,false);
// 定时模式,单位us,自动重装
timerAlarmWrite(timer,1000000,true);
// 启动定时器
timerAlarmEnable(timer);

函数讲解:

void timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload)
// hw_timer_t *timer: 定时器指针
// uint64_t alarm_value: 定时器的计数值,即触发时间间隔,单位us
// bool autoreload: 是否自动重载计数值,可选值为 true(自动重载)或 false(单次触发)
void timerAlarmEnable(hw_timer_t *timer)

2.4 示例代码

这个示例代码的效果是每隔100ms改变灯的状态

#include <Arduino.h>
#define LED 15
hw_timer_s *timer = NULL;
void timerINT()
{
static bool state = false;
digitalWrite(LED, state = !state);
}
void setup()
{
pinMode(LED, OUTPUT);
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, timerINT, true);
timerAlarmWrite(timer, 0.1*1000000, true);// 0.1 * 1000000us = 0.1s = 100ms
timerAlarmEnable(timer);
}
void loop()
{
// 循环不建议空跑,放个延时函数
delay(1000);
}

最后是软件定时器的示例代码
比较简单,就不多解释了。

#include <Arduino.h>
#include <Ticker.h> // 这个是ESP32 Arduino固件自带的库
#define LED 15
Ticker ticker;
void timerINT()
{
static bool state = false;
digitalWrite(LED, state = !state);
}
void setup()
{
pinMode(LED, OUTPUT);
ticker.once_ms(2000, timerINT);// 2000ms后执行一次timerINT, 之后就不再执行
ticker.attach(3, timerINT);// 每隔1s执行一次timerINT
}
void loop()
{
// 循环不建议空跑,放个延时函数
delay(1000);
}
posted @   ouyanglingle  阅读(64)  评论(0编辑  收藏  举报
编辑推荐:
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
· .NET 进程 stackoverflow异常后,还可以接收 TCP 连接请求吗?
阅读排行:
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 在缓慢中沉淀,在挑战中重生!2024个人总结!
· 大人,时代变了! 赶快把自有业务的本地AI“模型”训练起来!
· 从 Windows Forms 到微服务的经验教训
点击右上角即可分享
微信分享提示