חיישנים ומפעילים למיקרו-בקרים · פרק 14 — פרויקט מסכם: מערכת בקרה שלמהסמלי מקצוע 33.102 ו-33.002⌂📚🏠
פרק 14

פרויקט מסכם: מערכת בקרה שלמה

כיתות י״א–י״ב · 5 שעות לימוד

בפרקים הקודמים למדנו כל חלק בנפרד: חיישן ספרתי, מפסק גבול, גשר H, צג וזמזם. בפרק הזה נחבר אותם למערכת אמיתית אחת: מחסום חניון שסופר את המכוניות הנכנסות והיוצאות, מציג את מספר המקומות הפנויים, מתריע כשהחניון מלא, ומרים ומוריד את הזרוע בעזרת מנוע דו-כיווני עם שני מפסקי גבול. השומר יכול להפעיל אותו ידנית בכל רגע. נעבור יחד את כל שלבי העבודה ההנדסית: מן הדרישות במילים, דרך דיאגרמת המלבנים, טבלת ההדקים, תרשים המצבים ותרשים החיווט, ועד התוכנית המלאה ולבסוף תוכנית הבדיקות. אלה בדיוק השלבים שנעבור בפרויקט הגמר.

בסוף הפרק הזה נדע:
  • נכתוב את דרישות המערכת במשפטים שאפשר לבדוק, ונקשר כל דרישה לבדיקה.
  • נשרטט דיאגרמת מלבנים וטבלת מבואות ומוצאות, ונבחר הדקים ורכיבים בעזרת חישוב.
  • נתכנן מכונת מצבים למחסום עם תנאי בטיחות: היפוך כיוון כשיש מכשול, ופסק זמן שמוביל למצב תקלה.
  • נכתוב תוכנית שאינה עוצרת (millis()) עם מצבי enum, סינון קפיצות ומונה.
  • נבדוק את המערכת לפי תוכנית בדיקות ונשווה את הצג הטורי להתנהגות הצפויה.
  • נכיר דרכים להרחיב את הפרויקט (ESP32, בקר שני, PLC) ואת אופן הערכתו.

הגדרת דרישות המערכת (Requirements)

כל פרויקט מתחיל בשאלה: מה המערכת צריכה לעשות? לפני החיווט והתוכנית נכתוב את מה שבעל החניון רוצה, במשפטים קצרים שאפשר לבדוק: אחרי הבדיקה נוכל לומר "התקיים" או "לא התקיים".

דרישות המערכת: רשימה ממוספרת של מה שהמערכת צריכה לעשות ושל תנאי הבטיחות שהיא שומרת עליהם, כתובה בשפת המשתמש לפני כל החלטה טכנית. לכל דרישה יש סימון (R1, R2, …) שאליו נחזור בתוכנית הבדיקות.
סימוןהדרישה
R1מכונית לפני הזרוע והחניון אינו מלא: הזרוע עולה אוטומטית.
R2כל מכונית שעוברת מתחת לזרוע נספרת פעם אחת; הולך רגל אינו נספר.
R3כל מכונית שיוצאת בנתיב היציאה מופחתת מן הספירה.
R4הצג מציג את מספר המקומות הפנויים ואת מצב המחסום.
R5החניון מלא: האדומה דולקת, הירוקה כבויה, הזרוע אינה עולה אוטומטית, וצפצוף קצר למכונית חדשה.
R6הזרוע נעצרת במפסקי הגבול, ויורדת מעצמה 5 s אחרי שהמכונית עברה.
R7בטיחות: עצם מתחת לזרוע בזמן הירידה הופך מיד את התנועה לעלייה.
R8בטיחות: הזרוע לא הגיעה למפסק הגבול בתוך 6 s: המנוע נעצר, ומצב תקלה עם התרעה.
R9השומר מרים ומוריד את הזרוע בשני לחצנים בכל זמן, במקביל להפעלה האוטומטית, ובעזרתם מוציא את המערכת מתקלה.
נתיב כניסהנתיב יציאהבתוך החניוןהרחובENTRY · D2המנוע ומפסקי הגבולזרוע המחסוםBEAM · D4קרן הבטיחות מאחורי הזרועEXIT · D3
מבט מלמעלה על כניסת החניון: חיישן הכניסה לפני הזרוע, קרן הבטיחות אחריה, וחיישן היציאה בנתיב השני

נבנה דגם מוקטן ובו 3 מקומות בלבד, כדי למלא אותו מהר בבדיקה; בחניון אמיתי נשנה קבוע אחד בתוכנית. המכונית חוצה קודם את קרן הכניסה, אחר כך עוברת מתחת לזרוע וחוצה את קרן הבטיחות, ורק כשהיא יוצאת ממנה נספור אותה.

דיאגרמת מלבנים ובחירת חיישנים (Block Diagram)

מן הדרישות נגזור מה המערכת צריכה "לחוש" ומה היא צריכה "לעשות", ונשרטט זאת בדיאגרמת מלבנים: החיישנים משמאל, הבקר באמצע, והמפעילים והחיווי מימין.

חיישן כניסה ENTRYמחסום אורחיישן יציאה EXITמחסום אורקרן בטיחות BEAMמחסום אור מתחת לזרועמפסקי גבולLS_OPEN · LS_CLOSED (NC)לחצני הפעלה ידניתOPEN · CLOSEהבקרArduino Unoמכונת מצבים + מונהמעגל הנעה: גשר HL298Nמנוע הזרוע12 V DCצג LCD 16×2I2C: מקומות פנויים ומצבנוריות חיוויירוקה · אדומהזמזםמלא · תקלההזנה: 12 V למנוע, 5 V ללוגיקה, אדמה משותפת
דיאגרמת המלבנים של מחסום החניון: שבעה מבואות ספרתיים, ומוצאות למנוע ולחיווי
מחסום אור, חיישן שולח-מקלט: חיישן קרבה אופטי מסוג "שולח ומקלט" הניצבים זה מול זה: השולח מקרין קרן אינפרא-אדום, והמקלט משנה את המוצא שלו כשהקרן נקטעת. נבחר יחידה שעובדת ב-5 V ושהמוצא שלה NPN עם קולט פתוח (שוקעת), והוא נעשה LOW כשהקרן נקטעת.

אפשר היה להשתמש גם בחיישן על-קולי (מודד מרחק, אבל דורש תוכנית מדידה) או בלולאה השראתית ברצפה (מגלה רק מתכת, אבל דורשת חפירה ומעגל מיוחד). נבחר במחסום אור כי הוא נותן מוצא ספרתי פשוט. נקודת התורפה שלו היא שגם אדם קוטע אותו, ואותה נפתור בלוגיקה: נספור רק עצם שעבר קודם בחיישן הכניסה (R2).

אזהרה: חיישנים תעשייתיים עובדים לרוב ב-24 V: נחבר אותם לבקר דרך מצמד אופטי (פרק 8), ונוודא מהו סוג המוצא (NPN או PNP).

טבלת מבואות ומוצאות ובחירת רכיבים (I/O Table)

לפני כל חיווט נכתוב טבלת I/O (טבלת מבואות ומוצאות). הטבלה הזאת היא החוזה בין תרשים החיווט לבין התוכנית: כל הדק מופיע בשניהם באותו מספר. ההדקים D0 ו-D1 נשמרים לצג הטורי, ו-A4 ו-A5 לפס I2C.

ההדק (מבואות)השם בתוכניתמחובר אלפעיל כאשר
D2ENTRY_PINמקלט מחסום האור בכניסהLOW = מכונית לפני הזרוע
D3EXIT_PINמקלט מחסום האור ביציאהLOW = מכונית יוצאת
D4BEAM_PINמקלט קרן הבטיחותLOW = עצם מתחת לזרוע
D5LS_OPEN_PINמפסק גבול עליון, NCHIGH = הזרוע למעלה
D6LS_CLOSED_PINמפסק גבול תחתון, NCHIGH = הזרוע למטה
D7BTN_OPEN_PINלחצן OPEN ידניLOW = לחוץ
D8BTN_CLOSE_PINלחצן CLOSE ידניLOW = לחוץ
ההדק (מוצאות)השם בתוכניתמחובר אלפעיל כאשר
D9IN1_PINמבוא IN1 של L298NHIGH = הרמת הזרוע
D10IN2_PINמבוא IN2 של L298NHIGH = הורדת הזרוע
D11GREEN_PINנורית ירוקה + 330 ΩHIGH = יש מקום
D12RED_PINנורית אדומה + 330 ΩHIGH = מלא, או תקלה (מהבהבת)
A0BUZZER_PINזמזם דרך BC547HIGH = התרעה
A4 A5lcdצג LCD 16×2, כתובת 0x27SDA · SCL

כל המבואות מוגדרים עם INPUT_PULLUP, כלומר עם נגד משיכה למעלה פנימי: הלחצן או החיישן מחברים את ההדק לאדמה, ולכן הרמה הפעילה היא LOW. את מפסקי הגבול נחבר דרך המגע הסגור במנוחה (NC): כשהזרוע לוחצת עליהם המגע נפתח וההדק נעשה HIGH.

תכנון בטוח-כשל (Fail-safe): בחירת החיווט כך שהתקלה הסבירה ביותר (חוט קרוע) מובילה למצב הבטוח. חוט קרוע של מפסק NC נקרא כ"הזרוע הגיעה", ולכן המנוע נעצר במקום להמשיך להסתובב עד שישבור משהו. ואם שני המפסקים נקראים יחד "הגיעה", דבר שאינו אפשרי, התוכנית נכנסת למצב תקלה.
דוגמה 14.1
נחשב את הזרם במוצאי נוריות החיווי: כל נורית עם 330 Ω ומתח קדמי 2 V (ערך אופייני לפי גיליון הנתונים). האם הזרם מותר להדק של Uno?
פתרון:
ILED = VCC − VFR = 5 − 2330 = 9.1 mA

הזרם קטן מן 20 mA המומלצים להדק של Uno. את הזמזם נפעיל דרך טרנזיסטור BC547 עם נגד בסיס 1 kΩ (פרק 10), ולכן ההדק מספק לו רק זרם קטן; זרם הקולט של BC547 מותר עד 100 mA (ערך אופייני לפי גיליון הנתונים), וזה מספיק לזמזם קטן.

ILED ≈ 9.1 mA
שאלה 14.1: נגד המשיכה למעלה הפנימי ב-ATmega328P הוא בין 20 kΩ ל-50 kΩ (ערך אופייני לפי גיליון הנתונים). מהו הזרם בלחצן לחוץ בשני המקרים?

מנוע דו-כיווני: גשר H:

מנוע הזרוע הוא מנוע זרם ישר של 12 V. נהפוך את כיוון סיבובו בהיפוך קוטביות המתח עליו בעזרת גשר H ברכיב L298 שעל יחידת L298N. הבקר נותן שני אותות לוגיים, והגשר מעביר את זרם המנוע ממקור 12 V נפרד. לפי גיליון הנתונים L298 עומד בזרם של עד 2 A לכל גשר, ושני מבואות שווים עוצרים את המנוע במהירות (בלימה).

IN1 (D9)IN2 (D10)המנועהמצב בתוכנית
HIGHLOWמרים את הזרועOPENING
LOWHIGHמוריד את הזרועCLOSING
LOWLOWעצור (בלימה)CLOSED · OPEN · FAULT

במקום גשר H אפשר להשתמש בשני ממסרים עם מגע מחליף: המנוע בין שתי נקודות COM, שני מגעי NO אל +12 V ושני מגעי NC אל האדמה. לעולם לא נפעיל את שניהם יחד: זו נעילה הדדית. כשמשתמשים בשני מגענים למנוע תלת-מופעי, הפעלתם יחד היא קצר בין שני מופעים, ולכן שם הנעילה חובה במעגל עצמו בעזרת מגע NC, ולא רק בתוכנית.

אזהרה: L298 זקוק לדיודות חופש מהירות על המוצאות שלו (לפי גיליון הנתונים); רוב יחידות L298N כוללות אותן, אבל נוודא. מקור המנוע נפרד ועם נתיך, והאדמה שלו מחוברת לאדמה של Uno: בלי אדמה משותפת הגשר אינו מבין את אותות הבקר.

תרשים מצבים של המחסום (State Diagram)

מכונת מצבים: תיאור של מערכת שנמצאת בכל רגע במצב אחד מתוך רשימה סגורה. לכל מצב יש מוצאות קבועים (למשל: המנוע מרים), ועוברים למצב אחר רק כשמתרחש אירוע: חיישן השתנה, לחצן נלחץ, או שפסק זמן הסתיים. נשרטט אותה בתרשים מצבים: כל מצב הוא מלבן, וכל מעבר הוא חץ שעליו כתוב התנאי שלו. המחסום אינו רק "פתוח או סגור": הוא גם בתנועה, ועשוי להיות בתקלה.
מכונית בכניסה ויש מקוםאו לחצן OPENLS_OPEN5 s בלי מכונית מתחת לזרועאו לחצן CLOSELS_CLOSEDמכשולבקרןאו לחצן OPENלחצן CLOSEt > 6 st > 6 sOPENCLOSEOPENINGבעלייהCLOSEDסגורFAULTתקלה: המנוע עצורOPENפתוחCLOSINGבירידההתחלהמכל מצב: LS_OPEN ו-LS_CLOSED יחד ← FAULT
תרשים המצבים: באדום מעבר הבטיחות, ובכתום פסק הזמן שמוביל לתקלה

סינון הקפיצות מעכב את היפוך הכיוון ב-50 ms בלבד, והזרוע זזה בזמן הזה מרחק קטן מאוד. ובכל זאת הזרוע אינה נעצרת מיד בגלל ההתמדה, ולכן מרכיבים על זרוע אמיתית גם שפה גומית ומגבילים את הכוח שלה.

שאלה 14.2: מכונית שאורכה 4 m עוברת במהירות 10 km/h מתחת לקרן הבטיחות. כמה זמן נשארת הקרן קטועה? האם המכונית עלולה "להיעלם" בגלל סינון הקפיצות (50 ms)?
▶ניסוי חי — מחסום עם מפסקי גבולנפעיל מחסום עם מנוע דו-כיווני ושני מפסקי גבול במעגל המגענים, ונראה את הנעילה ההדדית.
⚠
המעבדה אינה זמינה כרגע. החומר בפרק שלם גם בלעדיה — ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר או פותחים אותו ישירות בקישור שמתחת למסגרת.
https://fahed-lab.com/control/gate_ls
פתיחת המעבדה בדף נפרד ↗ fahed-lab.com/control/gate_ls

תרשים חיווט והפעלה ראשונה (Wiring and Bring-up)

נחלק את החיווט לשני תרשימים: המוצאות והמבואות. מספרי ההדקים בשניהם לקוחים מאותה טבלת מבואות ומוצאות שממנה נבנים קבועי התוכנית.

LCD 16x2 I2CSDASCLVCCGNDArduino UnoA4A55VGNDD9D10D11D12A0L298NIN1IN2OUT1OUT2+12VGNDM+−12 V330 Ωירוקה330 Ωאדומה1 kΩ+5VBC547זמזם
המוצאות: הצג על I2C, גשר H עם מנוע 12 V, שתי נוריות החיווי, והזמזם דרך טרנזיסטור
אזהרה: נחבר ונפרק חוטים רק כשההזנה מנותקת, ובצבעים קבועים: אדום להזנה ושחור לאדמה. לא נזין את Uno מן הווסת 5 V של יחידת L298N כאשר Uno מחובר למחשב.
Arduino Uno5VD2D3D4D5D6D7D8GNDRX ENTRYVCCOUTGND5VENTRYRX EXITVCCOUTGND5VEXITRX BEAMVCCOUTGND5VBEAMLS_OPEN (NC)LS_CLOSED (NC)BTN OPENBTN CLOSE5Vנגדי משיכה למעלה פנימיים: INPUT_PULLUP
המבואות: שלושה מקלטי קרן (OUT עם קולט פתוח), שני מפסקי גבול (NC) ושני לחצנים. השולחים מוזנים רק מ-5 V ומן האדמה
טעות נפוצה: לחצן שמחובר בין ההדק לבין 5 V עם INPUT_PULLUP: ההדק תמיד HIGH והלחצן אינו פועל. עם INPUT_PULLUP הלחצן מחבר אל האדמה.

הפעלה ראשונה: לא נטען את התוכנית המלאה מיד, אלא נבדוק כל חלק בנפרד, כדי שהתקלות לא יתערבבו זו בזו:

  1. תוכנית קצרה שמדפיסה digitalRead() לכל מבוא: נקטע כל קרן ביד ונלחץ על כל לחצן ומפסק.
  2. נרים את הזרוע ביד ונראה שכל מפסק גבול משתנה במקום הנכון.
  3. נסובב את המנוע רגע קצר לכל כיוון ונוודא ש-IN1 מרים את הזרוע.
  4. תוכנית הדוגמה של הספרייה כותבת על הצג: אם לא מופיע דבר, נבדוק את כתובת I2C ואת הניגודיות.
  5. רק אחרי כל זה נטען את התוכנית המלאה.
שאלה 14.3: לוחצים OPEN והזרוע יורדת במקום לעלות, וממשיכה עד שהיא נתקלת בבסיס. מה התקלה ואיך מתקנים אותה? ולמה המנוע לא נעצר במפסק הגבול?

התוכנית המלאה (The Complete Sketch)

התוכנית היא מכונת מצבים שאינה עוצרת: אין בה delay() בשום מקום. כל מחזור של loop() נמשך פחות ממילישנייה, ויש בו שלושה צעדים קבועים: הספירה, אחריה מכונת המצבים (switch (state)), ואחריה החיווי.

התוכנית המלאה של מחסום החניון Arduino Uno · parking_gate.ino
// Parking gate controller - final project (chapter 14)
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

const byte ENTRY_PIN = 2;      // IR beam before the arm, LOW = car
const byte EXIT_PIN = 3;       // IR beam in the exit lane, LOW = car
const byte BEAM_PIN = 4;       // safety beam under the arm, LOW = blocked
const byte LS_OPEN_PIN = 5;    // NC limit switch, HIGH = arm is up
const byte LS_CLOSED_PIN = 6;  // NC limit switch, HIGH = arm is down
const byte BTN_OPEN_PIN = 7;   // manual buttons, LOW = pressed
const byte BTN_CLOSE_PIN = 8;
const byte IN1_PIN = 9, IN2_PIN = 10;   // L298N: IN1 = up, IN2 = down
const byte GREEN_PIN = 11, RED_PIN = 12, BUZZER_PIN = A0;

const int CAPACITY = 3;                 // places in the model lot
const unsigned long DEBOUNCE_MS = 50;
const unsigned long MOVE_MAX_MS = 6000;  // limit switch must be reached
const unsigned long HOLD_MS = 5000;      // stay open after the car passed
const unsigned long BEEP_MS = 500;

enum State { CLOSED, OPENING, OPEN, CLOSING, FAULT };
const char *NAMES[] = { "CLOSED", "OPENING", "OPEN", "CLOSING", "FAULT" };

enum { ENTRY, EXITS, BEAM, LS_OPEN, LS_CLOSED, B_OPEN, B_CLOSE, N_IN };
struct Input {
  byte pin;
  bool activeHigh;
  bool on, raw, rose, fell;   // debounced level, raw level, edges
  unsigned long t;            // time of the last raw change
};
Input in[N_IN] = {
  { ENTRY_PIN, false }, { EXIT_PIN, false }, { BEAM_PIN, false },
  { LS_OPEN_PIN, true }, { LS_CLOSED_PIN, true },
  { BTN_OPEN_PIN, false }, { BTN_CLOSE_PIN, false }
};

LiquidCrystal_I2C lcd(0x27, 16, 2);
State state = CLOSED;
unsigned long tState = 0, tBeep = 0;
int cars = 0;
bool armed = false, beep = false, lcdDirty = true;

bool active(const Input &k) {
  return digitalRead(k.pin) == (k.activeHigh ? HIGH : LOW);
}

void readInputs(unsigned long now) {
  for (byte i = 0; i < N_IN; i++) {
    Input &k = in[i];
    bool r = active(k);
    k.rose = k.fell = false;
    if (r != k.raw) { k.raw = r; k.t = now; }    // raw change: restart
    else if (r != k.on && now - k.t >= DEBOUNCE_MS) {
      k.on = r; k.rose = r; k.fell = !r;         // stable: accept it
    }
  }
}

void stamp(unsigned long now) {
  Serial.print("t="); Serial.print(now / 1000);
  Serial.print('.'); Serial.print((now / 100) % 10);
  Serial.print("  ");
}

void go(State s, unsigned long now) {   // the only place that moves
  state = s; tState = now; lcdDirty = true;
  digitalWrite(IN1_PIN, s == OPENING);   // one direction at most
  digitalWrite(IN2_PIN, s == CLOSING);
  if (s == CLOSED) armed = false;
  stamp(now); Serial.print("state: "); Serial.println(NAMES[s]);
}

void count(int d, unsigned long now) {
  cars += d; lcdDirty = true;
  stamp(now);
  Serial.print(d > 0 ? "car in   free = " : "car out  free = ");
  Serial.println(CAPACITY - cars);
}

void showLcd() {
  lcd.setCursor(0, 0);
  if (cars >= CAPACITY) lcd.print("PARKING FULL    ");
  else {
    lcd.print("Free places: "); lcd.print(CAPACITY - cars);
    lcd.print("  ");
  }
  lcd.setCursor(0, 1);
  lcd.print("Gate: "); lcd.print(NAMES[state]); lcd.print("   ");
  lcdDirty = false;
}

void setup() {
  Serial.begin(9600);
  for (byte i = 0; i < N_IN; i++) {
    pinMode(in[i].pin, INPUT_PULLUP);
    in[i].raw = in[i].on = active(in[i]);
  }
  pinMode(IN1_PIN, OUTPUT); pinMode(IN2_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT); pinMode(RED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  // position unknown after power-up: close first
  go(in[LS_CLOSED].on ? CLOSED : CLOSING, millis());
  lcd.init();
  lcd.backlight();
}

void loop() {
  unsigned long now = millis();
  readInputs(now);
  bool full = cars >= CAPACITY;

  // 1. counting
  if (in[ENTRY].on) armed = true;      // came through the entry lane
  if (in[BEAM].fell && armed) { armed = false; count(+1, now); }
  if (in[EXITS].fell && cars > 0) count(-1, now);
  if (in[ENTRY].rose && full) {
    beep = true; tBeep = now;
    stamp(now); Serial.println("lot FULL, entry refused");
  }
  full = cars >= CAPACITY;

  // 2. state machine
  if (in[LS_OPEN].on && in[LS_CLOSED].on && state != FAULT)
    go(FAULT, now);                     // both limits: broken wire
  switch (state) {
    case CLOSED:
      if (in[B_OPEN].rose || (in[ENTRY].on && !full)) go(OPENING, now);
      break;
    case OPENING:
      if (in[LS_OPEN].on) go(OPEN, now);
      else if (in[B_CLOSE].rose) go(CLOSING, now);
      else if (now - tState > MOVE_MAX_MS) go(FAULT, now);
      break;
    case OPEN:
      if (in[BEAM].on || (in[ENTRY].on && !full)) tState = now;
      if (in[B_CLOSE].rose && !in[BEAM].on) go(CLOSING, now);
      else if (now - tState >= HOLD_MS) go(CLOSING, now);
      break;
    case CLOSING:                       // obstacle: reverse at once
      if (in[BEAM].on || in[B_OPEN].rose) go(OPENING, now);
      else if (in[LS_CLOSED].on) go(CLOSED, now);
      else if (now - tState > MOVE_MAX_MS) go(FAULT, now);
      break;
    case FAULT:                         // motor off: wait for a person
      if (in[B_OPEN].rose) go(OPENING, now);
      else if (in[B_CLOSE].rose) go(CLOSING, now);
      break;
  }

  // 3. indication
  if (beep && now - tBeep >= BEEP_MS) beep = false;
  bool blink = (now / 500) % 2;
  digitalWrite(GREEN_PIN, !full && state != FAULT);
  digitalWrite(RED_PIN, state == FAULT ? blink : full);
  digitalWrite(BUZZER_PIN, state == FAULT || beep);
  if (lcdDirty) showLcd();
}
Freeplaces:2Gate:OPEN
השנייה 10: מכונית אחת בחניון
PARKINGFULLGate:CLOSED
השנייה 53: החניון מלא
PARKINGFULLGate:FAULT
השנייה 76: הזרוע תקועה
טעות נפוצה: כותבים delay(4000) כדי לחכות לירידת הזרוע: בזמן ההמתנה קרן הבטיחות אינה נקראת, והזרוע יורדת על המכונית. במערכת עם דרישות בטיחות כל המתנה נעשית בעזרת millis() ומצב.
הערה: millis() חוזר לאפס אחרי 232 ms, כלומר בערך 49.7 ימים. מכיוון שאנו מחשבים תמיד את ההפרש now - tState במספרים ללא סימן, פסקי הזמן נשארים נכונים גם ברגע החזרה לאפס.
▶ניסוי חי — מעבדת תוכניות Arduinoנדביק את התוכנית, נדמה את הלחצנים והחיישנים, ונעקוב אחר הצג הטורי לפני שנטען אותה ללוח.
⚠
המעבדה אינה זמינה כרגע. החומר בפרק שלם גם בלעדיה — ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר או פותחים אותו ישירות בקישור שמתחת למסגרת.
https://fahed-lab.com/micro/arduino_code_lab
פתיחת המעבדה בדף נפרד ↗ fahed-lab.com/micro/arduino_code_lab

תוכנית בדיקות (Test Plan)

תוכנית בדיקות היא טבלה שנכתוב לפני הבדיקה: לכל אירוע, המצב והמוצאות הצפויים; אחר כך נבצע ונרשום את התוצאה. כל דרישה מופיעה לפחות בשורה אחת.

#הדרישההאירועהצפויהתוצאה
1R4הפעלה כשהזרוע למטהCLOSED, ירוקה, "Free places: 3"☐
2R1 R6מכונית בכניסה, יש מקוםOPENING ואחר כך OPEN ב-LS_OPEN☐
3R2 R6המכונית חוצה את הקרןהספירה +1; אחרי 5 s ‏CLOSING ואחר כך CLOSED☐
4R2שתי מכוניות ברצףהזרוע נשארת למעלה, הספירה +2☐
5R5המקום האחרון התמלאאדומה, הירוקה כבויה, "PARKING FULL"☐
6R7 R2הולך רגל בקרן בזמן הירידהOPENING מיד; הספירה אינה משתנה☐
7R5מכונית בכניסה, החניון מלאנשאר CLOSED, צפצוף 0.5 s☐
8R3 R1מכונית יוצאת ואחרת ממתינההספירה −1 ואחר כך OPENING אוטומטית☐
9R8הזרוע תקועה בזמן הירידהFAULT אחרי 6 s: מנוע עצור, אדומה מהבהבת, זמזם☐
10R9לחצן CLOSE בזמן תקלהCLOSING ואחר כך CLOSED☐
11R9לחצן OPEN בלי מכוניתOPENING, OPEN, ואחרי 5 s הזרוע יורדת☐
12R8חוט LS_OPEN קרועFAULT מיד: שני המפסקים "פעילים" יחד☐

נבצע את שורות 1–10 כניסוי אחד. זמני האירועים בשניות: מכונית בכניסה ב-2 ומתחת לזרוע בין 7 ל-9; שתי מכוניות בין 20 ל-31; הולך רגל ב-38; מכונית רביעית ממתינה מ-52; מכונית יוצאת ב-56; הזרוע תקועה בין 69 ל-79; השומר לוחץ CLOSE ב-80. הזנו את הרצף הזה למודל של התוכנית בשפת פייתון, שמבצע את אותם תנאים בכל מילישנייה, וקיבלנו:

Serial Monitor · קריאה לדוגמה
t=0.0  state: CLOSED
t=2.0  state: OPENING
t=6.1  state: OPEN
t=9.0  car in   free = 2
t=14.0  state: CLOSING
t=18.0  state: CLOSED
t=20.0  state: OPENING
t=24.1  state: OPEN
t=27.0  car in   free = 1
t=31.0  car in   free = 0
t=36.0  state: CLOSING
t=38.0  state: OPENING
t=40.1  state: OPEN
t=45.1  state: CLOSING
t=49.1  state: CLOSED
t=52.0  lot FULL, entry refused
t=57.0  car out  free = 1
t=57.0  state: OPENING
t=61.1  state: OPEN
t=64.0  car in   free = 0
t=69.0  state: CLOSING
t=75.0  state: FAULT
t=80.0  state: CLOSING
t=84.1  state: CLOSED
01020304050607080050100t [s]מיקום הזרוע %FAULTמכשול
מיקום הזרוע באותו ניסוי: בירוק מכונית נכנסה, בכתום מכונית יצאה, והאזור האדום הוא מצב התקלה

נשווה את היומן לטבלה: בשנייה 38 הכיוון התהפך והולך הרגל לא נספר; ב-52 המכונית נדחתה; ב-57 יצאה מכונית והזרוע עלתה למכונית הממתינה; ב-75.0 נכנסנו למצב תקלה, 6 s אחרי תחילת הירידה. כל זמן מאוחר ב-50 ms מן האירוע בגלל סינון הקפיצות.

שאלה 14.4: לפי הניסוי, כמה שניות היה המנוע מחובר לזרם (מרים, מוריד או מנסה) במשך כל הניסוי?

הרחבות לפרויקט (Extensions)

דיווח אלחוטי עם ESP32: לוח ESP32 שולח בכל דקה את מספר המקומות הפנויים לדף באינטרנט, וכך הנהג יודע עוד לפני שהוא מגיע. עדיף להשאיר את Uno לבקרה ולהוסיף ESP32 לתקשורת בלבד: תקלה ברשת אינה עוצרת את המחסום.

אזהרה: ESP32 עובד ב-3.3 V ואינו עומד ב-5 V: בין המוצא של Uno לבין המבוא של ESP32 נשים מחלק מתח או ממיר רמות.

בקר שני: צג גדול ליד הרחוב מופעל על ידי בקר שני, והבקר הראשון שולח אליו את הספירה בכל שינוי. זו תקשורת בממשק מכונה-מכונה: שורת טקסט על קו טורי (TX אל RX עם אדמה משותפת).

אותו מחסום עם PLC: במפעל בונים מחסום כזה לרוב עם בקר מתוכנת (PLC), ואת התוכנית שלו כותבים בדיאגרמת סולם, כמו בפרק 2 של מקצוע 33.102:

ENTRYNOT_FULLPB_OPENBEAMK1LS_OPENK2K1מנוע: הרמהT_HOLDPB_CLOSEK2LS_CLOSEDBEAMPB_OPENK1K2מנוע: הורדה
דיאגרמת הסולם של המנוע: החזקה עצמית (K1, K2), מפסקי גבול NC, ונעילה הדדית בין K1 ל-K2

נשווה: ב-Arduino המצב הוא המשתנה state, וב-PLC הוא סליל K1 או K2 עם מגע החזקה עצמית. היפוך הכיוון הוא שני מגעים של BEAM: המגע הסגור במנוחה בשלב ההורדה עוצר את ההורדה, והמגע הפתוח במנוחה המקביל בשלב ההרמה מפעיל את ההרמה. פסק הזמן הוא קוצב זמן TON, והספירה היא מונה CTU/CTD עם השוואה שנותנת NOT_FULL. והנעילה ההדדית היא מגע K2 סגור במנוחה בשלב של K1 ולהפך, במקום הפונקציה go().

הערכת הפרויקט (Assessment Checklist)

התחוםנקודותמה נבדק✓
תיעוד25דרישות, דיאגרמת מלבנים, טבלת הדקים, תרשים מצבים, חיווט שתואם לתוכנית☐
סדר בחיווט20חוטים קצרים ומקובעים, צבעים קבועים, תוויות, מקור נפרד ואדמה משותפת☐
בטיחות25היפוך מול מכשול, פסק זמן ותקלה, מפסקי NC, נתיך, דיודות, בלי מתח רשת☐
בדיקות30תוכנית לפני הביצוע, כל דרישה נבדקה, יומן מצורף, הדגמת תקלה מול המורה☐

סיכום נוסחאות הפרק:

החוקהנוסחה
מקומות פנוייםfree = CAPACITY − cars
נגד לנורית חיוויR = VCC − VFIF
זרם בלחצן עם נגד משיכה למעלהI = VCCRPU
פסק הזמן לתנועהtmax = 1.5·tmove
זמן קטיעת הקרן על ידי מכוניתt = Lcarv

מילון מונחים:

עבריתEnglishالعربية
דרישות המערכתSystem requirementsمتطلبات النظام
מחסום חניוןParking barrierبوابة الموقف (الذراع)
דיאגרמת מלבניםBlock diagramمخطّط الكتل
מחסום אור (שולח-מקלט)Through-beam sensorالحاجز الضوئي
טבלת מבואות ומוצאותI/O tableجدول المداخل والمخارج
מפסק גבולLimit switchمفتاح نهاية المشوار
תכנון בטוח-כשלFail-safe designالتصميم الآمن عند العطل
נעילה הדדיתInterlockالإقفال المتبادل
מכונת מצביםState machineآلة الحالات
תרשים מצביםState diagramمخطّط الحالات
פסק זמןTimeoutالمهلة الزمنية
מצב תקלהFault stateحالة العطل
סינון קפיצותDebouncingإزالة الارتداد
תוכנית בדיקותTest planخطّة الاختبار
דיאגרמת סולםLadder diagramالمخطّط السلّمي
בקר מתוכנת (PLC)Programmable logic controllerالمتحكّم المنطقي القابل للبرمجة

שאלות לתרגול:

1.
רוצים להשתמש בתוכנית בחניון אמיתי שיש בו 40 מקומות.
א. איזו שורה משנים?
ב. כמה תווים כותבת התוכנית בשורה הראשונה של הצג כאשר החניון ריק? האם השורה מספיקה?
תשובות: א. const int CAPACITY = 40;. ב. ב-"Free places: 40" יש 15 תווים, ובשורה יש 16: היא מספיקה
2.
זרוע שאורכה 2.5 m עולה רבע סיבוב בתוך 3 s.
א. חשבו את מהירות קצה הזרוע.
ב. כמה זז הקצה בתוך 50 ms?
תשובות: א. v = 1.31 m/s. ב. Δs = 6.5 cm
3.
מדדנו את זמן התנועה במחסום אחר וקיבלנו 5.2 s. מהו הערך המתאים של MOVE_MAX_MS לפי כלל פי 1.5?
תשובות: 7800 ms
4.
רוצים זרם של 15 mA בנורית חיווי שהמתח הקדמי שלה 2.1 V.
א. חשבו את ההתנגדות.
ב. בחרו 220 Ω וחשבו את הזרם בפועל.
תשובות: א. R ≈ 193 Ω. ב. I ≈ 13.2 mA
5.
למה חיברנו את מפסקי הגבול דרך המגע NC? מה קורה בכל אחד מן החיבורים אם החוט של LS_CLOSED נקרע בזמן הירידה?
תשובות: עם NC החוט הקרוע נקרא "הגיעה", והמנוע נעצר מיד (בטוח). עם NO ההדק לעולם לא מגיע ל"הגיעה", והמנוע לוחץ על הבסיס עד פסק הזמן (6 s)
6.
מוסיפים מחסום שני ליציאה עם מנוע משלו.
א. אילו הדקים פנויים ב-Uno (בלי D0 ו-D1)?
ב. המחסום השני צריך: IN3, IN4, LS_OPEN2, LS_CLOSED2, BEAM2. האם ההדקים מספיקים?
ג. הציעו שני פתרונות.
תשובות: א. D13, A1, A2, A3 (4 הדקים). ב. נדרשים 5, לא מספיקים. ג. מרחיב הדקים על I2C, או לוח עם יותר הדקים, או בקר שני למחסום השני
7.
מצב לילה: בין 22:00 ל-06:00 הזרוע אינה עולה אוטומטית, אלא רק בלחצן OPEN.
א. איזה חיישן או יחידה מוסיפים כדי לדעת את השעה?
ב. איזה תנאי בתוכנית משנים?
תשובות: א. שעון זמן אמת (RTC) עם הספרייה RTClib. ב. במצב CLOSED: (in[ENTRY].on && !full && !night)
8.
אחרי הפסקת חשמל המונה מתחיל מאפס. מחליטים לשמור את הספירה בזיכרון EEPROM בכל שינוי. גיליון הנתונים מתיר בערך 100000 כתיבות לכל תא. בחניון עוברות 200 מכוניות ביום, וכל מכונית נכתבת פעמיים (כניסה ויציאה). אחרי כמה ימים התא נשחק?
תשובות: בערך 250 ימים; לכן מעבירים את הספירה בין תאים שונים, או שומרים אותה רק כשהמתח יורד
9.
מפעילים את התוכנית מההתחלה (הזרוע למטה, הספירה אפס). מכונית בכניסה בין 1 s ל-5 s, מתחת לזרוע בין 5 s ל-7 s, ואז השומר לוחץ OPEN ב-9 s. מה המצב ומה הספירה בשנייה 14?
תשובות: המצב CLOSING, והספירה 1 (לחיצה על OPEN במצב OPEN אינה עושה דבר)
10.
ביומן הבדיקה אין שורת car in בשנייה 39, אף שעצם יצא מקרן הבטיחות. למה?
תשובות: כי armed היה שקר: הולך הרגל לא עבר בחיישן הכניסה, ולכן אינו נספר (R2)
תשובות סופיות לשאלות הפרק
14.1 I = 0.1 mA עד 0.25 mA
14.2 t = 1.44 s, ארוך בהרבה מ-50 ms, ולכן היא אינה נעלמת
14.3 חוטי המנוע הפוכים: נחליף בין OUT1 ל-OUT2. בזמן OPENING התוכנית מחכה ל-LS_OPEN, והזרוע לחצה על LS_CLOSED; המנוע נעצר רק אחרי פסק הזמן
14.4 בערך 34.4 s
נוסחאון הספר: כל נוסחאות הפרק הזה מרוכזות, עם סרטוטיהן וטבלאות הסמלים שלהן, בפרק 15 — נוסחאון הספר.