Arduino 进阶:定时器中断、PWM 与 PID 控制的深度融合
👁 3 阅读 · 2026-08-14 · Arduino 进阶
本文面向具备一定基础的嵌入式开发者,深入探讨 Arduino 平台上的定时器中断、硬件 PWM 以及 PID 控制算法的协同设计。通过剖析 AVR 内核定时器工作原理,演示如何配置 CTC 模式产生精确中断,并利用 PWM 输出驱动执行器,最后结合增量式 PID 实现闭环控制。文章提供完整代码示例与工程注意事项,帮助读者摆脱 delay() 阻塞式编程,掌握实时控制系统的核心技巧。
## 一、为什么需要进阶?
普通 Arduino 编程中,`delay()` 和 `analogWrite()` 用起来简单,但无法满足高精度实时控制的需求。定时器中断让你获得微秒级的执行节奏,硬件 PWM 提供无 jitter 的波形,PID 则是闭环控制的灵魂。三者结合,才能构建真正的运动控制、温控或平衡机器人。
## 二、定时器中断原理与配置
AVR 芯片(如 ATmega328P)内置 Timer0、Timer1、Timer2。其中 Timer0 已被 Arduino 的 `millis()` 占用,所以进阶应用推荐使用 **Timer1(16位)** 或 **Timer2(8位)**。
### 1. CTC 模式(Clear Timer on Compare Match)
计数器从 0 递增到 OCRnx 时,自动清零并触发比较匹配中断。中断频率公式:
`f_interrupt = F_CPU / (2 * N * (OCRnA + 1))`
N 为预分频系数(1、8、64、256、1024)。
### 2. 配置 Timer1 产生 1kHz 中断
```c
void setupTimer1() {
cli(); // 关全局中断
TCCR1A = 0; // 正常端口操作
TCCR1B = 0;
TCNT1 = 0; // 计数器清零
OCR1A = 1999; // 16MHz / (2*8*2000) = 1kHz
TCCR1B |= (1 << WGM12); // CTC 模式
TCCR1B |= (1 << CS11); // 预分频 8
TIMSK1 |= (1 << OCIE1A); // 使能比较匹配中断
sei(); // 开全局中断
}
ISR(TIMER1_COMPA_vect) {
// 放置实时任务,如 PID 计算、传感器采样
pidCompute();
}
```
## 三、硬件 PWM:超越 analogWrite
`analogWrite()` 实际是软件模拟 PWM,频率固定约 490Hz,且占用定时器中断资源。进阶场景应使用硬件 PWM,通过寄存器直接配置,可调整频率、极性和分辨率。
### 1. Timer1 的 PWM 配置(相位修正模式)
```c
void setupPWM() {
// 设置 PB1 (Pin 9) 为输出
DDRB |= (1 << PB1);
// 相位修正 PWM,顶部值 ICR1
ICR1 = 159; // 20kHz 基准频率
TCCR1A |= (1 << WGM11);
TCCR1B |= (1 << WGM13);
// 非反向 PWM,输出 PB1
TCCR1A |= (1 << COM1A1);
// 预分频 1,PWM 频率 = 16MHz / (2*160) ≈ 50kHz
TCCR1B |= (1 << CS10);
OCR1A = 80; // 占空比 50%
}
```
注意:使用 Timer1 PWM 时,其比较中断会与 PWM 共享同一寄存器,需合理规划。推荐 **Timer1 只用于 PWM,Timer2 用于中断** 以避免冲突。
### 2. 标准库 API 与自定义兼容
Arduino 的 `analogWriteFrequency()`(仅限 Due/Zero)在 AVR 上不可用。若想变更频率,必须直接操作寄存器。这也意味着你的代码不再跨平台,但换来的是性能和可控性。
## 四、PID 控制核心实现
PID 在嵌入式端通常采用位置式或增量式。增量式输出控制量增量,适合电机调速;位置式输出绝对值,适合舵机或加热器。以下为增量式 PID 结构:
```c
struct PID {
float Kp, Ki, Kd;
float target;
float integral;
float prevError;
};
float pidUpdate(struct PID *pid, float measurement, float dt) {
float error = pid->target - measurement;
float P = pid->Kp * error;
pid->integral += error * dt;
float I = pid->Ki * pid->integral;
float derivative = (error - pid->prevError) / dt;
float D = pid->Kd * derivative;
pid->prevError = error;
return P + I + D;
}
```
### 在中断中调用 PID
将 PID 计算放入定时器中断,保证固定采样周期,避免 `delay()` 导致的时间漂移。注意中断中的代码要短且快,避免浮点运算过重(可改用浮点但保持简单)。
## 五、完整示例:直流电机速度闭环
电路:直流电机 + 编码器(或霍尔传感器)接入 Pin 2(中断0),PWM 输出 Pin 9(Timer1)。目标转速设定后,通过 PID 调整 PWM 占空比。
```c
// 引脚定义
#define ENC_PIN 2
#define PWM_PIN 9
volatile long encoderCount = 0;
float targetRPM = 3000;
float pwmValue = 0;
// 编码器中断,每转 20 脉冲,测速周期 10ms
ISR(INT0_vect) {
encoderCount++;
}
// 精确 2ms 定时器中断 (Timer2 CTC)
void setupTimer2() {
TCCR2A = 0;
TCCR2B = 0;
TCNT2 = 0;
OCR2A = 124; // 16MHz / (128 * (125)) = 1kHz → 1ms
TCCR2A |= (1 << WGM21); // CTC
TCCR2B |= (1 << CS22); // 预分频 64
TIMSK2 |= (1 << OCIE1A); // 注意:Timer2 比较 A
}
volatile int pidTickCounter = 0;
ISR(TIMER2_COMPA_vect) {
pidTickCounter++;
if (pidTickCounter >= 5) { // 5ms 执行一次 PID
pidTickCounter = 0;
// 测速:每 5ms 的脉冲数换算 RPM
static long prevCount = 0;
long diff = encoderCount - prevCount;
prevCount = encoderCount;
float rpm = (diff / 20.0) / (0.005) * 60.0; // 20 脉冲/转
// PID 计算
float error = targetRPM - rpm;
static float integral = 0;
static float prevErr = 0;
integral += error * 0.005;
float output = 0.05 * error + 0.01 * integral + 0.001 * (error - prevErr)/0.005;
prevErr = error;
// 输出限制 0-255
output = constrain(output, 0, 255);
OCR1A = output; // 直接写 PWM 寄存器
}
}
void setup() {
pinMode(ENC_PIN, INPUT_PULLUP);
pinMode(PWM_PIN, OUTPUT);
// 外部中断 0 触发方式:上升沿
EICRA |= (1 << ISC01);
EIMSK |= (1 << INT0);
setupTimer2(); // 启动定时器中断
setupTimer1PWM(); // 配置 Timer1 PWM,见上文
Serial.begin(115200);
}
void loop() {
// 主循环只负责显示和参数调整
static unsigned long lastPrint = 0;
if (millis() - lastPrint > 100) {
lastPrint = millis();
Serial.println(targetRPM);
}
}
```
## 六、注意事项与调参建议
- **中断优先级与嵌套**:AVR 中断不支持嵌套。若有多个中断,必须保证 ISR 执行时间小于最短中断周期,否则会丢事件。
- **共享寄存器冲突**:Timer1 的 OCR1A 同时控制 PWM 比较值,若还需使用比较中断,应使用 OCR1B。本例仅用 PWM,故安全。
- **浮点性能**:ATmega328P 无硬件浮点单元。PID 计算中的浮点运算在 8MHz 下耗时较长,可将采样周期放宽至 5ms 以上,或改用整数定标 PID。
- **抗积分饱和**:输出被钳位时,应暂停积分累加,防止系统恢复时超调。
- **编码器噪声**:电机电刷会产生电磁干扰,务必在编码器电源端加去耦电容,信号线使用屏蔽双绞线。
- **调试方法**:先用固定 PWM 开环验证电机响应,再引入 PID。调节时先调 P,再调 I,最后用 D 抑制振荡。
## 七、总结
通过寄存器级编程,Arduino 不再局限于学习玩具,而能胜任实时控制任务。定时器中断提供了精确的时间基准,硬件 PWM 保证了输出质量,PID 算法则让系统拥有自适应调节能力。掌握这三者,你便能向自平衡机器人、温控仪、云台稳定器等复杂项目迈出一大步。