Debounce Algorithm là gì?
Tại sao phím cơ học bounce và cách xử lý bằng counter-based debounce, timestamp debounce, scan buffer và state machine trong firmware.
Phím cơ học bounce: khi nhấn, tiếp điểm đóng mở nhiều lần trong vài đến vài chục ms trước khi ổn định. Điều khó chịu của bounce là nó không sai theo kiểu ổn định — có phím bounce 2 ms, có phím 10 ms, và cùng một phím cũng có thể khác nhau theo lực nhấn.
Debounce là bước lọc bắt buộc giữa matrix scan và HID report. Không có debounce, bàn phím gửi ký tự lặp ngẫu nhiên và không dùng được.
1. Bounce trông như thế nào
Nhấn phím tại t=0ms:
GPIO ───┐ ┌─┐ ┌──┐ ┌────────────── HIGH (nhả)
└──┘ └─┘ └──┘
0 2 4 6 8 10ms
Không debounce: PRESS, RELEASE, PRESS, RELEASE, PRESS (5 event)
Có debounce: PRESS (1 event sau khi tín hiệu ổn định)
2. Counter-based debounce
Cách đơn giản và phổ biến nhất: đếm số lần scan liên tiếp cùng trạng thái trước khi chấp nhận.
#define DEBOUNCE_COUNT 5 /* 5 scan × 1ms/scan = 5ms debounce */typedef struct { uint8_t stable; uint8_t counter; uint8_t raw_prev;} DebounceState_t;void Debounce_Update(DebounceState_t *d, uint8_t raw){ if (raw == d->raw_prev) { if (d->counter < DEBOUNCE_COUNT) d->counter++; if (d->counter == DEBOUNCE_COUNT) d->stable = raw; } else { d->counter = 0; d->raw_prev = raw; }}DEBOUNCE_COUNT = 5 với scan period 1 ms → debounce window 5 ms. Tăng lên 10–20 nếu switch bounce nhiều hơn.
3. Timestamp debounce
Thay vì đếm scan, dùng timestamp để đo thời gian ổn định. Tham số now_ms được truyền vào từ bên ngoài để giữ hàm portable:
#define DEBOUNCE_MS 10typedef struct { uint8_t stable; uint8_t raw_prev; uint32_t change_tick;} DebounceTs_t;void DebounceTs_Update(DebounceTs_t *d, uint8_t raw, uint32_t now_ms){ if (raw != d->raw_prev) { d->raw_prev = raw; d->change_tick = now_ms; } if ((now_ms - d->change_tick) >= DEBOUNCE_MS) { d->stable = raw; }}Trên STM32, now_ms có thể lấy từ HAL_GetTick(). Trên RTOS hoặc platform khác, nó có thể đến từ system tick riêng.
| Tiêu chí | Counter | Timestamp |
|---|---|---|
| Độ phức tạp | Đơn giản | Phức tạp hơn một chút |
| Debounce window | Phụ thuộc scan period | Cố định theo ms, không đổi |
| Scan period thay đổi | Window bị thay đổi theo | Không ảnh hưởng |
| RAM | 1 byte counter/phím | 4 byte tick/phím |
| Phù hợp với | Scan period cố định | Scan period không đều |
4. Scan-buffer debounce
Một cách khác là giữ nhiều raw scan gần nhất rồi xác nhận trạng thái ổn định bằng phép AND/OR trên các buffer. Cách này hợp với keyboard matrix vì mỗi lần scan thường tạo ra một bitmap phím.
Ví dụ với scan period 5 ms và 4 buffer:
scan[0] = newest
scan[1] = previous
scan[2] = older
scan[3] = oldest
Normal path có thể dùng 2 buffer mới nhất:
stable_on = scan[0] & scan[1] /* phím được xác nhận nhấn */
stable_off = ~(scan[0] | scan[1]) /* phím được xác nhận nhả */
Recovery path hoặc trạng thái nghi ngờ có thể dùng cả 4 buffer để chắc hơn.
5. Tích hợp vào scan loop
uint8_t key_stable[NUM_ROWS][NUM_COLS] = {0};uint8_t key_prev[NUM_ROWS][NUM_COLS] = {0};uint8_t debounce_cnt[NUM_ROWS][NUM_COLS] = {0};uint8_t raw_prev[NUM_ROWS][NUM_COLS] = {0};void Matrix_ScanAndDebounce(void){ Matrix_Scan(); /* cập nhật matrix_raw[r][c] */ for (uint8_t r = 0; r < NUM_ROWS; r++) { for (uint8_t c = 0; c < NUM_COLS; c++) { uint8_t raw = matrix_raw[r][c]; if (raw == raw_prev[r][c]) { if (debounce_cnt[r][c] < DEBOUNCE_COUNT) debounce_cnt[r][c]++; if (debounce_cnt[r][c] == DEBOUNCE_COUNT) key_stable[r][c] = raw; } else { debounce_cnt[r][c] = 0; raw_prev[r][c] = raw; } if (!key_prev[r][c] && key_stable[r][c]) OnKeyPress(r, c); if ( key_prev[r][c] && !key_stable[r][c]) OnKeyRelease(r, c); key_prev[r][c] = key_stable[r][c]; } }}6. Release debounce
Debounce cần áp dụng cho cả press lẫn release. Counter-based ở trên đã xử lý cả hai — counter reset mỗi khi raw state thay đổi dù là 0→1 hay 1→0.
Một số firmware dùng debounce window khác nhau cho press và release để tối ưu cảm giác gõ. Tuy nhiên điều này phụ thuộc loại switch và yêu cầu sản phẩm. Nếu chưa đo bounce thực tế, dùng cùng window cho press và release là lựa chọn an toàn hơn.
7. Key repeat
Sau khi đã lọc được press/release sạch, vẫn còn một câu hỏi khác: nếu người dùng giữ phím thì firmware nên làm gì?
Debounce xử lý bounce nhưng không xử lý key repeat. Key repeat là logic riêng ở tầng trên, dùng counter đếm thời gian giữ phím sau khi đã debounce xong.
Debounce → lọc bounce vật lý, output: press/release event
Key Repeat → đếm thời gian giữ, output: repeat event mỗi N ms
Bài liên quan
- Keyboard Matrix Fundamentals là gì?
- HID Keyboard Report Format là gì?
- Ring Buffer trong Embedded C là gì?
Nguồn tham khảo public
- Embedded.fm: Debouncing — Jack Ganssle
- STM32G0B1RE Reference Manual, Chapter: SysTick timer
Bài viết này hữu ích với bạn?
Chia sẻ, góp ý, hoặc ủng hộ nếu bạn thấy nội dung này có giá trị.
Nội dung liên quan
Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.
Thiết kế input pipeline trước tầng USB
Giải thích thiết kế tạo input từ keypad qua các tầng scan, debounce, key policy, event queue và những đánh đổi trước khi tạo HID report.
STM32G0 USB Lab: Key Input Pipeline
Keypad 4x4 matrix trên STM32G0B1RE: quét bằng BSRR/IDR trực tiếp, TIM6 5ms, debounce 2 mẫu, event queue và chính sách simultaneous-error cho ma trận không diode.
STM32 GPIO Register Access là gì?
Truy cập GPIO bằng BSRR và IDR thay vì HAL_GPIO_WritePin/ReadPin: tại sao, khi nào phù hợp, và cách áp dụng cho matrix scan row-by-row trên STM32.
Biến note thành bài viết hoàn chỉnh
Notes là nơi ghi nhanh khái niệm.