الحسّاسات والمشغّلات للمتحكّمات · الفصل 14 — مشروع ختامي: نظام تحكّم متكاملפרויקט מסכם: מערכת בקרה שלמה · 33.102⌂📚🏠
الفصل 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_PINLED أخضر + 330 ΩHIGH = يوجد مكان
D12RED_PINLED أحمر + 330 ΩHIGH = ممتلئ، أو عطل (يومض)
A0BUZZER_PINطنّان عبر BC547HIGH = إنذار
A4 A5lcdشاشة LCD 16×2، عنوان 0x27SDA · SCL

كل المداخل مع INPUT_PULLUP، أي مع مقاومة الرفع الداخلية: الزرّ أو الحسّاس يوصل الطرف إلى الأرضي، فالمستوى الفعّال LOW. مفتاحا نهاية المشوار نوصلهما بالتلامس المغلق في الراحة (NC): عندما يضغطهما الذراع ينفتح التلامس ويصبح الطرف HIGH.

التصميم الآمن عند العطل (תכנון בטוח-כשל (Fail-safe)): هو اختيار التوصيل بحيث يؤدي العطل الأكثر احتمالا (سلك مقطوع) إلى الحالة الآمنة. سلك مقطوع لمفتاح NC يُقرأ «وصل الذراع» فيتوقّف المحرّك بدل أن يدور حتى يكسر شيئا. وإذا قُرئ المفتاحان معا «وصل»، وهذا مستحيل، يدخل البرنامج حالة العطل.
مثال 14.1
نحسب التيار في مخرجَي مصباحَي البيان: كل LED مع 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 – מנוע דו-כיווני: גשר H:

محرّك الذراع محرّك تيار مستمر 12 V. نعكس اتجاهه بعكس قطبية الجهد عليه بواسطة جسر H (גשר 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 — لوحة القوانين الشاملة. (נוסחאון הספר)