官术网_书友最值得收藏!

第4章 C語言的嵌入式編程

本章首先通過編程語言的選擇問題介紹C語言編程的優點,然后討論C語言進行程序設計時涉及的一些問題,并簡要介紹了Freescale公司的單片機開發工具——CodeWarrior的使用方法。

4.1 編程語言的選擇

為了確定嵌入式系統合適的編程語言,需要了解以下問題:

① 計算機(如微控制器、微處理器或DSP芯片等)只接收“機器碼”(即目標代碼)指令。如果嚴格定義,機器碼才是計算機的語言,而不是程序員使用的其他語言。但如果由程序員去解釋機器碼,則工作量是非常巨大的,而且也容易出錯,是不可行的。

② 所有的軟件,例如匯編語言、C語言、C++語言、Java語言等,為了能夠被計算機執行,最終都必須翻譯成機器碼。

③ 嵌入式處理器的功能有限且內存有限,所以編程語言必須具有高效率。

④ 為嵌入式系統編程,經常需要對硬件進行底層訪問操作,這意味著至少要能夠讀寫特定的存儲器地址。

當然,語言的選擇問題還有一些并非技術方面的考慮:

① 如果每個項目開發都從頭編寫代碼,顯然軟件程序員是不樂意的。編程語言必須能夠支持創建靈活方便的庫,這樣同類的項目可以重用那些經過充分測試的代碼模塊。當使用新的處理器或升級處理器時,整個代碼系統移植到新系統應該是可行的,并且工作量盡可能少。

② 語言的選擇應該具有通用性。這樣才能保證比較容易產生更多的有經驗的開發人員,而且開發人員也容易獲得相關設計實例以及編程實踐信息。

③ 隨著系統和處理器的不斷升級,程序代碼往往需要經常進行維護。好的程序代碼應該是容易被理解的,而且并不僅僅容易被開發者理解,同時程序代碼的維護、升級也應該非常便利。

基于上述原因,我們需要的編程語言應該是:效率高的高級語言,能夠訪問底層硬件,并且是良性定義的。同時,該語言也支持我們想要開發使用的平臺。綜合考慮這些因素,C語言是非常合適的。

可以總結C語言的特性如下:

① 它屬于“中級語言”,不僅具有“高級語言”的特征(如支持函數和模塊),還有“低級語言”的特性(可以通過指針訪問硬件);

② 編程效率很高;

③ 十分流行且容易理解;

④ 即使是PC程序員,以前只使用過Java或C++語言,也能夠很快理解C語言的語法和編程方法;

⑤ 每一個嵌入式處理器(從8位到32位或以上)都有良好且得到充分驗證的C編譯器;

⑥ 容易找到C語言編程經驗的開發人員;

⑦ 容易找到有關資料、培訓課程及相關網站等技術支持。

但是有很多程序員還是對匯編語言情有獨鐘。是不是因為C語言的存在,就不需要再使用匯編語言編寫程序了呢?答案是否定的,因為還是有一部分程序必須使用匯編語言來編寫的,以下是需要使用匯編語言編寫程序的一部分情況說明。

系統的初始化,包括所有應用程序寄存器的初始化,各端口、各寄存器位在系統中的定義,棧指針的設置等,都需要使用匯編語言編寫,以建立C語言程序運行的環境。

中斷向量的初始化、中斷服務的入口和出口、開關中斷等,也需要使用匯編語言編寫,而中斷服務本身可以用C語言編寫,而用匯編語言調用C語言程序運行的環境。

用匯編語言編寫輸入/輸出口的輸入/輸出函數,在C語言程序中再調用這些函數。

通常可以用匯編語言編寫與硬件有關部分的程序,用C語言編寫與硬件無關部分的程序。如果同時使用了匯編語言和C語言編寫程序,處理好兩部分程序之間的參數傳遞是非常關鍵的。

對于單片機系統,與硬件相關部分的程序量不會很大,一般不會超過2 KB的機器碼。如果整個應用程序大于4 KB,則使用C語言編寫應用程序更合適。應用程序越大,使用高級語言的好處就越明顯,不必擔心C語言的效率或者運行速度問題。

4.2 C語言編程元素

4.2.1 全局變量和局部變量

變量是程序運行時在內存中存放數據的一個存儲空間。對嵌入式系統來說,它是RAM或ROM(甚至是處理器的寄存器)上的存儲單元。全局變量是為整個程序定義的,在程序運行中始終有效。用全局變量傳遞參數,是參數傳遞的常用方法。局部變量是在某個函數內部聲明的變量,它只能被該函數訪問。在嵌入式系統中,局部變量通常位于堆棧中。全局變量和局部變量的區別取決于在程序中的什么位置聲明它。全局變量必須在函數外部聲明,而局部變量則必須在一個函數內部聲明。

由于程序是固化在ROM中的,而不是下載到RAM中的。除非在應用程序運行開始后向RAM中下載什么,RAM中的內容在開機時是隨機的。這就要求在用C語言開發嵌入式應用程序時不要使用初始化變量。

當希望在多個源文件中共享變量時,需要確保定義和聲明的一致性。最好的安排是在某個相關的.c文件中定義,然后在.h頭文件中進行外部聲明,在需要使用的時候,只要包含對應的頭文件即可。定義變量的.c文件也應該包含該頭文件,以便編譯器檢查定義和聲明的一致性。

4.2.2 頭文件

通常在一個程序的開始部分進行頭文件包含操作。頭文件通常包括常量定義、變量定義、宏定義和函數聲明等,程序員可以在自己的程序中嵌入它們。內嵌庫中最常見的頭文件是標準輸入/輸出文件(stdio.h),該頭文件包含用于輸出信息和接收用戶鍵盤輸入的函數聲明。在很多情況下,出于特定系統要求,程序員通常需要創建自己的頭文件,并將它們包含在程序中。

要包含一個頭文件,必須在程序的開始部分使用編譯預處理指令#include,這會在4.2.3節中詳細論述。

另外一個有關頭文件的問題:可以在一個頭文件中包含另一個頭文件嗎?這是一個風格問題,引發了不少的爭論。它讓相關定義更難找到,更主要的是,如果一個文件被包含的兩次,它會導致重復定義錯誤。關于這個問題的解決,也會在4.2.3節中給出答案。

4.2.3 編譯預處理

1.用于包含文件的#include指令

任何C程序首先都要包含那些準備使用的頭文件和源文件,include是一個用于包含某個文件內容的預處理指令。以下給出可以被包含的文件:

● 包含代碼文件:這些文件是已經存在的代碼文件。

● 包含常量數據文件:這些文件是代碼文件,可以有擴展名.const。

● 包含字符串數據文件:這些文件是包含字符串的文件,可以帶擴展名.string、.str或者.txt。

● 包含初始數據文件:這些文件用于嵌入式系統掩模只讀存儲器的初始或默認數據,啟動程序運行后會被復制到RAM當中,可以具有擴展名.init。

● 包含基本變量文件:這些文件是存儲在RAM中的全局或者局部靜態變量文件,因為它們不具有初始(默認的)值,所謂靜態的意思是變量只有一個普通的變量地址實例,這些基本變量都被存儲在以.bss為擴展名的文件中。

● 包含頭文件:這是一個預處理指令,目的是要包含一組源文件的內容(代碼或者數據)。它們都是某個特定模塊的文件。頭文件的擴展名為.h。

對于我們而言,include指令是最常用的包含頭文件,其格式為

          #include <stdio.h>
          #include <math.h>
          #include "myheaderfile.h"

第1行和第2行語句告訴編譯器包含標準輸入/輸出庫和數學函數的頭文件。<>符號表示編譯器在指定位置頭文件。第3行的語句包含了自己創建的頭文件myheaderfile.h," "表示在當前源文件所在目錄下查找這個頭文件。

2.宏定義#define指令

C語言中允許用一個標識符來表示一個字符串,稱為宏。被定義為宏的標識符稱為宏名。在編譯預處理時,對程序中所有出現的宏名,都用宏定義中的字符串去代換,這稱為宏代換或宏展開。宏定義是由源程序中的宏定義命令完成的,宏代換是由預處理程序自動完成的。宏分為有參數和無參數兩種。

(1)無參宏定義

無參宏的宏名后不帶參數。其定義的一般形式為

          #define   標識符   字符串

其中,標識符為所定義的宏名;字符串可以是常數、表達式、格式串等。例如

          #define   HIGH    100
          #define   M       (a+b)

第一句定義符號HIGH為常數100。第二句定義標識符M來代替表達式(a+b)。在編寫源程序時,所有的(a+b)都可由M代替,而對源程序作編譯時,將先由預處理程序進行宏代換,即用(a+b)表達式去置換所有的宏名M,然后再進行編譯。

(2)帶參宏定義

C語言允許宏帶有參數。在宏定義中的參數稱為形式參數,在宏調用中的參數稱為實際參數。對帶參數的宏,在調用中,不僅要宏展開,而且要用實參去代換形參。

帶參宏定義的一般形式為

          #define   宏名(形參表)   字符串

帶參宏調用的一般形式為

            宏名(形參表)

例如,程序1

              #define  M(y)   y*y+3*y          //宏定義
                  ....
              k=M(5);                       //宏調用

在宏調用時,用實參5去代替形參y,經預處理,宏展開后的語句為

          k=5*5+3*5

程序2

          #define  MAX(a,b)   (a>b)?a:b      //宏定義

符號MAX是宏名稱,而第二部分(a>b)?a:b定義了這個宏,可以在a和b中選擇一個較大值作為輸出結果。

對于宏定義再做以下幾點說明:

① 宏定義是用宏名來表示一個字符串,在宏展開時又以該字符串取代宏名,這只是一種簡單的代換,字符串中可以含任何字符,可以是常數,也可以是表達式,預處理程序對它不作任何檢查。如有錯誤,只能在編譯已被宏展開后的源程序時發現。

② 宏定義不是說明或語句,在行末不必加分號,如加上分號則連分號也會一起置換的。

③ 宏定義必須寫在函數之外,其作用域為宏定義命令起到源程序結束,如果要終止其作用域可使用#undef命令。

最后再提一點,關于typedef和#define的區別,有時用戶對于這兩者的使用會有些迷茫。typedef關鍵字盡管在語法上是一種存儲類型,但正如其名稱所示,它用來定義新的類型名稱,而不是定義新的變量或函數。對于用戶定義類型,這兩者都可以使用,但一般來說,最好使用typedef,部分原因是它能正確處理指針類型。例如,考慮以下聲明

          typedef  char  *String_t;
          #define  String_d  char*
          String_t  s1, s2 ,s3;
          String_d  s4;

對于這個例子,s1、s2和s3都被定義成了char *,但s4卻被定義成了char型。

3.條件編譯指令

條件編譯指令包括#if、#ifdef、#ifndef、#else、#elif和#endif。這些指令用于根據某個表達式有條件的編譯一部分代碼。可以僅在程序開發過程中利用這些指令來編譯部分調試代碼。

指令#if和#endif用于選擇性地編譯某段代碼。#if后的表達式值為TURE或FALSE。如果是TRUE,#if和#endif之間的所有代碼將被編譯;否則,這些代碼將被忽略。

#else和#elif指令可以用于更靈活的選擇編譯功能,它們也必須同#if和#endif一起使用。

例如編寫的程序希望在多個嵌入式處理器平臺上執行,就可以通過上述指令選擇處理器中的某一個。

          #define  MC9S08  1
          #define  MC9S12  2
          #define  AT89C51  3
          #define  Processor  2
          void main(void)
          {
              #if Processor == MC9S08
                Instructions  A
          #elif Processor = = MC9S12
              Instructions  B
          #elif  Processor==AT89C51
              Instructions  C
          #else
              Instructions  D
          #endif
      }

上述代碼通過#if、#elif、#else和#endif定義了三種不同的嵌入式處理器,并選擇了其中MC9S12。當判斷出Processor的值是MC9S12,則“elif Processor == MC9S12”后面的指令被編譯,器與的代碼將被跳過。

還有兩條指令#ifdef和#ifndef。#ifdef表示如果宏已經定義,則編譯下面代碼;#ifndef表示如果宏沒有定義,則編譯下面代碼。前面提到的放置頭文件被多次包含的問題,這里可以得到解決了。

          #ifndef  __X_H__
          #define  __X_H__
          // … 頭文件x.h的其余部分
          #endif

這樣每一個頭文件都使用了一個獨一無二的宏名,這令頭文件可以自我識別,以便可以被安全地多次包含。

4.2.4 數據類型

數據命名后,就會在存儲器中分配地址,地址分配取決于數據類型。以Freescale公司的CodeWarrior開發工具為例,其中定義的數據類型如表4.1所示。

表4.1 CodeWarrior中規定的基本數據類型

對于表4.1中的float和double兩種類型的操作需要很多條指令,所以建議盡量避免在S12單片機中使用這兩種類型。

除了上述幾種基本數據類型外,C語言還有以下5種數據元素:

● array(數組):一組類型相同的數據元素;

● pointer(指針):存儲某種數據類型的地址的變量;

● structure(結構):一組類型不同的數據元素;

● union(聯合):由兩種不同數據類型共享的存儲元素;

● function(函數):函數本身也可以作為一種數據類型。

在聲明變量的時候,可以規定變量的訪問/存儲類型,C語言有6個訪問/存儲關鍵字:extern、auto、static、register、const和volatile。

① extern說明該變量在另一個目標代碼文件中聲明和定義過。這些變量可以被所有函數訪問。

② auto是默認的存儲類型,在一個代碼塊內(或在一個函數頭部作為參量)聲明的變量,無論有沒有訪問/存儲關鍵字auto,都屬于自動存儲類。該類具有自動存儲時期、代碼塊的作用域和空鏈接,如果未初始化,它的值是不確定的。在S12單片機中,這種類型的變量存放在棧中。一旦某個函數(一段程序)結束任務,這些用auto聲明的變量將從棧中清除,不再有效。另外,只有聲明這種變量的函數才有權訪問該變量。

③ static存儲類型與auto類型類似,但它存儲在RAM中而不是棧中,因此它在程序運行的整個過程中都有效。在C語言中,關鍵字static有三個明顯的作用:在函數體,一個被聲明為靜態的變量在這一函數被調用過程中維持其值不變;在模塊內(但在函數體外),一個被聲明為靜態的變量可以被模塊內所用函數訪問,但不能被模塊外的函數訪問,它是一個本地的全局變量;在模塊內,一個被聲明為靜態的函數只可被這一模塊內的其他函數調用,也就是說,這個函數被限制在聲明它的模塊的本地范圍內使用。

④ register聲明的變量表明要求編譯器使用(如果可能)微處理器中的一個寄存器來存放該變量。使用微處理器的寄存器存儲一個變量可以減少總線訪問存儲單元的時間、加速程序的運行。因此,若某個變量在程序中需要經常訪問,可以考慮這種存儲類型。

如果某個變量的值在程序運行中保持不變,則可以用const類型來聲明它,該變量通常存放在ROM中。一個const變量必須由程序員初始化。有的程序員認為“const意味著常數”,這種說法其實有一些問題,有一種理解認為const意味著“只讀”,這種理解更準確。下面說明以下幾個聲明的含義。

          const int a;
          int const a;
          const int *a;
          int * const a;
          int const * a const;

前兩個聲明的作用是一樣的,a是一個常整型數;第3個聲明意味著a是一個指向常整型數的指針(也就是,整型數是不可修改的,但指針可以);第4個聲明意味著a是一個指向整型數的常指針(也就是說指針指向的整型數是可以修改的,但指針是不可修改的);最后一個聲明意味著a是一個指向常整型數的常指針(也就是說,指針指向的整型數是不可修改的,同時指針也是不可修改的)。

⑤ const有以下作用:關鍵字const的作用是為給讀代碼的人員傳達非常有用的信息,實際上,聲明一個參數為常量是為了說明這個參數的應用目的;為優化器提供一些附加的信息,使用關鍵字const也許能產生更緊湊的代碼;合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,簡而言之,這樣可以減少bug的出現。有關const的所有用法,建議讀者參考Dan Saks的文章“const T vs.T const”。

⑥ volatile訪問類型表示它所聲明的變量值在程序運行中可能不經過相關指令就發生變化。在S12單片機中,當某個變量的值被硬件輸入端口改變時,這些變量應該用volatile聲明。S12中端口的聲明也用到了volatile,比如對PORTB的地址定義為

          #define PORTB (*((volatile unsigned char *)(0x0001)))

一般來說,volatile用在如下幾個地方:中斷服務程序中可能會修改的供其他程序檢測的變量需要加volatile、多任務環境下各任務間共享的標志應該加volatile、存儲器映射的硬件寄存器通常也要加volatile,因為每次對它的讀寫都可能由不同含義。

下面結合volatile的討論對之前提出的const是一個“只讀”量給一個例子說明,比如S12單片機中某個只讀的狀態寄存器,它需要volatile限定詞是有可能被意想不到地改變;而使用const是因為程序不應該試圖去修改它。

4.2.5 運算符

C語言有很多種運算符,如表4.2所示,C語言的運算符可以分為5大類:通用運算符、算術運算符、邏輯運算符、位操作運算符和一元運算符。

表4.2 C語言運算符

畢竟本書不是關于C語言的教材,所以對于一些常用運算符,這里就不作解釋了。本書主要對嵌入式程序設計中比較常用而普通C語言程序設計中又較少使用的位運算符作一簡要說明。

嵌入式系統總是要用戶對變量或寄存器進行位操作,而C語言的一個優勢就是它可以對某個存儲單元的內容進行位操作,如按位與、按位或和按位異或。移位運算符常用來將一個數乘以或除以2的冪運算。注意此處的除法是嚴格的整除而不是浮點除法。

如果希望將一個字節中某些位清0,而其他位保持不變,可以使用按位與運算。例如給定一個無符號字符型變量a,要求清除a的bit 3,可以使用下列語句。

          a &= 0xF7;

如果希望將一個字節中的某些位置1,而其他位保持不變,可以使用按位或運算。例如給定一個無符號字符型變量a,要求置位a的第3位,可以使用下列語句。

          a |= 0x08;

如果希望對某些位取反,而其他位保持不變,最好的方式是使用按位異或運算。例如給定一個無符號字符型變量a,要求將a的第7位取反,可以使用下列語句。

          a ^= 0x80;

4.2.6 指針

指針是存放其他變量地址的變量。例如,一個字符型變量指針存放的是該字符變量的地址。聲明一個指針變量的格式與聲明一個變量的格式相同,只是在變量名前加一個*運算符。例如

          unsigned char *a;

變量a被聲明為一個存放無符號字符型變量的起始地址。指針可以存放各種類型變量的地址,如字符、字符數組、整型、整型數組、單精度浮點或雙精度浮點等。由于指針存放的是一個變量的地址,所以必須確保這個指針存放的確實是一個內存地址。

前面已經舉例說明過,在S12單片機中,如果需要處理輸入或輸出端口時,使用指針很方便,例如前面舉過的例子:

          #define PORTB (*((volatile unsigned char *)(0x0001)))

指針在C語言中的另一個重要應用是動態內存分配。動態內存分配與我們見到的其他內存分配方式不同,區別在于動態內存分配的存儲單元在程序的運行過程中才確定。這些分配的內存通常來自RAM中未被使用的部分,我們稱這一部分為堆。動態內存分配常常用在不知道RAM的大小又想充分利用RAM的資源的情況下。動態內存分配的兩個主要函數是malloc()和free()。malloc()函數用于分配內存空間,而free()用于釋放被分配的內存空間。兩個函數的格式是

          void *malloc(sizeof(variable));
          void free(void *ptr);

malloc()函數返回一個可分配給任何指針變量的指針。如果申請的內存空間是可用的,返回申請空間的第一個存儲單元的地址;如果申請失敗,則返回空(NULL),所以一定要記住在使用任何指針前必須判斷它是否為空。

在嵌入式系統中程序設計中,程序員經常面臨者要求去訪問某特定的內存位置的情況。此時可以利用指針方便的實現這個要求。例如,要求設置一絕對地址為0x67A9的整型變量的值為0xAA66。可以用下面的代碼完成這一任務。

          int *ptr;
          ptr = (int *)0x67A9;
          *ptr = 0xAA66;

4.2.7 條件語句、循環語句及無限循環語句

1.條件語句

條件語句在程序中會經常多次使用。如果某個定義的條件能夠被滿足,那么執行緊跟在條件語句之后的大括號內的語句(或者不帶大括號的語句),否則程序會轉到下一條語句或者轉到另一組語句中執行。條件語句可以分為if語句和switch語句兩大類。

if語句的一般形式為

          if (<表達式>)
          {
              語句體;
          }

執行過程為首先計算<表達式>的值,如果該表達式的值為非零(TRUE),則執行其后的if子句,然后去執行if語句后的下一個語句;如果該表達式的值為零(FLASE),則跳過if子句,直接執行if語句后的下一個語句。

if-else語句的格式為

          if (<表達式>)
          {
              語句體1;
          }
          else
          {
              語句體2;
          }

執行過程為首先計算<表達式>的值,如果該表達式的值為非零(TRUE),則執行<語句體1>;如果該表達式的值為零(FLASE),則跳過<語句體1>,直接執行<語句體2>,兩者執行其一后再去執行if語句后的下一個語句。

if-else if-else語句的格式為

          if (<表達式1>)
          {
              語句體1;
          }
          else if (<表達式2>)
          {
              語句體2;
          }
          else
          {
              語句體3;
          }

執行過程為首先計算<表達式1>的值,如果該表達式的值為非零(TRUE),則執行<語句體1>;如果該表達式的值為零(FLASE),則跳過<語句體1>,計算<表達式2>的值;如果該表達式的值為非零(TRUE),則執行<語句體2>;如果該表達式的值為零(FLASE),則跳過<語句體2>,直接執行<語句體3>,三者執行其一后再去執行if語句后的下一個語句。

條件語句還有一種語句是switch,其格式為

          swtich (<表達式>)
          {
              case <常量表達式1>: <語句序列1>;break;
              case <常量表達式2>: <語句序列2>;break;
              case <常量表達式3>: <語句序列3>;break;
              ……
              case <常量表達式n>: <語句序列n>;break;
              [default:<語句序列n+1>]
          }

當根據某個條件判斷有多個可執行的分支時,應使用switch結構。switch-case的執行效率相對if-else較高,因為switch語句會生成一個跳轉表來指示實際的case分支的地址,而這個跳轉表的索引號與switch變量的值是相等的。switch不用像if-else那樣遍歷條件分支直到命中條件,而只需訪問對應索引號的表項從而達到定位分支的目的。總體上說,switch語句執行效率要高于同樣條件下的if-else,特別是當條件分支較多時。但switch語句占用較多的代碼空間,因為它要生成跳轉表,特別是當case常量分布范圍很大但實際有效值又比較少的情況,使得switch的空間利用率變得很低,還有就是switch語句只能處理case為常量的情況,對非常量的情況是無能為力的。例如,“if (a > 1 && a< 100)”是無法使用switch語句來處理的。

2.循環語句

C語言有三種不同的循環結構:for循環、while循環和do-while循環。

for循環的開頭包含一條初始化語句、一個循環條件判斷和一條更新語句。在更新語句后是一組指令組成的循環體,這組指令在循環條件滿足之前重復執行。

for循環語句格式為

          for (<初始化表達式>;<條件表達式>;<更新表達式>)
          {
              <循環體語句>
          }

while循環與for循環類似,都是重復執行循環體內的指令,但它在while后只有一個循環終止條件。

while循環語句格式為

          while (<表達式>)
          {
              <循環體語句>
          }

do-while循環基本上和while循環完全一樣,唯一的區別是do-while循環先執行循環體語句,再判斷終止條件。所以即使<表達式>的值為零(FLASE),循環體語句還是會被執行一次。

          do
          {
              <循環體語句>
          }
          while (<表達式>)

3.無限循環語句

使用C語言總是會被提醒無限循環是不希望發生的,因為這意味著程序永遠都不會結束,但是無限循環是嵌入式系統編程的一個特征。無限循環的硬件等價于一個系統時鐘(實時時鐘)或者一個正在運行的空閑計時器。程序中的main()函數就是一個大循環,其間有對函數進行的調用以及對中斷的處理等程序,而最終它必須回到起始處,系統主程序永遠都不能出現停止狀態。

實現無限循環可以使用以下幾種方案。

方案1

          while(1)
          {
              ……
          }

方案2

          for ( ; ; )
          {
              ……
          }

方案3

          Loop:
            ……
          goto Loop;

既然這里提到了goto語句,就要再多說幾句。有的人談到goto語句總是覺得永遠都不該使用它。對于goto語句,很早以前人們注意到隨意使用goto語句會很容易導致難以維護的混亂代碼。然而,不經思考就簡單禁止goto的使用并不能立即得到優美的程序。一個無規則的程序員也許使用奇怪的嵌套循環和布爾變量來取代goto,一樣能構造出復雜難懂的代碼。所以說程序設計風格就像寫作風格一樣,不能被僵化的教條所束縛,雖然并不提倡使用像goto這樣破壞程序可維護性的語句,但是有規劃的編寫程序代碼更為重要。

4.2.8 函數

本節給出函數的定義,并介紹它們在C語言中的作用,并將討論函數間參數的傳遞。

1.函數定義

函數是完成某個特定任務的一段獨立代碼,它必須具備三個特征:獨立性、靈活性、可移植性。一個函數必須獨立于程序的其他代碼,因為函數可以被不同的用戶調用。例如要求編寫一個函數濾除輸入信號中的噪聲(過濾)。你的程序將會被許多不同的程序調用,需要從不同的信號中去除某種噪聲。該函數必須獨立于使用該函數的程序,并且必須足夠靈活。如果編寫的去除噪聲的函數只能去除某個預先設定的頻率段內的噪聲,并且這個頻率段范圍無法改變,那么這個函數的價值就非常有限,尤其是與同頻率范圍用戶指定的函數相比。最后,函數還必須是可移植的。C語言給工業界帶來的革命就是提供可以在不同的硬件平臺上使用同一段代碼的能力,一旦某段代碼編寫完畢,只要有軟件(編譯器/匯編器)將其轉換為相應的機器代碼,它就應該能在不同的硬件上運行。要保證一個函數是可移植的,必須使這段代碼與程序的其他部分無關,也就是第一條獨立性的要求。

2.主程序

主程序也是一種函數,區別在于當程序名被調用時,這個函數首先被執行。主程序是程序執行的管理者,它包含程序的總體結構,通過調用其他不同的函數來處理、完成具體的任務,并因此避免親自處理這些任務。可以把主函數想象成一個在其他函數的幫助下控制各種命令執行的管理者。在主程序中定義了多程序調用,調用它們循環順序執行的模型如圖4.1所示。

圖4.1 主程序中多程序調用的程序模型

3.函數原型

函數在被調用前必須在程序的頭部聲明,這些聲明稱為函數原型。

函數聲明的格式為

          返回類型  函數名 ([變量類型1 變量名1],
                          [變量類型2 變量名2],
                              ……
                          [變量類型n 變量名n]);

其中,變量名是可選的,需要注意的是函數聲明最后必須有分號。以下給出幾個函數聲明的例子。

          int  compute(int,int);
          float  change(char name,float number,int a);
          double  find(unsigned int,float,double);

有時還會遇到以關鍵字extern作為函數聲明的情況,例如

          extern  not_here(int a,int b,int c);

extern關鍵字通知編譯器not_here函數的聲明不再這個源文件中,而是在另一個外部的程序中。

一旦函數在源文件的頭部聲明過,就必須在同一個源文件或其他將要與該源文件鏈接的源文件(庫)中定義它們,下面就討論一下函數的定義。

4.函數定義

當某個函數在一個源文件的開始部分聲明后,程序員必須在該源文件或其他伴隨的源文件中定義這個函數。函數還可以在伴隨的庫文件中定義。函數定義可以在程序的任何地方進行,但通常都位于主程序后。除了末尾沒有分號外,函數定義與函數聲明的格式差不多。例如,假設前面聲明的compute函數接受兩個輸入參數作為一個向量,計算該向量的長度并返回該長度值。該函數可以作如下定義:

          int  compute(int a,int b)
          {
              int  sum,result;
              sum = a*a + b*b;
              result = (int)(sqrt(sum));
              return(result);
          }

每個函數必須用正確的格式來定義:定義的開頭是一條包含返回類型和參數的語句,中間是完成該函數功能的語句,最后是一條返回語句。如果返回類型是void,最后的返回語句可以沒有。當然,嚴謹的程序員堅持認為在這種情況下也應該加一句“return( )”。

5.函數調用

在程序地任何地方,都可以用函數名和位于一對圓括號中的參數來調用某個函數。仍然以計算向量的長度為例,可以使用如下方式調用該函數。

          magnitude = compute (12, 24);

6.函數間參數的傳遞

在函數的調用過程中,調用者,即函數的觸發者可以向該函數傳遞多個參數。以上面一個例子作為參考,調用compute函數時,需要傳遞兩個整型參數,因為該函數在定義和聲明時就已經確定了兩個參數。

如果單純使用C語言編寫程序,可以不必關心函數間參數是如何傳遞的。但是如果希望匯編語言編寫的子程序和C語言編寫的函數實現相互調用,則匯編語言編寫的子程序與C語言編寫的函數應該具有相同的格式,這樣匯編語言程序就可以在C程序中被調用,同樣匯編語言程序也可以調用C的函數。必須徹底弄清楚函數的結構和參數傳遞方法,才能使匯編語言編寫的子程序符合C語言的函數格式,這里提供一種分析方法。

在C程序中,參數都是通過堆棧傳遞的,使用的C編譯器不同,參數進入堆棧的順序以及最后一個參數或第一個參數保存在什么地方也會有所不同,故與匯編語言程序的接口方式也會不同。在開發嵌入式應用程序中,因不可避免地會使用到匯編語言,故使用一個新的C交叉編譯工具軟件時,首先要搞清楚匯編語言程序和C程序之間是如何傳遞參數的。下面的例子是分析說明不同編譯器是如何處理參數傳遞的,這里以Freescale公司的CodeWarrior編譯器為例分析說明。

在C中定義以下函數

          int Add(int a, int b, int c)
          {
              return(a+b+c);
          }

編譯器生成如下匯編代碼

          PSHD
          LDD   6,SP
          ADDD  4,SP
          ADDD  0,SP
          PULX
          RTS

在C中調用上面定義的函數

          d = Add(1, 3, 5);

編譯器生成如下匯編代碼

          LDAB   #1
          CLRA
          PSHD
          LDAB   #3
          PSHD
          LDAB   #5
          BSR   Add
          LEAS   4,SP

現在,如果用匯編語言寫一個子程序,要求可以在C語言程序中調用,使用CodeWarrior交叉C編譯器,歸納起來可以得到參數傳遞的以下規則。

(1)返回參數

對于函數返回的參數,相當于return(n)中的n值,如果n是一個單字節數據(char),則在B寄存器中,即D寄存器的低字節;如果n是一個雙字節數據(int),在D寄存器中,低字節在B寄存器中,高字節在A寄存器中,對于返回值n是其他數據類型,則返回一個指向n的指針,也在D寄存器中。

(2)定義函數

定義函數時,如果只有一個形式參數,C程序會默認該參數已經放在D寄存器中,參數類型定義同上述返回參數;如果有兩個以上的形式參數,最后一個參數(最右端的)在D中,可以以堆棧指針為基地址,加上偏移量尋址其他參數,計算偏移量時要多加2,目的是避開調用該函數時堆棧中保存的程序返回地址。左邊第一個參數偏移量值最大,上例中程序調用時堆棧數據結構如圖4.2所示。

圖4.2 程序調用時堆棧數據結構

(3)調用函數(形式參數數目是固定的)

如果函數有固定數目的兩個以上的形式參數,調用前,從第一個參數(最左端的)開始,從左至右逐個壓入堆棧,留最后一個參數在D中,然后調用該函數。故在用匯編語言編寫該函數時,存取這些參數,應該到堆棧區以SP為指針,以偏移量+2,+4,…,進行存取。

(4)調用函數(形式參數數目是不固定的)

如果函數的形式參數數目是不固定的,如printf()函數,括號內為形式參數表,此時調用函數參數的入棧順序和(3)相反,編譯器會從右向左將參數全部推入堆棧。故定義函數時,到堆棧中訪問這些形式參數時,偏移量的順序也和固定數目的參數情況相反。

注意:計算偏移量時要多加3,目的是避開調用過程中本身壓入堆棧的子程序返回地址。可以模仿C交叉編譯器,用SP寄存器間址;也可以用指令TSX將堆棧指針傳給IX,用IX寄存器間址替代SP寄存器間址。

4.3 C程序編譯器與交叉編譯器

C語言程序設計需要兩個編譯器。一個編譯器在主機上運行,編譯器生成目標文件,編譯器可以是Turbo C、Borland C 、C++、VC等高級語言,用于開發、設計、測試以及調試目標系統;另一個編譯器是交叉編譯器,交叉編譯器也是在主機上運行的,但是它為目標系統生成機器代碼,對于大多數的嵌入式系統微處理器和微控制器來說,目標系統多選擇指定的或者商用的交叉編譯器使用。主機往往同時運行一個提供完整開發環境的編譯器和交叉編譯器,這意味著可以在主機上仿真、調試、模擬目標系統。

圖4.3給出了將匯編程序轉換為機器碼,最后得到一個ROM映像文件的過程。

圖4.3 匯編程序轉換ROM映像文件的過程

① 匯編器(Assembler)經過一種稱為匯編(Assembling)的步驟,將匯編語言程序翻譯為機器碼。

② 下一個步驟稱為鏈接(Linking),鏈接器(Linker)將這些代碼與其他必要的匯編語言代碼鏈接在一起。由于有多組代碼需要鏈接在一起,形成最后的二進制文件,因此鏈接是很有必要的。例如,如果匯編語言程序中有一個對延遲任務的引用,就可以有標準代碼來完成這個任務。延遲代碼必須與匯編語言代碼相鏈接,延遲代碼從某個地址開始是連續的,匯編語言軟件代碼從某個地址開始也是連續的。兩段代碼都必須處于不同的地址上,這些地址還必須是系統中的可用地址,鏈接器將這兩者鏈接在一起,鏈接后要在機器上運行的二進制文件通常稱為可執行文件或者簡稱為“.exe”文件。鏈接就是將代碼實際放入存儲器之前,必須重新分配代碼序列的排放過程。

③在下一個步驟中,當發現給定的立即數是一個物理RAM地址時,加載器(Loader)執行重新分配(Reallocating)代碼的任務。加載器是操作系統的一部分,讀取.exe文件之后將代碼放置到存儲器中。這個步驟也是很有必要的,因為可用的存儲器地址可能不是從0x0000開始的。在運行過程中,二進制代碼必須裝載到不同的地址上,加載器能夠找到適當的起始地址,也可以使用加載器將準備運行的程序裝載到RAM中。

④ 系統設計過程的最后一步是將代碼定位(Locating)為RAM映像,并將其永久存放到ROM可用的地址中。嵌入式系統不像計算機,有一個單獨的程序可以跟蹤運行過程中的可用地址。嵌入式系統開發者必須定義用于加載的可用地址,并創建用于永久定位代碼的文件。定位器(Locator)程序將重新分配鏈接過的文件,并且以靜態格式創建用于永久定位代碼的文件。這種格式可以是Intel Hex文件格式或者Freescale的S-record格式。

⑤ 最后利用一個稱為設備編程器的裝置,將ROM映像文件作為輸入,并最終將這個映像燒寫(Burn)到PROM或者EPROM中,或者由半導體廠商在工廠里,將映像文件做成一個嵌入式系統ROM的掩模,根據映像創建的掩模使ROM成為IC芯片的形式。

圖4.4給出了將一個C程序轉換為ROM映像文件的過程。編譯器(Compiler)產生目標代碼。編譯器根據處理器指令和其他說明對代碼進行匯編。作為編譯的最后一個步驟,嵌入式系統的C編譯器必須使用代碼優化器(Code-Optimizer)。優化器在鏈接之前,對代碼進行優化。在編譯之后,鏈接器(Linker)將目標代碼與其他必要的代碼鏈接在一起。例如,鏈接器將某些函數代碼包含進來,如printf和sqrt代碼。設備管理和驅動程序代碼(設備控制代碼)也是在這個階段鏈接的,如打印機設備管理和驅動程序代碼。鏈接之后,創建ROM映像文件的其他步驟與圖4.3中所示的步驟相同。

圖4.4 C程序轉換ROM映像文件的過程

4.4 CodeWarrior軟件簡介

CodeWarrior集成開發環境(IDE)可以用于MC9S12單片機的程序開發,有效地提高軟件開發效率。本節以CodeWarrior 4.6 for S12(X)版本為例簡要介紹CodeWarrior的安裝及使用方法,使讀者初步掌握如何在CodeWarrior下用C語言進行程序設計。

4.4.1 CodeWarrior的安裝

CodeWarrior 4.6 for S12(X)軟件可以在Freescale公司網站下載,運行安裝文件CW12_V4_6.exe后,開始解壓安裝文件,解壓完畢后,可以根據圖4.5至圖4.12的界面,完成軟件的安裝。

圖4.5 安裝歡迎界面

圖4.6 安裝許可協議

圖4.7 產品發布說明

圖4.8 選擇安裝路徑

圖4.9 選擇安裝類型

圖4.10 文件關聯選擇

圖4.11 安裝進度

圖4.12 安裝完成

軟件安裝完成后,可以通過選擇“開始→程序→Freescale CodeWarrior→CW for HC12 V4.6→CodeWarrior IDE”運行軟件,可以看到如圖4.13所示的軟件界面。

圖4.13 CodeWarrior集成開發環境界面

4.4.2 CodeWarrior使用簡介

下面簡單介紹使用CodeWarrior集成開發環境建立工程,編寫、調試程序的步驟及方法。

單擊“File”菜單下的“New…”,可以看到如圖4.14所示的界面。在左側選擇“HC(S)12 New Project Wizard”通過向導建立一個新工程,右側的“Project Name”和“Location”分別定義工程的名稱和工程存放位置。單擊“OK”按鈕,可以啟動新建工程向導,如圖4.15所示。

圖4.14 “新建”界面圖

圖4.15 新建工程向導歡迎界面

單擊“Next”按鈕進入下一個設置界面,如圖4.16所示。在其中選擇MC9S12DG128B型號微控制器,之后單擊“Next”按鈕進入下一設置界面,如圖4.17所示。

圖4.16 選擇微控制器型號

圖4.17 選擇編程語言

可以選擇僅適用匯編語言或者僅適用C語言,也可以同時選擇使用匯編語言和高級語言混合編程。這里選擇C語言進行程序設計,單擊“Next”按鈕進入下一設置界面,如圖4.18所示。

圖4.18 選擇是否使用處理器專家模式

這里可以選擇是否使用處理器專家模式,處理器專家自動代碼生成器將會極大的幫助開發者降低系統開發時間,提高代碼質量,同時還方便開發者將應用代碼移植到Freescale其他的微控制器上。本例中選擇不使用處理器專家模式,單擊“Next”按鈕進入下一設置界面,如圖4.19所示。

圖4.19 選擇是否使用PC-lint

這里可以選擇是否使用PC-lint。PC-lint是一個歷史悠久,功能異常強大的靜態代碼檢測工具。經過這么多年的發展,它不但能夠監測出許多語法邏輯上的隱患,而且還能夠有效地提出許多程序在空間利用、運行效率上的改進點。下面簡單歸納一下PC-lint的功能。

① PC-lint是一種靜態代碼檢測工具,可以說,PC-lint是一種更加嚴格的編譯器,不僅可以像普通編譯器那樣檢查出一般的語法錯誤,還可以檢查出那些雖然完全合乎語法要求,但很可能是潛在的、不易發現的錯誤。

② PC-lint不但可以檢測單個文件,也可以從整個項目的角度來檢測問題,因為C語言編譯器采用固有的單個編譯,有些問題在編譯器環境下很難被檢測,而PC-lint在檢查當前文件的同時還會檢查所有與之相關的文件,這會對開發者提供更大的幫助。

③ PC-lint a支持幾乎所有流行的編輯環境和編譯器。但是本例中選擇不使用PC-lint。單擊“Next”按鈕進入下一設置界面,如圖4.20所示。這里可以選擇加載什么級別的啟動代碼,本例選擇使用ANSI啟動代碼。單擊“Next”按鈕進入下一設置界面,如圖4.21所示。

圖4.20 選擇使用什么級別的啟動代碼

圖4.21 選擇是否使用浮點運算

這里可以選擇是否使用浮點運算及使用的浮點數類型。本例不使用浮點運算。單擊“Next”按鈕進入下一設置界面,如圖4.22所示。這里可以選擇存儲器類型,本例選擇默認類型。單擊“Next”按鈕進入下一設置界面,如圖4.23所示。

圖4.22 選擇存儲器類型

圖4.23 選擇調試器類型

這里可以選擇調試器類型,如果僅作程序仿真而不需要通過其他手段將程序下載到微控制器中,可以只選擇第一項。如果有BDM調試器,希望通過BDM調試器調試或者下載程序,應該選中第二項或者最后一項。單擊“Finish”按鈕完成新建工程工作,出現如圖4.24所示的IDE界面。

圖4.24 IDE界面

打開項目管理窗下“Sources”文件夾下的“main.c”文件,就可以進行程序編寫工作了。項目管理窗一些按鈕功能及文件的組織如圖4.25所示。

圖4.25 項目管理窗

當程序編寫完成后,可以單擊項目管理窗中工具欄中的調試(Debug)按鈕,進入在線調試狀態,如果此時有BDM調試器,就可以完成程序的下載和在線調試工作了。程序調試界面如圖4.26所示。此時,可以對系統程序進行單步、斷點、全速執行等調試工作,直至系統程序設計達到目標要求。

圖4.26 程序調試界面

主站蜘蛛池模板: 珠海市| 汉阴县| 四川省| 八宿县| 搜索| 姜堰市| 鸡东县| 建德市| 青州市| 博白县| 荔浦县| 府谷县| 白河县| 湘乡市| 深圳市| 福建省| 全椒县| 玉田县| 曲靖市| 山阳县| 诏安县| 聊城市| 明光市| 尤溪县| 吉木萨尔县| 阿合奇县| 耒阳市| 永康市| 盐山县| 扎囊县| 拜泉县| 徐闻县| 襄汾县| 砀山县| 息烽县| 南安市| 丰都县| 温泉县| 齐齐哈尔市| 加查县| 金川县|