第17章 A/D和D/A的学习

发布者:caijt最新更新时间:2020-07-23 来源: 51hei关键字:单片机 手机看文章 扫描二维码
随时随地手机看文章

从我们学到的知识了解到,我们的单片机是一个典型的数字系统。数字系统只能对输入的数字信号进行处理,其输出信号也是数字信号。但是在工业检测系统和日常生活中的许多物理量都是模拟量,比如温度、长度、压力、速度等等,这些模拟量可以通过传感器变成与之对应的电压、电流等电模拟量。为了实现数字系统对这些电模拟量的检测、运算和控制,就需要一个模拟量和数字量之间相互转换的过程。这节课我们就要学习这个相互转换过程。


17.1 A/D和D/A的基本概念
A/D是模拟量到数字量的转换,依靠的是模数转换器(Analog to Digital Converter),简称ADC;D/A是数字量到模拟量的转换,依靠的是数模转换器(Digital to Analog Converter),简称DAC。他们的道理是完全一样的,只是转换方向不同,因此我们讲解过程主要以A/D为例来讲解。


很多同学学到A/D这部分的时候,感觉是个难点,概念掌握不清楚。我个人认为主要原因不在于技术问题,而是不太会感悟生活。我们生活中有很多很多A/D的例子,只是没有在单片机领域里应用而已,下面我带着大家一起感悟一下A/D的概念。


什么是模拟量?就是指变量在一定范围内连续变化的量,也就是在一定范围内可以取任意值。比如我们米尺,从0到1米之间,可以是任意值。什么是任意值,也就是可以是1cm,也可以是1.001cm,当然也可以10.000......后边有无限个小数。总之,任何两个数字之间都有无限个中间值,所以称之为连续变化的量,也就是模拟量。


而我们用的米尺上被我们人为的做上了刻度符号,每两个刻度之间的间隔是1mm,这个刻度实际上就是我们对模拟量的数字化,由于有一定的间隔,不是连续的,所以在专业领域里我们称之为离散的。我们的ADC就是起到把连续的信号用离散的数字表达出来的作用。那么我们就可以使用米尺这个“ADC”来测量连续的长度或者高度这些模拟量。如图17-1一个简单的米尺刻度示意图。

ps5b153.jpg

图17-1 米尺刻度示意图


我们往杯子里倒水,水位会随着倒入的水量的多少而变化。现在就用这个米尺来测量我们杯子里的水位的高度。水位变化是连续的,而我们只能通过尺子上的刻度来读取水位的高度,获取我们想得到的水位的数字量信息。这个过程,就可以简单理解为我们电路中的ADC采样。


17.2 A/D的主要指标

我们在选取和使用A/D的时候,依靠什么指标来判断很重要。由于AD的种类很多,分为积分型、逐次逼近型、并行/串行比较型、Σ-Δ型等多种类型。同时指标也比较多,并且有的指标还有轻微差别,在这里我是以同学们便于理解的方法去讲解,如果和某一确定类型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、分辨率

分辨率是数字量变化一个最小刻度时,模拟信号的变化量,定义为满刻度量程与2n-1的

比值。5.10V的电压系统,使用8位的ADC进行测量,那么相当于0到255一共256个刻度,把5.10V平均分成了255份,那么分辨率就是5.10/255 = 0.02V。


4、INL(积分非线性度)和DNL(差分非线性度)

初学者最容易混淆的两个概念就是“分辨率”和“精度”,认为分辨率越高,则精度越高,而实际上,两者之间是没有必然联系的。分辨率是用来描述刻度划分的,而精度是用来描述准确程度的。同样一根米尺,刻度数相同,分辨率就相当,但是精度却可以相差很大,如图17-2所示。

ps5b154.jpg


图17-2 米尺精度对比


图17-2表示的精度一目了然,不需多说。和ADC精度关系重大的两个指标是INL(Integral NonLiner)和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.03之间。值得一提的是DNL并非一定小于1LSB,很多时候它会等于或大于1LSB,这就相当于是一定程度上的刻度紊乱,当实际电压保持不变时,ADC得出的结果可能会在几个数值之间跳动,很大程度上就是由于这个原因(但并不完全是,因为还有无时无处不在的干扰的影响)。


5、转换速率

转换速率,是指ADC每秒能进行采样转换的最大次数,单位是sps(或s/s、sa/s,即samples per second),它与ADC完成一次从模拟到数字的转换所需要的时间互为倒数关系。ADC的种类比较多,其中积分型的ADC转换时间是毫秒级的,属于低速ADC;逐次逼近型ADC转换时间是微妙级的,属于中速ADC;并行/串行的ADC的转换时间可达到纳秒级,属于高速ADC。


ADC的这几个主要指标大家先熟悉一下,对于其他的,作为一个入门级别的选手来说,先不着急深入理解。以后使用过程中遇到了,再查找相关资料深入学习,当前重点是在头脑中建立一个ADC的基本概念。


17.3 PCF8591的硬件接口

PCF8591是一个单电源低功耗的8位CMOS数据采集器件,具有4路模拟输入,1路模拟输出和一个串行I2C总线接口用来与MCU通信。3个地址引脚A0、A1、A2用于编程硬件地址,允许最多8个器件连接到I2C总线而不需要额外的片选电路。器件的地址、控制以及数据都是通过I2C总线来传输,我们先看一下PCF8591的原理图,如图17-3所示。

ps5b155.jpg

图17-3 PCF8591原理图


其中引脚1、2、3、4是4路模拟输入,引脚5、6、7是I2C总线的硬件地址,8脚是数字GND,9脚和10脚是I2C总线的SDA和SCL。12脚是时钟选择引脚,如果接高电平表示用外部时钟输入,接低电平则用内部时钟,我们这套电路用的是内部时钟,因此12脚直接接GND,同时11脚悬空。13脚是模拟GND,在实际开发中,如果有比较复杂的模拟电路,那么模拟GND部分在布局布线上要特别处理,而且和数字GND的连接也有多种方式,这里大家先了解即可。在我们板子上没有复杂的模拟部分电路,所以我们把模拟的GND和数字GND接到一起即可。14脚是基准源,15脚是DAC的模拟输出,16脚是供电电源VCC。


PCF8591的ADC是逐次逼近型的,转换速率算是中速,但是他的速度瓶颈在I2C通信上。由于I2C通信速度较慢,所以最终的PCF8591的转换速度,直接取决于I2C的通信速率。由于I2C速度的限制,所以PCF8591的算是个低速的AD和DA集成,主要应用在一些转换速度要求不高,希望成本较低的场合,比如电池供电设备,测量电池的供电电压,电压低于某一个值,报警提示更换电池等类似场合。


Vref基准电压的提供,方法一是采用简易的原则,直接接到VCC上去。但是由于VCC会受到整个线路的用电功耗情况影响,一来不是准确的5V,实测大多在4.8V左右,二来随着整个系统负载情况的变化会产生波动,所以只能用在简易的、对精度要求不高的场合。方法二是使用专门的基准电压器件,比如TL431,它可以提供一个精度很高的2.5V的电压基准,这是我们通常采用的方法。如图17-4所示。

ps5b156.jpg

图17-4 PCF8591电路图


图中J17是双排插针,大家可以根据自己的需求选择跳线帽短接还是使用杜邦线接其他外接电路,都是可以的。在这个地方,我们直接把J17的3脚和4脚用跳线帽短路起来,那么现在Vref的基准源就是2.5V了。分别把5和6、7和8、9和10、11和12用跳线帽短接起来的话,那么我们的AIN0实测的就是滑动变阻器的分压值,AIN1和AIN2测的是GND的值,AIN3测的是+5V的值。这里需要注意的是,AIN3虽然测的是+5V的值,但是对于AD来说,只要输入信号超过Vref基准源,它得到的始终都是最大值,即255,也就是说它实际上无法测量超过其Vref的电压信号。需要注意的是,所有输入信号的电压值都不能超过VCC,即+5V,否则可能会损坏ADC芯片。


17.4 PCF8591的软件编程

PCF8591的通信接口是I2C,那么编程肯定是符合这个协议的。单片机对PCF8591进行初始化,一共发送三个字节即可。第一个字节,和EEPROM类似,第一个字节是地址字节,其中7位代表地址,1位代表读写方向。地址高4位固定是1001,低三位是A2,A1,A0,这三位我们电路上都接了GND,因此也就是000,如图17-5所示。

ps5b157.jpg

图17-5 PCF8591地址字节

发送到PCF8591的第二个字节将被存储在控制寄存器,用于控制PCF8591的功能。其中第3位和第7位是固定的0,另外6位各自有各自的作用,如图17-6所示,我逐一介绍。

ps5b158.jpg

图17-6 PCF8591控制字节


控制字节的第6位是DA使能位,这一位置1表示DA输出引脚使能,会产生模拟电压输出功能。第4位和第5位可以实现把PCF8591的4路模拟输入配置成单端模式和差分模式,单端模式和差分模式的区别,我们17.4章节有介绍,这里大家只需要知道这两位是配置AD输入方式的控制位即可,如图17-7所示。

ps5b159.jpg

图17-7 PCF8591模拟输入配置方式


控制字节的第2位是自动增量控制位,自动增量的意思就是,比如我们一共有4个通道,当我们全部使用的时候,读完了通道0,下一次再读,会自动进入通道1进行读取,不需要我们指定下一个通道,由于A/D每次读到的数据,都是上一次的转换结果,所以同学们在使用自动增量功能的时候,要特别注意,当前读到的是上一个通道的值。为了保持程序的通用性,我们的代码没有使用这个功能,直接做了一个通用的程序。


控制字节的第0位和第1位就是通道选择位了,00、01、10、11代表了从0到3的一共4个通道选择。


发送给PCF8591的第三个字节D/A数据寄存器,表示D/A模拟输出的电压值。D/A模拟我们一会介绍,大家知道这个字节的作用即可。我们如果仅仅使用A/D功能的话,就可以不发送第三个字节。


下面我们用一个程序,把AIN0、AIN1、AIN3测到的电压值显示在液晶上,同时大家可以转动电位器,会发现AIN0的值发生变化。


/***********************lcd1602.c文件程序源代码*************************/

#include


#define LCD1602_DB   P0


sbit LCD1602_RS = P1^0;

sbit LCD1602_RW = P1^1;

sbit LCD1602_E  = P1^5;


void LcdWaitReady()  //等待液晶准备好

{

    unsigned char sta;


    LCD1602_DB = 0xFF;

    LCD1602_RS = 0;

    LCD1602_RW = 1;

    do

    {

        LCD1602_E = 1;

        sta = LCD1602_DB; //读取状态字

        LCD1602_E = 0;

    } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止

}

void LcdWriteCmd(unsigned char cmd)  //写入命令函数

{

    LcdWaitReady();

    LCD1602_RS = 0;

    LCD1602_RW = 0;

    LCD1602_DB = cmd;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

void LcdWriteDat(unsigned char dat)  //写入数据函数

{

    LcdWaitReady();

    LCD1602_RS = 1;

    LCD1602_RW = 0;

    LCD1602_DB = dat;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  //显示字符串,屏幕起始坐标(x,y),字符串指针str

{

    unsigned char addr;


    //由输入的显示坐标计算显示RAM的地址

    if (y == 0)

        addr = 0x00 + x; //第一行字符地址从0x00起始

    else

        addr = 0x40 + x; //第二行字符地址从0x40起始


    //由起始显示RAM地址连续写入字符串

    LcdWriteCmd(addr | 0x80); //写入起始地址

    while (*str != '')      //连续写入字符串数据,直到检测到结束符

    {

        LcdWriteDat(*str);

        str++;

    }

}

void LcdInit()  //液晶初始化函数

{

    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口

    LcdWriteCmd(0x0C);  //显示器开,光标关闭

    LcdWriteCmd(0x06);  //文字不动,地址自动+1

    LcdWriteCmd(0x01);  //清屏

}

/***********************I2C.c文件程序源代码*************************/

#include

#include


#define I2CDelay()  {_nop_();_nop_();_nop_();_nop_();}


sbit I2C_SCL = P3^7;

sbit I2C_SDA = P3^6;


void I2CStart()  //产生总线起始信号

{

    I2C_SDA = 1; //首先确保SDA、SCL都是高电平

    I2C_SCL = 1;

    I2CDelay();

    I2C_SDA = 0; //先拉低SDA

    I2CDelay();

    I2C_SCL = 0; //再拉低SCL

}

void I2CStop()   //产生总线停止信号

{

    I2C_SCL = 0; //首先确保SDA、SCL都是低电平

    I2C_SDA = 0;

    I2CDelay();

    I2C_SCL = 1; //先拉高SCL

    I2CDelay();

    I2C_SDA = 1; //再拉高SDA

    I2CDelay();

}

bit I2CWrite(unsigned char dat) //I2C总线写操作,待写入字节dat,返回值为应答状态

{

    bit ack;  //用于暂存应答位的值

    unsigned char mask;  //用于探测字节内某一位值的掩码变量


    for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行

    {

        if ((mask&dat) == 0)  //该位的值输出到SDA上

            I2C_SDA = 0;

        else

            I2C_SDA = 1;

        I2CDelay();

        I2C_SCL = 1;          //拉高SCL

        I2CDelay();

        I2C_SCL = 0;          //再拉低SCL,完成一个位周期

    }

    I2C_SDA = 1;   //8位数据发送完后,主机释放SDA,以检测从机应答

    I2CDelay();

    I2C_SCL = 1;   //拉高SCL

    ack = I2C_SDA; //读取此时的SDA值,即为从机的应答值

    I2CDelay();

    I2C_SCL = 0;   //再拉低SCL完成应答位,并保持住总线


    return (~ack); //应答值取反以符合通常的逻辑:0=不存在或忙或写入失败,1=存在且空闲或写入成功

}

unsigned char I2CReadNAK() //I2C总线读操作,并发送非应答信号,返回值为读到的字节

{

    unsigned char mask;

    unsigned char dat;


    I2C_SDA = 1;  //首先确保主机释放SDA

    for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行

    {

        I2CDelay();

        I2C_SCL = 1;      //拉高SCL

        if(I2C_SDA == 0)  //读取SDA的值

            dat &= ~mask; //为0时,dat中对应位清零

        else

            dat |= mask;  //为1时,dat中对应位置1

        I2CDelay();

        I2C_SCL = 0;      //再拉低SCL,以使从机发送出下一位

    }

    I2C_SDA = 1;   //8位数据发送完后,拉高SDA,发送非应答信号

    I2CDelay();

    I2C_SCL = 1;   //拉高SCL

    I2CDelay();

    I2C_SCL = 0;   //再拉低SCL完成非应答位,并保持住总线


    return dat;

}

unsigned char I2CReadACK() //I2C总线读操作,并发送应答信号,返回值为读到的字节

{

    unsigned char mask;

    unsigned char dat;


    I2C_SDA = 1;  //首先确保主机释放SDA

    for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行

[1] [2] [3] [4]
关键字:单片机 引用地址:第17章 A/D和D/A的学习

上一篇:第18章 RS485通信和Modbus协议
下一篇:第16章 红外通信和DS18B20温度传感器的学习

小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
  • 学习ARM开发(16)
    ARM有很多东西要学习,那么中断,就肯定是需要学习的东西。自从CPU引入中断以来,才真正地进入多任务系统工作,并且大大提高了工作效率。采 ...
  • 学习ARM开发(17)
    因为嵌入式系统里全部要使用中断的,那么我的S3C44B0怎么样中断流程呢?那我就需要了解整个流程了。要深入了解,最好的方法,就是去写程序 ...
  • 学习ARM开发(18)
    上一次已经了解ARM的中断处理过程,并且可以设置中断函数,那么它这样就可以工作了吗?答案是否定的。因为S3C44B0还有好几个寄存器是控制中 ...
  • 嵌入式系统调试仿真工具
    嵌入式硬件系统设计出来后就要进行调试,不管是硬件调试还是软件调试或者程序固化,都需要用到调试仿真工具。 随着处理器新品种、新 ...
  • 最近困扰在心中的一个小疑问终于解惑了~~
    最近在驱动方面一直在概念上不能很好的理解 有时候结合别人写的一点usb的例子能有点感觉,但是因为arm体系里面没有像单片机那样直接讲解引脚 ...
  • 学习ARM开发(1)
  • 学习ARM开发(2)
  • 学习ARM开发(4)
  • 学习ARM开发(6)
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved