目录
配置
打开usbd_conf.h文件,找到总配置开关,USE_USBD_HIDKEY配置为1。其他的为0,注意:USBD_INT_DEBUG 这个配置是在中断调试的配置,如果要运行这个工程,这个配置一定要配置为0。
设备解析
打开standard_hid_core.c文件,对应的头文件是standard_hid_core.h。在打开对应的配置的后,只会有一个设备类的文件生效
前面我们说过CDC_ACM是虚拟串口,那么HID类设备是属于什么设备?HID大类的定义为人机交互设备,可以通过这种设备来控制或读取主机数据,像日常的鼠标和键盘这种都属于HID类,工程下的HID例程为标准的HID实现,对应的功能是键盘,通过IO引脚的状态使MCU发送键值到主机达到模拟键盘的效果。
在上一章我们已经学习过的CDC_ACM的例程上,HID有什么不同,首先看配置描述符。
usb_hid_desc_config_set hid_config_desc = { .config = { .header = { .bLength = sizeof(usb_desc_config), .bDescriptorType = USB_DESCTYPE_CONFIG }, .wTotalLength = USB_HID_CONFIG_DESC_LEN, .bNumInterfaces = 0x01U, .bConfigurationValue = 0x01U, .iConfiguration = 0x00U, .bmAttributes = 0xA0U, .bMaxPower = 0x32U }, .hid_itf = { .header = { .bLength = sizeof(usb_desc_itf), .bDescriptorType = USB_DESCTYPE_ITF }, .bInterfaceNumber = 0x00U, .bAlternateSetting = 0x00U, .bNumEndpoints = 0x01U, .bInterfaceClass = USB_HID_CLASS, .bInterfaceSubClass = USB_HID_SUBCLASS_BOOT_ITF, .bInterfaceProtocol = USB_HID_PROTOCOL_KEYBOARD, .iInterface = 0x00U }, .hid_vendor = { .header = { .bLength = sizeof(usb_desc_hid), .bDescriptorType = USB_DESCTYPE_HID }, .bcdHID = 0x0111U, .bCountryCode = 0x00U, .bNumDescriptors = 0x01U, .bDescriptorType = USB_DESCTYPE_REPORT, .wDescriptorLength = USB_HID_REPORT_DESC_LEN, }, .hid_epin = { .header = { .bLength = sizeof(usb_desc_ep), .bDescriptorType = USB_DESCTYPE_EP }, .bEndpointAddress = HID_IN_EP, .bmAttributes = USB_EP_ATTR_INT, .wMaxPacketSize = HID_IN_PACKET, .bInterval = 0x40U } };
可以看出这是一个单一功能的HID设备,只有一个功能接口,这个接口下包含了一个HID特定的描述符以及唯一的IN中断端点(为了实现快速响应),这个情景下,我们不需要接收来自主机的数据,只需要发送数据到主机即可。
HID报告描述符
和CDC_ACM不同的是,HID类设备在完成常规枚举操作后,会对HID设备发出设备类请求,获取HID报告描述符,因为HID属于一个大类,有很多类型的交互设备,单向双向等等,主机怎么知道我们的设备是一个单向输入键盘,这个时候需要通过HID报告描述符,准确的向主机报告我们的设备特性以及协议。
const uint8_t hid_report_desc[USB_HID_REPORT_DESC_LEN] = { 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x06, /* USAGE (Keyboard) */ 0xa1, 0x01, /* COLLECTION (Application) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */ 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x95, 0x08, /* REPORT_COUNT (8) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ 0x95, 0x06, /* REPORT_COUNT (6) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x26, 0xFF, 0x00, /* LOGICAL_MAXIMUM (255) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */ 0x19, 0x00, /* USAGE_MINIMUM (Reserved (no event indicated)) */ 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */ 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ 0xc0 /* END_COLLECTION */ };
这是一个标准的HID键盘的报告描述符,想修改的话需要翻阅HID设备规范进行修改,根据不同的应用场景而定,这个描述符的获取是在设备类的请求,并非标准设备请求。
设备类请求处理
static uint8_t hid_req_handler (usb_dev *udev, usb_req *req) { uint8_t status = REQ_NOTSUPP; standard_hid_handler *hid = (standard_hid_handler *)udev->class_data[USBD_HID_INTERFACE]; switch (req->bRequest) { case GET_REPORT: /* no use for this driver */ break; case GET_IDLE: usb_transc_config(&udev->transc_in[0U], (uint8_t *)&hid->idle_state, 1U, 0U); status = REQ_SUPP; break; case GET_PROTOCOL: usb_transc_config(&udev->transc_in[0U], (uint8_t *)&hid->protocol, 1U, 0U); status = REQ_SUPP; break; case SET_REPORT: /* no use for this driver */ break; case SET_IDLE: hid->idle_state = (uint8_t)(req->wValue >> 8); status = REQ_SUPP; break; case SET_PROTOCOL: hid->protocol = (uint8_t)(req->wValue); status = REQ_SUPP; break; case USB_GET_DESCRIPTOR: if (USB_DESCTYPE_REPORT == (req->wValue >> 8)) { usb_transc_config(&udev->transc_in[0U], (uint8_t *)hid_report_desc, USB_MIN(USB_HID_REPORT_DESC_LEN, req->wLength), 0U); status = REQ_SUPP; } else if (USB_DESCTYPE_HID == (req->wValue >> 8U)) { usb_transc_config(&udev->transc_in[0U], (uint8_t *)(&(hid_config_desc.hid_vendor)), USB_MIN(9U, req->wLength), 0U); } break; default: break; } return status; }
可以看到设备类请求有 获取空闲状态 设置空闲状态 获取协议 设置协议等,但是不是所有的请求的都会有,而且像这种请求(空闲状态和协议),只是用一个变量记录一下值,响应一下主机的请求,避免总线错误,至于值对我们通信在当前框架中是并没有影响。可以看到USB_GET_DESCRIPTOR下的描述符有两个,根据请求的不同,一个是 hid_report_desc HID报告描述符 和 hid_config_desc.hid_vendor 配置描述符中定义的一个HID特定描述符,传输长度配置最小值是原因是主机在获取描述符的时候它不知道描述符多长都会试探性的给一个获取长度,如果描述符本身小于获取长度,那么就返回全部描述符,这个是合理的,主机在发现获取的数据长度小于预计的获取长度就知道没有数据了,返回的数据长度不能超过主机预计的获取长度。
通信框架设计
在设备类初始化函数中定义实例。
数据缓冲区大小HID_IN_PACKET的定义在全局配置文件。
可以看出我们这个唯一的通信端点的最大包大小也是8,明明可以设置到64,因为在HID设备协议中规定HID的数据必须为报告的格式,至于报告的格式又根据设备类型有不同的规定,我们的键盘是输入设备,输入报告的格式,前两个字节用于报告ID等指示,后6字节用于键值数据。
应用分析
设备类文件对外函数。
hid_itfop_register
这个函数的设计是一个接口函数,本意为包含了一个用户键盘函数的接口,这个接口下应该包含两个功能,一个是配置,一个是数据处理。
刚好一个是键盘的初始化,这个函数会在设备类初始化函数中被调用,也就是在枚举阶段就被调用,复合一个初始化的情景,然后数据处理函数中调用了一个被封装的函数 hid_report_send 发送数据。
hid_report_send
这个函数发送数据只是一个简单的接口,它只能动了一个标志位,
hid->prev_transfer_complete = 0U;
看一下这个标志位对通信的影响,当发送一个报告到主机后清0;
这个函数是IN事务的回调,意思是在一个报告成功发送到主机时调用,可以看到当data[2]数据不为0时,表示一定发送了有效键值的报告到主机,这个时候将清除键值,在HID报告中0数据为无键值,表示空闲,然后再发送一次,这个目的是什么?是为了做一个空闲处理,意思是再每一次键值发送过去后都会自动跟随一个空闲数据,以实现在无键值的时候进入空闲状态。
然后这个函数发送的空闲数据也会再次触发这个函数,但是再次进来后数据已经为0,所以不会再发送,然后上述标志位置1,总体看下来,这个标志位并无实际作用,只是一个指示,当这个标志位为0时表示正在发送数据到主机,为1时为空闲状态。
键盘输入
在运行后重新上电,可以看到设备管理器中多了一个HID设备。
这里有6个的原因是,我本身的电脑的键盘占了5个,单片机占了1个,学习到了这里我们也应该知道,实际生活的产品在固件上的设计是经过很多考量的,可以看出我电脑上的这个键盘是一个复合的HID设备,它的物理设备是一个,但是逻辑设备可以是多个,取决于接口的设计,接口是一个独立的功能单元,比如我们日常生活的键盘是不是可以控制键值之外还可以控制多媒体(亮度声音)等等,这些功能独立为一个逻辑设备。
但是它们在设备管理器显示是一样的,这是为什么?
因为键盘也是免驱设备,随插随用,自动加载标准的HID驱动,所以加载出来的设备信息都是默认的信息,那为什么我们要用winusb设备,是因为HID类设备虽然是免驱设备, 但是HID规范中指出:HID为发送输入报告必须拥有一个中断输入端点。中断IN端点使HID能够在非可预期的时间内向主机发送信息,主机驱动程序将使用中断事务来周期性的轮询设备以获得数据。
中断端点只适合小批量实时数据传输,和我们需要的场景有冲突,所以这个时候就需要设计一个winusb类设备了。