一、为什么需要进阶?

普通 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 算法则让系统拥有自适应调节能力。掌握这三者,你便能向自平衡机器人、温控仪、云台稳定器等复杂项目迈出一大步。