本教材現(xiàn)以連載的方式由網(wǎng)絡(luò)發(fā)布,并將于2014年由清華大學(xué)出版社出版最終完整版,版權(quán)歸作者和清華大學(xué)出版社所有。本著開源、分享的理念,本教材可以自由傳播及學(xué)習(xí)使用,但是務(wù)必請注明出處來自金沙灘工作室
從我們學(xué)到的知識了解到,我們的單片機是一個典型的數(shù)字系統(tǒng)。數(shù)字系統(tǒng)只能對輸入的數(shù)字信號進行處理,其輸出信號也是數(shù)字信號。但是在工業(yè)檢測系統(tǒng)和日常生活中的許多物理量都是模擬量,比如溫度、長度、壓力、速度等等,這些模擬量可以通過傳感器變成與之對應(yīng)的電壓、電流等電模擬量。為了實現(xiàn)數(shù)字系統(tǒng)對這些電模擬量的檢測、運算和控制,就需要一個模擬量和數(shù)字量之間相互轉(zhuǎn)換的過程。這節(jié)課我們就要學(xué)習(xí)這個相互轉(zhuǎn)換過程。
17.1 A/D和D/A的基本概念
A/D是模擬量到數(shù)字量的轉(zhuǎn)換,依靠的是模數(shù)轉(zhuǎn)換器(Analog to Digital Converter),簡稱ADC;D/A是數(shù)字量到模擬量的轉(zhuǎn)換,依靠的是數(shù)模轉(zhuǎn)換器(Digital to Analog Converter),簡稱DAC。他們的道理是完全一樣的,只是轉(zhuǎn)換方向不同,因此我們講解過程主要以A/D為例來講解。 很多同學(xué)學(xué)到A/D這部分的時候,感覺是個難點,概念掌握不清楚。我個人認(rèn)為主要原因不在于技術(shù)問題,而是不太會感悟生活。我們生活中有很多很多A/D的例子,只是沒有在單片機領(lǐng)域里應(yīng)用而已,下面我?guī)е蠹乙黄鸶形蛞幌?/font>A/D的概念。 什么是模擬量?就是指變量在一定范圍內(nèi)連續(xù)變化的量,也就是在一定范圍內(nèi)可以取任意值。比如我們米尺,從0到1米之間,可以是任意值。什么是任意值,也就是可以是1cm,也可以是1.001cm,當(dāng)然也可以10.000......后邊有無限個小數(shù)?傊,任何兩個數(shù)字之間都有無限個中間值,所以稱之為連續(xù)變化的量,也就是模擬量。 而我們用的米尺上被我們?nèi)藶榈淖錾狭丝潭确,每兩個刻度之間的間隔是1mm,這個刻度實際上就是我們對模擬量的數(shù)字化,由于有一定的間隔,不是連續(xù)的,所以在專業(yè)領(lǐng)域里我們稱之為離散的。我們的ADC就是起到把連續(xù)的信號用離散的數(shù)字表達出來的作用。那么我們就可以使用米尺這個“ADC”來測量連續(xù)的長度或者高度這些模擬量。如圖17-1一個簡單的米尺刻度示意圖。
ps5b153.jpg (11.1 KB, 下載次數(shù): 208)
下載附件
2013-11-13 23:50 上傳
圖17-1 米尺刻度示意圖 我們往杯子里倒水,水位會隨著倒入的水量的多少而變化,F(xiàn)在就用這個米尺來測量我們杯子里的水位的高度。水位變化是連續(xù)的,而我們只能通過尺子上的刻度來讀取水位的高度,獲取我們想得到的水位的數(shù)字量信息。這個過程,就可以簡單理解為我們電路中的ADC采樣。
17.2 A/D的主要指標(biāo)
我們在選取和使用 A/D的時候,依靠什么指標(biāo)來判斷很重要。由于AD的種類很多,分為積分型、逐次逼近型、并行/串行比較型、Σ-Δ型等多種類型。同時指標(biāo)也比較多,并且有的指標(biāo)還有輕微差別,具體可上 www.torrancerestoration.com查詢.在這里我是以同學(xué)們便于理解的方法去講解,如果和某一確定類型 A/D概念和原理有差別,也不會影響實際應(yīng)用。1、ADC的位數(shù)。 一個n位的ADC表示這個ADC共有2的n次方個刻度。8位的ADC,輸出的是從0 到255一共256個數(shù)字量,也就是2的8次方個數(shù)據(jù)刻度。 2、基準(zhǔn)源 基準(zhǔn)源,也叫基準(zhǔn)電壓,是ADC的一個重要指標(biāo),要想把輸入ADC的信號測量準(zhǔn)確,那么基準(zhǔn)源首先要準(zhǔn),基準(zhǔn)源的偏差會直接導(dǎo)致轉(zhuǎn)換結(jié)果的偏差。比如一根米尺,總長度本應(yīng)該是1米,假定這根米尺被火烤了一下,實際變成了1.2米,再用這根米尺測物體長度的話自然就有了較大的偏差。假如我們的基準(zhǔn)源應(yīng)該是5.10V,但是實際上提供的卻是4.5V,這樣誤把4.5V當(dāng)成了5.10V來處理的話,偏差也會比較大。 3、分辨率 分辨率是數(shù)字量變化一個最小刻度時,模擬信號的變化量,定義為滿刻度量程與2n-1的 比值。5.10V的電壓系統(tǒng),使用8位的ADC進行測量,那么相當(dāng)于0到255一共256個刻度,把5.10V平均分成了255份,那么分辨率就是5.10/255 = 0.02V。 4、INL(積分非線性度)和DNL(差分非線性度) 初學(xué)者最容易混淆的兩個概念就是“分辨率”和“精度”,認(rèn)為分辨率越高,則精度越高,而實際上,兩者之間是沒有必然聯(lián)系的。分辨率是用來描述刻度劃分的,而精度是用來描述準(zhǔn)確程度的。同樣一根米尺,刻度數(shù)相同,分辨率就相當(dāng),但是精度卻可以相差很大,如圖17-2所示。
ps5b154.jpg (10.9 KB, 下載次數(shù): 215)
下載附件
2013-11-13 23:50 上傳
圖17-2 米尺精度對比 圖17-2表示的精度一目了然,不需多說。和ADC精度關(guān)系重大的兩個指標(biāo)是INL(Integral NonLiner)和DNL(Differencial NonLiner)。 INL指的是ADC器件在所有的數(shù)值上對應(yīng)的模擬值,和真實值之間誤差最大的那一個點的誤差值,是ADC最重要的一個精度指標(biāo),單位是LSB。LSB(Least Significant Bit)是最低有效位的意思,那么它實際上對應(yīng)的就是ADC的分辨率。一個基準(zhǔn)為5.10V的8位ADC,它的分辨率就是0.02V,用它去測量一個電壓信號,得到的結(jié)果是100,就表示它測到的電壓值是100*0.02V=2V,假定它的INL是1LSB,就表示這個電壓信號真實的準(zhǔn)確值是在1.98V~2.02V之間的,按理想情況對應(yīng)得到的數(shù)字應(yīng)該是99~101,測量誤差是一個最低有效位,即1LSB。 DNL表示的是ADC相鄰兩個刻度之間最大的差異,單位是LSB。一把分辨率是1毫米的尺子,相鄰的刻度之間并不都剛好是1毫米,而總是會存在或大或小的誤差。同理,一個ADC的兩個刻度線之間也不總是準(zhǔn)確的等于分辨率,也是存在誤差,這個誤差就是DNL。一個基準(zhǔn)為5.10V的8位ADC,假定它的DNL是0.5LSB,那么當(dāng)它的轉(zhuǎn)換結(jié)果從100增加到101時,理想情況下實際電壓應(yīng)該增加0.02V,但DNL為0.5LSB的情況下實際電壓的增加值是在0.01~0.03之間。值得一提的是DNL并非一定小于1LSB,很多時候它會等于或大于1LSB,這就相當(dāng)于是一定程度上的刻度紊亂,當(dāng)實際電壓保持不變時,ADC得出的結(jié)果可能會在幾個數(shù)值之間跳動,很大程度上就是由于這個原因(但并不完全是,因為還有無時無處不在的干擾的影響)。 5、轉(zhuǎn)換速率 轉(zhuǎn)換速率,是指ADC每秒能進行采樣轉(zhuǎn)換的最大次數(shù),單位是sps(或s/s、sa/s,即samples per second),它與ADC完成一次從模擬到數(shù)字的轉(zhuǎn)換所需要的時間互為倒數(shù)關(guān)系。ADC的種類比較多,其中積分型的ADC轉(zhuǎn)換時間是毫秒級的,屬于低速ADC;逐次逼近型ADC轉(zhuǎn)換時間是微妙級的,屬于中速ADC;并行/串行的ADC的轉(zhuǎn)換時間可達到納秒級,屬于高速ADC。 ADC的這幾個主要指標(biāo)大家先熟悉一下,對于其他的,作為一個入門級別的選手來說,先不著急深入理解。以后使用過程中遇到了,再查找相關(guān)資料深入學(xué)習(xí),當(dāng)前重點是在頭腦中建立一個ADC的基本概念。
17.3 PCF8591的硬件接口
PCF8591是一個單電源低功耗的8位CMOS數(shù)據(jù)采集器件,具有4路模擬輸入,1路模擬輸出和一個串行I2C總線接口用來與MCU通信。3個地址引腳A0、A1、A2用于編程硬件地址,允許最多8個器件連接到I2C總線而不需要額外的片選電路。器件的地址、控制以及數(shù)據(jù)都是通過I2C總線來傳輸,我們先看一下PCF8591的原理圖,如圖17-3所示。
ps5b155.jpg (44.33 KB, 下載次數(shù): 214)
下載附件
2013-11-13 23:50 上傳
圖17-3 PCF8591原理圖 其中引腳1、2、3、4是4路模擬輸入,引腳5、6、7是I2C總線的硬件地址,8腳是數(shù)字GND,9腳和10腳是I2C總線的SDA和SCL。12腳是時鐘選擇引腳,如果接高電平表示用外部時鐘輸入,接低電平則用內(nèi)部時鐘,我們這套電路用的是內(nèi)部時鐘,因此12腳直接接GND,同時11腳懸空。13腳是模擬GND,在實際開發(fā)中,如果有比較復(fù)雜的模擬電路,那么模擬GND部分在布局布線上要特別處理,而且和數(shù)字GND的連接也有多種方式,這里大家先了解即可。在我們板子上沒有復(fù)雜的模擬部分電路,所以我們把模擬的GND和數(shù)字GND接到一起即可。14腳是基準(zhǔn)源,15腳是DAC的模擬輸出,16腳是供電電源VCC。 PCF8591的ADC是逐次逼近型的,轉(zhuǎn)換速率算是中速,但是他的速度瓶頸在I2C通信上。由于I2C通信速度較慢,所以最終的PCF8591的轉(zhuǎn)換速度,直接取決于I2C的通信速率。由于I2C速度的限制,所以PCF8591的算是個低速的AD和DA集成,主要應(yīng)用在一些轉(zhuǎn)換速度要求不高,希望成本較低的場合,比如電池供電設(shè)備,測量電池的供電電壓,電壓低于某一個值,報警提示更換電池等類似場合。 Vref基準(zhǔn)電壓的提供,方法一是采用簡易的原則,直接接到VCC上去。但是由于VCC會受到整個線路的用電功耗情況影響,一來不是準(zhǔn)確的5V,實測大多在4.8V左右,二來隨著整個系統(tǒng)負載情況的變化會產(chǎn)生波動,所以只能用在簡易的、對精度要求不高的場合。方法二是使用專門的基準(zhǔn)電壓器件,比如TL431,它可以提供一個精度很高的2.5V的電壓基準(zhǔn),這是我們通常采用的方法。如圖17-4所示。
ps5b156.jpg (50.86 KB, 下載次數(shù): 193)
下載附件
2013-11-13 23:50 上傳
圖17-4 PCF8591電路圖 圖中J17是雙排插針,大家可以根據(jù)自己的需求選擇跳線帽短接還是使用杜邦線接其他外接電路,都是可以的。在這個地方,我們直接把J17的3腳和4腳用跳線帽短路起來,那么現(xiàn)在Vref的基準(zhǔn)源就是2.5V了。分別把5和6、7和8、9和10、11和12用跳線帽短接起來的話,那么我們的AIN0實測的就是滑動變阻器的分壓值,AIN1和AIN2測的是GND的值,AIN3測的是+5V的值。這里需要注意的是,AIN3雖然測的是+5V的值,但是對于AD來說,只要輸入信號超過Vref基準(zhǔn)源,它得到的始終都是最大值,即255,也就是說它實際上無法測量超過其Vref的電壓信號。需要注意的是,所有輸入信號的電壓值都不能超過VCC,即+5V,否則可能會損壞ADC芯片。
17.4 PCF8591的軟件編程
PCF8591的通信接口是I2C,那么編程肯定是符合這個協(xié)議的。單片機對PCF8591進行初始化,一共發(fā)送三個字節(jié)即可。第一個字節(jié),和EEPROM類似,第一個字節(jié)是地址字節(jié),其中7位代表地址,1位代表讀寫方向。地址高4位固定是1001,低三位是A2,A1,A0,這三位我們電路上都接了GND,因此也就是000,如圖17-5所示。
ps5b157.jpg (11.65 KB, 下載次數(shù): 210)
下載附件
2013-11-13 23:50 上傳
圖17-5 PCF8591地址字節(jié) 發(fā)送到PCF8591的第二個字節(jié)將被存儲在控制寄存器,用于控制PCF8591的功能。其中第3位和第7位是固定的0,另外6位各自有各自的作用,如圖17-6所示,我逐一介紹。
ps5b158.jpg (4.17 KB, 下載次數(shù): 244)
下載附件
2013-11-13 23:50 上傳
圖17-6 PCF8591控制字節(jié) 控制字節(jié)的第6位是DA使能位,這一位置1表示DA輸出引腳使能,會產(chǎn)生模擬電壓輸出功能。第4位和第5位可以實現(xiàn)把PCF8591的4路模擬輸入配置成單端模式和差分模式,單端模式和差分模式的區(qū)別,我們17.4章節(jié)有介紹,這里大家只需要知道這兩位是配置AD輸入方式的控制位即可,如圖17-7所示。
ps5b159.jpg (23.12 KB, 下載次數(shù): 195)
下載附件
2013-11-13 23:50 上傳
圖17-7 PCF8591模擬輸入配置方式
控制字節(jié)的第2位是自動增量控制位,自動增量的意思就是,比如我們一共有4個通道,當(dāng)我們?nèi)渴褂玫臅r候,讀完了通道0,下一次再讀,會自動進入通道1進行讀取,不需要我們指定下一個通道,由于A/D每次讀到的數(shù)據(jù),都是上一次的轉(zhuǎn)換結(jié)果,所以同學(xué)們在使用自動增量功能的時候,要特別注意,當(dāng)前讀到的是上一個通道的值。為了保持程序的通用性,我們的代碼沒有使用這個功能,直接做了一個通用的程序。 控制字節(jié)的第0位和第1位就是通道選擇位了,00、01、10、11代表了從0到3的一共4個通道選擇。 發(fā)送給PCF8591的第三個字節(jié)D/A數(shù)據(jù)寄存器,表示D/A模擬輸出的電壓值。D/A模擬我們一會介紹,大家知道這個字節(jié)的作用即可。我們?nèi)绻麅H僅使用A/D功能的話,就可以不發(fā)送第三個字節(jié)。 下面我們用一個程序,把AIN0、AIN1、AIN3測到的電壓值顯示在液晶上,同時大家可以轉(zhuǎn)動電位器,會發(fā)現(xiàn)AIN0的值發(fā)生變化。 /***********************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() //等待液晶準(zhǔn)備好 { unsigned char sta;
LCD1602_DB = 0xFF; LCD1602_RS = 0; LCD1602_RW = 1; do { LCD1602_E = 1; sta = LCD1602_DB; //讀取狀態(tài)字 LCD1602_E = 0; } while (sta & 0x80); //bit7等于1表示液晶正忙,重復(fù)檢測直到其等于0為止 } void LcdWriteCmd(unsigned char cmd) //寫入命令函數(shù) { LcdWaitReady(); LCD1602_RS = 0; LCD1602_RW = 0; LCD1602_DB = cmd; LCD1602_E = 1; LCD1602_E = 0; } void LcdWriteDat(unsigned char dat) //寫入數(shù)據(jù)函數(shù) { 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) //顯示字符串,屏幕起始坐標(biāo)(x,y),字符串指針str { unsigned char addr;
//由輸入的顯示坐標(biāo)計算顯示RAM的地址 if (y == 0) addr = 0x00 + x; //第一行字符地址從0x00起始 else addr = 0x40 + x; //第二行字符地址從0x40起始
//由起始顯示RAM地址連續(xù)寫入字符串 LcdWriteCmd(addr | 0x80); //寫入起始地址 while (*str != '\0') //連續(xù)寫入字符串?dāng)?shù)據(jù),直到檢測到結(jié)束符 { LcdWriteDat(*str); str++; } } void LcdInit() //液晶初始化函數(shù) { LcdWriteCmd(0x38); //16*2顯示,5*7點陣,8位數(shù)據(jù)接口 LcdWriteCmd(0x0C); //顯示器開,光標(biāo)關(guān)閉 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() //產(chǎn)生總線起始信號 { I2C_SDA = 1; //首先確保SDA、SCL都是高電平 I2C_SCL = 1; I2CDelay(); I2C_SDA = 0; //先拉低SDA I2CDelay(); I2C_SCL = 0; //再拉低SCL } void I2CStop() //產(chǎn)生總線停止信號 { 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總線寫操作,待寫入字節(jié)dat,返回值為應(yīng)答狀態(tài) { bit ack; //用于暫存應(yīng)答位的值 unsigned char mask; //用于探測字節(jié)內(nèi)某一位值的掩碼變量
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位數(shù)據(jù)發(fā)送完后,主機釋放SDA,以檢測從機應(yīng)答 I2CDelay(); I2C_SCL = 1; //拉高SCL ack = I2C_SDA; //讀取此時的SDA值,即為從機的應(yīng)答值 I2CDelay(); I2C_SCL = 0; //再拉低SCL完成應(yīng)答位,并保持住總線
return (~ack); //應(yīng)答值取反以符合通常的邏輯:0=不存在或忙或?qū)懭胧。?/font>1=存在且空閑或?qū)懭氤晒?/font> } unsigned char I2CReadNAK() //I2C總線讀操作,并發(fā)送非應(yīng)答信號,返回值為讀到的字節(jié) { 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中對應(yīng)位清零 else dat |= mask; //為1時,dat中對應(yīng)位置1 I2CDelay(); I2C_SCL = 0; //再拉低SCL,以使從機發(fā)送出下一位 } I2C_SDA = 1; //8位數(shù)據(jù)發(fā)送完后,拉高SDA,發(fā)送非應(yīng)答信號 I2CDelay(); I2C_SCL = 1; //拉高SCL I2CDelay(); I2C_SCL = 0; //再拉低SCL完成非應(yīng)答位,并保持住總線
return dat; } unsigned char I2CReadACK() //I2C總線讀操作,并發(fā)送應(yīng)答信號,返回值為讀到的字節(jié) { 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中對應(yīng)位清零 else dat |= mask; //為1時,dat中對應(yīng)位置1 I2CDelay(); I2C_SCL = 0; //再拉低SCL,以使從機發(fā)送出下一位 } I2C_SDA = 0; //8位數(shù)據(jù)發(fā)送完后,拉低SDA,發(fā)送應(yīng)答信號 I2CDelay(); I2C_SCL = 1; //拉高SCL I2CDelay(); I2C_SCL = 0; //再拉低SCL完成應(yīng)答位,并保持住總線
return dat; } /***********************main.c文件程序源代碼*************************/ #include <reg52.h>
bit flag300ms = 1; //300ms定時標(biāo)志 unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié)
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的轉(zhuǎn)換值 ValueToString(str, val); //轉(zhuǎn)為字符串格式的電壓值 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) //讀取當(dāng)前的ADC轉(zhuǎn)換值,chn為ADC通道號0-3 { unsigned char val;
I2CStart(); if (!I2CWrite(0x48<<1)) //尋址PCF8591,如未應(yīng)答,則停止操作并返回0 { I2CStop(); return 0; } I2CWrite(0x40|chn); //寫入控制字節(jié),選擇轉(zhuǎn)換通道 I2CStart(); I2CWrite((0x48<<1)|0x01); //尋址PCF8591,指定后續(xù)為讀操作 I2CReadACK(); //先空讀一個字節(jié),提供采樣轉(zhuǎn)換時間 val = I2CReadNAK(); //讀取剛剛轉(zhuǎn)換完的值 I2CStop();
return val; } void ValueToString(unsigned char *str, unsigned char val) //ADC轉(zhuǎn)換值轉(zhuǎn)為實際電壓值的字符串形式 { val = (val*25) / 255; //電壓值=轉(zhuǎn)換結(jié)果*2.5V/255,式中的25隱含了一位十進制小數(shù) str[0] = (val/10) + '0'; //整數(shù)位字符 str[1] = '.'; //小數(shù)點 str[2] = (val%10) + '0'; //小數(shù)位字符 str[3] = 'V'; //電壓單位 str[4] = '\0'; //結(jié)束符 }
void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 12; //修正中斷響應(yīng)延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) 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中斷服務(wù)函數(shù) { static unsigned char tmr300ms = 0;
TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; tmr300ms++; if (tmr300ms >= 30) //定時300ms { tmr300ms = 0; flag300ms = 1; } } 細心閱讀程序的同學(xué)會發(fā)現(xiàn),我們程序在進行A/D讀取數(shù)據(jù)的時候,共使用了兩條程序去讀了2個字節(jié)。I2CReadACK(); val = I2CReadNAK();PCF8591的轉(zhuǎn)換時鐘是I2C的SCL,而A/D的特點是每次讀到的都是上一次的轉(zhuǎn)換結(jié)果,因此我們這里第一條語句的作用是產(chǎn)生一個整體的SCL時鐘提供給PCF8591進行A/D轉(zhuǎn)換,第二次是讀取當(dāng)前的轉(zhuǎn)換結(jié)果。如果我們只使用第二條語句的話,每次讀到的都是上一次的轉(zhuǎn)換結(jié)果。
17.5 A/D差分輸入信號
細心的同學(xué)在閱讀PCF8591手冊的時候,會發(fā)現(xiàn)控制字的第4位和第5位是用于控制PCF8591的模擬輸入引腳是單端輸入還是差分輸入。差分輸入是模擬電路常用的一個技巧,這里我們簡單介紹一些相關(guān)內(nèi)容。 從嚴(yán)格意義上來講,所有的信號都是差分信號,因為所有的電壓只能是相對于另外一個電壓而言。但是大多數(shù)系統(tǒng),我們都是把系統(tǒng)的GND作為基準(zhǔn)點。而對于A/D來說的差分輸入,通常情況下是除了GND以外,另外兩路幅度相同,極性相反的差分輸入信號,其實理解起來很簡單,就如同我們的蹺蹺板一樣。如圖17-8所示。
ps5b120.jpg (18.6 KB, 下載次數(shù): 206)
下載附件
2013-11-13 23:52 上傳
圖17-8 差分輸入原理 差分輸入的話,就不是單個輸入,而是由2個輸入端構(gòu)成的一組差分輸入。我們的PCF8591一共是4個模擬輸入端,可以配置成4種模式,最典型的是4個輸入端構(gòu)造成的兩路差分模式,如圖17-9所示。
ps5b121.jpg (8.56 KB, 下載次數(shù): 204)
下載附件
2013-11-13 23:52 上傳
圖17-9 PCF8591差分輸入模式 當(dāng)控制字的第4位和第5位都是1的時候,那么4路模擬被配置成2路差分模式輸入channel 0和channel 1。我們以channel 0為例,其中AIN0是正向輸入端,AIN1是反向輸入端,他們之間的信號輸入是幅度相同,極性相反的信號,通過減法器后,得到的是兩個輸入通道的差值,如圖17-10所示。
ps5b122.jpg (48.66 KB, 下載次數(shù): 195)
下載附件
2013-11-13 23:52 上傳
圖17-10 差分輸入信號 通常情況下,差分輸入的中線是基準(zhǔn)電壓的一半,我們的基準(zhǔn)電壓是2.5V,假如1.25V作為中線,V+是AIN0的輸入波形,V-是AIN1的輸入波形,Signal Value就是經(jīng)過減法器后的波形。很多A/D都采用差分的方式輸入,因為差分輸入方式比單端輸入來說,有很強的抗干擾能力。 1、單端輸入信號時,如果一線上發(fā)生干擾變化,比如幅度增大5mv,GND不變,測到的數(shù)據(jù)會有偏差;而差分信號輸入時,當(dāng)外界存在干擾信號時,幾乎同時被耦合到兩條線上,幅度增大5mv會同時增大5mv,而接收端關(guān)心的只是兩個信號的差值,所以外界的這種共模噪聲可以被完全抵消掉。 2、由于兩根信號的極性相反,他們對外輻射的電磁場可以相互抵消,有效的抑制釋放到外界的電磁能量。 在我們的KST-51開發(fā)板上,我們沒有做差分信號輸入的實驗環(huán)境,由于這個內(nèi)容在A/D部分比較重要,所以大家還是要學(xué)習(xí)一下的。
17.6 D/A輸出
D/A是和A/D剛好反方向,一個8位的D/A,從0到255,代表了0到2.55V的話,那么我們用單片機給第三個字節(jié)發(fā)送100,D/A引腳就會輸出一個1V的電壓,發(fā)送200就輸出一個2V的電壓,很簡單,我們用一個簡單的程序?qū)崿F(xiàn)出來,并且通過上、下按鍵可以增大輸出幅度值,每次增加或減小0.1V。如果有萬用表的話,可以直接測試一下板子上AOUT點的輸出電壓,觀察它的變化。由于PCF8591的偏置誤差最大是50mv(由數(shù)據(jù)手冊提供),所以我們用萬用表測到的電壓值和理論值之間的誤差就應(yīng)該在50mV以內(nèi)。 /***********************I2C.c文件程序源代碼*************************/ 略 /***********************keyboard.c文件程序源代碼*************************/ #include <reg52.h>
sbit KEY_IN_1 = P2^4; //矩陣按鍵的掃描輸入引腳1 sbit KEY_IN_2 = P2^5; //矩陣按鍵的掃描輸入引腳2 sbit KEY_IN_3 = P2^6; //矩陣按鍵的掃描輸入引腳3 sbit KEY_IN_4 = P2^7; //矩陣按鍵的掃描輸入引腳4 sbit KEY_OUT_1 = P2^3; //矩陣按鍵的掃描輸出引腳1 sbit KEY_OUT_2 = P2^2; //矩陣按鍵的掃描輸出引腳2 sbit KEY_OUT_3 = P2^1; //矩陣按鍵的掃描輸出引腳3 sbit KEY_OUT_4 = P2^0; //矩陣按鍵的掃描輸出引腳4
const unsigned char code KeyCodeMap[4][4] = { //矩陣按鍵編號到PC標(biāo)準(zhǔn)鍵盤鍵碼的映射表 { '1', '2', '3', 0x26 }, //數(shù)字鍵1、數(shù)字鍵2、數(shù)字鍵3、向上鍵 { '4', '5', '6', 0x25 }, //數(shù)字鍵4、數(shù)字鍵5、數(shù)字鍵6、向左鍵 { '7', '8', '9', 0x28 }, //數(shù)字鍵7、數(shù)字鍵8、數(shù)字鍵9、向下鍵 { '0', 0x1B, 0x0D, 0x27 } //數(shù)字鍵0、ESC鍵、 回車鍵、 向右鍵 }; unsigned char pdata KeySta[4][4] = { //全部矩陣按鍵的當(dāng)前狀態(tài) {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} };
extern void KeyAction(unsigned char keycode);
void KeyDriver() //按鍵動作驅(qū)動函數(shù) { unsigned char i, j; static unsigned char pdata backup[4][4] = { //按鍵值備份,保存前一次的值 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} };
for (i=0; i<4; i++) //循環(huán)掃描4*4的矩陣按鍵 { for (j=0; j<4; j++) { if (backup[ i][j] != KeySta[ i][j]) //檢測按鍵動作 { if (backup[ i][j] != 0) //按鍵按下時執(zhí)行動作 { KeyAction(KeyCodeMap[ i][j]); //調(diào)用按鍵動作函數(shù) } backup[ i][j] = KeySta[ i][j]; } } } } void KeyScan() //按鍵掃描函數(shù) { unsigned char i; static unsigned char keyout = 0; //矩陣按鍵掃描輸出計數(shù)器 static unsigned char keybuf[4][4] = { //按鍵掃描緩沖區(qū),保存一段時間內(nèi)的掃描值 {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF} };
//將一行的4個按鍵值移入緩沖區(qū) keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1; keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2; keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3; keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;
//消抖后更新按鍵狀態(tài) for (i=0; i<4; i++) //每行4個按鍵,所以循環(huán)4次 { if ((keybuf[keyout][ i] & 0x0F) == 0x00) { //連續(xù)4次掃描值為0,即16ms(4*4ms)內(nèi)都只檢測到按下狀態(tài)時,可認(rèn)為按鍵已按下 KeySta[keyout][ i] = 0; } else if ((keybuf[keyout][ i] & 0x0F) == 0x0F) { //連續(xù)4次掃描值為1,即16ms(4*4ms)內(nèi)都只檢測到彈起狀態(tài)時,可認(rèn)為按鍵已彈起 KeySta[keyout][ i] = 1; } }
//執(zhí)行下一次的掃描輸出 keyout++; keyout &= 0x03; switch (keyout) { case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break; case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break; case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break; case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break; default: break; } } /***********************main.c文件程序源代碼*************************/ #include <reg52.h>
unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié)
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(); } }
void SetDACOut(unsigned char val) //設(shè)置DAC輸出值 { I2CStart(); if (!I2CWrite(0x48<<1)) //尋址PCF8591,如未應(yīng)答,則停止操作并返回 { I2CStop(); return; } I2CWrite(0x40); //寫入控制字節(jié) I2CWrite(val); //寫如DA值 I2CStop(); } void KeyAction(unsigned char keycode) //按鍵動作函數(shù),根據(jù)鍵碼執(zhí)行相應(yīng)動作 { static unsigned char volt = 0; //輸出電壓值,隱含了一位十進制小數(shù)位
if (keycode == 0x26) //向上鍵,增加0.1V電壓值 { if (volt < 25) { volt++; SetDACOut(volt*255/25); //轉(zhuǎn)換為AD輸出值 } } else if (keycode == 0x28) //向下鍵,減小0.1V電壓值 { if (volt > 0) { volt--; SetDACOut(volt*255/25); //轉(zhuǎn)換為AD輸出值 } } } void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 34; //修正中斷響應(yīng)延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) 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中斷服務(wù)函數(shù) { TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; KeyScan(); }
17.7 PCF8591信號發(fā)生器
有了D/A這個武器,我們就不僅僅可以輸出方波信號了,可以輸出任意波形了,比如正弦波、三角波、鋸齒波等等。以正弦波為例,首先我們要建立一個正弦波的波表。這些不需要大家去逐一計算,可以通過搜索找到正弦波數(shù)據(jù)表,然后可以根據(jù)時間參數(shù)自己選取其中一定量數(shù)據(jù)作為我們程序的正弦波表,我們的程序代碼選取了32個點。 /***********************I2C.c文件程序源代碼*************************/ 略 /***********************keyboard.c文件程序源代碼********************/ 略 /***********************main.c文件程序源代碼************************/ #include <reg52.h>
unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié) unsigned char T1RH = 1; //T1重載值的高字節(jié) unsigned char T1RL = 1; //T1重載值的低字節(jié)
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; //波表指針
void SetWaveFreq(unsigned char freq); 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 pWave = SinWave; //默認(rèn)正弦波 SetWaveFreq(10); //默認(rèn)頻率10Hz
while(1) { KeyDriver(); } }
void KeyAction(unsigned char keycode) //按鍵動作函數(shù),根據(jù)鍵碼執(zhí)行相應(yīng)動作 { static unsigned char wave = 0;
if (keycode == 0x26) //向上鍵,切換波形 { if (wave == 0) { wave = 1; pWave = TriWave; } else if (wave == 1) { wave = 2; pWave = SawWave; } else { wave = 0; pWave = SinWave; } } } void SetDACOut(unsigned char val) //設(shè)置DAC輸出值 { I2CStart(); if (!I2CWrite(0x48<<1)) //尋址PCF8591,如未應(yīng)答,則停止操作并返回 { I2CStop(); return; } I2CWrite(0x40); //寫入控制字節(jié) I2CWrite(val); //寫如DA值 I2CStop(); } void SetWaveFreq(unsigned char freq) //設(shè)置輸出波形的頻率 { unsigned long tmp;
tmp = (11059200/12) / (freq*32); //定時器計數(shù)頻率,是波形頻率的32倍 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 36; //修正中斷響應(yīng)延時造成的誤差
T1RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T1RL = (unsigned char)tmp; TMOD &= 0x0F; //清零T1的控制位 TMOD |= 0x10; //配置T1為模式1 TH1 = T1RH; //加載T1重載值 TL1 = T1RL; ET1 = 1; //使能T1中斷 PT1 = 1; //設(shè)置為高優(yōu)先級 TR1 = 1; //啟動T1 } void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 34; //修正中斷響應(yīng)延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) 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中斷服務(wù)函數(shù) { TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; KeyScan(); } void InterruptTimer1() interrupt 3 //T1中斷服務(wù)函數(shù) { static unsigned char i = 0;
TH1 = T1RH; //定時器重新加載重載值 TL1 = T1RL; //循環(huán)輸出波表中的數(shù)據(jù) SetDACOut(pWave[ i]); i++; if (i >= 32) { i = 0; } } 這個程序可以通過“向上”按鍵來實現(xiàn)波形輸出切換,但是我們的D/A輸出沒有辦法接到顯示界面,所以我們用示波器抓出來波形給大家看一下,如圖17-11、圖17-12、圖17-13所示。
ps5b123.jpg (33.02 KB, 下載次數(shù): 227)
下載附件
2013-11-13 23:52 上傳
圖17-11 D/A輸出正弦波形
ps5b124.jpg (32.7 KB, 下載次數(shù): 233)
下載附件
2013-11-13 23:52 上傳
圖17-12 D/A輸出三角波形
ps5b125.jpg (30.69 KB, 下載次數(shù): 202)
下載附件
2013-11-13 23:52 上傳
圖17-13 D/A輸出鋸齒波形 這幾張圖可以直接說明我們實現(xiàn)的波形發(fā)生器的程序。細心的同學(xué)會發(fā)現(xiàn)我們波形上有很多小鋸齒,沒有平滑的連起來。這是因為我們DA最多只能輸出0~Vref之間的256個離散的電壓值,而不是連續(xù)的任意值,所以每個離散值都會持續(xù)一定的時間,然后跳變到下一個離散值,于是就呈現(xiàn)出了波形上的這種鋸齒。在實際開發(fā)中,我們只需要在DA后級加一級低通濾波電路,就可以讓帶鋸齒的波形變得平滑起來。
17.8 作業(yè)
1、掌握A/D和D/A的基本概念和性能指標(biāo)。 2、將AD采集到的數(shù)值顯示到數(shù)碼管上。 [size=12.0000pt]3、將信號發(fā)生器的程序改裝,可以通過按鍵實現(xiàn)頻率的調(diào)整。
|