یک تغییر منطقیِ کلید همیشه به معنی یک حرکت مکانیکیِ مستقل نیست. ممکن است پیش از پایدارشدن تماس، چند تغییر دیده شود. برای آزمودن منطق نرم‌افزار، لازم نیست رفتار فیزیکی کلید را جعل کنیم: دنباله‌ای ساختگی از سطح‌های دودویی می‌دهیم و دقیقاً می‌گوییم چه زمانی تغییر تأیید می‌شود. این درس، یک پیاده‌سازی رویدادمحور و مرجع مستقل دارد؛ کلید واقعی، وقفه، ساعت سیستم یا کنترل‌گر صنعتی اجرا نشده است.

۱. ورودی خام، نامزد و حالت تأییدشده سه مفهوم‌اند

دو حالت خام و تأییدشده در مدل رویدادمحورِ ورودی دودویی
تصویر مفهومی: دو حالت خام و تأییدشده در مدل رویدادمحورِ ورودی دودویی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

یادداشت Debounce a Switch از TI اصل جهشِ تماس و احتمال تحریک‌های ناخواسته را توضیح می‌دهد. از آن فقط برای بیان مسئله استفاده می‌کنیم؛ مدار RC، قطعهٔ پیشنهادی یا مقدار زمانیِ سازنده در این برنامه پیاده نمی‌شود. زمان سه‌میلی‌ثانیه‌ایِ مثال، قرارداد آموزشیِ خودمان است و برای یک کلید واقعی توصیه نشده است.

ورودی، مسیر کامل و قطعه‌ای‌ثابتِ سطح صفر یا یک است. هر رکورد، زمان صحیحِ میلی‌ثانیه و سطح جدید را می‌دهد؛ آن سطح تا رکورد بعدی باقی می‌ماند. سطح اولیه پیش از نخستین رکورد معلوم است. این مدل، فهرستِ کاملِ تغییرات را فرض می‌کند؛ نبودن رکورد در فاصلهٔ بین دو زمان، به معنی ناآگاهی از یک کلید واقعی یا نمونه‌برداریِ ناقص نیست.

raw سطح جاریِ مسیر و confirmed حالتِ پذیرفته‌شده‌اند. اگر raw با confirmed مخالف شود، آن را نامزد می‌کنیم؛ خروجی بلافاصله تغییر نمی‌کند. نامزد، زمان آغاز و مهلت دارد. بازگشتِ raw به confirmed، نامزد را لغو می‌کند. وجود نامزدِ یک در کنار confirmed صفر تناقض نیست؛ دقیقاً نشان می‌دهد تغییر هنوز از سیاست پایداری نگذشته است.

۲. شرط پایداری و رکورد تکراری را دقیق تعریف کنیم

نامزد تغییر حالت در انتظار گذشت یک بازهٔ پایداری قراردادی
تصویر مفهومی: نامزد تغییر حالت در انتظار گذشت یک بازهٔ پایداری قراردادی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

اگر نامزد در زمان s شروع شود و مدت قراردادی T باشد، مهلت s+T است. سیاست ما سطح نامزد را در تمام بازهٔ بستهٔ s تا s+T، شامل خودِ انتها، می‌خواهد. پس تنها گذشتن عددِ T کافی نیست؛ ورودی در لحظهٔ مهلت نیز باید همان سطح باشد. این قاعده، انتخابِ مدل آفلاین ماست و تعریف همگانیِ تمام debounceهای نرم‌افزاری یا سخت‌افزاری نیست.

رکوردی که همان سطح raw قبلی را تکرار کند، آغاز نامزد را تغییر نمی‌دهد. برای مثال، اگر در صفر سطح یک برسد و در دو میلی‌ثانیه دوباره یک ثبت شود، با T برابر سه، مهلت همچنان سه است. بازنشانیِ انتظار در هر رکورد، سیاست دیگری می‌سازد و می‌تواند نامزد را با گزارش‌های تکراری بی‌دلیل عقب بیندازد؛ این رفتار را نباید پنهانی وارد پیاده‌سازی کرد.

زمانِ رکوردهای ورودی باید اکیداً افزایشی باشد؛ دو رکورد خام در یک زمان پذیرفته نیستند. اما یک رکورد خام می‌تواند با مهلتِ داخلی هم‌زمان شود. این دو نوع هم‌زمانی متفاوت‌اند. مدت T بین یک و هزار میلی‌ثانیه و افق پایان بین صفر و ده‌هزار است؛ حداکثر ۲۰۴۸ رکورد می‌پذیریم. کران‌ها دامنهٔ مثال‌اند، نه ظرفیت پردازشی یا زمان‌بندیِ تضمین‌شده.

۳. رویدادِ روی مهلت را پیش از تأیید پردازش کنیم

تقدم تغییر ورودی بر تأیید هم‌زمان در یک گرهٔ تصمیم واحد
تصویر مفهومی: تقدم تغییر ورودی بر تأیید هم‌زمان در یک گرهٔ تصمیم واحد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فرض کنید در صفر سطح یک و دقیقاً در سه سطح صفر برسد. در قرارداد T برابر سه، رویدادِ سه ابتدا اجرا می‌شود و نامزد لغو می‌گردد؛ تغییرِ تأییدشده‌ای نداریم. اگر رویدادِ سه فقط یکِ تکراری باشد، نامزد باقی می‌ماند و همان‌جا تأیید می‌شود. تقدم، به نوع رویداد و برابری زمان وابسته است؛ ترتیبِ تصادفیِ ورود به یک صف را ملاک نمی‌گذاریم.

اگر مهلت از زمانِ رویداد بعدی کوچک‌تر باشد، تأیید را در خودِ مهلت ثبت می‌کنیم و سپس رویداد بعدی را می‌خوانیم. بعد از آخرین رکورد نیز افق پایان را بررسی می‌کنیم: مهلتِ مساوی پایان پذیرفته است؛ مهلتِ بعد از پایان، نامزدِ ناتمام می‌ماند. برگرداندنِ این وضعیت مفیدتر از اعلامِ ساختگیِ تأیید یا دورریختنِ بی‌توضیحِ انتظار است.

"""Offline debounce on a completely supplied piecewise-constant binary trace."""
import json
import platform
from itertools import product


def validate(edges, stable_ms, end_ms, initial, unit):
    if unit != "ms":
        raise ValueError("time unit must be exactly ms")
    if type(initial) is not int or initial not in (0, 1):
        raise ValueError("initial must be exact binary int")
    if type(stable_ms) is not int or not 1 <= stable_ms <= 1000:
        raise ValueError("stable_ms must be exact int in [1,1000]")
    if type(end_ms) is not int or not 0 <= end_ms <= 10000:
        raise ValueError("end_ms must be exact int in [0,10000]")
    if type(edges) not in (list, tuple) or len(edges) > 2048:
        raise ValueError("bounded list or tuple of events required")
    copied = []
    previous = -1
    for event in edges:
        if type(event) not in (tuple, list) or len(event) != 2:
            raise ValueError("event must be timestamp/level pair")
        at, level = event
        if type(at) is not int or not previous < at <= end_ms:
            raise ValueError("event times must increase strictly within horizon")
        if type(level) is not int or level not in (0, 1):
            raise ValueError("level must be exact binary int")
        copied.append((at, level))
        previous = at
    return copied


def debounce(edges, stable_ms, end_ms, *, initial=0, unit="ms"):
    events = validate(edges, stable_ms, end_ms, initial, unit)
    raw = confirmed = initial
    pending = None  # (candidate_level, start_ms, deadline_ms)
    changes = []
    for at, level in events:
        if pending is not None and pending[2] < at:
            confirmed = pending[0]
            changes.append((pending[2], confirmed))
            pending = None
        # Raw event first when an event and a confirmation deadline coincide.
        if level != raw:
            raw = level
            pending = None if raw == confirmed else (raw, at, at + stable_ms)
        # Repeated raw levels did not reset the start or deadline.
        if pending is not None and pending[2] == at:
            confirmed = pending[0]
            changes.append((at, confirmed))
            pending = None
    if pending is not None and pending[2] <= end_ms:
        confirmed = pending[0]
        changes.append((pending[2], confirmed))
        pending = None
    return {"changes": changes, "confirmed": confirmed, "raw": raw,
            "pending": None if pending is None else {
                "level": pending[0], "start_ms": pending[1], "deadline_ms": pending[2]}}


def dense_reference(edges, stable_ms, end_ms, initial):
    # Independent small-integer oracle: expand every millisecond, then look back.
    by_time = dict(edges)
    dense = []
    level = initial
    for at in range(end_ms + 1):
        if at in by_time:
            level = by_time[at]
        dense.append(level)
    confirmed = initial
    changes = []
    for at in range(stable_ms, end_ms + 1):
        window = dense[at - stable_ms:at + 1]
        if window[0] != confirmed and all(v == window[0] for v in window):
            confirmed = window[0]
            changes.append((at, confirmed))
    return changes, confirmed


def rejects(edges, stable_ms=3, end_ms=20, **kwargs):
    try:
        debounce(edges, stable_ms, end_ms, **kwargs)
    except (ValueError, TypeError):
        return True
    return False


def run_example():
    checks = {}
    def check(name, condition):
        checks[name] = bool(condition)
        if not checks[name]:
            raise AssertionError(name)

    trace = [(1, 1), (2, 0), (4, 1), (6, 1), (7, 0), (8, 1), (12, 0)]
    hand = debounce(trace, 3, 16)
    check("hand_bounce_trace_two_confirmations", hand == {"changes": [(11, 1), (15, 0)], "confirmed": 0, "raw": 0, "pending": None})
    check("no_events_preserves_both_initial_states", all(debounce([], 3, 10, initial=i) == {"changes": [], "confirmed": i, "raw": i, "pending": None} for i in (0, 1)))
    check("single_edge_confirms_at_deadline", debounce([(2, 1)], 3, 5)["changes"] == [(5, 1)])
    check("horizon_before_deadline_retains_candidate", debounce([(2, 1)], 3, 4) == {"changes": [], "confirmed": 0, "raw": 1, "pending": {"level": 1, "start_ms": 2, "deadline_ms": 5}})
    check("raw_reversal_at_deadline_cancels_confirmation", debounce([(0, 1), (3, 0)], 3, 10)["changes"] == [])
    check("repeated_level_at_deadline_does_not_cancel", debounce([(0, 1), (3, 1)], 3, 3)["changes"] == [(3, 1)])
    check("earlier_repeated_level_does_not_restart_wait", debounce([(0, 1), (2, 1)], 3, 3)["changes"] == [(3, 1)])
    check("earlier_return_cancels_candidate", debounce([(0, 1), (2, 0)], 3, 10)["changes"] == [])
    check("deadline_before_next_edge_commits_first", debounce([(0, 1), (4, 0)], 3, 7)["changes"] == [(3, 1), (7, 0)])
    check("initial_one_falling_transition", debounce([(1, 0)], 3, 4, initial=1)["changes"] == [(4, 0)])
    check("input_container_not_mutated", (lambda copy: debounce(trace, 3, 16) is not None and trace == copy)(list(trace)))
    cases = 0
    for initial in (0, 1):
        for stable in (1, 2, 3):
            for values in product((0, 1), repeat=6):
                events = list(enumerate(values))
                actual = debounce(events, stable, 8, initial=initial)
                expected_changes, expected_final = dense_reference(events, stable, 8, initial)
                if actual["changes"] != expected_changes or actual["confirmed"] != expected_final:
                    raise AssertionError("dense oracle")
                cases += 1
    check("independent_dense_oracle_384_traces_one_group", cases == 384)
    shifted = [(at + 5, level) for at, level in trace]
    check("common_timestamp_shift_preserves_changes", debounce(shifted, 3, 21)["changes"] == [(at + 5, level) for at, level in hand["changes"]])
    check("duplicate_or_reverse_event_times_rejected", rejects([(1, 1), (1, 0)]) and rejects([(2, 1), (1, 0)]))
    check("invalid_event_shapes_types_levels_rejected", all(rejects(x) for x in [[(1,)], [(1, 1, 2)], [(True, 1)], [(1.0, 1)], [(float("nan"), 1)], [(1, True)], [(1, 2)], [(1, "1")]]))
    check("invalid_initial_window_horizon_rejected", rejects([], initial=True) and rejects([], stable_ms=0) and rejects([], stable_ms=1.5) and rejects([], stable_ms=1001) and rejects([], end_ms=-1) and rejects([], end_ms=float("inf")) and rejects([(3, 1)], end_ms=2))
    check("unit_and_container_bounds_rejected", rejects([], unit="s") and rejects(iter([])) and rejects([(0, 0)] * 2049))
    check("zero_horizon_and_first_time_zero_defined", debounce([(0, 1)], 1, 0)["pending"] == {"level": 1, "start_ms": 0, "deadline_ms": 1} and debounce([(0, 1)], 1, 1)["changes"] == [(1, 1)])
    return {"python_version": platform.python_version(),
            "scope": "offline completely supplied piecewise-constant binary trace; integer ms; raw edge before simultaneous confirmation; no clock, hardware, sleep, interrupts or real-time guarantee",
            "input_domain": {"stable_ms": [1, 1000], "end_ms": [0, 10000], "max_events": 2048, "same_timestamp_events": "rejected", "repeated_level": "accepted without deadline reset"},
            "hand_trace_ms_level": [list(x) for x in trace], "stable_ms": 3, "end_ms": 16,
            "hand_result": {**hand, "changes": [list(x) for x in hand["changes"]]},
            "deadline_boundary": {"reversal_at_3_changes": [], "same_level_at_3_changes": [[3, 1]]},
            "independent_dense_reference_traces_in_one_group": cases, "dense_reference_step_ms": 1,
            "dense_oracle_scope": "these small integer-ms traces only; every intervening integer point and endpoint inspected; not actual sampled hardware or continuous-time observation guarantee",
            "checks": checks}


if __name__ == "__main__":
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

کل ورودی پیش از محاسبه اعتبارسنجی و کپی می‌شود. زمان و سطح باید int دقیق باشند؛ bool، رشته، float، NaN و بی‌نهایت تبدیل نمی‌شوند. واحد دقیقاً ms است و ثانیه با حدسِ اندازهٔ عدد پذیرفته نمی‌شود. تابع وضعیت ماندگاری بیرون از خود تغییر نمی‌دهد؛ ردشدن ورودیِ دیرتر، دربارهٔ تراکنش شبکه یا بازگردانیِ اثر سخت‌افزاری هیچ تضمینی ایجاد نمی‌کند.

۴. جواب دستی و مرجعِ متراکم، بدون ساعت واقعی

مرجع مستقلِ گام‌به‌گام در برابر پردازش رویدادهای پراکنده
تصویر مفهومی: مرجع مستقلِ گام‌به‌گام در برابر پردازش رویدادهای پراکنده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مسیر اصلی در زمان‌های یک، دو، چهار، شش، هفت، هشت و دوازده، به‌ترتیب سطح‌های یک، صفر، یک، یک، صفر، یک و صفر دارد. حالت اولیه صفر، T سه و پایان شانزده است. نامزدِ زمان چهار در هفت لغو می‌شود؛ زمان شش فقط تکرار است. نامزدِ هشت در یازده تأیید و نامزدِ دوازده در پانزده تأیید می‌شود؛ خروجی فقط همین دو گذار است.

{
  "python_version": "3.14.5",
  "scope": "offline completely supplied piecewise-constant binary trace; integer ms; raw edge before simultaneous confirmation; no clock, hardware, sleep, interrupts or real-time guarantee",
  "input_domain": {
    "stable_ms": [
      1,
      1000
    ],
    "end_ms": [
      0,
      10000
    ],
    "max_events": 2048,
    "same_timestamp_events": "rejected",
    "repeated_level": "accepted without deadline reset"
  },
  "hand_trace_ms_level": [
    [
      1,
      1
    ],
    [
      2,
      0
    ],
    [
      4,
      1
    ],
    [
      6,
      1
    ],
    [
      7,
      0
    ],
    [
      8,
      1
    ],
    [
      12,
      0
    ]
  ],
  "stable_ms": 3,
  "end_ms": 16,
  "hand_result": {
    "changes": [
      [
        11,
        1
      ],
      [
        15,
        0
      ]
    ],
    "confirmed": 0,
    "raw": 0,
    "pending": null
  },
  "deadline_boundary": {
    "reversal_at_3_changes": [],
    "same_level_at_3_changes": [
      [
        3,
        1
      ]
    ]
  },
  "independent_dense_reference_traces_in_one_group": 384,
  "dense_reference_step_ms": 1,
  "dense_oracle_scope": "these small integer-ms traces only; every intervening integer point and endpoint inspected; not actual sampled hardware or continuous-time observation guarantee",
  "checks": {
    "hand_bounce_trace_two_confirmations": true,
    "no_events_preserves_both_initial_states": true,
    "single_edge_confirms_at_deadline": true,
    "horizon_before_deadline_retains_candidate": true,
    "raw_reversal_at_deadline_cancels_confirmation": true,
    "repeated_level_at_deadline_does_not_cancel": true,
    "earlier_repeated_level_does_not_restart_wait": true,
    "earlier_return_cancels_candidate": true,
    "deadline_before_next_edge_commits_first": true,
    "initial_one_falling_transition": true,
    "input_container_not_mutated": true,
    "independent_dense_oracle_384_traces_one_group": true,
    "common_timestamp_shift_preserves_changes": true,
    "duplicate_or_reverse_event_times_rejected": true,
    "invalid_event_shapes_types_levels_rejected": true,
    "invalid_initial_window_horizon_rejected": true,
    "unit_and_container_bounds_rejected": true,
    "zero_horizon_and_first_time_zero_defined": true
  }
}

مرجع مستقل ابتدا تمام سطح‌های مسیرِ کوچک را روی نقاط صحیحِ میلی‌ثانیه‌ای باز می‌کند. سپس در هر زمان، پنجرهٔ شامل دو انتهای t−T تا t را نگاه می‌کند؛ فقط سطح ثابتِ مخالفِ confirmed مجاز به گذار است. این مرجع از نامزد و مهلتِ تابع اصلی استفاده نمی‌کند. چون تمام تغییراتِ مدل در زمان‌های صحیح و از پیش داده شده‌اند، نقطه‌ای از مسیرِ مدل میان این گام‌ها پنهان نیست؛ این فرض برای سیگنال واقعی اثبات نشده است.

itertools.product فضای محدودِ دنباله‌های صفر و یکِ شش‌رکوردی را می‌سازد. هر دو حالت اولیه و سه مقدار مدت، مجموعاً ۳۸۴ مسیرِ مرجع در یک گروه از هجده گروه‌اند. پاسخ دستی، تکرار، لغو روی مهلت، افق ناتمام و انتقال مشترکِ زمان هم بررسی می‌شوند. اجرای تازه یا تعداد نقاطِ مرجع، شمار گروه‌های مستقل را افزایش نمی‌دهد.

زمان‌بند عمومی رویداد حسگر و هیسترزیس ولتاژ مسائل دیگری دارند؛ هیسترزیس قبلی اصلاً زمان نداشت. این مدل، ولتاژ، لرزش واقعی، مدت فشار انسان یا ایمنی را تأیید نمی‌کند. کد با Python 3.14.5 اجرا و مستندات 3.14.8 خوانده شد؛ تاریخ دسترسی ۲۵ مهر ۱۴۰۵ و انتشار یادداشت TI اکتبر ۲۰۲۰ است.

۵. چهار پرسش با پاسخ کامل

ورودی تکراری، مرز پایان و جداسازی جهش از هیسترزیس ولتاژی
تصویر مفهومی: ورودی تکراری، مرز پایان و جداسازی جهش از هیسترزیس ولتاژی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: صفر تا سه سطح یک است ولی در سه صفر می‌شود؛ آیا یک تأیید می‌شود؟ پاسخ: خیر؛ انتهای بازه هم باید سطح نامزد باشد. رویدادِ سه نخست پردازش و نامزد لغو می‌شود؛ این تقدمِ صریحِ همین قرارداد است.

پرسش دوم: یکِ تکراری در دو، تأییدِ سه را به پنج می‌برد؟ پاسخ: خیر؛ تغییرِ raw رخ نداده و آغاز نامزد حفظ می‌شود. با نبودِ تغییر مخالف در سه، تأیید در سه باقی می‌ماند.

پرسش سوم: نامزد از دو و پایان چهار با T سه چه خروجی دارد؟ پاسخ: confirmed هنوز صفر و raw یک است؛ نامزد با آغاز دو و مهلت پنج بازمی‌گردد. فقط اگر افق به پنج برسد، می‌تواند در پنج تأیید شود.

پرسش چهارم: تطبیقِ مرجع، کارکرد وقفهٔ واقعی را ثابت می‌کند؟ پاسخ: خیر؛ فقط مسیرِ کاملِ ساختگی و سیاستِ قطعی آزموده شده‌اند. نمونه‌برداری، ساعت، تأخیر اجرای وظیفه، ویژگی تماس و مدار در این آزمایش حضور ندارند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.