###### tags: `CPP` `Arduino` # 04 if 流程控制與取餘數運算--設計漸亮效果 前面的教學讓我們可以用快速切換的方式呈現不同的亮度, 如果可以把亮燈的時間從 0 變化到 100us, 就可以呈現漸亮的效果。為了要做到這一點, 我們可以新增一個變數 `count`, 紀錄切換亮/熄的次數, 每切換 100 次, 即每 100\*100us = 10ms, 也就是 0.01 秒, 就將點亮的時間增加 1us, 這樣就可以每隔一小段時間讓 LED 變更亮一點。 為了能夠判斷是否已經切換亮/熄 100 次, 還需要新的功能, 叫做 [`if`](https://www.arduino.cc/reference/en/language/structure/control-structure/if/) 敘述, `if` 的語法如下: ```cpp if(判斷條件) { 條件成立時要執行的動作 } 判斷條件成不成立都會從這裡繼續 ``` `if` 的後面必須跟著一對小括號, 然後再跟著一個區塊。小括號內是要**判斷的條件**, 如果條件成立, 就會執行區塊內的敘述, 執行完後再從區塊後的敘述開始執行。如果條件不成立, 就會直接跳到區塊後的敘述。以我們的例子來說, 就可以利用這樣的寫法判斷是否已經切換 100 次: ```cpp if(count == 100) { duration_on = duration_on + 1; count = 0; } ``` 這裡的 [**`==`**](https://www.arduino.cc/reference/en/language/structure/comparison-operators/equalto/) 是比較兩邊的值是否相等?也就是比較變數 `count` 的值是不是 100?若是相等, 判斷就成立, 才會執行區塊內增加亮燈時長的敘述。 C++ 會用 [`boolean`](https://www.arduino.cc/reference/en/language/variables/data-types/boolean/) 這種型態的資料來表示比較結果, 它只有兩種可能值, [`true`](https://www.arduino.cc/reference/en/language/variables/constants/constants/) 代表**成立**、**真**的意思, 而 [`false`](https://www.arduino.cc/reference/en/language/variables/constants/constants/) 代表**不成立**、**假**的意思。 :::info 常用的比較運算如下: |運算|說明|運算|說明|運算|說明| |----|----|----|----|----|----| |[<](https://www.arduino.cc/reference/en/language/structure/comparison-operators/lessthan/)|小於|[<=](https://www.arduino.cc/reference/en/language/structure/comparison-operators/lessthanorequalto/)|小於或等於|[==](https://www.arduino.cc/reference/en/language/structure/comparison-operators/equalto/)|相等| |[>](https://www.arduino.cc/reference/en/language/structure/comparison-operators/greaterthan/)|大於|[>=](https://www.arduino.cc/reference/en/language/structure/comparison-operators/greaterthan/)|大於或等於|[!=](https://www.arduino.cc/reference/en/language/structure/comparison-operators/notequalto/)|不相等| ::: 以下就是依此修改的程式: ```cpp= int duration_on = 0; // 亮燈時間 int count = 0; // 計算切換次數 void setup() { pinMode(5, OUTPUT); } void loop() { if(count == 100) { duration_on = duration_on + 1; count = 0; } digitalWrite(5, LOW); delayMicroseconds(duration_on); digitalWrite(5, HIGH); delayMicroseconds(100 - duration_on); count = count + 1; } ``` 1. 第 2 行是新增的變數 `count`, 代表切換亮/熄的次數, 初始的值為 0。 2. 第 17 行是在 `loop` 的最後, 也就是切換亮/熄之後, 把 `count` 的值加 1, 表示又多切換了 1 次。注意到這裡等號的左右邊都出現了變數 `count`, 執行順序是**先計算等號右邊**, 用 `count` 的值加 1, 然後**將計算結果放回** `count`, 所以 `count` 的值就會增加 1。 ```mermaid flowchart LR count[/count/] exp[[0 + 1]] one([1]) count-- 取值 -->exp--運算-->one--存回-->count ``` 4. 第 9 行就是套用剛剛介紹的 `if` 判斷是否已經切換了 100 次? 6. 第 10 行就是在 `count` 達到 100, 也就是亮/熄切換 100 次時將點亮時間加長 1us 的程式。 8. 第 11 行是將 `count` 變回 0 **重新計數**, 這一行很重要, 如果忘了這一行, 之後 `if` 的判斷就會因為 `count` 已經超過 100 而不會成立了。 現在你可以重新上傳程式看看, 你應該會看到內建 LED 慢慢亮起來, 然後就熄掉了。如果來不及看到, 可以按一下板子上的 RESET 鈕重新執行。 ## 不同資料型別的問題 咦!剛剛的程式為什麼最後 LED 會熄掉呢?這是因為當 `duration_on` 超過 100 後, 用 `100 - duration_on` 計算的熄燈時間就會變成負數, 但是 `delayMicroseconds` 希望你給它的參數是 [`unsigned int`](https://www.arduino.cc/reference/en/language/variables/data-types/unsignedint/) 型別的資料, 這種資料是沒有負數的。遇到這種狀況時, C++ 會自動幫你轉換資料, `int` 型別的 -1 會被轉換成 `unsigned int` 的 4294967295, 以此作為熄燈的持續時間, 4294967295us 約是 1.2 小時, 所以會熄掉很久, 像是永遠不會亮了。 ## 多層的 if 如果要避免前述問題, 我們可以再加一個 `if` 判斷這種情況, 讓 LED 重新回到不亮後再慢慢亮起: ```cpp= int duration_on = 0; // 亮燈時間 int count = 0; // 計算測換次數 void setup() { pinMode(5, OUTPUT); } void loop() { if(count == 100) { duration_on = duration_on + 1; if(duration_on > 100) duration_on = 0; count = 0; } digitalWrite(5, LOW); delayMicroseconds(duration_on); digitalWrite(5, HIGH); delayMicroseconds(100 - duration_on); count = count + 1; } ``` 在第 11 行我們額外加了一個 `if` 判斷 `duration_on` 是否已經超過了100?如果是的話就把 `duration_on` 設回一開始的 0。這裡有兩點需要特別留意: 1. 如果 `if` 的區塊內只有**單一敘述**, 那麼可以省略區塊的左右大括號, 就像是第 12 行那樣。 2. `if` 內可以包含 `if`, 進行**多層**的條件判斷, 為了容易區分層次, 每一層區塊內的程式都會再往右邊縮, 像是 9~19 行是函式的區塊, 內縮了一層;10~13 行是第 1 個 `if` 的區塊, 又縮了一層;而第 12 行是第 2 個 `if` 的區塊, 又再縮了一層, 這樣可以清楚辨識每一個區塊的層次, 提升程式的易讀性。 :::info 在 Arduio IDE 中, 當你在左大括號後按下 <kbd>enter</kbd> 時, 它會自動幫你往右縮排, 是很貼心的設計。 ::: 重新上傳後, 就可以看到 LED 漸亮, 到最亮後又會熄掉漸亮不斷重複。 ## 使用取餘數運算減少程式行數 在剛剛的範例中, 我們希望 `duration_on` 的值會在 0~100 之間循環遞增, 所以使用了這樣的結構: ```CPP=10 duration_on = duration_on + 1; if(duration_on > 100) duration_on = 0; ``` 上述寫法佔了 3 行程式, 不過在 C++ 中提供有 [`%` 取餘數](https://www.arduino.cc/reference/en/language/structure/arithmetic-operators/remainder/)的運算, 會依照整數除法運算, 但是以餘數作為計算的結果。利用取餘數的運算, 就可以將這 3 行程式簡化成 1 行, 像是這樣: ```cpp duration_on = (duration_on + 1) % 101; ``` 一開始看可能覺得這行程式很奇怪, 每次遞增數值後再除以 101 取餘數, 我們可以一步步計算看看。當 `duration_on` 的值從 0 遞增到 100 的過程中, 因為 `duration_on` 的值都小於 101, 因此除以 101 的餘數就是自己不會變, 但是當 `duration_on` 的值變為 101 時, 除以 101 取餘數就會因為可以整除而得到 0, 如下所示: | duration_on | duration_on+1 | (durarion_on+1) % 101 | |:-----------:|:-------------:|:---------------------:| | 0 | 1 | 1 | | 1 | 2 | 2 | | 2 | 3 | 3 | | ... | ... | ... | | 99 | 100 | 100 | | 100 | 101 | 0 | 如此一來, `duration_on` 的值就會在 0~101 之間循環變化了。依此修改的完整程式如下: ```cpp= int duration_on = 0; // 亮燈時間 int count = 0; // 計算測換次數 void setup() { pinMode(5, OUTPUT); } void loop() { if(count == 100) { duration_on = (duration_on + 1) % 101; count = 0; } digitalWrite(5, LOW); delayMicroseconds(duration_on); digitalWrite(5, HIGH); delayMicroseconds(100 - duration_on); count = count + 1; } ``` 重新上傳就會發現程式雖然變短了, 但是功能不變。 要特別提醒的是, 在 C++ 程式中, 也和我們習慣的四則運算一樣, 會有優先順序, 如果你漏掉了小括號, 把剛剛的那一行程式寫成這樣: ```cpp duration_on = duration_on + 1 % 101; ``` 就會因為取餘數的優先順序大於加法, 所以會變成: ```cpp duration_on = duration_on + (1 % 101); ``` 由於 `1 % 101` 是 1, 所以 `duration_on` 就會不斷遞增, 程式就會出錯, 跟之前一樣只會出現一次漸亮的效果後就不會再亮了。為了避免出錯, 也省去我們記憶不同運算之間的優先順序, 建議您養成加上小括號的習慣, 強制安排個別運算之間的順序。 利用取餘數運算讓變數的值在某一區間內循環變化的技巧很常見, 既簡潔又方便, 一開始看到可能覺得不習慣, 多看幾次就會得心應手了。