【外设移植】USB设备之USB键盘+M61开发板

[复制链接]
查看3505 | 回复19 | 2024-1-8 17:32:01 | 显示全部楼层 |阅读模式

本帖最后由 bzhou830 于 2024-1-27 14:37 编辑

本帖最后由 bzhou830 于 2024-1-27 14:21 编辑

1. 介绍

为了能搞清楚USB协议,前面写过两遍关于USB协议基础的帖子,如果对USB协议不了解,可以去看看下列的基础内容。

【探索USB】01. USB协议基础 - 小安派S1&M61教程合集 - 物联网开发者社区-安信可论坛 - Powered by Discuz! (ai-thinker.com)

【探索USB】02. 设备的连接和枚举 - 小安派S1&M61教程合集 - 物联网开发者社区-安信可论坛 - Powered by Discuz! (ai-thinker.com)

这篇开始我们就使用M62板子来实现USB的设备开发,首先从最简单的USB HID键盘开始。固件的SDK使用的是CherryUSB开源USB固件。CherryUSB 是一个小而美的、可移植性高的、用于嵌入式系统的 USB 主从协议栈。设备端的协议执行流程如下图:

image.png

2. 描述符

CherryUSB给我们实现好了一套USB的设备协议,那我们就只需要使用的CheryyUSB提供的接口调用就可以实现自己想要的USB设备了。首先我们应该为我们的USB设备填描述符。描述符的种类很多,我们先得理解他们的含义。

设备 (Device) :就是一个实实在在的USB设备,比如USB鼠标,U盘。

配置(Configuration):一个USB设备可以有多种配置。比如4G上网卡就有2种配置:U盘、上网卡。第1次把4G上网卡插入电脑时,它是一个U盘,可以安装里面的程序。装好程序后,把它再次插入电脑,它就是一个上网卡。驱动程序可以选择让它工作于哪种配置,同一时间只能有一种配置有效。大多数的USB设备只有一种配置。

接口 (Interface) :每个配置下可以有多个接口,这个接口不是硬件上的接口,可以把这个接口理解为功能,一个接口就代表该设备当前支持的一种功能。

端点 (Endpoint) :每个接口可以有多个端点。USB主机和设备就是通过端点进行数据交互的。每个端点地址对应一个方向,例如端点2-IN,端点2-OUT,这两个含义完全不同。

对于HID设备来说还有HID描述符和HID报告描述符。

HID描述符:描述设备数据包的固定代码字节数组,包括设备支持多少个包,包有多大,以及包中每个字节和比特的含义。

HID报告描述符:HID报告描述符是一个硬编码的字节数组,用于描述设备的数据包。这包括:设备支持多少数据包,数据包有多大,以及数据包中每个字节和位的用途。

有关USB键盘鼠标的数据报格式可以看如下帖子:

USB-HID键鼠通讯数据格式 - 技术干货 - 物联网开发者社区-安信可论坛 - Powered by Discuz! (ai-thinker.com)

下面来一一看看他们都怎么写的。

2.1 设备描述符

cherryUSB中封装了宏来定义设备描述符

USB_DEVICE_DESCRIPTOR_INIT(USB_2_0, 0x00, 0x00, 0x00, USBD_VID, USBD_PID, 0x0002, 0x01)

详细说说这些字段表示的意思:

字段 释义
bLength 表示该描述符的长度。设备描述符的长度为18字节,写成十六进制就是0x12
bDeseriptorType 描述符的类型 。具体的取值如表355所列。设备描述符的编号为0x01。
bcdUSB 该设备所使用的USB协议的版本 。可以取20或者11等版本号。注意它是用BCD码来表示的例如USB20协议就是0x0200而USB1.1协议就是0x0110前面说过USB协议中使用的是小端结构,所以实际数据在传输时,是低字节在先的,也就是说USB20协议的bcdUSB拆成两个字节就是0x00和0x20。而USB1.1的 bcdUSB拆成两个字节就是0x10和0x01。
bDeviceClass 是设备所使用的类代码。设备的类代码由USB协会规定具体的类代码可查阅USB相关文档。对于大多数标准的USB设备类,该字段通常设置为0而在接口描述符中的bInterfaceClass中指定接口所实现的功能。当bDeviceClass为0时,下面的bDeviceSubClass也必须为0。如果bDeviceClass为0xFF表示是厂商自定义的设备类。
bDeviceSubClass 是设备所使用的子类代码。当类代码不为0和0xFF时子类代码由USB协议规定。当bDeviceClass为0时bDeviceSubClass也必须为0。
bDeviceProtocol 设备所使用的协议协议代码由USB协会规定。当该字段为0时表示设备不使用类所定义的协议。当该字段为0xFF时表示设备使用厂商自定义的协议。bDeviceProtocol必须要结合设备类和设备子类联合使用才有意义,因此当类代码为0时bDeviceProtocol应该也要为0。
bMaxPackeSize0 端点0的最大包长。它的取值可以为8、16、32、64。
idVender 厂商的ID号。该1D号由USB协会分配,不能随意使用。可以跟USB协会申请一个厂商ID号 。
idProduct 产品ID号。与厂商ID号不一样它是由生产厂商自己根据产品来编号的比较自由。
bcdDevice 设备版本号。当同一个产品升级后(例如修改了固件增加了某些功能)可以通过修改设备的版本号来区别。
iManufacturer 描述厂商的字符串的索引值,当该值为0时,表示没有厂商字符串。主机获取设备描述符时会将索引值放在wValue的第一字节中,用来选择不同的字符串。
iProduct 描述产品的字符串的索引值。当该值为0时,表示没有产品字符串。当第一次插上某个USB设备时,会在Windows的右下角弹出一个对话框,显示发现新硬件,并且会显示该设备的名称。其实这里显示的信息就是从产品字符串里获取来的。如果想让它显示出所需要的信息,应该修改产品字符串。
iSerialNumber 设备的序列号字符串索引值。最好给你的每个产品指定一个唯一的序列号好比每个英特尔的奔四处理器都有一个ID号一样。设备序列号可能被主机联合VID和PID用来区别不同的设备,有时同时连接多个具有相同VID.PID以及设备序列号的设备可能会导致设备无法正确识别。当该值为0时,表示没有序列号字符串。
bNumConfigurations 表示设备有多少种配置。每种配置都会有一个配置描述符主机通过发送设置配置来选择某一种配置。大部分的USB设备只有一个配置,即该字段的值为1。

2.2 配置描述符

同样的,cherryUSB中封装了宏来定义配置描述符

USB_CONFIG_DESCRIPTOR_INIT(USB_HID_CONFIG_DESC_SIZ, 0x01, 0x01, USB_CONFIG_BUS_POWERED, USBD_MAX_POWER),
字段 释义
bLength 表示该描述符的长度。标准的USB配置描述符的长度为9字节。
bDescriptorType 表示描述符的类型配置描述符的类型编码为0x02。
wTotalLength 表示整个配置描述符集合的总长度包括配置描述符、接口描述符、类特殊描述符(如果有)和端点描述符。注意低字节在先。
bNumInterfaces 表示该配置所支持的接口数量。通常功能单一的设备只具有一个接口(例如鼠标)而复合设备则具有多个接口(例如音频设备)。
bConfiguration 表示该配置的值。通常一个USB设备可以支持多个配置.bConfiguration就是每个配置的标识。设置配置请求时会发送一个配置值如果某个配置的bConfiguration值与它相匹配,就表示该配置被激活,为当前配置。
iConfiguration 大小为1字节,是描述该配置的字符串的索引值。如果该值为0则表示没有字符串。
bmAttributes 大小为1字节用来描述设备的一些特性。其中D7是保留的,必须要设置为1。D6表示供电方式,当D6为1时表示设备是自供电的;当D6为0时,表示设备是总线供电的。D5表示是否支持远程唤醒当D5为1时,支持远程唤醒。D4~D0保留,设置为0。
bMaxPower 大小为1字节,表示设备需要从总线获取的最大电流量,单位为2mA。例如如果需要200mA的最大电流,则该字节的值为100。

2.3 接口描述符

接口描述符是需要我们自己写成数组的:

    0x09,                          /* bLength: Interface Descriptor size */
    USB_DESCRIPTOR_TYPE_INTERFACE, /* bDescriptorType: Interface descriptor type */
    0x00,                          /* bInterfaceNumber: Number of Interface */
    0x00,                          /* bAlternateSetting: Alternate setting */
    0x01,                          /* bNumEndpoints */
    0x03,                          /* bInterfaceClass: HID */
    0x01,                          /* bInterfaceSubClass : 1=BOOT, 0=no boot */
    0x01,                          /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
    0,                             /* iConfiguration: Index of string descriptor */
字段 释义
bLength 大小为1字节,表示该描述符的长度。标准的USB接口描述符的长度为9字节。
bDescriptorType 大小为1字节,是描述符的类型。接口描述符的类型编码为0x04。
bInterfaceNumber 大小为1字节,表示该接口的编号。当一个配置具有多个接口时,每个接口的编号都不相同。从О开始依次递增对一-个配置的接口进行编号。
bAlternateSetting 大小为1字节,是该接口的备用编号。编号规则与 bInterfaceNumber一样,很少会使用该字段,设置为0。
bNumEndpoints 大小为1字节,是该接口所使用的端点数(不包括О端点)。如果该字段为0,则表示没有非О端点,只使用默认的控制端点。
blnterfaceClass, bInterfaceSubClass, bInterfaceProtocol 分别是接口所使用的类、子类以及协议﹐它们的代码由USB协会定义,跟设备描述符中的意义类似。通常在接口中定义设备的功能﹐而在设备描述符中将类、子类以及协议字段的值设置为0。
iConfiguration 大小为1字节,是描述该接口的字符串的索引值。如果该值为0,则表示没有字符串。

2.4 端点描述符

    0x07,                         /* bLength: Endpoint Descriptor size */
    USB_DESCRIPTOR_TYPE_ENDPOINT, /* bDescriptorType: */
    HID_INT_EP,                   /* bEndpointAddress: Endpoint Address (IN) */
    0x03,                         /* bmAttributes: Interrupt endpoint */
    HID_INT_EP_SIZE,              /* wMaxPacketSize: 4 Byte max */
    0x00,
    HID_INT_EP_INTERVAL, /* bInterval: Polling Interval */
    /* 34 */
    ///////////////////////////////////////
    /// string0 descriptor
    ///////////////////////////////////////
    USB_LANGID_INIT(USBD_LANGID_STRING),
    ///////////////////////////////////////
    /// string1 descriptor
    ///////////////////////////////////////
    0x14,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    'C', 0x00,                  /* wcChar0 */
    'h', 0x00,                  /* wcChar1 */
    'e', 0x00,                  /* wcChar2 */
    'r', 0x00,                  /* wcChar3 */
    'r', 0x00,                  /* wcChar4 */
    'y', 0x00,                  /* wcChar5 */
    'U', 0x00,                  /* wcChar6 */
    'S', 0x00,                  /* wcChar7 */
    'B', 0x00,                  /* wcChar8 */
    ///////////////////////////////////////
    /// string2 descriptor
    ///////////////////////////////////////
    0x26,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    'C', 0x00,                  /* wcChar0 */
    'h', 0x00,                  /* wcChar1 */
    'e', 0x00,                  /* wcChar2 */
    'r', 0x00,                  /* wcChar3 */
    'r', 0x00,                  /* wcChar4 */
    'y', 0x00,                  /* wcChar5 */
    'U', 0x00,                  /* wcChar6 */
    'S', 0x00,                  /* wcChar7 */
    'B', 0x00,                  /* wcChar8 */
    ' ', 0x00,                  /* wcChar9 */
    'H', 0x00,                  /* wcChar10 */
    'I', 0x00,                  /* wcChar11 */
    'D', 0x00,                  /* wcChar12 */
    ' ', 0x00,                  /* wcChar13 */
    'D', 0x00,                  /* wcChar14 */
    'E', 0x00,                  /* wcChar15 */
    'M', 0x00,                  /* wcChar16 */
    'O', 0x00,                  /* wcChar17 */
    ///////////////////////////////////////
    /// string3 descriptor
    ///////////////////////////////////////
    0x16,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    '2', 0x00,                  /* wcChar0 */
    '0', 0x00,                  /* wcChar1 */
    '2', 0x00,                  /* wcChar2 */
    '2', 0x00,                  /* wcChar3 */
    '1', 0x00,                  /* wcChar4 */
    '2', 0x00,                  /* wcChar5 */
    '3', 0x00,                  /* wcChar6 */
    '4', 0x00,                  /* wcChar7 */
    '5', 0x00,                  /* wcChar8 */
    '6', 0x00,                  /* wcChar9 */
    0x00
字段 释义
bLength 大小为1字节,表示该描述符的长度。标准的USB端点描述符的长度为5字节。
bDescriptorType 大小为1字节,表示描述符的类型。端点描述符的类型编码为0x05。
bEndpointAddress 大小为1字节,表示该端点的地址。最高位D7为该端点的传输方向,1为输人(有点像Input的第一个字母),0为输出(有点像Output的第一个字母)。D3~D0为端点号。D6~D4保留,设为0。
bmAttributes 大小为1字节,是该端点的属性。最低两位DI~DO表示该端点的传输类型,0为控制传输,1为等时传输,2为批量传输,3为中断传输。如果该端点是非等时传输的端点,那么D7~D2为保留值,设为0。如果该端点是等时传输的,则 D3~2表示同步的类型,0为无同步,1为异步,2为适配,3为同步;D5~D4表示用途,0为数据端点,1为反馈端点,2为暗含反馈的数据端点,3是保留值。D7~D6保留。
wMaxPackeSize 大小为1字节,是该端点所支持的最大包长度。注意低字节在先。对于全速模式和低速模式,D10~DO表示端点的最大包长,其他位保留为0。对于高速模式,D12~D11为每个帧附加的传输次数,具体请参看USB2.0协议。
bInterval 大小为1字节,表示该端点查询的时间。对于中断端点﹐表示查询的帧间隔数。对于等时传输以及高速模式的中断,批量传输﹐该字段的意义请参看USB2.0协议。

2.5 HID描述符

    0x09,                    /* bLength: HID Descriptor size */
    HID_DESCRIPTOR_TYPE_HID, /* bDescriptorType: HID */
    0x11,                    /* bcdHID: HID Class Spec release number */
    0x01,
    0x00,                          /* bCountryCode: Hardware target country */
    0x01,                          /* bNumDescriptors: Number of HID class descriptors to follow */
    0x22,                          /* bDescriptorType */
    HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength: Total length of Report descriptor */
    0x00,
字段 释义
bLength 大小为1字节,是该描述符的总长度。它的大小与该描述符中下级描述符的个数有关。例如,只有一个下级描述符时,总长度为1+1+2十1+1+1+2=9字节。
bDescriptorType 大小为1字节,是该描述符的编号。HID描述符的编号为0x21。
bcdHID 大小为2字节,是该设备所使用的HID协议的版本号。
bCountyCode 大小为1字节,是设备所适用的国家。通常我们的键盘是美式键盘,代码为33,即0x21。
bNumDescriptors 大小为1字节,是下级描述符的数量。该值至少为1,即至少要有一个报告描述符。下级描述符可以是报告描述符或物理描述符。
bDescriptorType 大小为1字节,是下级描述符的类型。报告描述符的编号为0x22,物理描述符编号为0x23。
bDescriptorLength 大小为2字节,是下级描述符的长度。当有多个下级描述符时, bDescritporType和 bDescriptorLength交替重复下去。

2.6 HID报告描述符

static const uint8_t hid_keyboard_report_desc[HID_KEYBOARD_REPORT_DESC_SIZE] = {
    0x05, 0x01, // USAGE_PAGE (Generic Desktop)
    0x09, 0x06, // USAGE (Keyboard)
    0xa1, 0x01, // COLLECTION (Application)
    0x05, 0x07, // USAGE_PAGE (Keyboard)
    0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
    0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0x01, // LOGICAL_MAXIMUM (1)
    0x75, 0x01, // REPORT_SIZE (1)
    0x95, 0x08, // REPORT_COUNT (8)
    0x81, 0x02, // INPUT (Data,Var,Abs)
    0x95, 0x01, // REPORT_COUNT (1)
    0x75, 0x08, // REPORT_SIZE (8)
    0x81, 0x03, // INPUT (Cnst,Var,Abs)
    0x95, 0x05, // REPORT_COUNT (5)
    0x75, 0x01, // REPORT_SIZE (1)
    0x05, 0x08, // USAGE_PAGE (LEDs)
    0x19, 0x01, // USAGE_MINIMUM (Num Lock)
    0x29, 0x05, // USAGE_MAXIMUM (Kana)
    0x91, 0x02, // OUTPUT (Data,Var,Abs)
    0x95, 0x01, // REPORT_COUNT (1)
    0x75, 0x03, // REPORT_SIZE (3)
    0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
    0x95, 0x06, // REPORT_COUNT (6)
    0x75, 0x08, // REPORT_SIZE (8)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0xFF, // LOGICAL_MAXIMUM (255)
    0x05, 0x07, // USAGE_PAGE (Keyboard)
    0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00, // INPUT (Data,Ary,Abs)
    0xc0        // END_COLLECTION
};

3. 设备的实现

在设备连接时设备端就需要向主机上报各种描述符,主机这时候就会识别到设备然后给USB设备分配端口(USB协议基础中的设备枚举)。

连接完成后设备就可以通过usbd_ep_start_write函数(cherryUSB提供的API)向主机上报数据了。

#include "usbd_core.h"
#include "usbd_hid.h"

#define USBD_VID           0xffff
#define USBD_PID           0xffff
#define USBD_MAX_POWER     100
#define USBD_LANGID_STRING 1033

#define HID_INT_EP          0x81
#define HID_INT_EP_SIZE     8
#define HID_INT_EP_INTERVAL 10

#define USB_HID_CONFIG_DESC_SIZ       34
#define HID_KEYBOARD_REPORT_DESC_SIZE 63

static const uint8_t hid_descriptor[] = {
    USB_DEVICE_DESCRIPTOR_INIT(USB_2_0, 0x00, 0x00, 0x00, USBD_VID, USBD_PID, 0x0002, 0x01),
    USB_CONFIG_DESCRIPTOR_INIT(USB_HID_CONFIG_DESC_SIZ, 0x01, 0x01, USB_CONFIG_BUS_POWERED, USBD_MAX_POWER),

    /************** Descriptor of Joystick Mouse interface ****************/
    /* 09 */
    0x09,                          /* bLength: Interface Descriptor size */
    USB_DESCRIPTOR_TYPE_INTERFACE, /* bDescriptorType: Interface descriptor type */
    0x00,                          /* bInterfaceNumber: Number of Interface */
    0x00,                          /* bAlternateSetting: Alternate setting */
    0x01,                          /* bNumEndpoints */
    0x03,                          /* bInterfaceClass: HID */
    0x01,                          /* bInterfaceSubClass : 1=BOOT, 0=no boot */
    0x01,                          /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
    0,                             /* iInterface: Index of string descriptor */
    /******************** Descriptor of Joystick Mouse HID ********************/
    /* 18 */
    0x09,                    /* bLength: HID Descriptor size */
    HID_DESCRIPTOR_TYPE_HID, /* bDescriptorType: HID */
    0x11,                    /* bcdHID: HID Class Spec release number */
    0x01,
    0x00,                          /* bCountryCode: Hardware target country */
    0x01,                          /* bNumDescriptors: Number of HID class descriptors to follow */
    0x22,                          /* bDescriptorType */
    HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength: Total length of Report descriptor */
    0x00,
    /******************** Descriptor of Mouse endpoint ********************/
    /* 27 */
    0x07,                         /* bLength: Endpoint Descriptor size */
    USB_DESCRIPTOR_TYPE_ENDPOINT, /* bDescriptorType: */
    HID_INT_EP,                   /* bEndpointAddress: Endpoint Address (IN) */
    0x03,                         /* bmAttributes: Interrupt endpoint */
    HID_INT_EP_SIZE,              /* wMaxPacketSize: 4 Byte max */
    0x00,
    HID_INT_EP_INTERVAL, /* bInterval: Polling Interval */
    /* 34 */
    ///////////////////////////////////////
    /// string0 descriptor
    ///////////////////////////////////////
    USB_LANGID_INIT(USBD_LANGID_STRING),
    ///////////////////////////////////////
    /// string1 descriptor
    ///////////////////////////////////////
    0x14,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    'C', 0x00,                  /* wcChar0 */
    'h', 0x00,                  /* wcChar1 */
    'e', 0x00,                  /* wcChar2 */
    'r', 0x00,                  /* wcChar3 */
    'r', 0x00,                  /* wcChar4 */
    'y', 0x00,                  /* wcChar5 */
    'U', 0x00,                  /* wcChar6 */
    'S', 0x00,                  /* wcChar7 */
    'B', 0x00,                  /* wcChar8 */
    ///////////////////////////////////////
    /// string2 descriptor
    ///////////////////////////////////////
    0x26,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    'C', 0x00,                  /* wcChar0 */
    'h', 0x00,                  /* wcChar1 */
    'e', 0x00,                  /* wcChar2 */
    'r', 0x00,                  /* wcChar3 */
    'r', 0x00,                  /* wcChar4 */
    'y', 0x00,                  /* wcChar5 */
    'U', 0x00,                  /* wcChar6 */
    'S', 0x00,                  /* wcChar7 */
    'B', 0x00,                  /* wcChar8 */
    ' ', 0x00,                  /* wcChar9 */
    'H', 0x00,                  /* wcChar10 */
    'I', 0x00,                  /* wcChar11 */
    'D', 0x00,                  /* wcChar12 */
    ' ', 0x00,                  /* wcChar13 */
    'D', 0x00,                  /* wcChar14 */
    'E', 0x00,                  /* wcChar15 */
    'M', 0x00,                  /* wcChar16 */
    'O', 0x00,                  /* wcChar17 */
    ///////////////////////////////////////
    /// string3 descriptor
    ///////////////////////////////////////
    0x16,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    '2', 0x00,                  /* wcChar0 */
    '0', 0x00,                  /* wcChar1 */
    '2', 0x00,                  /* wcChar2 */
    '2', 0x00,                  /* wcChar3 */
    '1', 0x00,                  /* wcChar4 */
    '2', 0x00,                  /* wcChar5 */
    '3', 0x00,                  /* wcChar6 */
    '4', 0x00,                  /* wcChar7 */
    '5', 0x00,                  /* wcChar8 */
    '6', 0x00,                  /* wcChar9 */
#ifdef CONFIG_USB_HS
    ///////////////////////////////////////
    /// device qualifier descriptor
    ///////////////////////////////////////
    0x0a,
    USB_DESCRIPTOR_TYPE_DEVICE_QUALIFIER,
    0x00,
    0x02,
    0x00,
    0x00,
    0x00,
    0x40,
    0x01,
    0x00,
#endif
    0x00
};

/* USB HID device Configuration Descriptor */
static uint8_t hid_desc[9] __ALIGN_END = {
    /* 18 */
    0x09,                    /* bLength: HID Descriptor size */
    HID_DESCRIPTOR_TYPE_HID, /* bDescriptorType: HID */
    0x11,                    /* bcdHID: HID Class Spec release number */
    0x01,
    0x00,                          /* bCountryCode: Hardware target country */
    0x01,                          /* bNumDescriptors: Number of HID class descriptors to follow */
    0x22,                          /* bDescriptorType */
    HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength: Total length of Report descriptor */
    0x00,
};

static const uint8_t hid_keyboard_report_desc[HID_KEYBOARD_REPORT_DESC_SIZE] = {
    0x05, 0x01, // USAGE_PAGE (Generic Desktop)
    0x09, 0x06, // USAGE (Keyboard)
    0xa1, 0x01, // COLLECTION (Application)
    0x05, 0x07, // USAGE_PAGE (Keyboard)
    0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
    0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0x01, // LOGICAL_MAXIMUM (1)
    0x75, 0x01, // REPORT_SIZE (1)
    0x95, 0x08, // REPORT_COUNT (8)
    0x81, 0x02, // INPUT (Data,Var,Abs)
    0x95, 0x01, // REPORT_COUNT (1)
    0x75, 0x08, // REPORT_SIZE (8)
    0x81, 0x03, // INPUT (Cnst,Var,Abs)
    0x95, 0x05, // REPORT_COUNT (5)
    0x75, 0x01, // REPORT_SIZE (1)
    0x05, 0x08, // USAGE_PAGE (LEDs)
    0x19, 0x01, // USAGE_MINIMUM (Num Lock)
    0x29, 0x05, // USAGE_MAXIMUM (Kana)
    0x91, 0x02, // OUTPUT (Data,Var,Abs)
    0x95, 0x01, // REPORT_COUNT (1)
    0x75, 0x03, // REPORT_SIZE (3)
    0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
    0x95, 0x06, // REPORT_COUNT (6)
    0x75, 0x08, // REPORT_SIZE (8)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0xFF, // LOGICAL_MAXIMUM (255)
    0x05, 0x07, // USAGE_PAGE (Keyboard)
    0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00, // INPUT (Data,Ary,Abs)
    0xc0        // END_COLLECTION
};

void usbd_configure_done_callback(void)
{
    /* no out ep, do nothing */
}

#define HID_STATE_IDLE 0
#define HID_STATE_BUSY 1

/*!< hid state ! Data can be sent only when state is idle  */
static volatile uint8_t hid_state = HID_STATE_IDLE;

void usbd_hid_int_callback(uint8_t ep, uint32_t nbytes)
{
    hid_state = HID_STATE_IDLE;
}

static struct usbd_endpoint hid_in_ep = {
    .ep_cb = usbd_hid_int_callback,
    .ep_addr = HID_INT_EP
};

struct usbd_interface intf0;

void hid_keyboard_init(void)
{
    usbd_desc_register(hid_descriptor);
    usbd_add_interface(usbd_hid_init_intf(&intf0, hid_keyboard_report_desc, HID_KEYBOARD_REPORT_DESC_SIZE));
    usbd_add_endpoint(&hid_in_ep);

    usbd_initialize();
}

void hid_keyboard_test(void)
{
    uint8_t sendbuffer[8] = { 0x00, 0x00, HID_KBD_USAGE_A, 0x00, 0x00, 0x00, 0x00, 0x00 }; //A

    bflb_l1c_dcache_clean_range(sendbuffer, 8);
    int ret = usbd_ep_start_write(HID_INT_EP, sendbuffer, 8);
    if (ret < 0) {
        return;
    }
    hid_state = HID_STATE_BUSY;
    while (hid_state == HID_STATE_BUSY) {
    }
}

在主函数中调用它, 如下就实现了一个不断向主机发送字母a的usb键盘设备。

#include <FreeRTOS.h>
#include "task.h"
#include "usbh_core.h"
#include "bflb_mtimer.h"
#include "board.h"

#define KEYBOARD_STACK_SIZE (1536)
#define KEYBOARD_TASK_PRIORITY (16)

extern void hid_keyboard_init(void);
extern void hid_keyboard_test(void);

static TaskHandle_t keyboard_task_hd;

void keyboard_task(void *params)
{
    hid_keyboard_init();
    while(1)
    {
        hid_keyboard_test();
        bflb_mtimer_delay_ms(500);
    }
}


int main(void)
{
    board_init();
    xTaskCreate(keyboard_task, "keyboard", 
        KEYBOARD_STACK_SIZE, NULL, KEYBOARD_TASK_PRIORITY, &keyboard_task_hd);
    vTaskStartScheduler();
    while (1) {
    }
}

为了能和真实的键盘对应上,这里特意使用矩阵键盘接上板子来读取矩阵键盘的键值,将对应的键值转换成对应的hid报告发送给电脑端。

image.png

硬件的接线顺序如下:

// 矩阵键盘的行线和列线的定义
const uint8_t rows[] = { GPIO_PIN_33, GPIO_PIN_32, GPIO_PIN_31, GPIO_PIN_30};
const uint8_t cols[] = { GPIO_PIN_29, GPIO_PIN_27, GPIO_PIN_25};

在矩阵键盘扫描的task里面循环扫描矩阵键盘,如果按键有按下直接转换成对应的hid键值:

void matrix_keys_task(void *params)
{
    int key_val = 0;
    matrix_keys_init();
    while(1)
    {
        key_val = get_key_val();
        if(key_val != 0)
        {
            printf("%x\r\n", key_val);
            switch(key_val)
            {
                case 0x0001:
                    key_val = 0x1e; // 1
                    break;
                case 0x0002:
                    key_val = 0x1f; // 2
                    break;
                case 0x0004:
                    key_val = 0x20; // 3
                    break;
                case 0x0008:
                    key_val = 0x21; // 4
                    break;
                case 0x0010:
                    key_val = 0x22; // 5
                    break;
                case 0x0020:
                    key_val = 0x23; // 6
                    break;
                case 0x0040:
                    key_val = 0x24; // 7
                    break;
                case 0x0080:
                    key_val = 0x25; // 8
                    break;
                case 0x0100:
                    key_val = 0x26; // 9
                    break;
                case 0x0200:
                    key_val = HID_KBD_USAGE_A;   // A
                    break;
                case 0x0400:
                    key_val = 0x27; //0
                    break;
                case 0x0800:
                    key_val = HID_KBD_USAGE_A + 1; // B
                    break;
            }

            if (xQueueSend(xQueue, &key_val, portMAX_DELAY) != pdPASS) 
            {
            }
        }
        bflb_mtimer_delay_ms(10);
    }
}

这些键值的定义可以在这里找到

image.png

在usb键盘的task里面则取接受消息,并把消息发松给pc端。

void keyboard_task(void *params)
{
    hid_keyboard_init();
    uint16_t receivedData = 0;
    while(1)
    {
        if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) 
        {
            printf("receivedData : %x\r\n", receivedData);
            // 处理接收到的数据
            hid_keyboard_test();
        }
        bflb_mtimer_delay_ms(500);
    }
}

4. USB描述符的查看

Windows下可以使用usbview来查看usb描述符信息,这有利于我们调试分析使用。

Snipaste_2024-01-08_16-40-05.png

5. USB协议调试软件

usb协议相对还是比较复杂的,任何的描述符写错了都可能导致我们的设备没办法正常的工作。这时候好用的调试软件就非常的重要了。

USBlyzer, WireShark都是可以用来抓USB数据包进行分析的。

本帖被以下淘专辑推荐:

选择去发光,而不是被照亮
回复

使用道具 举报

bzhou830 | 2024-1-8 17:33:25 | 显示全部楼层
本帖最后由 bzhou830 于 2024-4-23 09:32 编辑

代码在这里

08matrix_keys.zip

8.86 KB, 下载次数: 13

选择去发光,而不是被照亮
回复 支持 反对

使用道具 举报

爱笑 | 2024-1-8 17:43:07 | 显示全部楼层
效率挺高啊玛丽哥
用心做好保姆工作
回复 支持 反对

使用道具 举报

bzhou830 | 2024-1-8 17:58:28 | 显示全部楼层
爱笑 发表于 2024-1-8 17:43
效率挺高啊玛丽哥

年底了,要冲一冲
选择去发光,而不是被照亮
回复 支持 反对

使用道具 举报

1084504793 | 2024-1-8 18:47:24 | 显示全部楼层
回复

使用道具 举报

干簧管 | 2024-1-8 19:12:54 | 显示全部楼层
玛丽哥威武
回复 支持 反对

使用道具 举报

WT_0213 | 2024-1-9 09:50:48 | 显示全部楼层
厉害
回复

使用道具 举报

san | 2024-1-9 23:25:10 | 显示全部楼层
学习
回复

使用道具 举报

爱笑 | 2024-1-17 10:13:20 | 显示全部楼层
玛丽哥,你需要加上矩阵键盘的扫面实现相应的键位!
用心做好保姆工作
回复 支持 反对

使用道具 举报

bzhou830 | 2024-1-17 10:20:31 | 显示全部楼层
爱笑 发表于 2024-1-17 10:13
玛丽哥,你需要加上矩阵键盘的扫面实现相应的键位!

好的,
选择去发光,而不是被照亮
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则