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.
| Loại công tắc | Thời gian rung điển hình | Số cạnh |
|---|---|---|
| Nút bấm nhỏ trên bo mạch | 1 tới 5 ms | 2 tới 10 |
| Nút bấm cỡ lớn | 5 tới 20 ms | 5 tới 30 |
| Công tắc gạt | 10 tới 50 ms | 10 tới 100 |
| Rơ le cơ khí | 0.5 tới 5 ms | 3 tới 15 |
| Bàn phím cơ | 1 tới 10 ms | 3 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);
#endifdebounce.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ỏi | Hàm dùng | Ví dụ ứng dụng |
|---|---|---|
| Nút có đang được giữ không | Đọc mức đã lọc | Giữ để tăng nhanh, nhả thì dừng |
| Nút vừa được nhấn xuống chưa | Cạnh xuống của mức đã lọc | Chuyển chế độ, xác nhận lựa chọn |
| Nút vừa được nhả ra chưa | Cạnh lên của mức đã lọc | Kế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ạnh | Nhấ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ách | Linh kiện | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Mạch RC | Một điện trở, một tụ | Rẻ, không tốn chu kỳ CPU nào | Sườn tín hiệu dốc chậm, cần chân Schmitt trigger |
| Mạch RC cộng cổng Schmitt | Thêm một cổng logic | Tín hiệu ra rất sạch | Thêm linh kiện, thêm chỗ trên bo mạch |
| Mạch chốt RS | Hai cổng NAND, công tắc hai ngả | Không có độ trễ nào | Cần công tắc hai ngả, hiếm và đắt |
| Chip chống rung chuyên dụng | Một chip | Không cần tính toán gì | Đắt, chỉ dùng khi có rất nhiều nút |
| Phần mềm | Không có gì | Miễn phí, đổi ngưỡng lúc nào cũng được | Tố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ách | Dòng mã | RAM mỗi nút | Ngưỡng lọc |
|---|---|---|---|
| Bộ đếm thời gian | Khoảng 20 | 16 byte | Đổi được lúc chạy |
| Dịch bit 8 bit | 4 | 2 byte | Cố định 8 lần lấy mẫu |
| Dịch bit 16 bit | 4 | 4 byte | Cố định 16 lần lấy mẫu |
Tự làm thử
- 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.
- 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.
- 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.
- 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ữ.
- 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 >= nguongchứ không viếtnay >= 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.