一、为什么需要进阶?
普通 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 中断
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 配置(相位修正模式)
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 结构:
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 占空比。
// 引脚定义
#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 算法则让系统拥有自适应调节能力。掌握这三者,你便能向自平衡机器人、温控仪、云台稳定器等复杂项目迈出一大步。