ورودی کلید در Arduino Uno: پولآپ و debounce با ماشین حالت
کلید D2 به زمین، منطق active-low، حذف جهش تماس با مهلت مشخص و کد قابل دانلود برای Arduino Uno؛ همراه آزمون زمان و سرریز millis.
فهرست مطالب
برای خواندن یک کلید لحظهای معمولاً باز با Arduino Uno R3، کلید را بین D2 برد و GND ببندید و حالت INPUT_PULLUP را انتخاب کنید. در حالت رها، ورودی HIGH است و با فشردن کلید LOW میشود. شمارنده باید فقط پس از تأیید تغییر وضعیت افزایش پیدا کند؛ هر بار خواندن LOW به معنی یک فشار تازه نیست. در این تمرین همین رفتار را با ماشین حالت پیاده میکنیم و برای ورودیای که آرام نمیشود، مهلت و خروجی خطا داریم.
برد مرجع، Uno R3 پنجولتی با ATmega328P و هدف کامپایل arduino:avr:uno است. تمرین برای یک کلید محلی با سیم کوتاه نوشته شده است. فرضهای الکتریکی آن را بدون بررسی به ESP32 سهولتوسه یا Uno R4 منتقل نکنید. اگر مفهوم GPIO، حلقهٔ برنامه یا ماشین حالت برایتان تازه است، از راهنمای یادگیری سیستمهای نهفته شروع کنید.
خرید برد و وسایل تمرین
یک Uno R3، کلید لحظهای معمولاً باز، دو سیم جامپر کوتاه و کابل USB دیتادار مناسب برد لازم است. از LED روی برد استفاده میکنیم؛ LED خارجی لازم نیست. برای کلید، مدل یا آرایش پایهٔ خاصی فرض نشده است.
- گزینهٔ ایران: صفحهٔ مستقیم Uno CH340 در ECA. این کالا برد سازگار با رابط USB متفاوت معرفی شده و همان Uno R3 اصلی آردوینو نیست. در بررسی ۱۹ اکتبر ۲۰۲۶، گزینهٔ افزودن به سبد در صفحه دیده شد. نوشتهٔ دقیق روی MCU، پکیج قطعات، موجودی واقعی و همارزی الکتریکی برد تحویلی مستقلاً تأیید نشدهاند.
- برد مرجع خارجی: صفحهٔ مستقیم Arduino Uno Rev3 با شناسهٔ A000066 در فروشگاه سازنده. این لینک مربوط به برد کامل است، نه تراشهٔ ATmega328P تنها.
وجود دکمهٔ خرید تضمین موجودی یا اصالت نیست و وضعیت پس از تاریخ بررسی ممکن است تغییر کند. قیمت برای این تمرین ضروری نیست و درج نشده است. برد، کلید و کابل موجود خودتان نیز در صورت تطابق قابل استفادهاند.

۱. اول ورودی و پولآپ را بشناسیم
پایهٔ GPIO در حالت ورودی، سطح ولتاژ را میخواند. اگر یک کلید باز را بدون مسیر تعیین سطح به آن وصل کنیم، سطح قابل اتکایی نداریم. در هستهٔ AVR آردوینو، دستور INPUT_PULLUP جهت پایه را ورودی و پولآپ داخلی پورت را فعال میکند. با کلید باز، این مسیر ورودی را به سمت تغذیه میکشد. با بستهشدن کلید، ورودی به زمین وصل میشود. پس در برنامه مینویسیم rawPressed = digitalRead(2) == LOW: مقدار درستِ این متغیر یعنی کلید فشرده است، با اینکه سطح الکتریکی پایین است. به این قرارداد «فعال با سطح پایین» یا active-low میگوییم.
پولآپ وضعیت کلید رها را تعیین میکند؛ وظیفهاش حذف جهش تماس نیست. تماس مکانیکی ممکن است هنگام یک فشار یا رهاکردن، چند بار قطع و وصل شود. «سطح» یعنی وضعیت فعلی پذیرفتهشده؛ «لبه» یعنی تغییر بین دو وضعیت پذیرفتهشده. اگر در هر دور حلقه که LOW میخوانیم شمارنده را زیاد کنیم، یک فشار طولانی صدها بار شمرده میشود، حتی اگر هیچ جهشی وجود نداشته باشد.
۲. سیمکشی دو اتصال الکتریکی کلید
- برای سیمکشی USB را جدا کنید. یک اتصال کلید را به هدر شمارهٔ ۲ و اتصال دیگر را به هدر GND ببندید.
- کلید این تمرین را به 5V وصل نکنید؛ کلید فقط D2 را به زمین متصل میکند. D2 در برنامه باید ورودی با پولآپ بماند.
- اگر کلید چهارپایه دارید، زوجهای مشترک داخلی را از نقشهٔ همان کلید یا تست پیوستگی در حالت بیبرق پیدا کنید. شکل این مقاله دو اتصال الکتریکی را نشان میدهد، نه جهت نصب یک کلید چهارپایهٔ خاص.
- شمارهٔ روی هدر برد را بخوانید. D2 روی Uno به PD2 میکروکنترلر نگاشت شده و با پایهٔ شمارهٔ ۲ پکیج DIP یکی نیست. LED داخلی در نگاشت Uno مرجع روی D13 است.
۳. زمانها را با فرض روشن انتخاب کنیم
در این نمونه ۳۰ میلیثانیه ثبات در نمونههای خواندهشده و ۲۰۰ میلیثانیه مهلت یک دورهٔ ناآرام داریم. این اعداد، انتخاب آموزشیاند؛ جهش یک کلید واقعی اندازهگیری نشده و برای همهٔ کلیدها مشخصات تضمینی محسوب نمیشوند. هر تغییر مشاهدهشده، زمان ثبات ۳۰ میلیثانیه را از نو شروع میکند، اما مهلت ۲۰۰ میلیثانیه را عقب نمیاندازد.
نمونهبرداری در loop() و بدون delay() انجام میشود؛ بقیهٔ کارهای برنامه هم باید فرصت نمونهبرداری منظم بدهند. فرض کنید بیشترین فاصلهٔ دو خواندن ۵ میلیثانیه باشد. مشاهدهٔ اولین تغییر واقعی میتواند تا ۵ میلیثانیه دیر شود و فراخوانی تأیید نیز تا ۵ میلیثانیه بعد از پایان ۳۰ میلیثانیه برسد. پس برای یک تغییر که بعد از آن ثابت میماند، تأخیر تقریبی ۳۰ تا ۴۰ میلیثانیه به دست میآید. این محاسبهٔ بودجهٔ نمونهبرداری است، نه نتیجهٔ اندازهگیری روی Uno.
تغییرهایی که بین دو خواندن رخ دهند و برگردند دیده نمیشوند. چاپ سریال، محاسبهٔ طولانی یا کد مسدودکننده میتواند فرض فاصلهٔ ۵ میلیثانیه را نقض کند. پالسی که نامزد آن فقط ۲۹ میلیثانیه در نمونهها دوام بیاورد عمداً حذف میشود. این رفتار برای کلید انسانی مفید است و برای شمارندهٔ پالس سریع مناسب نیست. زمان بیشتر یعنی پاسخ کندتر در برابر فیلتر بیشتر. سیم بلند و محیط نویزی ممکن است به بایاس خارجی، حفاظت ورودی یا فیلتر سختافزاری با طراحی جداگانه نیاز داشته باشد؛ نرمافزار ورودی خارج از محدودهٔ مجاز را ایمن نمیکند.
۴. ماشین حالت چه تصمیمی میگیرد؟
| حالت | معنی و شرط خروج |
|---|---|
Startup | هنوز مبنا نداریم. پس از ۳۰ میلیثانیه ثبات نامزد، رویداد Ready صادر و مبنا پذیرفته میشود؛ لبهای ساخته نمیشود. |
StableReleased / StablePressed | سطح پذیرفتهشده حفظ میشود. خواندن مخالف، VerifyPress یا VerifyRelease و مهلت ثابت دوره را شروع میکند. |
VerifyPress / VerifyRelease | هر تغییر خام زمان نامزد را نو میکند. بعد از ۳۰ میلیثانیه ثبات، تغییر واقعی فقط یک بار پذیرفته میشود؛ برگشت به مبنای قبلی بدون رویداد است. |
Fault | در رسیدن دورهٔ ناآرام به ۲۰۰ میلیثانیه، UnstableFault یک بار صادر و اعتبار خروجی قطع میشود. پس از یک بازهٔ تازهٔ ۳۰ میلیثانیه ثبات از ورود به خطا، Recovered صادر و مبنا بدون ساختن لبه پذیرفته میشود. |
اگر در یک فراخوانی، هم مهلت تمام شده باشد و هم زمان ثبات کافی باشد، خطا اولویت دارد. بررسی مهلت در فراخوانی بعدی update() انجام میشود؛ وقفهای نداریم که دقیقاً در لحظهٔ ۲۰۰ میلیثانیه اجرا شود. دیرشدن حلقه هم میتواند خطا بدهد، حتی اگر جهش مشاهده نشده باشد. در حالت بررسی، سطح پذیرفتهشدهٔ قبلی معتبر میماند؛ این اعتبار تضمین سطح خام فعلی نیست. در Startup و Fault، استفادهکننده باید پیش از عمل بر اساس stablePressed()، مقدار valid() را بررسی کند.
اگر کلید هنگام روشنشدن نگه داشته شود، Ready یک مبنای «فشرده» میسازد و LED روشن میشود، اما شمارنده صفر میماند. بعد از بازیابی از خطا هم لبههای گمشده را حدس نمیزنیم. این یک سیاست صریح برای رویدادهاست و به معنی ادعای رها بودن کلید در زمان خطا نیست.
۵. دانلود و کامپایل همان کد
دانلود ZIP سورس تمرین شامل BoundedButton.ino، فایل BoundedDebounce.h، آزمون بومی و راهنمای فارسی و انگلیسی است. فایل هدر را کنار اسکچ، داخل پوشهٔ BoundedButton نگه دارید. در IDE برد Arduino Uno را انتخاب کنید یا از پوشهٔ code این فرمان را اجرا کنید:
arduino-cli compile --fqbn arduino:avr:uno --warnings all BoundedButton
پیامهای لبهٔ پذیرفتهشده با نرخ 115200 baud چاپ میشوند. LED روی برد نشانگر وضعیت است. فشار نگهداشتهشده یک رویداد فشار میدهد و رهاشدن تأییدشده یک رویداد رهاشدن. شمارندهٔ بدون علامت پس از ۲ به توان ۳۲ فشار پذیرفتهشده سرریز میکند و بعد از ریست حفظ نمیشود.
دو فایل کامل؛ رکورد آزمون در TEST-RESULTS.json داخل دانلود است.
BoundedDebounce.h
// SPDX-License-Identifier: MIT
// LCENJ educational example. Sampled input; not a safety-rated controller.
#ifndef LCENJ_BOUNDED_DEBOUNCE_H
#define LCENJ_BOUNDED_DEBOUNCE_H
#include <stdint.h>
class BoundedDebounce {
public:
enum State { Startup, StableReleased, VerifyPress, StablePressed,
VerifyRelease, Fault };
enum Event { None, Ready, Pressed, Released, UnstableFault, Recovered };
bool begin(bool rawPressed, uint32_t now, uint32_t debounceMs = 30,
uint32_t deadlineMs = 200) {
configured_ = debounceMs > 0 && deadlineMs > debounceMs &&
deadlineMs < UINT32_C(0x80000000);
debounceMs_ = debounceMs;
deadlineMs_ = deadlineMs;
candidate_ = rawPressed;
stable_ = false;
valid_ = false;
candidateSince_ = episodeSince_ = now;
state_ = Startup;
return configured_;
}
Event update(bool rawPressed, uint32_t now) {
if (!configured_) return None;
if (state_ == StableReleased || state_ == StablePressed) {
if (rawPressed == stable_) return None;
candidate_ = rawPressed;
candidateSince_ = episodeSince_ = now;
state_ = stable_ ? VerifyRelease : VerifyPress;
return None;
}
if (rawPressed != candidate_) {
candidate_ = rawPressed;
candidateSince_ = now;
}
// An unsettled episode has a fixed deadline, not extended by chatter.
// Deadline has priority if both conditions are observed on the same call.
if (state_ != Fault && elapsed(now, episodeSince_) >= deadlineMs_) {
state_ = Fault;
valid_ = false;
candidateSince_ = now; // Recovery requires a fresh settling window.
return UnstableFault;
}
if (elapsed(now, candidateSince_) < debounceMs_) return None;
const State previousState = state_;
const bool changed = candidate_ != stable_;
stable_ = candidate_;
valid_ = true;
state_ = stable_ ? StablePressed : StableReleased;
if (previousState == Startup) return Ready;
if (previousState == Fault) return Recovered;
if (!changed) return None;
return stable_ ? Pressed : Released;
}
bool valid() const { return configured_ && valid_; }
bool stablePressed() const { return stable_; }
State state() const { return state_; }
private:
static uint32_t elapsed(uint32_t now, uint32_t since) {
return uint32_t(now - since);
}
bool configured_ = false, candidate_ = false, stable_ = false, valid_ = false;
uint32_t debounceMs_ = 30, deadlineMs_ = 200;
uint32_t candidateSince_ = 0, episodeSince_ = 0;
State state_ = Startup;
};
#endif
BoundedButton.ino
// SPDX-License-Identifier: MIT
// Target: Arduino Uno R3 / arduino:avr:uno (ATmega328P, 16 MHz).
// Normally-open switch: board D2 -> switch -> GND. No external supply.
#include "BoundedDebounce.h"
const uint8_t BUTTON_PIN = 2;
BoundedDebounce button;
uint32_t pressCount = 0;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
Serial.begin(115200);
button.begin(digitalRead(BUTTON_PIN) == LOW, uint32_t(millis()), 30, 200);
}
void loop() {
const uint32_t now = uint32_t(millis());
const BoundedDebounce::Event event =
button.update(digitalRead(BUTTON_PIN) == LOW, now);
// State indicator: held at boot lights LED after Ready, but counts no press.
digitalWrite(LED_BUILTIN,
button.valid() && button.stablePressed() ? HIGH : LOW);
if (event == BoundedDebounce::Pressed) {
++pressCount; // Unsigned counter wraps after 2^32 accepted presses.
Serial.print(F("pressed count="));
Serial.println(pressCount);
} else if (event == BoundedDebounce::Released) {
Serial.println(F("released"));
} else if (event == BoundedDebounce::Ready) {
Serial.println(F("ready: baseline only"));
} else if (event == BoundedDebounce::UnstableFault) {
Serial.println(F("fault: input unsettled or polling delayed"));
} else if (event == BoundedDebounce::Recovered) {
Serial.println(F("recovered: baseline only"));
}
// No delay(). Serial logging can block; this is not a hard real-time loop.
}
۶. چرا تفاضل زمان در سرریز هم کار میکند؟
تابع زمان سپریشده از uint32_t(now - since) استفاده میکند. مثلاً اگر زمان شروع 4,294,967,280 باشد و ساعت پس از سرریز 14 را نشان بدهد، نتیجهٔ بدون علامت ۳۰ میلیثانیه است. مقایسهٔ مستقیم now >= since + 30 در مرز سرریز قابل اتکا نیست. طول چرخهٔ ساعت میلیثانیهای ۳۲بیتی تقریباً ۴۹٫۷۱ روز است: 2^32 / (1000 × 60 × 60 × 24).
هدر مقدار صفر برای debounce، مهلت کوچکتر یا مساوی debounce و مهلت حداقل ۲ به توان ۳۱ میلیثانیه را رد میکند. فراخوانیها باید به ترتیب زمانی و با فاصلهای بسیار کمتر از این نیمچرخه انجام شوند؛ از دستدادن یک چرخهٔ کامل ساعت بدون مشاهده قابل بازسازی نیست. آزمون بومی، حساب پیمانهای را میسنجد و اندازهگیری دقت نوسانساز یا جایگزین پیادهسازی تایمر AVR نیست.
۷. چه چیزی آزموده شده و چه چیزی باقی است؟
همان هدر دانلودی، هم در کامپایل Uno و هم در آزمون اجرایی رایانه استفاده شده است. آزمونها یک دنبالهٔ جهش با رویدادهای مورد انتظارِ ازپیشنوشتهشده، مرزهای دقیق ۲۹ و ۳۰ میلیثانیه، نگهداشتن هنگام راهاندازی، فشار طولانی، رهاکردن و فشار دوباره، پالس کوتاه، مهلت جهش، بازیابی، تنظیم نامعتبر و سرریز ساعت را پوشش میدهند. همچنین هزار انتقال قطعی همان دنباله روی ساعت ۳۲بیتی، با نمونههای تکراری همزمان در بخشی از اجراها، بررسی میکند که این تغییرها رویداد تازه نسازند.
در ۱۸ اکتبر ۲۰۲۶، هر ۱۲ گروه آزمون بومی با ۲۹٬۶۷۹ بررسی شرط موفق شدند. کامپایل واقعی AVR برای Uno با Arduino CLI نسخهٔ 1.5.1 و هستهٔ Arduino AVR نسخهٔ 1.8.8 موفق بود: ۲٬۹۸۶ بایت فلش و ۲۱۴ بایت RAM سراسری. RAM سراسری، اندازهگیری بیشترین مصرف پشته نیست. هسته در اولین ساخت بدون کش، هشدار پارامتر استفادهنشده داشت. کامپایل بومی با Zig نسخهٔ 0.14.1 در حالت C++11 انجام شد. رکورد آزمون دانلودی، هش سورس و فرمانها را مشخص میکند. این شواهد، سازگاری سورس و رفتار منطقی محدود به ورودیهای آزمون را نشان میدهند. آزمایش با کلید فیزیکی، ثبت شکل موج، آزمون نویز الکتریکی، آپلود روی برد و اندازهگیری زمان واقعی برای این مقاله انجام نشده است.
- روی برد خود ابتدا HIGH بودن حالت رها و LOW بودن فشار را با سیمکشی مورد نظر بررسی کنید.
- فشار و رهاکردن عمدی انجام دهید: هر فشار تأییدشده یک افزایش، نگهداشتن بدون تکرار. با کلید نگهداشتهشده ریست کنید و سیاست مبنا را ببینید.
- بیشترین فاصلهٔ حلقه را زیر بار واقعی برنامه اندازه بگیرید. پیش از تعیین زمان محصول، جهش تماس کلید خود را با ابزار مناسب مشاهده کنید.
- خرابی سیم و نویز محیط را جداگانه بررسی کنید. اتصال دائم به زمین شبیه کلید نگهداشتهشده و ورودی دائم بالا شبیه کلید رهاست؛ debounce بهتنهایی هیچکدام را تشخیص نمیدهد.
خطاهای رایج و تمرین بعدی
ورودی شناور معمولاً مسئلهٔ بایاس یا سیمکشی است؛ شمارش تکراری در نگهداشتن، اشتباه گرفتن سطح با لبه است. اتصال دو پایهٔ مشترک داخلی کلید چهارپایه، مستقل از فشار یک اتصال دائم میسازد. تأخیرهای طولانی هم میتوانند فشار را از دست بدهند یا خطای دیرشدن حلقه بسازند. پیش از تغییر زمان debounce، سطح الکتریکی را تشخیص دهید.
برای تمرین بعد، یک گزارش دورهای بدون توقف حلقه اضافه کنید و هدر کلید را تغییر ندهید؛ سپس اندازه بگیرید آیا فرض فاصلهٔ نمونهبرداری هنوز برقرار است. آموزش زمانبندی با millis اجرای کار دورهای را توضیح میدهد؛ تمرین حاضر تأیید ورودی و سیاست رویدادهای آن را اضافه میکند. راهنمای سیستمهای نهفته این مسیر را به ماشین حالت وصل میکند و راهنمای الکترونیک به بایاس، مسیر جریان و اندازهگیری میپردازد. رله یا بار برق شهر را وارد این تمرین اولیهٔ GPIO نکنید.
منابع فنی
- دیتاشیت برد Arduino Uno R3، شناسهٔ A000066 و پینآوت رسمی Uno Rev3.
- دیتاشیت سازندهٔ ATmega328P میزبانیشده در Arduino، بخش تنظیم پورت ورودی/خروجی. مشخصات MCU را با مشخصات هر برد سازگار یکی ندانید.
- کد pinMode و digitalRead در هستهٔ AVR نسخهٔ 1.8.8، نگاشت پایههای Uno و پیادهسازی millis.
- نمونهٔ رسمی InputPullupSerial آردوینو و نمونهٔ رسمی Debounce. هدر دارای مهلت و آزمونهای این مقاله، کد آموزشی تألیفیاند.
پرسش و پاسخ مقاله
سوال فنی خود را مطرح کنید
پرسشها و پاسخها پس از بررسی مدیر منتشر میشوند. نویسنده، پیشنویس در انتظار بررسی را میبیند.
هنوز پرسش تاییدشدهای برای این مقاله ثبت نشده است.