这篇文章上次修改于 325 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

USB HID上位机简单实现

CH549为例,实现简单的HID上位机功能。

修改HID报告描述符

在HID报告描述符中添加Vendor-Defined Usage Page,然后并设置REPORT_ID(每个子HID设备都应该设置,且不重复),这样就能在不新增端点的情况下,实现自定义HID报文功能。

        /* Vendor */
        0x06, 0xB1, 0xFF, // Usage Page (Vendor-Defined 178)
        0x09, 0x01,       // Usage (Vendor-Defined 1)
        0xA1, 0x01,       // Collection (Application)

        0x85, 0x04,       // REPORT_ID (4)
        0x09, 0x04,       // Usage (Vendor-Defined 4)
        0x15, 0x00,       // Logical Minimum (0)
        0x26, 0xFF, 0x00, // Logical Maximum (255)
        0x75, 0x08,       // Report Size (8)
        0x95, 0x3c,       // Report Count (60)
        0x91, 0x02,       // Output(Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)

        0x09, 0x01,       //   Usage (0x01)
        0x25, 0x00,       //   Logical Maximum (0)
        0x26, 0xFF, 0x00, //     Logical Maximum (255)
        0x75, 0x08,       //   Report Size (8)
        0x95, 0x3c,       //   Report Count (60)
        0x81, 0x02,       //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

        0xC0, // End Collection   
                /* 37 */

缺点是每个 HID 报文前都需要新增REPORT_ID号,用来区分功能。

烧录至单片机,连接到电脑后会显示为符合HID标准的供应商定义设备

单片机的发送和接收

发送数据

UINT8 SendHID[61] = {0}; //Report Count (60),多出的一个位是REPORT_ID

SendHID[0] = 4; //REPORT_ID
SendHID[1] = 0; //data
SendHID[2] = 1; //data

Enp2BlukIn(SendHID, sizeof(SendHID)); //发送到上位机,这里使用WCH提供的函数,IN端口2

接收数据

UINT8 HID_OUT_report[61] = {0};

/*******************************************************************************
 * Function Name  : DeviceInterrupt()
 * Description    : CH559USB中断处理函数
 *******************************************************************************/

......

case UIS_TOKEN_OUT | 2: // endpoint 2# 端点批量下传
            if (U_TOG_OK)       // 不同步的数据包将丢弃
            {
                len = USB_RX_LEN; //接收数据长度,数据从Ep2Buffer首地址开始存放
                for (i = 0; i < len; i++)
                {
                    if (Ep2Buffer[0] == 0x04) //当reportid==4
                    {
                        HID_OUT_report[i] = Ep2Buffer[i]; //接收到的数据保存到HID_OUT_report
                    }
                }
            }

            break;

......

上位机部分

通常会使用HIDAPI来实现,当然可以使用你喜欢的语言来写,这里就用Python来实现。

这里用到的是cython-hidapi,它的核心还是HIDAPI,你可以理解为套壳。

初始化

import hid
import time

vendor_id = 0x2b86 
usage_page = 0xffb1 # 对应HID报告描述符的 0x06, 0xB1, 0xFF, // Usage Page (Vendor-Defined 178)
# 通过VID和usage_page查找USB上的单片机设备,当然PID也是可以的

def init_usb(vendor_id, usage_page): 
    h = hid.device()
    hid_enumerate = hid.enumerate() # 导出所有的HID设备信息    
    for i in range(len(hid_enumerate)):
        if (hid_enumerate[i]['usage_page'] == usage_page and hid_enumerate[i]['vendor_id'] == vendor_id):
            device_path = hid_enumerate[i]['path'] 
    if (device_path == 0): return "Device not found"
    #遍历所有的HID设备,查找符合VID和usage_page的设备,获取路径
    
    h.open_path(device_path) #通过路径打开设备
    h.set_nonblocking(1)  # enable non-blocking mode

发送和接收

buffer = [0] * 60 
# 这里的列表元素个数必须为HID报告描述符所指定的个数,IN/OUT report都应为这个长度。
# 0x95, 0x3c,       //   Report Count (60)
# 不然Windows是不会处理这个report的,但是在Linux上就不会出现这个问题。
buffer[0] = 4 #report_id
buffer[1] = 0 #data
buffer[2] = 0 #data

def hid_report(vendor_id, usage_page, buffer): #调用函数发送数据,完成后等待单片机返回数据
    try:
        h.write(buffer) # 尝试向单片机发送数据
    except (OSError, ValueError):
        print("写入设备错误")
        return 1
    except NameError:
        print("未初始化设备")
        return 1
    while 1:
        try:
            d = h.read(64) #尝试从单片机循环接收数据
        except (OSError, ValueError):
            print("读取数据错误")
            return 2
        if d:
            print(">", d) #打印接收到的数据
            break
        if time.time() - time_start > 3: #接收超时
            return 2
    return d

链接和参考

CH549/CH548评估板说明及参考应用例程

USB HID报告描述符教程

ご注文はオトナココアですか?80