Tổng quan về giải pháp
Chuẩn giao tiếp USB-HID (Human Interface Device) đóng vai trò quan trọng trong việc kết nối các thiết bị ngoại vi như bàn phím, chuột hay cần điều hướng với máy tính. Bài viết này trình bày quy trình xây dựng một thiết bị HID chuyên biệt chạy trên vi điều khiển STM32F103C8T6. Hệ thống sẽ mô phỏng hoạt động của một bộ điều khiển bao gồm 4 nút bấm và cần điều hướng 2 trục, đồng thời hỗ trợ nhận tín hiệu điều khiển ngược lại từ máy chủ để bật/tắt đèn LED. Giải pháp này phù hợp cho các ứng dụng trong tự động hóa công nghiệp, hệ thống thu thập dữ liệu hoặc phát triển thiết bị nhập liệu đặc thù.
Thiết kế phần cứng
2.1 Danh mục linh kiện chính
| Thành phần | Thông số kỹ thuật | Vai trò trong hệ thống |
|---|---|---|
| Vi điều khiển | STM32F103C8T6 (Khung Cortex-M3, xung nhịp 72 MHz) | Xử lý giao thức, đóng gói báo cáo và điều khiển chân I/O |
| Cổng kết nối | Đầu cắm Mini-USB | Trao đổi dữ liệu chuẩn USB 2.0 tốc độ đầy đủ (12 Mbps) |
| Thiết bị nhập | 4 công tắc nhấn cơ học | Phát sinh sự kiện nhấn phím truyền lên máy chủ |
| Thiết bị xuất | 2 đèn LED chỉ thị | Phản hồi trạng thái nhận được lệnh điều khiển từ PC |
| Nguồn cấp | Bus 5V USB hoặc mạch ổn áp 3.3V | Cung cấp năng lượng cho toàn bộ mạch |
2.2 Sơ đồ đấu nối chân tín hiệu
| Mạch | Chân vi điều khiển | Ghi chú |
|---|---|---|
| Dữ liệu USB (-) | PA11 | Kết nối trực tiếp với chân D- |
| Dữ liệu USB (+) | PA12 | Kết nối trực tiếp với chân D+, cần điện trở kéo lên 1.5kΩ nếu mạch chưa tích hợp |
| Nút nhấn 1-4 | PA0 đến PA3 | Cấu hình chế độ đầu vào có điện trở kéo lên nội bộ |
| Đèn LED 1-2 | PB0 đến PB1 | Chế độ đầu ra đẩy-kéo (Push-Pull), mức cao kích sáng |
Triển khai phần mềm
3.1 Kiến trúc xử lý
Dòng dữ liệu di chuyển theo hai hướng rõ rệt thông qua các kênh endpoint vật lý. Chiều từ thiết bị lên máy tính sử dụng kênh IN để truyền trạng thái cảm biến và phím. Ngược lại, chiều từ máy tính xuống thiết bị dùng kênh OUT để gửi lệnh điều khiển ngoại vi. Toàn bộ quá trình được điều phối bởi trình điều khiển USB tích hợp sẵn và các hàm gọi lại (callback) trong vòng lặp chính.
3.2 Cấu hình môi trường phát triển
Việc khởi tạo môi trường lập trình dựa trên công cụ STM32CubeMX tập trung vào hai khía cạnh then chốt. Thứ nhất, cây phân tần số (PLL) phải được tinh chỉnh để tạo ra tín hiệu 48 MHz chính xác, đây là yêu cầu bắt buộc của bộ điều khiển USB FS. Thứ hai, trong mục Connectivity, chọn chế độ thiết bị USB OTG Full Speed, kích hoạt lớp giao diện HID và chỉ định các mã định danh nhà sản xuất (VID) cùng sản phẩm (PID) phù hợp với phân vùng quản lý thiết bị của hệ điều hành máy chủ.
3.3 Định nghĩa cấu trúc báo cáo (Report Descriptor)
Mã định danh báo cáo quyết định cách hệ điều hành đọc dữ liệu nhị phân. Dưới đây là mảng byte được biên dịch theo cú pháp HID, mô tả một bộ điều khiển bao gồm vùng nhấn nút, trục tọa độ và kênh phản hồi đèn:
const uint8_t MaTranMotaBaoCao[] = {
0x05, 0x01, // Trang sử dụng: Bàn phím/Máy tính để bàn
0x09, 0x05, // Chức năng: Gamepad
0xA1, 0x01, // Bắt đầu nhóm tập hợp
// Khu vực nút bấm (4 bit)
0x05, 0x09, // Trang nút
0x19, 0x01, 0x29, 0x04, // Phạm vi nút 1 đến 4
0x15, 0x00, 0x25, 0x01, // Giá trị logic 0-1
0x75, 0x01, 0x95, 0x04, // Kích thước 1 bit, đếm 4 lần
0x81, 0x02, // Dữ liệu nhập (Biến, Tuyệt đối)
0x75, 0x04, 0x95, 0x01, // Đệm 4 bit
0x81, 0x03, // Nhập hằng số
// Trục ngang (16 bit, 0-1023)
0x05, 0x01, 0x09, 0x30, // Trang chung, Trục X
0x16, 0x00, 0x00, 0x26, 0xFF, 0x03, // Giới hạn logic
0x75, 0x10, 0x95, 0x01, // Kích thước 16 bit, đếm 1
0x81, 0x02, // Dữ liệu nhập
// Trục dọc (16 bit, 0-1023)
0x09, 0x31, // Trục Y
0x16, 0x00, 0x00, 0x26, 0xFF, 0x03,
0x75, 0x10, 0x95, 0x01,
0x81, 0x02, // Dữ liệu nhập
// Khu vực đèn báo (2 bit)
0x09, 0x00, // Sử dụng chưa định danh
0x15, 0x00, 0x25, 0x01,
0x75, 0x01, 0x95, 0x02,
0x91, 0x02, // Dữ liệu xuất (Biến, Tuyệt đối)
0xC0 // Kết thúc tập hợp
};
3.4 Mã nguồn xử lý dữ liệu
Khởi tạo module truyền thông
Quá trình thiết lập ban đầu bao gồm việc đồng bộ hóa bộ dao động, kích hoạt bộ ngắt ngoại vi và đăng ký các bộ đệm bộ nhớ (PMA) cho các endpoint tương ứng.
void KhoiTaoHeThongUSB(void) {
CauHinhHeThong();
DongBoTongHopTong(48); // Ép xung USB lên 48 MHz
CaiDatBoNgatGiaoTiep();
KhoiTaoDongTroiUSB();
CaiDatBufferEP();
}
Đóng gói và đẩy báo cáo lên máy chủ
Dữ liệu thô từ các chân GPIO sẽ được sắp xếp lại theo thứ tự byte ít quan trọng (Little-Endian) trước khi được chuyển vào vùng đệm phần cứng để truyền đi.
typedef struct {
uint8_t masPhim;
uint8_t dung;
uint16_t giaTriTrucNgang;
uint16_t giaTriTrucDoc;
} KetCauDuLieuGui;
void TruyenBaoCaoNhap(KetCauDuLieuGui *thongTin) {
uint8_t ketCa[8] = {0};
ketCa[0] = thongTin->masPhim;
ketCa[2] = (uint8_t)(thongTin->giaTriTrucNgang & 0xFF);
ketCa[3] = (uint8_t)(thongTin->giaTriTrucNgang >> 8);
ketCa[4] = (uint8_t)(thongTin->giaTriTrucDoc & 0xFF);
ketCa[5] = (uint8_t)(thongTin->giaTriTrucDoc >> 8);
SaoChepSangBufferPMATuUser(ketCa, DiaChiEP1_TX, 8);
DatSoLuongGoiEP1(8);
HieuHuaTruyenEP1();
}
Nhận lệnh điều khiển từ máy tính
Khung dữ liệu nhận được từ endpoint OUT sẽ được giải mã ngay trong hàm ngắt, sau đó áp dụng trực tiếp lên chân điều khiển đèn.
void XuLyNgatEP2_Nhap(void) {
uint8_t duLieuNhan[1] = {0};
LayDuLieuTuBufferPMA(duLieuNhan, DiaChiEP2_RX, 1);
// Áp dụng mask bit 0 và bit 1 lên chân PB0, PB1
DieuKhienTrangThaiCh(GPIOB, GPIO_Pin_0, (duLieuNhan[0] & 0x01) ? SET : RESET);
DieuKhienTrangThaiCh(GPIOB, GPIO_Pin_1, (duLieuNhan[0] & 0x02) ? SET : RESET);
ChoPhepNhanLaiEP2();
}
Vòng lặp chính và đọc tín hiệu vật lý
Chương trình chính sẽ liên tục quét trạng thái chân nhấn, gán giá trị mặc định cho trục tọa độ và gọi hàm truyền dữ liệu theo chu kỳ cố định.
int main(void) {
KhoiTaoHeThong();
KhoiTaoHeThongUSB();
CaiDatChioVaoRa();
KetCauDuLieuGui baoCao = {0};
while (1) {
uint8_t tacPhim = 0;
if (!DocTrangThaiCh(GPIOA, GPIO_Pin_0)) tacPhim |= 1;
if (!DocTrangThaiCh(GPIOA, GPIO_Pin_1)) tacPhim |= 2;
if (!DocTrangThaiCh(GPIOA, GPIO_Pin_2)) tacPhim |= 4;
if (!DocTrangThaiCh(GPIOA, GPIO_Pin_3)) tacPhim |= 8;
baoCao.masPhim = tacPhim;
baoCao.giaTriTrucNgang = 512;
baoCao.giaTriTrucDoc = 512;
TruyenBaoCaoNhap(&baoCao);
ChonDo(10); // Tạm dừng 10 ms
}
}
Kiểm thử giao tiếp phía máy chủ
Để xác nhận dòng dữ liệu từ thiết bị đầu cuối hoạt động chính xác, có thể sử dụng ngôn ngữ Python cùng thư viện thao tác thiết bị HID. Đoạn mã sau minh họa cách lắng nghe luồng dữ liệu thô và giải mã các trường tương ứng:
import pywinusb.hid as hid_interface
import sys
def xu_ly_goi_nhan(mang_byte):
tram = mang_byte[0]
truc_x = mang_byte[2] | (mang_byte[3] << 8)
truc_y = mang_byte[4] | (mang_byte[5] << 8)
print(f"Phím: {bin(tram)} | Tọa độ X: {truc_x} | Tọa độ Y: {truc_y}")
def chay_kiem_thu():
tu_chon_thiet_bi = hid_interface.HidDeviceFilter(vendor_id=0x0483, product_id=0x5710)
danh_sach = tu_chon_thiet_bi.get_devices()
if not danh_sach:
print("Không tìm thấy thiết bị mục tiêu.")
return
ket_noi = danh_sach[0]
ket_noi.open()
ket_noi.set_raw_data_handler(xu_ly_goi_nhan)
print("Đã kết nối thành công. Nhấn Ctrl+C để thoát.")
try:
while True:
import time
time.sleep(0.5)
except KeyboardInterrupt:
ket_noi.close()
sys.exit(0)
if __name__ == "__main__":
chay_kiem_thu()
Xử lý các tình huống thường gặp
Quá trình tích hợp thiết bị USB-HID thường đối mặt với một số rào cản kỹ thuật cần được giải quyết kịp thời:
- Hệ điều hành không nhận diện thiết bị: Nguyên nhân phổ biến nhất là sai lệch tần số clock cung cấp cho module USB hoặc lỗi cú pháp trong mảng mô tả báo cáo. Cần kiểm tra lại cấu hình PLL và sử dụng bộ giải mã giao thức USB để đối chiếu cấu trúc descriptor.
- Bị trễ hoặc mất gói tin: Việc gửi dữ liệu quá thường xuyên có thể gây quá tải bộ đệm phần cứng. Hãy điều chỉnh khoảng thời gian giữa các lần truyền tải về mức hợp lý (khoảng 10-20 ms) và đảm bảo cơ chế đánh dấu vùng đệm đã trống trước khi ghi dữ liệu mới.
- Tín hiệu nhấn phím không ổn định: Hiện tượng nảy cơ học khi đóng mạch sẽ tạo ra nhiều xung nhiễu liên tiếp. Giải pháp tối ưu là tích hợp thuật toán lọc biên mềm hoặc thêm tụ chống rung (debounce) ngay tại chân đọc tín hiệu.