PIC 單片機 C 語言編程簡介(2)
首先必須強調(diào),在用
之時無需知道所定義的變量具體被放在哪個地址(除了 bank 必須聲明)。
真正需要絕對定位的只是單片機中的那些特殊功能寄存器,而這些寄存器的地址定位在
PICC 編譯環(huán)境所提供的頭文件中已經(jīng)實現(xiàn),無需用戶操心。編程員所要了解的也就是 PICC
是如何定義這些特殊功能寄存器和其中的相關控制位的名稱。好在 PICC 的定義標準基本上
按照芯片的數(shù)據(jù)手冊中的名稱描述進行,這樣就秉承了變量命名的一貫性。一個變量絕對定
位的例子如下:
unsigned char tmpData @ 0x20; //tmpData 定位在地址 0x20
千萬注意,PICC 對絕對定位的變量不保留地址空間。換句話說,上面變量 tmpData 的
地址是 0x20,但最后 0x20 處完全有可能又被分配給了其它變量使用,這樣就發(fā)生了地址沖
突。因此針對變量的絕對定位要特別小心。從筆者的應用經(jīng)驗看,在一般的程序設計中用戶
自定義的變量實在是沒有絕對定位的必要。
如果需要,位變量也可以絕對定位。但必須遵循上面介紹的位變量編址的方式。如果一
個普通變量已經(jīng)被絕對定位,那么此變量中的每個數(shù)據(jù)位就可以用下面的計算方式實現(xiàn)位變
量指派:
unsigned char tmpData @ 0x20; //tmpData 定位在地址 0x20
bit tmpBit0 @ tmpData*8+0;
bit tmpBit1 @ tmpData*8+1;
bit tmpBit2 @ tmpData*8+2;
如果 tmpData 事先沒有被絕對定位,那就不能用上面的位變量定位方式。
11.5.8
&O1540;
如果在一個 C
文件中必須將這些變量聲明成“extern”外部類型。例如程序文件 code1.c 中有如下定義:
bank1 unsigned char var1, var2;
//定義了 bank1 中的兩個變量
在另外一個程序文件 code2.c 中要對上面定義的變量進行操作,則必須在程序的開頭定義:
extern bank1 unsigned char var1, var2;
&O1540;
PICC
“volatile”。顧名思義,它說明了一個變量的值是會隨機變化的,即使程序沒有刻意對它進
行任何賦值操作。在單片機中,作為輸入的 IO 端口其內(nèi)容將是隨意變化的;在中斷內(nèi)被修
改的變量相對主程序流程來講也是隨意變化的;很多特殊功能寄存器的值也將隨著指令的運
行而動態(tài)改變。所有這種類型的變量必須將它們明確定義成“volatile”類型,例如:
volatile unsigned char STATUS @ 0x03;
volatile bit commFlag;
“volatile”類型定義在單片機的
器的優(yōu)化處理器這些變量是實實在在存在的,在優(yōu)化過程中不能無故消除。假定你的程序定
義了一個變量并對其作了一次賦值,但隨后就再也沒有對其進行任何讀寫操作,如果是非
volatile
情形是在使用某一個變量進行連續(xù)的運算操作時,這個變量的值將在第一次操作時被復制到
中間臨時變量中,如果它是非 volatile 型變量,則緊接其后的其它操作將有可能直接從臨時
變量中取數(shù)以提高運行效率,顯然這樣做后對于那些隨機變化的參數(shù)就會出問題。只要將其
定義成 volatile 類型后,編譯后的代碼就可以保證每次操作時直接從變量地址處取數(shù)。
&O1540;
如果變量定義前冠以“const”類型修飾,那么所有這些變量就成為常數(shù),程序運行過
程中不能對其修改。除了位變量,其它所有基本類型的變量或高級組合變量都將被存放在程
序空間(ROM 區(qū))以節(jié)約數(shù)據(jù)存儲空間。顯然,被定義在 ROM 區(qū)的變量是不能再在程序
中對其進行賦值修改的,這也是“const”的本來意義。實際上這些數(shù)據(jù)最終都將以“retlw”
的指令形式存放在程序空間,但 PICC 會自動編譯生成相關的附加代碼從程序空間讀取這些
常數(shù),編程員無需太多操心。例如:
const unsigned char name[]=”This is a demo”;
如果定義了
不能對其賦值修改。本來,不能修改的位變量沒有什么太多的實際意義,相信大家在實際編
程時不會大量用到。
&O1540;
按照標準
量全部清零。PICC 會在最后生成的機器碼中加入一小段初始化代碼來實現(xiàn)這一變量清零操
作,且這一操作將在 main 函數(shù)被調(diào)用之前執(zhí)行。問題是作為一個單片機的控制系統(tǒng)有很多
變量是不允許在程序復位后被清零的。為了達到這一目的,PICC
詞以聲明此類變量無需在復位時自動清零,編程員應該自己決定程序中的那些變量是必須聲
明成“persisten”類型,而且須自己判斷什么時候需要對其進行初始化賦值。例如:
persistent unsigned char hour,minute,second;
經(jīng)常用到的是如果程序經(jīng)上電復位后開始運行,那么需要將 persistent 型的變量初始化,
如果是其它形式的復位,例如看門狗引發(fā)的復位,則無需對 persistent 型變量作任何修改。
PIC
形。
11.5.9
PICC 中指針的基本概念和標準 C 語法沒有太多的差別。但是在 PIC 單片機這一特定的
架構上,指針的定義方式還是有幾點需要特別注意。
&O1540;
如果是匯編語言編程,實現(xiàn)指針尋址的方法肯定就是用 FSR 寄存器,PICC 也不例外。
為了生成高效的代碼,PICC 在編譯 C 原程序時將指向 RAM 的指針操作最終用 FSR 來實現(xiàn)
間接尋址。這樣就勢必產(chǎn)生一個問題:FSR 能夠直接連續(xù)尋址的范圍是 256 字節(jié)(bank0/1
或 bank2/3),要覆蓋最大 512 字節(jié)的內(nèi)部數(shù)據(jù)存儲空間,又該如何讓定義指針?PICC 還是
將這一問題留給編程員自己解決:在定義指針時必須明確指定該指針所適用的尋址區(qū)域,例
如:
unsigned char *ptr0; //①定義覆蓋 bank0/1 的指針
bank2 unsigned char *ptr1;
bank3 unsigned char *ptr2;
上面定義了三個指針變量,其中①指針沒有任何 bank 限定,缺省就是指向 bank0 和 bank1;
②和③一個指明了 bank2,另一個指明了 bank3,但實際上兩者是一樣的,因為一個指針可
以同時覆蓋兩個 bank 的存儲區(qū)域。另外,上面三個指針變量自身都存放在 bank0 中。我們
將在稍后介紹如何在其它 bank 中存放指針變量。
既然定義的指針有明確的 bank 適用區(qū)域,在對指針變量賦值時就必須實現(xiàn)類型匹配,
下面的指針賦值將產(chǎn)生一個致命錯誤:
unsigned char *ptr0;
bank2 unsigned char buff[8];
程序語句:
//定義指向 bank0/1 的指針
//定義 bank2 中的一個緩沖區(qū)
ptr0 = buff;
若出現(xiàn)此類錯誤的指針操作,PICC 在最后連接時會告知類似于下面的信息:
Fixup overflow in expression_r(...)
同樣的道理,若函數(shù)調(diào)用時用了指針作為傳遞參數(shù),也必須注意 bank 作用域的匹配,
而這點往往容易被忽視。假定有下面的函數(shù)實現(xiàn)發(fā)送一個字符串的功能:
void SendMessage(unsigned char *);
那么被發(fā)送的字符串必須位于 bank0 或 bank1 中。如果你還要發(fā)送位于 bank2 或 bank3 內(nèi)的
字符串,必須再另外單獨寫一個函數(shù):
void SendMessage_2(bank2 unsigned char *);
這兩個函數(shù)從內(nèi)部代碼的實現(xiàn)來看可以一模一樣,但傳遞的參數(shù)類型不同。
按筆者的應用經(jīng)驗體會,如果你看到了“Fixup overflow”的錯誤指示,幾乎可以肯定
是指針類型不匹配的賦值所至。請重點檢查程序中有關指針的操作。
&O1540;
如果一組變量是已經(jīng)被定義在 ROM 區(qū)的常數(shù),那么指向它的指針可以這樣定義:
const unsigned char company[]=”Microchip”;
const unsigned char *romPtr;
程序中可以對上面的指針變量賦值和實現(xiàn)取數(shù)操作:
romPtr
data = *romPtr++;
//定義 ROM 中的常數(shù)
//定義指向 ROM 的指針
反過來,下面的操作將是一個錯誤,因為該指針指向的是常數(shù)型變量,不能賦值。
*romPtr = data; //往指針指向的地址寫一個數(shù)
&O1540;
單片機編程時函數(shù)指針的應用相對較少,但作為標準 C 語法的一部分,PICC 同樣支持
函數(shù)指針調(diào)用。如果你對編譯原理有一定的了解,就應該明白在
構上實現(xiàn)函數(shù)指針調(diào)用的效率是不高的:PICC 將在 RAM 中建立一個調(diào)用返回表,真正的
調(diào)用和返回過程是靠直接修改 PC 指針來實現(xiàn)的。因此,除非特殊算法的需要,建議大家盡
量不要使用函數(shù)指針。
&O1540;
前面介紹的指針定義都是最基本的形式。和普通變量一樣,指針定義也可以在前面加上
特殊類型的修飾關鍵詞,例如“persistent”、“volatile”等??紤]指針本身還要限定其作用域,
因此 PICC 中的指針定義初看起來顯得有點復雜,但只要了解各部分的具體含義,理解一個
指針的實際用圖就變得很直接。
㈠ bank 修飾詞的位置含義
前面介紹的一些指針有的作用于 bank0/1,有的作用于 bank2/3,但它們本身的存放位置
全部在 bank0。顯然,在一個程序設計中指針變量將有可能被定位在任何可用的地址空間,
這時,bank 修飾詞出現(xiàn)的位置就是一個關鍵,看下面的例子:
//定義指向 bank0/1 的指針,指針變量為于 bank0 中
unsigned char *ptr0;
//定義指向 bank2/3 的指針,指針變量為于 bank0 中
bank2 unsigned char *ptr0;
//定義指向 bank2/3 的指針,指針變量為于 bank1 中
bank2 unsigned char * bank1 ptr0;
從中可以看出規(guī)律:前面的 bank 修飾詞指明了此指針的作用域;后面的 bank 修飾詞定義了
此指針變量自身的存放位置。只要掌握了這一法則,你就可以定義任何作用域的指針且可以
將指針變量放于任何 bank 中。
㈡ volatile、persistent 和 const 修飾詞的位置含義
如果能理解上面介紹的 bank 修飾詞的位置含義,實際上 volatile、persistent 和 const 這
些關鍵詞出現(xiàn)在前后不同位置上的含義規(guī)律是和 bank 一詞相一致的。例如:
//定義指向 bank0/1 易變型字符變量的指針,指針變量位于 bank0 中且自身為非易變型
volatile unsigned char *ptr0;
//定義指向 bank2/3 非易變型字符變量的指針,指針變量位于 bank1 中且自身為易變型
bank2 unsigned char * volatile bank1 ptr0;
//定義指向 ROM 區(qū)的指針,指針變量本身也是存放于 ROM 區(qū)的常數(shù)
const unsigned char * const ptr0;
亦即出現(xiàn)在前面的修飾詞其作用對象是指針所指處的變量;出現(xiàn)在后面的修飾詞其作用對象
就是指針變量自己。
11.6
PICC 中的子程序和函數(shù)
中檔系列的 PIC 單片機程序空間有分頁的概念,但用 C 語言編程時基本不用太多關心
代碼的分頁問題。因為所有函數(shù)或子程序調(diào)用時的頁面設定(如果代碼超過一個頁面)都由
編譯器自動生成的指令實現(xiàn)。
11.6.1
PICC 決定了 C 原程序中的一個函數(shù)經(jīng)編譯后生成的機器碼一定會放在同一個程序頁面
內(nèi)。中檔系列的 PIC 單片機其一個程序頁面的長度是 2K 字,換句話說,用 C 語言編寫的任
何一個函數(shù)最后生成的代碼不能超過 2K 字。一個良好的程序設計應該有一個清晰的組織結(jié)
構,把不同的功能用不同的函數(shù)實現(xiàn)是最好的方法,因此一個函數(shù) 2K 字長的限制一般不會
對程序代碼的編寫產(chǎn)生太多影響。如果為實現(xiàn)特定的功能確實要連續(xù)編寫很長的程序,這時
就必須把這些連續(xù)的代碼拆分成若干函數(shù),以保證每個函數(shù)最后編譯出的代碼不超過一個頁
面空間。
11.6.2
中檔系列 PIC 單片機的硬件堆棧深度為 8 級,考慮中斷響應需占用一級堆棧,所
有函數(shù)調(diào)用嵌套的最大深度不要超過 7 級。編程員必須自己控制子程序調(diào)用時的嵌套深
度以符合這一限制要求。
PICC 在最后編譯連接成功后可以生成一個連接定位映射文件(*.map),在此文件
中有詳細的函數(shù)調(diào)用嵌套指示圖“call graph”,建議大家要留意一下。其信息大致如下
(取自于一示范程序的編譯結(jié)果):
Call graph:
*_main size 0,0 offset 0
*
例 11-4
上面所舉的信息表明整個程序在正常調(diào)用子程序時嵌套最多為兩級(沒有考慮中斷)。因為
main
加入的庫函數(shù),這些函數(shù)調(diào)用從 C 原程序中無法直接看出,但在此嵌套指示圖上則一目了
然。
11.6.3
PICC 在編譯時將嚴格進行函數(shù)調(diào)用時的類型檢查。一個良好的習慣是在編寫程序代碼
前先聲明所有用到的函數(shù)類型。例如:
void Task(void);
unsigned char Temperature(void);
void BIN2BCD(unsigned char);
void TimeDisplay(unsigned char, unsigned char);
這些類型聲明確定了函數(shù)的入口參數(shù)和返回值類型,這樣編譯器在編譯代碼時就能保證生成
正確的機器碼。筆者在實際工作中有時碰到一些用戶聲稱發(fā)現(xiàn) C 編譯器生成了錯誤的代碼,
最后究其原因就是因為沒有事先聲明函數(shù)類型所致。
建議大家在編寫一個函數(shù)的原代碼時,立即將此函數(shù)的類型聲明復制到原文件的起始
處,見例 11-1;或是復制到專門的包含頭文件中,再在每個原程序模塊中引用。
11.6.4
PICC 可以實現(xiàn) C 語言的中斷服務程序。中斷服務程序有一個特殊的定義方法:
void interrupt ISR(void);
其中的函數(shù)名“ISR”可以改成任意合法的字母或數(shù)字組合,但其入口參數(shù)和返回參數(shù)類型
必須是“void”型,亦即沒有入口參數(shù)和返回參數(shù),且中間必須有一個關鍵詞“interrupt”。
中斷函數(shù)可以被放置在原程序的任意位置。因為已有關鍵詞“interrupt”聲明,PICC 在
最后進行代碼連接時會自動將其定位到 0x0004 中斷入口處,實現(xiàn)中斷服務響應。編譯器也
會實現(xiàn)中斷函數(shù)的返回指令“retfie”。一個簡單的中斷服務示范函數(shù)如下:
void
{
T0IF = 0;
//判 TMR0 中斷
//清除 TMR0 中斷標志
TMR1IF0;
}
//清除 TMR1 中斷標志
//中斷結(jié)束并返回
評論