Keyboard from Scratch: Prototype
Là một lập trình viên, bàn phím là một vật dụng bạn phải sờ vào hằng ngày, thậm chí số lần bạn sờ nó còn nhiều hơn số lần bạn sờ vào vợ hoặc bạn gái. Chính vì vậy, chúng ta phải đầu tư cho nó một cách xứng đáng, bằng 2 cách:
- Mua một cái bàn phím cơ
- Tự làm một cái
Và, với những ai tự gọi mình là kĩ sư (software engineer, backend engineer, frontend engineer, hay copy-pasta engineer, stackoverflow engineer,... nói chung là mọi thể loại engineer), chẳng có lý do gì để không tự làm một cái cho riêng mình, để tự mình quyết định bố cục (layout), màu sắc (keycaps), và tốc dộ gõ (key switches, scanning time).
Suy cho cùng, tự mình build một cái gì đó, và tự tay giải quyết những vấn đề hóc búa trong quá trình build, học hỏi và thu lại được kinh nghiệm cho bản thân, cũng có thể gọi là cái raison d'être của một engineer . Hay nói cách khác, như vậy mới gọi là dân chơi đúng nghĩa .
Bố cục bàn phím
Đa phần các bàn phím tên tuổi thường chỉ khác nhau về bố cục (layout), và đây là phần bất biến, sau khi sản xuất, còn lại những thứ khác (case, plate, key mapping...) thì đều có thể được custom tùy ý.
Đối với bản prototype này, chúng ta sẽ thiết kế một layout vô cùng phức tạp, như thế này:
Sau khi đã thiết kế xong bố cục thì chúng ta có thể move sang bước tiếp theo đó là thiết kế và cắt tấm đệm (plate).
Trong thiết kế của bàn phím cơ, có 2 cách lắp ghép (mount) swtiches, là Plate Mount và PCB Mount.
. Thế nên mình quyết định là tự viết riêng cho mình một bản firmware riêng.
Để cho đơn giản, thì mình sẽ sử dụng Teensyduino để lập trình. Đây là một add-on của Arduino IDE, cho phép chúng ta lập trình trên Teensy bằng bộ thư viện của Arduino, và sử dụng cấu trúc chương trình của Arduino.
Một firmware cơ bản sẽ là một event loop thực hiện các công việc sau:
- Scan: quét liên tục để tìm ra các phím được nhấn
- Processing: xây dựng một buffer chứa keycode của các phím đang được nhấn xuống, dựa trên keymap mà chúng ta đã thiết lập.
- Output: Gửi buffer này về máy tính thông qua cổng USB.
Trước khi bắt tay vào thực hiện code logic trên, chúng ta cần phải khai báo một vài thông số liên quan:
#include <Keyboard.h> const byte ROWS = 2;
const byte COLS = 2; char keys[ROWS][COLS] = { { 'A', 'B' }, { 'C', 'D' }
}; const byte rowPins[ROWS] = { 6, 7 };
const byte colPins[COLS] = { 2, 3 };
Ở trên, chúng ta include thư viện Keyboard.h
(đi kèm theo SDK của Arduino), chỉ vì chúng ta muốn sử dụng hàm Keyboard.print()
có trong thư viện này để gửi phím được nhấn về cho máy tính.
Tiếp theo, chúng ta khai báo 2 hằng ROWS
và COLS
quy định số hàng và số cột của bàn phím, mảng keys
chính là keymap, là mảng quy ước các kí tự nào thuộc về phím nào trên bàn phím của chúng ta, ở đây khi nhấn các phím 1, 2, 3, 4
thì bàn phím sẽ gửi về máy tính các kí tự A, B, C, D
.
Hai mảng rowPins
và colPins
lưu vị trí các chân cắm trên board điều khiển, theo như cách chúng ta đã nối dây ở phần trước.
Đến đây, nếu tinh ý thì các bạn sẽ nhận ra, là ở bài viết tiếp theo khi chúng ta xây dựng firmware cho một cái bàn phím thực sự, thì chỉ cần thay đổi các thông số ở trên là xong.
Bây giờ đến phần implement event loop cho firmware của chúng ta, đầy đủ 3 bước scan
, process
và output
:
void loop() { char code = scan(); if (code !== -1) { char keyCode = process(code); output(keyCode); } delay(50);
}
Vấn đề #1: Quét tín hiệu
Hàm scan()
của chúng ta sẽ có nhiệm vụ quét tất cả các hàng và các cột của mạch điện, kiểm tra xem phím nào được nhấn xuống và trả về một giá trị kiểu byte
chứa thông tin các phím được nhấn.
C không có kiểu byte nên chúng ta dùng kiểu char để thay thế.
Để cho đơn giản, thì chúng ta chỉ support việc nhấn một phím một lần, ở bài sau chúng ta sẽ cải tiến firmware để xử lý việc nhấn tổ hợp phím, macro,...
Việc quét phím được thực hiện thông qua thuật toán sau:
Nếu cảm thấy khó hiểu ở bước 3 và 4, bạn có thể đọc thêm cách hoạt động của các chân digital tại đây.
Vì mạch của chúng ta chỉ đơn giản gồm có 4 nút, mỗi hàng và mỗi cột chỉ có nhiều nhất là 2 phàn tử (index là 0 hoặc 1), vậy nên ta có thể "gói" hai giá trị hàng cột này vào cho một số kiểu byte
, bằng phương pháp dịch bit.
Giả sử chúng ta đang ở hàng r = 0
và cột c = 1
, chúng ta có thể chèn giá trị r
vào bit thứ nhất, và c
vào bit thứ 2:
$$ \texttt{n = (r << 0) | (c << 1)} $$
Khi cần đọc ngược lại thì cũng rất đơn giản:
$$ \begin{align} \texttt{r = (n >> 0) & 1} \\ \texttt{c = (n >> 1) & 1} \end{align} $$
char scan() { char code = -1; // Đưa tất cả các pin về trạng thái INPUT/HIGH for (int i = 0; i < ROWS; i++) { pinMode(rowPins[i], INPUT_PULLUP); digitalWrite(rowPins[i], HIGH); } for (int i = 0; i < COLS; i++) { pinMode(colPins[i], INPUT_PULLUP); digitalWrite(colPins[i], HIGH); } // Đưa từng hàng về trạng thái OUTPUT/LOW và quét for (int row = 0; row < ROWS; row++) { pinMode(rowPins[row], OUTPUT); digitalWrite(rowPins[row], LOW); for (int col = 0; col < COLS; col++) { if (!digitalRead(colPins[col])) { // Lưu giá trị hàng và cột thành một số int code = (row << 0) | (col << 1); } } pinMode(rowPins[row], INPUT_PULLUP); digitalWrite(rowPins[row], HIGH); } return code;
}
Vấn đề #2: Gửi tín hiệu về máy tính
Tiếp theo, chúng ta cần chuyển thông tin về hàng/cột nhận được thành kí tự đã được khai báo trong keymap.
Bước này khá là đơn giản, chỉ cần đọc giá trị trả về từ hàm scan()
và trả về giá trị tương ứng từ mảng keys
:
char process(char code) { return keys[(code >> 0) & 1][(code >> 1) & 1];
}
Hàm gửi tín hiệu output()
sẽ sử dụng hàm Keyboard.print
của bộ thư viện Keyboard.h
và truyền thông tin sang máy tính:
void output(char c) { Keyboard.print(c);
}
Đến bước này, bạn có thể compile và upload firmware vào Teensy để test thử.
Máy tính đã nhận diện được bàn phím mới, và gõ thì có ra được nội dung thật. Tuy nhiên sẽ có một vấn đề đó là hiện tượng nhấn một nút, máy tính sẽ in ra rất nhiều lần phím được nhấn. Đây gọi là hiện tượng key chatter.
Ở bài viết tiếp theo, chúng ta sẽ tìm hiểu sâu hơn về hiện tượng này, và implement kĩ thuật debounce để giải quyết nó.