模塊化程序設計
不知不覺我們的51單片機開發實例已經進行到第三十篇了,是時候進行一個總結和反思了,總結什麼?反思什麼呢?我們先從程序結構開始吧。
總結在前面的29個例子中的程序,我們會發現:所有的程序都在一個main.c文件中,第一個程序只有不到20行,電腦屏幕上直接能夠從頭看到尾,這個程序看起來非常直觀,所有程序語句都是一目了然的。隨著學習的深入,我們編寫的程序是越來越複雜了,程序的長度是瘋狂地增加。到了第29個實例,我們的程序量已經接近500行了,假設我們要找最後一個函數的內容,那就要用滑鼠撥拉好一會才能找到這個函數,是不是會有「好長啊」的感慨?
更為致命的是,我們這個程序中既有延時程序、又有液晶顯示程序、還有DS18B20的讀寫程序。所有這個程序都擠在一個main.c文件中,即使我們已經按照延時、顯示、讀寫進行了劃分,整體感覺是不是有些亂?
現在我們看看DS18B20學習實驗的第三個程序,這個程序裡面有很多函數,這些函數的聲明和定義在整個程序裡面佔了很大的比例。我們在編寫和使用這些函數的時候,需要不停的翻找相關的程序部分。顯得很麻煩。而且整個程序顯得有些亂。那麼能不能把程序精簡一下,能不能把具有相關功能的函數放在一起,能不能向我們使用頭文件的時候,直接使用一個包含命令就把一些相關功能包含到程序裡,而我們在程序裡只要調用我們用到的函數就能夠實現我們想要達到的目的呢?
答案是肯定的!
那麼該怎麼辦?今天我們就學習一下模塊化程序設計,把程序進行合理的規劃!
本實例的目的:了解模塊化程序設計的思路和方法。
本實例的設計思路是:將《基於proteus的51單片機開發實例29-單總線DS18B20的讀寫》中的程序代碼按照延時功能、LCD1602液晶顯示功能、DS18B20的讀寫控制功能這三個部分,使用模塊化程序設計的方法,將這三個部分分別封裝為三個.c和.h文件,然後在main.c文件中使用宏定義語句調用。從而完成模塊化程序設計。
模塊化程序設計
現在我們看看《基於proteus的51單片機開發實例29-單總線DS18B20的讀寫》中的程序代碼,這個程序裡面有很多函數,這些函數的聲明和定義在整個程序裡面佔了很大的比例。我們在編寫和使用這些函數的時候,因為很多函數都不是獨立,需要不停的翻找相關的程序部分。顯得很麻煩。而且整個程序顯得有些亂。
那麼能不能把程序精簡一下,能不能把具有相關功能的函數放在一起,能不能像我們使用頭文件的時候,直接使用一個包含命令就把一些相關功能包含到程序裡,而我們在程序裡只要調用我們用到的函數就能夠實現我們想要達到的目的呢?
答案是肯定的!
解決方法是:化整為零,把負載的程序按照功能分解成不同的小模塊,分別實現。
模塊化程序設計是指將實現同一功能的程序整合起來,封裝到一個程序模塊中,這樣在使用該功能的時候,可以直接調用該模塊中的相關函數進行操作。
在進行程序設計時把一個大的程序按照功能劃分為若干小的程序,每個小的程序完成一個確定的功能,在這些小的程序之間建立必要的聯繫,互相協作完成整個程序要完成的功能。通常我們稱這些小的程序為程序的模塊。
模塊化程序設計
具體到程序來說,模塊通常是指可以用一個名字調用的一個程序段。對於不同的程序設計語言,模塊的實現和名稱也不相同,在BASIC,FORTRAN語言中的模塊稱作子程序;C語言中的模塊叫函數
模塊化程序設計的思路是這樣的:將一個大的程序按功能分割成一些小模塊。
即:把具有相同功能的函數放在一個文件中,然後在主程序裡面用include預編譯命令包含xxx.h文件,這樣在主程序中就可以調用這個文件中的函數了,同樣的,在其它.c文件中,也可以使用include <at89x51.h」這條指令將I/O頭文件包含到主程序中)。同時將「.c」文件添加到主程序所在的項目組中。這樣就初步完成了模塊化程序設計框架。
這樣做的優點是,我們可以直接在「.h」文件中查找到我們需要的函數名稱,從而在主程序裡面直接調用,而不用去關心「.c」文件中的具體內容。如果我們要將該程序移植到不同型號的單片機上,我們同樣只需在「.h」文件中修改相應的埠定義即可。
一般情況下,程序中定義的函數和變量是有一定的作用域的,也就是說,我們在一個模塊中定義的變量和函數,它的作用域只限於本模塊文件和調用它的程序文件範圍內,而在沒有調用它的模塊程序裡面,如果調用它,編譯器是會提示錯誤的,它的函數是不能被使用的。
在對較為複雜的程序進行模塊化設計的時候,經常會遇到這樣一種情況:一個函數在不同的模塊之間都會用到,舉例來說,幾乎每一個程序中都會用到延時函數,也就是說一般的程序中都需要調用延時函數。出現這種情況該怎麼辦?難道需要在每個模塊中都定義相同的函數?但是我們都知道,程序中,不同的變量和函數只能使用不同的名稱,如果一個程序中有兩個名稱相同的變量或者函數的話,那程序編譯的時候會提示我們有重複定義的函數。難道要在不同的模塊中為相同功能的函數(功能相同,函數體的內容也相同)起不同的名字,這樣豈不是做了很多重複勞動,這樣的重複勞動還會造成程序的可讀性變得很差。怎麼辦?
模塊化程序設計
同樣的情況也會出現在不同模塊程序之間傳遞數據變量的時候。
在這樣的情況下,一種比較好的解決辦法是:使用文件包含命令「include&34; //34;DS18B20.h&include&34; //
也許有細心的朋友會發現,不同的include<reg51.h> //包含單片機寄存器的頭文件include&34; //34;DS18B20.h&include&34; //
看出來了吧?調用keil裡面自帶的頭文件時,文件名是用尖括號<>括起來的,而調用我們自己編寫的頭文件時,文件名是用&34;括起來的。這是為什麼呢?簡單說,使用尖括號<>括起來的頭文件,編譯器在編譯時,先從編譯器安裝的目錄下開始搜索這個文件,而用&34;括起來的頭文件,編譯的時候,先從我們建立的單片機項目文件中搜索該文件。
//Delay.h/*****************************************************函數功能:延時1ms(3j+2)*i=(3×33+2)×10=1010(微秒),可以認為是1毫秒***************************************************/void delay1ms();/*****************************************************函數功能:延時若干毫秒入口參數:n***************************************************/ void delaynms(unsigned int n);
//Delay.cinclude<intrins.h> //包含_nop_()函數定義的頭文件34;LCD1602.h&include&34; //34;Delay.h&include<reg51.h> //包含單片機寄存器的頭文件include&34; //34;DS18B20.h&include&34; //unsigned char code digit[10]={&34;}; //定義字符數組顯示數字unsigned char code Str[]={&34;}; //說明顯示的是溫度unsigned char code Error[]={&34;}; //說明沒有檢測到DS18B20unsigned char code Temp[]={&34;}; //說明顯示的是溫度unsigned char code Cent[]={&34;}; //溫度單位/*******************************************************************************以下是對液晶模塊的操作程序*******************************************************************************/sbit RS=P2^4; //寄存器選擇位,將RS位定義為P2.0引腳sbit RW=P2^5; //讀寫選擇位,將RW位定義為P2.1引腳sbit E=P2^6; //使能信號位,將E位定義為P2.2引腳sbit BF=P0^7; //忙碌標誌位,,將BF位定義為P0.7引腳/*****************************************************函數功能:判斷液晶模塊的忙碌狀態返回值:result。result=1,忙碌;result=0,不忙***************************************************/bit BusyTest(void) { bit result; RS=0; //根據規定,RS為低電平,RW為高電平時,可以讀狀態 RW=1; E=1; //E=1,才允許讀寫 _nop_(); //空操作 _nop_(); _nop_(); _nop_(); //空操作四個機器周期,給硬體反應時間 result=BF; //將忙碌標誌電平賦給result E=0; //將E恢復低電平 return result; }/*****************************************************函數功能:將模式設置指令或顯示地址寫入液晶模塊入口參數:dictate***************************************************/void WriteInstruction (unsigned char dictate){ while(BusyTest()==1); //如果忙就等待 RS=0; //根據規定,RS和R/W同時為低電平時,可以寫入指令 RW=0; E=0; //E置低電平(根據表8-6,寫指令時,E為高脈衝, // 就是讓E從0到1發生正跳變,所以應先置&34; _nop_(); _nop_(); //空操作兩個機器周期,給硬體反應時間 P0=dictate; //將數據送入P0口,即寫入指令或地址 _nop_(); _nop_(); _nop_(); _nop_(); //空操作四個機器周期,給硬體反應時間 E=1; //E置高電平 _nop_(); _nop_(); _nop_(); _nop_(); //空操作四個機器周期,給硬體反應時間 E=0; //當E由高電平跳變成低電平時,液晶模塊開始執行命令 }/*****************************************************函數功能:指定字符顯示的實際地址入口參數:x***************************************************/ void WriteAddress(unsigned char x) { WriteInstruction(x|0x80); //顯示位置的確定方法規定為&34; }/*****************************************************函數功能:將數據(字符的標準ASCII碼)寫入液晶模塊入口參數:y(為字符常量)***************************************************/ void WriteData(unsigned char y) { while(BusyTest()==1); RS=1; //RS為高電平,RW為低電平時,可以寫入數據 RW=0; E=0; //E置低電平(根據表8-6,寫指令時,E為高脈衝, // 就是讓E從0到1發生正跳變,所以應先置&34; P0=y; //將數據送入P0口,即將數據寫入液晶模塊 _nop_(); _nop_(); _nop_(); _nop_(); //空操作四個機器周期,給硬體反應時間 E=1; //E置高電平 _nop_(); _nop_(); _nop_(); _nop_(); //空操作四個機器周期,給硬體反應時間 E=0; //當E由高電平跳變成低電平時,液晶模塊開始執行命令 }/*****************************************************函數功能:對LCD的顯示模式進行初始化設置***************************************************/void LcdInitiate(void){ delaynms(15); //延時15ms,首次寫指令時應給LCD一段較長的反應時間 WriteInstruction(0x38); //顯示模式設置:16×2顯示,5×7點陣,8位數據接口 delaynms(5); //延時5ms ,給硬體一點反應時間 WriteInstruction(0x38); delaynms(5); //延時5ms ,給硬體一點反應時間 WriteInstruction(0x38); //連續三次,確保初始化成功 delaynms(5); //延時5ms ,給硬體一點反應時間 WriteInstruction(0x0c); //顯示模式設置:顯示開,無光標,光標不閃爍 delaynms(5); //延時5ms ,給硬體一點反應時間 WriteInstruction(0x06); //顯示模式設置:光標右移,字符不移 delaynms(5); //延時5ms ,給硬體一點反應時間 WriteInstruction(0x01); //清屏幕指令,將以前的顯示內容清除 delaynms(5); //延時5ms ,給硬體一點反應時間 } /******************************************************************************以下是與溫度有關的顯示設置 ******************************************************************************/ /*****************************************************函數功能:顯示沒有檢測到DS18B20***************************************************/ void display_error(void) { unsigned char i; WriteAddress(0x00); //寫顯示地址,將在第1行第1列開始顯示 i = 0; //從第一個字符開始顯示 while(Error[i] != &39;) //只要沒有寫到結束標誌,就繼續寫 { WriteData(Error[i]); //將字符常量寫入LCD i++; //指向下一個字符 delaynms(100); //延時100ms較長時間,以看清關於顯示的說明 } while(1) //進入死循環,等待查明原因 ;}/*****************************************************函數功能:顯示說明信息***************************************************/ void display_explain(void) { unsigned char i; WriteAddress(0x00); //寫顯示地址,將在第1行第1列開始顯示 i = 0; //從第一個字符開始顯示 while(Str[i] != &39;) //只要沒有寫到結束標誌,就繼續寫 { WriteData(Str[i]); //將字符常量寫入LCD i++; //指向下一個字符 delaynms(100); //延時100ms較長時間,以看清關於顯示的說明 } }/*****************************************************函數功能:顯示溫度符號***************************************************/ void display_symbol(void) { unsigned char i; WriteAddress(0x40); //寫顯示地址,將在第2行第1列開始顯示 i = 0; //從第一個字符開始顯示 while(Temp[i] != &39;) //只要沒有寫到結束標誌,就繼續寫 { WriteData(Temp[i]); //將字符常量寫入LCD i++; //指向下一個字符 delaynms(50); //延時1ms給硬體一點反應時間 } }/*****************************************************函數功能:顯示溫度的小數點***************************************************/ void display_dot(void){ WriteAddress(0x49); //寫顯示地址,將在第2行第10列開始顯示 WriteData(&39;); //將小數點的字符常量寫入LCD delaynms(50); //延時1ms給硬體一點反應時間 }/*****************************************************函數功能:顯示溫度的單位(Cent)***************************************************/ void display_cent(void){ unsigned char i; WriteAddress(0x4c); //寫顯示地址,將在第2行第13列開始顯示 i = 0; //從第一個字符開始顯示 while(Cent[i] != &39;) //只要沒有寫到結束標誌,就繼續寫 { WriteData(Cent[i]); //將字符常量寫入LCD i++; //指向下一個字符 delaynms(50); //延時1ms給硬體一點反應時間 } }/*****************************************************函數功能:顯示溫度的整數部分入口參數:x***************************************************/ void display_temp1(unsigned char x){ unsigned char j,k,l; //j,k,l分別儲存溫度的百位、十位和個位 j=x/100; //取百位 k=(x%100)/10; //取十位 l=x%10; //取個位 WriteAddress(0x46); //寫顯示地址,將在第2行第7列開始顯示 WriteData(digit[j]); //將百位數字的字符常量寫入LCD WriteData(digit[k]); //將十位數字的字符常量寫入LCD WriteData(digit[l]); //將個位數字的字符常量寫入LCD delaynms(50); //延時1ms給硬體一點反應時間 } /*****************************************************函數功能:顯示溫度的小數數部分入口參數:x***************************************************/ void display_temp2(unsigned char x){ WriteAddress(0x4a); //寫顯示地址,將在第2行第11列開始顯示 WriteData(digit[x]); //將小數部分的第一位數字字符常量寫入LCD delaynms(50); //延時1ms給硬體一點反應時間}
//DS18B20.hsbit DQ=P3^3;/*****************************************************函數功能:將DS18B20傳感器初始化,讀取應答信號出口參數:flag ***************************************************/bit Init_DS18B20(void);/*****************************************************函數功能:從DS18B20讀取一個字節數據出口參數:dat***************************************************/ unsigned char ReadOneChar(void);/*****************************************************函數功能:向DS18B20寫入一個字節數據入口參數:dat***************************************************/ WriteOneChar(unsigned char dat);/*****************************************************函數功能:做好讀溫度的準備***************************************************/ void ReadyReadTemp(void);
//DS18B20.cinclude<intrins.h> //包含_nop_()函數定義的頭文件34;LCD1602.h&include&34; //34;Delay.h&34;人為&include<reg51.h> //包含單片機寄存器的頭文件include&34; //34;DS18B20.h&include&34; //unsigned char time; //設置全局變量,專門用於嚴格延時/*****************************************************函數功能:主函數***************************************************/ void main(void) { unsigned char TL; //儲存暫存器的溫度低位 unsigned char TH; //儲存暫存器的溫度高位 unsigned char TN; //儲存溫度的整數部分 unsigned int TD; //儲存溫度的小數部分 LcdInitiate(); //將液晶初始化 delaynms(5); //延時5ms給硬體一點反應時間 if(Init_DS18B20()==1) display_error(); display_explain(); display_symbol(); //顯示溫度說明 display_dot(); //顯示溫度的小數點 display_cent(); //顯示溫度的單位 while(1) //不斷檢測並顯示溫度 { ReadyReadTemp(); //讀溫度準備 TL=ReadOneChar(); //先讀的是溫度值低位 TH=ReadOneChar(); //接著讀的是溫度值高位 TN=TH*16+TL/16; //實際溫度值=(TH*256+TL)/16,即:TH*16+TL/16 //這樣得出的是溫度的整數部分,小數部分被丟棄了 TD=(TL%16)*10/16; //計算溫度的小數部分,將餘數乘以10再除以16取整, //這樣得到的是溫度小數部分的第一位數字(保留1位小數) display_temp1(TN); //顯示溫度的整數部分 display_temp2(TD); //顯示溫度的小數部分 delaynms(10); } }
根據前面的說明,按照步驟建立起模塊化程序結構。編譯後,按照上一實例的方法進行仿真,觀察非模塊化方法編寫的程序,以及用模塊化方法編寫的程序在運行結果上是完全一樣的。
通過本實例,我們學習了如何將功能眾多,程序量大的單片機程序分成不同的模塊,從而使單片機程序看起來結構清晰,可讀性強,可移植性強。