# 韌體工程師應該要知道的 `0x10` 個問題 ## 預處理器(Preprocessor) ### Ques. 01 > 用預處理指令 `#define` 聲明一個常數,用以表示一年中有多少秒(忽略閏年問題) ```c= #define SECOND_PER_YEAR (365 * 24 * 60 * 60)UL ``` ##### 考點分析 : 1. `#define` 語法基本知識(Ex. 不能以分號結束、括號的使用) 2. 命名的習慣(Ex. 大寫及底線) 3. 了解到預處理會去計算常數運算式 4. 理解到運算式可能會在一個 16-bits 的機器上對整數產生溢位,因此需要 `L`,告訴編譯器對待運算式為 `long` 5. 如果你更改運算式為UL(i.e. unsigned long),你會有一個好的開始,因為你注意到了 signd 與 unsigned 型態的危險 ##### 補充 : 資料型態 | 型態 | 關鍵字 | 常見大小(32/64-bit) | 格式化符號 | | :- | :- | :-: | :-: | | 字元 | `char` | 1 | `%c` | | 無號字元 | `unsigned char` | 1 | `%u` | | 短整數| `short`/`short` | 2 | `%d` | | 無號短整數 | `unsigned short` | 2 | `%c` | | 整數 | `int` | 4 | `%d` | | 無號整數 | `unsigned int` | 4 | `%u` | | 長整數 | `long` | 4/8 | `%ld` | | 無號長整數 | `unsigned long` | 4/8 | `%lu` | | 單精度浮點數 | `float` | 4 | `%f` | | 倍精度浮點數 | `double` | 8 | `%f` | | 長倍精度浮點數 | `long double` | 12/16 | `%Lf` | * 字元/整數/長整數通常會預設為有號(signed),如果要特別強調可以使用 `signed char`/`signed int`/`signed long` * C 語言預設整數常數是 `int` 型態,浮點數常數是 `dluble` 型態,如果要指定為特定型態或是避免溢位,則需要加上適當的後綴字(可以混合使用) * `U`/`u` 表示 unsigned * `L`/`l` 表示 long * `LL`/`ll` 表示 long long * `F`/`f` 表示 float ```c= unsigned int a = 10U; long b = 10L; float c = 3.14F; long double d = 2.06L; unsigned long e = 25UL; ``` (後綴字不分大小寫,但通常使用大寫) ### Ques. 02 > 寫一個標準 MIN 巨集,這個巨集輸入兩個參數並回傳較小的那個 ```c= #define MIN(X, Y) ((X) >= (Y) ? (X) : (Y)) ``` ##### 考點分析 : 1. 對於 `#define` 用在 macro 中的基礎知識。這很重要,因為直到 inline 運算子變成標準 C 的一部分之前,巨集是方便產生行內程式碼的唯一方法。對於嵌入式系統來說,為了能達到要求的性能,行內程式碼經常是必須的方法 2. 三元運算子 `?:` 的認識 3. 懂得在巨集中小心地把參數用括號括起來。以及討論巨集的副作用,例如下面這個程式碼會發生什麼問題? ```c least = MIN(*p++,b); ``` Ans: 最後的 `*p` 的輸出結果可能會視情況被增加 1 到 2 次。 Why: 首先,巨集會被展開為 `least = ((*p++) >= (b) ? (*p++) : (b))` * 第一次遞增(`*p++`)後跟 `b` 的值比較 * 若 `*p++` 比較大,則執行第二次遞增(`*p++`)後回傳給 `least` * 若 `*p++` 比較小,則將 `b` 回傳給 `least` 且 `*p` 被遞增一次 ### Ques. 03 > 預處理指令 `#error` 的目的是什麼? **Syntax:** ```c #error message ``` 用於在編譯時輸出錯誤訊息並停止編譯。當編譯器遇到 `#error` 指令時會輸出指定的錯誤訊息並停止編譯。 **Ex.** ```c #include <stdio.h> //#define NUM 3 int main(void) { #ifndef NUM #error NOT define NUM #endif printf("NUM = %d", NUM); return 0; } ``` ```bash > gcc ./test.c ./test.c: In function 'main': ./test.c:6:10: error: #error NOT define NUM 6 | #error NOT define NUM | ^~~~~ ./test.c:9:24: error: 'NUM' undeclared (first use in this function) 9 | printf("NUM = %d", NUM); | ``` ## 無窮迴圈(Infinite loops) ### Ques. 04 > 無窮迴圈常出現在嵌入式系統中,試問要如何用 C 寫出一行無窮迴圈的程式 **第一種: 常見方式** ```c= while (1) { /* statement */ } ``` **第二種: K&R 推薦** ```c= for (;;) { /* statement */ } ``` ## 資料宣告(data declaration) ### Ques. 05 > 使用變數 `a` 給出以下定義 > 1. 一個整數(An integer) > 2. 一個指向整數的指標(A pointer to an integer) > 3. **一個指向指標的指標,它指向的指標是指向一個整數(A pointer to a pointer to an integer)** > 4. 一個有 10 個整數的陣列(An array of 10 integers) > 5. **一個有 10 個指標的陣列,該指標是指向一個整數(An array of 10 pointers to integers)** > 6. **一個指向有 10 個整數陣列的指標(A pointer to an array of 10 integers)** > 7. **一個指向函式的指標,該函式有一個整數參數且回傳一個整數(A pointer to a function that takes an integer as an argument and returns an integer)** > 8. **一個有 10 個指標的陣列,該指標指向一個函式,該函式有一個整數參數並回傳一個整數(An array of ten pointers to functions that take an integer argument and return an integer)** ```c= // 1. 整數 int a; // 2. 指向整數的指標 int *a; // 3. 指向指標的指標,它指向的指標是指向一個整數 int **a; // 4. 有 10 個整數的陣列 int a[10]; // 5. 有 10 個指標的陣列,該指標是指向一個整數 int *a[10]; // 6. 指向有 10 個整數陣列的指標 int (*a)[10]; // 7. 指向函式的指標,該函式有一個整數參數且回傳一個整數 int (*a) (int); // 8. 有 10 個指標的陣列,該指標指向一個函式,該函式有一個整數參數並回傳一個整數 int (*a[10])(int); ``` ## Static ### Ques. 06 > 關鍵字 `static` 的作用是什麼 關鍵字 `static` 的作用有 3 種 1. 在函式內被宣告為 `static` 的變數,會在函式結束後繼續保留它的數值 2. 在一個區塊(block)內,一個被宣告為靜態的變數可以被區塊內的所有函式存取,但不能被其他區塊中的函式存取,是一種區域型的全域變數 3. 在一個區塊(block)內,一個被宣告為靜態的函式只可以被這一個區塊內的其他函式呼叫。也就是這個函式被限制在宣告它的區塊的(本地)範圍內使用 ## Const ### Ques. 07 > 關鍵字 `const` 有什麼含義 關鍵字 `const` 表示 read-only。被 `const` 修飾後的變數在初始化後不能再被賦予其他值 ##### 附加問題 : 以下未完成的宣告代表什麼意思 1. `const int a;` 2. `int const a;` 3. `const int *a;` 4. `int *const a;` 5. `const int * const a;` Ans: 1. 一個整數常數 2. 一個整數常數 3. 指向整數常數的指標(整數值不能修改,指標指向可以) 4. 指向整數的常數指標(整數值可以修改,指標指向不能) 5. 指向整數常數的常數指標(整數值不可修改,指標指向不可修改) > [!Tip] > 從右往左解釋 ##### Reference: * [const T vs. T const ——Dan Saks 【翻译】](https://blog.csdn.net/Stephen_yu/article/details/51790075?locationNum=9) ## Volatile ### Ques. 08 > 關鍵字 `volatile` 有什麼含義,給出 3 個例子 `volatile` 關鍵字可以防止變數被改變,通常是發生在編譯時因為編譯器的最佳化效能或其他不可預期的原因造成。編譯器使用到被宣告為 `volatile` 的變數時會重新載入這個變數,而不是使用暫存器中的備份。常見舉例如下 : * 週邊裝置的硬體暫存器 * ISR 會存取到的非自動變數 * 多執行緒中,被多個任務共享的變數 ##### 進階問題 : 1. 一個參數可以同時是 `const` 也是 `volatile` 嗎?為什麼 2. 一個指標可以是 `volatile` 嗎?為什麼 3. 以下函式有何錯誤? ```c int square(volatile int *ptr) { return *ptr * *ptr; } ``` Ans: 1. 例如一個唯讀(read-only)的暫存器是 `volatile`,因為可能會被非預期的改變;同時它也是 `const`,因為程式不能修改它 2. 可以(但不常見)。例如 ISR 修改一個指向 buffer 的指標 3. 編譯沒錯,但有語義錯誤 因為 `ptr` 是指向一個 `volatile` 的參數,所以回傳值 `(*ptr) * (*ptr)` 會被編譯器解讀為類似以下函式 ```c int square(volatile int *ptr) { int temp1 = *ptr; // 第一次讀取 int temp2 = *ptr; // 第二次讀取 return temp1 * temp2; } ``` 如果 `ptr` 指向的變數在兩次讀取的過程中被改變(Ex. ISR),則最後的結果就不會是平方。應該在函式內部先把 `ptr` 指向的變數存到另一個區域變數後再做乘法。 ```c int square(volatile int *ptr){ int temp; temp = *ptr; return temp * temp; } ``` ## 位元操作(Bit operation) ### Ques. 09 > 嵌入式系統總是需要使用者去操作暫存器或者變數中的位元。給定一個整數變數 `a`,寫出兩個程式片段,第一個要去設定 `a` 的 bit 3,第二個要去清除 `a` 的 bit 3。在這兩個案例中,其他的位元保持不變。 **我的解法:** ```c a = a | 0x08; // set bit 3 to 1 a = a & (~0x08); // clear bit 3 to 0 ``` **高度可移植的方法:** ```c #define BIT3 (0x1<<3) void set_bit3(void) { a |= BIT3; } void clear_bit3(void) { a &= ~BIT3; } ``` ## 存取固定記憶體位址 ### Ques. 10 > 嵌入式系統常有一個特點是要求程式設計師去存取特定的記憶體位置。在某個專案中被要求設定一個絕對位址在 `0x67a9` 的整數變數為數值 `0xaa55`。編譯器是一個純 ANSI 編譯器。寫下程式碼來完成這個任務。 ```c int main() { int *ptr = (int *)0x67a9; *ptr = 0x2255; return 0; } ``` #### 考點分析 : 這個問題要考的是,知道為了存取一個絕對位置必須去型別轉換一個整數成一個指標,才能形成絕對位址並對這個指標作 dereference ## 中斷(Interrut) ### Ques. 11 > 中斷是嵌入式系統很重要的一部分。因此很多編譯器供應商提供一個標準 C 的擴展來支持中斷。典型的新關鍵字是 `__interrupt`。以下的程式碼使用 `__interrupt` 來定義一個中斷服務程式。評論以下這個程式碼 ```c __interrupt double compute_area(double radius) { double area = PI*radius*radius; printf("\nArea=%f", area); return area; } ``` 這個函式有以下幾項錯誤 : 1. ISR 不能有回傳值與參數 2. 不建議在 ISR 中作浮點數運算(太過複雜) 3. 不建議在 ISR 中作 `printf` > [!Note] > 2. 與 3. 與可重入性有關 ## 程式範例(Code example) ### Ques. 12 > 下方的程式碼將會輸出什麼,並且為什麼會這樣輸出? ```c void foo(void) { unsigned int a = 6; int b = -20; (a + b > 6) ? puts(">6"):puts("<=6"); } ``` ##### 考點分析 : 這題要考 C 語言中的自動轉型原則。當運算式中有 signed 與 unsigned 型態的時候,所有運算元都會自動轉換成 unsigned 類型。 因此 `a + b > 6` 中的 `b` 會被轉為是一個超大正整數,最後結果應該是 `>6` ### Ques. 13 > 評論以下的程式碼片段 ```c unsigned int zero = 0; unsigned int compzero = 0xFFFF; /* 1's complement of zero */ ``` ##### 考點分析 : `compzero = 0xFFFF` 的本質是 `0x1111 1111 1111 1111`。但事實上不是所有的平台對整數的限制都是 16-bit。比方說常見的 32/64 位元系統中,整數的大小是 4 bytes = 32-bits 的長度,此時 `compzero` 只有最低 16 位元是 `1`,最高的 16 位元都會是 `0`。 (i.e. `0x0000 0000 0000 0000 1111 1111 1111 1111 = 0x0000FFFF`) 正確做法應該是直接使用補數 ```c unsigned int compzero = ~0; ``` ## 動態記憶體分配(Dynamic Memory Allocation) ### Ques. 14 ★★★ > 雖然在非嵌入式系統上並不常見,嵌入式系統仍然在heap上做動態的配置記憶體。做動態記憶體配置在嵌入式系統上會有什麼問題? ## 自定義資料(Typedef) ### Ques. 15 > `typedef` 頻繁的在 C 語言中使用,來對一個已經存在的資料型態宣告為同義字。也可以用預處理器做類似的事,舉例來說,考慮下方的程式碼片段 : > > ```c= > #define dPS struct s * > typedef struct s * tPS; > ``` > > 以上兩種情況都是要定義 `dPS` 和 `tPS` 為一個指向結構 `s` 的指標。哪個方法比較好,並解釋為什麼? 使用 `typedef` 的方法(第二種)比較好。考慮以下的兩種宣告方式 ```c= dPS p1, p2; // 第一種 tPS p3, p4; // 第二種 ``` 第一種宣告方式會被擴展為 ```c struct s * p1, p2; ``` `p1` 是指向結構 `s` 的指標,但 `p2` 是一個真正的結構體,不是真正的(指向結構的)指標。 ## 遞增運算子(Increasing operator) ### Ques. 16 > C 語言允許一些令人驚訝的結構。以下這個結構合法嗎?如果合法的話這段程式碼會做什麼? > ```c= > int a = 5, b = 7, c; > c = a+++b; > ``` ##### 考點分析 : 根據 "maximum munch" 原則,編譯器應當能儘可能處理所有合法的用法。 以上程式碼片段等價於會被解析為 ```c c = (a++)+b ``` 因此最後輸出結果會是 * `a = 6;` * `b = 7;` * `c = 12;`