16_模数转换AD与数模转换DA

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

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 一个简单的米尺刻度示意图。

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

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、分辨率分辨率是数字量变化一个最小刻度时,模拟信号的变化量,定义为满刻度量程与2的n次方-1 的比值。假定 5.10V 的电压系统,使用 8 位的 ADC 进行测量,那么相当于 0~255 一共 256 个刻度把 5.10V 平均分成了 255 份,那么分辨率就是 5.10/255 = 0.02V。

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

        初学者最容易混淆的两个概念就是“分辨率”和“精度”,认为分辨率越高,则精度越高,而实际上,两者之间是没有必然联系的。分辨率是用来描述刻度划分的,而精度是用来描述准确程度的。同样一根米尺,刻度数相同,分辨率就相当,但是精度却可以相差很大,如图 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.03V 之间。值得一提的是 DNL 并非一定小于 1LSB,很多时候它会等于或大于 1LSB,这就相当于是一定程度上的刻度紊乱,当实际电压保持不变时,ADC 得出的结果可能会在几个数值之间跳动,很大程度上就是由于这个原因(但并不完全是,因为还有无时无处不在的干扰的影响)。

        5、转换速率

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

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

3.PCF8591的硬件接口

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

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

        图中 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 芯片。

4.PCF8591的软件编程

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

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

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

 

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

        发送给 PCF8591 的第三个字节 D/A 数据寄存器,表示 D/A 模拟输出的电压值。D/A 模拟我们一会介绍,大家知道这个字节的作用即可。我们如果仅仅使用 A/D 功能的话,就可以不发送第三个字节。下面我们用一个程序,把 AIN0、AIN1、AIN3 测到的电压值显示在液晶上,同时大家可以转动电位器,会发现 AIN0 的值发生变化。

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

(此处省略,可参考之前章节的代码)

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

(此处省略,可参考之前章节的代码)

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

#include <reg52.h>

bit flag300ms = 1;  //300ms定时标志
unsigned char T0RH = 0; //T0重载值的高字节
unsigned char T0RL = 0; //T0重载值的低字节

void ConfigTimer0(unsigned int ms);
unsigned char GetADCValue(unsigned char chn);
void ValueToString(unsigned char *str, unsigned char val);
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
extern void InitLcd1602();
extern void lcdShowStr(unsigned char x, unsigned char y, unsigned char *str);

void main()
{
    unsigned char val;
    unsigned char str[10];

    EA = 1;     //开总中断
    ConfigTimer0(10);   //配置T0定时10ms
    InitLcd1602();  //初始化液晶
    lcdShowStr(0, 0, "AIN0 AIN1 AIN3"); //显示通道指示
    while(1)
    {
        if(flag300ms)
        {
            flag300ms = 0;
            //显示通道0的电压
            val = GetADCValue(0);   //获取ADC通道0的转换值
            ValueToString(str, val);    //转为字符串格式的电压值
            lcdShowStr(0, 1, str);      //显示到液晶上
            //显示通道1的电压
            val = GetADCValue(1);
            ValueToString(str, val);
            lcdShowStr(5, 1, str);
            //显示通道3的电压
            val = GetADCValue(3);
            ValueToString(str, val);
            lcdShowStr(10, 1, str);
        }
    }
}

/* 读取当前的ADC转换值,chn-ADC通道号0~3 */
unsigned char GetADCValue(unsigned char chn)
{
    unsigned char val;
    I2CStart();
    if(!I2CWrite(0x48<<1))  //寻址PCF8591,如未应答,则停止操作并返回0
    {
        I2CStop();
        return 0;
    }
    I2CWrite(0x40 | chn);   //写入控制字节,选择转换通道
    I2CStart();
    I2CWrite((0x48 << 1) | 0x01);   //寻址PCF8591,指定后续为读操作
    I2CReadACK();   //先空读一个字节,提供采样转换时间
    val = I2CReadNAK();     //读取刚刚转换完的值
    I2CStop();

    return val;
}

/* ADC转换值转为实际电压值的字符串形式,str-字符串指针,val-AD转换值 */
void ValueToString(unsigned char *str,unsigned char val)
{
    //电压值=转换结果*2.5V/255,式中的25隐含了一位十进制小数
    val = (val * 25) / 255;
    str[0] = (val / 10) + '0';  //整数位字符
    str[1] = '.';       //小数点
    str[2] = (val % 10) + '0';      //小数位字符
    str[3] = 'V';   //电压符号
    str[4] = '\0';  //结束符
}

/*  配置并启动T0,ms-T0定时时间*/
void ConfigTimer0(unsigned int ms)
{
    unsigned long tmp;      //临时变量

    tmp = 11059200 / 12;    //定时器计数频率
    tmp = (tmp * ms) / 1000;    //计算所需的计数值
    tmp = 65536 - tmp;      //计算定时器重载值
    tmp = tmp + 32;     //补偿中断响应延时造成的误差
    T0RH = (unsigned char)(tmp >> 8);   //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清除T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;    //使能T0中断
    TR0 = 1;    //启动T0
}

/* T0中断服务函数,执行300ms定时 */
void InterruptTimer0() interrupt 1
{
    static unsigned char tmr300ms = 0;
    TH0 = T0RH;     //重新加载初值
    TL0 = T0RL;
    tmr300ms++;
    if(tmr300ms>=30)    //定时300ms
    {
        tmr300ms = 0;
        flag300ms = 1;
    }
}

        细心阅读程序的同学会发现,程序在进行 A/D 读取数据的时候,共使用了两条程序去读了 2 个字节:I2CReadACK(); val = I2CReadNAK(); PCF8591 的转换时钟是 I2C 的 SCL,8 个SCL 周期完成一次转换,所以当前的转换结果总是在下一个字节的 8 个 SCL 上才能读出,因此我们这里第一条语句的作用是产生一个整体的 SCL 时钟提供给 PCF8591 进行 A/D 转换,第二次是读取当前的转换结果。如果我们只使用第二条语句的话,每次读到的都是上一次的转换结果。

5.A/D差分输入信号

        在上一节已经提到过,控制字的第 4 位和第 5 位是用于控制 PCF8591 的模拟输入引脚是单端输入还是差分输入。差分输入是模拟电路常用的一个技巧,这里我们把相关知识做一些简单介绍。

        从严格意义上来讲,其实所有的信号都是差分信号,因为所有的电压只能是相对于另外一个电压而言。但是大多数系统,我们都是把系统的 GND 作为基准点。而对于 A/D 来说的差分输入,通常情况下是除了 GND 以外,另外两路幅度相同,极性相反的输入信号,其实理解起来很简单,就如同跷跷板一样。如图 17-8 所示。

        差分输入的话,就不是单个输入,而是由 2 个输入端构成的一组输入。PCF8591 一共是4 个模拟输入端,可以配置成 4 种模式,最典型的是 4 个输入端构造成的两路差分模式,如图 17-9 所示。

        当控制字的第 4 位和第 5 位都是 1 的时候,那么 4 路模拟被配置成 2 路差分模式输入channel 0 和 channel 1。我们以 channel 0 为例,其中 AIN0 是正向输入端,AIN1 是反向输入端,它们之间的信号输入是幅度相同,极性相反的信号,通过减法器后,得到的是两个输入通道的差值,如图 17-10 所示。

        通常情况下,差分输入的中线是基准电压的一半,我们的基准电压是 2.5V,假如 1.25V作为中线,V+是 AIN0 的输入波形,V-是 AIN1 的输入波形,Signal Value 就是经过减法器后的波形。很多 A/D 都采用差分的方式输入,因为差分输入方式比单端输入来说,有更强的抗干扰能力。

        单端输入信号时,如果一线上发生干扰变化,比如幅度增大 5mv,GND 不变,测到的数据会有偏差;而差分信号输入时,当外界存在干扰信号时,只要布线合理,大都同时被耦合到两条线上,幅度增大 5mv 会同时增大 5mv,而接收端关心的只是两个信号的差值,所以外界的这种共模噪声可以被完全抵消掉。由于两根信号的极性相反,它们对外辐射的电磁场可以相互抵消,有效的抑制释放到外界的电磁能量。

        在我们的 KST-51 开发板上,我们没有做差分信号输入的实验环境,由于这个内容在 A/D部分比较重要,所以还是介绍给大家,以供参考。

6.D/A输出

        D/A 是和 A/D 刚好反方向的,一个 8 位的 D/A,从 0~255,代表了 0~2.55V 的话,那么我们用单片机给第三个字节发送 100,D/A 引脚就会输出一个 1V 的电压,发送 200 就输出一个 2V 的电压,很简单,我们用一个简单的程序实现出来,并且通过上、下按键可以增大或减小输出幅度值,每次增加或减小 0.1V。如果有万用表的话,可以直接测试一下板子上AOUT 点的输出电压,观察它的变化。由于 PCF8591 的 DA 输出偏置误差最大是 50mv(由数据手册提供),所以我们用万用表测到的电压值和理论值之间的误差就应该在 50mV 以内。

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

(此处省略,可参考之前章节的代码)

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

(此处省略,可参考之前章节的代码)

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

#include<reg52.h>

unsigned char T0RH = 0;     //T0重载值的高字节
unsigned char T0RL = 0;     //T0重载值的低字节
void ConfigTimer0(unsigned int ms);
extern void KeyScan();
extern void KeyDriver();
extern void I2CStart();
extern void I2CStop();
extern bit I2CWrite(unsigned char dat);

void main()
{
    EA = 1;     //打开总中断
    ConfigTimer0(1);    //配置T0定时1ms

    while(1)
    {
        KeyDriver();    //调用按键驱动
    }
}

/* 设置DAC输出值,val-设定值 */
void SetDACOut(unsigned char val)
{
    I2CStart();
    if(!I2CWrite(0x48<<1))  //寻址PCF8591,如未应答,则停止操作并返回
    {
        I2CStop();
        return;
    }
    I2CWrite(0X40);     //写入控制字节
    I2CWrite(val);      //写入DA值
    I2CStop();
}

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void KeyAction(unsigned char keycode)
{
    static unsigned char volt = 0;  //输出电压值,隐含了一位十进制小数位
    if(keycode==0x26)   //向上键,增加0.1V电压值
    {
        if(volt<25)
        {
            volt++;
            SetDACOut(volt * 255 / 25);     //转换为AD输出值
        }
    }
    else if(keycode==0x28)  //向下键,减小0.1V电压值
    {
        if(volt>0)
        {
            volt--;
            SetDACOut(volt * 255 / 25); //转换为AD输出值
        }
    }

}
/* 配置并启动T0,ms-T0定时时间 */
void ConfigTimer0(unsigned int ms)
{
    unsigned long tmp;  //临时变量
    tmp = 11059200 / 12;    //定时器计数频率
    tmp = (tmp * ms) / 1000;    //计算所需的计数值
    tmp = 65536 - tmp;  //计算定时器重载值
    tmp = tmp + 28;     //补偿中断响应延时造成的误差
    T0RH = (unsigned char)(tmp >> 8);   //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xf0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;    //使能T0中断
    TR0 = 1;    //启动T0
}
/* T0中断服务函数,执行按键扫描 */
void InterruptTimer0() interrupt 1
{
    TH0 = T0RH;     //重新加载重载值
    TL0 = T0RL;
    KeyScan();      //按键扫描
}

7.简易信号发生器实例

        有了 D/A 这个武器,我们就不仅仅可以输出方波信号了,可以输出任意波形了,比如正弦波、三角波、锯齿波等等。以正弦波为例,首先我们要建立一个正弦波的波表。这些不需要大家去逐一计算,可以通过搜索找到正弦波数据表,然后可以根据时间参数自己选取其中一定量数据作为我们程序的正弦波表,我们的程序代码选取了 32 个点。

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

(此处省略,可参考之前章节的代码)

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

(此处省略,可参考之前章节的代码)

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

#include <reg52.h>

unsigned char 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,
};

unsigned char 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,
};

unsigned char 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,
};

unsigned char code *pWave;  //波表指针
unsigned char T0RH = 0; //T0重载值的高字节
unsigned char T0RL = 0; //T0重载值的低字节
unsigned char T1RH = 1; //T1重载值的高字节
unsigned char T1RL = 1; //T1重载值的低字节

void ConfigTimer0(unsigned int ms);
void SetWaveFreq(unsigned char freq);
extern void KeyScan();
extern void KeyDriver();
extern void I2CStart();
extern void I2CStop();
extern bit I2CWrite(unsigned char dat);

void main()
{
    EA = 1; //开总中断
    ConfigTimer0(1);    //配置T0定时1ms
    pWave = SinWave;    //默认正弦波
    SetWaveFreq(10);    //默认频率10Hz

    while(1)
    {
        KeyDriver();    //调用按键驱动
    }
}
/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void KeyAction(unsigned char keycode)
{
    static unsigned char i = 0;
    if(keycode==0x26)   //向上键,切换波形
    {
        //在3种波形间循环切换
        if(i==0)
        {
            i = 1;
            pWave = TriWave;
        }
        else if(i==1)
        {
            i = 2;
            pWave = SawWave;
        }
        else
        {
            i = 0;
            pWave = SinWave;
        }
    }
}
/* 设置DAC输出值,val-设定值 */
void SetDACOut(unsigned char val)
{
    I2CStart();
    if(!I2CWrite(0x48<<1))  //寻址PCF8591,如未应答,则停止操作并返回
    {
        I2CStop();
        return;
    }
    I2CWrite(0x40); //写入控制字节
    I2CWrite(val);  //写入DA值
    I2CStop();
}
/* 设置输出波形的频率,freq-设定频率 */
void SetWaveFreq(unsigned char freq)
{
    unsigned long tmp;
    tmp = (11059200 / 12) / (freq * 32);    //定时器计数频率,是波形频率的32倍
    tmp = 65536 - tmp;  //计算定时器重载值
    tmp = tmp + 33;     //修正中断响应延时造成的误差
    T1RH = (unsigned char)(tmp >> 8);   //定时器重载值拆分为高低字节
    T1RL = (unsigned char)tmp;
    TMOD &= 0x0F;   //清零T1的控制位
    TMOD |= 0x10;   //配置T1为模式1
    TH1 = T1RH; //加载T1重载值
    TL1 = T1RL;
    ET1 = 1;    //使能T1中断
    PT1 = 1;    //设置为高优先级
    TR1 = 1;    //启动T1
}
/* 配置并启动T0,ms-T0定时时间 */
void ConfigTimer0(unsigned int ms)
{
    unsigned long tmp;  //临时变量

    tmp = 11059200 / 12;    //定时器技术计数频率
    tmp = (tmp * ms) / 1000;    //计算所需的计数值
    tmp = 65536 - tmp;  //计算定时器重载值
    tmp = tmp + 28; //补偿中断响应延时造成的误差
    T0RH = (unsigned char)(tmp >> 8);   //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清除T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH; //加载T0为模式1
    TL0 = T0RL;
    ET0 = 1;    //使能T0中断
    TR0 = 1;    //启动T0
}
/* T0中断服务函数,执行按键扫描 */
void InterruptTimer0() interrupt 1
{
    TH0 = T0RH; //重新加载重载值
    TL0 = T0RL;
    KeyScan();  //按键扫描
}

/* T1中断服务函数,执行波形输出 */
void InterruptTimer1() interrupt 3
{
    static unsigned char i = 0;
    TH1 = T1RH; //重新加载重载值
    TL1 = T1RL;
    //循环输出波表中的数据
    SetDACOut(pWave[i]);
    i++;
    if(i>=32)
    {
        i = 0;
    }
}

        这个程序可以通过“向上”按键来实现波形输出切换,波形输出的定时刷新由定时器 T1定时来完成,改变 T1 的定时周期即可改变波形的输出频率。D/A 输出没有办法接到显示界面,所以我们用示波器抓出来波形给大家看一下,如图 17-11、图 17-12、图 17-13 所示。

        这几张图可以直观的看到我们程序输出的波形。细心的同学会发现我们波形上有很多小锯齿,没有平滑的连起来。这是因为我们 DA 最多只能输出 0~Vref 之间的 256 个离散的电压值,而不是连续的任意值,所以每个离散值都会持续一定的时间,然后跳变到下一个离散值,于是就呈现出了波形上的这种锯齿。在实际开发中,我们只需要在 DA 后级加一级低通滤波电路,就可以让带锯齿的波形变得平滑起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值