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 , 比例转换的总结如以下公式: 在上面的公式中,X是数字输出,n 是数字输出的位数。利用上面的公式,解出 X ,你就可以知道计算机获得一个给定输入电压时所读取的数字值。同样,如果知道了 X 的值,也可以用上面的公式在程序中计算出所供给的电压值。这在需要把实际电压显示在 LED 显示器上的时侯会很有用。 隐藏在这个公式中的一个问题是精确度。测量的精确度,或者说能测量到的最小增量计算如下: V(resolution) = V(fulliscale) / 2的n次方 - 1 对一个8位的,输入量程为 5V 的转换器来说,它的精度计算如下: V(resolution) = 5V / (2的8次方 -1)= 5V / 255 = 20mV(近似)
具体操作分三步(在下面的实践中解释每一步): 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 / 28 -1 解得 X = 255;
在此计算出最小量程(0v模拟量)对应的数字量:0/5 = X / 28 -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++); }