# 韌體工程師的0x10個問題 ## 1. 預處理器(Preprocessor) 用預處理指令#define 宣告一個常數,用以表示1年中有多少秒(忽略閏年問題) ```c #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL ``` 1. #define語法的基本知識,如不能以分號結束、括號的使用。 2. 了解到預處理器會去計算常數的運算式,因此它是簡潔的,比起你自己真的去計算一年中有多少秒數 3. 了解到運算式可能會在一個16bit的機器上對整數產生溢位(>2^16),因此需要用L,告訴編譯器對待運算式為Long。 4. 如果你更改運算式為UL(意指unsigned long),你會有一個好的開始,因為你注意到了signd與unsigned型態的危險。 ```c #define MIN(A,B) ((A) <= (B) ? (A):(B)) ``` **括號很重要!** ```c least=MIN(*p++,b); ``` 在這個巨集定義中,MIN(A,B)會返回A和B中較小的那個值。在給least賦值時, *p++和b會作為參數傳遞給MIN宏。*p++表示取指標p指向的值,然後將指標向後移動一位。b是一個變數。MIN(*p++, b)將返回*p++和b中較小的那個值,然後將這個值賦給least。 ## 2. 無窮迴圈(Infinite loops) ```c while(1){ ... } // while(邏輯表達式){ 反覆執行的語句 } ``` ## 3.使用變數a,給出下面的定義: a)一個整型數 (An integer) ```c int a; ``` b)一個指向整數的指標 (A pointer to an integer) ```c int *a; ``` c)一個指向指標的指標,它指向的指標是指向一個整型數(A pointer to a pointer to an integer) ```c int **a; ``` d)一個有10個整數型的陣列 (An array of 10 integers) ```c int arr[10]; ``` e)一個有10個指標的陣列,該指標是指向一個整數型的(An array of 10 pointers to integers) ```c int *arr[10]; ``` f)一個指向有10個整數型陣列的指標 (A pointer to an array of 10 integers) ```c int (*arr)[10]; ``` g)一個指向函數的指標,該函數有一個整數型參數並返回一個整數 (A pointer to a function that takes an integer as an argument and returns an integer) ```c int (*a)(int); ``` h)一個有10個指標的陣列,該指標指向一個函數,該函數有一個整數型參數並返回一個整數 (An array of ten pointers to functions that take an integer argument and return an integer) ```c int (*a[10])(int); ``` **注意!** int *ptr[10]; 這是一個包含10個 int* 指針的數組,不是您可能認為的指向包含10個整數的數組的指針。 int (*ptr)[10]; 這是一個指向包含10個整數的數組的指針。 我相信這與 int *ptr; 相同,因為兩者都可以指向一個數組,但給定的形式只能指向包含10個整數的數組。 ## 4.關鍵字static的作用是什麼? 1.在函數體,一個被宣告為靜態的變數在這函數被呼叫過程中維持其值不變。 2.在模組內(但在函數體外),一個被宣告為靜態的變數可以被模組內所用函數訪問,但不能被模組外其它函數訪問。它是一個本地的全域變數。 3.在模組內,一個被宣告為靜態的函數只可被這一模組內的其它函數呼叫。那就是,這個函數被限制在聲明它的模組的本地範圍內使用。 * 補充 在C語言中,static的三種作用: 1.隱藏功能,利用這一特性可以在不同的檔案中定義同名函式和同名變數,而不必擔心命名衝突。 2.保持變數內容的持久,儲存在靜態資料區的變數會在程式剛開始執行時就完成初始化,也是唯一的一次初始化。 3.預設初始化為0,其實全域性變數也具備這一屬性,因為全域性變數也儲存在靜態資料區。在靜態資料區,記憶體中所有的位元組預設值都是0x00。 ## 5.關鍵字const有什麼含意? const 代表 “read-only”。資料型別被const修飾的變數在「初始化」之外,不能再被賦予值。 (合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的程式碼修改。) 附加問題,這些未完成的宣告代表什麼? const int a; int const a; const int *a; int * const a; int const * a const; 含意: a是一個常數型整數 同(1) 一個指向常數型整數的指標(整數無法修改,但指標可以) 一個指向整數的常數型指標(指標指向的整數可以修改,但指標是不可以修改的) 一個指向常數型整數的常數型指標(指標指向的整數不可以修改,同時指標本身也是不可以修改的) ## 6.關鍵字volatile(易變的)有什麼含意?並給出三個不同的例子。 一個被定義為volatile的變數代表說這個變數會不可預期的被改變。因此,編譯器就不會去假設這個變數的值了。具體來說,編譯器在用到這個變數時,每次都會小心地重載這個變數,而不是使用保存在暫存器的備份。Volatile變數的範例如下: ( a ) 周邊設備的硬體暫存器,如狀態暫存器 ( b ) 中斷服務函式(ISR)中會訪問到的非自動變數(Non-automatic variables) ( c ) 多執行緒應用中,被多個任務共享的變數 例子: 並行設備的硬體暫存器 (如︰狀態暫存器) ISR會修改的全域變數或static變數 多執行緒下被多個task共享的變數 /volatile變數代表其所儲存的內容會不定時地被改變,宣告volatile變數用來告訴編譯器 (Compiler) 不要對該變數做任何最佳化操作,凡牽涉讀取該volatile變數的操作,保證會到該變數的實體位址讀取,而不會讀取CPU暫存器的內容 (提升效能) 。舉個例子,某一硬體狀態暫存器 (Status Register)就必須宣告volatile關鍵字,因為該狀態暫存器會隨時改變,宣告volatile便可確保每次讀取的內容都是最新的。/ 進階問題: ( a )一個參數可以同時是const也是volatile嗎?解釋為什麼。 是的。一個例子是只讀的狀態暫存器。它是volatile因為它可能被意想不到地改變。它是const因為程式不應該試圖去修改它。 ( b )一個指標可以是volatile 嗎?解釋為什麼。 是的,儘管這並不常見。一個例子是中斷服務函式修改一個指向buffer的指標時。 ( c )下面的函數有什麼錯誤︰ ```c int square(volatile int *ptr){ return *ptr * *ptr; } ``` 這段程式碼的目的是用來返指標*ptr指向值的平方,但是,由於 *ptr指向一個volatile型參數,編譯器將產生類似下面的程式碼︰ ```c int square(volatile int *ptr){ int a, b; a = *ptr; b = *ptr; return a*b; } ``` 因為*ptr的值可能會被不預期的改變,因此a和b可能是不同的。所以,這段程式碼可能返回不是你所期望的平方值!正確寫法的程式碼如下︰ ```c int square(volatile int *ptr){ int a; a = *ptr; return a*a; } ``` ## 7.位元操作(Bit manipulation) ## 8.嵌入式系統經常具有要求程式員去存取某特定的記憶體位置的特點。 在某個專案中被要求設定一個絕對位址在0x67a9的整數變數為數值0xaa55。編譯器是一個純ANSI編譯器。寫下程式碼來完成這個任務。 這個問題測試你是否知道為了存取一個絕對位置,去型別轉換一個整數成一個指標這是合法的。確切的語法根據每個人的風格因人而異。典型的程式碼如下: ```c int main(){ int *ptr = (int *)0x67a9; *ptr = 0xaa55; } ``` ## 9.中斷是嵌入式系統很重要的一部分。因此,很多編譯器供應商提供一個標準C的擴展來支持中斷。 典型的,這個新的key word是__interrupt。以下的程式碼使用__interrupt來定義一個中斷service routine。評論這個程式碼: __interrupt double compute_area(double radius) { double area = PI*radius*radius; printf("\nArea=%f", area); return area; } 這個函數有太多錯誤了,問題如下: ISR不能返回一個值 ISR不能傳遞參數 在許多編譯器/處理器中,浮點數操作是不可重入的(re-entrant)。有些處理器/編譯器需要讓多餘的暫存器入棧(PUSH入堆疊),有些處理器/編譯器就是不允許在ISR中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。 與第三點類似,printf通常會有可重入和效能的問題。 ## 10.下方的程式碼將會輸出什麼,並且為什麼會這樣輸出? void foo(void) { unsigned int a = 6; int b = -20; (a + b > 6) ? puts(">6"):puts("<=6"); } 這個問題測試你是否懂得C語言中的整數自動轉型原則。 這題的答案會輸出“> 6”。因為當表達式中存在singed與unsinged型態的時候,所有的運算元都會自動轉換為無符號類型(unsigned)。 因此–20變成了一個非常大的正整數,並且這個表達式計算出的結果大於6。這是個在嵌入式系統非常重要的點,因為unsigned的資料型態應該會被頻繁的使用。 ## 11.評論以下的程式碼片段? unsigned int zero = 0; unsigned int compzero = 0xFFFF; /* 1's complement of zero */ 對於一個int型不是16位元的機器來說,它將會導致錯誤。 正確的程式碼如下: unsigned int compzero = ~0; 這段程式碼的目的是計算零的1's補數(即將所有位元反轉),並將結果存儲在 compzero 中。然而,這段程式碼在一個不是16位元的整數機器上可能會導致錯誤,因為使用了固定的16位元值。為了使這段程式碼通用並適用於任何大小的整數,應該使用 ~0 來代替 0xFFFF。 ## 12.儘管不像非嵌入式計算機那麼常見,嵌入式系統還是有從堆積(heap)中動態分發內存的過程的。那麼嵌入式系統中,動態分發記憶體可能發生的問題是什麼? 會發生的問題像是記憶體碎片,碎片收集(垃圾回收)的問題,變量的生命週期(變數的執行時間)等等。 内存碎片化/内存泄漏 ## 13. Typedef 頻繁的在C語言中使用,來對一個已經存在的資料型態宣告為同義字。也可以用預處理器做類似的事。 例如,思考一下下面的例子︰ #define dPS struct s * typedef struct s * tPS; 以上兩種情況都是要定義dPS和tPS為一個指向結構s的指標。哪個方法比較好,並解釋為什麼? Ans:typedef更好。思考下面的例子: dPS p1, p2; tPS p3, p4; 第一個式子會被擴展成struct s * p1, p2; 上面程式碼定義p1為一個指向結構的指標,p2為一個實際的結構變數,這並不是我們原本想要的。 第二個式子正確定義了p3和p4兩個指標。 ## 14. C語言允許一些令人驚訝的結構。這個結構合法嗎?如果合法的話這段程式碼會做什麼? ```c int a = 5, b = 7, c; c = a+++b; ``` 根據“maximum munch”原則,編譯器應當能儘可能處理所有合法的用法。因此,上面的程式碼被處理成︰ c = a++ + b; 因此,在這個程式碼執行之後,a = 6, b = 7 & c = 12; (其實a++ 就是後做,先運算完之後再++)