機電之家資源網
單片機首頁|單片機基礎|單片機應用|單片機開發(fā)|單片機文案|軟件資料下載|音響制作|電路圖下載 |嵌入式開發(fā)
培訓信息
贊助商
PIC單片機C語言編程教程
PIC單片機C語言編程教程
 更新時間:2008-7-26 17:03:52  點擊數:3
【字體: 字體顏色

PIC 單片機 C 語言編程簡介

用  C  語言來開發(fā)單片機系統(tǒng)軟件最大的好處是編寫代碼效率高、軟件調試直觀、維護升級方便、

代碼的重復利用率高、便于跨平臺的代碼移植等等,因此 C 語言編程在單片機系統(tǒng)設計中已得到越

來越廣泛的運用。針對 PIC 單片機的軟件開發(fā),同樣可以用 C 語言實現。

但在單片機上用 C 語言寫程序和在 PC 機上寫程序絕對不能簡單等同,F在的 PC 機資

源十分豐富,運算能力強大,因此程序員在寫 PC 機的應用程序時幾乎不用關心編譯后的可

執(zhí)行代碼在運行過程中需要占用多少系統(tǒng)資源,也基本不用擔心運行效率有多高。寫單片機

的  C 程序最關鍵的一點是單片機內的資源非常有限,控制的實時性要求又很高,因此,如

果沒有對單片機體系結構和硬件資源作詳盡的了解,以筆者的愚見認為是無法寫出高質量實

用的 C 語言程序。這就是為什么前面所有章節(jié)中的的示范代碼全部用基礎的匯編指令實現

的原因,希望籍此能使讀者對  PIC  單片機的指令體系和硬件資源有深入了解,在這基礎之

上再來討論 C 語言編程,就有水到渠成的感覺。

本書圍繞中檔系列 PIC 單片機來展開討論,Microchip 公司自己沒有針對中低檔系列 PIC

單片機的 C 語言編譯器,但很多專業(yè)的第三方公司有眾多支持 PIC 單片機的 C 語言編譯器

提供,常見的有 Hitech、CCS、IAR、Bytecraft 等公司。其中筆者最常用的是 Hitech 公司的

PICC 編譯器,它穩(wěn)定可靠,編譯生成的代碼效率高,在用 PIC 單片機進行系統(tǒng)設計和開發(fā)

的工程師群體中得到廣泛認可。其正式完全版軟件需要購置,但在其網站上有限時的試用版

供用戶評估。另外,Hitech 公司針對廣大 PIC 的業(yè)余愛好者和初學者還提供了完全免費的學

習版 PICC-Lite 編譯器套件,它的使用方式和完全版相同,只是支持的 PIC 單片機型號限制

在 PIC16F84、PIC16F877 和 PIC16F628 等幾款。這幾款 Flash 型的單片機因其所具備的豐富

的片上資源而最適用于單片機學習入門,因此筆者建議感興趣的讀者可從 PICC-Lite 入手掌

握 PIC 單片機的 C 語言編程。

在此列出幾個主要的針對 PIC 單片機的 C 編譯器相關連接網址,供讀者參考:

Hitech-PICC:       www.htsoft.com


IAR:www.iar.com

CCS:www.ccsinfo.com/picc.shtml

ByteCraft:www.bytecraft.com/mpccaps.html


本章將介紹 Hitech-PICC 編譯器的一些基本概念,由于篇幅所限將不涉及 C 語言的標準

語法和基礎知識介紹,因為在這些方面都有大量的書籍可以參考。重點突出針對  PIC  單片

機的特點而所需要特別注意的地方。

11.2

Hitech-PICC 編譯器

PICC 基本上符合 ANSI 標準,除了一點:它不支持函數的遞歸調用。其主要原因是因

為 PIC 單片機特殊的堆棧結構。在前面介紹 PIC 單片機架構時已經詳細說明了 PIC 單片機

中的堆棧是硬件實現的,其深度已隨芯片而固定,無法實現需要大量堆棧操作的遞歸算法;

另外在 PIC 單片機中實現軟件堆棧的效率也不是很高,為此,PICC 編譯器采用一種叫做“靜

態(tài)覆蓋”的技術以實現對    C  語言函數中的局部變量分配固定的地址空間。經這樣處理后產

生出的機器代碼效率很高,按筆者實際使用的體會,當代碼量超過 4K 字后,C 語言編譯出

的代碼長度和全部用匯編代碼實現時的差別已經不是很大(<10%),當然前提是在整個  C

代碼編寫過程中須時時處處注意所編寫語句的效率,而如果沒有對 PIC 單片機的內核結構、

各功能模塊及其匯編指令深入了解,要做到這點是很難的。

11.3

MPLAB-IDE 內掛接 PICC

PICC 編譯器可以直接掛接在 MPLAB-IDE 集成開發(fā)平臺下,實現一體化的編譯連接和

原代碼調試。使用 MPLAB-IDE 內的調試工具 ICE2000、ICD2 和軟件模擬器都可以實現原

代碼級的程序調試,非常方便。

首先必須在你的計算機中安裝PICC編譯器,無論是完全版還是學習版都可以和

MPLAB-IDE掛接。安裝成功后可以進入IDE,選擇菜單項Project Set Language Tool

Locations…,打開語言工具掛接設置對話框,如圖 11-1 所示:

 

圖 11-1  MPLAB-IDE 語言工具設置對話框

在對話框中選擇“HI-TECH PICC Toolsuite”欄,展開可執(zhí)行文件組“Executable”后,

列出了將被  MPLAB-IDE   后臺調用的編譯器所用到的所有可執(zhí)行文件,其中有匯編編譯器

“PICC Assembler”、C 原程序編譯器“PICC Compiler”和連接定位程序“PICC Linker”。同

時在此列表中還顯示了對應的可執(zhí)行程序名,請注意在這里都是“PICC.EXE”。用鼠標分別

點擊選中這三項可執(zhí)行文件,觀察對話框下面“Location”一欄中顯示的文件路徑,用

“Browse…”按紐,從計算機中已經安裝的 PICC 編譯器文件夾中選擇 PICC.EXE 文件。實

際上 PICC.EXE 只是一個調度管理程序,它會按照所輸入的文件擴展名自動調用對應的編譯

器和連接器,用戶要注意的是 C 語言原程序擴展名用“.c”,匯編原程序用“.as”即可。

工具掛接完成后,在建立項目時可以選擇語言工具為“HI-TECH PICC”,具體步驟可以

參閱第三章 3.1.3 節(jié),此處不再重復。項目建立完成后可以加入 C 或匯編原程序,也可以加

入已有的庫文件或已經編譯的目標文件。最常見的是只加入 C 原程序。用 C 語言編程的好

處是可以實現模塊化編程。程序編寫者應盡量把相互獨立的控制任務用多個獨立的 C 原程序文件實

現,如果程序量較大,一般不要把所有的代碼寫在一個文件內。

圖 11-2 列出的是筆者建立的一個項目中所有 C 原程序模塊,其中主控、數值計算、I2C 總線操

作、命令按鍵處理和液晶顯示驅動等不同的功能分別在不同的獨立的原程序模塊中實現。



圖 11-2  C 語言多模塊編程

11.4         PIC 單片機的 C 語言原程序基本框架

基于 PICC 編譯環(huán)境編寫 PIC 單片機程序的基本方式和標準 C 程序類似,程序一般由以

下幾個主要部分組成:

&O1540;    在程序的最前面用#include 預處理指令引用包含頭文件,其中必須包含一個編譯器

提供的“pic.h”文件,實現單片機內特殊寄存器和其它特殊符號的聲明;

&O1540;    用“__CONFIG”預處理指令定義芯片的配置位;

&O1540;    聲明本模塊內被調用的所有函數的類型,PICC 將對所調用的函數進行嚴格的類型

匹配檢查;

&O1540;    定義全局變量或符號替換;

&O1540;    實現函數(子程序),特別注意 main 函數必須是一個沒有返回的死循環(huán)。

 

下面的例 11-1 為一個 C 原程序的范例,供大家參考。

#include          //包含單片機內部資源預定義

#include “pc68.h”       //包含自定義頭文件

//定義芯片工作時的配置位

__CONFIG (HS & PROTECT & PWRTEN & BOREN & WDTDIS);

//聲明本模塊中所調用的函數類型

void SetSFR(void);

void Clock(void);

void KeyScan(void);

void Measure(void);

void LCD_Test(void);

void LCD_Disp(unsigned char);

//定義變量

unsigned char second, minute, hour;

bit flag1,flag2;

//函數和子程序

void main(void)

{

  SetSFR();

  PORTC =  0x00;

  TMR1H +=  TMR1H_CONST;

  LED1  = LED_OFF;

 

 

  LCD_Test();

 

 

  //程序工作主循環(huán)

  while(1) {

     asm(“clrwdt”); 

     Clock();  

     KeyScan();

     Measure();

     SetSFR(); 

  }

}

//清看門狗

//更新時鐘

//掃描鍵盤

//數據測量

//刷新特殊功能寄存器

11.5

PICC 中的變量定義


例 11-1  C 語言原程序框架舉例

11.5.1     PICC 中的基本變量類型


PICC 遵循 Little-endian  標準,多字節(jié)變量的低字節(jié)放在存儲空間的低地址,高字節(jié)放

在高地址。

11.5.2     PICC 中的高級變量

基于表 11-1 的基本變量,除了 bit 型位變量外,PICC 完全支持數組、結構和聯(lián)合等復

合型高級變量,這和標準的 C 語言所支持的高級變量類型沒有什么區(qū)別。例如:

數組:unsigned int data[10];

結構:struct commInData {

         unsigned char  inBuff[8];

         unsigned char  getPtr, putPtr;

      };

聯(lián)合:union int_Byte {

         unsigned char  c[2];

         unsigned int i;

      };

例 11-2  C 語言高級變量舉例

11.5.3     PICC 對數據寄存器 bank 的管理

 

為了使編譯器產生最高效的機器碼,PICC 把單片機中數據寄存器的 bank 問題交由編程

員自己管理,因此在定義用戶變量時你必須自己決定這些變量具體放在哪一個 bank 中。如

果沒有特別指明,所定義的變量將被定位在 bank0,例如下面所定義的這些變量:

unsigned char buffer[32];

bit flag1,flag2;

float val[8];

 

 

除了 bank0 內的變量聲明時不需特殊處理外,定義在其它 bank 內的變量前面必須加上

相應的 bank 序號,例如:

bank1 unsigned char buffer[32];        //變量定位在 bank1 中


bank2 bit flag1,flag2;

bank3 float  val[8];


//變量定位在 bank2 中

//變量定位在 bank3 中


 

 

中檔系列 PIC 單片機數據寄存器的一個 bank 大小為 128 字節(jié),刨去前面若干字節(jié)的特

殊功能寄存器區(qū)域,在 C 語言中某一 bank 內定義的變量字節(jié)總數不能超過可用 RAM 字節(jié)

數。如果超過 bank 容量,在最后連接時會報錯,大致信息如下:

Error[000]   : Can't find 0x12C words for psect rbss_1 in segment BANK1

 

 

連接器告訴你總共有 0x12C(300)個字節(jié)準備放到 bank1 中但 bank1 容量不夠。顯然,只

有把一部分原本定位在 bank1 中的變量改放到其它 bank 中才能解決此問題。

 

雖然變量所在的 bank 定位必須由編程員自己決定,但在編寫原程序時進行變量存取操

作前無需再特意編寫設定 bank 的指令。C 編譯器會根據所操作的對象自動生成對應 bank 設

定的匯編指令。為避免頻繁的 bank 切換以提高代碼效率,盡量把實現同一任務的變量定位

在同一個 bank 內;對不同 bank 內的變量進行讀寫操作時也盡量把位于相同 bank 內的變量

歸并在一起進行連續(xù)操作。

11.5.4     PICC 中的局部變量

 

PICC 把所有函數內部定義的 auto 型局部變量放在 bank0。為節(jié)約寶貴的存儲空間,它

采用了一種被叫做“靜態(tài)覆蓋”的技術來實現局部變量的地址分配。其大致的原理是在編譯

器編譯原代碼時掃描整個程序中函數調用的嵌套關系和層次,算出每個函數中的局部變量字

節(jié)數,然后為每個局部變量分配一個固定的地址,且按調用嵌套的層次關系各變量的地址可

以相互重疊。利用這一技術后所有的動態(tài)局部變量都可以按已知的固定地址地進行直接尋

址,用 PIC 匯編指令實現的效率最高,但這時不能出現函數遞歸調用。PICC 在編譯時會嚴

格檢查遞歸調用的問題并認為這是一個嚴重錯誤而立即終止編譯過程。

既然所有的局部變量將占用 bank0 的存儲空間,因此用戶自己定位在 bank0 內的變量字

節(jié)數將受到一定的限制,在實際使用時需注意。

 

11.5.5     PICC 中的位變量

 

bit 型位變量只能是全局的或靜態(tài)的。PICC 將把定位在同一 bank 內的 8 個位變量合并

成一個字節(jié)存放于一個固定地址。因此所有針對位變量的操作將直接使用  PIC  單片機的位

操作匯編指令高效實現。基于此,位變量不能是局部自動型變量,也無法將其組合成復合型

高級變量。

 

PICC 對整個數據存儲空間實行位編址,0x000 單元的第 0 位是位地址 0x0000,以此后

推,每個字節(jié)有 8 個位地址。編制位地址的意義純粹是為了編譯器最后產生匯編級位操作指

令而用,對編程人員來說基本可以不管。但若能了解位變量的位地址編址方式就可以在最后

程序調試時方便地查找自己所定義的位變量,如果一個位變量 flag1 被編址為 0x123,那么

實際的存儲空間位于:

字節(jié)地址=0x123/8 = 0x24

位偏移      =0x123%8 = 3

即 flag1 位變量位于地址為 0x24 字節(jié)的第 3 位。在程序調試時如果要觀察 flag1 的變化,必

須觀察地址為 0x24 的字節(jié)而不是 0x123。

 

PIC  單片機的位操作指令是非常高效的。因此,PICC 在編譯原代碼時只要有可能,對

普通變量的操作也將以最簡單的位操作指令來實現。假設一個字節(jié)變量  tmp  最后被定位在

地址 0x20,那么


tmp |= 0x80

tmp &= 0xf7


=>   bsf

=>   bcf


0x20,7

0x20,3


if (tmp&0xfe)


=>   btfsc 0x20,0


即所有只對變量中某一位操作的 C 語句代碼將被直接編譯成匯編的位操作指令。雖然編程

時可以不用太關心,但如果能了解編譯器是如何工作的,那將有助于引導我們寫出高效簡介

的 C 語言原程序。

 

在有些應用中需要將一組位變量放在同一個字節(jié)中以便需要時一次性地進行讀寫,這一

功能可以通過定義一個位域結構和一個字節(jié)變量的聯(lián)合來實現,例如:

 

union {

   struct  {

      unsigned  b0:  1;

      unsigned  b1:  1;

      unsigned  b2:  1;

      unsigned  b3:  1;

      unsigned  b4:  1;

      unsigned  b5:  1;

      unsigned   : 2; //最高兩位保留

   } oneBit;

   unsigned char allBits;

} myFlag;

例 11-3      定義位變量于同一字節(jié)

需要存取其中某一位時可以

myFlag.oneBit.b3=1; //b3 位置 1

一次性將全部位清零時可以

myFlag.allBits=0;          //全部位變量清 0

 

 

當程序中把非位變量進行強制類型轉換成位變量時,要注意編譯器只對普通變量的最低

位做判別:如果最低位是 0,則轉換成位變量 0;如果最低位是 1,則轉換成位變量 1。而標

準的 ANSI-C 做法是判整個變量值是否為 0。另外,函數可以返回一個位變量,實際上此返

回的位變量將存放于單片機的進位位中帶出返回。

 

11.5.6     PICC 中的浮點數

 

PICC 中描述浮點數是以 IEEE-754 標準格式實現的。此標準下定義的浮點數為 32 位長,

在單片機中要用 4 個字節(jié)存儲。為了節(jié)約單片機的數據空間和程序空間,PICC 專門提供了

一種長度為 24 位的截短型浮點數,它損失了浮點數的一點精度,但浮點運算的效率得以提

高。在程序中定義的 float 型標準浮點數的長度固定為 24 位,雙精度 double 型浮點數一般

也是 24 位長,但可以在程序編譯選項中選擇 double 型浮點數為 32 位,以提高計算的精度。

 

一般控制系統(tǒng)中關心的是單片機的運行效率,因此在精度能夠滿足的前提下盡量選擇

24 位的浮點數運算。

 

11.5.7     PICC 中變量的絕對定位

 

首先必須強調,在用  C  語言寫程序時變量一般由編譯器和連接器最后定位,在寫程序

之時無需知道所定義的變量具體被放在哪個地址(除了 bank 必須聲明)。

 

真正需要絕對定位的只是單片機中的那些特殊功能寄存器,而這些寄存器的地址定位在

PICC 編譯環(huán)境所提供的頭文件中已經實現,無需用戶操心。編程員所要了解的也就是 PICC

是如何定義這些特殊功能寄存器和其中的相關控制位的名稱。好在 PICC 的定義標準基本上

按照芯片的數據手冊中的名稱描述進行,這樣就秉承了變量命名的一貫性。一個變量絕對定

位的例子如下:

unsigned char tmpData @ 0x20; //tmpData 定位在地址 0x20

千萬注意,PICC 對絕對定位的變量不保留地址空間。換句話說,上面變量 tmpData 的

地址是 0x20,但最后 0x20 處完全有可能又被分配給了其它變量使用,這樣就發(fā)生了地址沖

突。因此針對變量的絕對定位要特別小心。從筆者的應用經驗看,在一般的程序設計中用戶

自定義的變量實在是沒有絕對定位的必要。

 

如果需要,位變量也可以絕對定位。但必須遵循上面介紹的位變量編址的方式。如果一

個普通變量已經被絕對定位,那么此變量中的每個數據位就可以用下面的計算方式實現位變

量指派:

unsigned char tmpData @ 0x20; //tmpData 定位在地址 0x20

bit tmpBit0 @ tmpData*8+0;      //tmpBit0 對應于 tmpData 第 0 位

bit tmpBit1 @ tmpData*8+1;      //tmpBit0 對應于 tmpData 第 1 位

bit tmpBit2 @ tmpData*8+2;      //tmpBit0 對應于 tmpData 第 2 位

 

 

如果 tmpData 事先沒有被絕對定位,那就不能用上面的位變量定位方式。

 

11.5.8     PICC 的其它變量修飾關鍵詞

 

&O1540;    extern  —  外部變量聲明

 

如果在一個 C  程序文件中要使用一些變量但其原型定義寫在另外的文件中,那么在本

文件中必須將這些變量聲明成“extern”外部類型。例如程序文件 code1.c 中有如下定義:


bank1 unsigned char var1, var2;  


//定義了 bank1 中的兩個變量


在另外一個程序文件 code2.c 中要對上面定義的變量進行操作,則必須在程序的開頭定義:

extern bank1 unsigned char var1, var2;       //聲明位于 bank1 的外部變量

 

 

&O1540;    volatile  — 易變型變量聲明

 

PICC  中還有一個變量修飾詞在普通的  C  語言介紹中一般是看不到的,這就是關鍵詞

“volatile”。顧名思義,它說明了一個變量的值是會隨機變化的,即使程序沒有刻意對它進

行任何賦值操作。在單片機中,作為輸入的 IO 端口其內容將是隨意變化的;在中斷內被修

改的變量相對主程序流程來講也是隨意變化的;很多特殊功能寄存器的值也將隨著指令的運

行而動態(tài)改變。所有這種類型的變量必須將它們明確定義成“volatile”類型,例如:

volatile unsigned char STATUS @ 0x03;

volatile bit commFlag;

 

 

“volatile”類型定義在單片機的      C 語言編程中是如此的重要,是因為它可以告訴編譯

器的優(yōu)化處理器這些變量是實實在在存在的,在優(yōu)化過程中不能無故消除。假定你的程序定

義了一個變量并對其作了一次賦值,但隨后就再也沒有對其進行任何讀寫操作,如果是非

volatile  型變量,優(yōu)化后的結果是這個變量將有可能被徹底刪除以節(jié)約存儲空間。另外一種

情形是在使用某一個變量進行連續(xù)的運算操作時,這個變量的值將在第一次操作時被復制到

中間臨時變量中,如果它是非 volatile 型變量,則緊接其后的其它操作將有可能直接從臨時

變量中取數以提高運行效率,顯然這樣做后對于那些隨機變化的參數就會出問題。只要將其

定義成 volatile 類型后,編譯后的代碼就可以保證每次操作時直接從變量地址處取數。

 

&O1540;    const —  常數型變量聲明

 

如果變量定義前冠以“const”類型修飾,那么所有這些變量就成為常數,程序運行過

程中不能對其修改。除了位變量,其它所有基本類型的變量或高級組合變量都將被存放在程

序空間(ROM 區(qū))以節(jié)約數據存儲空間。顯然,被定義在 ROM 區(qū)的變量是不能再在程序

中對其進行賦值修改的,這也是“const”的本來意義。實際上這些數據最終都將以“retlw”

的指令形式存放在程序空間,但 PICC 會自動編譯生成相關的附加代碼從程序空間讀取這些

常數,編程員無需太多操心。例如:

const unsigned char name[]=”This is a demo”;  //定義一個常量字符串

如果定義了  “const”類型的位變量,那么這些位變量還是被放置在 RAM 中,但程序

不能對其賦值修改。本來,不能修改的位變量沒有什么太多的實際意義,相信大家在實際編

程時不會大量用到。

 

&O1540;    persistent  —   非初始化變量聲明

 

按照標準  C  語言的做法,程序在開始運行前首先要把所有定義的但沒有預置初值的變

量全部清零。PICC 會在最后生成的機器碼中加入一小段初始化代碼來實現這一變量清零操

作,且這一操作將在 main 函數被調用之前執(zhí)行。問題是作為一個單片機的控制系統(tǒng)有很多

變量是不允許在程序復位后被清零的。為了達到這一目的,PICC  提供了“persistent”修飾

詞以聲明此類變量無需在復位時自動清零,編程員應該自己決定程序中的那些變量是必須聲

明成“persisten”類型,而且須自己判斷什么時候需要對其進行初始化賦值。例如:

persistent unsigned char hour,minute,second;  //定義時分秒變量

經常用到的是如果程序經上電復位后開始運行,那么需要將 persistent 型的變量初始化,

如果是其它形式的復位,例如看門狗引發(fā)的復位,則無需對 persistent 型變量作任何修改。

PIC  單片機內提供了各種復位的判別標志,用戶程序可依具體設計靈活處理不同的復位情

形。

 

11.5.9     PICC 中的指針

 

PICC 中指針的基本概念和標準 C 語法沒有太多的差別。但是在 PIC 單片機這一特定的

架構上,指針的定義方式還是有幾點需要特別注意。

 

&O1540;    指向 RAM 的指針

 

如果是匯編語言編程,實現指針尋址的方法肯定就是用 FSR 寄存器,PICC 也不例外。

為了生成高效的代碼,PICC 在編譯 C 原程序時將指向 RAM 的指針操作最終用 FSR 來實現

間接尋址。這樣就勢必產生一個問題:FSR 能夠直接連續(xù)尋址的范圍是 256 字節(jié)(bank0/1

或 bank2/3),要覆蓋最大 512 字節(jié)的內部數據存儲空間,又該如何讓定義指針?PICC 還是

將這一問題留給編程員自己解決:在定義指針時必須明確指定該指針所適用的尋址區(qū)域,例

如:

unsigned char *ptr0; //①定義覆蓋 bank0/1 的指針

bank2 unsigned char *ptr1;  //②定義覆蓋 bank2/3 的指針

bank3 unsigned char *ptr2;  //③定義覆蓋 bank2/3 的指針

上面定義了三個指針變量,其中①指針沒有任何 bank 限定,缺省就是指向 bank0 和 bank1;

②和③一個指明了 bank2,另一個指明了 bank3,但實際上兩者是一樣的,因為一個指針可

以同時覆蓋兩個 bank 的存儲區(qū)域。另外,上面三個指針變量自身都存放在 bank0 中。我們

將在稍后介紹如何在其它 bank 中存放指針變量。

 

既然定義的指針有明確的 bank 適用區(qū)域,在對指針變量賦值時就必須實現類型匹配,

下面的指針賦值將產生一個致命錯誤:


unsigned char *ptr0;

bank2 unsigned char buff[8];

程序語句:


//定義指向 bank0/1 的指針

//定義 bank2 中的一個緩沖區(qū)


ptr0 = buff;  //錯誤!試圖將 bank2 內的變量地址賦給指向 bank0/1 的指針

若出現此類錯誤的指針操作,PICC 在最后連接時會告知類似于下面的信息:

Fixup overflow in expression (...)

 

 

同樣的道理,若函數調用時用了指針作為傳遞參數,也必須注意 bank 作用域的匹配,

而這點往往容易被忽視。假定有下面的函數實現發(fā)送一個字符串的功能:

void SendMessage(unsigned char *);

那么被發(fā)送的字符串必須位于 bank0 或 bank1 中。如果你還要發(fā)送位于 bank2 或 bank3 內的

字符串,必須再另外單獨寫一個函數:

void SendMessage_2(bank2 unsigned char *);

這兩個函數從內部代碼的實現來看可以一模一樣,但傳遞的參數類型不同。

 

按筆者的應用經驗體會,如果你看到了“Fixup overflow”的錯誤指示,幾乎可以肯定

是指針類型不匹配的賦值所至。請重點檢查程序中有關指針的操作。

 

&O1540;    指向 ROM 常數的指針

 

如果一組變量是已經被定義在 ROM 區(qū)的常數,那么指向它的指針可以這樣定義:


const unsigned char company[]=”Microchip”;

const unsigned char *romPtr;

程序中可以對上面的指針變量賦值和實現取數操作:

romPtr      = company;  //指針賦初值

data = *romPtr++;  //取指針指向的一個數,然后指針加 1


//定義 ROM 中的常數

//定義指向 ROM 的指針


反過來,下面的操作將是一個錯誤,因為該指針指向的是常數型變量,不能賦值。

*romPtr = data; //往指針指向的地址寫一個數

&O1540;    指向函數的指針

 

單片機編程時函數指針的應用相對較少,但作為標準 C 語法的一部分,PICC 同樣支持

函數指針調用。如果你對編譯原理有一定的了解,就應該明白在  PIC  單片機這一特定的架

構上實現函數指針調用的效率是不高的:PICC 將在 RAM 中建立一個調用返回表,真正的

調用和返回過程是靠直接修改 PC 指針來實現的。因此,除非特殊算法的需要,建議大家盡

量不要使用函數指針。

 

&O1540;    指針的類型修飾

 

前面介紹的指針定義都是最基本的形式。和普通變量一樣,指針定義也可以在前面加上

特殊類型的修飾關鍵詞,例如“persistent”、“volatile”等。考慮指針本身還要限定其作用域,

因此 PICC 中的指針定義初看起來顯得有點復雜,但只要了解各部分的具體含義,理解一個

指針的實際用圖就變得很直接。

 

㈠ bank 修飾詞的位置含義

前面介紹的一些指針有的作用于 bank0/1,有的作用于 bank2/3,但它們本身的存放位置

全部在 bank0。顯然,在一個程序設計中指針變量將有可能被定位在任何可用的地址空間,

這時,bank 修飾詞出現的位置就是一個關鍵,看下面的例子:

//定義指向 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 這

些關鍵詞出現在前后不同位置上的含義規(guī)律是和 bank 一詞相一致的。例如:

//定義指向 bank0/1 易變型字符變量的指針,指針變量位于 bank0 中且自身為非易變型

volatile unsigned char *ptr0;

//定義指向 bank2/3 非易變型字符變量的指針,指針變量位于 bank1 中且自身為易變型

bank2 unsigned char * volatile bank1 ptr0;

//定義指向 ROM 區(qū)的指針,指針變量本身也是存放于 ROM 區(qū)的常數

const unsigned char * const ptr0;

亦即出現在前面的修飾詞其作用對象是指針所指處的變量;出現在后面的修飾詞其作用對象

就是指針變量自己。


11.6


PICC 中的子程序和函數

 

中檔系列的 PIC 單片機程序空間有分頁的概念,但用 C 語言編程時基本不用太多關心


代碼的分頁問題。因為所有函數或子程序調用時的頁面設定(如果代碼超過一個頁面)都由

編譯器自動生成的指令實現。

 

11.6.1     函數的代碼長度限制

 

PICC 決定了 C 原程序中的一個函數經編譯后生成的機器碼一定會放在同一個程序頁面

內。中檔系列的 PIC 單片機其一個程序頁面的長度是 2K 字,換句話說,用 C 語言編寫的任

何一個函數最后生成的代碼不能超過 2K 字。一個良好的程序設計應該有一個清晰的組織結

構,把不同的功能用不同的函數實現是最好的方法,因此一個函數 2K 字長的限制一般不會

對程序代碼的編寫產生太多影響。如果為實現特定的功能確實要連續(xù)編寫很長的程序,這時

就必須把這些連續(xù)的代碼拆分成若干函數,以保證每個函數最后編譯出的代碼不超過一個頁

面空間。

 

11.6.2     調用層次的控制

 

中檔系列 PIC 單片機的硬件堆棧深度為 8 級,考慮中斷響應需占用一級堆棧,所

有函數調用嵌套的最大深度不要超過 7 級。編程員必須自己控制子程序調用時的嵌套深

度以符合這一限制要求。

 

PICC 在最后編譯連接成功后可以生成一個連接定位映射文件(*.map),在此文件

中有詳細的函數調用嵌套指示圖“call graph”,建議大家要留意一下。其信息大致如下

(取自于一示范程序的編譯結果):

Call graph:

*_main size 0,0 offset 0

     _RightShift_C

*    _Task size 0,1 offset 0

     lwtoft

     ftmul size 0,0 offset 0

         ftunpack1

         ftunpack2

     ftadd size 0,0 offset 0

         ftunpack1

         ftunpack2

         ftdenorm

例 11-4  C 函數調用層次圖

上面所舉的信息表明整個程序在正常調用子程序時嵌套最多為兩級(沒有考慮中斷)。因為

main  函數不可能返回,故其不用計算在嵌套級數中。其中有些函數調用是編譯代碼時自動

加入的庫函數,這些函數調用從 C 原程序中無法直接看出,但在此嵌套指示圖上則一目了

然。

 

11.6.3     函數類型聲明

 

PICC 在編譯時將嚴格進行函數調用時的類型檢查。一個良好的習慣是在編寫程序代碼

前先聲明所有用到的函數類型。例如:

void Task(void);

unsigned char Temperature(void);

void BIN2BCD(unsigned char);

void TimeDisplay(unsigned char, unsigned char);

這些類型聲明確定了函數的入口參數和返回值類型,這樣編譯器在編譯代碼時就能保證生成

 

正確的機器碼。筆者在實際工作中有時碰到一些用戶聲稱發(fā)現 C 編譯器生成了錯誤的代碼,

最后究其原因就是因為沒有事先聲明函數類型所致。

 

建議大家在編寫一個函數的原代碼時,立即將此函數的類型聲明復制到原文件的起始

處,見例 11-1;或是復制到專門的包含頭文件中,再在每個原程序模塊中引用。

 

11.6.4     中斷函數的實現

 

PICC 可以實現 C 語言的中斷服務程序。中斷服務程序有一個特殊的定義方法:

void interrupt ISR(void);

其中的函數名“ISR”可以改成任意合法的字母或數字組合,但其入口參數和返回參數類型

必須是“void”型,亦即沒有入口參數和返回參數,且中間必須有一個關鍵詞“interrupt”。

 

中斷函數可以被放置在原程序的任意位置。因為已有關鍵詞“interrupt”聲明,PICC 在

最后進行代碼連接時會自動將其定位到 0x0004 中斷入口處,實現中斷服務響應。編譯器也

會實現中斷函數的返回指令“retfie”。一個簡單的中斷服務示范函數如下:

void  interrupt  ISR(void) //中斷服務程序

{


   if (T0IE &&  T0IF) 

   {

T0IF = 0;

      //在此加入 TMR0 中斷服務

   }


//判 TMR0 中斷

 

 

//清除 TMR0 中斷標志


   if (TMR1IE && TMR1IF)       //判 TMR1 中斷

   {


TMR1IF0;

      //在此加入 TMR1 中斷服務

   }

}


//清除 TMR1 中斷標志

 

 

 

//中斷結束并返回


 

 

例 11-5  C 語言中斷函數舉例

 

 

PICC 會自動加入代碼實現中斷現場的保護,并在中斷結束時自動恢復現場,所以編程

員無需象編寫匯編程序那樣加入中斷現場保護和恢復的額外指令語句。但如果在中斷服務程

序中需要修改某些全局變量時,是否需要保護這些變量的初值將由編程員自己決定和實施。

 

用 C 語言編寫中斷服務程序必須遵循高效的原則:

&O1540;    代碼盡量簡短,中斷服務強調的是一個“快”字。

&O1540;    避免在中斷內使用函數調用。雖然 PICC 允許在中斷里調用其它函數,但為了解決

遞歸調用的問題,此函數必須為中斷服務獨家專用。既如此,不妨把原本要寫在其

它函數內的代碼直接寫在中斷服務程序中。

&O1540;    避免在中斷內進行數學運算。數學運算將很有可能用到庫函數和許多中間變量,就

算不出現遞歸調用的問題,光在中斷入口和出口處為了保護和恢復這些中間臨時變

量就需要大量的開銷,嚴重影響中斷服務的效率。

中檔系列 PIC 單片機的中斷入口只有一個,因此整個程序中只能有一個中斷服務函數。

 

11.6.5     標準庫函數

 

PICC 提供了較完整的 C 標準庫函數支持,其中包括數學運算函數和字符串操作函數。

在程序中使用這些現成的庫函數時需要注意的是入口參數必須在 bank0 中。

 

如果需要用到數學函數,則應在程序前  “#include ”      包含頭文件;如果要使

用字符串操作函數,就需要包含“#include ”頭文件。在這些頭文件中提供了函數

類型的聲明。通過直接查看這些頭文件就可以知道 PICC 提供了哪些標準庫函數。

 

C  語言中常用的格式化打印函數“printf/sprintf”用在單片機的程序中時要特別謹慎。

printf/sprintf 是一個非常大的函數,一旦使用,你的程序代碼長度就會增加很多。除非是在

編寫試驗性質的代碼,可以考慮使用格式化打印函數以簡化測試程序;一般的最終產品設計

都是自己編寫最精簡的代碼實現特定格式的數據顯示和輸出。本來,在單片機應用中輸出的

數據格式都相對簡單而且固定,實現起來應該很容易。

 

對于標準 C 語言的控制臺輸入(scanf)/輸出(printf)函數,PICC 需要用戶自己編寫

其底層函數 getch()和 putch()。在單片機系統(tǒng)中實現 scanf/printf 本來就沒什么太多意義,如

果一定要實現,只要編寫好特定的  getch()和 putch()函數,你就可以通過任何接口輸入或輸

出格式化的數據。


11.7

 

PICC 定義特殊區(qū)域值

 

PICC 提供了相關的預處理指令以實現在原程序中定義單片機的配置字和標記單元。

 

11.7.1     定義工作配置字

 

在原程序中定義 PIC 單片機工作配置字的重要性在前面章節(jié)中已經闡述。在用 PICC 寫


程序時同樣可以在 C 原程序中定義,具體方式如下:

__CONFIG (HS & UNPROTECT & PWRTEN & BORDIS & WDTEN);

上面的關鍵詞“__CONFIG”(注意前面有兩個下劃線符)專門用于是芯片配置字的設

定,后面括號中的各項配置位符號在特定型號單片機的頭文件中已經定義(注意不是  pic.h

頭文件),相互之間用邏輯“與”操作符組合在一起。這樣定義的配置字信息最后將和程序

代碼一起放入同一個 HEX 文件。

 

在這里列出了適用于  16F7x  系列單片機配置位符號預定義,其它型號或系列的單片機

配置字定義方式類似,使用前查閱一下對應的頭文件即可。

/*振蕩器配置*/


#define RC

#define HS


0x3FFF // RC 振蕩

0x3FFE // HS 模式

 

#define XT

#define LP

 

 

/*看門狗配置*/


 

0x3FFD // XT 模式

0x3FFC // LP 模式


#define WDTEN  0x3FFF //  看門狗打開


#define WDTDIS

 

 

/*上電延時定時器配置*/

#define PWRTEN


0x3FFB //  看門狗關閉

0x3FF7 //  上電延時定時器打開


#define PWRTDIS               0x3FFF    //     上電延時定時器關閉

/*低電壓復位配置*/

#define BOREN    0x3FFF //  低電壓復位允許


#define BORDIS

 

/*代碼保護配置*/


0x3FBF //  低電壓復位禁止


#define UNPROTECT  0x3FFF    //     沒有代碼保護

#define PROTECT               0x3FEF    //     程序代碼保護

 

例 11-6 頭文件預定義的配置信息符號

11.7.2     定義芯片標記單元

PIC 單片機中的標記單元定義可以用下面的__IDLOC(注意前面有兩個下劃線符)預處

理指令實現,方法如下:

__IDLOC (1234);

其特殊之處是括號內的值全部為 16 進制數,不需要用“0x”引導。這樣上面的定義就設定

了標記單元內容為 01020304。

11.8


MPLAB-IDE 中實現 PICC 的編譯選項設置

 

在 11.3 節(jié)中已經介紹了如何實現 PICC 和 MPLAB-IDE 開發(fā)平臺的掛接。一旦項目建立


成功、程序編寫完成后即可以通過 MPLAB 環(huán)境下的項目管理工具實現程序的編譯、連接和

調試。它們的含義分別

是:


-項目維護(Make):MPLAB 檢查項目中的原程序文件,只編譯那些在上次編

譯后又被修改過的原程序,最后進行連接;

-項目重建(Build All):項目中的所有原程序文件,不管是否有修改,都將被

重新編譯一次,最后進行連接。

 

也可以通過 Project 菜單選擇“Make”或“Build All”實現項目編譯。不管采用何種方

式,在啟動編譯過程前一般都要設定一些編譯選項。

11.8.1     選擇單片機型號

 

在選擇 PICC 作為語言工具并建立了項目后,同樣通過菜單項 Configure&O1616;Select Device

在 MPLAB 環(huán)境中選擇具體單片機型號。請回顧一下例 11-1 的代碼,我們在原程序一開始

使用了“#include ”實現了相關單片機的一些預定義符號的直接引用,但沒有具體指

明是哪一個型號。實際上,“pic.h”頭文件只是一個簡單的管理工具(條件判別),它會按照

MPLAB 所選擇的特定型號的單片機,把真正對應的頭文件包含進來。有興趣者可以直接用

文本編輯工具打開 pic.h 文件查看其是如何根據不同的單片機型號包含對應的頭文件。

 

這樣對編程員而言,程序中只需加上一句“#include ”即可。

 

11.8.2     PICC 普通編譯選項(General)設定

 

參考第三章  3.2.7 節(jié)的內容和圖 3-20  的指示說明,啟動編譯選項設定對話框。在使用

PICC 語言工具時對話框的內容和用 MPAMS 匯編工具相比完全不同。圖 11-3 為 PICC 編譯

環(huán)境下普通選項設定的界面。

 

在此界面中用戶唯一能改變的是編譯器查找頭文件時的指定路徑(Include Path),實際

上如果編譯器安裝沒有問題,在此界面中這些普通選項的設定無需任何改動,編譯器會自動

到缺省認定的路徑中(編譯器安裝后的相關路徑)查找編譯所需的各類文件。

               

圖 11-3  PICC 普通選項設定                 圖 11-4  PICC 全局選項設定

11.8.3     PICC 全局選項設定(PICC Global)

 

全局選項將影響項目中所有 C 和匯編原程序的編譯,詳細的設定內容見圖 11-4。其中

必須關注的有:

&O1540;    Compile for MPLAB ICD:如果你準備用 ICD 調試 C 語言編譯后的代碼,那么此項

就必須打鉤選中。這樣編譯后的結果就能保證  ICD 本身使用的芯片資源(一小部

分的程序和數據空間)不被應用程序所占用。

&O1540;    Treat ‘char’ as signed:為了提高編譯后的代碼效率,PICC 缺省認定‘char’型變量也

是無符號數。如果在設計中需要使用帶符號的‘char’型變量,此項就應該被選中。

&O1540;    Floating point ‘double’ width:同樣為了提高編譯后的代碼效率,PICC     缺省認定

‘double’型的雙精度浮點數變量的實現長度為 24 位(等同于普通 float 型浮點數)。

在這里可以選擇使其長度達 32 位。這樣數值計算的精度將得到提高,但代碼長度

將增加,計算速度也會降低,所以請在權衡利弊后作出你自己的決定。

 

11.8.4     C 編譯器選項設定(PICC Compiler)

 

項目中所有的 C 原程序都將通過 C 編譯器編譯成機器碼,這些選項決定了 C 編譯器是

如何工作的。所有選項又分為兩組:普通選項(General)和高級選項(Advanced),分別見

圖 11-5A 和 11-5B。

C 編譯器的普通選項最重要的就是針對代碼優(yōu)化的設定。如果沒有特殊原因,應該設定

全局優(yōu)化級別為 9 級(最高級別優(yōu)化),同時使用匯編級優(yōu)化,這樣最終得到的代碼效率最

高(長度和執(zhí)行速度兩方面)。按筆者的使用經驗,僅從代碼長度去比較,使用最高級別優(yōu)

化后代碼長度至少可以減少 20%(2K 字以上的程序)。而且 PICC 的優(yōu)化器相當可靠,一般

0 onclick="javascript:window.open(this.src);" style="cursor:pointer;" onload="javascript:if(this.width>600)this.style.width='600px';" border="0" />0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" border=0>                     0 onclick="javascript:window.open(this.src);" style="cursor:pointer;" onload="javascript:if(this.width>600)this.style.width='600px';" border="0" />0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" border=0>

(A) 常用選項                                      (B)     高級選項

                       圖 11-5      C 編譯器選項設定

不會因為使用優(yōu)化從而使生成的程序出現錯誤。碰到的一些問題也基本都是用戶編寫的原程

序有漏洞所導致,例如一些變量應該是 volatile 型但編程員沒有明確定義,在優(yōu)化前程序可

以正常運行,一旦使用優(yōu)化,程序運行就出現異常。顯然,把出現的這些問題歸罪到編譯器

是毫無道理的。

 

使用優(yōu)化后可能對原程序級的調試帶來一些不便之處。因 PICC 可能會重組編譯后的代

碼,例如多處重復的代碼可能會改成同一個子程序調用以節(jié)約程序空間,這樣在調試過程中

跟蹤原程序時可能會出現程序亂跳的現象,這基本是正常的。若為了強調更直觀的代碼調試

過程,你可以將優(yōu)化級別降低甚至關閉所有優(yōu)化功能,這樣調試時程序的運行就可以按部就

班了。

C 編譯器的高級選項設定基本都是針對診斷信息輸出的,和生成的代碼無關。用得相對

較多的選項有:

&O1540;    Generate assembly list file:編譯器生成 C 原程序的匯編列表文件(*.lst)。在此文件

中列出了每一行  C  原代碼對應的匯編指令,但這些都是優(yōu)化前的代碼。簡單的一

條  C 語句被翻譯成匯編指令后可能有好幾條。有時匯編列表文件可以作為解決問

題的輔助手段。如果你懷疑編譯器生成的代碼有錯誤,不妨先產生對應的匯編列表

文件,看看在優(yōu)化前一條 C 語句被編譯后的匯編碼到底是什么。

&O1540;    Compile to assembly only:這一選項的作用是把&n, bsp;     C  原程序編譯成匯編指令文件

(*.as),此時將不生成目標文件,也不進行最后的連接定位。這一選項在 C 和匯

編混合編程時特別有用。通過解讀 C 程序對應的匯編指令,可以掌握 C 程序中存

取變量的具體方法,然后用在自己編寫的匯編指令中。我們將在稍后專門做介紹。

11.8.5     連接器選項設定(PICC Linker)

 

連接器 PICC Linker 的選項基本不用作太多的改變,在圖 11-6 的對話框中顯示了可設定的各類

項目。其中有兩項有用的信息輸出可以考慮加以利用:

&O1540;    Generate map file:生成連接定位映射文件。在此映射文件中詳細列出了所有程

序用到的變量的具體物理地址;所有函數的入口地址;函數相互之間調用的層次關系和深度等。這

些信息對于程序的調試將非常有用。此文件將以擴展名“*.map”的形式存放在同一個項目路徑

下,需要時可以用任何文本編輯器打開觀察。

 



圖 11-6  PICC 連接器選項設定

&O1540;    Display memory-segment usage:顯示詳細的內存分配和使用情況報告。用戶可以

了解到程序空間和數據存儲器空間資源分配的細節(jié)。下面列舉了在一個項目編譯后實際的內存使用

信息,為方便理解筆者用“//”添加了一些注釋:

Psect Usage Map:        //程序段定位表

Psect     |  Contents                    |  Memory Range

----------|------------------------------|--------------------

powerup   | Power on reset code          |  $0000 - $0003

intentry  | Interrupt service routine    | $0004 - $000C

intcode   | Interrupt service routine    | $000D - $002C

intret    | Interrupt service routine    | $002D - $0035

init      | Initialization code          |  $0036 - $003D

end_init  | Initialization code          | $003E - $0040

clrtext   | Memory  clearing  code       |  $0041 - $0047

const3    | Strings and constant data    | $0048 - $0060

const     | Strings and constant data    | $0061 - $0071

const2    | Strings and constant data    | $0072 - $0076

text      | Program and library code     | $0576 - $0582

text      | Program and library code     | $0583 - $07C7

float_te  | Arithmetic routine code      | $07C8 - $07FF

rbss_0    |  Bank 0  RAM variables       |  $0021 - $0042

temp      | Temporary RAM data           | $0043 - $0047

nvram     |  Persistent RAM data          |  $0048 - $004A

intsave   | Registers saved on interrupt | $004B - $004D

intsave   | Registers saved on interrupt | $007F - $007F

intsave_1 | Saved copy of W in bank 1    | $00FF - $00FF

rbit_0    |  Bank 0  bit variables       |  $0100 - $0104

config    | User-programmed CONFIG bits  | $2007 - $2007

Memory Usage Map:  

//程序空間代碼定位地址分布

//存儲空間使用情況報告

Program  ROM   $0000 -  $0076  $0077 (   119) words

Program  ROM   $0576 -  $07FF  $028A (   650) words

                               $0301 (   769)  words  total Program ROM 

//bank0 數據空間變量地址分布

Bank  0 RAM    $0021 -  $004D  $002D (    45) bytes

Bank  0 RAM    $007F -  $007F  $0001 (     1) bytes

                               $002E (    46) bytes  total Bank 0  RAM 

//bank1 數據空間變量地址分布

Bank 1 RAM    $00FF - $00FF    $0001 (     1) bytes total Bank 1 RAM  

//bank0 數據空間位變量地址分布

Bank 0 Bits   $0100 - $0104    $0005 (     5) bits  total Bank 0 Bits 

//配置字地址

Config Data   $2007 - $2007    $0001 (     1) words total Config Data 

Program statistics:

//程序總體資源消耗統(tǒng)計

Total ROM used      769 words (18.8%)        //生成代碼字總數和程序空間使用率

Total RAM used       48  bytes (25.0%)       //使用數據字節(jié)數和數據空間使用率

例 11-7        編譯后程序使用的內存信息

11.8.6     匯編器選項設定(PICC Assembler)

PICC 環(huán)境提供了自己的匯編編譯器,它和 Microchip 公司提供的 MPASM 編譯器在原

程序的語法表達方面要求稍有不同。另外,PICC 的匯編編譯器要求輸入原程序文件的擴展

名是“*.as”,而 MPASM 缺省認定的原程序以“*.asm”為擴展名。

在基于 PICC 編譯環(huán)境下開發(fā) PIC 單片機的 C 語言應用程序時基本無需關心其匯編編譯

器,除非是在混合語言編程時用匯編語言編寫完整的匯編原程序模塊文件。其編譯選項設定

的對話框見圖 11-7,最重要的是優(yōu)化使能控制項“Enable optimization”,一般情況下應該使

用匯編器的優(yōu)化以節(jié)約程序空間。

0 onclick="javascript:window.open(this.src);" style="cursor:pointer;" onload="javascript:if(this.width>600)this.style.width='600px';" border="0" />0){if(image.width>=510){this.width=510;this.height=image.height*510/image.width;}}" border=0>

圖 11-7  PICC 匯編器選項設定

11.9

C 和匯編混合編程

有兩個原因決定了用 C  語言進行單片機應用程序開發(fā)時使用匯編語句的必要性:單片


機的一些特殊指令操作在標準的 C 語言語法中沒有直接對應的描述,例如 PIC 單片機的清

看門狗指令“clrwdt”和休眠指令“sleep”;單片機系統(tǒng)強調的是控制的實時性,為了實現這

一要求,有時必須用匯編指令實現部分代碼以提高程序運行的效率。這樣,一個項目中就會

出現 C 和匯編混合編程的情形,我們在此討論一些混合編程的基本方法和技巧。

11.9.1     嵌入行內匯編的方法

在  C  原程序中直接嵌入匯編指令是最直接最容易的方法。如果只需要嵌入少量幾條的

匯編指令,PICC 提供了一個類似于函數的語句:

asm(“clrwdt”);

雙引號中可以編寫任何一條 PIC 的標準匯編指令。例如:

for (;;) {

   asm("clrwdt");       //清看門狗

   Task();

   ClockRun();


   asm("sleep");

   asm("nop"); 

}


//休眠

//空操作延時

 

 

例 11-8       逐行嵌入匯編的方式

如果需要編寫一段連續(xù)的匯編指令,PICC 支持另外一種語法描述:用“#asm”開始匯

編指令段,用“#endasm”結束。例如下面的一段嵌入匯編指令實現了將       0x20~0x7F  間的

RAM 全部清零:

#asm

   MOVlw    0x20

   MOVwf    _FSR

   clrf     _INDF

   incf     _FSR,f

   btfss    _FSR,7

   goto     $-3

#endasm

例 11-9    整段嵌入匯編的方式

11.9.2     匯編指令尋址 C 語言定義的全局變量

C 語言中定義的全局或靜態(tài)變量尋址是最容易的,因為這些變量的地址已知且固定。按

C 語言的語法標準,所有 C 中定義的符號在編譯后將自動在前面添加一下劃線符“_”,因

此,若要在匯編指令中尋址 C 語言定義的各類變量,一定要在變量前加上一“_”符號,我

們在上面例 11-9 中已經體現了這一變量引用的法則,因為 FSR 和 INDF 等所有特殊寄存器

是以 C 語言語法定義的,因此匯編中需要對其尋址時前面必須添加下劃線。

對于 C 語言中用戶自定義的全局變量,用行內匯編指令尋址時也同樣必須加上“_”    ,

下面的例 11-10 說明了具體的引用方法:

volatile unsigned char tmp;          //定義位于 bank0 的字符型全局變量

void Test(void)

{

   #asm

clrf     _STATUS 

   MOVlw    0x10

   MOVwf    _tmp

#endasm

   if (tmp==0x10) {

      ;

}

}

//測試程序

//開始行內匯編

//選擇 bank0

//設定初值

//tmp=0x10

//結束行內匯編

//開始 C 語言程序


例 11-10       行內匯編尋址 C 全局變量(位于 bank0)

上面的例子說明了匯編指令中尋址 C 語言所定義變量的基本方法。PICC 在編譯處理嵌

入的行內匯編指令時將會原封不動地把這些指令復制成最后的機器碼。所有對 C 編譯器所

作的優(yōu)化設定對這些行內匯編指令而言將不起任何作用。編程員必須自己負責編寫最高效的

匯編代碼,同時處理變量所在的 bank 設定。對于定義在其它 bank 中的變量,還必須在匯編

指令中加以明確指示,見例 11-11 的代碼范例。

volatile bank1 unsigned char tmpBank1;      //定義位于 bank1 的字符型全局變量

volatile bank2 unsigned char tmpBank2;      //定義位于 bank2 的字符型全局變量

volatile bank3 unsigned char tmpBank3;      //定義位于 bank3 的字符型全局變量

void Test(void)

{

   #asm

bcf      _STATUS,6

bsf      _STATUS,5

MOVlw   0x10

MOVwf    _tmpBank1^0x80

bsf      _STATUS,6

bcf      _STATUS,5

MOVlw   0x20

//測試程序

//開始行內匯編

//選擇 bank1

//設定初值

//tmpBank1=0x10

//選擇 bank2

//設定初值


MOVwf    _tmpBank1^0x100         //tmpBank2=0x20

bsf      _STATUS,6

bsf      _STATUS,5

MOVlw   0x30


//選擇 bank3

//設定初值


MOVwf    _tmpBank1^0x180         //tmpBank1=0x30


}


#endasm


//結束行內匯編

例 11-11       行內匯編尋址 C 全局變量(非 bank0 變量)


通過上面的代碼實例,我們可以掌握這樣一個規(guī)律:在行內匯編指令中尋址 C  語言定

義的全局變量時,除了在尋址前設定正確的 bank 外,在指令描述時還必須在變量上異或其

所在 bank 的起始地址,實際上位于 bank0 的變量在匯編指令中尋址時也可以這樣理解,只

是異或的是 0x00,可以省略。如果你了解 PIC 單片機的匯編指令編碼格式,上面異或的 bank

起始地址是無法在真正的匯編指令中體現的,其目的純粹是為了告訴 PICC 連接器變量所在

的 bank,以便連接器進行 bank 類別檢查。

11.9.3     匯編指令尋址 C 函數的局部變量

前面已經提到,PICC 對自動型局部變量(包括函數調用時的入口參數)采用一種“靜

態(tài)覆蓋”技術對每一個變量確定一個固定地址(位于    bank0),因此嵌入的匯編指令對其尋

址時只需采用數據寄存器的直接尋址方式即可,唯一要考慮的是如何才能在編寫程序時知道

這些局部變量的尋址符號(具體地址在最后連接后才能決定,編程時無需關心)。一個最實

用也是最可靠的方法是先編寫一小段  C 代碼,其中有最簡單的局部變量操作指令,然后參

考圖 11-5(B)對話框選擇“Compile to assembly only”,把此 C 原代碼編譯成對應的 PICC 匯

編指令;查看 C 編譯器生成的匯編指令是如何尋址這些局部變量的,你自己編寫的行內匯

編指令就采用同樣的尋址方式。例如,例 11-12 的一小段 C 原代碼編譯出的匯編指令

//C 原程序代碼

void Test(unsigned char inVar1, inVar2) 

{

   unsigned char tmp1, tmp2;

inVar1++;

inVar2--;

tmp1 = 1;

tmp2 = 2;

}

//編譯器生成的匯編指令

_Test

;      _tmp1 assigned to ?a_Test+0      //tmp1 的尋址符為  ?a_Test+0


_Test$tmp1 set


?a_Test


;      _tmp2 assigned to ?a_Test+1      //tmp2 的尋址符為  ?a_Test+1


_Test$tmp2 set


?a_Test+1


;      _inVar1 assigned to ?a_Test+2     //inVar1 的尋址符為  ?a_Test+2

_Test$inVar1 set ?a_Test+2

44line


;_inVar1 stored from w  

bcf

bcf

MOVwf ?a_Test+2


//第一個字符型行參由 W 寄存器傳遞


;ht16.c: 43: unsigned char tmp1, tmp2;

incf

45line

;ht16.c: 45: inVar2--;


decf

46line

;ht16.c: 46: tmp1 = 1;

clrf

incf

47line

;ht16.c: 47: tmp2 = 2;

MOVlw 2

MOVwf ?a_Test+1

48line

;ht16.c: 48: }

return


//行參 inVar2 的尋址符為 ?_Test


例 11-12  PICC 實現局部變量操作的尋址方式

基于上面得到的 PICC 編譯后局部變量的尋址方式,我們在 C 語言程序中用嵌入匯編指

令時必須采樣同樣的尋址符以實現對應變量的存取操作,見下面的例 11-13。

//C 原程序代碼

void Test(unsigned char inVar1, inVar2) 

{

   unsigned char tmp1, tmp2;

#asm

//開始嵌入匯編


incf

decf

MOVlw

addwf

rrf

0x10


?a_Test+0,f

?a_Test+1,f

?a_Test+2,f

?_Test,w


//tmp1++;

//tmp2--;

//inVar1 +=

//inVar2 循環(huán)右移一位

}


rrf

#endasm


?_Test,f

//結束嵌入匯編


例 11-13     嵌入匯編指令實現局部變量尋址操作

 

如果局部變量為多字節(jié)形式組成,例如整型數、長整型等,必須按照 PICC 約定的存儲

格式進行存取。前面已經說明了 PICC 采用“Little endian”格式,低字節(jié)放在低地址,高字

節(jié)放在高地址。下面的例 11-14 實現了一個整型數的循環(huán)移位,在 C 語言中沒有直接針對循

環(huán)移位的語法操作,用標準 C 指令實現的效率較低。

//16 位整型數循環(huán)右移若干位

unsigned int RR_Shift16(unsigned int var, unsigned char count) 

{

}


while(count--)

{

   #asm 

   rrf  ?_RR_Shift16+0,w

   rrf  ?_RR_Shift16+1,f

   rrf  ?_RR_Shift16+0,f

#endasm

}

return(var);


//移位次數控制

//開始嵌入匯編

//最低位送入 C

//var 高字節(jié)右移 1 位,C 移入最高位

//var 低字節(jié)右移 1 位

//結束嵌入匯編

//返回結果


例 11-14      嵌入匯編指令對多字節(jié)變量的操作

11.9.4     混合編程的一些經驗

C  和匯編語言混合編程可以使單片機應用程序的開發(fā)效率和程序本身的運行效率達到

最佳的配合。筆者從實際應用中得到一些經驗供讀者一起分享。

㈠  慎用匯編指令

 

相比于匯編語言,用 C  語言編程的優(yōu)勢是毋庸置疑的:開發(fā)效率大大提高、人性化的

語句指令加上模塊化的程序易于日常管理和維護、程序在不同平臺間的移植方便。所以既然

用了 C 語言編程,就盡量避免使用嵌入匯編指令或整個地編寫匯編指令模塊文件。PICC 已

具備高效的優(yōu)化功能,如果在寫 C 原程序時就十分注意程序的編譯和運行效率問題,加上

PICC 的后道編譯優(yōu)化,最后得到的代碼效率不會比全部用匯編編寫的代碼差多少,尤其是

程序量較大時。另外,PICC 對數據存儲空間的利用率肯定比用戶人工定位變量時的利用率

要高,同時還提供完整的庫函數支持。C 語言的語法功能強大,能夠高效率地實現絕大部分

控制和運算功能。因此,除了一些十分強調單片機運行時間的代碼或 C 語言沒有直接對應

的操作可以考慮用匯編指令實現外,其它部分都應該用 C 語言編寫。

 

以上面的例 11-14 進一步說明,變量的循環(huán)右移操作用 C 語言實現非常不方便,PIC 單

片機已有對應的移位操作匯編指令,因此用嵌入匯編的形式實現效率最高。同時對移位次數

的控制,本質上說變量 count 的遞減判零也可以直接用匯編指令實現,但這樣做節(jié)約不了多

少代碼,用標準的 C 語言描述更直觀,更易于維護。

 

一句話:用了 C 語言后,就不要再老想著用匯編。

 

㈡  盡量使用嵌入匯編

 

這和上面的慎用匯編指令的說法并不矛盾。如果確實需要用匯編指令實現部分代碼以提

高運行效率,應盡量使用行內匯編,避免編寫純匯編文件(*.as 文件)。

 

雖然 PICC 支持 C 和匯編原程序模塊存在于同一個項目中,但要編寫純匯編文件必須首

先了解 PICC 特有的匯編語法結構。Hitech 公司提供了完整的文檔介紹其匯編器的使用方法,

有興趣者可以從其網站上下載 PICC 的用戶使用手冊查看。

 

筆者認為,類似于純匯編文件的代碼也可以在 C 語言框架下實現,方法是基于 C 標準

語法定義所有的變量和函數名,包括需要傳遞的形式參數、返回參數和局部變量,但函數內

部的指令基本用嵌入匯編指令編寫,只有最后的返回參數用 C 語句實現。這樣做后函數的

運行效率和純匯編編寫時幾乎一模一樣,但各參數的傳遞統(tǒng)一用  C 標準實現,這樣管理和

維護就比較方便。例如下面的例 11-15 實現一個字節(jié)變量的偶校驗位計算。

bit EvenParity(unsigned char data)

{

    #asm


    swapf   ?a_EvenParity+0,w

    xorwf   ?a_EvenParity+0,f

    rrf     ?a_EvenParity+0,w

    xorwf   ?a_EvenParity+0,f

    btfsc   ?a_EvenParity+0,2

    incf    ?a_EvenParity+0,f

    #endasm

    //至此,data 的最低位即為偶校驗位

    if  (data&0x01)  return(1);

    else  return(0);

}


//入口參數 data 的尋址符為 ?a_EvenParity+0


例 11-15  C 函數框架中使用嵌入匯編指令

㈢  盡量使用全局變量進行參數傳遞

 

使用全局變量最大的好處是尋址直觀,只需在 C 語言定義的變量名前增加一個下劃線

符即可在匯編語句中尋址;使用全局變量進行參數傳遞的效率也比形參高。編寫單片機的 C

程序時不能死硬強求教科書上的模塊化編程而大量采用行參和局部變量的做法,在開發(fā)編程

時應視實際情況靈活變通,一切以最高的代碼效率為目標。

  • 上一篇: PICC18使用說明
  • 下一篇: 基于16F877A和DS18B20的測溫程序
  • 發(fā)表評論   告訴好友   打印此文  收藏此頁  關閉窗口  返回頂部
    熱點文章
     
    推薦文章
     
    相關文章
    網友評論:(只顯示最新5條。)
    關于我們 | 聯(lián)系我們 | 廣告合作 | 付款方式 | 使用幫助 | 機電之家 | 會員助手 | 免費鏈接

    點擊這里給我發(fā)消息66821730(技術支持)點擊這里給我發(fā)消息66821730(廣告投放) 點擊這里給我發(fā)消息41031197(編輯) 點擊這里給我發(fā)消息58733127(審核)
    本站提供的機電設備,機電供求等信息由機電企業(yè)自行提供,該企業(yè)負責信息內容的真實性、準確性和合法性。
    機電之家對此不承擔任何保證責任,有侵犯您利益的地方請聯(lián)系機電之家,機電之家將及時作出處理。
    Copyright 2007 機電之家 Inc All Rights Reserved.機電之家-由機電一體化網更名-聲明
    電話:0571-87774297 傳真:0571-87774298
    杭州濱興科技有限公司提供技術支持

    主辦:杭州市高新區(qū)(濱江)機電一體化學會
    中國行業(yè)電子商務100強網站

    網站經營許可證:浙B2-20080178-1