Hech qachon kutmaydigan dasturlar: millis() va holatlar
Nega delay() robotni koʻr qiladi, millis() taymeri bir necha ishni qanday birga yuritadi va nolga qaytishga nega chidaydi, holatlar mashinasi esa toʻsiqni aylanib oʻtuvchi robotni qanday boshqaradi.
Asosiysi
- delay() butun sketchni toʻxtatadi: kutib turgan robot na oʻlchay oladi, na biror narsaga javob bera oladi.
- millis() plata ishga tushgandan beri oʻtgan millisekundlarni sanaydi; vaqtni unsigned long turidagi oʻzgaruvchilarda saqlang.
- if (now - timer >= interval) andozasi hech qachon kutmaydi va millis() taxminan 49,7 kundan keyin nolga qaytganda ham toʻgʻri ishlayveradi.
- Bayroq — loop() aylanishlari orasida «ha» yoki «yoʻq» maʼlumotini eslab qoladigan bool oʻzgaruvchi.
- Holatlar mashinasida bitta holat oʻzgaruvchisi boʻladi; har bir holat oʻz ishini bajaradi va qachon almashish kerakligini tekshiradi.
Arduino Uno R3Simulyatorda bor× 1Narxni doʻkonda koʻring
Svetodiod moduli 5 mm, qizilSimulyatorda bor× 1Narxni doʻkonda koʻring
delay() dan foydalanish oson, lekin u butun sketchni toʻxtatadi. Kutib turgan robot atrofga qaray olmaydi. Yechim — kutish oʻrniga soatni tekshirib turish. Buni uchta vosita amalga oshiradi: millis() taymerlari, bayroqlar (flag) va holatlar mashinasi (state machine).
delay() aslida nima qiladi
delay(1000) paytida Uno bir soniya davomida sketchingizning boshqa hech bir qatorini bajarmaydi. U datchikni oʻqiy olmaydi, motorga yangi buyruq bera olmaydi, xabar chiqara olmaydi. Faqat vaqtni sanash kabi fondagi ishlar davom etadi.
Oddiy ish uchun bu muammo emas. Birinchi yurish sketchi kvadratning har bir tomoni uchun delay(STRAIGHT_MS) dan, yaʼni 1200 ms kutishdan foydalanadi. Bu robotda datchik yoʻq, boshqa qiladigan ishi ham yoʻq. Endi unga masofa datchigi oʻrnatilganini tasavvur qiling. Har bir tomonda 1,2 soniya davomida robot atrofga qaramas edi. Shu vaqt ichida oldida paydo boʻlgan devorga urilib ketardi.
delay() bilan bir vaqtda ikkita ishni bajarish ham qiyin. delay() yordamida bitta svetodiodni har 500 ms da, ikkinchisini esa har 170 ms da miltillatib koʻring. Har bir delay() ikkala svetodiodni ham ushlab qoladi. Umumiy jadvalni qoʻlda hisoblab chiqishingizga toʻgʻri keladi. Uchinchi ishni qoʻshishingiz bilanoq esa bu jadval buziladi.
Baʼzi funksiyalar oʻzi ham biroz kutadi. Robot sketchlarimizda pulseIn() aks-sadoni 25 ms gacha kutadi. Bunday qisqa kutishlar maqbul. Muammo — robot atrofni kuzatishi kerak boʻlgan paytdagi uzoq kutish.
millis(): siz oʻqiy oladigan soat
millis() plata ishga tushgandan beri oʻtgan millisekundlar sonini qaytaradi. Sketchingiz ishlayotganda bu son fonda oʻz-oʻzidan oshib boradi. U unsigned long turidagi qiymat qaytaradi: 0 dan 4 294 967 295 gacha boʻlgan butun son. Har bir vaqt qiymatini ham unsigned long turidagi oʻzgaruvchida saqlang. int yetmaydi: Unoda u faqat 32 767 gacha boʻlgan sonlarni saqlay oladi. Millisekund hisobida bu 33 soniyadan ham kam.
Guruch pishirishni koʻz oldingizga keltiring. Siz 20 daqiqa davomida qozon yonida tik turmaysiz. Boshlagan vaqtingizni eslab qolasiz. Keyin vaqti-vaqti bilan soatga qarab qoʻyasiz: 20 daqiqa oʻtdimi? Oraliqda boshqa ishlar bilan band boʻlasiz. millis() taymeri ham xuddi shunday ishlaydi.
Taymer andozasi
Mana TwoTimers sketchidagi ikkita taymerdan biri:
unsigned long now = millis();
if (now - moduleTimer >= MODULE_MS) {
moduleTimer = now;
moduleOn = !moduleOn;
digitalWrite(LED_MODULE, moduleOn);
}now - moduleTimer— svetodiod oxirgi marta oʻzgarganidan beri oʻtgan vaqt.>= MODULE_MSsoʻraydi: kamida 500 ms oʻtdimi?- Agar oʻtgan boʻlsa, sketch yangi vaqtni
moduleTimerga saqlaydi, bayroqni teskarisiga oʻzgartiradi va pinga yangi qiymat beradi. - Agar oʻtmagan boʻlsa, sketch bu blokni tashlab ketadi. U kutmaydi. Keyingi ishga oʻtadi va
loop()ning navbatdagi aylanishida yana soʻraydi.
== emas, >= dan foydalaning. Roppa-rosa 500 ms boʻlgan paytda sketch boshqa ish bilan band boʻlishi mumkin, masalan, pulseIn() ichida. >= bilan taymer biroz kechikib boʻlsa ham ishlaydi. == bilan esa u oʻz lahzasini oʻtkazib yuborishi va hech qachon ishlamasligi mumkin.
Nega ayirish nolga qaytishda ham toʻgʻri ishlaydi
4 294 967 295 ms dan, yaʼni taxminan 49,7 kundan keyin millis() yana 0 dan boshlaydi. Buni nolga qaytish (wrap) yoki toʻlib ketish (overflow) deyishadi.
Ishorasiz (unsigned) sonlar soat siferblatidagidek sanaladi. 12 soatlik siferblatda soat 11 dan soat 2 gacha 3 soat oʻtadi, garchi 2 soni 11 dan kichik boʻlsa ham. unsigned long bilan ayirish ham xuddi shunday ishlaydi. U doim oʻtgan vaqtni oldinga qarab sanab beradi.
Haqiqiy sonlar bilan misol:
moduleTimer4 294 967 000 ga teng: bu nolga qaytishdan 296 ms oldin.now204 ga teng: bu nolga qaytishdan 204 ms keyin.- Haqiqatda oʻtgan vaqt: 296 + 204 = 500 ms.
- Uno
unsigned longturida 204 - 4 294 967 000 ayirmasini hisoblaydi va roppa-rosa 500 ni oladi. Taymer oʻz vaqtida ishlaydi.
Boshqacha yozish usuli, yaʼni now >= moduleTimer + MODULE_MS, bu yerda notoʻgʻri ishlaydi. 4 294 967 000 + 500 yigʻindisi nolga qaytib, 204 ga aylanadi. Taymer oʻrnatilgan zahoti now hali ham taxminan 4 294 967 000 boʻladi. Bu 204 dan katta, shuning uchun tekshiruv darhol rost boʻladi va millis() nolga qaytgunicha har bir aylanishda rostligicha qolaveradi.
Robot kamdan-kam hollarda 49 kun uzluksiz ishlaydi. Lekin toʻgʻri andozani yozish qiyinroq emas, baʼzi qurilmalar esa, masalan, issiqxonani boshqaradigan kontroller, oylab ishlaydi.
Ikki svetodiod, ikki taymer
// Two timers: two LEDs blink at different speeds, and loop() never waits.
// Wiring: LED module S -> D2, - -> GND. The Uno's own L LED is on D13.
const byte LED_MODULE = 2;
const byte LED_BOARD = 13;
const unsigned long MODULE_MS = 500; // the module toggles every 500 ms
const unsigned long BOARD_MS = 170; // the L LED every 170 ms
unsigned long moduleTimer = 0;
unsigned long boardTimer = 0;
bool moduleOn = false;
bool boardOn = false;
void setup() {
pinMode(LED_MODULE, OUTPUT);
pinMode(LED_BOARD, OUTPUT);
}
void loop() {
unsigned long now = millis();
// "Has enough time passed?" Subtracting stays right even when millis() wraps round after 49.7 days.
if (now - moduleTimer >= MODULE_MS) {
moduleTimer = now;
moduleOn = !moduleOn;
digitalWrite(LED_MODULE, moduleOn);
}
if (now - boardTimer >= BOARD_MS) {
boardTimer = now;
boardOn = !boardOn;
digitalWrite(LED_BOARD, boardOn);
}
// Anything else can run here, because nothing above ever waits.
}TwoTimers har bir svetodiodga alohida taymer beradi: oxirgi oʻzgarish vaqtini saqlaydigan oʻzgaruvchi va oʻzgarmas oraliq.
| Svetodiod | Pin | Oʻzgarish oraligʻi | Bitta toʻliq miltillash |
|---|---|---|---|
| Svetodiod moduli | D2 | 500 ms (MODULE_MS) | 1 s: soniyasiga bir marta |
| Unodagi L svetodiodi | D13 | 170 ms (BOARD_MS) | 340 ms: soniyasiga deyarli uch marta |
loop() soniyasiga koʻp minglab marta ishlaydi. Aksariyat aylanishlarda ikkala tekshiruv ham «hali emas» deydi va hech narsa sodir boʻlmaydi. Vaqti-vaqti bilan ulardan birining sharti bajariladi: u oʻz svetodiodini oʻzgartiradi va vaqtni saqlaydi. Hech bir taymer boshqasini kutmaydi.
Ulanish Blink loyihasidagi bilan bir xil: svetodiod modulining S pini D2 ga, - pini esa GND ga ulanadi.
Oʻzgaruvchilar qayerda eʼlon qilinganiga eʼtibor bering. moduleTimer, boardTimer, moduleOn va boardOn sketch boshida, loop() dan tashqarida eʼlon qilingan. loop() ichida eʼlon qilingan oʻzgaruvchi har bir aylanishda qaytadan yaratiladi va oʻz qiymatini unutadi.
Bayroqlar: «ha» yoki «yoʻq»ni eslab qolish
moduleOn va boardOn — bayroqlar, yaʼni «ha» yoki «yoʻq» maʼlumotini eslab qoladigan bool oʻzgaruvchilar. moduleOn = !moduleOn bayroqni teskarisiga oʻzgartiradi: true qiymati false ga, false esa true ga aylanadi. Keyin digitalWrite(LED_MODULE, moduleOn) pinni true boʻlsa HIGH, false boʻlsa LOW qiladi.
Bayroqlar oddiy maʼlumotlar uchun qulay: svetodiod yoniqmi, robot ishga tushdimi? Bir nechta rejimi bor robotda esa koʻp bayroqlar chalkashlikka olib keladi. Ulardan ikkitasi bir vaqtda true boʻlib qolishiga hech narsa toʻsqinlik qilmaydi, hatto bu hech qanday maʼnoga ega boʻlmasa ham. Rejimlar uchun holatlar mashinasidan foydalaning.
Simulyatorda nimani koʻrasiz
Simulyatorda Blink loyihasidagi stol robotini yigʻing: Uno va svetodiod moduli, S esa D2 ga ulangan. Yuqoridagi TwoTimers sketchini «Kod» boʻlimiga qoʻying va «Yuklash» tugmasini bosing. Stolda Uno noutbukning USB portidan quvvat oladi. Svetodiod moduli soniyasiga bir marta, L svetodiodi esa soniyasiga deyarli uch marta miltillaydi. 500 soni 170 ga karrali boʻlmagani uchun ikki svetodiod turli lahzalarda oʻzgaradi va ularning bir-biriga nisbatan miltillash tartibi doim siljib boradi. Hech bir svetodiod boshqasini hech qachon kutmaydi.
Holatlar mashinasi: robot nima qilayotgani uchun bitta oʻzgaruvchi
Toʻsiqni aylanib oʻtuvchi robotda ketma-ket keladigan uchta ish bor: oldinga yurish (Drive), orqaga yurish (Back), burilish (Turn). Har bir ish — holat. Robot doim aynan bitta holatda boʻladi.
enum class State : byte { Drive, Back, Turn };
State state = State::Drive;enum class uchta nomli qiymatga ega yangi tur yaratadi. : byte bu turdagi qiymatni RAM ning bitta baytida saqlaydi. state oʻzgaruvchisi joriy holatni saqlaydi. Nega oddiy sonlar emas? int bilan state = 2 yozuvi oʻquvchiga hech narsa demaydi, state = State::Turn esa aniq maʼno beradi. enum class qatʼiy hamdir: kompilyator oddiy sonni State sifatida qabul qilmaydi. Nomlar esa State ichida qoladi, shuning uchun ular sketchdagi boshqa nomlar bilan aralashib ketmaydi.
loop() ichida switch (state) joriy holatning case qismiga oʻtadi. Har bir case bloki ikki ishni bajaradi. U motorlarni sozlaydi va holatni almashtirish vaqti kelgan-kelmaganini tekshiradi. break case blokini tugatadi.
enter(next) funksiyasi holatni almashtiradi. U vaqtni stateStart ga ham saqlaydi, shunda har bir holat oʻz vaqtini oʻzi oʻlchay oladi. Keyingi holat Drive boʻlsa, funksiya lastPing ni 0 ga tenglaydi. Shunda robot eski masofa bilan yurib ketish oʻrniga darhol qaytadan oʻlchaydi.
Oʻlchash holat emas. Uning loop() boshida oʻz taymeri bor: har 60 ms da, istalgan holatda. Shuning uchun robot orqaga yurayotganda va burilayotganda ham oʻlchashda davom etadi.
// Obstacle avoider: drive ahead; when something is close, back off and turn.
// Written without delay() in loop(): a state machine and millis() timers,
// so the robot keeps measuring while it moves.
// L298N: ENA -> D5, IN1 -> D4, IN2 -> D7 (left), ENB -> D6, IN3 -> D8, IN4 -> D12 (right)
// HC-SR04: TRIG -> D9, ECHO -> D10
const byte ENA = 5, IN1 = 4, IN2 = 7;
const byte ENB = 6, IN3 = 8, IN4 = 12;
const byte TRIG = 9, ECHO = 10;
const int CRUISE = 190; // forward speed, 0..255
const int STOP_CM = 25; // closer than this: avoid it
const unsigned long PING_MS = 60; // the HC-SR04 needs a pause between pings
const unsigned long BACK_MS = 300;
const unsigned long TURN_MS = 380;
enum class State : byte { Drive, Back, Turn };
State state = State::Drive;
unsigned long stateStart = 0;
unsigned long lastPing = 0;
int distanceCm = 400;
void setMotor(byte enablePin, byte inA, byte inB, int speed) {
speed = constrain(speed, -255, 255);
digitalWrite(inA, speed > 0);
digitalWrite(inB, speed < 0);
analogWrite(enablePin, abs(speed));
}
void drive(int left, int right) {
setMotor(ENA, IN1, IN2, left);
setMotor(ENB, IN3, IN4, right);
}
// Distance in centimetres; 400 when no echo came back (nothing within about 4 m).
int readDistanceCm() {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH); // a 10 microsecond pulse starts a measurement
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long us = pulseIn(ECHO, HIGH, 25000UL); // wait at most 25 ms
if (us == 0) return 400;
return us / 58; // sound goes there and back: 58 us per cm
}
void enter(State next) {
state = next;
stateStart = millis();
if (next == State::Drive) lastPing = 0; // measure again before driving on
}
void setup() {
const byte pins[] = {ENA, IN1, IN2, ENB, IN3, IN4, TRIG};
for (byte pin : pins) pinMode(pin, OUTPUT);
pinMode(ECHO, INPUT);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - lastPing >= PING_MS) {
lastPing = now;
distanceCm = readDistanceCm();
Serial.println(distanceCm);
}
switch (state) {
case State::Drive:
drive(CRUISE, CRUISE);
if (distanceCm < STOP_CM) enter(State::Back);
break;
case State::Back:
drive(-160, -160);
if (now - stateStart >= BACK_MS) enter(State::Turn);
break;
case State::Turn:
drive(170, -170); // spin right on the spot
if (now - stateStart >= TURN_MS) enter(State::Drive);
break;
}
}Holatlar diagrammasini jadval shaklida chizing
Qogʻozda har bir holatni doira, har bir oʻtishni esa oʻq bilan chizish mumkin. Jadval xuddi shu maʼlumotni saqlaydi va uni klaviaturada yozish osonroq:
| Holat | Robot nima qiladi | Qachon almashadi | Keyingi holat |
|---|---|---|---|
| Drive | drive(CRUISE, CRUISE): 190 tezlikda oldinga |
masofa STOP_CM (25 sm) dan kam boʻlganda | Back |
| Back | drive(-160, -160): 160 tezlikda orqaga |
Back ga kirgandan BACK_MS (300 ms) oʻtgach | Turn |
| Turn | drive(170, -170): joyida oʻngga aylanadi |
Turn ga kirgandan TURN_MS (380 ms) oʻtgach | Drive |
Oʻz robotingizni rejalashtirish uchun avval shunday jadvalni toʻldiring: har bir holat uchun bitta qator. Keyin har bir qator uchun bitta case yozing. «Qachon almashadi» ustuni if ga, «Keyingi holat» ustuni esa enter() chaqiruviga aylanadi.
Yangi xatti-harakat — yangi qator. Masalan, Stop holati biror narsa 10 sm dan yaqin turgan paytda robotni joyida ushlab turishi, yoʻl ochilganda esa yana Drive holatiga qaytishi mumkin. Buning uchun Stop uchun bitta qator va Drive qatoriga bitta yangi shart qoʻshasiz. Qolgan hammasi oʻzgarmaydi.
Toʻsiqni aylanib oʻtuvchi robotni arenada ishga tushiring va Serial Monitor (port monitori) oynasini kuzating. Masofalar taxminan har 60 ms da kelib turadi, robot orqaga yurayotganda va burilayotganda ham. Agar bu holatlarda delay() boʻlganida, sonlar toʻxtab qolardi. Robot toʻliq holda Toʻsiqni aylanib oʻtuvchi robot loyihasida berilgan.
Oʻzingizni tekshiring
1Nega datchikli robot uchun delay() muammo tugʻdiradi?
delay() davomida sketchning boshqa hech bir qatori bajarilmaydi. Kutish tugamaguncha robot na oʻlchay oladi, na biror narsaga javob bera oladi.
2millis() yana 0 ga qaytganda now - timer >= interval nega xavfsiz?
Ayirish unsigned long turida soat siferblatidagidek aylanib hisoblanadi, shuning uchun natija baribir haqiqatda oʻtgan vaqtga teng boʻladi.
3Toʻsiqni aylanib oʻtuvchi robotni Turn holatidan nima chiqaradi?
Vaqt: robot Turn holatiga kirganidan beri 380 ms (TURN_MS) oʻtgach, enter(State::Drive) uni yana Drive holatiga qaytaradi.
Topshiriq
TwoTimers sketchini oʻzgartiring: svetodiod moduli soniyasiga bir marta qisqa chaqnasin, yaʼni 100 ms yonsin, keyin 900 ms oʻchiq tursin. L svetodiodi esa avvalgidek har 170 ms da oʻzgarib tursin. Sketchni stolda yuklang va ikkala svetodiod hali ham bir vaqtda ishlashini tekshiring.
Maslahat
Qancha kutish kerakligi svetodiod hozir yoniq yoki oʻchiqligiga bogʻliq boʻlishi mumkin.