ATtiny85定时器Timer0详解:从寄存器配置到PWM、CTC实战应用 1. 从“小钢炮”ATtiny85的定时器说起如果你玩过Arduino大概率听说过ATtiny85这颗芯片。它只有8个引脚却五脏俱全是很多小型、低功耗项目的首选。但当你从Arduino的analogWrite()和delay()这类“傻瓜式”函数转向想精细控制一个舵机的角度、生成特定频率的方波或者做一个精准的延时触发器时你会发现真正的力量藏在它的“定时器/计数器”里。尤其是Timer/Counter0这是ATtiny85上最核心、最灵活的一个定时器模块PWM输出、精确延时、频率测量都离不开它。很多人觉得它寄存器多、配置复杂容易一头雾水。今天我们就抛开库函数直接跟寄存器对话把ATtiny85的8位Timer0从模式配置到PWM、CTC应用彻底讲透。你会发现一旦掌握了它你就解锁了这颗“小钢炮”单片机绝大部分的定时和波形生成能力。2. 解剖Timer0它到底是什么能干什么在深入配置之前我们必须先搞清楚Timer0的本质。它不是一个简单的“秒表”而是一个可以计数、可以比较、可以产生中断的复杂数字电路模块。理解它的几个核心组成部分是后续一切操作的基础。2.1 核心部件计数器TCNT0与时钟源Timer0的核心是一个8位的向上/向下计数器名为TCNT0。你可以把它想象成一个水桶时钟信号就像水滴。每来一个时钟脉冲一滴水TCNT0这个“水位”就增加1向上计数或减少1向下计数。这个计数器的值你随时可以通过程序读取或写入。那么“水滴”从哪里来这就是时钟源的选择它决定了计数器跑得快还是慢。ATtiny85的Timer0有多个时钟源可选通过TCCR0B寄存器的CS02:CS00位来配置无时钟源 (CS[2:0]000)定时器停止。常用于精确控制定时器的启停。系统时钟 (CK)这是最常用的。比如你的单片机主频是8MHz内部RC振荡器默认那么计数器每1/8,000,000秒125纳秒就计一次数。太快了所以通常需要分频。系统时钟分频CS[2:0]001(1分频),010(8分频),011(64分频),100(256分频),101(1024分频)。分频越大计数器累加越慢定时周期就越长。例如8MHz主频下选择1024分频则计数频率为 8MHz / 1024 7812.5 Hz每个计数周期约为128微秒。外部时钟T0引脚CS[2:0]110(下降沿触发),111(上升沿触发)。此时计数器由外部信号驱动可以用来测量外部脉冲的频率或宽度。关键点选择时钟源和分频系数是决定你定时器“时间基准”的第一步。你需要根据想要的定时周期或PWM频率来反推合适的配置。2.2 两大输出比较寄存器OCR0A与OCR0B如果说TCNT0是自由奔跑的指针那么OCR0A和OCR0B就是两个你预设好的“标杆”。它们是两个8位的输出比较寄存器。Timer0的工作模式很大程度上就是由TCNT0与这两个“标杆”的比较结果来定义的。OCR0A通常与OC0A物理引脚PB0 Arduino引脚5绑定功能最强可以影响定时器的计数模式如在CTC模式下决定复位点。OCR0B通常与OC0B物理引脚PB1 Arduino引脚6绑定。你可以随时给OCR0A和OCR0B赋值。当定时器运行时硬件会不断地将TCNT0的值与OCR0A、OCR0B的值进行比较。一旦相等就会触发“输出比较匹配”事件。这个事件可以产生两种结果触发中断让CPU跳转到中断服务程序去执行特定任务。控制输出引脚电平自动地改变OC0A或OC0B引脚的电平状态这是生成PWM波形的硬件基础。2.3 模式选择器TCCR0A与TCCR0B寄存器所有的配置最终都体现在两个寄存器里TCCR0A(Timer/Counter Control Register A) 和TCCR0B(Timer/Counter Control Register B)。它们是Timer0的“大脑”。TCCR0A寄存器COM0A1:COM0A0和COM0B1:COM0B0这两组位控制着当TCNT0与OCR0A或OCR0B匹配时对应的OC0A/OC0B引脚该如何动作例如置高、置低、翻转或用于PWM输出。这是配置PWM输出的关键。WGM01:WGM00波形生成模式位的一部分。它们与TCCR0B寄存器中的WGM02位共同决定Timer0的四种主要工作模式。TCCR0B寄存器WGM02波形生成模式位的最高位与TCCR0A中的两位共同构成3位的模式选择码WGM2[2:0]。CS02:CS00就是我们前面提到的时钟选择位。一个常见的误区很多人以为PWM是一种独立的“模式”。实际上PWM是特定波形生成模式WGM下结合特定的输出比较行为COM所产生的结果。WGM模式决定了TCNT0如何计数比如是数到255回头还是数到OCR0A回头而COM行为决定了匹配时引脚做什么。两者结合才产生了我们看到的PWM信号。3. 四大工作模式深度解析与配置实战ATtiny85的Timer0主要有四种工作模式由WGM2[2:0]三位编码决定。我们逐一拆解。3.1 模式0普通模式 (WGM0)这是最简单的模式。计数器TCNT0从0开始一直向上计数到最大值255因为它是8位计数器2^8 - 1 255然后溢出回到0重新开始如此循环。计数行为0 - 1 - 2 - ... - 255 - 0 - 1 - ...溢出点固定在255。溢出时会置位TOV0标志位如果开启了溢出中断就会触发中断。输出比较在此模式下OCR0A和OCR0B只是普通的比较寄存器。当TCNT0的值与它们相等时会触发比较匹配中断或事件但通常不用于直接驱动引脚生成PWM因为溢出周期是固定的256个时钟 ticks不灵活。主要用途简单的定时/延时。例如你想让一个LED每1秒闪烁一次。你可以设置一个分频让计数器每溢出一次代表10毫秒然后在溢出中断里软件计数100次就是1秒。配置示例实现一个1ms的定时中断假设系统时钟为8MHz#include avr/io.h #include avr/interrupt.h // 计算要定时1ms即0.001秒。时钟频率8MHz每个tick为1/8,000,000 0.125us。 // 需要的 ticks 数 0.001 / 0.125e-6 8000。 // 计数器最大256所以必须分频。选择64分频则每个tick周期 64 / 8MHz 8us。 // 1ms需要的 ticks 1000us / 8us 125。 // 因此我们让计数器从 (256 - 125) 131 开始计数数125次后溢出。 #define TIMER_START_VAL (131) // 256 - 125 void timer0_init() { TCCR0A 0; // WGM[2:0]0普通模式 TCCR0B 0; TCNT0 TIMER_START_VAL; // 设置计数器初始值 TIMSK | (1 TOIE0); // 使能Timer0溢出中断 TCCR0B | (1 CS01) | (1 CS00); // 时钟选择64分频 (CS[2:0]011) sei(); // 开启全局中断 } ISR(TIMER0_OVF_vect) { // 每次进入此中断说明过去了1ms TCNT0 TIMER_START_VAL; // 重要必须重装初值因为普通模式是从0开始我们通过重装来修正。 // 在这里执行你的1ms任务例如更新软件计数器、扫描按键等。 }注意在普通模式下使用溢出中断做精确定时必须在中断服务程序ISR中手动重装计数器初值如上例所示。否则计数器将从0开始计数到255周期就变了。3.2 模式2CTC模式 (Clear Timer on Compare Match, WGM2)CTC模式是精准定时和频率生成的利器。在这种模式下计数器TCNT0不再是数到255回头而是数到你设定的一个目标值OCR0A就清零。计数行为0 - 1 - 2 - ... -OCR0A- 0 - 1 - ...清零点由OCR0A寄存器动态定义。这比普通模式的固定255灵活得多。输出比较当TCNT0等于OCR0A时不仅计数器清零还会置位OCF0A标志可触发中断。同时OC0A引脚可以根据COM0A[1:0]的设置进行“翻转”Toggle这可以直接生成占空比为50%的方波而OCR0B可以作为第二个比较点用于在同一个周期内的不同时刻触发事件或中断。主要用途生成精确频率的方波通过设置OCR0A的值和时钟分频可以非常精确地控制OC0A引脚输出方波的频率。频率计算公式f_OC0A f_CPU / (2 * N * (1 OCR0A))其中N是分频系数。产生固定周期的中断用于需要严格周期性的任务调度。配置示例在OC0A引脚PB0输出一个1KHz的方波系统时钟8MHz#include avr/io.h void timer0_ctc_init() { DDRB | (1 DDB0); // 设置PB0 (OC0A) 为输出模式 // 计算OCR0A: f_OC0A f_CPU / (2 * N * (1 OCR0A)) // 目标 f_OC0A 1000 Hz, f_CPU 8e6 Hz. // 先选分频N。为了得到较大的OCR0A值分辨率高选N64。 // 公式变形OCR0A (f_CPU / (2 * N * f_OC0A)) - 1 // OCR0A (8e6 / (2 * 64 * 1000)) - 1 (8e6 / 128000) - 1 62.5 - 1 61.5 // 取整 OCR0A 62。实际频率 8e6 / (2 * 64 * (621)) ≈ 1001.98 Hz误差很小。 OCR0A 62; // 设置比较匹配值 // TCCR0A: COM0A01 (OC0A匹配时翻转), WGM[1:0]10 (CTC模式使用OCR0A) TCCR0A (1 COM0A0) | (1 WGM01); // TCCR0B: WGM020 (CTC模式), CS[2:0]011 (64分频) TCCR0B (1 CS01) | (1 CS00); // 启动定时器 }这段代码配置好后PB0引脚就会自动输出一个1KHz的方波无需CPU干预。这是硬件实现的非常精准且不占用CPU时间。3.3 模式1与模式3相位修正PWM与快速PWM模式 (WGM1, 3)这两种模式是生成PWM信号的主力军。它们的核心区别在于计数方式和由此产生的PWM频率和谐波特性。快速PWM模式 (Fast PWM, WGM3)计数行为计数器从0一直向上计数到255最大值然后立即溢出归零接着开始下一个周期。PWM生成原理以OC0A为例你需要设置COM0A[1:0]2(非反转模式) 或3(反转模式)。非反转模式当TCNT0从0开始计数时OC0A引脚置高。当TCNT0的值与OCR0A匹配时OC0A引脚清零。在计数器溢出回到0的瞬间OC0A再次置高开始下一个周期。因此OCR0A的值直接决定了高电平的宽度。OCR0A0时输出恒低OCR0A255时输出恒高实际上在快速PWM模式OCR0A255时匹配发生在最大值引脚会在周期末尾被清零一瞬间并非严格恒高通常用OCR0A255表示100%占空比是一种特殊处理。PWM频率f_PWM f_CPU / (N * 256)其中N是分频系数。频率固定且较高。特点频率高但对称性差因为只在向上计数时匹配一次适用于开关电源、DAC等对频率要求高、对对称性要求不高的场合。相位修正PWM模式 (Phase Correct PWM, WGM1)计数行为计数器从0向上计数到255然后向下计数回到0完成一个完整的“三角波”周期。PWM生成原理同样以非反转模式为例。在向上计数过程中当TCNT0与OCR0A匹配时OC0A引脚清零。在向下计数过程中当TCNT0再次与OCR0A匹配时OC0A引脚置高。PWM频率f_PWM f_CPU / (N * 510)。因为一个周期要经历0-255-0总共510个tick所以频率是快速PWM的一半。特点频率较低但产生的PWM波形关于中心对称。这种对称性使得它的谐波能量主要集中在基频的奇数倍上更容易被滤波。因此相位修正PWM是驱动电机、舵机、音频设备等对波形质量要求较高的理想选择可以减少电机的嗡嗡声和发热。配置示例在OC0B引脚PB1使用相位修正PWM驱动一个舵机系统时钟8MHz舵机控制信号是一个周期约为20ms50Hz高电平宽度在0.5ms到2.5ms之间的PWM信号。我们需要生成一个50Hz的相位修正PWM并通过调节OCR0B来改变脉宽。#include avr/io.h void timer0_pwm_init() { DDRB | (1 DDB1); // 设置PB1 (OC0B) 为输出模式 // 目标PWM频率 50 Hz。 // 对于相位修正PWM: f_PWM f_CPU / (N * 510) // 求 N f_CPU / (f_PWM * 510) 8e6 / (50 * 510) ≈ 313.7 // 查看分频系数1,8,64,256,1024。313.7最接近256。 // 选择 N256则实际频率 f_PWM_actual 8e6 / (256 * 510) ≈ 61.27 Hz。这个频率舵机也能接受常见范围40-200Hz。 // 如果需要更精确的50Hz可能需要调整系统时钟频率或使用16位定时器。 // 计算OCR0B的值来控制脉宽。 // 计数器从0到255再到0一个周期有510个计数tick。 // 每个tick的时间 1 / (f_CPU / N) N / f_CPU 256 / 8e6 32 us。 // 对于0.5ms脉宽需要的 ticks 500us / 32us ≈ 15.6取整16。 // 对于2.5ms脉宽需要的 ticks 2500us / 32us ≈ 78.1取整78。 // 在相位修正PWM模式下OCR0B的值代表的是匹配点。在非反转模式下高电平时间对应于计数器从底部到OCR0B匹配清零再从OCR0B匹配置高到底部的时间这里需要澄清。 // 实际上对于相位修正PWM占空比 OCR0A / 255。但这是对于单斜率近似的简化。严格来说在非反转模式下高电平时间 (255 - OCR0A) / (f_PWM * 255) * 2这容易混淆。 // 更可靠的方法是**实验法**先设置一个中间值比如OCR0B64用示波器测量脉宽然后线性调整。 // TCCR0A: COM0B1:COM0B02 (非反转PWM模式OC0B在向上匹配时清零向下匹配时置高 不对要查表) // 对于Phase Correct PWM模式COM0B1:COM0B02 的含义是在向上匹配时清除OC0B在向下匹配时置位OC0B。这产生了中心对齐的PWM。 // WGM[1:0]01 (相位修正PWM模式TOP0xFF) TCCR0A (1 COM0B1) | (1 WGM00); // TCCR0B: WGM020, CS[2:0]100 (256分频) TCCR0B (1 CS02); } void set_servo_angle(uint8_t angle) { // 假设角度范围0-180度对应脉宽0.5ms-2.5ms // 根据实验校准的线性映射公式。例如测得OCR0B16对应0.5msOCR0B78对应2.5ms。 // 则映射公式OCR0B_value 16 (angle / 180.0) * (78 - 16) uint16_t pulse_width_ticks 16 (angle * (78 - 16) / 180); if (pulse_width_ticks 255) pulse_width_ticks 255; // 防止溢出 OCR0B (uint8_t)pulse_width_ticks; }关键提示驱动舵机时务必使用相位修正PWM模式。快速PWM模式的不对称性可能导致舵机抖动、发热甚至损坏。上例中的脉宽与OCR0B的映射关系需要根据实际测量的时钟频率和分频进行校准公式占空比 OCR0x / 255在相位修正PWM模式下是一个很好的近似起点。3.4 模式比较与选型速查表为了帮你快速决策这里有一个对比表格模式 (WGM)名称计数方式TOP值主要应用场景关键特点0普通模式向上计数到255溢出0xFF (固定)简单的软件定时、延时简单周期固定需软件重装初值实现精确定时。2CTC模式向上计数到OCR0A清零OCR0A (可调)精确频率方波输出、固定周期中断频率/周期可精确编程OC0A引脚可硬件输出50%占空比方波。1相位修正PWM向上计数到255再向下计数到00xFF (固定)电机控制、舵机控制、音频应用产生对称的PWM波形谐波特性好频率较低。3快速PWM向上计数到255溢出0xFF (固定)LED调光、开关电源、DAC产生高频PWM但波形不对称。4. 实战进阶双路PWM与中断协同ATtiny85的Timer0强大之处在于它可以同时管理两路输出OC0A和OC0B并且可以灵活地使用中断。4.1 配置双路独立PWM在快速PWM或相位修正PWM模式下OCR0A和OCR0B可以独立设置从而在PB0和PB1引脚上产生两路占空比可独立调节的PWM信号。但请注意它们的TOP值在模式1和3TOP0xFF下两路PWM的周期频率是相同的由时钟分频和TOP值决定。占空比分别由OCR0A和OCR0B控制。对于非反转模式占空比 OCR0x / 255。配置代码示例快速PWM双路void timer0_dual_pwm_init() { DDRB | (1 DDB0) | (1 DDB1); // PB0和PB1都设为输出 // TCCR0A: 配置OC0A和OC0B均为非反转快速PWM模式 // COM0A1:0 2 (非反转PWM), COM0B1:0 2 (非反转PWM) // WGM1:0 3 (快速PWM模式TOP0xFF) TCCR0A (1 COM0A1) | (1 COM0B1) | (1 WGM01) | (1 WGM00); // TCCR0B: WGM020 (快速PWM模式), 选择时钟分频例如64分频 TCCR0B (1 CS01) | (1 CS00); // CS[2:0]011 OCR0A 64; // 设置OC0A占空比 ~25% (64/255) OCR0B 191; // 设置OC0B占空比 ~75% (191/255) }4.2 巧妙使用输出比较中断除了溢出中断Timer0还有两个输出比较匹配中断TIMER0_COMPA_vect匹配OCR0A和TIMER0_COMPB_vect匹配OCR0B。这两个中断非常有用可以让你在计数器运行的特定时刻执行代码实现更复杂的时间序列控制。应用场景1在CTC模式下产生非50%占空比的复杂波形虽然CTC模式硬件上只能在OC0A引脚产生50%占空比方波但我们可以结合OCR0B和比较匹配B中断在软件中控制另一个引脚生成任意占空比的波形。#include avr/io.h #include avr/interrupt.h volatile uint8_t pwm_high_ticks 20; // 高电平ticks数 volatile uint8_t pwm_low_ticks 80; // 低电平ticks数 (周期100 ticks) ISR(TIMER0_COMPA_vect) { // 当TCNT0 OCR0A时触发这是周期的结束点 // 我们可以在这里开始一个新的高电平周期 PORTB | (1 PB2); // 假设用PB2输出自定义PWM OCR0B pwm_high_ticks; // 设置第一次匹配点高电平结束 } ISR(TIMER0_COMPB_vect) { // 当TCNT0 OCR0B时触发 // 如果是高电平结束点则拉低引脚并设置低电平结束点 static uint8_t phase 0; if (phase 0) { PORTB ~(1 PB2); // 结束高电平 OCR0B pwm_high_ticks pwm_low_ticks; // 实际上在CTC模式下OCR0B不能大于OCR0A。此方案有缺陷。 phase 1; } else { // 这个例子在CTC模式下不完善因为OCR0B必须OCR0A。更好的方案是使用普通模式或PWM模式。 } } void custom_pwm_init() { DDRB | (1 DDB2); // 配置为CTC模式TOPOCR0A OCR0A 100; // 设定整个PWM周期为100个ticks OCR0B pwm_high_ticks; // 第一次匹配点 TIMSK | (1 OCIE0A) | (1 OCIE0B); // 使能比较匹配A和B中断 TCCR0A (1 WGM01); // CTC模式 TCCR0B (1 CS01); // 8分频启动定时器 sei(); }这个例子意在说明思路但在CTC模式下OCR0B不能大于OCR0A限制了其生成低占空比波形的能力。更通用的方案是使用普通模式在溢出中断和比较匹配中断中分别重装TCNT0和OCR0B实现完全可编程的周期和占空比。应用场景2多任务时间片调度你可以设置一个固定的CTC中断比如1ms一次在这个中断服务程序里更新一个系统时钟计数器。然后在其他代码中检查这个计数器来实现非阻塞的延时、定时执行任务等功能。这是许多简单RTOS或任务调度器的基础。5. 避坑指南与性能优化直接操作寄存器功能强大但也容易踩坑。下面是一些血泪教训总结。5.1 模式与引脚映射的陷阱引脚复用PB0和PB1除了是普通GPIO还是OC0A和OC0B。当你启用Timer0的PWM或CTC输出功能时即设置了COM0x[1:0]为非零值必须将对应引脚的数据方向寄存器DDRB设置为输出否则波形无法输出到引脚。但如果你只使用定时器中断而不需要硬件输出则可以保持引脚为输入状态避免干扰其他电路。模式冲突WGM[2:0]、COM0A[1:0]和COM0B[1:0]的配置是相互关联的。数据手册中的波形图表格是终极参考。例如在普通模式WGM0下某些COM设置是无效的。配置前最好画个简单的位值表格。TOP值理解在CTC模式下TOP是OCR0A在两种PWM模式下TOP是固定的255。这直接影响频率计算和OCR0x的有效范围。OCR0x的值不能超过TOP值。5.2 中断服务程序ISR的编写规范保持简短ISR应该尽可能快地执行完毕。避免在ISR内进行复杂的计算、浮点运算或长时间的循环。通常只做设置标志、更新计数器、读写端口等简单操作。使用volatile变量在ISR和主程序之间共享的变量必须用volatile关键字声明防止编译器优化导致数据不同步。及时清除中断标志有些中断标志需要手动清除如溢出标志TOV0在进入中断后会自动清除但最好查一下数据手册确认而比较匹配标志OCF0A/OCF0B在进入中断后是自动清除的。但为了安全可以在ISR开始时读取一下相关的寄存器如TCNT0有时能起到清除标志的作用。注意全局中断开关在sei()开启全局中断后中断随时可能发生。如果主程序中的某段代码不能被中断可以用cli()暂时关闭全局中断但这段代码要非常短否则会影响定时精度。5.3 计算精度与时钟源选择系统时钟精度如果你使用内部RC振荡器默认8MHz其精度可能只有±10%。这对于要求精确定时或频率的应用如串口通信、精确舵机控制可能不够。此时可以考虑使用外部晶振或者使用CTC模式输出波形然后通过校准内部RC振荡器通过OSCCAL寄存器来提高精度。分频系数的选择分频系数N决定了定时器的基本时间单位和最大定时长度。对于8位定时器在8MHz下无分频N1每个tick 0.125us最大定时 256 * 0.125us 32us。太短很少用。1024分频N1024每个tick 128us最大定时 256 * 128us ≈ 32.8ms。适合几十毫秒级别的定时。如果需要更长的定时如1秒必须结合软件计数器在中断中累加。CTC模式频率公式f_OCnA f_CPU / (2 * N * (1 OCRnA))。注意OCRnA是8位寄存器最大值255。所以能生成的最低频率受限于OCR0A255和最大分频N1024。在8MHz下最低频率约为 8e6 / (2 * 1024 * 256) ≈ 15.26 Hz。如果需要更低的频率就需要用软件分频了。5.4 功耗考量定时器只要运行就会消耗电能。在电池供电的低功耗项目中务必注意不用时关闭如果某个功能模块不需要定时器将TCCR0B中的CS[2:0]设为000停止时钟源。选择合适的分频在满足定时要求的前提下使用更大的分频系数可以让计数器跑得更慢减少触发中断的频率从而降低CPU被唤醒的次数节省功耗。休眠模式结合在深度休眠如Power-down模式下大多数时钟都停止了定时器也无法工作。但你可以配置一个看门狗定时器WDT或使用外部中断来唤醒MCU唤醒后再启动Timer0进行精确计时。最后调试定时器相关功能时一个逻辑分析仪或者哪怕一个简单的示波器都是 invaluable 的。亲眼看到引脚上的波形、测量频率和占空比比任何计算和想象都来得直接和可靠。从最简单的LED呼吸灯改变PWM占空比开始实验逐步过渡到舵机控制、蜂鸣器发声你会对ATtiny85的这颗8位定时器有越来越深刻的掌控感。