# 韌體工程師的0x10個問題 ### preprocessor(預處理器) * 用於在編譯器處理程式之前預掃描原始碼,完成標頭檔包含, 巨集擴展, 條件編譯, 行控制(line control)等操作。 * 以 # 開頭都是前置處理器,例如:#include, #define ### 巨集(macro) * 我們可以運用巨集,來消除重複,也就是透過前置處理器指令去**定義文字替換的規則**,產生另一組程式碼,甚至還能創造語言本身沒有的語法 * 但是,巨集撰寫、除錯或者是維護,不是一件容易的事情。因為要讓巨集能適用諸多場合,往往必須加入更多額外的巨集程式碼,或者是特定環境的條件編譯,令巨集變得複雜。 ### #define 識別名稱 代換標記 * **識別名稱**:即替換內容的縮寫,一般來說是使用大寫較易辨認。 * **代換標記**:通常可以是常數、字串或是函數。 ### 1. 用預處理指令#define 聲明一個常數,用以表示1年中有多少秒 (忽略閏年問題) ```c= #define SECONDS_PER_YEAR(60 * 60 * 24 * 365)UL or #define SECONDS_PER_YEAR(60UL * 60UL * 24UL * 365UL) ``` 含意: 1. `#define`語法的基本知識,如**不能以分號結束、括號的使用**。 2. 了解到預處理器會去計算常數的運算式,因此它是簡潔的,比起你自己真的去計算一年中有多少秒數 3. 了解到運算式可能會在一個16bit的機器上對整數產生溢位(>2^16),因此需要用L,告訴編譯器對待運算式為Long。 4. 如果你更改運算式為UL(意指unsigned long),你會有一個好的開始,因為你注意到了signd與unsigned型態的危險。 #### * 補充 | 型態 | 大小(bytes) | | ----------- |:-----------:| | short | 2 | | int | 4 | | long | 4 | | float | 4 | | double | 8 | | long double | 16 | | char | 1 | ### 2. 寫一個"標準的" MIN macro。這個macro接收兩個參數並且回傳這兩個參數中較小的那個 ``` #define MIN(A,B) ((A) <= (B) ? (A):(B)) ``` 含意: 1. 對於`#define` 用在macros中的基礎知識。這很重要,因為直到inline operator變成標準C的一部分之前,巨集是方便產生行內程式碼的唯一方法。對於嵌入式系統來說,為了能達到要求的性能,行內程式碼經常是必須的方法。 2. 三元運算子的知識。(`a ? b : c` 當a為真則執行b,當a為否則執行c。) 這個運算子存在在C語言中的原因是,它使得編譯器能產生比if-then-else更優化的程式碼,了解這個用法是很重要的。 3. 懂得在巨集中小心地把參數用括號括起來。以及討論巨集的副作用,例如下面這個程式碼會發生什麼問題? ``` least=MIN(*p++,b); ``` Ans : `*p++`在進行比較時和輸出結果時的值會不一樣,`p++`會執行兩次。(我的想法) ### 3. 預處理指令#error的目的是什麼? #error就是生成編譯錯誤的訊息,然後會停止編譯,可以用在檢查程式是否是照自己所預想的執行。其語法格式為: ``` #error error-message ``` ### 無窮迴圈(Infinite loops) ### 4. 無窮迴圈常出現在嵌入式系統。如何用C寫一行無窮迴圈的程式 1. 首選方案是: ``` while(1){ ... } // while(邏輯表達式){ 反覆執行的語句 } ``` 只要邏輯表達式為真,就會反覆執行大括弧裡面的語句,直到邏輯表達式為假,循環結束,只要把邏輯表達式寫成1則循環成為死循環。 原文網址:https://kknews.cc/code/r9aqx4o.html。 2. 另一種常見的寫法: ``` for(;;){ .. } ``` 這是K&R(C語言書的作者)喜歡用的方式,並且是唯一一個方法來達成無窮迴圈且通過Lint。 3. 第三個方法是使用goto:(組合語言) ``` Loop: ... goto Loop; ``` ### 5. 使用變數a,給出下面的定義: #### a)一個整型數 (An integer) #### b)一個指向整數的指標 (A pointer to an integer) #### c)一個指向指標的指標,它指向的指標是指向一個整型數(A pointer to a pointer to an integer) #### d)一個有10個整數型的陣列 (An array of 10 integers) #### e)一個有10個指標的陣列,該指標是指向一個整數型的(An array of 10 pointers to integers) #### f)一個指向有10個整數型陣列的指標 (A pointer to an array of 10 integers) #### g)一個指向函數的指標,該函數有一個整數型參數並返回一個整數 (A pointer to a function that takes an integer as an argument and returns an integer) #### h)一個有10個指標的陣列,該指標指向一個函數,該函數有一個整數型參數並返回一個整數 (An array of ten pointers to functions that take an integer argument and return an integer) Ans: a) int a; b) int *a; c) int **a; d) int a[10]; e) int *a[10]; f) int (*a )[10]; g) int (*a)(int); h) int (*a[10])(int); ### 6. 關鍵字static的作用是什麼? static有三種不同的用法:(好處) 1. 在函數區間內(in funtion block),一個被宣告為靜態的**變數**,在函數被呼叫的過程中其值維持不變。 2. 在一個Block(ie. {...} )內 (但在函數外),一個被宣告為靜態的**變數**可以被Block內所有的函數存取,但不能被其他Block中的函數存取。它是一個本地的全局變量。(local的global變量) 3. 在一個block內,一個被宣告為靜態的**函數**,只可以被這一個block內的其他函數呼叫。也就是這個函數被限制在宣告它的Block的(本地)範圍內使用。 ------ //另一種答案// 1. function內做靜態變數宣告,在這一函數被呼叫的過程中其值維持不變 2. function外做靜態變數宣告或靜態函數宣告,代表此變數或此函數只能被同file的函數做存取 #### * [補充](https://www.itread01.com/article/1447398632.html) 在C語言中,static的三種作用: 1. 隱藏功能,利用這一特性可以在不同的檔案中定義同名函式和同名變數,而不必擔心命名衝突。 2. 保持變數內容的持久,儲存在靜態資料區的變數會在程式剛開始執行時就完成初始化,也是唯一的一次初始化。 3. 預設初始化為0,其實全域性變數也具備這一屬性,因為全域性變數也儲存在靜態資料區。在靜態資料區,記憶體中所有的位元組預設值都是0x00。 ### 7. 關鍵字const有什麼含意? const 代表 “read-only”。資料型別被const修飾的變數在「初始化」之外,不能再被賦予值。 (合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的程式碼修改。) #### 附加問題,這些未完成的宣告代表什麼? 1. const int a; 2. int const a; 3. const int *a; 4. int * const a; 5. int const * a const; 含意: 1. a是一個常數型整數 2. 同(1) 3. 一個指向常數型整數的指標(整數無法修改,但指標可以) 4. 一個指向整數的常數型指標(指標指向的整數可以修改,但指標是不可以修改的) 5. 一個指向常數型整數的常數型指標(指標指向的整數不可以修改,同時指標本身也是不可以修改的) ### 8. 關鍵字volatile(易變的)有什麼含意?並給出三個不同的例子。 一個被定義為volatile的變數代表說這個變數會不可預期的被改變。因此,編譯器就不會去假設這個變數的值了。具體來說,編譯器在用到這個變數時,每次都會小心地重載這個變數,而不是使用保存在暫存器的備份。Volatile變數的範例如下: ( a ) 周邊設備的硬體暫存器,如狀態暫存器 ( b ) 中斷服務函式(ISR)中會訪問到的非自動變數(Non-automatic variables) ( c ) 多執行緒應用中,被多個任務共享的變數 > 例子: 1. 並行設備的硬體暫存器 (如︰狀態暫存器) 2. ISR會修改的全域變數或static變數 3. 多執行緒下被多個task共享的變數 /volatile變數代表其所儲存的內容會不定時地被改變,宣告volatile變數用來告訴編譯器 (Compiler) 不要對該變數做任何最佳化操作,凡牽涉讀取該volatile變數的操作,保證會到該變數的實體位址讀取,而不會讀取CPU暫存器的內容 (提升效能) 。舉個例子,某一硬體狀態暫存器 (Status Register)就必須宣告volatile關鍵字,因為該狀態暫存器會隨時改變,宣告volatile便可確保每次讀取的內容都是最新的。/ #### 進階問題: ( a )一個參數可以同時是const也是volatile嗎?解釋為什麼。 ( b )一個指標可以是volatile 嗎?解釋為什麼。 ( c )下面的函數有什麼錯誤︰ ``` int square(volatile int *ptr){ return *ptr * *ptr; } ``` 答案如下: ( a )是的,舉例說明像是"read only的狀態暫存器"。它是volatile,因為它可能會被非預期的改變;它是const,因為程式不應該試圖修改它。 ( b )是的,儘管這並不常見。一個例子是中斷服務函式修改一個指向buffer的指標時。 ( c )這段程式碼的目的是用來返指標***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; } ``` ### 位元操作(Bit operation) 在 C 中提供的位元運算子,分別是AND、OR、NOT、XOR與補數。 ### 9. 嵌入式系統總是需要使用者去操作在暫存器或者變數中的位元。 給一個整數變數a,寫出兩段程式碼。第一個要設定a的bit3,第二個要清除a的bit3。在以上兩個操作中,要保持其它位不變。 Ans: 用 #defines 和 bit masks 操作。解決方法如下: ```c= //寫法一 #define BIT3(0x1<<3) static int a; void set_bit3(void){ a |= BIT3; } void clear_bit3(void){ a &= ~BIT3; } -------------- //寫法二 #define BIT3 (1U << 3) int main() { int a = 0; /* set bit 3 */ a |= BIT3; /* clear bit 3 */ a &= ~BIT3; } ``` 另一種用macro的寫法: ```c= #define SET_BIT(p,n) ((p) |= (1 << (n))) #define CLEAR_BIT(p,n) ((p) &= ~(1 << (n))) #define FLIP_BIT(p,n) ((p) ^= (1 << (n))) #define CHECK_BIT(p,n) ((p) & (1 << (n)) ``` 重點是要看到明白的常數,以及使用 |= 和 &= ~結構。 ### 10. 嵌入式系統經常具有要求程式員去存取某特定的記憶體位置的特點。 在某個專案中被要求設定一個絕對位址在0x67a9的整數變數為數值0xaa55。編譯器是一個純ANSI編譯器。寫下程式碼來完成這個任務。 這個問題測試你是否知道為了存取一個絕對位置,去**型別轉換一個整數**成一個指標這是合法的。確切的語法根據每個人的風格因人而異。典型的程式碼如下: ```c= int main(){ int *ptr = (int *)0x67a9; *ptr = 0xaa55; } ``` ### 中斷(interrupt) 當CPU在執行程式時,遇到外部或內部的緊急事件須優先處理,因此暫停執行當前的程式,轉而服務突發的事件。直到服務完畢,再回到原先的暫停處(記憶體地址)繼續執行原本尚未完成的程式。 ### 11. 中斷是嵌入式系統很重要的一部分。因此,很多編譯器供應商提供一個標準C的擴展來支持中斷。 典型的,這個新的key word是__interrupt。以下的程式碼使用__interrupt來定義一個中斷service routine。評論這個程式碼: ```c= __interrupt double compute_area(double radius) { double area = PI*radius*radius; printf("\nArea=%f", area); return area; } ``` 這個函數有太多錯誤了,問題如下: 1) ISR不能返回一個值 2) ISR不能傳遞參數 3) 在許多編譯器/處理器中,浮點數操作是不可重入的(re-entrant)。有些處理器/編譯器需要讓多餘的暫存器入棧(PUSH入堆疊),有些處理器/編譯器就是不允許在ISR中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。 4) 與第三點類似,printf通常會有可重入和效能的問題。 ### 12. 下方的程式碼將會輸出什麼,並且為什麼會這樣輸出? ```c= 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的資料型態應該會被頻繁的使用。 ### 13. 評論以下的程式碼片段? ```c= unsigned int zero = 0; unsigned int compzero = 0xFFFF; /* 1's complement of zero */ ``` 對於一個int型不是16位元的機器來說,它將會導致錯誤。 正確的程式碼如下: ```c= unsigned int compzero = ~0; ``` 這個問題真的可以知道應試者是否了解字長在處理器的重要性。好的嵌入式程式設計師會清楚的知道硬體的細節與它的限制,然後電腦程式設計師傾向忽視硬體,並把它視為一個無法避免的煩惱。 ### 14. 儘管不像非嵌入式計算機那麼常見,嵌入式系統還是有從堆積(heap)中動態分發內存的過程的。那麼嵌入式系統中,動態分發記憶體可能發生的問題是什麼? 會發生的問題像是記憶體碎片,碎片收集(垃圾回收)的問題,變量的生命週期(變數的執行時間)等等。 ### 15. 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兩個指標。 ### 16. C語言允許一些令人驚訝的結構。這個結構合法嗎?如果合法的話這段程式碼會做什麼? ```c= int a = 5, b = 7, c; c = a+++b; ``` 根據“maximum munch”原則,編譯器應當能儘可能處理所有合法的用法。因此,上面的程式碼被處理成︰ ``` c = a++ + b; ``` 因此,在這個程式碼執行之後,a = 6, b = 7 & c = 12; (其實a++ 就是後做,先運算完之後再++) --- ## 其他筆記 * malloc 函數 > malloc 代表 memory allocation,用來配置指定大小的記憶體空間,傳回新空間第一個位元組的記憶體位址,配置的空間處於尚未初始化的狀態 malloc int 一維陣列 int *ptr = (int *) malloc(sizeof(int) * 3); * free 函數 > 釋放之前使用 malloc 或 calloc 函數所配置的記憶體空間。 --- ### [](https://stackoverflow.com/questions/8208021/how-to-increment-a-pointer-address-and-pointers-value/8208106#8208106)How to increment a pointer address and pointer's value? ``` p++; // use it then move to next int position ++p; // move to next int and then use it ++*p; // increments the value by 1 then use it ++(*p); // increments the value by 1 then use it ++*(p); // increments the value by 1 then use it *p++; // use the value of p then moves to next position (*p)++; // use the value of p then increment the value *(p)++; // use the value of p then moves to next position *++p; // moves to the next int location then use that value *(++p); // moves to next location then use that value ``` --- ## 程式題目 1+2+4+7+...+106 的總和? ``` #include <stdio.h> int main() { int Sum = 0,i = 1,j = 1; while(i <= 106){ Sum = Sum + i; printf("i=%d Sum=%d\n", i, Sum); i = i + j; j = j + 1; } printf("Sum=%d", Sum); system("pause"); //可不寫,類似於視窗中的請按任意鍵繼續(?) return 0; } ``` ## 資料參考 https://medium.com/@racktar7743 https://creteken.pixnet.net/blog/post/24524138-c%E8%AA%9E%E8%A8%80%E6%B8%AC%E8%A9%A6-%E6%87%89%E7%9F%A5%E9%81%93%E7%9A%840x10%E5%80%8B%E5%9F%BA%E6%9C%AC%E5%95%8F%E9%A1%8C