嵌入式系統/C 程式設計
C 程式語言可能是嵌入式系統程式設計中最流行的程式語言。(在之前嵌入式系統/嵌入式系統簡介#本書將使用哪些程式語言?中我們提到了其他流行的程式語言)。
大多數 C 程式設計師都被寵壞了,因為他們是在有標準庫實現的環境中程式設計,而且經常可以使用許多其他庫。事實是,在嵌入式系統中,很少有程式設計師習慣使用的庫,但有時嵌入式系統可能沒有完整的標準庫,如果有的話。很少有嵌入式系統具有動態連結功能,因此,如果要使用標準庫函式,它們通常需要直接連結到可執行檔案中。由於空間限制,通常不可能連結整個庫檔案,並且程式設計師經常被迫“自己編寫”標準 C 庫實現,如果他們想要使用它們。雖然有些庫很笨重,不適合在微控制器上使用,但許多開發系統仍然包含 C 程式設計師最常用的標準庫。
由於程式碼效率、降低的開銷和開發時間,C 仍然是微控制器開發人員非常流行的語言。C 提供低階控制,被認為比彙編更容易閱讀。許多免費的 C 編譯器可用於各種開發平臺。編譯器是 IDE 的一部分,具有 ICD 支援、斷點、單步執行和彙編視窗。近年來,C 編譯器的效能已大大提高,據說它們與彙編一樣好,具體取決於你問的是誰。大多數工具現在提供了用於自定義編譯器最佳化的選項。此外,使用 C 可以提高可移植性,因為 C 程式碼可以為不同型別的處理器編譯。
以下是一個使用 C 更改位的示例
清除位
PORTH &= 0xF5; // Changes bits 1 and 3 to zeros using C PORTH &= ~0x0A; // Same as above but using inverting the bit mask - easier to see which bits are cleared
設定位
PORTH |= 0x0A; // Set bits 1 and 3 to one using the OR
在彙編中,這將是
清除位
BCLR PORTH,$0A ;//Changes bits 1 and 3 to zeros using 68HC12 ASM
設定位
BSET PORTH,$0A ;//Changes bits 1 and 3 to ones using 68HC12 ASM
C 語言是標準化的,有一些每個人都知道和喜歡的運算子。然而,許多微處理器具有 C 編譯器可能不會使用的功能。C 編譯器可能產生的機器程式碼效率低於手工編寫的組合語言。例如,8051 和 PIC 微控制器都有用於直接設定和檢查位元組中單個位的彙編指令。C 程式可以編寫為使用“位域”單獨影響位,但編譯器產生的機器程式碼輸出可能不如某些微處理器上一次一位的機器操作快。
位域是一個很少有 C 程式設計師有經驗的主題,雖然它在很長一段時間內一直是語言的標準化部分。位域允許程式設計師訪問未對齊的部分的記憶體,甚至訪問小於位元組的部分。讓我們建立一個示例
struct _bitfield {
flagA : 1;
flagB : 1;
nybbA : 4;
byteA : 8;
}
冒號將欄位的名稱與其大小以位為單位,而不是以位元組為單位隔開。突然之間,瞭解什麼數字可以放入什麼長度的欄位中變得非常重要。例如,flagA 和 flagB 欄位都是 1 位,因此它們只能儲存布林值(1 或 0)。nybbA 欄位可以儲存 4 位,最大值為 15(一個十六進位制數字)。
位域中的欄位可以像普通結構一樣被精確地定址。例如,以下語句都是有效的
struct _bitfield field; field.flagA = 1; field.flagB = 0; field.nybbA = 0x0A; field.byteA = 255;
位域中的單個欄位不使用儲存型別,因為您手動定義每個欄位佔用的位數。另請參閱"在結構中宣告和使用位域";"允許的位域型別"。
但是,位域中的欄位可以用關鍵字“signed”或“unsigned”限定,即使未指定,也會隱含“signed”。
如果一個 1 位欄位被標記為 signed,它有 +1 和 0 的值。請允許我引用c2:BitField:一個可以包含 1 的帶符號 1 位位域是編譯器中的錯誤。
重要的是要注意,不同的編譯器可能會在位域中以不同的順序排列欄位,因此程式設計師永遠不應該嘗試將位域作為整型物件訪問。如果不在您的個人編譯器上進行試錯測試,則不可能知道位域中的欄位將以什麼順序排列。
此外,位域像任何其他資料物件一樣,在給定機器上對齊到某個邊界。
C 語言支援設定一個結構,該結構完全匹配記憶體對映 I/O 裝置的位元組和位級佈局。[1]
變數宣告中的“const”是編寫它的程式設計師對程式不會更改變數值的承諾。
在嵌入式系統中使用“const”有兩種略微不同的原因。
其中一個原因與桌面應用程式相同
通常,結構、陣列或字串使用指標傳遞給函式。當該引數被描述為“const”時,例如當頭檔案說
void print_string( char const * the_string );
時,這是編寫該函式的程式設計師對該函式不會修改結構、陣列或字串中任何專案的承諾。(如果該標頭檔案在實現該函式的檔案中被正確地 #include,那麼當該實現被編譯時,編譯器將在編譯時檢查該承諾,如果該承諾被違反,則會給出錯誤)。
在桌面應用程式中,如果從原始碼中刪除所有“const”宣告,這樣的程式將編譯成完全相同的可執行檔案——但編譯器將不會檢查這些承諾。
當其他程式設計師有一個重要的資料想要傳遞給該函式時,他可以簡單地透過閱讀標頭檔案來確保該函式不會修改這些資料。(如果沒有“const”,他要麼必須檢視函式實現的原始碼以確保他的資料沒有被修改(並擔心下次更新該實現可能會修改該資料),要麼建立一個數據的臨時副本傳遞給該函式,保持原始版本不變)。
使用“const”的另一個原因是特定於嵌入式系統的
在許多嵌入式系統中,程式 Flash(或 ROM)比 RAM 多得多。使用如下定義的“.c”檔案
char * months[] = {
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December",
};
強制編譯器將所有這些字串儲存在程式 Flash 中,然後在啟動時將這些值複製到 RAM 中的一個位置。如果程式實際上從未修改過這些字串(如經常發生的那樣),這會浪費寶貴的 RAM。透過將宣告修改為
char const * const months[] = { ... };
,我們告訴編譯器,我們承諾永遠不會修改這些字串(或它們在陣列中的順序),因此編譯器可以自由地將所有這些字串儲存在程式 Flash 中,並在需要時從 Flash 中獲取原始值。這為確實會發生變化的變數節省了 RAM。
(如果您使用如下定義
static char * months[] = { ... };
,一些編譯器足夠聰明,可以自己判斷程式是否實際修改了這些字串。如果程式確實修改了這些字串,那麼編譯器當然必須將它們放在 RAM 中。但如果不是,編譯器可以自由地在程式 Flash 中只儲存這些字串一次)。
普林斯頓架構微控制器使用完全相同的指令來訪問 RAM 和程式 Flash。
此類架構的 C 編譯器通常將所有宣告為“const”的資料放入程式 Flash 中。函式既不知道也不關心它們是在處理來自 RAM 的資料還是程式 Flash 中的資料;相同的“讀取”指令無論函式接收的是指向 RAM 的指標還是指向程式 Flash 的指標,都能正確地工作。
不幸的是,哈佛架構微控制器使用完全不同的指令來訪問 RAM 和程式快閃記憶體(通常它們還有另一套指令來訪問 EEPROM,以及另一套來訪問外部儲存晶片)。這使得編寫一個可以從程式的某個部分呼叫以從 ROM 中打印出常量字串(例如“November”)的子例程(例如 puts()),並且可以從程式的另一個部分呼叫以打印出 RAM 中的變數字串,變得很困難。
不幸的是,不同的 C 編譯器(即使針對同一晶片)也需要不同的、不相容的技術,才能讓 C 程式設計師告訴 C 編譯器將資料放入 ROM 中。C 程式設計師至少有 3 種方法來告訴 C 編譯器將資料放入 ROM 中。
(1) 有些人聲稱,使用“const”修飾符來表示某些資料應該儲存在 ROM 中是濫用符號。 [2] 這些人通常建議使用一些非標準的屬性或儲存說明符,例如“PROGMEM”或“rom”[3],在變數定義和函式引數上,以表示“型別指標”型別為“值駐留在程式快閃記憶體中,而不是 RAM 中”。不幸的是,不同的編譯器有不同的、不相容的方法來指定資料可以放置在 ROM 中。通常這些人使用具有 2 個版本的函式庫,每個函式都處理字串(等等);一個版本用於 RAM 中的字串,另一個版本用於 ROM 中的字串。這種技術使用最少的 RAM,但通常需要比其他技術更多的 ROM。
(2) 有些函式庫假設資料在 RAM 中。當程式設計師希望使用實際上在 ROM 中的資料呼叫這些函式時,程式設計師必須確保資料首先臨時複製到 RAM 中的緩衝區,然後使用該緩衝區的地址呼叫該函式。這種技術使用最少的 ROM 來儲存庫,但在每次涉及 ROM 中資料的函式呼叫時,它使用比其他技術更多的 ROM 和 RAM。
(3) 有些函式庫使用可以處理從一個位置用 RAM 中的字串呼叫,從其他位置用 ROM 中的字串呼叫的函式。這通常需要“胖指標”又名“通用指標”,這些指標具有額外的位,用於指示指標是指向 RAM 中的內容還是 ROM 中的內容。每次這樣的庫使用指標時,執行程式碼都會檢查這些位,以檢視是執行“從 RAM 讀取”指令還是“從 ROM 讀取”指令。 [4][5][6][7][8][9] 這是在其他系統中使用的“胖指標”和“標記指標”的特例,這些系統根據指向物件的型別執行不同的程式碼,其中“指標”包括型別資訊和目標地址。 [10][11]
在變數宣告中,“易變的”告訴我們和編譯器,該變數的值可能在任何時間改變,透過某種方式,在程式碼的這一部分的正常流程之外。這些變化可能是由硬體引起的,例如外設、多處理器系統中的另一個處理器或中斷服務例程。
“易變的”關鍵字告訴編譯器不要進行某些最佳化,這些最佳化只適用於儲存在 RAM 或 ROM 中的“正常”變數,這些變數完全受此 C 程式的控制。
嵌入式程式設計的全部意義在於它與外部世界的通訊——而輸入和輸出裝置都需要“易變的”關鍵字。
至少有 3 種類型的最佳化,“易變的”會關閉它們
- “讀取”最佳化——如果沒有“易變的”,C 編譯器假設一旦程式將變數讀入暫存器,它就不需要在每次原始碼中提到該變數時重新讀取該變數,而是可以使用暫存器中快取的值。這對於 ROM 和 RAM 中的正常值非常有效,但對於輸入外設來說卻完全失敗。外部世界以及內部計時器和計數器經常發生變化,使快取的值變得陳舊和無關。
- “寫入”最佳化——如果沒有“易變的”,C 編譯器假設對不同變數進行寫入的順序並不重要,只有對特定變數的最後一次寫入才是真正重要的。這對於 RAM 中的正常值非常有效,但對於典型輸出外設來說卻完全失敗。透過序列埠傳送“左轉 90 度,前進 10 步,左轉 90 度,前進 10 步”與“最佳化”為透過序列埠傳送“0”完全不同。
- 指令重新排序——如果沒有“易變的”,C 編譯器假設可以重新排序指令。編譯器可能會決定更改變數分配的順序,以更好地利用暫存器。這對於 I/O 外設來說可能會完全失敗,例如,你寫入一個位置來獲取樣本,然後從另一個位置讀取該樣本。重新排序這些指令將意味著讀取舊的/陳舊的/未定義的樣本,然後告訴外設獲取新的樣本(這將被忽略)。
根據你的硬體和編譯器功能,其他最佳化(SIMD、迴圈展開、並行化、流水線)也可能會受到影響。
很多人不理解“const”和“volatile”的組合。正如我們在之前的 嵌入式系統/記憶體 中討論的那樣,嵌入式系統有許多種記憶體。
許多輸入外設——例如自由執行的計時器和鍵盤介面——必須宣告為“const volatile”,因為它們既 (a) 透過此 C 程式之外的方式改變值,又 (b) 此 C 程式不應該向它們寫入值(向 10 鍵鍵盤寫入值沒有任何意義)。
在絕大多數情況下,當人們用 C 語言編寫程式碼時,他們會將該程式碼透過 C 編譯器執行到某些個人計算機上,以獲得本地可執行檔案。使用嵌入式系統的人員然後將該本地可執行檔案下載到嵌入式系統中,並執行它。
但是,少數使用嵌入式系統的人員做了一些不同的事情。
- 有些人使用 C 直譯器,例如 [5] 或互動式 C 或 可擴充套件互動式 C (EiC)。他們將 C 原始碼下載到嵌入式系統中,然後在嵌入式系統本身執行直譯器。(更多 C 直譯器列在另一本華夏公益教科書中,C 程式設計/C 編譯器參考列表)。
- 有些人有幸使用“大型”嵌入式系統,這些系統可以執行標準 C 編譯器(它在 Linux 或 BSD 上執行標準 GCC;或者它在 FreeDos 上執行 DJGPP 移植的 GCC;或者它在 Windows 上執行 MinGW 移植的 GCC;或者它在 Linux 或 Windows 上執行 Tiny C 編譯器;或者其他一些 C 編譯器)。他們將 C 原始碼下載到嵌入式系統中,然後在嵌入式系統本身執行編譯器。
也許用於嵌入式系統的 C 編譯器和用於臺式計算機的 C 編譯器之間最大的區別是“平臺”和“目標”之間的區別。“平臺”是 C 編譯器執行的地方——也許是執行 Linux 的筆記型電腦或執行 Windows 的桌上型電腦。“目標”是 C 編譯器生成的執行程式碼將執行的地方——嵌入式系統中的 CPU,通常沒有任何底層作業系統。
GCC 編譯器是[需要引用] 用於嵌入式系統的最流行的 C 編譯器。GCC 最初是為 32 位普林斯頓架構 CPU 開發的。因此,它相對容易移植到目標 ARM 核心微控制器,例如 XScale 和 Atmel AT91RM9200;Atmel AVR32 AP7 系列;MIPS 核心微控制器,例如 Microchip PIC32;以及 Freescale 68k/ColdFire 處理器。
編寫編譯器的人員還(更困難地)將 GCC 移植到目標德州儀器 MSP430 16 位 MCU;Microchip PIC24 和 dsPIC 16 位微控制器;8 位 Atmel AVR 微控制器;8 位 Freescale 68HC11 微控制器。
其他微控制器與 32 位普林斯頓架構 CPU 有很大不同。許多編譯器編寫者認為,開發一個獨立的 C 編譯器會更好,而不是試圖將 GCC 的圓柱形塞入 8 位哈佛架構微控制器目標的方形孔中。
SDCC——適用於英特爾 8051、Maxim 80DS390、Zilog Z80、摩托羅拉 68HC08、Microchip PIC16、Microchip PIC18 的小型裝置 C 編譯器 http://sdcc.sourceforge.net/
有一些備受尊敬的公司出售商業 C 編譯器。你可以找到適用於幾乎所有微控制器的商業 C 編譯器,包括上面列出的微控制器。尚未列出的流行微控制器(即,唯一已知的 C 編譯器是商業 C 編譯器的微控制器)包括 Cypress M8C MCU;Microchip PIC10 和 Microchip PIC12 MCU;等等。
- C語言程式設計/編譯器 包含大量的 C 編譯器和 C 直譯器。
- 維基百科: 位域
- c2:BitField
- C語言程式設計/變數 和 C++語言程式設計/程式語言/C++/程式碼/語句/變數 還討論了 "const" 和 "volatile" 變數。
- ARM 技術支援常見問題解答: “const” 和 “volatile” 的使用
- "Volatile 作為承諾" 丹·薩克斯的文章
- Nullstone: "Volatile" "經驗資料表明,對易失性物件的錯誤最佳化是 C 最佳化器中最常見的缺陷之一。"
- 組合使用 "const" 和 "volatile" 的一些錯誤理解: [6], [7], ...
- "使用 volatile" 阿肖克·K·帕塔克的文章
- 瓊斯,奈傑爾。 "針對 8 位 MCU 的高效 C 程式碼" 嵌入式系統程式設計,1998 年 11 月。(提到了 "const volatile 變數";提到了 "通用指標" 與 "型別化指標" 等)。
- 約翰·雷格赫。 "九種使用 volatile 損壞系統程式碼的方式"。2010 年。(提到了 "const 和 volatile 同時使用" 對於宣告定時器暫存器很有用)。
- 維基百科: 小型裝置 C 編譯器 支援多種流行的微控制器。SDCC 是 Intel 8051 相容微控制器的唯一開源 C 編譯器。
- "面向微控制器的免費 C/C++ 編譯器和交叉編譯器"
- ↑ 埃裡克·S·雷蒙德。 "C 結構體打包的失落藝術".
- ↑ "程式空間中的資料: 關於 const 的說明"
- ↑ "面向 PICmicro 的 BoostC C 編譯器參考手冊"
- ↑ "Crossware C 編譯器手冊: 8051 特定功能: 通用指標" [1]
- ↑ 奧拉夫·菲弗。 "在 8051 C 編譯器中使用指標、陣列、結構體和聯合體: 通用指標" [2]
- ↑ 以撒·馬裡諾·巴瓦雷斯科。 "面向 MPLAB-C18 編譯器的通用指標"。 [3] [4]
- ↑ "SDCC 編譯器使用者指南"。第 "3.5.1.8 指向 MCS51/DS390 特定記憶體空間的指標" 節。第 "4.6.16 通用指標" 節。
- ↑ 約翰·哈特曼。 "Intel 8051: 3 位元組通用指標".
- ↑ "Cx51 使用者指南: 通用指標".
- ↑ 馬克·S·米勒。 "胖指標".
- ↑ "真正簡單的記憶體管理: 胖指標" 描述了一種簡單、與 RTOS 實現 相容的垃圾收集和記憶體碎片整理方案 - 它永遠不會 "停止世界"。