⏱️ Timer در میکروپایتون: چند کار همزمان روی رزبری پای پیکو بدون sleep
تا حالا هر برنامهای که نوشتهایم با time.sleep صبر میکرد. این برای یک LED چشمکزن عالی است، ولی کافی است بخواهی دو چراغ با دو سرعت مختلف چشمک بزنند و همزمان یک سنسور هم خوانده شود تا ببینی sleep همهچیز را قفل میکند. در این آموزش یاد میگیری با Timer در میکروپایتون و الگوی ticks_ms روی رزبری پای پیکو چند کار را همزمان پیش ببری. این همان مفهومی است که در آردوینو به آن «millis به جای delay» میگویند.
مشکل sleep کجاست؟
این کد را ببین. میخواهیم LED اول هر ۲۰۰ میلیثانیه و LED دوم هر ۷۰۰ میلیثانیه عوض شود:
while True:
led_fast.toggle()
time.sleep_ms(200)
led_slow.toggle()
time.sleep_ms(700)
نتیجه؟ هر دو LED با هم یک چرخهی ۹۰۰ میلیثانیهای دارند و هیچکدام سرعت درستش را ندارد. در طول هر sleep پردازنده واقعاً «خواب» است: دکمه خوانده نمیشود، نمایشگر بهروز نمیشود و هیچ کار دیگری انجام نمیشود. راه حل این است که به جای «صبر کن»، بپرسیم «آیا وقتش رسیده؟».
چه چیزی میسازیم؟
دو LED بیرونی که با دو سرعت کاملاً مستقل چشمک میزنند، یک شمارندهی ثانیه که در سریال مانیتور چاپ میشود، و LED داخلی برد که سه ثانیه بعد از روشن شدن خودش خاموش میشود؛ همه همزمان و بدون اینکه حلقهی اصلی جایی معطل بماند.
قطعهها و اتصالها
- یک رزبری پای پیکو
- دو LED با دو مقاومت ۲۲۰ اهم
| قطعه | پایهی پیکو |
|---|---|
| LED سریع (از راه مقاومت ۲۲۰ اهم) | GP15، پایهی فیزیکی ۲۰ |
| LED کند (از راه مقاومت ۲۲۰ اهم) | GP14، پایهی فیزیکی ۱۹ |
| کاتد هر دو LED | GND |
| LED داخلی | GP25 روی پیکوی معمولی (روی Pico W بنویس Pin("LED")) |
روش اول: machine.Timer
تایمر یک ساعت زنگدار سختافزاری است: به آن میگویی «هر ۲۰۰ میلیثانیه این تابع را صدا بزن» و خودش، مستقل از حلقهی اصلی، این کار را انجام میدهد.
from machine import Pin, Timer
import time
led_fast = Pin(15, Pin.OUT)
led_slow = Pin(14, Pin.OUT)
onboard = Pin(25, Pin.OUT)
seconds = 0
def blink_fast(t):
led_fast.toggle()
def blink_slow(t):
led_slow.toggle()
def count_second(t):
global seconds
seconds += 1
t1 = Timer(period=200, mode=Timer.PERIODIC, callback=blink_fast)
t2 = Timer(period=700, mode=Timer.PERIODIC, callback=blink_slow)
t3 = Timer(freq=1, mode=Timer.PERIODIC, callback=count_second)
# One-shot timer: switch the on-board LED off 3 s after start
onboard.on()
t4 = Timer(period=3000, mode=Timer.ONE_SHOT, callback=lambda t: onboard.off())
shown = -1
while True:
if seconds != shown: # heavy work (printing) stays in the main loop
shown = seconds
print("uptime:", shown, "s")
time.sleep_ms(10)
توضیح خطهای مهم
from machine import Pin, Timer: کلاسTimerهم مثلPinدر ماژولmachineاست.def blink_fast(t): تابعی که تایمر صدا میزند همیشه یک ورودی میگیرد: خود شیء تایمر. حتی اگر استفادهاش نکنی، باید آن را بنویسی، وگرنهTypeErrorمیگیری.Timer(period=200, mode=Timer.PERIODIC, callback=blink_fast): هر ۲۰۰ میلیثانیهblink_fastرا اجرا میکند، تا ابد.periodبر حسب میلیثانیه است.Timer(freq=1, ...): به جای دوره میتوانی فرکانس بدهی؛freq=1یعنی یک بار در ثانیه،freq=10یعنی ده بار.global seconds: چون تابع یک متغیر بیرونی را تغییر میدهد، باید این را بگوییم؛ وگرنه پایتون خیال میکندsecondsمتغیر محلی است.Timer.ONE_SHOT: فقط یک بار، بعد از ۳۰۰۰ میلیثانیه، اجرا میشود. برای کارهای «بعداً انجام بده» مثل خاموش کردن خودکار چراغ پله عالی است.lambdaیک تابع یکخطی بیاسم است.- حلقهی اصلی: به جای اینکه تابع تایمر خودش چاپ کند، فقط عدد را زیاد میکند و حلقهی اصلی هر وقت عدد عوض شد، چاپ میکند. این الگوی «پرچم» قلب برنامهنویسی با تایمر و وقفه است.
t1.deinit(). اگر برنامه را در Thonny با Stop قطع کنی، تایمرهای PERIODIC ممکن است هنوز روی برد واقعی کار کنند و LED همچنان چشمک بزند؛ با یک ریاستارت نرم (Ctrl+D) یا deinit خاموششان کن.روش دوم: الگوی ticks_ms در حلقهی اصلی
اگر کارهایت زیاد یا کمی سنگیناند (مثل خواندن سنسور DHT یا نوشتن روی LCD)، روش سادهتر و امنتر این است که در خود حلقهی اصلی، زمان را بپرسی. این دقیقاً همان الگوی millis() در آردوینو است:
from machine import Pin
import time
led_fast = Pin(15, Pin.OUT)
led_slow = Pin(14, Pin.OUT)
FAST_MS = 200
SLOW_MS = 700
last_fast = time.ticks_ms()
last_slow = time.ticks_ms()
while True:
now = time.ticks_ms()
if time.ticks_diff(now, last_fast) >= FAST_MS:
last_fast = time.ticks_add(last_fast, FAST_MS)
led_fast.toggle()
if time.ticks_diff(now, last_slow) >= SLOW_MS:
last_slow = time.ticks_add(last_slow, SLOW_MS)
led_slow.toggle()
time.sleep_ms(1)
time.ticks_ms(): تعداد میلیثانیههایی که از روشن شدن برد گذشته.time.ticks_diff(now, last_fast): فاصلهی دو زمان. حتماً از این تابع استفاده کن، نه تفریق معمولی؛ شمارندهی ticks بعد از مدتی دور میزند و از صفر شروع میکند و فقطticks_diffاین را درست حساب میکند.time.ticks_add(last_fast, FAST_MS): زمان نوبت بعدی را دقیق جلو میبرد، تا تأخیرهای کوچک روی هم جمع نشوند.time.sleep_ms(1): یک استراحت یکمیلیثانیهای که به هیچ کاری لطمه نمیزند ولی پردازنده را بیدلیل داغ نمیکند.
کدام روش را انتخاب کنم؟
| machine.Timer | ticks_ms در حلقه | |
|---|---|---|
| دقت زمانبندی | خیلی خوب، مستقل از حلقهی اصلی | خوب، به شرطی که حلقه جایی گیر نکند |
| کار سنگین مجاز است؟ | نه، تابع باید خیلی کوتاه باشد | بله |
| سادگی و اشکالزدایی | متوسط | ساده |
| مناسب برای | چشمک، شمارش، نمونهبرداری منظم | بیشتر پروژهها: سنسور، نمایشگر، منو |
در عمل خیلی وقتها هر دو را با هم به کار میبری: تایمر یک پرچم را روشن میکند و حلقهی اصلی کار اصلی را انجام میدهد. روی برد واقعی برای پروژههای بزرگتر ماژول asyncio هم هست که چند «وظیفه» را با await کنار هم اجرا میکند؛ ولی برای شروع، همین دو روش کافیاند.
اجرا در شبیهساز
در تینکرکد فارسی مثال چشمکزن با MicroPython روی پیکو را باز کن، یک LED دیگر با مقاومت روی GP14 اضافه کن و هر کدام از دو کد بالا را جای کد مثال بگذار. شبیهساز میکروپایتون واقعی را اجرا میکند، پس Timer، ticks_ms و ticks_diff همان رفتار برد را دارند و چاپها در سریال مانیتور میآیند. پیکو و میکروپایتون جزو اشتراک پلاس هستند: مثال برای خواندن رایگان است و برای اجرا پلاس لازم داری. اگر میخواهی همین مفهوم را رایگان روی آردوینو تمرین کنی، درس چند کار همزمان و پروژهی چند کار همزمان با آردوینو را ببین.
اشکالهای رایج
- TypeError: function takes 0 positional arguments but 1 were given: تابع تایمر را بدون ورودی نوشتهای.
def blink_fast():را بهdef blink_fast(t):تغییر بده. - تابع تایمر فقط یک بار اجرا میشود:
mode=Timer.PERIODICرا جا انداختهای یا ONE_SHOT نوشتهای. - عدد شمارنده بالا نمیرود: خط
global secondsدر تابع نیست. - برنامه کند یا نامنظم میشود: در تابع تایمر
sleep، حلقه، چاپ زیاد یا کار با LCD گذاشتهای. آن را به حلقهی اصلی ببر. - LED بعد از ساعتها کار، دیگر چشمک نمیزند: در روش دوم به جای
ticks_diffتفریق معمولی نوشتهای و شمارنده دور زده است.
تایمر خواهر دوقلوی وقفه است؛ اگر هنوز وقفهی دکمه را ندیدهای، دکمه و وقفه با میکروپایتون را بخوان. برای مرور پایههای زبان هم آموزش میکروپایتون از صفر و صفحهی میکروپایتون را ببین.
تایمر پیکو را در شبیهساز امتحان کن ⏱️سؤالهای رایج
فرق Timer.PERIODIC با Timer.ONE_SHOT چیست؟
تایمر PERIODIC تا وقتی متوقفش نکنی، هر بار بعد از گذشت period تابع را دوباره صدا میزند. ONE_SHOT فقط یک بار، بعد از همان مدت، تابع را اجرا میکند و خودش خاموش میشود.
چند Timer میتوانم روی پیکو بسازم؟
روی پورت rp2 تایمرها مجازیاند و میتوانی چندتا با Timer() بسازی. ولی هر تایمر یک وقفه است؛ اگر کارهای زیادی داری، معمولاً یک تایمر یا الگوی ticks_ms در حلقهی اصلی سادهتر و قابلاعتمادتر است.
چرا نباید در تابع Timer از sleep یا print زیاد استفاده کنم؟
تابع تایمر شبیه تابع وقفه است و وسط کار برنامهی اصلی اجرا میشود. اگر طول بکشد، بقیهی کارها عقب میافتند. در آن فقط یک متغیر یا پرچم را عوض کن و کار سنگین را در حلقهی اصلی انجام بده.
مقالههای مرتبط
نصب میکروپایتون روی رزبری پای پیکو و راهاندازی Thonny، قدمبهقدم
برنامهنویسی رزبری پای پیکو با زبان آردوینو (C++ و هستهی arduino-pico)
میکروپایتون یا C++ آردوینو؟ مقایسهی کامل برای انتخاب زبان میکروکنترلر
کیپد ۴×۴ با آردوینو: طرز کار ماتریسی، کتابخانهی Keypad و قفل رمزدار با سروو
همهی آموزشهای رزبری پای پیکو و میکروپایتون ← · همهی مقالهها