嵌入式C語言之—模組化編程

來源:互聯網
上載者:User

當你在一個項目小組做一個相對較複雜的工程時,意味著你不再獨自單幹。你需要和你的小組成員分工合作,一起完成項目,這就要求小組成員各自負責一部分工程。比如你可能只是負責通訊或者顯示這一塊。這個時候,你就應該將自己的這一塊程式寫成一個模組,單獨調試,留出介面供其它模組調用。最後,小組成員都將自己負責的模組寫完並調試無誤後,由項目組長進行組合調試。像這些場合就要求程式必須模組化。模組化的好處是很多的,不僅僅是便於分工,它還有助於程式的調試,有利於程式結構的劃分,還能增加程式的可讀性和可移植性。

初學者往往搞不懂如何模組化編程,其實它是簡單易學,而且又是組織良好程式結構行之有效方法之一.

本文將先大概講一下模組化的方法和注意事項,最後將以初學者使用最廣的keil c編譯器為例,給出模組化編程的詳細步驟。

模組化程式設計應該理解以下概述:

(1) 模組即是一個.c 檔案和一個.h 檔案的結合,標頭檔(.h)中是對於該模組介面的聲明;

這一條概括了模組化的實現方法和實質:將一個功能模組的代碼單獨編寫成一個.c檔案,然後把該模組的介面函數放在.h檔案中.舉例:假如你用到液晶顯示,那麼你可能會寫一個液晶驅動模組,以實現字元、漢字和映像的現實,命名為: led_device.c,該模組的.c檔案大體可以寫成:

/*************************************************************************

* 液晶驅動模組

*

* 文 件: lcd_device.c

* 編 寫 人: 小瓶蓋

* 描 述:液晶串列顯示驅動模組,提供字元、漢字、和映像的實現介面

* 編寫時間: 2009.07.03

* 版 本:1.2

*************************************************************************/

#include …

//定義變數

 unsigned char value;//全域變數

//定義函數

//這是本模組第一個函數,起到延時作用,只供本模組的函數調用,所以用到static關鍵字修飾

/********************延時子程式************************/

static void delay (uint us) //delay time

{}

//這是本模組的第二個函數,要在其他模組中調用

/*********************寫字元程式**************************

** 功能:向LCD寫入字元

** 參數:dat_comm 為1寫入的是資料,為0寫入的是指令

content 為寫入的數字或指令

******************************************************/

void wr_lcd (uchar dat_comm,uchar content)

{}

……

……

/***************************** END Files***********************************/

註:此處唯寫出這兩個函數,第一個延時函數的作用範圍是模組內,第二個,它是其它模組需要的。為了簡化,此處並沒有寫出函數體.

.h檔案中給出模組的介面.在上面的例子中, 向LCD寫入字元函數:wr_lcd (uchar dat_comm,uchar content)就是一個介面函數,因為其它模組會調用它,那麼.h檔案中就必須將這個函式宣告為外部函數(使用extrun關鍵字修飾),另一個延時函數:void delay (uint us)只是在本模組中使用(本地函數,用static關鍵字修飾),因此它是不需要放到.h檔案中的。

.h檔案格式如下:

/*****************************************************************************

* 液晶驅動模組 標頭檔

*

* 文 件: lcd_device.h

* 編 寫 人: 小瓶蓋

* 編寫時間: 2010.07.03

* 版 本:1.0

*********************************************************************************/

//聲明全域變數

extern unsigned char value;

//聲明介面函數

extern void wr_lcd (uchar dat_comm,uchar content); //向LCD寫入字元

……

/***************************** END Files***********************************/

這裡注意三點:

1. 在keil 編譯器中,extern這個關鍵字即使不聲明,編譯器也不會報錯,且程式運行良好,但不保證使用其它編譯器也如此。強烈建議加上,養成良好的編程規範。

2. .c檔案中的函數只有其它模組使用時才會出現在.h檔案中,像本地延時函數static void delay (uint us)即使出現在.h檔案中也是在做無用功,因為其它模組根本不去調用它,實際上也調用不了它(static關鍵字的限制作用)。

3.注意本句最後一定要加分號”;”,相信有不少同學遇到過這個奇怪的編譯器報錯: error C132: 'xxxx': not in formal parameter list,這個錯誤其實是.h的函式宣告的最後少了分號的緣故。

模組的應用:假如需要在LCD菜單模組lcd_menu.c中使用液晶驅動模組lcd_device.c中的函數void wr_lcd (uchar dat_comm,uchar content),只需在LCD菜單模組的lcd_menu.c檔案中加入液晶驅動模組的標頭檔lcd_device.h即可.

/***************************************************************************

* 液晶菜單模組

*

* 文 件: lcd_menu.c

* 編 寫 人: 小瓶蓋

* 說 明:LCD菜單模組,最多實現256級菜單,與硬體無關。

* 編寫時間: 2010.07.03

* 版 本:1.0

**************************************************************************/

#include“lcd_device.h //包含液晶驅動程式標頭檔,之後就可以在該.c檔案中調用//lcd_device.h中的全域函數,使用液晶驅動程式裡的全域//變數(如果有的話)。

//調用向LCD寫入字元函數

wr_lcd (0x01,0x30);

//對全域變數賦值

value=0xff;

(2) 某模組提供給其它模組調用的外部函數及資料需在.h 中檔案中冠以extern 關鍵字聲明;

這句話在上面的例子中已經有體現,即某模組提供給其它模組調用的外部函數和全域變數需在.h 中檔案中冠以extern 關鍵字聲明,下面重點說一下全域變數的使用。使用模組化編程的一個痛點(相對於新手)就是全域變數的設定,初學者往往很難想通模組與模組公用的變數是如何?的,常規的做法就是本句提到的,在.h檔案中外部資料冠以extern關鍵字聲明。比如上例的變數value就是一個全域變數,若是某個模組也使用這個變數,則和使用外部函數一樣,只需在使用的模組.c檔案中包含#include“lcd_device.h”即可。

另一種處理模組間全域變數的方法來自於嵌入式作業系統uCOS-II,這個作業系統處理全域變數的方法比較特殊,也比較難以理解,但學會之後妙用無窮,這個方法只需用在標頭檔中定義一次。方法為:

在定義所有全域變數(uCOS-II將所有全域變數定義在一個.h檔案內)的.h標頭檔中:

#ifdef xxx_GLOBALS

#define xxx_EXT

#else

#define xxx_EXT extern

#endif

.H 檔案中每個全域變數都加上了xxx_EXT的首碼。xxx 代表模組的名字。

該模組的.C檔案中有以下定義:

#define xxx_GLOBALS

#include "includes.h"

當編譯器處理.C檔案時,它強制xxx_EXT(在相應.H檔案中可以找到)為空白,(因為xxx_GLOBALS已經定義)。所以編譯器給每個全域變數分配記憶體空間,而當編譯器處理其他.C 檔案時,xxx_GLOBAL沒有定義,xxx_EXT 被定義為extern,這樣使用者就可以調用外部全域變數。為了說明這個概念,可以參見uC/OS_II.H,其中包括以下定義:

#ifdef OS_GLOBALS

#define OS_EXT

#else

#define OS_EXT extern

#endif

OS_EXT INT32U OSIdleCtr;

OS_EXT INT32U OSIdleCtrRun;

OS_EXT INT32U OSIdleCtrMax;

同時,uCOS_II.H 有中以下定義:

#define OS_GLOBALS

#include “includes.h”

當編譯器處理uCOS_II.C 時,它使得標頭檔變成如下所示,因為OS_EXT 被設定為空白。

INT32U OSIdleCtr;

INT32U OSIdleCtrRun;

INT32U OSIdleCtrMax;

這樣編譯器就會將這些全域變數分配在記憶體中。當編譯器處理其他.C 檔案時,標頭檔變成了如下的樣子,因為OS_GLOBAL沒有定義,所以OS_EXT 被定義為extern。

extern INT32U OSIdleCtr;

extern INT32U OSIdleCtrRun;

extern INT32U OSIdleCtrMax;

在這種情況下,不產生記憶體配置,而任何 .C檔案都可以使用這些變數。這樣的就只需在 .H檔案中定義一次就可以了。

(3) 模組內的函數和全域變數需在.c 檔案開頭冠以static 關鍵字聲明;

這句話主要講述了關鍵字static的作用。Static是一個相當重要的關鍵字,他能對函數和變數做一些約束,而且可以傳遞一些資訊。比如上例在LCD驅動模組.c檔案中定義的延時函數static void delay (uint us),這個函數冠以static修飾,一方面是限定了函數的作用範圍只是在本模組中起作用,另一方面也給人傳達這樣的資訊:該函數不會被其他模組調用。下面詳細說一下這個關鍵字的作用,在C 語言中,關鍵字static 有三個明顯的作用:

1.在函數體,一個被聲明為靜態變數在這一函數被調用過程中維持其值不變。

2.在模組內(但在函數體外),一個被聲明為靜態變數可以被模組內所用函數訪問,但不能被模組外其它函數訪問。它是一個本地的全域變

量。

3.在模組內,一個被聲明為靜態函數只可被這一模組內的其它函數調用。那就是,這個函數被限制在聲明它的模組的本地範圍內使用。

前兩個都比較容易理解,最後一個作用就是剛剛舉例中提到的延時函數(static void delay (uint us)),本地化函數是有相當好的作用的。

(4) 永遠不要在.h 檔案中定義變數!

呵呵,似乎有點危言聳聽的感覺,但我想也不會有多少人會在.h檔案中定義變數的。

比較一下代碼:

代碼一:

/*module1.h*/

int a = 5; /* 在模組1 的.h 檔案中定義int a */

/*module1 .c*/

#include "module1.h" /* 在模組1 中包含模組1 的.h 檔案 */

/*module2 .c*/

#include "module1.h" /* 在模組2 中包含模組1 的.h 檔案 */

/*module3 .c*/

#include "module1.h" /* 在模組3 中包含模組1 的.h 檔案 */

以上程式的結果是在模組1、2、3 中都定義了整型變數a,a 在不同的模組中對應不同的地址元,這個世界上從來不需要這樣的程式。正確的做法是:

代碼二:

/*module1.h*/

extern int a; /* 在模組1 的.h 檔案中聲明int a */

/*module1 .c*/

#include "module1.h" /* 在模組1 中包含模組1 的.h 檔案 */

int a = 5; /* 在模組1 的.c 檔案中定義int a */

/*module2 .c*/

#include "module1.h" /* 在模組2 中包含模組1 的.h 檔案 */

/*module3 .c*/

#include "module1.h" /* 在模組3 中包含模組1 的.h 檔案 */

這樣如果模組1、2、3 操作a 的話,對應的是同一片記憶體單元。

註:

一個嵌入式系統通常包括兩類(注意是兩類,不是兩個)模組:

(1)硬體驅動模組,一種特定硬體對應一個模組;

(2)軟體功能模組,其模組的劃分應滿足低偶合、高內聚的要求。

下面以keil C 編譯器為例,講一下模組化編程的步驟。

下面這個程式分為三層,共7個模組,共同為主程式服務(它們之間也會相互調用)。

程式的結構圖如下所示:

程式主要模組和功能簡介:

一. 底層驅動

1. 紅外鍵盤:程式通過紅外鍵盤進行操作。紅外鍵盤獨佔定時器0和外部中斷0,以實現紅外解碼和鍵盤索引值的識別。紅外鍵盤定義了五個按鍵,分別為上翻、下翻、左翻、右翻和確認鍵。

2. LCD液晶顯示:程式主要通過LCD顯示資訊,LCD液晶顯示驅動提供顯示漢字、圖形和ASCII碼的函數介面。可以全屏、單行顯示漢字,任意位置顯示ASCII碼,還可以全屏、半屏顯示圖形。

二. 功能模組

1. LCD菜單程式:菜單程式可以使人機互動更加方便、容易。本菜單程式的菜單層級深度受RAM大小的限制,每增加一級菜單將多消耗4位元組的RAM。菜單程式主要完成菜單功能函數的調度,LCD顯示重新整理。

2. 計算機程式:實現65536以內的加、減、乘、除,超出範圍會出現溢出,溢出發生時,LCD顯示“錯誤:出現溢出”的錯誤提示,同時本次運算被忽略。對於負數會顯示“-”號,除數為零時LCD顯示“錯誤:除數為零”的錯誤提示。

3. 開機次數記憶程式:主要對基於IIC匯流排的EEPROM進行讀寫,單片機每次上電後,將開機次數寫入EEPROM.

4. 串口測試程式:進入該程式後,單片機向電腦發送字串“Hello Word!”,發送數字24(以字元的形式顯示)。編寫此程式的目的是為了能夠方便的向電腦發送字串和變數,便於程式的調試。串口佔用串口資源,與頻率測配量序共用定時器1

5. 頻率測量:複用定時器1,佔用外部中斷1,實現5~20KHZ頻率的測量.

三. 主程式

主程式主要完成程式的初始化,LCD菜單顯示,監視鍵盤程式並根據索引值更新菜單。

步驟為:

1.建立工程。

2.點擊File—New(或者點擊快捷表徵圖:),建立一個文檔。

3.點擊File—Save(或者點擊快捷表徵圖:),儲存建立的文檔,在檔案名稱後填寫LCD_device.c(液晶驅動模組:
LCD_device,提供顯示漢字、字元和映像的介面),點擊確定。

在該文檔內編寫LCD驅動程式。

4. 點擊File—New(或者點擊快捷表徵圖:),再建立一個文檔。

5. 點擊File—Save(或者點擊快捷表徵圖:),儲存建立的文檔,在檔案名稱後填寫LCD_device.h(液晶驅動模組的標頭檔,模組的介面和全域變數在這裡定義)。點擊確定。在該文檔中整理全域變數和介面函數。以上步驟之後的效果見:

至此,液晶驅動模組書寫完畢,可以對這個模組單獨的調試。

6.重複以上步驟2~5,定義 紅外鍵盤模組:key.c與key.h

菜單模組:menu.c與menu.h

串口通訊模組:uart_.c與uart.h

計算機模組:counter.c與counter.h

頻率測量模組:mea_fre.c與mea_fre.h

開機次數記憶模組:eepram.c與eepram.h

7.重複以上步驟2~3,定義主程式main.c

最終效果如所示:

完成1~7個步驟後,有些小白就習慣性的點擊編譯按鈕了,這時候會出現兩個警告資訊:

*** WARNING L1: UNRESOLVED EXTERNAL SYMBOL

*** WARNING L2: REFERENCE MADE TO UNRESOLVED EXTERNAL

這是因為你只是編寫好了程式模組,卻沒有把他們加入到工程的緣故。

解決方案:在Project Workspace框中,右擊Source group 1檔案夾,選擇Add Files to Group‘Source Group 1’,在彈出的對話方塊中添加你的.c檔案即可。

遙想很久很久以前,我也對上面的兩個警告有過親身體會。那時候我還在大學,周圍有一大群的好哥們一起玩,想逃課逃課,想睡覺睡覺,現在…想起來只剩唏噓!!!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.