# 韌體工程師的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++ 就是後做,先運算完之後再++)