导读:《蓝桥杯单片机组》专栏文章是博主2018年参加蓝桥杯的单片机组比赛所做的学习笔记,在当年的比赛中,博主是获得了省赛一等奖,国赛二等奖的成绩。成绩虽谈不上最好,但至少问心无愧。如今2021年回头再看该系列文章,仍然感触颇多。为了能更好地帮助到单片机初学者,今年特地抽出时间对当年的文章逻辑和结构进行重构,以达到初学者快速上手的目的。需要指出的是,由于本人水平有限,如有错误还请读者指出,非常感谢。那么,接下来让我们一起开始愉快的学习吧。
前面的学习中,我们依次突破了单片机初学过程中的两个难点:中断和时序。今天在此基础上,我们继续来学习一下ADC模块,即:数模/模数转换模块。关于数模和模数的概念如果学过了数电,那理解起来自然没有难度。如果第一次接触,还是要花点时间好好消化下的。废话不多说,马上开始搞,今天使用的AD/DA转换模组是:PCFPCF8591,代码下载到Github<传送门>。
一、基础理论
1.1、A/D 重要指标
1、ADC 的位数 一个 n 位的 ADC 表示这个 ADC 共有 2 的 n 次方个刻度。8 位的 ADC,输出的是从 0~255 一共 256 个数字量,也就是 2 的 8 次方个数据刻度。
2、基准源 基准源,也叫基准电压,是 ADC 的一个重要指标,要想把输入 ADC 的信号测量准确,那么基准源首先要准,基准源的偏差会直接导致转换结果的偏差。比如一根米尺,总长度本应该是 1 米,假定这根米尺被火烤了一下,实际变成了 1.2 米,再用这根米尺测物体长度的话自然就有了较大的偏差。假如我们的基准源应该是 5.10V,但是实际上提供的却是 4.5V,这样误把 4.5V 当成了 5.10V 来处理的话,偏差也会比较大。
3、分辨率 分辨率是数字量变化一个最小刻度时,模拟信号的变化量,定义为满刻度量程与 2^n -1 的比值。假定 5.10V 的电压系统,使用 8 位的 ADC 进行测量,那么相当于 0~255 一共 256 个刻度把 5.10V 平均分成了 255 份,那么分辨率就是 5.10/255 = 0.02V。
分辨率和精度并不是一个概念,详细看这里。
4、INL(积分非线性度)和 DNL(差分非线性度)
ADC精度关系重大的两个指标是INL(IntegralNonLiner)和 DNL(Differencial NonLiner) INL 指的是 ADC 器件在所有的数值上对应的模拟值,和真实值之间误差最大的那一个点的误差值,是 ADC 最重要的一个精度指标,单位是 LSB。
LSB(Least Significant Bit)是最低有效位的意思,那么它实际上对应的就是 ADC的分辨率。一个基准为5.10V的8位ADC,它的分辨率就是 0.02V,用它去测量一个电压信号,得到的结果是 100,就表示它测到的电压值是 100*0.02V=2V,假定它的 INL 是 1LSB,就表示这个电压信号真实的准确值是在1.98V~2.02V 之间的,按理想情况对应得到的数字应该是 99~101,测量误差是一个最低有效位,即 1LSB。
DNL 表示的是 ADC 相邻两个刻度之间最大的差异,单位也是 LSB。一把分辨率是 1 毫米的尺子,相邻的刻度之间并不都刚好是 1 毫米,而总是会存在或大或小的误差。同理,一个 ADC 的两个刻度线之间也不总是准确的等于分辨率,也是存在误差,这个误差就是 DNL。
一个基准为 5.10V 的 8 位 ADC,假定它的 DNL 是 0.5LSB,那么当它的转换结果从 100 增加到 101 时,理想情况下实际电压应该增加 0.02V,但 DNL 为 0.5LSB 的情况下实际电压的增加值是在 0.01~0.03V 之间。值得一提的是 DNL 并非一定小于 1LSB,很多时候它会等于或大于 1LSB,这就相当于是一定程度上的刻度紊乱,当实际电压保持不变时,ADC 得出的结果可能会在几个数值之间跳动,很大程度上就是由于这个原因(但并不完全是,因为还有无时无处不在的干扰的影响)。
5、转换速率 转换速率,是指 ADC 每秒能进行采样转换的最大次数,单位是 sps (或 s/s、sa/s,即 samplesper second),它与 ADC 完成一次从模拟到数字的转换所需要的时间互为倒数关系。ADC 的种类比较多,其中积分型的 ADC 转换时间是毫秒级的,属于低速 ADC;逐次逼近型 ADC转换时间是微秒级的,属于中速 ADC;并行/串行的 ADC 的转换时间可达到纳秒级,属于高速 ADC。
1.2、PCF8591介绍
PCF8591 是一个单电源低功耗的 8 位 CMOS 数据采集器件,具有 4 路模拟输入,1 路模拟输出和一个串行 I 2 C 总线接口用来与单片机通信。与前面讲过的 24C02 类似,3 个地址引脚 A0、A1、A2 用于编程硬件地址,允许最多 8 个器件连接到 I 2 C 总线而不需要额外的片选电路。器件的地址、控制以及数据都是通过 I 2 C 总线来传输
这里写图片描述
其中引脚 1、2、3、4 是 4 路模拟输入,引脚 5、6、7 是 I 2 C 总线的硬件地址,8 脚是数字地 GND,9 脚和 10 脚是 I 2 C 总线的 SDA 和 SCL。12 脚是时钟选择引脚,如果接高电平表示用外部时钟输入,接低电平则用内部时钟,我们这套电路用的是内部时钟,因此 12 脚直接接 GND,同时 11 脚悬空。13 脚是模拟地 AGND,在实际开发中,如果有比较复杂的模拟电路,那么 AGND 部分在布局布线上要特别处理,而且和 GND 的连接也有多种方式,这个板子上没有复杂的模拟部分电路,所以我们把 AGND 和 GND 接到一起。14 脚是基准源,15 脚是 DAC 的模拟输出,16 脚是供电电源 VCC。
PCF8591 的 ADC 是逐次逼近型的,转换速率算是中速,但是它的速度瓶颈在 I 2 C 通信上。由于 I 2 C 通信速度较慢,所以最终的 PCF8591 的转换速度,直接取决于 I 2 C 的通信速率。由于 I 2 C 速度的限制,所以 PCF8591 得算是个低速的 AD 和 DA 的集成,主要应用在一些转换速度要求不高,希望成本较低的场合,比如电池供电设备,测量电池的供电电压,电压低于某一个值,报警提示更换电池等类似场合。
ref 基准电压的提供有两种方法。一是采用简易的原则,直接接到 VCC 上去,但是由于 VCC 会受到整个线路的用电功耗情况影响,一来不是准确的 5V,实测大多在 4.8V 左右,二来随着整个系统负载情况的变化会产生波动,所以只能用在简易的、对精度要求不高的场合。方法二是使用专门的基准电压器件,比如 TL431,它可以提供一个精度很高的 2.5V 的电压基准。
这里写图片描述
蓝桥的板子是直接接到VCC上的,不过它还进行了并联电容的处理。
这里写图片描述
对于AD 来说,只要输入信号超过 Vref 基准源,它得到的始终都是最大值,即 255,也就是说它实际上无法测量超过其 Vref 的电压信号的。需要注意的是,所有输入信号的电压值都不能超过 VCC,即+5V,否则可能会损坏 ADC 芯片。(注意Vref和VCC不一定相等,它取决于你采用哪种方式接线!)
1.3、PCF8591编程介绍
PCF8591 的通信接口是 I 2 C,那么编程肯定是要符合这个协议的。单片机对 PCF8591 进行初始化,一共发送三个字节即可!
第一个字节:器件地址字节
这里写图片描述
其中 7 位代表地址,1 位代表读写方向。地址高 4 位固定是 0b1001,低三位是 A2,A1,A0,这三位我们电路上都接了 GND,因此也就是 0b000。
第二个字节:器件控制字节
控制字节的第 6 位是 DA 使能位,这一位置 1 表示 DA 输出引脚使能,会产生模拟电压输出功能。
第4位和第5位可以实现把PCF8591的4路模拟输入配置成单端模式和差分模式,是配置 AD输入方式的控制位。单端模式和差分模式的区别。
控制字节的第 2 位是自动增量控制位,自动增量的意思就是,比如我们一共有 4 个通道,当我们全部使用的时候,读完了通道 0,下一次再读,会自动进入通道 1 进行读取,不需要我们指定下一个通道。
注意:由于 A/D 每次读到的数据,都是上一次的转换结果,所以在使用自动增量功能的时候,要特别注意,当前读到的是上一个通道的值。 为了保持程序的通用性,代码没有使用这个功能,而是直接做了一个通用的程序,可以参考一下!
具体实现: 程序在进行 A/D 读取数据的时候,共使用了两条程序去读了 2 个字节:I2CReadACK();
val = I2CReadNAK();
PCF8591 的转换时钟是 I2C 的 SCL,8 个SCL 周期完成一次转换,所以当前的转换结果总是在下一个字节的 8 个 SCL 上才能读出,因此我们这里第一条语句的作用是产生一个整体的 SCL 时钟提供给 PCF8591 进行 A/D 转换,第二次是读取当前的转换结果。如果我们只使用第二条语句的话,每次读到的都是上一次的转换结果。
控制字节的第 0 位和第 1 位就是通道选择位了,00、01、10、11 代表了从 0 到 3 的一共4 个通道选择。
第三个字节 D/A 数据寄存器
如果仅仅使用A/D功能,这一个字节可不进行控制!
二、动手实验
2.1、ADC实验中对应的一段核心代码!
u8 GetADCValue(u8 ch)
{
u8 val;
I2CStart();
if(I2CWrite(0x48<<1) == 0) //寻器件地址 - 写
{
I2CStop();
return 0; //这里并不像I2C那样使用break,现在是读东西,读不到就得返回0.
}
I2CWrite(0x40 | ch);//对应单端模式 - 通道数
I2CStart();
I2CWrite((0x48<<1) | 0x01);//寻器件地址 - 读
I2CReadACK();
val = I2CReadNAK();//这次的值,是在下8个SCL输出,所以先空读然后再去NAK读!
I2CStop();
return val;
}
void ValueToString(u8 *str, u8 val)//!!注意这里的处理技巧,把电压扩大了10倍!!!
{
val = (val*50) / 255; //电压5V,256个刻度分成255份!
str[0] = (val/10) + '0';
str[1] = '.';
str[2] = (val%10) + '0';
str[3] = 'V';
str[4] = '\0';
}
2.2、DAC当然也有重点!
void SetDACOut(u8 val)
{
I2CStart();
if(!I2CWrite(0x48<<1))
{
I2CStop();
return;
}
I2CWrite(0x40);
I2CWrite(val);
I2CStop();
}
void KeyAction(u8 keycode)
{
static u8 volt = 0;
if(keycode == 0x26)
{
if(volt < 50)
{
volt++;
SetDACOut((volt*255)/50);//输入数字量,注意区别ADC的公式!
}
}
else if(keycode == 0x28)
{
if(volt > 0)
{
volt--;
SetDACOut((volt*255)/50);//输入数字量,注意区别ADC的公式!
}
}
}
2.3、DAC做的波形发生器!
u8 code SinWave[] = { //正弦波波表
127, 152, 176, 198, 217, 233, 245, 252,
255, 252, 245, 233, 217, 198, 176, 152,
127, 102, 78, 56, 37, 21, 9, 2,
0, 2, 9, 21, 37, 56, 78, 102,
};
u8 code TriWave[] = { //三角波波表
0, 16, 32, 48, 64, 80, 96, 112,
128, 144, 160, 176, 192, 208, 224, 240,
255, 240, 224, 208, 192, 176, 160, 144,
128, 112, 96, 80, 64, 48, 32, 16,
};
u8 code SawWave[] = { //锯齿波表
0, 8, 16, 24, 32, 40, 48, 56,
64, 72, 80, 88, 96, 104, 112, 120,
128, 136, 144, 152, 160, 168, 176, 184,
192, 200, 208, 216, 224, 232, 240, 248,
};
.....
void SetWaveFreq(u8 freq)
{
u32 tmp;
tmp = (11059200/12) / (freq*32); //计数器计数频率是波形频率的32倍!
tmp = 65536 - tmp;
T1RH = (u8)(tmp>>8);
T1RL = (u8)tmp;
TMOD &= 0x0F;
TMOD |= 0x10;
TH1 = T1RH;
TL1 = T1RL;
ET1 = 1;
PT1 = 1; //设置高优先级!
TR1 = 1;
}
...
void InterrupTimer1() interrupt 3
{
static u8 i=0;
TH1 = T1RH;
TL1 = T1RL;
SetDACOut(pWave[i]);
i++;
if(i >= 32)
{
i=0;
}
}
上面关键是看void SetWaveFreq(u8 freq)
的实现!
2.4、赛前封装的PCF8591函数相关
#include "config.h"
#include "i2c.h"
#define VCC 48//电压是扩大10倍以后的电压。。注意此处的电压是实测的电压值!!!
u8 GetADCValue(u8 ch)
{
u8 val;
I2CStart();
if(!(I2CWrite(0x48<<1)))
{
I2CStop();
return 0;
}
I2CWrite(0x40|ch);
I2CStart();
I2CWrite((0x48<<1) | 0x01);
I2CReadACK();
val = I2CReadNAK();
I2CStop();
val = (val*VCC)/255;//val扩大十倍,手动加小数点
return val;
}
void SetDACOut(u8 val)//输入也是一样的道理,默认输入扩大十倍,然后处理.
{
val = (val*255)/VCC;
I2CStart();
if(!(I2CWrite(0x48<<1)))
{
I2CStop();
return;
}
I2CWrite(0x40);
I2CWrite(val);
I2CStop();
}
GetADCValue
在获取相关的AD值
以后,然后进行了转换再输出!
SetDACOut
是直接设置相关的DA值,注意输入的是扩大十倍以后的电压值!
蓝桥板子的AIN×
输入介绍:
AIN0
:是接到右边排针上,可以用杜邦线连外部模拟电压信号 AIN1
:是接到光敏电阻上 AIN2
:放大器的输出端 AIN3
:是接到滑动变阻器Rb2上
注意DA功能输出在右边倒数第二个排针引脚上,D/A。倒数第三个排针是可以接外部任意电压信号的口。
小结:本篇文章主要介绍了单片机学习中的一个重头戏:AD/DA操作,并结合了常见的AD/DA操作方式进行了详细的介绍。在该部分学习中比较困难的就是模拟数字这个东西的理解,一个新的概念一次理解不到位也没关系,多来几次。相信在某一个瞬间,你也能有醍醐灌顶的感觉。
希望大家多多支持我的原创文章。如有错误,请大家及时指正,非常感谢。