Thiết bị đầu cuối USB-HID tùy chỉnh dựa trên vi điều khiển STM32

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ầnThông số kỹ thuậtVai trò trong hệ thống
Vi điều khiểnSTM32F103C8T6 (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-USBTrao đổi dữ liệu chuẩn USB 2.0 tốc độ đầy đủ (12 Mbps)
Thiết bị nhập4 công tắc nhấn cơ họcPhát sinh sự kiện nhấn phím truyền lên máy chủ
Thiết bị xuất2 đè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ấpBus 5V USB hoặc mạch ổn áp 3.3VCung 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ạchChân vi điều khiểnGhi chú
Dữ liệu USB (-)PA11Kết nối trực tiếp với chân D-
Dữ liệu USB (+)PA12Kế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-4PA0 đến PA3Cấu hình chế độ đầu vào có điện trở kéo lên nội bộ
Đèn LED 1-2PB0 đến PB1Chế độ đầ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.

Thẻ: STM32 USB-HID C python Giao thức USB

Đăng vào ngày 18 tháng 8 lúc 04:00