المداخل الرقمية: توصيل حسّاس رقمي بالمتحكّم
الحسّاس الرقمي يخبر المتحكّم بشيء واحد: نعم أو لا. زرّ مضغوط أو متروك، جسم أمام الحسّاس أو لا، باب مفتوح أو مغلق. يبدو ذلك بسيطا، لكن خلف الدالة digitalRead() أسئلة كهربائية حقيقية: ما الجهد الذي يُقرأ 1؟ ماذا يقرأ طرف غير موصول؟ لماذا يعدّ البرنامج عشر ضغطات بدل ضغطة واحدة؟ وكيف نوصل حسّاسا يعمل بجهد يختلف عن جهد المتحكّم بدون أن نُتلفه؟ في هذا الفصل نجيب عن هذه الأسئلة، ونكتب لكل حالة دائرة توصيل وبرنامجا كاملا.
- نحدّد مستويات الجهد التي يقرؤها Arduino Uno و ESP32 كـ HIGH وكـ LOW، والمنطقة غير المعرّفة بينهما.
- نشرح مشكلة المدخل العائم ونحلّها بمقاومة رفع أو خفض، خارجية أو داخلية، ونحسب تيارها.
- نعدّ الضغطات بكشف الحافة، ونزيل ارتداد التلامس برمجيا بالدالة millis().
- نصمّم مُجزِّء جهد لقراءة حسّاس يعمل بـ 5 V بمتحكّم يعمل بـ 3.3 V.
- نعزل إشارة صناعية 24 V بمقرن ضوئي ونحسب مقاومتي الدائرة.
- نعدّ نبضات سريعة بمقاطعة خارجية attachInterrupt().
רמות לוגיות במבוא ספרתי – المستويات المنطقية في المدخل الرقمي (Logic Levels)
المدخل الرقمي (מבוא ספרתי) لا يقيس الجهد مثل الفولتمتر. هو يقارن جهد الطرف بحدّين، ويعيد إلى البرنامج قيمة واحدة من اثنتين: HIGH (1) أو LOW (0).
في المتحكّم ATmega328P الذي في لوحة Arduino Uno تُعطى العتبتان في ورقة البيانات ككسر من جهد التغذية VCC، وفي ESP32 ككسر من VDD:
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| VIH | أصغر جهد يُقرأ بالتأكيد HIGH | [V] |
| VIL | أكبر جهد يُقرأ بالتأكيد LOW | [V] |
| VCC | جهد تغذية المتحكّم، 5 V في Uno | [V] |
وفي ESP32 (حسب ورقة البيانات): VIH = 0.75·VDD و VIL = 0.25·VDD ، حيث VDD = 3.3 V.
0.8 V < 1.5 V ، لذلك يُقرأ LOW. الجهد 4.1 V > 3 V ، لذلك يُقرأ HIGH.
الجهد 2.2 V يقع بين 1.5 V و 3 V: في المنطقة غير المعرّفة. القراءة غير مضمونة، وقد تتغيّر من مرة إلى أخرى.
מבוא צף ונגדי משיכה – المدخل العائم ومقاومات الرفع والخفض (Floating Input, Pull-up, Pull-down)
ننظر إلى زرّ موصول بين 5 V والطرف D7 فقط (الرسم الأيمن). عند الضغط يصل الطرف إلى 5 V ويُقرأ 1. لكن عند ترك الزر لا يوصل الطرف بأي شيء، فهو عائم، والقراءة لا معنى لها. الحل: مقاومة تشدّ الطرف إلى مستوى ثابت عندما يكون الزر متروكا.
في البرنامج نعرّف الطرف كمدخل عادي pinMode(7, INPUT)، فيعيد digitalRead(7) القيمة 1 عند الضغط و 0 عند الترك.
مع 220 Ω يصبح التيار 22.7 mA : هدر بلا فائدة، فالمدخل لا يحتاج إلى تيار تقريبا. وعند ترك الزر يمر في المقاومة تيار التسرّب فقط:
وهذا أصغر بكثير من VIL = 1.5 V ، فيُقرأ الطرف 0 بأمان.
مقاومة الرفع والمنطق المعكوس – נגד משיכה למעלה ולוגיקה הפוכה:
هذا هو المنطق المعكوس (לוגיקה הפוכה): الضغط يعطي 0 والترك يعطي 1. التوصيل بمقاومة رفع هو الأكثر شيوعا، لأن الزر يوصل بـ GND فقط، ولا نمدّ سلك 5 V إلى لوحة الأزرار. وفي ATmega328P توجد مقاومة رفع داخلية على كل طرف، نشغّلها بالأمر pinMode(pin, INPUT_PULLUP)، فلا نحتاج إلى مقاومة خارجية أصلا. قيمتها بين 20 kΩ و 50 kΩ (قيمة نموذجية حسب ورقة البيانات)، أي أنها ليست قيمة دقيقة.
const int BTN = 2;
const int LED = 13; // on-board LED
void setup() {
pinMode(BTN, INPUT_PULLUP); // idle = HIGH
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
int raw = digitalRead(BTN);
bool pressed = (raw == LOW); // inverted logic
digitalWrite(LED, pressed ? HIGH : LOW);
Serial.print("raw = ");
Serial.print(raw);
if (pressed) Serial.println(" pressed");
else Serial.println(" released");
delay(300);
}raw = 1 released raw = 1 released raw = 1 released raw = 0 pressed raw = 0 pressed raw = 1 released raw = 1 released
קפיצות מגע, זיהוי קצה וסינון קפיצות – ارتداد التلامس وكشف الحافة (Contact Bounce and Debouncing)
عدّ الضغطات: كشف الحافة – זיהוי קצה:
نريد أن نعدّ كم مرة ضُغط الزر. الخطأ الأول: نزيد العدّاد ما دام الزر مضغوطا:
if (digitalRead(BTN) == LOW) { // wrong: true in every loop while held
count++;
}الدالة loop() تدور آلاف المرات في الثانية، فضغطة واحدة تزيد العدّاد مئات أو آلاف المرات. الضغطة الواحدة هي تغيّر الحالة وليست الحالة نفسها، ولذلك نبحث عن الحافة.
لكن كشف الحافة وحده لا يكفي: كل ارتدادة هي حافة هابطة أيضا. لذلك نجمع في البرنامج التالي كشف الحافة مع إزالة الارتداد.
إزالة الارتداد بالدالة millis() – סינון קפיצות בתוכנה:
نحفظ في lastChange زمن آخر تغيّر في القراءة. إذا مرّ منذ ذلك الحين DEBOUNCE_MS ميلي ثانية، والقراءة تختلف عن الحالة المقبولة stableState، نقبلها. وإذا كانت الحالة الجديدة LOW فهذه حافة هابطة مؤكّدة: ضغطة واحدة. لا نستعمل delay() لأنه يوقف البرنامج كله، وmillis() يعطي الزمن منذ التشغيل بدون توقف.
const int BTN = 2;
const unsigned long DEBOUNCE_MS = 30;
int lastReading = HIGH; // previous raw reading
int stableState = HIGH; // accepted state
unsigned long lastChange = 0, presses = 0;
void setup() {
pinMode(BTN, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
int reading = digitalRead(BTN);
if (reading != lastReading) { // contact moved
lastChange = now; // restart timer
lastReading = reading;
}
bool settled = (now - lastChange >= DEBOUNCE_MS);
if (settled && reading != stableState) {
stableState = reading; // accept it
if (stableState == LOW) { // falling edge
presses++;
Serial.print("t = ");
Serial.print(now);
Serial.print(" ms presses = ");
Serial.println(presses);
}
}
}t = 1232 ms presses = 1 t = 2482 ms presses = 2 t = 3743 ms presses = 3
t = 1200 ms presses = 1 t = 1200 ms presses = 2 t = 1201 ms presses = 3 t = 1202 ms presses = 4 t = 1380 ms presses = 5 t = 1381 ms presses = 6
ضغطنا الزر 3 مرات، والشاشة الأولى تعطي 3 ضغطات. في الشاشة الثانية وضعنا DEBOUNCE_MS = 0، فتُقبل كل حافة فورا، وهذا هو كشف الحافة وحده. الضغطة الأولى وحدها عُدّت 6 مرات: 4 حواف في الضغط و 2 عند الترك، عندما لمس المعدن مرة أخرى للحظة. وبعد الضغطات الثلاث وصل العدّاد إلى 17 بدل 3.
آخر تغيّر في القراءة كان عند 1202.6 ms ، والدالة millis() تعطي عددا صحيحا، فحُفظ lastChange = 1202.
تُقبل الحالة الجديدة عندما يمر DEBOUNCE_MS (أي 30 ms) بدون تغيّر:
التأخير بين اللمس والطباعة 32 ms لا يشعر به الإنسان، والارتدادات كلها قد صُفّيت.
חיבור חיישן 5 V לבקר 3.3 V – توصيل حسّاس 5 V بمتحكّم 3.3 V (Level Shifting with a Voltage Divider)
كثير من وحدات الحسّاسات تعمل بـ 5 V ومخرجها الرقمي 5 V ، مثل طرف ECHO في الحسّاس فوق الصوتي HC-SR04 (الفصل 7). لا نوصل هذا المخرج مباشرة بطرف ESP32 أو بطرف الراسبيري باي. أبسط حلّ للإشارات البطيئة نسبيا هو مُجزِّء الجهد (מחלק מתח):
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| Vin | جهد مخرج الحسّاس | [V] |
| Vout | الجهد على طرف المتحكّم | [V] |
| R1 , R2 | المقاومة على التوالي، والمقاومة إلى GND | [Ω] |
شرطان للتصميم: Vout ≤ 3.3 V حتى عندما يكون Vin أعلى قليلا من 5 V ، و Vout ≥ VIH = 2.475 V حتى يُقرأ 1 بثقة.
القيمة القياسية 18 kΩ تعطي عند 5.25 V الجهد 3.38 V ، أكثر من 3.3 V. لذلك نختار R2 = 15 kΩ:
3 V ≥ 2.475 V : يُقرأ HIGH، و 3.15 V < 3.3 V : الطرف آمن.
const int SENSOR = 27; // via 10k / 15k divider
int lastState = LOW;
unsigned long objects = 0;
void setup() {
pinMode(SENSOR, INPUT);
Serial.begin(9600);
}
void loop() {
int state = digitalRead(SENSOR);
if (lastState == LOW && state == HIGH) {
objects++; // rising edge
Serial.print("objects = ");
Serial.println(objects);
}
lastState = state;
}objects = 1 objects = 2 objects = 3
בידוד אות תעשייתי 24 V במצמד אופטי – عزل إشارة صناعية 24 V بمقرن ضوئي (Optocoupler Isolation)
الحسّاسات الصناعية ومفاتيح نهاية المشوار في لوحات التحكّم تعمل عادة بـ 24 V مستمر. مُجزِّء الجهد هنا ليس حلا جيدا: الأسلاك طويلة، والضجيج والقمم الكهربائية في المصنع قد تصل إلى المتحكّم. الحل الصناعي هو العزل.
نستعمل المقرن الضوئي الشائع PC817. القيم من ورقة البيانات (قيم نموذجية حسب ورقة البيانات): جهد الـ LED الأمامي VF ≈ 1.2 V ؛ نسبة نقل التيار (יחס העברת זרם) CTR = ICIF لا تقل عن 50% عند IF = 5 mA ؛ وجهد التشبّع VCE,sat ≤ 0.2 V.
المنطق هنا معكوس أيضا: إشارة 24 V موجودة ← الـ LED يضيء ← الترانزستور يوصل ← الطرف D4 عند 0 V (LOW). بدون إشارة الترانزستور مفتوح، ومقاومة الرفع RPU تشدّ الطرف إلى 5 V (HIGH).
(أ) نحسب RLED لتيار IF = 5 mA ونختار قيمة قياسية ونحسب قدرتها.
(ب) نفحص أن الترانزستور يتشبّع مع RPU = 10 kΩ.
(أ) جهد المقاومة هو جهد الإشارة ناقص جهد الـ LED:
نختار القيمة القياسية 4.7 kΩ ونحسب التيار والقدرة الحقيقيين:
نختار مقاومة قدرتها 0.25 W على الأقل.
(ب) التيار الذي تحتاجه مقاومة الرفع حتى ينزل الطرف إلى جهد التشبّع، والتيار الذي يضمنه المقرن:
2.43 mA > 0.48 mA : الترانزستور يتشبّع، وجهد الطرف نحو 0.2 V ، أقل بكثير من VIL = 1.5 V.
const int IN24 = 4; // opto output, 10k pull-up
int lastState = HIGH; // HIGH = no 24 V signal
unsigned long items = 0;
void setup() {
pinMode(IN24, INPUT);
Serial.begin(9600);
}
void loop() {
int state = digitalRead(IN24);
if (lastState == HIGH && state == LOW) {
items++; // 24 V appeared
Serial.print("items = ");
Serial.println(items);
}
lastState = state;
}items = 1 items = 2 items = 3
פסיקות חומרה לספירת פולסים – المقاطعات الخارجية لعدّ النبضات السريعة (Hardware Interrupts)
في كل البرامج السابقة قرأ loop() الطرف مرة بعد مرة. هذه الطريقة تسمى الاستطلاع (תשאול). إذا انشغل البرنامج بعمل طويل، مثل قراءة حسّاس بطيء أو كتابة على شاشة، قد تأتي نبضة قصيرة وتذهب بدون أن يراها.
مثال: مروحة حاسوب لها طرف TACH يعطي نبضتين في كل دورة (حسب المواصفة الشائعة لمراوح الحاسوب). المخرج من نوع «المجمّع المفتوح»: الترانزستور يشدّ الخط إلى GND فقط، فيحتاج إلى مقاومة رفع إلى 5 V. المروحة تتغذّى بـ 12 V ، لكن مخرجها لا يحمل 12 V ، ولذلك يكفي أرضي مشترك.
const int TACH = 2; // INT0 on Uno
const unsigned long PULSES_PER_REV = 2;
volatile unsigned long pulses = 0; // set in ISR
unsigned long lastPrint = 0;
void onPulse() { pulses++; } // ISR: short
void setup() {
pinMode(TACH, INPUT); // 10k pull-up to 5V
attachInterrupt(digitalPinToInterrupt(TACH),
onPulse, FALLING);
Serial.begin(9600);
}
void loop() {
if (millis() - lastPrint >= 1000) {
lastPrint += 1000;
noInterrupts(); // copy, then reset
unsigned long n = pulses;
pulses = 0;
interrupts();
Serial.print("pulses = ");
Serial.print(n);
Serial.print(" rpm = ");
Serial.println(n * 60 / PULSES_PER_REV);
}
}pulses = 0 rpm = 0 pulses = 30 rpm = 900 pulses = 46 rpm = 1380 pulses = 54 rpm = 1620 pulses = 58 rpm = 1740 pulses = 59 rpm = 1770
- معالج المقاطعة قصير: يزيد العدّاد فقط. لا Serial ولا delay() داخله، وإلا قد يتجمّد البرنامج.
- المتغيّر الذي يتغيّر في المعالج يُعرَّف volatile، حتى يقرأه loop() من الذاكرة في كل مرة.
- المتحكّم في Uno يقرأ متغيّرا من 4 بايتات على عدة خطوات، لذلك ننسخه بين noInterrupts() و interrupts().
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| n | سرعة الدوران | [rpm] |
| N | عدد النبضات في ثانية واحدة | — |
| p | عدد النبضات في كل دورة | — |
نبضة كل 16.9 ms تقريبا: برنامج فيه delay(100) مثلا كان سيفوّت معظمها، أما المقاطعة فتلتقطها كلها.
ملخّص قوانين الفصل:
| القانون | الصيغة |
|---|---|
| عتبة المستوى العالي في ATmega328P (Uno) | VIH = 0.6·VCC |
| عتبة المستوى المنخفض في ATmega328P (Uno) | VIL = 0.3·VCC |
| عتبة المستوى العالي في ESP32 | VIH = 0.75·VDD |
| عتبة المستوى المنخفض في ESP32 | VIL = 0.25·VDD |
| تيار مقاومة الرفع أو الخفض عند الضغط | I = VCCR |
| مُجزِّء الجهد | Vout = Vin·R2R1 + R2 |
| مقاومة الـ LED في المقرن الضوئي | RLED = Vin − VFIF |
| شرط التشبّع في المقرن الضوئي | CTR·IF ≥ VCC − VCE,satRPU |
| سرعة الدوران من النبضات | n = 60·Np |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية (كما تظهر في الامتحان) | English |
|---|---|---|
| المدخل الرقمي | מבוא ספרתי | Digital input |
| المستوى المنطقي | רמה לוגית | Logic level |
| عتبة المستوى العالي / المنخفض | סף רמה גבוהה / נמוכה | V_IH / V_IL threshold |
| المنطقة غير المعرّفة | תחום לא מוגדר | Undefined region |
| المدخل العائم | מבוא צף | Floating input |
| مقاومة الرفع (pull-up) | נגד משיכה למעלה | Pull-up resistor |
| مقاومة الخفض (pull-down) | נגד משיכה למטה | Pull-down resistor |
| المنطق المعكوس | לוגיקה הפוכה | Inverted (active-low) logic |
| ارتداد التلامس | קפיצות מגע (ריטוט) | Contact bounce |
| إزالة الارتداد | סינון קפיצות | Debouncing |
| كشف الحافة | זיהוי קצה | Edge detection |
| الحافة الصاعدة / الهابطة | קצה עולה / יורד | Rising / falling edge |
| مُجزِّء الجهد | מחלק מתח | Voltage divider |
| المقرن الضوئي | מצמד אופטי | Optocoupler |
| نسبة نقل التيار | יחס העברת זרם | Current transfer ratio (CTR) |
| الاستطلاع | תשאול | Polling |
| المقاطعة | פסיקה | Interrupt |
| معالج المقاطعة | מטפל בפסיקה | Interrupt service routine (ISR) |
أسئلة للتمرين:
(أ) 0.5 V
(ب) 1.6 V
(ج) 2.9 V؟