AVR8位单片机PWM、ADC的使用
在32位机价格一路卷到几毛钱的今天,8位机的生存空间被一再压缩,目前市场上常见的8位机基本就4种架构:8051,STM8,PIC,AVR。51应该是大部分电子类专业学生的启蒙老师,经典的bit操作和寄存器,还记得点亮第一个LED的喜悦,不过在轻量化,智能化,小封装的各种需求下,8位机逐渐不够看了,尤其是智能化,8位机粗糙的ALU和简陋的外设完全不够用。但是8位机仍有部分暂时无法取代的优势,比如极低功耗,5V逻辑电平无需转换,价格便宜,还有就是存量市场,旧方案一般不会轻易改动。
我现在手里的就是存量方案,之前已经稳定使用,所以不能轻易换,芯片是ATtiny85,ATmel(被Microchip收购)公司出品,在各大电子商城上可以看到价格并不便宜,即使最大批量还是要10块钱一个,体积也不算小,属实是坑爹,不过已经成熟的方案也没什么好抱怨的,接下来记录一下一些常规用法。


我们使用的是SOP8封装,初次使用需要注意这个SOP8比常规的SOP8胖一些,所以画封装时,两列引脚间距要加宽,两外它有6个通用IO,不过注意到PB5和RESET复用,这个脚一般不能当IO用,需要上拉,想要用作IO除非引脚非常紧张,而且要通过改熔丝位来启用它,一旦启用,就没法当RESET用,也没法烧录了。
所以强烈不建议用PB5作IO。
ATtiny85用SPI口下载程序,需要用到PB0,PB1,PB2,PB5加上VCC和GND,6个引脚。
VCC <-> +1.8V ~ +5.5V
MISO <-> MISO
MOSI <-> MOSI
SCL <-> SCL
RESET <-> RESET
GND <-> GND
下载器需要购买AVR-ISP或者PICKIT,部分商家出售仿品。
这次我们用PB1,PB4作PWM输出,对应OC1A,OC1B,PB2做ADC采样,这样设置没有冲突,OC1A和OC1B使用了芯片的Timer/Counter1,相比Timer/Counter0,无法使用相位可调的PWM,不过项目只需要用到PWM调光,因此普通PWM足够。至于ADC,是打算用来换色的,通过不同电平来决定亮起的颜色。所以在上面的基础还要再添加一个IO输出,对于ATtiny85来说依然够用。
接线不在这里展开,这篇笔记用于记录程序写法,AVR单片机开发需要使用ATmel Studio,集成了工具链无需自行配置,而库文件则是官方的avrlib,需要注意AVR不支持bit操作,而是纯寄存器操作,从51转过来的需要时间适应。
下面简单介绍一下AVR单片机的PWM和ADC外设:
一个PWM波形需要有频率和占空比,AVR单片机如何通过Timer/Counter输出周期信号呢,方法就是问现成的周期信号借,而现有的周期信号就是主时钟,主时钟给你了信号,但是占空比是固定的,怎么调整?办法就是数数,数到设定的脉冲数,信号就发生跳变,再数到下一个设定的脉冲数,再发生跳变,这样就通过数数和跳变,生成了一个新的周期信号。下图是datasheet中的原理图,OCn(也就是输出通道)会在TCNTn达到寄存器OCRnx设定值时被置0,数到最大值255时再次置1,周期是固定的,即0数到255的时间,另一路
OCn
输出的极性相反,据此可以得到输出PWM的频率为:其中fclk为主时钟频率,一般Attiny85内置晶振是8M,但主时钟是默认经过分频的1M,想用8M时钟需要禁用熔丝位CLKDIV,N为分频倍率,由CSx设定。

由于我们使用的不是Timer/Counter0而是Timer/Counter1,他们的配置和功能不完全一样,所幸的是,都能输出PWM波形(Timer/Counter1没有介绍PWM波形生成原理,故只能贴一张Timer/Counter0的)。Timer/Counter1配置PWM输出原理基本一致,根据上面的介绍,完成PWM配置需要设置分频,设置计数值OCRnx,还有输出极性,输出通道,除了这4项,还有一些配置,这里总结一下:
CS[13:10] 分频1~65536倍,Timer/Counter0最多256倍,属于TCCR1
PMW1A、PWM1B OC1A、OC1B通道PWM使能,PWM1A属于TCCR1,PWM1B属于GTCCR
OCR1A、OCR1B 两路PWM计数值,达到后OC1A,OC1B分别输出0,都是独立8位寄存器
OCR1C 计数封顶值TOP,达到后OCn置位,Timer/Counter1独有,
比如TC0的计数器固定在255时置位,TC1可以修改为128,64等
独立8位寄存器
COM1A[1:0]、COM1B[1:0] 设置输出极性,COM1A属于TCCR1,COM1B属于GTCCR
TCNT1 计数值,启动后开始根据时钟周期数自增,独立8位寄存器
CTC1 使能该位,让TCNT1达到TOP后清零,否则TCNT1在达到TOP后会
继续自增(OCn也不会在达到TOP时清零)
DDRB 将PB1,PB4配置为输出,独立8位寄存器
其他的寄存器我们可以暂时忽略。
由此我们可以写出一个简单的初始化代码:
#include<avr/io.h>
#include<util/delay.h> //这两个头文件分别包含外设和延时各项定义
void init_PWM(void)
{
DDRB |= (1 << PB1) | (1 << PB4);
TCNT1 = 0; //TCNT1清零
OCR1C = 255; //计数封顶值255
OCR1A = 64; //OC1A占空比25%
OCR1B = 128; //OC1B占空比50%
TCCR1 |= (1 << CTC1)| (1 << CS12) | (1 << CS11) | (1 << CS10)
| (1 << PWM1A) | (1 << COM1A1); //64倍分频,启用PWM功能,OC1A极性不翻转
GTCCR |= (1 << PWM1B) | (1 << COM1B1); //启用通道B的PWM功能,OC1B极性不翻转
}
这样一个PWM初始化函数就写好了。接下来我们再看一看ADC的原理和使用。
ADC的原理可以分为FLASH,SAR,积分器,∑-∆,流水线等,FLASH是最简单的一种,就是用到大量并列的比较器和一串分压电阻,每个比较器都接到输入电压和一串的分压节点上,这样通过比较器输出的01值表示电平的数值。FLASH实现最简单,转换速度也快,但是电路资源消耗较大,功耗也大,不适合做成高分辨率的ADC。其他几种ADC原理比较复杂,感兴趣的可自行搜索。
ADC的流程包括采样,保持,量化,编码,采样就是按时间间隔采集引脚上的电平,保持就是在不采样的时候,维持住之前采样的电平,量化就是把采到电压分成离散的电平值,编码就是把量化后的电平值通过二进制表示。
ATtiny85的ADC最高10bit,也可以设置为8bit,但由于本身是8位机,读出10位ADC结果要读两个寄存器,精度要求不高的场合,直接用8bit就够了。ATtiny85有4路可选的ADC通道,PB2,PB3,PB4,PB5(RESET脚,不推荐),但是它内部只有一个ADC转换器,所以不能同时使用多路输入,不过它还有一路内置的温度采样同样用到了ADC,但不需要外接引脚,此外,ATtiny85的ADC支持差分输入;ATtiny85的参考电压可以设为内部的1.1V,2.56V,外部VCC,或者用户自定义的VREF;ADC采样周期类似于Timer,都需要问主时钟借,所以也同样需要分频器这个东西。
根据上面的描述,配置ADC需要设置位数/分辨率,参考电压,输入通道,还有一些依附的参数,下面总结一下用到的寄存器:
MUX[3:0] 输入选择,可配置单通道,差分,温度采样,属于ADMUX寄存器
REF[2:0] 选择参考电压,属于ADMUX寄存器
ADLAR 转换结果左对齐,作用就是设置分辨率为8bit,不启用则为10bit
ADEN 启用ADC,属于ADCSRA寄存器
ADSC 触发一次ADC转换,完成后自动清0,属于ADCSRA
ADPS[2:0] 设置分频倍率,属于ADCSRA
ADCH、ADCL 转换结果保存位置,若ADLAR=1,则只需要读取ADCH,独立8位寄存器
ADTS[2:0] 设置ADC触发方式,属于ADCSRB寄存器
现在我们配置一个由PB2输入,8位分辨率,手动触发的ADC,示例程序如下:
void init_ADC(void)
{
ADMUX |= (1 << MUX0) | (1 << ADLAR); //8bit,PB2输入
ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADEN); //64倍分频,启用ADC
//delay_ms(1); //等待电平稳定
//ADCSRA |= (1 << ADSC); //预采样
//while(ADCSRA & (1 << ADSC));
//void(ADCH); //丢弃预采样数据
}
uint8_t read_ADC(void)
{
ADCSRA |= (1 << ADSC); //触发采样
while(ADCSRA & (1 << ADSC); //等待转换完成
return ADCH; //返回8位结果
}
值得一提的是,启动后第一次采样消耗的时间比后续的采样长不少,如果需要做上电采样的话,需要注意电源时序,另外,上电后的第一组采样可能在电平没完全建立的情况下产生,若使用内部参考电压,则电平稳定时间更长,所以建议在初始化程序中进行一次预采样并丢弃该组数据。
浙公网安备 33010602011771号