Your button fires twice. Here is why, and here is the fix.
أو تصفّح كل الدروس.
- 503قطعة على المنضدة
- 44لوحة تعمل الآن
- 1.00xزمن حقيقي، على كل لوحة
المفتاح اللمسي قبة معدنية صغيرة تُضغط على نقطة تلامس، ولا تهبط وتستقر. إنها تهبط، وترتد جزءًا من الميلي متر، وتهبط ثانية، وتكرر ذلك بضع مرات قبل أن تستقر، كما لا تتوقف عملة سقطت لحظة ملامستها الطاولة أول مرة. كهربائيًا، تنفتح نقطة التلامس وتنغلق عدة مرات في أول ملي ثوانٍ من الضغطة، ومرة أخرى عند الترك، قبل أن تثبت. والكود الذي يعدّ الحواف على ذلك الطرف يعدّ خطأً، لأن ضغطة واحدة قد تُسجَّل مرتين أو ثلاثًا أو أكثر.
يحاكي Mokxi هذا بدل أن يخفيه. كل زر ضغط على المنضدة يرتد افتراضيًا، وفق نمط اسمه typical: من ثلاثة إلى ثمانية انتقالات، تستقر بين 1 و5 ملي ثانية بعد بدء الضغطة. وهذا قريب من مفتاح لمسي حقيقي، ويعني أن الدائرة أعلاه ليست عرضًا مُعدًّا لخلل؛ بل هي الخلل نفسه، يحدث بالطريقة نفسها التي يحدث بها على مكتبك.
ما يعرضه المحاكي
البرنامج الذي يعمل أعلاه هو مثال interrupt المدمج في Uno. يربط معالج مقاطعة بالطرف 2 يُنفَّذ عند كل حافة هابطة (كل لحظة يهبط فيها الطرف من 5 V إلى 0 V) ويضيف واحدًا إلى عداد، دون أي منطق لإزالة الارتداد فيه عن قصد. افتح الدائرة في المحرر، وافتح الشاشة التسلسلية، واضغط الزر مرة واحدة بنظافة، كما تضغط أي زر. العدد المطبوع بعد ضغطة واحدة غالبًا اثنان أو ثلاثة وليس واحدًا، والـLED المدمج، الذي يتبدّل بحسب زوجية العداد، ينتهي أحيانًا في الحالة التي لم تتوقعها. لا شيء معطّل. ارتد المفتاح، ورأى المعالج كل ارتداد ضغطة منفصلة، وعدّ كل واحدة منها بأمانة، لأن هذا بالضبط ما يطلبه الكود.
هذا الدرس كله في جملة واحدة: المفتاح، وليس الكود، هو الذي أنتج الضغطات الزائدة، ويجب أن يكون الحل في الكود لأن المفتاح لا يمكن جعله نظيفًا تمامًا.
ثلاث طرق لإصلاحه
الحل الأول، والأرخص، هو نافذة إزالة ارتداد برمجية: بعد أي تغيّر على الطرف، تجاهل التغيّرات اللاحقة لوقت قصير، طويل بما يكفي لتغطية زمن الاستقرار وقصير بما يكفي ألا تُحسب ضغطة ثانية حقيقية ارتدادًا. ما بين 5 و20 ملي ثانية يغطي مفتاحًا نموذجيًا بارتياح.
الثاني حل عتادي: مكثف صغير على طرفي المفتاح. يبطّئ صعود الجهد وهبوطه بما يكفي لألا تصل الارتدادات أبدًا إلى انتقال منطقي كامل، فلا يرى الطرف حقًا إلا حافة نظيفة واحدة. يعمل هذا على المنضدة لأن المكثف الحقيقي يحتاج وقتًا حقيقيًا للشحن والتفريغ، ويعمل في Mokxi للسبب نفسه: يكامل المكثف هنا عبر الزمن باستجابة RC أسية حقيقية بدل أن يبدّل لحظيًا، لذلك تصمد نسخة مصغّرة من هذا الحل في المحاكي فعلًا، وليس نظريًا فقط.
الثالث هو إزالة الارتداد داخل معالج المقاطعة نفسه، وهذا يحتاج قاعدة إضافية واحدة: يجب أن يفعل المعالج أقل ما يمكن، لأنه يعمل بينما المقاطعات الأخرى معلّقة. سجّل الوقت وانسحب؛ ونفّذ اتخاذ القرار الفعلي، بما فيه نافذة إزالة الارتداد، داخل loop().
نسخة millis()
برنامج toggle المدمج في Uno هو الحل البرمجي كاملًا، على الطرف 2 نفسه الذي يستخدمه عرض الارتداد. يحتفظ بآخر قراءة ووقت آخر تغيّر لها، ولا يتصرف عند تغيّر إلا بعد أن تمر DEBOUNCE_MS، أي خمسة وعشرون ملي ثانية، منذ التغيّر السابق. اضغط الزر مرة واحدة والبرنامج يعمل فيضيء المصباح على الطرف 13؛ اضغطه مرة أخرى فينطفئ، مرة واحدة تمامًا لكل ضغطة، وهذا هو الهدف كله.
لاحظ ما الذي يقارنه الكود فعلًا. إنه لا يعدّ كم من الوقت ضُغط الزر؛ بل يقيس كم مضى منذ آخر تغيّر مقبول، على أي من الحافتين، الضغط أو الترك. وهذا التمييز هو ما يجعل النافذة متماثلة: يُتجاهل الارتداد بعد الضغط مباشرةً، ويُتجاهل بعد الترك مباشرةً، بقاعدة الخمسة والعشرين ملي ثانية نفسها التي تغطي الاثنين دون معالجة خاصة لأي الحافتين هي.
const int BUTTON_PIN = 2;
const int LAMP_PIN = 13;
const unsigned long DEBOUNCE_MS = 25;
int lastReading = HIGH;
unsigned long lastChange = 0;
bool lampOn = false;
void loop() {
int reading = digitalRead(BUTTON_PIN);
unsigned long now = millis();
if (reading != lastReading && now - lastChange >= DEBOUNCE_MS) {
lastChange = now;
lastReading = reading;
if (reading == LOW) {
lampOn = !lampOn;
digitalWrite(LAMP_PIN, lampOn ? HIGH : LOW);
}
}
}مسألة مقاومة الرفع
يوصّل كلٌّ من عرض الارتداد وحلّه الزر مباشرةً بالطرف 2 دون مقاومة خارجية، لأن الكود يشغّل مقاومة الرفع الداخلية للطرف بـINPUT_PULLUP. تمسك هذه المقاومة الطرف عند 5 V حتى يوصله الزر بالأرضي، ولهذا يُقرأ الطرف LOW أثناء الضغط على الزر وليس HIGH، وهي تفصيلة تربك الناس أول مرة يرونها لأنها عكس ما يبدو أن الزر ينبغي أن يفعله.
البديل مقاومة رفع خارجية حقيقية، مقاومة 10 kilohm من الطرف إلى 5 V، والزر يسحب الشبكة نفسها إلى الأرضي عند الضغط. كهربائيًا الفكرة واحدة؛ إلا أن الداخلية أضعف (نحو 35 kilohm) ولا تحتاج قطعة إضافية. وتبني صفحة مقاومة الرفع النسخة الخارجية من الصفر، وتستحق القراءة إذا بدت لك الداخلية يومًا كأنها ظهرت من العدم.
لماذا ضُبط النمط الافتراضي هكذا
النمط typical، من ثلاثة إلى ثمانية انتقالات تستقر خلال 1 إلى 5 ملي ثانية، مفتاح لمسي عادي عن قصد، من النوع الموجود في كل مجموعة لوحة تجارب تقريبًا، وليس أسوأ حالة اختيرت لتبدو المشكلة أسوأ مما هي. ويوجد نمط worst أيضًا، يصل إلى عشرين انتقالًا خلال 10 إلى 20 ملي ثانية، أقرب إلى مفتاح متعب أو مفتاح حد ذي ذراع طويلة استُعمل كثيرًا، ويستحق اختبار نافذة إزالة الارتداد عليه بعد معالجة الحالة النموذجية: فالحل المضبوط على ارتداد 5 ملي ثانية دون هامش قد يفشل مع مفتاح يرتجف 15.
الأسئلة
كم ينبغي أن يكون تأخير إزالة الارتداد؟
ما بين 5 و20 ملي ثانية يناسب مفتاحًا لمسيًا نموذجيًا، وهو أيضًا النطاق الذي يستقر ضمنه نمط الارتداد الافتراضي في Mokxi. إذا قصر أكثر من اللازم مرّ بعض الارتداد، وإذا طال أكثر من اللازم بدأت الضغطة المزدوجة السريعة تبدو غير مستجيبة.
هل أحتاج المكثف إذا كنت أزيل الارتداد برمجيًا أصلًا؟
لا. الاثنان بديلان يحلان المشكلة نفسها في مكانين مختلفين، واستخدامهما معًا ليس خطأ، بل زيادة لا حاجة إليها. اختر ما يناسب الدائرة: البرمجة لا تكلّف شيئًا إضافيًا، والعتاد يعمل حتى لو لم يتغير الكود الذي يقرأ الطرف.
لماذا ما زالت مقاطعتي تُنفَّذ مرتين بعد إضافة فحص إزالة الارتداد؟
غالبًا لأن فحص نافذة إزالة الارتداد نفسه داخل معالج المقاطعة إلى جانب عمل بطيء، أو لأن الوقت يُقارَن بقيمة مخزّنة خاطئة. اجعل المعالج يقرأ الساعة ويضبط علَمًا فقط، وأجرِ المقارنة داخل loop().
هل يحتاج INPUT_PULLUP إلى مقاومة خارجية أيضًا؟
لا. إنه يشغّل مقاومة رفع مدمجة في ATmega328P نفسها، لذلك يحتاج الزر إلى سلكين فقط: واحد إلى الطرف، وآخر إلى GND.
ما قيمة المكثف التي تزيل ارتداد مفتاح نموذجي عتاديًا؟
نحو 100 نانوفاراد على طرفي المفتاح، وغالبًا مع مقاومة صغيرة على التوالي للحد من تيار التفريغ عند أول ضغطة، نقطة انطلاق شائعة؛ والقيمة الصحيحة تعتمد على المفتاح، ويستحق الأمر فحصها على المنضدة بالقطعة الفعلية بدل الوثوق برقم واحد لكل مفتاح.
هل إزالة الارتداد مشكلة للأزرار فقط؟
لا. أي تلامس ميكانيكي يرتد، بما فيه المرحّلات ومفاتيح التبديل ومفاتيح الحد على محرك أو باب. والحل نفسه ينطبق: نافذة زمنية بعد أول تغيّر، أو مكثف على طرفي التلامس، أيهما يناسب الدائرة التي تقرؤه.
واصل
ابنِ هذا على أرض الواقع
افتح المحرر وغيّر قيمة وشاهد الرقم يتحرك معها. لا شيء لتثبّته ولا حاجة إلى حساب.