前言
此文檔主要是針對有一定C/C++編程基礎,並打算用Keil從事C51開發的開發人員。C51涉及的知識比較多,但是入門基本的開發,還是容易的。
C51簡介
1. C51概念
C51繼承於C語言,主要運行於51內核的單片機平臺。單片機,單片微型計算機器(SingleChipMicrocomputer)的簡稱,又稱微控制單元(MicroControllerUnit,MCU)。MCU由CPU、RAM、ROM、I/O、中斷系統、晶振等組成。51內核的單片機都是8位的,因爲cpu訪問RAM、ROM以及I/O都是8Bit操作的。基於51內核的單片機有很多種,如8051、80515等。
存儲器:包括片內存儲器、片外存儲器。片內存儲器一般包括256字節的片內RAM,和8K字節的程序存儲器ROM。片外儲存器XRAM可達64K字節。
256字節的片內RAM:
名稱 | 地址範圍 | 備註 |
工作寄存器組0 | 0x00—0x07 | 此組用作默認的寄存器 |
工作寄存器組1 | 0x08—0x0F | 此組常用作中斷函數寄存器 |
工作寄存器組2 | 0x10—0x17 | 工作寄存器若未使用,可用作堆棧計算 |
工作寄存器組3 | 0x18—0x1F | |
位尋址區 | 0x20—0x2F | 此區域可位尋址(bit) |
堆棧區 | 0x30—0x7F | 此區域可用於堆棧計算 |
0x80—0xFF | 地址爲8倍數SFR可位尋址 |
引腳:51單片機的管理一般主要包括電源、時鐘、控制和I/O腳。
l 電源:VCC,接電源;VSS接地。
l 時鐘:接晶振。
l 控制線:ALE、EA等控制腳。
l I/O線:4個8位並行I/O端口,分別爲P0、P1、P2、P3口,每個口8個引腳共32引腳。
2. C51語法
1. 數據類型
基本數據類型 | 名稱 | 長度 | 取值滿園 |
unsigned char | 無符號類型 | 1byte | 0~255 |
signed char | 有符號類型 | 1byte | -128~127 |
unsigned int | 無符號類型 | 2byte | 0~65536 |
signed int | 有符號類型 | 2byte | -32768~32767 |
unsigned long | 無符號類型 | 4byte | 0~4294967295 |
signed long | 有符號類型 | 4byte | -2147483648~2147483647 |
float | 浮點類型 | 4byte | +-1.75494E-38~+-3.402834E+38 |
bit | 位類型 | 1bit | 0/1 |
Sfr | 8位特殊功能寄存器 | 1byte | 0~255 |
sfr16 | 16位特殊功能寄存器 | 2byte | 0~65536 |
sbit | 可尋址位類型 | 1bit | 0/1 |
l 定義:臨時變量只能定義在代碼塊的最開始,即”{“後面,且變量定義前不能有非定義語句。因爲Keil能夠在編譯時就比較好確定棧內存,如果超過限定內存會報編譯錯誤。
l 申明:如果外部文件需要使用全局變量,使用extern來限定。
l 初始化:全局變量和局部變量,如果沒有指定初始化值,默認爲0。
l bit:位類型,bit指定的變量可以直接按拉尋址,所以需要特殊的內存匹配,即位尋址區(0x20—0x2F),可申明最多128個bit類型的變量。
l sfr:特殊功能寄存器(special function register),只能定義在函數體外,且必須指定特殊功能寄存器地址。
l sfr16:16位的特殊功能寄存器,可以完成一些需要16位的特殊功能。
l sbit:特殊功能寄存器位變量,是針對sfr的。但是因爲位尋址效率的問題,所以只有地址爲8倍數的sfr纔可以被位尋址。sbit也只能定義在函數體外。用法如下:
sfr IE = 0xA8;
sbit EXO = IE^0; // 0xA8的第1位,相當於IE口的第0管腳
sbit EX7 = IE^7; // 0xA8的第8位,相當於IE口的第7管腳
sbit EX01 = 0xA8;// 0xA8的第1位,相當於IE口的第0管腳
sbit EX71 = 0xAF;// 0xA8的第8位,相當於IE口的第7管腳
sbit EXO2 =0xA8^0; // 0xA8的第1位,相當於IE口的第0管腳
sbit EX72 =0xA8^7; // 0xA8的第8位,相當於IE口的第7管腳
2. 儲存器類型和指針
存儲器類型 | 描述 |
data | 直接尋址的片內RAM低128Byte,訪問速度快 |
badta | 片內RAM的可位尋址區(20H~2FH),允許字節和位混合訪問 |
idata | 間接尋址訪問的片內RAM,允許訪問全部片內RAM |
pdada | 用Rn間接訪問的片外低256Byte |
xdata | 用DPTR間接訪問片外RAM,允許訪問全部64KB的片外RAM |
code | 程序存儲器64KB ROM |
char* pStr; // 指針佔3個字節,第1字節標識存儲器類型,第2字節爲指針存儲地址的高字節,第3字節爲指針存儲地址的低字節。(未註明存儲器類型即爲默認存儲器類型,由Keil的編譯環境控制,且默認的存儲器類型是修飾指針的)
char* idatapStr1; // 指針佔3個字節,此處指定指針值的存儲器類型爲idata。
char* xdatapStr2; // 指針佔用3個字節
char* codepStr3; // 指針佔用3個字節,code的作用類似於const
char idata *pStr4; // 指針佔用1個字節,idata是修飾pStr4指向的內容。idata表示的片內RAM最多隻256字節,所以pStr4也只需要1個字節即可表示。
char xdata *pStr5; // 指針佔用2個字節,xdata修飾的是pStr5指向的內容,而xdata表示的片外內存最多64K,所以2個字節足夠。
char code *pStr6; // 指針佔用2個字節
char idata *xdata pStr7; // 指針佔用1個字節,因爲pStr7指向的內容是idata
char xdata *idata pStr8; // 指針佔用2個字節,因爲pStr8指向的內容是xdata
綜上所述,指針的大小分兩類,未指明指向內容的存儲器類型,此類指針大小爲3字節。
指明瞭指針指向內容的存儲器類型的指針大小爲內容存儲器的大小。
注:因爲指針的大小及類型不是固件的,所以兩個類型不同的指針不能賦值。
3. volatile
volatile是用來限定變量告訴編譯器,此變量可能會被特殊的情況修改,所以對此變量的操作不要做優化,直接從內存上存取。Keil編譯器爲了執行效率,都是將變量存放到寄存器上再來操作的,這就導致第二次取變量值時,可能是直接從寄存器中取值,而不是從內存上讀取。那麼有一些特殊操作可能發生在二次取值過程中,導致第二次取值沒有更新到。
char xdata g_value1;
char volatile xdatag_value2;
int main()
{
char cArg = g_value1; // 1
900000 MOV DPTR,#C_STARTUP(0x0000)
E0 MOVX A,@DPTR
FE MOV R6,A
if (g_value1 != cArg) // 2
6E XRL A,R6 // g_value1此刻是直接從寄存器中取值
cArg = 6;
cArg =g_value2;
900001 MOV DPTR,#g_value2(0x0001)
E0 MOVX A,@DPTR
FE MOV R6,A
if (g_value2 != cArg)
E0 MOVX A,@DPTR // g_value2此刻是從內存中取值
6E XRL A,R6
cArg = 5;
return 0;
}
上面的代碼很好的體現了volatile的特殊作用。如果在1和2之間有一些特殊情況修改了變量g_value1將導致錯誤。所以這樣的一些特殊情況涉及到的變量,必須用volatile修飾,具體如下:
l 彙編代碼修改的變量(一般情況下彙編代碼不要修改外部變量)
l 硬件寄存器修改的變量(即指硬件寄存器值本身)
l 中斷函數修改的全局變量
(注:VS中的全局變量都是直接從內存中存取,不需要volatile修飾)
4. 中斷函數
中斷,顧名思義就是中斷當前代碼的執行流。中斷函數即中斷之後響應的函數。中斷根據中斷源類型不同,主要分爲:外部中斷,定時器中斷、串口中斷等。中斷的實現原理是,CPU執行每條代碼前,都會去檢查中斷標誌位,如果中斷標誌位有信號,即響應中斷函數。函數的執行是需要堆棧和寄存器的,那麼中斷函數的執行如何不破壞當前函數的堆棧和寄存器呢?C51有一個特殊的實現方式,即提供多套工作寄存器器,且堆棧和當前代碼共用。這樣當中斷函數執行時,原有代碼的運行環境就得以保存,中斷函數結束後,就可以恢復當前代碼執行流程。
中斷函數的格式如下:void ExternINT0(void) interrupt 0using1
interrupt 0表示0號中斷。using 1表示使用工作寄存器組1,如果不指定則使用默認工作寄存器組0,可能會與通用函數的工作寄存器衝突。另外中斷函數中的使用的全局變量如果和主流存在同時寫操作,那麼在主流寫時,需要關閉中斷防止寫衝突,等寫完成之後再開啓中斷。
void ExternINT0(void) interrupt 0using1
{
g_value1++;
}
int main()
{
EA = 1; // 開啓所有中斷
EX0 = 1; //開啓外部中斷0,對應0號中斷
IT0 = 1; // 外部中斷0觸發方式
IE0 = 1; // 手動觸發中斷0,中斷函數執行後,此標誌變爲0
return 0;
}
5. 絕對地址
因爲C51是直接與硬件交互,爲了提高代碼的執行效率,硬件的一些特殊功能可能會直接訪問指定地址的變量和執行指定地址的函數。這些指定地址是固定不變的,是絕對地址。
l 變量絕對地址定位
char idata myVar_at_0x40; // _at_關鍵字,指定idata區域的絕對地址0x40;編譯鏈接之後在MAP文件中可以找到如下信息00000040H IDATA BYTE myVar,即表明myVar在idata區的0x40處
char xdata myVal _at_ 0x400; // 指定xdata區域的0x400;
struct link list idata _at_ 0x40; // list at idata 0x40
char xdata text[256] _at_ 0xE000; // array at xdata 0xE000
在Options for target->LX51 Locate->User Segments編譯框中添加:
?CO?text(x:0xE000)
l 函數絕對地址定位
在Options for target->LX51 Locate->User Segments編譯框中添加:
?PR?MyTest?MAIN(0x4000)
PR表示program Executable program code
MyTest表示函數名
MAIN表示所在文件名
0x4000表示函數絕對地址
l 絕對地址的訪問
1. 可以通過彙編直接訪問絕對地址變量及函數
2. 可以通過將絕對地址賦值給指針訪問變量及函數
6. 可重入(reentrant)
一個函數如果被主流程調用,另外又被中斷函數調用,那麼這個函數即重入了。這樣的函數可能存在問題。如下列,主流程執行到2,然後觸發中斷函數了,並同樣執行了下列函數,那麼寄存器Exam可能已經被修改。即使此時退回到主流執行2,結果可能是錯誤的。這個時候Keil引入了reentrant,通用模擬堆棧用來避免此類問題。因爲是模擬的,效率低,非必須不要使用。
unsigned int Test(int nVal)reentrant
{
unsigned int nRet = 0;
Exam = nRet; // 1
nRet =Square_Exam( ); // 2
return nRet;
}
7. 看門狗
看門狗(Watch Dog),其作用是監控單片機程序的運行,如果程序跑飛或者進入死循環等意外狀態,看門狗則將單片機進行復位,讓程序重新開始運行。看門狗的實現方式,有硬件方式和軟件方式。硬件方式是通過外部芯片來監控,並由外部芯片來控制復位。軟件方式,即單片機本身通過定時器來對程序運行狀態進行定時監控,如果在發現狀態則將單片機復位。因爲看門狗主要是通過定時器實現,所以看門狗定時器(Watch Dog Timer,WDT)一般會作爲一個獨立的組成部分。
看門狗(WDT)設計原理是,一個定時器模塊,一個輸入端(叫喂狗,kicking the dog or servicethe dog),還有一個控制單片機的RST端。每隔一段時間必須喂狗將WDT清0。如果指定時間沒有檢測到喂狗,看門狗定時器即會超時,然後重置RST端讓單片機復位。
89C51默認是不帶看門狗的。89S51是在89C51的基礎上進行的擴展,添加了看門狗功能,此處用89S51爲例說明看門狗具體用法。以下資料來自89C51的Datasheets。
WDT是爲了解決CPU程序運行時可能進入混亂或死循環而設置,它由一個14Bit計數器和看門狗復位SFR(WDTRST)構成。外部復位時,WDT默認爲關閉狀態,要打開WDT,用戶必須按順序將0IEH和OEIH寫到WDTRST寄存器(SFR地址爲OA6H),當啓動了WDT,它會隨晶體振盪器在每個機器週期計數,除硬件復位或WDT溢出復位外沒有其它方法關閉WDT,當WDT溢出,將使RST引腳輸出高電平的復位脈衝。
89S51的看門狗計數器是固定的,在指定的時間內必須喂狗,否則會溢出復位。其他有些看門狗能夠設置溢出時間,每次溢出時重新設置看門狗計數或定時。看門狗設計的重點在喂狗,如果喂狗設計不合理可能導致正常代碼也觸發WDT溢出復位。
#include <AT89X51.h>
sfr WDTRST = 0xA6;
int main()
{
WDTRST=0x1E;
WDTRST=0xE1; // 初始化看門狗
while (1)
{
WDTRST=0x1E;
WDTRST=0xE1; //喂狗,如果不喂狗,則會Reset
}
return 0;
}
8. 其他
l C51不支持引用。
l static修飾函數和變量,讓函數和變量只能爲本文件所使用,避免命名衝突。
l C51支持位域,位域的效率比bit低。位域立即數寫相對消耗資源少,如果是變量賦值,會涉及到保存恢復累加器A,會消耗非常多的資源且效率低。
l C51的變量會有默認初始值0,但是顯式初始值會使代碼更直觀清晰。
Keil的使用
1. Kei的基本功能
Keil μVision2是一個集成開發環境(IDE,支持編輯、編譯、鏈接、調試)。Keil主要包括以下幾大模塊:
l Cx51:C文件編譯器,負責將C文編譯成可重定位的目標文件。
l Ax51:彙編文件編譯器,負責將彙編文件編譯成可重定位的目標文件。
l BL51:鏈接器/定位器,組成可重定位的目標文件,生成絕對目標文件。
l LX51:擴展鏈接器/定位器,優化了BL51的功能,可以生成更小的目標文件。
l LIB51:庫管理器,從目標文件生成鏈接器可使用的庫文件。
l OH51:將目標文件轉換成Inter Hex文件。
2. Keil使用C51的基本設置
1. Device
Device主要是用來設計代碼運行單片機的內核的,這裏的設置主要影響兩個地方,一是影響默認包含的寄存器頭文件,二是影響仿真調試。像1581因爲是選擇自定義的寄存器頭文件,所以無論是選擇80515還是89C51,其實都不影響的。此處的關鍵是要勾選UseExtended Liner(LX51)insteated of BL51,因爲能夠提升鏈接性,降低鏈接目標文件大小。
2. Target
l 頻率在此設置主要是針對通用性單片機的,並且用來仿真用的。具體的單片機需要使用中具體設置相關寄存器。
l Memeoy Model:主要是設置默認變量的內存類型,如果選擇Small,表明棧及默認(未顯式指明內存類型)的變量都是idata類型。如果選擇Compact,則表明是pdata類型。如果選擇Large,則表明是xdata類型。一般默認選擇Small。
l Off-chip Code Momory是用來指定代碼段的位置,如果不設置由系統自動分配。
l Off-chip Xdata Memroy是用來指定xdata類型變量的代碼位置,此處指定爲0x6000,大小0x0C00。
3. Output
4. Listing
5. User
6. C51
7. A51
8. LX51 Locate
LX51 Locate,定位器,主要是變量及函數地址定位的設置。
9. LX51 Misc
LX51 Misc,鏈接器有關雜項的設置。如果少量函數定位地址,建議在Locate選項裏直接設置。如果不通過文件設置鏈接器,則可以在Control Misc中設置REMOVEUNUSED,這樣未被使用到的函數便不會被鏈接,但是會被編譯檢測語法。這樣可以少用一些宏來限制鏈接函數鏈接,提升代碼可讀性。
10. Manage Project Item
11. Options For File
3. Keil調試C51
l Keil自帶的仿真器還是非常強大的,可以用來調試通用的C51代碼,對於學習C51瞭解C51的運行機制,幫助非常大。
l 調試時查看彙編代碼,能夠讓我們考慮如何更好的優化代碼,以及學習彙編代碼。
l Peripherals提供的仿真外圍器,能夠設置查看中斷器、定時器、看門狗、I/O口等。
l Registers、Watch窗口能夠查看寄存器、變量的實時值。
l Memroy區間,在Address框後面輸入“字母:數據”即可以查看相應區域的內存值。其中字母C、D、I、X,分別代表代碼存儲空間、直接尋址的片內存儲空間、間接尋址的片內存儲空間、擴展的外部RAM單元值、鍵入C:0即可顯示從0開始的ROM單元中的值,即查看程序的二進制代碼。
4. Keil生成文件介紹
l START.A51,啓動文件,完成單片機的堆棧的初始化。如果想代碼復位時,變量不進行默認初始化,則需要修改此文件。默認一般不修改。如果不使用此文件,編譯器會用默認的啓動文件。
l .uvproj,Keil4的項目啓動文件。
l .LST,記錄對應文件在編譯器的行號,以及最後統計佔用的代碼空間。
l .OBJ,編譯後生成的目標,供鏈接器使用。
l ._i,記錄文件編譯的優化級別,當前項目宏,以及生成文件路徑等。
l .MAP,記錄內存映射情況,例如一些指定絕對函數的函數變量均可以在此處查看。另外,有時如果要查看還有哪些絕對地址空餘,也可以查看此文件。最後生成總結Program Size: data=15.0 xdata=2 const=0 code=206,總結各區間的大小,code是最終代碼的大小,而不是單指代碼段大小。
l .hex,Keil默認生成的是hex文件,是一種Inter主導的在能夠在單片機上運行的文件格式,其內容是16進制,兩個字符表示1個BYTE數。所以hex文件會比二進制bin文件大近一倍。Hex文件描述性更強,有比Bin更強大的描述性。
l .bin,bin文件Keil默認是不生成的,一般是通過一些小工具如Hex2Bin.exe等按照一定的規則將hex文件轉換成bin文件,這樣能夠節省大量空間。
5. Keil生成使用Lib文件以及C51的模塊化
l Keil建立Lib工程非常簡單,添加相應的文件,然後在Output窗口,勾選上“Create Library”即可。
l Keil使用Lib文件,同樣簡單,即在目標工程中,添加要使用的Lib文件,然後在要使用的文件裏添加相應的頭文件即可。
l 模塊化是所有編程語言最重要的概念之一,模塊化是提高代碼複用,降低代碼耦合的最重要手段之一。因爲C51是面向過程的開發語言,所以缺乏很多高格語言的模塊化手段。但是我們依然可以通過現有的條件,儘量做到模塊化。模塊化從大到小,依次是工程->文件->函數。
n 函數是最小的模塊化模塊,所以函數應當儘可能只做一件事,做到儘可能的獨立。
n 文件是中等模塊,一個文件應該是一類功能函數的集合。甚至可以借鑑面嚮對象語言的一些優點,即一個文件也可以只在相應的h文件中暴露部分函數接口。並且一些函數可以用static限定爲當前文件作用域,防止其他文件使用。因爲沒有其他高級語言那樣有類名或作用域名對接口進行修飾,表明接口是某個模塊下的。所以可以用縮寫的文件名作爲函數前綴用以標記函數屬於某個模塊。
n 工程模塊,當一個項目較大時,可以建立多個工程,其中一個執行文件工程和多個Lib庫工程。建立多個工程,能夠有效降低代碼的耦合性。
附件
1. SFR的講解
1. /* BYTE Register */ SFR(SpecialFunction Register),範圍0x80~0xFF.
2. sfr P0 = 0x80; // 對應P0.0~P0.7,通常用來控制8位Flash數據,RAM地址爲0x80
3. sfr P1 = 0x90; // 對應P1.0~P1.7,暫未使用,RAM地址爲0x90
4. sfr P2 = 0xA0; // 對應P2.0~P2.7,CE0~CE4,以及串口相關
5. sfr P3 = 0xB0; // 對應P3.0~P3.7,控制ALE,CLE,DQS等
6. sfr PSW = 0xD0; // Program Status Word,詳見/* PSW */
7. sfr ACC = 0xE0; // 累加器A,多用來作計算中間值
8. sfr B = 0xF0; // 累加器B
9.
10.sfr SP = 0x81; // Stack Pointer,存放棧頂指針
11.sfr DPL = 0x82; // DPTR低位
12.sfr DPH = 0x83; // DPTR高位
13.
14.sfr PCON = 0x87; // Power Control Register,,默認不修改
15.sfr TCON = 0x88; // Timer Control Register,詳見/* TCON */
16.sfr TMOD = 0x89; // Timer/Counter Mode Control Register,詳見/* TCON */
17.sfr TL0 = 0x8A; // Timer Low 0
18.sfr TL1 = 0x8B; // Timer Low 1
19.sfr TH0 = 0x8C; // Timer High 0
20.sfr TH1 = 0x8D; // Timer High 1
21.sfr CKCON = 0x8E; // Clock Contrl,值0爲最快模式
22.
23.sfr EIF = 0x91; // Extern Interrupt Flag,默認不修改
24.sfr WTST = 0x92; // Wait Status,控制等待狀態週期,值0爲最快模式
25.sfr DPX0 = 0x93; // 中斷優先0,默認不修改
26.sfr SCON = 0x98; // Serial Control Register,詳見/* SCON */
27.
28.sfr IE = 0xA8; // Interrupt Enable Regiester,詳見/* IE */
29.sfr IP = 0xB8; // Interrupt Priority,0表示所有中斷同一級別
30.
31.sfr EIE = 0xE8; // Error Interrupt Enable,默認不修改
32.
33.sfr MXAX = 0xEA; // 默認不修改
34.
35.sfr EIP = 0xF8; // 指令指針寄存器
36.
37.// 只有地址被8整除的SFR纔可以位尋址
38./* BIT Register */
39./* PSW */
40.sbit CY = 0xD7; // Carry,進位標誌位
41.sbit AC = 0xD6; // Assistant Carry,輔助進位標誌位
42.sbit F0 = 0xD5; // 軟件標誌
43.sbit RS1 = 0xD4; // 工作寄存器組選擇1
44.sbit RS0 = 0xD3; // 工作寄存器組選擇0
45.sbit OV = 0xD2; // 溢出標誌
46.sbit P = 0xD0; // 代碼執行奇偶標誌位P
47.
48./* TCON */
49.sbit TF1 = 0x8F; // 定時器T1溢出標誌位,默認不修改
50.sbit TR1 = 0x8E; // 功能同上
51.sbit TF0 = 0x8D; // 定時器T0溢出標誌位,默認不修改
52.sbit TR0 = 0x8C; // 功能同上
53.sbit IE1 = 0x8B; // 外部中斷1請求標誌位,默認不修改
54.sbit IT1 = 0x8A; // 外部中斷1觸發方式控制位,默認不修改
55.sbit IE0 = 0x89; // 外部中斷0請求標誌位,默認0復位
56.sbit IT0 = 0x88; // 外部中斷0觸發方式控制位,默認0復位
57.
58./* SCON */ // SG1581未提供此功能,可以不用關注
59.sbit SM0 = 0x9F;
60.sbit SM1 = 0x9E;
61.sbit SM20 = 0x9D;
62.sbit REN0 = 0x9C;
63.sbit TB80 = 0x9B;
64.sbit RB80 = 0x9A;
65.sbit TI0 = 0x99;
66.sbit RI0 = 0x98;
67.
68./* IE */
69.sbit EAL = 0xAF; // Enable All Interrupt
70.sbit WDT = 0xAE; // 未使用
71.sbit ET2 = 0xAD; // Enable Timer 2 interrupt
72.sbit ES0 = 0xAC; // Enable UART1 interrupt,通用異步收發傳輸器(Universal Asynchronous Receiver/Transmitter),
73.sbit ET1 = 0xAB; // Enable Timer 1 interrupt
74.sbit EX1 = 0xAA; // Enable INT1 interrupt
75.sbit ET0 = 0xA9; // Enable Timer 0 interrupt
76.sbit EX0 = 0xA8; // Enable INT0 interrupt
77.// 1. 初始化MCU時,防止中斷打斷初始化,所以關閉中斷
78.// 2. 傳輸數據時,防止中斷打斷數據傳輸,所以關閉中斷
79.// 3. 切換Code時,防止中斷打斷初始化,所以關閉中斷