AVR单片机TCB定时器:输入捕获、单次触发与PWM模式实战详解

AVR单片机TCB定时器:输入捕获、单次触发与PWM模式实战详解

1. 从“定时”到“捕获”:TCB定时器的角色演变

在嵌入式开发,尤其是AVR单片机项目中,定时器是驱动一切周期性任务、测量时间间隔、生成精确波形的核心引擎。我们最熟悉的往往是TC0、TC1这类通用定时器,用于延时、PWM输出等常规操作。然而,当项目需求变得复杂,比如需要精确测量一个外部脉冲的宽度、在特定延迟后执行一次高精度动作,或者需要更灵活的PWM生成时,通用定时器有时会显得捉襟见肘。这时,AVR DA/DB/DD等新系列单片机中引入的TCB(Timer/Counter Type B)定时器,就成为了解决问题的利器。

TCB定时器被设计为一种“增强型”定时器,它剥离了TC0/TC1上一些复杂的多模式功能,专注于三件事:高精度的输入捕获、可靠的单次触发(One-Shot)以及简洁的PWM生成。它的设计哲学是“专精而非全能”,这使得它在特定场景下比通用定时器更高效、更易用。很多开发者初次接触TCB时,可能会被其相对简单的寄存器配置所迷惑,以为它功能有限。但恰恰是这种简洁性,让它避免了模式冲突和配置陷阱,在输入捕获和单次定时任务中表现出了极高的稳定性和精度。

我个人在多个电机测速、超声波测距和精密定时开关项目中,都深度依赖TCB。例如,在测量直流电机编码器脉冲频率时,TCB的输入捕获模式配合事件系统,可以几乎无开销地记录下脉冲边沿的精确时刻,避免了在中断中处理大量代码带来的时间抖动。这种“把专业的事交给专业的硬件”的思路,正是TCB的价值所在。接下来,我将抛开数据手册的平铺直叙,结合真实踩坑和实战经验,带你深入TCB的这三个核心模式,看看它们如何解决实际工程问题。

2. 核心模式一:输入捕获模式——捕捉瞬间的艺术

输入捕获功能是TCB的看家本领,其核心目的是精确记录某个外部事件(通常是引脚上的边沿信号)发生时,定时器计数器的瞬时值。这个看似简单的操作,却是测量脉冲宽度、频率、占空比乃至实现数字解调的基础。

2.1 工作原理与时钟源选择:精度从何而来

TCB本质上是一个16位的向上计数器(TCNT)。在输入捕获模式下,它持续在后台运行计数。当配置的捕获引脚(通常是芯片复用引脚,如TCB0_WO对应的PA2)上发生指定的电平时钟(上升沿、下降沿或任意边沿)时,硬件会自动将当前TCNT的值锁存到捕获寄存器(CCMP)中,并可以产生一个中断标志。

这里第一个关键选择是时钟源(CLKSEL)。TCB的时钟可以来自:

  1. 系统时钟(CLK_PER):这是最常用的选择,计时精度最高。如果你的系统时钟是20MHz,那么每个计数周期就是50ns。这是高精度测量的基础。
  2. 预分频后的系统时钟:当需要捕获较长的时间间隔时,可以对系统时钟进行2、4、8...等分频,以扩展计数范围,避免计数器过快溢出。
  3. 事件系统(Event System):这是AVR单片机的一大特色。TCB可以直接由另一个外设(如另一个定时器的溢出、ADC转换完成)触发的事件来驱动计数。这可以实现完全由硬件联动的精密时序控制,无需CPU干预。例如,可以用TC0的溢出事件来驱动TCB计数,从而实现两个定时器的级联,获得远超16位的计数范围。

注意:数据手册中可能还会提到TCA作为时钟源。在AVR DA/DB系列中,这通常是通过事件系统间接实现的,而非直接连接。配置时务必查阅当前芯片的“复用信号(MUX)”表格,确认正确的信号路径。

选择时钟源的核心原则是:在保证不溢出的前提下,尽可能使用更高的计时分辨率。例如,要测量一个预计最大10ms的脉冲,系统时钟20MHz。如果直接使用系统时钟,计数器在10ms内会计数200,000次,远小于65535(16位溢出值),此时应直接使用系统时钟以获得50ns的分辨率。如果脉冲可能长达1秒,则必须使用分频或事件系统级联来扩展量程。

2.2 实战配置:从寄存器到代码

我们以AVR128DA48为例,配置TCB0在PA2引脚上捕获上升沿,使用系统时钟,并使能中断。

第一步:引脚配置PA2引脚需要配置为输入,并启用上拉电阻(如果需要)以消除抖动。更重要的是,要通过PORTMUX.TCBROUTEA寄存器将TCB0的输出路由到正确的端口。对于DA系列,TCB0默认路由到PA2,但显式设置是个好习惯。

// 设置PA2为输入,启用上拉(根据外部电路决定是否需要) PORTA.DIRCLR = PIN2_bm; PORTA.PIN2CTRL = PORT_PULLUPEN_bm; // 启用上拉 // 确认TCB0路由到PORTA (对于DA系列,这常常是默认值,但显式设置更安全) PORTMUX.TCBROUTEA = PORTMUX_TCB0_PORTA_gc;

第二步:TCB模式与时钟配置这是核心步骤。我们需要配置TCB0.CTRLA(控制A)、TCB0.CTRLB(控制B)和TCB0.EVCTRL(事件控制)寄存器。

// TCB0.CTRLA - 控制A寄存器 // 选择时钟源:系统时钟不分频 (CLK_PER) // 同时启用定时器(ENABLE位) TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // TCB0.CTRLB - 控制B寄存器 // 设置模式为输入捕获(CNTMODE=1) // 设置捕获边沿为上升沿(CCMPINIT=0, CCMPEN=1? 注意:不同系列位域名称可能不同) // 对于DA系列,更常见的配置是: TCB0.CTRLB = TCB_CNTMODE_INT_gc; // 输入捕获模式,使用中断 // 边沿选择通常在EVCTRL或INTCTRL寄存器,需要仔细查手册 // TCB0.EVCTRL - 事件控制寄存器 // 使能输入捕获事件,并选择边沿 TCB0.EVCTRL = TCB_CAPTEI_bm | TCB_EDGE_bm; // 使能捕获事件,上升沿触发

第三步:中断配置使能捕获中断,并设置中断控制器。

// TCB0.INTCTRL - 中断控制寄存器 TCB0.INTCTRL = TCB_CAPT_bm; // 使能捕获中断 // 在合适的地方(如main函数初始化部分)使能全局中断 sei();

第四步:中断服务程序(ISR)当中断发生时,读取CCMP寄存器值,并清除中断标志。

ISR(TCB0_INT_vect) { // 读取捕获值 uint16_t capture_value = TCB0.CCMP; // 处理捕获值,例如计算与上一次捕获的时间差 static uint16_t last_capture = 0; uint16_t period = capture_value - last_capture; // 注意处理计数器溢出! last_capture = capture_value; // 根据时钟频率,将period转换为时间(微秒、纳秒) // uint32_t time_us = (period * 1000000UL) / F_CPU; // 必须手动清除中断标志位! TCB0.INTFLAGS = TCB_CAPT_bm; }

2.3 避坑指南:溢出处理与噪声滤波

坑1:计数器溢出16位计数器最大计数值为65535。如果两次捕获事件间隔过长,计数器可能已经溢出一次或多次。简单的current - last计算会得到错误结果。正确的处理方法是使用溢出中断或利用捕获值本身的特性进行扩展

一种稳健的方法是启用定时器溢出中断(如果支持),或者在捕获中断中检查TCNT是否接近65535并做预判。更通用的方法是使用32位或64位的软件扩展计数器。在每次捕获中断中,不仅读取CCMP,还读取一个软件维护的、记录溢出次数的overflow_count

volatile uint32_t overflow_count = 0; volatile uint32_t last_capture_extended = 0; ISR(TCB0_INT_vect) { uint16_t capture_hardware = TCB0.CCMP; uint32_t capture_extended = (overflow_count << 16) | capture_hardware; uint32_t period_extended = capture_extended - last_capture_extended; last_capture_extended = capture_extended; // 使用扩展后的值进行时间计算 TCB0.INTFLAGS = TCB_CAPT_bm; } // 假设有溢出中断(如果TCB支持,或使用另一个定时器监控) ISR(OTHER_OVF_VECTOR) { overflow_count++; }

坑2:输入噪声与抖动机械开关或长导线可能引入毛刺,导致误触发。TCB本身硬件滤波能力有限。解决方案有:

  • 软件去抖:在中断中,不立即处理,而是启动一个短延时(如几毫秒)后再读取引脚状态确认。但这会损失实时性。
  • 硬件滤波:在输入引脚加RC低通滤波电路。这是最有效的方法。
  • 利用事件系统:可以先通过一个具有数字滤波功能的外设(如外部中断INT,某些型号支持滤波)对信号进行整形,再通过事件系统触发TCB捕获。这实现了硬件级的可靠触发。

3. 核心模式二:单次触发模式——精准的“一次性”定时

单次触发模式是TCB另一个极具价值的模式。顾名思义,它让定时器在启动后计数到一个预定值(CCMP寄存器设定),然后自动停止,并产生中断。这完美解决了“在精确延时后执行一个动作”的需求,并且在计时期间完全不需要CPU参与,节省了软件定时器轮询的开销。

3.1 为何需要单次触发?对比软件延时

假设你需要让一个LED在系统启动5秒后点亮。新手可能会写一个_delay_ms(5000)。这个函数是阻塞的,CPU在这5秒内什么也做不了。使用通用定时器溢出中断,你需要配置比较匹配值,并不断重置计数器,逻辑稍显复杂。

TCB的单次触发模式则非常优雅:

  1. CCMP设置为5秒对应的计数值(例如,5s * 20MHz = 100,000,000,显然超出16位,因此必须使用分频或级联)。
  2. 将模式设置为单次触发(CNTMODE=2)。
  3. 启动定时器(写CTRLA或触发事件)。
  4. CPU可以立即去处理其他任务。
  5. 5秒后,TCB计数到CCMP值,自动停止,并触发中断。你在中断里点亮LED即可。

3.2 配置详解与级联应用

配置单次触发模式的关键寄存器是TCBn.CTRLBTCBn.CCMP

// 假设使用系统时钟128分频,则每个计数周期为 6.4us (20MHz / 128) // 要定时5ms: 5ms / 6.4us ≈ 781.25,取整为781 uint16_t target_count = 781; // 1. 先写CCMP目标值 TCB0.CCMP = target_count; // 2. 配置模式为单次触发,并选择时钟 // CLKSEL选择CLK_PER/128,模式为单次 TCB0.CTRLA = TCB_CLKSEL_CLKDIV128_gc; // 先不启用ENABLE TCB0.CTRLB = TCB_CNTMODE_SINGLE_gc; // 单次触发模式 // 3. 通过软件或事件启动定时器 // 方法A: 软件启动 TCB0.CTRLA |= TCB_ENABLE_bm; // 写入ENABLE位,计数器开始从0计数 // 方法B: 通过事件系统启动(更灵活,可由其他外设触发) // 需要配置EVCTRL寄存器,并配置事件生成器。

对于更长的定时(超过16位计数器最大范围),就需要级联(Cascading)。最常用的方法是利用事件系统,让另一个定时器(如TCA)的溢出作为TCB的计数时钟(CLKSEL选择事件),或者用TCA的溢出事件来“踢”一下TCB的计数使能。这样,一个32位甚至更长的“虚拟定时器”就由硬件构成了。

3.3 实战案例:硬件看门狗与精确时序链

单次触发模式的一个高级应用是构建一个硬件看门狗。你可以配置一个TCB为单次模式,定时比如100ms。在主循环的关键位置,你通过写TCBn.CNT = 0来重置计数器(在单次模式下,写入CNT寄存器可以重置计数器并重新开始)。如果程序跑飞,无法及时“喂狗”,TCB就会超时触发中断,在中断里执行系统复位。

另一个案例是构建无需CPU干预的精确时序链。例如,控制一个雷达模块:先发一个10us的触发脉冲,等待100us后打开ADC采样窗口,采样持续50us后关闭。这可以用三个TCB(或一个TCA加两个TCB)配合事件系统完成:

  1. TCB0单次模式,10us后触发中断,在中断里拉低触发引脚,并通过事件系统启动TCB1
  2. TCB1单次模式,100us后触发中断,在中断里启动ADC,并通过事件系统启动TCB2
  3. TCB2单次模式,50us后触发中断,在中断里停止ADC。 整个流程由硬件自动执行,时序精度可达时钟周期级别,CPU只需初始化,之后可休眠以省电。

4. 核心模式三:PWM输出模式——简洁的波形生成器

虽然TCA是更强大的PWM发生器,但TCB也提供了基本的8位PWM输出功能。它的PWM模式相对简单,配置快捷,适用于只需要一路简单PWM的场合,比如控制LED亮度、驱动蜂鸣器或生成一个基础舵机信号。

4.1 TCB PWM的工作原理与局限性

TCB在PWM模式下,计数器工作在8位模式(计数范围0-255)。它比较TCNTCCMP寄存器(此时CCMP的低8位CCMPL作为比较值),根据比较结果驱动输出引脚电平。

  • TCNT < CCMPL时,输出高电平(或低电平,取决于极性配置)。
  • TCNT >= CCMPL时,输出反转。

因此,CCMPL的值直接决定了占空比。PWM频率由时钟源和计数周期决定:Fpwm = Fclk / (256 * Prescaler)

TCB PWM的主要局限性在于:

  1. 分辨率固定为8位:对于需要高精度调光或电机控制的应用可能不够。
  2. 通常只有一路输出:每个TCB只能控制一个引脚。
  3. 灵活性较低:缺少TCA那样的波形缓冲、双斜率模式等高级功能。

所以,TCB的PWM模式定位是补充和简化。当你只需要一路简单的PWM,且不想配置复杂的TCA多通道时,TCB是绝佳选择。

4.2 配置步骤与占空比计算

配置TCB0在PA2引脚输出PWM,频率约为1kHz(系统时钟20MHz)。

计算参数:目标频率Fpwm = 1kHz = 1000 HzFpwm = Fclk / (256 * Prescaler)=>Prescaler = Fclk / (256 * Fpwm) = 20,000,000 / (256 * 1000) ≈ 78.125预分频系数必须取2的幂次。最接近的是64(得到~1220Hz)或128(得到~610Hz)。我们选择128分频,得到约610Hz。

配置代码:

// 1. 引脚配置:将PA2设置为输出,因为要输出PWM波形 PORTA.DIRSET = PIN2_bm; // 路由TCB0输出到PA2 (对于DA系列,通常默认即是) PORTMUX.TCBROUTEA = PORTMUX_TCB0_PORTA_gc; // 2. 配置TCB为PWM模式 // 先停止定时器 TCB0.CTRLA = 0; // 设置PWM模式 (CNTMODE=3),并可能设置输出极性(取决于CTRLB具体位) // 对于DA系列,PWM模式通常通过CTRLB.CNTMODE选择,且可能自动使能输出 TCB0.CTRLB = TCB_CNTMODE_PWM8_gc; // 8位PWM模式 // 如果需要反相输出,可以设置相应的位,例如:TCB0.CTRLB |= TCB_CCMPINIT_bm; (请查手册确认) // 3. 设置比较值决定占空比 (0-255) // 例如,设置50%占空比:255 * 0.5 = 127.5,取整128 TCB0.CCMP = 128; // CCMPL被用作比较值 // 4. 配置时钟并启动定时器 // 使用CLK_PER/128 作为时钟源 TCB0.CTRLA = TCB_CLKSEL_CLKDIV128_gc | TCB_ENABLE_bm;

运行以上代码后,你应该能在PA2引脚上用示波器测量到频率约610Hz,占空比约50%的PWM方波。

4.3 动态调整占空比与注意事项

在程序运行中动态改变PWM占空比非常简单,只需更新CCMP寄存器即可。硬件会在下一个PWM周期自动采用新值。

// 将亮度从50%渐变到100% for (uint8_t duty = 128; duty <= 255; duty++) { TCB0.CCMP = duty; _delay_ms(10); // 简单的延时实现渐变效果 }

重要提示:在PWM模式下,CCMP寄存器是双缓冲的。这意味着你写入的新值不会立即生效,而是要等到当前PWM周期结束(计数器归零时)才会加载。这避免了在周期中间改变占空比可能产生的毛刺脉冲。但这也意味着,如果你需要非常同步地更新多路PWM(例如TCB和TCA),需要考虑它们周期是否对齐。

另一个常见问题是输出引脚无信号。请按以下顺序排查:

  1. 引脚方向:是否已设置为输出(DIRSET)?
  2. 引脚复用PORTMUX.TCBROUTEA是否已正确配置?不同型号芯片的TCB输出可能路由到不同引脚,必须查表确认。
  3. 模式是否正确CTRLB.CNTMODE是否确为PWM模式?
  4. 定时器使能CTRLA.ENABLE位是否置1?
  5. CCMP值CCMP是否被设置为一个介于0和255之间的值?如果为0,则输出恒低;如果为255,则输出恒高(或反之,取决于极性)。

5. 模式融合与高级应用:超越数据手册的用法

掌握了三种基本模式后,我们可以将它们组合,或者利用TCB的其他特性(如事件系统接口、异步计数)来实现更巧妙的系统设计。

5.1 输入捕获与单次触发的组合:测量长脉冲

假设要测量一个可能长达数秒的高电平脉冲宽度,直接输入捕获会因计数器频繁溢出而难以处理。我们可以结合两种模式:

  1. 将TCB配置为单次触发模式CCMP设置为一个较长的定时(如1秒),使用低频时钟源。
  2. 将脉冲信号同时连接到TCB的捕获引脚和外部中断引脚。
  3. 上升沿触发外部中断,在中断中启动TCB单次定时器
  4. 下降沿触发TCB的输入捕获(通过事件系统或引脚变化)。
  5. 在TCB的输入捕获中断中,读取CCMP值(这是下降沿时刻的计数值),同时TCB单次定时器仍在后台计数。
  6. 如果脉冲在单次定时器超时(1秒)前结束,我们就能通过捕获值精确计算脉宽(精度为单次定时器的时钟分辨率)。
  7. 如果脉冲超过1秒,单次定时器会先产生溢出中断,我们在中断里记录“已过了1秒”,然后重置并重启单次定时器,继续测量。最终脉宽 =溢出次数 * 1秒 + 最后一次捕获值对应的时间

这种方法用硬件完成了大部分计时工作,软件只需处理溢出次数,大大减轻了CPU负担和中断延迟误差。

5.2 利用事件系统构建外设联动

事件系统是AVR新系列单片机的“神经系统”,TCB是其重要参与者。它可以作为事件的使用者(Consumer),也可以作为事件的产生者(Generator)

作为使用者:TCB可以被其他外设的事件触发。例如:

  • 由ADC转换完成触发单次定时:ADC转换完成后,产生事件,触发TCB开始一个单次定时,用于实现精确的采样保持时间。
  • 由TCA溢出触发计数:实现硬件级联,扩展定时范围。

作为产生者:TCB可以产生事件去驱动其他外设。例如:

  • TCB输入捕获触发DMA:当捕获发生时,产生事件,触发DMA将捕获值CCMP直接搬运到内存数组,实现零CPU开销的数据记录。
  • TCB单次定时结束触发DAC输出:用于生成精确的时序波形。

配置事件系统的核心是EVSYS.CHANNELnEVSYS.USERTCBn寄存器。你需要指定事件生成器(如ADC0_RESRDY)连接到某个事件通道,再将这个通道分配给TCB作为异步事件输入。

// 示例:配置ADC0转换完成事件,通过通道0触发TCB0计数 // 1. 选择事件生成器 (ADC0转换完成) 连接到通道0 EVSYS.CHANNEL0 = EVSYS_GENERATOR_ADC0_RESRDY_gc; // 2. 将通道0的事件用户设置为TCB0的事件输入 EVSYS.USERTCB0 = EVSYS_USER_CHANNEL0_gc; // 3. 配置TCB0使用异步事件作为计数使能或时钟(具体取决于CTRLA.CLKSEL选择) TCB0.CTRLA = TCB_CLKSEL_EVENT_gc; // 使用事件作为时钟源 // 或者,在单次模式下,用事件来启动计数 // TCB0.EVCTRL = TCB_STARTEI_bm; // 使能事件启动

5.3 低功耗设计中的TCB

在电池供电设备中,TCB可以成为低功耗的关键。由于其可以配置为在异步模式下(使用外部32.768kHz晶体或内部超低功耗振荡器)运行,并且能够通过事件系统与其他外设联动,因此可以在CPU深度睡眠(SLEEP_MODE_STANDBY)时仍然工作。

一个典型场景是周期性唤醒

  1. 配置TCB为单次触发模式,时钟源选择外部32.768kHz时钟(CLK_PER外部)。
  2. 设置CCMP为需要的睡眠时间对应的计数值。
  3. 使能TCB中断。
  4. 启动TCB,然后让CPU进入STANDBY睡眠模式。
  5. TCB在后台独立计数,超时后产生中断,该中断可以将CPU唤醒。由于使用的是32kHz时钟,功耗极低,定时却非常精准。

这种方案比使用看门狗定时器(WDT)唤醒精度更高,比使用RTC定时器更灵活(因为TCB可以有多路)。在需要长时间、多组复杂定时的低功耗数据记录器中,这种设计非常有效。