pcf8591的通信接口是i2c,那么编程肯定是符合这个协议的。单片机对pcf8591进行初始化,一共发送三个字节即可。第一个字节,和eeprom类似,第一个字节是地址字节,其中7位代表地址,1位代表读写方向。地址高4位固定是1001,低三位是a2,a1,a0,这三位我们电路上都接了gnd,因此也就是000,如图1所示。
图1pcf8591地址字节
发送到pcf8591的第二个字节将被存储在控制寄存器,用于控制pcf8591的功能。其中第3位和第7位是固定的0,另外6位各自有各自的作用,如图2所示,我逐一介绍。
图2pcf8591控制字节
控制字节的第6位是da使能位,这一位置1表示da输出引脚使能,会产生模拟电压输出功能。第4位和第5位可以实现把pcf8591的4路模拟输入配置成单端模式和差分模式,单端模式和差分模式的区别,我们17.4章节有介绍,这里大家只需要知道这两位是配置ad输入方式的控制位即可,如图3所示。
图3pcf8591模拟输入配置方式
控制字节的第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>
#definelcd1602_dbp0
sbitlcd1602_rs=p1^0;
sbitlcd1602_rw=p1^1;
sbitlcd1602_e=p1^5;
voidlcdwaitready()//等待液晶准备好
{
unsignedcharsta;
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为止
}
voidlcdwritecmd(unsignedcharcmd)//写入命令函数
{
lcdwaitready();
lcd1602_rs=0;
lcd1602_rw=0;
lcd1602_db=cmd;
lcd1602_e=1;
lcd1602_e=0;
}
voidlcdwritedat(unsignedchardat)//写入数据函数
{
lcdwaitready();
lcd1602_rs=1;
lcd1602_rw=0;
lcd1602_db=dat;
lcd1602_e=1;
lcd1602_e=0;
}
voidlcdshowstr(unsignedcharx,unsignedchary,constunsignedchar*str)//显示字符串,屏幕起始坐标(x,y),字符串指针str
{
unsignedcharaddr;
//由输入的显示坐标计算显示ram的地址
if(y==0)
addr=0x00+x;//第一行字符地址从0x00起始
else
addr=0x40+x;//第二行字符地址从0x40起始
//由起始显示ram地址连续写入字符串
lcdwritecmd(addr|0x80);//写入起始地址
while(*str!='\0')//连续写入字符串数据,直到检测到结束符
{
lcdwritedat(*str);
str++;
}
}
voidlcdinit()//液晶初始化函数
{
lcdwritecmd(0x38);//16*2显示,5*7点阵,8位数据接口
lcdwritecmd(0x0c);//显示器开,光标关闭
lcdwritecmd(0x06);//文字不动,地址自动+1
lcdwritecmd(0x01);//清屏
}
/***********************i2c.c文件程序源代码*************************/
#include<reg52.h>
#include<intrins.h>
#definei2cdelay(){_nop_();_nop_();_nop_();_nop_();}
sbiti2c_scl=p3^7;
sbiti2c_sda=p3^6;
voidi2cstart()//产生总线起始信号
{
i2c_sda=1;//首先确保sda、scl都是高电平
i2c_scl=1;
i2cdelay();
i2c_sda=0;//先拉低sda
i2cdelay();
i2c_scl=0;//再拉低scl
}
voidi2cstop()//产生总线停止信号
{
i2c_scl=0;//首先确保sda、scl都是低电平
i2c_sda=0;
i2cdelay();
i2c_scl=1;//先拉高scl
i2cdelay();
i2c_sda=1;//再拉高sda
i2cdelay();
}
biti2cwrite(unsignedchardat)//i2c总线写操作,待写入字节dat,返回值为应答状态
{
bitack;//用于暂存应答位的值
unsignedcharmask;//用于探测字节内某一位值的掩码变量
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=存在且空闲或写入成功
}
unsignedchari2creadnak()//i2c总线读操作,并发送非应答信号,返回值为读到的字节
{
unsignedcharmask;
unsignedchardat;
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完成非应答位,并保持住总线
returndat;
}
unsignedchari2creadack()//i2c总线读操作,并发送应答信号,返回值为读到的字节
{
unsignedcharmask;
unsignedchardat;
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完成应答位,并保持住总线
returndat;
}
/***********************main.c文件程序源代码*************************/
#include<reg52.h>
bitflag300ms=1;//300ms定时标志
unsignedchart0rh=0;//t0重载值的高字节
unsignedchart0rl=0;//t0重载值的低字节
unsignedchargetadcvalue(unsignedcharchn);
voidvaluetostring(unsignedchar*str,unsignedcharval);
voidconfigtimer0(unsignedintms);
externvoidlcdinit();
externvoidlcdshowstr(unsignedcharx,unsignedchary,constunsignedchar*str);
externvoidi2cstart();
externvoidi2cstop();
externunsignedchari2creadack();
externunsignedchari2creadnak();
externbiti2cwrite(unsignedchardat);
voidmain()
{
unsignedcharval;
unsignedcharstr[10];
ea=1;//开总中断
configtimer0(10);//配置t0定时10ms
lcdinit();//初始化液晶
lcdshowstr(0,0,ain0ain1ain3);//显示通道指示
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);
}
}
}
unsignedchargetadcvalue(unsignedcharchn)//读取当前的adc转换值,chn为adc通道号0-3
{
unsignedcharval;
i2cstart();
if(!i2cwrite(0x48<<1))//寻址pcf8591,如未应答,则停止操作并返回0
{
i2cstop();
return0;
}
i2cwrite(0x40|chn);//写入控制字节,选择转换通道
i2cstart();
i2cwrite((0x48<<1)|0x01);//寻址pcf8591,指定后续为读操作
i2creadack();//先空读一个字节,提供采样转换时间
val=i2creadnak();//读取刚刚转换完的值
i2cstop();
returnval;
}
voidvaluetostring(unsignedchar*str,unsignedcharval)//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';//结束符
}
voidconfigtimer0(unsignedintms)//t0配置函数
{
unsignedlongtmp;
tmp=11059200/12;//定时器计数频率
tmp=(tmp*ms)/1000;//计算所需的计数值
tmp=65536-tmp;//计算定时器重载值
tmp=tmp+12;//修正中断响应延时造成的误差
t0rh=(unsignedchar)(tmp>>8);//定时器重载值拆分为高低字节
t0rl=(unsignedchar)tmp;
tmod&=0xf0;//清零t0的控制位
tmod|=0x01;//配置t0为模式1
th0=t0rh;//加载t0重载值
tl0=t0rl;
et0=1;//使能t0中断
tr0=1;//启动t0
}
voidinterrupttimer0()interrupt1//t0中断服务函数
{
staticunsignedchartmr300ms=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转换,第二次是读取当前的转换结果。如果我们只使用第二条语句的话,每次读到的都是上一次的转换结果。