A/D 有点乱

A/D有点小麻烦,需要模数/数模互转,我游荡了好久才找到门。

一、理论知识

二、实践为主

三、误差分析

四、代码分析

五、杂七杂八

.

 

一、理论知识

       模数/数模转换基本上是一个比例上的问题。也就是说,由ADC产生的数字值是和输入电压与转换器量程的比值相关的。例如,如果2V的电压输入到一个满量程为5V的ADC,则数字输出结果应该是ADC输出的最大数字量的40%(2V/5V = 0.4)。

    可用的 ADC 可以提供各种和输出数字范围。输出的数字范围通常以“位”来表示,如 8 位, 10 位等。输出的位数决定了可以从转换器输出端读取的数值范围。一个8位转换器可以提供 0 - 255(2的8次方减去一)数值范围的输出,而 10位转换器可以提供0 - 1023(2的10次方 -1)数值范围的输出。

    前面的例子--2V的电压输入到一个量程为5V的转换器---中,8位的转换器会输出 255 的 40% ,即 102 , 比例转换的总结如以下公式:
                  V(in) / V(fullscale) =  X / 2的n次方 - 1

    在上面的公式中,X是数字输出,n 是数字输出的位数。利用上面的公式,解出 X ,你就可以知道计算机获得一个给定输入电压时所读取的数字值。同样,如果知道了 X 的值,也可以用上面的公式在程序中计算出所供给的电压值。这在需要把实际电压显示在 LED 显示器上的时侯会很有用。

    隐藏在这个公式中的一个问题是精确度。测量的精确度,或者说能测量到的最小增量计算如下:

    V(resolution) = V(fulliscale) / 2的n次方  - 1

    对一个8位的,输入量程为 5V 的转换器来说,它的精度计算如下:

         V(resolution) = 5V / (2的8次方 -1)= 5V / 255 = 20mV(近似)
   
    那么,能够测量得到的最小的电压增量将会是 20mV 。如果用这个转换器来测量小于 20mV的值(例如 5mV)是不恰当的。

具体操作分三步(在下面的实践中解释每一步):

1、确定量程。

2、模数转换。

3、确定系数。

 

二、实践一下

有了上面的理论,下面来实践下。原理图大概这样:

我所用芯片是PCF8591(8位),我们来读取下AIN0、AIN1、AIN2、AIN3输入的电压(A/D采集的是电压,若输入为电流,则需要串联个电阻,最好再加上个电压跟随器),并用1602显示。

 

第一步、确定量程。

这里的量程指输入电压的范围。这里为0—5v,因为我这里是单片机开发板,VCC为5V,拿AIN0通道来说,当J2插针断开,则AIN0将恒为输入5v,当把J2插针短接,则AIN0输入0V。

第二步、模数转换。

如何转换呢,我们根据上面的公式来。V(in) / V(fullscale) = X / 2n -1;

首先计算出最大量程(5v模拟量)对应的数字量:5/5 = X / 2-1  解得 X = 255;

在此计算出最小量程(0v模拟量)对应的数字量:0/5 = X / 2-1  解得 X = 0; (我觉得这个应该不用计算,只取最大量程的就好,反正是得系数嘛)

第三步、确定系数。

由第二步得5V模拟量对应数字量255,则模数对应关系为(即他们之间的系数):255 / 5 = 51  (有了这个系数,每次单片机读取一个数字量,就让它除以系数即得到对应输入的模拟量。)

最后显示的结果大概是这样(从左到右,从上到下依次为AIN0、AIN1、AIN2、AIN3): 

AIN0直接接VCC (J2是断开的) 所以它会一直保持5.0V。

AIN1是悬空的,这个值我们不看,数据手册中推荐:暂时不用的输入端最好接VCC。

AIN2、AIN3各自接一个滑动变阻器,值我们随便调。

 

 三、误差分析

1、A/D转换是有误差的,看代码就知道了。假设还是前面的条件不变,你输入一个3.3v的电压,X = 255 * 3.3 / 5  。解出模拟量X实际应该是:168.3 ,但实际上可能是168,因为数字量的范围是0—255(芯片是8位的),无法表示浮点型,丢掉小数点已经失真了。

2、 现在3.3V对应的数字量168已经有了,在单片机中,我们要利用系数判断这个数字量对应的模拟量:168 / 51 = 3.294 ,结果接近3.3。当然为了尽可能精确我们可以从小数点后n位开始向前四舍五入,具体如何操作(这里从小数点后第二位开始四舍五入):

              1)、将整形数168转为浮点类型:float t = (float) 168;

              2)、将3.294 加上 0.05,变为:3.344

              3)、保存个位   :将值 3.344 直接赋给一个uchar类型,则将舍弃小数点后数据

                    保存十分位 :将3.344 乘以10再模除10,将结果赋给uchar类型

               具体如何保留小数点操作,下面代码分析。

 

四、代码分析

#include "iic.h"     // 引入IIC总线操作库
#include "1602.h"     // 引入1602用于显示
#include <reg52.h>

uchar AIN0,AIN1,AIN2,AIN3;           //
uchar TempData[8];
uchar FL[] = "                ";   // 1602 第一行显示
uchar SL[] = "                ";   // 1602 第二行显示
void display();
void ad_4in();

void main()
{
    
    INIT_1602();  // 1602液晶初始化
    IIC_INIT();      // IIC总线初始化

    while(1)
    {
        ad_4in(); // A/D转换
        display();
    }
}

void ad_4in()
{
    uchar temp;    

    // 处理AIN0通道
    temp=read_byte(0x91,0x40 | 0);    
    AIN0 = read_byte(0x91,0x40 | 0);   // 读AIN0通道数据
    TempData[0]= AIN0 / 51;     // 个位
    TempData[1]=(((uchar)(((float)AIN0 / 51) * 10)) % 10);    // 小数点后一位

    // 处理AIN1通道
    temp=read_byte(0x91,0x40 | 1);    
    AIN1 = read_byte(0x91,0x40 | 1);   // 读AIN1通道数据
    TempData[2]= AIN1 / 51;
    TempData[3]=(((uchar)(((float)AIN1 / 51) * 10)) % 10);

    // 处理AIN2通道
    temp=read_byte(0x91,0x40 | 2);
    AIN2 = read_byte(0x91,0x40 | 2);   // 读AIN2通道数据
    TempData[4]= AIN2 / 51;
    TempData[5]=(((uchar)(((float)AIN2 / 51) * 10)) % 10);

    // 处理AIN3通道
    temp=read_byte(0x91,0x40 | 3);
    AIN3 = read_byte(0x91,0x40 | 3);   // 读AIN3通道数据
    TempData[6]= AIN3 / 51;
    TempData[7]=(((uchar)(((float)AIN3 / 51) * 10)) % 10);
}

void display()
{
    FL[2]='0' + TempData[0];  // +'0'转换为字符型
    FL[4]='0' + TempData[1];
    FL[3]='.';
    FL[6]='V';
    
    FL[9]='0' + TempData[2];
    FL[11]='0' + TempData[3];
    FL[10]='.';
    FL[13]='V';
        
    SL[2]='0' + TempData[4];
    SL[4]='0' + TempData[5];
    SL[3]='.';
    SL[6]='V';
    
    SL[9]='0' + TempData[6];
    SL[11]='0' + TempData[7];
    SL[10]='.';
    SL[13]='V';
    
    write_str_lcd(0,0,FL);     // 从第一行第一列开始写数据
    write_str_lcd(1,0,SL);     // 从第二行第一列开始写数据
}

我们以这段代码为例分析下:

// 处理AIN2通道
1  temp=read_byte(0x91,0x40 | 2);
2  AIN2 = read_byte(0x91,0x40 | 2);   // 读AIN2通道数据
3  TempData[4]= AIN2 / 51;
4  TempData[5]=(((uchar)(((float)AIN2 / 51) * 10)) % 10);

第一行的作用我也有点乱,我觉得根本没用,但是不加的话结果是错误的,可能是我手册没看仔细,我在研究一下........

第二行是读取AIN3通道的数据

第三行是直接取个位,其他的都不管了

第四行是取小数点后一位,我们分解开来:

(float)AIN2  // 将AIN3通道的数字量转为浮点类型
((float)AIN2 / 51)  // 得到数字量对应的模拟量
(uchar)(((float)AIN2 / 51) * 10)  // 乘10,并将浮点型转为整形,为下一步获取小数点后一位做准备(只有整数才能进行模除操作)
(((uchar)(((float)AIN2 / 51) * 10)) % 10)  // 得到小数点后一位

五、杂七杂八

iic.c的代码以前贴过了
1602.h
#ifndef __1602_H__
#define __1602_H__
#include "delay.h"

void INIT_1602();                                 // 初始化1602液晶
void write_byte_lcd(uchar x,uchar y,uchar dat);      // 向LCD写一字节(字符)
void write_str_lcd(uchar x,uchar y,uchar *dat);      // 想LCD写一字符串
void clear_lcd();                                  // 清屏

#endif

// INIT_1602();
// write_byte_lcd(7,0,'h');
// write_str_lcd(3,1,"hicjiajia");
// clear_lcd();
1602.c
/***************************
       hicjiajia@gmail.com
****************************/

#include <reg52.h>
#include "1602.h"

sbit RS = P1^0;       // RS
sbit RW = P1^1;       // RW
sbit EN = P2^5;       // EN
sbit DU = P2^6;
sbit WE = P2^7;

#define IO_PORT P0              // LCD液晶接口为P0

void cmg88();                  // 管段数码管
void INIT_1602();             // 初始化1602液晶
bit  isBusy();                  // 测试是否忙,1:忙,0:不忙
void write_com(uchar dat);      // 写命令
void write_data(uchar dat);      // 写数据



void cmg88()
{

    DU = 0;

    WE = 0;    
}

void INIT_1602()
{
    cmg88();
    /*
    write_com(0x38);  // 显示方式设置
    delay(1); 
    write_com(0x06);  // 没写一字符,指针增1
    delay(1);
    write_com(0x08);  // 光标不闪烁不显示
    delay(1);
    write_com(0x01);  // 启动后清屏    
    */
    write_com(0x38);    /*显示模式设置*/ 
    delay(5);  
    write_com(0x08);    /*显示关闭*/ 
    write_com(0x01);    /*显示清屏*/ 
    write_com(0x06);    /*显示光标移动设置*/ 
    delay(5); 
    write_com(0x0C);    /*显示开及光标设置*/
    write_com(0x01);    /*显示清屏*/
}

bit isBusy()    //判断是否忙:RS = 0;RW = 1;EN = 高电平
{
    IO_PORT = 0xFF;
    RS = 0;
    RW = 1;
    EN = 1;
    delay_us(1);
    return (bit)(IO_PORT & 0x80);  // 取最高位BF的值,它决定了LCD是否已做好下一轮准备,1:没准备好,0:准备好了
}

void write_com(uchar dat)  // 写命令:RS = 0;RW = 0;EN = 下降沿
{
    while(isBusy());
    RS = 0;
    RW = 0;
    EN = 1;
    delay_us(1);
    IO_PORT = dat;
    delay_us(1);
    EN = 0;           // 下降沿写入数据
    delay_us(1);
}

void write_data(uchar dat)    // 写数据:RS = 1;RW = 0;EN = 下降沿
{
    while(isBusy());
    RS = 1;
    RW = 0;
    EN = 1;
    delay_us(1);
    IO_PORT = dat;
    delay_us(1);
    EN = 0;
    delay_us(1);
}

void clear_lcd()      // 清屏
{
    write_com(0x01);  // LCD清屏指令
    delay(1);
}

void write_byte_lcd(uchar x,uchar y,uchar dat)
{
    if (x == 0){
        write_com(0x80 + y);
    }else{
        write_com(0x80 + 0x40 + y);
    }
    write_data(dat);
}


void write_str_lcd(uchar x,uchar y,uchar *dat)
{
    if (x == 0){
        write_com(0x80 + y);
    }else{
        write_com(0x80 + 0x40 + y);
    }
    while(*dat) write_data(*dat++);
}
 
原文地址:https://www.cnblogs.com/hicjiajia/p/2446261.html