Semorphe

不要用 delay

讓板子同時做兩件事。 · ⏱ 約 35 分鐘

你會學到三件事

  1. delay 的真正代價:那段時間什麼都做不了
  2. millis() 是「開機到現在幾毫秒」
  3. 看時間,而不是等時間」——這是嵌入式最重要的一個轉念

開始之前

想同時做兩件事:一顆燈每 1 秒閃、另一顆每 0.3 秒閃。

delay 寫寫看——你會發現寫不出來

那不是你不夠聰明,是 delay 這個工具做不到。

一、delay 的代價

delay(1000);

這一行的意思不是「一秒後做下一件事」, 而是「接下來一秒,這塊板子什麼都不做」。

按鈕按了?沒讀到。感測器變了?沒看到。序列埠來了資料?漏掉了。

delay 不是「等一下」,是「停機一下」。

二、millis()

unsigned long t = millis();

millis() 回傳「從開機到現在過了幾毫秒」。它一直在跑,不需要你等。

⚠️ 型態一定要 unsigned long。用 int 的話 32 秒就溢位了。

三、看時間,而不是等時間

unsigned long last = 0;

void loop() {
    if (millis() - last >= 1000) {
        last = millis();
        // ← 這裡放「每秒要做一次」的事
    }
}

讀法:「現在的時間減掉上次做的時間,如果超過 1000 毫秒,就做一次。」

loop 還是每秒跑幾萬次——而那一段每秒只進去一次

其餘幾萬次它是空跑的,所以它有空做別的事

用 delay        ███████等待███████  做一下  ███████等待███████
用 millis       ·······················做一下·····················
                 ↑ 這些點都是空的,可以塞別的事

四、狀態

要讓燈交替,得記住「現在是亮還是暗」:

int state = LOW;
...
if (state == LOW) state = HIGH;
else state = LOW;
digitalWrite(LED_BUILTIN, state);

⚠️ statelast 都要宣告在 loop 外面。

宣告在裡面的話,每次 loop 重來它們都會重置—— 而 loop 一秒重來幾萬次,等於它們永遠不會改變。

這是「狀態」這個概念第一次真的有必要: 有些東西必須活得比一次 loop 更久。

五、⚠️ 為什麼是 millis() - last 而不是 millis() > last + 1000

millis() 大約 49 天會轉回 0(溢位)。

寫成 millis() - last 的話,即使剛好轉回去,減法的結果還是對的 (因為 unsigned 的減法會繞回來)。寫成 > 那種就會卡死。

這種寫法叫「慣用法」——它看起來只是換個寫法,而它是對的那一個。

完成的樣子

unsigned long last = 0;
int state = LOW;

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    if (millis() - last >= 1000) {
        last = millis();
        if (state == LOW) state = HIGH;
        else state = LOW;
        digitalWrite(LED_BUILTIN, state);
    }
}

換你了

第二顆燈,讓它每 300 毫秒閃一次——用另一組 last2 / state2

兩顆燈會各閃各的,互不干擾。delay 做不到這件事。

再回頭做第 4 課那個「按一下切換」——現在你有工具了。

這一課你做了什麼

如果卡住了

你看到 多半是因為
燈完全不閃 last / state 宣告在 loop 裡面了
大約 32 秒後行為亂掉 millis() 存進 int 了。要 unsigned long
兩顆燈互相干擾 共用了同一個 last
還是很卡 某個地方還留著 delay
在編輯器打開這一課 →