Learning Objectives
- 說明 time.sleep 為什麼會讓程式錯過按鈕或感測器的變化
- 用 ticks_ms 與 ticks_diff 判斷「時間到了沒」,寫出不卡住的閃燈
- 說明為什麼比較時間要用 ticks_diff,不能直接相減
- 用 machine.Timer 定期執行函式,並知道回呼函式要寫得短
睡著的程式什麼都做不了
到目前為止,控制時間的方法只有一個:time.sleep()。它很好懂,但程式在 sleep 的時候完全停在那一行,
不會去看按鈕、不會讀感測器,這叫阻塞(blocking)。
只做一件事的時候沒問題。可是真的作品通常要同時處理好幾件事:
- 指示燈每 0.5 秒閃一次
- 隨時注意按鈕有沒有被按
- 每 2 秒讀一次溫度,每 10 秒上傳一次
如果每件事都用 sleep 等,等待的時間會加在一起,程式反應變慢,短暫的輸入也會被漏掉。下一個範例就是這種情況。
逐步看:sleep 讓程式錯過按鈕
按鈕在 1.2 秒按下、1.5 秒放開(看上方的按鈕)。程式每 1 秒才醒來一次看按鈕,
你可以拖到第 1.2 秒附近:那時程式停在 time.sleep(1) 裡。
不要等,改成「看時間到了沒」
換一個想法:迴圈跑得很快,每一圈都看一下時鐘,時間到了才做事,沒到就跳過,繼續處理別的事。
last = time.ticks_ms() # 記下上一次做事的時間
while True:
now = time.ticks_ms()
if time.ticks_diff(now, last) >= 500: # 距離上次滿 500 ms 了嗎?
做事()
last = now
處理其他事() # 每一圈都會執行到
| 函式 | 作用 |
|---|---|
time.ticks_ms() |
從某個起點開始經過的毫秒數(MicroPython 專有;另有 ticks_us() 以微秒計) |
time.ticks_diff(a, b) |
a 比 b 晚多少,結果可以是負數 |
time.ticks_add(t, 毫秒) |
t 之後(或之前)幾毫秒的時間點,用來算期限 |
為什麼不直接相減
ticks_ms() 的數字不會一直變大:到了上限會回繞(wrap around)重新從 0 開始,就像時鐘走過 12 點又回到 1 點。
上限多少依開發板而定,MicroPython 只保證是 2 的次方。剛好跨過上限時,now - last 會算出一個很大的負數,
程式就以為時間倒流了。ticks_diff() 會正確處理回繞,所以官方文件要求:比較 ticks 一律用 ticks_diff,
不要用 -、<、> 直接比。
設定期限的寫法也一樣:
deadline = time.ticks_add(time.ticks_ms(), 200) # 200 ms 之後
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
做一點事()
time.sleep_ms(10)這種很短的等待仍然可以用:它讓迴圈不會空轉,耗電較少,反應也只慢 10 ms。
逐步看:一邊閃燈、一邊看按鈕
按鈕一樣在 1.2 秒左右按下。這次迴圈每 0.1 秒跑一圈,每一圈都看按鈕;LED 則是看時間到了才切換。
跳到第 30 步附近,看 now 與 last_blink 的差什麼時候達到 500。
讓硬體幫你計時:machine.Timer
另一種做法是交給硬體計時器:設定好間隔,時間到了硬體就呼叫你的函式,主程式完全不用管,和第 04 課的中斷很像。
from machine import Timer
tim = Timer(0)
tim.init(period=500, mode=Timer.PERIODIC, callback=tick) # 每 500 ms 呼叫 tick
tim.init(period=2000, mode=Timer.ONE_SHOT, callback=done) # 2 秒後呼叫一次 done
tim.deinit() # 停止
Timer.PERIODIC:每隔period毫秒重複呼叫;Timer.ONE_SHOT:只呼叫一次。- 原版 ESP32 有 4 個硬體計時器,編號 0~3。有些開發板(例如 Raspberry Pi Pico)可以寫
Timer(-1)使用軟體計時器,ESP32 的 MicroPython 目前不支援,請用 0~3。 - 回呼函式的規則和中斷處理函式一樣:要短,改計數或旗標就好,複雜的事交給主程式。
兩種做法怎麼選?要處理的事情少、時間要求不嚴格時,ticks_ms 寫在主迴圈裡最好懂,也比較好除錯;
需要固定節奏、不想讓主迴圈的快慢影響時,用 Timer。
逐步看:計時器在背景閃燈
主程式只是每秒印一行,從來沒有切換 LED;每 500 ms,程式會跳進 tick 函式執行一次。比較 ticks 增加的速度與主程式印出的次數。
小測驗:阻塞與 ticks
迴圈裡有 time.sleep(2),按鈕只在每圈開頭讀一次。使用者按了 0.3 秒就放開,最可能發生什麼事?
下面模擬「時間到了才做事」的迴圈,times 是每一圈讀到的 ticks_ms()。會印出什麼?
times = [0, 120, 260, 390, 510, 640, 770, 900, 1030]
last = 0
for now in times:
if now - last >= 500:
print(now, end=" ")
last = now
為什麼 MicroPython 官方文件要求用 ticks_diff() 比較兩個 ticks 值?
小測驗:Timer
tim.init(period=250, mode=Timer.PERIODIC, callback=tick) 之後主程式 time.sleep(2),這段時間 tick 大約被呼叫幾次?
在 ESP32 的 MicroPython 上,下面哪一行不能用?
重點整理
time.sleep()會阻塞:睡著的時候程式看不到按鈕、讀不到感測器。- 非阻塞的寫法:迴圈每一圈讀
ticks_ms(),用ticks_diff(now, last) >= 間隔判斷時間到了沒,到了才做事並更新last。 - ticks 的值到上限會回繞,比較一律用
ticks_diff(),算期限用ticks_add()。 machine.Timer:init(period=…, mode=Timer.PERIODIC 或 ONE_SHOT, callback=…)讓硬體定時呼叫函式,deinit()停止;ESP32 用 0~3 號計時器,不支援Timer(-1)。- 中斷與計時器的回呼函式都要短,重的工作交給主程式。
下一課控制 NeoPixel 全彩燈條,動畫效果正好用得上這一課的計時方法。