你的位置: 首页 > 家电弱电 > 单片机

PCF8591的软件编程

2016-11-25 17:21:56 | 人围观 | 评论:

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

PCF8591地址字节

图1 PCF8591地址字节

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

   图17-6 PCF8591控制字节

图2 PCF8591控制字节

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

PCF8591模拟输入配置方式

图3 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 <reg52.h>

 

#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 != '/0')      //连续写入字符串数据,直到检测到结束符

    {

        LcdWriteDat(*str);

        str++;

    }

}

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

{

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

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

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

    LcdWriteCmd(0x01);  //清屏

}

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

#include <reg52.h>

#include <intrins.h>

 

#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) //从高位到低位依次进行

    {

        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 = 0;   //8位数据发送完后,拉低SDA,发送应答信号

    I2CDelay();

    I2C_SCL = 1;   //拉高SCL

    I2CDelay();

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

 

    return dat;

}

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

#include <reg52.h>

 

bit flag300ms = 1;       //300ms定时标志

unsigned char T0RH = 0;  //T0重载值的高字节

unsigned char T0RL = 0;  //T0重载值的低字节

 

unsigned char GetADCValue(unsigned char chn);

void ValueToString(unsigned char *str, unsigned char val);

void ConfigTimer0(unsigned int ms);

extern void LcdInit();

extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);

extern void I2CStart();

extern void I2CStop();

extern unsigned char I2CReadACK();

extern unsigned char I2CReadNAK();

extern bit I2CWrite(unsigned char dat);

 

void main ()

{

    unsigned char val;

    unsigned char str[10];

    

    EA = 1;           //开总中断

    ConfigTimer0(10); //配置T0定时10ms

    LcdInit();        //初始化液晶    

    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(6, 1, str);

            //显示通道3的电压

            val = GetADCValue(3);

            ValueToString(str, val);

            LcdShowStr(12, 1, str);

        }

    }

}

 

unsigned char GetADCValue(unsigned char chn)  //读取当前的ADC转换值,chn为ADC通道号0-3

{

    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;

}

void ValueToString(unsigned char *str, unsigned char val)  //ADC转换值转为实际电压值的字符串形式

{

    val = (val*25) / 255;    //电压值=转换结果*2.5V/255,式中的25隐含了一位十进制小数

    str[0] = (val/10) + '0'; //整数位字符

    str[1] = '.';            //小数点

    str[2] = (val%10) + '0'; //小数位字符

    str[3] = 'V';            //电压单位

    str[4] = '/0';           //结束符

}

 

void ConfigTimer0(unsigned int ms)  //T0配置函数

{

    unsigned long tmp;

    

    tmp = 11059200 / 12;      //定时器计数频率

    tmp = (tmp * ms) / 1000;  //计算所需的计数值

    tmp = 65536 - tmp;        //计算定时器重载值

    tmp = tmp + 12;           //修正中断响应延时造成的误差

    

    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

}

void InterruptTimer0() interrupt 1  //T0中断服务函数

{

    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,而A/D的特点是每次读到的都是上一次的转换结果,因此我们这里第一条语句的作用是产生一个整体的SCL时钟提供给PCF8591进行A/D转换,第二次是读取当前的转换结果。如果我们只使用第二条语句的话,每次读到的都是上一次的转换结果。





标签:

相关内容推荐: