关于STM32的USB设备库DIY机械键盘

前言

为什么想写这个呢,首先一方面是因为自己喜欢DIY一些小玩意,另一方面关于USB-HID的东西断断续续的学习了不少东西,想总结整理一下。其次就是网络上关于STM32制作USB-HID的案例很多,我这里尽量说一些不一样的,以便大家参考,内容主要还是关于机械键盘方面的。

本来早就应该写的,自己懒,然后又喜欢拖延,最近想着利用下班的时间将它陆续整理完善吧。

DIY机械键盘相比于客制化机械键盘更为彻底,也没有客制化出来的机械键盘好用,我追寻的只是DIY给我带来的乐趣与成就感。

如果只是想要一把好用的键盘,建议还是买一把量产键盘。客制化烧钱,有这个钱,能卖高至少一个等级的量产键盘。

前后做了三种键盘,GH60,Keycool84,还有标准的87配列。有的送人了,有的还在自己手上。其中GH60和Keycool84的PCB是自己设计的,代码也是自己敲的,很丑陋,代码习惯不好,就不放出来献丑了,后面主要还是告诉各位实现原理与方法。

一 STM32的设备库与USB协议

STM32的设备库中有很多的例程,比如DFU,MSC,HID等等,或者直接使用STM32CubeMX生成的CoustomHID工程也可以一样使用,如何移植,如何让自己的PCBA在电脑上成功的识别为一个HID设备,我这里就不过多赘述了,网上有大量的例子可以参考。因为库中的例子都是依托STM32的评估板来做的,所以修改起来会比较麻烦,倒不如后一种方法省事。

重点说一句,就是HID设备的特别之处就在于HID描述符和报告描述符,你这个设备发送给电脑的数据有啥用,需要电脑给你什么数据,都是通过报告描述符来实现的。而这两个描述符中的各个数据有什么作用,在HID Usage Table这个文档中都有说明,而且,写的很详细。但是,但是,但是是英文文档,又臭又长,看了下一段,忘了上一段。所以不想去看的也可以直接复制下面的描述符直接用。

1.1 设备描述符

const uint8_t CustomHID_DeviceDescriptor[CUSTOMHID_SIZ_DEVICE_DESC] =
  {
    0x12,                       /*bLength:18Bytes */
    USB_DEVICE_DESCRIPTOR_TYPE, /*bDescriptorType*/
    0x00,                       /*bcdUSB */
    0x02,
    0x00,                       /*bDeviceClass*/
    0x00,                       /*bDeviceSubClass*/
    0x00,                       /*bDeviceProtocol*/
    0x40,                       /*bMaxPacketSize:64Bytes*/
    0x83,                       /*idVendor (0x0483)*/
    0x04,
    0x12,                       /*idProduct = 0x5750*/
    0x34,
    0x00,                       /*bcdDevice rel. 2.00*/
    0x02,
    1,                          /*Index of string descriptor describing
                                              manufacturer */
    2,                          /*Index of string descriptor describing
                                             product*/
    3,                          /*Index of string descriptor describing the
                                             device serial number */
    0x01                        /*bNumConfigurations*/
  }

关于设备描述符,我只说一个点,即idVendor和idProduct这两个数据。这两个数值是不能随便定义的,但是你随便定义也没有关系。我在调试的过程中发现这样的一个问题,如果我更改了譬如配置描述符的内容,但是没有更改上面这两个数据,电脑就显示上一次的设备,而不是你更改了的设备。只有在更改了这两个数据以后,PC才会重新识别这个设备。这个情况在Win7上是如此。Win10没有出现过。

1.2 配置描述符

    0x09, /* bLength: Configuration Descriptor size */
    USB_CONFIGURATION_DESCRIPTOR_TYPE, /* bDescriptorType: Configuration */
    CUSTOMHID_SIZ_CONFIG_DESC,
    /* wTotalLength: Bytes returned */
    0x00,
    0x01,         /* bNumInterfaces: 1 interface */
    0x01,         /* bConfigurationValue: Configuration value */
    0x00,         /* iConfiguration: Index of string descriptor describing
                                 the configuration*/
    0x80,         /* bmAttributes: Self powered */
    0x64,         /* MaxPower 200 mA: this current is used for detecting Vbus */

关于配置描述符,bNumInterfaces说的是你这个设备有多少个接口。还有最后一个0xC8这个数据表示你这个设备需要从总线上获取多少电流,这个视你设备功耗决定。0xC8 * 2 = 400mA,最大能获取500mA的电流。

1.3 接口描述符

    0x09,         /* bLength: Interface Descriptor size */
    USB_INTERFACE_DESCRIPTOR_TYPE,/* bDescriptorType: Interface descriptor type */
    0x00,         /* bInterfaceNumber: Number of Interface */
    0x00,         /* bAlternateSetting: Alternate setting */
    0x02,         /* bNumEndpoints,except endpoint 0 */
    0x03,         /* bInterfaceClass: HID */
    0x01,         /* bInterfaceSubClass : 1=BOOT, 0=no boot */
    0x01,         /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
    0,            /* iInterface: Index of string descriptor */

关于接口描述符,如果只需要一个接口,直接复制粘贴就能用。另外注意的是,多接口复合设备中,只能有一个Boot设备,即bInterfaceSubClass这个变量,只有一个接口可以为1,其他的为0。具体为什么,我也不清楚,有知道的朋友可以回复一下,谢谢。因为我有一次定义了两个Boot接口,设备没有枚举成功。

1.4 HID描述符

    0x09,         /* bLength: HID Descriptor size */
    HID_DESCRIPTOR_TYPE, /* bDescriptorType: HID */
    0x10,         /* bcdHID: HID Class Spec release number */
    0x01,
    0x00,         /* bCountryCode: Hardware target country */
    0x01,         /* bNumDescriptors: Number of HID class descriptors to follow */
    0x22,         /* bDescriptorType */
    CUSTOMHID_SIZ_REPORT_DESC,/* wItemLength: Total length of Report descriptor */
    0x00,

关于HID描述符,CUSTOMHID_SIZ_REPORT_DESC这个值需要根据报告描述符的长度来修改。

1.5 端点描述符

    0x07,          /* bLength: Endpoint Descriptor size */
    USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */

    0x81,          /* bEndpointAddress: Endpoint Address (IN) */
    0x03,          /* bmAttributes: Interrupt endpoint */
    0x08,          /* wMaxPacketSize: 8 Bytes max */
    0x00,
    0x05,          /* bInterval: Polling Interval (5 ms) */
    
    0x07,	/* bLength: Endpoint Descriptor size */
    USB_ENDPOINT_DESCRIPTOR_TYPE,	/* bDescriptorType: */
			/*	Endpoint descriptor type */
    0x02,	/* bEndpointAddress:Endpoint Address (OUT) */
    0x03,	/* bmAttributes: Interrupt endpoint */
    0x01,	/* wMaxPacketSize: 1 Bytes max  */
    0x00,
    0x05,	/* bInterval: Polling Interval (5 ms) */

关于端点描述符,bEndpointAddress表示端点地址,表示当前这个接口所需要的端点资源,输入(相对于主机而言)端点最高位为1,输出(相对于主机而言)端点最高位为0。然后说一句,HID设备一般都是使用中断端点进行数据传输。wMaxPacketSize表示该端点上数据传输的数量。bInterval表示主机查询设备数据的时间间隔,如果设置的太长,则键盘输入延迟很高,深有体会。

1.6 报告描述符

    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   /* 63 */

关于报告描述符,具体每一个数据什么意思,可以参考前面说的文档HID Usage Table。这个就是标准6KRO键盘的报告描述符。

至于还有 字符串 描述符,不说了,有用,用处不大,不修改直接用,没有一点关系。

二 多媒体按键的实现方法

2.1 关于复合设备和组合设备

我倒不是去纠结两个概念的含义,只是想说目前比较主流的实现多媒体按键的方法就上面两种。其实多个接口一个设备的这种设备按照定义应该称之为组合设备。我下面讲的方法也是通过组合设备,即一个设备下多个接口描述符来实现的。
说明如下,才发现没有Visio,就用这个简单示意一下

就是说一个配置描述符下可以挂多个接口描述符,每个接口做自己的事情,多媒体按键就是通过另外一个接口来实现的。

2.2 多媒体按键的实现方法

如果一个HID设备中需要实现标准键盘,鼠标,多媒体按键功能,第一种方法就是通过三个接口来实现。第二种方法就是只使用两个接口,第二个接口通过报告ID这么一个字段,来向PC机报告我这一包数据是鼠标的数据还是多媒体按键数据。还有第三种方法,就是前面说的复合设备,即把多个设备作为一个hub,这样也可以实现(这个方法我没有证实过)。

    /* system control */
    0x05, 0x01,                      // USAGE_PAGE (Generic Desktop)
    0x09, 0x80,                      // USAGE (System Control)
    0xa1, 0x01,                      // COLLECTION (Application)
    0x85, 0x02,                      //   REPORT_ID (2)
    0x15, 0x01,                      //   LOGICAL_MINIMUM (0x1)
    0x26, 0xb7, 0x00,                //   LOGICAL_MAXIMUM (0xb7)
    0x19, 0x01,                      //   USAGE_MINIMUM (0x1)
    0x29, 0xb7,                      //   USAGE_MAXIMUM (0xb7)
    0x75, 0x10,                      //   REPORT_SIZE (16)
    0x95, 0x01,                      //   REPORT_COUNT (1)
    0x81, 0x00,                      //   INPUT (Data,Array,Abs)
    0xc0,                            // END_COLLECTION    /* 24 */
    /* consumer */
    0x05, 0x0c,                      // USAGE_PAGE (Consumer Devices)
    0x09, 0x01,                      // USAGE (Consumer Control)
    0xa1, 0x01,                      // COLLECTION (Application)
    0x85, 0x03,                      //   REPORT_ID (3)
    0x15, 0x01,                      //   LOGICAL_MINIMUM (0x1)
    0x26, 0x9c, 0x02,                //   LOGICAL_MAXIMUM (0x29c)
    0x19, 0x01,                      //   USAGE_MINIMUM (0x1)
    0x2a, 0x9c, 0x02,                //   USAGE_MAXIMUM (0x29c)
    0x75, 0x10,                      //   REPORT_SIZE (16)
    0x95, 0x01,                      //   REPORT_COUNT (1)
    0x81, 0x00,                      //   INPUT (Data,Array,Abs)
    0xc0,                            // END_COLLECTION    /* 25 */

上面的代码中,仅展示了一个例子,使用Report ID来完成Consumer数据和System Control数据的区分。比如报告ID为2的字段表示这一帧的数据是System Control数据,如果报告ID为3,则表示这次的数据是Consumer数据。

额外提一句,如果使用的报告ID,那么发送给PC机的数据就需要附上报告ID这个字节,且这个ID必须在所有数据的最前面。

2.3 如何在键盘的工程上添加这个功能

1、首先在usb_desc.c文件中得定义这个多媒体的报告描述符,也有在里面包含鼠标的数据的,这个随意,只要复合HID标准即可。然后在对应的.h文件中也要声明这个变量。
2、修改前面说的配置描述符,bNumInterfaces这个字段更改为2,表示有两个接口。
3、在第一个接口描述符后面,添加第二个接口的描述符,bInterfaceNumber这个字段改为1,这是序号为1(起始序号为0)的接口描述符,然后依次添加这个接口的HID描述符和端点描述符。
4、在usb_conf.h中添加相应的端点资源。用到哪个端点,就添加哪个端点。

#define BTABLE_ADDRESS      (0x00)

/* EP0  */
/* rx/tx buffer base address */
#define ENDP0_RXADDR        (0x30)
#define ENDP0_TXADDR        (0x70)

/* EP1  */
/* tx buffer base address */
#define ENDP1_TXADDR        (0xB0)

/* EP2  */
/* Rx buffer base address */
#define ENDP2_RXADDR        (0xF0)

/* EP3  */
/* Tx buffer base address */
#define ENDP3_TXADDR        (0x130)

比如说,我这里用到了三个端点,除了端点0 (控制端点)以外,端点1用来发送键盘数据,端点2用来接收键盘上灯的数据,端点3用来发送多媒体数据。

重点来了!!!

这里每一个宏定义后的值并不是可以随便定义的,如果翻看一下STM32的参考手册USB章节,发现USB模块中有512字节的缓冲区,这个缓缓区和CAN是共用的。但重点不在这里,如果我把端点0的ENDP0_RXADDR定义为0x10,那么这样USB设备接收的数据或者发送的数据就会出错。为什么呢?看下图。
USB的分组缓冲区对应的缓冲区描述表项定位
包缓冲区中有一个叫缓冲区描述表的东西,这个东西的位置也位于包缓冲区中。它的意义就在于告诉USB设备,哪个端点的数据放置与缓冲区的哪个位置。而不管我们定义的端点是否为双向端点,即不管我们是否同时使用一个端点来收发数据,它的缓冲区描述表都在那里,都要占据8个字节。而且这个缓冲区描述表的位置就位于包缓冲区的开始的位置。也就是说,如果我使用了三个端点,那么ENDP0_RXADDR的值最小也得是3*8 = 0x18。至于你定义在0x30这个位置,只是说对于包缓冲区的 空间利用不充分,因为缓冲区描述表后面的部分是都可以用来作为数据缓冲区的,但如果定义的小了,则会造成通信异常。
5、这里修改好以后,在usb_prop.c文件中,需要定义如下几个函数。并在相应头文件中声明。

uint8_t *CustomHID_GetHIDDescriptor(uint16_t Length)
{
  return Standard_GetDescriptorData(Length, &CustomHID_Hid_Descriptor);
}

uint8_t *Meida_GetHIDDescriptor(uint16_t Length)
{
  return Standard_GetDescriptorData(Length, &Media_Hid_Descriptor);
}

uint8_t *CustomHID_GetReportDescriptor(uint16_t Length)
{
  return Standard_GetDescriptorData(Length, &CustomHID_Report_Descriptor);
}

uint8_t *Media_GetReportDescriptor(uint16_t Length)
{
  return Standard_GetDescriptorData(Length, &Media_Report_Descriptor);
}

6、最后。看第一条注释语句,这里为什么是这样,有知道的朋友,欢迎指点迷津。曾经头皮挠破了,不曾想是这里的原因。

RESULT CustomHID_Data_Setup(uint8_t RequestNo)
{
  uint8_t *(*CopyRoutine)(uint16_t);
  
  /* 复合设备一定要将此语句注释掉,否则只识别第一个设备 */
//   if (pInformation->USBwIndex != 0)         
//     return USB_UNSUPPORT;    
  
  CopyRoutine = NULL;
  
  if ((RequestNo == GET_DESCRIPTOR)
      && (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT))
      && (pInformation->USBwIndex0 < 3))
  {
    
    if (pInformation->USBwValue1 == REPORT_DESCRIPTOR)
    {
        if (pInformation->USBwIndex0 == 0)
        {
            CopyRoutine = CustomHID_GetReportDescriptor;
        
        }
        else if (pInformation->USBwIndex0 == 1)
        {
            CopyRoutine = Media_GetReportDescriptor;
        }
    }
    else if (pInformation->USBwValue1 == HID_DESCRIPTOR_TYPE)
    {
        if (pInformation->USBwIndex0 == 0)
        {
            CopyRoutine = CustomHID_GetHIDDescriptor;
        
        }
        else if (pInformation->USBwIndex0 == 1)
        {
            CopyRoutine = Meida_GetHIDDescriptor;
        }
    }
    
  } /* End of GET_DESCRIPTOR */

7、好了,到这里,一个带多媒体按键的键盘组合设备差不多是完成了,除了还需要对其端点的回调函数定义。插上电脑,应该可以正确的识别为一个设备了。

三 六键无冲与全键无冲

3.1 关于按键冲突

为什么会有按键冲突?请看下面这张图。

这里以一个普通的4个按键的矩阵为例来说明。
以上图为例,2×2的键盘矩阵,一般键盘的扫描方式,都是按列扫描,A,B,C,D都是连接在单片机的IO上,A,B设置为输入,然后C,D设置为输出,先让C输出1,D输出0,分别读取A,B引脚的电平,如果此时S3按下,应该是A读到0,B读到1。然后再让D输出1,C输出0,分别读取A,B电平,应该是A读到0,B也读到0。这样就可以获得S1-S4的按键状态了。
还有一种按键扫描方式,不过很少用,先让A,B输入,C,D输出,让C,D同时输出1,读取A,B电平,保存为temp1,然后再让A,B为输出,C,D为输入,A,B同时输出1,读取C,D电平,保存为temp2,然后根据temp1和temp2的组合值来判断是哪个按键按下。

按键冲突的出现:当按键S1,S2,S3同时按下时,无论采用上述的哪种扫描方法,对于单片机来说,判断结果都是S1,,S2,S3,S4这四个按键都被按下了。

按键冲突的解决办法:
1:通过改变按键的布局,比如,W,E,S,D这四个按键在某一个游戏操作时容易被同时按下,这就需要在原理图设计时不能把这四个按键放置为上图的形势。
2:二极管的单向导电性。如下图。
加入二极管后的电路
因为按键冲突的实质原因是由于电路形成通路导致的,而二极管因为其特性而将上述可能出现的情况完全解决了,当然缺点就是成本增加了。

3.2 关于六键无冲

网上查找键盘的数据格式,大多都是如下的一个说明。

键盘发送给PC的数据每次8个字节
BYTE1 BYTE2 BYTE3 BYTE4 BYTE5 BYTE6 BYTE7 BYTE8
定义分别是:
BYTE1 –
|–bit0: Left Control是否按下,按下为1
|–bit1: Left Shift 是否按下,按下为1
|–bit2: Left Alt 是否按下,按下为1
|–bit3: Left GUI 是否按下,按下为1
|–bit4: Right Control是否按下,按下为1
|–bit5: Right Shift 是否按下,按下为1
|–bit6: Right Alt 是否按下,按下为1
|–bit7: Right GUI 是否按下,按下为1
BYTE2 – 暂不清楚,有的地方说是保留位
BYTE3–BYTE8 – 这六个为普通按键
键盘经过测试。
例如:键盘发送一帧数据 02 00 0x04 0x05 00 00 00 00
表示同时按下了Left Shift + ‘a’+‘b’三个键

这就误导我以为键盘就只能一次最多发送六个按键数据(除了Ctrl等控制按键外)。这也就有了六键无冲的由来。因为一次最多发送六个按键的数据。后来网上找了很多的资料,也去看了看HID v1.11这个协议的部分内容。

结果如下:为什么一次最多只能发送六个按键值,这是因为只有上述的这个格式的键盘在BIOS下才能有效。至于在系统环境下,你想发送多少个按键值,随意,你开心就好。因此,我完全可以将报告描述符更改为如下形势。

    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, 0x0D, // REPORT_COUNT (14)		// 更改这一句
    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   /* 63 */

这样六键无冲键盘一下子就成了14键无冲键盘,这个值是随意可以设置的。当然,这里更改了,端点描述符也需要更改一下数据包的大小。
如果你乐意,你想就这样把键盘设置为全键无冲键盘,比如104配列的键盘,除了8个控制按键外,完全可以把REPORT_COUNT的值改为96。

3.3 关于全键无冲与实现方式

上面其实已经讲了全键无冲,顾名思义就是所有按键按下都不会有冲突,电脑都可以响应我们所按下的所有按键。

这里主要讲一下实现的方式。实现方式有很多种,我讲一下我知道的几种。

1:3.2节讲的方法,通过更改报告描述符的方式。

2:复合设备,定义一个组合设备。比如一个接口发送六个字节的数据。那么我定义一个五个接口的组合设备,这样我是不是可以做到30键无冲了呢。

3:最常用的方法就是,把每一个按键映射为一个字节的一个位,以前一个按键就需要一个字节,现在一个字节就可以表示8个按键,这样,只需要16个字节就能表示128个按键了,除开其中有几个bit位无意义之外,这样一个全键盘的104个按键就能通过一个16字节的USB数据包来表示了。
这种映射为bit方式的报告描述符如下:

    0x05, 0x01,                           // Usage Page (Generic Desktop),
    0x09, 0x06,                           // Usage (Keyboard),
    0xA1, 0x01,                           // Collection (Application),
    // bitmap of modifiers
    0x75, 0x01,                           //   Report Size (1),
    0x95, 0x08,                           //   Report Count (8),
    0x05, 0x07,                           //   Usage Page (Key Codes),
    0x19, 0xE0,                           //   Usage Minimum (224),
    0x29, 0xE7,                           //   Usage Maximum (231),
    0x15, 0x00,                           //   Logical Minimum (0),
    0x25, 0x01,                           //   Logical Maximum (1),
    0x81, 0x02,                           //   Input (Data, Variable, Absolute), ;Modifier byte
    // LED output report
    0x95, 0x05,                           //   Report Count (5),
    0x75, 0x01,                           //   Report Size (1),
    0x05, 0x08,                           //   Usage Page (LEDs),
    0x19, 0x01,                           //   Usage Minimum (1),
    0x29, 0x05,                           //   Usage Maximum (5),
    0x91, 0x02,                           //   Output (Data, Variable, Absolute),
    0x95, 0x01,                           //   Report Count (1),
    0x75, 0x03,                           //   Report Size (3),
    0x91, 0x03,                           //   Output (Constant),
    // bitmap of keys
    0x95, 0x78,                           //   Report Count (120),
    0x75, 0x01,                           //   Report Size (1),
    0x15, 0x00,                           //   Logical Minimum (0),
    0x25, 0x01,                           //   Logical Maximum(1),
    0x05, 0x07,                           //   Usage Page (Key Codes),
    0x19, 0x00,                           //   Usage Minimum (0),
    0x29, 0x77,                           //   Usage Maximum (),
    0x81, 0x02,                           //   Input (Data, Variable, Absolute),
    0xc0                                  // End Collection   /* 57 */

数据包的第一个字节仍旧表示控制按键。

四 STM32的DFU升级方式

这里以后专门出一篇文章来写吧,和本文的关系不大,主要是方便没有烧写工具的朋友可以自己更改代码,做一把自己想要的键盘。
DFU很简单,相比于HID,DFU,USB更多的则是用来做MSC设备,当然,对于电子工程师来说,更有用的设备当然是CDC设备啦。这样可以节省一颗CH340的芯片。仅仅只是调试时使用,一颗CH340未免太浪费,好歹几块钱一颗呢。

后记

犹记得大学毕业时,找工作面试,网龙网络的一个工程师问我,“++a和a++有什么区别?“现在想来,当初真是初生牛犊不怕虎,什么都不懂,想着岗位高薪就去面试。

毕业后工作好些年,遇到了很多的良师益友,从他们身上学到了不少东西,这里肯定要感谢他们的分享与不吝赐教。所以我觉得学会了,理解了,就拿出来大家一起分享。正如,文明的进步依靠的是不同文明之间的碰撞与融合。

我后面没有附上这次DIY键盘的源代码,这是因为我在代码中使用了很多的全局变量,没有用结构体去封装。使用了很多的if else语句,CPU运行效率低下。也许它实现了一个键盘的功能,但真不能算一个合格的工程。其实代码这些东西都可以参考参考tmk方案或者qmk方案,里面的有一些代码都写的很精简而有效率。这两个方案在github上是开源的。

这篇文章,断断续续写了快一个月,很多是删了重写的,旨在为了让更多的人都能看懂。不想因为我的随意,而使这篇文章而变得晦涩难懂。如果我写的大家都看不懂,那简直是一坨狗屎,也就失去了分享的意义。

一晃已经进入自己的第26个年头了,可我依旧是一条年轻的小咸鱼,谨以此纪念自己的25岁生日,生日快乐,老藏。

  • 42
    点赞
  • 100
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值