Việc giao tiếp giữa vi điều khiển và máy tính thông qua cổng USB thường đòi hỏi phải cài đặt driver đặc thù trên hệ điều hành. Tuy nhiên, sử dụng lớp thiết bị HID (Human Interface Device) cho phép bỏ qua bước này vì các hệ điều hành hiện đại như Windows đã tích hợp sẵn driver hỗ trợ. Bài viết này hướng dẫn kỹ thuật cấu hình vi điều khiển APM32F427 thành một thiết bị HID tùy chỉnh (Custom HID) để trao đổi dữ liệu với PC sử dụng USB middleware chính hãng từ Geehy.
1. Tổng Quan Về Lớp Thiết Bị USB HID
USB HID là một lớp thiết bị chuẩn trong giao thức USB, thường được biết đến qua các thiết bị như chuột, bàn phím hoặc tay cầm chơi game. Đặc điểm nổi bật của lớp này là không yêu cầu người dùng phát triển driver riêng trên máy tính. Chỉ cần tuân thủ đúng chuẩn mô tả báo cáo (Report Descriptor), ứng dụng trên PC có thể giao tiếp với MCU thông qua các API hệ thống chuẩn.
2. Các Bước Triển Khai Custom HID Trên APM32F427
2.1. Chuẩn Hóa Project Cơ Sở
Bộ SDK của APM32F427 đã tích hợp sẵn nhiều ví dụ về USB. Để xây dựng thiết bị Custom HID, phương án tối ưu là sao chép ví dụ OTGD_Custom_HID_Keyboard có sẵn trong SDK và thực hiện các điều chỉnh cần thiết. Điều này giúp tận dụng được cấu hình phần cứng và stack USB đã được kiểm chứng.
2.2. Định Nghĩa Report Descriptor
Report Descriptor là cấu trúc dữ liệu quan trọng nhất, dùng để mô tả format dữ liệu mà thiết bị gửi và nhận. Đối với ứng dụng tùy chỉnh, chúng ta cần sử dụng Vendor Defined Usage Page để tránh xung đột với các thiết bị chuẩn khác. Dưới đây là cấu hình mô tả báo cáo cho phép truyền nhận dữ liệu 64 byte:
/**
* @brief Report descriptor cho Custom HID
*/
uint8_t g_hid_report_descriptor[USBD_CUSTOM_HID_REPORT_DESC_SIZE] =
{
/* USER CODE BEGIN 0 */
0x06, 0xFF, 0x00, /* USAGE_PAGE (Vendor Defined Page 0xFF00) */
0x09, 0x01, /* USAGE (Vendor Defined Usage 0x01) */
0xA1, 0x01, /* COLLECTION (Application) */
/* Cấu hình báo cáo đầu vào (Input Report) - MCU gửi lên PC */
0x09, 0x03, /* USAGE ID (Vendor Defined) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x26, 0xFF, 0x00, /* LOGICAL_MAXIMUM (255) */
0x75, 0x08, /* REPORT_SIZE (8 bits) */
0x95, 64, /* REPORT_COUNT (64 bytes) */
0x81, 0x02, /* INPUT (Data, Var, Abs) */
/* Cấu hình báo cáo đầu ra (Output Report) - PC gửi xuống MCU */
0x09, 0x04, /* USAGE ID (Vendor Defined) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x26, 0xFF, 0x00, /* LOGICAL_MAXIMUM (255) */
0x75, 0x08, /* REPORT_SIZE (8 bits) */
0x95, 64, /* REPORT_COUNT (64 bytes) */
0x91, 0x02, /* OUTPUT (Data, Var, Abs) */
0xC0 /* END_COLLECTION */
/* USER CODE END 0 */
};
Lưu ý cần cập nhật macro USBD_CUSTOM_HID_REPORT_DESC_SIZE trong file header tương ứng để khớp với kích thước thực tế của mảng mô tả trên (34 byte).
2.3. Xử Lý Sự Kiện Nhận Dữ Liệu
Trong file usbd_custom_hid_if.c, hàm callback USBD_FS_CUSTOM_HID_ItfReceive sẽ được gọi khi có gói tin USB đến. Để đồng bộ hóa với vòng lặp chính, chúng ta sử dụng một biến cờ toàn cục để thông báo trạng thái.
volatile uint8_t g_usb_data_ready = 0;
/**
* @brief Handler xử lý khi nhận dữ liệu từ host
* @param buffer: Con trỏ dữ liệu nhận được
* @param length: Độ dài dữ liệu
* @retval Trạng thái hoạt động USB
*/
USBD_STA_T USBD_FS_CUSTOM_HID_ItfReceive(uint8_t *buffer, uint8_t *length)
{
USBD_STA_T status = USBD_OK;
UNUSED(buffer);
UNUSED(length);
/* Kích hoạt cờ thông báo có dữ liệu mới */
g_usb_data_ready = 1;
/* Chuẩn bị cho gói tin nhận tiếp theo */
USBD_CUSTOM_HID_RxPacket(&gUsbDeviceFS);
return status;
}
2.4. Xây Dựng Hàm API Tầng Ứng Dụng
Để thuận tiện cho việc gọi hàm trong chương trình chính, cần đóng gói các thao tác đọc ghi vào các hàm riêng biệt.
Hàm đọc dữ liệu từ bộ đệm USB:
uint32_t CustomHID_ReadBuffer(uint8_t *dest, uint32_t max_len)
{
USBD_CUSTOM_HID_INFO_T *hid_info = (USBD_CUSTOM_HID_INFO_T*)gUsbDeviceFS.devClass[gUsbDeviceFS.classID]->classData;
if (hid_info == NULL || hid_info->report == NULL)
{
return 0;
}
uint32_t actual_len = (max_len < hid_info->reportSize) ? max_len : hid_info->reportSize;
/* Sao chép dữ liệu từ bộ đệm USB vào biến đích */
memcpy(dest, hid_info->report, actual_len);
return actual_len;
}
Hàm gửi dữ liệu lên PC:
uint32_t CustomHID_WriteBuffer(uint8_t *src, uint32_t len)
{
/* Kiểm tra trạng thái thiết bị trước khi gửi */
if (gUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED)
{
return 0;
}
/* Giới hạn độ dài gói tin theo kích thước EP */
uint32_t send_len = (len > 64) ? 64 : len;
if (USBD_OK != USBD_CUSTOM_HID_TxReport(&gUsbDeviceFS, src, send_len))
{
return 0;
}
return send_len;
}
2.5. Tích Hợp Vào Vòng Lặp Chính
Trong hàm main, chúng ta sẽ khởi tạo hệ thống và thực hiện kiểm tra vòng lặp. Ví dụ dưới đây thực hiện chức năng echo: nhận dữ liệu từ PC và gửi trả lại nguyên vẹn.
int main(void)
{
uint8_t rx_buffer[64];
memset(rx_buffer, 0, sizeof(rx_buffer));
/* Cấu hình phần cứng và hệ thống */
DAL_DeviceConfig();
/* Vòng lặp vô hạn */
while (1)
{
/* Kiểm tra cờ sự kiện nhận dữ liệu */
if (g_usb_data_ready == 1)
{
/* Reset cờ sự kiện */
g_usb_data_ready = 0;
/* Đọc dữ liệu từ stack USB */
CustomHID_ReadBuffer(rx_buffer, 64);
/* Xử lý logic ở đây (ví dụ: phân tích lệnh) */
/* Gửi dữ liệu phản hồi lại PC */
CustomHID_WriteBuffer(rx_buffer, 64);
}
/* Các tác vụ nền khác có thể chạy tại đây */
}
}
3. Kiểm Tra Và Xác Minh Hoạt Động
3.1. Xác Nhận Thiết Bị Trên Hệ Điều Hành
Sau khi biên dịch và nạp firmware vào board APM32F427, kết nối cáp USB với máy tính. Mở Device Manager trên Windows, mục Human Interface Devices sẽ hiển thị thiết bị mới nhận diện. Tên thiết bị sẽ tương ứng với chuỗi mô tả đã cấu hình trong firmware (ví dụ: APM32 Custom HID). Việc xuất hiện thiết bị này mà không báo lỗi driver chứng tỏ quá trình enum USB đã thành công.
3.2. Kiểm Tra Truyền Nhận Dữ Liệu
Để xác thực luồng dữ liệu, sử dụng các công cụ kiểm tra USB HID trên PC. Cấu hình công cụ để chọn đúng Vendor ID và Product ID của thiết bị. Thiết lập chế độ hiển thị và gửi dữ liệu dạng Hex.
Khi gửi một chuỗi byte bất kỳ từ công cụ trên PC, firmware sẽ nhận dữ liệu thông qua callback, kích hoạt cờ và thực hiện gửi trả lại qua hàm CustomHID_WriteBuffer. Nếu công cụ trên PC nhận được đúng chuỗi dữ liệu đã gửi, quá trình giao tiếp hai chiều đã hoạt động chính xác. Cơ chế này có thể mở rộng để thực hiện các lệnh điều khiển phức tạp hơn trong các dự án thực tế.