Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 47.522 phút đọc

Chống rung nút bấm

Sau bài này bạn sẽ làm được

  • Mô tả hiện tượng rung tiếp điểm và thang thời gian của nó
  • Cài chống rung bằng bộ đếm thời gian không chặn
  • So sánh chống rung phần mềm với mạch lọc phần cứng
  • Phát hiện cạnh lên và cạnh xuống thay vì chỉ đọc mức

Nhấn một cái nút và bộ đếm nhảy sáu đơn vị. Không phải mã sai, mà là tiếp điểm cơ khí thật sự đóng mở nhiều lần trong vài chục mili giây. Đây là bài học đầu tiên về việc thế giới vật lý không sạch sẽ như mô hình.

#Hiện tượng rung tiếp điểm

Rung tiếp điểm
Khi hai miếng kim loại của công tắc chạm nhau, chúng nảy lên xuống vài lần trước khi tiếp xúc ổn định. Mỗi lần nảy tạo ra một cạnh tín hiệu, nên một lần nhấn cho ra hàng chục cạnh.
Một lần nhấn tạo ra sáu cạnh trong khoảng mười lăm mili giây. Tín hiệu sau lọc chỉ đổi mức đúng một lần.
Loại công tắcThời gian rung điển hìnhSố cạnh
Nút bấm nhỏ trên bo mạch1 tới 5 ms2 tới 10
Nút bấm cỡ lớn5 tới 20 ms5 tới 30
Công tắc gạt10 tới 50 ms10 tới 100
Rơ le cơ khí0.5 tới 5 ms3 tới 15
Bàn phím cơ1 tới 10 ms3 tới 20
dem-canh.c
/* Chương trình chứng minh hiện tượng: đếm mọi cạnh xuống. */
#include "gpio.h"

static volatile uint32_t so_canh = 0;
static uint8_t truoc = 1;

int main(void)
{
    gpio_cau_hinh(GPIOC, 13, GPIO_VAO, GPIO_KEO_LEN);
    uart_khoi_tao();

    for (;;) {
        uint8_t hien = gpio_doc(GPIOC, 13);

        if (truoc == 1 && hien == 0) {      /* cạnh xuống */
            ++so_canh;
            uart_in_uint(so_canh);
            uart_gui_chuoi("\r\n");
        }

        truoc = hien;
    }
}
terminal
# Nhấn nút ĐÚNG MỘT lần
./giam-sat-uart
1
2
3
4
5
6
7
# Nhấn lần thứ hai
(tiep tuc)
8
9
10
11

#Ba cách chữa sai

Cách sai 1: chờ bằng vòng lặp rỗng

Vòng lặp chờ rỗng
if (gpio_doc(GPIOC, 13) == 0) {
    for (volatile uint32_t i = 0; i < 100000; ++i) { }   /* chờ khoảng 20 ms */

    if (gpio_doc(GPIOC, 13) == 0) xu_ly_nhan();
}

/* Ba vấn đề:
   1. CHẶN hoàn toàn: 20 ms đó không làm được việc gì khác.
   2. Số vòng lặp phụ thuộc tần số lõi và mức tối ưu, không cố định.
   3. volatile làm vòng lặp không bị xóa, nhưng cũng làm nó rất chậm. */
So mốc thời gian
/* Không chặn: ghi lại mốc thời gian rồi quay lại làm việc khác */
if (get_tick() - lan_doi_cuoi >= 20u) { ... }

Cách sai 2: chỉ đọc một lần rồi tin

Đọc một lần
uint8_t nhan = gpio_doc(GPIOC, 13);

if (nhan == 0) xu_ly_nhan();      /* đọc trúng lúc nảy thì sai */
Chờ ổn định
/* Đọc nhiều lần và chỉ tin khi ổn định đủ lâu */

Cách sai 3: đếm số lần đọc giống nhau

Đếm số lần đọc
static uint8_t dem = 0;

if (gpio_doc(GPIOC, 13) == 0) ++dem; else dem = 0;

if (dem >= 10) xu_ly_nhan();      /* 10 lần đọc liên tiếp */

/* Vấn đề: 10 lần đọc là bao nhiêu thời gian?
   Phụ thuộc tốc độ vòng lặp chính, mà tốc độ đó đổi theo
   những việc khác đang làm. Có lúc là 1 ms, có lúc là 100 ms. */
Đếm trong ngắt định kỳ
/* Đếm số lần đọc CHỈ đúng khi vòng lặp chạy với chu kỳ cố định,
   ví dụ khi gọi từ một ngắt định thời 1 ms. Lúc đó nó rất tốt: */
void SysTick_Handler(void)      /* chạy đúng mỗi 1 ms */
{
    if (gpio_doc(GPIOC, 13) == 0) { if (dem < 20) ++dem; }
    else                          dem = 0;

    on_dinh = (dem >= 20);       /* ổn định 20 ms */
}

#Chống rung không chặn

debounce.h
#ifndef DEBOUNCE_H
#define DEBOUNCE_H

#include <stdint.h>

#include "stm32f4.h"

typedef struct {
    GPIO_TypeDef *cong;
    uint8_t       chan;
    uint8_t       muc_nhan;     /* mức điện áp tương ứng với nhấn */
    uint8_t       on_dinh;      /* trạng thái đã lọc */
    uint8_t       tho_truoc;    /* lần đọc thô gần nhất */
    uint32_t      moc;          /* thời điểm lần đổi thô cuối */
    uint16_t      nguong_ms;
} Nut;

void    nut_khoi_tao(Nut *n, GPIO_TypeDef *cong, uint8_t chan,
                     uint8_t muc_nhan, uint16_t nguong_ms);
uint8_t nut_cap_nhat(Nut *n, uint32_t tick_ms);

#endif
debounce.c
#include "debounce.h"
#include "gpio.h"

void nut_khoi_tao(Nut *n, GPIO_TypeDef *cong, uint8_t chan,
                  uint8_t muc_nhan, uint16_t nguong_ms)
{
    n->cong      = cong;
    n->chan      = chan;
    n->muc_nhan  = muc_nhan;
    n->nguong_ms = nguong_ms;

    gpio_cau_hinh(cong, chan, GPIO_VAO,
                  muc_nhan ? GPIO_KEO_XUONG : GPIO_KEO_LEN);

    n->tho_truoc = gpio_doc(cong, chan);
    n->on_dinh   = n->tho_truoc;
    n->moc       = 0u;
}

/* Gọi thường xuyên từ vòng lặp chính. Trả về 1 nếu trạng thái
   ổn định vừa THAY ĐỔI, tức có một sự kiện nhấn hoặc nhả. */
uint8_t nut_cap_nhat(Nut *n, uint32_t tick_ms)
{
    uint8_t tho = gpio_doc(n->cong, n->chan);

    if (tho != n->tho_truoc) {
        /* Tín hiệu thô vừa đổi: bấm lại đồng hồ, chưa tin gì cả */
        n->tho_truoc = tho;
        n->moc       = tick_ms;

        return 0u;
    }

    /* Tín hiệu thô giữ nguyên. Đã đủ lâu chưa? */
    if (tho != n->on_dinh && (tick_ms - n->moc) >= n->nguong_ms) {
        n->on_dinh = tho;

        return 1u;      /* có sự kiện */
    }

    return 0u;
}

/* Trạng thái đã lọc, dùng khi cần biết nút đang giữ hay không */
static inline uint8_t nut_dang_nhan(const Nut *n)
{
    return n->on_dinh == n->muc_nhan;
}
main.c
#include "debounce.h"
#include "gpio.h"
#include "systick.h"

int main(void)
{
    Nut     nut;
    uint8_t che_do = 0u;

    systick_khoi_tao();                   /* tick 1 ms, xem Bài 50.1 */
    gpio_cau_hinh(GPIOA, 5, GPIO_RA, GPIO_KHONG_KEO);
    nut_khoi_tao(&nut, GPIOC, 13, 0u, 20u);   /* nhấn là mức 0, ngưỡng 20 ms */

    for (;;) {
        uint32_t nay = systick_lay_tick();

        if (nut_cap_nhat(&nut, nay) && nut_dang_nhan(&nut)) {
            che_do = (uint8_t)((che_do + 1u) % 3u);
            uart_in_uint(che_do);
        }

        /* Vòng lặp chính vẫn làm việc khác, không bị chặn */
        cap_nhat_den(che_do, nay);
    }
}
terminal
# Nhấn nút ba lần
./giam-sat-uart
1
2
0

#Phát hiện cạnh thay vì đọc mức

Câu hỏiHàm dùngVí dụ ứng dụng
Nút có đang được giữ khôngĐọc mức đã lọcGiữ để tăng nhanh, nhả thì dừng
Nút vừa được nhấn xuống chưaCạnh xuống của mức đã lọcChuyển chế độ, xác nhận lựa chọn
Nút vừa được nhả ra chưaCạnh lên của mức đã lọcKết thúc thao tác kéo
Nút được giữ bao lâu rồiĐo khoảng cách giữa hai cạnhNhấn ngắn và nhấn giữ làm hai việc khác nhau
su-kien-nut.c
typedef enum {
    NUT_KHONG_GI = 0,
    NUT_NHAN,
    NUT_NHA,
    NUT_NHAN_NGAN,
    NUT_NHAN_GIU,
} SuKienNut;

typedef struct {
    Nut      loc;
    uint32_t moc_nhan;
    uint8_t  da_bao_giu;
    uint16_t nguong_giu_ms;
} NutDayDu;

SuKienNut nut_su_kien(NutDayDu *n, uint32_t nay)
{
    uint8_t doi = nut_cap_nhat(&n->loc, nay);

    if (doi) {
        if (nut_dang_nhan(&n->loc)) {
            n->moc_nhan   = nay;
            n->da_bao_giu = 0u;

            return NUT_NHAN;
        }

        /* Vừa nhả. Nếu chưa báo giữ thì đây là một lần nhấn ngắn. */
        if (!n->da_bao_giu) return NUT_NHAN_NGAN;

        return NUT_NHA;
    }

    /* Không đổi trạng thái, nhưng có thể đã giữ đủ lâu */
    if (nut_dang_nhan(&n->loc) && !n->da_bao_giu &&
        (nay - n->moc_nhan) >= n->nguong_giu_ms) {
        n->da_bao_giu = 1u;

        return NUT_NHAN_GIU;
    }

    return NUT_KHONG_GI;
}
terminal
# Nhấn nhanh rồi nhả
./giam-sat-su-kien
t=1204  NUT_NHAN
t=1318  NUT_NHAN_NGAN
# Nhấn và giữ hai giây
(tiep tuc)
t=2841  NUT_NHAN
t=3841  NUT_NHAN_GIU
t=4902  NUT_NHA

#Lọc bằng phần cứng

CáchLinh kiệnƯu điểmNhược điểm
Mạch RCMột điện trở, một tụRẻ, không tốn chu kỳ CPU nàoSườn tín hiệu dốc chậm, cần chân Schmitt trigger
Mạch RC cộng cổng SchmittThêm một cổng logicTín hiệu ra rất sạchThêm linh kiện, thêm chỗ trên bo mạch
Mạch chốt RSHai cổng NAND, công tắc hai ngảKhông có độ trễ nàoCần công tắc hai ngả, hiếm và đắt
Chip chống rung chuyên dụngMột chipKhông cần tính toán gìĐắt, chỉ dùng khi có rất nhiều nút
Phần mềmKhông có gìMiễn phí, đổi ngưỡng lúc nào cũng đượcTốn vài chu kỳ CPU mỗi vòng lặp
Tính giá trị mạch RC
/* Mạch: nút nối chân xuống đất, điện trở kéo lên R, tụ C nối chân xuống đất.

   Hằng số thời gian:  tau = R * C

   Muốn lọc rung 20 ms thì cần tau khoảng 10 ms.

   Chọn R = 10 kilo ohm, C = 1 micro farad:
     tau = 10000 * 0.000001 = 0.01 s = 10 ms

   Thời gian nạp tới ngưỡng logic cao, khoảng 0.7 * Vcc:
     t = tau * ln(1 / (1 - 0.7)) = 10 * 1.2 = 12 ms

   Đủ để bỏ qua rung 5 tới 20 ms, và người dùng không cảm nhận được. */

Bộ lọc dịch bit, gọn và rất hiệu quả

loc-dich-bit.c
/* Gọi từ ngắt định thời 1 ms. Giữ lịch sử 8 lần đọc gần nhất
   trong một byte, và chỉ đổi trạng thái khi cả 8 lần giống nhau. */
static uint8_t lich_su = 0xFFu;
static uint8_t on_dinh = 1u;

void SysTick_Handler(void)
{
    lich_su = (uint8_t)((lich_su << 1) | gpio_doc(GPIOC, 13));

    if      (lich_su == 0x00u) on_dinh = 0u;      /* 8 lần liên tiếp là 0 */
    else if (lich_su == 0xFFu) on_dinh = 1u;      /* 8 lần liên tiếp là 1 */
    /* Ngoài hai trường hợp đó thì giữ nguyên, tức đang rung */
}
CáchDòng mãRAM mỗi nútNgưỡng lọc
Bộ đếm thời gianKhoảng 2016 byteĐổi được lúc chạy
Dịch bit 8 bit42 byteCố định 8 lần lấy mẫu
Dịch bit 16 bit44 byteCố định 16 lần lấy mẫu

Tự làm thử

  1. Viết chương trình đếm mọi cạnh xuống của một nút và in ra UART, rồi nhấn mười lần và ghi lại tổng số cạnh.
  2. Cài chống rung bằng bộ đếm thời gian, thử ngưỡng 1, 5, 20 và 100 ms rồi ghi lại ngưỡng nhỏ nhất còn hoạt động ổn định.
  3. Viết phép so sánh mốc thời gian theo kiểu cộng rồi so, và mô phỏng bộ đếm quấn vòng để thấy nó sai.
  4. Cài hàm trả về năm loại sự kiện nút, và dùng nó cho một nút vừa chuyển chế độ khi nhấn ngắn vừa khôi phục mặc định khi giữ.
  5. Cài bộ lọc dịch bit song song cho tám nút trên cùng một cổng, đo RAM dùng so với cách một struct mỗi nút.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Tiếp điểm cơ khí nảy 1 tới 50 mili giây, tạo ra hàng chục cạnh cho một lần nhấn.
  • Vòng lặp chờ rỗng chặn chương trình và không cho thời gian xác định. Dùng mốc thời gian không chặn.
  • Luôn viết nay - moc >= nguong chứ không viết nay >= moc + nguong, để đúng cả khi bộ đếm quấn vòng.
  • Trả về sự kiện chứ không trả về mức, để người gọi không phải tự nhớ trạng thái trước và không xử lý lặp.
  • Bộ lọc dịch bit gọn hơn nhiều khi có nhiều nút, và lọc song song theo bit xử lý tới 32 nút trong vài dòng.