- 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 得出的结果可能会在几个数值之间跳动,很大程度上就是由于这个原因(但并不完全是,因为还有无时无处不在的干扰的影响)。
PCF8591是一个单电源低功耗的 8 位 CMOS 数据采集器件,具有 4 路模拟输入,1 路模拟输出和一个串行 I 2 C 总线接口用来与单片机通信。与前面讲过的 24C02 类似,3 个地址引脚 A0、A1、A2 用于编程硬件地址,允许最多 8 个器件连接到I2C 总线而不需要额外的片选电路。器件的地址、控制以及数据都是通过 I2C 总线来传输。
其中引脚 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 的集成,主要应用在一些转换速度要求不高,希望成本较低的场合,比如电池供电设备,测量电池的供电电压,电压低于某一个值,报警提示更换电池等类似场合。
Vref 基准电压的提供有两种方法。一是采用简易的原则,直接接到 VCC 上去,但是由于 VCC 会受到整个线路的用电功耗情况影响,一来不是准确的 5V,实测大多在 4.8V 左右,二来随着整个系统负载情况的变化会产生波动,所以只能用在简易的、对精度要求不高的场合。方法二是使用专门的基准电压器件,比如 TL431,它可以提供一个精度很高的 2.5V 的电压基准。
对于AD 来说,只要输入信号超过 Vref 基准源,它得到的始终都是最大值,即 255,也就是说它实际上无法测量超过其 Vref 的电压信号的。需要注意的是,所有输入信号的电压值都不能超过 VCC,即+5V,否则可能会损坏 ADC 芯片。在CT107D开发板上,Vref是直接接到了VCC上。
PCF8591 的通信接口是 i2C,那么编程肯定是要符合这个协议的。单片机对 PCF8591 进行初始化,一共发送三个字节即可。
第一个字节,和E2PROM类似,是器件地址字节,其中7位代表地址,一位代表读写方向。地址高四位固定是 0b1001,低三位是A2、A1、A0,这三位在电路上都接到了GND,因此也就是0b00,如下图所示:
第二个字节,将被存储在控制寄存器里,用于控制PCF8591的功能。其中第3位和第7位是固定的0,另外6位各自有各自的作用,如下图所示:
控制字节的第 6 位是 DA 使能位,这一位置 1 表示 DA 输出引脚使能,会产生模拟电压输出功能。
第4位和第5位可以实现把PCF8591的4路模拟输入配置成单端模式和差分模式,是配置 AD输入方式的控制位。单端模式和差分模式的区别。 如下图所示:
控制字节的第 2 位是自动增量控制位,自动增量的意思就是,比如一共有 4 个通道,当全部使用的时候,读完了通道 0,下一次再读,会自动进入通道 1 进行读取,不需要我们指定下一个通道。
注意:由于 A/D 每次读到的数据,都是上一次的转换结果,所以在使用自动增量功能的时候,要特别注意,当前读到的是上一个通道的值。 为了保持程序的通用性,代码没有使用这个功能,而是直接做了一个通用的程序。
控制字节的第 0 位和第 1 位就是通道选择位了,00、01、10、11 代表了从 0 到 3 的一共4 个通道选择。
第三个字节,D/A数据寄存器,表示D/A模拟输出的电压值。如果仅仅使用A/D功能,可不发送第三个字节!
pcf8591.c
#include
#include "iic.h"
#define PCF8591W 0x90
#define PCF8591R 0x91
/*******************************************************************************
* 函数名 :Read_AIN
* 输入值 :unsigned char chn
* 返回值 :unsigend char dat
* 作者 :guyao
* 时间 :2021/2/22
* 功能描述:读取PCF8591AIN采集数据
* 备注 :chn为PCF8591的通道
*******************************************************************************/
unsigned char Read_AIN(unsigned char chn)
{
unsigned char dat;
EA = 0;
IIC_Start();
IIC_SendByte(PCF8591W);//PCF8591写地址
IIC_WaitAck();
IIC_SendByte(chn);//写入PCF8591控制字节
IIC_WaitAck();
IIC_Stop();
somenop;
IIC_Start();
IIC_SendByte(PCF8591R);//PCF8591读地址
IIC_WaitAck();
dat = IIC_RecByte();//读取PCF8591通道3的数据
IIC_Ack(0);
IIC_Stop();
EA = 1;
return dat;
}
/*******************************************************************************
* 函数名 :ValueToString
* 输入值 :unsigned char *str, unsigned char val
* 返回值 :none
* 作者 :guyao
* 时间 :2021/2/22
* 功能描述:将PCF8591AIN采集的数据转换为字符型
* 备注 :注意这里把电压扩大了10倍
*******************************************************************************/
void ValueToString(unsigned char *str, unsigned char val)
{
val = (val*50)/255;//电压5v,256个刻度分成255份
str[0] = val / 10 + 48; //0~9对应assica表 48~57
str[1] = '.';
str[2] = val % 10 + 48;
str[3] = 'V';
}
注意:在程序里我设置开始读ADC值时关闭中断,避免ADC测量程序被中断打断,影响精准度
uart.c - - 串口打印输出从AIN3通道测量的电压值
#include "sys.h"
#define FOCS 11059200L
bit Txdflag;
void Uart_Init(uint baud)
{
AUXR &= 0XBF; //设置定时器1为12T
AUXR &= 0XFE; //选择定时器1作为波特率发生器
SCON = 0X40; //工作方式1
PCON &= 0X7F; //波特率不加倍
TMOD &= 0X0F;
TMOD |= 0x20;
TH1 = TL1 = 256 - FOCS/32/12/baud;
ET1 = 0; //关闭定时器1中断
ES = 1; //打开串口中断
TR1 = 1; //启动定时器1
}
void Uart_Interrupt() interrupt 4
{
if(TI)//发送数据完毕
{
TI = 0;
Txdflag = 1;
}
}
void Uart_Print(unsigned char *str)
{
unsigned char len;
len = sizeof(str)+ 1;
while(len != 0)
{
Txdflag = 0;
SBUF = *str++;
while(!Txdflag);
len--;
if(len == 0)
{
SBUF = '\t';
while(!Txdflag);
}
}
}
nixie.c - - 数码管显示电压值
#include "sys.h"
// 0 1 2 3 4 5 6 7
uchar code nixie[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,
// 8 9 a b c d e f u
0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e,0xc1}; //共阳数码管码字
uchar NixieBuff[] = {0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff};
uchar smg1,smg2,smg3,smg4,smg5,smg6,smg7,smg8;
uchar code Symbol[] = {0xff,0xbf}; //全灭,-
void Nixie_Scan()
{
static uchar index=0;
P2 = (P2 & 0X1F)|0xe0;
P0 = 0XFF; //消影
P2 = (P2 & 0X1F)|0xc0;
P0 = 0x01<<index;
P2 = (P2 & 0X1F)|0xe0;
P0 = NixieBuff[index];
P2 &= 0X1F;
index++;
index &= 0x07;
}
void Nixie_Show()
{
NixieBuff[0] = nixie[smg1]&0x7f;
NixieBuff[1] = nixie[smg2];
NixieBuff[2] = nixie[smg3];
NixieBuff[3] = Symbol[smg4];
NixieBuff[4] = Symbol[smg5];
NixieBuff[5] = Symbol[smg6];
NixieBuff[6] = Symbol[smg7];
NixieBuff[7] = Symbol[smg8];
}
void Nixie_Drive(unsigned char val)
{
val = (val*50)/255;
smg1 = val/10;
smg2 = val%10;
smg3 = 16;
smg4= smg5= smg6 = smg7= 0;
}
sys.c
#include "sys.h"
extern bit Read_AIN_Falg100ms;
extern bit Uart_Print_Flag1s;
/**
*@brief 外设初始化
*@param[in] none
*@return none
**/
void ALL_Init()
{
P2 = (P2&0x1f)|0xa0; //打开Y5C
P0 = 0x00; //关闭蜂鸣器&继电器
P2 = (P2&0x1f)|0xe0; //打开Y7C
P0 = 0xff; //关闭数码管
P2 = (P2&0x1f)|0x80; //打开Y4C
P0 = 0xff; //关闭LED
P2 = P2&0x1f; //关闭所用使能
}
/**
*@brief 延时函数
*@param[in] 延时多少ms(0~65535)
*@return none
**/
void Operate_Delay(u16 ms)
{
u16 i;
for(ms;ms>0;ms--)
for(i=921;i>0;i--);
}
void Timer0Init(void) //1毫秒@11.0592MHz
{
AUXR |= 0x80; //定时器时钟1T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0xCD; //设置定时初值
TH0 = 0xD4; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1;
EA = 1;
}
void Timer0() interrupt 1
{
static uint i,n;
i++;
n++;
if(n==100)
{
Read_AIN_Falg100ms = 1;
n = 0;
}
if(i==1000)
{
i = 0;
Uart_Print_Flag1s = 1;
}
Nixie_Show();
Nixie_Scan();
}
main.c
#include "sys.h"
unsigned char buf[4] = {0};
bit Read_AIN_Falg100ms = 0;
bit Uart_Print_Flag1s = 0;
void main()
{
unsigned char val;
ALL_Init();
Timer0Init();
Uart_Init(9600);
while(1)
{
//100ms读取一次电压值
if(Read_AIN_Falg100ms)
{
val = Read_AIN(0x03);
Read_AIN_Falg100ms = 0;
}
//串口每1s打印一次电压值
if(Uart_Print_Flag1s)
{
Uart_Print(buf);
Uart_Print_Flag1s = 0;
}
ValueToString(buf,val);
Nixie_Drive(val);
}
}
根据原理图,j3这排针脚中的第19个引脚为模拟量输出引脚,输出电压量程为0~5v
对于 val 的取值,应该在0~255 当val = 255时 对应P19口输出5V电压
/*******************************************************************************
* 函数名 :SetDACOut
* 输入值 :unsigned char val
* 返回值 :none
* 作者 :guyao
* 时间 :2021/2/23
* 功能描述:输入电压值,设置DAC输出值
* 备注 :val为设定的电压值
*******************************************************************************/
void SetDACOut(unsigned char val)
{
IIC_Start();
if(IIC_WaitAck())
{
IIC_Stop();
return;
}
IIC_SendByte(0x40);//使能AD输出
IIC_SendByte(val);//输出值
IIC_Stop();
}