跳到主要内容

中断与定时器

中断和定时器是 Arduino 高级编程的核心概念。掌握它们可以实现精确的时间控制、响应外部事件而不阻塞主程序,以及实现多任务处理。

什么是中断

中断是一种机制,当特定事件发生时,处理器会暂停当前正在执行的程序,转而去执行一个特殊的函数(中断服务程序),执行完毕后再返回原来的位置继续执行。

为什么需要中断

传统的事件检测方式是"轮询"——在主循环中不断检查某个条件:

// 轮询方式:一直在检查按钮状态
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
// 处理按钮按下
}
// 其他代码...
}

轮询的问题在于:如果按钮在 delay() 期间被按下,程序会错过这个事件。而中断可以立即响应,不受 delay() 影响。

中断的优势

  • 实时响应:事件发生时立即处理
  • 不阻塞主程序:主循环可以专注做其他事情
  • 精确计时:定时器中断可以实现微秒级精度
  • 低功耗:可以让 CPU 休眠,等待中断唤醒

外部中断

外部中断由外部引脚的电平变化触发。Arduino Uno 有两个外部中断引脚:D2(中断 0)和 D3(中断 1)。

attachInterrupt() 函数

attachInterrupt(interrupt, ISR, mode);
参数说明
interrupt中断号(0 或 1)或使用 digitalPinToInterrupt(pin)
ISR中断服务程序函数名
mode触发模式

触发模式

模式说明
LOW低电平时触发(持续触发)
CHANGE电平变化时触发(上升沿或下降沿)
RISING上升沿触发(从低到高)
FALLING下降沿触发(从高到低)

基本示例:按钮中断

const int BUTTON_PIN = 2;   // 中断引脚 D2
const int LED_PIN = 13;

volatile bool ledState = false; // volatile 关键字很重要!

void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);

// 附加中断:下降沿触发(按钮按下时)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), toggleLED, FALLING);

Serial.begin(9600);
Serial.println("中断测试:按下按钮切换 LED");
}

void loop() {
// 主循环可以做其他事情
Serial.println("主循环运行中...");
delay(1000);
}

// 中断服务程序
void toggleLED() {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}

volatile 关键字

在中断中使用的变量必须声明为 volatile。这告诉编译器该变量可能在任何时候被改变(由中断),因此编译器不会对其进行优化。

// 正确:使用 volatile
volatile int counter = 0;

void ISR() {
counter++; // 安全
}

// 错误:未使用 volatile
int counter = 0; // 编译器可能优化掉这个变量

void ISR() {
counter++; // 可能出现问题
}

中断服务程序的规则

中断服务程序(ISR)必须遵循以下规则:

  1. 要短要快:ISR 应该尽快执行完毕,通常只设置标志位
  2. 不能用 delay()delay() 在中断中不起作用
  3. 慎用 Serial:串口通信可能造成问题
  4. 变量要 volatile:共享变量必须声明为 volatile

改进的按钮中断示例

const int BUTTON_PIN = 2;
const int LED_PIN = 13;

volatile bool buttonPressed = false; // 标志位

void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);

attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);

Serial.begin(9600);
}

void loop() {
// 在主循环中处理事件
if (buttonPressed) {
buttonPressed = false; // 清除标志

// 在这里处理按钮事件
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
Serial.println("按钮被按下");
}

// 其他任务...
}

void buttonISR() {
buttonPressed = true; // 只设置标志,不做其他事情
}

分离中断

使用 detachInterrupt() 可以禁用中断:

// 禁用中断 0(D2)
detachInterrupt(digitalPinToInterrupt(2));

// 或使用中断号
detachInterrupt(0);

各开发板的中断引脚

开发板可用中断引脚
Uno, Nano, MiniD2, D3
MegaD2, D3, D18, D19, D20, D21
LeonardoD0, D1, D2, D3, D7
Due所有数字引脚

定时器中断

定时器中断由内部定时器产生,可以按固定时间间隔触发。这比 delay() 更加精确和灵活。

Arduino Uno 的定时器

Arduino Uno(ATmega328P)有三个定时器:

定时器位数用途
Timer08位millis()delay()、PWM D5/D6
Timer116位PWM D9/D10
Timer28位PWM D3/D11
注意

使用定时器中断可能会影响相关的 PWM 引脚和延时函数。Timer0 控制 millis()delay(),修改它会导致时间函数失效。

使用 TimerOne 库

TimerOne 库简化了 Timer1 的使用。安装方法:项目加载库管理库 → 搜索 "TimerOne"。

#include <TimerOne.h>

const int LED_PIN = 13;
volatile bool ledState = false;

void setup() {
pinMode(LED_PIN, OUTPUT);

// 初始化 Timer1,周期 1 秒(单位:微秒)
Timer1.initialize(1000000); // 1,000,000 微秒 = 1 秒

// 附加中断服务程序
Timer1.attachInterrupt(timerISR);

Serial.begin(9600);
Serial.println("定时器中断测试");
}

void loop() {
// 主循环可以做其他事情
Serial.print("运行时间: ");
Serial.print(millis() / 1000);
Serial.println(" 秒");
delay(500);
}

// 定时器中断服务程序
void timerISR() {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}

改变定时器周期

Timer1.setPeriod(500000);  // 设置周期为 0.5 秒

产生 PWM 信号

TimerOne 可以在 D9 和 D10 产生高精度 PWM:

#include <TimerOne.h>

void setup() {
Timer1.initialize(20000); // 20ms 周期(适合舵机)

// 在 D9 产生 PWM,占空比 10%(舵机中间位置)
Timer1.pwm(9, 512); // 0-1023 范围
}

void loop() {
// 改变占空比
for (int i = 100; i <= 512; i++) {
Timer1.setPwmDuty(9, i);
delay(10);
}
}

直接操作定时器寄存器

对于更精确的控制,可以直接操作寄存器。以下代码设置 Timer2 产生 1kHz 的中断:

void setup() {
Serial.begin(9600);

// 禁用全局中断
cli();

// 重置 Timer2 控制寄存器
TCCR2A = 0;
TCCR2B = 0;
TCNT2 = 0; // 计数器初值

// 设置比较匹配值
// 16MHz / 64 预分频 / 250 = 1000Hz
OCR2A = 249;

// CTC 模式
TCCR2A |= (1 << WGM21);

// 64 预分频
TCCR2B |= (1 << CS22);

// 启用比较匹配中断
TIMSK2 |= (1 << OCIE2A);

// 启用全局中断
sei();

Serial.println("Timer2 配置完成,1kHz 中断");
}

void loop() {
// 主程序
}

// Timer2 比较匹配中断服务程序
ISR(TIMER2_COMPA_vect) {
// 每 1ms 执行一次
// 在这里执行定时任务
}

定时器中断频率计算

定时器中断频率公式:

f中断=f时钟预分频×(比较值+1)f_{中断} = \frac{f_{时钟}}{预分频 \times (比较值 + 1)}

对于 Arduino Uno(16MHz):

预分频比较值中断频率
642491000 Hz (1ms)
641242000 Hz (0.5ms)
256624991 Hz (1s)

引脚变化中断

除了外部中断,Arduino 还支持引脚变化中断(Pin Change Interrupt),可以在更多引脚上检测变化。

PCINT 引脚

Arduino Uno 的引脚变化中断分组:

PCINT 组引脚中断向量
PCINT0D8-D13PCINT0_vect
PCINT1A0-A5PCINT1_vect
PCINT2D0-D7PCINT2_vect

启用引脚变化中断

// 在 D8 上启用引脚变化中断
void setup() {
pinMode(8, INPUT_PULLUP);

// 启用引脚变化中断
PCICR |= (1 << PCIE0); // 启用 PCINT0 组(D8-D13)
PCMSK0 |= (1 << PCINT0); // 启用 D8 的引脚变化检测

Serial.begin(9600);
}

void loop() {
// 主循环
}

// 引脚变化中断服务程序
ISR(PCINT0_vect) {
Serial.println("D8 状态变化");
}

综合示例:精确频率计

使用定时器中断和外部中断制作频率计:

const int INPUT_PIN = 2;  // 测量信号输入

volatile unsigned long pulseCount = 0;
volatile unsigned long frequency = 0;
unsigned long lastUpdate = 0;

void setup() {
Serial.begin(9600);

pinMode(INPUT_PIN, INPUT);

// 配置外部中断(每个脉冲计数)
attachInterrupt(digitalPinToInterrupt(INPUT_PIN), countPulse, RISING);

// 配置 Timer1 每秒触发一次
cli();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 15624; // 16MHz / 1024 / 15625 = 1Hz
TCCR1B |= (1 << WGM12) | (1 << CS12) | (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
sei();

Serial.println("频率计已启动");
}

void loop() {
if (millis() - lastUpdate > 1000) {
Serial.print("频率: ");
Serial.print(frequency);
Serial.println(" Hz");
lastUpdate = millis();
}
}

// 脉冲计数中断
void countPulse() {
pulseCount++;
}

// Timer1 比较匹配中断(每秒)
ISR(TIMER1_COMPA_vect) {
frequency = pulseCount; // 保存计数值
pulseCount = 0; // 重置计数器
}

综合示例:多任务调度器

使用定时器中断实现简单的多任务调度:

#include <TimerOne.h>

// 任务结构体
struct Task {
void (*func)(); // 任务函数
unsigned int period; // 执行周期(ms)
unsigned int counter; // 计数器
};

// 任务列表
Task tasks[] = {
{task1, 100, 0}, // 每 100ms 执行
{task2, 500, 0}, // 每 500ms 执行
{task3, 1000, 0}, // 每 1000ms 执行
};

const int NUM_TASKS = 3;
volatile bool tick = false;

void setup() {
Serial.begin(9600);

// 初始化 Timer1,1ms 中断
Timer1.initialize(1000);
Timer1.attachInterrupt(timerTick);

Serial.println("多任务调度器启动");
}

void loop() {
if (tick) {
tick = false;
scheduler();
}
}

// 定时器中断(1ms)
void timerTick() {
tick = true;
}

// 任务调度器
void scheduler() {
for (int i = 0; i < NUM_TASKS; i++) {
tasks[i].counter++;
if (tasks[i].counter >= tasks[i].period) {
tasks[i].counter = 0;
tasks[i].func();
}
}
}

// 任务 1:读取传感器
void task1() {
int value = analogRead(A0);
// 处理传感器数据...
}

// 任务 2:更新显示
void task2() {
Serial.print("时间: ");
Serial.print(millis() / 1000);
Serial.println("s");
}

// 任务 3:检查状态
void task3() {
// 检查系统状态...
}

中断优先级

当多个中断同时发生时,需要了解优先级:

  1. 复位(最高优先级)
  2. 外部中断
  3. 定时器中断
  4. 串口中断
  5. 其他中断

中断不能被同优先级的中断打断,但可以被更高优先级的中断打断。

常见问题

1. 中断中使用 Serial 打印异常

原因:Serial 本身使用中断,在中断中使用可能导致冲突。

解决:在 ISR 中只设置标志,在主循环中打印。

2. 变量值不正确

原因:共享变量未声明为 volatile

解决:在 ISR 中使用的变量必须声明为 volatile

3. 定时器中断影响 millis()

原因:修改了 Timer0。

解决:避免修改 Timer0,使用 Timer1 或 Timer2。

4. 中断丢失

原因:中断服务程序执行时间太长。

解决:保持 ISR 简短,只做必要的操作。

调试技巧

测量中断响应时间

const int TEST_PIN = 9;

void setup() {
pinMode(TEST_PIN, OUTPUT);
attachInterrupt(digitalPinToInterrupt(2), testISR, FALLING);
}

void loop() {
// 触发测试:D2 连接到 GND
}

void testISR() {
// 立即拉高测试引脚
digitalWrite(TEST_PIN, HIGH); // 用示波器测量延迟
digitalWrite(TEST_PIN, LOW);
}

统计中断频率

volatile unsigned long isrCount = 0;
unsigned long lastPrint = 0;

void setup() {
Serial.begin(9600);
// 配置中断...
}

void loop() {
if (millis() - lastPrint > 1000) {
Serial.print("中断频率: ");
Serial.print(isrCount);
Serial.println(" Hz");
isrCount = 0;
lastPrint = millis();
}
}

void myISR() {
isrCount++;
}

下一步

掌握了中断和定时器后,你已经具备了开发复杂 Arduino 项目的能力。接下来我们将学习如何持久化存储数据。