本教材現以連載的方式由網絡發布,并將于2014年由清華大學出版社出版最終完整版,版權歸作者和清華大學出版社所有。本著開源、分享的理念,本教材可以自由傳播及學習使用,但是務必請注明出處來自金沙灘工作室
從我們學到的知識了解到,我們的單片機是一個典型的數字系統。數字系統只能對輸入的數字信號進行處理,其輸出信號也是數字信號。但是在工業檢測系統和日常生活中的許多物理量都是模擬量,比如溫度、長度、壓力、速度等等,這些模擬量可以通過傳感器變成與之對應的電壓、電流等電模擬量。為了實現數字系統對這些電模擬量的檢測、運算和控制,就需要一個模擬量和數字量之間相互轉換的過程。這節課我們就要學習這個相互轉換過程。
17.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一個簡單的米尺刻度示意圖。
ps5b153.jpg (11.1 KB, 下載次數: 208)
下載附件
2013-11-13 23:50 上傳
圖17-1 米尺刻度示意圖 我們往杯子里倒水,水位會隨著倒入的水量的多少而變化。現在就用這個米尺來測量我們杯子里的水位的高度。水位變化是連續的,而我們只能通過尺子上的刻度來讀取水位的高度,獲取我們想得到的水位的數字量信息。這個過程,就可以簡單理解為我們電路中的ADC采樣。
17.2 A/D的主要指標
我們在選取和使用 A/D的時候,依靠什么指標來判斷很重要。由于AD的種類很多,分為積分型、逐次逼近型、并行/串行比較型、Σ-Δ型等多種類型。同時指標也比較多,并且有的指標還有輕微差別,具體可上 www.zg4o1577.cn查詢.在這里我是以同學們便于理解的方法去講解,如果和某一確定類型 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、分辨率 分辨率是數字量變化一個最小刻度時,模擬信號的變化量,定義為滿刻度量程與2n-1的 比值。5.10V的電壓系統,使用8位的ADC進行測量,那么相當于0到255一共256個刻度,把5.10V平均分成了255份,那么分辨率就是5.10/255 = 0.02V。 4、INL(積分非線性度)和DNL(差分非線性度) 初學者最容易混淆的兩個概念就是“分辨率”和“精度”,認為分辨率越高,則精度越高,而實際上,兩者之間是沒有必然聯系的。分辨率是用來描述刻度劃分的,而精度是用來描述準確程度的。同樣一根米尺,刻度數相同,分辨率就相當,但是精度卻可以相差很大,如圖17-2所示。
ps5b154.jpg (10.9 KB, 下載次數: 215)
下載附件
2013-11-13 23:50 上傳
圖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.03之間。值得一提的是DNL并非一定小于1LSB,很多時候它會等于或大于1LSB,這就相當于是一定程度上的刻度紊亂,當實際電壓保持不變時,ADC得出的結果可能會在幾個數值之間跳動,很大程度上就是由于這個原因(但并不完全是,因為還有無時無處不在的干擾的影響)。 5、轉換速率 轉換速率,是指ADC每秒能進行采樣轉換的最大次數,單位是sps(或s/s、sa/s,即samples per second),它與ADC完成一次從模擬到數字的轉換所需要的時間互為倒數關系。ADC的種類比較多,其中積分型的ADC轉換時間是毫秒級的,屬于低速ADC;逐次逼近型ADC轉換時間是微妙級的,屬于中速ADC;并行/串行的ADC的轉換時間可達到納秒級,屬于高速ADC。 ADC的這幾個主要指標大家先熟悉一下,對于其他的,作為一個入門級別的選手來說,先不著急深入理解。以后使用過程中遇到了,再查找相關資料深入學習,當前重點是在頭腦中建立一個ADC的基本概念。
17.3 PCF8591的硬件接口
PCF8591是一個單電源低功耗的8位CMOS數據采集器件,具有4路模擬輸入,1路模擬輸出和一個串行I2C總線接口用來與MCU通信。3個地址引腳A0、A1、A2用于編程硬件地址,允許最多8個器件連接到I2C總線而不需要額外的片選電路。器件的地址、控制以及數據都是通過I2C總線來傳輸,我們先看一下PCF8591的原理圖,如圖17-3所示。
ps5b155.jpg (44.33 KB, 下載次數: 214)
下載附件
2013-11-13 23:50 上傳
圖17-3 PCF8591原理圖 其中引腳1、2、3、4是4路模擬輸入,引腳5、6、7是I2C總線的硬件地址,8腳是數字GND,9腳和10腳是I2C總線的SDA和SCL。12腳是時鐘選擇引腳,如果接高電平表示用外部時鐘輸入,接低電平則用內部時鐘,我們這套電路用的是內部時鐘,因此12腳直接接GND,同時11腳懸空。13腳是模擬GND,在實際開發中,如果有比較復雜的模擬電路,那么模擬GND部分在布局布線上要特別處理,而且和數字GND的連接也有多種方式,這里大家先了解即可。在我們板子上沒有復雜的模擬部分電路,所以我們把模擬的GND和數字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所示。
ps5b156.jpg (50.86 KB, 下載次數: 193)
下載附件
2013-11-13 23:50 上傳
圖17-4 PCF8591電路圖 圖中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芯片。
17.4 PCF8591的軟件編程
PCF8591的通信接口是I2C,那么編程肯定是符合這個協議的。單片機對PCF8591進行初始化,一共發送三個字節即可。第一個字節,和EEPROM類似,第一個字節是地址字節,其中7位代表地址,1位代表讀寫方向。地址高4位固定是1001,低三位是A2,A1,A0,這三位我們電路上都接了GND,因此也就是000,如圖17-5所示。
ps5b157.jpg (11.65 KB, 下載次數: 210)
下載附件
2013-11-13 23:50 上傳
圖17-5 PCF8591地址字節 發送到PCF8591的第二個字節將被存儲在控制寄存器,用于控制PCF8591的功能。其中第3位和第7位是固定的0,另外6位各自有各自的作用,如圖17-6所示,我逐一介紹。
ps5b158.jpg (4.17 KB, 下載次數: 244)
下載附件
2013-11-13 23:50 上傳
圖17-6 PCF8591控制字節 控制字節的第6位是DA使能位,這一位置1表示DA輸出引腳使能,會產生模擬電壓輸出功能。第4位和第5位可以實現把PCF8591的4路模擬輸入配置成單端模式和差分模式,單端模式和差分模式的區別,我們17.4章節有介紹,這里大家只需要知道這兩位是配置AD輸入方式的控制位即可,如圖17-7所示。
ps5b159.jpg (23.12 KB, 下載次數: 195)
下載附件
2013-11-13 23:50 上傳
圖17-7 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轉換,第二次是讀取當前的轉換結果。如果我們只使用第二條語句的話,每次讀到的都是上一次的轉換結果。
17.5 A/D差分輸入信號
細心的同學在閱讀PCF8591手冊的時候,會發現控制字的第4位和第5位是用于控制PCF8591的模擬輸入引腳是單端輸入還是差分輸入。差分輸入是模擬電路常用的一個技巧,這里我們簡單介紹一些相關內容。 從嚴格意義上來講,所有的信號都是差分信號,因為所有的電壓只能是相對于另外一個電壓而言。但是大多數系統,我們都是把系統的GND作為基準點。而對于A/D來說的差分輸入,通常情況下是除了GND以外,另外兩路幅度相同,極性相反的差分輸入信號,其實理解起來很簡單,就如同我們的蹺蹺板一樣。如圖17-8所示。
ps5b120.jpg (18.6 KB, 下載次數: 206)
下載附件
2013-11-13 23:52 上傳
圖17-8 差分輸入原理 差分輸入的話,就不是單個輸入,而是由2個輸入端構成的一組差分輸入。我們的PCF8591一共是4個模擬輸入端,可以配置成4種模式,最典型的是4個輸入端構造成的兩路差分模式,如圖17-9所示。
ps5b121.jpg (8.56 KB, 下載次數: 204)
下載附件
2013-11-13 23:52 上傳
圖17-9 PCF8591差分輸入模式 當控制字的第4位和第5位都是1的時候,那么4路模擬被配置成2路差分模式輸入channel 0和channel 1。我們以channel 0為例,其中AIN0是正向輸入端,AIN1是反向輸入端,他們之間的信號輸入是幅度相同,極性相反的信號,通過減法器后,得到的是兩個輸入通道的差值,如圖17-10所示。
ps5b122.jpg (48.66 KB, 下載次數: 195)
下載附件
2013-11-13 23:52 上傳
圖17-10 差分輸入信號 通常情況下,差分輸入的中線是基準電壓的一半,我們的基準電壓是2.5V,假如1.25V作為中線,V+是AIN0的輸入波形,V-是AIN1的輸入波形,Signal Value就是經過減法器后的波形。很多A/D都采用差分的方式輸入,因為差分輸入方式比單端輸入來說,有很強的抗干擾能力。 1、單端輸入信號時,如果一線上發生干擾變化,比如幅度增大5mv,GND不變,測到的數據會有偏差;而差分信號輸入時,當外界存在干擾信號時,幾乎同時被耦合到兩條線上,幅度增大5mv會同時增大5mv,而接收端關心的只是兩個信號的差值,所以外界的這種共模噪聲可以被完全抵消掉。 2、由于兩根信號的極性相反,他們對外輻射的電磁場可以相互抵消,有效的抑制釋放到外界的電磁能量。 在我們的KST-51開發板上,我們沒有做差分信號輸入的實驗環境,由于這個內容在A/D部分比較重要,所以大家還是要學習一下的。
17.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的偏置誤差最大是50mv(由數據手冊提供),所以我們用萬用表測到的電壓值和理論值之間的誤差就應該在50mV以內。 /***********************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標準鍵盤鍵碼的映射表 { '1', '2', '3', 0x26 }, //數字鍵1、數字鍵2、數字鍵3、向上鍵 { '4', '5', '6', 0x25 }, //數字鍵4、數字鍵5、數字鍵6、向左鍵 { '7', '8', '9', 0x28 }, //數字鍵7、數字鍵8、數字鍵9、向下鍵 { '0', 0x1B, 0x0D, 0x27 } //數字鍵0、ESC鍵、 回車鍵、 向右鍵 }; unsigned char pdata KeySta[4][4] = { //全部矩陣按鍵的當前狀態 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} };
extern void KeyAction(unsigned char keycode);
void KeyDriver() //按鍵動作驅動函數 { 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++) //循環掃描4*4的矩陣按鍵 { for (j=0; j<4; j++) { if (backup[ i][j] != KeySta[ i][j]) //檢測按鍵動作 { if (backup[ i][j] != 0) //按鍵按下時執行動作 { KeyAction(KeyCodeMap[ i][j]); //調用按鍵動作函數 } backup[ i][j] = KeySta[ i][j]; } } } } void KeyScan() //按鍵掃描函數 { unsigned char i; static unsigned char keyout = 0; //矩陣按鍵掃描輸出計數器 static unsigned char keybuf[4][4] = { //按鍵掃描緩沖區,保存一段時間內的掃描值 {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF} };
//將一行的4個按鍵值移入緩沖區 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;
//消抖后更新按鍵狀態 for (i=0; i<4; i++) //每行4個按鍵,所以循環4次 { if ((keybuf[keyout][ i] & 0x0F) == 0x00) { //連續4次掃描值為0,即16ms(4*4ms)內都只檢測到按下狀態時,可認為按鍵已按下 KeySta[keyout][ i] = 0; } else if ((keybuf[keyout][ i] & 0x0F) == 0x0F) { //連續4次掃描值為1,即16ms(4*4ms)內都只檢測到彈起狀態時,可認為按鍵已彈起 KeySta[keyout][ i] = 1; } }
//執行下一次的掃描輸出 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重載值的高字節 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(); } }
void SetDACOut(unsigned char val) //設置DAC輸出值 { I2CStart(); if (!I2CWrite(0x48<<1)) //尋址PCF8591,如未應答,則停止操作并返回 { I2CStop(); return; } I2CWrite(0x40); //寫入控制字節 I2CWrite(val); //寫如DA值 I2CStop(); } 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輸出值 } } } void ConfigTimer0(unsigned int ms) //T0配置函數 { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數頻率 tmp = (tmp * ms) / 1000; //計算所需的計數值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 34; //修正中斷響應延時造成的誤差
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中斷服務函數 { TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; KeyScan(); }
17.7 PCF8591信號發生器
有了D/A這個武器,我們就不僅僅可以輸出方波信號了,可以輸出任意波形了,比如正弦波、三角波、鋸齒波等等。以正弦波為例,首先我們要建立一個正弦波的波表。這些不需要大家去逐一計算,可以通過搜索找到正弦波數據表,然后可以根據時間參數自己選取其中一定量數據作為我們程序的正弦波表,我們的程序代碼選取了32個點。 /***********************I2C.c文件程序源代碼*************************/ 略 /***********************keyboard.c文件程序源代碼********************/ 略 /***********************main.c文件程序源代碼************************/ #include <reg52.h>
unsigned char T0RH = 0; //T0重載值的高字節 unsigned char T0RL = 0; //T0重載值的低字節 unsigned char T1RH = 1; //T1重載值的高字節 unsigned char T1RL = 1; //T1重載值的低字節
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; //默認正弦波 SetWaveFreq(10); //默認頻率10Hz
while(1) { KeyDriver(); } }
void KeyAction(unsigned char keycode) //按鍵動作函數,根據鍵碼執行相應動作 { 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) //設置DAC輸出值 { I2CStart(); if (!I2CWrite(0x48<<1)) //尋址PCF8591,如未應答,則停止操作并返回 { I2CStop(); return; } I2CWrite(0x40); //寫入控制字節 I2CWrite(val); //寫如DA值 I2CStop(); } void SetWaveFreq(unsigned char freq) //設置輸出波形的頻率 { unsigned long tmp;
tmp = (11059200/12) / (freq*32); //定時器計數頻率,是波形頻率的32倍 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 36; //修正中斷響應延時造成的誤差
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 } void ConfigTimer0(unsigned int ms) //T0配置函數 { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數頻率 tmp = (tmp * ms) / 1000; //計算所需的計數值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 34; //修正中斷響應延時造成的誤差
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中斷服務函數 { TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; KeyScan(); } void InterruptTimer1() interrupt 3 //T1中斷服務函數 { static unsigned char i = 0;
TH1 = T1RH; //定時器重新加載重載值 TL1 = T1RL; //循環輸出波表中的數據 SetDACOut(pWave[ i]); i++; if (i >= 32) { i = 0; } } 這個程序可以通過“向上”按鍵來實現波形輸出切換,但是我們的D/A輸出沒有辦法接到顯示界面,所以我們用示波器抓出來波形給大家看一下,如圖17-11、圖17-12、圖17-13所示。
ps5b123.jpg (33.02 KB, 下載次數: 227)
下載附件
2013-11-13 23:52 上傳
圖17-11 D/A輸出正弦波形
ps5b124.jpg (32.7 KB, 下載次數: 233)
下載附件
2013-11-13 23:52 上傳
圖17-12 D/A輸出三角波形
ps5b125.jpg (30.69 KB, 下載次數: 202)
下載附件
2013-11-13 23:52 上傳
圖17-13 D/A輸出鋸齒波形 這幾張圖可以直接說明我們實現的波形發生器的程序。細心的同學會發現我們波形上有很多小鋸齒,沒有平滑的連起來。這是因為我們DA最多只能輸出0~Vref之間的256個離散的電壓值,而不是連續的任意值,所以每個離散值都會持續一定的時間,然后跳變到下一個離散值,于是就呈現出了波形上的這種鋸齒。在實際開發中,我們只需要在DA后級加一級低通濾波電路,就可以讓帶鋸齒的波形變得平滑起來。
17.8 作業
1、掌握A/D和D/A的基本概念和性能指標。 2、將AD采集到的數值顯示到數碼管上。 [size=12.0000pt]3、將信號發生器的程序改裝,可以通過按鍵實現頻率的調整。
|