1. 从“玩具”到“利器”:重新认识ATtiny85的PWM
提到ATtiny85,很多人的第一印象是“便宜”、“简单”,甚至“玩具”。确实,这个只有8个引脚、8KB闪存的AVR微控制器,常被用于一些简单的LED闪烁、按键检测等场景。但如果你也这么想,那可能就错过了它身上一个极其强大且实用的功能:硬件PWM。我最初接触ATtiny85时,也只是把它当作一个更小的Arduino来用,直到有一次需要在一个极其紧凑的空间里驱动一个微型舵机,同时还要维持低功耗,我才被迫去深挖它的数据手册。结果发现,这颗小小的芯片内部,藏着两套相当完整的定时器系统,其PWM功能的灵活性和精度,远超我的预期。今天,我们就抛开Arduino的analogWrite()封装,直接深入到寄存器层面,把ATtiny85的定时器PWM模式彻底讲透。无论你是想精确控制LED亮度、驱动舵机、还是构建简单的电机调速系统,理解这些底层机制,都能让你从“能用”进阶到“精通”,真正榨干这颗芯片的每一分性能。
2. ATtiny85的定时器系统架构总览
在深入PWM配置之前,我们必须先搞清楚ATtiny85为我们提供了什么样的“计时武器库”。与STM32等资源丰富的MCU不同,ATtiny85的资源非常精简,因此它的定时器设计也高度集成和复用。
ATtiny85内部有两个独立的定时器/计数器:
- 定时器/计数器0 (Timer/Counter0):这是一个8位定时器,也是功能最丰富的一个。它支持多种工作模式,包括普通的定时溢出、CTC(比较匹配清零)模式,以及我们今天重点要讲的快速PWM模式和相位修正PWM模式。它有两个独立的输出比较单元(OCR0A和OCR0B),这意味着它可以同时生成两路独立的PWM信号,通常映射到芯片的PB0 (OC0A)和PB1 (OC0B)引脚。这是最常用、最灵活的PWM发生器。
- 定时器/计数器1 (Timer/Counter1):这是一个特殊的16位定时器,但它被设计主要用于模拟比较器和ADC噪声抑制等特定功能。虽然它理论上也能用于PWM,但其配置更为复杂,且输出引脚可能与其他功能复用,在纯粹的PWM应用中没有Timer0方便,因此本文主要聚焦于Timer0。
这里有一个关键点需要理解:定时器和PWM生成器本质上是同一套硬件。定时器就像一个不断向上或向下计数的“时钟”,而PWM功能是通过配置“比较匹配”机制来实现的。当计数器的值达到我们预设的“比较值”时,输出引脚的电平就会发生翻转。通过控制比较值占整个计数周期的比例,我们就得到了占空比可调的PWM波。ATtiny85的硬件负责自动完成计数、比较和引脚电平切换,完全不占用CPU资源,这才是硬件PWM的核心价值。
3. 核心寄存器详解:配置PWM的“控制面板”
配置ATtiny85的PWM,本质上就是操作几个特定的寄存器。我们不需要记住所有位,但必须理解每个关键位的作用。以下是对Timer0相关核心寄存器的逐位拆解。
3.1 TCCR0A 和 TCCR0B:模式与时钟源的控制核心
这两个寄存器是Timer0的“大脑”,决定了它如何工作以及跑得多快。
TCCR0A (Timer/Counter Control Register A)这个寄存器主要控制波形生成模式(即PWM模式)和输出比较的行为。
| 位 | 名称 | 功能描述 | 对PWM的意义 |
|---|---|---|---|
| 7:6 | COM0A1:0 | 通道A比较输出模式 | 决定OC0A(PB0)引脚在比较匹配时的行为。例如,设置为0b10时,在快速PWM模式下,匹配时清零,计数器顶部时置位,产生正向PWM。 |
| 5:4 | COM0B1:0 | 通道B比较输出模式 | 同上,控制OC0B(PB1)引脚。两路PWM可以独立配置。 |
| 3 | – | 保留位 | 必须写0。 |
| 2 | – | 保留位 | 必须写0。 |
| 1:0 | WGM01:0 | 波形生成模式位[1:0] | 与TCCR0B中的WGM02位共同决定定时器的工作模式。这是选择快速PWM还是相位修正PWM的关键。 |
TCCR0B (Timer/Counter Control Register B)这个寄存器主要控制时钟源(预分频器),并补充了模式选择位。
| 位 | 名称 | 功能描述 | 对PWM的意义 |
|---|---|---|---|
| 7 | FOC0A | 强制输出比较A | 在非PWM模式下手动触发比较匹配,PWM模式下无效。 |
| 6 | FOC0B | 强制输出比较B | 同上。 |
| 5 | – | 保留位 | 必须写0。 |
| 4 | – | 保留位 | 必须写0。 |
| 3 | WGM02 | 波形生成模式位[2] | 与TCCR0A的WGM01:0共同组成3位的模式选择码。 |
| 2:0 | CS02:0 | 时钟选择位 | 这是PWM频率的“调速器”。选择系统时钟(或外部时钟)的预分频系数。例如,0b001表示无分频(时钟直接驱动计数器),0b010表示8分频,0b011表示64分频,以此类推。分频越大,计数器累加越慢,PWM频率就越低。 |
模式选择(WGM02:0)与PWM类型的关系:对于PWM,我们主要关注以下两种模式,由WGM02:0这三位共同决定:
- 模式3 (WGM02:0 = 0b011): 快速PWM模式。计数器从0一直累加到最大值(TOP值,通常是255),然后立即清零,重新开始。在比较匹配时,输出引脚根据COM0x1:0的设置发生动作(如清零),在计数器溢出(回到0)时再次动作(如置位)。这种模式产生的PWM频率固定,且占空比调节分辨率高。其频率计算公式为:
PWM频率 = 系统时钟频率 / (预分频系数 * 256)。例如,在8MHz系统时钟、预分频系数为64的情况下,PWM频率 = 8,000,000 / (64 * 256) ≈ 488.28 Hz。这个频率非常适合LED调光。 - 模式1 (WGM02:0 = 0b001): 相位修正PWM模式。计数器从0累加到TOP值(通常是255),然后递减回0,如此往复。输出引脚在“向上计数匹配”和“向下计数匹配”时都可能发生动作,这导致PWM波的中心始终对齐,对称性更好。其频率计算公式为:
PWM频率 = 系统时钟频率 / (预分频系数 * 510)。因为计数周期是0->255->0,总共510个时钟 ticks。同样的8MHz/64分频下,频率约为245 Hz。相位修正PWM产生的谐波更少,常用于电机控制、音频等对波形对称性有要求的场合。
3.2 OCR0A 和 OCR0B:占空比的“设定点”
这两个寄存器是8位的输出比较寄存器。它们的值直接决定了PWM波的占空比。
- OCR0A: 与OC0A(PB0)引脚绑定。
- OCR0B: 与OC0B(PB1)引脚绑定。
工作原理:在快速PWM模式下,计数器TCNT0从0到255循环。我们向OCR0x中写入一个0到255之间的值。当TCNT0计数到小于OCR0x的值时,输出引脚为一种状态(例如高电平);当TCNT0等于或大于OCR0x时,引脚变为另一种状态(例如低电平)。因此,占空比 = (OCR0x值) / 256。写入0,占空比0%(常低);写入255,占空比约99.6%(常高);写入128,占空比50%。
注意:在相位修正PWM模式下,由于有向上和向下计数过程,逻辑稍复杂,但最终效果同样是OCR0x的值越大,高电平时间越长。
3.3 TCNT0、TIMSK 与 TIFR:计数与中断
- TCNT0: 8位计数器寄存器。你可以直接读写它,但在PWM模式下通常不需要操作它,硬件会自动管理其计数。在需要精确同步或产生特殊波形时,直接操作它可能有用。
- TIMSK (Timer/Counter Interrupt Mask Register)和TIFR (Timer/Counter Interrupt Flag Register): 这两个寄存器管理定时器的中断(如溢出中断、比较匹配中断)。在纯粹的PWM输出应用中,由于硬件自动控制引脚,我们通常不需要开启中断,这样可以节省CPU资源和功耗。中断主要用于需要CPU在特定时刻(如每次PWM周期开始或结束时)介入处理的场景。
4. 实战配置:从零生成两路PWM信号
理论讲完了,我们动手配置。假设我们的需求是:在ATtiny85上,使用内部8MHz RC振荡器,在PB0和PB1引脚上生成两路快速PWM信号,其中PB0的PWM频率约为976Hz(预分频8),占空比50%;PB1的PWM频率相同,占空比25%。
4.1 步骤一:确定工作模式与时钟源
- 选择模式:我们需要快速PWM模式,即WGM02:0 = 0b011。
- 计算频率与预分频:目标频率约976Hz。根据公式
频率 = F_CPU / (分频系数 * 256)。- 代入F_CPU = 8,000,000 Hz, 目标频率 = 976 Hz。
- 所需分频系数 = F_CPU / (频率 * 256) = 8,000,000 / (976 * 256) ≈ 32.0。
- 查看可用的预分频选项(1, 8, 64, 256, 1024),8分频(CS02:0=0b010)得到的实际频率是 8,000,000 / (8 * 256) = 3906.25 Hz,太高了。64分频(CS02:0=0b011)得到 488.28 Hz,又太低了。这里就出现了一个取舍:ATtiny85的Timer0预分频是固定的几个档位,无法做到任意频率。对于许多应用(如LED调光、舵机),频率在一定范围内即可。我们选择8分频,得到3.9kHz的PWM。这个频率远高于人眼视觉暂留,用于LED调光无闪烁感,且远高于舵机所需的50Hz,需要通过软件或其他方式(如使用OCR0A作为TOP值,即模式7)来进一步降低频率,但那样会降低占空比分辨率。本例我们先按标准快速PWM模式配置。
- 确定输出模式:我们希望得到正向PWM(即占空比越大,高电平时间越长)。在快速PWM模式下,对应COM0A1:0或COM0B1:0设置为
0b10(比较匹配时清零,TOP时置位)。
4.2 步骤二:编写寄存器配置代码
我们使用纯C语言和AVR-GCC编写,不依赖Arduino库。
#include <avr/io.h> void setup_pwm() { // 1. 配置PB0(OC0A)和PB1(OC0B)为输出模式 DDRB |= (1 << DDB0) | (1 << DDB1); // 设置PB0和PB1为输出 // 2. 配置TCCR0A寄存器 // COM0A1:0 = 0b10, 快速PWM,OC0A非反向模式(匹配清零,TOP置位) // COM0B1:0 = 0b10, 快速PWM,OC0B非反向模式 // WGM01:0 = 0b11 (与TCCR0B的WGM02一起构成模式3) TCCR0A = (1 << COM0A1) | (0 << COM0A0) | (1 << COM0B1) | (0 << COM0B0) | (1 << WGM01) | (1 << WGM00); // 3. 配置TCCR0B寄存器 // WGM02 = 0 (模式3的第三位是0) // CS02:0 = 0b010, 选择时钟源为系统时钟/8 (预分频8) TCCR0B = (0 << WGM02) | (0 << CS02) | (1 << CS01) | (0 << CS00); // 0b010 // 4. 设置占空比 // 占空比 = OCR0x / 256 // 50% 占空比: 256 * 0.5 = 128 OCR0A = 128; // 25% 占空比: 256 * 0.25 = 64 OCR0B = 64; } int main(void) { setup_pwm(); while (1) { // 主循环为空,PWM由硬件自动生成,不占用CPU // 可以在这里动态修改OCR0A/OCR0B来改变占空比 // 例如:OCR0A = some_variable; } }4.3 步骤三:动态调节与验证
代码中的setup_pwm()函数完成了静态配置。在实际应用中,我们经常需要动态改变占空比。这非常简单,只需要在程序运行过程中,直接修改OCR0A或OCR0B寄存器的值即可。硬件会在下一个PWM周期自动应用新的比较值,实现无缝的亮度或速度变化。
// 示例:让LED呼吸(PWM占空比渐变) #include <util/delay.h> int main(void) { uint8_t brightness = 0; int8_t direction = 1; setup_pwm(); while (1) { // 更新占空比 OCR0A = brightness; // 简单的延时,控制呼吸速度 _delay_ms(10); // 更新亮度值,实现往复渐变 brightness += direction; if (brightness == 0 || brightness == 255) { direction = -direction; } } }验证方法:
- 示波器/逻辑分析仪:这是最准确的方法。探头连接到PB0或PB1,可以看到标准的3.9kHz方波,并且高电平宽度会随着
OCR0A值的变化而平滑变化。 - LED观察:将一个LED(串联一个220Ω限流电阻)接到PB0和GND之间。运行呼吸灯代码,你应该能看到LED平滑地从暗变亮再变暗。如果频率太低(比如几十Hz),你会看到闪烁;如果频率在几百Hz以上,人眼看到的就是亮度变化。
5. 高级应用与避坑指南
掌握了基础配置后,我们来看看如何应对更复杂的需求,以及那些容易踩坑的地方。
5.1 如何获得非标准的PWM频率?
标准快速PWM模式的TOP值是固定的255(8位满量程)。这是导致频率受限于F_CPU / (分频 * 256)的根本原因。ATtiny85的Timer0提供了另一种模式(模式7,快速PWM,且TOP = OCR0A),允许我们通过设置OCR0A寄存器来定义TOP值。
配置方法:
- 设置
WGM02:0 = 0b111(模式7)。 - 此时,计数器的最大值不再是255,而是你写入
OCR0A的值。 - PWM频率公式变为:
频率 = F_CPU / (分频系数 * (1 + OCR0A))。 - 注意:在此模式下,OCR0A寄存器用于设定频率(TOP值),而OCR0B寄存器仍然用于设定其自身通道的占空比。这意味着通道A(OC0A)在此模式下不能输出可变占空比的PWM,它会在计数器达到TOP时翻转,固定输出50%占空比的方波(或根据COM0A设置的其他固定模式)。通道B(OC0B)则正常工作。
这种模式非常适合需要特定频率(如舵机所需的50Hz)的应用。例如,要产生50Hz的PWM(周期20ms),系统时钟8MHz,预分频64:
- 所需计数值 = F_CPU / (分频 * 频率) = 8,000,000 / (64 * 50) = 2500。
- 由于OCR0A是8位寄存器,最大值为255,显然2500远超其范围。这说明用Timer0产生极低频率(如50Hz)的PWM是非常困难的,因为计数值会溢出。对于舵机控制,通常的实践是:
- 使用Timer0的CTC模式产生一个高频中断(如1ms),在中断服务程序里用软件计数和操作IO口来生成50Hz的舵机PWM脉冲。这会占用CPU资源。
- 考虑使用Timer1(16位)的特定模式,或者换用其他更适合低频PWM的芯片(如ATTiny13A的某些模式或STM32)。
5.2 相位修正PWM vs 快速PWM:到底选哪个?
这是一个常见的选择题。我们可以用一个简单的表格来对比:
| 特性 | 快速PWM | 相位修正PWM |
|---|---|---|
| 计数器行为 | 从0单向上数到TOP,然后立即归零 | 从0上数到TOP,再从TOP下数到0 |
| 输出对称性 | 不对称。脉冲中心随占空比移动 | 对称。脉冲中心始终对齐 |
| 等效频率 | F_CPU / (N * 256) | F_CPU / (N * 510) |
| 电机/音频应用 | 可能产生更多可闻噪音 | 更优,谐波成分少,运行更平稳 |
| LED调光 | 更优,频率更高,无闪烁感 | 可用,但频率较低 |
选择建议:
- 驱动LED、MOSFET开关:优先选择快速PWM,以获得更高的刷新率,避免低频闪烁。
- 驱动直流电机、音频DAC:优先选择相位修正PWM,以获得更平滑的驱动效果和更低的噪音。
- 驱动舵机:舵机控制信号是周期20ms、脉宽0.5ms-2.5ms的单一脉冲,对频率精度要求高,对对称性不敏感。用定时器产生标准50Hz方波再滤波的方式并不常用。更常见的做法是使用任意定时器(甚至软件延时)来精确控制一个IO口的高电平时间。如果非要用硬件PWM,则需要计算出一个非常接近50Hz的频率(如使用模式7调整TOP值),并确保脉冲高电平时间可调范围覆盖0.5ms-2.5ms。
5.3 常见问题与排查
没有PWM输出,引脚一直是高电平或低电平
- 检查DDRx:确保你使用的引脚(如PB0/PB1)已通过
DDRB |= (1 << DDB0)设置为输出模式。 - 检查COM0x1:0位:确认已设置为PWM模式(如
0b10或0b11),而不是0b00(断开)或0b01(翻转,在PWM模式下行为可能异常)。 - 检查物理连接:确认引脚没有与其他外设复用,并且电路连接正确。
- 检查DDRx:确保你使用的引脚(如PB0/PB1)已通过
PWM频率不对
- 检查时钟源(CS02:0):确认预分频系数配置正确。
0b001是无分频,0b010是8分频,最容易搞混。 - 检查系统时钟(F_CPU):你的代码编译时定义的
F_CPU宏是否与实际MCU运行的时钟频率一致?如果使用内部RC振荡器,默认可能是1MHz或8MHz,需要通过熔丝位或代码进行配置。 - 检查模式(WGM02:0):确认你配置的是想要的PWM模式。快速PWM和相位修正PWM的频率计算公式不同。
- 检查时钟源(CS02:0):确认预分频系数配置正确。
占空比调节不线性或范围不对
- 检查OCR0x赋值:确保你写入OCR0A/OCR0B的值在0-255之间。8位寄存器,写入大于255的值会被截断。
- 理解占空比定义:在非反向快速PWM模式下,
OCR0x=0意味着占空比接近0%(始终低电平),OCR0x=255意味着占空比接近100%(始终高电平)。如果你希望0对应0%,255对应100%,需要确认模式是否支持。有些模式(如TOP=OCR0A的模式7)下,占空比计算方式不同。
两路PWM互相干扰
- 独立配置:OCR0A和OCR0B是独立的,通常不会干扰。但请确保你分别设置了COM0A和COM0B位。
- 共用TOP值:在标准快速PWM模式下,两路PWM共用同一个计数器TOP值(255),因此它们的频率永远是相同的。你只能独立调节它们的占空比。
6. 超越基础:用PWM实现DAC与电机控制
理解了寄存器级别的PWM后,它的应用就不再局限于点亮LED了。
6.1 构建一个简易的8位DAC
PWM本质是一个数字开关信号,但通过一个低通滤波器(通常是一个简单的RC电路),我们可以将其平滑成模拟电压。这就是PWM DAC的基本原理。
实现步骤:
- 配置Timer0输出一路固定频率的PWM(频率越高,滤波后纹波越小,但频率受限于RC时间常数和PWM分辨率)。推荐使用快速PWM模式,频率在几十kHz以上。
- 在PWM输出引脚(如PB0)和GND之间,连接一个RC低通滤波器。例如,一个1kΩ电阻串联到引脚,电阻另一端连接一个10nF电容到地,滤波器的截止频率约为
1/(2πRC) ≈ 16kHz。这个滤波器的输出点就是模拟电压。 - 通过程序改变
OCR0A的值,改变PWM占空比,经过RC滤波后,输出点的平均电压就会变化:Vout ≈ (OCR0A / 256) * Vcc。 - 用万用表直流电压档测量滤波电容两端的电压,当你动态修改OCR0A时,应该能看到电压的平滑变化。
注意:这种DAC的精度和稳定性受限于电源电压Vcc的稳定性、RC元件的精度、以及负载阻抗。它不适合高精度应用,但对于生成一个可变的参考电压、驱动LED模拟调光等场景,成本极低且非常有效。
6.2 驱动直流电机与H桥考虑
直接用一个IO口的PWM驱动一个小型直流电机(如玩具电机)是可行的,但只能单向调速。如果需要控制电机的正反转,就需要用到H桥电路。ATtiny85可以生成两路PWM,但这通常不足以直接控制一个完整的H桥(需要4个控制信号)。常见的做法是:
- 单向调速:直接将PWM输出通过一个晶体管(如MOSFET)连接到电机。PWM占空比直接控制电机两端的平均电压,实现调速。
- 简单正反转(需额外逻辑):使用一路PWM控制速度,再使用ATtiny85的两个普通IO口控制H桥的方向使能。例如,IO1高、IO2低为正转;IO1低、IO2高为反转;两者同时为低则刹车,同时为高则短路(应避免)。PWM信号则连接到H桥的“使能”或公共PWM输入脚(如果H桥芯片支持)。这种方式下,PWM频率和占空比控制速度,两个IO口的电平组合控制方向。
- 互补PWM与死区控制:这是高级电机驱动(如无刷直流)中的概念。ATtiny85的Timer0不支持硬件互补PWM输出和死区插入。互补PWM需要两路完全同步、且互为反相的PWM信号来控制H桥的上下管,并插入死区时间防止上下管直通。这在ATtiny85上需要复杂的软件模拟或使用更高级的定时器(如某些ARM Cortex-M芯片的高级定时器)。
因此,对于ATtiny85,更现实的电机控制方案是驱动小型有刷直流电机进行单向调速,或者配合集成了逻辑控制的H桥驱动芯片(如L298N、DRV8833)来实现正反转和调速,这时ATtiny85只提供方向信号和一路PWM速度信号。
7. 总结与资源延伸
通过直接操作TCCR0A、TCCR0B、OCR0A、OCR0B这几个核心寄存器,我们完全掌握了ATtiny85 Timer0的PWM生成能力。从简单的LED呼吸灯,到可调压的DAC,再到电机控制的基础,硬件PWM为我们提供了不占用CPU的精准时间控制能力。
几个关键收获:
- 模式选择是根本:WGM02:0三位决定了定时器是作为定时器、CTC还是PWM发生器,以及是快速PWM还是相位修正PWM。
- 时钟源决定频率:CS02:0位选择的预分频系数,直接决定了PWM的基础频率。频率和分辨率(8位)之间存在权衡。
- 比较寄存器决定占空比:OCR0A和OCR0B的值就是占空比的设定值,修改它们就能实时改变输出。
- 输出模式决定极性:COM0A1:0和COM0B1:0位决定了引脚电平如何响应比较匹配,是生成正向还是反向PWM。
进一步探索的方向:
- Timer1的PWM:虽然更复杂,但Timer1是16位的,可以提供更精细的频率控制和更长的周期,适合需要更低频率或更高分辨率PWM的应用。
- 睡眠模式下的PWM:ATtiny85可以在休眠(如Idle模式)时继续运行定时器,这意味着你可以用极低的功耗维持PWM输出,非常适合电池供电设备。
- 与其他外设协同:可以尝试用定时器的溢出中断或比较匹配中断来同步其他操作,例如在每一个PWM周期开始时采样ADC,实现闭环控制。
最后,最权威的资料永远是芯片的数据手册(Datasheet)。在Microchip官网搜索“ATtiny85 Datasheet”,找到其中关于“8-bit Timer/Counter0 with PWM”和“16-bit Timer/Counter1”的章节,那里有最完整的寄存器描述、时序图和模式真值表。当你遇到任何不确定的配置时,回头查阅数据手册,是解决问题最可靠的途径。