引言

在Arduino平台上,delayMicroseconds()虽能提供微秒级延时,但其基于软件循环,极易被中断打断,无法保证脉冲序列的精确性。硬件定时器则通过独立计数与比较匹配,在硬件层面生成精确波形,是高频脉冲输出的首选方案。然而,要真正发挥其威力,必须深入理解其边界条件。

硬件定时器基础

定时器与预分频器

Arduino Uno(ATmega328P)拥有三个定时器:Timer0(8位)、Timer1(16位)、Timer2(8位)。其中Timer0已被系统用于millis(),不建议占用。Timer1和Timer2均可用于脉冲生成,但分辨率不同。

  • Timer1:16位计数器,最大计数值65535,适合较长周期。
  • Timer2:8位计数器,最大255,但支持异步时钟(外部32.768kHz晶振),适合低功耗场景。

预分频器(Prescaler)决定计数时钟频率。例如,16MHz系统时钟下,预分频为8时,计数频率为2MHz,即每0.5μs计数一次,分辨率0.5μs。

比较匹配与波形生成

定时器通过比较寄存器(OCRnx)与计数器值匹配,自动翻转输出引脚(CTC模式)或产生中断。利用CTC模式(Clear Timer on Compare Match),可生成精确方波,无需CPU干预。

边界条件分析

1. 时钟源与预分频的取舍

预分频越小,分辨率越高,但可生成的最大周期越短。以Timer1为例,16MHz无预分频时,分辨率62.5ns,但16位计数器最大周期仅4.096ms。若需更长周期,必须增大预分频,但分辨率随之下降。

关键点:选择预分频时,需同时满足最小脉冲宽度和最大周期需求。例如,生成1μs至10ms的脉冲,预分频8(分辨率0.5μs)可覆盖,但若需0.1μs分辨率,则必须无预分频,且周期上限受限。

2. 定时器分辨率与溢出

8位定时器(Timer2)在预分频1时,计数到255仅需16μs(16MHz),若需更长周期,需在中断中软件扩展计数,但中断延迟会引入抖动。16位定时器则更灵活,但需注意溢出中断(TOV)与比较匹配中断(OCF)的优先级。

3. 中断延迟的不可预测性

若使用中断服务程序(ISR)来翻转引脚,则脉冲间隔受中断响应时间影响。AVR中断响应典型为10-20个时钟周期(约0.6-1.25μs@16MHz),但若其他中断正在执行,延迟可能更长。因此,高精度脉冲序列应使用硬件输出比较(OCnx引脚自动翻转),而非ISR软件翻转

4. 代码执行时间与原子性

在修改定时器寄存器时,若被中断打断,可能导致中间状态。例如,写入16位寄存器TCNT1时,需先写高字节再写低字节,期间若发生比较匹配,可能产生错误波形。因此,需使用原子操作(如关闭中断)或利用临时寄存器。

5. 引脚复用与输出极性

OC1A(Pin9)和OC1B(Pin10)可自动翻转,但需配置为输出模式。同时,注意引脚是否被其他外设占用(如SPI、I2C)。

实战:使用Timer1生成精确脉冲序列

需求

生成频率为10kHz(周期100μs)的方波,占空比50%,要求抖动小于±0.5μs。

配置步骤

  1. 设置引脚:将Pin9设为输出。
  2. 配置Timer1为CTC模式
    • 设置TCCR1A的WGM12位(CTC模式)。
    • 选择预分频:系统时钟16MHz,预分频8,计数频率2MHz,周期0.5μs。
    • 计算比较值:周期100μs / 0.5μs = 200,故OCR1A = 199(计数从0到199,共200个时钟)。
  3. 使能比较匹配中断(可选,用于同步其他操作,但波形输出不依赖中断)。
  4. 启动定时器:设置CS10位(预分频8)。

完整代码示例

// 使用Timer1 CTC模式生成10kHz方波于Pin9
// 无需中断,硬件自动翻转

void setup() {
  // 设置Pin9为输出(OC1A)
  DDRB |= (1 << PB1); // Pin9是PB1

  // 配置Timer1
  TCCR1A = 0; // 清零
  TCCR1B = 0;
  TCNT1 = 0; // 初始计数

  // CTC模式:WGM12=1,WGM13=0,WGM11:10=0
  TCCR1B |= (1 << WGM12);

  // 比较值:周期100μs,预分频8,计数频率2MHz,OCR1A = 100μs * 2MHz - 1 = 199
  OCR1A = 199;

  // 设置OC1A在比较匹配时翻转(COM1A0=1,COM1A1=0)
  TCCR1A |= (1 << COM1A0);

  // 启动定时器,预分频8(CS11=1)
  TCCR1B |= (1 << CS11);
}

void loop() {
  // 主循环可执行其他任务,脉冲生成由硬件完成
}

代码说明

  • 通过COM1A0位设置OC1A在比较匹配时自动翻转,无需软件干预。
  • 预分频8,分辨率0.5μs,满足±0.5μs抖动要求。
  • 注意:若需改变频率,需重新计算OCR1A,并注意写入时的原子性(此处为16位寄存器,但OCR1A的写入在CTC模式下是缓冲的,比较匹配时更新,故安全)。

边界条件测试与验证

使用逻辑分析仪或示波器测量实际波形。测试不同预分频和比较值,观察抖动。若发现抖动,可能原因:

  • 电源噪声或引脚负载过大。
  • 其他中断干扰(但硬件输出不受影响)。
  • 定时器寄存器被意外修改(如库函数)。

注意事项

  • 避免使用delay()或millis():它们依赖Timer0,可能干扰系统时钟。
  • 关闭全局中断:在修改关键寄存器时,使用cli()sei()保护。
  • 考虑时钟源精度:Arduino板载晶振精度通常为±20ppm,对微秒级影响不大,但若需更高精度,需外部晶振或温补晶振。
  • 多任务冲突:若同时使用多个定时器,需规划好资源。

总结

利用硬件定时器生成微秒级脉冲序列,关键在于理解预分频、比较匹配和中断的边界条件。通过合理选择预分频和利用硬件输出比较,可避免中断延迟,实现高精度波形。本文提供的配置方法可扩展至任意频率,只需调整OCR值。嵌入式开发中,细节决定成败,掌握这些边界条件,你将能自信应对更复杂的时序需求。