文章程式碼顯示

2018年2月4日 星期日

《筆記》C語言 - 06_補充3:用於 MCU 的重要觀念 : const、static、volatile 排列組合彙整(用於變數、陣列) 、多文件的 extern 變數、有號數與無號數的計算

const

const 是一個 C 語言的關鍵字,在宣告一個變數時使用 const 可以告訴編譯器這個變量是不允許被改變的,也就等價的變成一個常量(常數)。

注意! 只是等價而已,實際上是在宣告一個「唯讀變量」

任何被宣告成為 const 的變數,在程式中都不允許用賦值的方式進行修改,否則在編譯時期就會報錯。

對於 MCU 而言有一個明顯的好處在於使用 const 定義一個變數後,在記憶體的分配上會將這個變數(或陣列)放置到 ROM(or Flash ROM) 裡面而不是普通變量所存放的 RAM 裡。考量 MCU 的 ROM 通常會比 RAM 大許多,這樣的配置有助於我們合理的分配記憶體。

假設我們宣告一個變數為以下(參考此處)

char array[]={1,2,3,4,5,6};

在 MCU 剛上電時 ROM 裡面就存放這些陣列的值,RAM 存放 array 這個陣列名稱,在程式進入 main() 之前會有一個稱為 bootloader 的東西,它會負責做一些前置作業然後再讓程式進入 main() 。這些前置作業中有一項是讓存放在 ROM 裡的值重新放到 RAM 去,如此一來就佔用了 RAM 的 6 bytes 空間

假設這個變數 a 在你的程式裡面都不會進行修改,純粹是用來當做一個"常數"在使用時,我們應該宣告成

const char array[]={1,2,3,4,5,6};

如此一來全部的資料就會都放在 ROM 裡面。當然,你不能在程式中修改 ROM 裡面的值。

值得注意的是,有些 MCU 的 ROM 存取速度會較 RAM 慢 ( 大部分的 MCU ROM & RAM 存取速度一樣) ,此時我們為了速度還是乖乖的讓他存放在 RAM 裡面進行操作就好。

: 使用指標 call by reference 的方式仍然可以更改一個宣告為 const 的值。(特例)

static

在 C 語言中,關鍵字 static 有幾個明顯的作用︰

1. 宣告一個 static 區域變量,則其可被 Block 內所有的函數存取,但不能被 Block 外的其它函數存取

2. 一個 static 全域變數的建立會發生在程式進入 main() 之前;一個 static 區域變數的建立則會等到該行語句被執行時,且只會被初始化一次

3. 當我們宣告一個區域變數為 static ,在該區塊結束後,其儲存的變量將會被保留

宣告一個區域遍數為 static 的主要作用在於統計一個函數被調用的次數,假如我們在這個函數裡面宣告一個普通的區域變量 int A;,並且拿來存放被調用的次數。這樣一來會發生每次調用這個函數就會初始化 A 這個區域變量為 0  ,沒辦法達到我們要的目的。

所以我們可以宣告一個 static int B ;

同樣的,區域變量若沒有被給予初始值則一律自動給 0,但因為 static 的特性,所以 B 只會被初始化一次,如此一來就可以保存這個函數被調用的次數。

4. 若在一個大型專案中的某個文件裡宣告一個全域變量為 static則該變量成為靜態全域變量。使得該全域變數只能在這個文件中被使用,也就是把整個文件當成一個 Block (在 extern 節中有更詳盡的解釋)

5. 它是一個 "區域的全域變數"。此觀念需延伸到 class(類)的應用。宣告為 static 的靜態成員將會被多個 object 之間共享,而不是僅限於某個 object 私有參考

const static

綜合上述兩者的特性。

volatile

volatile 指明這個變數是有可能被不預期的改變,則當我們要調用這個變數時,強制從記憶體中讀取



這個修飾詞值得被我們加以討論,原因在於 MCU 領域常常會使用到中斷函數,任何與中斷有關的變數都應該宣告為 volatile,使其都在記憶體中存取這個變數

在我們撰寫 C 語言後,需要通過編譯器幫我們翻譯成為組合語言(或機械語言),翻譯的結果會因為不同的 compiler(編譯器) 產生有不同結果,而在這過程中也會經過一些優化

其中有一項會與 volatile 有關。

要理解這部分的優化之前,要先對 MCU 在處理一個變數時,該變數與「記憶體」還有「暫存器」的關係有些概念,可參考此連結 的 馮紐曼結構(就是廚師炒菜那段)。

由此可知, 當 MCU 要對這個變數進行運算時,會先將變數從記憶體中提取出來並且放到暫存器中然後進行運算,待這個變數的運算結束後再放回暫存器裡面,等到 MCU 閒的時候才會把變數從暫存器中放回記憶體,在該變數暫放的期間若又要對這個變數進行運算,則直接從暫存器中提取,如此一來就加快了運算的速度。

這會有什麼問題產生呢? 我們看下面的例子

在變數沒有宣告為 volatile 時

int number = 10;

int A = number;

...

...

...

int B = number;

我們在程式中的第一行宣告一個變數 number 並且給予初始值 10,並將它放在變數 A 裡面。經過一連串的程式碼,在這之中我們都沒有任何對於變數 number 的修改語句出現,假設我們在第 11 行的地方又出現將 number 的值放在變數 B 裡面,此時編譯器發現從第 3 行到第 11 行之間我們都沒有用類似 number = 5 這樣的賦值方式來進行修改,它就會自動把上次的資料(暫放在暫存器的值)放到 B 裡面,而不會從 number 的記憶體位置重新讀取一次值再放到 B

順帶一提,這裡講的暫存器就是在《高階》寫程式Arduino教學 - 05:淺談 MCU 架構(以8051舉例) 提到的「通用暫存器」

所以問題到底在哪呢?

一個中斷服務子程序是有可能去修改到 number 的值的,假設程式運行到第 5 行的時候外部觸發了一個中斷子程序並且修改了 number 的值,當程式運行到第 11 行需要 number 這個值時,很有可能會抓到未修改過的 number 的值。

所以針對一個在程式的主體以及中斷服務子程序內都會共用的變量,我們應該宣告為 volatile 。這可以告訴編譯器我們不要優化這個語句,當每次遇到需要 number 的值時,直接從 number 的記憶體位置進行讀取,也不會將這個變數的值暫放在暫存器之中

中斷與主函數共用的變數應該宣告為 voltaile 範例程式碼(以下節錄自此)

volatile int etx_rcvd = FALSE;
void main() 
{     
    ... 
    while (!ext_rcvd) 
    { // Wait } 
    ... 
}

interrupt void rx_isr(void) 
{
    ... 
    if (ETX == rx_char) { 
        etx_rcvd = TRUE; 
    } 
    ... 
}


針對讀取 MCU 特殊暫存器的值,也必須宣告為 volatile

接下來我們更深入的探討有關於 MCU 那些特殊暫存器的部分,假射在一個應用中需要我們一直監測某個狀態暫存器的值

嵌入式系統的硬件實體中通常包含一些複雜的外圍設備(例如 I/O),操作這些設備中所使用的暫存器其值往往隨著程序的流程同步的進行改變。

一個非常簡單的例子

假設我們有一個 8 位的狀態暫存器映射在地址 0x1234 上,系統需要我們一直監測狀態暫存器的值,直到它的值不為 0 為止。(有關於指標請看後續章節)

通常錯誤的實現方法是

UINT1 * ptr = (UINT1 *) 0x1234;    // Wait for register to become non-zero.
while (*ptr == 0);         // Do something else.

一旦編譯器打開了最佳化選項,這種寫法肯定會失敗,其會生成類似如下的組合語言:

mov ptr, #0x1234 
    mov a, @ptr 
loop 
    bz loop 

編譯器優化了這個變數,將其讀入暫存器中( mov a, @ptr),如果編譯器發現從變數相關的上下文看變數的值總是不變的,那麼就沒有必要從記憶體中重新去讀取它。

為了強迫編譯器按照我們的意願進行編譯,我們修改指標的聲明為

UINT1 volatile * ptr = (UINT1 volatile *) 0x1234;
while (*ptr == 0);


對應的彙編程式碼為:

    mov ptr, #0x1234
loop
    mov a, @ptr
    bz loop


如此一來才會得到正確的結果

最後是「多排程應用程序」的狀況

volatile int cntr; 
void task1(void)
{ 
    cntr = 0; 
    while (cntr == 0) { 
        sleep(1); 
    } 
    ... 
}

void task2(void)
{ 
    ... 
    cntr++; 
    sleep(10); 
    . .. 
}

一個共享的全域變數在概念上通常和前面中斷處理程序中提到的情形是一樣的,所以這種情況下所有共享的全域變數都要被聲明為 volatile

Hint : volatile int a 與 int volatile a 是相同的宣告

volatile const 可以一起宣告?

同時使用 const 和 volatile 來定義一個變量是沒有問題的,例如 volatile const int x

const 含義 「這個變數是唯讀的,代表我們不能在程式中對該變數的值進行修改」
volatile 含義是 「取消編譯器對該變數的優化,且指明該變數隨時會被改變」

x 應該是唯讀不應該被修改的,但是它也可能被外部的 中斷、共享的線程... 通過某種方式被修改,所以這裡也不該被編譯器優化。

雖然它是唯讀不該被修改的,但它還是有可能會改變,我們在程式中對該變數進行使用的時候,還是要每次都去讀它的值。

順帶一提,volatile 類型的變量是默認存放在 RAM 中的,volative const 也會被分配到 RAM 中,但又帶有「唯讀」的特性,程序中無法對 volative const 定義的常量進行修改。

這聽起來有點微妙!? 但可以舉一個實際的例子 =>  MCU 中的「唯獨狀態暫存器

extern

我們曾經學過宣告在任何函式定義之外的變數,屬於全域變數

int flag;

全域變數可以讓相同檔案中,此變數宣告之後的所有函式進行存取

若我們想要在第二個檔案裡也想取得這個全域變數 flag 的話,則第二個檔案必須含有以下的宣告

extern int flag;

在這個宣告中,儲存類別修飾詞 extern 會告訴編譯器「變數 flag 可能定義在本檔案稍後的位置,或定義在其它檔案中」

編譯器會通知連結器,本檔案中含有指向變數 flag 的未解析參考(編譯器並不知道定義 flag 的位置,因此它會讓連結器找出 flag )

範例如下(假設專案 file 裡面具有三個檔案分別為 main.c, file1.c 以及 file2.c 其內含的程式碼分別為 :

/* main.c */
#include "stdio.h"
#include "stdlib.h"
// 這裡不用去執行類似 #include file1.c 等等的語句
// 若堅持使用,則會出現變數重複定義的報錯訊息

extern int file1_number;
extern int file2_number;

int main() {
 printf("file1_number = %d \n",file1_number);
 printf("file2_number = %d",file2_number);
 return 0;
}

/* file1.c */
int file1_number = 1;

/* file2.c */
int file2_number = 2;



執行結果如上

順道一提,還記得我們幾乎在每一個檔案中都有使用 #define "studio.h" 文件,此文件裡面包含了許多函式的「函式原型」,其跟 extern 同一道理(函式原型不需要使用 extern 修飾詞,但需要使用 #include 來囊括函式原型進來)

函式原型的主要作用在於告訴編譯器,它所指定的函式(標頭)可能定義在本檔案稍後的位置,或定義在不同的檔案中

另一方面,前面則提到,若宣告一個全域變數為 static 時,這個變數將被限制於這個檔案中使用,儘管別的檔案中使用 extern int flag ,仍舊存取不到這個變數

static int flag;


上例中我將 file1 改為,出現報錯

/* file1.c */
static int file1_number = 1;


有號數與無號數的計算

#include "stdio.h"
#include "stdlib.h"

int main() {

 unsigned int a = 5;
 int b = -20;
 ( (a+b) >= 0 ) ? printf(" a+b 大於等於0") : printf(" a+b 小於0");

}


這是一個很大的陷阱題

為什麼 (a+b) = (5+(-20)) = -15

但出現的結果會 "大於等於0" 呢?

其原因在於, a 是一個 unsigned int (無符號數) 而 b 是一個 int (有符號數)

在 C語言中 一個無符號數與一個有符號數做運算時,會強制都轉型為無符號數(unsigned)。這會使得變數 b 變成一個非常大的正整數。


↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引